@formicoidea/labre-framework-wardley 0.35.0 → 0.36.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/presets.js CHANGED
@@ -1,6 +1,6 @@
1
- import { ConnectorMode, PointStyle, ShapeStyle, StrokeStyle, } from '@formicoidea/labre-core/model';
1
+ import { ConnectorMode, DEFAULT_POLYGON_VERTICES, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
- import { ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, LABEL_DEFAULT, 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, } 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, 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
@@ -18,6 +18,38 @@ export const WARDLEY_NODE_SIZE = {
18
18
  method: { w: METHOD_SIZE, h: METHOD_SIZE },
19
19
  market: { w: MARKET_SIZE, h: MARKET_SIZE },
20
20
  pipeline: { w: PIPELINE_WIDTH, h: PIPELINE_HEIGHT },
21
+ porter: { w: PORTER_SIZE, h: PORTER_SIZE },
22
+ accelerator: ACCELERATOR_SIZE,
23
+ decelerator: ACCELERATOR_SIZE,
24
+ // The RECT's size, because a kind gets one entry and the rect is the common
25
+ // area. The polygon's is {@link WARDLEY_AREA_SIZE} right below — the one
26
+ // place where a kind's canonical box depends on the shape it is drawn as.
27
+ area: AREA_RECT_SIZE,
28
+ };
29
+ /**
30
+ * The size an area is born at, per shape.
31
+ *
32
+ * Beside {@link WARDLEY_NODE_SIZE} rather than in it, because the table above
33
+ * is keyed by KIND and these two artefacts are one kind: a rectangle 240 × 160
34
+ * covers a band of the map, and the polygon is born square so its default
35
+ * pentagon is regular. `WARDLEY_NODE_SIZE.area` is the rect's, so anything that
36
+ * asks a kind for its canonical box — the morph does — still gets an answer.
37
+ */
38
+ export const WARDLEY_AREA_SIZE = {
39
+ rect: AREA_RECT_SIZE,
40
+ polygon: AREA_POLYGON_SIZE,
41
+ };
42
+ /**
43
+ * The kinds drawn as a native POLYGON, and the outline each one is.
44
+ *
45
+ * The table {@link wardleyNodeProps} reads to decide the shape type, so a kind's
46
+ * membership here is the whole of what makes it an arrow rather than a circle.
47
+ * A fresh array per call: `vertices` goes into a document, and a shared frozen
48
+ * literal handed to two elements would be one array two elements point at.
49
+ */
50
+ const NODE_VERTICES_OF = {
51
+ accelerator: ACCELERATOR_VERTICES,
52
+ decelerator: DECELERATOR_VERTICES,
21
53
  };
22
54
  /**
23
55
  * The words a kind is created NEXT TO — the prompt an artefact nobody has named
@@ -30,6 +62,8 @@ export const WARDLEY_NODE_LABEL = {
30
62
  method: METHOD_LABEL,
31
63
  market: MARKET_LABEL,
32
64
  pipeline: PIPELINE_LABEL,
65
+ accelerator: ACCELERATOR_LABEL,
66
+ decelerator: DECELERATOR_LABEL,
33
67
  };
34
68
  /**
35
69
  * The fill a kind is born with. White for every circle that means "a thing on
@@ -45,6 +79,29 @@ const NODE_FILL_OF = {
45
79
  // White at ~60% opacity: a pipeline is a bar you place components ON, so the
46
80
  // map has to stay visible through it. The 1px border stays opaque.
47
81
  pipeline: PIPELINE_FILL,
82
+ // Opaque white, like every other circle: the RED is in the four arrows, and a
83
+ // filled disk would fight the letter it is there to carry.
84
+ porter: NODE_FILL,
85
+ // Flat grey. The reference draws these arrows with a gradient; this canvas
86
+ // has no gradient fill, so a solid mid-grey under a thick dark border is what
87
+ // reads as the same solid arrow at the zoom an architect works at.
88
+ accelerator: ACCELERATOR_FILL,
89
+ decelerator: ACCELERATOR_FILL,
90
+ // Peace light at ~25 % opacity, for the reason the pipeline's white wash
91
+ // gives: a zone is drawn OVER the components it groups, so the map has to
92
+ // stay readable through it. The 1px border stays opaque.
93
+ area: AREA_FILL,
94
+ };
95
+ /**
96
+ * The rim colour of the kinds that do not wear the house one.
97
+ *
98
+ * A table with a single entry, and it is the honest way to say what an area is:
99
+ * every other artefact is a black-rimmed drawing of a thing in the value chain,
100
+ * and a zone is a coloured wash that FRAMES those drawings. A border in the
101
+ * same near-black would read as one more outline on a map already full of them.
102
+ */
103
+ const NODE_STROKE_OF = {
104
+ area: AREA_STROKE,
48
105
  };
49
106
  /** A box centred on a point — how every Wardley creation site places a node. */
50
107
  export function wardleyCenteredBox(cx, cy, w, h) {
@@ -58,18 +115,27 @@ export function wardleyCanonicalBox(kind, cx, cy) {
58
115
  /**
59
116
  * Every prop a Wardley node is created with, for one kind and one box.
60
117
  *
61
- * Five of the six are a native ELLIPSE and differ only in fill and size: the
118
+ * Most of the pack is a native ELLIPSE and differs only in fill and size: the
62
119
  * glyph — the anchor's silhouette, the ecosystem's hatched donut, the method's
63
120
  * inner circle — is drawn by the renderer from `kind`, off this same model, so
64
121
  * stroke and fill stay editable from the shape toolbar exactly like any other
65
- * shape's. The pipeline is the exception and the reason `clearOf` exists on this
66
- * pack: it is a RECT, and it is the only kind that writes `radius`.
122
+ * shape's. Two families are the exception, and they are the reason `clearOf`
123
+ * exists on this pack: the pipeline and the area are RECTS and the only kinds
124
+ * that write `radius`, and the accelerator / decelerator are POLYGONS — the
125
+ * only kinds that write `vertices` and `isClosed` from here.
67
126
  *
68
127
  * No `text`: a Wardley label is a separate free-text element grouped with the
69
- * node (see `addLabel` in `actions.ts`), never words stored on the shape.
128
+ * node (see `addLabel` in `actions.ts`), never words stored on the shape. The
129
+ * porter and the area are the two exceptions, and neither writes it HERE: their
130
+ * inner text is content the author types, added at the creation site.
70
131
  */
71
132
  export function wardleyNodeProps(kind, box) {
72
- const rect = kind === 'pipeline';
133
+ // An area is born a RECT, and {@link wardleyAreaProps} is what turns the
134
+ // other half of the kind into a polygon: the two artefacts are one kind
135
+ // because the `shapeType` is the whole of what differs between them, so the
136
+ // common description lives here and the outline is spread over it.
137
+ const rect = kind === 'pipeline' || kind === 'area';
138
+ const outline = NODE_VERTICES_OF[kind];
73
139
  return {
74
140
  type: 'wardleyNode',
75
141
  kind,
@@ -77,11 +143,14 @@ export function wardleyNodeProps(kind, box) {
77
143
  // keeps driving the rendering. The role is what every rule reads — no rule
78
144
  // will ever look at a shape type.
79
145
  role: WARDLEY_ROLE[kind],
80
- shapeType: rect ? 'rect' : 'ellipse',
146
+ shapeType: outline ? 'polygon' : rect ? 'rect' : 'ellipse',
81
147
  filled: true,
82
148
  fillColor: NODE_FILL_OF[kind],
83
- strokeColor: NODE_STROKE,
84
- strokeWidth: NODE_STROKE_WIDTH,
149
+ strokeColor: NODE_STROKE_OF[kind] ?? NODE_STROKE,
150
+ // A thick rim on the arrows, the house thin one everywhere else: an arrow
151
+ // with a 1px border reads as an outline drawing rather than as the solid
152
+ // arrow the reference draws.
153
+ strokeWidth: outline ? ACCELERATOR_STROKE_WIDTH : NODE_STROKE_WIDTH,
85
154
  shapeStyle: ShapeStyle.General,
86
155
  roughness: 0,
87
156
  // Square corners, and ONLY on the kind that has corners at all. Spread
@@ -89,9 +158,75 @@ export function wardleyNodeProps(kind, box) {
89
158
  // truth about the ellipses — and because a key some kinds write and others
90
159
  // do not is precisely what `wardleyMorphClears` is for.
91
160
  ...(rect ? { radius: 0 } : {}),
161
+ // The outline, and the fact that it closes. A fresh array of fresh pairs:
162
+ // this goes into a document, and two elements sharing one literal would be
163
+ // two elements sharing one array.
164
+ ...(outline
165
+ ? { vertices: outline.map(([x, y]) => [x, y]), isClosed: true }
166
+ : {}),
92
167
  xywh: box.xywh,
93
168
  };
94
169
  }
170
+ /**
171
+ * An AREA, as props — one kind, two shapes.
172
+ *
173
+ * The second discriminator this pack needed, and the reason it is a parameter
174
+ * rather than a second `kind`: a rectangular zone and a polygonal one are the
175
+ * same statement about the map ("all of this is one thing"), drawn with a
176
+ * different number of corners. A second kind would have to be kept in step in
177
+ * the role table, the legend, the export and the morph's key union, for a
178
+ * difference the `shapeType` already carries.
179
+ *
180
+ * Everything but the outline comes from {@link wardleyNodeProps}, which is the
181
+ * house rule: the fill, the rim and the role are the area's, wherever it is
182
+ * built from. What is added here is the two shapes and the fact that an area's
183
+ * NAME is its own inner text.
184
+ *
185
+ * ## The text is the name, and the zone never grows to fit it
186
+ *
187
+ * `TextFitMode.Overflow`, exactly as the porter's letter and the inertia bar:
188
+ * an area is a boundary drawn around real components, so a long name must never
189
+ * push that boundary out and swallow a component the author did not mean to
190
+ * include. Top-left, because a zone's name belongs in its corner rather than
191
+ * across the middle of the map it covers — and no `text` at all, so the editor
192
+ * a double-click opens starts on an empty line.
193
+ */
194
+ export function wardleyAreaProps(shape, box) {
195
+ const props = {
196
+ ...wardleyNodeProps('area', box),
197
+ // A thin rim under its own name, so the zone frames the map rather than
198
+ // adding one more black outline to it.
199
+ strokeWidth: AREA_STROKE_WIDTH,
200
+ // The name, as the shape's own text: left-aligned in the top corner, and a
201
+ // fit mode that never resizes the zone.
202
+ color: NODE_STROKE,
203
+ fontFamily: FontFamily.Inter,
204
+ fontSize: LABEL_FONT_SIZE,
205
+ textAlign: 'left',
206
+ textVerticalAlign: 'top',
207
+ textFitMode: TextFitMode.Overflow,
208
+ };
209
+ if (shape !== 'polygon')
210
+ return props;
211
+ props.shapeType = 'polygon';
212
+ // The editor's own default outline — a regular pentagon — copied FRESH,
213
+ // because `vertices` goes into a document and two elements sharing one
214
+ // literal would be two elements sharing one array. The author then moves the
215
+ // corners from the shape toolbar's vertex editor.
216
+ props.vertices = DEFAULT_POLYGON_VERTICES.map(([x, y]) => [x, y]);
217
+ props.isClosed = true;
218
+ // …and a polygon has no corner radius. Deleted rather than never written,
219
+ // because the base description is the RECT's — same idiom, and same reason,
220
+ // as `wardleyMorphProps`: a patch cannot express absence, so a key that means
221
+ // nothing here must not be sitting in the Y.Map saying it does.
222
+ delete props.radius;
223
+ return props;
224
+ }
225
+ /** Where an area of the given shape is born, centred on a point. */
226
+ export function wardleyAreaBox(shape, cx, cy) {
227
+ const { w, h } = WARDLEY_AREA_SIZE[shape];
228
+ return wardleyCenteredBox(cx, cy, w, h);
229
+ }
95
230
  /**
96
231
  * The pipeline's handle: a node-sized square straddling the body's top edge, and
97
232
  * the only place a connector may land on a pipeline.
@@ -180,6 +315,172 @@ export function wardleyMarketLinkPairs(dots) {
180
315
  [dots[2], dots[0]],
181
316
  ];
182
317
  }
318
+ /**
319
+ * The four arrow outlines, north first and then clockwise.
320
+ *
321
+ * Normalized, so they are the same seven points at every size — the BOX carries
322
+ * the scale — and one list per direction rather than one list plus a `rotate`,
323
+ * because a rotated element is one the selection, the resize handles and every
324
+ * bounding-box reader then have to de-rotate. Each is: tip, the two head
325
+ * corners, and the four corners of the shaft, with the head
326
+ * `headLength / length` of the way down and the shaft `width / headWidth` wide.
327
+ */
328
+ const PORTER_HEAD = PORTER_ARROW.headLength / PORTER_ARROW.length; // 0.4
329
+ const PORTER_SHAFT_LOW = (1 - PORTER_ARROW.width / PORTER_ARROW.headWidth) / 2;
330
+ const PORTER_SHAFT_HIGH = 1 - PORTER_SHAFT_LOW;
331
+ const PORTER_VERTICES = [
332
+ // North — the tip at the top edge, the shaft running back down to the rim.
333
+ [
334
+ [0.5, 0],
335
+ [1, PORTER_HEAD],
336
+ [PORTER_SHAFT_HIGH, PORTER_HEAD],
337
+ [PORTER_SHAFT_HIGH, 1],
338
+ [PORTER_SHAFT_LOW, 1],
339
+ [PORTER_SHAFT_LOW, PORTER_HEAD],
340
+ [0, PORTER_HEAD],
341
+ ],
342
+ // East — the same outline turned a quarter, written out rather than rotated.
343
+ [
344
+ [1, 0.5],
345
+ [1 - PORTER_HEAD, 1],
346
+ [1 - PORTER_HEAD, PORTER_SHAFT_HIGH],
347
+ [0, PORTER_SHAFT_HIGH],
348
+ [0, PORTER_SHAFT_LOW],
349
+ [1 - PORTER_HEAD, PORTER_SHAFT_LOW],
350
+ [1 - PORTER_HEAD, 0],
351
+ ],
352
+ // South.
353
+ [
354
+ [0.5, 1],
355
+ [0, 1 - PORTER_HEAD],
356
+ [PORTER_SHAFT_LOW, 1 - PORTER_HEAD],
357
+ [PORTER_SHAFT_LOW, 0],
358
+ [PORTER_SHAFT_HIGH, 0],
359
+ [PORTER_SHAFT_HIGH, 1 - PORTER_HEAD],
360
+ [1, 1 - PORTER_HEAD],
361
+ ],
362
+ // West.
363
+ [
364
+ [0, 0.5],
365
+ [PORTER_HEAD, 0],
366
+ [PORTER_HEAD, PORTER_SHAFT_LOW],
367
+ [1, PORTER_SHAFT_LOW],
368
+ [1, PORTER_SHAFT_HIGH],
369
+ [PORTER_HEAD, PORTER_SHAFT_HIGH],
370
+ [PORTER_HEAD, 1],
371
+ ],
372
+ ];
373
+ /** Whether a direction runs along X — the axis its box is long on. */
374
+ const PORTER_HORIZONTAL = [false, true, false, true];
375
+ /** Unit vector per direction, north first and then clockwise. */
376
+ const PORTER_DIRECTIONS = [
377
+ [0, -1],
378
+ [1, 0],
379
+ [0, 1],
380
+ [-1, 0],
381
+ ];
382
+ /**
383
+ * The four arrows of a Porter's-forces glyph, for a circle of the given radius
384
+ * centred on (cx, cy) — a box and an outline each.
385
+ *
386
+ * The ONE description of the geometry, so the creation site, the map legend and
387
+ * the tests read the same numbers rather than three sets that agreed the day
388
+ * they were written. Each arrow starts `PORTER_ARROW.gap` clear of the rim —
389
+ * the arrows push against the circle, they do not touch it — and runs
390
+ * `PORTER_ARROW.length` further out, tip at the far end.
391
+ *
392
+ * Every distance scales WITH the radius, and that is what makes a legend row the
393
+ * same drawing at another size: a 12-unit glyph carrying four map-sized arrows
394
+ * would not be a small Porter, it would be a circle somebody stabbed.
395
+ */
396
+ export function wardleyPorterArrows(cx, cy, radius = PORTER_SIZE / 2) {
397
+ const scale = radius / (PORTER_SIZE / 2);
398
+ const near = radius + PORTER_ARROW.gap * scale;
399
+ const long = PORTER_ARROW.length * scale;
400
+ const across = PORTER_ARROW.headWidth * scale;
401
+ return PORTER_DIRECTIONS.map(([dx, dy], index) => {
402
+ const horizontal = PORTER_HORIZONTAL[index];
403
+ const w = horizontal ? long : across;
404
+ const h = horizontal ? across : long;
405
+ // The near edge of the box sits `near` from the centre along the direction;
406
+ // the box then extends outward, and is centred on the other axis.
407
+ const x = horizontal ? (dx > 0 ? cx + near : cx - near - long) : cx - w / 2;
408
+ const y = horizontal ? cy - h / 2 : dy > 0 ? cy + near : cy - near - long;
409
+ return {
410
+ xywh: new Bound(x, y, w, h).serialize(),
411
+ vertices: PORTER_VERTICES[index].map(([vx, vy]) => [vx, vy]),
412
+ };
413
+ });
414
+ }
415
+ /**
416
+ * One of those arrows as props: a filled POLYGON on a `wardleyNode`, and
417
+ * deliberately ROLE-LESS.
418
+ *
419
+ * A polygon and not a connector, which is the first fix the recette of #210
420
+ * asked for: a connector's triangle head is sized off its stroke width, so the
421
+ * heads arrived longer than the arrows and covered the circle, the letter and
422
+ * the double-click target. A polygon's outline is exactly the seven points
423
+ * above.
424
+ *
425
+ * A `wardleyNode` and not a plain `shape`, which is the second: recette v2
426
+ * found that `ShapeElementView` gives EVERY plain shape a double-click that
427
+ * mounts the inner-text editor, and mounting it grew an arrow from 24 units
428
+ * high to 44 — a deformation that survived Escape. An arrow is not a thing you
429
+ * write in. Posting it as a `wardleyNode` moves it onto `WardleyNodeView`,
430
+ * which opens an editor only on the artefact that HAS a letter; the renderer is
431
+ * unaffected, because a Wardley node draws itself through the native shape
432
+ * renderer and that renderer already dispatches on `shapeType`.
433
+ *
434
+ * Role-less, and `kind: 'porter'` with it, for the reason the market's three
435
+ * inner dots already give — they are `kind: 'component'` and carry no role: this
436
+ * is the glyph's own wiring rather than anything the author drew, so a role
437
+ * would make every composite report an overlap with itself (W3), would offer
438
+ * four arrows to the OWM writer, and would let a stray label bind to one.
439
+ * `undefined` writes no key at all, exactly as it does for those dots.
440
+ *
441
+ * Filled rather than stroked, so nothing about them is sized off a line width
442
+ * ever again.
443
+ */
444
+ export function wardleyPorterArrowProps(arrow) {
445
+ return {
446
+ type: 'wardleyNode',
447
+ kind: 'porter',
448
+ role: undefined,
449
+ shapeType: 'polygon',
450
+ vertices: arrow.vertices,
451
+ isClosed: true,
452
+ filled: true,
453
+ fillColor: WARDLEY_RED,
454
+ // The same red, so the arrow has no rim of another colour around it.
455
+ strokeColor: WARDLEY_RED,
456
+ strokeWidth: 0,
457
+ shapeStyle: ShapeStyle.General,
458
+ roughness: 0,
459
+ xywh: arrow.xywh,
460
+ };
461
+ }
462
+ /**
463
+ * The letter inside the circle, as shape-text props.
464
+ *
465
+ * A plain STRING and not a `Y.Text`: `ShapeElementModel.propsToY` builds the
466
+ * Y type on the way into the document, and this package depends on yjs only in
467
+ * its tests. `TextFitMode.Overflow` is the load-bearing line — the glyph has a
468
+ * canonical size that says "external force" at a glance, so the circle must
469
+ * never grow to fit whatever the author types into it, exactly as the inertia
470
+ * bar never deforms around its own text.
471
+ */
472
+ export function wardleyPorterLetterProps(letter, fontSize = PORTER_LETTER_FONT_SIZE) {
473
+ return {
474
+ text: letter,
475
+ color: NODE_STROKE,
476
+ fontFamily: FontFamily.Inter,
477
+ fontSize,
478
+ textAlign: 'center',
479
+ textHorizontalAlign: 'center',
480
+ textVerticalAlign: 'center',
481
+ textFitMode: TextFitMode.Overflow,
482
+ };
483
+ }
183
484
  /**
184
485
  * The box {@link wardleyMorphProps} hands {@link wardleyNodeProps} and then
185
486
  * throws away. Never written to a document: the morph places the element
@@ -207,13 +508,36 @@ export function wardleyMorphProps(kind) {
207
508
  return props;
208
509
  }
209
510
  /**
210
- * Every key ANY kind's props may carry — the union over the whole pack.
511
+ * The kinds that can actually MORPH — the one family `morph.ts` declares.
512
+ *
513
+ * Restated here rather than imported, and that is a circular-import dodge with
514
+ * a real reason behind it: `morph.ts` reads this module for the presets, so
515
+ * this module cannot read it back. `morph.unit.spec.ts` asserts the two lists
516
+ * are the same set, so the restatement cannot drift.
517
+ *
518
+ * It exists because {@link wardleyMorphClears} needs a UNION, and a union over
519
+ * the whole pack is the wrong one. Since the accelerator and the decelerator
520
+ * joined it, "every key any kind writes" includes `vertices` and `isClosed` —
521
+ * keys of two POLYGONS that are in no family and that nothing may ever morph
522
+ * into. Left unrestricted, morphing a market back to a component would emit a
523
+ * delete for `vertices` on an ellipse that never had one: harmless today, and a
524
+ * patch that says something untrue about the pack the day a morphable kind
525
+ * gains an outline.
526
+ */
527
+ export const WARDLEY_MORPHABLE_KINDS = [
528
+ 'component',
529
+ 'market',
530
+ 'ecosystem',
531
+ 'pipeline',
532
+ ];
533
+ /**
534
+ * Every key a MORPHABLE kind's props may carry — the union over that family.
211
535
  *
212
536
  * Computed rather than listed, so a preset that starts spreading a key
213
537
  * conditionally is covered on the day it is added rather than on the day
214
538
  * somebody notices.
215
539
  */
216
- const EVERY_MORPH_KEY = new Set(Object.keys(WARDLEY_NODE_SIZE).flatMap(kind => Object.keys(wardleyMorphProps(kind))));
540
+ const EVERY_MORPH_KEY = new Set(WARDLEY_MORPHABLE_KINDS.flatMap(kind => Object.keys(wardleyMorphProps(kind))));
217
541
  /**
218
542
  * The fields to DELETE after morphing to `kind` — the keys some other kind
219
543
  * writes and this one does not.
package/dist/roles.d.ts CHANGED
@@ -41,6 +41,10 @@ export declare const WARDLEY_ROLE: {
41
41
  readonly market: "wardley:market";
42
42
  readonly ecosystem: "wardley:ecosystem";
43
43
  readonly method: "wardley:method";
44
+ readonly porter: "wardley:porter";
45
+ readonly accelerator: "wardley:accelerator";
46
+ readonly decelerator: "wardley:decelerator";
47
+ readonly area: "wardley:area";
44
48
  readonly dependency: "wardley:dependency";
45
49
  readonly map: "wardley:map";
46
50
  readonly changeArrow: "wardley:change-arrow";
package/dist/roles.js CHANGED
@@ -7,6 +7,10 @@ export const WARDLEY_ROLE = {
7
7
  market: 'wardley:market',
8
8
  ecosystem: 'wardley:ecosystem',
9
9
  method: 'wardley:method',
10
+ porter: 'wardley:porter',
11
+ accelerator: 'wardley:accelerator',
12
+ decelerator: 'wardley:decelerator',
13
+ area: 'wardley:area',
10
14
  dependency: 'wardley:dependency',
11
15
  map: 'wardley:map',
12
16
  changeArrow: 'wardley:change-arrow',
@@ -54,6 +58,69 @@ const DEFS = [
54
58
  kind: 'node',
55
59
  labelKey: 'com.labre.wardley.role.method',
56
60
  },
61
+ /**
62
+ * A Porter's-forces glyph: one of the five forces of external competition
63
+ * bearing on the map (R relative competition, L struggle for survival, E
64
+ * struggle to establish — the letter inside the circle says which).
65
+ *
66
+ * NO `parent`, and that is the whole declaration rather than an omission. A
67
+ * market and an ecosystem specialise `component` because they ARE links in
68
+ * the value chain drawn at another grain. This one is not a link in it at
69
+ * all: it is a pressure applied from OUTSIDE, so no rule written on
70
+ * `wardley:component` — W3's overlap pairs above all — may ever match it. A
71
+ * force is allowed to sit on top of the components it presses against; that
72
+ * is what pressing against them looks like.
73
+ */
74
+ {
75
+ id: WARDLEY_ROLE.porter,
76
+ kind: 'node',
77
+ labelKey: 'com.labre.wardley.role.porter',
78
+ labelFallback: "Porter's forces",
79
+ },
80
+ /**
81
+ * An accelerator, and its mirror the decelerator: something that speeds
82
+ * evolution up, or slows it down.
83
+ *
84
+ * NO `parent`, for the reason the porter above gives and one of its own. An
85
+ * accelerator is not a link in the value chain drawn at another grain — it is
86
+ * an annotation of the CLIMATE, a statement about the pace at which the map
87
+ * moves rather than about what depends on what. So no rule written on
88
+ * `wardley:component` may match one: W3's overlap pairs above all, since an
89
+ * accelerator is drawn precisely where the components it accelerates are.
90
+ *
91
+ * Two roles and not one with a direction, because the two say opposite
92
+ * things: "this is going faster" and "this is being held back" are not the
93
+ * same claim read from either end, and a rule about one must never fall on
94
+ * the other.
95
+ */
96
+ {
97
+ id: WARDLEY_ROLE.accelerator,
98
+ kind: 'node',
99
+ labelKey: 'com.labre.wardley.role.accelerator',
100
+ labelFallback: 'Accelerator',
101
+ },
102
+ {
103
+ id: WARDLEY_ROLE.decelerator,
104
+ kind: 'node',
105
+ labelKey: 'com.labre.wardley.role.decelerator',
106
+ labelFallback: 'Decelerator',
107
+ },
108
+ /**
109
+ * An area: a zone of the map, drawn around the components it groups.
110
+ *
111
+ * NO `parent`, and for the sharpest version of the reason the porter and the
112
+ * accelerator already give. A zone is not a link in the value chain at any
113
+ * grain — it is a statement about a REGION of the map — and it is drawn
114
+ * precisely ON TOP of the components it names, so a rule written on
115
+ * `wardley:component` would report an overlap (W3) on every area that does
116
+ * its job. Covering the chain is what a zone IS.
117
+ */
118
+ {
119
+ id: WARDLEY_ROLE.area,
120
+ kind: 'node',
121
+ labelKey: 'com.labre.wardley.role.area',
122
+ labelFallback: 'Area',
123
+ },
57
124
  /**
58
125
  * The value-chain link: "A needs B".
59
126
  *
@@ -20,6 +20,42 @@ export declare const wardleyMarketIcon: import("lit-html").TemplateResult<2>;
20
20
  export declare const wardleyEcosystemIcon: import("lit-html").TemplateResult<2>;
21
21
  /** Component + method (validated METH-A): component inscribed in a colored outer circle (default grey). */
22
22
  export declare const wardleyMethodIcon: import("lit-html").TemplateResult<2>;
23
+ /**
24
+ * Porter's forces: a lettered circle pressed on from the four cardinal
25
+ * directions by short solid arrows.
26
+ *
27
+ * The arrows are RED like {@link wardleyArrowIcon}'s, because on this notation
28
+ * red is what "a force acting on the map" looks like; they are SOLID where the
29
+ * evolution arrow is dashed, which is the one thing that tells the two apart at
30
+ * 24 pixels. The circle keeps `currentColor` like every other artefact glyph.
31
+ */
32
+ export declare const wardleyPorterIcon: import("lit-html").TemplateResult<2>;
33
+ /**
34
+ * Accelerator / decelerator: the same fat arrow the canvas draws, at 24 px.
35
+ *
36
+ * `currentColor` outline over a light fill, like the other artefact glyphs —
37
+ * the canvas's flat grey would fight the toolbar's own theming, and the
38
+ * DIRECTION is what tells the two apart anyway, which is exactly what the two
39
+ * icons differ on and nothing else. The seven points are the outline of
40
+ * `ACCELERATOR_VERTICES` scaled onto the 24-unit box (a 22 × 18 arrow centred
41
+ * on it), so the button and the shape it draws are the same drawing.
42
+ */
43
+ export declare const wardleyAcceleratorIcon: import("lit-html").TemplateResult<2>;
44
+ /** The mirror of {@link wardleyAcceleratorIcon}: the same arrow, pointing left. */
45
+ export declare const wardleyDeceleratorIcon: import("lit-html").TemplateResult<2>;
46
+ /**
47
+ * Area (rectangle) and area (polygon): the zone, translucent, at 24 px.
48
+ *
49
+ * The pair differs on the OUTLINE and on nothing else, which is the whole of
50
+ * what the two commands differ on. A `currentColor` rim over a light fill like
51
+ * every other artefact glyph — the canvas draws the zone in Peace, but a
52
+ * coloured button would claim the colour is the notation, and it is a default
53
+ * the author changes from the toolbar. The pentagon is the editor's own default
54
+ * polygon, so the button is the shape the click produces.
55
+ */
56
+ export declare const wardleyAreaRectIcon: import("lit-html").TemplateResult<2>;
57
+ /** The same zone with corners you move: the editor's own default pentagon. */
58
+ export declare const wardleyAreaPolygonIcon: import("lit-html").TemplateResult<2>;
23
59
  /** Legend: a bordered box with glyph + line rows. */
24
60
  export declare const wardleyLegendIcon: import("lit-html").TemplateResult<2>;
25
61
  /** Opportunity gradient background: axes + green differential hump + red operational bump. */
@@ -98,6 +98,64 @@ export const wardleyMethodIcon = svg `<svg width="24" height="24" viewBox="0 0 2
98
98
  <circle cx="12" cy="12" r="9.6" fill="#d9d9d9" stroke="#1f2328" stroke-width="1.2"/>
99
99
  <circle cx="12" cy="12" r="4.6" fill="#fff" stroke="#1f2328" stroke-width="1.2"/>
100
100
  </svg>`;
101
+ /**
102
+ * Porter's forces: a lettered circle pressed on from the four cardinal
103
+ * directions by short solid arrows.
104
+ *
105
+ * The arrows are RED like {@link wardleyArrowIcon}'s, because on this notation
106
+ * red is what "a force acting on the map" looks like; they are SOLID where the
107
+ * evolution arrow is dashed, which is the one thing that tells the two apart at
108
+ * 24 pixels. The circle keeps `currentColor` like every other artefact glyph.
109
+ */
110
+ export const wardleyPorterIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
111
+ <circle cx="12" cy="12" r="5.4" fill="#fff" stroke="currentColor" stroke-width="1.3"/>
112
+ <g stroke="${RED}" stroke-width="1.4" stroke-linecap="round">
113
+ <line x1="12" y1="6" x2="12" y2="3.4"/>
114
+ <line x1="18" y1="12" x2="20.6" y2="12"/>
115
+ <line x1="12" y1="18" x2="12" y2="20.6"/>
116
+ <line x1="6" y1="12" x2="3.4" y2="12"/>
117
+ </g>
118
+ <g fill="${RED}">
119
+ <path d="M12 1.4 L14 4.4 H10 Z"/>
120
+ <path d="M22.6 12 L19.6 14 V10 Z"/>
121
+ <path d="M12 22.6 L10 19.6 H14 Z"/>
122
+ <path d="M1.4 12 L4.4 10 V14 Z"/>
123
+ </g>
124
+ </svg>`;
125
+ /**
126
+ * Accelerator / decelerator: the same fat arrow the canvas draws, at 24 px.
127
+ *
128
+ * `currentColor` outline over a light fill, like the other artefact glyphs —
129
+ * the canvas's flat grey would fight the toolbar's own theming, and the
130
+ * DIRECTION is what tells the two apart anyway, which is exactly what the two
131
+ * icons differ on and nothing else. The seven points are the outline of
132
+ * `ACCELERATOR_VERTICES` scaled onto the 24-unit box (a 22 × 18 arrow centred
133
+ * on it), so the button and the shape it draws are the same drawing.
134
+ */
135
+ export const wardleyAcceleratorIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
136
+ <path d="M1 8.0 H13.1 V3 L23 12 L13.1 21 V16 H1 Z" fill="#f0f0f0" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
137
+ </svg>`;
138
+ /** The mirror of {@link wardleyAcceleratorIcon}: the same arrow, pointing left. */
139
+ export const wardleyDeceleratorIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
140
+ <path d="M23 8.0 H10.9 V3 L1 12 L10.9 21 V16 H23 Z" fill="#f0f0f0" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
141
+ </svg>`;
142
+ /**
143
+ * Area (rectangle) and area (polygon): the zone, translucent, at 24 px.
144
+ *
145
+ * The pair differs on the OUTLINE and on nothing else, which is the whole of
146
+ * what the two commands differ on. A `currentColor` rim over a light fill like
147
+ * every other artefact glyph — the canvas draws the zone in Peace, but a
148
+ * coloured button would claim the colour is the notation, and it is a default
149
+ * the author changes from the toolbar. The pentagon is the editor's own default
150
+ * polygon, so the button is the shape the click produces.
151
+ */
152
+ export const wardleyAreaRectIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
153
+ <rect x="2.6" y="5" width="18.8" height="14" rx="1" fill="#f0f0f0" fill-opacity="0.8" stroke="currentColor" stroke-width="1.4"/>
154
+ </svg>`;
155
+ /** The same zone with corners you move: the editor's own default pentagon. */
156
+ export const wardleyAreaPolygonIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
157
+ <path d="M12 2.8 L21.4 9.6 L17.8 20.8 H6.2 L2.6 9.6 Z" fill="#f0f0f0" fill-opacity="0.8" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
158
+ </svg>`;
101
159
  /** Legend: a bordered box with glyph + line rows. */
102
160
  export const wardleyLegendIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
103
161
  <rect x="3" y="4" width="18" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="1.3"/>
@@ -1 +1,21 @@
1
+ import { type Color, type Palette, type ShapeElementModel } from '@formicoidea/labre-core/model';
2
+ /**
3
+ * What a picked swatch WRITES on a Wardley node.
4
+ *
5
+ * The identity on every artefact but one. A zone is drawn over the components
6
+ * it groups, so its fill is a WASH — `#c6dbfc40`, the alpha being the whole of
7
+ * what keeps the map readable underneath — and a picker that wrote the swatch
8
+ * as-is would replace it with an opaque hue and hide the map the zone annotates
9
+ * (recette of #213). So a swatch picked for an area keeps the zone's alpha.
10
+ *
11
+ * Only a bare 6-digit hex is touched: a value that already carries alpha is
12
+ * somebody's deliberate choice from the custom picker, and a theme token
13
+ * (`--affine-…`) is not a hex at all and must reach the document intact.
14
+ */
15
+ export declare function wardleyFillColor(model: ShapeElementModel, value: Color): Color;
16
+ /**
17
+ * From the default editor palette we keep ONLY the neutrals — the historical
18
+ * colours are dropped in favour of the Wardley swatches above.
19
+ */
20
+ export declare const WARDLEY_PALETTE_LIST: Palette[];
1
21
  export declare const wardleyNodeToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;