diff --git a/frontend/src/bindings b/frontend/src/bindings
index 6285c0c..568e9bb 160000
--- a/frontend/src/bindings
+++ b/frontend/src/bindings
@@ -1 +1 @@
-Subproject commit 6285c0c88e6f515d519fe01860f3f6770b2274e4
+Subproject commit 568e9bbe5a3c5961e103557d286a100a06805fd2
diff --git a/frontend/src/components/shared/PlaceableGraph.tsx b/frontend/src/components/shared/PlaceableGraph.tsx
index 783bb87..83e7c33 100644
--- a/frontend/src/components/shared/PlaceableGraph.tsx
+++ b/frontend/src/components/shared/PlaceableGraph.tsx
@@ -242,9 +242,57 @@ function detourPath(
return {d, mid};
}
+/**
+ * Same-column edge: rectangular path that routes between nodes vertically.
+ * Path: exits right from source → goes vertical to target level →
+ * goes left back to column → enters from left into target.
+ *
+ * This creates an upside-down U or right-side U that curves around
+ * intermediate nodes in the same column without doubling back.
+ */
+function sameColumnPath(
+ srcX: number, srcY: number,
+ tgtX: number, tgtY: number,
+): { d: string; mid: { x: number; y: number } } {
+ const hw = NODE_W / 2;
+ const pad = 25; // How far right to swing
+
+ // Exit and entry points
+ const exitX = srcX + hw;
+ const entryX = tgtX - hw; // = srcX - hw (same column)
+ const rightX = srcX + hw + pad;
+ const leftX = srcX - hw - pad;
+ const flipY = tgtY - (tgtY > srcY ? 1 : -1) * (NODE_H / 2);
+
+ // Path: right → vertical → left
+ const r = 7;
+ const v = tgtY > srcY ? r : -r;
+ const d = [
+ `M ${exitX} ${srcY}`,
+ `L ${rightX - r} ${srcY}`,
+ `Q ${rightX} ${srcY}, ${rightX} ${srcY + v}`,
+ `L ${rightX} ${flipY - v}`,
+ `Q ${rightX} ${flipY}, ${rightX - r} ${flipY}`,
+ `L ${leftX + r} ${flipY}`,
+ `Q ${leftX} ${flipY}, ${leftX} ${flipY + v}`,
+ `L ${leftX} ${tgtY - v}`,
+ `Q ${leftX} ${tgtY}, ${leftX + r} ${tgtY}`,
+ `L ${entryX} ${tgtY}`
+ ].join(" ");
+
+ // Label positioned on the right side at the midpoint height
+ const mid = {
+ x: rightX,
+ y: flipY,
+ };
+
+ return {d, mid};
+}
+
/**
* Compute all routed edges. Edges spanning multiple columns with intermediate
* nodes get detour paths; others get direct quadratic bezier curves.
+ * Same-column edges get an S-curve to the right.
* Self-loops get an arc path above the node.
*/
function computeRoutedEdges(
@@ -310,7 +358,11 @@ function computeRoutedEdges(
}
}
- if (!hasIntermediate) {
+ if (srcCol === tgtCol) {
+ // Same-column edge: rectangular path avoiding backward cut
+ const {d, mid} = sameColumnPath(src.x, src.y, tgt.x, tgt.y);
+ result.push({index: i, d, mid, label: e.label, tooltip: e.tooltip, edgeType: e.edgeType});
+ } else if (!hasIntermediate) {
// Direct edge with pair offset
const offset = pairOffsets[i];
const d = directPath(src.x, src.y, tgt.x, tgt.y, offset);
@@ -344,6 +396,30 @@ function computeRoutedEdges(
}
}
+ // Post-process: spread apart label midpoints that would visually overlap.
+ // This handles crossing edges (e.g. Na→N+1b and Nb→N+1a) whose midpoints
+ // coincide at the center of the quadrilateral formed by the four nodes.
+ const LABEL_COL_W = 52; // approximate label box half-width threshold
+ const LABEL_COL_H = 22; // approximate label box height + margin
+ for (let i = 0; i < result.length; i++) {
+ if (!result[i].label || result[i].isSelfLoop) continue;
+ for (let j = i + 1; j < result.length; j++) {
+ if (!result[j].label || result[j].isSelfLoop) continue;
+ const dx = Math.abs(result[i].mid.x - result[j].mid.x);
+ const dy = Math.abs(result[i].mid.y - result[j].mid.y);
+ if (dx < LABEL_COL_W && dy < LABEL_COL_H) {
+ const push = (LABEL_COL_H - dy) / 2 + 2;
+ if (result[i].mid.y <= result[j].mid.y) {
+ result[i] = {...result[i], mid: {...result[i].mid, y: result[i].mid.y - push}};
+ result[j] = {...result[j], mid: {...result[j].mid, y: result[j].mid.y + push}};
+ } else {
+ result[i] = {...result[i], mid: {...result[i].mid, y: result[i].mid.y + push}};
+ result[j] = {...result[j], mid: {...result[j].mid, y: result[j].mid.y - push}};
+ }
+ }
+ }
+ }
+
return result;
}
@@ -796,6 +872,30 @@ export function PlaceableGraph(props: PlaceableGraphProps) {
onCleanup(() => svg.on(".zoom", null));
});
+ const drawEdge= (edge: RoutedEdge, isHov: boolean) => {
+ const bw = () => Math.max(60, edge.label.length * 6.2 + 16);
+ const bh = 18;
+ return (
+