@formicoidea/labre-framework-wardley 0.37.0 → 0.38.1
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 +18 -27
- package/dist/actions.js +39 -85
- package/dist/presets.d.ts +39 -1
- package/dist/presets.js +68 -2
- package/dist/templates/index.js +75 -167
- package/dist/templates/maps.js +62 -50
- package/package.json +2 -2
package/dist/actions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { indexOverBackgrounds, type StackedElement } from '@formicoidea/labre-core/blocks/surface';
|
|
1
2
|
import { WardleyBackgroundElementModel, type WardleyBgVariant } from '@formicoidea/labre-core/model';
|
|
2
|
-
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
3
3
|
import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
4
4
|
import { type GfxController } from '@formicoidea/labre-core/std/gfx';
|
|
5
5
|
import { type WardleyExportBoard } from './export.js';
|
|
@@ -109,41 +109,32 @@ export declare function createWardleyAccelerator(gfx: GfxController, kind: 'acce
|
|
|
109
109
|
* to the depth {@link wardleyAreaIndexOver} works out.
|
|
110
110
|
*/
|
|
111
111
|
export declare function createWardleyArea(gfx: GfxController, shape: WardleyAreaShape): void;
|
|
112
|
+
/**
|
|
113
|
+
* Send one already-placed area to the depth it belongs at — see
|
|
114
|
+
* {@link createWardleyArea} for why a zone is lowered at all.
|
|
115
|
+
*
|
|
116
|
+
* Extracted so the TEMPLATE can call it: a snapshot cannot express this, because
|
|
117
|
+
* the depth is relative to whatever map is already on the board rather than to
|
|
118
|
+
* anything inside the template.
|
|
119
|
+
*/
|
|
120
|
+
export declare function lowerWardleyArea(gfx: GfxController, id: string): void;
|
|
112
121
|
/**
|
|
113
122
|
* One already-placed element, as {@link wardleyAreaIndexOver} needs to read it.
|
|
114
123
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
124
|
+
* The Wardley name for the shared {@link StackedElement}, kept so this
|
|
125
|
+
* framework's callers and tests keep reading in its own vocabulary.
|
|
117
126
|
*/
|
|
118
|
-
export
|
|
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
|
-
}
|
|
127
|
+
export type WardleyStackedElement = StackedElement;
|
|
126
128
|
/**
|
|
127
129
|
* How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
|
|
128
130
|
* covers, and below everything else. `null` means the back of the surface.
|
|
129
131
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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.
|
|
132
|
+
* The reasoning — and the recette of #213 that produced it — now lives with
|
|
133
|
+
* {@link indexOverBackgrounds}, because a frame drawn on a map needs the very
|
|
134
|
+
* same depth for the very same reason. This alias keeps the Wardley name for
|
|
135
|
+
* the callers and tests written against it.
|
|
145
136
|
*/
|
|
146
|
-
export declare
|
|
137
|
+
export declare const wardleyAreaIndexOver: typeof indexOverBackgrounds;
|
|
147
138
|
/**
|
|
148
139
|
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
149
140
|
*
|
package/dist/actions.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { backgroundSize, DefaultTool, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
|
|
1
|
+
import { backgroundSize, DefaultTool, indexOverBackgrounds, 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,
|
|
4
|
+
import { ConnectorMode, FontWeight, FrameworkBackgroundElementModel, PointStyle, StrokeStyle, 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 {
|
|
8
|
+
import { 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,
|
|
14
|
-
import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
|
|
13
|
+
import { HANDLE_SIZE, INERTIA_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, PORTER_DEFAULT_LETTER, WARDLEY_RED, } from './node/consts.js';
|
|
14
|
+
import { wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyHandleBox, wardleyHandleProps, wardleyInertiaProps, WARDLEY_LABEL_H, WARDLEY_LABEL_W, wardleyLabelProps, 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
|
|
@@ -39,8 +39,8 @@ import { WARDLEY_ROLE } from './roles.js';
|
|
|
39
39
|
* The fix is to make the variant part of the declaration — one axis/end-label
|
|
40
40
|
* set per variant, each naming its own `labelKey` — rather than a bag of prop
|
|
41
41
|
* overrides applied at creation. Out of scope here (it changes what a variant
|
|
42
|
-
* IS)
|
|
43
|
-
*
|
|
42
|
+
* IS). This is now the ONLY copy: the four background templates are derived
|
|
43
|
+
* from these four commands, so the table goes away in one place.
|
|
44
44
|
*/
|
|
45
45
|
const BACKGROUND_VARIANT_DEFAULTS = {
|
|
46
46
|
classic: {},
|
|
@@ -62,16 +62,10 @@ const BACKGROUND_VARIANT_DEFAULTS = {
|
|
|
62
62
|
// the grey gradient differs.
|
|
63
63
|
'evolution-gradient': {},
|
|
64
64
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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;
|
|
65
|
+
// The label box, now owned by `presets.ts` — the local names stay so the four
|
|
66
|
+
// placements below keep reading as they did.
|
|
67
|
+
const LABEL_H = WARDLEY_LABEL_H;
|
|
68
|
+
const LABEL_W = WARDLEY_LABEL_W;
|
|
75
69
|
function finish(gfx, id) {
|
|
76
70
|
gfx.doc.captureSync();
|
|
77
71
|
gfx.tool.setTool(DefaultTool);
|
|
@@ -106,21 +100,7 @@ function addNode(surface, kind, cx, cy, extra = {}) {
|
|
|
106
100
|
}
|
|
107
101
|
/** Add a native free-text label (same Inter family as the axis labels). */
|
|
108
102
|
function addLabel(surface, text, x, y, textAlign = 'left', fontWeight = FontWeight.Regular) {
|
|
109
|
-
return surface.addElement(
|
|
110
|
-
type: 'text',
|
|
111
|
-
text,
|
|
112
|
-
fontWeight,
|
|
113
|
-
// Semantic identity (PF1, revised in PF13.4): a Wardley label is a free
|
|
114
|
-
// text element like any other, so its ROLE is the only thing that tells W3
|
|
115
|
-
// it must not land on top of a node. A free text the user typed elsewhere
|
|
116
|
-
// stays neutral and is never evaluated.
|
|
117
|
-
role: WARDLEY_ROLE.label,
|
|
118
|
-
fontFamily: FontFamily.Inter,
|
|
119
|
-
fontSize: LABEL_FONT_SIZE,
|
|
120
|
-
color: NODE_STROKE,
|
|
121
|
-
textAlign,
|
|
122
|
-
xywh: new Bound(x, y, LABEL_W, LABEL_H).serialize(),
|
|
123
|
-
});
|
|
103
|
+
return surface.addElement(wardleyLabelProps(text, x, y, textAlign, fontWeight));
|
|
124
104
|
}
|
|
125
105
|
/** Create a wardley map background of the given variant, viewport-centered. */
|
|
126
106
|
export function createWardleyBackground(gfx, variant = 'classic') {
|
|
@@ -174,23 +154,9 @@ export function createWardleyInertia(gfx) {
|
|
|
174
154
|
return;
|
|
175
155
|
const { w, h } = INERTIA_SIZE;
|
|
176
156
|
const { centerX, centerY } = gfx.viewport;
|
|
177
|
-
const id = gfx.surface.addElement({
|
|
178
|
-
type: 'shape',
|
|
179
|
-
shapeType: 'rect',
|
|
180
|
-
// The inertia bar has no element type of its own — it IS a plain filled
|
|
181
|
-
// rect — so the role is the whole of its semantics (PF13.5).
|
|
182
|
-
role: WARDLEY_ROLE.inertia,
|
|
183
|
-
filled: true,
|
|
184
|
-
fillColor: INERTIA_COLOR,
|
|
185
|
-
strokeColor: INERTIA_COLOR,
|
|
186
|
-
strokeWidth: 0,
|
|
187
|
-
shapeStyle: ShapeStyle.General,
|
|
188
|
-
roughness: 0,
|
|
189
|
-
radius: 0,
|
|
190
|
-
// the inertia bar has a canonical size: text overflows, never deforms
|
|
191
|
-
textFitMode: TextFitMode.Overflow,
|
|
157
|
+
const id = gfx.surface.addElement(wardleyInertiaProps({
|
|
192
158
|
xywh: new Bound(centerX - w / 2, centerY - h / 2, w, h).serialize(),
|
|
193
|
-
});
|
|
159
|
+
}));
|
|
194
160
|
finish(gfx, id);
|
|
195
161
|
}
|
|
196
162
|
/**
|
|
@@ -329,16 +295,28 @@ export function createWardleyArea(gfx, shape) {
|
|
|
329
295
|
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
330
296
|
const xywh = wardleyAreaBox(shape, cx, cy);
|
|
331
297
|
const id = surface.addElement(wardleyAreaProps(shape, { xywh }));
|
|
332
|
-
|
|
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
|
-
}
|
|
298
|
+
lowerWardleyArea(gfx, id);
|
|
340
299
|
finish(gfx, id);
|
|
341
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Send one already-placed area to the depth it belongs at — see
|
|
303
|
+
* {@link createWardleyArea} for why a zone is lowered at all.
|
|
304
|
+
*
|
|
305
|
+
* Extracted so the TEMPLATE can call it: a snapshot cannot express this, because
|
|
306
|
+
* the depth is relative to whatever map is already on the board rather than to
|
|
307
|
+
* anything inside the template.
|
|
308
|
+
*/
|
|
309
|
+
export function lowerWardleyArea(gfx, id) {
|
|
310
|
+
const surface = gfx.surface;
|
|
311
|
+
const model = surface?.getElementById(id);
|
|
312
|
+
if (!surface || !model)
|
|
313
|
+
return;
|
|
314
|
+
const over = wardleyAreaIndexOver(stackedElementsOf(surface, id), Bound.deserialize(model.xywh));
|
|
315
|
+
// `null` is "nothing under it": the back of the surface, minted by the
|
|
316
|
+
// layer itself so an empty board gets the layer's own initial index rather
|
|
317
|
+
// than a key invented here.
|
|
318
|
+
model.index = over ?? gfx.layer.getReorderedIndex(model, 'back');
|
|
319
|
+
}
|
|
342
320
|
/** The top-level canvas elements of a surface, minus the one being placed. */
|
|
343
321
|
function stackedElementsOf(surface, exclude) {
|
|
344
322
|
return surface.elementModels
|
|
@@ -353,36 +331,12 @@ function stackedElementsOf(surface, exclude) {
|
|
|
353
331
|
* How deep a freshly drawn zone goes: just ABOVE the framework backgrounds it
|
|
354
332
|
* covers, and below everything else. `null` means the back of the surface.
|
|
355
333
|
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
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.
|
|
334
|
+
* The reasoning — and the recette of #213 that produced it — now lives with
|
|
335
|
+
* {@link indexOverBackgrounds}, because a frame drawn on a map needs the very
|
|
336
|
+
* same depth for the very same reason. This alias keeps the Wardley name for
|
|
337
|
+
* the callers and tests written against it.
|
|
371
338
|
*/
|
|
372
|
-
export
|
|
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
|
-
}
|
|
339
|
+
export const wardleyAreaIndexOver = indexOverBackgrounds;
|
|
386
340
|
/**
|
|
387
341
|
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
388
342
|
*
|
package/dist/presets.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type WardleyNodeKind } from '@formicoidea/labre-core/model';
|
|
1
|
+
import { FontWeight, type WardleyNodeKind } from '@formicoidea/labre-core/model';
|
|
2
2
|
/**
|
|
3
3
|
* What a Wardley artefact IS, as props — the ONE description the creation sites
|
|
4
4
|
* and the morph both read.
|
|
@@ -98,6 +98,44 @@ export declare function wardleyNodeProps(kind: WardleyArtefactKind, box: {
|
|
|
98
98
|
}): Record<string, unknown> & {
|
|
99
99
|
type: string;
|
|
100
100
|
};
|
|
101
|
+
/** Height of the native free-text labels (Inter, size 18). */
|
|
102
|
+
export declare const WARDLEY_LABEL_H: number;
|
|
103
|
+
/**
|
|
104
|
+
* Width of a label box, whatever it reads.
|
|
105
|
+
*
|
|
106
|
+
* A number rather than a measurement, which is why a RIGHT-aligned label has to
|
|
107
|
+
* subtract it: the box does not shrink to the words, so the only way to make
|
|
108
|
+
* the words end on a given edge is to start the box a full width before it.
|
|
109
|
+
*/
|
|
110
|
+
export declare const WARDLEY_LABEL_W = 120;
|
|
111
|
+
/**
|
|
112
|
+
* The NAME beside an artefact, as props — a native free-text element.
|
|
113
|
+
*
|
|
114
|
+
* Here rather than at the creation site for the reason the rest of this pack
|
|
115
|
+
* gives: the templates re-stated it by hand and drifted (a 140-wide box against
|
|
116
|
+
* this 120), so what a Wardley label IS has to have exactly one description.
|
|
117
|
+
*
|
|
118
|
+
* `text` is a plain STRING, as `Surface.addElement` takes it; a snapshot writes
|
|
119
|
+
* the same field as a serialized `Y.Text`, which is the one key an authoring
|
|
120
|
+
* kit overrides on the way into a template.
|
|
121
|
+
*/
|
|
122
|
+
export declare function wardleyLabelProps(text: string, x: number, y: number, textAlign?: 'left' | 'center' | 'right', fontWeight?: FontWeight): Record<string, unknown> & {
|
|
123
|
+
type: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* An INERTIA bar, as props — a plain filled rect, and the whole of its
|
|
127
|
+
* semantics is the role.
|
|
128
|
+
*
|
|
129
|
+
* A preset for the same reason every other one here exists: the creation site
|
|
130
|
+
* and the shipped maps both draw one, and the hand-written copy in the
|
|
131
|
+
* templates had already lost `textFitMode` — the one line that keeps the bar's
|
|
132
|
+
* canonical size when somebody types into it.
|
|
133
|
+
*/
|
|
134
|
+
export declare function wardleyInertiaProps(box: {
|
|
135
|
+
xywh: string;
|
|
136
|
+
}): Record<string, unknown> & {
|
|
137
|
+
type: string;
|
|
138
|
+
};
|
|
101
139
|
/**
|
|
102
140
|
* An AREA, as props — one kind, two shapes.
|
|
103
141
|
*
|
package/dist/presets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ConnectorMode, DEFAULT_POLYGON_VERTICES, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
|
|
1
|
+
import { ConnectorMode, DEFAULT_POLYGON_VERTICES, FontFamily, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
|
|
2
2
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
3
|
-
import { ACCELERATOR_FILL, ACCELERATOR_LABEL, ACCELERATOR_SIZE, ACCELERATOR_STROKE_WIDTH, ACCELERATOR_VERTICES, AREA_FILL, AREA_POLYGON_SIZE, AREA_RECT_SIZE, AREA_STROKE, AREA_STROKE_WIDTH, DECELERATOR_LABEL, DECELERATOR_VERTICES, ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, LABEL_DEFAULT, LABEL_FONT_SIZE, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, PORTER_ARROW, PORTER_LETTER_FONT_SIZE, PORTER_SIZE, WARDLEY_RED, } from './node/consts.js';
|
|
3
|
+
import { ACCELERATOR_FILL, ACCELERATOR_LABEL, ACCELERATOR_SIZE, ACCELERATOR_STROKE_WIDTH, ACCELERATOR_VERTICES, AREA_FILL, AREA_POLYGON_SIZE, AREA_RECT_SIZE, AREA_STROKE, AREA_STROKE_WIDTH, DECELERATOR_LABEL, DECELERATOR_VERTICES, ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, LABEL_DEFAULT, LABEL_FONT_SIZE, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, PORTER_ARROW, PORTER_LETTER_FONT_SIZE, PORTER_SIZE, WARDLEY_RED, } from './node/consts.js';
|
|
4
4
|
import { WARDLEY_ROLE } from './roles.js';
|
|
5
5
|
/**
|
|
6
6
|
* The size a kind is BORN at, which for Wardley is the notation rather than a
|
|
@@ -167,6 +167,72 @@ export function wardleyNodeProps(kind, box) {
|
|
|
167
167
|
xywh: box.xywh,
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
+
/** Height of the native free-text labels (Inter, size 18). */
|
|
171
|
+
export const WARDLEY_LABEL_H = LABEL_FONT_SIZE + 8;
|
|
172
|
+
/**
|
|
173
|
+
* Width of a label box, whatever it reads.
|
|
174
|
+
*
|
|
175
|
+
* A number rather than a measurement, which is why a RIGHT-aligned label has to
|
|
176
|
+
* subtract it: the box does not shrink to the words, so the only way to make
|
|
177
|
+
* the words end on a given edge is to start the box a full width before it.
|
|
178
|
+
*/
|
|
179
|
+
export const WARDLEY_LABEL_W = 120;
|
|
180
|
+
/**
|
|
181
|
+
* The NAME beside an artefact, as props — a native free-text element.
|
|
182
|
+
*
|
|
183
|
+
* Here rather than at the creation site for the reason the rest of this pack
|
|
184
|
+
* gives: the templates re-stated it by hand and drifted (a 140-wide box against
|
|
185
|
+
* this 120), so what a Wardley label IS has to have exactly one description.
|
|
186
|
+
*
|
|
187
|
+
* `text` is a plain STRING, as `Surface.addElement` takes it; a snapshot writes
|
|
188
|
+
* the same field as a serialized `Y.Text`, which is the one key an authoring
|
|
189
|
+
* kit overrides on the way into a template.
|
|
190
|
+
*/
|
|
191
|
+
export function wardleyLabelProps(text, x, y, textAlign = 'left', fontWeight = FontWeight.Regular) {
|
|
192
|
+
return {
|
|
193
|
+
type: 'text',
|
|
194
|
+
text,
|
|
195
|
+
fontWeight,
|
|
196
|
+
// Semantic identity (PF1, revised in PF13.4): a Wardley label is a free
|
|
197
|
+
// text element like any other, so its ROLE is the only thing that tells W3
|
|
198
|
+
// it must not land on top of a node. A free text the user typed elsewhere
|
|
199
|
+
// stays neutral and is never evaluated.
|
|
200
|
+
role: WARDLEY_ROLE.label,
|
|
201
|
+
fontFamily: FontFamily.Inter,
|
|
202
|
+
fontSize: LABEL_FONT_SIZE,
|
|
203
|
+
color: NODE_STROKE,
|
|
204
|
+
textAlign,
|
|
205
|
+
xywh: new Bound(x, y, WARDLEY_LABEL_W, WARDLEY_LABEL_H).serialize(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* An INERTIA bar, as props — a plain filled rect, and the whole of its
|
|
210
|
+
* semantics is the role.
|
|
211
|
+
*
|
|
212
|
+
* A preset for the same reason every other one here exists: the creation site
|
|
213
|
+
* and the shipped maps both draw one, and the hand-written copy in the
|
|
214
|
+
* templates had already lost `textFitMode` — the one line that keeps the bar's
|
|
215
|
+
* canonical size when somebody types into it.
|
|
216
|
+
*/
|
|
217
|
+
export function wardleyInertiaProps(box) {
|
|
218
|
+
return {
|
|
219
|
+
type: 'shape',
|
|
220
|
+
shapeType: 'rect',
|
|
221
|
+
// The inertia bar has no element type of its own — it IS a plain filled
|
|
222
|
+
// rect — so the role is the whole of its semantics (PF13.5).
|
|
223
|
+
role: WARDLEY_ROLE.inertia,
|
|
224
|
+
filled: true,
|
|
225
|
+
fillColor: INERTIA_COLOR,
|
|
226
|
+
strokeColor: INERTIA_COLOR,
|
|
227
|
+
strokeWidth: 0,
|
|
228
|
+
shapeStyle: ShapeStyle.General,
|
|
229
|
+
roughness: 0,
|
|
230
|
+
radius: 0,
|
|
231
|
+
// the inertia bar has a canonical size: text overflows, never deforms
|
|
232
|
+
textFitMode: TextFitMode.Overflow,
|
|
233
|
+
xywh: box.xywh,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
170
236
|
/**
|
|
171
237
|
* An AREA, as props — one kind, two shapes.
|
|
172
238
|
*
|
package/dist/templates/index.js
CHANGED
|
@@ -1,71 +1,34 @@
|
|
|
1
|
-
import { makeTemplateSnapshot,
|
|
2
|
-
import { ConnectorMode,
|
|
3
|
-
import {
|
|
1
|
+
import { makeTemplateSnapshot, templateFromCommand, } from '@formicoidea/labre-core/gfx/template';
|
|
2
|
+
import { ConnectorMode, PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
4
|
+
import { lowerWardleyArea } from '../actions.js';
|
|
5
|
+
import { wardleyCommands } from '../commands.js';
|
|
6
|
+
import { LINK_GREY, LINK_STROKE_WIDTH, WARDLEY_RED } from '../node/consts.js';
|
|
4
7
|
import { WARDLEY_ROLE } from '../roles.js';
|
|
5
8
|
import { wardleyMaps } from './maps.js';
|
|
6
|
-
import { ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_FONT_SIZE, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_WIDTH, WARDLEY_RED, } from '../node/consts.js';
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The Wardley palette — DERIVED from the toolbox, one template per command.
|
|
11
|
+
*
|
|
12
|
+
* Every single-artefact entry below is what its command actually draws, run
|
|
13
|
+
* once against a recording surface. It used to be a hand-written restatement of
|
|
14
|
+
* the same artefacts, and it had drifted exactly as far as a copy drifts: the
|
|
15
|
+
* four backgrounds had lost `role: wardley:map` and `resizeEnabled` (#77), no
|
|
16
|
+
* composite carried the `group` the toolbox has written since #51, the inertia
|
|
17
|
+
* bar had lost its `textFitMode`, label boxes were 140 against a `LABEL_W` of
|
|
18
|
+
* 120, and five commands had no template at all. Derived, none of that can
|
|
19
|
+
* happen again — and `templates-parity.unit.spec.ts` re-runs each command and
|
|
20
|
+
* compares, so the day a creation site changes the palette changes with it.
|
|
21
|
+
*
|
|
22
|
+
* The two shipped MAPS stay hand-composed (`maps.ts`): a canonical map is an
|
|
23
|
+
* arrangement of a dozen artefacts, which no single command draws. They are
|
|
24
|
+
* built on the same presets, and the same test checks their composition.
|
|
12
25
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
benefit: {
|
|
21
|
-
yAxisTitle: '',
|
|
22
|
-
visibilityHigh: 'Benefit',
|
|
23
|
-
visibilityLow: 'Investment',
|
|
24
|
-
showCornerLabels: false,
|
|
25
|
-
},
|
|
26
|
-
'evolution-gradient': {},
|
|
27
|
-
};
|
|
28
|
-
const bg = (variant, w = REF_WIDTH) => {
|
|
29
|
-
const h = Math.round((w * 9) / 16);
|
|
30
|
-
return {
|
|
31
|
-
type: 'wardley',
|
|
32
|
-
variant,
|
|
33
|
-
...VARIANT_DEFAULTS[variant],
|
|
34
|
-
xywh: `[0,0,${w},${h}]`,
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
/** A wardley node ellipse positioned by top-left. */
|
|
38
|
-
function node(kind, x, y, d = NODE_SIZE, fill = NODE_FILL, strokeWidth = NODE_STROKE_WIDTH,
|
|
39
|
-
// Neutral for the market's inner dots, exactly as `createWardleyMarket` does:
|
|
40
|
-
// glyph wiring, not artefacts.
|
|
41
|
-
neutral = false) {
|
|
42
|
-
return {
|
|
43
|
-
type: 'wardleyNode',
|
|
44
|
-
kind,
|
|
45
|
-
// A template must produce the same typed artefacts as the toolbox, so a
|
|
46
|
-
// map started from a preset validates like a hand-drawn one.
|
|
47
|
-
role: neutral ? undefined : WARDLEY_ROLE[kind],
|
|
48
|
-
shapeType: 'ellipse',
|
|
49
|
-
filled: true,
|
|
50
|
-
fillColor: fill,
|
|
51
|
-
strokeColor: NODE_STROKE,
|
|
52
|
-
strokeWidth,
|
|
53
|
-
shapeStyle: ShapeStyle.General,
|
|
54
|
-
roughness: 0,
|
|
55
|
-
xywh: `[${x},${y},${d},${d}]`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function label(x, y, str, align = 'left') {
|
|
59
|
-
return {
|
|
60
|
-
type: 'text',
|
|
61
|
-
text: surfaceText(str),
|
|
62
|
-
role: WARDLEY_ROLE.label,
|
|
63
|
-
color: NODE_STROKE,
|
|
64
|
-
fontFamily: FontFamily.Inter,
|
|
65
|
-
fontSize: LABEL_FONT_SIZE,
|
|
66
|
-
textAlign: align === 'center' ? TextAlign.Center : TextAlign.Left,
|
|
67
|
-
xywh: `[${x},${y},140,26]`,
|
|
68
|
-
};
|
|
26
|
+
/** The command a derived template is the picture of. Throws rather than skips. */
|
|
27
|
+
function byId(id) {
|
|
28
|
+
const command = wardleyCommands.find(entry => entry.id === id);
|
|
29
|
+
if (!command)
|
|
30
|
+
throw new Error(`[wardley] templates: no command "${id}"`);
|
|
31
|
+
return command;
|
|
69
32
|
}
|
|
70
33
|
/**
|
|
71
34
|
* A Wardley connector for the palette samples.
|
|
@@ -100,99 +63,16 @@ function connect(source, target, opts = {}) {
|
|
|
100
63
|
target,
|
|
101
64
|
};
|
|
102
65
|
}
|
|
103
|
-
const inertia = (x = 0, y = 0) => ({
|
|
104
|
-
type: 'shape',
|
|
105
|
-
shapeType: 'rect',
|
|
106
|
-
role: WARDLEY_ROLE.inertia,
|
|
107
|
-
filled: true,
|
|
108
|
-
fillColor: INERTIA_COLOR,
|
|
109
|
-
strokeColor: INERTIA_COLOR,
|
|
110
|
-
strokeWidth: 0,
|
|
111
|
-
shapeStyle: ShapeStyle.General,
|
|
112
|
-
roughness: 0,
|
|
113
|
-
radius: 0,
|
|
114
|
-
xywh: `[${x},${y},${INERTIA_SIZE.w},${INERTIA_SIZE.h}]`,
|
|
115
|
-
});
|
|
116
|
-
/** Pipeline composite: body rect + handle square (straddling top) + label. */
|
|
117
|
-
function pipeline() {
|
|
118
|
-
return {
|
|
119
|
-
label: label(0, 0, 'Pipeline', 'center'),
|
|
120
|
-
body: {
|
|
121
|
-
type: 'wardleyNode',
|
|
122
|
-
kind: 'pipeline',
|
|
123
|
-
role: WARDLEY_ROLE.pipeline,
|
|
124
|
-
shapeType: 'rect',
|
|
125
|
-
filled: true,
|
|
126
|
-
fillColor: PIPELINE_FILL,
|
|
127
|
-
strokeColor: NODE_STROKE,
|
|
128
|
-
strokeWidth: NODE_STROKE_WIDTH,
|
|
129
|
-
shapeStyle: ShapeStyle.General,
|
|
130
|
-
roughness: 0,
|
|
131
|
-
radius: 0,
|
|
132
|
-
xywh: `[0,39,${PIPELINE_WIDTH},${PIPELINE_HEIGHT}]`,
|
|
133
|
-
},
|
|
134
|
-
handle: {
|
|
135
|
-
type: 'wardleyNode',
|
|
136
|
-
kind: 'handle',
|
|
137
|
-
role: WARDLEY_ROLE.handle,
|
|
138
|
-
shapeType: 'rect',
|
|
139
|
-
filled: true,
|
|
140
|
-
fillColor: NODE_FILL,
|
|
141
|
-
strokeColor: NODE_STROKE,
|
|
142
|
-
strokeWidth: NODE_STROKE_WIDTH,
|
|
143
|
-
shapeStyle: ShapeStyle.General,
|
|
144
|
-
roughness: 0,
|
|
145
|
-
radius: 0,
|
|
146
|
-
xywh: `[${PIPELINE_WIDTH / 2 - HANDLE_SIZE / 2},${39 - HANDLE_SIZE / 2},${HANDLE_SIZE},${HANDLE_SIZE}]`,
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
/** Market composite: outer circle + 3 inner dots wired in a triangle + label. */
|
|
151
|
-
function market() {
|
|
152
|
-
const R = MARKET_SIZE / 2;
|
|
153
|
-
const c = R; // center within the [0,0,MARKET_SIZE,MARKET_SIZE] box
|
|
154
|
-
const rho = MARKET_DOT_RING;
|
|
155
|
-
const sin60 = Math.sqrt(3) / 2;
|
|
156
|
-
const verts = [
|
|
157
|
-
[0, -rho],
|
|
158
|
-
[rho * sin60, rho / 2],
|
|
159
|
-
[-rho * sin60, rho / 2],
|
|
160
|
-
];
|
|
161
|
-
const dotAt = (vx, vy) => node('component', c + vx - MARKET_DOT_SIZE / 2, c + vy - MARKET_DOT_SIZE / 2, MARKET_DOT_SIZE, NODE_FILL, MARKET_DOT_STROKE_WIDTH, true);
|
|
162
|
-
// Neutral on purpose: the triangle is the market glyph's own wiring, not a
|
|
163
|
-
// dependency the user drew (same rule as `createWardleyMarket`).
|
|
164
|
-
const tri = (a, b) => ({
|
|
165
|
-
type: 'connector',
|
|
166
|
-
mode: ConnectorMode.Straight,
|
|
167
|
-
stroke: MARKET_LINK_COLOR,
|
|
168
|
-
strokeStyle: StrokeStyle.Solid,
|
|
169
|
-
strokeWidth: MARKET_LINK_WIDTH,
|
|
170
|
-
frontEndpointStyle: PointStyle.None,
|
|
171
|
-
rearEndpointStyle: PointStyle.None,
|
|
172
|
-
source: { id: a },
|
|
173
|
-
target: { id: b },
|
|
174
|
-
});
|
|
175
|
-
return {
|
|
176
|
-
circle: node('market', 0, 0, MARKET_SIZE, NODE_FILL),
|
|
177
|
-
d0: dotAt(verts[0][0], verts[0][1]),
|
|
178
|
-
d1: dotAt(verts[1][0], verts[1][1]),
|
|
179
|
-
d2: dotAt(verts[2][0], verts[2][1]),
|
|
180
|
-
t0: tri('d0', 'd1'),
|
|
181
|
-
t1: tri('d1', 'd2'),
|
|
182
|
-
t2: tri('d2', 'd0'),
|
|
183
|
-
label: label(MARKET_SIZE + 8, 2, 'Market'),
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
const single = (el) => ({
|
|
187
|
-
a: el,
|
|
188
|
-
});
|
|
189
|
-
const nodeWithLabel = (kind, d, fill, name) => ({
|
|
190
|
-
n: node(kind, 0, 0, d, fill),
|
|
191
|
-
l: label(d + 8, d / 2 - 13, name),
|
|
192
|
-
});
|
|
193
66
|
const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
|
|
194
67
|
const bgPreview = (extra = '') => `<svg ${ATTRS} fill="none"><path d="M22 12 V64 H120" stroke="#3b3d42" stroke-width="2"/><path d="M44 12 V64 M68 12 V64 M94 12 V64" stroke="#9aa0a6" stroke-width="0.8"/>${extra}</svg>`;
|
|
195
68
|
const dotPreview = (fill, sw = 2) => `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="${fill}" stroke="#1f2328" stroke-width="${sw}"/></svg>`;
|
|
69
|
+
/** The two fat arrows: the same outline, mirrored — the direction IS the kind. */
|
|
70
|
+
const arrowPreview = (rightwards) => `<svg ${ATTRS} fill="none"><path d="${rightwards
|
|
71
|
+
? 'M32 32 H80 V23 L103 40 L80 57 V48 H32 Z'
|
|
72
|
+
: 'M103 32 H55 V23 L32 40 L55 57 V48 H103 Z'}" fill="#bfbfbf" stroke="#1f2328" stroke-width="1.8" stroke-linejoin="round"/></svg>`;
|
|
73
|
+
/** The zone, in its Peace wash — the pair differs on the outline and on nothing else. */
|
|
74
|
+
const areaPreview = (outline) => `<svg ${ATTRS} fill="none">${outline}</svg>`;
|
|
75
|
+
/** A hand-composed template — what is left once the artefacts are derived. */
|
|
196
76
|
function tpl(name, preview, elements) {
|
|
197
77
|
return {
|
|
198
78
|
name,
|
|
@@ -201,27 +81,55 @@ function tpl(name, preview, elements) {
|
|
|
201
81
|
content: makeTemplateSnapshot(elements, name),
|
|
202
82
|
};
|
|
203
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* A zone, plus the one thing a snapshot cannot say.
|
|
86
|
+
*
|
|
87
|
+
* `createWardleyArea` lowers the zone the moment it exists, to just above the
|
|
88
|
+
* framework backgrounds it covers — otherwise the wash sits on top of every
|
|
89
|
+
* component it groups and eats their clicks. That depth is relative to the map
|
|
90
|
+
* ALREADY ON THE BOARD, which a snapshot knows nothing about, so the panel
|
|
91
|
+
* replays it on the freshly inserted element instead.
|
|
92
|
+
*/
|
|
93
|
+
function areaTemplate(id, preview) {
|
|
94
|
+
return {
|
|
95
|
+
...templateFromCommand(byId(id), preview),
|
|
96
|
+
afterInsert: (std, insertedIds) => lowerWardleyArea(std.get(GfxControllerIdentifier), insertedIds[0]),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
204
99
|
export const wardleyTemplateCategory = {
|
|
205
100
|
name: 'Wardley',
|
|
206
101
|
templates: [
|
|
207
102
|
...wardleyMaps,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
103
|
+
templateFromCommand(byId('wardley.addBackground'), bgPreview(), 'Map background'),
|
|
104
|
+
templateFromCommand(byId('wardley.addOpportunityBackground'), bgPreview('<rect x="22" y="12" width="98" height="52" fill="#eef4fb" opacity="0.6"/>'), 'Opportunity gradient'),
|
|
105
|
+
templateFromCommand(byId('wardley.addBenefitBackground'), bgPreview('<rect x="22" y="12" width="98" height="26" fill="#e6eef8" opacity="0.6"/>'), 'Benefit gradient'),
|
|
106
|
+
templateFromCommand(byId('wardley.addEvolutionBackground'), bgPreview('<rect x="22" y="12" width="98" height="52" fill="#e3e2e4" opacity="0.5"/>'), 'Evolution gradient'),
|
|
107
|
+
templateFromCommand(byId('wardley.addComponent'), dotPreview('#ffffff', 1.5), 'Component'),
|
|
108
|
+
templateFromCommand(byId('wardley.addMethod'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#d9d9d9" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="7" fill="#fff" stroke="#1f2328"/></svg>`, 'Method'),
|
|
109
|
+
templateFromCommand(byId('wardley.addMarket'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="16" fill="#fff" stroke="#1f2328"/><circle cx="67" cy="30" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="75" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="59" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><path d="M67 30 L75 46 L59 46 Z" stroke="#1f2328" stroke-width="0.8" fill="none"/></svg>`, 'Market'),
|
|
110
|
+
templateFromCommand(byId('wardley.addEcosystem'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="11" fill="none" stroke="#1f2328"/><circle cx="67" cy="40" r="5" fill="#fff" stroke="#1f2328"/></svg>`, 'Ecosystem'),
|
|
111
|
+
templateFromCommand(byId('wardley.addAnchor'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="36" r="3.5" fill="#1f2328"/><path d="M59 48 q8 -9 16 0" stroke="#1f2328" stroke-width="1.5" fill="none"/></svg>`, 'Anchor'),
|
|
112
|
+
templateFromCommand(byId('wardley.addPipeline'), `<svg ${ATTRS} fill="none"><rect x="34" y="40" width="66" height="14" fill="#fff" stroke="#1f2328"/><rect x="60" y="33" width="14" height="14" fill="#fff" stroke="#1f2328"/></svg>`, 'Pipeline'),
|
|
113
|
+
templateFromCommand(byId('wardley.addInertia'), `<svg ${ATTRS} fill="none"><rect x="63" y="22" width="8" height="36" fill="#1f2328"/></svg>`, 'Inertia'),
|
|
114
|
+
templateFromCommand(byId('wardley.addPorter'), `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><g stroke="${WARDLEY_RED}" stroke-width="2" stroke-linecap="round"><line x1="67" y1="26" x2="67" y2="19"/><line x1="81" y1="40" x2="88" y2="40"/><line x1="67" y1="54" x2="67" y2="61"/><line x1="53" y1="40" x2="46" y2="40"/></g><g fill="${WARDLEY_RED}"><path d="M67 13 L71 21 H63 Z"/><path d="M94 40 L86 44 V36 Z"/><path d="M67 67 L63 59 H71 Z"/><path d="M40 40 L48 36 V44 Z"/></g></svg>`),
|
|
115
|
+
templateFromCommand(byId('wardley.addAccelerator'), arrowPreview(true)),
|
|
116
|
+
templateFromCommand(byId('wardley.addDecelerator'), arrowPreview(false)),
|
|
219
117
|
// NEUTRAL on purpose (`docs/adr/0010` § Compatibility). This is a horizontal
|
|
220
118
|
// stroke bound to nothing — a sample of a STYLE, in a palette. It was typed
|
|
221
119
|
// `wardley:dependency` only because the helper defaults to that role, and a
|
|
222
120
|
// sample of a stroke makes no claim about who depends on whom. Same call the
|
|
223
121
|
// market glyph's own wiring already makes.
|
|
224
|
-
|
|
225
|
-
|
|
122
|
+
//
|
|
123
|
+
// Hand-written, and the one pair that cannot derive: `linkTool` and
|
|
124
|
+
// `evolutionArrow` ACTIVATE a tool rather than draw anything, so there is no
|
|
125
|
+
// artefact to record — the user draws it.
|
|
126
|
+
tpl('Link', `<svg ${ATTRS} fill="none"><path d="M24 40 H110" stroke="#666" stroke-width="2.4"/></svg>`, {
|
|
127
|
+
a: connect({ position: [0, 0] }, { position: [160, 0] }, { typed: false }),
|
|
128
|
+
}),
|
|
129
|
+
tpl('Evolution arrow', `<svg ${ATTRS} fill="none"><path d="M24 40 H100" stroke="#d6455d" stroke-width="2.4" stroke-dasharray="6 4"/><path d="M98 33 L112 40 L98 47 Z" fill="#d6455d"/></svg>`, {
|
|
130
|
+
a: connect({ position: [0, 0] }, { position: [160, 0] }, { evolution: true, typed: false }),
|
|
131
|
+
}),
|
|
132
|
+
areaTemplate('wardley.addAreaRect', areaPreview('<rect x="24" y="16" width="87" height="48" rx="2" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5"/>')),
|
|
133
|
+
areaTemplate('wardley.addAreaPolygon', areaPreview('<path d="M67 12 L110 43 L94 68 H40 L24 43 Z" fill="#c6dbfc" fill-opacity="0.25" stroke="#5b9cf6" stroke-width="1.5" stroke-linejoin="round"/>')),
|
|
226
134
|
],
|
|
227
135
|
};
|
package/dist/templates/maps.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { makeTemplateSnapshot, surfaceText, } from '@formicoidea/labre-core/gfx/template';
|
|
1
|
+
import { makeTemplateSnapshot, surfaceText, surfaceYMap, } from '@formicoidea/labre-core/gfx/template';
|
|
2
2
|
import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
|
|
3
|
-
import {
|
|
3
|
+
import { LINK_GREY, LINK_STROKE_WIDTH, NODE_FILL, NODE_SIZE, NODE_STROKE, WARDLEY_RED, } from '../node/consts.js';
|
|
4
4
|
import { backgroundPlot, backgroundZoneBoundaries, } from '@formicoidea/labre-core/blocks/surface';
|
|
5
5
|
import { WARDLEY_BACKGROUND } from '../background.js';
|
|
6
|
+
import { WARDLEY_LABEL_H, wardleyInertiaProps, wardleyLabelProps, wardleyNodeProps, } from '../presets.js';
|
|
6
7
|
import { WARDLEY_ROLE } from '../roles.js';
|
|
7
8
|
/**
|
|
8
9
|
* Authoring kit for canonical Wardley maps. Positions are given as
|
|
@@ -68,71 +69,73 @@ const bg = (variant = 'classic') => ({
|
|
|
68
69
|
variant,
|
|
69
70
|
xywh: `[0,0,${W},${H}]`,
|
|
70
71
|
});
|
|
72
|
+
/**
|
|
73
|
+
* A component of a shipped map, at (evolution, value).
|
|
74
|
+
*
|
|
75
|
+
* {@link wardleyNodeProps} says what a component IS; the two overrides are
|
|
76
|
+
* NOTATION and stay: a thick rim marks a user "need", and a red one marks the
|
|
77
|
+
* future position of a capability that has not moved yet.
|
|
78
|
+
*/
|
|
71
79
|
function dot(e, v, sw, stroke = NODE_STROKE, fill = NODE_FILL) {
|
|
72
80
|
const cx = ex(e);
|
|
73
81
|
const cy = vy(v);
|
|
74
82
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// from a preset validates exactly like a hand-drawn one.
|
|
79
|
-
role: WARDLEY_ROLE.component,
|
|
80
|
-
shapeType: 'ellipse',
|
|
81
|
-
filled: true,
|
|
83
|
+
...wardleyNodeProps('component', {
|
|
84
|
+
xywh: `[${cx - D / 2},${cy - D / 2},${D},${D}]`,
|
|
85
|
+
}),
|
|
82
86
|
fillColor: fill,
|
|
83
87
|
strokeColor: stroke,
|
|
84
88
|
strokeWidth: sw,
|
|
85
|
-
shapeStyle: ShapeStyle.General,
|
|
86
|
-
roughness: 0,
|
|
87
|
-
xywh: `[${cx - D / 2},${cy - D / 2},${D},${D}]`,
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
const comp = (e, v) => dot(e, v, 1);
|
|
91
92
|
const future = (e, v) => dot(e, v, 2, WARDLEY_RED);
|
|
93
|
+
/** A stakeholder: an anchor, drawn a third bigger than a component. */
|
|
92
94
|
function stake(e, v) {
|
|
93
95
|
const cx = ex(e);
|
|
94
96
|
const cy = vy(v);
|
|
95
97
|
const d = 24;
|
|
96
|
-
return {
|
|
97
|
-
type: 'wardleyNode',
|
|
98
|
-
kind: 'anchor',
|
|
99
|
-
role: WARDLEY_ROLE.anchor,
|
|
100
|
-
shapeType: 'ellipse',
|
|
101
|
-
filled: true,
|
|
102
|
-
fillColor: NODE_FILL,
|
|
103
|
-
strokeColor: NODE_STROKE,
|
|
104
|
-
strokeWidth: 1,
|
|
105
|
-
shapeStyle: ShapeStyle.General,
|
|
106
|
-
roughness: 0,
|
|
98
|
+
return wardleyNodeProps('anchor', {
|
|
107
99
|
xywh: `[${cx - d / 2},${cy - d / 2},${d},${d}]`,
|
|
108
|
-
};
|
|
100
|
+
});
|
|
109
101
|
}
|
|
110
102
|
function lbl(e, v, text, o = {}) {
|
|
111
103
|
const cx = ex(e);
|
|
112
104
|
const cy = vy(v);
|
|
105
|
+
// 200 rather than the toolbox's `WARDLEY_LABEL_W`: a shipped map names
|
|
106
|
+
// things like "Capture a moment", and a box narrower than its words wraps
|
|
107
|
+
// them onto two lines. The width is layout, not notation — what the preset
|
|
108
|
+
// owns is everything else about the label.
|
|
113
109
|
const w = o.w ?? 200;
|
|
114
110
|
const dx = o.dx ?? 12;
|
|
115
111
|
const dy = o.dy ?? -10;
|
|
116
112
|
const align = o.align ?? 'left';
|
|
117
113
|
const x = align === 'right' ? cx - w - dx : align === 'center' ? cx - w / 2 : cx + dx;
|
|
118
114
|
return {
|
|
119
|
-
|
|
120
|
-
text: surfaceText(text),
|
|
121
|
-
// The NAME of an artefact, so it carries the label role W3 is written on.
|
|
115
|
+
// The NAME of an artefact — {@link wardleyLabelProps} says what that is.
|
|
122
116
|
// The free texts these presets also use for notes and legends stay neutral:
|
|
123
117
|
// they name nothing and nothing measures them.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
...wardleyLabelProps(text, x, cy + dy, align),
|
|
119
|
+
// A snapshot stores the words as a serialized `Y.Text`, where
|
|
120
|
+
// `addElement` takes a plain string.
|
|
121
|
+
text: surfaceText(text),
|
|
122
|
+
// A wider box for a long name; the deliberate departures a shipped map
|
|
123
|
+
// makes on top of the preset.
|
|
124
|
+
xywh: `[${x},${cy + dy},${w},${WARDLEY_LABEL_H}]`,
|
|
125
|
+
...(o.size === undefined ? {} : { fontSize: o.size }),
|
|
126
|
+
...(o.color === undefined ? {} : { color: o.color }),
|
|
127
|
+
...(o.neutral ? { role: undefined } : {}),
|
|
134
128
|
};
|
|
135
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* The group a node and its name travel as — what the toolbox has written since
|
|
132
|
+
* #51 and what these maps had never carried, so dragging a component out of a
|
|
133
|
+
* shipped map left its name behind.
|
|
134
|
+
*/
|
|
135
|
+
const pair = (node, label) => ({
|
|
136
|
+
type: 'group',
|
|
137
|
+
children: surfaceYMap({ [node]: true, [label]: true }),
|
|
138
|
+
});
|
|
136
139
|
/**
|
|
137
140
|
* A link between two nodes of a shipped map.
|
|
138
141
|
*
|
|
@@ -172,19 +175,7 @@ function link(a, b, o = {}) {
|
|
|
172
175
|
function inertia(e, v) {
|
|
173
176
|
const cx = ex(e);
|
|
174
177
|
const cy = vy(v);
|
|
175
|
-
return {
|
|
176
|
-
type: 'shape',
|
|
177
|
-
shapeType: 'rect',
|
|
178
|
-
role: WARDLEY_ROLE.inertia,
|
|
179
|
-
filled: true,
|
|
180
|
-
fillColor: INERTIA_COLOR,
|
|
181
|
-
strokeColor: INERTIA_COLOR,
|
|
182
|
-
strokeWidth: 0,
|
|
183
|
-
shapeStyle: ShapeStyle.General,
|
|
184
|
-
roughness: 0,
|
|
185
|
-
radius: 0,
|
|
186
|
-
xywh: `[${cx - 4},${cy - 22},8,44]`,
|
|
187
|
-
};
|
|
178
|
+
return wardleyInertiaProps({ xywh: `[${cx - 4},${cy - 22},8,44]` });
|
|
188
179
|
}
|
|
189
180
|
function panel(x, y, w, h) {
|
|
190
181
|
return {
|
|
@@ -274,34 +265,49 @@ function teaShop() {
|
|
|
274
265
|
dy: -28,
|
|
275
266
|
w: 120,
|
|
276
267
|
}),
|
|
268
|
+
businessG: pair('business', 'businessL'),
|
|
277
269
|
public: stake(0.78, 0.93),
|
|
278
270
|
publicL: lbl(0.78, 0.93, 'Public', { align: 'center', dy: -28, w: 120 }),
|
|
271
|
+
publicG: pair('public', 'publicL'),
|
|
279
272
|
cupOfTea: comp(0.62, 0.74),
|
|
280
273
|
cupOfTeaL: lbl(0.62, 0.74, 'Cup of Tea', { align: 'right' }),
|
|
274
|
+
cupOfTeaG: pair('cupOfTea', 'cupOfTeaL'),
|
|
281
275
|
cup: comp(0.8, 0.7),
|
|
282
276
|
cupL: lbl(0.8, 0.7, 'Cup'),
|
|
277
|
+
cupG: pair('cup', 'cupL'),
|
|
283
278
|
tea: comp(0.83, 0.6),
|
|
284
279
|
teaL: lbl(0.83, 0.6, 'Tea'),
|
|
280
|
+
teaG: pair('tea', 'teaL'),
|
|
285
281
|
hotWater: comp(0.8, 0.47),
|
|
286
282
|
hotWaterL: lbl(0.8, 0.47, 'Hot Water'),
|
|
283
|
+
hotWaterG: pair('hotWater', 'hotWaterL'),
|
|
287
284
|
water: comp(0.81, 0.34),
|
|
288
285
|
waterL: lbl(0.81, 0.34, 'Water'),
|
|
286
|
+
waterG: pair('water', 'waterL'),
|
|
289
287
|
kettle: comp(0.36, 0.38),
|
|
290
288
|
kettleL: lbl(0.36, 0.38, 'Kettle', { align: 'right', dy: 6 }),
|
|
289
|
+
kettleG: pair('kettle', 'kettleL'),
|
|
291
290
|
electric: future(0.56, 0.38),
|
|
292
291
|
electricL: lbl(0.56, 0.38, 'Electric Kettle'),
|
|
292
|
+
electricG: pair('electric', 'electricL'),
|
|
293
293
|
power: comp(0.7, 0.1),
|
|
294
294
|
powerL: lbl(0.7, 0.1, 'Power', { align: 'right', dy: 6 }),
|
|
295
|
+
powerG: pair('power', 'powerL'),
|
|
295
296
|
powerFut: future(0.88, 0.1),
|
|
296
297
|
powerFutL: lbl(0.88, 0.1, 'Power'),
|
|
298
|
+
powerFutG: pair('powerFut', 'powerFutL'),
|
|
297
299
|
// ABOVE the link it annotates, not across it. Written on the line it reads
|
|
298
300
|
// as a label nobody can read — which is the finding W3 raises, and it was
|
|
299
301
|
// raising it on the map that ships as the canonical example.
|
|
302
|
+
//
|
|
303
|
+
// NEUTRAL: it is a remark about a LINK, not the name of an artefact, so it
|
|
304
|
+
// travels with nothing and nothing measures it (see `LblOpts.neutral`).
|
|
300
305
|
limitedBy: lbl(0.56, 0.43, 'limited by', {
|
|
301
306
|
align: 'center',
|
|
302
307
|
w: 120,
|
|
303
308
|
size: 13,
|
|
304
309
|
dy: -34,
|
|
310
|
+
neutral: true,
|
|
305
311
|
}),
|
|
306
312
|
ann1a: ann(0.5, 0.385),
|
|
307
313
|
ann1t: annTxt(0.5, 0.385, '1'),
|
|
@@ -337,6 +343,7 @@ function kodak() {
|
|
|
337
343
|
title: title("Wardley map of Kodak's 2005 inertia to digital"),
|
|
338
344
|
user: stake(0.54, 0.92),
|
|
339
345
|
userL: lbl(0.54, 0.92, 'User'),
|
|
346
|
+
userG: pair('user', 'userL'),
|
|
340
347
|
capture: dot(CAPTURE[0], CAPTURE[1], 3),
|
|
341
348
|
// To the LEFT, like the other two capability names: to the right of this
|
|
342
349
|
// node runs the future dependency towards digital storage, and a name
|
|
@@ -345,16 +352,21 @@ function kodak() {
|
|
|
345
352
|
captureL: lbl(CAPTURE[0], CAPTURE[1], 'Capture a moment', {
|
|
346
353
|
align: 'right',
|
|
347
354
|
}),
|
|
355
|
+
captureG: pair('capture', 'captureL'),
|
|
348
356
|
film: comp(0.52, 0.62),
|
|
349
357
|
filmL: lbl(0.52, 0.62, 'Film camera', { align: 'right' }),
|
|
358
|
+
filmG: pair('film', 'filmL'),
|
|
350
359
|
digital: future(0.74, 0.62),
|
|
351
360
|
digitalL: lbl(0.74, 0.62, 'Digital camera', { color: WARDLEY_RED }),
|
|
361
|
+
digitalG: pair('digital', 'digitalL'),
|
|
352
362
|
roll: comp(0.52, 0.4),
|
|
353
363
|
rollL: lbl(0.52, 0.4, 'Photographic film', { align: 'right' }),
|
|
364
|
+
rollG: pair('roll', 'rollL'),
|
|
354
365
|
storage: future(STORAGE[0], STORAGE[1]),
|
|
355
366
|
storageL: lbl(STORAGE[0], STORAGE[1], 'Digital storage', {
|
|
356
367
|
color: WARDLEY_RED,
|
|
357
368
|
}),
|
|
369
|
+
storageG: pair('storage', 'storageL'),
|
|
358
370
|
inertiaBar: inertia(barE, barV),
|
|
359
371
|
l1: link('user', 'capture'),
|
|
360
372
|
l2: link('capture', 'film'),
|
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.38.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"author": "lajola",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@formicoidea/labre-core": "0.
|
|
34
|
+
"@formicoidea/labre-core": "0.38.1",
|
|
35
35
|
"lit": "^3.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|