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