@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/actions.d.ts +108 -6
- package/dist/actions.js +172 -11
- 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 +3 -2
- package/dist/index.js +7 -2
- package/dist/legend.d.ts +27 -1
- package/dist/legend.js +277 -3
- 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 +151 -5
- package/dist/presets.js +336 -12
- package/dist/reading.js +30 -4
- package/dist/roles.d.ts +4 -0
- package/dist/roles.js +67 -0
- 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 +10 -0
- 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/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
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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
|
};
|
package/dist/node/consts.d.ts
CHANGED
|
@@ -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";
|