@formicoidea/labre-framework-cynefin 0.41.0 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,77 @@
1
- import { GfxElementModelView, GfxViewInteractionExtension, } from '@formicoidea/labre-core/std/gfx';
1
+ import { FrameworkBackgroundView, hitTestBackgroundLabel, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { translateKey, } from '@formicoidea/labre-core/shared/services';
3
+ import { GfxViewInteractionExtension } from '@formicoidea/labre-core/std/gfx';
4
+ import { cynefinCroppedXYWH } from './crop.js';
5
+ import { cynefinLabelHits } from './labels.js';
2
6
  /**
3
7
  * View for the Cynefin background. Registering it ensures `gfx.view.get(model)`
4
8
  * returns a view (required so move / select interactions work).
9
+ *
10
+ * Extends {@link FrameworkBackgroundView} rather than
11
+ * `DeclaredBackgroundView`: this diagram is a figurative reproduction of the
12
+ * official SVG and has no `FrameworkBackgroundDef` behind it, so it says where
13
+ * its labels are in its own reference space. The gesture itself — where a
14
+ * double-click is allowed to land, the in-place `<input>`, the commit that
15
+ * refuses to write an untouched value — is the shared one (issue #355).
5
16
  */
6
- export class CynefinView extends GfxElementModelView {
17
+ export class CynefinView extends FrameworkBackgroundView {
7
18
  static { this.type = 'cynefin'; }
19
+ labelAt(lx, ly, w, h) {
20
+ const translate = (wording) => translateKey(this.gfx.std, ...wording);
21
+ const hit = hitTestBackgroundLabel(cynefinLabelHits(this.model, w, h, translate), lx, ly);
22
+ return hit ? { prop: hit.prop, text: hit.text } : null;
23
+ }
8
24
  }
9
25
  /**
10
- * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
11
- * true (toggled from the toolbar). Moving / selecting stays available.
26
+ * Resize gating and, when the handle is let go, the crop.
27
+ *
28
+ * ## Gating
29
+ *
30
+ * The resize handles are hidden unless `model.resizeEnabled` is true (toggled
31
+ * from the toolbar). Moving / selecting stays available.
32
+ *
33
+ * ## The crop
34
+ *
35
+ * The diagram is a fixed drawing fitted uniformly into the element, so dragged
36
+ * off its proportion the element letterboxes: 400 model units of nothing either
37
+ * side at 1600 × 600. `onResizeEnd` brings the frame back onto the picture (see
38
+ * `./crop.ts`) — the drawing is neither deformed nor truncated, the element
39
+ * simply stops claiming room it does not paint.
40
+ *
41
+ * ## Why here, and why one undo step
42
+ *
43
+ * A resize STASHES `xywh` on the way in, writes the model on every move, and
44
+ * commits the stash in `onResizeEnd`, inside the manager's own
45
+ * `store.transact` (`std/gfx/interactivity/manager.ts`). Writing the cropped
46
+ * box BEFORE calling `default` therefore changes what is committed rather than
47
+ * adding a second write: the whole gesture stays one Yjs write, and one undo
48
+ * step. `captureSync` would do the opposite here — it would close the step and
49
+ * make the crop a second one.
50
+ *
51
+ * This seam is also what keeps the crop a GESTURE. A cascade watching `xywh`
52
+ * would fire on a peer's resize too and re-crop, on every screen, a frame
53
+ * nobody on that screen touched; and it would fire on a document being loaded.
54
+ * The readonly guard is belt and braces — `handleElementResize` already refuses
55
+ * to start in a readonly store — but it is the invariant, so it is stated.
12
56
  */
13
57
  export const CynefinInteraction = GfxViewInteractionExtension(CynefinView.type, {
14
- handleResize({ model }) {
58
+ handleResize({ std, model }) {
15
59
  return {
16
60
  beforeResize({ set }) {
17
61
  if (!model.resizeEnabled) {
18
62
  set({ allowedHandlers: [] });
19
63
  }
20
64
  },
65
+ onResizeEnd(context) {
66
+ if (!std.store.readonly) {
67
+ const cropped = cynefinCroppedXYWH(model.xywh);
68
+ // No write at all when the frame is already on the drawing: an
69
+ // undo entry for a gesture that changed nothing is a bug.
70
+ if (cropped)
71
+ model.xywh = cropped;
72
+ }
73
+ context.default(context);
74
+ },
21
75
  };
22
76
  },
23
77
  });
@@ -0,0 +1,25 @@
1
+ import type { BackgroundLabelHit } from '@formicoidea/labre-core/blocks/surface';
2
+ import type { CynefinElementModel } from '@formicoidea/labre-core/model';
3
+ import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
4
+ import type { DomainBlock } from './consts.js';
5
+ /** Resolves a wording against the host's catalogue, or falls back to English. */
6
+ export type CynefinWordingResolver = (wording: ChromeWording) => string;
7
+ /**
8
+ * What a domain heading SAYS: the user's own word, else the vocabulary.
9
+ *
10
+ * The very precedence `backgroundLabelText` applies to a declared background —
11
+ * user text wins over the catalogue, the catalogue over the shipped English —
12
+ * spelled out here because this diagram has no declaration to apply it for it.
13
+ * An empty string is a real answer (the user erased the word), which is why the
14
+ * test is against `undefined` and not against falsiness.
15
+ */
16
+ export declare function cynefinHeadingText(model: CynefinElementModel, block: DomainBlock, translate: CynefinWordingResolver): string;
17
+ /**
18
+ * Clickable boxes of the four domain headings, in ELEMENT-LOCAL coordinates.
19
+ *
20
+ * Built from the same reference positions and the same uniform letterbox fit
21
+ * the renderer draws them with, so the target tracks the word at any size, and
22
+ * empty while the headings are hidden — a label that is not painted may not be
23
+ * aimed at either.
24
+ */
25
+ export declare function cynefinLabelHits(model: CynefinElementModel, w: number, h: number, translate: CynefinWordingResolver): BackgroundLabelHit[];
@@ -0,0 +1,40 @@
1
+ import { backgroundTextHitBox } from '@formicoidea/labre-core/blocks/surface';
2
+ import { refScale } from '../utils.js';
3
+ import { DOMAINS, HEADING_SIZE, REF_H, REF_W } from './consts.js';
4
+ /**
5
+ * What a domain heading SAYS: the user's own word, else the vocabulary.
6
+ *
7
+ * The very precedence `backgroundLabelText` applies to a declared background —
8
+ * user text wins over the catalogue, the catalogue over the shipped English —
9
+ * spelled out here because this diagram has no declaration to apply it for it.
10
+ * An empty string is a real answer (the user erased the word), which is why the
11
+ * test is against `undefined` and not against falsiness.
12
+ */
13
+ export function cynefinHeadingText(model, block, translate) {
14
+ const own = model[block.prop];
15
+ return own === undefined || own === null
16
+ ? translate(block.heading)
17
+ : String(own);
18
+ }
19
+ /**
20
+ * Clickable boxes of the four domain headings, in ELEMENT-LOCAL coordinates.
21
+ *
22
+ * Built from the same reference positions and the same uniform letterbox fit
23
+ * the renderer draws them with, so the target tracks the word at any size, and
24
+ * empty while the headings are hidden — a label that is not painted may not be
25
+ * aimed at either.
26
+ */
27
+ export function cynefinLabelHits(model, w, h, translate) {
28
+ if (!model.showTitles)
29
+ return [];
30
+ const { s, ox, oy } = refScale(w, h, REF_W, REF_H);
31
+ return DOMAINS.map(block => {
32
+ const text = cynefinHeadingText(model, block, translate);
33
+ return {
34
+ id: block.prop,
35
+ prop: block.prop,
36
+ text,
37
+ ...backgroundTextHitBox(text, HEADING_SIZE * s, ox + block.x * s, oy + block.hy * s),
38
+ };
39
+ });
40
+ }
@@ -1,13 +1,14 @@
1
1
  import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
2
2
  /**
3
3
  * Visual constants for the Estuarine framework map, reproduced from the official
4
- * SVG (viewBox 0 0 690 801). All geometry is authored in that fixed reference
5
- * space; the renderer reads every coordinate below as a RATIO of it and maps it
6
- * onto the element's real width and height independently, so a stretched map
7
- * gets a longer time axis and a taller energy axis rather than the same drawing
8
- * letterboxed (see `EstuarineFit` in `./element-renderer.ts`). The e axis is
9
- * vertical & double-headed (energy), the t axis horizontal & single-headed
10
- * (time only flows one way).
4
+ * SVG (viewBox 0 0 690 801). All geometry is authored in that fixed space; the
5
+ * renderer reads every coordinate below as a RATIO of the CROPPED window onto
6
+ * it ({@link REF_X} … {@link REF_H}) and maps it onto the element's real width
7
+ * and height independently, so a stretched map gets a longer time axis and a
8
+ * taller energy axis rather than the same drawing letterboxed (see
9
+ * `EstuarineFit` in `./element-renderer.ts`). The e axis is vertical &
10
+ * double-headed (energy), the t axis horizontal & single-headed (time only
11
+ * flows one way).
11
12
  *
12
13
  * The three curve legends below (`LABELS`) carry a `wording`, resolved by
13
14
  * `element-renderer.ts` through the `CanvasRenderer` it is handed at paint
@@ -15,8 +16,40 @@ import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
15
16
  * italic `e` / `t` axis LETTERS (`AXIS_LABELS`) are notation, not words, and
16
17
  * are deliberately NOT keyed (PO decision).
17
18
  */
18
- export declare const REF_W = 690;
19
- export declare const REF_H = 801;
19
+ /**
20
+ * The CROPPED window onto the authored 690 × 801 SVG space — the rectangle the
21
+ * element's bounds are mapped onto.
22
+ *
23
+ * The SVG's own viewBox is loose around the drawing: the t axis stops at
24
+ * `x = 616 / 690` and the e axis at `y = 763 / 801`, so a board born on the raw
25
+ * viewBox carried dead space on every side (measured on the painted pixels at
26
+ * birth size: 16.6 left, 12.6 top, 35.5 right, 20.4 bottom, in model units).
27
+ * The board's own border is therefore nowhere near its drawing, which is both
28
+ * ugly and awkward — a background is grabbed BY that border
29
+ * (`framework-background/hit-test.ts`).
30
+ *
31
+ * So the reference box is the ink's bounding box grown by {@link REF_MARGIN},
32
+ * not the viewBox. `REF_X` / `REF_Y` is where that window starts in authored
33
+ * coordinates; every authored number in this file keeps its ORIGINAL value and
34
+ * the renderer subtracts the origin (`ax` / `ay`), so nothing about the drawing
35
+ * moves relative to anything else — only the frame around it tightens.
36
+ *
37
+ * `MAP_SCALE` (see `../presets.ts`) multiplies these, so a new board is born
38
+ * smaller while its drawing keeps exactly the size it had.
39
+ */
40
+ export declare const REF_X = 8;
41
+ export declare const REF_Y = 4.5;
42
+ export declare const REF_W = 659;
43
+ export declare const REF_H = 786.5;
44
+ /**
45
+ * Space left between the ink and the edge of the board, in reference units.
46
+ *
47
+ * Small but not zero: a stroke is centred on its path, a glyph is measured
48
+ * generously, and a board whose ink touched its own border would look clipped.
49
+ * ~6 units is a little under one axis width (8) — enough to read as a margin,
50
+ * little enough that the border is visibly AROUND the drawing.
51
+ */
52
+ export declare const REF_MARGIN = 6;
20
53
  export declare const COLORS: {
21
54
  readonly axis: "#941253";
22
55
  /** Italic e / t axis letters. */
@@ -42,12 +75,38 @@ export declare const T_AXIS: {
42
75
  readonly x2: 616;
43
76
  };
44
77
  export declare const AXIS_WIDTH = 8;
45
- /** Filled arrowhead triangles: [[tipX,tipY],[baseAX,baseAY],[baseBX,baseBY]]. */
46
- export declare const ARROWHEADS: ReadonlyArray<readonly [
47
- readonly [number, number],
48
- readonly [number, number],
49
- readonly [number, number]
50
- ]>;
78
+ /**
79
+ * A filled arrowhead, welded to the END OF ITS AXIS.
80
+ *
81
+ * Declared the way the shared primitive declares one (`drawAxis`,
82
+ * `blocks/surface/src/framework-background/renderer.ts`): the head has ONE
83
+ * proportional anchor — {@link at}, the point where its axis stops, which
84
+ * travels with the stretch — and every other number below is an offset from
85
+ * that anchor in FIXED units, painted at the isotropic `strokeScale`.
86
+ *
87
+ * That split is the whole point. The vertices used to be three absolute
88
+ * authored points, so a stretch moved the tip by `sx` while the renderer
89
+ * rebuilt the base from the tip at the isotropic factor: as soon as
90
+ * `sx ≠ strokeScale` the triangle tore away from the line it belongs to
91
+ * (a 29-unit gap at 1600 × 400, the head drowning in the stroke at 400 × 1600).
92
+ * Anchoring the head to the axis end instead keeps it soldered at every ratio,
93
+ * and reproduces the authored SVG exactly when `sx === sy` — which is what the
94
+ * numbers below were read off.
95
+ */
96
+ export interface EstuarineArrowhead {
97
+ /** Authored end of the axis this head is welded to — the ratio anchor. */
98
+ at: readonly [number, number];
99
+ /** Unit vector pointing OUT of the axis, towards the tip. */
100
+ dir: readonly [number, number];
101
+ /** How far past the axis end the tip sits, in fixed units. */
102
+ tip: number;
103
+ /** How far SHORT of the axis end the base sits: the weld's overlap. */
104
+ overlap: number;
105
+ /** Half the base's width, in fixed units. */
106
+ halfWidth: number;
107
+ }
108
+ /** The three heads: both ends of the e axis, the right end of the t axis. */
109
+ export declare const ARROWHEADS: readonly EstuarineArrowhead[];
51
110
  /** Liminal: green boundary rising gently then dipping at the right end. */
52
111
  export declare const LIMINAL_PATH = "M 63 193 C 67 192, 78 189, 85 188 C 92 187, 100 186, 107 185 C 114 184, 122 183, 129 183 C 136 183, 144 183, 151 183 C 158 183, 166 183, 173 183 C 180 183, 188 184, 195 185 C 202 186, 210 188, 217 189 C 224 190, 232 192, 239 194 C 246 196, 254 198, 261 201 C 268 204, 276 207, 283 210 C 290 213, 298 217, 305 220 C 312 223, 320 226, 327 230 C 334 234, 342 238, 349 242 C 356 246, 364 250, 371 255 C 378 260, 386 264, 393 269 C 400 274, 408 278, 415 283 C 422 288, 430 292, 437 297 C 444 302, 451 306, 458 310 C 465 314, 473 319, 480 323 C 487 327, 495 332, 502 335 C 509 338, 517 341, 524 343 C 531 345, 539 348, 546 349 C 553 350, 561 350, 568 350 C 575 350, 583 348, 590 346 C 597 344, 605 340, 612 336 C 619 332, 626 325, 633 319 C 640 313, 648 302, 651 298 C 654 294, 654 295, 654 294";
53
112
  export declare const LIMINAL_WIDTH = 4.5;
@@ -59,10 +118,22 @@ export declare const VOLATILE_PATH = "M 58 446 C 61 447, 70 451, 76 454 C 82 457
59
118
  export declare const VOLATILE_WIDTH = 5;
60
119
  /** Every {@link LABELS} wording, for `translations.ts`'s manifest. */
61
120
  export declare const ESTUARINE_CANVAS_WORDINGS: readonly ChromeWording[];
62
- /** Uppercase legends: anchored centre, alphabetic baseline, with letter-spacing. */
121
+ /**
122
+ * Uppercase legends: anchored centre, alphabetic baseline, with letter-spacing.
123
+ *
124
+ * `prop` names the model key a double-click renames the legend into (issue
125
+ * #355), and `visibleProp` the toggle that shows it — the legend is painted
126
+ * with its curve and hidden with it, so it is aimable exactly when it is drawn.
127
+ *
128
+ * The three legends and nothing else: the italic `e` / `t` axis letters are
129
+ * notation, not words (PO decision, and the reason they carry no i18n key
130
+ * either), so they are not renamable.
131
+ */
63
132
  export declare const LABELS: {
64
133
  readonly counterfactual: {
65
134
  readonly wording: ChromeWording;
135
+ readonly prop: "counterfactualLabel";
136
+ readonly visibleProp: "showCounterfactual";
66
137
  readonly x: 422;
67
138
  readonly y: 25;
68
139
  readonly size: 20;
@@ -70,6 +141,8 @@ export declare const LABELS: {
70
141
  };
71
142
  readonly liminal: {
72
143
  readonly wording: ChromeWording;
144
+ readonly prop: "liminalLabel";
145
+ readonly visibleProp: "showLiminal";
73
146
  readonly x: 316;
74
147
  readonly y: 192;
75
148
  readonly size: 18;
@@ -77,23 +150,41 @@ export declare const LABELS: {
77
150
  };
78
151
  readonly volatile: {
79
152
  readonly wording: ChromeWording;
153
+ readonly prop: "volatileLabel";
154
+ readonly visibleProp: "showVolatile";
80
155
  readonly x: 219;
81
156
  readonly y: 783;
82
157
  readonly size: 20;
83
158
  readonly color: "#e63322";
84
159
  };
85
160
  };
86
- /** Italic Georgia axis letters (left-anchored, alphabetic baseline). */
161
+ /**
162
+ * Italic Georgia axis letters (left-anchored, alphabetic baseline).
163
+ *
164
+ * Each letter NAMES an axis, so it is declared against that axis the way a
165
+ * declared background declares a word (`backgroundPoint`,
166
+ * `blocks/surface/src/framework-background/def.ts`): a proportional anchor
167
+ * (`at`, the end of the axis it names) plus a FIXED `dx`/`dy` gap, painted at
168
+ * the isotropic `strokeScale` like the glyph itself.
169
+ *
170
+ * They used to be absolute authored positions projected by `sx`/`sy` while the
171
+ * glyph was typed isotropically, so the gap between letter and axis grew or
172
+ * collapsed with the ratio — the `t` climbing onto its own axis at 1600 × 400,
173
+ * falling far below it at 400 × 1600. `at + d·strokeScale` is exactly the
174
+ * authored position when `sx === sy`, and a constant gap everywhere else.
175
+ */
87
176
  export declare const AXIS_LABELS: {
88
177
  readonly e: {
89
178
  readonly text: "e";
90
- readonly x: 14;
91
- readonly y: 138;
179
+ readonly at: readonly [43.5, 97];
180
+ readonly dx: -29.5;
181
+ readonly dy: 41;
92
182
  };
93
183
  readonly t: {
94
184
  readonly text: "t";
95
- readonly x: 580;
96
- readonly y: 685;
185
+ readonly at: readonly [616, 649];
186
+ readonly dx: -36;
187
+ readonly dy: 36;
97
188
  };
98
189
  readonly size: 34;
99
190
  };
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Visual constants for the Estuarine framework map, reproduced from the official
3
- * SVG (viewBox 0 0 690 801). All geometry is authored in that fixed reference
4
- * space; the renderer reads every coordinate below as a RATIO of it and maps it
5
- * onto the element's real width and height independently, so a stretched map
6
- * gets a longer time axis and a taller energy axis rather than the same drawing
7
- * letterboxed (see `EstuarineFit` in `./element-renderer.ts`). The e axis is
8
- * vertical & double-headed (energy), the t axis horizontal & single-headed
9
- * (time only flows one way).
3
+ * SVG (viewBox 0 0 690 801). All geometry is authored in that fixed space; the
4
+ * renderer reads every coordinate below as a RATIO of the CROPPED window onto
5
+ * it ({@link REF_X} … {@link REF_H}) and maps it onto the element's real width
6
+ * and height independently, so a stretched map gets a longer time axis and a
7
+ * taller energy axis rather than the same drawing letterboxed (see
8
+ * `EstuarineFit` in `./element-renderer.ts`). The e axis is vertical &
9
+ * double-headed (energy), the t axis horizontal & single-headed (time only
10
+ * flows one way).
10
11
  *
11
12
  * The three curve legends below (`LABELS`) carry a `wording`, resolved by
12
13
  * `element-renderer.ts` through the `CanvasRenderer` it is handed at paint
@@ -14,8 +15,40 @@
14
15
  * italic `e` / `t` axis LETTERS (`AXIS_LABELS`) are notation, not words, and
15
16
  * are deliberately NOT keyed (PO decision).
16
17
  */
17
- export const REF_W = 690;
18
- export const REF_H = 801;
18
+ /**
19
+ * The CROPPED window onto the authored 690 × 801 SVG space — the rectangle the
20
+ * element's bounds are mapped onto.
21
+ *
22
+ * The SVG's own viewBox is loose around the drawing: the t axis stops at
23
+ * `x = 616 / 690` and the e axis at `y = 763 / 801`, so a board born on the raw
24
+ * viewBox carried dead space on every side (measured on the painted pixels at
25
+ * birth size: 16.6 left, 12.6 top, 35.5 right, 20.4 bottom, in model units).
26
+ * The board's own border is therefore nowhere near its drawing, which is both
27
+ * ugly and awkward — a background is grabbed BY that border
28
+ * (`framework-background/hit-test.ts`).
29
+ *
30
+ * So the reference box is the ink's bounding box grown by {@link REF_MARGIN},
31
+ * not the viewBox. `REF_X` / `REF_Y` is where that window starts in authored
32
+ * coordinates; every authored number in this file keeps its ORIGINAL value and
33
+ * the renderer subtracts the origin (`ax` / `ay`), so nothing about the drawing
34
+ * moves relative to anything else — only the frame around it tightens.
35
+ *
36
+ * `MAP_SCALE` (see `../presets.ts`) multiplies these, so a new board is born
37
+ * smaller while its drawing keeps exactly the size it had.
38
+ */
39
+ export const REF_X = 8;
40
+ export const REF_Y = 4.5;
41
+ export const REF_W = 659;
42
+ export const REF_H = 786.5;
43
+ /**
44
+ * Space left between the ink and the edge of the board, in reference units.
45
+ *
46
+ * Small but not zero: a stroke is centred on its path, a glyph is measured
47
+ * generously, and a board whose ink touched its own border would look clipped.
48
+ * ~6 units is a little under one axis width (8) — enough to read as a margin,
49
+ * little enough that the border is visibly AROUND the drawing.
50
+ */
51
+ export const REF_MARGIN = 6;
19
52
  export const COLORS = {
20
53
  axis: '#941253',
21
54
  /** Italic e / t axis letters. */
@@ -33,23 +66,32 @@ export const E_AXIS = { x: 43.5, y1: 97, y2: 763 };
33
66
  /** t axis (horizontal, single-headed → right): y, left x, right x. */
34
67
  export const T_AXIS = { y: 649, x1: 28, x2: 616 };
35
68
  export const AXIS_WIDTH = 8;
36
- /** Filled arrowhead triangles: [[tipX,tipY],[baseAX,baseAY],[baseBX,baseBY]]. */
69
+ /** The three heads: both ends of the e axis, the right end of the t axis. */
37
70
  export const ARROWHEADS = [
38
- [
39
- [43.5, 72],
40
- [30, 100],
41
- [57, 100],
42
- ], // e — top
43
- [
44
- [43.5, 785],
45
- [30, 758],
46
- [57, 758],
47
- ], // e — bottom
48
- [
49
- [643, 649],
50
- [613, 636],
51
- [613, 662],
52
- ], // t — right
71
+ // e — top
72
+ {
73
+ at: [E_AXIS.x, E_AXIS.y1],
74
+ dir: [0, -1],
75
+ tip: 25,
76
+ overlap: 3,
77
+ halfWidth: 13.5,
78
+ },
79
+ // e — bottom
80
+ {
81
+ at: [E_AXIS.x, E_AXIS.y2],
82
+ dir: [0, 1],
83
+ tip: 22,
84
+ overlap: 5,
85
+ halfWidth: 13.5,
86
+ },
87
+ // t — right
88
+ {
89
+ at: [T_AXIS.x2, T_AXIS.y],
90
+ dir: [1, 0],
91
+ tip: 27,
92
+ overlap: 3,
93
+ halfWidth: 13,
94
+ },
53
95
  ];
54
96
  /** Liminal: green boundary rising gently then dipping at the right end. */
55
97
  export const LIMINAL_PATH = 'M 63 193 C 67 192, 78 189, 85 188 C 92 187, 100 186, 107 185 C 114 184, 122 183, 129 183 C 136 183, 144 183, 151 183 C 158 183, 166 183, 173 183 C 180 183, 188 184, 195 185 C 202 186, 210 188, 217 189 C 224 190, 232 192, 239 194 C 246 196, 254 198, 261 201 C 268 204, 276 207, 283 210 C 290 213, 298 217, 305 220 C 312 223, 320 226, 327 230 C 334 234, 342 238, 349 242 C 356 246, 364 250, 371 255 C 378 260, 386 264, 393 269 C 400 274, 408 278, 415 283 C 422 288, 430 292, 437 297 C 444 302, 451 306, 458 310 C 465 314, 473 319, 480 323 C 487 327, 495 332, 502 335 C 509 338, 517 341, 524 343 C 531 345, 539 348, 546 349 C 553 350, 561 350, 568 350 C 575 350, 583 348, 590 346 C 597 344, 605 340, 612 336 C 619 332, 626 325, 633 319 C 640 313, 648 302, 651 298 C 654 294, 654 295, 654 294';
@@ -78,10 +120,22 @@ export const ESTUARINE_CANVAS_WORDINGS = [
78
120
  LABEL_LIMINAL,
79
121
  LABEL_VOLATILE,
80
122
  ];
81
- /** Uppercase legends: anchored centre, alphabetic baseline, with letter-spacing. */
123
+ /**
124
+ * Uppercase legends: anchored centre, alphabetic baseline, with letter-spacing.
125
+ *
126
+ * `prop` names the model key a double-click renames the legend into (issue
127
+ * #355), and `visibleProp` the toggle that shows it — the legend is painted
128
+ * with its curve and hidden with it, so it is aimable exactly when it is drawn.
129
+ *
130
+ * The three legends and nothing else: the italic `e` / `t` axis letters are
131
+ * notation, not words (PO decision, and the reason they carry no i18n key
132
+ * either), so they are not renamable.
133
+ */
82
134
  export const LABELS = {
83
135
  counterfactual: {
84
136
  wording: LABEL_COUNTERFACTUAL,
137
+ prop: 'counterfactualLabel',
138
+ visibleProp: 'showCounterfactual',
85
139
  x: 422,
86
140
  y: 25,
87
141
  size: 20,
@@ -89,6 +143,8 @@ export const LABELS = {
89
143
  },
90
144
  liminal: {
91
145
  wording: LABEL_LIMINAL,
146
+ prop: 'liminalLabel',
147
+ visibleProp: 'showLiminal',
92
148
  x: 316,
93
149
  y: 192,
94
150
  size: 18,
@@ -96,16 +152,32 @@ export const LABELS = {
96
152
  },
97
153
  volatile: {
98
154
  wording: LABEL_VOLATILE,
155
+ prop: 'volatileLabel',
156
+ visibleProp: 'showVolatile',
99
157
  x: 219,
100
158
  y: 783,
101
159
  size: 20,
102
160
  color: COLORS.volatile,
103
161
  },
104
162
  };
105
- /** Italic Georgia axis letters (left-anchored, alphabetic baseline). */
163
+ /**
164
+ * Italic Georgia axis letters (left-anchored, alphabetic baseline).
165
+ *
166
+ * Each letter NAMES an axis, so it is declared against that axis the way a
167
+ * declared background declares a word (`backgroundPoint`,
168
+ * `blocks/surface/src/framework-background/def.ts`): a proportional anchor
169
+ * (`at`, the end of the axis it names) plus a FIXED `dx`/`dy` gap, painted at
170
+ * the isotropic `strokeScale` like the glyph itself.
171
+ *
172
+ * They used to be absolute authored positions projected by `sx`/`sy` while the
173
+ * glyph was typed isotropically, so the gap between letter and axis grew or
174
+ * collapsed with the ratio — the `t` climbing onto its own axis at 1600 × 400,
175
+ * falling far below it at 400 × 1600. `at + d·strokeScale` is exactly the
176
+ * authored position when `sx === sy`, and a constant gap everywhere else.
177
+ */
106
178
  export const AXIS_LABELS = {
107
- e: { text: 'e', x: 14, y: 138 },
108
- t: { text: 't', x: 580, y: 685 },
179
+ e: { text: 'e', at: [E_AXIS.x, E_AXIS.y1], dx: -29.5, dy: 41 },
180
+ t: { text: 't', at: [T_AXIS.x2, T_AXIS.y], dx: -36, dy: 36 },
109
181
  size: 34,
110
182
  };
111
183
  export const LABEL_LETTER_SPACING = 4;
@@ -109,6 +109,13 @@ export declare function applyEstuarineTransform(ctx: CanvasRenderingContext2D, w
109
109
  * arrowheads and every word. Their POSITION follows the stretch — an axis
110
110
  * ends where the map now ends — while their SHAPE does not, because a
111
111
  * stretched arrowhead or a squashed letter is a defect, never a feature.
112
+ *
113
+ * One rule keeps that split honest, and it is the shared primitive's own
114
+ * (`drawAxis` / `backgroundPoint`): **an anchor is a RATIO of the frame, and
115
+ * every offset from an anchor is in FIXED units.** A shape must never take
116
+ * its anchor from one and its offset from the other — that mix is exactly
117
+ * what tore the arrowheads off their axes and slid the `e` / `t` letters
118
+ * around on a stretched map.
112
119
  * - **Stretched reference space**, entered by {@link applyEstuarineTransform}:
113
120
  * the three curves, which are boundaries across the plane and must cover
114
121
  * whatever plane the user has made.