feat(erd-editor): Lay tables out with ELK, from an Auto Layout menu - #445
Conversation
schema-gc and export-png each carried their own withTimeout, identical but for the message it rejects with, and the layout service about to arrive would have made a third. The one copy now lives in utils/promise.ts and takes that message as an argument. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The force simulation was the only way to place a schema, and it says nothing about which table feeds which. ELK answers that: the context menu item is now a submenu of Force, Flow and the two Tree directions, and the same four reach the editor through one emitter action that carries the placement. Flow is the layered algorithm handed a port per relationship endpoint, ordered by the column row it leaves a table at, so ELK weighs the crossings the editor will really draw. A fixed order runs clockwise from the top left corner, which is why an east side is listed down the rows and a west side back up them. ELK is 1.6 MB of transpiled Java, so it is reached only from a shared worker and only through a dynamic import: a static one is evaluated before any statement in its own module, and the stub that gives the worker realm a document would then be too late to be read. There is no in-process rung, which would put a second copy of that bundle in every build that splits no chunk off. A dev server discovers neither import on its own, so three configs name it. Unlike the simulation there is nothing to weigh before a layout lands, so the three ELK placements apply as they arrive, take no preview overlay and leave one undo step behind them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ELK registers the metadata of nine algorithms by default, and the placements name one. The list is read off the options rather than written out, because a placement naming an algorithm left out of it would be answered with an error from inside a worker, which is a poor place to find one. The bundle is unchanged: every algorithm is compiled into it either way, and trimming that would take a fork of the java build for about a sixth of its weight, most of which is the layered algorithm this uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Added
export const ELK_ALGORITHMS: string[] = [
...new Set(
Object.values(TablePlacement)
.filter(isElkPlacement)
.map(placement => elkLayoutOptions(placement)['elk.algorithm'])
),
];Verified the layout is byte-identical for all three placements with and without the option, before making the change. On trimming the bundle itselfWorth recording, since it comes up: a custom build can drop unused algorithms —
Dropping all six buys ~15% of 5,347 kB, and GWT already prunes within plugins, so the realised saving would be less. Paying for that with a forked Java/GWT build published to npm — the only shipping route, since vendoring the blob would put ~400 kB gzip into |
The force simulation was the only way to place a schema, and it says nothing about which table feeds which. This adds three ELK placements beside it.
The menu
Automatic Table PlacementbecomesAuto Layout, and the flat item becomes a submenu. Quick search ($mod+K) mirrors it.layered+ ports,SIMPLEnode placementlayered, directionDOWNlayered, directionRIGHTAll four reach the editor through one emitter action carrying the placement, the way
openDiffVieweralready worked.Flow, and why the ports matter
A relationship in an ERD meets a specific column, so each endpoint becomes an ELK port ordered by the row it sits on, under
portConstraints: FIXED_ORDER. ELK then weighs the crossings the editor will really draw rather than treating a table as one anonymous box.A fixed order runs clockwise from the top left corner, so an east side is listed down the rows and a west side back up them. That was measured against ELK, not assumed — listing a west side downwards comes out upside down, and two tests fail if the reversal is dropped.
nodePlacement.strategy: SIMPLEis what centres a branch point over what it feeds; the default pulls it onto the longest straight run its layer allows. Verified at real table sizes.Why a worker, and two traps on the way there
elkjs is 1.6 MB of transpiled Java — larger than the editor itself — so it is reached only from a shared worker, and never touches the main chunk (
grep 'Cannot construct an ELK' dist/index.jsfinds nothing). There is deliberately no in-process rung: that would put a second copy of the bundle in every build that splits no chunk off, and it did, until the fallback came out.Two failures were only visible past the unit and browser suites:
_Worker is not a constructor.elk-worker.min.jsdecides it is the worker script whendocumentis undefined andselfis not, installs anonmessagehandler and exports no constructor. Inside a SharedWorker that is exactly the case. A stub document fixes it — but a static import of elkjs hoists above the statement that installs the stub, so the fix worked in the dev server and not in the build. ELK is now reached through a dynamic import, and a source-invariant test pins that no shipped file imports it statically.A page reload mid-layout. Vite's dependency scanner does not walk a worker entry, so elkjs was discovered at runtime and the re-optimisation reloaded the page, losing the document. Three configs name it in
optimizeDeps.include;appneeds the nested@dineug/erd-editor > elkjs/...spelling because elkjs is not its own dependency.Behaviour
Unlike the simulation there is nothing to weigh before a layout lands, so the ELK placements apply as they arrive — no Apply/Cancel step, no preview overlay, and one undo step puts every table back. The overlay is why a pointless zoom-out sat between the click and the result; it now opens for Force only.
Verification
pnpm check,pnpm test(7,370 inerd-editor),pnpm build,pnpm size(+6,780 B, 5,500 B under budget)e2e/specs/automatic-table-placement.spec.ts— the one layer that reads the built package, which is where both traps above showed upappfrom a cold cache: menu → Flow → tables placed, no console or page errorsNotes for review
layeredVertical/layeredHorizontal:layeredis ELK's own algorithm name, and the real tree algorithm (mrtree) is not what these run.🤖 Generated with Claude Code