Skip to content

Canvas ux - #49

Merged
dbose merged 6 commits into
mainfrom
canvas-ux
Sep 30, 2026
Merged

dbose merged 6 commits into
mainfrom
canvas-ux

Conversation

@dbose

@dbose dbose commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Makes the Studio Canvas usable for large, domain-organised models (surfaced by a 46-entity erwin import). Five commits, each self-contained.

Hover flicker (e64bc55)

Hovering a PK/FK rebuilt every node + edge, flickering the whole screen on large models. Split hover highlighting into a lightweight effect that mutates only dimmed/highlighted in place; the heavy structural derivation no longer re-runs on mouse-move.

Subject-area clustering layout + coloring (9ba8f5d)

A single dagre pass stacked a domain constellation into one tall column. Added a clustered layout: group by subject area, lay each group out compactly (dagre for small connected groups, grid otherwise), pack into a roughly-square grid. Also fixed coloring — reversed/erwin models carry membership only on the area (subject_areas[].members), so every card had fallen back to grey; membership is now resolved from both the direct pointer and the inverted members list. Fund fixture aspect 0.05 → 1.2, all 46 cards colored across 5 domains.

Flow-direction toggle (3e339d8)

A direction control grouped with the layout picker cycles dagre flow → ↓ ← ↑ (rankdir LR/TB/RL/BT) and re-applies the current layout. Remembers the last layout; default stays LR.

Toolbar redesign + subject-area filter drawer (432cfcb)

Replaced the single cramped glyph-soup row (whose wrapping legend shoved the bar taller) with a two-row grouped toolbar using inline SVG icons (Panels / View / Data groups), adopting the toolbar shell + icons.tsx from feat-semantic-compare-merge so the branches converge on one toolbar. Turned the static subject-area legend into an interactive left slide-out drawer: one row per domain (color chip, name, entity count); click to focus one (hide the rest, fit to it), checkboxes multi-select a union, "Show all" clears, with a filter-count badge on the hamburger. A shared hiddenByArea predicate drives React Flow hidden; the hover effect preserves it so filtered nodes never re-appear.

Verified live

A pip upgrade of modelith-dbt can leave an mdl.exe console-script shim whose
embedded interpreter path no longer resolves under spawn — it still runs from
a shell (so `where mdl` and a manual `mdl --version` work), but the extension
spawns it and gets ENOENT, so every model action failed until reload.

Two fixes in mdl.ts:
- findMdl re-validates a cached ABSOLUTE binary path still exists before
  trusting it (an upgrade can relocate the install), re-resolving if not.
- runMdl, on a spawn ENOENT, drops the cache and retries once via a freshly
  resolved binary — which falls through to `<python> -m mdl_cli` (no shim),
  the reliable path. So a broken shim after an upgrade auto-recovers instead
  of failing until VS Code reloads.

Typecheck + all extension gates pass. Extension 0.3.18.
Hovering a PK/FK (or any attribute row) on a 50+ entity model made the whole
canvas flicker. The attribute-hover highlighting fed the heavy node/edge
derivation effect (hoveredAttr/hoveredEdge were in its deps), so every mouse
move over a row rebuilt all N nodes and M edges and reset them into React
Flow — a full re-render storm.

Split hover out into a lightweight overlay: the heavy effect now handles only
structural view state (search + selection), and a separate effect applies the
hover dim/highlight by mutating just the affected nodes'/edges' data fields in
place (same identities and positions, and it returns the previous object
untouched when nothing changed, so React Flow re-renders only what actually
differs). The highlight behavior is unchanged — hovering an FK still dims
unrelated entities and lights up the joined columns on both sides.

Verified on the ibor model: real hover dims 11 nodes, highlights the 2 joined
entities and their paired rows, with zero viewport re-layout (no flicker).
Typecheck clean, canvas builds.

(The large-model LAYOUT quality is a separate follow-up: subject-area
clustering, since dagre stacks a constellation into one column and the grid
ignores FKs.)
Large models auto-laid-out by a single dagre pass stack a domain
constellation into one tall column. Add a clustered layout that groups
entities by subject area, lays each group out compactly, then packs the
groups into a roughly-square grid of clusters.

The same missing membership resolution also blanked entity COLORING: a
reversed/erwin model carries subject-area membership only on the area
(subject_areas[].members lists conceptual ULIDs), leaving
entity.conceptual.subject_area null — so every card fell back to
NO_SA_COLOR and the whole model read as one grey blob with an empty
legend.

- Resolve each entity's subject area {id, name} from BOTH the direct
  conceptual.subject_area pointer AND the inverted members list (via the
  entity's conceptual id). Use the id for node color, the name for
  layout grouping.
- Per cluster: dagre only for a small, well-connected group (<=8 nodes,
  a real internal chain); everything else grids, since a large or
  cross-domain-linked group collapses into a single dagre rank.
- auto picks clustered past 40 entities when the model has >1 area, else
  the grid packer; a 'Clustered (by area)' option forces it.
Add a direction control grouped with the layout picker (⌗ ▾ →) that
cycles the dagre flow direction → ↓ ← ↑ (rankdir LR/TB/RL/BT) and
re-applies the current layout. Lets a large model flow left-to-right,
top-to-bottom, or reversed — the same 'top-left-to-bottom-right' framing
without a separate layout per direction.

- layout.ts: thread a LayoutDir through layoutGraph → dagrePass /
  hierarchicalLayout / clusteredLayout; grid ignores it (no-op).
- The picker remembers the last chosen layout, so cycling the direction
  re-lays that layout rather than resetting to auto. Direction persists
  across relayouts; default stays LR (no change on load).
The toolbar was a single non-wrapping flex row cramming brand, search, a
subject-area colour legend, stats, and ~11 cryptic Unicode glyph buttons.
With several domains the legend wrapped and shoved the whole bar taller,
and subject areas were a static key with no way to focus one.

Redesign:
- Two-row grouped toolbar with inline-SVG icons (no font dependency,
  crisp on every OS). Row 1: hamburger, brand, search, stats. Row 2:
  + Entity, then labelled Panels / View / Data groups. Ports the toolbar
  shell + icons.tsx from feat-semantic-compare-merge so both branches
  converge on one toolbar; keeps the layout picker + flow-direction
  toggle already on this branch.
- New left slide-out drawer (CanvasDrawer): the subject-area colour key
  becomes an interactive FILTER — one row per domain with colour chip,
  name, and entity count. Click a domain to show ONLY it (hide the rest,
  fit to it); checkboxes multi-select a union; 'Show all' clears. A count
  badge on the hamburger shows how many areas are filtered.
- Canvas hide engine: a shared hiddenByArea predicate sets React Flow
  hidden on out-of-filter nodes (auto-hides their edges); when filtering,
  the layout runs over only the visible nodes so the focused domain fills
  the canvas. The lightweight hover effect preserves hidden, so hovering
  a PK/FK never un-hides a filtered node.
- Drop the old wrapping .legend; port the grouped-button CSS; add the
  drawer + sa-row styles (the first slide transition in the canvas).

Validated live on the 46-entity / 5-domain fund model: focus, multi-
select union, show-all, hover-vs-hide consistency, minimap, and colouring
all correct; no console errors.
@dbose
dbose merged commit 0ea7a23 into main Sep 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant