From 990d607d4b41afb7d0fa1e671b1f9f4ba10ab5a2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:26:35 +0100 Subject: [PATCH] fix: normalize main flow section to (0,0) origin in frontend The root cause of the Y-offset: ELK places main flow nodes at arbitrary positions (e.g., y=679) within its root graph, and the frontend rendered them at those raw positions. Handler sections were already normalized via shiftNodes, but the main section was not. Now useDiagramData.ts applies the same normalization to the main section: computes bounding box, shifts nodes and edges so the section starts at (0,0). This fixes the Y-offset regardless of what ELK produces internally. Removed the backend normalizePositions (was ineffective because handler nodes at y=12 dominated the global minimum, preventing meaningful shift of main flow nodes at y=679). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/diagram/ElkDiagramRenderer.java | 62 +------------------ .../ProcessDiagram/useDiagramData.ts | 22 +++++-- 2 files changed, 19 insertions(+), 65 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java index 89ae853e..8ba5cb7c 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java @@ -339,10 +339,9 @@ public class ElkDiagramRenderer implements DiagramRenderer { positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points)); } - // Normalize: shift all nodes and edges so bounding box starts at (0, 0). - // ELK can place nodes at arbitrary positions within the root graph; - // normalizing ensures the output starts at the origin regardless. - normalizePositions(positionedNodes, positionedEdges); + // Note: normalization (shifting to 0,0 origin) is handled per-section + // in the frontend's useDiagramData.ts, which separates main flow from + // handler sections and normalizes each independently. // Compute bounding box from all positioned nodes and edges double totalWidth = 0; @@ -364,61 +363,6 @@ public class ElkDiagramRenderer implements DiagramRenderer { return new LayoutResult(layout, nodeColors, compoundInfos); } - /** - * Shift all positioned nodes and edge points so the bounding box starts at (0, 0). - * This compensates for ELK placing nodes at non-zero origins within its root graph. - */ - private void normalizePositions(List nodes, List edges) { - // Find minimum x/y across all nodes (recursively including children) - double minX = Double.MAX_VALUE; - double minY = Double.MAX_VALUE; - for (PositionedNode pn : allNodes(nodes)) { - if (pn.x() < minX) minX = pn.x(); - if (pn.y() < minY) minY = pn.y(); - } - for (PositionedEdge pe : edges) { - for (double[] pt : pe.points()) { - if (pt[0] < minX) minX = pt[0]; - if (pt[1] < minY) minY = pt[1]; - } - } - - if (minX <= 0 && minY <= 0) return; // Already at or past origin - - // Shift nodes — PositionedNode is a record, so we must rebuild - double shiftX = minX > 0 ? minX : 0; - double shiftY = minY > 0 ? minY : 0; - - for (int i = 0; i < nodes.size(); i++) { - nodes.set(i, shiftNode(nodes.get(i), shiftX, shiftY)); - } - - // Shift edge points in-place - for (PositionedEdge pe : edges) { - for (double[] pt : pe.points()) { - pt[0] -= shiftX; - pt[1] -= shiftY; - } - } - } - - /** Recursively shift a PositionedNode and its children by (dx, dy). */ - private PositionedNode shiftNode(PositionedNode pn, double dx, double dy) { - List shiftedChildren = List.of(); - if (pn.children() != null && !pn.children().isEmpty()) { - shiftedChildren = new ArrayList<>(); - for (PositionedNode child : pn.children()) { - shiftedChildren.add(shiftNode(child, dx, dy)); - } - } - return new PositionedNode( - pn.id(), pn.label(), pn.type(), - pn.x() - dx, pn.y() - dy, - pn.width(), pn.height(), - shiftedChildren - ); - } - // ---------------------------------------------------------------- // SVG drawing helpers // ---------------------------------------------------------------- diff --git a/ui/src/components/ProcessDiagram/useDiagramData.ts b/ui/src/components/ProcessDiagram/useDiagramData.ts index fbf6a3f9..a4d9e2df 100644 --- a/ui/src/components/ProcessDiagram/useDiagramData.ts +++ b/ui/src/components/ProcessDiagram/useDiagramData.ts @@ -55,20 +55,30 @@ export function useDiagramData( e => mainNodeIds.has(e.sourceId) && mainNodeIds.has(e.targetId), ); - // Compute main section bounding box + // Normalize main section to start at (0, 0) — ELK can place nodes + // at arbitrary positions within its root graph const mainBounds = computeBounds(mainNodes); + const mainOffX = mainBounds.minX; + const mainOffY = mainBounds.minY; + const shiftedMainNodes = shiftNodes(mainNodes, mainOffX, mainOffY); + const shiftedMainEdges = mainEdges.map(e => ({ + ...e, + points: e.points.map(p => [p[0] - mainOffX, p[1] - mainOffY]), + })); + const mainWidth = mainBounds.maxX - mainBounds.minX; + const mainHeight = mainBounds.maxY - mainBounds.minY; const sections: DiagramSection[] = [ { label: 'Main Route', - nodes: mainNodes, - edges: mainEdges, + nodes: shiftedMainNodes, + edges: shiftedMainEdges, offsetY: 0, }, ]; - let currentY = mainBounds.maxY + SECTION_GAP; - let maxWidth = mainBounds.maxX; + let currentY = mainHeight + SECTION_GAP; + let maxWidth = mainWidth; const addHandlerSections = ( handlers: { label: string; nodes: DiagramNode[] }[], @@ -106,7 +116,7 @@ export function useDiagramData( // Then error handlers addHandlerSections(errorSections, 'error'); - const totalWidth = Math.max(layout.width ?? 0, mainBounds.maxX, maxWidth); + const totalWidth = Math.max(layout.width ?? 0, mainWidth, maxWidth); const totalHeight = currentY; return { sections, totalWidth, totalHeight };