@formicoidea/labre-framework-wardley 0.58.0 → 0.60.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 +3 -19
- package/dist/actions.js +103 -149
- package/dist/import.js +9 -28
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/interchange-headless.d.ts +1 -0
- package/dist/interchange-headless.js +3 -0
- package/dist/toolbox.d.ts +94 -0
- package/dist/toolbox.js +348 -0
- package/package.json +2 -2
package/dist/actions.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
|
4
4
|
import { type GfxController, type GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
|
|
5
5
|
import { type WardleyExportBoard } from './export.js';
|
|
6
6
|
import { type WardleyAreaShape, type WardleyArtefactKind } from './presets.js';
|
|
7
|
+
import { WARDLEY_AXIS_SEED } from './toolbox.js';
|
|
7
8
|
/**
|
|
8
9
|
* Standalone creation/activation actions for the Wardley toolbox — the
|
|
9
10
|
* BEHAVIOUR layer, shared by every surface. They only depend on the
|
|
@@ -14,25 +15,7 @@ import { type WardleyAreaShape, type WardleyArtefactKind } from './presets.js';
|
|
|
14
15
|
* every surface goes through. `WardleyActionSource` is gone with it — the
|
|
15
16
|
* segment/module discrimination it carried is now `CommandInvocation.surface`.
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
-
* The three axis words a translated variant's background is seeded with —
|
|
19
|
-
* declared once, so the resolver below and the manifest
|
|
20
|
-
* (`../translations.ts`) read the very same fallback.
|
|
21
|
-
*/
|
|
22
|
-
export declare const WARDLEY_AXIS_SEED: {
|
|
23
|
-
readonly opportunity: {
|
|
24
|
-
readonly key: "com.labre.wardley.seed.axis-opportunity";
|
|
25
|
-
readonly fallback: "Opportunity";
|
|
26
|
-
};
|
|
27
|
-
readonly benefit: {
|
|
28
|
-
readonly key: "com.labre.wardley.seed.axis-benefit";
|
|
29
|
-
readonly fallback: "Benefit";
|
|
30
|
-
};
|
|
31
|
-
readonly investment: {
|
|
32
|
-
readonly key: "com.labre.wardley.seed.axis-investment";
|
|
33
|
-
readonly fallback: "Investment";
|
|
34
|
-
};
|
|
35
|
-
};
|
|
18
|
+
export { WARDLEY_AXIS_SEED };
|
|
36
19
|
/**
|
|
37
20
|
* The single-circle node flavours: one connectable ellipse + a label to its
|
|
38
21
|
* right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
|
|
@@ -116,6 +99,7 @@ export declare function createWardleyPorter(gfx: GfxController): void;
|
|
|
116
99
|
* SemiBold, which no other Wardley label is: these two are annotations laid
|
|
117
100
|
* over a map that is already full of names, and the weight is what keeps the
|
|
118
101
|
* word readable as a remark ABOUT the map rather than as one more component.
|
|
102
|
+
* Both rules are the builder's (`wardleyNodeElements`).
|
|
119
103
|
*/
|
|
120
104
|
export declare function createWardleyAccelerator(gfx: GfxController, kind: 'accelerator' | 'decelerator'): void;
|
|
121
105
|
/**
|
package/dist/actions.js
CHANGED
|
@@ -2,7 +2,7 @@ import { backgroundSize, containingFrame, DefaultTool, indexOverBackgrounds, run
|
|
|
2
2
|
import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
|
|
3
3
|
import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
|
|
4
4
|
import { PolygonTool } from '@formicoidea/labre-core/gfx/shape';
|
|
5
|
-
import { ConnectorMode,
|
|
5
|
+
import { ConnectorMode, FrameworkBackgroundElementModel, TextElementModel, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
|
|
6
6
|
import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
7
7
|
import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
|
|
8
8
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
@@ -12,9 +12,10 @@ import { WARDLEY_SVG_IMPORT } from './interchange.js';
|
|
|
12
12
|
import { wardleySafeFilename } from './export.js';
|
|
13
13
|
import { wardleyBoardFrom } from './export-board.js';
|
|
14
14
|
import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
|
|
15
|
-
import { INERTIA_SIZE,
|
|
16
|
-
import { wardleyAreaBox, wardleyAreaProps,
|
|
15
|
+
import { INERTIA_SIZE, PORTER_SIZE } from './node/consts.js';
|
|
16
|
+
import { wardleyAreaBox, wardleyAreaProps, WARDLEY_AREA_SIZE, WARDLEY_EDGE_STYLE, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeLabelKey, } from './presets.js';
|
|
17
17
|
import { WARDLEY_ROLE } from './roles.js';
|
|
18
|
+
import { WARDLEY_AXIS_SEED, wardleyAreaElement, wardleyInertiaElement, wardleyMapElement, wardleyMarketElements, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
|
|
18
19
|
/**
|
|
19
20
|
* Standalone creation/activation actions for the Wardley toolbox — the
|
|
20
21
|
* BEHAVIOUR layer, shared by every surface. They only depend on the
|
|
@@ -25,61 +26,27 @@ import { WARDLEY_ROLE } from './roles.js';
|
|
|
25
26
|
* every surface goes through. `WardleyActionSource` is gone with it — the
|
|
26
27
|
* segment/module discrimination it carried is now `CommandInvocation.surface`.
|
|
27
28
|
*/
|
|
29
|
+
// Declared with the editor-free toolbox (`docs/adr/0045`), which writes the
|
|
30
|
+
// English fallbacks; re-exported here unchanged.
|
|
31
|
+
export { WARDLEY_AXIS_SEED };
|
|
28
32
|
/**
|
|
29
|
-
* The three axis words a translated variant's background is seeded with
|
|
30
|
-
*
|
|
31
|
-
* (`../translations.ts`) read the very same fallback.
|
|
32
|
-
*/
|
|
33
|
-
export const WARDLEY_AXIS_SEED = {
|
|
34
|
-
opportunity: {
|
|
35
|
-
key: 'com.labre.wardley.seed.axis-opportunity',
|
|
36
|
-
fallback: 'Opportunity',
|
|
37
|
-
},
|
|
38
|
-
benefit: {
|
|
39
|
-
key: 'com.labre.wardley.seed.axis-benefit',
|
|
40
|
-
fallback: 'Benefit',
|
|
41
|
-
},
|
|
42
|
-
investment: {
|
|
43
|
-
key: 'com.labre.wardley.seed.axis-investment',
|
|
44
|
-
fallback: 'Investment',
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* Per-variant default label overrides applied at creation (all remain editable
|
|
49
|
-
* afterwards via the inline editor / toggles). The gradient itself is driven by
|
|
50
|
-
* `variant` in the renderer.
|
|
33
|
+
* The three axis words a translated variant's background is seeded with,
|
|
34
|
+
* resolved through the host's catalogue.
|
|
51
35
|
*
|
|
52
36
|
* Resolved through the translation seam since the i18n pass (#278): the ten
|
|
53
37
|
* label props still default to `undefined` on the declaration itself, but
|
|
54
38
|
* `opportunity` and `benefit` are the two variants whose Y axis is PROSE the
|
|
55
|
-
* gesture bakes into the document at creation — `WARDLEY_AXIS_SEED`
|
|
56
|
-
*
|
|
57
|
-
* exactly like every other seed in this file.
|
|
58
|
-
*
|
|
59
|
-
* Industrialized…), which are declaration `labelKey`s and were already fully
|
|
60
|
-
* localisable — this function names no override for them.
|
|
39
|
+
* gesture bakes into the document at creation — `WARDLEY_AXIS_SEED` is what a
|
|
40
|
+
* host's catalogue is offered for the three words, resolved HERE, once,
|
|
41
|
+
* exactly like every other seed in this file. Which variant writes which word
|
|
42
|
+
* is the builder's (`wardleyMapElement`).
|
|
61
43
|
*/
|
|
62
|
-
function
|
|
44
|
+
function axisSeeds(gfx) {
|
|
63
45
|
const axis = (def) => translateKey(gfx.std, def.key, def.fallback);
|
|
64
46
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
yAxisTitle: axis(WARDLEY_AXIS_SEED.opportunity),
|
|
69
|
-
showVisibilityLabels: false,
|
|
70
|
-
showCornerLabels: false,
|
|
71
|
-
},
|
|
72
|
-
// The Y axis splits into Benefit (top) / Investment (bottom) around a zero
|
|
73
|
-
// line drawn by the renderer.
|
|
74
|
-
benefit: {
|
|
75
|
-
yAxisTitle: '',
|
|
76
|
-
visibilityHigh: axis(WARDLEY_AXIS_SEED.benefit),
|
|
77
|
-
visibilityLow: axis(WARDLEY_AXIS_SEED.investment),
|
|
78
|
-
showCornerLabels: false,
|
|
79
|
-
},
|
|
80
|
-
// Keeps the classic labels (Value Chain / Uncharted / Industrialized…); only
|
|
81
|
-
// the grey gradient differs.
|
|
82
|
-
'evolution-gradient': {},
|
|
47
|
+
opportunity: axis(WARDLEY_AXIS_SEED.opportunity),
|
|
48
|
+
benefit: axis(WARDLEY_AXIS_SEED.benefit),
|
|
49
|
+
investment: axis(WARDLEY_AXIS_SEED.investment),
|
|
83
50
|
};
|
|
84
51
|
}
|
|
85
52
|
function finish(gfx, id) {
|
|
@@ -97,26 +64,63 @@ function group(gfx, ids) {
|
|
|
97
64
|
return result.groupId || ids[0];
|
|
98
65
|
}
|
|
99
66
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* the "Change type" dropdown read that one description, so a market drawn here
|
|
104
|
-
* and a component morphed into one are the same element (`presets.ts`).
|
|
67
|
+
* Write what a toolbox builder returned (`toolbox.ts`, `docs/adr/0045`) and
|
|
68
|
+
* return the id of the LAST element — the outermost group of a composite, or
|
|
69
|
+
* the one element of a single artefact — which is what the gesture selects.
|
|
105
70
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* the
|
|
71
|
+
* The builder is the ONE assembly of an artefact, shared with a host that has
|
|
72
|
+
* no editor; what stays here is what only an editor has. Local ids are minted
|
|
73
|
+
* in order, so a connector's ends and a group's members, which always come
|
|
74
|
+
* after the elements they name, resolve to the real ids. A group goes through
|
|
75
|
+
* {@link createGroupCommand} as it always has, which writes the group's
|
|
76
|
+
* translated title and its layer: the builder only says who the members are.
|
|
110
77
|
*/
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
78
|
+
function place(gfx, elements) {
|
|
79
|
+
const surface = gfx.surface;
|
|
80
|
+
if (!surface)
|
|
81
|
+
return;
|
|
82
|
+
const minted = new Map();
|
|
83
|
+
const real = (local) => minted.get(local) ?? local;
|
|
84
|
+
let last;
|
|
85
|
+
for (const { id: local, ...props } of elements) {
|
|
86
|
+
if (props.type === 'group') {
|
|
87
|
+
const children = Object.keys(props.children);
|
|
88
|
+
last = group(gfx, children.map(real));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
for (const end of ['source', 'target']) {
|
|
92
|
+
const ref = props[end];
|
|
93
|
+
if (ref?.id !== undefined)
|
|
94
|
+
props[end] = { ...ref, id: real(ref.id) };
|
|
95
|
+
}
|
|
96
|
+
last = surface.addElement(props);
|
|
97
|
+
}
|
|
98
|
+
if (typeof local === 'string' && last)
|
|
99
|
+
minted.set(local, last);
|
|
100
|
+
}
|
|
101
|
+
return last;
|
|
102
|
+
}
|
|
103
|
+
/** The point that centres a box of `w`×`h` on the viewport. */
|
|
104
|
+
function viewportTopLeft(gfx, w, h) {
|
|
105
|
+
const { centerX, centerY } = gfx.viewport;
|
|
106
|
+
return { x: centerX - w / 2, y: centerY - h / 2 };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A labelled kind's name prompt, resolved HERE, once — the prompt a node
|
|
110
|
+
* nobody has named still carries, exactly like a BPMN node's caption.
|
|
111
|
+
* `gfx.std` is how this file reaches the translation seam: these creation
|
|
112
|
+
* sites take `GfxController`, not `BlockStdScope`, directly (`WardleyView`
|
|
113
|
+
* reads the same member).
|
|
114
|
+
*/
|
|
115
|
+
function seedName(gfx, kind) {
|
|
116
|
+
return translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]);
|
|
116
117
|
}
|
|
117
|
-
/**
|
|
118
|
-
function
|
|
119
|
-
|
|
118
|
+
/** Place a node + name composite of one kind, centred on the viewport. */
|
|
119
|
+
function placeNode(gfx, kind) {
|
|
120
|
+
const { w, h } = WARDLEY_NODE_SIZE[kind];
|
|
121
|
+
const id = place(gfx, wardleyNodeElements(kind, viewportTopLeft(gfx, w, h), seedName(gfx, kind)));
|
|
122
|
+
if (id)
|
|
123
|
+
finish(gfx, id);
|
|
120
124
|
}
|
|
121
125
|
/** Create a wardley map background of the given variant, viewport-centered. */
|
|
122
126
|
export function createWardleyBackground(gfx, variant = 'classic') {
|
|
@@ -132,22 +136,12 @@ export function createWardleyBackground(gfx, variant = 'classic') {
|
|
|
132
136
|
atLeastWidth = Math.max(atLeastWidth, ew);
|
|
133
137
|
atLeastHeight = Math.max(atLeastHeight, eh);
|
|
134
138
|
}
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// declaration owns it, so a templated map and a hand-drawn one agree.
|
|
142
|
-
role: WARDLEY_BACKGROUND.role,
|
|
143
|
-
// A map is a frame you place things on, not a shape you nudge — the
|
|
144
|
-
// declaration decides, and the toolbar toggle takes over from there.
|
|
145
|
-
resizeEnabled: WARDLEY_BACKGROUND.geometry.resizable,
|
|
146
|
-
variant,
|
|
147
|
-
...backgroundVariantDefaults(gfx)[variant],
|
|
148
|
-
xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
|
|
149
|
-
});
|
|
150
|
-
finish(gfx, id);
|
|
139
|
+
const size = backgroundSize(WARDLEY_BACKGROUND, atLeastWidth, atLeastHeight);
|
|
140
|
+
const id = place(gfx, [
|
|
141
|
+
wardleyMapElement(variant, viewportTopLeft(gfx, size.width, size.height), axisSeeds(gfx), undefined, size),
|
|
142
|
+
]);
|
|
143
|
+
if (id)
|
|
144
|
+
finish(gfx, id);
|
|
151
145
|
}
|
|
152
146
|
/**
|
|
153
147
|
* Create a single-circle node (component / anchor / ecosystem / method):
|
|
@@ -155,30 +149,14 @@ export function createWardleyBackground(gfx, variant = 'classic') {
|
|
|
155
149
|
* move together (enter the group to reposition / edit the label).
|
|
156
150
|
*/
|
|
157
151
|
export function createWardleyNode(gfx, kind) {
|
|
158
|
-
|
|
159
|
-
if (!surface)
|
|
160
|
-
return;
|
|
161
|
-
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
162
|
-
const nodeId = addNode(surface, kind, cx, cy);
|
|
163
|
-
const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
|
|
164
|
-
const labelId = addLabel(surface,
|
|
165
|
-
// Resolved HERE, once — the prompt a node nobody has named still carries,
|
|
166
|
-
// exactly like a BPMN node's caption. `gfx.std` is how this file reaches
|
|
167
|
-
// the translation seam: these creation sites take `GfxController`, not
|
|
168
|
-
// `BlockStdScope`, directly (`WardleyView` reads the same member).
|
|
169
|
-
translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]), x, y, textAlign);
|
|
170
|
-
finish(gfx, group(gfx, [nodeId, labelId]));
|
|
152
|
+
placeNode(gfx, kind);
|
|
171
153
|
}
|
|
172
154
|
/** Create an inertia bar (filled black rect). */
|
|
173
155
|
export function createWardleyInertia(gfx) {
|
|
174
|
-
if (!gfx.surface)
|
|
175
|
-
return;
|
|
176
156
|
const { w, h } = INERTIA_SIZE;
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}));
|
|
181
|
-
finish(gfx, id);
|
|
157
|
+
const id = place(gfx, [wardleyInertiaElement(viewportTopLeft(gfx, w, h))]);
|
|
158
|
+
if (id)
|
|
159
|
+
finish(gfx, id);
|
|
182
160
|
}
|
|
183
161
|
/**
|
|
184
162
|
* Create a pipeline: a wide thin native rect body (white semi-transparent,
|
|
@@ -188,20 +166,10 @@ export function createWardleyInertia(gfx) {
|
|
|
188
166
|
* moves as one. Pure composition of native elements — no custom type / view.
|
|
189
167
|
*/
|
|
190
168
|
export function createWardleyPipeline(gfx) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const bodyId = addNode(gfx.surface, 'pipeline', cx, cy);
|
|
196
|
-
// Handle: a node-sized WardleyNode square straddling the top edge. Inherits
|
|
197
|
-
// `centerAnchorOnly` so connectors attach to its center only.
|
|
198
|
-
const handleId = gfx.surface.addElement(wardleyHandleProps({ xywh: wardleyHandleBox(cx, cy) }));
|
|
199
|
-
// Label centered horizontally on the pipeline, sitting ABOVE the handle.
|
|
200
|
-
const { x, y, textAlign } = wardleyLabelBoxFor('pipeline', cx, cy);
|
|
201
|
-
const labelId = addLabel(gfx.surface, translateKey(gfx.std, wardleyNodeLabelKey('pipeline'), WARDLEY_NODE_LABEL.pipeline), x, y, textAlign);
|
|
202
|
-
// Nested groups: (handle + label), then (body + that group).
|
|
203
|
-
const innerId = group(gfx, [handleId, labelId]);
|
|
204
|
-
finish(gfx, group(gfx, [bodyId, innerId]));
|
|
169
|
+
const { w, h } = WARDLEY_NODE_SIZE.pipeline;
|
|
170
|
+
const id = place(gfx, wardleyPipelineElements(viewportTopLeft(gfx, w, h), seedName(gfx, 'pipeline')));
|
|
171
|
+
if (id)
|
|
172
|
+
finish(gfx, id);
|
|
205
173
|
}
|
|
206
174
|
/**
|
|
207
175
|
* Create a market: a large thin-bordered circle (the connectable market node)
|
|
@@ -211,19 +179,10 @@ export function createWardleyPipeline(gfx) {
|
|
|
211
179
|
* everything is grouped into one object.
|
|
212
180
|
*/
|
|
213
181
|
export function createWardleyMarket(gfx) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// Outer circle = the market node (connectable, center-only).
|
|
219
|
-
const circleId = addNode(surface, 'market', cx, cy);
|
|
220
|
-
// 3 inner component nodes (thick border, no label) at the triangle vertices.
|
|
221
|
-
const dotIds = wardleyMarketDotBoxes(cx, cy).map(xywh => surface.addElement(wardleyMarketDotProps({ xywh })));
|
|
222
|
-
// Triangle: 3 attached connectors (auto-route center-to-center, clipped).
|
|
223
|
-
const connIds = wardleyMarketLinkPairs(dotIds).map(([a, b]) => surface.addElement(wardleyMarketLinkProps(a, b)));
|
|
224
|
-
const { x, y, textAlign } = wardleyLabelBoxFor('market', cx, cy);
|
|
225
|
-
const labelId = addLabel(surface, translateKey(gfx.std, wardleyNodeLabelKey('market'), WARDLEY_NODE_LABEL.market), x, y, textAlign);
|
|
226
|
-
finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
|
|
182
|
+
const { w, h } = WARDLEY_NODE_SIZE.market;
|
|
183
|
+
const id = place(gfx, wardleyMarketElements(viewportTopLeft(gfx, w, h), seedName(gfx, 'market')));
|
|
184
|
+
if (id)
|
|
185
|
+
finish(gfx, id);
|
|
227
186
|
}
|
|
228
187
|
/**
|
|
229
188
|
* Create a Porter's-forces glyph: a white circle carrying ONE letter, pushed on
|
|
@@ -243,13 +202,9 @@ export function createWardleyMarket(gfx) {
|
|
|
243
202
|
* outside the rim — exactly as the market's triangle is its own.
|
|
244
203
|
*/
|
|
245
204
|
export function createWardleyPorter(gfx) {
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
250
|
-
const circleId = addNode(surface, 'porter', cx, cy, wardleyPorterLetterProps(PORTER_DEFAULT_LETTER));
|
|
251
|
-
const arrowIds = wardleyPorterArrows(cx, cy).map(arrow => surface.addElement(wardleyPorterArrowProps(arrow)));
|
|
252
|
-
finish(gfx, group(gfx, [circleId, ...arrowIds]));
|
|
205
|
+
const id = place(gfx, wardleyPorterElements(viewportTopLeft(gfx, PORTER_SIZE, PORTER_SIZE)));
|
|
206
|
+
if (id)
|
|
207
|
+
finish(gfx, id);
|
|
253
208
|
}
|
|
254
209
|
/**
|
|
255
210
|
* Create an accelerator or a decelerator: one fat native POLYGON and the words
|
|
@@ -274,18 +229,10 @@ export function createWardleyPorter(gfx) {
|
|
|
274
229
|
* SemiBold, which no other Wardley label is: these two are annotations laid
|
|
275
230
|
* over a map that is already full of names, and the weight is what keeps the
|
|
276
231
|
* word readable as a remark ABOUT the map rather than as one more component.
|
|
232
|
+
* Both rules are the builder's (`wardleyNodeElements`).
|
|
277
233
|
*/
|
|
278
234
|
export function createWardleyAccelerator(gfx, kind) {
|
|
279
|
-
|
|
280
|
-
if (!surface)
|
|
281
|
-
return;
|
|
282
|
-
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
283
|
-
const nodeId = addNode(surface, kind, cx, cy);
|
|
284
|
-
// Which side, and which alignment, are the placement's answer: the
|
|
285
|
-
// decelerator's mirrored label is one of the two rules `presets.ts` states.
|
|
286
|
-
const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
|
|
287
|
-
const labelId = addLabel(surface, translateKey(gfx.std, wardleyNodeLabelKey(kind), WARDLEY_NODE_LABEL[kind]), x, y, textAlign, FontWeight.SemiBold);
|
|
288
|
-
finish(gfx, group(gfx, [nodeId, labelId]));
|
|
235
|
+
placeNode(gfx, kind);
|
|
289
236
|
}
|
|
290
237
|
/**
|
|
291
238
|
* Create an AREA: a translucent zone of the map, drawn as a rectangle or as a
|
|
@@ -311,8 +258,15 @@ export function createWardleyArea(gfx, shape) {
|
|
|
311
258
|
if (!surface)
|
|
312
259
|
return;
|
|
313
260
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
314
|
-
|
|
315
|
-
|
|
261
|
+
// The rectangle is a toolbox item (`wardleyAreaElement`); the polygon is
|
|
262
|
+
// drawn corner by corner from the palette ({@link activateWardleyAreaPolygon})
|
|
263
|
+
// and placed whole only by a caller of this function.
|
|
264
|
+
const { w, h } = WARDLEY_AREA_SIZE.rect;
|
|
265
|
+
const id = shape === 'rect'
|
|
266
|
+
? place(gfx, [wardleyAreaElement(viewportTopLeft(gfx, w, h))])
|
|
267
|
+
: surface.addElement(wardleyAreaProps(shape, { xywh: wardleyAreaBox(shape, cx, cy) }));
|
|
268
|
+
if (!id)
|
|
269
|
+
return;
|
|
316
270
|
lowerWardleyArea(gfx, id);
|
|
317
271
|
finish(gfx, id);
|
|
318
272
|
}
|
package/dist/import.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ConnectorMode, FontFamily, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model/headless';
|
|
2
2
|
import { WARDLEY_BACKGROUND } from './background.js';
|
|
3
3
|
import { OWM_DEFAULT_MAP_HEIGHT, OWM_DEFAULT_MAP_WIDTH, OWM_LABEL_HEIGHT, OWM_LABEL_WIDTH, OWM_SCOPE, OWM_TAIL_ATTR, OWM_KEYWORDS, OWM_TITLE_ATTR, owmDefaultPlot, owmPointOf, WARDLEY_OWM_FORMAT_ID, } from './export.js';
|
|
4
|
-
import { HANDLE_SIZE, INERTIA_SIZE, LABEL_GAP, LABEL_FONT_SIZE,
|
|
4
|
+
import { HANDLE_SIZE, INERTIA_SIZE, LABEL_GAP, LABEL_FONT_SIZE, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, ECOSYSTEM_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, WARDLEY_RED, } from './node/consts.js';
|
|
5
5
|
import { WARDLEY_NODE_SIZE, wardleyInertiaProps, wardleyNodeProps, } from './presets.js';
|
|
6
6
|
import { WARDLEY_ROLE } from './roles.js';
|
|
7
|
+
import { wardleyChangeArrowElement, wardleyDependencyElement, } from './toolbox.js';
|
|
7
8
|
/**
|
|
8
9
|
* The OWM import remarks whose wording is FIXED, as `[key, English]` pairs.
|
|
9
10
|
*
|
|
@@ -778,36 +779,16 @@ export function layoutWardleyStatements(statements, options) {
|
|
|
778
779
|
});
|
|
779
780
|
elements.push(label(evolution.becomes, cx + NODE_SIZE / 2 + LABEL_GAP, cy - OWM_LABEL_HEIGHT / 2, TextAlign.Left, WARDLEY_RED));
|
|
780
781
|
}
|
|
782
|
+
// The two edges are the toolbar's own (`toolbox.ts`, `docs/adr/0045`): an
|
|
783
|
+
// arrow read from a file and one drawn with the tool are one assembly.
|
|
781
784
|
evolutions.forEach((evolution, index) => {
|
|
782
|
-
elements.push(
|
|
783
|
-
type: 'connector',
|
|
784
|
-
mode: ConnectorMode.Straight,
|
|
785
|
-
role: WARDLEY_ROLE.changeArrow,
|
|
786
|
-
stroke: WARDLEY_RED,
|
|
787
|
-
strokeStyle: StrokeStyle.Dash,
|
|
788
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
789
|
-
frontEndpointStyle: PointStyle.None,
|
|
790
|
-
rearEndpointStyle: PointStyle.Triangle,
|
|
791
|
-
source: { id: identity(evolution) },
|
|
792
|
-
target: { id: twinHandles[index] },
|
|
793
|
-
});
|
|
785
|
+
elements.push(wardleyChangeArrowElement(identity(evolution), twinHandles[index]));
|
|
794
786
|
});
|
|
795
787
|
for (const link of links) {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
// needs — the verb of `wardley:dependency` (ADR 0010), which is exactly
|
|
801
|
-
// what `A->B` says in this DSL. Never inverted.
|
|
802
|
-
role: WARDLEY_ROLE.dependency,
|
|
803
|
-
stroke: LINK_GREY,
|
|
804
|
-
strokeStyle: StrokeStyle.Solid,
|
|
805
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
806
|
-
frontEndpointStyle: PointStyle.None,
|
|
807
|
-
rearEndpointStyle: PointStyle.None,
|
|
808
|
-
source: { id: link.from },
|
|
809
|
-
target: { id: link.to },
|
|
810
|
-
});
|
|
788
|
+
// The value-chain link. `source` is the CONSUMER and `target` is what it
|
|
789
|
+
// needs — the verb of `wardley:dependency` (ADR 0010), which is exactly
|
|
790
|
+
// what `A->B` says in this DSL. Never inverted.
|
|
791
|
+
elements.push(wardleyDependencyElement(link.from, link.to));
|
|
811
792
|
}
|
|
812
793
|
for (const inertia of inertias) {
|
|
813
794
|
const [cx, cy] = owmPointOf(plot, inertia.visibility, inertia.evolution);
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export { WARDLEY_BACKGROUND } from './background.js';
|
|
|
27
27
|
*/
|
|
28
28
|
export { exportWardleyOwm, exportWardleyOwmWithWarnings, OWM_SCOPE, type OwmPlot, owmCoordsOf, owmPlotOf, owmPointOf, WARDLEY_OWM_FORMAT_ID, type WardleyExportBoard, wardleySafeFilename, } from './export.js';
|
|
29
29
|
export { wardleyBoardFrom } from './export-board.js';
|
|
30
|
+
export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, type WardleyMapTexts, wardleyMarketElements, type WardleyNodeItemKind, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
|
|
30
31
|
export { importWardleyOwm } from './import.js';
|
|
31
32
|
export { importWardleySvg } from './svg-import.js';
|
|
32
33
|
export { wardleyBoardOf, wardleyExportElementsOf, wardleyMapsSelected, } from './actions.js';
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,8 @@ export { WARDLEY_BACKGROUND } from './background.js';
|
|
|
39
39
|
*/
|
|
40
40
|
export { exportWardleyOwm, exportWardleyOwmWithWarnings, OWM_SCOPE, owmCoordsOf, owmPlotOf, owmPointOf, WARDLEY_OWM_FORMAT_ID, wardleySafeFilename, } from './export.js';
|
|
41
41
|
export { wardleyBoardFrom } from './export-board.js';
|
|
42
|
+
// The toolbar as pure builders (`docs/adr/0045`), also on `./interchange`.
|
|
43
|
+
export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, wardleyMarketElements, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
|
|
42
44
|
export { importWardleyOwm } from './import.js';
|
|
43
45
|
// The SVG reader that recognises a map (ADR 0032), pure for the same reason:
|
|
44
46
|
// labre-mcp calls the very function the command does.
|
|
@@ -62,3 +62,4 @@ export declare const WARDLEY_SVG_FORMAT: InterchangeFormat;
|
|
|
62
62
|
* {@link WARDLEY_OWM_IMPORT} beside it stays the reference Wardley import.
|
|
63
63
|
*/
|
|
64
64
|
export declare const WARDLEY_SVG_IMPORT: InterchangeImportCapability;
|
|
65
|
+
export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, type WardleyMapTexts, wardleyMarketElements, type WardleyNodeItemKind, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
|
|
@@ -89,3 +89,6 @@ export const WARDLEY_SVG_IMPORT = {
|
|
|
89
89
|
direction: 'import',
|
|
90
90
|
run: importWardleySvg,
|
|
91
91
|
};
|
|
92
|
+
// The toolbar as pure builders (`docs/adr/0045`): what a host with no editor
|
|
93
|
+
// places to draw the very elements a user places from the toolbar.
|
|
94
|
+
export { WARDLEY_AXIS_SEED, WARDLEY_TOOLBOX, wardleyAreaElement, wardleyChangeArrowElement, wardleyDependencyElement, wardleyInertiaElement, wardleyMapElement, wardleyMarketElements, wardleyNodeElements, wardleyPipelineElements, wardleyPorterElements, } from './toolbox.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { FrameworkToolbox, SerializedElementProps, ToolboxPoint } from '@formicoidea/labre-core/interchange';
|
|
2
|
+
import type { WardleyBgVariant } from '@formicoidea/labre-core/model';
|
|
3
|
+
/**
|
|
4
|
+
* Wardley's toolbar as pure builders (`docs/adr/0045`): what the sub-menu's
|
|
5
|
+
* creation actions (`actions.ts`) place, with no editor behind them. The
|
|
6
|
+
* actions call these, so an artefact a host builds and one a user places from
|
|
7
|
+
* the toolbar are the same elements.
|
|
8
|
+
*
|
|
9
|
+
* Each builder is the TOOLBAR's artefact, not the OWM reader's: the reader
|
|
10
|
+
* (`import.ts`) lays a whole file out with a wider label box and a larger
|
|
11
|
+
* anchor, and draws no group. Every prop that is not geometry is the same on
|
|
12
|
+
* both paths (`toolbox.unit.spec.ts` holds the two to it).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The three axis words a translated variant's background is seeded with —
|
|
16
|
+
* declared once, so the builder below, the editor action that resolves them
|
|
17
|
+
* and the manifest (`translations.ts`) read the very same fallback. Moved here
|
|
18
|
+
* from `actions.ts`, which re-exports it unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export declare const WARDLEY_AXIS_SEED: {
|
|
21
|
+
readonly opportunity: {
|
|
22
|
+
readonly key: "com.labre.wardley.seed.axis-opportunity";
|
|
23
|
+
readonly fallback: "Opportunity";
|
|
24
|
+
};
|
|
25
|
+
readonly benefit: {
|
|
26
|
+
readonly key: "com.labre.wardley.seed.axis-benefit";
|
|
27
|
+
readonly fallback: "Benefit";
|
|
28
|
+
};
|
|
29
|
+
readonly investment: {
|
|
30
|
+
readonly key: "com.labre.wardley.seed.axis-investment";
|
|
31
|
+
readonly fallback: "Investment";
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/** The axis words a map variant writes, by seed name; English when absent. */
|
|
35
|
+
export type WardleyMapTexts = Partial<Record<keyof typeof WARDLEY_AXIS_SEED, string>>;
|
|
36
|
+
/**
|
|
37
|
+
* A map background of one variant at `at`, at the declaration's reference
|
|
38
|
+
* size unless `size` says otherwise (the editor passes the size of the
|
|
39
|
+
* biggest map already on the board, which only an editor can read).
|
|
40
|
+
*/
|
|
41
|
+
export declare function wardleyMapElement(variant: WardleyBgVariant, at: ToolboxPoint, texts?: WardleyMapTexts, local?: string, size?: {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
}): SerializedElementProps;
|
|
45
|
+
/** The kinds drawn as one node and the name beside it, grouped. */
|
|
46
|
+
export type WardleyNodeItemKind = 'component' | 'method' | 'ecosystem' | 'anchor' | 'accelerator' | 'decelerator';
|
|
47
|
+
/**
|
|
48
|
+
* A single node at `at` (the top-left of the node's own box) and its name
|
|
49
|
+
* beside it, grouped: a component, a method, an ecosystem, an anchor, or one
|
|
50
|
+
* of the two climate arrows, whose name sits on the side their shaft is on.
|
|
51
|
+
*/
|
|
52
|
+
export declare function wardleyNodeElements(kind: WardleyNodeItemKind, at: ToolboxPoint, name?: string, local?: string): SerializedElementProps[];
|
|
53
|
+
/**
|
|
54
|
+
* A market at `at` (the top-left of its 30-unit circle): the circle, its
|
|
55
|
+
* three role-less dots wired into a triangle by three attached connectors,
|
|
56
|
+
* and its name, all in one group.
|
|
57
|
+
*/
|
|
58
|
+
export declare function wardleyMarketElements(at: ToolboxPoint, name?: string, local?: string): SerializedElementProps[];
|
|
59
|
+
/**
|
|
60
|
+
* A pipeline at `at` (the top-left of its 120×25 body): the body, the
|
|
61
|
+
* `handle` square astride its top edge (the only place a connector lands),
|
|
62
|
+
* the name above it, and two nested groups — (handle + name), then (body +
|
|
63
|
+
* that group) — as `createWardleyPipeline` has always built it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function wardleyPipelineElements(at: ToolboxPoint, name?: string, local?: string): SerializedElementProps[];
|
|
66
|
+
/**
|
|
67
|
+
* A Porter's-forces glyph at `at` (the top-left of its 60-unit circle): the
|
|
68
|
+
* circle carrying ONE letter as its own inner text (R, L or E; R when
|
|
69
|
+
* absent) and the four role-less red arrows pushing on it, grouped.
|
|
70
|
+
*/
|
|
71
|
+
export declare function wardleyPorterElements(at: ToolboxPoint, letter?: string, local?: string): SerializedElementProps[];
|
|
72
|
+
/** An inertia bar at `at`: a plain filled rect whose role is its meaning. */
|
|
73
|
+
export declare function wardleyInertiaElement(at: ToolboxPoint, local?: string): SerializedElementProps;
|
|
74
|
+
/**
|
|
75
|
+
* A rectangular AREA at `at`, its name its own inner text (the toolbar
|
|
76
|
+
* creates it empty). The editor then lowers it under the components it
|
|
77
|
+
* covers (`lowerWardleyArea`), a depth relative to what is already on the
|
|
78
|
+
* board: a host that writes one sets that depth itself.
|
|
79
|
+
*/
|
|
80
|
+
export declare function wardleyAreaElement(at: ToolboxPoint, name?: string, local?: string): SerializedElementProps;
|
|
81
|
+
/**
|
|
82
|
+
* The value-chain link the Link tool draws: `from` is the CONSUMER and `to`
|
|
83
|
+
* what it needs (ADR 0010), never inverted.
|
|
84
|
+
*/
|
|
85
|
+
export declare function wardleyDependencyElement(from: string, to: string, label?: string): SerializedElementProps;
|
|
86
|
+
/** The evolution arrow the Arrow tool draws, from a component to its twin. */
|
|
87
|
+
export declare function wardleyChangeArrowElement(from: string, to: string, label?: string): SerializedElementProps;
|
|
88
|
+
/**
|
|
89
|
+
* Every item of the Wardley toolbar, in the toolbar's order. The polygon
|
|
90
|
+
* AREA is not here: it is a drawing tool, the author clicks its corners, and
|
|
91
|
+
* a prefabricated outline is precisely what the toolbar refuses to place
|
|
92
|
+
* (`activateWardleyAreaPolygon`).
|
|
93
|
+
*/
|
|
94
|
+
export declare const WARDLEY_TOOLBOX: FrameworkToolbox;
|
package/dist/toolbox.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { ConnectorMode, FontWeight } from '@formicoidea/labre-core/model/headless';
|
|
2
|
+
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
3
|
+
import { WARDLEY_BACKGROUND } from './background.js';
|
|
4
|
+
import { wardleyCommandsManifest } from './commands-manifest.js';
|
|
5
|
+
import { INERTIA_SIZE, PORTER_DEFAULT_LETTER, PORTER_SIZE, } from './node/consts.js';
|
|
6
|
+
import { WARDLEY_AREA_SIZE, wardleyAreaProps, wardleyCanonicalBox, WARDLEY_EDGE_STYLE, wardleyHandleBox, wardleyHandleProps, wardleyInertiaProps, wardleyLabelBoxFor, wardleyLabelProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
|
|
7
|
+
import { WARDLEY_ROLE } from './roles.js';
|
|
8
|
+
/**
|
|
9
|
+
* Wardley's toolbar as pure builders (`docs/adr/0045`): what the sub-menu's
|
|
10
|
+
* creation actions (`actions.ts`) place, with no editor behind them. The
|
|
11
|
+
* actions call these, so an artefact a host builds and one a user places from
|
|
12
|
+
* the toolbar are the same elements.
|
|
13
|
+
*
|
|
14
|
+
* Each builder is the TOOLBAR's artefact, not the OWM reader's: the reader
|
|
15
|
+
* (`import.ts`) lays a whole file out with a wider label box and a larger
|
|
16
|
+
* anchor, and draws no group. Every prop that is not geometry is the same on
|
|
17
|
+
* both paths (`toolbox.unit.spec.ts` holds the two to it).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The three axis words a translated variant's background is seeded with —
|
|
21
|
+
* declared once, so the builder below, the editor action that resolves them
|
|
22
|
+
* and the manifest (`translations.ts`) read the very same fallback. Moved here
|
|
23
|
+
* from `actions.ts`, which re-exports it unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export const WARDLEY_AXIS_SEED = {
|
|
26
|
+
opportunity: {
|
|
27
|
+
key: 'com.labre.wardley.seed.axis-opportunity',
|
|
28
|
+
fallback: 'Opportunity',
|
|
29
|
+
},
|
|
30
|
+
benefit: {
|
|
31
|
+
key: 'com.labre.wardley.seed.axis-benefit',
|
|
32
|
+
fallback: 'Benefit',
|
|
33
|
+
},
|
|
34
|
+
investment: {
|
|
35
|
+
key: 'com.labre.wardley.seed.axis-investment',
|
|
36
|
+
fallback: 'Investment',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const given = (text, fallback) => text?.trim() || fallback;
|
|
40
|
+
/**
|
|
41
|
+
* Per-variant default labels written at creation (all remain editable
|
|
42
|
+
* afterwards). `opportunity` and `benefit` are the two variants whose Y axis
|
|
43
|
+
* is PROSE baked into the document; `classic` and `evolution-gradient` keep
|
|
44
|
+
* the declaration's own labels, which are `labelKey`s and need no seed.
|
|
45
|
+
*/
|
|
46
|
+
function mapVariantProps(variant, texts) {
|
|
47
|
+
const axis = (seed) => given(texts[seed], WARDLEY_AXIS_SEED[seed].fallback);
|
|
48
|
+
switch (variant) {
|
|
49
|
+
case 'opportunity':
|
|
50
|
+
// The Y axis becomes "Opportunity"; phase labels keep the classic ones.
|
|
51
|
+
return {
|
|
52
|
+
yAxisTitle: axis('opportunity'),
|
|
53
|
+
showVisibilityLabels: false,
|
|
54
|
+
showCornerLabels: false,
|
|
55
|
+
};
|
|
56
|
+
case 'benefit':
|
|
57
|
+
// The Y axis splits into Benefit (top) / Investment (bottom) around a
|
|
58
|
+
// zero line drawn by the renderer.
|
|
59
|
+
return {
|
|
60
|
+
yAxisTitle: '',
|
|
61
|
+
visibilityHigh: axis('benefit'),
|
|
62
|
+
visibilityLow: axis('investment'),
|
|
63
|
+
showCornerLabels: false,
|
|
64
|
+
};
|
|
65
|
+
default:
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A map background of one variant at `at`, at the declaration's reference
|
|
71
|
+
* size unless `size` says otherwise (the editor passes the size of the
|
|
72
|
+
* biggest map already on the board, which only an editor can read).
|
|
73
|
+
*/
|
|
74
|
+
export function wardleyMapElement(variant, at, texts = {}, local = 'map', size = WARDLEY_BACKGROUND.geometry) {
|
|
75
|
+
return {
|
|
76
|
+
type: WARDLEY_BACKGROUND.type,
|
|
77
|
+
id: local,
|
|
78
|
+
// The map is a first-class role: validation rules position artefacts
|
|
79
|
+
// against `wardley:map`, never against the `wardley` element type.
|
|
80
|
+
role: WARDLEY_BACKGROUND.role,
|
|
81
|
+
// A map is a frame you place things on, not a shape you nudge.
|
|
82
|
+
resizeEnabled: WARDLEY_BACKGROUND.geometry.resizable,
|
|
83
|
+
variant,
|
|
84
|
+
...mapVariantProps(variant, texts),
|
|
85
|
+
xywh: new Bound(at.x, at.y, size.width, size.height).serialize(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** The centre of a box of size `w`×`h` whose top-left is `at`. */
|
|
89
|
+
const centreOf = (at, w, h) => ({
|
|
90
|
+
cx: at.x + w / 2,
|
|
91
|
+
cy: at.y + h / 2,
|
|
92
|
+
});
|
|
93
|
+
/** The name beside a labelled kind centred on (cx, cy). */
|
|
94
|
+
function labelOf(kind, name, cx, cy, id) {
|
|
95
|
+
const { x, y, textAlign } = wardleyLabelBoxFor(kind, cx, cy);
|
|
96
|
+
return {
|
|
97
|
+
...wardleyLabelProps(given(name, WARDLEY_NODE_LABEL[kind]), x, y, textAlign,
|
|
98
|
+
// The two climate arrows are the only SemiBold names: an annotation laid
|
|
99
|
+
// over a map already full of names (`createWardleyAccelerator`).
|
|
100
|
+
kind === 'accelerator' || kind === 'decelerator'
|
|
101
|
+
? FontWeight.SemiBold
|
|
102
|
+
: FontWeight.Regular),
|
|
103
|
+
id,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const groupOf = (id, members) => ({
|
|
107
|
+
type: 'group',
|
|
108
|
+
id,
|
|
109
|
+
children: Object.fromEntries(members.map(m => [m.id, true])),
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* A single node at `at` (the top-left of the node's own box) and its name
|
|
113
|
+
* beside it, grouped: a component, a method, an ecosystem, an anchor, or one
|
|
114
|
+
* of the two climate arrows, whose name sits on the side their shaft is on.
|
|
115
|
+
*/
|
|
116
|
+
export function wardleyNodeElements(kind, at, name, local = kind) {
|
|
117
|
+
const { w, h } = WARDLEY_NODE_SIZE[kind];
|
|
118
|
+
const { cx, cy } = centreOf(at, w, h);
|
|
119
|
+
const members = [
|
|
120
|
+
{
|
|
121
|
+
...wardleyNodeProps(kind, { xywh: wardleyCanonicalBox(kind, cx, cy) }),
|
|
122
|
+
id: `${local}:node`,
|
|
123
|
+
},
|
|
124
|
+
labelOf(kind, name, cx, cy, `${local}:label`),
|
|
125
|
+
];
|
|
126
|
+
return [...members, groupOf(`${local}:group`, members)];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A market at `at` (the top-left of its 30-unit circle): the circle, its
|
|
130
|
+
* three role-less dots wired into a triangle by three attached connectors,
|
|
131
|
+
* and its name, all in one group.
|
|
132
|
+
*/
|
|
133
|
+
export function wardleyMarketElements(at, name, local = 'market') {
|
|
134
|
+
const { w, h } = WARDLEY_NODE_SIZE.market;
|
|
135
|
+
const { cx, cy } = centreOf(at, w, h);
|
|
136
|
+
const circle = {
|
|
137
|
+
...wardleyNodeProps('market', {
|
|
138
|
+
xywh: wardleyCanonicalBox('market', cx, cy),
|
|
139
|
+
}),
|
|
140
|
+
id: `${local}:node`,
|
|
141
|
+
};
|
|
142
|
+
const dots = wardleyMarketDotBoxes(cx, cy).map((xywh, index) => ({
|
|
143
|
+
...wardleyMarketDotProps({ xywh }),
|
|
144
|
+
id: `${local}:dot-${index + 1}`,
|
|
145
|
+
}));
|
|
146
|
+
const links = wardleyMarketLinkPairs(dots.map(dot => dot.id)).map(([a, b], index) => ({
|
|
147
|
+
...wardleyMarketLinkProps(a, b),
|
|
148
|
+
id: `${local}:link-${index + 1}`,
|
|
149
|
+
}));
|
|
150
|
+
const members = [
|
|
151
|
+
circle,
|
|
152
|
+
...dots,
|
|
153
|
+
...links,
|
|
154
|
+
labelOf('market', name, cx, cy, `${local}:label`),
|
|
155
|
+
];
|
|
156
|
+
return [...members, groupOf(`${local}:group`, members)];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A pipeline at `at` (the top-left of its 120×25 body): the body, the
|
|
160
|
+
* `handle` square astride its top edge (the only place a connector lands),
|
|
161
|
+
* the name above it, and two nested groups — (handle + name), then (body +
|
|
162
|
+
* that group) — as `createWardleyPipeline` has always built it.
|
|
163
|
+
*/
|
|
164
|
+
export function wardleyPipelineElements(at, name, local = 'pipeline') {
|
|
165
|
+
const { w, h } = WARDLEY_NODE_SIZE.pipeline;
|
|
166
|
+
const { cx, cy } = centreOf(at, w, h);
|
|
167
|
+
const body = {
|
|
168
|
+
...wardleyNodeProps('pipeline', {
|
|
169
|
+
xywh: wardleyCanonicalBox('pipeline', cx, cy),
|
|
170
|
+
}),
|
|
171
|
+
id: `${local}:node`,
|
|
172
|
+
};
|
|
173
|
+
const handle = {
|
|
174
|
+
...wardleyHandleProps({ xywh: wardleyHandleBox(cx, cy) }),
|
|
175
|
+
id: `${local}:handle`,
|
|
176
|
+
};
|
|
177
|
+
const label = labelOf('pipeline', name, cx, cy, `${local}:label`);
|
|
178
|
+
const inner = groupOf(`${local}:handle-group`, [handle, label]);
|
|
179
|
+
return [body, handle, label, inner, groupOf(`${local}:group`, [body, inner])];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A Porter's-forces glyph at `at` (the top-left of its 60-unit circle): the
|
|
183
|
+
* circle carrying ONE letter as its own inner text (R, L or E; R when
|
|
184
|
+
* absent) and the four role-less red arrows pushing on it, grouped.
|
|
185
|
+
*/
|
|
186
|
+
export function wardleyPorterElements(at, letter, local = 'porter') {
|
|
187
|
+
const { cx, cy } = centreOf(at, PORTER_SIZE, PORTER_SIZE);
|
|
188
|
+
const members = [
|
|
189
|
+
{
|
|
190
|
+
...wardleyNodeProps('porter', {
|
|
191
|
+
xywh: wardleyCanonicalBox('porter', cx, cy),
|
|
192
|
+
}),
|
|
193
|
+
...wardleyPorterLetterProps(given(letter, PORTER_DEFAULT_LETTER)),
|
|
194
|
+
id: `${local}:node`,
|
|
195
|
+
},
|
|
196
|
+
...wardleyPorterArrows(cx, cy).map((arrow, index) => ({
|
|
197
|
+
...wardleyPorterArrowProps(arrow),
|
|
198
|
+
id: `${local}:arrow-${index + 1}`,
|
|
199
|
+
})),
|
|
200
|
+
];
|
|
201
|
+
return [...members, groupOf(`${local}:group`, members)];
|
|
202
|
+
}
|
|
203
|
+
/** An inertia bar at `at`: a plain filled rect whose role is its meaning. */
|
|
204
|
+
export function wardleyInertiaElement(at, local = 'inertia') {
|
|
205
|
+
const { w, h } = INERTIA_SIZE;
|
|
206
|
+
return {
|
|
207
|
+
...wardleyInertiaProps({
|
|
208
|
+
xywh: new Bound(at.x, at.y, w, h).serialize(),
|
|
209
|
+
}),
|
|
210
|
+
id: local,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A rectangular AREA at `at`, its name its own inner text (the toolbar
|
|
215
|
+
* creates it empty). The editor then lowers it under the components it
|
|
216
|
+
* covers (`lowerWardleyArea`), a depth relative to what is already on the
|
|
217
|
+
* board: a host that writes one sets that depth itself.
|
|
218
|
+
*/
|
|
219
|
+
export function wardleyAreaElement(at, name, local = 'area') {
|
|
220
|
+
const { w, h } = WARDLEY_AREA_SIZE.rect;
|
|
221
|
+
const trimmed = name?.trim();
|
|
222
|
+
return {
|
|
223
|
+
...wardleyAreaProps('rect', {
|
|
224
|
+
xywh: new Bound(at.x, at.y, w, h).serialize(),
|
|
225
|
+
}),
|
|
226
|
+
...(trimmed ? { text: trimmed } : {}),
|
|
227
|
+
id: local,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function edge(kind, from, to, label) {
|
|
231
|
+
return {
|
|
232
|
+
type: 'connector',
|
|
233
|
+
mode: ConnectorMode.Straight,
|
|
234
|
+
// Two roles, never one specialising the other: W1 is about where an
|
|
235
|
+
// arrow points and must never fall on a dependency.
|
|
236
|
+
role: kind === 'link' ? WARDLEY_ROLE.dependency : WARDLEY_ROLE.changeArrow,
|
|
237
|
+
...WARDLEY_EDGE_STYLE[kind],
|
|
238
|
+
source: { id: from },
|
|
239
|
+
target: { id: to },
|
|
240
|
+
...(label?.trim() ? { text: label.trim() } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The value-chain link the Link tool draws: `from` is the CONSUMER and `to`
|
|
245
|
+
* what it needs (ADR 0010), never inverted.
|
|
246
|
+
*/
|
|
247
|
+
export function wardleyDependencyElement(from, to, label) {
|
|
248
|
+
return edge('link', from, to, label);
|
|
249
|
+
}
|
|
250
|
+
/** The evolution arrow the Arrow tool draws, from a component to its twin. */
|
|
251
|
+
export function wardleyChangeArrowElement(from, to, label) {
|
|
252
|
+
return edge('arrow', from, to, label);
|
|
253
|
+
}
|
|
254
|
+
/** The toolbar command's label, so a host names an item as the toolbar does. */
|
|
255
|
+
function label(commandId) {
|
|
256
|
+
const row = wardleyCommandsManifest.find(r => r.id === commandId);
|
|
257
|
+
return {
|
|
258
|
+
labelKey: row?.labelKey ?? commandId,
|
|
259
|
+
labelFallback: row?.labelFallback ?? commandId,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const mapItem = (id, commandId, variant, texts) => ({
|
|
263
|
+
id,
|
|
264
|
+
...label(commandId),
|
|
265
|
+
texts,
|
|
266
|
+
size: {
|
|
267
|
+
w: WARDLEY_BACKGROUND.geometry.width,
|
|
268
|
+
h: WARDLEY_BACKGROUND.geometry.height,
|
|
269
|
+
},
|
|
270
|
+
build: (at, slots, local) => [wardleyMapElement(variant, at, slots, local)],
|
|
271
|
+
});
|
|
272
|
+
const nodeItem = (kind, commandId) => ({
|
|
273
|
+
id: kind,
|
|
274
|
+
...label(commandId),
|
|
275
|
+
texts: ['name'],
|
|
276
|
+
size: WARDLEY_NODE_SIZE[kind],
|
|
277
|
+
build: (at, texts, local) => wardleyNodeElements(kind, at, texts?.name, local),
|
|
278
|
+
});
|
|
279
|
+
const dependency = {
|
|
280
|
+
id: 'dependency',
|
|
281
|
+
...label('wardley.linkTool'),
|
|
282
|
+
build: wardleyDependencyElement,
|
|
283
|
+
};
|
|
284
|
+
const changeArrow = {
|
|
285
|
+
id: 'change-arrow',
|
|
286
|
+
...label('wardley.evolutionArrow'),
|
|
287
|
+
build: wardleyChangeArrowElement,
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Every item of the Wardley toolbar, in the toolbar's order. The polygon
|
|
291
|
+
* AREA is not here: it is a drawing tool, the author clicks its corners, and
|
|
292
|
+
* a prefabricated outline is precisely what the toolbar refuses to place
|
|
293
|
+
* (`activateWardleyAreaPolygon`).
|
|
294
|
+
*/
|
|
295
|
+
export const WARDLEY_TOOLBOX = {
|
|
296
|
+
framework: 'wardley',
|
|
297
|
+
items: [
|
|
298
|
+
mapItem('map', 'wardley.addBackground', 'classic', []),
|
|
299
|
+
mapItem('opportunity-map', 'wardley.addOpportunityBackground', 'opportunity', ['opportunity']),
|
|
300
|
+
mapItem('benefit-map', 'wardley.addBenefitBackground', 'benefit', [
|
|
301
|
+
'benefit',
|
|
302
|
+
'investment',
|
|
303
|
+
]),
|
|
304
|
+
mapItem('evolution-map', 'wardley.addEvolutionBackground', 'evolution-gradient', []),
|
|
305
|
+
nodeItem('component', 'wardley.addComponent'),
|
|
306
|
+
nodeItem('method', 'wardley.addMethod'),
|
|
307
|
+
{
|
|
308
|
+
id: 'market',
|
|
309
|
+
...label('wardley.addMarket'),
|
|
310
|
+
texts: ['name'],
|
|
311
|
+
size: WARDLEY_NODE_SIZE.market,
|
|
312
|
+
build: (at, texts, local) => wardleyMarketElements(at, texts?.name, local),
|
|
313
|
+
},
|
|
314
|
+
nodeItem('ecosystem', 'wardley.addEcosystem'),
|
|
315
|
+
nodeItem('anchor', 'wardley.addAnchor'),
|
|
316
|
+
{
|
|
317
|
+
id: 'pipeline',
|
|
318
|
+
...label('wardley.addPipeline'),
|
|
319
|
+
texts: ['name'],
|
|
320
|
+
size: WARDLEY_NODE_SIZE.pipeline,
|
|
321
|
+
build: (at, texts, local) => wardleyPipelineElements(at, texts?.name, local),
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'inertia',
|
|
325
|
+
...label('wardley.addInertia'),
|
|
326
|
+
texts: [],
|
|
327
|
+
size: INERTIA_SIZE,
|
|
328
|
+
build: (at, _texts, local) => [wardleyInertiaElement(at, local)],
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: 'porter',
|
|
332
|
+
...label('wardley.addPorter'),
|
|
333
|
+
texts: ['letter'],
|
|
334
|
+
size: WARDLEY_NODE_SIZE.porter,
|
|
335
|
+
build: (at, texts, local) => wardleyPorterElements(at, texts?.letter, local),
|
|
336
|
+
},
|
|
337
|
+
nodeItem('accelerator', 'wardley.addAccelerator'),
|
|
338
|
+
nodeItem('decelerator', 'wardley.addDecelerator'),
|
|
339
|
+
{
|
|
340
|
+
id: 'area',
|
|
341
|
+
...label('wardley.addAreaRect'),
|
|
342
|
+
texts: ['name'],
|
|
343
|
+
size: WARDLEY_AREA_SIZE.rect,
|
|
344
|
+
build: (at, texts, local) => [wardleyAreaElement(at, texts?.name, local)],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
links: [dependency, changeArrow],
|
|
348
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formicoidea/labre-framework-wardley",
|
|
3
3
|
"description": "Labre wardley framework for @formicoidea/labre-core.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.60.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"author": "lajola",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@formicoidea/labre-core": "0.
|
|
38
|
+
"@formicoidea/labre-core": "0.60.0",
|
|
39
39
|
"lit": "^3.2.0"
|
|
40
40
|
}
|
|
41
41
|
}
|