@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/presets.js
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { ConnectorMode, DEFAULT_POLYGON_VERTICES, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
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
|
+
import { WARDLEY_ROLE } from './roles.js';
|
|
5
|
+
/**
|
|
6
|
+
* The size a kind is BORN at, which for Wardley is the notation rather than a
|
|
7
|
+
* default.
|
|
8
|
+
*
|
|
9
|
+
* A market is 30 across and a component is 18 because that is what tells a
|
|
10
|
+
* reader which is which — the two are the same white circle otherwise. Same for
|
|
11
|
+
* the ecosystem's 40 and the pipeline's 120×25 bar. This is the table the morph
|
|
12
|
+
* re-centres an artefact on, and the one the palette draws from.
|
|
13
|
+
*/
|
|
14
|
+
export const WARDLEY_NODE_SIZE = {
|
|
15
|
+
component: { w: NODE_SIZE, h: NODE_SIZE },
|
|
16
|
+
anchor: { w: NODE_SIZE, h: NODE_SIZE },
|
|
17
|
+
ecosystem: { w: ECOSYSTEM_SIZE, h: ECOSYSTEM_SIZE },
|
|
18
|
+
method: { w: METHOD_SIZE, h: METHOD_SIZE },
|
|
19
|
+
market: { w: MARKET_SIZE, h: MARKET_SIZE },
|
|
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,
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The words a kind is created NEXT TO — the prompt an artefact nobody has named
|
|
56
|
+
* still carries, and the only string a morph is ever allowed to rewrite.
|
|
57
|
+
*/
|
|
58
|
+
export const WARDLEY_NODE_LABEL = {
|
|
59
|
+
component: LABEL_DEFAULT.component,
|
|
60
|
+
anchor: LABEL_DEFAULT.anchor,
|
|
61
|
+
ecosystem: ECOSYSTEM_LABEL,
|
|
62
|
+
method: METHOD_LABEL,
|
|
63
|
+
market: MARKET_LABEL,
|
|
64
|
+
pipeline: PIPELINE_LABEL,
|
|
65
|
+
accelerator: ACCELERATOR_LABEL,
|
|
66
|
+
decelerator: DECELERATOR_LABEL,
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The fill a kind is born with. White for every circle that means "a thing on
|
|
70
|
+
* the value chain"; the method's grey is the one that carries meaning — it
|
|
71
|
+
* ENCODES the chosen method and stays editable from the toolbar.
|
|
72
|
+
*/
|
|
73
|
+
const NODE_FILL_OF = {
|
|
74
|
+
component: NODE_FILL,
|
|
75
|
+
anchor: NODE_FILL,
|
|
76
|
+
ecosystem: NODE_FILL,
|
|
77
|
+
method: METHOD_FILL,
|
|
78
|
+
market: NODE_FILL,
|
|
79
|
+
// White at ~60% opacity: a pipeline is a bar you place components ON, so the
|
|
80
|
+
// map has to stay visible through it. The 1px border stays opaque.
|
|
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,
|
|
105
|
+
};
|
|
106
|
+
/** A box centred on a point — how every Wardley creation site places a node. */
|
|
107
|
+
export function wardleyCenteredBox(cx, cy, w, h) {
|
|
108
|
+
return new Bound(cx - w / 2, cy - h / 2, w, h).serialize();
|
|
109
|
+
}
|
|
110
|
+
/** The canonical box of one kind, centred where the artefact already stands. */
|
|
111
|
+
export function wardleyCanonicalBox(kind, cx, cy) {
|
|
112
|
+
const { w, h } = WARDLEY_NODE_SIZE[kind];
|
|
113
|
+
return wardleyCenteredBox(cx, cy, w, h);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Every prop a Wardley node is created with, for one kind and one box.
|
|
117
|
+
*
|
|
118
|
+
* Most of the pack is a native ELLIPSE and differs only in fill and size: the
|
|
119
|
+
* glyph — the anchor's silhouette, the ecosystem's hatched donut, the method's
|
|
120
|
+
* inner circle — is drawn by the renderer from `kind`, off this same model, so
|
|
121
|
+
* stroke and fill stay editable from the shape toolbar exactly like any other
|
|
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.
|
|
126
|
+
*
|
|
127
|
+
* No `text`: a Wardley label is a separate free-text element grouped with the
|
|
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.
|
|
131
|
+
*/
|
|
132
|
+
export function wardleyNodeProps(kind, box) {
|
|
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];
|
|
139
|
+
return {
|
|
140
|
+
type: 'wardleyNode',
|
|
141
|
+
kind,
|
|
142
|
+
// Semantic identity (PF1): posted next to `kind`, which stays untouched and
|
|
143
|
+
// keeps driving the rendering. The role is what every rule reads — no rule
|
|
144
|
+
// will ever look at a shape type.
|
|
145
|
+
role: WARDLEY_ROLE[kind],
|
|
146
|
+
shapeType: outline ? 'polygon' : rect ? 'rect' : 'ellipse',
|
|
147
|
+
filled: true,
|
|
148
|
+
fillColor: NODE_FILL_OF[kind],
|
|
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,
|
|
154
|
+
shapeStyle: ShapeStyle.General,
|
|
155
|
+
roughness: 0,
|
|
156
|
+
// Square corners, and ONLY on the kind that has corners at all. Spread
|
|
157
|
+
// conditionally rather than written as 0 everywhere, because that is the
|
|
158
|
+
// truth about the ellipses — and because a key some kinds write and others
|
|
159
|
+
// do not is precisely what `wardleyMorphClears` is for.
|
|
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
|
+
: {}),
|
|
167
|
+
xywh: box.xywh,
|
|
168
|
+
};
|
|
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
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The pipeline's handle: a node-sized square straddling the body's top edge, and
|
|
232
|
+
* the only place a connector may land on a pipeline.
|
|
233
|
+
*
|
|
234
|
+
* It inherits `centerAnchorOnly` from the model like every other Wardley node,
|
|
235
|
+
* and it carries `wardley:handle` — a role of its own, so nothing that judges
|
|
236
|
+
* components ever judges it.
|
|
237
|
+
*/
|
|
238
|
+
export function wardleyHandleProps(box) {
|
|
239
|
+
return {
|
|
240
|
+
type: 'wardleyNode',
|
|
241
|
+
kind: 'handle',
|
|
242
|
+
role: WARDLEY_ROLE.handle,
|
|
243
|
+
shapeType: 'rect',
|
|
244
|
+
filled: true,
|
|
245
|
+
fillColor: NODE_FILL,
|
|
246
|
+
strokeColor: NODE_STROKE,
|
|
247
|
+
strokeWidth: NODE_STROKE_WIDTH,
|
|
248
|
+
shapeStyle: ShapeStyle.General,
|
|
249
|
+
roughness: 0,
|
|
250
|
+
radius: 0,
|
|
251
|
+
xywh: box.xywh,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** Where a handle sits, given the body's centre: astride the top edge. */
|
|
255
|
+
export function wardleyHandleBox(cx, cy) {
|
|
256
|
+
const top = cy - WARDLEY_NODE_SIZE.pipeline.h / 2;
|
|
257
|
+
return wardleyCenteredBox(cx, top, HANDLE_SIZE, HANDLE_SIZE);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* One of the market's three inner dots — thicker-bordered, and deliberately
|
|
261
|
+
* ROLE-LESS.
|
|
262
|
+
*
|
|
263
|
+
* They are the GLYPH's own wiring rather than artefacts the user placed, the
|
|
264
|
+
* same reason the triangle between them carries no role: they sit inside the
|
|
265
|
+
* market circle by construction, so a role would make every market composite
|
|
266
|
+
* report an overlap with itself (W3).
|
|
267
|
+
*/
|
|
268
|
+
export function wardleyMarketDotProps(box) {
|
|
269
|
+
return {
|
|
270
|
+
type: 'wardleyNode',
|
|
271
|
+
kind: 'component',
|
|
272
|
+
role: undefined,
|
|
273
|
+
shapeType: 'ellipse',
|
|
274
|
+
filled: true,
|
|
275
|
+
fillColor: NODE_FILL,
|
|
276
|
+
strokeColor: NODE_STROKE,
|
|
277
|
+
strokeWidth: MARKET_DOT_STROKE_WIDTH,
|
|
278
|
+
shapeStyle: ShapeStyle.General,
|
|
279
|
+
roughness: 0,
|
|
280
|
+
xywh: box.xywh,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** The three dot boxes of a market centred on (cx, cy) — an upright triangle. */
|
|
284
|
+
export function wardleyMarketDotBoxes(cx, cy) {
|
|
285
|
+
const rho = MARKET_DOT_RING;
|
|
286
|
+
const sin60 = Math.sqrt(3) / 2;
|
|
287
|
+
return [
|
|
288
|
+
[0, -rho],
|
|
289
|
+
[rho * sin60, rho / 2],
|
|
290
|
+
[-rho * sin60, rho / 2],
|
|
291
|
+
].map(([vx, vy]) => wardleyCenteredBox(cx + vx, cy + vy, MARKET_DOT_SIZE, MARKET_DOT_SIZE));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* One side of the market's triangle: an ATTACHED connector, so it auto-routes
|
|
295
|
+
* centre to centre and follows the dots on move and resize.
|
|
296
|
+
*/
|
|
297
|
+
export function wardleyMarketLinkProps(sourceId, targetId) {
|
|
298
|
+
return {
|
|
299
|
+
type: 'connector',
|
|
300
|
+
mode: ConnectorMode.Straight,
|
|
301
|
+
source: { id: sourceId },
|
|
302
|
+
target: { id: targetId },
|
|
303
|
+
stroke: MARKET_LINK_COLOR,
|
|
304
|
+
strokeStyle: StrokeStyle.Solid,
|
|
305
|
+
strokeWidth: MARKET_LINK_WIDTH,
|
|
306
|
+
frontEndpointStyle: PointStyle.None,
|
|
307
|
+
rearEndpointStyle: PointStyle.None,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/** The pairs the triangle wires, given the three dots in creation order. */
|
|
311
|
+
export function wardleyMarketLinkPairs(dots) {
|
|
312
|
+
return [
|
|
313
|
+
[dots[0], dots[1]],
|
|
314
|
+
[dots[1], dots[2]],
|
|
315
|
+
[dots[2], dots[0]],
|
|
316
|
+
];
|
|
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
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The box {@link wardleyMorphProps} hands {@link wardleyNodeProps} and then
|
|
486
|
+
* throws away. Never written to a document: the morph places the element
|
|
487
|
+
* itself, from {@link wardleyCanonicalBox}, in `afterMorph`.
|
|
488
|
+
*/
|
|
489
|
+
const DISCARDED_BOX = '[0,0,0,0]';
|
|
490
|
+
/** What a morph must never rewrite: identity, geometry, and the user's words. */
|
|
491
|
+
const NOT_A_MORPH = ['type', 'xywh', 'text'];
|
|
492
|
+
/**
|
|
493
|
+
* What a kind is worth to a node that ALREADY EXISTS — {@link wardleyNodeProps}
|
|
494
|
+
* minus the three things a morph has no business touching.
|
|
495
|
+
*
|
|
496
|
+
* The whole preset and not `{kind, role}`, and on this pack it is visibly not a
|
|
497
|
+
* formality: a pipeline is a rect with square corners and a semi-transparent
|
|
498
|
+
* fill, and the other three members of the family are opaque white ellipses. A
|
|
499
|
+
* two-key patch would turn a component into a "pipeline" still drawn as a
|
|
500
|
+
* circle.
|
|
501
|
+
*/
|
|
502
|
+
export function wardleyMorphProps(kind) {
|
|
503
|
+
const props = {
|
|
504
|
+
...wardleyNodeProps(kind, { xywh: DISCARDED_BOX }),
|
|
505
|
+
};
|
|
506
|
+
for (const key of NOT_A_MORPH)
|
|
507
|
+
delete props[key];
|
|
508
|
+
return props;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
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.
|
|
535
|
+
*
|
|
536
|
+
* Computed rather than listed, so a preset that starts spreading a key
|
|
537
|
+
* conditionally is covered on the day it is added rather than on the day
|
|
538
|
+
* somebody notices.
|
|
539
|
+
*/
|
|
540
|
+
const EVERY_MORPH_KEY = new Set(WARDLEY_MORPHABLE_KINDS.flatMap(kind => Object.keys(wardleyMorphProps(kind))));
|
|
541
|
+
/**
|
|
542
|
+
* The fields to DELETE after morphing to `kind` — the keys some other kind
|
|
543
|
+
* writes and this one does not.
|
|
544
|
+
*
|
|
545
|
+
* `radius` for every kind but the pipeline, and that is not a formality either:
|
|
546
|
+
* a patch cannot express absence, so a pipeline morphed back to a component
|
|
547
|
+
* would keep `radius: 0` sitting in the Y.Map, silently in force on an ellipse.
|
|
548
|
+
* Derived rather than listed, so the day a preset gains a conditional key it is
|
|
549
|
+
* covered without anybody remembering to come here.
|
|
550
|
+
*/
|
|
551
|
+
export function wardleyMorphClears(kind) {
|
|
552
|
+
const present = new Set(Object.keys(wardleyMorphProps(kind)));
|
|
553
|
+
return [...EVERY_MORPH_KEY].filter(key => !present.has(key));
|
|
554
|
+
}
|
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',
|
|
@@ -55,16 +59,84 @@ const DEFS = [
|
|
|
55
59
|
labelKey: 'com.labre.wardley.role.method',
|
|
56
60
|
},
|
|
57
61
|
/**
|
|
58
|
-
*
|
|
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
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* The value-chain link: "A needs B".
|
|
59
126
|
*
|
|
60
127
|
* The comment above was the whole of the convention until `docs/adr/0010`
|
|
61
128
|
* made it DATA. Tier 1 of that ADR is generic — `source` is the subject of
|
|
62
129
|
* the role's verb, `target` its object — and tier 2 is this `direction`
|
|
63
|
-
* block: the verb of THIS role is "
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
130
|
+
* block: the verb of THIS role is "needs", so its source is the consumer and
|
|
131
|
+
* its target is what the consumer needs. Needs descend from source to target;
|
|
132
|
+
* value flows back up. W4 evaluates exactly that reading, and the tool hint
|
|
133
|
+
* below is what makes it a statement the user made rather than a by-product
|
|
134
|
+
* of which end their finger landed on first.
|
|
135
|
+
*
|
|
136
|
+
* The verb read "depends on" until the PO recette of 02/09/2026: one word, in
|
|
137
|
+
* the user's own vocabulary, on a chip that is laid ALONG the link and must
|
|
138
|
+
* not outgrow it. Only the FALLBACK moved — `verbKey` is stable, and the
|
|
139
|
+
* French wording ("A besoin") comes from the host's catalogue.
|
|
68
140
|
*/
|
|
69
141
|
{
|
|
70
142
|
id: WARDLEY_ROLE.dependency,
|
|
@@ -73,9 +145,15 @@ const DEFS = [
|
|
|
73
145
|
labelFallback: 'Dependency',
|
|
74
146
|
direction: {
|
|
75
147
|
verbKey: 'com.labre.wardley.role.dependency.verb',
|
|
76
|
-
verbFallback: '
|
|
148
|
+
verbFallback: 'needs',
|
|
77
149
|
gestureHintKey: 'com.labre.wardley.role.dependency.gesture',
|
|
78
150
|
gestureHintFallback: 'Drag from the component that has the need to what it needs.',
|
|
151
|
+
// The house primary, on THIS chip and no other (same recette). The value
|
|
152
|
+
// chain is what a Wardley map is read along, and the PO asked for the
|
|
153
|
+
// relation that carries it to be unmistakable — a decision about one
|
|
154
|
+
// relation, so it is declared on that relation rather than on the
|
|
155
|
+
// mechanism, and every other framework's reveal keeps the colour it had.
|
|
156
|
+
chipColor: '#2563eb',
|
|
79
157
|
},
|
|
80
158
|
},
|
|
81
159
|
// The map itself: the axes frame the other roles are positioned against.
|