@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,7 +1,8 @@
1
- import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { ElementRendererExtension, taggedPath2D, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { translateKey, } from '@formicoidea/labre-core/shared/services';
3
3
  import { FONT_FAMILY } from '../utils.js';
4
- 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
+ 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, REF_X, REF_Y, T_AXIS, VOLATILE_PATH, VOLATILE_WIDTH, } from './consts.js';
5
+ import { estuarineLegendText } from './labels.js';
5
6
  /**
6
7
  * The three reference curves are drawn as a permanent GHOST (PO arbitration,
7
8
  * 26/08/2026): dashed, translucent, never solid again.
@@ -40,21 +41,21 @@ export function estuarineCurves() {
40
41
  return (_curves ??= [
41
42
  {
42
43
  key: 'liminal',
43
- path: new Path2D(LIMINAL_PATH),
44
+ path: taggedPath2D(LIMINAL_PATH),
44
45
  color: COLORS.liminal,
45
46
  width: LIMINAL_WIDTH,
46
47
  visibleProp: 'showLiminal',
47
48
  },
48
49
  {
49
50
  key: 'volatile',
50
- path: new Path2D(VOLATILE_PATH),
51
+ path: taggedPath2D(VOLATILE_PATH),
51
52
  color: COLORS.volatile,
52
53
  width: VOLATILE_WIDTH,
53
54
  visibleProp: 'showVolatile',
54
55
  },
55
56
  {
56
57
  key: 'counterfactual',
57
- path: new Path2D(COUNTERFACTUAL_PATH),
58
+ path: taggedPath2D(COUNTERFACTUAL_PATH),
58
59
  color: COLORS.counterfactual,
59
60
  width: COUNTERFACTUAL_WIDTH,
60
61
  visibleProp: 'showCounterfactual',
@@ -81,6 +82,10 @@ export function estuarineFit(w, h) {
81
82
  export function applyEstuarineTransform(ctx, w, h) {
82
83
  const fit = estuarineFit(w, h);
83
84
  ctx.scale(fit.sx, fit.sy);
85
+ // The crop: authored (REF_X, REF_Y) is the element's own origin. Applied
86
+ // INSIDE the scale so the translation is expressed in authored units, which
87
+ // is the space the `Path2D` this transform is meant for is written in.
88
+ ctx.translate(-REF_X, -REF_Y);
84
89
  return fit;
85
90
  }
86
91
  /**
@@ -96,6 +101,13 @@ export function applyEstuarineTransform(ctx, w, h) {
96
101
  * arrowheads and every word. Their POSITION follows the stretch — an axis
97
102
  * ends where the map now ends — while their SHAPE does not, because a
98
103
  * stretched arrowhead or a squashed letter is a defect, never a feature.
104
+ *
105
+ * One rule keeps that split honest, and it is the shared primitive's own
106
+ * (`drawAxis` / `backgroundPoint`): **an anchor is a RATIO of the frame, and
107
+ * every offset from an anchor is in FIXED units.** A shape must never take
108
+ * its anchor from one and its offset from the other — that mix is exactly
109
+ * what tore the arrowheads off their axes and slid the `e` / `t` letters
110
+ * around on a stretched map.
99
111
  * - **Stretched reference space**, entered by {@link applyEstuarineTransform}:
100
112
  * the three curves, which are boundaries across the plane and must cover
101
113
  * whatever plane the user has made.
@@ -110,10 +122,10 @@ export const estuarine = (model, ctx, matrix, renderer) => {
110
122
  .rotateSelf(model.rotate)
111
123
  .translateSelf(-cx, -cy));
112
124
  const fit = estuarineFit(w, h);
113
- /** Authored x → element x. Proportional: `43.5 / 690` of the real width. */
114
- const ax = (x) => x * fit.sx;
125
+ /** Authored x → element x, through the cropped window (see `./consts.ts`). */
126
+ const ax = (x) => (x - REF_X) * fit.sx;
115
127
  /** Authored y → element y. */
116
- const ay = (y) => y * fit.sy;
128
+ const ay = (y) => (y - REF_Y) * fit.sy;
117
129
  ctx.lineCap = 'round';
118
130
  ctx.lineJoin = 'round';
119
131
  // ── Axes ────────────────────────────────────────────────────────────
@@ -129,24 +141,37 @@ export const estuarine = (model, ctx, matrix, renderer) => {
129
141
  ctx.moveTo(ax(T_AXIS.x1), ay(T_AXIS.y));
130
142
  ctx.lineTo(ax(T_AXIS.x2), ay(T_AXIS.y));
131
143
  ctx.stroke();
132
- // Each head is pinned by its TIP — which travels to the real end of its axis
133
- // — and then built from the authored offsets at the isotropic scale, so the
134
- // triangle keeps its shape at any aspect ratio.
135
- for (const [[tx, ty], [px, py], [qx, qy]] of ARROWHEADS) {
136
- const tipX = ax(tx);
137
- const tipY = ay(ty);
138
- const k = fit.strokeScale;
144
+ // Each head is welded to the END OF ITS AXIS — the one proportional anchor —
145
+ // and built entirely from fixed offsets around that point, exactly as the
146
+ // shared primitive's `drawAxis` builds one. Anchor by ratio, offset by fixed
147
+ // units, never a mix of the two on the two ends of one shape: that is the
148
+ // rule the old formula broke by pinning the tip proportionally and then
149
+ // rebuilding the base from it at the isotropic scale.
150
+ const k = fit.strokeScale;
151
+ for (const head of ARROWHEADS) {
152
+ const [dx, dy] = head.dir;
153
+ // Across the axis, same fixed units — the base's half-width.
154
+ const [px, py] = [-dy, dx];
155
+ const endX = ax(head.at[0]);
156
+ const endY = ay(head.at[1]);
157
+ // The base sits SHORT of the axis end, so the stroke dies inside the
158
+ // triangle and the two never come apart however the map is pulled.
159
+ const baseX = endX - dx * head.overlap * k;
160
+ const baseY = endY - dy * head.overlap * k;
161
+ const spreadX = px * head.halfWidth * k;
162
+ const spreadY = py * head.halfWidth * k;
139
163
  ctx.beginPath();
140
- ctx.moveTo(tipX, tipY);
141
- ctx.lineTo(tipX + (px - tx) * k, tipY + (py - ty) * k);
142
- ctx.lineTo(tipX + (qx - tx) * k, tipY + (qy - ty) * k);
164
+ ctx.moveTo(endX + dx * head.tip * k, endY + dy * head.tip * k);
165
+ ctx.lineTo(baseX - spreadX, baseY - spreadY);
166
+ ctx.lineTo(baseX + spreadX, baseY + spreadY);
143
167
  ctx.closePath();
144
168
  ctx.fill();
145
169
  }
146
170
  // Uppercase legend (centre-anchored, alphabetic baseline, letter-spaced).
147
171
  // Anchored proportionally, typed isotropically — never inside the stretch.
148
172
  const hasSpacing = 'letterSpacing' in ctx;
149
- const legend = (l) => {
173
+ const legend = (key) => {
174
+ const l = LABELS[key];
150
175
  ctx.fillStyle = l.color;
151
176
  ctx.font = `600 ${l.size * fit.strokeScale}px ${FONT_FAMILY}`;
152
177
  ctx.textAlign = 'center';
@@ -154,7 +179,9 @@ export const estuarine = (model, ctx, matrix, renderer) => {
154
179
  if (hasSpacing) {
155
180
  ctx.letterSpacing = `${LABEL_LETTER_SPACING * fit.strokeScale}px`;
156
181
  }
157
- ctx.fillText(tr(l.wording), ax(l.x), ay(l.y));
182
+ // The user's own word when they have renamed this legend, the catalogue's
183
+ // otherwise — the same precedence a declared background applies.
184
+ ctx.fillText(estuarineLegendText(model, key, tr), ax(l.x), ay(l.y));
158
185
  if (hasSpacing)
159
186
  ctx.letterSpacing = '0px';
160
187
  };
@@ -173,16 +200,21 @@ export const estuarine = (model, ctx, matrix, renderer) => {
173
200
  ctx.setLineDash([...GHOST_DASH]);
174
201
  ctx.stroke(curve.path);
175
202
  ctx.restore();
176
- legend(LABELS[curve.key]);
203
+ legend(curve.key);
177
204
  }
178
205
  // ── Italic e / t axis letters ───────────────────────────────────────
206
+ // A letter names an axis, so it rides the END of that axis (proportional)
207
+ // at a FIXED gap from it — the same anchor-plus-offset a declared background
208
+ // writes as `backgroundPoint`. The glyph and the gap then scale together, so
209
+ // the letter sits the same distance from its axis at every ratio.
179
210
  if (model.showAxisLabels) {
180
211
  ctx.fillStyle = COLORS.axisLabel;
181
- ctx.font = `italic 700 ${AXIS_LABELS.size * fit.strokeScale}px Georgia, serif`;
212
+ ctx.font = `italic 700 ${AXIS_LABELS.size * k}px Georgia, serif`;
182
213
  ctx.textAlign = 'left';
183
214
  ctx.textBaseline = 'alphabetic';
184
- ctx.fillText(AXIS_LABELS.e.text, ax(AXIS_LABELS.e.x), ay(AXIS_LABELS.e.y));
185
- ctx.fillText(AXIS_LABELS.t.text, ax(AXIS_LABELS.t.x), ay(AXIS_LABELS.t.y));
215
+ for (const letter of [AXIS_LABELS.e, AXIS_LABELS.t]) {
216
+ ctx.fillText(letter.text, ax(letter.at[0]) + letter.dx * k, ay(letter.at[1]) + letter.dy * k);
217
+ }
186
218
  }
187
219
  };
188
220
  export const EstuarineRendererExtension = ElementRendererExtension('estuarine', estuarine);
@@ -1,11 +1,19 @@
1
+ import type { EditableBackgroundLabel } from '@formicoidea/labre-core/blocks/surface';
2
+ import { FrameworkBackgroundView } from '@formicoidea/labre-core/blocks/surface';
1
3
  import type { EstuarineElementModel } from '@formicoidea/labre-core/model';
2
- import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
3
4
  /**
4
5
  * View for the Estuarine background. Registering it ensures `gfx.view.get(model)`
5
6
  * returns a view (required so move / select interactions work).
7
+ *
8
+ * Extends {@link FrameworkBackgroundView} rather than
9
+ * `DeclaredBackgroundView`: the map is a reproduction of the official SVG with
10
+ * a stretch of its own and no `FrameworkBackgroundDef` behind it, so it says
11
+ * where its three legends are through the very fit the renderer anchors them
12
+ * with. The gesture itself is the shared one (issue #355).
6
13
  */
7
- export declare class EstuarineView extends GfxElementModelView<EstuarineElementModel> {
14
+ export declare class EstuarineView extends FrameworkBackgroundView<EstuarineElementModel> {
8
15
  static type: string;
16
+ protected labelAt(lx: number, ly: number, w: number, h: number): EditableBackgroundLabel | null;
9
17
  }
10
18
  /**
11
19
  * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
@@ -1,10 +1,25 @@
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 { estuarineFit } from './element-renderer.js';
5
+ import { estuarineLabelHits } from './labels.js';
2
6
  /**
3
7
  * View for the Estuarine 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`: the map is a reproduction of the official SVG with
12
+ * a stretch of its own and no `FrameworkBackgroundDef` behind it, so it says
13
+ * where its three legends are through the very fit the renderer anchors them
14
+ * with. The gesture itself is the shared one (issue #355).
5
15
  */
6
- export class EstuarineView extends GfxElementModelView {
16
+ export class EstuarineView extends FrameworkBackgroundView {
7
17
  static { this.type = 'estuarine'; }
18
+ labelAt(lx, ly, w, h) {
19
+ const translate = (wording) => translateKey(this.gfx.std, ...wording);
20
+ const hit = hitTestBackgroundLabel(estuarineLabelHits(this.model, estuarineFit(w, h), translate), lx, ly);
21
+ return hit ? { prop: hit.prop, text: hit.text } : null;
22
+ }
8
23
  }
9
24
  /**
10
25
  * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
@@ -0,0 +1,40 @@
1
+ import type { BackgroundLabelHit } from '@formicoidea/labre-core/blocks/surface';
2
+ import type { EstuarineElementModel } from '@formicoidea/labre-core/model';
3
+ import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
4
+ import { LABELS } from './consts.js';
5
+ /** Resolves a wording against the host's catalogue, or falls back to English. */
6
+ export type EstuarineWordingResolver = (wording: ChromeWording) => string;
7
+ /** The three curve legends, in the order the renderer paints them. */
8
+ export type EstuarineLegendKey = keyof typeof LABELS;
9
+ /**
10
+ * Only the factors the legends need out of `EstuarineFit`.
11
+ *
12
+ * Taken as a parameter rather than imported: the fit is computed in
13
+ * `./element-renderer.ts`, which reads this module to know what a legend says,
14
+ * and a module cycle to save two fields would be a poor trade.
15
+ */
16
+ export interface EstuarineLegendFit {
17
+ sx: number;
18
+ sy: number;
19
+ /** The one undeformed factor — type is never stretched. */
20
+ strokeScale: number;
21
+ }
22
+ /**
23
+ * What a legend SAYS: the user's own word, else the vocabulary.
24
+ *
25
+ * The very precedence `backgroundLabelText` applies to a declared background —
26
+ * user text wins over the catalogue, the catalogue over the shipped English —
27
+ * spelled out here because this map has no declaration to apply it for it. An
28
+ * empty string is a real answer (the user erased the word), which is why the
29
+ * test is against `undefined` and not against falsiness.
30
+ */
31
+ export declare function estuarineLegendText(model: EstuarineElementModel, key: EstuarineLegendKey, translate: EstuarineWordingResolver): string;
32
+ /**
33
+ * Clickable boxes of the visible legends, in ELEMENT-LOCAL coordinates.
34
+ *
35
+ * Anchored through the same fit the renderer anchors them with: proportionally
36
+ * in both directions — through the same cropped window (`REF_X` / `REF_Y`) —
37
+ * and typed isotropically. A legend whose curve is switched off is not painted
38
+ * and is therefore not aimable either.
39
+ */
40
+ export declare function estuarineLabelHits(model: EstuarineElementModel, fit: EstuarineLegendFit, translate: EstuarineWordingResolver): BackgroundLabelHit[];
@@ -0,0 +1,50 @@
1
+ import { backgroundTextHitBox } from '@formicoidea/labre-core/blocks/surface';
2
+ import { LABEL_LETTER_SPACING, LABELS, REF_X, REF_Y } from './consts.js';
3
+ /**
4
+ * What a legend SAYS: the user's own word, else the vocabulary.
5
+ *
6
+ * The very precedence `backgroundLabelText` applies to a declared background —
7
+ * user text wins over the catalogue, the catalogue over the shipped English —
8
+ * spelled out here because this map has no declaration to apply it for it. An
9
+ * empty string is a real answer (the user erased the word), which is why the
10
+ * test is against `undefined` and not against falsiness.
11
+ */
12
+ export function estuarineLegendText(model, key, translate) {
13
+ const label = LABELS[key];
14
+ const own = model[label.prop];
15
+ return own === undefined || own === null
16
+ ? translate(label.wording)
17
+ : String(own);
18
+ }
19
+ /**
20
+ * Clickable boxes of the visible legends, in ELEMENT-LOCAL coordinates.
21
+ *
22
+ * Anchored through the same fit the renderer anchors them with: proportionally
23
+ * in both directions — through the same cropped window (`REF_X` / `REF_Y`) —
24
+ * and typed isotropically. A legend whose curve is switched off is not painted
25
+ * and is therefore not aimable either.
26
+ */
27
+ export function estuarineLabelHits(model, fit, translate) {
28
+ const props = model;
29
+ const hits = [];
30
+ for (const key of Object.keys(LABELS)) {
31
+ const label = LABELS[key];
32
+ if (!props[label.visibleProp])
33
+ continue;
34
+ const text = estuarineLegendText(model, key, translate);
35
+ const box = backgroundTextHitBox(text, label.size * fit.strokeScale, (label.x - REF_X) * fit.sx, (label.y - REF_Y) * fit.sy, 'center');
36
+ // A legend is letter-spaced, so it is wider than the shared estimate: half
37
+ // of the extra width on each side, since the word is centred on its anchor.
38
+ const spread = (Math.max(text.length - 1, 0) * LABEL_LETTER_SPACING * fit.strokeScale) /
39
+ 2;
40
+ hits.push({
41
+ id: key,
42
+ prop: label.prop,
43
+ text,
44
+ ...box,
45
+ minX: box.minX - spread,
46
+ maxX: box.maxX + spread,
47
+ });
48
+ }
49
+ return hits;
50
+ }
package/dist/presets.d.ts CHANGED
@@ -8,9 +8,20 @@
8
8
  * the estuarine background was drawn at scale 1 while the button drew it at
9
9
  * 1.2. Derived from one table, neither can happen again.
10
10
  */
11
- /** Estuarine map default scale (REF aspect, scaled up so it reads on canvas). */
11
+ /**
12
+ * Estuarine map default scale: how many canvas units one reference unit of the
13
+ * drawing is worth when the map is born. The DRAWING's size, in other words —
14
+ * which is why it is this number, and not the birth box, that is the constant.
15
+ */
12
16
  export declare const MAP_SCALE = 1.2;
13
- /** The box an estuarine map is born in — the size the toolbox draws. */
17
+ /**
18
+ * The box an estuarine map is born in — the size the toolbox draws.
19
+ *
20
+ * Derived from the CROPPED reference box (`estuarine/consts.ts`), so the board
21
+ * is born just big enough to hold its drawing plus a thin margin. It shrank
22
+ * when the crop tightened (828 × 961.2 → 790.8 × 943.8) and the drawing inside
23
+ * it did not change size by a single unit — `MAP_SCALE` is still 1.2.
24
+ */
14
25
  export declare const ESTUARINE_MAP_W: number;
15
26
  export declare const ESTUARINE_MAP_H: number;
16
27
  /** The box a Cynefin frame is born in. */
package/dist/presets.js CHANGED
@@ -12,9 +12,20 @@ import { ESTUARINE_ROLE } from './estuarine/roles.js';
12
12
  * the estuarine background was drawn at scale 1 while the button drew it at
13
13
  * 1.2. Derived from one table, neither can happen again.
14
14
  */
15
- /** Estuarine map default scale (REF aspect, scaled up so it reads on canvas). */
15
+ /**
16
+ * Estuarine map default scale: how many canvas units one reference unit of the
17
+ * drawing is worth when the map is born. The DRAWING's size, in other words —
18
+ * which is why it is this number, and not the birth box, that is the constant.
19
+ */
16
20
  export const MAP_SCALE = 1.2;
17
- /** The box an estuarine map is born in — the size the toolbox draws. */
21
+ /**
22
+ * The box an estuarine map is born in — the size the toolbox draws.
23
+ *
24
+ * Derived from the CROPPED reference box (`estuarine/consts.ts`), so the board
25
+ * is born just big enough to hold its drawing plus a thin margin. It shrank
26
+ * when the crop tightened (828 × 961.2 → 790.8 × 943.8) and the drawing inside
27
+ * it did not change size by a single unit — `MAP_SCALE` is still 1.2.
28
+ */
18
29
  export const ESTUARINE_MAP_W = EST_W * MAP_SCALE;
19
30
  export const ESTUARINE_MAP_H = EST_H * MAP_SCALE;
20
31
  /** The box a Cynefin frame is born in. */
@@ -3,7 +3,7 @@ import { FontFamily, ShapeStyle, TextAlign } from '@formicoidea/labre-core/model
3
3
  import { NOTATION_NEUTRALS } from '@formicoidea/labre-core/shared/consts';
4
4
  import { translateKey, } from '@formicoidea/labre-core/shared/services';
5
5
  import { cynefinEstuarineCommands } from '../commands.js';
6
- import { HEX_SIZE } from '../estuarine/consts.js';
6
+ import { HEX_SIZE, REF_X, REF_Y } from '../estuarine/consts.js';
7
7
  import { CYNEFIN_H, CYNEFIN_W, cynefinBackgroundProps, ESTUARINE_MAP_H, ESTUARINE_MAP_W, estuarineHexagonBox, estuarineHexagonProps, estuarineMapProps, MAP_SCALE, } from '../presets.js';
8
8
  /**
9
9
  * The Cynefin / Estuarine palettes — DERIVED from the toolbox, one template per
@@ -12,8 +12,8 @@ import { CYNEFIN_H, CYNEFIN_W, cynefinBackgroundProps, ESTUARINE_MAP_H, ESTUARIN
12
12
  * Every single-artefact entry below is what its command actually draws, run
13
13
  * once against a recording surface. Hand-written, they had drifted exactly as
14
14
  * far as a copy drifts: the hexagons had lost the `textFitMode` #52 gave them,
15
- * and the estuarine background was drawn at scale 1 (690×801) where the button
16
- * draws it at 1.2 (828×961). Derived, neither can happen again — and
15
+ * and the estuarine background was drawn at scale 1 where the button draws it
16
+ * at `MAP_SCALE`. Derived, neither can happen again — and
17
17
  * `templates-parity.unit.spec.ts` re-runs each command and compares.
18
18
  *
19
19
  * The two COMPOSITIONS stay hand-written: a sorting board and a constraint map
@@ -47,12 +47,19 @@ function sticky(x, y, text) {
47
47
  };
48
48
  }
49
49
  /**
50
- * A constraint on the shipped map, placed by its CENTRE in the map's reference
51
- * space (690×801) — the frame the drawing was laid out in — carried onto the
52
- * box the map is actually born at.
50
+ * A constraint on the shipped map, placed by its CENTRE in the map's AUTHORED
51
+ * space (the 690×801 the drawing was laid out in), carried onto the box the map
52
+ * is actually born at.
53
+ *
54
+ * `- REF_X` / `- REF_Y` is the map's crop: the board's own origin is authored
55
+ * `(REF_X, REF_Y)`, not `(0, 0)`, so a hexagon placed on a curve stays on that
56
+ * curve. Derived, never restated — tightening the crop moves the drawing and
57
+ * the constraints on it together.
53
58
  */
59
+ const mapX = (cx) => (cx - REF_X) * MAP_SCALE;
60
+ const mapY = (cy) => (cy - REF_Y) * MAP_SCALE;
54
61
  const hexAt = (cx, cy) => estuarineHexagonProps({
55
- xywh: estuarineHexagonBox(cx * MAP_SCALE, cy * MAP_SCALE),
62
+ xywh: estuarineHexagonBox(mapX(cx), mapY(cy)),
56
63
  });
57
64
  const CAPTION_W = 120;
58
65
  const CAPTION_H = 24;
@@ -69,8 +76,8 @@ const CAPTION_GAP = 8;
69
76
  * Neutral, and staying so: a name is nobody's artefact.
70
77
  */
71
78
  function caption(cx, cy, str) {
72
- const x = cx * MAP_SCALE - CAPTION_W / 2;
73
- const y = cy * MAP_SCALE + HEX_SIZE / 2 + CAPTION_GAP;
79
+ const x = mapX(cx) - CAPTION_W / 2;
80
+ const y = mapY(cy) + HEX_SIZE / 2 + CAPTION_GAP;
74
81
  return {
75
82
  type: 'text',
76
83
  text: surfaceText(str),
@@ -171,7 +178,7 @@ function tr(std, wording) {
171
178
  * A hand-composed template — what is left once the artefacts are derived.
172
179
  *
173
180
  * The seeds a template writes into the document go through the translation
174
- * seam at placement (ADR 0016); `content` stays the English build, and
181
+ * seam at placement (ADR 0023); `content` stays the English build, and
175
182
  * `localize` rebuilds the same snapshot with translated seeds.
176
183
  */
177
184
  function tpl(name, preview, build, nameKey) {
@@ -0,0 +1,31 @@
1
+ import type { FrameworkPalette, NamedPalette } from '@formicoidea/labre-core/components/color-picker';
2
+ import type { ChromeWording } from '@formicoidea/labre-core/shared/services';
3
+ /**
4
+ * The Cynefin / Estuarine swatches' own names, resolved by
5
+ * `resolvePaletteLabel` (`@labre/affine-components/color-picker`) through each
6
+ * swatch's own {@link NamedPalette.labelWording} — the mechanism
7
+ * `WARDLEY_PALETTE_WORDINGS` established, reused here without either package
8
+ * naming the other.
9
+ *
10
+ * Exported so `../translations.ts` can list them under source `chrome`: a
11
+ * swatch's name is toolbar chrome, re-rendered on every locale switch, never
12
+ * seeded into a document.
13
+ */
14
+ export declare const CYNEFIN_ESTUARINE_PALETTE_WORDING_ITERATE: ChromeWording;
15
+ export declare const CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL: ChromeWording;
16
+ export declare const CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL_DARK: ChromeWording;
17
+ export declare const CYNEFIN_ESTUARINE_PALETTE_WORDING_VOLATILE: ChromeWording;
18
+ export declare const CYNEFIN_ESTUARINE_PALETTE_WORDING_AXIS: ChromeWording;
19
+ export declare const CYNEFIN_ESTUARINE_PALETTE_WORDINGS: readonly ChromeWording[];
20
+ /**
21
+ * From the default editor palette we keep ONLY the neutrals — the historical
22
+ * colours say nothing in this notation.
23
+ */
24
+ export declare const CYNEFIN_ESTUARINE_PALETTE_LIST: NamedPalette[];
25
+ /**
26
+ * This pack's page of the colour pickers' carousel (`docs/adr/0027`).
27
+ * Registered from the FLAG-GATED view extension, because offering hues is
28
+ * TOOLING: a board drawn while the flag was on keeps every colour it was
29
+ * painted with when the flag goes off (`docs/adr/0009`).
30
+ */
31
+ export declare const CYNEFIN_ESTUARINE_FRAMEWORK_PALETTE: FrameworkPalette;
@@ -0,0 +1,105 @@
1
+ import { neutralPalettes } from '@formicoidea/labre-core/gfx/shape';
2
+ import { COLORS as CYNEFIN_COLORS } from '../cynefin/consts.js';
3
+ import { COLORS as ESTUARINE_COLORS } from '../estuarine/consts.js';
4
+ /**
5
+ * The Cynefin / Estuarine swatches' own names, resolved by
6
+ * `resolvePaletteLabel` (`@labre/affine-components/color-picker`) through each
7
+ * swatch's own {@link NamedPalette.labelWording} — the mechanism
8
+ * `WARDLEY_PALETTE_WORDINGS` established, reused here without either package
9
+ * naming the other.
10
+ *
11
+ * Exported so `../translations.ts` can list them under source `chrome`: a
12
+ * swatch's name is toolbar chrome, re-rendered on every locale switch, never
13
+ * seeded into a document.
14
+ */
15
+ export const CYNEFIN_ESTUARINE_PALETTE_WORDING_ITERATE = [
16
+ 'com.labre.cynefin.palette.iterate-teal',
17
+ 'Iterate teal',
18
+ ];
19
+ export const CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL = [
20
+ 'com.labre.estuarine.palette.liminal-green',
21
+ 'Liminal green',
22
+ ];
23
+ export const CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL_DARK = [
24
+ 'com.labre.estuarine.palette.liminal-green-dark',
25
+ 'Liminal green dark',
26
+ ];
27
+ export const CYNEFIN_ESTUARINE_PALETTE_WORDING_VOLATILE = [
28
+ 'com.labre.estuarine.palette.volatile-red',
29
+ 'Volatile red',
30
+ ];
31
+ export const CYNEFIN_ESTUARINE_PALETTE_WORDING_AXIS = [
32
+ 'com.labre.estuarine.palette.axis-plum',
33
+ 'Axis plum',
34
+ ];
35
+ export const CYNEFIN_ESTUARINE_PALETTE_WORDINGS = [
36
+ CYNEFIN_ESTUARINE_PALETTE_WORDING_ITERATE,
37
+ CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL,
38
+ CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL_DARK,
39
+ CYNEFIN_ESTUARINE_PALETTE_WORDING_VOLATILE,
40
+ CYNEFIN_ESTUARINE_PALETTE_WORDING_AXIS,
41
+ ];
42
+ /**
43
+ * The hues the two boards of this pack are actually drawn in.
44
+ *
45
+ * ONE page for two diagrams, because they are one framework id
46
+ * (`cynefin-estuarine`) and an author works across both. Cynefin contributes
47
+ * the teal of its "iterate" curve — the only colour on a frame whose other
48
+ * inks are the shared notation neutrals — and Estuarine the three its curve
49
+ * legends are drawn in plus the plum of its two axes.
50
+ *
51
+ * Every value is read off the two `COLORS` tables rather than restated, so the
52
+ * shelf and the painted frames cannot drift apart. They are SHORTCUTS, never
53
+ * constraints: nothing on either board reads a colour.
54
+ */
55
+ const CYNEFIN_ESTUARINE_PALETTES = [
56
+ {
57
+ key: 'Iterate teal',
58
+ value: CYNEFIN_COLORS.teal,
59
+ labelWording: CYNEFIN_ESTUARINE_PALETTE_WORDING_ITERATE,
60
+ },
61
+ {
62
+ key: 'Liminal green',
63
+ value: ESTUARINE_COLORS.liminal,
64
+ labelWording: CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL,
65
+ },
66
+ // The LIMINAL legend's own darker green — the curve's colour is too light to
67
+ // read as words over the map.
68
+ {
69
+ key: 'Liminal green dark',
70
+ value: ESTUARINE_COLORS.liminalLabel,
71
+ labelWording: CYNEFIN_ESTUARINE_PALETTE_WORDING_LIMINAL_DARK,
72
+ },
73
+ {
74
+ key: 'Volatile red',
75
+ value: ESTUARINE_COLORS.volatile,
76
+ labelWording: CYNEFIN_ESTUARINE_PALETTE_WORDING_VOLATILE,
77
+ },
78
+ {
79
+ key: 'Axis plum',
80
+ value: ESTUARINE_COLORS.axis,
81
+ labelWording: CYNEFIN_ESTUARINE_PALETTE_WORDING_AXIS,
82
+ },
83
+ ];
84
+ /**
85
+ * From the default editor palette we keep ONLY the neutrals — the historical
86
+ * colours say nothing in this notation.
87
+ */
88
+ export const CYNEFIN_ESTUARINE_PALETTE_LIST = [
89
+ ...CYNEFIN_ESTUARINE_PALETTES,
90
+ ...neutralPalettes(),
91
+ ];
92
+ /**
93
+ * This pack's page of the colour pickers' carousel (`docs/adr/0027`).
94
+ * Registered from the FLAG-GATED view extension, because offering hues is
95
+ * TOOLING: a board drawn while the flag was on keeps every colour it was
96
+ * painted with when the flag goes off (`docs/adr/0009`).
97
+ */
98
+ export const CYNEFIN_ESTUARINE_FRAMEWORK_PALETTE = {
99
+ framework: 'cynefin-estuarine',
100
+ labelWording: [
101
+ 'com.labre.framework.cynefin-estuarine',
102
+ 'Cynefin / Estuarine',
103
+ ],
104
+ palettes: CYNEFIN_ESTUARINE_PALETTE_LIST,
105
+ };
@@ -7,6 +7,8 @@ export declare class EdgelessCynefinEstuarineSeniorButton extends EdgelessCynefi
7
7
  enableActiveBackground: boolean;
8
8
  type: typeof EmptyTool;
9
9
  private _toggleMenu;
10
+ /** Lit while one of THIS framework's artefacts is armed. See the Wardley one. */
11
+ private get _armed();
10
12
  render(): import("lit-html").TemplateResult<1>;
11
13
  }
12
14
  export {};
@@ -1,7 +1,7 @@
1
1
  import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
3
3
  import { translateKey } from '@formicoidea/labre-core/shared/services';
4
- import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
4
+ import { armedArtefact, EdgelessToolbarToolMixin, } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
5
5
  import { SignalWatcher } from '@formicoidea/labre-core/global/lit';
6
6
  import { css, html, LitElement } from 'lit';
7
7
  import { cynefinToolbarIcon } from './icons.js';
@@ -62,6 +62,10 @@ export class EdgelessCynefinEstuarineSeniorButton extends EdgelessToolbarToolMix
62
62
  const menu = this.createPopper('edgeless-cynefin-estuarine-menu', this);
63
63
  menu.element.edgeless = this.edgeless;
64
64
  }
65
+ /** Lit while one of THIS framework's artefacts is armed. See the Wardley one. */
66
+ get _armed() {
67
+ return armedArtefact(this.gfx)?.owner === 'cynefin-estuarine';
68
+ }
65
69
  render() {
66
70
  return html `<edgeless-toolbar-button
67
71
  class="ce-button"
@@ -69,7 +73,7 @@ export class EdgelessCynefinEstuarineSeniorButton extends EdgelessToolbarToolMix
69
73
  ? ''
70
74
  : translateKey(this.edgeless.std, 'com.labre.framework.cynefin-estuarine', 'Cynefin / Estuarine')}
71
75
  .tooltipOffset=${4}
72
- .active=${!!this.popper}
76
+ .active=${!!this.popper || this._armed}
73
77
  @click=${this._toggleMenu}
74
78
  >
75
79
  <div class="ce-root">
@@ -8,6 +8,7 @@ import { ESTUARINE_READING } from './estuarine/reading.js';
8
8
  import { ESTUARINE_ROLES } from './estuarine/roles.js';
9
9
  import { ESTUARINE_TOOLBAR_WORDINGS } from './estuarine/toolbar/config.js';
10
10
  import { CYNEFIN_ESTUARINE_TEMPLATE_CATEGORY_WORDINGS, CYNEFIN_ESTUARINE_TEMPLATE_NAME_WORDINGS, CYNEFIN_ESTUARINE_TEMPLATE_SEEDS, } from './templates/index.js';
11
+ import { CYNEFIN_ESTUARINE_PALETTE_WORDINGS } from './toolbar/palette.js';
11
12
  /**
12
13
  * THIS framework's contribution to the translation-key manifest.
13
14
  *
@@ -76,4 +77,12 @@ ESTUARINE_CANVAS_WORDINGS.map(([key, fallback]) => ({
76
77
  key,
77
78
  fallback,
78
79
  source: 'chrome',
80
+ })),
81
+ // The carousel swatches' own names (`toolbar/palette.ts`) — chrome,
82
+ // re-rendered by the colour picker on every locale switch, never seeded
83
+ // into a document. Same treatment as Wardley's and EDGY's.
84
+ CYNEFIN_ESTUARINE_PALETTE_WORDINGS.map(([key, fallback]) => ({
85
+ key,
86
+ fallback,
87
+ source: 'chrome',
79
88
  })));
package/dist/utils.d.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Uniform fit of a fixed reference design (`refW × refH`) into an element of
3
- * size `w × h`: the scale factor plus the centering offsets (letterboxed).
4
- * Keeps the artwork undistorted at any element size.
2
+ * Uniform fit of a fixed reference design into an element, THE one in the
3
+ * library: `@labre/affine-block-surface`'s `refScale`. Re-exported here because
4
+ * both diagrams of this package draw an authored artwork and have always called
5
+ * it by this name — the body used to be copied here, and in two other packages
6
+ * besides (R37, one source of truth).
5
7
  */
6
- export declare function refScale(w: number, h: number, refW: number, refH: number): {
7
- s: number;
8
- ox: number;
9
- oy: number;
10
- };
8
+ export { refScale } from '@formicoidea/labre-core/blocks/surface';
11
9
  export declare const FONT_FAMILY = "Inter, sans-serif";
package/dist/utils.js CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Uniform fit of a fixed reference design (`refW × refH`) into an element of
3
- * size `w × h`: the scale factor plus the centering offsets (letterboxed).
4
- * Keeps the artwork undistorted at any element size.
2
+ * Uniform fit of a fixed reference design into an element, THE one in the
3
+ * library: `@labre/affine-block-surface`'s `refScale`. Re-exported here because
4
+ * both diagrams of this package draw an authored artwork and have always called
5
+ * it by this name — the body used to be copied here, and in two other packages
6
+ * besides (R37, one source of truth).
5
7
  */
6
- export function refScale(w, h, refW, refH) {
7
- const s = Math.min(w / refW, h / refH);
8
- return { s, ox: (w - refW * s) / 2, oy: (h - refH * s) / 2 };
9
- }
8
+ export { refScale } from '@formicoidea/labre-core/blocks/surface';
10
9
  export const FONT_FAMILY = 'Inter, sans-serif';