Commit Graph

4 Commits

Author SHA1 Message Date
dayuan.jiang
0b03e15336 feat(diagram-engine): corners, borderless fills, shadows and strikethrough
Four more Tailwind classes, all four verified against draw.io's own source in
public/drawio rather than against a prose reference — which is how three earlier
exclusions turned out to be wrong:

  rounded-*      mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to
                 absolute pixels and halves it, so the same class is the same
                 corner on every box. Previously excluded as 'percentage only'
  shadow-sm..xl  mxShape.js:505-535 — getShadowStyle reads five independent
                 params, not one flag, so Tailwind's offset+blur rungs map one
                 to one. Previously excluded as 'six sizes collapse to one'
  line-through   mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both
                 mxText.js:723 and :1040. The bitmask has four bits, not three
  border-none    the only one of the four that adds something previously
                 inexpressible: a fill with no outline

Also fixes an edge style growing 76 characters per re-layout, without bound. The
router recomputes ports on every pass, and appending them to a style recovered
from the canvas — which already carried the previous pass's eight port keys —
grew the string forever. draw.io resolves duplicates last-wins so the arrow
always looked right; a byte-identity check is what caught it.

Two traps found while wiring the readback, both the same shape: a value the
THEME emits being recorded as one the model asked for. strokeColor=none from a
filled or ghost role, and rounded=0 from the fallback style. Either one would
outlive a set_role, since that clears style but keeps text.

Deliberately not included, with reasons in tw.ts: per-side borders and per-corner
radius (both would take the shape slot, and what a node IS matters more than
which of its edges show), per-side padding (draw.io's keys pad the label, not the
room left for children), text-shadow (a bare flag with no offset or blur),
opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading
(absent from draw.io — zero grep hits, not merely coarse).

615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is
real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while
an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
dayuan.jiang
6b5fd613f2 feat(diagram-engine): label avoidance, paired opposite edges, semantic group colours
A git-workflow flowchart rendered with no overlaps but read poorly. Three
distinct causes, each fixed and measured:

1. Edge labels sat on boxes and on each other (4 collisions on the
   reported diagram; 280 across 250 generated flowcharts). The router
   keeps LINES off the boxes but a label renders at its edge's midpoint,
   which on a long edge is beside exactly the things the line was routed
   around. placeLabels slides each label along its own edge to a clear
   spot — longest edges first, midpoint-outward tries — written as the
   geometry's relative x, which draw.io natively supports. Corpus: 280
   label collisions -> 8.

2. A->B and B->A were routed independently, so "git add" ran straight
   while "git reset" wandered through a different corridor with a kink.
   Opposite edges that agree on axis now get two absolute parallel tracks
   in the strip where the two boxes overlap, a constant 24px apart,
   converted back to port fractions. Zero crossing regressions.

3. All boxes rendered the same white, because the render layer's
   fill/stroke support was never reachable: neither add_box's schema nor
   draw_graph's nodes exposed it. Rather than exposing raw hex (the model
   picks mismatched saturations, differently every time), nodes take a
   semantic group name and the engine maps groups to a fixed palette of
   six paired fill/strokes in order of first appearance. The model names
   the zones - remote vs local vs temp - and never touches a colour.

532 unit tests pass; the 5 diagram e2e tests pass in a real browser.
2026-08-09 18:59:34 +09:00
dayuan.jiang
00ebf91b90 fix(diagram-engine): eliminate arrows drawn through boxes, complete the route search
A user's approval-workflow flowchart came out with the return arrow drawn
straight through two unrelated steps, and a second report showed arrows
leaving a box and bending straight back across it. Measured over 250
generated flowcharts (2722 edges): 347 arrows crossed an unrelated box and
215 waypoints landed inside a shape. Four defects, each measured in
isolation:

1. The invisible layer containers draw_graph emits were handed to the
   router as frames, so every clean return path was rejected for
   "trespassing" on a border that is not drawn, and the fallback cut
   through two boxes. Excluding invisible containers: 347 -> 218 crossing
   arrows, no diagram made worse.

2. The router chose the horizontal-vs-vertical axis BEFORE searching, so
   when the only clean corridor ran along the other axis it was never
   looked at. A complete two-bend candidate generator that tries both
   trunk axes, all four sides at each end, and the port fractions:
   218 -> 27. (An independent ablation measured the axis pre-choice alone
   at a 40% per-edge failure rate.)

3. Nothing stopped a route's first leg from turning back across its own
   source shape - the obstacle test exempts an edge's own endpoints, and
   must, since the line has to touch them. A terminal-leg rule refuses
   such routes outright: 215 -> 4 hooks.

4. A two-bend search cannot express the staircase needed when a box sits
   directly between two vertically aligned nodes (21 of the last 27
   crossings). Added the orthogonal visibility graph + A* from Wybrow,
   Marriott & Stuckey, "Orthogonal Connector Routing" (GD 2009) - the
   libavoid algorithm - as the backstop when the candidate search finds
   nothing. The interesting-points grid is provably sufficient: any valid
   route shrinks onto it without getting longer or gaining bends. The A*
   state is (point, incoming direction) with libavoid's bend cost of 10,
   and the admissible bends-remaining heuristic, so it returns a cheapest
   route, not merely a route. Implemented from the paper, not ported.

After all four: 0 crossing arrows and 0 hooks over the same 250 diagrams,
page area unchanged (494k px^2 mean), 260ms for the whole corpus, mean
0.82 bends per edge. The shape ladder still runs first, so routes that
were already clean are byte-identical.

Also post-nudge validation now checks the whole path (the nudge pass only
reverts the single segment it moved, judged in isolation) and restores the
search's route if nudging made it dirty.
2026-08-09 18:18:54 +09:00
dayuan.jiang
a3814f702d feat(diagram-engine): flowcharts, swimlanes, sequence diagrams and mind maps
Extends the declarative engine past cloud architecture. The tool routing was
divided by icon library — AWS through the engine, everything else hand-written
XML — which is the wrong axis. What matters is the LAYOUT SHAPE.

Measured first: a six-step approval flow declared in its natural order comes out
as one column, because the layout only arranges what nesting tells it to and
never looked at the arrows. That forces the arrow from the decision to its second
branch to jump over the first branch.

graph.ts computes what the layout should have looked at: layer assignment by
longest path, cycle breaking so a loop is drawn without setting the order, and
barycentre sweeping to cut edge crossings. It emits ordinary container
operations, so layout, routing and round-tripping are unchanged — reaching zero
arrows-through-boxes on a 14-node pipeline and zero crossings on a bipartite
graph whose declared order forces three.

Three new container kinds, each because one layout rule cannot serve them all:

  pool     — swimlanes. Lanes are real cells and each step is parented to its
             band, so dragging a step to another role records the change.
  sequence — participants across the top, one lifeline cell per participant so
             head and line stay together on a drag. Messages bypass the router:
             a message's height IS its order.
  radial   — mind maps and org charts. Children are a flat list and the
             hierarchy comes from the links, because a branch is a box and a box
             cannot hold children.

Flowchart box shapes (diamond, stadium, parallelogram, document) so a reader can
tell a branch from a step.

Two bugs the new tests caught: the duplicate-link guard blocked a sequence
diagram from having two messages between the same pair, and the fallback message
numbering was shared across containers, pushing a second diagram's messages off
its own lifelines.

523 unit tests and 17 diagram e2e tests pass. Every kind verified round-trip
stable to a fixed point, and rendered in a real browser — draw.io keeps the
lifeline shape and the lane markers.
2026-08-09 13:49:11 +09:00