import { type MindMapEdge, type MindMapNode } from '@/types/map'; /** Index enfant -> parents directs, construit une seule fois par operation. */ export function buildChildrenMap(nodes: MindMapNode[]): Map { const children = new Map(); for (const node of nodes) { if (!node.parentId) continue; const list = children.get(node.parentId); if (list) list.push(node.id); else children.set(node.parentId, [node.id]); } return children; } export function buildNodeMap(nodes: MindMapNode[]): Map { return new Map(nodes.map((node) => [node.id, node])); } /** Racines de la carte : noeuds sans parent (ou dont le parent n'existe plus). */ export function getRootNodes(nodes: MindMapNode[]): MindMapNode[] { const ids = new Set(nodes.map((node) => node.id)); return nodes.filter((node) => !node.parentId || !ids.has(node.parentId)); } /** Profondeur de chaque noeud depuis sa racine (racine = 0). */ export function computeDepths(nodes: MindMapNode[]): Map { const nodeMap = buildNodeMap(nodes); const depths = new Map(); const resolve = (id: string, seen: Set): number => { const cached = depths.get(id); if (cached !== undefined) return cached; if (seen.has(id)) return 0; // securite : cycle impossible mais on ne boucle pas seen.add(id); const node = nodeMap.get(id); const parentId = node?.parentId; const depth = parentId && nodeMap.has(parentId) ? resolve(parentId, seen) + 1 : 0; depths.set(id, depth); return depth; }; for (const node of nodes) resolve(node.id, new Set()); return depths; } /** Tous les descendants d'un noeud (lui-meme exclu). */ export function getDescendantIds(nodes: MindMapNode[], rootId: string): string[] { const children = buildChildrenMap(nodes); const result: string[] = []; const queue = [...(children.get(rootId) ?? [])]; while (queue.length > 0) { const current = queue.shift() as string; result.push(current); const next = children.get(current); if (next) queue.push(...next); } return result; } /** Un noeud plus tous ses descendants, sans doublon. */ export function withDescendants(nodes: MindMapNode[], ids: string[]): string[] { const collected = new Set(); for (const id of ids) { collected.add(id); for (const descendant of getDescendantIds(nodes, id)) collected.add(descendant); } return [...collected]; } /** Noeuds masques parce qu'un ancetre est replie. */ export function getHiddenNodeIds(nodes: MindMapNode[]): Set { const hidden = new Set(); const children = buildChildrenMap(nodes); for (const node of nodes) { if (!node.collapsed) continue; const queue = [...(children.get(node.id) ?? [])]; while (queue.length > 0) { const current = queue.shift() as string; if (hidden.has(current)) continue; hidden.add(current); const next = children.get(current); if (next) queue.push(...next); } } return hidden; } /** Nombre de descendants masques par un noeud replie. */ export function countHiddenDescendants(nodes: MindMapNode[], nodeId: string): number { return getDescendantIds(nodes, nodeId).length; } /** Empeche de rattacher un noeud a l'un de ses propres descendants. */ export function wouldCreateCycle( nodes: MindMapNode[], childId: string, nextParentId: string, ): boolean { if (childId === nextParentId) return true; return getDescendantIds(nodes, childId).includes(nextParentId); } /** Recalcule les liens a partir de la hierarchie : parentId fait autorite. */ export function rebuildEdges(nodes: MindMapNode[]): MindMapEdge[] { const ids = new Set(nodes.map((node) => node.id)); return nodes .filter((node) => node.parentId && ids.has(node.parentId)) .map((node) => ({ id: `e-${node.parentId}-${node.id}`, source: node.parentId as string, target: node.id, })); } /** * Restaure `parentId` a partir des liens. * Utilise a l'import, si un fichier fournit des liens mais pas de hierarchie. */ export function rebuildHierarchyFromEdges( nodes: MindMapNode[], edges: MindMapEdge[], ): MindMapNode[] { const parentByChild = new Map(); for (const edge of edges) { if (!parentByChild.has(edge.target)) parentByChild.set(edge.target, edge.source); } return nodes.map((node) => node.parentId === undefined || node.parentId === null ? { ...node, parentId: parentByChild.get(node.id) ?? null } : node, ); } /** Prochaine position libre pour un nouvel enfant, avant tout auto-layout. */ export function suggestChildPosition( nodes: MindMapNode[], parent: MindMapNode, horizontal: boolean, direction: 1 | -1, ): { x: number; y: number } { const siblings = nodes.filter((node) => node.parentId === parent.id); const offset = siblings.length; if (horizontal) { return { x: parent.position.x + direction * 220, y: parent.position.y + offset * 72 - (siblings.length > 0 ? 36 : 0), }; } return { x: parent.position.x + offset * 200 - (siblings.length > 0 ? 100 : 0), y: parent.position.y + direction * 140, }; }