@formicoidea/labre-framework-cynefin 0.32.0 → 0.33.0

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.
package/dist/actions.js CHANGED
@@ -4,6 +4,7 @@ import { Bound } from '@formicoidea/labre-core/global/gfx';
4
4
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
5
5
  import { REF_H as CYN_H, REF_W as CYN_W } from './cynefin/consts.js';
6
6
  import { HEX_FILL, HEX_SIZE, HEX_STROKE, HEX_VERTICES, REF_H as EST_H, REF_W as EST_W, } from './estuarine/consts.js';
7
+ import { ESTUARINE_ROLE } from './estuarine/roles.js';
7
8
  /**
8
9
  * Creation actions for the Cynefin / Estuarine toolbox — lifted out of
9
10
  * `toolbar/menu.ts` by PF3. This is also the framework that emitted NO
@@ -38,6 +39,10 @@ export function createEstuarineMap(std) {
38
39
  const { centerX, centerY } = gfx.viewport;
39
40
  const id = gfx.surface.addElement({
40
41
  type: 'estuarine',
42
+ // The map is this framework's ROOT INSTANCE: the role is what makes the
43
+ // Map quality checklist reachable on it (WS4). Cynefin's background gets
44
+ // none, on purpose — see `estuarine/roles.ts`.
45
+ role: ESTUARINE_ROLE.map,
41
46
  xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
42
47
  });
43
48
  finish(gfx, id);
@@ -49,6 +54,9 @@ export function createConstraintHexagon(std) {
49
54
  const { centerX: cx, centerY: cy } = gfx.viewport;
50
55
  const id = gfx.surface.addElement({
51
56
  type: 'shape',
57
+ // A hexagon is a plain polygon on the canvas: nothing about its geometry
58
+ // says "constraint". The role is the only place that does.
59
+ role: ESTUARINE_ROLE.constraint,
52
60
  shapeType: 'polygon',
53
61
  vertices: HEX_VERTICES,
54
62
  filled: true,
@@ -19,7 +19,11 @@ export const COLORS = {
19
19
  */
20
20
  export const DARK_BACK_PATHS = [
21
21
  // Main arc: top segment (Complex|Complicated) then left segment (Complex|Chaotic)
22
- ['M 550.1 17 A 296 296 0 0 1 338 328.5 A 448.7 448.7 0 0 1 26 331', 15.5, false],
22
+ [
23
+ 'M 550.1 17 A 296 296 0 0 1 338 328.5 A 448.7 448.7 0 0 1 26 331',
24
+ 15.5,
25
+ false,
26
+ ],
23
27
  // Thin "Confusion" arc sweeping down towards the cliff
24
28
  ['M 649 294 C 644 382, 588 462, 440 506', 5, false],
25
29
  ];
@@ -34,7 +38,11 @@ export const DARK_FRONT_PATHS = [
34
38
  // Thick descending branch with the bottom elbow (right edge of the cliff)
35
39
  ['M 340 332 C 390 440, 437 525, 472 632 Q 479 658, 453 700', 15.5, true],
36
40
  // Thin left line (left edge of the cliff)
37
- ['M 345 356 C 372 440, 408 540, 413 655 C 414 685, 412 710, 412 738', 4, false],
41
+ [
42
+ 'M 345 356 C 372 440, 408 540, 413 655 C 414 685, 412 710, 412 738',
43
+ 4,
44
+ false,
45
+ ],
38
46
  ];
39
47
  /** Cliff hatching: [x1,y1,x2,y2], lineWidth 3. */
40
48
  export const HATCHES = [
@@ -51,14 +59,30 @@ export const HATCHES = [
51
59
  ];
52
60
  /** Dashed Complicated↔Clear boundary, as oriented square pavings: [x,y,size,rotateDeg]. */
53
61
  export const DASH_RECTS = [
54
- [511, 245, 13.8, 206.8], [530, 255.5, 13.7, 205.2], [549.5, 265.5, 13.6, 203.5],
55
- [569.5, 274.5, 13.5, 201.9], [590, 282.5, 13.5, 200.2], [610.5, 289.5, 13.4, 198.6],
56
- [631, 296, 13.3, 196.9], [673.5, 307, 13.2, 193.6], [695, 311.5, 13.1, 192.0],
57
- [716, 315.5, 13.0, 190.3], [738, 319, 12.9, 188.7], [759.5, 322, 12.8, 187.0],
58
- [781, 324.5, 12.8, 185.4], [803, 325.5, 12.7, 183.7], [824.5, 326.5, 12.6, 182.1],
59
- [846, 327.5, 12.5, 180.5], [868, 327.5, 12.4, 178.8], [889.5, 326.5, 12.3, 177.2],
60
- [912, 325.5, 12.2, 175.5], [933, 323.5, 12.1, 173.9], [954.5, 321.5, 12.1, 172.2],
61
- [976, 318, 12.0, 170.6], [998, 314, 11.9, 168.9], [1019, 310, 11.8, 167.3],
62
+ [511, 245, 13.8, 206.8],
63
+ [530, 255.5, 13.7, 205.2],
64
+ [549.5, 265.5, 13.6, 203.5],
65
+ [569.5, 274.5, 13.5, 201.9],
66
+ [590, 282.5, 13.5, 200.2],
67
+ [610.5, 289.5, 13.4, 198.6],
68
+ [631, 296, 13.3, 196.9],
69
+ [673.5, 307, 13.2, 193.6],
70
+ [695, 311.5, 13.1, 192.0],
71
+ [716, 315.5, 13.0, 190.3],
72
+ [738, 319, 12.9, 188.7],
73
+ [759.5, 322, 12.8, 187.0],
74
+ [781, 324.5, 12.8, 185.4],
75
+ [803, 325.5, 12.7, 183.7],
76
+ [824.5, 326.5, 12.6, 182.1],
77
+ [846, 327.5, 12.5, 180.5],
78
+ [868, 327.5, 12.4, 178.8],
79
+ [889.5, 326.5, 12.3, 177.2],
80
+ [912, 325.5, 12.2, 175.5],
81
+ [933, 323.5, 12.1, 173.9],
82
+ [954.5, 321.5, 12.1, 172.2],
83
+ [976, 318, 12.0, 170.6],
84
+ [998, 314, 11.9, 168.9],
85
+ [1019, 310, 11.8, 167.3],
62
86
  ];
63
87
  export const DOMAINS = [
64
88
  {
@@ -82,7 +106,11 @@ export const DOMAINS = [
82
106
  lines: [
83
107
  { lead: 'Sense', rest: ' the context with analytical methods', y: 71 },
84
108
  { lead: 'Analyse', rest: ' observations', y: 90 },
85
- { lead: 'Respond', rest: ' by applying one of many good solutions', y: 109 },
109
+ {
110
+ lead: 'Respond',
111
+ rest: ' by applying one of many good solutions',
112
+ y: 109,
113
+ },
86
114
  ],
87
115
  },
88
116
  {
@@ -92,7 +120,11 @@ export const DOMAINS = [
92
120
  subheading: 'Un-ordered system',
93
121
  sy: 609,
94
122
  lines: [
95
- { lead: 'Act', rest: ' on the context to stabilize (it or yourself)', y: 627 },
123
+ {
124
+ lead: 'Act',
125
+ rest: ' on the context to stabilize (it or yourself)',
126
+ y: 627,
127
+ },
96
128
  { lead: 'Sense', rest: ' how the context reacts', y: 646 },
97
129
  { lead: 'Respond', rest: ' by re-acting', y: 665 },
98
130
  ],
@@ -106,7 +138,11 @@ export const DOMAINS = [
106
138
  lines: [
107
139
  { lead: 'Sense', rest: ' the context with analytical methods', y: 627 },
108
140
  { lead: 'Categorize', rest: ' observations', y: 646 },
109
- { lead: 'Respond', rest: ' by applying tried and true practices', y: 665 },
141
+ {
142
+ lead: 'Respond',
143
+ rest: ' by applying tried and true practices',
144
+ y: 665,
145
+ },
110
146
  ],
111
147
  },
112
148
  ];
@@ -13,7 +13,10 @@ export const cynefin = (model, ctx, matrix) => {
13
13
  const [, , w, h] = model.deserializedXYWH;
14
14
  const cx = w / 2;
15
15
  const cy = h / 2;
16
- ctx.setTransform(matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy));
16
+ ctx.setTransform(matrix
17
+ .translateSelf(cx, cy)
18
+ .rotateSelf(model.rotate)
19
+ .translateSelf(-cx, -cy));
17
20
  const { s, ox, oy } = refScale(w, h, REF_W, REF_H);
18
21
  ctx.translate(ox, oy);
19
22
  ctx.scale(s, s);
@@ -3,10 +3,55 @@ import { CynefinElementModel } from '@formicoidea/labre-core/model';
3
3
  import { ToolbarModuleExtension, } from '@formicoidea/labre-core/shared/services';
4
4
  import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
5
5
  import { html } from 'lit';
6
- const ResizeIcon = html `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H5v4M15 19h4v-4" /><path d="M5 5l6 6M19 19l-6-6" /></svg>`;
7
- const TitlesIcon = html `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7h14M9 7v11" /></svg>`;
8
- const DescIcon = html `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8h14M5 12h14M5 16h9" /></svg>`;
9
- const LiminalIcon = html `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 17 C9 6 15 6 19 7" /></svg>`;
6
+ const ResizeIcon = html `<svg
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ stroke-width="1.6"
13
+ stroke-linecap="round"
14
+ stroke-linejoin="round"
15
+ >
16
+ <path d="M9 5H5v4M15 19h4v-4" />
17
+ <path d="M5 5l6 6M19 19l-6-6" />
18
+ </svg>`;
19
+ const TitlesIcon = html `<svg
20
+ width="24"
21
+ height="24"
22
+ viewBox="0 0 24 24"
23
+ fill="none"
24
+ stroke="currentColor"
25
+ stroke-width="1.6"
26
+ stroke-linecap="round"
27
+ stroke-linejoin="round"
28
+ >
29
+ <path d="M5 7h14M9 7v11" />
30
+ </svg>`;
31
+ const DescIcon = html `<svg
32
+ width="24"
33
+ height="24"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ stroke-width="1.6"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ >
41
+ <path d="M5 8h14M5 12h14M5 16h9" />
42
+ </svg>`;
43
+ const LiminalIcon = html `<svg
44
+ width="24"
45
+ height="24"
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ stroke-width="1.6"
50
+ stroke-linecap="round"
51
+ stroke-linejoin="round"
52
+ >
53
+ <path d="M5 17 C9 6 15 6 19 7" />
54
+ </svg>`;
10
55
  function booleanToggle(id, tooltip, icon, prop) {
11
56
  return {
12
57
  id,
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * Visual constants for the Estuarine framework map, reproduced from the official
3
3
  * SVG (viewBox 0 0 690 801). All geometry is authored in that fixed reference
4
- * space and scaled uniformly to the element bounds by the renderer. The e axis
5
- * is vertical & double-headed (energy), the t axis horizontal & single-headed
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
6
9
  * (time only flows one way).
7
10
  */
8
11
  export declare const REF_W = 690;
@@ -33,7 +36,11 @@ export declare const T_AXIS: {
33
36
  };
34
37
  export declare const AXIS_WIDTH = 8;
35
38
  /** Filled arrowhead triangles: [[tipX,tipY],[baseAX,baseAY],[baseBX,baseBY]]. */
36
- export declare const ARROWHEADS: ReadonlyArray<readonly [readonly [number, number], readonly [number, number], readonly [number, number]]>;
39
+ export declare const ARROWHEADS: ReadonlyArray<readonly [
40
+ readonly [number, number],
41
+ readonly [number, number],
42
+ readonly [number, number]
43
+ ]>;
37
44
  /** Liminal: green boundary rising gently then dipping at the right end. */
38
45
  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";
39
46
  export declare const LIMINAL_WIDTH = 4.5;
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * Visual constants for the Estuarine framework map, reproduced from the official
3
3
  * SVG (viewBox 0 0 690 801). All geometry is authored in that fixed reference
4
- * space and scaled uniformly to the element bounds by the renderer. The e axis
5
- * is vertical & double-headed (energy), the t axis horizontal & single-headed
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
6
9
  * (time only flows one way).
7
10
  */
8
11
  export const REF_W = 690;
@@ -26,9 +29,21 @@ export const T_AXIS = { y: 649, x1: 28, x2: 616 };
26
29
  export const AXIS_WIDTH = 8;
27
30
  /** Filled arrowhead triangles: [[tipX,tipY],[baseAX,baseAY],[baseBX,baseBY]]. */
28
31
  export const ARROWHEADS = [
29
- [[43.5, 72], [30, 100], [57, 100]], // e — top
30
- [[43.5, 785], [30, 758], [57, 758]], // e — bottom
31
- [[643, 649], [613, 636], [613, 662]], // t — right
32
+ [
33
+ [43.5, 72],
34
+ [30, 100],
35
+ [57, 100],
36
+ ], // e — top
37
+ [
38
+ [43.5, 785],
39
+ [30, 758],
40
+ [57, 758],
41
+ ], // e — bottom
42
+ [
43
+ [643, 649],
44
+ [613, 636],
45
+ [613, 662],
46
+ ], // t — right
32
47
  ];
33
48
  /** Liminal: green boundary rising gently then dipping at the right end. */
34
49
  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';
@@ -41,9 +56,27 @@ export const VOLATILE_PATH = 'M 58 446 C 61 447, 70 451, 76 454 C 82 457, 88 462
41
56
  export const VOLATILE_WIDTH = 5;
42
57
  /** Uppercase legends: anchored centre, alphabetic baseline, with letter-spacing. */
43
58
  export const LABELS = {
44
- counterfactual: { text: 'COUNTER FACTUAL', x: 422, y: 25, size: 20, color: COLORS.label },
45
- liminal: { text: 'LIMINAL', x: 316, y: 192, size: 18, color: COLORS.liminalLabel },
46
- volatile: { text: 'VOLATILE', x: 219, y: 783, size: 20, color: COLORS.volatile },
59
+ counterfactual: {
60
+ text: 'COUNTER FACTUAL',
61
+ x: 422,
62
+ y: 25,
63
+ size: 20,
64
+ color: COLORS.label,
65
+ },
66
+ liminal: {
67
+ text: 'LIMINAL',
68
+ x: 316,
69
+ y: 192,
70
+ size: 18,
71
+ color: COLORS.liminalLabel,
72
+ },
73
+ volatile: {
74
+ text: 'VOLATILE',
75
+ x: 219,
76
+ y: 783,
77
+ size: 20,
78
+ color: COLORS.volatile,
79
+ },
47
80
  };
48
81
  /** Italic Georgia axis letters (left-anchored, alphabetic baseline). */
49
82
  export const AXIS_LABELS = {
@@ -1,11 +1,117 @@
1
1
  import { type ElementRenderer } from '@formicoidea/labre-core/blocks/surface';
2
2
  import type { EstuarineElementModel } from '@formicoidea/labre-core/model';
3
+ /**
4
+ * The three reference curves are drawn as a permanent GHOST (PO arbitration,
5
+ * 26/08/2026): dashed, translucent, never solid again.
6
+ *
7
+ * The reasoning is what an Estuarine line IS. Liminal, Volatile and
8
+ * Counter-factual are not measurements — they are boundaries a group argues
9
+ * itself into, and the printed map is a support for that argument, not a
10
+ * verdict about where the boundary lies. A solid stroke made the tool's own
11
+ * curve look like the answer, and the group's negotiated line (drawn on top,
12
+ * with a brush or a connector) look like an annotation on it. Dashing the
13
+ * reference inverts that: the tool suggests, the group states.
14
+ *
15
+ * The toggles keep their meaning exactly — ON shows the ghost, OFF hides it.
16
+ * What changed is only how ON looks, plus the ~600 ms reveal animation the
17
+ * moment a toggle flips (see `./ghost-overlay.ts`), which exists so a line
18
+ * that comes back at 45 % opacity is still SEEN arriving.
19
+ *
20
+ * The legends stay solid. A label is a name, not a boundary.
21
+ */
22
+ /** Dash pattern of the ghost, in reference-space units. */
23
+ export declare const GHOST_DASH: readonly number[];
24
+ /** Opacity of the permanent ghost. */
25
+ export declare const GHOST_ALPHA = 0.45;
26
+ /** Which `EstuarineElementModel` flag shows a given curve. */
27
+ export type EstuarineCurveVisibility = 'showLiminal' | 'showVolatile' | 'showCounterfactual';
28
+ export interface EstuarineCurve {
29
+ key: 'liminal' | 'volatile' | 'counterfactual';
30
+ path: Path2D;
31
+ color: string;
32
+ width: number;
33
+ visibleProp: EstuarineCurveVisibility;
34
+ }
35
+ export declare function estuarineCurves(): readonly EstuarineCurve[];
36
+ /**
37
+ * How the fixed 690 × 801 reference design maps onto an element of `w × h`.
38
+ *
39
+ * ## Why an Estuarine map STRETCHES (PO recette, 26/08/2026)
40
+ *
41
+ * It used to be fitted uniformly and letterboxed (`refScale`), so widening the
42
+ * background left the drawing at its authored proportions, centred, with short
43
+ * axes floating in empty margins. That is wrong for THIS frame, and the reason
44
+ * is what an Estuarine map is: a coordinate system. The e axis measures energy
45
+ * to change, the t axis measures time, and the three curves are the boundaries
46
+ * a group negotiates ACROSS that plane. Give the user more room and they mean
47
+ * "more plane" — a longer time axis, a taller energy axis — not "the same
48
+ * picture, bigger". So both directions follow the element independently.
49
+ *
50
+ * **Cynefin, right next door, deliberately keeps the uniform letterbox**: its
51
+ * background is a figurative drawing — a cliff, hand-drawn arcs, a hatched
52
+ * fall — and a figurative drawing has proportions that stretching would simply
53
+ * damage. Two frames, two answers, and the difference is not an oversight.
54
+ *
55
+ * ## The two factors
56
+ *
57
+ * `sx` / `sy` stretch positions and paths. `strokeScale` is the ISOTROPIC
58
+ * factor for everything that must not be deformed — stroke widths, arrowhead
59
+ * triangles, font sizes, letter-spacing — taken as the geometric mean of the
60
+ * two, the usual area-preserving stand-in for "one scale" when there are two.
61
+ *
62
+ * At the authored ratio `sx === sy`, so all three collapse to the single old
63
+ * factor and `ox`/`oy` were zero: a map that has not been stretched paints
64
+ * exactly the pixels it painted before this change.
65
+ */
66
+ export interface EstuarineFit {
67
+ /** Reference x → element x. */
68
+ sx: number;
69
+ /** Reference y → element y. */
70
+ sy: number;
71
+ /** The one undeformed factor: widths, arrowheads, type. */
72
+ strokeScale: number;
73
+ /**
74
+ * `lineWidth` correction for a path stroked INSIDE the stretched space.
75
+ *
76
+ * Canvas transforms the pen along with the path, so under `scale(sx, sy)` a
77
+ * nominal width `L` paints somewhere between `L·sx` and `L·sy` depending on
78
+ * the direction of the segment. There is no exact single number for a curve
79
+ * that runs in every direction; we approximate the pen's effective widening
80
+ * by the ARITHMETIC mean of the two factors and divide it out, which lands
81
+ * the ghost on {@link strokeScale} on average and keeps it within the
82
+ * sx/sy spread everywhere else. Exactly `1` at the authored ratio.
83
+ */
84
+ curveLineScale: number;
85
+ }
86
+ export declare function estuarineFit(w: number, h: number): EstuarineFit;
87
+ /**
88
+ * Put `ctx` into the map's STRETCHED reference space, where the authored
89
+ * geometry (a `Path2D` built from `./consts.ts`) lands on the element's real
90
+ * bounds in both directions.
91
+ *
92
+ * The single source of truth for that transform, and it has to stay single:
93
+ * the ghost overlay paints the very same curves one layer above and must land
94
+ * on them to the pixel. Everything that must NOT be stretched — axes,
95
+ * arrowheads, legends — is drawn outside it, in element coordinates, from the
96
+ * same {@link EstuarineFit}.
97
+ */
98
+ export declare function applyEstuarineTransform(ctx: CanvasRenderingContext2D, w: number, h: number): EstuarineFit;
3
99
  /**
4
100
  * Canvas renderer for the Estuarine framework map — reproduces the official SVG:
5
101
  * the e (vertical, double-headed) / t (horizontal, single-headed) axes and the
6
102
  * three reference curves (Liminal / Volatile / Counter-factual), each with its
7
- * legend and individually hideable. Drawn in the fixed reference space and
8
- * scaled uniformly to the element bounds.
103
+ * legend and individually hideable.
104
+ *
105
+ * Two spaces, and which one a mark belongs to is the whole design:
106
+ *
107
+ * - **Element coordinates**, entered by mapping each authored coordinate
108
+ * through {@link EstuarineFit} by hand (`ax` / `ay` below): the axes, their
109
+ * arrowheads and every word. Their POSITION follows the stretch — an axis
110
+ * ends where the map now ends — while their SHAPE does not, because a
111
+ * stretched arrowhead or a squashed letter is a defect, never a feature.
112
+ * - **Stretched reference space**, entered by {@link applyEstuarineTransform}:
113
+ * the three curves, which are boundaries across the plane and must cover
114
+ * whatever plane the user has made.
9
115
  */
10
116
  export declare const estuarine: ElementRenderer<EstuarineElementModel>;
11
117
  export declare const EstuarineRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
@@ -1,83 +1,186 @@
1
1
  import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
- import { FONT_FAMILY, refScale } from '../utils.js';
2
+ import { FONT_FAMILY } from '../utils.js';
3
3
  import { ARROWHEADS, AXIS_LABELS, AXIS_WIDTH, COLORS, COUNTERFACTUAL_PATH, COUNTERFACTUAL_WIDTH, E_AXIS, LABEL_LETTER_SPACING, LABELS, LIMINAL_PATH, LIMINAL_WIDTH, REF_H, REF_W, T_AXIS, VOLATILE_PATH, VOLATILE_WIDTH, } from './consts.js';
4
+ /**
5
+ * The three reference curves are drawn as a permanent GHOST (PO arbitration,
6
+ * 26/08/2026): dashed, translucent, never solid again.
7
+ *
8
+ * The reasoning is what an Estuarine line IS. Liminal, Volatile and
9
+ * Counter-factual are not measurements — they are boundaries a group argues
10
+ * itself into, and the printed map is a support for that argument, not a
11
+ * verdict about where the boundary lies. A solid stroke made the tool's own
12
+ * curve look like the answer, and the group's negotiated line (drawn on top,
13
+ * with a brush or a connector) look like an annotation on it. Dashing the
14
+ * reference inverts that: the tool suggests, the group states.
15
+ *
16
+ * The toggles keep their meaning exactly — ON shows the ghost, OFF hides it.
17
+ * What changed is only how ON looks, plus the ~600 ms reveal animation the
18
+ * moment a toggle flips (see `./ghost-overlay.ts`), which exists so a line
19
+ * that comes back at 45 % opacity is still SEEN arriving.
20
+ *
21
+ * The legends stay solid. A label is a name, not a boundary.
22
+ */
23
+ /** Dash pattern of the ghost, in reference-space units. */
24
+ export const GHOST_DASH = [12, 10];
25
+ /** Opacity of the permanent ghost. */
26
+ export const GHOST_ALPHA = 0.45;
27
+ /**
28
+ * The three curves, with their `Path2D` built ONCE.
29
+ *
30
+ * A lazy memo (`??=`) rather than a module-level constant, and that is not a
31
+ * micro-optimisation: `Path2D` does not exist under Node, and `./consts.ts` —
32
+ * which this module imports — is pulled in by the unit specs. Building the
33
+ * paths at import time would make merely importing this framework's constants
34
+ * throw in every non-browser environment. Built on the first PAINT instead,
35
+ * which by definition happens on a canvas.
36
+ */
37
+ let _curves;
38
+ export function estuarineCurves() {
39
+ return (_curves ??= [
40
+ {
41
+ key: 'liminal',
42
+ path: new Path2D(LIMINAL_PATH),
43
+ color: COLORS.liminal,
44
+ width: LIMINAL_WIDTH,
45
+ visibleProp: 'showLiminal',
46
+ },
47
+ {
48
+ key: 'volatile',
49
+ path: new Path2D(VOLATILE_PATH),
50
+ color: COLORS.volatile,
51
+ width: VOLATILE_WIDTH,
52
+ visibleProp: 'showVolatile',
53
+ },
54
+ {
55
+ key: 'counterfactual',
56
+ path: new Path2D(COUNTERFACTUAL_PATH),
57
+ color: COLORS.counterfactual,
58
+ width: COUNTERFACTUAL_WIDTH,
59
+ visibleProp: 'showCounterfactual',
60
+ },
61
+ ]);
62
+ }
63
+ export function estuarineFit(w, h) {
64
+ const sx = w / REF_W;
65
+ const sy = h / REF_H;
66
+ const strokeScale = Math.sqrt(sx * sy);
67
+ return { sx, sy, strokeScale, curveLineScale: strokeScale / ((sx + sy) / 2) };
68
+ }
69
+ /**
70
+ * Put `ctx` into the map's STRETCHED reference space, where the authored
71
+ * geometry (a `Path2D` built from `./consts.ts`) lands on the element's real
72
+ * bounds in both directions.
73
+ *
74
+ * The single source of truth for that transform, and it has to stay single:
75
+ * the ghost overlay paints the very same curves one layer above and must land
76
+ * on them to the pixel. Everything that must NOT be stretched — axes,
77
+ * arrowheads, legends — is drawn outside it, in element coordinates, from the
78
+ * same {@link EstuarineFit}.
79
+ */
80
+ export function applyEstuarineTransform(ctx, w, h) {
81
+ const fit = estuarineFit(w, h);
82
+ ctx.scale(fit.sx, fit.sy);
83
+ return fit;
84
+ }
4
85
  /**
5
86
  * Canvas renderer for the Estuarine framework map — reproduces the official SVG:
6
87
  * the e (vertical, double-headed) / t (horizontal, single-headed) axes and the
7
88
  * three reference curves (Liminal / Volatile / Counter-factual), each with its
8
- * legend and individually hideable. Drawn in the fixed reference space and
9
- * scaled uniformly to the element bounds.
89
+ * legend and individually hideable.
90
+ *
91
+ * Two spaces, and which one a mark belongs to is the whole design:
92
+ *
93
+ * - **Element coordinates**, entered by mapping each authored coordinate
94
+ * through {@link EstuarineFit} by hand (`ax` / `ay` below): the axes, their
95
+ * arrowheads and every word. Their POSITION follows the stretch — an axis
96
+ * ends where the map now ends — while their SHAPE does not, because a
97
+ * stretched arrowhead or a squashed letter is a defect, never a feature.
98
+ * - **Stretched reference space**, entered by {@link applyEstuarineTransform}:
99
+ * the three curves, which are boundaries across the plane and must cover
100
+ * whatever plane the user has made.
10
101
  */
11
102
  export const estuarine = (model, ctx, matrix) => {
12
103
  const [, , w, h] = model.deserializedXYWH;
13
104
  const cx = w / 2;
14
105
  const cy = h / 2;
15
- ctx.setTransform(matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy));
16
- const { s, ox, oy } = refScale(w, h, REF_W, REF_H);
17
- ctx.translate(ox, oy);
18
- ctx.scale(s, s);
106
+ ctx.setTransform(matrix
107
+ .translateSelf(cx, cy)
108
+ .rotateSelf(model.rotate)
109
+ .translateSelf(-cx, -cy));
110
+ const fit = estuarineFit(w, h);
111
+ /** Authored x → element x. Proportional: `43.5 / 690` of the real width. */
112
+ const ax = (x) => x * fit.sx;
113
+ /** Authored y → element y. */
114
+ const ay = (y) => y * fit.sy;
19
115
  ctx.lineCap = 'round';
20
116
  ctx.lineJoin = 'round';
21
117
  // ── Axes ────────────────────────────────────────────────────────────
118
+ // Drawn in element coordinates so the e axis spans the real height and the
119
+ // t axis the real width, while `lineWidth` stays one honest thickness
120
+ // instead of being fattened in whichever direction the map was pulled.
22
121
  ctx.strokeStyle = COLORS.axis;
23
122
  ctx.fillStyle = COLORS.axis;
24
- ctx.lineWidth = AXIS_WIDTH;
123
+ ctx.lineWidth = AXIS_WIDTH * fit.strokeScale;
25
124
  ctx.beginPath();
26
- ctx.moveTo(E_AXIS.x, E_AXIS.y1);
27
- ctx.lineTo(E_AXIS.x, E_AXIS.y2);
28
- ctx.moveTo(T_AXIS.x1, T_AXIS.y);
29
- ctx.lineTo(T_AXIS.x2, T_AXIS.y);
125
+ ctx.moveTo(ax(E_AXIS.x), ay(E_AXIS.y1));
126
+ ctx.lineTo(ax(E_AXIS.x), ay(E_AXIS.y2));
127
+ ctx.moveTo(ax(T_AXIS.x1), ay(T_AXIS.y));
128
+ ctx.lineTo(ax(T_AXIS.x2), ay(T_AXIS.y));
30
129
  ctx.stroke();
31
- for (const [[tx, ty], [ax, ay], [bx, by]] of ARROWHEADS) {
130
+ // Each head is pinned by its TIP — which travels to the real end of its axis
131
+ // — and then built from the authored offsets at the isotropic scale, so the
132
+ // triangle keeps its shape at any aspect ratio.
133
+ for (const [[tx, ty], [px, py], [qx, qy]] of ARROWHEADS) {
134
+ const tipX = ax(tx);
135
+ const tipY = ay(ty);
136
+ const k = fit.strokeScale;
32
137
  ctx.beginPath();
33
- ctx.moveTo(tx, ty);
34
- ctx.lineTo(ax, ay);
35
- ctx.lineTo(bx, by);
138
+ ctx.moveTo(tipX, tipY);
139
+ ctx.lineTo(tipX + (px - tx) * k, tipY + (py - ty) * k);
140
+ ctx.lineTo(tipX + (qx - tx) * k, tipY + (qy - ty) * k);
36
141
  ctx.closePath();
37
142
  ctx.fill();
38
143
  }
39
144
  // Uppercase legend (centre-anchored, alphabetic baseline, letter-spaced).
145
+ // Anchored proportionally, typed isotropically — never inside the stretch.
40
146
  const hasSpacing = 'letterSpacing' in ctx;
41
147
  const legend = (l) => {
42
148
  ctx.fillStyle = l.color;
43
- ctx.font = `600 ${l.size}px ${FONT_FAMILY}`;
149
+ ctx.font = `600 ${l.size * fit.strokeScale}px ${FONT_FAMILY}`;
44
150
  ctx.textAlign = 'center';
45
151
  ctx.textBaseline = 'alphabetic';
46
- if (hasSpacing)
47
- ctx.letterSpacing = `${LABEL_LETTER_SPACING}px`;
48
- ctx.fillText(l.text, l.x, l.y);
152
+ if (hasSpacing) {
153
+ ctx.letterSpacing = `${LABEL_LETTER_SPACING * fit.strokeScale}px`;
154
+ }
155
+ ctx.fillText(l.text, ax(l.x), ay(l.y));
49
156
  if (hasSpacing)
50
157
  ctx.letterSpacing = '0px';
51
158
  };
52
- // ── Liminal (green) ─────────────────────────────────────────────────
53
- if (model.showLiminal) {
54
- ctx.strokeStyle = COLORS.liminal;
55
- ctx.lineWidth = LIMINAL_WIDTH;
56
- ctx.stroke(new Path2D(LIMINAL_PATH));
57
- legend(LABELS.liminal);
58
- }
59
- // ── Volatile (red) ──────────────────────────────────────────────────
60
- if (model.showVolatile) {
61
- ctx.strokeStyle = COLORS.volatile;
62
- ctx.lineWidth = VOLATILE_WIDTH;
63
- ctx.stroke(new Path2D(VOLATILE_PATH));
64
- legend(LABELS.volatile);
65
- }
66
- // ── Counter-factual (dark) ──────────────────────────────────────────
67
- if (model.showCounterfactual) {
68
- ctx.strokeStyle = COLORS.counterfactual;
69
- ctx.lineWidth = COUNTERFACTUAL_WIDTH;
70
- ctx.stroke(new Path2D(COUNTERFACTUAL_PATH));
71
- legend(LABELS.counterfactual);
159
+ // ── The three curves, as ghosts ─────────────────────────────────────
160
+ // `save`/`restore` around each one so neither the stretch nor the dash nor
161
+ // the alpha leaks onto the legend that follows it — a legend is a name, and
162
+ // names stay solid, upright and undeformed.
163
+ for (const curve of estuarineCurves()) {
164
+ if (!model[curve.visibleProp])
165
+ continue;
166
+ ctx.save();
167
+ applyEstuarineTransform(ctx, w, h);
168
+ ctx.strokeStyle = curve.color;
169
+ ctx.lineWidth = curve.width * fit.curveLineScale;
170
+ ctx.globalAlpha = GHOST_ALPHA;
171
+ ctx.setLineDash([...GHOST_DASH]);
172
+ ctx.stroke(curve.path);
173
+ ctx.restore();
174
+ legend(LABELS[curve.key]);
72
175
  }
73
176
  // ── Italic e / t axis letters ───────────────────────────────────────
74
177
  if (model.showAxisLabels) {
75
178
  ctx.fillStyle = COLORS.axisLabel;
76
- ctx.font = `italic 700 ${AXIS_LABELS.size}px Georgia, serif`;
179
+ ctx.font = `italic 700 ${AXIS_LABELS.size * fit.strokeScale}px Georgia, serif`;
77
180
  ctx.textAlign = 'left';
78
181
  ctx.textBaseline = 'alphabetic';
79
- ctx.fillText(AXIS_LABELS.e.text, AXIS_LABELS.e.x, AXIS_LABELS.e.y);
80
- ctx.fillText(AXIS_LABELS.t.text, AXIS_LABELS.t.x, AXIS_LABELS.t.y);
182
+ ctx.fillText(AXIS_LABELS.e.text, ax(AXIS_LABELS.e.x), ay(AXIS_LABELS.e.y));
183
+ ctx.fillText(AXIS_LABELS.t.text, ax(AXIS_LABELS.t.x), ay(AXIS_LABELS.t.y));
81
184
  }
82
185
  };
83
186
  export const EstuarineRendererExtension = ElementRendererExtension('estuarine', estuarine);