Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions packages/viewer/src/client/renderers/sgcr/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,12 @@ function assignCoordinates(
// at one node can never visually stack (an "other artifact" the vision pass caught). PORT_STEP ≥
// arrowhead width is the by-construction guarantee for arrowhead separation.
const PORT_STEP = 16;
// Arrowheads sit at an edge's TARGET, so only TARGET-side ports need the full arrowhead-clearance
// spacing. SOURCE-side ports (no arrowhead) only need to be visually distinct — packing them tighter
// lets a fork keep its in-line child's port centred (straight connector) instead of shoving it off.
const SOURCE_STEP = 8;
const LOOP_W = 26; // cell cross reserved on the right for a self-loop
const reversedOf = new Map(edgeRecs.map((er) => [er.id, er.reversed]));

// Build wires (one per unit sub-edge) first so we know each channel's load + each node's port demand.
const wires: Wire[] = [];
Expand Down Expand Up @@ -468,7 +473,7 @@ function assignCoordinates(
// + arrowheads can't stack) and within the node face; an endpoint outside the face clamps to the face
// edge (that edge must turn — it's genuinely off to the side). Ordering by neighbour cross also keeps
// the fan from self-crossing at the node.
const distribute = (it: Item, list: Wire[]) => {
const distribute = (it: Item, list: Wire[], isFar: boolean) => {
if (it.isDummy) { for (const w of list) setPort(w, it.id, it.cc); return; }
if (list.length === 0) return;
const parity = it.rank % 2 === 1 ? 0.5 : 0;
Expand All @@ -478,21 +483,25 @@ function assignCoordinates(
it.portHi = hiI;
const sorted = list.slice().sort((p, q) => otherCross(p, it.id) - otherCross(q, it.id) || (p.edgeId < q.edgeId ? -1 : p.edgeId > q.edgeId ? 1 : 0));
const n = sorted.length;
// Forward pass: each port at its desired (clamped) col, pushed right to keep PORT_STEP spacing.
// A port carries an arrowhead (→ needs full PORT_STEP) iff `it` is the edge's TARGET end here: the
// higher-rank end of a forward edge, or the lower-rank end of a reversed one. Flat edges keep the
// full step (conservative). The min gap before port i is PORT_STEP if either neighbour is a target.
const isTarget = sorted.map((w) => w.flat || (isFar ? !!reversedOf.get(w.edgeId) : !reversedOf.get(w.edgeId)));
const gap = (i: number) => (isTarget[i - 1] || isTarget[i] ? PORT_STEP : SOURCE_STEP);
// Forward pass: each port at its desired (clamped) col, pushed right to keep the per-gap spacing.
const cols: number[] = [];
let prev = -Infinity;
for (let i = 0; i < n; i++) {
let col = Math.max(loI, Math.min(hiI, Math.round(otherCross(sorted[i], it.id))));
if (col < prev + PORT_STEP) col = prev + PORT_STEP;
cols[i] = col; prev = col;
if (i > 0 && col < cols[i - 1] + gap(i)) col = cols[i - 1] + gap(i);
cols[i] = col;
}
// If the forward pass overflowed the face (cluster pushed past hiI), pull back from the right.
if (cols[n - 1] > hiI) { let next = hiI; for (let i = n - 1; i >= 0; i--) { if (cols[i] > next) cols[i] = next; next = cols[i] - PORT_STEP; } }
if (cols[n - 1] > hiI) { let next = hiI; for (let i = n - 1; i >= 0; i--) { if (cols[i] > next) cols[i] = next; next = cols[i] - (i > 0 ? gap(i) : 0); } }
for (let i = 0; i < n; i++) setPort(sorted[i], it.id, cols[i] + parity);
};
for (const it of items.values()) {
distribute(it, farWires.get(it.id)!);
distribute(it, nearWires.get(it.id)!);
distribute(it, farWires.get(it.id)!, true);
distribute(it, nearWires.get(it.id)!, false);
}

// STRAIGHTEN SPINE EDGES (Brandes-Köpf-style turn minimization): each node's PRIMARY wire on a face
Expand Down
Loading