fix: normalize main flow section to (0,0) origin in frontend
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 49s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 22:26:35 +01:00
parent 0df7735d20
commit 990d607d4b
2 changed files with 19 additions and 65 deletions

View File

@@ -339,10 +339,9 @@ public class ElkDiagramRenderer implements DiagramRenderer {
positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points)); positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
} }
// Normalize: shift all nodes and edges so bounding box starts at (0, 0). // Note: normalization (shifting to 0,0 origin) is handled per-section
// ELK can place nodes at arbitrary positions within the root graph; // in the frontend's useDiagramData.ts, which separates main flow from
// normalizing ensures the output starts at the origin regardless. // handler sections and normalizes each independently.
normalizePositions(positionedNodes, positionedEdges);
// Compute bounding box from all positioned nodes and edges // Compute bounding box from all positioned nodes and edges
double totalWidth = 0; double totalWidth = 0;
@@ -364,61 +363,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
return new LayoutResult(layout, nodeColors, compoundInfos); 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<PositionedNode> nodes, List<PositionedEdge> 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<PositionedNode> 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 // SVG drawing helpers
// ---------------------------------------------------------------- // ----------------------------------------------------------------

View File

@@ -55,20 +55,30 @@ export function useDiagramData(
e => mainNodeIds.has(e.sourceId) && mainNodeIds.has(e.targetId), 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 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[] = [ const sections: DiagramSection[] = [
{ {
label: 'Main Route', label: 'Main Route',
nodes: mainNodes, nodes: shiftedMainNodes,
edges: mainEdges, edges: shiftedMainEdges,
offsetY: 0, offsetY: 0,
}, },
]; ];
let currentY = mainBounds.maxY + SECTION_GAP; let currentY = mainHeight + SECTION_GAP;
let maxWidth = mainBounds.maxX; let maxWidth = mainWidth;
const addHandlerSections = ( const addHandlerSections = (
handlers: { label: string; nodes: DiagramNode[] }[], handlers: { label: string; nodes: DiagramNode[] }[],
@@ -106,7 +116,7 @@ export function useDiagramData(
// Then error handlers // Then error handlers
addHandlerSections(errorSections, 'error'); 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; const totalHeight = currentY;
return { sections, totalWidth, totalHeight }; return { sections, totalWidth, totalHeight };