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));
}
// 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<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
// ----------------------------------------------------------------

View File

@@ -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 };