@formicoidea/labre-framework-wardley 0.23.2 → 0.24.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/consts.d.ts +71 -0
- package/{src/consts.ts → dist/consts.js} +62 -72
- package/dist/descriptor.d.ts +7 -0
- package/{src/descriptor.ts → dist/descriptor.js} +4 -5
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +14 -0
- package/dist/element-renderer.js +159 -0
- package/dist/element-view.d.ts +20 -0
- package/dist/element-view.js +121 -0
- package/dist/gradient.d.ts +17 -0
- package/dist/gradient.js +111 -0
- package/{src/index.ts → dist/index.d.ts} +1 -1
- package/dist/index.js +1 -0
- package/dist/label-layout.d.ts +20 -0
- package/dist/label-layout.js +72 -0
- package/dist/legend.d.ts +11 -0
- package/dist/legend.js +332 -0
- package/dist/node/consts.d.ts +106 -0
- package/{src/node/consts.ts → dist/node/consts.js} +100 -109
- package/dist/node/node-renderer.d.ts +16 -0
- package/dist/node/node-renderer.js +105 -0
- package/{src/node/node-view.ts → dist/node/node-view.d.ts} +10 -11
- package/dist/node/node-view.js +9 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +171 -0
- package/dist/templates/maps.d.ts +2 -0
- package/dist/templates/maps.js +246 -0
- package/dist/toolbar/config.d.ts +74 -0
- package/dist/toolbar/config.js +205 -0
- package/dist/toolbar/icons.d.ts +30 -0
- package/{src/toolbar/icons.ts → dist/toolbar/icons.js} +50 -66
- package/dist/toolbar/node-config.d.ts +1 -0
- package/{src/toolbar/node-config.ts → dist/toolbar/node-config.js} +20 -28
- package/dist/toolbar/senior-tool.d.ts +1 -0
- package/{src/toolbar/senior-tool.ts → dist/toolbar/senior-tool.js} +9 -10
- package/dist/toolbar/wardley-menu.d.ts +52 -0
- package/dist/toolbar/wardley-menu.js +407 -0
- package/dist/toolbar/wardley-senior-button.d.ts +17 -0
- package/dist/toolbar/wardley-senior-button.js +113 -0
- package/dist/view.d.ts +6 -0
- package/dist/view.js +35 -0
- package/package.json +15 -6
- package/src/effects.ts +0 -17
- package/src/element-renderer.ts +0 -242
- package/src/element-view.ts +0 -143
- package/src/gradient.ts +0 -137
- package/src/label-layout.ts +0 -126
- package/src/legend.ts +0 -438
- package/src/node/node-renderer.ts +0 -142
- package/src/templates/index.ts +0 -236
- package/src/templates/maps.ts +0 -283
- package/src/toolbar/config.ts +0 -280
- package/src/toolbar/wardley-menu.ts +0 -552
- package/src/toolbar/wardley-senior-button.ts +0 -154
- package/src/view.ts +0 -39
|
@@ -1,109 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Visual constants for Wardley nodes (component + anchor) and the connector /
|
|
3
|
-
* inertia presets created from the Wardley menu.
|
|
4
|
-
*
|
|
5
|
-
* The node is a NATIVE ellipse (ShapeElementModel-derived) so its stroke
|
|
6
|
-
* width / colors are editable via the shape toolbar; these are just the
|
|
7
|
-
* pre-formatted defaults at creation. The person glyph (anchor) is expressed as
|
|
8
|
-
* ratios of the circle radius R so it stays proportional at any size.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export const
|
|
65
|
-
export const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*/
|
|
71
|
-
export const
|
|
72
|
-
export const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
export const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* outer circle (colored fill + border, the base ellipse) + a white component
|
|
102
|
-
* circle drawn at its center. Default fill is a neutral grey.
|
|
103
|
-
*/
|
|
104
|
-
export const METHOD_SIZE = 35;
|
|
105
|
-
export const METHOD_FILL = '#d9d9d9';
|
|
106
|
-
export const METHOD = {
|
|
107
|
-
centerRatio: 0.5, // inner white component radius / R
|
|
108
|
-
};
|
|
109
|
-
export const METHOD_LABEL = 'Component';
|
|
1
|
+
/**
|
|
2
|
+
* Visual constants for Wardley nodes (component + anchor) and the connector /
|
|
3
|
+
* inertia presets created from the Wardley menu.
|
|
4
|
+
*
|
|
5
|
+
* The node is a NATIVE ellipse (ShapeElementModel-derived) so its stroke
|
|
6
|
+
* width / colors are editable via the shape toolbar; these are just the
|
|
7
|
+
* pre-formatted defaults at creation. The person glyph (anchor) is expressed as
|
|
8
|
+
* ratios of the circle radius R so it stays proportional at any size.
|
|
9
|
+
*/
|
|
10
|
+
/** Default node diameter (= the map label font size, 18). White fill, thin border. */
|
|
11
|
+
export const NODE_SIZE = 18;
|
|
12
|
+
export const NODE_FILL = '#ffffff';
|
|
13
|
+
export const NODE_STROKE = '#1f2328';
|
|
14
|
+
/** Thin border, matching the link / silhouette line weight. */
|
|
15
|
+
export const NODE_STROKE_WIDTH = 1;
|
|
16
|
+
/**
|
|
17
|
+
* Person glyph (`kind: 'anchor'`) ratios of R, mirroring the validated icon
|
|
18
|
+
* ANCH-B (circle r6 → head r1.8 at cy-2.6 ; shoulders cubic touching the
|
|
19
|
+
* border at ±(4.6, 3.9) with controls at ±(3.8, 0.1)).
|
|
20
|
+
*/
|
|
21
|
+
export const ANCHOR = {
|
|
22
|
+
headR: 0.3,
|
|
23
|
+
headCY: -0.433,
|
|
24
|
+
shoulderEndX: 0.767,
|
|
25
|
+
shoulderEndY: 0.65,
|
|
26
|
+
shoulderCtrlX: 0.633,
|
|
27
|
+
shoulderCtrlY: 0.017,
|
|
28
|
+
};
|
|
29
|
+
/** Native text label, same family as the axis labels, size 18. */
|
|
30
|
+
export const LABEL_FONT_SIZE = 18;
|
|
31
|
+
export const LABEL_GAP = 8;
|
|
32
|
+
export const LABEL_DEFAULT = { component: 'Component', anchor: 'Anchor' };
|
|
33
|
+
/**
|
|
34
|
+
* Pipeline defaults. The body is a wide, thin native rect (≈ 1.4× the node
|
|
35
|
+
* diameter tall) with a white slightly-transparent fill and a node-weight
|
|
36
|
+
* border. The handle is a node-sized square straddling the top edge — the only
|
|
37
|
+
* connection point (center anchor). Both reuse the WardleyNode (rect) so they
|
|
38
|
+
* stay native and editable; the body is made non-connectable in the model.
|
|
39
|
+
*/
|
|
40
|
+
export const PIPELINE_HEIGHT = Math.round(NODE_SIZE * 1.4); // 25
|
|
41
|
+
export const PIPELINE_WIDTH = 120;
|
|
42
|
+
/** White ~60% opacity — fill only; the 1px border stays opaque. */
|
|
43
|
+
export const PIPELINE_FILL = '#ffffff99';
|
|
44
|
+
/** Handle square = node diameter. */
|
|
45
|
+
export const HANDLE_SIZE = NODE_SIZE;
|
|
46
|
+
export const PIPELINE_LABEL = 'Pipeline';
|
|
47
|
+
/**
|
|
48
|
+
* Connector line width. Connectors are constrained to the LineWidth enum
|
|
49
|
+
* {2,4,6,8,10,12}, so the thinnest available (2) is used — as close as possible
|
|
50
|
+
* to the 1px node border.
|
|
51
|
+
*/
|
|
52
|
+
export const LINK_STROKE_WIDTH = 2;
|
|
53
|
+
/** Wardley red ("future"/evolution) — matches the validated arrow icon. */
|
|
54
|
+
export const WARDLEY_RED = '#d6455d';
|
|
55
|
+
/** Dependency link grey. */
|
|
56
|
+
export const LINK_GREY = '#666666';
|
|
57
|
+
/** Inertia bar color + size. */
|
|
58
|
+
export const INERTIA_COLOR = '#1f2328';
|
|
59
|
+
export const INERTIA_SIZE = { w: 8, h: 44 };
|
|
60
|
+
/**
|
|
61
|
+
* Market node (composite): a large thin-bordered circle containing 3 small
|
|
62
|
+
* thick-bordered component nodes wired into a triangle by native connectors.
|
|
63
|
+
*/
|
|
64
|
+
export const MARKET_SIZE = 30;
|
|
65
|
+
export const MARKET_DOT_SIZE = 8;
|
|
66
|
+
/** Radius of the circle on which the 3 inner node centers sit. */
|
|
67
|
+
export const MARKET_DOT_RING = 8;
|
|
68
|
+
/** Inner nodes have a thicker border than the outer circle. */
|
|
69
|
+
export const MARKET_DOT_STROKE_WIDTH = 2;
|
|
70
|
+
/** Triangle connectors: thin + dark, no arrows. */
|
|
71
|
+
export const MARKET_LINK_WIDTH = 1;
|
|
72
|
+
export const MARKET_LINK_COLOR = NODE_STROKE;
|
|
73
|
+
export const MARKET_LABEL = 'Market';
|
|
74
|
+
/**
|
|
75
|
+
* Ecosystem node: a single connectable circle drawn as a GLYPH — a double border
|
|
76
|
+
* at the rim (outer circle + a 2nd inscribed circle with a thin blank band
|
|
77
|
+
* between them) and diagonal hatching confined to the inner donut (from the 2nd
|
|
78
|
+
* border down to a hollow central circle; the hatch never reaches the outer
|
|
79
|
+
* border). Ratios are of R so the glyph scales with the circle.
|
|
80
|
+
*/
|
|
81
|
+
export const ECOSYSTEM_SIZE = 40;
|
|
82
|
+
export const ECOSYSTEM = {
|
|
83
|
+
secondBorderRatio: 0.88, // 2nd (inscribed) border
|
|
84
|
+
centerRatio: 0.43, // central hollow circle
|
|
85
|
+
hatchOuterRatio: 0.86, // hatch stays just inside the 2nd border
|
|
86
|
+
hatchSpacingRatio: 0.15, // gap between hatch lines
|
|
87
|
+
};
|
|
88
|
+
export const ECOSYSTEM_LABEL = 'Ecosystem';
|
|
89
|
+
/**
|
|
90
|
+
* Method node: a component inscribed in a slightly larger outer circle whose
|
|
91
|
+
* FILL color encodes the chosen method (editable via the toolbar). Glyph = the
|
|
92
|
+
* outer circle (colored fill + border, the base ellipse) + a white component
|
|
93
|
+
* circle drawn at its center. Default fill is a neutral grey.
|
|
94
|
+
*/
|
|
95
|
+
export const METHOD_SIZE = 35;
|
|
96
|
+
export const METHOD_FILL = '#d9d9d9';
|
|
97
|
+
export const METHOD = {
|
|
98
|
+
centerRatio: 0.5, // inner white component radius / R
|
|
99
|
+
};
|
|
100
|
+
export const METHOD_LABEL = 'Component';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ElementRenderer } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { type WardleyNodeElementModel } from '@formicoidea/labre-core/model';
|
|
3
|
+
/**
|
|
4
|
+
* Renderer for a Wardley node. The circle is drawn by REUSING the native shape
|
|
5
|
+
* renderer (so stroke width, colors and theme behave exactly like a native
|
|
6
|
+
* ellipse). On top of it, a glyph is drawn for two kinds:
|
|
7
|
+
* - `anchor` → an inscribed person (head + shoulders), clipped to the circle.
|
|
8
|
+
* - `ecosystem` → a double border at the rim + diagonal hatching confined to the
|
|
9
|
+
* inner donut + a hollow central circle.
|
|
10
|
+
* All glyph strokes use the model's (editable) stroke color; the white band and
|
|
11
|
+
* the hollow center come from the base fill, so colors stay editable.
|
|
12
|
+
*/
|
|
13
|
+
export declare const wardleyNode: ElementRenderer<WardleyNodeElementModel>;
|
|
14
|
+
export declare const WardleyNodeRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
|
|
15
|
+
identifier: import("@formicoidea/labre-core/_pkgs/global/di").ServiceIdentifier<ElementRenderer<WardleyNodeElementModel>>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
|
|
3
|
+
import { DefaultTheme } from '@formicoidea/labre-core/model';
|
|
4
|
+
import { ANCHOR, ECOSYSTEM, METHOD, NODE_FILL } from './consts';
|
|
5
|
+
/**
|
|
6
|
+
* Renderer for a Wardley node. The circle is drawn by REUSING the native shape
|
|
7
|
+
* renderer (so stroke width, colors and theme behave exactly like a native
|
|
8
|
+
* ellipse). On top of it, a glyph is drawn for two kinds:
|
|
9
|
+
* - `anchor` → an inscribed person (head + shoulders), clipped to the circle.
|
|
10
|
+
* - `ecosystem` → a double border at the rim + diagonal hatching confined to the
|
|
11
|
+
* inner donut + a hollow central circle.
|
|
12
|
+
* All glyph strokes use the model's (editable) stroke color; the white band and
|
|
13
|
+
* the hollow center come from the base fill, so colors stay editable.
|
|
14
|
+
*/
|
|
15
|
+
export const wardleyNode = (model, ctx, matrix, renderer, rc, bound) => {
|
|
16
|
+
const [, , w, h] = model.deserializedXYWH;
|
|
17
|
+
const cx = w / 2;
|
|
18
|
+
const cy = h / 2;
|
|
19
|
+
// Capture the element-local transform BEFORE the shape renderer mutates the
|
|
20
|
+
// matrix, so the glyph can be drawn in the same space afterwards.
|
|
21
|
+
const glyphMatrix = DOMMatrix.fromMatrix(matrix)
|
|
22
|
+
.translateSelf(cx, cy)
|
|
23
|
+
.rotateSelf(model.rotate)
|
|
24
|
+
.translateSelf(-cx, -cy);
|
|
25
|
+
// Native ellipse (fill / stroke / theme handled natively).
|
|
26
|
+
shapeRenderer(model, ctx, matrix, renderer, rc, bound);
|
|
27
|
+
if (model.kind !== 'anchor' &&
|
|
28
|
+
model.kind !== 'ecosystem' &&
|
|
29
|
+
model.kind !== 'method')
|
|
30
|
+
return;
|
|
31
|
+
const strokeWidth = model.strokeWidth || 1;
|
|
32
|
+
const R = Math.min(w, h) / 2 - strokeWidth / 2;
|
|
33
|
+
const color = renderer.getColorValue(model.strokeColor, DefaultTheme.shapeStrokeColor, true);
|
|
34
|
+
ctx.setTransform(glyphMatrix);
|
|
35
|
+
// ── Anchor: person glyph (clipped to the circle) ────────────────────
|
|
36
|
+
if (model.kind === 'anchor') {
|
|
37
|
+
ctx.save();
|
|
38
|
+
ctx.beginPath();
|
|
39
|
+
ctx.arc(cx, cy, R - strokeWidth / 2, 0, Math.PI * 2);
|
|
40
|
+
ctx.clip();
|
|
41
|
+
ctx.lineWidth = strokeWidth;
|
|
42
|
+
ctx.strokeStyle = color;
|
|
43
|
+
ctx.lineCap = 'round';
|
|
44
|
+
ctx.lineJoin = 'round';
|
|
45
|
+
// head
|
|
46
|
+
ctx.beginPath();
|
|
47
|
+
ctx.arc(cx, cy + ANCHOR.headCY * R, ANCHOR.headR * R, 0, Math.PI * 2);
|
|
48
|
+
ctx.stroke();
|
|
49
|
+
// rounded shoulders (extremities sit on the circle border)
|
|
50
|
+
const sx = ANCHOR.shoulderEndX * R;
|
|
51
|
+
const sy = ANCHOR.shoulderEndY * R;
|
|
52
|
+
const kx = ANCHOR.shoulderCtrlX * R;
|
|
53
|
+
const ky = ANCHOR.shoulderCtrlY * R;
|
|
54
|
+
ctx.beginPath();
|
|
55
|
+
ctx.moveTo(cx - sx, cy + sy);
|
|
56
|
+
ctx.bezierCurveTo(cx - kx, cy + ky, cx + kx, cy + ky, cx + sx, cy + sy);
|
|
57
|
+
ctx.stroke();
|
|
58
|
+
ctx.restore();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// ── Method: a white component inscribed in the colored outer circle ──
|
|
62
|
+
if (model.kind === 'method') {
|
|
63
|
+
const rInner = R * METHOD.centerRatio;
|
|
64
|
+
ctx.fillStyle = NODE_FILL;
|
|
65
|
+
ctx.lineWidth = strokeWidth;
|
|
66
|
+
ctx.strokeStyle = color;
|
|
67
|
+
ctx.beginPath();
|
|
68
|
+
ctx.arc(cx, cy, rInner, 0, Math.PI * 2);
|
|
69
|
+
ctx.fill();
|
|
70
|
+
ctx.stroke();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// ── Ecosystem: double border + hatched inner donut + hollow center ──
|
|
74
|
+
const rBorder2 = R * ECOSYSTEM.secondBorderRatio;
|
|
75
|
+
const rCenter = R * ECOSYSTEM.centerRatio;
|
|
76
|
+
const rHatch = R * ECOSYSTEM.hatchOuterRatio;
|
|
77
|
+
// Hatch confined to the donut [rCenter, rHatch] (even-odd clip = annulus).
|
|
78
|
+
ctx.save();
|
|
79
|
+
ctx.beginPath();
|
|
80
|
+
ctx.arc(cx, cy, rHatch, 0, Math.PI * 2);
|
|
81
|
+
ctx.moveTo(cx + rCenter, cy);
|
|
82
|
+
ctx.arc(cx, cy, rCenter, 0, Math.PI * 2);
|
|
83
|
+
ctx.clip('evenodd');
|
|
84
|
+
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.6);
|
|
85
|
+
ctx.strokeStyle = color;
|
|
86
|
+
const step = R * ECOSYSTEM.hatchSpacingRatio;
|
|
87
|
+
for (let d = -2 * R; d <= 2 * R; d += step) {
|
|
88
|
+
ctx.beginPath();
|
|
89
|
+
ctx.moveTo(cx - R, cy - R + d);
|
|
90
|
+
ctx.lineTo(cx + R, cy + R + d);
|
|
91
|
+
ctx.stroke();
|
|
92
|
+
}
|
|
93
|
+
ctx.restore();
|
|
94
|
+
// Double border (2nd inscribed circle) + central hole border. No fill: the
|
|
95
|
+
// white band and hollow center come from the base ellipse fill.
|
|
96
|
+
ctx.lineWidth = strokeWidth;
|
|
97
|
+
ctx.strokeStyle = color;
|
|
98
|
+
ctx.beginPath();
|
|
99
|
+
ctx.arc(cx, cy, rBorder2, 0, Math.PI * 2);
|
|
100
|
+
ctx.stroke();
|
|
101
|
+
ctx.beginPath();
|
|
102
|
+
ctx.arc(cx, cy, rCenter, 0, Math.PI * 2);
|
|
103
|
+
ctx.stroke();
|
|
104
|
+
};
|
|
105
|
+
export const WardleyNodeRendererExtension = ElementRendererExtension('wardleyNode', wardleyNode);
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { WardleyNodeElementModel } from '@formicoidea/labre-core/model';
|
|
2
|
-
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
1
|
+
import type { WardleyNodeElementModel } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
3
|
+
/**
|
|
4
|
+
* View for a Wardley node. Registering it ensures `gfx.view.get(model)` returns
|
|
5
|
+
* a view (required so move / select / connector interactions work). The label
|
|
6
|
+
* is a separate native text element, so no custom editing is needed here.
|
|
7
|
+
*/
|
|
8
|
+
export declare class WardleyNodeView extends GfxElementModelView<WardleyNodeElementModel> {
|
|
9
|
+
static type: string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
2
|
+
/**
|
|
3
|
+
* View for a Wardley node. Registering it ensures `gfx.view.get(model)` returns
|
|
4
|
+
* a view (required so move / select / connector interactions work). The label
|
|
5
|
+
* is a separate native text element, so no custom editing is needed here.
|
|
6
|
+
*/
|
|
7
|
+
export class WardleyNodeView extends GfxElementModelView {
|
|
8
|
+
static { this.type = 'wardleyNode'; }
|
|
9
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { makeTemplateSnapshot, surfaceText, } from '@formicoidea/labre-core/gfx/template';
|
|
2
|
+
import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { REF_WIDTH } from '../consts';
|
|
4
|
+
import { wardleyMaps } from './maps';
|
|
5
|
+
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';
|
|
6
|
+
const VARIANT_DEFAULTS = {
|
|
7
|
+
classic: {},
|
|
8
|
+
opportunity: {
|
|
9
|
+
yAxisTitle: 'Opportunity',
|
|
10
|
+
showVisibilityLabels: false,
|
|
11
|
+
showCornerLabels: false,
|
|
12
|
+
},
|
|
13
|
+
benefit: {
|
|
14
|
+
yAxisTitle: '',
|
|
15
|
+
visibilityHigh: 'Benefit',
|
|
16
|
+
visibilityLow: 'Investment',
|
|
17
|
+
showCornerLabels: false,
|
|
18
|
+
},
|
|
19
|
+
'evolution-gradient': {},
|
|
20
|
+
};
|
|
21
|
+
const bg = (variant, w = REF_WIDTH) => {
|
|
22
|
+
const h = Math.round((w * 9) / 16);
|
|
23
|
+
return { type: 'wardley', variant, ...VARIANT_DEFAULTS[variant], xywh: `[0,0,${w},${h}]` };
|
|
24
|
+
};
|
|
25
|
+
/** A wardley node ellipse positioned by top-left. */
|
|
26
|
+
function node(kind, x, y, d = NODE_SIZE, fill = NODE_FILL, strokeWidth = NODE_STROKE_WIDTH) {
|
|
27
|
+
return {
|
|
28
|
+
type: 'wardleyNode',
|
|
29
|
+
kind,
|
|
30
|
+
shapeType: 'ellipse',
|
|
31
|
+
filled: true,
|
|
32
|
+
fillColor: fill,
|
|
33
|
+
strokeColor: NODE_STROKE,
|
|
34
|
+
strokeWidth,
|
|
35
|
+
shapeStyle: ShapeStyle.General,
|
|
36
|
+
roughness: 0,
|
|
37
|
+
xywh: `[${x},${y},${d},${d}]`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function label(x, y, str, align = 'left') {
|
|
41
|
+
return {
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: surfaceText(str),
|
|
44
|
+
color: NODE_STROKE,
|
|
45
|
+
fontFamily: FontFamily.Inter,
|
|
46
|
+
fontSize: LABEL_FONT_SIZE,
|
|
47
|
+
textAlign: align === 'center' ? TextAlign.Center : TextAlign.Left,
|
|
48
|
+
xywh: `[${x},${y},140,26]`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function connect(source, target, opts = {}) {
|
|
52
|
+
return {
|
|
53
|
+
type: 'connector',
|
|
54
|
+
mode: ConnectorMode.Straight,
|
|
55
|
+
stroke: opts.red ? WARDLEY_RED : LINK_GREY,
|
|
56
|
+
strokeStyle: opts.red ? StrokeStyle.Dash : StrokeStyle.Solid,
|
|
57
|
+
strokeWidth: LINK_STROKE_WIDTH,
|
|
58
|
+
frontEndpointStyle: PointStyle.None,
|
|
59
|
+
rearEndpointStyle: opts.red ? PointStyle.Triangle : PointStyle.None,
|
|
60
|
+
source,
|
|
61
|
+
target,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const inertia = (x = 0, y = 0) => ({
|
|
65
|
+
type: 'shape',
|
|
66
|
+
shapeType: 'rect',
|
|
67
|
+
filled: true,
|
|
68
|
+
fillColor: INERTIA_COLOR,
|
|
69
|
+
strokeColor: INERTIA_COLOR,
|
|
70
|
+
strokeWidth: 0,
|
|
71
|
+
shapeStyle: ShapeStyle.General,
|
|
72
|
+
roughness: 0,
|
|
73
|
+
radius: 0,
|
|
74
|
+
xywh: `[${x},${y},${INERTIA_SIZE.w},${INERTIA_SIZE.h}]`,
|
|
75
|
+
});
|
|
76
|
+
/** Pipeline composite: body rect + handle square (straddling top) + label. */
|
|
77
|
+
function pipeline() {
|
|
78
|
+
return {
|
|
79
|
+
label: label(0, 0, 'Pipeline', 'center'),
|
|
80
|
+
body: {
|
|
81
|
+
type: 'wardleyNode',
|
|
82
|
+
kind: 'pipeline',
|
|
83
|
+
shapeType: 'rect',
|
|
84
|
+
filled: true,
|
|
85
|
+
fillColor: PIPELINE_FILL,
|
|
86
|
+
strokeColor: NODE_STROKE,
|
|
87
|
+
strokeWidth: NODE_STROKE_WIDTH,
|
|
88
|
+
shapeStyle: ShapeStyle.General,
|
|
89
|
+
roughness: 0,
|
|
90
|
+
radius: 0,
|
|
91
|
+
xywh: `[0,39,${PIPELINE_WIDTH},${PIPELINE_HEIGHT}]`,
|
|
92
|
+
},
|
|
93
|
+
handle: {
|
|
94
|
+
type: 'wardleyNode',
|
|
95
|
+
kind: 'handle',
|
|
96
|
+
shapeType: 'rect',
|
|
97
|
+
filled: true,
|
|
98
|
+
fillColor: NODE_FILL,
|
|
99
|
+
strokeColor: NODE_STROKE,
|
|
100
|
+
strokeWidth: NODE_STROKE_WIDTH,
|
|
101
|
+
shapeStyle: ShapeStyle.General,
|
|
102
|
+
roughness: 0,
|
|
103
|
+
radius: 0,
|
|
104
|
+
xywh: `[${PIPELINE_WIDTH / 2 - HANDLE_SIZE / 2},${39 - HANDLE_SIZE / 2},${HANDLE_SIZE},${HANDLE_SIZE}]`,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Market composite: outer circle + 3 inner dots wired in a triangle + label. */
|
|
109
|
+
function market() {
|
|
110
|
+
const R = MARKET_SIZE / 2;
|
|
111
|
+
const c = R; // center within the [0,0,MARKET_SIZE,MARKET_SIZE] box
|
|
112
|
+
const rho = MARKET_DOT_RING;
|
|
113
|
+
const sin60 = Math.sqrt(3) / 2;
|
|
114
|
+
const verts = [
|
|
115
|
+
[0, -rho],
|
|
116
|
+
[rho * sin60, rho / 2],
|
|
117
|
+
[-rho * sin60, rho / 2],
|
|
118
|
+
];
|
|
119
|
+
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);
|
|
120
|
+
const tri = (a, b) => ({
|
|
121
|
+
type: 'connector',
|
|
122
|
+
mode: ConnectorMode.Straight,
|
|
123
|
+
stroke: MARKET_LINK_COLOR,
|
|
124
|
+
strokeStyle: StrokeStyle.Solid,
|
|
125
|
+
strokeWidth: MARKET_LINK_WIDTH,
|
|
126
|
+
frontEndpointStyle: PointStyle.None,
|
|
127
|
+
rearEndpointStyle: PointStyle.None,
|
|
128
|
+
source: { id: a },
|
|
129
|
+
target: { id: b },
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
circle: node('market', 0, 0, MARKET_SIZE, NODE_FILL),
|
|
133
|
+
d0: dotAt(verts[0][0], verts[0][1]),
|
|
134
|
+
d1: dotAt(verts[1][0], verts[1][1]),
|
|
135
|
+
d2: dotAt(verts[2][0], verts[2][1]),
|
|
136
|
+
t0: tri('d0', 'd1'),
|
|
137
|
+
t1: tri('d1', 'd2'),
|
|
138
|
+
t2: tri('d2', 'd0'),
|
|
139
|
+
label: label(MARKET_SIZE + 8, 2, 'Market'),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const single = (el) => ({ a: el });
|
|
143
|
+
const nodeWithLabel = (kind, d, fill, name) => ({
|
|
144
|
+
n: node(kind, 0, 0, d, fill),
|
|
145
|
+
l: label(d + 8, d / 2 - 13, name),
|
|
146
|
+
});
|
|
147
|
+
const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
|
|
148
|
+
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>`;
|
|
149
|
+
const dotPreview = (fill, sw = 2) => `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="${fill}" stroke="#1f2328" stroke-width="${sw}"/></svg>`;
|
|
150
|
+
function tpl(name, preview, elements) {
|
|
151
|
+
return { name, type: 'template', preview, content: makeTemplateSnapshot(elements, name) };
|
|
152
|
+
}
|
|
153
|
+
export const wardleyTemplateCategory = {
|
|
154
|
+
name: 'Wardley',
|
|
155
|
+
templates: [
|
|
156
|
+
...wardleyMaps,
|
|
157
|
+
tpl('Map background', bgPreview(), { bg: bg('classic') }),
|
|
158
|
+
tpl('Opportunity gradient', bgPreview('<rect x="22" y="12" width="98" height="52" fill="#eef4fb" opacity="0.6"/>'), { bg: bg('opportunity') }),
|
|
159
|
+
tpl('Benefit gradient', bgPreview('<rect x="22" y="12" width="98" height="26" fill="#e6eef8" opacity="0.6"/>'), { bg: bg('benefit') }),
|
|
160
|
+
tpl('Evolution gradient', bgPreview('<rect x="22" y="12" width="98" height="52" fill="#e3e2e4" opacity="0.5"/>'), { bg: bg('evolution-gradient') }),
|
|
161
|
+
tpl('Component', dotPreview('#ffffff', 1.5), nodeWithLabel('component', NODE_SIZE, NODE_FILL, 'Component')),
|
|
162
|
+
tpl('Anchor', `<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>`, nodeWithLabel('anchor', NODE_SIZE, NODE_FILL, 'Anchor')),
|
|
163
|
+
tpl('Ecosystem', `<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>`, nodeWithLabel('ecosystem', ECOSYSTEM_SIZE, NODE_FILL, 'Ecosystem')),
|
|
164
|
+
tpl('Method', `<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>`, nodeWithLabel('method', METHOD_SIZE, METHOD_FILL, 'Component')),
|
|
165
|
+
tpl('Pipeline', `<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()),
|
|
166
|
+
tpl('Market', `<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()),
|
|
167
|
+
tpl('Inertia', `<svg ${ATTRS} fill="none"><rect x="63" y="22" width="8" height="36" fill="#1f2328"/></svg>`, single(inertia())),
|
|
168
|
+
tpl('Link', `<svg ${ATTRS} fill="none"><path d="M24 40 H110" stroke="#666" stroke-width="2.4"/></svg>`, single(connect({ position: [0, 0] }, { position: [160, 0] }))),
|
|
169
|
+
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>`, single(connect({ position: [0, 0] }, { position: [160, 0] }, { red: true }))),
|
|
170
|
+
],
|
|
171
|
+
};
|