Skip to content

fix(canvas): Pool, Register and Parameter value rows sit inside the node (v0.21.2) - #333

Merged
MerciHanrim merged 3 commits into
mainfrom
fix/value-row-alignment
Oct 7, 2026
Merged

MerciHanrim merged 3 commits into
mainfrom
fix/value-row-alignment

Conversation

@MerciHanrim

@MerciHanrim MerciHanrim commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Part of #332, released as v0.21.2 (the release date is set to the actual merge day before merging).

The bug

In a Pool, a Parameter and a Register, the title starts after its kind chip, but the rows under it started at the content edge, 14 px before the title text. On the Pool's slanted side the value sat on or across the drawn outline (down to -9.7 px), and a Register's value sat inside the keyboard-focus ring. Measured on main 7e66da2 and on production v0.21.1 (the two agree within 0.6 px). This has been so since the first implementation; it is not a regression of #325.

Scope

  • In scope: a Pool's value and capacity, a Parameter's value and unit, a Register's result and its = expr preview.
  • Measured and deferred: the Source, Drain and Converter mode lines, the Gate's rows and two-line Pool titles. Their pointed, notched or slanted outlines grow in proportion to the node's width, so the same rule widened 37 template nodes by up to 49 px and made 33 titles worse. They need a silhouette with fixed-px point, notch and slant depths, which is separate structural work.

The fix

  • src/components/nodes/rowFit.ts (pure, fitRows) decides, from what NodeFrame measures, where each row starts and how wide the node must be. silhouette.ts gains fillSpanAt, which reads the fill straight from the drawn path.
  • Each row starts where the title text starts, along the stack's physical start (the canvas is left-to-right in every language, so an Arabic unit with dir="auto" still starts on the chip's side), and stays at least 8 px (the focus ring plus 2) inside the fill at the row's own height, at both ends.
  • A node keeps its width when that already holds; otherwise only that node widens, by the minimum, up to the 260 px maximum, and only for a row that is then whole. A row too long even for the widest node ends in its ellipsis where it is.
  • Title clearance: a title that had 8 px or more keeps at least max(8 px, its clearance - 0.5 px); one that had less keeps all of it.
  • Height comes first: a widening that would unwrap a title, and so change the node's height, does not happen. Where a rule stops a widening, the row is cut with a visible ellipsis (1234567.89 reads 12345…). Only the display is cut; stored values, files and calculations are unchanged.
  • The fit is taken in the same pass as the height, when the rendered strings, the language, the fonts or the height change, never per animation frame. It reaches src/index.css as custom properties (a physical margin-left, and a plain px max-width: a % inside min() is cyclic in the intrinsic pass and drops the whole max-width) and a min-width.
  • Unchanged: node heights, positions, saved coordinates, files, digests, the engine and simulation results; Source, Drain, Converter, Gate and End rows.

Before and after

Measured against main 7e66da2 on the same machine, main served from its own copy on its own port:

Result
Template nodes that widen (Coffee, gacha, early MMO; 183 nodes) 1: early MMO r_income, 126.84 to 132.5 px (+5.66)
Template node heights, positions, connection paths unchanged (0 of 232 paths)
Sub-pixel two Registers whose = expr row is cut differ by 0.016 px
Pool, Parameter and Register rows in view 246, smallest clearance 8.05 px, none crossing
Titles none below its rule; the largest drop is 21 to 20.58 px
2,400-Parameter import (5 alternating rounds) median 3899 ms on main, 4303 ms here (+10.4 %, a one-time mount cost; the pure fit is 69 ms of it)

Visual baselines

26 updated, each reviewed against main's own capture:

  • the rows moving (canvas-refresh L2, the Coffee template views, auto-frames, frames-activity, playback depart);
  • Registers and Parameters in the test fixtures widening, with their outline, ports, invalid flag and rings following the new outline (canvas-refresh matrix, flow colours, run distinction, the Register unit row).

No title, height, position, port or connection changes outside a widened node. 20 other snapshots changed within tolerance and are not updated, nor state-inspector-light, which alternates between two images on main itself.

Tests

  • src/components/nodes/rowFit.test.ts: the fill read from the path, the fit on a grid of widths, the title and height rules.
  • e2e/value-row-alignment.spec.ts (13) and value-row-alignment.mobile.spec.ts (3): every Pool, Parameter and Register row against the drawn outline (Range glyphs, isPointInFill at the row's top, middle and bottom) in a synthetic graph and the three templates, light, dark, forced colours, ar and a phone; selection, keyboard focus, Focus mode and the activity overlay moving nothing; Source, Drain and Converter untouched; the content digest; and a dev-only measurement counter that rises only on a frame whose row text changed (idle, pan, zoom and playback frames take no measurement).
  • e2e/whats-new.spec.ts pins 0.21.2. The existing long-label tests (a value never clipped) pass unchanged.

Release

  • Version 0.21.2, .changes/value-row-alignment.json, release note release:0.21.2 with three lines in 18 languages, 16 without native review. They say "nodes that show a value" rather than naming the kinds, so the copy guards' per-kind key counts stay as they are.
  • Copy guards move only their exact pins: catalog 1033 to 1036, runtime 1255 to 1258. All three lines read the same in pt-BR and pt-PT, so pt-PT's difference stays at 277 (249 outside the password keys, against its quarter bound of 249.75); no ru line carries a new ё word. No bound is widened.
  • Docs: docs/node-shell-content-in-vessel.md ("Follow-up — value and detail rows"), CHANGELOG.md, README.md.

Verification (local, at the head commit)

  • npx tsc -b, oxlint (39 warnings, the existing baseline, 0 errors), 3,223 unit tests, all 21 source checks of the CI checks job; the web, portable, PWA and production-branch builds with their closure and notice checks.
  • The list: 2,013 tests, main's 1,997 plus exactly these 16 (13 chromium, 3 mobile), none removed, each in exactly one of the 5 shards.
  • The dev measurement counter is only on the dev bridge: 0 occurrences of it or of __loop in the web, portable and PWA bundles, and the production and portable specs assert there is no bridge.
  • End to end in Chrome on a freshly started dev server, without retries: every screenshot spec on chromium and mobile plus the new specs and the data import, twice in a row after the update, 337 passed, 5 skipped by design, both times; the whole mobile project 112 passed, 4 skipped; the related specs (What's new, i18n, nodes, templates, canvas, routing, data import, frames) 853 passed, 3 skipped; the production bundle 16 of 16, the PWA 19 of 19, the portable file 16 of 16.

CI

  • Run 37621453781 at 9e83e1c, the first commit, failed two screenshot specs, each with its retry: register-unit-row (shard 3, chromium, 117 px) and forced-colors-L2 on mobile (shard 4). Every other job passed. It is kept as it is, not re-run.

  • Cause: the runner drew a Parameter's and a Pool's value and detail rows half a pixel left of the new baselines, while the titles and every other row matched to the pixel (ink offset 0.000). rowFit.ts rounded each row start up to a half pixel, so a title start measured at 29.00001 became 29.5 on the machine that made the baselines and stayed 29 on the runner. The runner's rows were the aligned ones.

  • Fix, da47662: a row aligned to the title takes the title's own start to the nearest 1/64 px (a layout unit), so it shares the title's sub-pixel phase; only a clearance to the outline is rounded up, and a row's room down, both to 1/64 px. Re-rendered locally, the two rows are identical to the runner's capture. Three of the 26 baselines change again, the same rows half a pixel left and one invalid Register's outline by a sub-pixel width: register-unit-row, forced-colors-L2 on chromium and flow-colour-states. No other snapshot changes.

  • Checked again at da47662, locally: every screenshot spec on chromium and mobile with the new specs and the data import, twice in a row without an update, 337 passed, 5 skipped by design, both times; smallest row clearance 8.05 px, the title rule held; npx tsc -b, oxlint (39), 3,223 unit tests, all 21 source checks; the list 2,013 (the same as at 9e83e1c), each test in exactly one of the 5 shards, the same placement.

  • Run 37628234548 at da47662 failed two screenshot specs in shard 4, each with an identical retry: matrix light L2 on chromium (108 px) and forced-colors L2 on mobile (313 px). Every other job passed. It is kept as it is, not re-run.

  • Cause: da47662 replaced only the three baselines that failed locally against the half-pixel ones, but all 26 baselines this pull request updates had been captured with the half-pixel rounding; 21 of them still showed it, and two of those went over the tolerance on the runner. The runner's trace shows the same row starts as here (for example the Gold Pool's 14px).

  • Fix, the third commit: every snapshot re-captured on the final code (da47662, clean tree), and of the 26 exactly the 21 that differ are replaced; the 5 identical ones stay; no other baseline, no product code and no tolerance changes. Playwright's own comparator, at each spec's tolerance, judges the committed baselines against the runner's two images as 108 and 313 px (the runner's own numbers) and the new ones as passing. What is left between the two machines is the anti-aliasing of the ≤ and Δ glyphs (likely a fallback font), within tolerance.

  • Run 37637340044 at 8e05244, the third commit, passed every job without a retry. The five shards ran exactly the 2,013 listed tests, each once (2,006 passed, 7 skipped by design, 0 failed, 0 retried); the production bundle 16 of 16, the PWA 19 of 19. This third run is the final approval evidence; the two failed runs above stay as they are.

  • The preview at 8e05244 (the production bundle on its Cloudflare preview origin, through the UI only, in fresh Chrome contexts): it reads v0.21.2 · build 8e05244 on a desktop and v0.21.2 · 8e05244 in the phone's ⋯ menu, and has no dev bridge. Every Pool, Parameter and Register row in view, in the synthetic graph and the three templates, in light, dark, forced colours and on a phone (125 rows), is 8 px or more inside the outline at both ends and starts at the title; the smallest clearance is 8.03 px. No page error. Two earlier passes of this check read the version from the wrong element (the document title, then the desktop brand on the phone); only the corrected pass counts.

Not claimed

  • No manual check of this pull request's preview, and no manual screen-reader check; no text or accessibility tree change beyond the release notes.

…ode (v0.21.2)

Issue #332. Version 0.21.2.

Cause: the rows under a node's title (a Pool's value and capacity, a Parameter's value and unit, a Register's result and `= expr`) started at the content edge, 14 px before the title text. On the Pool's slanted side the value sat on or across the drawn outline (down to -9.7 px), and a Register's value sat inside the keyboard-focus ring. This has been so since the first implementation.

Fix: src/components/nodes/rowFit.ts (pure) decides, from what NodeFrame measures, where each row starts and how wide the node must be: each row starts at the title text along the stack's physical start (the canvas is left-to-right in every language), at least 8 px (the focus ring plus 2) inside the fill at the row's own height at both ends. A node keeps its width when that already holds; otherwise only that node widens, by the minimum, up to 260 px, and only for a row that is then whole. A title that had 8 px or more keeps at least max(8, its clearance - 0.5); one that had less keeps all of it. A widening that would unwrap a title, and so change the node's height, does not happen. Where a rule stops a widening, the row is cut with a visible ellipsis; only the display is cut. silhouette.ts gains fillSpanAt, which reads the fill straight from the drawn path. The fit is taken in the same pass as the height, when the rendered strings, the language, the fonts or the height change, never per animation frame; it reaches index.css as custom properties (a physical margin-left, a plain px max-width) and a min-width. Source, Drain, Converter, Gate and End rows are unchanged: their pointed, notched or slanted outlines grow with the width, so the same rule widened 37 template nodes by up to 49 px (deferred).

Measured against main 7e66da2 on the same machine: in the three templates exactly one node widens (early MMO r_income, 126.84 to 132.5 px); node heights, positions and every drawn connection path are unchanged (0 of 232 paths); two Registers whose `= expr` row is cut differ by 0.016 px. 246 in-scope rows, smallest clearance 8.05 px, none crossing. A 2,400-Parameter import: median 3899 ms on main, 4303 ms here over five alternating rounds (+10.4 %, a one-time mount cost); the pure fit is 69 ms of it.

Tests: src/components/nodes/rowFit.test.ts (the fill, the fit on a grid of widths, the title and height rules). e2e/value-row-alignment.spec.ts (13) and value-row-alignment.mobile.spec.ts (3) read every row against the drawn outline (Range glyphs, isPointInFill) in a synthetic graph and the three templates, light, dark, forced colours, ar and a phone; states that move nothing; Source, Drain and Converter untouched; the content digest; a dev-only measurement counter that rises only on a frame whose row text changed. e2e/whats-new.spec.ts pins 0.21.2.

Visual baselines: 26 updated, each one reviewed against main's own capture: the rows moving, and Registers and Parameters in the test fixtures widening with their outline, ports, invalid flag and rings. No title, height, position, port or connection change outside a widened node. The 20 snapshots that changed within tolerance are not updated, nor state-inspector-light, which alternates between two images on main itself.

Release: three release-note lines in 18 languages, 16 without native review; they say "nodes that show a value" rather than naming the kinds, so the copy guards' per-kind key counts stay as they are. The guards move only their exact pins (catalog 1033 to 1036, runtime 1255 to 1258); all three lines read the same in pt-BR and pt-PT, so pt-PT's difference stays at 277 (249 outside the password keys), and no ru line carries a new ё word. Docs: docs/node-shell-content-in-vessel.md, CHANGELOG.md and README.md.
@MerciHanrim MerciHanrim added the bug Something isn't working label Oct 7, 2026
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying cozy-loop-studio with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8e05244
Status: ✅  Deploy successful!
Preview URL: https://1d34280f.cozy-loop-studio.pages.dev
Branch Preview URL: https://fix-value-row-alignment.cozy-loop-studio.pages.dev

View logs

…e draws them alike

Issue #332, CI of the first commit. Two screenshot specs failed on the runner (register-unit-row, and the mobile forced-colors-L2): a Parameter's and a Pool's value and detail rows were drawn half a pixel left of the new baselines. The titles and every other row matched the runner exactly.

Cause: rowFit.ts rounded each row start UP to a half pixel. A title start measured a hair above a whole pixel (29.00001) became 29.5 on this machine and stayed 29 on the runner, so the row sat half a pixel right of its title locally and the baselines captured that.

Fix: a row aligned to the title takes the title's own start to the nearest 1/64 px (a layout unit), so it shares the title's sub-pixel phase; a clearance to the outline is only rounded up and a row's room only down, both to 1/64 px. The start and the minimum width keep four decimals. Re-rendered here, the two rows are now identical to the runner's capture (offset 0.000, same ink).

Baselines: three of the 26 already updated in this pull request change again, the same rows half a pixel left and one invalid Register's outline by a sub-pixel width (register-unit-row, forced-colors-L2 on chromium, flow-colour-states). No other snapshot changes.
Issue #332, CI of the second commit. Two screenshot specs failed on the runner, each with an identical retry: matrix light L2 on chromium (108 px) and forced-colors L2 on mobile (313 px), both in shard 4.

Cause: the second commit replaced only the three baselines that failed here against the half-pixel ones. All 26 baselines this pull request updates had been captured with the half-pixel rounding, and 21 of them still showed it; most stayed within this machine's tolerance, two did not on the runner's.

Fix: every snapshot re-captured on the final code (da47662, clean tree), and of the 26 exactly the 21 that differ are replaced; the 5 identical ones stay, and no other baseline, no product code and no tolerance changes. Playwright's own comparator, at each spec's tolerance, judges the committed baselines against the runner's two images as 108 and 313 px, the runner's own numbers, and the new ones as passing. The runner's rows now sit where these do; what is left between the machines is the `≤` and `Δ` glyphs' anti-aliasing (likely a fallback font), within tolerance.
@MerciHanrim
MerciHanrim merged commit c5e7d8f into main Oct 7, 2026
10 checks passed
@MerciHanrim
MerciHanrim deleted the fix/value-row-alignment branch October 7, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant