import type { Class_NodeElement } from '../Elements/Node'; import { Class_LevelTag } from '../types/Tag'; import type { NodePositioning } from './NodePositioning'; export declare class NodePositioningParametric { private readonly np; constructor(np: NodePositioning); private get drawingArea(); /** * Place verticalement les enfants d'une opération STRUCTURELLE (désagrégation, * expansion latérale, englobement) dans le slot vertical du parent `[parent_top, * parent_top + parent_h]`, selon le mode d'écart configuré sur la DrawingArea * (`effective_gap_mode` = surcharge transitoire ?? réglage global), cf. * Type_DisaggregationGap : * - 'fill' : écart égal pour remplir exactement le slot (≥ 0). Historique #1231. * - 'keep' : aucun repositionnement VERTICAL (les enfants gardent leur position_y). * - 'children_dy' : empile depuis `parent_top`, écart = shape_position_dy de chaque enfant. * - 'constant' : empile depuis `parent_top`, écart = disaggregation_gap_value, réécrit dans dy. * * N.B. : ne touche QUE position_y (+ shape_position_dy selon le mode). L'appelant gère * position_u / position_x (qui diffèrent selon l'opération : colonne du parent pour la * désagrégation, colonne adjacente décalée pour l'expansion) et les applique TOUJOURS, * même en 'keep' (qui ne conserve que le Y des enfants). * L'ordre du tableau `children` détermine l'empilement (haut → bas). */ layoutChildrenInParentSlot(children: Class_NodeElement[], parent_top: number, parent_h: number): void; /** * Point d'entrée unique pour le recompute du layout paramétrique (PR 3). * * Traite une colonne (ensemble de nœuds visibles partageant un même * `position_u`) comme une pile verticale triée par `position_v` croissant, * ancrée sur le `position_y` courant du nœud de plus petit V, et espacée * par `shape_position_dy` via `stackNodesVertically` (cf. PR 2). * * **Responsabilité stricte : empilement géométrique uniquement.** `position_v` * est supposé déjà à jour à l'entrée — les call sites qui ont besoin de le * recalculer (nouveau diagramme, bascule de mode, data tag change) doivent * appeler `computeParametricV` avant. Cette séparation des responsabilités * était le point 1 de la discussion PR 3 : V est une donnée métier, le * recompute est un calcul géométrique pur. * * **Limitation de l'étape 1 (ce commit)** : les containers ne sont **pas** * traités récursivement. Les nœuds enfants d'un container (i.e. ceux avec * `dimensions_as_child.some(d => d.container_mode)`) sont exclus du * stacking de colonne — l'ancien chemin `Node.applyPosition` les prend en * charge via sa logique `nodeAbove`. L'intégration récursive des * containers comme sous-colonnes est prévue dans un commit ultérieur de * PR 3. * * **Scopes supportés** : * - `{ type: 'all' }` : toutes les colonnes top-level de la drawing area. * - `{ type: 'column', u: number }` : une seule colonne (utile pour fin * de drag, désagrégation latérale). * - `{ type: 'subtree', node }` : réservé à l'étape containers récursifs * (commit ultérieur) — non implémenté ici, lève une erreur explicite. * * Les nœuds « échange » (tag `type de noeud` / `echange`) sont exclus du * stacking, comme dans tous les autres chemins paramétriques. * * **Dead code temporaire** : tant que `Node.applyPosition` n'est pas * réduit à un pass-through, appeler `recomputeParametricLayout` n'a aucun * effet visible — le prochain `applyPosition` écrase les positions qu'on * vient de poser. C'est volontaire : ce commit ajoute uniquement la * plomberie, la bascule de `applyPosition` et la migration des call sites * viennent dans un commit séparé pour isoler les régressions éventuelles. */ recomputeParametricLayout(scope: { type: 'all'; } | { type: 'column'; u: number; } | { type: 'subtree'; node: Class_NodeElement; }): void; /** * Ré-empile les enfants de chaque cadre englobant (`container_mode`) sur la position et la * hauteur COURANTES du cadre — pendant de la Phase C de `recomputeParametricLayout`, mais pour * les modes de positionnement NON-parametric (absolu, proportionnel, échelle adaptée). * * Pourquoi : dans ces modes, le placement global (`anchorAbsoluteNodesByCenter`, * `anchorProportionalNodes`…) garde le CENTRE de chaque enfant fixe quand sa taille change * (changement de datatag/vue/échelle). Des enfants empilés jointivement (écart constant) finissent * donc par se chevaucher ou se disperser dès que leur valeur change. * * Empilement À PLAT des FEUILLES : on collecte les feuilles réelles (pas les sous-cadres) dans * l'ordre hiérarchique et on les espace UNIFORMÉMENT — écart identique quel que soit le niveau * d'imbrication. Empiler récursivement les sous-cadres ajouterait leurs marges (`shape_margin_top` * /`_bottom`) entre deux groupes → l'écart casserait au 2ᵉ niveau. Chaque sous-cadre est ensuite * réancré (`reanchorTiedFrame`) pour envelopper ses feuilles ; sa taille suit via `_envelopeSize()`. * Le cadre de premier niveau garde sa position (il sert d'ancre). * * L'écart est résolu par `containerChildGap` : en mode 'constant' il est lu EN DIRECT sur * `disaggregation_gap_value` (éditer la valeur ré-englobe au prochain dessin, sans être figé dans * les feuilles) ; sinon = `shape_position_dy` persisté. En mode 'keep' rien n'est ré-empilé. * * À appeler en FIN de placement (après le mode global + `anchorParametricNodesToAbsolute`), pour * écraser le re-centrage individuel des feuilles. Nœuds « échange » et enfants invisibles exclus. * * #365 — la passe s'applique à tout cadre de premier niveau, **visible ou non** : c'est la * visibilité des FEUILLES qui compte, pas celle du cadre (cf. `containerRootsToRestack`). */ restackContainerChildren(keeps?: (leaf: Class_NodeElement) => boolean): void; /** * Les CHAÎNES d'empilement des cadres englobants : par cadre de premier niveau, ses feuilles * visibles à plat, dans l'ordre hiérarchique (DFS + tri par v) — les sous-cadres eux-mêmes n'y * figurent pas, seulement les vraies feuilles. * * SOURCE UNIQUE des deux sens de lecture, comme `parametricColumnChains` pour les colonnes : * `restackContainerChildren` (écarts → positions) et `settleParametricStacksFromY` (positions → * écarts) parcourent la même chaîne. * * #365 — sur TOUS les nœuds, pas seulement les visibles : un cadre englobant peut être masqué * (sa visibilité suit ses flux propres) alors que ses membres sont dessinés, et ses enfants * doivent être empilés quand même. Cf. `containerRootsToRestack`. */ containerLeafChains(): { container: Class_NodeElement; leaves: Class_NodeElement[]; }[]; /** Cadre englobant : parent d'au moins une dimension en `container_mode`. */ isContainerParent(n: Class_NodeElement): boolean; /** * Enfants directs VISIBLES d'un cadre englobant, dédupliqués sur les dimensions * `container_mode` (nœuds « échange » écartés). Ordre du fichier, non trié. */ containerDirectChildren(container: Class_NodeElement): Class_NodeElement[]; /** * #372 — GROUPES d'empilement des cadres englobants : les enfants directs visibles de chaque * cadre (racine ET sous-cadres), déjà ordonnés comme la pile les parcourt. Un groupe par cadre * — et non la liste à plat des feuilles — parce que c'est À L'INTÉRIEUR d'un cadre que l'ordre * se relit : la collecte des feuilles de `restackContainerChildren` retrie cadre par cadre, si * bien qu'un `position_v` permuté par-dessus la frontière d'un sous-cadre serait sans effet. * * Vide en mode d'écart 'keep' : les membres y gardent leur position posée à la main, il n'y a * ni empilement à reproduire ni écart à régler. */ containerChildGroups(): Class_NodeElement[][]; /** * Back-calcule `shape_position_dy` de chaque nœud visible depuis sa `position_y` * absolue. Pour chaque colonne (groupée par `position_u`), les nœuds sont triés par * y et le dy de chacun est déduit du gap avec le nœud précédent. Utilisé à la bascule * absolu→paramétrique et en fin de drag pour que le déplacement vertical d'un nœud * persiste (sinon `applyPosition` rappelle le nœud à sa position dérivée du dy). * Retourne le nombre de chevauchements clampés (raw_dy < 0 → dy = 0). */ backCalculateShapePositionDyFromY(): number; /** * Déduit `position_u` depuis `position_x` pour les nœuds visibles non * verrouillés. À appeler explicitement aux endroits où une position absolue * vient d'être modifiée (drop de ghost link, fin de drag, contraction). Ce * calcul **ne fait plus partie** de `computeParametrization` pour éviter le * couplage bidirectionnel u ↔ x qui faisait dériver les colonnes à chaque * recalcul (notamment quand l'envelope d'un container modifie x). * * **Clustering plutôt que rounding indépendant (PR 3 step 5)** : l'ancienne * implémentation faisait `u = Math.round(x / dx)` sur chaque nœud * indépendamment. Deux nœuds visuellement alignés (à 1-2 px près) tombaient * parfois de part et d'autre de la frontière de rounding (ex. x=1898.88 → * u=9 et x=1901.47 → u=10 avec dx=200, frontière à 1900), ce qui les * affectait à des colonnes différentes sans intention utilisateur. * * La nouvelle implémentation regroupe d'abord les nœuds en **clusters** * (tri par x croissant, puis fusion glissante : un nœud rejoint le cluster * courant si son x est à moins de `tolerance` du max-x du cluster), puis * calcule un `u` commun par cluster. Conséquences : * * - Deux nœuds quasi alignés tombent dans le même cluster → même `u`, * toujours, peu importe où ils sont par rapport aux frontières de * rounding. * - Un cluster contenant un nœud `u`-verrouillé hérite du `u` du verrou * (le verrou définit la colonne d'autorité). * - Un cluster sans verrou calcule son `u` depuis le x moyen du cluster, * ce qui reste proche de l'ancien comportement pour les colonnes * bien-formées. * * `tolerance` est fixée à 5 % de `dx` (plafonnée à 10 px min), valeur bien * au-dessus du bruit pixel et très en dessous d'une demi-colonne. */ inferPositionUFromX(): void; /** * Nœuds éligibles à une colonne : visibles et non taggés « échange » (ces derniers sont * placés par arrangeTrade et n'appartiennent à aucune colonne). Les nœuds `u`-verrouillés * restent dans leur cluster — ils en ancrent la valeur. */ private nodesEligibleForColumns; /** * Regroupe les nœuds en bandes d'après leur position sur `axis`, par fusion glissante : on trie * sur l'axe puis on ouvre un nouveau cluster dès que l'écart au max du cluster courant dépasse * la tolérance. C'est le max — et non la valeur de tête — qui est la bonne référence : une * chaîne de nœuds distants deux à deux de moins que la tolérance forme une seule bande, même si * les extrêmes en sont plus éloignés. * * Clusters retournés dans l'ordre croissant. `axis = 'x'` donne les COLONNES (diagramme * horizontal), `axis = 'y'` les RANGÉES (diagramme vertical) ; la tolérance suit l'écart de * référence du même axe (`shape_position_dx` / `dy` du style par défaut). */ private clusterNodesByAxis; /** * Colonnes ORDINALES (0, 1, 2…) déduites des `position_x` courants. Ne mute rien. * * Distinct de `inferPositionUFromX`, qui écrit `position_u` en arrondissant `x / dx` : cet * arrondi peut attribuer le même `u` à deux colonnes voisines ou en sauter une. Pour décider * si un flux « recule », seul l'ORDRE des colonnes compte, et l'ordinal est strictement * monotone en x. */ computeColumnsFromX(): { [node_id: string]: number; }; /** * Rangées ORDINALES (0, 1, 2…) déduites des `position_y` courants — pendant vertical de * `computeColumnsFromX`. Ne mute rien. * * Sert au statut recyclage des flux VERTICAUX (`shape_orientation === 'vv'`), pour qui * « reculer » veut dire remonter, pas aller vers la gauche. */ computeRowsFromY(): { [node_id: string]: number; }; /** * sankeyapplication#153 — Recalcul incrémental du statut recyclage après une action de * l'utilisateur (typiquement un déplacement de nœud). Un flux dont la cible ne se trouve plus * à droite de sa source passe en recyclage, et réciproquement. * * Ne déplace AUCUN nœud et ne touche ni `position_u` ni `position_v` : une mise en page * manuelle est préservée telle quelle. Le verrouillage tri-state de l'utilisateur prime. * * @returns pour chaque flux dont le statut a changé, sa valeur précédente (pour l'undo). */ updateRecyclingFromPositions(only_touching_nodes?: Set): { [link_id: string]: boolean; }; /** Cf. NodePositioningCyclesCore.lockRecyclingStatusDivergences (passe post-chargement #153). */ lockRecyclingStatusDivergences(): string[]; /** * Computes u,v for nodes in the drawing area * Utilise l'algorithme amélioré * * Quand `use_horizontal_index` est true, `position_u` est recalculé via l'analyse * topologique (detectAllCyclesAndOptimize). Sinon, `position_u` est supposé déjà * à jour (ne plus dériver depuis x ici — appeler `inferPositionUFromX` côté caller * si nécessaire). */ computeParametrization(use_horizontal_index: boolean): void; private computeColumns; applyVForLevelTag(columns: { [_: number]: Class_NodeElement[]; }, tag: Class_LevelTag): void; computeParametricV(tag: Class_LevelTag | undefined): void; computeParametricVForTagg(tag: Class_LevelTag): void; /** * Apply v aggregation for nodes */ applyVAgregate(node: Class_NodeElement): void; /** * Apply v disaggregation for nodes */ applyVDesagregate(node: Class_NodeElement, current_v: number, tag: Class_LevelTag): number; /** * Reposition visible nodes so that their left/top side is close to a grid line */ _arrangeNodesToGrid(): void; }