@formicoidea/labre-framework-wardley 0.35.0 → 0.37.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/legend.js CHANGED
@@ -3,7 +3,8 @@ import { ConnectorElementModel, ConnectorMode, FontFamily, PointStyle, ShapeElem
3
3
  import { Bound } from '@formicoidea/labre-core/global/gfx';
4
4
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
5
5
  import { GRADIENT_GREEN, GRADIENT_RED } from './gradient.js';
6
- import { INERTIA_COLOR, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, METHOD_FILL, NODE_FILL, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, WARDLEY_RED, } from './node/consts.js';
6
+ import { ACCELERATOR_FILL, ACCELERATOR_STROKE_WIDTH, ACCELERATOR_VERTICES, AREA_FILL, AREA_STROKE, AREA_STROKE_WIDTH, DECELERATOR_VERTICES, INERTIA_COLOR, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, METHOD_FILL, NODE_FILL, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PORTER_DEFAULT_LETTER, WARDLEY_RED, } from './node/consts.js';
7
+ import { wardleyPorterArrowProps, wardleyPorterArrows } from './presets.js';
7
8
  const LEGEND_ORDER = [
8
9
  'component',
9
10
  'anchor',
@@ -14,6 +15,10 @@ const LEGEND_ORDER = [
14
15
  'link',
15
16
  'arrow',
16
17
  'inertia',
18
+ 'porter',
19
+ 'accelerator',
20
+ 'decelerator',
21
+ 'area',
17
22
  ];
18
23
  /** Default (editable) descriptions for each legend row. */
19
24
  const LEGEND_DESC = {
@@ -26,6 +31,10 @@ const LEGEND_DESC = {
26
31
  link: 'Need relation (parent → child)',
27
32
  arrow: 'Evolution / movement (red = future)',
28
33
  inertia: 'Inertia to change',
34
+ porter: "Porter's forces (external competition: R relative, L survival, E establish)",
35
+ accelerator: 'Accelerator (speeds evolution up)',
36
+ decelerator: 'Decelerator (slows evolution down)',
37
+ area: 'Area (zone of the map)',
29
38
  };
30
39
  /** Gradient-meaning block, keyed by variant (caption + 2-colour swatch). */
31
40
  const LEGEND_GRADIENT = {
@@ -42,11 +51,105 @@ const LEGEND_GRADIENT = {
42
51
  swatch: ['#9aa0a6', '#cfd2d6'],
43
52
  },
44
53
  };
54
+ /* ── Porter's five forces: the panel under the rows ───────────────────── */
55
+ /** The panel's own numbers. Model units, like every other measure here. */
56
+ const PORTER_PANEL = {
57
+ pad: 12,
58
+ titleFs: 16,
59
+ titleH: 22,
60
+ /** Between the title and the diagram, and between the diagram and caption. */
61
+ gapAfterTitle: 8,
62
+ gapBeforeCaption: 10,
63
+ /** The glyph's radius — half the map's, so the panel reads as a diagram. */
64
+ radius: 15,
65
+ /** Clearance between the arrow tips and the boxes they point at. */
66
+ clearance: 4,
67
+ /** The three letters, drawn on the glyph's centre as a free text. */
68
+ letterFs: 9,
69
+ boxFs: 12,
70
+ sideW: 150,
71
+ sideH: 34,
72
+ endW: 170,
73
+ endH: 24,
74
+ captionFs: 13,
75
+ captionH: 21,
76
+ backing: '#e5e7eb',
77
+ backingStroke: '#9aa0a6',
78
+ };
79
+ /** The four forces the boxes name, north first and then clockwise. */
80
+ const PORTER_FORCES = [
81
+ 'Threat of new entrants',
82
+ 'Bargaining power of customers',
83
+ 'Threat of substitutes',
84
+ 'Bargaining power of suppliers',
85
+ ];
86
+ /** What the panel's own glyph reads: the notation, not one force. */
87
+ const PORTER_PANEL_LETTERS = 'R/L/E';
88
+ /** What the letters mean, spelled out under the diagram. */
89
+ const PORTER_CAPTION = 'R/L/E = Relative competition, or struggLe for survival, or struggle to Establish';
90
+ /**
91
+ * How far a porter glyph of the given radius reaches, arrow tips included.
92
+ *
93
+ * DERIVED from the shared geometry rather than written down: the boxes must
94
+ * stand clear of the arrows, and the day somebody lengthens `PORTER_ARROW` a
95
+ * hard-coded 38 here would silently let a tip enter a box.
96
+ */
97
+ function porterReach(radius) {
98
+ return Math.max(...wardleyPorterArrows(0, 0, radius).flatMap(arrow => {
99
+ const [x, y, w, h] = Bound.deserialize(arrow.xywh).toXYWH();
100
+ return [Math.abs(x), Math.abs(y), Math.abs(x + w), Math.abs(y + h)];
101
+ }));
102
+ }
103
+ /**
104
+ * The panel's geometry for a given width — the PO's reference drawing, as
105
+ * numbers.
106
+ *
107
+ * A pure function so the layout can be asserted without a surface, and so the
108
+ * one thing that must hold — that none of the four boxes overlaps the glyph
109
+ * they surround — is checkable rather than eyeballed.
110
+ */
111
+ export function porterPanelLayout(w) {
112
+ const p = PORTER_PANEL;
113
+ const inner = w - p.pad * 2;
114
+ // Where a box's near edge sits: past the arrow tips, never on them.
115
+ const clear = porterReach(p.radius) + p.clearance;
116
+ const diagramH = (clear + p.endH) * 2;
117
+ const cx = w / 2;
118
+ const cy = p.pad + p.titleH + p.gapAfterTitle + diagramH / 2;
119
+ const captionY = cy + diagramH / 2 + p.gapBeforeCaption;
120
+ return {
121
+ w,
122
+ h: captionY + p.captionH + p.pad,
123
+ title: [p.pad, p.pad, inner, p.titleH],
124
+ center: [cx, cy],
125
+ radius: p.radius,
126
+ boxes: [
127
+ {
128
+ xywh: [cx - p.endW / 2, cy - clear - p.endH, p.endW, p.endH],
129
+ label: PORTER_FORCES[0],
130
+ },
131
+ {
132
+ xywh: [cx + clear, cy - p.sideH / 2, p.sideW, p.sideH],
133
+ label: PORTER_FORCES[1],
134
+ },
135
+ {
136
+ xywh: [cx - p.endW / 2, cy + clear, p.endW, p.endH],
137
+ label: PORTER_FORCES[2],
138
+ },
139
+ {
140
+ xywh: [cx - clear - p.sideW, cy - p.sideH / 2, p.sideW, p.sideH],
141
+ label: PORTER_FORCES[3],
142
+ },
143
+ ],
144
+ caption: [p.pad, captionY, inner, p.captionH],
145
+ };
146
+ }
45
147
  /**
46
148
  * Build a "Legend" group from real, editable elements (white rect frame +
47
149
  * "Legend" text + one row of [real component glyph + description text] per
48
150
  * Wardley component TYPE present inside the background's perimeter + a
49
- * gradient-meaning block when the background is a gradient variant). A snapshot
151
+ * gradient-meaning block when the background is a gradient variant + the
152
+ * five-forces panel when a Porter's-forces glyph is on the map). A snapshot
50
153
  * is created on each call; everything is grouped so it can be moved / resized /
51
154
  * edited and is dropped bottom-left of the background.
52
155
  */
@@ -77,6 +180,13 @@ export function createWardleyLegend(std, bg) {
77
180
  // market triangle connectors (NODE_STROKE) are ignored.
78
181
  }
79
182
  else if (el instanceof ShapeElementModel) {
183
+ // The inertia bar is the only plain shape this legend describes, and it
184
+ // is recognised by its FILL. A Porter's-forces arrow is a plain shape too
185
+ // — a filled red polygon, the glyph's own wiring — and it is `WARDLEY_RED`
186
+ // rather than `INERTIA_COLOR`, so it falls through here and is described
187
+ // by the porter circle it belongs to. Asserted in `porter.unit.spec.ts`:
188
+ // an arrow that started answering this test would put an "Inertia to
189
+ // change" row in the legend of a map with no inertia bar on it.
80
190
  if (el.fillColor === INERTIA_COLOR)
81
191
  present.add('inertia');
82
192
  }
@@ -97,7 +207,13 @@ export function createWardleyLegend(std, bg) {
97
207
  const variant = bg.variant;
98
208
  const grad = variant !== 'classic' ? LEGEND_GRADIENT[variant] : null;
99
209
  const gradH = grad ? 12 + GRAD_ROW_H : 0;
100
- const H = PAD * 2 + TITLE_H + rows.length * ROW_H + gradH;
210
+ // The five-forces panel, and ONLY when a force is actually on the map: a
211
+ // legend of a map with no porter on it has to come out byte-identical to the
212
+ // one it came out as before this panel existed. Pinned in
213
+ // `porter.unit.spec.ts`.
214
+ const panel = present.has('porter') ? porterPanelLayout(W - PAD * 2) : null;
215
+ const panelH = panel ? 12 + panel.h : 0;
216
+ const H = PAD * 2 + TITLE_H + rows.length * ROW_H + gradH + panelH;
101
217
  const x0 = bx + 50;
102
218
  const y0 = by + bh - 56 - H;
103
219
  const text = (t, x, y, w, h, fontSize, align = 'left') => surface.addElement({
@@ -240,6 +356,94 @@ export function createWardleyLegend(std, bg) {
240
356
  }));
241
357
  return [circle, ...dots, ...conns];
242
358
  }
359
+ case 'porter': {
360
+ // The same drawing as the map's, at the row's scale: the helper scales
361
+ // the gap, the shaft and the head with the radius, so what a reader
362
+ // sees here is a small Porter and not a circle with four map-sized
363
+ // spikes through it.
364
+ //
365
+ // 6 and not the 8 the other circles get, because this glyph is the only
366
+ // one wider than its own circle: arrows included it spans
367
+ // `2 * (R + (gap + length) * R / 30)`, which at 8 would overrun a row
368
+ // 30 units tall. At 6 it comes to just over `ROW_H` and sits inside its
369
+ // line — and it is a RATIO, so the PO's doubling of the map glyph left
370
+ // this row exactly where it was.
371
+ const R = 6;
372
+ const FS = 8;
373
+ const circle = surface.addElement({
374
+ type: 'wardleyNode',
375
+ kind: 'porter',
376
+ shapeType: 'ellipse',
377
+ filled: true,
378
+ fillColor: NODE_FILL,
379
+ strokeColor: NODE_STROKE,
380
+ strokeWidth: NODE_STROKE_WIDTH,
381
+ shapeStyle: ShapeStyle.General,
382
+ roughness: 0,
383
+ xywh: new Bound(cx - R, cy - R, R * 2, R * 2).serialize(),
384
+ });
385
+ const arrows = wardleyPorterArrows(cx, cy, R).map(arrow => surface.addElement(wardleyPorterArrowProps(arrow)));
386
+ // The letter is the notation, so a legend that dropped it would be
387
+ // describing a circle rather than the glyph it stands for — but here it
388
+ // is a SEPARATE text element rather than the circle's inner text, which
389
+ // is what the map glyph uses. A shape lays its text out inside padding
390
+ // (`SHAPE_TEXT_VERTICAL_PADDING`) larger than this 12-unit box, so at
391
+ // font size 8 the character was pushed out under the circle (recette
392
+ // v2). A free text has no padding to overflow, and its box is placed on
393
+ // the circle's own centre. Role-less like every other legend glyph.
394
+ const letter = text(PORTER_DEFAULT_LETTER, cx - R, cy - FS / 2 - 1, R * 2, FS + 2, FS, 'center');
395
+ return [circle, ...arrows, letter];
396
+ }
397
+ case 'accelerator':
398
+ case 'decelerator': {
399
+ // The map's own outline, at the row's scale: `vertices` are normalized
400
+ // to the box, so the SAME seven points draw a 30 × 18 legend arrow and
401
+ // a 48 × 40 canvas one. Role-less like every glyph in this box — a
402
+ // legend documents the map, it is not part of it.
403
+ const gw = 30;
404
+ const gh = 18;
405
+ return [
406
+ surface.addElement({
407
+ type: 'wardleyNode',
408
+ kind: type,
409
+ shapeType: 'polygon',
410
+ vertices: (type === 'accelerator'
411
+ ? ACCELERATOR_VERTICES
412
+ : DECELERATOR_VERTICES).map(([vx, vy]) => [vx, vy]),
413
+ isClosed: true,
414
+ filled: true,
415
+ fillColor: ACCELERATOR_FILL,
416
+ strokeColor: NODE_STROKE,
417
+ strokeWidth: ACCELERATOR_STROKE_WIDTH,
418
+ shapeStyle: ShapeStyle.General,
419
+ roughness: 0,
420
+ xywh: new Bound(cx - gw / 2, cy - gh / 2, gw, gh).serialize(),
421
+ }),
422
+ ];
423
+ }
424
+ case 'area': {
425
+ // A small translucent rect, and the rect even when the map's zones are
426
+ // polygons: the row says what a ZONE is, and the number of corners is
427
+ // the author's choice rather than part of the notation. Same wash and
428
+ // same rim as the canvas draws, so the swatch is recognisable.
429
+ const gw = 34;
430
+ const gh = 20;
431
+ return [
432
+ surface.addElement({
433
+ type: 'wardleyNode',
434
+ kind: 'area',
435
+ shapeType: 'rect',
436
+ filled: true,
437
+ fillColor: AREA_FILL,
438
+ strokeColor: AREA_STROKE,
439
+ strokeWidth: AREA_STROKE_WIDTH,
440
+ shapeStyle: ShapeStyle.General,
441
+ roughness: 0,
442
+ radius: 0,
443
+ xywh: new Bound(cx - gw / 2, cy - gh / 2, gw, gh).serialize(),
444
+ }),
445
+ ];
446
+ }
243
447
  case 'link':
244
448
  return [
245
449
  surface.addElement({
@@ -331,6 +535,76 @@ export function createWardleyLegend(std, bg) {
331
535
  });
332
536
  ids.push(text(grad.caption, x0 + PAD + GLYPH_W + GAP, cyRow - GRAD_ROW_H / 2, TEXT_W, GRAD_ROW_H, TEXT_FS));
333
537
  }
538
+ // Porter's five forces: the panel the PO's reference draws, under everything
539
+ // else. It is not a row — a row says what a glyph IS, and this says what the
540
+ // notation MEANS: four named pressures around one circle, and the three
541
+ // letters spelled out. Only drawn when a force is on the map, so a legend
542
+ // without one is the legend it always was.
543
+ if (panel) {
544
+ const px = x0 + PAD;
545
+ const py = ry + gradH + 12;
546
+ const p = PORTER_PANEL;
547
+ // The backing: square-cornered and grey, so the panel reads as a figure
548
+ // set into the legend rather than as one more entry in it.
549
+ ids.push(surface.addElement({
550
+ type: 'shape',
551
+ shapeType: 'rect',
552
+ filled: true,
553
+ fillColor: p.backing,
554
+ strokeColor: p.backingStroke,
555
+ strokeWidth: 1,
556
+ shapeStyle: ShapeStyle.General,
557
+ roughness: 0,
558
+ radius: 0,
559
+ xywh: new Bound(px, py, panel.w, panel.h).serialize(),
560
+ }));
561
+ const [tx, ty, tw, th] = panel.title;
562
+ ids.push(text("Porter's five forces", px + tx, py + ty, tw, th, p.titleFs));
563
+ const [cx, cy] = panel.center;
564
+ ids.push(surface.addElement({
565
+ type: 'wardleyNode',
566
+ kind: 'porter',
567
+ shapeType: 'ellipse',
568
+ filled: true,
569
+ fillColor: NODE_FILL,
570
+ strokeColor: NODE_STROKE,
571
+ strokeWidth: NODE_STROKE_WIDTH,
572
+ shapeStyle: ShapeStyle.General,
573
+ roughness: 0,
574
+ xywh: new Bound(px + cx - panel.radius, py + cy - panel.radius, panel.radius * 2, panel.radius * 2).serialize(),
575
+ }));
576
+ for (const arrow of wardleyPorterArrows(px + cx, py + cy, panel.radius)) {
577
+ ids.push(surface.addElement(wardleyPorterArrowProps(arrow)));
578
+ }
579
+ // All three letters at once: this circle stands for the NOTATION rather
580
+ // than for one force, so picking one of them would make the panel say that
581
+ // a Porter is an R.
582
+ //
583
+ // A SEPARATE text element, like the porter row above and for the same
584
+ // reason (recette v2): a shape lays its text out inside a padding
585
+ // (`SHAPE_TEXT_PADDING`) wider than this 30-unit circle, so the characters
586
+ // would be pushed outside it. A free text has no padding to overflow, and
587
+ // its box is placed on the circle's own centre.
588
+ ids.push(text(PORTER_PANEL_LETTERS, px + cx - panel.radius, py + cy - p.letterFs / 2 - 1, panel.radius * 2, p.letterFs + 2, p.letterFs, 'center'));
589
+ for (const box of panel.boxes) {
590
+ const [bx, by, bw, bh] = box.xywh;
591
+ ids.push(surface.addElement({
592
+ type: 'shape',
593
+ shapeType: 'rect',
594
+ filled: true,
595
+ fillColor: '#ffffff',
596
+ strokeColor: NODE_STROKE,
597
+ strokeWidth: 1,
598
+ shapeStyle: ShapeStyle.General,
599
+ roughness: 0,
600
+ radius: 0,
601
+ xywh: new Bound(px + bx, py + by, bw, bh).serialize(),
602
+ }));
603
+ ids.push(text(box.label, px + bx + 4, py + by + 3, bw - 8, bh - 6, p.boxFs, 'center'));
604
+ }
605
+ const [capX, capY, capW, capH] = panel.caption;
606
+ ids.push(text(PORTER_CAPTION, px + capX, py + capY, capW, capH, p.captionFs, 'center'));
607
+ }
334
608
  // 4. Group everything and select it.
335
609
  const [, result] = std.command.exec(createGroupCommand, { elements: ids });
336
610
  gfx.selection.set({
package/dist/natures.d.ts CHANGED
@@ -47,4 +47,35 @@ export declare const WARDLEY_NATURE: {
47
47
  readonly practice: "wardley:nature/practice";
48
48
  readonly knowledge: "wardley:nature/knowledge";
49
49
  };
50
+ /**
51
+ * The Porter's-forces **competition** tag — which of the forces a glyph marks.
52
+ *
53
+ * The second tag of this pack, and the one that shows the mechanism was worth
54
+ * building: it is declared beside the nature, on the SAME pack, and it applies
55
+ * to a role the nature deliberately never reaches. A force is not a link in the
56
+ * value chain (`roles.ts` says why `wardley:porter` has no parent), so nothing
57
+ * written on `wardley:component` touches it — and this tag, written on
58
+ * `wardley:porter` alone, touches nothing else.
59
+ *
60
+ * ## Why it duplicates the letter, and why that is not a duplication
61
+ *
62
+ * The letter in the circle IS the notation: an architect reads `L` off the map
63
+ * and knows what it says. But a letter is not a FACT anything can query — no
64
+ * rule, no reading, no host report can ask "which forces does this map name?"
65
+ * of a `Y.Text` holding one character. The tag is the queryable half, and
66
+ * `WardleyPorterWatcher` keeps the two saying the same thing in both
67
+ * directions, so neither is the master and the author can work in whichever
68
+ * one is in front of them.
69
+ *
70
+ * `cardinality: 'single'` for the reason the nature gives, and one the drawing
71
+ * gives too: there is exactly one letter in the circle, so a second value would
72
+ * be a qualification the map could not show.
73
+ */
74
+ export declare const WARDLEY_COMPETITION_TAG_ID = "wardley:competition";
75
+ /** The three forces, as value ids. Ids are forever; a def is only deprecated. */
76
+ export declare const WARDLEY_COMPETITION: {
77
+ readonly relative: "wardley:competition/relative";
78
+ readonly struggle: "wardley:competition/struggle";
79
+ readonly establish: "wardley:competition/establish";
80
+ };
50
81
  export declare const WARDLEY_TAG_DEFS: UniverseTagDefs;
package/dist/natures.js CHANGED
@@ -47,6 +47,37 @@ export const WARDLEY_NATURE = {
47
47
  practice: `${WARDLEY_NATURE_TAG_ID}/practice`,
48
48
  knowledge: `${WARDLEY_NATURE_TAG_ID}/knowledge`,
49
49
  };
50
+ /**
51
+ * The Porter's-forces **competition** tag — which of the forces a glyph marks.
52
+ *
53
+ * The second tag of this pack, and the one that shows the mechanism was worth
54
+ * building: it is declared beside the nature, on the SAME pack, and it applies
55
+ * to a role the nature deliberately never reaches. A force is not a link in the
56
+ * value chain (`roles.ts` says why `wardley:porter` has no parent), so nothing
57
+ * written on `wardley:component` touches it — and this tag, written on
58
+ * `wardley:porter` alone, touches nothing else.
59
+ *
60
+ * ## Why it duplicates the letter, and why that is not a duplication
61
+ *
62
+ * The letter in the circle IS the notation: an architect reads `L` off the map
63
+ * and knows what it says. But a letter is not a FACT anything can query — no
64
+ * rule, no reading, no host report can ask "which forces does this map name?"
65
+ * of a `Y.Text` holding one character. The tag is the queryable half, and
66
+ * `WardleyPorterWatcher` keeps the two saying the same thing in both
67
+ * directions, so neither is the master and the author can work in whichever
68
+ * one is in front of them.
69
+ *
70
+ * `cardinality: 'single'` for the reason the nature gives, and one the drawing
71
+ * gives too: there is exactly one letter in the circle, so a second value would
72
+ * be a qualification the map could not show.
73
+ */
74
+ export const WARDLEY_COMPETITION_TAG_ID = 'wardley:competition';
75
+ /** The three forces, as value ids. Ids are forever; a def is only deprecated. */
76
+ export const WARDLEY_COMPETITION = {
77
+ relative: `${WARDLEY_COMPETITION_TAG_ID}/relative`,
78
+ struggle: `${WARDLEY_COMPETITION_TAG_ID}/struggle`,
79
+ establish: `${WARDLEY_COMPETITION_TAG_ID}/establish`,
80
+ };
50
81
  export const WARDLEY_TAG_DEFS = {
51
82
  formatVersion: 1,
52
83
  // The id of this PACK, not of the framework: several packs may extend
@@ -62,10 +93,12 @@ export const WARDLEY_TAG_DEFS = {
62
93
  description: 'What kind of thing this component is: an activity, data, a practice or knowledge.',
63
94
  cardinality: 'single',
64
95
  appliesTo: [WARDLEY_ROLE.component],
65
- // No `order`: it is the only tag this pack declares, and an absent order
66
- // sorts by SEED order, which is what puts the library's pack ahead of a
67
- // client's extension without either having to know about the other. A
68
- // number here would be a claim about packs that do not exist yet.
96
+ // No `order`, on either tag of this pack: an absent order sorts by SEED
97
+ // order, which is what puts the library's pack ahead of a client's
98
+ // extension without either having to know about the other. A number here
99
+ // would be a claim about packs that do not exist yet — and it would say
100
+ // nothing anyway, since the two tags below apply to disjoint roles and
101
+ // are therefore never offered in the same dropdown.
69
102
  values: [
70
103
  {
71
104
  id: WARDLEY_NATURE.activity,
@@ -89,5 +122,32 @@ export const WARDLEY_TAG_DEFS = {
89
122
  },
90
123
  ],
91
124
  },
125
+ {
126
+ id: WARDLEY_COMPETITION_TAG_ID,
127
+ label: 'Competition',
128
+ description: "Which of Porter's forces this glyph marks: relative competition, a struggle for survival, or a struggle to establish.",
129
+ cardinality: 'single',
130
+ appliesTo: [WARDLEY_ROLE.porter],
131
+ values: [
132
+ {
133
+ id: WARDLEY_COMPETITION.relative,
134
+ // The label carries the letter, because the letter is what the map
135
+ // shows: a reader picking a value here is choosing what the circle
136
+ // will say, not filling in a form about it.
137
+ label: 'Relative competition (R)',
138
+ description: 'Established players competing against one another on a market that exists.',
139
+ },
140
+ {
141
+ id: WARDLEY_COMPETITION.struggle,
142
+ label: 'Struggle for survival (L)',
143
+ description: 'Pressure on a player to stay in a market it is already in.',
144
+ },
145
+ {
146
+ id: WARDLEY_COMPETITION.establish,
147
+ label: 'Struggle to establish (E)',
148
+ description: 'Pressure on a newcomer trying to get into a market.',
149
+ },
150
+ ],
151
+ },
92
152
  ],
93
153
  };
@@ -104,3 +104,126 @@ export declare const METHOD: {
104
104
  centerRatio: number;
105
105
  };
106
106
  export declare const METHOD_LABEL = "Component";
107
+ /**
108
+ * Porter's forces (composite): a large white circle carrying ONE letter, pushed
109
+ * on from the four cardinal directions by solid red arrows.
110
+ *
111
+ * Twice the market's diameter, at the PO's recette of #210: this is not a link
112
+ * in the value chain drawn a little differently, it is a pressure bearing on the
113
+ * whole map, and it has to read as one at the zoom an architect actually works
114
+ * at. Every other number here is proportional to it.
115
+ *
116
+ * The letter IS the notation — **R** relative competition, **L** struggle for
117
+ * survival, **E** struggle to establish — which is why it is written as the
118
+ * shape's own inner text rather than as a label beside it: a double-click opens
119
+ * the native shape editor on the circle, and typing another letter is the whole
120
+ * of the edit. It is also why this glyph carries no label at all: the three
121
+ * words behind the three letters are the map's vocabulary, not a name the
122
+ * author gives one force.
123
+ */
124
+ export declare const PORTER_SIZE = 60;
125
+ /**
126
+ * The four arrows, read by `wardleyPorterArrows`: each starts `gap` outside the
127
+ * circle's rim and runs `length` further out, a `width`-wide shaft carrying a
128
+ * head `headLength` long and `headWidth` across.
129
+ *
130
+ * They are drawn as filled POLYGONS rather than as connectors with a triangle
131
+ * endpoint, and the recette of #210 is why: a connector sizes its head off its
132
+ * STROKE WIDTH (`DEFAULT_ARROW_SIZE` × strokeWidth / 2), which is a number about
133
+ * a line rather than about this notation — the head came out longer than the
134
+ * arrow, swallowed the circle, the letter and the double-click, and the glyph
135
+ * rendered as a solid red star. A polygon's head is the number written here and
136
+ * nothing else, at any size.
137
+ */
138
+ export declare const PORTER_ARROW: {
139
+ gap: number;
140
+ length: number;
141
+ /** Shaft width. */
142
+ width: number;
143
+ headLength: number;
144
+ headWidth: number;
145
+ };
146
+ /** The letter, sized to sit inside the circle without touching the rim. */
147
+ export declare const PORTER_LETTER_FONT_SIZE = 28;
148
+ /** The letter a fresh glyph opens on: relative competition, the commonest read. */
149
+ export declare const PORTER_DEFAULT_LETTER = "R";
150
+ /**
151
+ * Accelerators and decelerators — the climate annotations.
152
+ *
153
+ * A fat solid arrow with a flat grey fill and a thick dark border: as close as
154
+ * a native polygon gets to the reference's solid arrow, because this canvas has
155
+ * no gradient fill. The two are the same drawing mirrored — an accelerator
156
+ * points RIGHT, towards the commodity end of the evolution axis, a decelerator
157
+ * points LEFT, back towards genesis — and that direction IS the notation, which
158
+ * is why the outlines are written out below rather than expressed as one shape
159
+ * plus a `rotate`: a rotated element is one the selection, the resize handles
160
+ * and every bounding-box reader then have to de-rotate.
161
+ *
162
+ * Neither is a link in the value chain, so neither carries a component role
163
+ * (see `roles.ts`): they say something about the CLIMATE the map is drawn in.
164
+ */
165
+ export declare const ACCELERATOR_SIZE: {
166
+ w: number;
167
+ h: number;
168
+ };
169
+ /** Flat grey: the arrow is a annotation, and must not out-shout the chain. */
170
+ export declare const ACCELERATOR_FILL = "#bfbfbf";
171
+ /** A thick dark border — `NODE_STROKE`, like every other artefact's rim. */
172
+ export declare const ACCELERATOR_STROKE_WIDTH = 2;
173
+ /**
174
+ * The accelerator's outline, normalized to its box, pointing RIGHT.
175
+ *
176
+ * Seven points: the shaft's two left corners, the head's upper barb, the tip,
177
+ * the head's lower barb, and back along the shaft. `0.55` is where the head
178
+ * starts, `0.28 / 0.72` the shaft's own edges.
179
+ */
180
+ export declare const ACCELERATOR_VERTICES: readonly (readonly [number, number])[];
181
+ /** The decelerator: the same outline mirrored `x → 1 − x`, pointing LEFT. */
182
+ export declare const DECELERATOR_VERTICES: readonly (readonly [number, number])[];
183
+ export declare const ACCELERATOR_LABEL = "Accelerator";
184
+ export declare const DECELERATOR_LABEL = "Decelerator";
185
+ /**
186
+ * Areas — the zones of the map.
187
+ *
188
+ * A ring you draw AROUND components to say "all of this is one thing": a
189
+ * business unit, a team's territory, a scope under discussion. It is not a link
190
+ * in the value chain and not an annotation of the climate either, which is why
191
+ * it carries a role with no parent and sits in a category of its own in the
192
+ * toolbox.
193
+ *
194
+ * Two shapes, one kind: a RECT for the common case and a POLYGON for a zone
195
+ * whose outline has to follow the components it groups (its corners are then
196
+ * moved from the shape toolbar's vertex editor). The `shapeType` is what says
197
+ * which, so nothing about an area is a second kind to keep in step.
198
+ *
199
+ * A zone is drawn OVER the map it groups, so the fill is the load-bearing
200
+ * number here: Peace light at ~25 % opacity, which is a wash the components
201
+ * underneath still read through. The border is the saturated Peace of the same
202
+ * swatch, thin like every other artefact's rim — the zone must frame the map,
203
+ * never compete with it.
204
+ */
205
+ export declare const AREA_RECT_SIZE: {
206
+ w: number;
207
+ h: number;
208
+ };
209
+ /** The polygon is born square: its default outline is a regular pentagon. */
210
+ export declare const AREA_POLYGON_SIZE: {
211
+ w: number;
212
+ h: number;
213
+ };
214
+ /** Peace light at ~25 % opacity — an 8-digit hex, exactly as `PIPELINE_FILL`. */
215
+ export declare const AREA_FILL = "#c6dbfc40";
216
+ /** Peace, the saturated shade of the same swatch. */
217
+ export declare const AREA_STROKE = "#5b9cf6";
218
+ export declare const AREA_STROKE_WIDTH = 1;
219
+ /**
220
+ * The word the legend describes an area by.
221
+ *
222
+ * NOT written into the document: an area is created NAMELESS, so the editor
223
+ * that a double-click opens starts on an empty line and the first thing typed
224
+ * is the zone's own name. The other artefacts carry a placeholder because their
225
+ * name is a text element beside them and an empty one would be invisible; an
226
+ * area's name is its own inner text, and a placeholder there would be a word
227
+ * the author has to delete before writing theirs.
228
+ */
229
+ export declare const AREA_LABEL = "Area";