/** * Guides d'alignement affiches pendant un deplacement. * * Volontairement simple : on compare les bords et les centres du noeud deplace * a ceux des autres noeuds visibles, et on ne garde que la meilleure * correspondance sur chaque axe. Aucun etat global, aucune entree d'historique. */ export interface GuideBox { id: string; x: number; y: number; width: number; height: number; } export interface AlignmentGuide { orientation: 'vertical' | 'horizontal'; /** Coordonnee de la ligne (x pour verticale, y pour horizontale). */ position: number; /** Etendue de la ligne, pour ne pas traverser tout le canvas. */ start: number; end: number; } export const GUIDE_THRESHOLD = 6; type Candidate = { value: number; span: [number, number] }; function verticalCandidates(box: GuideBox): Candidate[] { const span: [number, number] = [box.y, box.y + box.height]; return [ { value: box.x, span }, { value: box.x + box.width / 2, span }, { value: box.x + box.width, span }, ]; } function horizontalCandidates(box: GuideBox): Candidate[] { const span: [number, number] = [box.x, box.x + box.width]; return [ { value: box.y, span }, { value: box.y + box.height / 2, span }, { value: box.y + box.height, span }, ]; } function bestMatch( moving: Candidate[], others: GuideBox[], extract: (box: GuideBox) => Candidate[], threshold: number, ): { position: number; span: [number, number] } | null { let best: { position: number; span: [number, number]; distance: number } | null = null; for (const other of others) { for (const candidate of extract(other)) { for (const source of moving) { const distance = Math.abs(source.value - candidate.value); if (distance > threshold) continue; if (best && distance >= best.distance) continue; best = { position: candidate.value, span: [ Math.min(source.span[0], candidate.span[0]), Math.max(source.span[1], candidate.span[1]), ], distance, }; } } } return best ? { position: best.position, span: best.span } : null; } /** Au plus un guide vertical et un guide horizontal. */ export function computeAlignmentGuides( moving: GuideBox, others: GuideBox[], threshold: number = GUIDE_THRESHOLD, ): AlignmentGuide[] { const candidates = others.filter((box) => box.id !== moving.id); if (candidates.length === 0) return []; const guides: AlignmentGuide[] = []; const vertical = bestMatch( verticalCandidates(moving), candidates, verticalCandidates, threshold, ); if (vertical) { guides.push({ orientation: 'vertical', position: vertical.position, start: vertical.span[0], end: vertical.span[1], }); } const horizontal = bestMatch( horizontalCandidates(moving), candidates, horizontalCandidates, threshold, ); if (horizontal) { guides.push({ orientation: 'horizontal', position: horizontal.position, start: horizontal.span[0], end: horizontal.span[1], }); } return guides; }