Skip to content
Merged
Show file tree
Hide file tree
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
34 changes: 31 additions & 3 deletions canvas/src/ModelCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,36 @@ export function ModelCanvas({
}
}
layoutedRef.current = true;
// Only auto-fit when there is NO saved layout — a restored arrangement must not
// be re-zoomed away.
if (!hasSaved) requestAnimationFrame(() => fitView({ padding: 0.15, duration: 300 }));
// Fit the viewport to what the user is actually looking at.
// - Filtering: ALWAYS frame just the visible subset. Every entity is still in the
// node array (hidden ones flagged), each keeping its full-model position, so a
// plain fitView() zooms to the whole 79-node extent and leaves the handful of
// filtered cards as specks in a corner (onlyRenderVisibleElements can even cull
// them). We bound the VISIBLE nodes from their positions — same approach as the
// search-framing effect — because fitView({nodes}) can't measure an off-screen
// (culled) node, whereas a rect built from positions works regardless.
// - Not filtering: fit all, but only when there is NO saved layout — a restored
// arrangement must not be re-zoomed away.
if (filtering) {
const vis = merged.filter((n) => !n.hidden);
if (vis.length) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const n of vis) {
const h = nodeHeight(n.data.entity);
minX = Math.min(minX, n.position.x);
minY = Math.min(minY, n.position.y);
maxX = Math.max(maxX, n.position.x + NODE_WIDTH);
maxY = Math.max(maxY, n.position.y + h);
}
const rect = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
requestAnimationFrame(() => fitBounds(rect, { padding: 0.2, duration: 300 }));
}
} else if (!hasSaved) {
requestAnimationFrame(() => fitView({ padding: 0.15, duration: 300 }));
}
emitEdges(merged);
return merged;
});
Expand All @@ -554,6 +581,7 @@ export function ModelCanvas({
activeAreas,
endpointAttrsByEntity,
fitView,
fitBounds,
layoutStore,
onRouteCommit,
onAnchorCommit,
Expand Down
4 changes: 4 additions & 0 deletions canvas/src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ function dagrePass(
marginx: 24,
marginy: 24,
ranker: "network-simplex",
// ER graphs routinely have FK cycles (self-refs, mutual references). Without a
// feedback-arc-set pass dagre's rank assignment degrades on a cyclic subgraph; greedy
// acyclic-ing keeps the layered "sources higher, derived lower" hierarchy readable.
acyclicer: "greedy",
});
g.setDefaultEdgeLabel(() => ({}));

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading