import { type MindMapNode, type NodeSide } from '@/types/map'; import { buildChildrenMap, buildNodeMap, getOrderedChildren, getRootNodes } from '@/utils/tree'; /** * Cotes gauche / droite d'une mind map bilaterale. * * Le modele est volontairement minimal : seul un enfant direct d'une racine * porte un `side`. Tous ses descendants heritent du cote de leur branche de * niveau 1, ce qui evite d'entretenir la meme information sur chaque noeud et * garantit qu'une branche ne peut pas se retrouver a cheval sur les deux cotes. * * `order` reste la seule source de verite de l'ordre : on filtre les enfants * par cote, puis on respecte leur ordre existant. Il n'y a jamais de * `leftOrder` / `rightOrder` a maintenir en parallele. */ /** Poids d'une branche : elle-meme plus tous ses descendants. */ function branchWeight(children: Map, rootId: string): number { let total = 1; const queue = [...(children.get(rootId) ?? [])]; while (queue.length > 0) { const current = queue.shift() as string; total += 1; const next = children.get(current); if (next) queue.push(...next); } return total; } /** * Repartition gloutonne et deterministe. * * Les branches sans cote sont traitees de la plus lourde a la plus legere et * placees du cote le moins charge. C'est suffisant pour equilibrer visuellement * une carte reelle, sans construire un solveur : a poids egal, l'ordre de * fratrie tranche, donc le resultat est reproductible. */ export function assignSides( branches: Array<{ id: string; weight: number; order: number; side?: NodeSide }>, ): Map { const result = new Map(); let leftLoad = 0; let rightLoad = 0; for (const branch of branches) { if (!branch.side) continue; result.set(branch.id, branch.side); if (branch.side === 'left') leftLoad += branch.weight; else rightLoad += branch.weight; } const pending = branches .filter((branch) => !branch.side) .sort((a, b) => (b.weight !== a.weight ? b.weight - a.weight : a.order - b.order)); for (const branch of pending) { // A charge egale, la droite d'abord : c'est le cote de lecture naturel. const side: NodeSide = rightLoad <= leftLoad ? 'right' : 'left'; result.set(branch.id, side); if (side === 'left') leftLoad += branch.weight; else rightLoad += branch.weight; } return result; } /** Branches de niveau 1 : les enfants directs des racines, dans l'ordre. */ export function getLevelOneBranches(nodes: MindMapNode[]): MindMapNode[] { const branches: MindMapNode[] = []; for (const root of getRootNodes(nodes)) { branches.push(...getOrderedChildren(nodes, root.id)); } return branches; } /** * Cote effectif de chaque noeud. * * Fonction pure : elle ne modifie pas le document. Une branche sans cote defini * en recoit un ici aussi, par le meme algorithme que l'attribution explicite : * une branche ajoutee apres coup se place donc immediatement du bon cote, sans * qu'il faille reecrire la carte. */ export function resolveNodeSides(nodes: MindMapNode[]): Map { const children = buildChildrenMap(nodes); const branches = getLevelOneBranches(nodes).map((node) => ({ id: node.id, weight: branchWeight(children, node.id), order: node.order ?? 0, side: node.side, })); const byBranch = assignSides(branches); const sides = new Map(); // Les racines restent au centre : leur cote ne sert qu'aux cas degenres. for (const root of getRootNodes(nodes)) sides.set(root.id, 'right'); for (const [branchId, side] of byBranch) { sides.set(branchId, side); const queue = [...(children.get(branchId) ?? [])]; while (queue.length > 0) { const current = queue.shift() as string; sides.set(current, side); const next = children.get(current); if (next) queue.push(...next); } } // Securite : un noeud orphelin d'une donnee incoherente reste positionnable. for (const node of nodes) if (!sides.has(node.id)) sides.set(node.id, 'right'); return sides; } /** * Materialise les cotes manquants dans le document. * * Seules les branches de niveau 1 sans cote sont completees : un cote choisi a * la main n'est jamais ecrase. Retourne le tableau d'origine si rien ne change. */ export function materializeSides(nodes: MindMapNode[]): MindMapNode[] { const branches = getLevelOneBranches(nodes); if (branches.every((branch) => branch.side)) return nodes; const resolved = resolveNodeSides(nodes); const branchIds = new Set(branches.map((branch) => branch.id)); return nodes.map((node) => branchIds.has(node.id) && !node.side ? { ...node, side: resolved.get(node.id) ?? 'right' } : node, ); } /** * Branche de niveau 1 dont depend un noeud, c'est-a-dire le noeud dont le * `side` fait foi pour lui. `null` pour une racine. */ export function findBranchRoot(nodes: MindMapNode[], nodeId: string): MindMapNode | null { const byId = buildNodeMap(nodes); let current = byId.get(nodeId); if (!current) return null; const guard = new Set(); while (current.parentId && byId.has(current.parentId) && !guard.has(current.id)) { guard.add(current.id); const parent = byId.get(current.parentId) as MindMapNode; if (!parent.parentId || !byId.has(parent.parentId)) return current; current = parent; } return null; }