import { type MindMapNode } from '@/types/map'; import { getTheme, type MapTheme } from '@/features/editor/themes'; import { resolveNodeAppearance } from '@/features/editor/nodeStyleResolver'; import { countRenderedLines, measureLongestLine, type FontSpec } from '@/utils/textMeasure'; export interface NodeSize { width: number; height: number; } /** Marge interne reservee aux indicateurs (note, lien) affiches dans le noeud. */ const INDICATOR_WIDTH = 18; /** * Estimation de la taille d'un noeud. * * Utilisee tant que le DOM n'a pas mesure le noeud : premier auto-layout, * export d'une carte importee, calcul des bornes. Elle reproduit les regles * appliquees par le rendu (padding, icone, retour a la ligne, largeur max). */ export function estimateNodeSize(node: MindMapNode, theme: MapTheme = getTheme(undefined)): NodeSize { const appearance = resolveNodeAppearance(node, node.type === 'root' ? 0 : 1, theme); const font: FontSpec = { fontSize: appearance.fontSize, fontFamily: appearance.fontFamily, fontWeight: appearance.fontWeight, fontStyle: appearance.fontStyle, }; const text = node.text || ' '; const iconInline = Boolean(node.icon) && appearance.iconPosition !== 'top'; const iconAbove = Boolean(node.icon) && appearance.iconPosition === 'top'; const decorations = (iconInline ? appearance.iconSize + appearance.iconGap : 0) + (node.note ? INDICATOR_WIDTH : 0) + (node.url ? INDICATOR_WIDTH : 0); const horizontalChrome = appearance.paddingX * 2 + decorations + appearance.borderWidth * 2; const verticalChrome = appearance.paddingY * 2 + appearance.borderWidth * 2; // Largeur let width: number; let textWidth: number; if (appearance.widthMode === 'fixed' && appearance.width) { width = appearance.width; textWidth = Math.max(16, width - horizontalChrome); } else { const naturalTextWidth = measureLongestLine(text, font); const cap = appearance.wrap ? appearance.maxTextWidth : Number.POSITIVE_INFINITY; textWidth = Math.min(naturalTextWidth, cap); width = Math.max(appearance.minWidth, Math.ceil(textWidth + horizontalChrome)); } // Hauteur if (appearance.heightMode === 'fixed' && appearance.height) { return { width: Math.round(width), height: Math.round(appearance.height) }; } const lines = countRenderedLines( text, font, appearance.wrap ? textWidth : Number.POSITIVE_INFINITY, ); const textHeight = lines * appearance.fontSize * appearance.lineHeight; const iconHeight = iconAbove ? appearance.iconSize + appearance.iconGap : 0; const height = Math.max( appearance.minHeight, Math.ceil(textHeight + iconHeight + verticalChrome), ); // Un cercle doit rester circulaire : on prend la plus grande des deux mesures. if (appearance.circle && appearance.widthMode === 'auto') { const side = Math.max(width, height); return { width: side, height: side }; } return { width: Math.round(width), height: Math.round(height) }; } /** Taille reelle si elle a ete mesuree dans le DOM, sinon estimation. */ export function resolveNodeSize( node: MindMapNode, measured?: Map, theme?: MapTheme, ): NodeSize { const known = measured?.get(node.id); if (known && known.width > 0 && known.height > 0) return known; return estimateNodeSize(node, theme); }