@formicoidea/labre-framework-wardley 0.34.2 → 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/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({
@@ -0,0 +1,129 @@
1
+ import { type MorphSpec } from '@formicoidea/labre-core/blocks/surface';
2
+ import { WardleyNodeElementModel } from '@formicoidea/labre-core/model';
3
+ import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
4
+ /**
5
+ * What a Wardley artefact may BECOME — the declaration behind the "Change type"
6
+ * dropdown on a selected component's contextual toolbar.
7
+ *
8
+ * ## One family, and why it is exactly these four
9
+ *
10
+ * A component, a market, an ecosystem and a pipeline are four ways of saying
11
+ * the SAME thing: something the value chain depends on. The map's own reading
12
+ * agrees — `roles.ts` makes `market` and `ecosystem` children of `component` in
13
+ * the role tree — and the modelling gesture the morph exists for is the one
14
+ * every mapper makes: you draw a component, you look at the map, and you
15
+ * realise that what you drew is a market of many suppliers, or a whole
16
+ * ecosystem, or a pipeline of choices. Today that costs a delete, a re-draw,
17
+ * every dependency re-attached and the name typed again.
18
+ *
19
+ * The family is DATA and nothing derives it — the same call BPMN and C4 make.
20
+ * Deriving it from `roleIsA` would have been wrong in both directions here: it
21
+ * would reach from market to component and not back, and it would say nothing
22
+ * at all about the pipeline, which is not a specialisation of anything and is
23
+ * still the fourth way of drawing the same claim.
24
+ *
25
+ * ## The two kinds deliberately left out
26
+ *
27
+ * - **`anchor`** is the user, and the need they have. It is a role of its own
28
+ * precisely because it is NOT a component (`roles.ts`): it is what the value
29
+ * chain hangs from, not a link in it. A menu offering "turn this user into a
30
+ * pipeline" would invite a map with no anchor at all — the one thing a
31
+ * Wardley map cannot be read without.
32
+ * - **`method`** is not an artefact but an annotation ON one: the coloured ring
33
+ * says how a component is being BUILT (buy / build / outsource), and its fill
34
+ * encodes the choice. Morphing it away would silently discard that decision,
35
+ * and morphing into it would have to invent one.
36
+ *
37
+ * `handle` is in no family either, and does not need excluding by name: it is
38
+ * not an artefact at all, so nothing selects it and {@link wardleyNodeOfComponent}
39
+ * never resolves to one.
40
+ */
41
+ export declare const WARDLEY_MORPH_FAMILIES: readonly (readonly WardleyMorphKind[])[];
42
+ /** The kinds this framework offers as morph targets. */
43
+ export type WardleyMorphKind = 'component' | 'market' | 'ecosystem' | 'pipeline';
44
+ /**
45
+ * The `wardleyNode` a selected GROUP is the artefact of — `undefined` when the
46
+ * group is not one.
47
+ *
48
+ * Every Wardley artefact in this family is a composite: a component is a circle
49
+ * and its label, a market adds three dots and a triangle, and a pipeline is a
50
+ * body plus a handle plus a label in a nested group. So what a click selects is
51
+ * the outer group, and what carries `kind` is one node inside it — RECURSIVELY
52
+ * inside it, because the pipeline's own creation nests.
53
+ *
54
+ * Three conditions, and each one excludes a piece of the notation rather than
55
+ * an accident:
56
+ *
57
+ * - a `WardleyNodeElementModel`, which leaves out the labels and the connectors;
58
+ * - with a ROLE, which leaves out the market's three neutral dots — they are the
59
+ * glyph's wiring and carry none by construction (`presets.ts`);
60
+ * - whose kind is in the family, which leaves out the pipeline's `handle` and
61
+ * any `anchor` or `method` somebody grouped in beside the artefact.
62
+ *
63
+ * TWO candidates is a REFUSAL rather than a first-wins pick, exactly as C4's
64
+ * resolution refuses a group of two components: morphing "it" would mean
65
+ * picking one by document order, and the honest answer to an ambiguous
66
+ * selection is nothing at all. A plain lasso round three shapes, a C4 component
67
+ * and a group of two Wardley components all answer `undefined`, so none of them
68
+ * is ever offered the menu.
69
+ */
70
+ export declare function wardleyNodeOfComponent(model: GfxPrimitiveElementModel): WardleyNodeElementModel | undefined;
71
+ /**
72
+ * The name an artefact should carry once it has morphed — or `null` when the
73
+ * name is the AUTHOR's and must not be touched.
74
+ *
75
+ * Exactly one case rewrites: the label is the source kind's own creation
76
+ * prompt, letter for letter, which is what an artefact nobody has named still
77
+ * says. Everything else — a name typed over it, the prompt with a word added, a
78
+ * cleared label, another kind's prompt — is content, and content survives a
79
+ * morph untouched. The same timid rule `c4MorphedTitle` states, and for the same
80
+ * reason: a morph may not take away words somebody put on the picture.
81
+ *
82
+ * Pure and total over every string: the input is a canvas text element somebody
83
+ * may have typed anything into.
84
+ */
85
+ export declare function wardleyMorphedLabel(from: WardleyMorphKind, to: WardleyMorphKind, rawText: string | null | undefined): string | null;
86
+ /**
87
+ * Everything the morph owes a Wardley composite beyond the props patch — the
88
+ * `afterMorph` half of the spec, run inside the generic module's one
89
+ * `captureSync` so all of it is a single ctrl+z.
90
+ *
91
+ * ## The geometry, and why this framework writes it
92
+ *
93
+ * A DEVIATION from BPMN and C4, decided by the PO: those two keep the box the
94
+ * user gave the element, because there a size is a preference. Here it is the
95
+ * NOTATION. A market is drawn bigger than a component so a reader can tell them
96
+ * apart at a glance, an ecosystem bigger again, and a pipeline is a 120-wide bar
97
+ * — a market left at a component's 18 pixels is an unreadable smudge with three
98
+ * dots crammed into it. So the target's canonical size is applied, CENTRED on
99
+ * where the artefact already stands: the thing does not move, it becomes the
100
+ * size its notation says it is. The label keeps its own place, which is the
101
+ * user's.
102
+ *
103
+ * ## And why the rest is here rather than in the patch
104
+ *
105
+ * Because a market IS its three dots and a pipeline IS its handle. A patch can
106
+ * write `kind: 'market'` onto a circle and the result is a circle that claims to
107
+ * be a market and does not look like one; a patch cannot create an element, nor
108
+ * move a connector somebody drew. That is exactly what `afterMorph` is for, and
109
+ * the ORDER below is the whole of the care it needs: the glyph the artefact is
110
+ * LEAVING comes apart before the one it is arriving at is built, so a
111
+ * market → pipeline never has three dots and a handle at once.
112
+ */
113
+ export declare function wardleyMorphComposite(selected: GfxPrimitiveElementModel, from: WardleyMorphKind, to: WardleyMorphKind): void;
114
+ /**
115
+ * Wardley's morph declaration, handed to the generic `morphToolbarConfig`.
116
+ *
117
+ * `modelType` is the native `GroupElementModel` and not
118
+ * `WardleyNodeElementModel`, because every artefact in this family is a
119
+ * composite and the group is what a click on one selects. Everything that makes
120
+ * that safe is in {@link wardleyNodeOfComponent}: the toolbar's own homogeneity
121
+ * test only proves that each selected element is A group, and the resolution
122
+ * proves that each one is one of OURS.
123
+ *
124
+ * `propsOf` is the shipped creation builder minus `type` / `xywh` / `text`
125
+ * ({@link wardleyMorphProps}), so the palette and the morph cannot disagree
126
+ * about what a pipeline looks like, and `clearOf` removes the keys the target
127
+ * does not write — `radius`, which only the pipeline has.
128
+ */
129
+ export declare const WARDLEY_MORPH_SPEC: MorphSpec<WardleyMorphKind>;