@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/actions.d.ts +111 -31
- package/dist/actions.js +184 -98
- package/dist/commands-manifest.js +40 -0
- package/dist/commands.d.ts +18 -1
- package/dist/commands.js +96 -7
- package/dist/export.js +50 -2
- package/dist/import.d.ts +5 -3
- package/dist/import.js +53 -8
- package/dist/index.d.ts +4 -1
- package/dist/index.js +16 -1
- package/dist/legend.d.ts +27 -1
- package/dist/legend.js +277 -3
- package/dist/morph.d.ts +129 -0
- package/dist/morph.js +405 -0
- package/dist/natures.d.ts +31 -0
- package/dist/natures.js +64 -4
- package/dist/node/consts.d.ts +123 -0
- package/dist/node/consts.js +122 -0
- package/dist/node/node-view.d.ts +82 -4
- package/dist/node/node-view.js +108 -4
- package/dist/node/porter-watcher.d.ts +119 -0
- package/dist/node/porter-watcher.js +255 -0
- package/dist/porter-competition.d.ts +36 -0
- package/dist/porter-competition.js +50 -0
- package/dist/presets.d.ts +276 -0
- package/dist/presets.js +554 -0
- package/dist/roles.d.ts +4 -0
- package/dist/roles.js +85 -7
- package/dist/toolbar/icons.d.ts +36 -0
- package/dist/toolbar/icons.js +58 -0
- package/dist/toolbar/node-config.d.ts +20 -0
- package/dist/toolbar/node-config.js +100 -7
- package/dist/view.js +40 -2
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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/morph.d.ts
ADDED
|
@@ -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>;
|