@formicoidea/labre-framework-wardley 0.36.0 → 0.38.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.
package/dist/actions.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { indexOverBackgrounds, type StackedElement } from '@formicoidea/labre-core/blocks/surface';
1
2
  import { WardleyBackgroundElementModel, type WardleyBgVariant } from '@formicoidea/labre-core/model';
2
- import { Bound } from '@formicoidea/labre-core/global/gfx';
3
3
  import type { BlockStdScope } from '@formicoidea/labre-core/std';
4
4
  import { type GfxController } from '@formicoidea/labre-core/std/gfx';
5
5
  import { type WardleyExportBoard } from './export.js';
@@ -109,41 +109,32 @@ export declare function createWardleyAccelerator(gfx: GfxController, kind: 'acce
109
109
  * to the depth {@link wardleyAreaIndexOver} works out.
110
110
  */
111
111
  export declare function createWardleyArea(gfx: GfxController, shape: WardleyAreaShape): void;
112
+ /**
113
+ * Send one already-placed area to the depth it belongs at — see
114
+ * {@link createWardleyArea} for why a zone is lowered at all.
115
+ *
116
+ * Extracted so the TEMPLATE can call it: a snapshot cannot express this, because
117
+ * the depth is relative to whatever map is already on the board rather than to
118
+ * anything inside the template.
119
+ */
120
+ export declare function lowerWardleyArea(gfx: GfxController, id: string): void;
112
121
  /**
113
122
  * One already-placed element, as {@link wardleyAreaIndexOver} needs to read it.
114
123
  *
115
- * Three fields and no model, so the placement can be reasoned about — and
116
- * tested — without a document: what it is, where it is, and how deep.
124
+ * The Wardley name for the shared {@link StackedElement}, kept so this
125
+ * framework's callers and tests keep reading in its own vocabulary.
117
126
  */
118
- export interface WardleyStackedElement {
119
- /** Fractional index. Sorts lexicographically, which IS the paint order. */
120
- index: string;
121
- /** Serialized box. */
122
- xywh: string;
123
- /** Whether this is a framework BACKGROUND — a map, a pool, a C4 board… */
124
- isBackground: boolean;
125
- }
127
+ export type WardleyStackedElement = StackedElement;
126
128
  /**
127
129
  * How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
128
130
  * covers, and below everything else. `null` means the back of the surface.
129
131
  *
130
- * "The back" alone was the first answer and it was wrong, as the recette of
131
- * #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so a
132
- * zone sent behind the whole surface went behind the map and vanished. What a
133
- * zone must be under is the artefacts it groups; what it must be over is the
134
- * canvas they are drawn on. Those are two different depths, and only the first
135
- * one is "the back".
136
- *
137
- * So: find the topmost background the zone actually OVERLAPS — the map it was
138
- * drawn on, and not some other framework's board parked elsewhere on the same
139
- * canvas — and mint a key between it and whatever sits directly above it. With
140
- * no background under the zone at all there is nothing to clear, and the back
141
- * of the surface is right again.
142
- *
143
- * `siblings` need not be sorted: fractional indexes sort lexicographically, so
144
- * this sorts them itself and reads the paint order straight off the strings.
132
+ * The reasoning — and the recette of #213 that produced it — now lives with
133
+ * {@link indexOverBackgrounds}, because a frame drawn on a map needs the very
134
+ * same depth for the very same reason. This alias keeps the Wardley name for
135
+ * the callers and tests written against it.
145
136
  */
146
- export declare function wardleyAreaIndexOver(siblings: readonly WardleyStackedElement[], box: Bound): string | null;
137
+ export declare const wardleyAreaIndexOver: typeof indexOverBackgrounds;
147
138
  /**
148
139
  * Read an SVG the user picks as a SKETCH, and say what it cost.
149
140
  *
package/dist/actions.js CHANGED
@@ -1,17 +1,17 @@
1
- import { backgroundSize, DefaultTool, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundSize, DefaultTool, indexOverBackgrounds, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
3
  import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
4
- import { ConnectorMode, FontFamily, FontWeight, FrameworkBackgroundElementModel, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
4
+ import { ConnectorMode, FontWeight, FrameworkBackgroundElementModel, PointStyle, StrokeStyle, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
5
5
  import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
6
6
  import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
7
7
  import { Bound } from '@formicoidea/labre-core/global/gfx';
8
- import { generateKeyBetween, GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
8
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
9
9
  import { WARDLEY_BACKGROUND } from './background.js';
10
10
  import { WARDLEY_SVG_IMPORT } from './interchange.js';
11
11
  import { wardleyBoardFrom, wardleySafeFilename, } from './export.js';
12
12
  import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
13
- import { HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, NODE_STROKE, PORTER_DEFAULT_LETTER, WARDLEY_RED, } from './node/consts.js';
14
- import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
13
+ import { HANDLE_SIZE, INERTIA_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, PORTER_DEFAULT_LETTER, WARDLEY_RED, } from './node/consts.js';
14
+ import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyInertiaProps, WARDLEY_LABEL_H, WARDLEY_LABEL_W, wardleyLabelProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
15
15
  import { WARDLEY_ROLE } from './roles.js';
16
16
  /**
17
17
  * Standalone creation/activation actions for the Wardley toolbox — the
@@ -39,8 +39,8 @@ import { WARDLEY_ROLE } from './roles.js';
39
39
  * The fix is to make the variant part of the declaration — one axis/end-label
40
40
  * set per variant, each naming its own `labelKey` — rather than a bag of prop
41
41
  * overrides applied at creation. Out of scope here (it changes what a variant
42
- * IS); duplicated verbatim in `templates/index.ts`, and both copies go away
43
- * together.
42
+ * IS). This is now the ONLY copy: the four background templates are derived
43
+ * from these four commands, so the table goes away in one place.
44
44
  */
45
45
  const BACKGROUND_VARIANT_DEFAULTS = {
46
46
  classic: {},
@@ -62,16 +62,10 @@ const BACKGROUND_VARIANT_DEFAULTS = {
62
62
  // the grey gradient differs.
63
63
  'evolution-gradient': {},
64
64
  };
65
- /** Height of the native free-text labels (Inter, size 18). */
66
- const LABEL_H = LABEL_FONT_SIZE + 8;
67
- /**
68
- * Width of a label box, whatever it reads.
69
- *
70
- * A number rather than a measurement, which is why a RIGHT-aligned label has to
71
- * subtract it: the box does not shrink to the words, so the only way to make
72
- * the words end on a given edge is to start the box a full width before it.
73
- */
74
- const LABEL_W = 120;
65
+ // The label box, now owned by `presets.ts` — the local names stay so the four
66
+ // placements below keep reading as they did.
67
+ const LABEL_H = WARDLEY_LABEL_H;
68
+ const LABEL_W = WARDLEY_LABEL_W;
75
69
  function finish(gfx, id) {
76
70
  gfx.doc.captureSync();
77
71
  gfx.tool.setTool(DefaultTool);
@@ -106,21 +100,7 @@ function addNode(surface, kind, cx, cy, extra = {}) {
106
100
  }
107
101
  /** Add a native free-text label (same Inter family as the axis labels). */
108
102
  function addLabel(surface, text, x, y, textAlign = 'left', fontWeight = FontWeight.Regular) {
109
- return surface.addElement({
110
- type: 'text',
111
- text,
112
- fontWeight,
113
- // Semantic identity (PF1, revised in PF13.4): a Wardley label is a free
114
- // text element like any other, so its ROLE is the only thing that tells W3
115
- // it must not land on top of a node. A free text the user typed elsewhere
116
- // stays neutral and is never evaluated.
117
- role: WARDLEY_ROLE.label,
118
- fontFamily: FontFamily.Inter,
119
- fontSize: LABEL_FONT_SIZE,
120
- color: NODE_STROKE,
121
- textAlign,
122
- xywh: new Bound(x, y, LABEL_W, LABEL_H).serialize(),
123
- });
103
+ return surface.addElement(wardleyLabelProps(text, x, y, textAlign, fontWeight));
124
104
  }
125
105
  /** Create a wardley map background of the given variant, viewport-centered. */
126
106
  export function createWardleyBackground(gfx, variant = 'classic') {
@@ -174,23 +154,9 @@ export function createWardleyInertia(gfx) {
174
154
  return;
175
155
  const { w, h } = INERTIA_SIZE;
176
156
  const { centerX, centerY } = gfx.viewport;
177
- const id = gfx.surface.addElement({
178
- type: 'shape',
179
- shapeType: 'rect',
180
- // The inertia bar has no element type of its own — it IS a plain filled
181
- // rect — so the role is the whole of its semantics (PF13.5).
182
- role: WARDLEY_ROLE.inertia,
183
- filled: true,
184
- fillColor: INERTIA_COLOR,
185
- strokeColor: INERTIA_COLOR,
186
- strokeWidth: 0,
187
- shapeStyle: ShapeStyle.General,
188
- roughness: 0,
189
- radius: 0,
190
- // the inertia bar has a canonical size: text overflows, never deforms
191
- textFitMode: TextFitMode.Overflow,
157
+ const id = gfx.surface.addElement(wardleyInertiaProps({
192
158
  xywh: new Bound(centerX - w / 2, centerY - h / 2, w, h).serialize(),
193
- });
159
+ }));
194
160
  finish(gfx, id);
195
161
  }
196
162
  /**
@@ -329,16 +295,28 @@ export function createWardleyArea(gfx, shape) {
329
295
  const { centerX: cx, centerY: cy } = gfx.viewport;
330
296
  const xywh = wardleyAreaBox(shape, cx, cy);
331
297
  const id = surface.addElement(wardleyAreaProps(shape, { xywh }));
332
- const model = surface.getElementById(id);
333
- if (model) {
334
- const over = wardleyAreaIndexOver(stackedElementsOf(surface, id), Bound.deserialize(xywh));
335
- // `null` is "nothing under it": the back of the surface, minted by the
336
- // layer itself so an empty board gets the layer's own initial index rather
337
- // than a key invented here.
338
- model.index = over ?? gfx.layer.getReorderedIndex(model, 'back');
339
- }
298
+ lowerWardleyArea(gfx, id);
340
299
  finish(gfx, id);
341
300
  }
301
+ /**
302
+ * Send one already-placed area to the depth it belongs at — see
303
+ * {@link createWardleyArea} for why a zone is lowered at all.
304
+ *
305
+ * Extracted so the TEMPLATE can call it: a snapshot cannot express this, because
306
+ * the depth is relative to whatever map is already on the board rather than to
307
+ * anything inside the template.
308
+ */
309
+ export function lowerWardleyArea(gfx, id) {
310
+ const surface = gfx.surface;
311
+ const model = surface?.getElementById(id);
312
+ if (!surface || !model)
313
+ return;
314
+ const over = wardleyAreaIndexOver(stackedElementsOf(surface, id), Bound.deserialize(model.xywh));
315
+ // `null` is "nothing under it": the back of the surface, minted by the
316
+ // layer itself so an empty board gets the layer's own initial index rather
317
+ // than a key invented here.
318
+ model.index = over ?? gfx.layer.getReorderedIndex(model, 'back');
319
+ }
342
320
  /** The top-level canvas elements of a surface, minus the one being placed. */
343
321
  function stackedElementsOf(surface, exclude) {
344
322
  return surface.elementModels
@@ -353,36 +331,12 @@ function stackedElementsOf(surface, exclude) {
353
331
  * How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
354
332
  * covers, and below everything else. `null` means the back of the surface.
355
333
  *
356
- * "The back" alone was the first answer and it was wrong, as the recette of
357
- * #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so a
358
- * zone sent behind the whole surface went behind the map and vanished. What a
359
- * zone must be under is the artefacts it groups; what it must be over is the
360
- * canvas they are drawn on. Those are two different depths, and only the first
361
- * one is "the back".
362
- *
363
- * So: find the topmost background the zone actually OVERLAPS — the map it was
364
- * drawn on, and not some other framework's board parked elsewhere on the same
365
- * canvas — and mint a key between it and whatever sits directly above it. With
366
- * no background under the zone at all there is nothing to clear, and the back
367
- * of the surface is right again.
368
- *
369
- * `siblings` need not be sorted: fractional indexes sort lexicographically, so
370
- * this sorts them itself and reads the paint order straight off the strings.
334
+ * The reasoning — and the recette of #213 that produced it — now lives with
335
+ * {@link indexOverBackgrounds}, because a frame drawn on a map needs the very
336
+ * same depth for the very same reason. This alias keeps the Wardley name for
337
+ * the callers and tests written against it.
371
338
  */
372
- export function wardleyAreaIndexOver(siblings, box) {
373
- const stack = [...siblings].sort((a, b) => a.index < b.index ? -1 : a.index > b.index ? 1 : 0);
374
- // The LAST match, which is the topmost: a board with two maps on it gets the
375
- // zone above the one it is actually drawn over, whichever was drawn first.
376
- const under = stack.reduce((found, element, at) => element.isBackground &&
377
- Bound.deserialize(element.xywh).isOverlapWithBound(box)
378
- ? at
379
- : found, -1);
380
- if (under < 0)
381
- return null;
382
- // `null` for the upper bound when the background is the topmost element
383
- // there is — `generateKeyBetween` reads that as "append after".
384
- return generateKeyBetween(stack[under].index, stack[under + 1]?.index ?? null);
385
- }
339
+ export const wardleyAreaIndexOver = indexOverBackgrounds;
386
340
  /**
387
341
  * Read an SVG the user picks as a SKETCH, and say what it cost.
388
342
  *
package/dist/presets.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type WardleyNodeKind } from '@formicoidea/labre-core/model';
1
+ import { FontWeight, type WardleyNodeKind } from '@formicoidea/labre-core/model';
2
2
  /**
3
3
  * What a Wardley artefact IS, as props — the ONE description the creation sites
4
4
  * and the morph both read.
@@ -98,6 +98,44 @@ export declare function wardleyNodeProps(kind: WardleyArtefactKind, box: {
98
98
  }): Record<string, unknown> & {
99
99
  type: string;
100
100
  };
101
+ /** Height of the native free-text labels (Inter, size 18). */
102
+ export declare const WARDLEY_LABEL_H: number;
103
+ /**
104
+ * Width of a label box, whatever it reads.
105
+ *
106
+ * A number rather than a measurement, which is why a RIGHT-aligned label has to
107
+ * subtract it: the box does not shrink to the words, so the only way to make
108
+ * the words end on a given edge is to start the box a full width before it.
109
+ */
110
+ export declare const WARDLEY_LABEL_W = 120;
111
+ /**
112
+ * The NAME beside an artefact, as props — a native free-text element.
113
+ *
114
+ * Here rather than at the creation site for the reason the rest of this pack
115
+ * gives: the templates re-stated it by hand and drifted (a 140-wide box against
116
+ * this 120), so what a Wardley label IS has to have exactly one description.
117
+ *
118
+ * `text` is a plain STRING, as `Surface.addElement` takes it; a snapshot writes
119
+ * the same field as a serialized `Y.Text`, which is the one key an authoring
120
+ * kit overrides on the way into a template.
121
+ */
122
+ export declare function wardleyLabelProps(text: string, x: number, y: number, textAlign?: 'left' | 'center' | 'right', fontWeight?: FontWeight): Record<string, unknown> & {
123
+ type: string;
124
+ };
125
+ /**
126
+ * An INERTIA bar, as props — a plain filled rect, and the whole of its
127
+ * semantics is the role.
128
+ *
129
+ * A preset for the same reason every other one here exists: the creation site
130
+ * and the shipped maps both draw one, and the hand-written copy in the
131
+ * templates had already lost `textFitMode` — the one line that keeps the bar's
132
+ * canonical size when somebody types into it.
133
+ */
134
+ export declare function wardleyInertiaProps(box: {
135
+ xywh: string;
136
+ }): Record<string, unknown> & {
137
+ type: string;
138
+ };
101
139
  /**
102
140
  * An AREA, as props — one kind, two shapes.
103
141
  *
package/dist/presets.js CHANGED
@@ -1,6 +1,6 @@
1
- import { ConnectorMode, DEFAULT_POLYGON_VERTICES, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
1
+ import { ConnectorMode, DEFAULT_POLYGON_VERTICES, FontFamily, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
- import { ACCELERATOR_FILL, ACCELERATOR_LABEL, ACCELERATOR_SIZE, ACCELERATOR_STROKE_WIDTH, ACCELERATOR_VERTICES, AREA_FILL, AREA_POLYGON_SIZE, AREA_RECT_SIZE, AREA_STROKE, AREA_STROKE_WIDTH, DECELERATOR_LABEL, DECELERATOR_VERTICES, ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, LABEL_DEFAULT, LABEL_FONT_SIZE, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, PORTER_ARROW, PORTER_LETTER_FONT_SIZE, PORTER_SIZE, WARDLEY_RED, } from './node/consts.js';
3
+ import { ACCELERATOR_FILL, ACCELERATOR_LABEL, ACCELERATOR_SIZE, ACCELERATOR_STROKE_WIDTH, ACCELERATOR_VERTICES, AREA_FILL, AREA_POLYGON_SIZE, AREA_RECT_SIZE, AREA_STROKE, AREA_STROKE_WIDTH, DECELERATOR_LABEL, DECELERATOR_VERTICES, ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, LABEL_DEFAULT, LABEL_FONT_SIZE, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, PORTER_ARROW, PORTER_LETTER_FONT_SIZE, PORTER_SIZE, WARDLEY_RED, } from './node/consts.js';
4
4
  import { WARDLEY_ROLE } from './roles.js';
5
5
  /**
6
6
  * The size a kind is BORN at, which for Wardley is the notation rather than a
@@ -167,6 +167,72 @@ export function wardleyNodeProps(kind, box) {
167
167
  xywh: box.xywh,
168
168
  };
169
169
  }
170
+ /** Height of the native free-text labels (Inter, size 18). */
171
+ export const WARDLEY_LABEL_H = LABEL_FONT_SIZE + 8;
172
+ /**
173
+ * Width of a label box, whatever it reads.
174
+ *
175
+ * A number rather than a measurement, which is why a RIGHT-aligned label has to
176
+ * subtract it: the box does not shrink to the words, so the only way to make
177
+ * the words end on a given edge is to start the box a full width before it.
178
+ */
179
+ export const WARDLEY_LABEL_W = 120;
180
+ /**
181
+ * The NAME beside an artefact, as props — a native free-text element.
182
+ *
183
+ * Here rather than at the creation site for the reason the rest of this pack
184
+ * gives: the templates re-stated it by hand and drifted (a 140-wide box against
185
+ * this 120), so what a Wardley label IS has to have exactly one description.
186
+ *
187
+ * `text` is a plain STRING, as `Surface.addElement` takes it; a snapshot writes
188
+ * the same field as a serialized `Y.Text`, which is the one key an authoring
189
+ * kit overrides on the way into a template.
190
+ */
191
+ export function wardleyLabelProps(text, x, y, textAlign = 'left', fontWeight = FontWeight.Regular) {
192
+ return {
193
+ type: 'text',
194
+ text,
195
+ fontWeight,
196
+ // Semantic identity (PF1, revised in PF13.4): a Wardley label is a free
197
+ // text element like any other, so its ROLE is the only thing that tells W3
198
+ // it must not land on top of a node. A free text the user typed elsewhere
199
+ // stays neutral and is never evaluated.
200
+ role: WARDLEY_ROLE.label,
201
+ fontFamily: FontFamily.Inter,
202
+ fontSize: LABEL_FONT_SIZE,
203
+ color: NODE_STROKE,
204
+ textAlign,
205
+ xywh: new Bound(x, y, WARDLEY_LABEL_W, WARDLEY_LABEL_H).serialize(),
206
+ };
207
+ }
208
+ /**
209
+ * An INERTIA bar, as props — a plain filled rect, and the whole of its
210
+ * semantics is the role.
211
+ *
212
+ * A preset for the same reason every other one here exists: the creation site
213
+ * and the shipped maps both draw one, and the hand-written copy in the
214
+ * templates had already lost `textFitMode` — the one line that keeps the bar's
215
+ * canonical size when somebody types into it.
216
+ */
217
+ export function wardleyInertiaProps(box) {
218
+ return {
219
+ type: 'shape',
220
+ shapeType: 'rect',
221
+ // The inertia bar has no element type of its own — it IS a plain filled
222
+ // rect — so the role is the whole of its semantics (PF13.5).
223
+ role: WARDLEY_ROLE.inertia,
224
+ filled: true,
225
+ fillColor: INERTIA_COLOR,
226
+ strokeColor: INERTIA_COLOR,
227
+ strokeWidth: 0,
228
+ shapeStyle: ShapeStyle.General,
229
+ roughness: 0,
230
+ radius: 0,
231
+ // the inertia bar has a canonical size: text overflows, never deforms
232
+ textFitMode: TextFitMode.Overflow,
233
+ xywh: box.xywh,
234
+ };
235
+ }
170
236
  /**
171
237
  * An AREA, as props — one kind, two shapes.
172
238
  *
package/dist/reading.js CHANGED
@@ -106,10 +106,36 @@ export const WARDLEY_READING = {
106
106
  tagId: WARDLEY_NATURE_TAG_ID,
107
107
  conventions: WARDLEY_NAMING_CONVENTIONS,
108
108
  },
109
- // The value-chain link. ADR 0010 § 2: `source` is the subject of the role's
110
- // verb, and this role's verb is "depends on" — so the source is the consumer
111
- // and the target is what it needs.
112
- relation: { edgeRole: WARDLEY_ROLE.dependency },
109
+ /**
110
+ * The value-chain link. ADR 0010 § 2: `source` is the subject of the role's
111
+ * verb, and this role's verb is "needs" — so the source is the consumer and
112
+ * the target is what it needs.
113
+ *
114
+ * The two wordings are Wardley's own and say so: a consumer is drawn ABOVE
115
+ * what it needs, which is a fact about a value chain and about nothing else.
116
+ * They lived in the panel until every framework gained a reading, and they
117
+ * keep their keys and their English to the letter — a host that already
118
+ * translated them translates nothing twice.
119
+ *
120
+ * `geometry: 'vertical'` is the claim those two parentheses make, stated
121
+ * where the engine can read it: this board's vertical axis IS the order, so
122
+ * a link drawn against it contradicts the drawing (W4) and the value flow can
123
+ * be read from the bottom up. Wardley is the only framework that says it.
124
+ */
125
+ relation: {
126
+ edgeRole: WARDLEY_ROLE.dependency,
127
+ sides: {
128
+ consumer: {
129
+ labelKey: 'com.labre.reading.relations.consumers',
130
+ labelFallback: 'Consumers (above)',
131
+ },
132
+ supplier: {
133
+ labelKey: 'com.labre.reading.relations.suppliers',
134
+ labelFallback: 'Suppliers (below)',
135
+ },
136
+ },
137
+ geometry: 'vertical',
138
+ },
113
139
  frame: {
114
140
  backgroundRole: WARDLEY_ROLE.map,
115
141
  background: WARDLEY_BACKGROUND,
@@ -1,71 +1,34 @@
1
- import { makeTemplateSnapshot, surfaceText, } from '@formicoidea/labre-core/gfx/template';
2
- import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
3
- import { REF_WIDTH } from '../consts.js';
1
+ import { makeTemplateSnapshot, templateFromCommand, } from '@formicoidea/labre-core/gfx/template';
2
+ import { ConnectorMode, PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
3
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
4
+ import { lowerWardleyArea } from '../actions.js';
5
+ import { wardleyCommands } from '../commands.js';
6
+ import { LINK_GREY, LINK_STROKE_WIDTH, WARDLEY_RED } from '../node/consts.js';
4
7
  import { WARDLEY_ROLE } from '../roles.js';
5
8
  import { wardleyMaps } from './maps.js';
6
- import { ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_FONT_SIZE, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_WIDTH, WARDLEY_RED, } from '../node/consts.js';
7
9
  /**
8
- * Verbatim copy of `BACKGROUND_VARIANT_DEFAULTS` in `../actions.ts` — see the
9
- * TODO there: both copies write English prose into the document for the two
10
- * value-chain variants, and both go away together when the variant becomes
11
- * part of the declaration.
10
+ * The Wardley palette — DERIVED from the toolbox, one template per command.
11
+ *
12
+ * Every single-artefact entry below is what its command actually draws, run
13
+ * once against a recording surface. It used to be a hand-written restatement of
14
+ * the same artefacts, and it had drifted exactly as far as a copy drifts: the
15
+ * four backgrounds had lost `role: wardley:map` and `resizeEnabled` (#77), no
16
+ * composite carried the `group` the toolbox has written since #51, the inertia
17
+ * bar had lost its `textFitMode`, label boxes were 140 against a `LABEL_W` of
18
+ * 120, and five commands had no template at all. Derived, none of that can
19
+ * happen again — and `templates-parity.unit.spec.ts` re-runs each command and
20
+ * compares, so the day a creation site changes the palette changes with it.
21
+ *
22
+ * The two shipped MAPS stay hand-composed (`maps.ts`): a canonical map is an
23
+ * arrangement of a dozen artefacts, which no single command draws. They are
24
+ * built on the same presets, and the same test checks their composition.
12
25
  */
13
- const VARIANT_DEFAULTS = {
14
- classic: {},
15
- opportunity: {
16
- yAxisTitle: 'Opportunity',
17
- showVisibilityLabels: false,
18
- showCornerLabels: false,
19
- },
20
- benefit: {
21
- yAxisTitle: '',
22
- visibilityHigh: 'Benefit',
23
- visibilityLow: 'Investment',
24
- showCornerLabels: false,
25
- },
26
- 'evolution-gradient': {},
27
- };
28
- const bg = (variant, w = REF_WIDTH) => {
29
- const h = Math.round((w * 9) / 16);
30
- return {
31
- type: 'wardley',
32
- variant,
33
- ...VARIANT_DEFAULTS[variant],
34
- xywh: `[0,0,${w},${h}]`,
35
- };
36
- };
37
- /** A wardley node ellipse positioned by top-left. */
38
- function node(kind, x, y, d = NODE_SIZE, fill = NODE_FILL, strokeWidth = NODE_STROKE_WIDTH,
39
- // Neutral for the market's inner dots, exactly as `createWardleyMarket` does:
40
- // glyph wiring, not artefacts.
41
- neutral = false) {
42
- return {
43
- type: 'wardleyNode',
44
- kind,
45
- // A template must produce the same typed artefacts as the toolbox, so a
46
- // map started from a preset validates like a hand-drawn one.
47
- role: neutral ? undefined : WARDLEY_ROLE[kind],
48
- shapeType: 'ellipse',
49
- filled: true,
50
- fillColor: fill,
51
- strokeColor: NODE_STROKE,
52
- strokeWidth,
53
- shapeStyle: ShapeStyle.General,
54
- roughness: 0,
55
- xywh: `[${x},${y},${d},${d}]`,
56
- };
57
- }
58
- function label(x, y, str, align = 'left') {
59
- return {
60
- type: 'text',
61
- text: surfaceText(str),
62
- role: WARDLEY_ROLE.label,
63
- color: NODE_STROKE,
64
- fontFamily: FontFamily.Inter,
65
- fontSize: LABEL_FONT_SIZE,
66
- textAlign: align === 'center' ? TextAlign.Center : TextAlign.Left,
67
- xywh: `[${x},${y},140,26]`,
68
- };
26
+ /** The command a derived template is the picture of. Throws rather than skips. */
27
+ function byId(id) {
28
+ const command = wardleyCommands.find(entry => entry.id === id);
29
+ if (!command)
30
+ throw new Error(`[wardley] templates: no command "${id}"`);
31
+ return command;
69
32
  }
70
33
  /**
71
34
  * A Wardley connector for the palette samples.
@@ -100,99 +63,16 @@ function connect(source, target, opts = {}) {
100
63
  target,
101
64
  };
102
65
  }
103
- const inertia = (x = 0, y = 0) => ({
104
- type: 'shape',
105
- shapeType: 'rect',
106
- role: WARDLEY_ROLE.inertia,
107
- filled: true,
108
- fillColor: INERTIA_COLOR,
109
- strokeColor: INERTIA_COLOR,
110
- strokeWidth: 0,
111
- shapeStyle: ShapeStyle.General,
112
- roughness: 0,
113
- radius: 0,
114
- xywh: `[${x},${y},${INERTIA_SIZE.w},${INERTIA_SIZE.h}]`,
115
- });
116
- /** Pipeline composite: body rect + handle square (straddling top) + label. */
117
- function pipeline() {
118
- return {
119
- label: label(0, 0, 'Pipeline', 'center'),
120
- body: {
121
- type: 'wardleyNode',
122
- kind: 'pipeline',
123
- role: WARDLEY_ROLE.pipeline,
124
- shapeType: 'rect',
125
- filled: true,
126
- fillColor: PIPELINE_FILL,
127
- strokeColor: NODE_STROKE,
128
- strokeWidth: NODE_STROKE_WIDTH,
129
- shapeStyle: ShapeStyle.General,
130
- roughness: 0,
131
- radius: 0,
132
- xywh: `[0,39,${PIPELINE_WIDTH},${PIPELINE_HEIGHT}]`,
133
- },
134
- handle: {
135
- type: 'wardleyNode',
136
- kind: 'handle',
137
- role: WARDLEY_ROLE.handle,
138
- shapeType: 'rect',
139
- filled: true,
140
- fillColor: NODE_FILL,
141
- strokeColor: NODE_STROKE,
142
- strokeWidth: NODE_STROKE_WIDTH,
143
- shapeStyle: ShapeStyle.General,
144
- roughness: 0,
145
- radius: 0,
146
- xywh: `[${PIPELINE_WIDTH / 2 - HANDLE_SIZE / 2},${39 - HANDLE_SIZE / 2},${HANDLE_SIZE},${HANDLE_SIZE}]`,
147
- },
148
- };
149
- }
150
- /** Market composite: outer circle + 3 inner dots wired in a triangle + label. */
151
- function market() {
152
- const R = MARKET_SIZE / 2;
153
- const c = R; // center within the [0,0,MARKET_SIZE,MARKET_SIZE] box
154
- const rho = MARKET_DOT_RING;
155
- const sin60 = Math.sqrt(3) / 2;
156
- const verts = [
157
- [0, -rho],
158
- [rho * sin60, rho / 2],
159
- [-rho * sin60, rho / 2],
160
- ];
161
- const dotAt = (vx, vy) => node('component', c + vx - MARKET_DOT_SIZE / 2, c + vy - MARKET_DOT_SIZE / 2, MARKET_DOT_SIZE, NODE_FILL, MARKET_DOT_STROKE_WIDTH, true);
162
- // Neutral on purpose: the triangle is the market glyph's own wiring, not a
163
- // dependency the user drew (same rule as `createWardleyMarket`).
164
- const tri = (a, b) => ({
165
- type: 'connector',
166
- mode: ConnectorMode.Straight,
167
- stroke: MARKET_LINK_COLOR,
168
- strokeStyle: StrokeStyle.Solid,
169
- strokeWidth: MARKET_LINK_WIDTH,
170
- frontEndpointStyle: PointStyle.None,
171
- rearEndpointStyle: PointStyle.None,
172
- source: { id: a },
173
- target: { id: b },
174
- });
175
- return {
176
- circle: node('market', 0, 0, MARKET_SIZE, NODE_FILL),
177
- d0: dotAt(verts[0][0], verts[0][1]),
178
- d1: dotAt(verts[1][0], verts[1][1]),
179
- d2: dotAt(verts[2][0], verts[2][1]),
180
- t0: tri('d0', 'd1'),
181
- t1: tri('d1', 'd2'),
182
- t2: tri('d2', 'd0'),
183
- label: label(MARKET_SIZE + 8, 2, 'Market'),
184
- };
185
- }
186
- const single = (el) => ({
187
- a: el,
188
- });
189
- const nodeWithLabel = (kind, d, fill, name) => ({
190
- n: node(kind, 0, 0, d, fill),
191
- l: label(d + 8, d / 2 - 13, name),
192
- });
193
66
  const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
194
67
  const bgPreview = (extra = '') => `<svg ${ATTRS} fill="none"><path d="M22 12 V64 H120" stroke="#3b3d42" stroke-width="2"/><path d="M44 12 V64 M68 12 V64 M94 12 V64" stroke="#9aa0a6" stroke-width="0.8"/>${extra}</svg>`;
195
68
  const dotPreview = (fill, sw = 2) => `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="${fill}" stroke="#1f2328" stroke-width="${sw}"/></svg>`;
69
+ /** The two fat arrows: the same outline, mirrored — the direction IS the kind. */
70
+ const arrowPreview = (rightwards) => `<svg ${ATTRS} fill="none"><path d="${rightwards
71
+ ? 'M32 32 H80 V23 L103 40 L80 57 V48 H32 Z'
72
+ : 'M103 32 H55 V23 L32 40 L55 57 V48 H103 Z'}" fill="#bfbfbf" stroke="#1f2328" stroke-width="1.8" stroke-linejoin="round"/></svg>`;
73
+ /** The zone, in its Peace wash — the pair differs on the outline and on nothing else. */
74
+ const areaPreview = (outline) => `<svg ${ATTRS} fill="none">${outline}</svg>`;
75
+ /** A hand-composed template — what is left once the artefacts are derived. */
196
76
  function tpl(name, preview, elements) {
197
77
  return {
198
78
  name,
@@ -201,27 +81,55 @@ function tpl(name, preview, elements) {
201
81
  content: makeTemplateSnapshot(elements, name),
202
82
  };
203
83
  }
84
+ /**
85
+ * A zone, plus the one thing a snapshot cannot say.
86
+ *
87
+ * `createWardleyArea` lowers the zone the moment it exists, to just above the
88
+ * framework backgrounds it covers — otherwise the wash sits on top of every
89
+ * component it groups and eats their clicks. That depth is relative to the map
90
+ * ALREADY ON THE BOARD, which a snapshot knows nothing about, so the panel
91
+ * replays it on the freshly inserted element instead.
92
+ */
93
+ function areaTemplate(id, preview) {
94
+ return {
95
+ ...templateFromCommand(byId(id), preview),
96
+ afterInsert: (std, insertedIds) => lowerWardleyArea(std.get(GfxControllerIdentifier), insertedIds[0]),
97
+ };
98
+ }
204
99
  export const wardleyTemplateCategory = {
205
100
  name: 'Wardley',
206
101
  templates: [
207
102
  ...wardleyMaps,
208
- tpl('Map background', bgPreview(), { bg: bg('classic') }),
209
- tpl('Opportunity gradient', bgPreview('<rect x="22" y="12" width="98" height="52" fill="#eef4fb" opacity="0.6"/>'), { bg: bg('opportunity') }),
210
- tpl('Benefit gradient', bgPreview('<rect x="22" y="12" width="98" height="26" fill="#e6eef8" opacity="0.6"/>'), { bg: bg('benefit') }),
211
- tpl('Evolution gradient', bgPreview('<rect x="22" y="12" width="98" height="52" fill="#e3e2e4" opacity="0.5"/>'), { bg: bg('evolution-gradient') }),
212
- tpl('Component', dotPreview('#ffffff', 1.5), nodeWithLabel('component', NODE_SIZE, NODE_FILL, 'Component')),
213
- tpl('Anchor', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="36" r="3.5" fill="#1f2328"/><path d="M59 48 q8 -9 16 0" stroke="#1f2328" stroke-width="1.5" fill="none"/></svg>`, nodeWithLabel('anchor', NODE_SIZE, NODE_FILL, 'Anchor')),
214
- tpl('Ecosystem', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="11" fill="none" stroke="#1f2328"/><circle cx="67" cy="40" r="5" fill="#fff" stroke="#1f2328"/></svg>`, nodeWithLabel('ecosystem', ECOSYSTEM_SIZE, NODE_FILL, 'Ecosystem')),
215
- tpl('Method', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#d9d9d9" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="7" fill="#fff" stroke="#1f2328"/></svg>`, nodeWithLabel('method', METHOD_SIZE, METHOD_FILL, 'Component')),
216
- tpl('Pipeline', `<svg ${ATTRS} fill="none"><rect x="34" y="40" width="66" height="14" fill="#fff" stroke="#1f2328"/><rect x="60" y="33" width="14" height="14" fill="#fff" stroke="#1f2328"/></svg>`, pipeline()),
217
- tpl('Market', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="16" fill="#fff" stroke="#1f2328"/><circle cx="67" cy="30" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="75" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="59" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><path d="M67 30 L75 46 L59 46 Z" stroke="#1f2328" stroke-width="0.8" fill="none"/></svg>`, market()),
218
- tpl('Inertia', `<svg ${ATTRS} fill="none"><rect x="63" y="22" width="8" height="36" fill="#1f2328"/></svg>`, single(inertia())),
103
+ templateFromCommand(byId('wardley.addBackground'), bgPreview(), 'Map background'),
104
+ templateFromCommand(byId('wardley.addOpportunityBackground'), bgPreview('<rect x="22" y="12" width="98" height="52" fill="#eef4fb" opacity="0.6"/>'), 'Opportunity gradient'),
105
+ templateFromCommand(byId('wardley.addBenefitBackground'), bgPreview('<rect x="22" y="12" width="98" height="26" fill="#e6eef8" opacity="0.6"/>'), 'Benefit gradient'),
106
+ templateFromCommand(byId('wardley.addEvolutionBackground'), bgPreview('<rect x="22" y="12" width="98" height="52" fill="#e3e2e4" opacity="0.5"/>'), 'Evolution gradient'),
107
+ templateFromCommand(byId('wardley.addComponent'), dotPreview('#ffffff', 1.5), 'Component'),
108
+ templateFromCommand(byId('wardley.addMethod'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#d9d9d9" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="7" fill="#fff" stroke="#1f2328"/></svg>`, 'Method'),
109
+ templateFromCommand(byId('wardley.addMarket'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="16" fill="#fff" stroke="#1f2328"/><circle cx="67" cy="30" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="75" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="59" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><path d="M67 30 L75 46 L59 46 Z" stroke="#1f2328" stroke-width="0.8" fill="none"/></svg>`, 'Market'),
110
+ templateFromCommand(byId('wardley.addEcosystem'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="11" fill="none" stroke="#1f2328"/><circle cx="67" cy="40" r="5" fill="#fff" stroke="#1f2328"/></svg>`, 'Ecosystem'),
111
+ templateFromCommand(byId('wardley.addAnchor'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="36" r="3.5" fill="#1f2328"/><path d="M59 48 q8 -9 16 0" stroke="#1f2328" stroke-width="1.5" fill="none"/></svg>`, 'Anchor'),
112
+ templateFromCommand(byId('wardley.addPipeline'), `<svg ${ATTRS} fill="none"><rect x="34" y="40" width="66" height="14" fill="#fff" stroke="#1f2328"/><rect x="60" y="33" width="14" height="14" fill="#fff" stroke="#1f2328"/></svg>`, 'Pipeline'),
113
+ templateFromCommand(byId('wardley.addInertia'), `<svg ${ATTRS} fill="none"><rect x="63" y="22" width="8" height="36" fill="#1f2328"/></svg>`, 'Inertia'),
114
+ templateFromCommand(byId('wardley.addPorter'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><g stroke="${WARDLEY_RED}" stroke-width="2" stroke-linecap="round"><line x1="67" y1="26" x2="67" y2="19"/><line x1="81" y1="40" x2="88" y2="40"/><line x1="67" y1="54" x2="67" y2="61"/><line x1="53" y1="40" x2="46" y2="40"/></g><g fill="${WARDLEY_RED}"><path d="M67 13 L71 21 H63 Z"/><path d="M94 40 L86 44 V36 Z"/><path d="M67 67 L63 59 H71 Z"/><path d="M40 40 L48 36 V44 Z"/></g></svg>`),
115
+ templateFromCommand(byId('wardley.addAccelerator'), arrowPreview(true)),
116
+ templateFromCommand(byId('wardley.addDecelerator'), arrowPreview(false)),
219
117
  // NEUTRAL on purpose (`docs/adr/0010` § Compatibility). This is a horizontal
220
118
  // stroke bound to nothing — a sample of a STYLE, in a palette. It was typed
221
119
  // `wardley:dependency` only because the helper defaults to that role, and a
222
120
  // sample of a stroke makes no claim about who depends on whom. Same call the
223
121
  // market glyph's own wiring already makes.
224
- tpl('Link', `<svg ${ATTRS} fill="none"><path d="M24 40 H110" stroke="#666" stroke-width="2.4"/></svg>`, single(connect({ position: [0, 0] }, { position: [160, 0] }, { typed: false }))),
225
- tpl('Evolution arrow', `<svg ${ATTRS} fill="none"><path d="M24 40 H100" stroke="#d6455d" stroke-width="2.4" stroke-dasharray="6 4"/><path d="M98 33 L112 40 L98 47 Z" fill="#d6455d"/></svg>`, single(connect({ position: [0, 0] }, { position: [160, 0] }, { evolution: true, typed: false }))),
122
+ //
123
+ // Hand-written, and the one pair that cannot derive: `linkTool` and
124
+ // `evolutionArrow` ACTIVATE a tool rather than draw anything, so there is no
125
+ // artefact to record — the user draws it.
126
+ tpl('Link', `<svg ${ATTRS} fill="none"><path d="M24 40 H110" stroke="#666" stroke-width="2.4"/></svg>`, {
127
+ a: connect({ position: [0, 0] }, { position: [160, 0] }, { typed: false }),
128
+ }),
129
+ tpl('Evolution arrow', `<svg ${ATTRS} fill="none"><path d="M24 40 H100" stroke="#d6455d" stroke-width="2.4" stroke-dasharray="6 4"/><path d="M98 33 L112 40 L98 47 Z" fill="#d6455d"/></svg>`, {
130
+ a: connect({ position: [0, 0] }, { position: [160, 0] }, { evolution: true, typed: false }),
131
+ }),
132
+ areaTemplate('wardley.addAreaRect', areaPreview('<rect x="24" y="16" width="87" height="48" rx="2" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5"/>')),
133
+ areaTemplate('wardley.addAreaPolygon', areaPreview('<path d="M67 12 L110 43 L94 68 H40 L24 43 Z" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5" stroke-linejoin="round"/>')),
226
134
  ],
227
135
  };
@@ -1,8 +1,9 @@
1
- import { makeTemplateSnapshot, surfaceText, } from '@formicoidea/labre-core/gfx/template';
1
+ import { makeTemplateSnapshot, surfaceText, surfaceYMap, } from '@formicoidea/labre-core/gfx/template';
2
2
  import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
3
- import { INERTIA_COLOR, LABEL_FONT_SIZE, LINK_GREY, LINK_STROKE_WIDTH, NODE_FILL, NODE_SIZE, NODE_STROKE, WARDLEY_RED, } from '../node/consts.js';
3
+ import { LINK_GREY, LINK_STROKE_WIDTH, NODE_FILL, NODE_SIZE, NODE_STROKE, WARDLEY_RED, } from '../node/consts.js';
4
4
  import { backgroundPlot, backgroundZoneBoundaries, } from '@formicoidea/labre-core/blocks/surface';
5
5
  import { WARDLEY_BACKGROUND } from '../background.js';
6
+ import { WARDLEY_LABEL_H, wardleyInertiaProps, wardleyLabelProps, wardleyNodeProps, } from '../presets.js';
6
7
  import { WARDLEY_ROLE } from '../roles.js';
7
8
  /**
8
9
  * Authoring kit for canonical Wardley maps. Positions are given as
@@ -68,71 +69,73 @@ const bg = (variant = 'classic') => ({
68
69
  variant,
69
70
  xywh: `[0,0,${W},${H}]`,
70
71
  });
72
+ /**
73
+ * A component of a shipped map, at (evolution, value).
74
+ *
75
+ * {@link wardleyNodeProps} says what a component IS; the two overrides are
76
+ * NOTATION and stay: a thick rim marks a user "need", and a red one marks the
77
+ * future position of a capability that has not moved yet.
78
+ */
71
79
  function dot(e, v, sw, stroke = NODE_STROKE, fill = NODE_FILL) {
72
80
  const cx = ex(e);
73
81
  const cy = vy(v);
74
82
  return {
75
- type: 'wardleyNode',
76
- kind: 'component',
77
- // Templates carry the same semantic roles as the toolbox, so a map started
78
- // from a preset validates exactly like a hand-drawn one.
79
- role: WARDLEY_ROLE.component,
80
- shapeType: 'ellipse',
81
- filled: true,
83
+ ...wardleyNodeProps('component', {
84
+ xywh: `[${cx - D / 2},${cy - D / 2},${D},${D}]`,
85
+ }),
82
86
  fillColor: fill,
83
87
  strokeColor: stroke,
84
88
  strokeWidth: sw,
85
- shapeStyle: ShapeStyle.General,
86
- roughness: 0,
87
- xywh: `[${cx - D / 2},${cy - D / 2},${D},${D}]`,
88
89
  };
89
90
  }
90
91
  const comp = (e, v) => dot(e, v, 1);
91
92
  const future = (e, v) => dot(e, v, 2, WARDLEY_RED);
93
+ /** A stakeholder: an anchor, drawn a third bigger than a component. */
92
94
  function stake(e, v) {
93
95
  const cx = ex(e);
94
96
  const cy = vy(v);
95
97
  const d = 24;
96
- return {
97
- type: 'wardleyNode',
98
- kind: 'anchor',
99
- role: WARDLEY_ROLE.anchor,
100
- shapeType: 'ellipse',
101
- filled: true,
102
- fillColor: NODE_FILL,
103
- strokeColor: NODE_STROKE,
104
- strokeWidth: 1,
105
- shapeStyle: ShapeStyle.General,
106
- roughness: 0,
98
+ return wardleyNodeProps('anchor', {
107
99
  xywh: `[${cx - d / 2},${cy - d / 2},${d},${d}]`,
108
- };
100
+ });
109
101
  }
110
102
  function lbl(e, v, text, o = {}) {
111
103
  const cx = ex(e);
112
104
  const cy = vy(v);
105
+ // 200 rather than the toolbox's `WARDLEY_LABEL_W`: a shipped map names
106
+ // things like "Capture a moment", and a box narrower than its words wraps
107
+ // them onto two lines. The width is layout, not notation — what the preset
108
+ // owns is everything else about the label.
113
109
  const w = o.w ?? 200;
114
110
  const dx = o.dx ?? 12;
115
111
  const dy = o.dy ?? -10;
116
112
  const align = o.align ?? 'left';
117
113
  const x = align === 'right' ? cx - w - dx : align === 'center' ? cx - w / 2 : cx + dx;
118
114
  return {
119
- type: 'text',
120
- text: surfaceText(text),
121
- // The NAME of an artefact, so it carries the label role W3 is written on.
115
+ // The NAME of an artefact — {@link wardleyLabelProps} says what that is.
122
116
  // The free texts these presets also use for notes and legends stay neutral:
123
117
  // they name nothing and nothing measures them.
124
- role: WARDLEY_ROLE.label,
125
- color: o.color ?? NODE_STROKE,
126
- fontFamily: FontFamily.Inter,
127
- fontSize: o.size ?? LABEL_FONT_SIZE,
128
- textAlign: align === 'right'
129
- ? TextAlign.Right
130
- : align === 'center'
131
- ? TextAlign.Center
132
- : TextAlign.Left,
133
- xywh: `[${x},${cy + dy},${w},26]`,
118
+ ...wardleyLabelProps(text, x, cy + dy, align),
119
+ // A snapshot stores the words as a serialized `Y.Text`, where
120
+ // `addElement` takes a plain string.
121
+ text: surfaceText(text),
122
+ // A wider box for a long name; the deliberate departures a shipped map
123
+ // makes on top of the preset.
124
+ xywh: `[${x},${cy + dy},${w},${WARDLEY_LABEL_H}]`,
125
+ ...(o.size === undefined ? {} : { fontSize: o.size }),
126
+ ...(o.color === undefined ? {} : { color: o.color }),
127
+ ...(o.neutral ? { role: undefined } : {}),
134
128
  };
135
129
  }
130
+ /**
131
+ * The group a node and its name travel as — what the toolbox has written since
132
+ * #51 and what these maps had never carried, so dragging a component out of a
133
+ * shipped map left its name behind.
134
+ */
135
+ const pair = (node, label) => ({
136
+ type: 'group',
137
+ children: surfaceYMap({ [node]: true, [label]: true }),
138
+ });
136
139
  /**
137
140
  * A link between two nodes of a shipped map.
138
141
  *
@@ -172,19 +175,7 @@ function link(a, b, o = {}) {
172
175
  function inertia(e, v) {
173
176
  const cx = ex(e);
174
177
  const cy = vy(v);
175
- return {
176
- type: 'shape',
177
- shapeType: 'rect',
178
- role: WARDLEY_ROLE.inertia,
179
- filled: true,
180
- fillColor: INERTIA_COLOR,
181
- strokeColor: INERTIA_COLOR,
182
- strokeWidth: 0,
183
- shapeStyle: ShapeStyle.General,
184
- roughness: 0,
185
- radius: 0,
186
- xywh: `[${cx - 4},${cy - 22},8,44]`,
187
- };
178
+ return wardleyInertiaProps({ xywh: `[${cx - 4},${cy - 22},8,44]` });
188
179
  }
189
180
  function panel(x, y, w, h) {
190
181
  return {
@@ -274,34 +265,49 @@ function teaShop() {
274
265
  dy: -28,
275
266
  w: 120,
276
267
  }),
268
+ businessG: pair('business', 'businessL'),
277
269
  public: stake(0.78, 0.93),
278
270
  publicL: lbl(0.78, 0.93, 'Public', { align: 'center', dy: -28, w: 120 }),
271
+ publicG: pair('public', 'publicL'),
279
272
  cupOfTea: comp(0.62, 0.74),
280
273
  cupOfTeaL: lbl(0.62, 0.74, 'Cup of Tea', { align: 'right' }),
274
+ cupOfTeaG: pair('cupOfTea', 'cupOfTeaL'),
281
275
  cup: comp(0.8, 0.7),
282
276
  cupL: lbl(0.8, 0.7, 'Cup'),
277
+ cupG: pair('cup', 'cupL'),
283
278
  tea: comp(0.83, 0.6),
284
279
  teaL: lbl(0.83, 0.6, 'Tea'),
280
+ teaG: pair('tea', 'teaL'),
285
281
  hotWater: comp(0.8, 0.47),
286
282
  hotWaterL: lbl(0.8, 0.47, 'Hot Water'),
283
+ hotWaterG: pair('hotWater', 'hotWaterL'),
287
284
  water: comp(0.81, 0.34),
288
285
  waterL: lbl(0.81, 0.34, 'Water'),
286
+ waterG: pair('water', 'waterL'),
289
287
  kettle: comp(0.36, 0.38),
290
288
  kettleL: lbl(0.36, 0.38, 'Kettle', { align: 'right', dy: 6 }),
289
+ kettleG: pair('kettle', 'kettleL'),
291
290
  electric: future(0.56, 0.38),
292
291
  electricL: lbl(0.56, 0.38, 'Electric Kettle'),
292
+ electricG: pair('electric', 'electricL'),
293
293
  power: comp(0.7, 0.1),
294
294
  powerL: lbl(0.7, 0.1, 'Power', { align: 'right', dy: 6 }),
295
+ powerG: pair('power', 'powerL'),
295
296
  powerFut: future(0.88, 0.1),
296
297
  powerFutL: lbl(0.88, 0.1, 'Power'),
298
+ powerFutG: pair('powerFut', 'powerFutL'),
297
299
  // ABOVE the link it annotates, not across it. Written on the line it reads
298
300
  // as a label nobody can read — which is the finding W3 raises, and it was
299
301
  // raising it on the map that ships as the canonical example.
302
+ //
303
+ // NEUTRAL: it is a remark about a LINK, not the name of an artefact, so it
304
+ // travels with nothing and nothing measures it (see `LblOpts.neutral`).
300
305
  limitedBy: lbl(0.56, 0.43, 'limited by', {
301
306
  align: 'center',
302
307
  w: 120,
303
308
  size: 13,
304
309
  dy: -34,
310
+ neutral: true,
305
311
  }),
306
312
  ann1a: ann(0.5, 0.385),
307
313
  ann1t: annTxt(0.5, 0.385, '1'),
@@ -337,6 +343,7 @@ function kodak() {
337
343
  title: title("Wardley map of Kodak's 2005 inertia to digital"),
338
344
  user: stake(0.54, 0.92),
339
345
  userL: lbl(0.54, 0.92, 'User'),
346
+ userG: pair('user', 'userL'),
340
347
  capture: dot(CAPTURE[0], CAPTURE[1], 3),
341
348
  // To the LEFT, like the other two capability names: to the right of this
342
349
  // node runs the future dependency towards digital storage, and a name
@@ -345,16 +352,21 @@ function kodak() {
345
352
  captureL: lbl(CAPTURE[0], CAPTURE[1], 'Capture a moment', {
346
353
  align: 'right',
347
354
  }),
355
+ captureG: pair('capture', 'captureL'),
348
356
  film: comp(0.52, 0.62),
349
357
  filmL: lbl(0.52, 0.62, 'Film camera', { align: 'right' }),
358
+ filmG: pair('film', 'filmL'),
350
359
  digital: future(0.74, 0.62),
351
360
  digitalL: lbl(0.74, 0.62, 'Digital camera', { color: WARDLEY_RED }),
361
+ digitalG: pair('digital', 'digitalL'),
352
362
  roll: comp(0.52, 0.4),
353
363
  rollL: lbl(0.52, 0.4, 'Photographic film', { align: 'right' }),
364
+ rollG: pair('roll', 'rollL'),
354
365
  storage: future(STORAGE[0], STORAGE[1]),
355
366
  storageL: lbl(STORAGE[0], STORAGE[1], 'Digital storage', {
356
367
  color: WARDLEY_RED,
357
368
  }),
369
+ storageG: pair('storage', 'storageL'),
358
370
  inertiaBar: inertia(barE, barV),
359
371
  l1: link('user', 'capture'),
360
372
  l2: link('capture', 'film'),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-framework-wardley",
3
3
  "description": "Labre wardley framework for @formicoidea/labre-core.",
4
- "version": "0.36.0",
4
+ "version": "0.38.1",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -31,7 +31,7 @@
31
31
  "dist"
32
32
  ],
33
33
  "dependencies": {
34
- "@formicoidea/labre-core": "0.36.0",
34
+ "@formicoidea/labre-core": "0.38.1",
35
35
  "lit": "^3.2.0"
36
36
  }
37
37
  }