diff --git a/.changes/compact-nodes.json b/.changes/compact-nodes.json new file mode 100644 index 00000000..f6f78225 --- /dev/null +++ b/.changes/compact-nodes.json @@ -0,0 +1 @@ +{ "type": "user-facing", "releaseNoteId": "release:0.21.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c4a9ffd..3f26ab87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable Loop Studio releases, newest first. Behavioral changes are pinned in versioned spec documents (see the [README](README.md#technical-reference)); this file is the narrative history, not the contract. +## v0.21.0 — 2026-10-07 + +Compact nodes (issue #325, the last of three parts): more of a large graph fits in view. The contract is [`docs/flow-colour-and-compact-nodes.md`](docs/flow-colour-and-compact-nodes.md) FC-7. + +- **Shorter, a little narrower.** A one-line node is 56 px tall instead of 64 (a one-line Pool 58, which its title and value need), with 12 px of side padding instead of 16 (Source and Drain 22 instead of 26 at their pointed end, Gate 26 instead of 30). Of the 386 nodes in the bundled templates and a sample of every kind, 235 got shorter. +- **Content still decides.** 56 is a floor, not a forced height: a two-line title, a capacity row, every Parameter and every Register keep the height their content needs, the title's maximum width is unchanged so long titles wrap where they did, and no outline is squeezed or reversed at any height. +- **Nothing a file holds changes.** Node positions, saved diagrams, share links, digests and simulation results are the same; connections attach 3 to 4 px higher and orthogonal routes are recomputed. The ports and their 18 px hit area keep their size, and the selection, focus, invalid, run and Focus-mode marks keep their order. + +**No migration.** Three release-note lines in 18 languages, 16 of them without native review. The informational `meta.tool` string is now `loop-studio/0.21.0`. + ## v0.20.0 — 2026-10-07 Flow colours beyond the canvas (issue #325, the second of three parts). The contract is [`docs/flow-colour-and-compact-nodes.md`](docs/flow-colour-and-compact-nodes.md) FC-5 and FC-6. diff --git a/README.md b/README.md index e4f011ab..4702de63 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,18 @@ Additional feature-specific design documents (localization, mobile, module system, large-graph readability, simulation playback, edge routing, data import, …) live under [`docs/`](docs/). -## Latest — v0.20.0 +## Latest — v0.21.0 + +Compact nodes: more of a large graph fits in view. + +- **Shorter, a little narrower**: a one-line node is 56 px tall instead of 64, with less + side padding, so more of a big graph fits at the same zoom +- **Content still decides**: two-line titles, capacity rows, Parameters and Registers keep + the height they need, and long titles wrap as before +- **Nothing in a file changes**: positions, saved diagrams and simulation results stay the + same; connections attach a few pixels higher + +## v0.20.0 Flow colours beyond the canvas. @@ -175,18 +186,8 @@ A fix release: the guided tour says each step once. - **Focus stays on Next or Back** while the steps change, and ending the tour returns it to the Help button (More on a phone) -## v0.18.1 - -A fix release: the menus answer the keyboard the same way. - -- **Every menu button** opens at its first item from the keyboard, moves with the arrows, - Home and End, and closes with Escape or Tab, back to its button -- **Settings and `⋯`** are disclosures you Tab through; Theme is a choice of one -- **On a phone**, a sheet takes focus when it opens, and Escape in a sheet opened from - More goes back to More; the run bar and the update bar stay usable - -See [`CHANGELOG.md`](CHANGELOG.md) for the full notes of these releases, v0.18.0 (the -third-party open-source licenses in the About dialog), v0.17.2 (the +See [`CHANGELOG.md`](CHANGELOG.md) for the full notes of these releases, v0.18.1 (one +keyboard contract for every menu), v0.18.0 (the third-party open-source licenses in the About dialog), v0.17.2 (the Temporary session button drawn like the menu buttons), v0.17.1 (share links compressed with the browser's own Compression Streams), v0.17.0 (password-protected share links), v0.16.0 (the storage gate, temporary sessions and the diff --git a/docs/flow-colour-and-compact-nodes.md b/docs/flow-colour-and-compact-nodes.md index d7789adf..f462c24c 100644 --- a/docs/flow-colour-and-compact-nodes.md +++ b/docs/flow-colour-and-compact-nodes.md @@ -54,7 +54,7 @@ The selected node differs from a resting one only by a darker stroke 0.5 px heav | parameter | 19 | 162 / 179 / 179 | — | 86 | | register | 16 | 127 / 260 / 260 | 86 | 102 | -CSS: min-width 118 (Gate 134), max-width 260, base height 64 (`BASE_NODE_H`); body padding `6px 16px`, Source right 26, Drain left 26, Gate `8px 30px`, End right 24, Parameter and Register inline 15. Title 14 px / 600 / line-height 1.15, max-width 135, wraps to two lines; value 18 px mono; sub 12 px; title-to-value gap 1 px; chip-to-title gap 6 px. The height is measured in JS from the content and clamped per kind; the width is CSS. Ports: an 8 x 8 resource circle and a 7 x 7 state diamond, both with an 18 px hit area (`::before`, inset -5 px). Levels of detail: L2 at zoom 0.8 and above, L1 from 0.45 (no sub line), L0 below (no text, outline 2 px, kind dot). +CSS (before PR 3; see FC-7 for the compact values): min-width 118 (Gate 134), max-width 260, base height 64 (`BASE_NODE_H`); body padding `6px 16px`, Source right 26, Drain left 26, Gate `8px 30px`, End right 24, Parameter and Register inline 15. Title 14 px / 600 / line-height 1.15, max-width 135, wraps to two lines; value 18 px mono; sub 12 px; title-to-value gap 1 px; chip-to-title gap 6 px. The height is measured in JS from the content and clamped per kind; the width is CSS. Ports: an 8 x 8 resource circle and a 7 x 7 state diamond, both with an 18 px hit area (`::before`, inset -5 px). Levels of detail: L2 at zoom 0.8 and above, L1 from 0.45 (no sub line), L0 below (no text, outline 2 px, kind dot). ## FC-2. Data @@ -210,8 +210,11 @@ The gacha zones take the colour their frames already carry. Uncoloured: every Pa ## FC-7. Compact nodes (PR 3) -- A typical one-line flow node is 64 px high and needs about 32–38 px of it for its content. First estimate, to confirm with before / after shots against the Slice 0 set: base height 64 → 56 (-12.5 %), inline padding 16 → 12 px (Source and Drain 26 → 22, Gate 30 → 26), title max-width unchanged so long titles still wrap in every language. -- The visible port may shrink; its 18 px hit area does not. No saved coordinate moves; edges may reroute, and the MMO graph is checked for it (`routeMap` assumes 130 x 64 when a size is missing; auto frames use a 150 x 40 canonical size). +- The base height is **56** (it was 64): `BASE_NODE_H` in `src/components/nodes/silhouette.ts` and `.nodef { height }`. It is a floor, not a forced height: the box is still measured from its content (`stack + VESSEL_INSET_Y[kind] + 2 x VESSEL_MIN_PAD_Y`, clamped to `[BASE_NODE_H, MAX_NODE_H[kind]]`), so content that needs more grows past it as before. Measured on 386 nodes (a three-node-per-kind graph, Coffee, Gacha, MMO, MMO in German and Korean; the formula matched every measured height at 64 and at 56): Source, Drain, Converter, Gate and End sit at 56; a one-line Pool needs 58 (title and value, its 12 px inset, 4 px clear above and below) and settles there, so the Pool is not squeezed to 56; a two-line title, a capacity row and every Parameter (70 to 86) and Register (86 to 102) keep their content height. 235 of the 386 nodes got shorter. +- Inline padding 16 -> 12 px (Source right and Drain left 26 -> 22, Gate `8px 30px` -> `8px 26px`); the End's right 24, the Parameter's and the Register's width-scaled 15, the title max-width 135 and the min and max widths (118, Gate 134, 260) are unchanged, so long titles still wrap where they did. A content-sized node is up to 8 px narrower (4 for the End). +- Every silhouette is drawn by its height function from 56 up; at 64 and above each returns the path it always did (at exactly 64, the historic path byte for byte). Below 64 the End's and the Register's rounded caps meet at mid-height instead of inserting a straight run, so no outline reverses at any height (`silhouette.test.ts` walks every height of every kind). +- The visible ports (8 x 8, 7 x 7) and their 18 px hit area are unchanged. Ports sit at half the box height, so edges attach 3 to 4 px higher and orthogonal routes are recomputed; no saved coordinate moves, nothing is written to a file, and digests and simulation are unchanged (a node's size never reaches a document). +- `routeMap`'s size for a node not yet measured is 130 x `BASE_NODE_H`, so a one-line node routes the same before and after it is measured. Auto frames keep their 150 x 40 canonical footprint (results independent of rendered size, §AF8); a frame is that footprint's box plus 24 px, so a 56 / 58 px node now has 8 / 6 px to spare at the bottom instead of none. ## FC-8. Tests that hold this contract diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L0-chromium-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L0-chromium-win32.png index c787e961..e8b41c3d 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L0-chromium-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L0-chromium-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-chromium-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-chromium-win32.png index c3968fca..86e3e4e9 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-chromium-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-chromium-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-mobile-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-mobile-win32.png index 0b146e9a..511f006b 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-mobile-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/forced-colors-L2-mobile-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-chromium-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-chromium-win32.png index 1f2303f6..54d3811e 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-chromium-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-chromium-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-mobile-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-mobile-win32.png index 5af4d65e..2c82a671 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-mobile-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L1-mobile-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-chromium-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-chromium-win32.png index f215e8e4..e9ea41ae 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-chromium-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-chromium-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-mobile-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-mobile-win32.png index 7795b3fd..893824b5 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-mobile-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-dark-L2-mobile-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L0-chromium-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L0-chromium-win32.png index 01857560..21b377fa 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L0-chromium-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L0-chromium-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-chromium-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-chromium-win32.png index cfea9c4f..e86561af 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-chromium-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-chromium-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-mobile-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-mobile-win32.png index 7b5df074..f753b754 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-mobile-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L1-mobile-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-chromium-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-chromium-win32.png index c5520a5b..ecf08347 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-chromium-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-chromium-win32.png differ diff --git a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-mobile-win32.png b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-mobile-win32.png index c42c342d..5b78af84 100644 Binary files a/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-mobile-win32.png and b/e2e/canvas-refresh-visual.spec.ts-snapshots/matrix-light-L2-mobile-win32.png differ diff --git a/e2e/edge-routing.spec.ts b/e2e/edge-routing.spec.ts index 88aa105e..66ab66b7 100644 --- a/e2e/edge-routing.spec.ts +++ b/e2e/edge-routing.spec.ts @@ -445,14 +445,18 @@ test.describe('edge routing — Slice 1', () => { hit: hit.getAttribute('d'), markerEnd: vis.getAttribute('marker-end') ?? vis.style.markerEnd ?? '', labelTransform: label?.style.transform ?? '', - mid: route ? [Math.round(route.mid.x), Math.round(route.mid.y)] : null, + mid: route ? [route.mid.x, route.mid.y] : null, } }) expect(consumers.hit).toBe(consumers.d) // BaseEdge draws both from one path expect(consumers.markerEnd).toMatch(/url\(#/) // the renderer-owned direction marker // the label anchor is the route's arc-length midpoint - expect(consumers.labelTransform).toContain(`${consumers.mid![0]}px`) - expect(consumers.labelTransform).toContain(`${consumers.mid![1]}px`) + // compared unrounded: a port can sit on a half pixel (a 58 px compact Pool + // puts it at 29, #325 PR 3), and the label is placed at the exact midpoint + const at = /translate\(([-0-9.]+)px,\s*([-0-9.]+)px\)\s*$/.exec(consumers.labelTransform) + expect(at, `label transform ${consumers.labelTransform}`).not.toBeNull() + expect(Number(at![1])).toBeCloseTo(consumers.mid![0], 6) + expect(Number(at![2])).toBeCloseTo(consumers.mid![1], 6) await test.step('running sim: the playback token walks the orthogonal d; a selection change does not restart it', async () => { // prime one step (tokenless) so `gold` holds a unit and the drain edge `e_gd` diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-forced-minimap-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-forced-minimap-chromium-win32.png index 9d3ce43a..3b0f48bc 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-forced-minimap-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-forced-minimap-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-dark-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-dark-chromium-win32.png index 280f85ef..24b6e3f9 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-dark-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-dark-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-light-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-light-chromium-win32.png index 58245b27..4387e9d1 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-light-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-minimap-light-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-dark-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-dark-chromium-win32.png index f764b34c..99ceb8ff 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-dark-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-dark-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-light-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-light-chromium-win32.png index 39d3aa1b..a20e9697 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-light-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-coffee-light-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-dark-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-dark-chromium-win32.png index 73fee714..4ebcef47 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-dark-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-dark-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-light-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-light-chromium-win32.png index 51891b41..0ca0460e 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-light-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-gacha-light-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-dark-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-dark-chromium-win32.png index d5f28bc6..759c83e2 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-dark-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-dark-chromium-win32.png differ diff --git a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-light-chromium-win32.png b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-light-chromium-win32.png index 40609188..6ed4c2eb 100644 Binary files a/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-light-chromium-win32.png and b/e2e/flow-colour-views-visual.spec.ts-snapshots/flow-views-template-mmo-light-chromium-win32.png differ diff --git a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L0-chromium-win32.png b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L0-chromium-win32.png index b63f272e..6555fe84 100644 Binary files a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L0-chromium-win32.png and b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L0-chromium-win32.png differ diff --git a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L2-chromium-win32.png b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L2-chromium-win32.png index 961c1b0b..70b91fa9 100644 Binary files a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L2-chromium-win32.png and b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-dark-L2-chromium-win32.png differ diff --git a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L0-chromium-win32.png b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L0-chromium-win32.png index 112afb09..2e786f3c 100644 Binary files a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L0-chromium-win32.png and b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L0-chromium-win32.png differ diff --git a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L2-chromium-win32.png b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L2-chromium-win32.png index b9470b60..76d2421c 100644 Binary files a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L2-chromium-win32.png and b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-light-L2-chromium-win32.png differ diff --git a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-states-chromium-win32.png b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-states-chromium-win32.png index 713cd589..a1750a8d 100644 Binary files a/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-states-chromium-win32.png and b/e2e/flow-colour-visual.spec.ts-snapshots/flow-colour-states-chromium-win32.png differ diff --git a/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-active-resource-coffee-z05-chromium-win32.png b/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-active-resource-coffee-z05-chromium-win32.png index bd5fae99..49f8bbd4 100644 Binary files a/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-active-resource-coffee-z05-chromium-win32.png and b/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-active-resource-coffee-z05-chromium-win32.png differ diff --git a/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-four-state-mmo-z075-chromium-win32.png b/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-four-state-mmo-z075-chromium-win32.png index 0582bd8e..9cb2ed8d 100644 Binary files a/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-four-state-mmo-z075-chromium-win32.png and b/e2e/forced-colors-edge-tell.spec.ts-snapshots/forced-four-state-mmo-z075-chromium-win32.png differ diff --git a/e2e/gacha-banner-zones.spec.ts b/e2e/gacha-banner-zones.spec.ts index ec519fff..ba7d7bb8 100644 --- a/e2e/gacha-banner-zones.spec.ts +++ b/e2e/gacha-banner-zones.spec.ts @@ -649,6 +649,46 @@ test.describe('3-zone gacha banner comparison Template', () => { } }) + test('the Premium Pickup `+1` labels of e_pickup_34 and e_pickup_36 keep clear of each other with compact nodes, in EN/KO/JA', async ({ + page, + }) => { + // #325 PR 3 (compact nodes), layout round 7 of + // scripts/gen-gacha-banner-zones-example.ts — with 56 / 58 px nodes both + // edges ran through the one band between the first two flow rows and their + // `+1` labels overlapped in ko. `e_pickup_36` now leaves its bottom port + // downward through one waypoint into the band under the second row. This + // pins the pair the broader every-label test above caught. + await openApp(page) + await resetAll(page) + await pickDesktopTemplate(page, EN_NAME) + await page.setViewportSize({ width: 1600, height: 900 }) + const waypoints = await page.evaluate( + () => + (window as unknown as { __loop: { graph: { getState: () => { edges: { id: string; data?: { waypoints?: unknown } }[] } } } }) + .__loop.graph.getState() + .edges.find((e) => e.id === 'e_pickup_36')?.data?.waypoints, + ) + expect(waypoints).toEqual([{ x: 3200, y: 650 }]) + for (const locale of ['en', 'ko', 'ja'] as const) { + await setLocale(page, locale) + await page.evaluate(() => + (window as unknown as { __loop: { rf: { setViewport: (v: object) => void } } }).__loop.rf.setViewport({ x: 800 - 3200, y: 450 - 600, zoom: 1 }), + ) + const boxes = await page.evaluate(() => + ['e_pickup_34', 'e_pickup_36'].map((id) => { + const el = document.querySelector(`.edge-label[data-edge-id="${id}"]`) as HTMLElement | null + const r = el?.getBoundingClientRect() + return r ? { id, text: (el!.textContent || '').trim(), x: r.x, y: r.y, w: r.width, h: r.height } : null + }), + ) + expect(boxes.every((b) => b !== null && b.w > 0 && b.h > 0), `${locale}: both labels render`).toBe(true) + const [a, b] = boxes as { id: string; text: string; x: number; y: number; w: number; h: number }[] + expect([a.text, b.text]).toEqual(['+1', '+1']) + const overlap = a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y + expect(overlap, `${locale}: ${a.id} ${JSON.stringify(a)} overlaps ${b.id} ${JSON.stringify(b)}`).toBe(false) + } + }) + test('comparison frame fully contains every comparison-row node, in EN/KO/JA, at both reviewed viewports', async ({ page, }) => { diff --git a/e2e/i18n-visual.spec.ts-snapshots/ko-mobile-app-chromium-win32.png b/e2e/i18n-visual.spec.ts-snapshots/ko-mobile-app-chromium-win32.png index 75532662..6790605f 100644 Binary files a/e2e/i18n-visual.spec.ts-snapshots/ko-mobile-app-chromium-win32.png and b/e2e/i18n-visual.spec.ts-snapshots/ko-mobile-app-chromium-win32.png differ diff --git a/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-chromium-win32.png b/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-chromium-win32.png index ab5af618..cf83590d 100644 Binary files a/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-chromium-win32.png and b/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-chromium-win32.png differ diff --git a/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-mixed-chromium-win32.png b/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-mixed-chromium-win32.png index 68fc35ac..ef7fb5da 100644 Binary files a/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-mixed-chromium-win32.png and b/e2e/large-graph-readability.spec.ts-snapshots/auto-frames-mixed-chromium-win32.png differ diff --git a/e2e/large-graph-readability.spec.ts-snapshots/frames-activity-chromium-win32.png b/e2e/large-graph-readability.spec.ts-snapshots/frames-activity-chromium-win32.png index 0a897dca..a906ac03 100644 Binary files a/e2e/large-graph-readability.spec.ts-snapshots/frames-activity-chromium-win32.png and b/e2e/large-graph-readability.spec.ts-snapshots/frames-activity-chromium-win32.png differ diff --git a/e2e/large-graph-readability.spec.ts-snapshots/run-distinction-states-chromium-win32.png b/e2e/large-graph-readability.spec.ts-snapshots/run-distinction-states-chromium-win32.png index 111abc32..d3a26bbc 100644 Binary files a/e2e/large-graph-readability.spec.ts-snapshots/run-distinction-states-chromium-win32.png and b/e2e/large-graph-readability.spec.ts-snapshots/run-distinction-states-chromium-win32.png differ diff --git a/e2e/model-nodes-visual.spec.ts-snapshots/register-unit-row-chromium-win32.png b/e2e/model-nodes-visual.spec.ts-snapshots/register-unit-row-chromium-win32.png index 62d07b73..a65caf49 100644 Binary files a/e2e/model-nodes-visual.spec.ts-snapshots/register-unit-row-chromium-win32.png and b/e2e/model-nodes-visual.spec.ts-snapshots/register-unit-row-chromium-win32.png differ diff --git a/e2e/node-long-label.spec.ts b/e2e/node-long-label.spec.ts index cfb31bad..cabf7451 100644 --- a/e2e/node-long-label.spec.ts +++ b/e2e/node-long-label.spec.ts @@ -85,11 +85,13 @@ test.describe('§MML1 — long node labels stay inside the box', () => { await page.waitForTimeout(250) // let the ResizeObserver grow-pass settle }) - test('a short label is unchanged — one line, 64 px box', async ({ page }) => { + // docs/flow-colour-and-compact-nodes.md FC-7 (#325 PR 3) — the compact floor + // is 56, and a one-line Pool settles at the 58 its title and value need + test('a short label stays one line — a one-line Pool is the compact 58 px box', async ({ page }) => { const b = await box(page, 'short') expect(b.titleLines).toBe(1) - expect(b.boxH).toBe(64) - expect(b.viewBox).toBe('0 0 120 64') + expect(b.boxH).toBe(58) + expect(b.viewBox).toBe('0 0 120 58') }) test('an official-length label wraps to ≤ 2 lines and the box grows to fit', async ({ page }) => { @@ -427,7 +429,9 @@ test.describe('content ⊂ vessel — path-aware (isPointInFill)', () => { }) } - test('a Source / plain Pool are unchanged — still the 64 px base box', async ({ page }) => { + // FC-7 (#325 PR 3): a Source sits at the 56 px compact floor, a plain one-line + // Pool at the 58 its content needs + test('a Source / plain Pool take the compact box — 56 / 58 px', async ({ page }) => { await openApp(page) await resetAll(page) await importGraph(page, SHELL) @@ -436,7 +440,7 @@ test.describe('content ⊂ vessel — path-aware (isPointInFill)', () => { src: (document.querySelector('.react-flow__node[data-id="src"] .nodef') as HTMLElement).offsetHeight, poolPlain: (document.querySelector('.react-flow__node[data-id="poolPlain"] .nodef') as HTMLElement).offsetHeight, })) - expect(h.src).toBe(64) - expect(h.poolPlain).toBe(64) + expect(h.src).toBe(56) + expect(h.poolPlain).toBe(58) }) }) diff --git a/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-chromium-win32.png b/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-chromium-win32.png index 1a174482..a1894c49 100644 Binary files a/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-chromium-win32.png and b/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-chromium-win32.png differ diff --git a/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-mobile-win32.png b/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-mobile-win32.png index 7880cb9f..9f211d71 100644 Binary files a/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-mobile-win32.png and b/e2e/playback-visual.spec.ts-snapshots/play-depart-dark-L2-mobile-win32.png differ diff --git a/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-chromium-win32.png b/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-chromium-win32.png index 16681efd..3376eff6 100644 Binary files a/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-chromium-win32.png and b/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-chromium-win32.png differ diff --git a/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-mobile-win32.png b/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-mobile-win32.png index 9430b7e7..014ef117 100644 Binary files a/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-mobile-win32.png and b/e2e/playback-visual.spec.ts-snapshots/play-depart-forced-colors-L2-mobile-win32.png differ diff --git a/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-chromium-win32.png b/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-chromium-win32.png index 8fafff7f..74ba8866 100644 Binary files a/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-chromium-win32.png and b/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-chromium-win32.png differ diff --git a/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-mobile-win32.png b/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-mobile-win32.png index c8616ff4..047c9f83 100644 Binary files a/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-mobile-win32.png and b/e2e/playback-visual.spec.ts-snapshots/play-depart-light-L2-mobile-win32.png differ diff --git a/e2e/playback-visual.spec.ts-snapshots/play-travel-L0-chromium-win32.png b/e2e/playback-visual.spec.ts-snapshots/play-travel-L0-chromium-win32.png index 73bdacb2..b0546c14 100644 Binary files a/e2e/playback-visual.spec.ts-snapshots/play-travel-L0-chromium-win32.png and b/e2e/playback-visual.spec.ts-snapshots/play-travel-L0-chromium-win32.png differ diff --git a/e2e/whats-new.spec.ts b/e2e/whats-new.spec.ts index fa530dcd..01d84f7a 100644 --- a/e2e/whats-new.spec.ts +++ b/e2e/whats-new.spec.ts @@ -238,7 +238,7 @@ test.describe('closing the notice and opening the panel are different things', ( expect(text).not.toContain('text unavailable') } } - expect(RELEASE_NOTES.map((n) => n.version)).toEqual(['0.20.0', '0.19.0', '0.18.2', '0.18.1', '0.18.0', '0.17.2', '0.17.1', '0.17.0', '0.16.0', '0.15.3', '0.15.2', '0.15.1', '0.15.0', '0.14.0']) + expect(RELEASE_NOTES.map((n) => n.version)).toEqual(['0.21.0', '0.20.0', '0.19.0','0.18.2', '0.18.1', '0.18.0', '0.17.2', '0.17.1', '0.17.0', '0.16.0', '0.15.3', '0.15.2', '0.15.1', '0.15.0', '0.14.0']) await page.keyboard.press('Escape') await expect(panel).toHaveCount(0) diff --git a/examples/gacha-banner-zones.json b/examples/gacha-banner-zones.json index 11eb19b7..40d950f1 100644 --- a/examples/gacha-banner-zones.json +++ b/examples/gacha-banner-zones.json @@ -2027,7 +2027,13 @@ "expr": "+1", "timing": "afterPull", "when": "source-fired", - "route": "orthogonal" + "route": "orthogonal", + "waypoints": [ + { + "x": 3200, + "y": 650 + } + ] } }, { diff --git a/package-lock.json b/package-lock.json index 7697157d..43f3117a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "loop-studio", - "version": "0.20.0", + "version": "0.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loop-studio", - "version": "0.20.0", + "version": "0.21.0", "dependencies": { "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", diff --git a/package.json b/package.json index ab26372d..c4bb884f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "loop-studio", "private": true, - "version": "0.20.0", + "version": "0.21.0", "type": "module", "engines": { "node": ">=22.12.0" diff --git a/scripts/gen-gacha-banner-zones-example.ts b/scripts/gen-gacha-banner-zones-example.ts index 235d1ffb..fad04c02 100644 --- a/scripts/gen-gacha-banner-zones-example.ts +++ b/scripts/gen-gacha-banner-zones-example.ts @@ -355,7 +355,16 @@ const recommendedRunConfig: RecommendedRunConfig = { // same point. A short, explicit waypoint per edge — opposite sides of that // shared point — separates them; still purely cosmetic routing data, never // an engine change. +// Layout round 7 (#325 PR 3, compact nodes, Lumi 2026-10-07) — with the nodes +// 56 / 58 px tall instead of 64, `e_pickup_34` (SR hit -> Pulls made) and +// `e_pickup_36` (Pickup hit -> SSR count) both ran their horizontal leg through +// the one 38 px band between the first two flow rows, 14 px apart, and their +// two `+1` labels overlapped in ko. The band is too narrow to hold both labels +// clear of each other AND of the rows, so `e_pickup_36` alone now leaves its +// bottom port DOWNWARD into the free band under the second row (y 632..670) +// through one waypoint; its label lands there, clear of every label and node. const WAYPOINTS: Record = { + e_pickup_36: [{ x: 3200, y: 650 }], e_standard_12: [{ x: 1200, y: 550 }], e_standard_13: [{ x: 1320, y: 550 }], e_pickup_7: [{ x: 2680, y: 600 }], diff --git a/src/components/nodes/nodes.tsx b/src/components/nodes/nodes.tsx index 2c33e86c..47271683 100644 --- a/src/components/nodes/nodes.tsx +++ b/src/components/nodes/nodes.tsx @@ -41,8 +41,9 @@ import { useNodeActivityOpacity } from '../frames/useActivityTint' // ── N1 "Vessel" silhouettes ────────────────────────────────────────────── // The outer shape carries the node's role. Type colour is used only on a small // chip, never to fill the silhouette. Selection and firing are separate cues. -// The viewBox is `0 0 120 h` — `h` is the measured body height (>= 64 once a -// title wraps to two lines). `./silhouette` regenerates each of the seven paths +// The viewBox is `0 0 120 h` — `h` is the measured body height, floored at +// `BASE_NODE_H` (56, the compact floor of docs/flow-colour-and-compact-nodes.md +// FC-7) and grown by the content. `./silhouette` regenerates each of the paths // for `h`, keeping stroke / radius / notch fixed (docs/mmo-multilingual-layout.md // §MML1b); at h = 64 it returns the historic path verbatim. @@ -190,14 +191,15 @@ function NodeFrame({ // docs/mmo-multilingual-layout.md §MML1b + docs/node-shell-content-in-vessel.md — // the box height is driven by the RENDERED content, so the stack always sits - // inside the DRAWN vessel (not the 64px bounding box): the vessel path insets + // inside the DRAWN vessel (not the bounding box): the vessel path insets // its own top/bottom caps (`VESSEL_INSET_Y`) and a `parameter` / `register` - // capsule is only ~40px tall inside the 64px box, so a title + value + `= expr` + // capsule is 24px shorter than its box, so a title + value + `= expr` // stack spilled past the outline at both ends. Measure `.nodef__stack`'s real // height, add the vessel inset + a min clear gap top and bottom, clamp to the // kind's silhouette range, and redraw the vessel + handles at that height. - // `clampNodeHeight` floors at `BASE_NODE_H`, so a short node (Source / Drain / - // Gate, a plain Pool) is byte-identical to before. The ResizeObserver settles + // `clampNodeHeight` floors at `BASE_NODE_H` (56), so a short node (Source / + // Drain / Gate / Converter / End) sits at the floor and a one-line Pool at the + // 58 its title and value need. The ResizeObserver settles // in one pass: the SVG is `position: absolute`, so a viewBox change never // feeds back into the box height. const stackRef = useRef(null) diff --git a/src/components/nodes/silhouette.test.ts b/src/components/nodes/silhouette.test.ts index 1f63da23..a0954163 100644 --- a/src/components/nodes/silhouette.test.ts +++ b/src/components/nodes/silhouette.test.ts @@ -18,8 +18,9 @@ const KINDS: NodeKind[] = [ 'register', ] -// the historic hard-coded silhouettes — the parametric function must return -// these byte-for-byte at (and below) the base height +// the historic hard-coded silhouettes, drawn at 64 — the parametric function +// must still return these byte-for-byte at 64, so a node whose content keeps it +// at 64 or taller draws exactly as before the compact floor (FC-7) const HISTORIC: Record = { pool: 'M32 6 H88 Q95 6 96 13 L112 52 Q113 58 107 58 H13 Q7 58 8 52 L24 13 Q25 6 32 6 Z', source: 'M14 8 Q8 8 8 14 V50 Q8 56 14 56 H84 L114 32 L84 8 Z', @@ -41,22 +42,72 @@ const nums = (d: string) => (d.match(/-?\d+(?:\.\d+)?/g) ?? []).map(Number) // bottom-edge `H14` would otherwise read its x as a y). const topCapYs = (d: string) => nums(d).slice(0, 8).filter((v, i) => i % 2 === 1 && v <= 20) -describe('silhouettePath — base height is byte-identical', () => { +/** every point the path names, in drawing order (control points included), for + * the `M H V L Q Z` commands these silhouettes use */ +function points(d: string): { x: number; y: number }[] { + const out: { x: number; y: number }[] = [] + let x = 0 + let y = 0 + for (const [, cmd, args] of d.matchAll(/([MHVLQZ])([^MHVLQZ]*)/g)) { + const a = nums(args) + if (cmd === 'H') x = a[0] + else if (cmd === 'V') y = a[0] + else if (cmd === 'M' || cmd === 'L') [x, y] = a + else if (cmd === 'Q') { + out.push({ x: a[0], y: a[1] }) + ;[x, y] = [a[2], a[3]] + } else continue + out.push({ x, y }) + } + return out +} + +/** how many times the outline's y changes direction going once around it: a + * vessel that goes down one side and back up the other changes exactly twice; + * a reversed segment (a cap drawn past its own end) adds two more */ +function yTurns(d: string): number { + const ys = points(d).map((p) => p.y) + const steps = ys.map((v, i) => Math.sign(ys[(i + 1) % ys.length] - v)).filter((s) => s !== 0) + let turns = 0 + for (let i = 0; i < steps.length; i++) if (steps[i] !== steps[(i + 1) % steps.length]) turns++ + return turns +} + +describe('silhouettePath — the compact floor (FC-7)', () => { + it('the base height is 56', () => { + expect(BASE_NODE_H).toBe(56) + }) for (const k of KINDS) { - it(`${k}: h = 64 returns the historic path verbatim`, () => { - expect(silhouettePath(k, BASE_NODE_H)).toBe(HISTORIC[k]) + it(`${k}: h = 64 still returns the historic path verbatim`, () => { + expect(silhouettePath(k, 64)).toBe(HISTORIC[k]) }) - it(`${k}: any h ≤ 64 returns the historic path`, () => { - expect(silhouettePath(k, 40)).toBe(HISTORIC[k]) - expect(silhouettePath(k)).toBe(HISTORIC[k]) + it(`${k}: below the floor it draws the floor`, () => { + expect(silhouettePath(k, 40)).toBe(silhouettePath(k, BASE_NODE_H)) + expect(silhouettePath(k)).toBe(silhouettePath(k, BASE_NODE_H)) + }) + it(`${k}: at every height from the floor to its ceiling the outline stays inside the box and never reverses`, () => { + for (let H = BASE_NODE_H; H <= MAX_NODE_H[k]; H++) { + const d = silhouettePath(k, H) + for (const p of points(d)) { + expect(p.x, `${k} at ${H}`).toBeGreaterThanOrEqual(0) + expect(p.x, `${k} at ${H}`).toBeLessThanOrEqual(120) + expect(p.y, `${k} at ${H}`).toBeGreaterThanOrEqual(0) + expect(p.y, `${k} at ${H}`).toBeLessThanOrEqual(H) + } + expect(yTurns(d), `${k} at ${H}: ${d}`).toBe(2) + } }) } + it('the End and the Register meet their caps at mid-height below 64', () => { + expect(silhouettePath('end', 56)).toBe('M28 8 H92 Q112 8 112 28 Q112 48 92 48 H28 Q8 48 8 28 Q8 8 28 8 Z') + expect(silhouettePath('register', 56)).toBe('M14 12 H110 Q118 12 118 28 Q118 44 110 44 H14 Q6 44 6 28 Q6 12 14 12 Z') + }) }) describe('silhouettePath — growth', () => { for (const k of KINDS) { const grown = silhouettePath(k, 88) - it(`${k}: a taller path differs from the base and is still a closed subpath`, () => { + it(`${k}: a taller path differs from the historic one and is still a closed subpath`, () => { expect(grown).not.toBe(HISTORIC[k]) expect(grown.startsWith('M')).toBe(true) expect(grown.trimEnd().endsWith('Z')).toBe(true) @@ -69,10 +120,10 @@ describe('silhouettePath — growth', () => { it(`${k}: the top-cap Y coordinates are unchanged`, () => { for (const y of topCapYs(HISTORIC[k])) expect(topCapYs(grown)).toContain(y) }) - it(`${k}: some coordinate reaches below the base shape's extent`, () => { + it(`${k}: some coordinate reaches below the historic shape's extent`, () => { const baseMaxY = Math.max(...nums(HISTORIC[k])) expect(Math.max(...nums(grown))).toBeGreaterThan(baseMaxY - 1) - // the grown path names a Y at least (88 - 12) = 76, past the base's ~58 bottom + // the grown path names a Y at least (88 - 12) = 76, past the historic ~58 bottom expect(nums(grown).some((v) => v >= 76)).toBe(true) }) } @@ -87,8 +138,9 @@ describe('silhouettePath — growth', () => { describe('clampNodeHeight', () => { it('floors at the base height', () => { - expect(clampNodeHeight('pool', 40)).toBe(64) - expect(clampNodeHeight('pool', 64)).toBe(64) + expect(clampNodeHeight('pool', 40)).toBe(56) + expect(clampNodeHeight('pool', 56)).toBe(56) + expect(clampNodeHeight('pool', 58)).toBe(58) }) it('caps at the per-kind ceiling', () => { expect(clampNodeHeight('gate', 500)).toBe(MAX_NODE_H.gate) diff --git a/src/components/nodes/silhouette.ts b/src/components/nodes/silhouette.ts index 2130f646..6b1984d0 100644 --- a/src/components/nodes/silhouette.ts +++ b/src/components/nodes/silhouette.ts @@ -1,19 +1,31 @@ // docs/mmo-multilingual-layout.md §MML1b — height-parametric vessel silhouettes. // -// The node body can grow past the historic 64px once a title wraps to two -// lines. A `preserveAspectRatio="none"` SVG would then distort every shape, so -// each of the seven kinds gets its own path function: the top and bottom cap -// regions keep their pixel offsets, corner radii / notch / endbar are fixed, -// and only the straight middle stretches. At `h = 64` every function returns a -// path pixel-identical to the previous hard-coded silhouette (asserted by -// `silhouette.test.ts`). +// The node body grows past the base height once its content needs it (a title +// wrapped to two lines, a capacity row). A `preserveAspectRatio="none"` SVG +// would then distort every shape, so each kind gets its own path function: the +// top and bottom cap regions keep their pixel offsets, corner radii / notch / +// endbar are fixed, and only the straight middle stretches. +// +// docs/flow-colour-and-compact-nodes.md FC-7 (issue #325, PR 3) — compact +// nodes: the base height is 56 (it was 64). 56 is a FLOOR, not a forced +// height: the box is measured from its content (`nodes.tsx`), so a one-line +// Pool settles at 58 and anything taller grows as before. At every height of 64 +// and above each function returns the path it always did (at exactly 64, the +// historic hard-coded silhouette, byte for byte; asserted by +// `silhouette.test.ts`). Below 64 the End's and the Register's rounded caps +// meet at mid-height instead of inserting a straight run, so no path ever +// draws a reversed segment. // // The SVG viewBox is `0 0 120 h`; the x geometry (width 120) never changes. import type { NodeKind } from '../../model/types' -/** The design height every silhouette was drawn at. */ -export const BASE_NODE_H = 64 +/** The smallest box height (the compact floor, FC-7). */ +export const BASE_NODE_H = 56 + +/** The height every silhouette was first drawn at; from here up the End and the + * Register insert a straight run between their caps. */ +const CAP_RUN_H = 64 /** docs/flow-colour-and-compact-nodes.md FC-4.1 — the rings' distance from the * silhouette, in screen px (every stroke is non-scaling, so these hold at any @@ -79,9 +91,8 @@ export const MAX_NODE_H: Record = { const n = (v: number) => Math.round(v * 100) / 100 -/** The exact historic paths, returned verbatim at the base height so every - * non-grown node (equilibrium / deadlock, short user labels, most of Coffee) - * is byte-identical to before this change — no visual-snapshot churn. +/** The vessel outline for `kind` at body height `h` (viewBox `0 0 120 h`), + * clamped to the kind's range [`BASE_NODE_H`, `MAX_NODE_H`]. * * `parameter` / `register` were RE-CUT (docs/node-shell-content-in-vessel.md * "curved corner" follow-up): the old capsule / tag inset its left+right edges @@ -96,27 +107,12 @@ const n = (v: number) => Math.round(v * 100) / 100 * (`x1…8 × mid±8`, the historic notch height). Together with a width-scaled * `padding-inline` on the two bodies this keeps every content corner ≥ 2 CSS * px inside the fill at all widths / locales (measured ≥ 3.75 register / - * ≥ 2.75 parameter). The top+bottom cap offsets are - * UNCHANGED (`y12 … H−12`, `VESSEL_INSET_Y` still 24) — #167's height growth is - * untouched. (A true stadium / semicircular-ended capsule cannot contain the - * content: it pinches in hard exactly where the chip / title corner sits, - * 4 px below the top edge.) */ -const BASE: Record = { - pool: 'M32 6 H88 Q95 6 96 13 L112 52 Q113 58 107 58 H13 Q7 58 8 52 L24 13 Q25 6 32 6 Z', - source: 'M14 8 Q8 8 8 14 V50 Q8 56 14 56 H84 L114 32 L84 8 Z', - drain: 'M6 32 L34 8 H104 Q112 8 112 15 V49 Q112 56 104 56 H34 Z', - gate: 'M60 3 L117 32 L60 61 L3 32 Z', - converter: - 'M14 8 H106 Q112 8 112 14 L82 32 L112 50 Q112 56 106 56 H14 Q8 56 8 50 L38 32 L8 14 Q8 8 14 8 Z', - end: 'M28 8 H92 Q112 8 112 32 Q112 56 92 56 H28 Q8 56 8 32 Q8 8 28 8 Z', - parameter: 'M14 12 H106 Q112 12 112 18 V46 Q112 52 106 52 H14 Q8 52 8 46 V40 H1 V24 H8 V18 Q8 12 14 12 Z', - register: 'M14 12 H110 Q118 12 118 32 Q118 52 110 52 H14 Q6 52 6 32 Q6 12 14 12 Z', -} - -/** The vessel outline for `kind` at body height `h` (viewBox `0 0 120 h`). */ + * ≥ 2.75 parameter). The top+bottom cap offsets are `y12 … H−12` + * (`VESSEL_INSET_Y` 24). (A true stadium / semicircular-ended capsule cannot + * contain the content: it pinches in hard exactly where the chip / title + * corner sits, 4 px below the top edge.) */ export function silhouettePath(kind: NodeKind, h = BASE_NODE_H): string { const H = Math.max(BASE_NODE_H, Math.min(h, MAX_NODE_H[kind])) - if (H <= BASE_NODE_H) return BASE[kind] const mid = n(H / 2) switch (kind) { case 'pool': @@ -131,17 +127,22 @@ export function silhouettePath(kind: NodeKind, h = BASE_NODE_H): string { case 'converter': return `M14 8 H106 Q112 8 112 14 L82 ${mid} L112 ${n(H - 14)} Q112 ${n(H - 8)} 106 ${n(H - 8)} H14 Q8 ${n(H - 8)} 8 ${n(H - 14)} L38 ${mid} L8 14 Q8 8 14 8 Z` case 'end': - // fixed cap radius (Q through x112/y-cap); a straight V is inserted for growth - return `M28 8 H92 Q112 8 112 32 V${n(H - 32)} Q112 ${n(H - 8)} 92 ${n(H - 8)} H28 Q8 ${n(H - 8)} 8 ${n(H - 32)} V32 Q8 8 28 8 Z` + // the rounded caps meet at mid-height up to 64; above it a straight V + // keeps the cap radius fixed + return H <= CAP_RUN_H + ? `M28 8 H92 Q112 8 112 ${mid} Q112 ${n(H - 8)} 92 ${n(H - 8)} H28 Q8 ${n(H - 8)} 8 ${mid} Q8 8 28 8 Z` + : `M28 8 H92 Q112 8 112 32 V${n(H - 32)} Q112 ${n(H - 8)} 92 ${n(H - 8)} H28 Q8 ${n(H - 8)} 8 ${n(H - 32)} V32 Q8 8 28 8 Z` case 'parameter': // body `x8…112` r6; left tab `x1…8` × `mid ± 8` (historic notch height), // centred on the left edge's vertical middle return `M14 12 H106 Q112 12 112 18 V${n(H - 18)} Q112 ${n(H - 12)} 106 ${n(H - 12)} H14 Q8 ${n(H - 12)} 8 ${n(H - 18)} V${n(mid + 8)} H1 V${n(mid - 8)} H8 V18 Q8 12 14 12 Z` case 'register': // flattened lozenge: edges `x14…110`, elliptical ends bulging to x6 / x118 - // at mid-height; a straight V grows the middle (identical structure to the - // historic capsule, just wider + flatter-topped) - return `M14 12 H110 Q118 12 118 32 V${n(H - 32)} Q118 ${n(H - 12)} 110 ${n(H - 12)} H14 Q6 ${n(H - 12)} 6 ${n(H - 32)} V32 Q6 12 14 12 Z` + // at mid-height; up to 64 the ends meet at mid-height, above it a straight + // V grows the middle + return H <= CAP_RUN_H + ? `M14 12 H110 Q118 12 118 ${mid} Q118 ${n(H - 12)} 110 ${n(H - 12)} H14 Q6 ${n(H - 12)} 6 ${mid} Q6 12 14 12 Z` + : `M14 12 H110 Q118 12 118 32 V${n(H - 32)} Q118 ${n(H - 12)} 110 ${n(H - 12)} H14 Q6 ${n(H - 12)} 6 ${n(H - 32)} V32 Q6 12 14 12 Z` } } diff --git a/src/engine/templateFlowColours.test.ts b/src/engine/templateFlowColours.test.ts index 5205b262..29e30e29 100644 --- a/src/engine/templateFlowColours.test.ts +++ b/src/engine/templateFlowColours.test.ts @@ -63,8 +63,9 @@ const TEMPLATES: { file: string; flows: TemplateFlows; colours: string[]; conten file: 'gacha-banner-zones.json', flows: GACHA_BANNER_ZONES_FLOWS, colours: ['sage', 'violet', 'rose'], - contentDigest: '525fbb11a247b772e948ae10d6bad3c41e4de29be46821e5a210b8540c2e5e47', - fullContentDigest: '525fbb11a247b772e948ae10d6bad3c41e4de29be46821e5a210b8540c2e5e47', + // #325 PR 3: with the `e_pickup_36` routing waypoint of layout round 7 + contentDigest: 'ac61fc8bd006ca5b1bff67e9d2246e3874c883bbb4a729596064690f60e51b67', + fullContentDigest: 'ac61fc8bd006ca5b1bff67e9d2246e3874c883bbb4a729596064690f60e51b67', }, { file: 'mmo-progression.json', diff --git a/src/i18n/itCopy.test.ts b/src/i18n/itCopy.test.ts index a6418f50..9ecd0714 100644 --- a/src/i18n/itCopy.test.ts +++ b/src/i18n/itCopy.test.ts @@ -205,15 +205,15 @@ const IDENTICAL_TO_EN: ReadonlyArray = [ // ------------------------------------------------------------------ shape describe('it copy — the surfaces exist and are complete', () => { it('has exactly the base key set', () => { - expect(KEYS).toHaveLength(1027) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines) + expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) expect(Object.keys(IT).sort()).toEqual([...KEYS].sort()) }) it('covers all four runtime surfaces, at the measured sizes', () => { const per: Record = {} for (const r of RUNTIME) per[r.surface.split('/')[0]!] = (per[r.surface.split('/')[0]!] ?? 0) + 1 - expect(per).toEqual({ catalog: 1027, template: 196, frame: 7, module: 19 }) - expect(RUNTIME).toHaveLength(1249) + expect(per).toEqual({ catalog: 1030, template: 196, frame: 7, module: 19 }) + expect(RUNTIME).toHaveLength(1252) }) it('EVERY row has a non-empty ENGLISH side — the vacuity guard', () => { diff --git a/src/i18n/locales/ar/ui.ts b/src/i18n/locales/ar/ui.ts index 1b188077..3f9c284b 100644 --- a/src/i18n/locales/ar/ui.ts +++ b/src/i18n/locales/ar/ui.ts @@ -589,6 +589,9 @@ const ui = { 'whatsNew.v0200.templates': 'تُفتح الآن ثلاثة من القوالب المضمّنة وتدفقاتها الرئيسية ملوّنة، ونتائجها لا تتغيّر.', 'whatsNew.v0200.once': 'في قسم اللون في المفتّش يظهر كل لون مرة واحدة: «الأخيرة» و«في هذا المستند» لا تعرضان الألوان الظاهرة أعلاها.', 'whatsNew.v0200.phone': 'على الهاتف يعرض المفتّش للقراءة فقط اللون في سطر واحد: نقطة مع اسم اللون ورمز اللون.', + 'whatsNew.v0210.compact': 'أصبحت العقد أكثر إحكامًا: العقدة ذات السطر الواحد أقصر وأضيق قليلًا، فيتسع على الشاشة جزء أكبر من المخطط الكبير.', + 'whatsNew.v0210.grows': 'العقدة التي تحتاج إلى مساحة ما زالت تكبر: تحتفظ العناوين ذات السطرين والصفوف الإضافية بارتفاعها، وتلتف العناوين الطويلة كما من قبل.', + 'whatsNew.v0210.same': 'لا تتغير مواضع العقد ولا الملفات المحفوظة ولا نتائج المحاكاة؛ وأصبحت الاتصالات تتصل بكل عقدة في موضع أعلى قليلًا.', } as const export default ui diff --git a/src/i18n/locales/de/ui.ts b/src/i18n/locales/de/ui.ts index a2482be7..20b8a8aa 100644 --- a/src/i18n/locales/de/ui.ts +++ b/src/i18n/locales/de/ui.ts @@ -624,6 +624,9 @@ const ui = { 'whatsNew.v0200.templates': 'Drei der mitgelieferten Vorlagen öffnen sich jetzt mit farbigen Hauptflüssen; ihre Ergebnisse bleiben gleich.', 'whatsNew.v0200.once': 'Im Abschnitt Farbe der Eigenschaften erscheint jede Farbe nur einmal: Zuletzt verwendet und In diesem Dokument lassen die Farben weg, die schon darüber stehen.', 'whatsNew.v0200.phone': 'Auf dem Smartphone zeigen die schreibgeschützten Eigenschaften eine Farbe in einer Zeile: einen Punkt mit dem Namen der Farbe und ihrem Hex-Wert.', + 'whatsNew.v0210.compact': 'Knoten sind kompakter: Ein einzeiliger Knoten ist niedriger und etwas schmaler, sodass mehr von einem großen Graphen auf den Bildschirm passt.', + 'whatsNew.v0210.grows': 'Ein Knoten, der Platz braucht, wächst weiterhin: Zweizeilige Titel und zusätzliche Zeilen behalten ihre Höhe, und lange Titel brechen wie bisher um.', + 'whatsNew.v0210.same': 'Knotenpositionen, gespeicherte Dateien und Simulationsergebnisse bleiben unverändert; Verbindungen setzen jetzt etwas höher am Knoten an.', } satisfies Record export default ui diff --git a/src/i18n/locales/en/ui.ts b/src/i18n/locales/en/ui.ts index a019169f..af1cff6a 100644 --- a/src/i18n/locales/en/ui.ts +++ b/src/i18n/locales/en/ui.ts @@ -533,6 +533,9 @@ const ui = { 'whatsNew.v0200.templates': 'Three of the bundled templates now open with their main flows coloured, and their results are unchanged.', 'whatsNew.v0200.once': 'The Inspector’s Colour section shows each colour once: Recent and In this document leave out the colours already shown above them.', 'whatsNew.v0200.phone': 'On a phone, the read-only Inspector shows a colour as one line: a dot with the colour’s name and hex value.', + 'whatsNew.v0210.compact': 'Nodes are more compact: a one-line node is shorter and a little narrower, so more of a large graph fits in view at once.', + 'whatsNew.v0210.grows': 'A node that needs the room still grows: two-line titles and extra rows keep their height, and long titles wrap as before.', + 'whatsNew.v0210.same': 'Node positions, saved files and simulation results are unchanged; connections now attach a little higher on each node.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/es-419/ui.ts b/src/i18n/locales/es-419/ui.ts index a62f1d22..150fd845 100644 --- a/src/i18n/locales/es-419/ui.ts +++ b/src/i18n/locales/es-419/ui.ts @@ -547,6 +547,9 @@ const ui = { 'whatsNew.v0200.templates': 'Tres de las plantillas incluidas ahora se abren con sus flujos principales en color; sus resultados no cambian.', 'whatsNew.v0200.once': 'En la sección Color de Propiedades, cada color aparece una sola vez: Recientes y En este documento omiten los colores que ya se muestran arriba.', 'whatsNew.v0200.phone': 'En un teléfono, Propiedades en modo de solo lectura muestra un color en una línea: un punto con el nombre del color y su valor hexadecimal.', + 'whatsNew.v0210.compact': 'Los nodos son más compactos: un nodo de una línea es más bajo y algo más estrecho, así que cabe en pantalla una parte mayor de un grafo grande.', + 'whatsNew.v0210.grows': 'Un nodo que necesita espacio sigue creciendo: los títulos de dos líneas y las filas adicionales mantienen su altura, y los títulos largos se ajustan como antes.', + 'whatsNew.v0210.same': 'Las posiciones de los nodos, los archivos guardados y los resultados de la simulación no cambian; las conexiones ahora se unen un poco más arriba en cada nodo.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/es-ES/ui.ts b/src/i18n/locales/es-ES/ui.ts index 6b652e85..be4a1f27 100644 --- a/src/i18n/locales/es-ES/ui.ts +++ b/src/i18n/locales/es-ES/ui.ts @@ -561,6 +561,9 @@ const ui = { 'whatsNew.v0200.templates': 'Tres de las plantillas incluidas ahora se abren con sus flujos principales en color; sus resultados no cambian.', 'whatsNew.v0200.once': 'En la sección Color de Propiedades, cada color aparece una sola vez: Recientes y En este documento omiten los colores que ya se muestran arriba.', 'whatsNew.v0200.phone': 'En un teléfono, Propiedades en modo de solo lectura muestra un color en una línea: un punto con el nombre del color y su valor hexadecimal.', + 'whatsNew.v0210.compact': 'Los nodos son más compactos: un nodo de una línea es más bajo y algo más estrecho, así que cabe en pantalla una parte mayor de un grafo grande.', + 'whatsNew.v0210.grows': 'Un nodo que necesita espacio sigue creciendo: los títulos de dos líneas y las filas adicionales mantienen su altura, y los títulos largos se ajustan como antes.', + 'whatsNew.v0210.same': 'Las posiciones de los nodos, los archivos guardados y los resultados de la simulación no cambian; las conexiones ahora se unen un poco más arriba en cada nodo.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/fr/ui.ts b/src/i18n/locales/fr/ui.ts index ee755afc..0f3bd74c 100644 --- a/src/i18n/locales/fr/ui.ts +++ b/src/i18n/locales/fr/ui.ts @@ -618,6 +618,9 @@ const ui = { 'whatsNew.v0200.templates': 'Trois des modèles fournis s’ouvrent désormais avec leurs flux principaux en couleur ; leurs résultats ne changent pas.', 'whatsNew.v0200.once': 'Dans la section Couleur des Propriétés, chaque couleur n’apparaît qu’une fois : Récentes et Dans ce document omettent les couleurs déjà affichées au-dessus.', 'whatsNew.v0200.phone': 'Sur un téléphone, les Propriétés en lecture seule affichent une couleur sur une ligne : une pastille avec le nom de la couleur et sa valeur hex.', + 'whatsNew.v0210.compact': 'Les nœuds sont plus compacts : un nœud d’une ligne est moins haut et un peu plus étroit, pour qu’une plus grande partie d’un grand graphe tienne à l’écran.', + 'whatsNew.v0210.grows': 'Un nœud qui a besoin de place grandit toujours : les titres sur deux lignes et les lignes supplémentaires gardent leur hauteur, et les titres longs passent à la ligne comme avant.', + 'whatsNew.v0210.same': 'Les positions des nœuds, les fichiers enregistrés et les résultats de simulation ne changent pas ; les connexions s’attachent maintenant un peu plus haut sur chaque nœud.', } satisfies Record export default ui diff --git a/src/i18n/locales/it/ui.ts b/src/i18n/locales/it/ui.ts index 0fbdcd65..f1fbcb7d 100644 --- a/src/i18n/locales/it/ui.ts +++ b/src/i18n/locales/it/ui.ts @@ -563,6 +563,9 @@ const ui = { 'whatsNew.v0200.templates': 'Tre dei modelli inclusi ora si aprono con i flussi principali colorati; i loro risultati non cambiano.', 'whatsNew.v0200.once': 'Nella sezione Colore dell’Ispettore ogni colore compare una sola volta: Recenti e In questo documento tralasciano i colori già mostrati sopra.', 'whatsNew.v0200.phone': 'Sul telefono l’Ispettore in sola lettura mostra un colore su una riga: un punto con il nome del colore e il suo valore esadecimale.', + 'whatsNew.v0210.compact': 'I nodi sono più compatti: un nodo su una riga è più basso e un po’ più stretto, così una parte maggiore di un grafo grande entra nello schermo.', + 'whatsNew.v0210.grows': 'Un nodo che ha bisogno di spazio cresce comunque: i titoli su due righe e le righe in più mantengono la loro altezza, e i titoli lunghi vanno a capo come prima.', + 'whatsNew.v0210.same': 'Le posizioni dei nodi, i file salvati e i risultati della simulazione non cambiano; le connessioni ora si agganciano un po’ più in alto su ogni nodo.', } satisfies Record export default ui diff --git a/src/i18n/locales/ja/ui.ts b/src/i18n/locales/ja/ui.ts index 22b012d4..86f95385 100644 --- a/src/i18n/locales/ja/ui.ts +++ b/src/i18n/locales/ja/ui.ts @@ -568,6 +568,9 @@ const ui = { 'whatsNew.v0200.templates': '付属のテンプレートのうち 3 つが、主なフローに色を付けた状態で開くようになりました。結果は変わりません。', 'whatsNew.v0200.once': 'インスペクターの色の欄では、同じ色は一度だけ表示されます。「最近」と「このドキュメントの色」には、上にすでにある色は表示されません。', 'whatsNew.v0200.phone': 'スマートフォンの読み取り専用のインスペクターでは、色を 1 行で表示します。色の点と、色の名前と HEX 値です。', + 'whatsNew.v0210.compact': 'ノードがよりコンパクトになりました。1 行のノードが少し低く細くなり、大きなグラフを画面に多く収められます。', + 'whatsNew.v0210.grows': '場所が必要なノードはこれまでどおり大きくなります。2 行のタイトルや追加の行は高さを保ち、長いタイトルも以前と同じく折り返します。', + 'whatsNew.v0210.same': 'ノードの位置、保存したファイル、シミュレーション結果は変わりません。接続は各ノードの少し上側につながるようになりました。', } satisfies Record export default ui diff --git a/src/i18n/locales/ko/ui.ts b/src/i18n/locales/ko/ui.ts index dac17bb2..ff252154 100644 --- a/src/i18n/locales/ko/ui.ts +++ b/src/i18n/locales/ko/ui.ts @@ -536,6 +536,9 @@ const ui = { 'whatsNew.v0200.templates': '기본 제공 템플릿 세 개가 주요 흐름에 색상을 입힌 채로 열리며, 결과는 이전과 같습니다.', 'whatsNew.v0200.once': '인스펙터의 색상 영역에서는 각 색상이 한 번만 나옵니다. 최근과 이 문서의 색상에는 위에 이미 나온 색상이 빠집니다.', 'whatsNew.v0200.phone': '휴대폰의 읽기 전용 인스펙터에서는 색상을 한 줄로 보여 줍니다. 색상 점과 함께 이름과 HEX 값이 표시됩니다.', + 'whatsNew.v0210.compact': '노드가 더 촘촘해졌습니다. 한 줄 노드가 조금 낮고 좁아져 큰 그래프를 화면에 더 많이 담을 수 있습니다.', + 'whatsNew.v0210.grows': '공간이 필요한 노드는 그대로 커집니다. 두 줄 제목과 추가 행은 높이를 유지하고, 긴 제목도 이전처럼 줄바꿈됩니다.', + 'whatsNew.v0210.same': '노드 위치, 저장된 파일, 시뮬레이션 결과는 바뀌지 않습니다. 연결은 이제 각 노드에서 조금 더 위쪽에 붙습니다.', } satisfies Record export default ui diff --git a/src/i18n/locales/nl/ui.ts b/src/i18n/locales/nl/ui.ts index dfd2b7a5..3d351484 100644 --- a/src/i18n/locales/nl/ui.ts +++ b/src/i18n/locales/nl/ui.ts @@ -544,6 +544,9 @@ const ui = { 'whatsNew.v0200.templates': 'Drie van de meegeleverde sjablonen openen nu met hun hoofdstromen in kleur; hun resultaten veranderen niet.', 'whatsNew.v0200.once': 'In het onderdeel Kleur van de Inspector staat elke kleur maar één keer: Laatst gebruikt en In dit document laten de kleuren weg die al hoger staan.', 'whatsNew.v0200.phone': 'Op een telefoon toont de alleen-lezen Inspector een kleur op één regel: een stip met de naam van de kleur en de hexwaarde.', + 'whatsNew.v0210.compact': 'Knooppunten zijn compacter: een knooppunt van één regel is lager en iets smaller, zodat er meer van een groot diagram op het scherm past.', + 'whatsNew.v0210.grows': 'Een knooppunt dat ruimte nodig heeft, groeit nog steeds: titels van twee regels en aanvullende regels houden hun hoogte, en lange titels lopen door zoals voorheen.', + 'whatsNew.v0210.same': 'Posities van knooppunten, opgeslagen bestanden en simulatieresultaten veranderen niet; verbindingen sluiten nu iets hoger op elk knooppunt aan.', } as const export default ui diff --git a/src/i18n/locales/pt-BR/ui.ts b/src/i18n/locales/pt-BR/ui.ts index 4a545b42..6f2a520c 100644 --- a/src/i18n/locales/pt-BR/ui.ts +++ b/src/i18n/locales/pt-BR/ui.ts @@ -549,6 +549,9 @@ const ui = { 'whatsNew.v0200.templates': 'Três dos templates incluídos agora abrem com os fluxos principais em cor; os resultados não mudam.', 'whatsNew.v0200.once': 'Na área Cor do Inspetor, cada cor aparece uma só vez: Recentes e Neste documento omitem as cores já mostradas acima.', 'whatsNew.v0200.phone': 'No celular, o Inspetor somente leitura mostra uma cor numa linha: um ponto com o nome da cor e o seu valor hexadecimal.', + 'whatsNew.v0210.compact': 'Os nós estão mais compactos: um nó de uma linha é mais baixo e um pouco mais estreito, e assim cabe mais de um grafo grande de uma só vez.', + 'whatsNew.v0210.grows': 'Um nó que precisa de espaço continua a crescer: títulos de duas linhas e linhas adicionais mantêm a altura, e títulos longos passam para a linha seguinte como antes.', + 'whatsNew.v0210.same': 'As posições dos nós, os arquivos salvos e os resultados da simulação não mudam; as conexões agora se ligam um pouco mais acima em cada nó.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/pt-PT/ui.ts b/src/i18n/locales/pt-PT/ui.ts index a6e26893..8349aca4 100644 --- a/src/i18n/locales/pt-PT/ui.ts +++ b/src/i18n/locales/pt-PT/ui.ts @@ -563,6 +563,9 @@ const ui = { 'whatsNew.v0200.templates': 'Três dos templates incluídos agora abrem com os fluxos principais em cor; os resultados não mudam.', 'whatsNew.v0200.once': 'Na área Cor do Inspetor, cada cor aparece uma só vez: Recentes e Neste documento omitem as cores já mostradas acima.', 'whatsNew.v0200.phone': 'No telemóvel, o Inspetor somente leitura mostra uma cor numa linha: um ponto com o nome da cor e o seu valor hexadecimal.', + 'whatsNew.v0210.compact': 'Os nós estão mais compactos: um nó de uma linha é mais baixo e um pouco mais estreito, e assim cabe mais de um grafo grande de uma só vez.', + 'whatsNew.v0210.grows': 'Um nó que precisa de espaço continua a crescer: títulos de duas linhas e linhas adicionais mantêm a altura, e títulos longos passam para a linha seguinte como antes.', + 'whatsNew.v0210.same': 'As posições dos nós, os ficheiros guardados e os resultados da simulação não mudam; as ligações passam a prender-se um pouco mais acima em cada nó.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/ru/ui.ts b/src/i18n/locales/ru/ui.ts index 3c1e6c22..1f99f571 100644 --- a/src/i18n/locales/ru/ui.ts +++ b/src/i18n/locales/ru/ui.ts @@ -617,6 +617,9 @@ const ui = { 'whatsNew.v0200.templates': 'Три встроенных шаблона теперь открываются с окрашенными основными потоками; их результаты не меняются.', 'whatsNew.v0200.once': 'В разделе «Цвет» инспектора каждый цвет показан один раз: в «Недавних» и «В этом документе» нет цветов, уже показанных выше.', 'whatsNew.v0200.phone': 'На телефоне инспектор только для чтения показывает цвет одной строкой: точка с названием цвета и его шестнадцатеричным значением.', + 'whatsNew.v0210.compact': 'Узлы стали компактнее: однострочный узел ниже и немного уже, поэтому на экране помещается больше большого графа.', + 'whatsNew.v0210.grows': 'Узел, которому нужно место, по-прежнему растёт: двухстрочные заголовки и дополнительные строки сохраняют высоту, а длинные заголовки переносятся как раньше.', + 'whatsNew.v0210.same': 'Положения узлов, сохранённые файлы и результаты симуляции не меняются; связи теперь присоединяются к каждому узлу чуть выше.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/th/ui.ts b/src/i18n/locales/th/ui.ts index b7b43183..b85d44f4 100644 --- a/src/i18n/locales/th/ui.ts +++ b/src/i18n/locales/th/ui.ts @@ -609,6 +609,9 @@ const ui = { 'whatsNew.v0200.templates': 'เทมเพลตที่มาพร้อมแอปสามรายการเปิดขึ้นพร้อมโฟลว์หลักที่ใส่สีไว้แล้ว และผลลัพธ์ไม่เปลี่ยนแปลง', 'whatsNew.v0200.once': 'ในส่วนสีของตัวตรวจสอบ แต่ละสีจะแสดงเพียงครั้งเดียว รายการล่าสุดและในเอกสารนี้จะไม่แสดงสีที่อยู่ด้านบนแล้ว', 'whatsNew.v0200.phone': 'บนโทรศัพท์ ตัวตรวจสอบแบบอ่านอย่างเดียวจะแสดงสีเป็นบรรทัดเดียว คือจุดสีพร้อมชื่อสีและรหัสสี', + 'whatsNew.v0210.compact': 'โหนดกะทัดรัดขึ้น โหนดบรรทัดเดียวเตี้ยลงและแคบลงเล็กน้อย จึงแสดงกราฟขนาดใหญ่บนหน้าจอได้มากขึ้น', + 'whatsNew.v0210.grows': 'โหนดที่ต้องการพื้นที่ยังขยายได้ตามเดิม ชื่อสองบรรทัดและแถวเพิ่มเติมยังคงความสูงไว้ และชื่อยาวยังตัดบรรทัดเหมือนเดิม', + 'whatsNew.v0210.same': 'ตำแหน่งโหนด ไฟล์ที่บันทึกไว้ และผลการจำลองไม่เปลี่ยน ตอนนี้เส้นเชื่อมจะต่อกับแต่ละโหนดในตำแหน่งที่สูงขึ้นเล็กน้อย', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/tr/ui.ts b/src/i18n/locales/tr/ui.ts index ec183c0b..d653124d 100644 --- a/src/i18n/locales/tr/ui.ts +++ b/src/i18n/locales/tr/ui.ts @@ -603,6 +603,9 @@ const ui = { 'whatsNew.v0200.templates': 'Hazır şablonlardan üçü artık ana akışları renkli olarak açılıyor; sonuçları değişmiyor.', 'whatsNew.v0200.once': 'İnceleyici’nin Renk bölümünde her renk yalnızca bir kez görünür: Son kullanılanlar ve Bu belgede, yukarıda zaten görünen renkleri göstermez.', 'whatsNew.v0200.phone': 'Telefonda salt okunur İnceleyici bir rengi tek satırda gösterir: rengin adı ve onaltılık değeriyle bir nokta.', + 'whatsNew.v0210.compact': 'Düğümler daha kompakt: tek satırlık bir düğüm daha alçak ve biraz daha dar, böylece büyük bir grafiğin daha fazlası ekrana sığar.', + 'whatsNew.v0210.grows': 'Yere ihtiyacı olan bir düğüm yine büyür: iki satırlık başlıklar ve ek satırlar yüksekliklerini korur, uzun başlıklar da eskisi gibi alt satıra geçer.', + 'whatsNew.v0210.same': 'Düğüm konumları, kaydedilmiş dosyalar ve simülasyon sonuçları değişmez; bağlantılar artık her düğüme biraz daha yukarıdan bağlanır.', } as const export type UiKey = keyof typeof ui diff --git a/src/i18n/locales/vi/ui.ts b/src/i18n/locales/vi/ui.ts index b15184ab..de1cad1c 100644 --- a/src/i18n/locales/vi/ui.ts +++ b/src/i18n/locales/vi/ui.ts @@ -549,6 +549,9 @@ const ui = { 'whatsNew.v0200.templates': 'Ba mẫu có sẵn giờ mở ra với các luồng chính đã được tô màu; kết quả của chúng không thay đổi.', 'whatsNew.v0200.once': 'Trong mục Màu của Bảng thuộc tính, mỗi màu chỉ xuất hiện một lần: Gần đây và Trong tài liệu này bỏ qua các màu đã hiện ở phía trên.', 'whatsNew.v0200.phone': 'Trên điện thoại, Bảng thuộc tính chỉ đọc hiển thị màu trên một dòng: một chấm màu cùng tên màu và mã màu.', + 'whatsNew.v0210.compact': 'Nút gọn hơn: nút một dòng thấp hơn và hẹp hơn một chút, nên màn hình chứa được nhiều hơn của một đồ thị lớn.', + 'whatsNew.v0210.grows': 'Nút cần chỗ vẫn lớn ra: tiêu đề hai dòng và các dòng thêm giữ nguyên chiều cao, tiêu đề dài vẫn xuống dòng như trước.', + 'whatsNew.v0210.same': 'Vị trí nút, tệp đã lưu và kết quả mô phỏng không đổi; liên kết giờ gắn vào mỗi nút ở vị trí cao hơn một chút.', } satisfies Record export default ui diff --git a/src/i18n/locales/zh-Hans/ui.ts b/src/i18n/locales/zh-Hans/ui.ts index 4ead9f1e..76f3f59f 100644 --- a/src/i18n/locales/zh-Hans/ui.ts +++ b/src/i18n/locales/zh-Hans/ui.ts @@ -541,6 +541,9 @@ const ui = { 'whatsNew.v0200.templates': '三个内置模板现在打开时主要流程已设置颜色,结果与之前相同。', 'whatsNew.v0200.once': '属性面板的颜色部分中,每种颜色只出现一次:“最近”和“本文档中的颜色”不再列出上方已显示的颜色。', 'whatsNew.v0200.phone': '在手机上,只读属性面板用一行显示颜色:一个色点,以及颜色名称和十六进制值。', + 'whatsNew.v0210.compact': '节点更紧凑了:单行节点更矮、略窄,屏幕上能显示更多的大型图。', + 'whatsNew.v0210.grows': '需要空间的节点仍会变大:两行标题和额外的行保持原有高度,长标题照常换行。', + 'whatsNew.v0210.same': '节点位置、已保存的文件和模拟结果都不变;连线现在连接在每个节点稍高的位置。', } satisfies Record export default ui diff --git a/src/i18n/locales/zh-Hant/ui.ts b/src/i18n/locales/zh-Hant/ui.ts index e3632721..7942332c 100644 --- a/src/i18n/locales/zh-Hant/ui.ts +++ b/src/i18n/locales/zh-Hant/ui.ts @@ -567,6 +567,9 @@ const ui = { 'whatsNew.v0200.templates': '三個內建範本現在開啟時主要流程已設定顏色,結果與之前相同。', 'whatsNew.v0200.once': '屬性面板的顏色區段中,每種顏色只會出現一次:「最近」和「此文件中的顏色」不再列出上方已顯示的顏色。', 'whatsNew.v0200.phone': '在手機上,唯讀屬性面板以一行顯示顏色:一個色點,以及顏色名稱和十六進位值。', + 'whatsNew.v0210.compact': '節點更緊湊了:單行節點更矮、略窄,螢幕上能顯示更多的大型圖。', + 'whatsNew.v0210.grows': '需要空間的節點仍會變大:兩行標題和額外的列保持原有高度,長標題照常換行。', + 'whatsNew.v0210.same': '節點位置、已儲存的檔案和模擬結果都不變;連線現在連接在每個節點稍高的位置。', } satisfies Record export default ui diff --git a/src/i18n/nlCopy.test.ts b/src/i18n/nlCopy.test.ts index 70c43ee4..0fa9a6af 100644 --- a/src/i18n/nlCopy.test.ts +++ b/src/i18n/nlCopy.test.ts @@ -261,15 +261,15 @@ const IDENTICAL_TO_EN: ReadonlyArray = [ // ------------------------------------------------------------------ shape describe('nl copy — the surfaces exist and are complete', () => { it('has exactly the base key set', () => { - expect(KEYS).toHaveLength(1027) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines) + expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) expect(Object.keys(NL).sort()).toEqual([...KEYS].sort()) }) it('covers all four runtime surfaces, at the measured sizes', () => { const per: Record = {} for (const r of RUNTIME) per[r.surface.split('/')[0]!] = (per[r.surface.split('/')[0]!] ?? 0) + 1 - expect(per).toEqual({ catalog: 1027, template: 196, frame: 7, module: 19 }) - expect(RUNTIME).toHaveLength(1249) + expect(per).toEqual({ catalog: 1030, template: 196, frame: 7, module: 19 }) + expect(RUNTIME).toHaveLength(1252) }) it('EVERY row has a non-empty ENGLISH side — the vacuity guard', () => { diff --git a/src/i18n/ptPtCopy.test.ts b/src/i18n/ptPtCopy.test.ts index f5c5715b..db12810a 100644 --- a/src/i18n/ptPtCopy.test.ts +++ b/src/i18n/ptPtCopy.test.ts @@ -58,7 +58,7 @@ describe('pt-PT copy — the region audit over pt-BR', () => { // ---------------------------------------------------------------- shape it('has exactly the same key set as pt-BR', () => { expect(Object.keys(ptValues).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1027) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines) + expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) }) it('differs from pt-BR on exactly the audited keys', () => { @@ -139,7 +139,10 @@ describe('pt-PT copy — the region audit over pt-BR', () => { // The flow colour views (0.20.0) moved it from 273 to 274: one of its four // release-note lines - `telemóvel` not `celular`; the other three read the // same in both. - expect(DELTA).toHaveLength(274) + // Compact nodes (0.21.0) moved it from 274 to 275: one of its three + // release-note lines - `ficheiros guardados` not `arquivos salvos`, `ligações` + // not `conexões`; the other two read the same in both. + expect(DELTA).toHaveLength(275) // and it is a real audit, not a rewrite — most of the catalog agrees. // // The password strings of issue #300 are counted apart. The word for a @@ -183,7 +186,7 @@ describe('pt-PT copy — the region audit over pt-BR', () => { // outside them, the quarter bound stands as it always did const rest = KEYS.filter((k) => !isPasswordKey(k)) const restDelta = DELTA.filter((k) => !isPasswordKey(k)) - expect(restDelta).toHaveLength(246) + expect(restDelta).toHaveLength(247) expect(restDelta.length).toBeLessThan(rest.length / 4) }) diff --git a/src/i18n/ruCopy.test.ts b/src/i18n/ruCopy.test.ts index 81644ca3..db7e5fb4 100644 --- a/src/i18n/ruCopy.test.ts +++ b/src/i18n/ruCopy.test.ts @@ -43,7 +43,7 @@ describe('ru copy — the first Cyrillic catalog', () => { // ------------------------------------------------------------------ shape it('has exactly the base key set', () => { expect(Object.keys(ruValues).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1027) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines) + expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) }) // ----------------------------------------------------------------- script @@ -246,6 +246,7 @@ describe('ru copy — the first Cyrillic catalog', () => { 'приведён', 'прошёл', 'разделённые', + 'растёт', 'своё', 'скруглённые', 'скруглёнными', diff --git a/src/i18n/thCopy.test.ts b/src/i18n/thCopy.test.ts index d67b5699..7e941330 100644 --- a/src/i18n/thCopy.test.ts +++ b/src/i18n/thCopy.test.ts @@ -88,11 +88,11 @@ const englishFor = (surface: string, id: string): string => { describe('th copy — the first no-space script', () => { it('has exactly the base key set', () => { expect(Object.keys(TH).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1027) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines) + expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) }) it('covers all three runtime surfaces', () => { - expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1027) + expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1030) expect(RUNTIME.filter(([s]) => s.startsWith('template/'))).toHaveLength(196) expect(RUNTIME.filter(([s]) => s.startsWith('frame/'))).toHaveLength(7) expect(RUNTIME.filter(([s]) => s.startsWith('module/'))).toHaveLength(19) diff --git a/src/i18n/trCopy.test.ts b/src/i18n/trCopy.test.ts index b66c90e0..30c1adba 100644 --- a/src/i18n/trCopy.test.ts +++ b/src/i18n/trCopy.test.ts @@ -30,7 +30,7 @@ const KEYS = Object.keys(EN) describe('tr copy — the first Turkish catalog', () => { it('has exactly the base key set', () => { expect(Object.keys(TR).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1027) // issue #300: +33; issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines) + expect(KEYS).toHaveLength(1030) // issue #300: +33; issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) }) }) diff --git a/src/i18n/viCopy.test.ts b/src/i18n/viCopy.test.ts index 794a98be..c4508c84 100644 --- a/src/i18n/viCopy.test.ts +++ b/src/i18n/viCopy.test.ts @@ -132,11 +132,11 @@ const englishFor = (surface: string, id: string): string => { describe('vi copy — the first locale English hides inside', () => { it('has exactly the base key set', () => { expect(Object.keys(VI).sort()).toEqual(KEYS.slice().sort()) - expect(KEYS).toHaveLength(1027) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines) + expect(KEYS).toHaveLength(1030) // issue #301 decision 1: +4 (share.unavailable and the three 0.17.1 release-note lines); the session chip: +3 (the three 0.17.2 release-note lines); the licence screen: +12 (nine strings of the view and the three 0.18.0 release-note lines); the menu keyboard: +3 (the three 0.18.1 release-note lines); the tour announcement: +4 (tour.nav.announce and the three 0.18.2 release-note lines); the flow colour: +20 (the sixteen strings of the Colour section and the four 0.19.0 release-note lines); the flow colour views: +4 (the four 0.20.0 release-note lines); compact nodes: +3 (the three 0.21.0 release-note lines) }) it('covers all four runtime surfaces', () => { - expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1027) + expect(RUNTIME.filter(([s]) => s === 'catalog')).toHaveLength(1030) expect(RUNTIME.filter(([s]) => s.startsWith('template/'))).toHaveLength(196) expect(RUNTIME.filter(([s]) => s.startsWith('frame/'))).toHaveLength(7) expect(RUNTIME.filter(([s]) => s.startsWith('module/'))).toHaveLength(19) diff --git a/src/index.css b/src/index.css index 265c565d..4be2e901 100644 --- a/src/index.css +++ b/src/index.css @@ -1720,9 +1720,10 @@ a.sheet__row { redrawn at that height by `./silhouette`, so `preserveAspectRatio="none"` no longer distorts it. */ max-width: 260px; - height: 64px; /* +4px to seat the Plex type scale (title 14 / value 18). - A two-line title replaces this with an inline `height` (the measured body - height, capped per kind) and the vessel is redrawn at that height. */ + height: 56px; /* docs/flow-colour-and-compact-nodes.md FC-7 — the compact + floor (it was 64). Content that needs more (a one-line Pool settles at 58, a + two-line title, a capacity row) replaces this with an inline `height` (the + measured body height, capped per kind) and the vessel is redrawn at it. */ font-family: var(--font-sans); font-size: 12px; filter: var(--elev-node); @@ -1957,7 +1958,7 @@ a.sheet__row { flex-direction: column; justify-content: center; width: 100%; - padding: 6px 16px; + padding: 6px 12px; /* FC-7: inline 16 -> 12 */ pointer-events: none; } /* the un-stretched content column — its natural height drives the box height @@ -1971,13 +1972,13 @@ a.sheet__row { max-width: 100%; } .nodef--source .nodef__body { - padding-right: 26px; + padding-right: 22px; /* FC-7: 26 -> 22, still clear of the arrow tip */ } .nodef--drain .nodef__body { - padding-left: 26px; + padding-left: 22px; /* FC-7: 26 -> 22 */ } .nodef--gate .nodef__body { - padding: 8px 30px; + padding: 8px 26px; /* FC-7: inline 30 -> 26 */ align-items: center; } .nodef--end .nodef__body { diff --git a/src/model/sha256Baseline.test.ts b/src/model/sha256Baseline.test.ts index a480d68b..f410126d 100644 --- a/src/model/sha256Baseline.test.ts +++ b/src/model/sha256Baseline.test.ts @@ -68,10 +68,26 @@ describe('SHA-256 after the replacement, against what the replaced code computed // are kept, checked only by the explicit legacy test below. const COLOURED_SINCE = new Set(['coffee-roastery.json', 'gacha-banner-zones.json', 'mmo-progression.json']) -/** a document's text without its flow colours: the file as it was recorded */ -const withoutFlowColours = (text: string): string => { - const doc = JSON.parse(text) as { nodes: { data: Record }[]; edges: { data?: Record }[] } +// #325 PR 3 (compact nodes) - one more cosmetic addition since the baseline: the +// gacha Template's layout round 7 gave `e_pickup_36` one routing waypoint +// (scripts/gen-gacha-banner-zones-example.ts) so its `+1` label no longer meets +// `e_pickup_34`'s on the shorter nodes. Routing is never engine data, so the +// engine digest still matches; the legacy projection removes exactly this one +// waypoint, and fails if it is not exactly what PR 3 added. +const ADDED_WAYPOINTS: Record> = { + 'gacha-banner-zones.json': { e_pickup_36: [{ x: 3200, y: 650 }] }, +} + +/** a document's text without what was added after the #301 baseline: the flow + * colours (PR 2) and the one PR 3 waypoint - the file as it was recorded */ +const legacyProjection = (file: string, text: string): string => { + const doc = JSON.parse(text) as { nodes: { data: Record }[]; edges: { id: string; data?: Record }[] } for (const el of [...doc.nodes, ...doc.edges]) if (el.data) delete el.data.accent + for (const [edgeId, added] of Object.entries(ADDED_WAYPOINTS[file] ?? {})) { + const edge = doc.edges.find((e) => e.id === edgeId) + expect(edge?.data?.waypoints, `${file} ${edgeId}: the waypoint PR 3 added`).toEqual(added) + delete edge!.data!.waypoints + } return JSON.stringify(doc) } @@ -105,11 +121,11 @@ describe('the digests of real documents are unchanged', () => { }) } - it('legacy (#301 baseline): the three coloured Templates without their flow colours give the recorded digests', async () => { + it('legacy (#301 baseline): the three coloured Templates without their flow colours (PR 2) and the gacha e_pickup_36 waypoint (PR 3) give the recorded digests', async () => { const legacy = EXAMPLE_GRAPH_DIGESTS.filter((g) => COLOURED_SINCE.has(g.file)) expect(legacy.map((g) => g.file).sort()).toEqual([...COLOURED_SINCE].sort()) for (const g of legacy) { - const p = deserialize(withoutFlowColours(read(g.file))) + const p = deserialize(legacyProjection(g.file, read(g.file))) const doc = docOf(p) expect(p.modelVersion, g.file).toBe(g.modelVersion) expect(await semanticDigest({ nodes: p.nodes, edges: p.edges }, p.modelVersion), g.file).toBe(g.semanticDigest) diff --git a/src/releaseNotes/releaseNotes.ts b/src/releaseNotes/releaseNotes.ts index cd894b10..be95f927 100644 --- a/src/releaseNotes/releaseNotes.ts +++ b/src/releaseNotes/releaseNotes.ts @@ -38,6 +38,15 @@ export const releaseNoteIdFor = (version: string): ReleaseNoteId => `release:${v * mechanics, no test counts, no pull-request numbers. */ export const RELEASE_NOTES: readonly ReleaseNote[] = [ + { + // issue #325 PR 3 - compact nodes: a 56 px floor instead of 64, less side + // padding, content still deciding; the date is the day it is deployed, set + // right before the merge + id: 'release:0.21.0', + version: '0.21.0', + date: '2026-10-07', + items: ['whatsNew.v0210.compact', 'whatsNew.v0210.grows', 'whatsNew.v0210.same'], + }, { // issue #325 PR 2 - flow colours in the minimap, the timeline and three // bundled templates, each colour offered once in the Inspector, and a diff --git a/src/store/routeMap.ts b/src/store/routeMap.ts index 6bf4d68b..1fb1c430 100644 --- a/src/store/routeMap.ts +++ b/src/store/routeMap.ts @@ -25,9 +25,12 @@ import { type RouteResult, ROUTER_VERSION, } from '../components/edges/orthogonalRoute' +import { BASE_NODE_H } from '../components/nodes/silhouette' +// the size of a node React Flow has not measured yet; the height is the node's +// own floor, so a one-line node routes the same before and after it is measured const DEFAULT_W = 130 -const DEFAULT_H = 64 +const DEFAULT_H = BASE_NODE_H type Key = { nodes: LoopNode[]; edges: LoopEdge[]; v: number; sig: string } let cacheKey: Key | null = null diff --git a/test/orthogonalRoute.golden.test.ts b/test/orthogonalRoute.golden.test.ts index e7e902c9..28aa0fcd 100644 --- a/test/orthogonalRoute.golden.test.ts +++ b/test/orthogonalRoute.golden.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { execSync } from 'node:child_process' import { readFileSync, writeFileSync } from 'node:fs' import { computeOrthogonalRoute, ROUTER_VERSION, type RouteResult } from '../src/components/edges/orthogonalRoute' -import { boundaryCases, exampleCases, goldenCases, ORTHO_EXAMPLES, type Case } from './routeCases' +import { boundaryCases, exampleCases, goldenCases, goldenExampleCases, ORTHO_EXAMPLES, type Case } from './routeCases' // docs/edge-routing.md §ER3 — the FROZEN route bytes. // @@ -123,9 +123,13 @@ describe('orthogonalRoute — golden route bytes', () => { expect(corpus).toEqual(Object.keys(fixture.cases).sort()) }) + // the inputs are the examples with the routing data added since the fixture + // was frozen projected out (`WAYPOINTS_ADDED_SINCE_GOLDEN`: #325 PR 3's one + // gacha waypoint), so the e763eeb bytes still hold; the path WITH it is + // pinned in the describe below for (const graph of ORTHO_EXAMPLES) { - it(`${graph} — every orthogonal edge routes byte-identically`, { timeout: 60_000 }, () => { - const { missing, mismatched } = checkGroup(exampleCases(graph)) + it(`${graph} — every orthogonal edge routes byte-identically (waypoints added since e763eeb projected out)`, { timeout: 60_000 }, () => { + const { missing, mismatched } = checkGroup(goldenExampleCases(graph)) expect({ missing, mismatched: mismatched.slice(0, 5) }).toEqual({ missing: [], mismatched: [] }) expect(mismatched.length).toBe(0) }) @@ -144,3 +148,18 @@ describe('orthogonalRoute — golden route bytes', () => { expect(invalid.length).toBeGreaterThan(0) // §ER4 invalid-waypoint path is pinned too }) }) + +describe('orthogonalRoute — routing added since the golden', () => { + it('gacha e_pickup_36 with its #325 PR 3 waypoint (the ko `+1` label overlap fix) leaves its bottom port downward into the band under the second row', () => { + const c = exampleCases('gacha-banner-zones').find((x) => x.name === 'gacha-banner-zones#e_pickup_36')! + expect(c.input.waypoints).toEqual([{ x: 3200, y: 650 }]) + expect(snap(computeOrthogonalRoute(c.input))).toEqual({ + d: 'M 3829.5 634 L 3829.5 644 Q 3829.5 650 3823.5 650 L 2546 650 Q 2540 650 2540 644 L 2540 470', + hitD: 'M 3829.5 634 L 3829.5 650 L 2540 650 L 2540 470', + routeClass: 'orthogonal', + mid: [3102.75, 650], + endAngle: -1.5707963267948966, + invalidWaypoint: false, + }) + }) +}) diff --git a/test/routeCases.ts b/test/routeCases.ts index d27ede28..b4f6e101 100644 --- a/test/routeCases.ts +++ b/test/routeCases.ts @@ -22,8 +22,12 @@ import { Position } from '@xyflow/react' import { readFileSync } from 'node:fs' import type { Box, Pt, RouteInput } from '../src/components/edges/orthogonalRoute' +/** must match routeMap's size for a node not measured yet (130 x + * `BASE_NODE_H`, the compact floor of #325 PR 3); duplicated like PAD so a + * change to it is visible here. Every node of every example has a measured + * size in the fixture, so neither value reaches the golden routes. */ const DEFAULT_W = 130 -const DEFAULT_H = 64 +const DEFAULT_H = 56 /** must match ROUTE_PAD in the router; duplicated so a change to it is visible here */ const PAD = 12 @@ -133,6 +137,29 @@ export function exampleCases(graph: (typeof ORTHO_EXAMPLES)[number]): Case[] { return inputsFromDoc(graph, doc, MEASURED[graph] ?? {}) } +/** Routing data an example gained AFTER the golden fixture was frozen at + * e763eeb, by edge. #325 PR 3 (compact nodes): the gacha Template's layout + * round 7 gave `e_pickup_36` one waypoint so its `+1` label no longer overlaps + * `e_pickup_34`'s in ko on the shorter nodes. The router did not change, so + * the golden keeps its e763eeb bytes: its inputs project exactly this waypoint + * out, and the routed path WITH it is pinned as its own case. */ +export const WAYPOINTS_ADDED_SINCE_GOLDEN: Partial>> = { + 'gacha-banner-zones': { e_pickup_36: [{ x: 3200, y: 650 }] }, +} + +/** `exampleCases` with the waypoints added since the golden projected out — + * each one only if it is exactly the listed value, else this throws */ +export function goldenExampleCases(graph: (typeof ORTHO_EXAMPLES)[number]): Case[] { + const doc = readJson(`../examples/${graph}.json`) + for (const [edgeId, added] of Object.entries(WAYPOINTS_ADDED_SINCE_GOLDEN[graph] ?? {})) { + const edge = doc.edges.find((e) => e.id === edgeId) + if (JSON.stringify(edge?.data?.waypoints) !== JSON.stringify(added)) + throw new Error(`${graph}#${edgeId}: expected the waypoint added since the golden, ${JSON.stringify(added)}, found ${JSON.stringify(edge?.data?.waypoints)}`) + delete edge!.data!.waypoints + } + return inputsFromDoc(graph, doc, MEASURED[graph] ?? {}) +} + /** mmo-progression with EVERY edge forced orthogonal — 144 edges over 97 nodes, * the heaviest routing load the app can be put under (differential only). */ export function stressCases(): Case[] { @@ -423,5 +450,5 @@ export function latticeCases(seed: number, count: number): Case[] { /** every case the golden fixture pins: the real corpus + the boundaries. */ export function goldenCases(): Case[] { - return [...ORTHO_EXAMPLES.flatMap((g) => exampleCases(g)), ...boundaryCases()] + return [...ORTHO_EXAMPLES.flatMap((g) => goldenExampleCases(g)), ...boundaryCases()] }