@formicoidea/labre-framework-wardley 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +108 -6
- package/dist/actions.js +172 -11
- package/dist/commands-manifest.js +40 -0
- package/dist/commands.d.ts +18 -1
- package/dist/commands.js +96 -7
- package/dist/export.js +50 -2
- package/dist/import.d.ts +5 -3
- package/dist/import.js +53 -8
- package/dist/index.d.ts +3 -2
- package/dist/index.js +7 -2
- package/dist/legend.d.ts +27 -1
- package/dist/legend.js +277 -3
- package/dist/natures.d.ts +31 -0
- package/dist/natures.js +64 -4
- package/dist/node/consts.d.ts +123 -0
- package/dist/node/consts.js +122 -0
- package/dist/node/node-view.d.ts +82 -4
- package/dist/node/node-view.js +108 -4
- package/dist/node/porter-watcher.d.ts +119 -0
- package/dist/node/porter-watcher.js +255 -0
- package/dist/porter-competition.d.ts +36 -0
- package/dist/porter-competition.js +50 -0
- package/dist/presets.d.ts +151 -5
- package/dist/presets.js +336 -12
- package/dist/roles.d.ts +4 -0
- package/dist/roles.js +67 -0
- package/dist/toolbar/icons.d.ts +36 -0
- package/dist/toolbar/icons.js +58 -0
- package/dist/toolbar/node-config.d.ts +20 -0
- package/dist/toolbar/node-config.js +100 -7
- package/dist/view.js +10 -0
- package/package.json +2 -2
package/dist/actions.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { WardleyBackgroundElementModel, type WardleyBgVariant } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
2
3
|
import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
3
4
|
import { type GfxController } from '@formicoidea/labre-core/std/gfx';
|
|
4
5
|
import { type WardleyExportBoard } from './export.js';
|
|
5
|
-
import { type WardleyArtefactKind } from './presets.js';
|
|
6
|
+
import { type WardleyAreaShape, type WardleyArtefactKind } from './presets.js';
|
|
6
7
|
/**
|
|
7
8
|
* The single-circle node flavours: one connectable ellipse + a label to its
|
|
8
9
|
* right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
|
|
@@ -10,12 +11,15 @@ import { type WardleyArtefactKind } from './presets.js';
|
|
|
10
11
|
*
|
|
11
12
|
* A subset of {@link WardleyArtefactKind}, and named away from
|
|
12
13
|
* `WardleyNodeKind` on purpose: the model declares a type of that name with
|
|
13
|
-
*
|
|
14
|
-
* source of the semantic vocabulary — is a trap. The market
|
|
15
|
-
* are composites with creation functions of their
|
|
16
|
-
*
|
|
14
|
+
* eleven values, and two homonyms of different cardinality — one of them the
|
|
15
|
+
* source of the semantic vocabulary — is a trap. The market, the pipeline and
|
|
16
|
+
* the Porter's-forces glyph are composites with creation functions of their
|
|
17
|
+
* own; the accelerator and the decelerator are not circles at all and place
|
|
18
|
+
* their label on the side their arrow points from
|
|
19
|
+
* ({@link createWardleyAccelerator}); the area is a zone with no label element
|
|
20
|
+
* at all ({@link createWardleyArea}); the handle is not an artefact.
|
|
17
21
|
*/
|
|
18
|
-
export type WardleySingleCircleKind = Exclude<WardleyArtefactKind, 'market' | 'pipeline'>;
|
|
22
|
+
export type WardleySingleCircleKind = Exclude<WardleyArtefactKind, 'market' | 'pipeline' | 'porter' | 'accelerator' | 'decelerator' | 'area'>;
|
|
19
23
|
/** Create a wardley map background of the given variant, viewport-centered. */
|
|
20
24
|
export declare function createWardleyBackground(gfx: GfxController, variant?: WardleyBgVariant): void;
|
|
21
25
|
/**
|
|
@@ -42,6 +46,104 @@ export declare function createWardleyPipeline(gfx: GfxController): void;
|
|
|
42
46
|
* everything is grouped into one object.
|
|
43
47
|
*/
|
|
44
48
|
export declare function createWardleyMarket(gfx: GfxController): void;
|
|
49
|
+
/**
|
|
50
|
+
* Create a Porter's-forces glyph: a white circle carrying ONE letter, pushed on
|
|
51
|
+
* from the four cardinal directions by solid red arrows, all grouped as one
|
|
52
|
+
* object.
|
|
53
|
+
*
|
|
54
|
+
* It marks an EXTERNAL competition force acting on the map — which is why the
|
|
55
|
+
* circle carries `wardley:porter` and nothing under `wardley:component`: it is
|
|
56
|
+
* not a link in the value chain, and no rule written about the chain has
|
|
57
|
+
* anything to say about a pressure applied from outside it.
|
|
58
|
+
*
|
|
59
|
+
* No label, and that is the notation rather than an omission: the letter says
|
|
60
|
+
* which force this is (R relative competition, L struggle for survival, E
|
|
61
|
+
* struggle to establish) and it is the circle's OWN inner text, so a
|
|
62
|
+
* double-click opens the native shape editor on it (`node/node-view.ts`). The
|
|
63
|
+
* four arrows are the glyph's own wiring — role-less filled red polygons,
|
|
64
|
+
* outside the rim — exactly as the market's triangle is its own.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createWardleyPorter(gfx: GfxController): void;
|
|
67
|
+
/**
|
|
68
|
+
* Create an accelerator or a decelerator: one fat native POLYGON and the words
|
|
69
|
+
* that say what it is, grouped like every other artefact.
|
|
70
|
+
*
|
|
71
|
+
* Not a link in the value chain, and not a pressure on it either: an annotation
|
|
72
|
+
* of the CLIMATE, saying that something is speeding evolution up (an arrow
|
|
73
|
+
* pointing right, towards commodity) or holding it back (the mirror, pointing
|
|
74
|
+
* left). Which way it points IS the notation, so the two are separate kinds
|
|
75
|
+
* with outlines of their own rather than one kind with a `rotate`.
|
|
76
|
+
*
|
|
77
|
+
* ## Where the words go, and why it is not always the right
|
|
78
|
+
*
|
|
79
|
+
* Every other single artefact on this canvas wears its name on its right,
|
|
80
|
+
* because a circle has no direction to disagree with. An arrow does. The
|
|
81
|
+
* reference puts the words on the side the arrow's SHAFT is on — right of an
|
|
82
|
+
* accelerator, left of a decelerator — so the reading runs into the arrow
|
|
83
|
+
* rather than across it, and the label is aligned towards it (left-aligned on
|
|
84
|
+
* the right, right-aligned on the left) so the words end against the shaft
|
|
85
|
+
* whatever their length.
|
|
86
|
+
*
|
|
87
|
+
* SemiBold, which no other Wardley label is: these two are annotations laid
|
|
88
|
+
* over a map that is already full of names, and the weight is what keeps the
|
|
89
|
+
* word readable as a remark ABOUT the map rather than as one more component.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createWardleyAccelerator(gfx: GfxController, kind: 'accelerator' | 'decelerator'): void;
|
|
92
|
+
/**
|
|
93
|
+
* Create an AREA: a translucent zone of the map, drawn as a rectangle or as a
|
|
94
|
+
* polygon whose corners the author then moves.
|
|
95
|
+
*
|
|
96
|
+
* One element and no group, which makes it the plainest creation site in this
|
|
97
|
+
* file — and the difference is the name. Every other artefact wears its name as
|
|
98
|
+
* a text element beside it, so every other artefact is a group of two. A zone's
|
|
99
|
+
* name belongs INSIDE the zone, so it is the shape's own inner text: created
|
|
100
|
+
* empty, opened by a double-click on the zone (`node/node-view.ts`), and set in
|
|
101
|
+
* a fit mode that never resizes the boundary around it.
|
|
102
|
+
*
|
|
103
|
+
* ## Why it is sent to the back
|
|
104
|
+
*
|
|
105
|
+
* A zone is drawn precisely OVER the components it groups, and the surface
|
|
106
|
+
* paints in index order: an area added last would sit on top of everything it
|
|
107
|
+
* surrounds — hiding it under a wash, and worse, intercepting every click meant
|
|
108
|
+
* for a component inside it. So the element is lowered the moment it exists,
|
|
109
|
+
* to the depth {@link wardleyAreaIndexOver} works out.
|
|
110
|
+
*/
|
|
111
|
+
export declare function createWardleyArea(gfx: GfxController, shape: WardleyAreaShape): void;
|
|
112
|
+
/**
|
|
113
|
+
* One already-placed element, as {@link wardleyAreaIndexOver} needs to read it.
|
|
114
|
+
*
|
|
115
|
+
* Three fields and no model, so the placement can be reasoned about — and
|
|
116
|
+
* tested — without a document: what it is, where it is, and how deep.
|
|
117
|
+
*/
|
|
118
|
+
export interface WardleyStackedElement {
|
|
119
|
+
/** Fractional index. Sorts lexicographically, which IS the paint order. */
|
|
120
|
+
index: string;
|
|
121
|
+
/** Serialized box. */
|
|
122
|
+
xywh: string;
|
|
123
|
+
/** Whether this is a framework BACKGROUND — a map, a pool, a C4 board… */
|
|
124
|
+
isBackground: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
|
|
128
|
+
* covers, and below everything else. `null` means the back of the surface.
|
|
129
|
+
*
|
|
130
|
+
* "The back" alone was the first answer and it was wrong, as the recette of
|
|
131
|
+
* #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so a
|
|
132
|
+
* zone sent behind the whole surface went behind the map and vanished. What a
|
|
133
|
+
* zone must be under is the artefacts it groups; what it must be over is the
|
|
134
|
+
* canvas they are drawn on. Those are two different depths, and only the first
|
|
135
|
+
* one is "the back".
|
|
136
|
+
*
|
|
137
|
+
* So: find the topmost background the zone actually OVERLAPS — the map it was
|
|
138
|
+
* drawn on, and not some other framework's board parked elsewhere on the same
|
|
139
|
+
* canvas — and mint a key between it and whatever sits directly above it. With
|
|
140
|
+
* no background under the zone at all there is nothing to clear, and the back
|
|
141
|
+
* of the surface is right again.
|
|
142
|
+
*
|
|
143
|
+
* `siblings` need not be sorted: fractional indexes sort lexicographically, so
|
|
144
|
+
* this sorts them itself and reads the paint order straight off the strings.
|
|
145
|
+
*/
|
|
146
|
+
export declare function wardleyAreaIndexOver(siblings: readonly WardleyStackedElement[], box: Bound): string | null;
|
|
45
147
|
/**
|
|
46
148
|
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
47
149
|
*
|
package/dist/actions.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { backgroundSize, DefaultTool, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
2
|
import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
|
|
3
3
|
import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
|
|
4
|
-
import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
|
|
4
|
+
import { ConnectorMode, FontFamily, FontWeight, FrameworkBackgroundElementModel, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
|
|
5
5
|
import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
6
6
|
import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
|
|
7
7
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
8
|
-
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
8
|
+
import { generateKeyBetween, GfxControllerIdentifier, } from '@formicoidea/labre-core/std/gfx';
|
|
9
9
|
import { WARDLEY_BACKGROUND } from './background.js';
|
|
10
10
|
import { WARDLEY_SVG_IMPORT } from './interchange.js';
|
|
11
11
|
import { wardleyBoardFrom, wardleySafeFilename, } from './export.js';
|
|
12
12
|
import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
|
|
13
|
-
import { HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, NODE_STROKE, WARDLEY_RED, } from './node/consts.js';
|
|
14
|
-
import { wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, } from './presets.js';
|
|
13
|
+
import { HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, NODE_STROKE, PORTER_DEFAULT_LETTER, WARDLEY_RED, } from './node/consts.js';
|
|
14
|
+
import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
|
|
15
15
|
import { WARDLEY_ROLE } from './roles.js';
|
|
16
16
|
/**
|
|
17
17
|
* Standalone creation/activation actions for the Wardley toolbox — the
|
|
@@ -64,6 +64,14 @@ const BACKGROUND_VARIANT_DEFAULTS = {
|
|
|
64
64
|
};
|
|
65
65
|
/** Height of the native free-text labels (Inter, size 18). */
|
|
66
66
|
const LABEL_H = LABEL_FONT_SIZE + 8;
|
|
67
|
+
/**
|
|
68
|
+
* Width of a label box, whatever it reads.
|
|
69
|
+
*
|
|
70
|
+
* A number rather than a measurement, which is why a RIGHT-aligned label has to
|
|
71
|
+
* subtract it: the box does not shrink to the words, so the only way to make
|
|
72
|
+
* the words end on a given edge is to start the box a full width before it.
|
|
73
|
+
*/
|
|
74
|
+
const LABEL_W = 120;
|
|
67
75
|
function finish(gfx, id) {
|
|
68
76
|
gfx.doc.captureSync();
|
|
69
77
|
gfx.tool.setTool(DefaultTool);
|
|
@@ -81,18 +89,27 @@ function group(gfx, ids) {
|
|
|
81
89
|
/**
|
|
82
90
|
* Add a wardley node of one kind, at its canonical size, centred on (cx, cy).
|
|
83
91
|
*
|
|
84
|
-
* The
|
|
85
|
-
* "Change type" dropdown read that one description, so a market drawn here
|
|
86
|
-
* a component morphed into one are the same element (`presets.ts`).
|
|
92
|
+
* The APPEARANCE is {@link wardleyNodeProps}' and nothing else: the palette and
|
|
93
|
+
* the "Change type" dropdown read that one description, so a market drawn here
|
|
94
|
+
* and a component morphed into one are the same element (`presets.ts`).
|
|
95
|
+
*
|
|
96
|
+
* `extra` is for what a preset cannot carry — the Porter's-forces letter, which
|
|
97
|
+
* is CONTENT the author then edits, not a look. `wardleyMorphProps` strips
|
|
98
|
+
* `text` for exactly that reason, so a preset that wrote one would be a preset
|
|
99
|
+
* the morph has to un-write.
|
|
87
100
|
*/
|
|
88
|
-
function addNode(surface, kind, cx, cy) {
|
|
89
|
-
return surface.addElement(
|
|
101
|
+
function addNode(surface, kind, cx, cy, extra = {}) {
|
|
102
|
+
return surface.addElement({
|
|
103
|
+
...wardleyNodeProps(kind, { xywh: wardleyCanonicalBox(kind, cx, cy) }),
|
|
104
|
+
...extra,
|
|
105
|
+
});
|
|
90
106
|
}
|
|
91
107
|
/** Add a native free-text label (same Inter family as the axis labels). */
|
|
92
|
-
function addLabel(surface, text, x, y, textAlign = 'left') {
|
|
108
|
+
function addLabel(surface, text, x, y, textAlign = 'left', fontWeight = FontWeight.Regular) {
|
|
93
109
|
return surface.addElement({
|
|
94
110
|
type: 'text',
|
|
95
111
|
text,
|
|
112
|
+
fontWeight,
|
|
96
113
|
// Semantic identity (PF1, revised in PF13.4): a Wardley label is a free
|
|
97
114
|
// text element like any other, so its ROLE is the only thing that tells W3
|
|
98
115
|
// it must not land on top of a node. A free text the user typed elsewhere
|
|
@@ -102,7 +119,7 @@ function addLabel(surface, text, x, y, textAlign = 'left') {
|
|
|
102
119
|
fontSize: LABEL_FONT_SIZE,
|
|
103
120
|
color: NODE_STROKE,
|
|
104
121
|
textAlign,
|
|
105
|
-
xywh: new Bound(x, y,
|
|
122
|
+
xywh: new Bound(x, y, LABEL_W, LABEL_H).serialize(),
|
|
106
123
|
});
|
|
107
124
|
}
|
|
108
125
|
/** Create a wardley map background of the given variant, viewport-centered. */
|
|
@@ -222,6 +239,150 @@ export function createWardleyMarket(gfx) {
|
|
|
222
239
|
const labelId = addLabel(surface, WARDLEY_NODE_LABEL.market, cx + R + LABEL_GAP, cy - LABEL_H / 2);
|
|
223
240
|
finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
|
|
224
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Create a Porter's-forces glyph: a white circle carrying ONE letter, pushed on
|
|
244
|
+
* from the four cardinal directions by solid red arrows, all grouped as one
|
|
245
|
+
* object.
|
|
246
|
+
*
|
|
247
|
+
* It marks an EXTERNAL competition force acting on the map — which is why the
|
|
248
|
+
* circle carries `wardley:porter` and nothing under `wardley:component`: it is
|
|
249
|
+
* not a link in the value chain, and no rule written about the chain has
|
|
250
|
+
* anything to say about a pressure applied from outside it.
|
|
251
|
+
*
|
|
252
|
+
* No label, and that is the notation rather than an omission: the letter says
|
|
253
|
+
* which force this is (R relative competition, L struggle for survival, E
|
|
254
|
+
* struggle to establish) and it is the circle's OWN inner text, so a
|
|
255
|
+
* double-click opens the native shape editor on it (`node/node-view.ts`). The
|
|
256
|
+
* four arrows are the glyph's own wiring — role-less filled red polygons,
|
|
257
|
+
* outside the rim — exactly as the market's triangle is its own.
|
|
258
|
+
*/
|
|
259
|
+
export function createWardleyPorter(gfx) {
|
|
260
|
+
const surface = gfx.surface;
|
|
261
|
+
if (!surface)
|
|
262
|
+
return;
|
|
263
|
+
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
264
|
+
const circleId = addNode(surface, 'porter', cx, cy, wardleyPorterLetterProps(PORTER_DEFAULT_LETTER));
|
|
265
|
+
const arrowIds = wardleyPorterArrows(cx, cy).map(arrow => surface.addElement(wardleyPorterArrowProps(arrow)));
|
|
266
|
+
finish(gfx, group(gfx, [circleId, ...arrowIds]));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Create an accelerator or a decelerator: one fat native POLYGON and the words
|
|
270
|
+
* that say what it is, grouped like every other artefact.
|
|
271
|
+
*
|
|
272
|
+
* Not a link in the value chain, and not a pressure on it either: an annotation
|
|
273
|
+
* of the CLIMATE, saying that something is speeding evolution up (an arrow
|
|
274
|
+
* pointing right, towards commodity) or holding it back (the mirror, pointing
|
|
275
|
+
* left). Which way it points IS the notation, so the two are separate kinds
|
|
276
|
+
* with outlines of their own rather than one kind with a `rotate`.
|
|
277
|
+
*
|
|
278
|
+
* ## Where the words go, and why it is not always the right
|
|
279
|
+
*
|
|
280
|
+
* Every other single artefact on this canvas wears its name on its right,
|
|
281
|
+
* because a circle has no direction to disagree with. An arrow does. The
|
|
282
|
+
* reference puts the words on the side the arrow's SHAFT is on — right of an
|
|
283
|
+
* accelerator, left of a decelerator — so the reading runs into the arrow
|
|
284
|
+
* rather than across it, and the label is aligned towards it (left-aligned on
|
|
285
|
+
* the right, right-aligned on the left) so the words end against the shaft
|
|
286
|
+
* whatever their length.
|
|
287
|
+
*
|
|
288
|
+
* SemiBold, which no other Wardley label is: these two are annotations laid
|
|
289
|
+
* over a map that is already full of names, and the weight is what keeps the
|
|
290
|
+
* word readable as a remark ABOUT the map rather than as one more component.
|
|
291
|
+
*/
|
|
292
|
+
export function createWardleyAccelerator(gfx, kind) {
|
|
293
|
+
const surface = gfx.surface;
|
|
294
|
+
if (!surface)
|
|
295
|
+
return;
|
|
296
|
+
const { w } = WARDLEY_NODE_SIZE[kind];
|
|
297
|
+
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
298
|
+
const nodeId = addNode(surface, kind, cx, cy);
|
|
299
|
+
const rightwards = kind === 'accelerator';
|
|
300
|
+
const labelId = addLabel(surface, WARDLEY_NODE_LABEL[kind],
|
|
301
|
+
// A label box is a fixed LABEL_W wide whatever it reads, so a right-aligned
|
|
302
|
+
// one has to start a box-width before the edge the words must end on.
|
|
303
|
+
rightwards ? cx + w / 2 + LABEL_GAP : cx - w / 2 - LABEL_GAP - LABEL_W, cy - LABEL_H / 2, rightwards ? 'left' : 'right', FontWeight.SemiBold);
|
|
304
|
+
finish(gfx, group(gfx, [nodeId, labelId]));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Create an AREA: a translucent zone of the map, drawn as a rectangle or as a
|
|
308
|
+
* polygon whose corners the author then moves.
|
|
309
|
+
*
|
|
310
|
+
* One element and no group, which makes it the plainest creation site in this
|
|
311
|
+
* file — and the difference is the name. Every other artefact wears its name as
|
|
312
|
+
* a text element beside it, so every other artefact is a group of two. A zone's
|
|
313
|
+
* name belongs INSIDE the zone, so it is the shape's own inner text: created
|
|
314
|
+
* empty, opened by a double-click on the zone (`node/node-view.ts`), and set in
|
|
315
|
+
* a fit mode that never resizes the boundary around it.
|
|
316
|
+
*
|
|
317
|
+
* ## Why it is sent to the back
|
|
318
|
+
*
|
|
319
|
+
* A zone is drawn precisely OVER the components it groups, and the surface
|
|
320
|
+
* paints in index order: an area added last would sit on top of everything it
|
|
321
|
+
* surrounds — hiding it under a wash, and worse, intercepting every click meant
|
|
322
|
+
* for a component inside it. So the element is lowered the moment it exists,
|
|
323
|
+
* to the depth {@link wardleyAreaIndexOver} works out.
|
|
324
|
+
*/
|
|
325
|
+
export function createWardleyArea(gfx, shape) {
|
|
326
|
+
const surface = gfx.surface;
|
|
327
|
+
if (!surface)
|
|
328
|
+
return;
|
|
329
|
+
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
330
|
+
const xywh = wardleyAreaBox(shape, cx, cy);
|
|
331
|
+
const id = surface.addElement(wardleyAreaProps(shape, { xywh }));
|
|
332
|
+
const model = surface.getElementById(id);
|
|
333
|
+
if (model) {
|
|
334
|
+
const over = wardleyAreaIndexOver(stackedElementsOf(surface, id), Bound.deserialize(xywh));
|
|
335
|
+
// `null` is "nothing under it": the back of the surface, minted by the
|
|
336
|
+
// layer itself so an empty board gets the layer's own initial index rather
|
|
337
|
+
// than a key invented here.
|
|
338
|
+
model.index = over ?? gfx.layer.getReorderedIndex(model, 'back');
|
|
339
|
+
}
|
|
340
|
+
finish(gfx, id);
|
|
341
|
+
}
|
|
342
|
+
/** The top-level canvas elements of a surface, minus the one being placed. */
|
|
343
|
+
function stackedElementsOf(surface, exclude) {
|
|
344
|
+
return surface.elementModels
|
|
345
|
+
.filter(model => model.id !== exclude && model.group === null)
|
|
346
|
+
.map(model => ({
|
|
347
|
+
index: model.index,
|
|
348
|
+
xywh: model.xywh,
|
|
349
|
+
isBackground: model instanceof FrameworkBackgroundElementModel,
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
|
|
354
|
+
* covers, and below everything else. `null` means the back of the surface.
|
|
355
|
+
*
|
|
356
|
+
* "The back" alone was the first answer and it was wrong, as the recette of
|
|
357
|
+
* #213 found: a Wardley map is a framework BACKGROUND — an opaque one — so a
|
|
358
|
+
* zone sent behind the whole surface went behind the map and vanished. What a
|
|
359
|
+
* zone must be under is the artefacts it groups; what it must be over is the
|
|
360
|
+
* canvas they are drawn on. Those are two different depths, and only the first
|
|
361
|
+
* one is "the back".
|
|
362
|
+
*
|
|
363
|
+
* So: find the topmost background the zone actually OVERLAPS — the map it was
|
|
364
|
+
* drawn on, and not some other framework's board parked elsewhere on the same
|
|
365
|
+
* canvas — and mint a key between it and whatever sits directly above it. With
|
|
366
|
+
* no background under the zone at all there is nothing to clear, and the back
|
|
367
|
+
* of the surface is right again.
|
|
368
|
+
*
|
|
369
|
+
* `siblings` need not be sorted: fractional indexes sort lexicographically, so
|
|
370
|
+
* this sorts them itself and reads the paint order straight off the strings.
|
|
371
|
+
*/
|
|
372
|
+
export function wardleyAreaIndexOver(siblings, box) {
|
|
373
|
+
const stack = [...siblings].sort((a, b) => a.index < b.index ? -1 : a.index > b.index ? 1 : 0);
|
|
374
|
+
// The LAST match, which is the topmost: a board with two maps on it gets the
|
|
375
|
+
// zone above the one it is actually drawn over, whichever was drawn first.
|
|
376
|
+
const under = stack.reduce((found, element, at) => element.isBackground &&
|
|
377
|
+
Bound.deserialize(element.xywh).isOverlapWithBound(box)
|
|
378
|
+
? at
|
|
379
|
+
: found, -1);
|
|
380
|
+
if (under < 0)
|
|
381
|
+
return null;
|
|
382
|
+
// `null` for the upper bound when the background is the topmost element
|
|
383
|
+
// there is — `generateKeyBetween` reads that as "append after".
|
|
384
|
+
return generateKeyBetween(stack[under].index, stack[under + 1]?.index ?? null);
|
|
385
|
+
}
|
|
225
386
|
/**
|
|
226
387
|
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
227
388
|
*
|
|
@@ -119,6 +119,46 @@ export const wardleyCommandsManifest = [
|
|
|
119
119
|
scope: 'edgeless',
|
|
120
120
|
defaultKeys: { mac: ['w', 'i'], other: ['w', 'i'] },
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
id: 'wardley.addPorter',
|
|
124
|
+
owner: 'wardley',
|
|
125
|
+
labelKey: 'com.labre.commands.wardley.addPorter',
|
|
126
|
+
labelFallback: "Porter's forces",
|
|
127
|
+
scope: 'edgeless',
|
|
128
|
+
defaultKeys: { mac: [], other: [] },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'wardley.addAccelerator',
|
|
132
|
+
owner: 'wardley',
|
|
133
|
+
labelKey: 'com.labre.commands.wardley.addAccelerator',
|
|
134
|
+
labelFallback: 'Accelerator',
|
|
135
|
+
scope: 'edgeless',
|
|
136
|
+
defaultKeys: { mac: [], other: [] },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'wardley.addDecelerator',
|
|
140
|
+
owner: 'wardley',
|
|
141
|
+
labelKey: 'com.labre.commands.wardley.addDecelerator',
|
|
142
|
+
labelFallback: 'Decelerator',
|
|
143
|
+
scope: 'edgeless',
|
|
144
|
+
defaultKeys: { mac: [], other: [] },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'wardley.addAreaRect',
|
|
148
|
+
owner: 'wardley',
|
|
149
|
+
labelKey: 'com.labre.commands.wardley.addAreaRect',
|
|
150
|
+
labelFallback: 'Area (rectangle)',
|
|
151
|
+
scope: 'edgeless',
|
|
152
|
+
defaultKeys: { mac: [], other: [] },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'wardley.addAreaPolygon',
|
|
156
|
+
owner: 'wardley',
|
|
157
|
+
labelKey: 'com.labre.commands.wardley.addAreaPolygon',
|
|
158
|
+
labelFallback: 'Area (polygon)',
|
|
159
|
+
scope: 'edgeless',
|
|
160
|
+
defaultKeys: { mac: [], other: [] },
|
|
161
|
+
},
|
|
122
162
|
{
|
|
123
163
|
id: 'wardley.importOwm',
|
|
124
164
|
owner: 'wardley',
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import type { CommandDescriptor } from '@formicoidea/labre-core/std';
|
|
2
2
|
import type { TemplateResult } from 'lit';
|
|
3
3
|
/**
|
|
4
|
-
* The Wardley registry: the
|
|
4
|
+
* The Wardley registry: the eighteen toolbox entries, then the two directions
|
|
5
5
|
* of the OWM DSL and the SVG fallback (`docs/adr/0012`).
|
|
6
|
+
*
|
|
7
|
+
* ## Nineteen nominations, and the curation decision behind them
|
|
8
|
+
*
|
|
9
|
+
* `addPorter` spent the last seat of ADR 0014 R4's `SENIOR_MENU_CAP + 1`
|
|
10
|
+
* budget, and `addAccelerator` / `addDecelerator` are the two past it. The PO
|
|
11
|
+
* made the call on 2026-09-03 (Amendment 2026-09-03 under R4): **every Wardley
|
|
12
|
+
* artefact nominates the row**. The row itself is unchanged — thirteen
|
|
13
|
+
* arbitrated seats plus "More artefacts…", the cap of fourteen it has had since
|
|
14
|
+
* the OWM pair tipped this framework over — so what the decision buys is that
|
|
15
|
+
* a newly-drawn artefact is ELIGIBLE for a seat, and recency and frequency
|
|
16
|
+
* decide who gets one. Nothing becomes unreachable: the catalogue lists
|
|
17
|
+
* everything, which is the registry's own invariant.
|
|
18
|
+
*
|
|
19
|
+
* `addAreaRect` and `addAreaPolygon` are the two after those, and they land
|
|
20
|
+
* under that same amendment rather than reopening it: they nominate like every
|
|
21
|
+
* other Wardley artefact, the row is still thirteen arbitrated seats plus "More
|
|
22
|
+
* artefacts…", and the catalogue grows by a section of its own.
|
|
6
23
|
*/
|
|
7
24
|
export declare const wardleyCommands: CommandDescriptor[];
|
|
8
25
|
/** `iconKey` → template. Never travels through either manifest (ADR 0008). */
|
package/dist/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
2
|
-
import { activateWardleyConnector, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsOnBoard, } from './actions.js';
|
|
2
|
+
import { activateWardleyConnector, createWardleyAccelerator, createWardleyArea, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, createWardleyPorter, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsOnBoard, } from './actions.js';
|
|
3
3
|
import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.js';
|
|
4
|
-
import { wardleyAnchorIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, } from './toolbar/icons.js';
|
|
4
|
+
import { wardleyAcceleratorIcon, wardleyAnchorIcon, wardleyAreaPolygonIcon, wardleyAreaRectIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyDeceleratorIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, wardleyPorterIcon, } from './toolbar/icons.js';
|
|
5
5
|
const SPECS = [
|
|
6
6
|
{
|
|
7
7
|
id: 'addBackground',
|
|
@@ -12,6 +12,7 @@ const SPECS = [
|
|
|
12
12
|
category: 'backgrounds',
|
|
13
13
|
kind: 'artefact',
|
|
14
14
|
element: 'background:classic',
|
|
15
|
+
board: true,
|
|
15
16
|
run: gfx => createWardleyBackground(gfx, 'classic'),
|
|
16
17
|
},
|
|
17
18
|
{
|
|
@@ -21,6 +22,7 @@ const SPECS = [
|
|
|
21
22
|
category: 'backgrounds',
|
|
22
23
|
kind: 'artefact',
|
|
23
24
|
element: 'background:opportunity',
|
|
25
|
+
board: true,
|
|
24
26
|
run: gfx => createWardleyBackground(gfx, 'opportunity'),
|
|
25
27
|
},
|
|
26
28
|
{
|
|
@@ -30,6 +32,7 @@ const SPECS = [
|
|
|
30
32
|
category: 'backgrounds',
|
|
31
33
|
kind: 'artefact',
|
|
32
34
|
element: 'background:benefit',
|
|
35
|
+
board: true,
|
|
33
36
|
run: gfx => createWardleyBackground(gfx, 'benefit'),
|
|
34
37
|
},
|
|
35
38
|
{
|
|
@@ -39,6 +42,7 @@ const SPECS = [
|
|
|
39
42
|
category: 'backgrounds',
|
|
40
43
|
kind: 'artefact',
|
|
41
44
|
element: 'background:evolution-gradient',
|
|
45
|
+
board: true,
|
|
42
46
|
run: gfx => createWardleyBackground(gfx, 'evolution-gradient'),
|
|
43
47
|
},
|
|
44
48
|
{
|
|
@@ -137,6 +141,69 @@ const SPECS = [
|
|
|
137
141
|
element: 'node:inertia',
|
|
138
142
|
run: createWardleyInertia,
|
|
139
143
|
},
|
|
144
|
+
// Last of the nodes, and last of the row: a Porter's force is the only
|
|
145
|
+
// artefact on this palette that is not part of the map — it is what presses
|
|
146
|
+
// on it from outside — so it reads as an annotation you reach for once the
|
|
147
|
+
// chain is drawn. Keyless: the `w` chord already seats eight artefacts, and
|
|
148
|
+
// a framework binds past that by host override rather than by default.
|
|
149
|
+
{
|
|
150
|
+
id: 'addPorter',
|
|
151
|
+
label: "Porter's forces",
|
|
152
|
+
iconKey: 'wardley.porter',
|
|
153
|
+
category: 'nodes',
|
|
154
|
+
kind: 'artefact',
|
|
155
|
+
element: 'node:porter',
|
|
156
|
+
run: createWardleyPorter,
|
|
157
|
+
},
|
|
158
|
+
// The two climate annotations, after the force and for the same reason: they
|
|
159
|
+
// are not links in the value chain either, and they are reached for once the
|
|
160
|
+
// chain is drawn and the question becomes how fast it is moving. Keyless,
|
|
161
|
+
// like the porter — the `w` chord seats eight artefacts and a framework binds
|
|
162
|
+
// past that by host override.
|
|
163
|
+
{
|
|
164
|
+
id: 'addAccelerator',
|
|
165
|
+
label: 'Accelerator',
|
|
166
|
+
iconKey: 'wardley.accelerator',
|
|
167
|
+
category: 'nodes',
|
|
168
|
+
kind: 'artefact',
|
|
169
|
+
element: 'node:accelerator',
|
|
170
|
+
run: gfx => createWardleyAccelerator(gfx, 'accelerator'),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'addDecelerator',
|
|
174
|
+
label: 'Decelerator',
|
|
175
|
+
iconKey: 'wardley.decelerator',
|
|
176
|
+
category: 'nodes',
|
|
177
|
+
kind: 'artefact',
|
|
178
|
+
element: 'node:decelerator',
|
|
179
|
+
run: gfx => createWardleyAccelerator(gfx, 'decelerator'),
|
|
180
|
+
},
|
|
181
|
+
// The zones, last of the toolbox and after the connectors: an area is drawn
|
|
182
|
+
// around a chain that already exists, so it is the last gesture of a map and
|
|
183
|
+
// reads as the last entry of the row. Two entries and ONE kind — the shape is
|
|
184
|
+
// the choice, and offering it as two buttons is what saves the author a trip
|
|
185
|
+
// through "switch type" for the commonest case.
|
|
186
|
+
//
|
|
187
|
+
// Keyless, like the porter and the two climate arrows: the `w` chord seats
|
|
188
|
+
// eight artefacts and a framework binds past that by host override.
|
|
189
|
+
{
|
|
190
|
+
id: 'addAreaRect',
|
|
191
|
+
label: 'Area (rectangle)',
|
|
192
|
+
iconKey: 'wardley.area-rect',
|
|
193
|
+
category: 'areas',
|
|
194
|
+
kind: 'artefact',
|
|
195
|
+
element: 'node:area-rect',
|
|
196
|
+
run: gfx => createWardleyArea(gfx, 'rect'),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'addAreaPolygon',
|
|
200
|
+
label: 'Area (polygon)',
|
|
201
|
+
iconKey: 'wardley.area-polygon',
|
|
202
|
+
category: 'areas',
|
|
203
|
+
kind: 'artefact',
|
|
204
|
+
element: 'node:area-polygon',
|
|
205
|
+
run: gfx => createWardleyArea(gfx, 'polygon'),
|
|
206
|
+
},
|
|
140
207
|
];
|
|
141
208
|
/**
|
|
142
209
|
* The sentence a tool shows under its label — the role's own gesture hint
|
|
@@ -171,7 +238,7 @@ const toolboxCommands = SPECS.map((spec, order) => ({
|
|
|
171
238
|
: { mac: [], other: [] },
|
|
172
239
|
availability: 'always',
|
|
173
240
|
run: std => spec.run(std.get(GfxControllerIdentifier)),
|
|
174
|
-
telemetry: { framework: 'wardley', element: spec.element },
|
|
241
|
+
telemetry: { framework: 'wardley', element: spec.element, board: spec.board },
|
|
175
242
|
}));
|
|
176
243
|
/**
|
|
177
244
|
* The IMPORT — the first Wardley command whose subject is a whole map rather
|
|
@@ -183,9 +250,9 @@ const toolboxCommands = SPECS.map((spec, order) => ({
|
|
|
183
250
|
* the sub-menu is the first thing a user opens, and "start from the map
|
|
184
251
|
* somebody sent me" belongs in that row beside "start from a component" —
|
|
185
252
|
* asking them to find the catalogue sidepanel first is the friction the
|
|
186
|
-
* decision names. It is
|
|
187
|
-
*
|
|
188
|
-
*
|
|
253
|
+
* decision names. It is the `+1` of ADR 0014's R4 — the single deliberate
|
|
254
|
+
* over-nomination the PO signed off on, and the reason this framework's
|
|
255
|
+
* nomination budget is `SENIOR_MENU_CAP + 1` rather than the cap itself.
|
|
189
256
|
*
|
|
190
257
|
* No `'contextual-toolbar'`: a contextual toolbar is a statement about a
|
|
191
258
|
* SELECTION, and the moment this command is most wanted is on a board with
|
|
@@ -323,8 +390,25 @@ const importSvgCommand = {
|
|
|
323
390
|
telemetry: { framework: 'wardley', element: 'board:import-svg' },
|
|
324
391
|
};
|
|
325
392
|
/**
|
|
326
|
-
* The Wardley registry: the
|
|
393
|
+
* The Wardley registry: the eighteen toolbox entries, then the two directions
|
|
327
394
|
* of the OWM DSL and the SVG fallback (`docs/adr/0012`).
|
|
395
|
+
*
|
|
396
|
+
* ## Nineteen nominations, and the curation decision behind them
|
|
397
|
+
*
|
|
398
|
+
* `addPorter` spent the last seat of ADR 0014 R4's `SENIOR_MENU_CAP + 1`
|
|
399
|
+
* budget, and `addAccelerator` / `addDecelerator` are the two past it. The PO
|
|
400
|
+
* made the call on 2026-09-03 (Amendment 2026-09-03 under R4): **every Wardley
|
|
401
|
+
* artefact nominates the row**. The row itself is unchanged — thirteen
|
|
402
|
+
* arbitrated seats plus "More artefacts…", the cap of fourteen it has had since
|
|
403
|
+
* the OWM pair tipped this framework over — so what the decision buys is that
|
|
404
|
+
* a newly-drawn artefact is ELIGIBLE for a seat, and recency and frequency
|
|
405
|
+
* decide who gets one. Nothing becomes unreachable: the catalogue lists
|
|
406
|
+
* everything, which is the registry's own invariant.
|
|
407
|
+
*
|
|
408
|
+
* `addAreaRect` and `addAreaPolygon` are the two after those, and they land
|
|
409
|
+
* under that same amendment rather than reopening it: they nominate like every
|
|
410
|
+
* other Wardley artefact, the row is still thirteen arbitrated seats plus "More
|
|
411
|
+
* artefacts…", and the catalogue grows by a section of its own.
|
|
328
412
|
*/
|
|
329
413
|
export const wardleyCommands = [
|
|
330
414
|
...toolboxCommands,
|
|
@@ -347,6 +431,11 @@ export const wardleyCommandIcons = {
|
|
|
347
431
|
'wardley.link': wardleyLinkIcon,
|
|
348
432
|
'wardley.arrow': wardleyArrowIcon,
|
|
349
433
|
'wardley.inertia': wardleyInertiaIcon,
|
|
434
|
+
'wardley.porter': wardleyPorterIcon,
|
|
435
|
+
'wardley.accelerator': wardleyAcceleratorIcon,
|
|
436
|
+
'wardley.decelerator': wardleyDeceleratorIcon,
|
|
437
|
+
'wardley.area-rect': wardleyAreaRectIcon,
|
|
438
|
+
'wardley.area-polygon': wardleyAreaPolygonIcon,
|
|
350
439
|
'wardley.import-owm': wardleyImportOwmIcon,
|
|
351
440
|
'wardley.export-owm': wardleyExportOwmIcon,
|
|
352
441
|
'wardley.import-svg': wardleyImportSvgIcon,
|