@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/morph.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { ConnectorElementModel, GroupElementModel, TextElementModel, WardleyNodeElementModel, } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { wardleyCommandIcons, wardleyCommands } from './commands.js';
|
|
4
|
+
import { wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, wardleyMorphClears, wardleyMorphProps, } from './presets.js';
|
|
5
|
+
import { WARDLEY_ROLE } from './roles.js';
|
|
6
|
+
/**
|
|
7
|
+
* What a Wardley artefact may BECOME — the declaration behind the "Change type"
|
|
8
|
+
* dropdown on a selected component's contextual toolbar.
|
|
9
|
+
*
|
|
10
|
+
* ## One family, and why it is exactly these four
|
|
11
|
+
*
|
|
12
|
+
* A component, a market, an ecosystem and a pipeline are four ways of saying
|
|
13
|
+
* the SAME thing: something the value chain depends on. The map's own reading
|
|
14
|
+
* agrees — `roles.ts` makes `market` and `ecosystem` children of `component` in
|
|
15
|
+
* the role tree — and the modelling gesture the morph exists for is the one
|
|
16
|
+
* every mapper makes: you draw a component, you look at the map, and you
|
|
17
|
+
* realise that what you drew is a market of many suppliers, or a whole
|
|
18
|
+
* ecosystem, or a pipeline of choices. Today that costs a delete, a re-draw,
|
|
19
|
+
* every dependency re-attached and the name typed again.
|
|
20
|
+
*
|
|
21
|
+
* The family is DATA and nothing derives it — the same call BPMN and C4 make.
|
|
22
|
+
* Deriving it from `roleIsA` would have been wrong in both directions here: it
|
|
23
|
+
* would reach from market to component and not back, and it would say nothing
|
|
24
|
+
* at all about the pipeline, which is not a specialisation of anything and is
|
|
25
|
+
* still the fourth way of drawing the same claim.
|
|
26
|
+
*
|
|
27
|
+
* ## The two kinds deliberately left out
|
|
28
|
+
*
|
|
29
|
+
* - **`anchor`** is the user, and the need they have. It is a role of its own
|
|
30
|
+
* precisely because it is NOT a component (`roles.ts`): it is what the value
|
|
31
|
+
* chain hangs from, not a link in it. A menu offering "turn this user into a
|
|
32
|
+
* pipeline" would invite a map with no anchor at all — the one thing a
|
|
33
|
+
* Wardley map cannot be read without.
|
|
34
|
+
* - **`method`** is not an artefact but an annotation ON one: the coloured ring
|
|
35
|
+
* says how a component is being BUILT (buy / build / outsource), and its fill
|
|
36
|
+
* encodes the choice. Morphing it away would silently discard that decision,
|
|
37
|
+
* and morphing into it would have to invent one.
|
|
38
|
+
*
|
|
39
|
+
* `handle` is in no family either, and does not need excluding by name: it is
|
|
40
|
+
* not an artefact at all, so nothing selects it and {@link wardleyNodeOfComponent}
|
|
41
|
+
* never resolves to one.
|
|
42
|
+
*/
|
|
43
|
+
export const WARDLEY_MORPH_FAMILIES = [
|
|
44
|
+
// Declaration order is menu order, and it opens on the plain component: the
|
|
45
|
+
// undecorated artefact is the honest first draft, and the three others are
|
|
46
|
+
// refinements of it — the same call `commands.ts` makes about the senior row.
|
|
47
|
+
['component', 'market', 'ecosystem', 'pipeline'],
|
|
48
|
+
];
|
|
49
|
+
/** The same set, as a lookup — the gate every resolution below runs through. */
|
|
50
|
+
const MORPH_KINDS = new Set(WARDLEY_MORPH_FAMILIES.flat());
|
|
51
|
+
/**
|
|
52
|
+
* The creation command that draws each kind, keyed BY that kind.
|
|
53
|
+
*
|
|
54
|
+
* Derived from `telemetry.element`, which is where the kind is already written
|
|
55
|
+
* down (`node:market`) and is documented as a historical value that must not be
|
|
56
|
+
* renamed. Deriving rather than restating is what stops a second table of
|
|
57
|
+
* labels and icons drifting from the one the sub-menu and the catalogue read.
|
|
58
|
+
*/
|
|
59
|
+
const NODE_COMMANDS = new Map(wardleyCommands.flatMap(command => {
|
|
60
|
+
const element = command.telemetry?.element;
|
|
61
|
+
return element?.startsWith('node:')
|
|
62
|
+
? [[element.slice('node:'.length), command]]
|
|
63
|
+
: [];
|
|
64
|
+
}));
|
|
65
|
+
/** A kind's wording: the creation command's own key and English. */
|
|
66
|
+
function labelOf(kind) {
|
|
67
|
+
const command = NODE_COMMANDS.get(kind);
|
|
68
|
+
return {
|
|
69
|
+
key: command?.labelKey,
|
|
70
|
+
fallback: command?.labelFallback ?? kind,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** A kind's icon: the creation command's own, reused rather than redrawn. */
|
|
74
|
+
function iconOf(kind) {
|
|
75
|
+
const iconKey = NODE_COMMANDS.get(kind)?.iconKey;
|
|
76
|
+
return ((iconKey && wardleyCommandIcons[iconKey]) ||
|
|
77
|
+
wardleyCommandIcons['wardley.component']);
|
|
78
|
+
}
|
|
79
|
+
/* ── Resolving the composite ───────────────────────────────────────────── */
|
|
80
|
+
/**
|
|
81
|
+
* Every element under a group, however deeply nested.
|
|
82
|
+
*
|
|
83
|
+
* Recursive because the pipeline's own creation site nests — (handle + label),
|
|
84
|
+
* then (body + that) — so a flat pass over `childElements` would miss half of
|
|
85
|
+
* one of the four artefacts this file is about.
|
|
86
|
+
*/
|
|
87
|
+
function* descendants(group) {
|
|
88
|
+
for (const child of group.childElements) {
|
|
89
|
+
yield child;
|
|
90
|
+
if (child instanceof GroupElementModel)
|
|
91
|
+
yield* descendants(child);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The `wardleyNode` a selected GROUP is the artefact of — `undefined` when the
|
|
96
|
+
* group is not one.
|
|
97
|
+
*
|
|
98
|
+
* Every Wardley artefact in this family is a composite: a component is a circle
|
|
99
|
+
* and its label, a market adds three dots and a triangle, and a pipeline is a
|
|
100
|
+
* body plus a handle plus a label in a nested group. So what a click selects is
|
|
101
|
+
* the outer group, and what carries `kind` is one node inside it — RECURSIVELY
|
|
102
|
+
* inside it, because the pipeline's own creation nests.
|
|
103
|
+
*
|
|
104
|
+
* Three conditions, and each one excludes a piece of the notation rather than
|
|
105
|
+
* an accident:
|
|
106
|
+
*
|
|
107
|
+
* - a `WardleyNodeElementModel`, which leaves out the labels and the connectors;
|
|
108
|
+
* - with a ROLE, which leaves out the market's three neutral dots — they are the
|
|
109
|
+
* glyph's wiring and carry none by construction (`presets.ts`);
|
|
110
|
+
* - whose kind is in the family, which leaves out the pipeline's `handle` and
|
|
111
|
+
* any `anchor` or `method` somebody grouped in beside the artefact.
|
|
112
|
+
*
|
|
113
|
+
* TWO candidates is a REFUSAL rather than a first-wins pick, exactly as C4's
|
|
114
|
+
* resolution refuses a group of two components: morphing "it" would mean
|
|
115
|
+
* picking one by document order, and the honest answer to an ambiguous
|
|
116
|
+
* selection is nothing at all. A plain lasso round three shapes, a C4 component
|
|
117
|
+
* and a group of two Wardley components all answer `undefined`, so none of them
|
|
118
|
+
* is ever offered the menu.
|
|
119
|
+
*/
|
|
120
|
+
export function wardleyNodeOfComponent(model) {
|
|
121
|
+
if (!(model instanceof GroupElementModel))
|
|
122
|
+
return undefined;
|
|
123
|
+
let found;
|
|
124
|
+
for (const child of descendants(model)) {
|
|
125
|
+
if (!(child instanceof WardleyNodeElementModel))
|
|
126
|
+
continue;
|
|
127
|
+
if (child.role === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
if (!MORPH_KINDS.has(child.kind))
|
|
130
|
+
continue;
|
|
131
|
+
if (found)
|
|
132
|
+
return undefined;
|
|
133
|
+
found = child;
|
|
134
|
+
}
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
/* ── The structural half of the morph ──────────────────────────────────── */
|
|
138
|
+
/**
|
|
139
|
+
* The name an artefact should carry once it has morphed — or `null` when the
|
|
140
|
+
* name is the AUTHOR's and must not be touched.
|
|
141
|
+
*
|
|
142
|
+
* Exactly one case rewrites: the label is the source kind's own creation
|
|
143
|
+
* prompt, letter for letter, which is what an artefact nobody has named still
|
|
144
|
+
* says. Everything else — a name typed over it, the prompt with a word added, a
|
|
145
|
+
* cleared label, another kind's prompt — is content, and content survives a
|
|
146
|
+
* morph untouched. The same timid rule `c4MorphedTitle` states, and for the same
|
|
147
|
+
* reason: a morph may not take away words somebody put on the picture.
|
|
148
|
+
*
|
|
149
|
+
* Pure and total over every string: the input is a canvas text element somebody
|
|
150
|
+
* may have typed anything into.
|
|
151
|
+
*/
|
|
152
|
+
export function wardleyMorphedLabel(from, to, rawText) {
|
|
153
|
+
return (rawText ?? '').trim() === WARDLEY_NODE_LABEL[from]
|
|
154
|
+
? WARDLEY_NODE_LABEL[to]
|
|
155
|
+
: null;
|
|
156
|
+
}
|
|
157
|
+
/** The centre of an element, from the box it currently occupies. */
|
|
158
|
+
function centreOf(model) {
|
|
159
|
+
const [x, y, w, h] = model.deserializedXYWH;
|
|
160
|
+
return [x + w / 2, y + h / 2];
|
|
161
|
+
}
|
|
162
|
+
/** The label of a composite — `undefined` when there is not exactly one. */
|
|
163
|
+
function labelOfComposite(group) {
|
|
164
|
+
let found;
|
|
165
|
+
for (const child of descendants(group)) {
|
|
166
|
+
if (!(child instanceof TextElementModel))
|
|
167
|
+
continue;
|
|
168
|
+
if (child.role !== WARDLEY_ROLE.label)
|
|
169
|
+
continue;
|
|
170
|
+
if (found)
|
|
171
|
+
return undefined;
|
|
172
|
+
found = child;
|
|
173
|
+
}
|
|
174
|
+
return found;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Point every connector that named `fromId` at `toId` instead.
|
|
178
|
+
*
|
|
179
|
+
* The whole point of morphing rather than re-drawing: a component wired into
|
|
180
|
+
* half a value chain becomes a pipeline and keeps every dependency it had. The
|
|
181
|
+
* endpoint is rewritten to the bare `{ id }` and the `position` is dropped on
|
|
182
|
+
* purpose — a position is stored RELATIVE to the element it names, so carrying
|
|
183
|
+
* one across to a different box would land the arrow somewhere nobody chose.
|
|
184
|
+
* Every Wardley node is `centerAnchorOnly` anyway, so a connector this framework
|
|
185
|
+
* drew never had one.
|
|
186
|
+
*/
|
|
187
|
+
function reanchor(surface, fromId, toId) {
|
|
188
|
+
for (const element of surface.elementModels) {
|
|
189
|
+
if (!(element instanceof ConnectorElementModel))
|
|
190
|
+
continue;
|
|
191
|
+
if (element.isLocked())
|
|
192
|
+
continue;
|
|
193
|
+
if (element.source?.id === fromId) {
|
|
194
|
+
surface.updateElement(element.id, { source: { id: toId } });
|
|
195
|
+
}
|
|
196
|
+
if (element.target?.id === fromId) {
|
|
197
|
+
surface.updateElement(element.id, { target: { id: toId } });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Take the market's glyph apart: the three neutral dots, and the triangle
|
|
203
|
+
* wiring them.
|
|
204
|
+
*
|
|
205
|
+
* Identified the way {@link wardleyNodeOfComponent} identifies what it must NOT
|
|
206
|
+
* resolve to — a `wardleyNode` with no role — and the connectors by the fact
|
|
207
|
+
* that both their ends name one of those dots. Never by document order, and
|
|
208
|
+
* never by "the connectors in this group": a dependency the user drew from
|
|
209
|
+
* their market to something else is a child of nothing and must survive, and one
|
|
210
|
+
* they drew INTO the composite still has one end outside it.
|
|
211
|
+
*/
|
|
212
|
+
function removeMarketGlyph(group) {
|
|
213
|
+
const surface = group.surface;
|
|
214
|
+
const dots = [...descendants(group)].filter((child) => child instanceof WardleyNodeElementModel && child.role === undefined);
|
|
215
|
+
const dotIds = new Set(dots.map(dot => dot.id));
|
|
216
|
+
for (const child of [...descendants(group)]) {
|
|
217
|
+
if (!(child instanceof ConnectorElementModel))
|
|
218
|
+
continue;
|
|
219
|
+
const { id: sourceId } = child.source ?? {};
|
|
220
|
+
const { id: targetId } = child.target ?? {};
|
|
221
|
+
if (sourceId === undefined || targetId === undefined)
|
|
222
|
+
continue;
|
|
223
|
+
if (!dotIds.has(sourceId) || !dotIds.has(targetId))
|
|
224
|
+
continue;
|
|
225
|
+
surface.deleteElement(child.id);
|
|
226
|
+
}
|
|
227
|
+
for (const dot of dots)
|
|
228
|
+
surface.deleteElement(dot.id);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Build the market's glyph round a centre: three dots and the triangle between
|
|
232
|
+
* them, from the very same constants `createWardleyMarket` uses.
|
|
233
|
+
*
|
|
234
|
+
* Added to the group AFTER the circle they sit in, which is also the painting
|
|
235
|
+
* order the creation site produces — a market drawn from the sub-menu and one
|
|
236
|
+
* morphed into being are the same stack of elements.
|
|
237
|
+
*/
|
|
238
|
+
function addMarketGlyph(group, cx, cy) {
|
|
239
|
+
const surface = group.surface;
|
|
240
|
+
const dotIds = wardleyMarketDotBoxes(cx, cy).map(xywh => surface.addElement(wardleyMarketDotProps({ xywh })));
|
|
241
|
+
const linkIds = wardleyMarketLinkPairs(dotIds).map(([a, b]) => surface.addElement(wardleyMarketLinkProps(a, b)));
|
|
242
|
+
for (const id of [...dotIds, ...linkIds]) {
|
|
243
|
+
const element = surface.getElementById(id);
|
|
244
|
+
if (element)
|
|
245
|
+
group.addChild(element);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Give a pipeline the handle it is connected through, and move the user's own
|
|
250
|
+
* links onto it.
|
|
251
|
+
*
|
|
252
|
+
* ## The handle lands FLAT in the outer group — a documented simplification
|
|
253
|
+
*
|
|
254
|
+
* `createWardleyPipeline` nests: it groups (handle + label), then groups that
|
|
255
|
+
* with the body. Nothing depends on the nesting — the handle is found by kind
|
|
256
|
+
* and the label by role, both recursively — and a morph that rebuilt it would
|
|
257
|
+
* have to move the user's label into a group it never asked for. So the morph
|
|
258
|
+
* adds the handle as a sibling of the body. A pipeline morphed into being is
|
|
259
|
+
* therefore ONE group of three elements where a drawn one is a group of two;
|
|
260
|
+
* they behave identically on the canvas, in the exporter and under every rule,
|
|
261
|
+
* and the reverse morph flattens a drawn one the same way.
|
|
262
|
+
*/
|
|
263
|
+
function addHandle(group, carrier, cx, cy) {
|
|
264
|
+
const surface = group.surface;
|
|
265
|
+
const handleId = surface.addElement(wardleyHandleProps({ xywh: wardleyHandleBox(cx, cy) }));
|
|
266
|
+
const handle = surface.getElementById(handleId);
|
|
267
|
+
if (handle)
|
|
268
|
+
group.addChild(handle);
|
|
269
|
+
// A pipeline body declares `connectable === false`, so every link the user
|
|
270
|
+
// drew to the component has to move to the handle or it would point at
|
|
271
|
+
// something that no longer accepts it.
|
|
272
|
+
reanchor(surface, carrier.id, handleId);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Take the handle away, and give the user's links back to the body.
|
|
276
|
+
*
|
|
277
|
+
* The mirror of {@link addHandle}, plus the one piece of tidying the nesting
|
|
278
|
+
* makes necessary: a pipeline DRAWN from the sub-menu keeps its label in an
|
|
279
|
+
* inner group with the handle, and removing the handle would leave a group of
|
|
280
|
+
* one behind. The label is promoted into the outer group and the empty wrapper
|
|
281
|
+
* deleted, which is what leaves a morphed artefact looking exactly like a drawn
|
|
282
|
+
* one of its new kind.
|
|
283
|
+
*/
|
|
284
|
+
function removeHandle(group, carrier) {
|
|
285
|
+
const surface = group.surface;
|
|
286
|
+
const handle = [...descendants(group)].find((child) => child instanceof WardleyNodeElementModel && child.kind === 'handle');
|
|
287
|
+
if (!handle)
|
|
288
|
+
return;
|
|
289
|
+
// Before the deletion, never after: the links have to name something.
|
|
290
|
+
reanchor(surface, handle.id, carrier.id);
|
|
291
|
+
const inner = handle.group;
|
|
292
|
+
surface.deleteElement(handle.id);
|
|
293
|
+
if (!(inner instanceof GroupElementModel) || inner === group)
|
|
294
|
+
return;
|
|
295
|
+
// Emptied first, so `deleteElement` — which deletes a group's children with
|
|
296
|
+
// it — has nothing left to take.
|
|
297
|
+
for (const orphan of [...inner.childElements]) {
|
|
298
|
+
inner.removeChild(orphan);
|
|
299
|
+
group.addChild(orphan);
|
|
300
|
+
}
|
|
301
|
+
surface.deleteElement(inner.id);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Keep the artefact's own name saying what the artefact now is.
|
|
305
|
+
*
|
|
306
|
+
* The one exception to "the words and the placement are the user's": the
|
|
307
|
+
* placeholder rule of {@link wardleyMorphedLabel}. In place and in one
|
|
308
|
+
* transaction, because the `Y.Text` instance is what a bound editor holds, and
|
|
309
|
+
* inside the caller's `captureSync`, so the rewrite is part of the same single
|
|
310
|
+
* ctrl+z as the kind that made it necessary.
|
|
311
|
+
*/
|
|
312
|
+
function rewriteLabel(group, from, to) {
|
|
313
|
+
const label = labelOfComposite(group);
|
|
314
|
+
if (!label || label.isLocked())
|
|
315
|
+
return;
|
|
316
|
+
const text = label.text.toString().trim();
|
|
317
|
+
const next = wardleyMorphedLabel(from, to, text);
|
|
318
|
+
if (next === null || next === text)
|
|
319
|
+
return;
|
|
320
|
+
label.surface.store.transact(() => {
|
|
321
|
+
label.text.delete(0, label.text.length);
|
|
322
|
+
label.text.insert(0, next);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Everything the morph owes a Wardley composite beyond the props patch — the
|
|
327
|
+
* `afterMorph` half of the spec, run inside the generic module's one
|
|
328
|
+
* `captureSync` so all of it is a single ctrl+z.
|
|
329
|
+
*
|
|
330
|
+
* ## The geometry, and why this framework writes it
|
|
331
|
+
*
|
|
332
|
+
* A DEVIATION from BPMN and C4, decided by the PO: those two keep the box the
|
|
333
|
+
* user gave the element, because there a size is a preference. Here it is the
|
|
334
|
+
* NOTATION. A market is drawn bigger than a component so a reader can tell them
|
|
335
|
+
* apart at a glance, an ecosystem bigger again, and a pipeline is a 120-wide bar
|
|
336
|
+
* — a market left at a component's 18 pixels is an unreadable smudge with three
|
|
337
|
+
* dots crammed into it. So the target's canonical size is applied, CENTRED on
|
|
338
|
+
* where the artefact already stands: the thing does not move, it becomes the
|
|
339
|
+
* size its notation says it is. The label keeps its own place, which is the
|
|
340
|
+
* user's.
|
|
341
|
+
*
|
|
342
|
+
* ## And why the rest is here rather than in the patch
|
|
343
|
+
*
|
|
344
|
+
* Because a market IS its three dots and a pipeline IS its handle. A patch can
|
|
345
|
+
* write `kind: 'market'` onto a circle and the result is a circle that claims to
|
|
346
|
+
* be a market and does not look like one; a patch cannot create an element, nor
|
|
347
|
+
* move a connector somebody drew. That is exactly what `afterMorph` is for, and
|
|
348
|
+
* the ORDER below is the whole of the care it needs: the glyph the artefact is
|
|
349
|
+
* LEAVING comes apart before the one it is arriving at is built, so a
|
|
350
|
+
* market → pipeline never has three dots and a handle at once.
|
|
351
|
+
*/
|
|
352
|
+
export function wardleyMorphComposite(selected, from, to) {
|
|
353
|
+
if (!(selected instanceof GroupElementModel))
|
|
354
|
+
return;
|
|
355
|
+
const carrier = wardleyNodeOfComponent(selected);
|
|
356
|
+
if (!carrier)
|
|
357
|
+
return;
|
|
358
|
+
const [cx, cy] = centreOf(carrier);
|
|
359
|
+
carrier.surface.updateElement(carrier.id, {
|
|
360
|
+
xywh: wardleyCanonicalBox(to, cx, cy),
|
|
361
|
+
});
|
|
362
|
+
if (from === 'market')
|
|
363
|
+
removeMarketGlyph(selected);
|
|
364
|
+
if (from === 'pipeline')
|
|
365
|
+
removeHandle(selected, carrier);
|
|
366
|
+
if (to === 'market')
|
|
367
|
+
addMarketGlyph(selected, cx, cy);
|
|
368
|
+
if (to === 'pipeline')
|
|
369
|
+
addHandle(selected, carrier, cx, cy);
|
|
370
|
+
rewriteLabel(selected, from, to);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Wardley's morph declaration, handed to the generic `morphToolbarConfig`.
|
|
374
|
+
*
|
|
375
|
+
* `modelType` is the native `GroupElementModel` and not
|
|
376
|
+
* `WardleyNodeElementModel`, because every artefact in this family is a
|
|
377
|
+
* composite and the group is what a click on one selects. Everything that makes
|
|
378
|
+
* that safe is in {@link wardleyNodeOfComponent}: the toolbar's own homogeneity
|
|
379
|
+
* test only proves that each selected element is A group, and the resolution
|
|
380
|
+
* proves that each one is one of OURS.
|
|
381
|
+
*
|
|
382
|
+
* `propsOf` is the shipped creation builder minus `type` / `xywh` / `text`
|
|
383
|
+
* ({@link wardleyMorphProps}), so the palette and the morph cannot disagree
|
|
384
|
+
* about what a pipeline looks like, and `clearOf` removes the keys the target
|
|
385
|
+
* does not write — `radius`, which only the pipeline has.
|
|
386
|
+
*/
|
|
387
|
+
export const WARDLEY_MORPH_SPEC = {
|
|
388
|
+
framework: 'wardley',
|
|
389
|
+
families: WARDLEY_MORPH_FAMILIES,
|
|
390
|
+
modelType: GroupElementModel,
|
|
391
|
+
resolveTarget: wardleyNodeOfComponent,
|
|
392
|
+
kindOf: (model) => model instanceof WardleyNodeElementModel && MORPH_KINDS.has(model.kind)
|
|
393
|
+
? model.kind
|
|
394
|
+
: undefined,
|
|
395
|
+
// The NODE's role, which is the only role a Wardley artefact has: the group
|
|
396
|
+
// carries none, the label carries `wardley:label` (which says it is a name,
|
|
397
|
+
// not what it names) and the market's dots carry none at all.
|
|
398
|
+
roleOf: kind => WARDLEY_ROLE[kind],
|
|
399
|
+
propsOf: wardleyMorphProps,
|
|
400
|
+
clearOf: wardleyMorphClears,
|
|
401
|
+
afterMorph: wardleyMorphComposite,
|
|
402
|
+
labelOf,
|
|
403
|
+
iconOf,
|
|
404
|
+
label: morphLabel('com.labre.morph.toolbar.label', 'Change type'),
|
|
405
|
+
};
|
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";
|