Skip to content

feat(diagram): grid-aligned editing, automatic orthogonal routing, editable connection shapes, text inside node outlines and re-placed Templates (v0.25.0) - #345

Merged
MerciHanrim merged 10 commits into
mainfrom
feat/diagram-grid
Oct 10, 2026
Merged

MerciHanrim merged 10 commits into
mainfrom
feat/diagram-grid

Conversation

@MerciHanrim

@MerciHanrim MerciHanrim commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Part of #344: Diagram Readability as one migration (v0.25.0), so coordinates, routes, Templates, node outlines and the visual baselines change once. Nodes and frames snap to a 16 px grid with smart guides, connections are routed at right angles around nodes, a connection can be Curved, Straight or Orthogonal with bend points, the five shaped node kinds keep their text inside their outline, and all five Templates are placed again. An ordinary older document is re-placed once when it opens; records (revisions, proposals, a revision-based Project autosave) are not. The engine, the RNG and simulation results are unchanged.

What changes

  • The grid and the port row (docs/diagram-layout.md §DL1–§DL3). Resource ports sit on a fixed row 28 px below a node's top; a node is on the grid when its left edge and port row are multiples of 16. New nodes land on the grid; a drag snaps as one, a frame drag its top-left, a resize its corner; arrow keys move 16 px (Shift 64 px). Alt is a free move; moving a frame alone is Ctrl / ⌘ (was Alt).
  • Smart guides (§DL3.6, smartGuides.ts, SmartGuideLayer.tsx). Within 6 screen px a drag is pulled to another node's port row, then its centre or edges, then the grid; dashed landing lines and solid alignment lines; the grabbed point never changes the landing. Alt shows them faint and pulls nothing. Not on the phone. Display and input only.
  • The one-time conversion and Tidy to grid (§DL2, §DL2.8–§DL2.10). layoutVersion (1) is written on every save; an ordinary document without it is re-placed once on open, before its history starts, by the pure replace.ts rules (grid, related-row merge, 48 x 32 clearance by inserted space with no order reversed, frames rebuilt from their held and covered nodes with their padding kept, waypoints snapped), and its unshaped connections become automatic orthogonal. Revisions, proposals and a revision-based Project autosave keep their recorded layout and shapes. Tidy to grid (desktop Controls rail) applies the same re-placement as ONE undo entry, never changes a shape, and is off while locked.
  • The guarded router (docs/edge-routing.md §ER14). computeGuardedRoute never crosses a node fill, own end nodes included (stubs only), with the clearance ladder 12 / 6 / 2 / 0 px, then the outer corridor; blocked only where another node covers a port. A same-port fan shares only its 16 px stub and branches there. Labels take the first free slot on their own route, then up to two soft reroute rounds. A provisional map at once and the full generation in slices; aria-busy on the canvas until the first routes are drawn (§ER14.5). Failures are reported in routeDiagnostics(), never hidden.
  • Connection shapes and bend points (§ER15–§ER16, §DL4, docs/specs/SEMANTICS-R10.md). Route: Orthogonal (default), Curved, Straight; Manual orthogonal is Orthogonal with 1 to 64 bend points. Add bend (click or Enter), drag, arrow keys, Delete, Reset to automatic; each one undo step; grid-snapped, Alt free; nothing while locked. Curved and Straight do not avoid nodes but their labels take a free slot. route: "straight" is loop-revision/10 (Frozen); a graph without one is byte-identical in revision bytes and digests. The phone draws every shape and offers no route editing.
  • Text inside the outline (fix(canvas): text stays inside the Pool, Source, Drain, Converter and Gate outlines at every width #337, docs/node-shell-content-in-vessel.md). The Pool, Source, Drain, Converter and Gate are drawn for their own width with fixed-depth features, and every painted element keeps 8 px inside the drawn outline in all 18 languages; ports and the conversion mark read the outline as drawn. canonicalBox stays an upper bound of the drawn box; the conversion reads the frozen pre-fix(canvas): text stays inside the Pool, Source, Drain, Converter and Gate outlines at every width #337 box (legacyCanonicalBoxBeforeOutlineContainment) for frame membership.
  • The Templates (§DL5). All five are placed by placeTemplate with each node at its widest box over the 18 languages plus 48 x 32 clearance, FRAME_GAP 32, every connection Auto (no Manual exception; the Gacha Template's two waypoints are gone). Opening views: Coffee at about 0.5 in 1280 x 800 (floor 0.46); MMO with a keep rect so Character creation, Active character, Starter encounters and Starter Lv 1–5 open whole in every language with the minimap expanded or collapsed (1.040 at 1600 x 1000, smallest text 12.5 px; §DL5.6); Gacha unchanged.

Compatibility

v0.24.0 and earlier open a v0.25.0 diagram with a Straight connection but draw it Curved, refuse the project header of such a revision, and drop route: "straight" when saving again, without a warning (measured, SEMANTICS-R10.md §R10-5). Round-tripping through an earlier version is not supported.

Contract changes

  • docs/diagram-layout.md (new): the grid, the port row, re-placement, the conversion and its record exceptions, frames, Tidy, snapping and smart guides, connection shapes, the Templates and their opening views.
  • docs/edge-routing.md §ER14–§ER16: the guarded router, fans and labels, the route settle contract, Curved / Straight, the bend point editor.
  • docs/specs/SEMANTICS-R10.md (new, Frozen): loop-revision/10, the straight connection.
  • docs/node-shell-content-in-vessel.md: the width-parametric outlines (fix(canvas): text stays inside the Pool, Source, Drain, Converter and Gate outlines at every width #337) and the 8 px containment.
  • docs/visual-language.md: the side handles stay on the 28 px port row.
  • docs/large-graph-readability.md, docs/large-graph-readability-auto-frames.md, docs/contextual-inline-help.md: Ctrl / ⌘ for a frame alone, the 16 / 64 px keyboard steps, the re-measured auto-frame counts.
  • docs/mmo-multilingual-layout.md §MML3: the port row and the keep rect.

Tests

  • New e2e specs: diagram-grid (8), smart-guides (7), connection-shapes (17), outline-containment (11); frame-a11y +1 (an off-grid frame lands on the grid).
  • Seven titles renamed for the changed behaviour, none removed: the 16 / 64 px frame and node keyboard steps, Ctrl+drag for a frame alone and its one-time note, the side handles on the port row (two), and the Source / Drain / Converter mode rows inside the outline.
  • Test list: chromium 1,928 → 1,972 (+44), mobile 117 and portable 15 unchanged, dist 16 and pwa 19 unchanged.
  • Unit tests 3,289 → 3,465: re-placement (replace.ts, idempotence, users' coordinates, the frozen legacy box table), the guarded router and fans, the R10 reading rules, outline containment, Template placement, smart guides, the keep fit cap and the MMO keep union.

Baselines

All 76 captured from the final rendering and compared on every channel.

  • 57 replaced: 49 that failed and 8 inside the tolerance that show the new grid, routes or outlines — canvas-refresh-visual (16), flow-colour-views-visual (9), flow-colour-visual (5), forced-colors-edge-tell (2), i18n-visual (4), large-graph-readability (8), minimap (2), model-nodes-visual (1), playback-visual (8), state-ui (2).
  • 6 intentionally retained: 3 unrelated drift, inside the tolerance and not caused by this change (data-import-quickstart-chromium, ko-monte-carlo-chromium, ko-review-chromium), and 3 known noise, 0 px over the threshold (data-import-inline-errors-chromium, mobile-sheet-toggles-dark-mobile, mobile-sheet-toggles-light-mobile).
  • 13 byte-identical.

Checks run locally

  • npx tsc -b, unit tests 3,465 passed, every source check:* (25), oxlint at its 39-warning baseline.
  • Every spec that owns baselines, 1 worker, retries 0: chromium and mobile 334 passed, 5 skipped, 0 failed, 0 retried; all 76 baselines byte-identical to the approved set before and after the run (on the tree before the release-note copy, which no baseline shows).
  • The affected specs after the final fixes, 1 worker, retries 0: minimap-responsive + template-load-viewport 16 passed; whats-new (chromium and mobile, every language) 96 passed; the dist suite 16 passed.
  • The MMO opening view in 18 languages x 1600 x 1000 / 1280 x 800 x minimap expanded / collapsed: the four core start nodes whole and clear of the minimap in all 72.
  • At users' coordinates (the Templates as saved before this release, converted, 18 languages): no blocked, outer or fan-overlapping route, no frame cut, node overlap or line through a node.
  • One earlier full local e2e found three test defects, fixed here (a requestAnimationFrame stub leak in register-expr-authoring, a missing route settle wait in dist, a one-axis minimap check in minimap-responsive, which then exposed the MMO opening-view defect fixed with keep) and one environment interruption (a network restart). The final full regression is the CI shard union of this PR.
  • CI split: the first CI run of this PR (38032960035, 30bf3d4) cancelled shard 4/5 at the 20-minute job limit after 379 of its 386 tests, with no failure; the weights are refreshed from that run and the suite now runs as six time-cut shards, each predicted 3 min 59 s or more under the limit (docs/ci-e2e-shards.md). Its one flaky retry (i18n-zh-hans.spec.ts:91, an 8 s app-ready timeout) is recorded on test(e2e): the zh-Hant descriptive-copy wrapping test slows down on CI and ended a browser session #305.
  • CI run 38034892101 (1b2824d, six shards, attempt 1): 2,105 + dist 16 + PWA 19, 0 failed; the aggregate passed with one known test(e2e): the zh-Hant descriptive-copy wrapping test slows down on CI and ended a browser session #305 zh-Hant startup retry (whats-new.spec.ts:1002, recorded on test(e2e): the zh-Hant descriptive-copy wrapping test slows down on CI and ended a browser session #305).

Release

v0.25.0: package.json, the release-note entry (five lines in 18 languages, 16 without native review; the Spanish lines are identical in es-ES and es-419 and the Portuguese lines in pt-BR and pt-PT), 15 added and 2 revised strings, .changes/diagram-grid.json, CHANGELOG.md, README.md. The release date (2026-10-10) is checked against the Seoul day right before the merge.

…out migration, Tidy to grid

Part of #344 (the v0.25.0 Diagram Readability migration). Local checkpoint, not
for release on its own: release notes, change declaration and baselines are
settled once after step 6.

- 16 px grid and a fixed resource port row 28 px below each node's top, drawn
  on the outline; the router starts there.
- Drags, drops, palette clicks, Insert module and imports land on the grid;
  arrow keys move 16 / 64 px; Alt = free move, Ctrl / Command = frame only.
- layoutVersion 1: ordinary documents, files, share links and the autosave are
  re-placed once on open, before history; revision and proposal records keep
  their coordinates. Tidy to grid is one undo step.
- Fixes found by the full e2e: ports paint above the silhouette, a zero-delta
  frame click never snaps, the click ending a snapped frame gesture is
  swallowed.
- Gacha Template layout round 8: two waypoints.
- Screenshot baselines NOT updated (36 known Step 1 mismatches, recorded).
…abels, sliced route map

- Guarded router: own end nodes are obstacles, clearance ladder 12/6/2/0 then an outer corridor, a geometric fill check on every result, blocked only when a port is covered.
- Port fans: shared stub of at most 16 px, staggered branch points, reserved stubs and first legs, fanBlocked fallback.
- Labels on free slots of the own route, two soft re-routing rounds.
- Route map: provisional map during gestures, full generation in id-ordered slices (the A* search pauses every 1,024 expansions), atomic commit, cancellation, cold start busy with no temporary paths, a small generation cache.
- Migration writes route orthogonal for ordinary legacy documents; new connections are orthogonal; route absent stays Bezier.
- Related e2e wait for settled routes; the content-direction manifest lists the label measure copy.

Part of #344
…op-revision/10

- loop-revision/10 (SEMANTICS-R10.md, Frozen): route "straight"; Curved stays route-absent; Manual = orthogonal with bend points; the header declares its semantics; v0.24.0 behaviour measured on production.
- Inspector: Route select (Orthogonal / Curved / Straight), Automatic / Manual state, Add bend and Reset to automatic; the run note says which edits restart the run.
- Canvas: Add bend by click or Enter (snapped along the segment), bend handles with drag (16 px grid, Alt free), arrow keys, Delete, Escape; refused drops go back; one undo entry per edit; nothing under the edit lock or on the phone.
- Manual routes turn at each bend point (spans carry the arrival direction).
- Curved and Straight labels take a free slot on their own line; records before /10 keep their recorded label places until a shape edit or Tidy (labelLayoutVersion in the autosaved Project header).
- Undo of a shape or bend edit does not reset the run; a kind change keeps the shape.
- New strings in 18 languages; locale guards and the ICU manifest updated.

Part of #344
…entre / edges, grid

- A pointer drag lands by one correction: another node's port row, then its centre or edges, then the 16 px grid, within 6 screen px; the reference is the node position and port row (a selection: its bounding box), never the grabbed point.
- Guides during the drag: a dashed placement line on both axes and a solid line to every node exactly aligned; faint and no pull with Alt; the row and column for 1.2 s after an arrow-key move; not on the phone.
- Only nearby nodes are compared (grid buckets built once per drag); display and input only, no stored field or digest changes.

Part of #344
- placeTemplate: order-keeping re-placement sized by each node's widest box over the 18 languages plus a 48 x 32 px clearance; frames travel with their nodes (>= 24 px margin); every connection Auto orthogonal, no Manual exception needed.
- The Coffee and MMO builders and the Gacha generator call it; every shipped file equals its source's output; the menu-open framings follow the same content.
- Measured in 18 languages: no blocked port, no fan overlap past a stub, no outer route; no line through a node, label on a node or label, frame cut or node overlap.
- Tests: the router golden and the #301 digest chain read the Templates as they stood before this step; current content digests re-pinned (engine digests asserted unchanged); four specs no longer depend on the old geometry.

Part of #344
…ng view

- placeTemplate: two frames that stood apart keep FRAME_GAP (32 px) between them, by inserted space (every node beyond the nearer frame's far edge moves one grid step); no order reversed, the 48 x 32 clearance unchanged. Coffee supply / roasting no longer touch; Gacha's zone titles clear the comparison frame.
- Coffee and Gacha regenerated from their sources; Gacha initialView height 1120 -> 1136 for the same content; content digests re-pinned (engine digests unchanged).
- MMO initialView narrowed to 896 x 368: readability before the whole start path (zoom about 1.09 at 1600 x 1000, node text 13 px smallest, 15.2 px median); the first flow stays in view, Reset view and the minimap show the rest.

Part of #344
…he new base

- The Pool, Source, Drain, Converter and Gate are drawn for their own width with fixed-depth features, and every painted element keeps 8 px inside the drawn outline (the #337 V0 contract); the dev-only experiment switch is removed.
- Ports and the Converter's conversion mark are read on the outline as drawn at the node's width; the mark is read once the fit is in place.
- The Templates are placed by their widest #337 boxes over the 18 languages (48 x 32 clearance): no blocked, outer or fan-overlapping route, no line through a node, label on a node, frame cut or node overlap, every element 8 px inside its outline.
- canonicalBox stays an upper bound of the drawn box; the one-time conversion and Tidy to grid keep the same 48 x 32 clearance and rebuild each frame around the nodes it held (frozen pre-#337 boxes, full containment) and the nodes it showed (centre inside), keeping its padding, at least 24 px.
- At users' coordinates (the Templates as saved before step 4, migrated, 18 languages): no blocked, outer or fan-overlapping route, no frame cut, node overlap or line through a node.
- Menu-open views: MMO at 1.05 with every core start node whole; Coffee on its operating flow at about 0.5 in a 1280 x 800 window.

Part of #344
…tart nodes kept whole

- The 57 approved baselines replaced from the final rendering; the other 19 of the 76 are byte-identical.
- MMO opening view: `initialView.keep` holds the core start nodes (Character creation, Active character, Starter encounters, Starter Lv 1-5) at their widest box over the 18 languages; the zoom is capped so they open whole at any pane size, minimap expanded or collapsed, and the minimap counts only where it would overlap them on both axes (1.040 at 1600 x 1000, 0.725 at 1280 x 800, 0.772 collapsed; the floor still wins on a phone). Only MMO carries `keep`.
- e2e: the minimap check is a 2D intersection, and the core start nodes must be whole; register-expr-authoring restores the real requestAnimationFrame in a finally and asserts it is back; dist waits for the product's route settle signal (aria-busy cleared, every path drawn) before reading routes. The 5 s timeout and the 3 px bound are unchanged.
- Unit: the keep cap (MMO at 1300 x 862 and 980 x 662, the both-axes corner rule, no-overlap cases, the floor), and `keep` as the tight union of the core boxes in `templateBoxes.json`.
- docs/diagram-layout.md §DL5.5-§DL5.6, docs/mmo-multilingual-layout.md §MML3.

Part of #344
…migration

- What's new: the three grid / ports / tidy lines are replaced by five (grid-aligned editing, clearer automatic routing, editable connection shapes, text inside nodes, reworked templates) in 18 languages; es-419 equals es-ES and pt-PT equals pt-BR. Date 2026-10-10, checked again right before the merge.
- CHANGELOG v0.25.0 covers the whole release, with the record exceptions under Migration (Review showing the real layout differences is intended) and a separate Compatibility warning (v0.24.0 and earlier drop a Straight connection on saving). 17 strings: 15 added, 2 revised.
- README: the five items in short.
- Locale guards: key counts 1077 -> 1079; `px` declared as the grid unit in es-419, it, nl, th and vi; the ru yo list; the es-419 Alt / Ctrl / Mac scope no longer names the grid line.

Part of #344
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying cozy-loop-studio with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1b2824d
Status: ✅  Deploy successful!
Preview URL: https://f9f0d1ef.cozy-loop-studio.pages.dev
Branch Preview URL: https://feat-diagram-grid.cozy-loop-studio.pages.dev

View logs

- Run 38032960035 (`30bf3d4`) cancelled shard 4/5 at the 20-minute job limit after 379 of its 386 tests; the weights were last refreshed on 2026-10-03 and the new spec files had no sample.
- `e2e/shard-weights.json`: that run added as a sample for all 148 spec files, from the four uploaded reports and, for shard 4 (no report when cancelled), the durations its log printed for the 379 tests that ran plus the local full run's durations for the 7 it never reached.
- `.github/workflows/ci.yml`: the shard matrix and the `/N` of the run step go from 5 to 6; the `e2e` aggregate still needs every shard, dist and PWA job.
- With the new weights five shards predict a longest job of 19 min 10 s (outside the budget); six predict 14 min 46 s to 16 min 01 s, 3 min 59 s to 5 min 14 s under the limit. `check:e2e-shards`: every one of the 2,105 listed tests in exactly one of 6 shards.
- docs/ci-e2e-shards.md: the six shards and why.

Part of #344
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