@formicoidea/labre-framework-wardley 0.23.2 → 0.23.3
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 +145 -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
package/src/legend.ts
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
|
|
2
|
-
import {
|
|
3
|
-
ConnectorElementModel,
|
|
4
|
-
ConnectorMode,
|
|
5
|
-
FontFamily,
|
|
6
|
-
PointStyle,
|
|
7
|
-
ShapeElementModel,
|
|
8
|
-
ShapeStyle,
|
|
9
|
-
StrokeStyle,
|
|
10
|
-
type WardleyBackgroundElementModel,
|
|
11
|
-
WardleyNodeElementModel,
|
|
12
|
-
} from '@formicoidea/labre-core/model';
|
|
13
|
-
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
14
|
-
import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
15
|
-
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
16
|
-
|
|
17
|
-
import { GRADIENT_GREEN, GRADIENT_RED } from './gradient';
|
|
18
|
-
import {
|
|
19
|
-
INERTIA_COLOR,
|
|
20
|
-
LINK_GREY,
|
|
21
|
-
LINK_STROKE_WIDTH,
|
|
22
|
-
MARKET_DOT_STROKE_WIDTH,
|
|
23
|
-
MARKET_LINK_COLOR,
|
|
24
|
-
MARKET_LINK_WIDTH,
|
|
25
|
-
METHOD_FILL,
|
|
26
|
-
NODE_FILL,
|
|
27
|
-
NODE_STROKE,
|
|
28
|
-
NODE_STROKE_WIDTH,
|
|
29
|
-
PIPELINE_FILL,
|
|
30
|
-
WARDLEY_RED,
|
|
31
|
-
} from './node/consts';
|
|
32
|
-
|
|
33
|
-
/** Component kinds the legend can describe, in display order. */
|
|
34
|
-
type LegendType =
|
|
35
|
-
| 'component'
|
|
36
|
-
| 'anchor'
|
|
37
|
-
| 'market'
|
|
38
|
-
| 'ecosystem'
|
|
39
|
-
| 'method'
|
|
40
|
-
| 'pipeline'
|
|
41
|
-
| 'link'
|
|
42
|
-
| 'arrow'
|
|
43
|
-
| 'inertia';
|
|
44
|
-
|
|
45
|
-
const LEGEND_ORDER: LegendType[] = [
|
|
46
|
-
'component',
|
|
47
|
-
'anchor',
|
|
48
|
-
'market',
|
|
49
|
-
'ecosystem',
|
|
50
|
-
'method',
|
|
51
|
-
'pipeline',
|
|
52
|
-
'link',
|
|
53
|
-
'arrow',
|
|
54
|
-
'inertia',
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
/** Default (editable) descriptions for each legend row. */
|
|
58
|
-
const LEGEND_DESC: Record<LegendType, string> = {
|
|
59
|
-
component: 'Need / capability (activity, practice, data…)',
|
|
60
|
-
anchor: 'Stakeholder (customer, user…)',
|
|
61
|
-
market: 'Market (set of actors)',
|
|
62
|
-
ecosystem: 'Ecosystem',
|
|
63
|
-
method: 'Component + method (color = phase)',
|
|
64
|
-
pipeline: 'Pipeline (possible choices for a capability)',
|
|
65
|
-
link: 'Need relation (parent → child)',
|
|
66
|
-
arrow: 'Evolution / movement (red = future)',
|
|
67
|
-
inertia: 'Inertia to change',
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
type GradientVariant = Exclude<WardleyBackgroundElementModel['variant'], 'classic'>;
|
|
71
|
-
|
|
72
|
-
/** Gradient-meaning block, keyed by variant (caption + 2-colour swatch). */
|
|
73
|
-
const LEGEND_GRADIENT: Record<
|
|
74
|
-
GradientVariant,
|
|
75
|
-
{ caption: string; swatch: [string, string] }
|
|
76
|
-
> = {
|
|
77
|
-
opportunity: {
|
|
78
|
-
caption:
|
|
79
|
-
'Opportunity gradient: differential value (green) vs operational value (red).',
|
|
80
|
-
swatch: [GRADIENT_GREEN, GRADIENT_RED],
|
|
81
|
-
},
|
|
82
|
-
benefit: {
|
|
83
|
-
caption: 'Gradient: investment (red) then benefit (green).',
|
|
84
|
-
swatch: [GRADIENT_RED, GRADIENT_GREEN],
|
|
85
|
-
},
|
|
86
|
-
'evolution-gradient': {
|
|
87
|
-
caption:
|
|
88
|
-
"Gradient representing the growth of Wardley's evolution function.",
|
|
89
|
-
swatch: ['#9aa0a6', '#cfd2d6'],
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Build a "Legend" group from real, editable elements (white rect frame +
|
|
95
|
-
* "Legend" text + one row of [real component glyph + description text] per
|
|
96
|
-
* Wardley component TYPE present inside the background's perimeter + a
|
|
97
|
-
* gradient-meaning block when the background is a gradient variant). A snapshot
|
|
98
|
-
* is created on each call; everything is grouped so it can be moved / resized /
|
|
99
|
-
* edited and is dropped bottom-left of the background.
|
|
100
|
-
*/
|
|
101
|
-
export function createWardleyLegend(
|
|
102
|
-
std: BlockStdScope,
|
|
103
|
-
bg: WardleyBackgroundElementModel
|
|
104
|
-
) {
|
|
105
|
-
const gfx = std.get(GfxControllerIdentifier);
|
|
106
|
-
const surface = gfx.surface;
|
|
107
|
-
if (!surface) return;
|
|
108
|
-
|
|
109
|
-
const [bx, by, , bh] = bg.deserializedXYWH;
|
|
110
|
-
|
|
111
|
-
// 1. Detect which component types are present inside the perimeter.
|
|
112
|
-
const present = new Set<LegendType>();
|
|
113
|
-
for (const el of gfx.getElementsByBound(Bound.deserialize(bg.xywh), {
|
|
114
|
-
type: 'canvas',
|
|
115
|
-
})) {
|
|
116
|
-
// Note: WardleyNodeElementModel extends ShapeElementModel, so the order of
|
|
117
|
-
// these instanceof checks matters.
|
|
118
|
-
if (el instanceof WardleyNodeElementModel) {
|
|
119
|
-
if (el.kind !== 'handle') present.add(el.kind);
|
|
120
|
-
} else if (el instanceof ConnectorElementModel) {
|
|
121
|
-
if (el.strokeStyle === StrokeStyle.Dash || el.stroke === WARDLEY_RED) {
|
|
122
|
-
present.add('arrow');
|
|
123
|
-
} else if (el.stroke === LINK_GREY) {
|
|
124
|
-
present.add('link');
|
|
125
|
-
}
|
|
126
|
-
// market triangle connectors (NODE_STROKE) are ignored.
|
|
127
|
-
} else if (el instanceof ShapeElementModel) {
|
|
128
|
-
if (el.fillColor === INERTIA_COLOR) present.add('inertia');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const rows = LEGEND_ORDER.filter(t => present.has(t));
|
|
132
|
-
|
|
133
|
-
// 2. Layout (model units). The text column is wide enough for one-line
|
|
134
|
-
// descriptions; the gradient row is taller as its caption may wrap.
|
|
135
|
-
const PAD = 16;
|
|
136
|
-
const TITLE_H = 28;
|
|
137
|
-
const ROW_H = 30;
|
|
138
|
-
const GLYPH_W = 46;
|
|
139
|
-
const GAP = 12;
|
|
140
|
-
const TEXT_FS = 15;
|
|
141
|
-
const TITLE_FS = 18;
|
|
142
|
-
const TEXT_W = 360;
|
|
143
|
-
const GRAD_ROW_H = 40;
|
|
144
|
-
const W = PAD * 2 + GLYPH_W + GAP + TEXT_W;
|
|
145
|
-
|
|
146
|
-
const variant = bg.variant;
|
|
147
|
-
const grad = variant !== 'classic' ? LEGEND_GRADIENT[variant] : null;
|
|
148
|
-
const gradH = grad ? 12 + GRAD_ROW_H : 0;
|
|
149
|
-
const H = PAD * 2 + TITLE_H + rows.length * ROW_H + gradH;
|
|
150
|
-
|
|
151
|
-
const x0 = bx + 50;
|
|
152
|
-
const y0 = by + bh - 56 - H;
|
|
153
|
-
|
|
154
|
-
const text = (
|
|
155
|
-
t: string,
|
|
156
|
-
x: number,
|
|
157
|
-
y: number,
|
|
158
|
-
w: number,
|
|
159
|
-
h: number,
|
|
160
|
-
fontSize: number,
|
|
161
|
-
align: 'left' | 'center' = 'left'
|
|
162
|
-
) =>
|
|
163
|
-
surface.addElement({
|
|
164
|
-
type: 'text',
|
|
165
|
-
text: t,
|
|
166
|
-
fontFamily: FontFamily.Inter,
|
|
167
|
-
fontSize,
|
|
168
|
-
color: NODE_STROKE,
|
|
169
|
-
textAlign: align,
|
|
170
|
-
xywh: new Bound(x, y, w, h).serialize(),
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// ── glyph builders (real, editable elements), centred on (cx, cy) ─────
|
|
174
|
-
const ellipse = (
|
|
175
|
-
kind: 'component' | 'anchor' | 'ecosystem' | 'method',
|
|
176
|
-
d: number,
|
|
177
|
-
fill: string,
|
|
178
|
-
sw: number,
|
|
179
|
-
cx: number,
|
|
180
|
-
cy: number
|
|
181
|
-
) =>
|
|
182
|
-
surface.addElement({
|
|
183
|
-
type: 'wardleyNode',
|
|
184
|
-
kind,
|
|
185
|
-
shapeType: 'ellipse',
|
|
186
|
-
filled: true,
|
|
187
|
-
fillColor: fill,
|
|
188
|
-
strokeColor: NODE_STROKE,
|
|
189
|
-
strokeWidth: sw,
|
|
190
|
-
shapeStyle: ShapeStyle.General,
|
|
191
|
-
roughness: 0,
|
|
192
|
-
xywh: new Bound(cx - d / 2, cy - d / 2, d, d).serialize(),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const glyph = (type: LegendType, cx: number, cy: number): string[] => {
|
|
196
|
-
switch (type) {
|
|
197
|
-
case 'component':
|
|
198
|
-
return [ellipse('component', 16, NODE_FILL, NODE_STROKE_WIDTH, cx, cy)];
|
|
199
|
-
case 'anchor':
|
|
200
|
-
return [ellipse('anchor', 16, NODE_FILL, NODE_STROKE_WIDTH, cx, cy)];
|
|
201
|
-
case 'ecosystem':
|
|
202
|
-
return [ellipse('ecosystem', 20, NODE_FILL, NODE_STROKE_WIDTH, cx, cy)];
|
|
203
|
-
case 'method':
|
|
204
|
-
return [ellipse('method', 18, METHOD_FILL, NODE_STROKE_WIDTH, cx, cy)];
|
|
205
|
-
case 'inertia':
|
|
206
|
-
return [
|
|
207
|
-
surface.addElement({
|
|
208
|
-
type: 'shape',
|
|
209
|
-
shapeType: 'rect',
|
|
210
|
-
filled: true,
|
|
211
|
-
fillColor: INERTIA_COLOR,
|
|
212
|
-
strokeColor: INERTIA_COLOR,
|
|
213
|
-
strokeWidth: 0,
|
|
214
|
-
shapeStyle: ShapeStyle.General,
|
|
215
|
-
roughness: 0,
|
|
216
|
-
radius: 0,
|
|
217
|
-
xywh: new Bound(cx - 2.5, cy - 11, 5, 22).serialize(),
|
|
218
|
-
}),
|
|
219
|
-
];
|
|
220
|
-
case 'pipeline': {
|
|
221
|
-
const bw2 = 34;
|
|
222
|
-
const bh2 = 12;
|
|
223
|
-
const hd = 10;
|
|
224
|
-
const top = cy - bh2 / 2;
|
|
225
|
-
return [
|
|
226
|
-
surface.addElement({
|
|
227
|
-
type: 'wardleyNode',
|
|
228
|
-
kind: 'pipeline',
|
|
229
|
-
shapeType: 'rect',
|
|
230
|
-
filled: true,
|
|
231
|
-
fillColor: PIPELINE_FILL,
|
|
232
|
-
strokeColor: NODE_STROKE,
|
|
233
|
-
strokeWidth: NODE_STROKE_WIDTH,
|
|
234
|
-
shapeStyle: ShapeStyle.General,
|
|
235
|
-
roughness: 0,
|
|
236
|
-
radius: 0,
|
|
237
|
-
xywh: new Bound(cx - bw2 / 2, top, bw2, bh2).serialize(),
|
|
238
|
-
}),
|
|
239
|
-
surface.addElement({
|
|
240
|
-
type: 'wardleyNode',
|
|
241
|
-
kind: 'handle',
|
|
242
|
-
shapeType: 'rect',
|
|
243
|
-
filled: true,
|
|
244
|
-
fillColor: NODE_FILL,
|
|
245
|
-
strokeColor: NODE_STROKE,
|
|
246
|
-
strokeWidth: NODE_STROKE_WIDTH,
|
|
247
|
-
shapeStyle: ShapeStyle.General,
|
|
248
|
-
roughness: 0,
|
|
249
|
-
radius: 0,
|
|
250
|
-
xywh: new Bound(cx - hd / 2, top - hd / 2, hd, hd).serialize(),
|
|
251
|
-
}),
|
|
252
|
-
];
|
|
253
|
-
}
|
|
254
|
-
case 'market': {
|
|
255
|
-
const R = 11;
|
|
256
|
-
const dr = 3;
|
|
257
|
-
const rho = 6;
|
|
258
|
-
const sin60 = Math.sqrt(3) / 2;
|
|
259
|
-
const circle = surface.addElement({
|
|
260
|
-
type: 'wardleyNode',
|
|
261
|
-
kind: 'market',
|
|
262
|
-
shapeType: 'ellipse',
|
|
263
|
-
filled: true,
|
|
264
|
-
fillColor: NODE_FILL,
|
|
265
|
-
strokeColor: NODE_STROKE,
|
|
266
|
-
strokeWidth: NODE_STROKE_WIDTH,
|
|
267
|
-
shapeStyle: ShapeStyle.General,
|
|
268
|
-
roughness: 0,
|
|
269
|
-
xywh: new Bound(cx - R, cy - R, R * 2, R * 2).serialize(),
|
|
270
|
-
});
|
|
271
|
-
const verts = [
|
|
272
|
-
[0, -rho],
|
|
273
|
-
[rho * sin60, rho / 2],
|
|
274
|
-
[-rho * sin60, rho / 2],
|
|
275
|
-
];
|
|
276
|
-
const dots = verts.map(([vx, vy]) =>
|
|
277
|
-
surface.addElement({
|
|
278
|
-
type: 'wardleyNode',
|
|
279
|
-
kind: 'component',
|
|
280
|
-
shapeType: 'ellipse',
|
|
281
|
-
filled: true,
|
|
282
|
-
fillColor: NODE_FILL,
|
|
283
|
-
strokeColor: NODE_STROKE,
|
|
284
|
-
strokeWidth: MARKET_DOT_STROKE_WIDTH,
|
|
285
|
-
shapeStyle: ShapeStyle.General,
|
|
286
|
-
roughness: 0,
|
|
287
|
-
xywh: new Bound(cx + vx - dr, cy + vy - dr, dr * 2, dr * 2).serialize(),
|
|
288
|
-
})
|
|
289
|
-
);
|
|
290
|
-
const conns = [
|
|
291
|
-
[dots[0], dots[1]],
|
|
292
|
-
[dots[1], dots[2]],
|
|
293
|
-
[dots[2], dots[0]],
|
|
294
|
-
].map(([a, b]) =>
|
|
295
|
-
surface.addElement({
|
|
296
|
-
type: 'connector',
|
|
297
|
-
mode: ConnectorMode.Straight,
|
|
298
|
-
source: { id: a },
|
|
299
|
-
target: { id: b },
|
|
300
|
-
stroke: MARKET_LINK_COLOR,
|
|
301
|
-
strokeStyle: StrokeStyle.Solid,
|
|
302
|
-
strokeWidth: MARKET_LINK_WIDTH,
|
|
303
|
-
frontEndpointStyle: PointStyle.None,
|
|
304
|
-
rearEndpointStyle: PointStyle.None,
|
|
305
|
-
})
|
|
306
|
-
);
|
|
307
|
-
return [circle, ...dots, ...conns];
|
|
308
|
-
}
|
|
309
|
-
case 'link':
|
|
310
|
-
return [
|
|
311
|
-
surface.addElement({
|
|
312
|
-
type: 'connector',
|
|
313
|
-
mode: ConnectorMode.Straight,
|
|
314
|
-
source: { position: [cx - 18, cy + 6] },
|
|
315
|
-
target: { position: [cx + 18, cy - 6] },
|
|
316
|
-
stroke: LINK_GREY,
|
|
317
|
-
strokeStyle: StrokeStyle.Solid,
|
|
318
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
319
|
-
frontEndpointStyle: PointStyle.None,
|
|
320
|
-
rearEndpointStyle: PointStyle.None,
|
|
321
|
-
}),
|
|
322
|
-
];
|
|
323
|
-
case 'arrow':
|
|
324
|
-
return [
|
|
325
|
-
surface.addElement({
|
|
326
|
-
type: 'connector',
|
|
327
|
-
mode: ConnectorMode.Straight,
|
|
328
|
-
source: { position: [cx - 18, cy] },
|
|
329
|
-
target: { position: [cx + 16, cy] },
|
|
330
|
-
stroke: WARDLEY_RED,
|
|
331
|
-
strokeStyle: StrokeStyle.Dash,
|
|
332
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
333
|
-
frontEndpointStyle: PointStyle.None,
|
|
334
|
-
rearEndpointStyle: PointStyle.Triangle,
|
|
335
|
-
}),
|
|
336
|
-
];
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
// 3. Create the elements.
|
|
341
|
-
std.store.captureSync();
|
|
342
|
-
const ids: string[] = [];
|
|
343
|
-
|
|
344
|
-
// White frame.
|
|
345
|
-
ids.push(
|
|
346
|
-
surface.addElement({
|
|
347
|
-
type: 'shape',
|
|
348
|
-
shapeType: 'rect',
|
|
349
|
-
filled: true,
|
|
350
|
-
fillColor: '#ffffff',
|
|
351
|
-
strokeColor: '#cfd2d6',
|
|
352
|
-
strokeWidth: 1,
|
|
353
|
-
shapeStyle: ShapeStyle.General,
|
|
354
|
-
roughness: 0,
|
|
355
|
-
radius: 6,
|
|
356
|
-
xywh: new Bound(x0, y0, W, H).serialize(),
|
|
357
|
-
})
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
// Title.
|
|
361
|
-
ids.push(
|
|
362
|
-
text('Legend', x0 + PAD, y0 + PAD, W - PAD * 2, TITLE_FS + 6, TITLE_FS)
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
// Rows.
|
|
366
|
-
let ry = y0 + PAD + TITLE_H;
|
|
367
|
-
for (const t of rows) {
|
|
368
|
-
const cyRow = ry + ROW_H / 2;
|
|
369
|
-
ids.push(...glyph(t, x0 + PAD + GLYPH_W / 2, cyRow));
|
|
370
|
-
ids.push(
|
|
371
|
-
text(
|
|
372
|
-
LEGEND_DESC[t],
|
|
373
|
-
x0 + PAD + GLYPH_W + GAP,
|
|
374
|
-
cyRow - (TEXT_FS + 8) / 2,
|
|
375
|
-
TEXT_W,
|
|
376
|
-
TEXT_FS + 8,
|
|
377
|
-
TEXT_FS
|
|
378
|
-
)
|
|
379
|
-
);
|
|
380
|
-
ry += ROW_H;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Gradient meaning block: a separator, then [2-colour swatch | caption].
|
|
384
|
-
if (grad) {
|
|
385
|
-
const sepY = ry + 4;
|
|
386
|
-
ids.push(
|
|
387
|
-
surface.addElement({
|
|
388
|
-
type: 'shape',
|
|
389
|
-
shapeType: 'rect',
|
|
390
|
-
filled: true,
|
|
391
|
-
fillColor: '#cfd2d6',
|
|
392
|
-
strokeColor: '#cfd2d6',
|
|
393
|
-
strokeWidth: 0,
|
|
394
|
-
shapeStyle: ShapeStyle.General,
|
|
395
|
-
roughness: 0,
|
|
396
|
-
radius: 0,
|
|
397
|
-
xywh: new Bound(x0 + PAD, sepY, W - PAD * 2, 1).serialize(),
|
|
398
|
-
})
|
|
399
|
-
);
|
|
400
|
-
const cyRow = sepY + 8 + GRAD_ROW_H / 2;
|
|
401
|
-
const sw = 14;
|
|
402
|
-
const sgap = 2;
|
|
403
|
-
const sx = x0 + PAD + GLYPH_W / 2 - (sw * 2 + sgap) / 2;
|
|
404
|
-
grad.swatch.forEach((col, i) => {
|
|
405
|
-
ids.push(
|
|
406
|
-
surface.addElement({
|
|
407
|
-
type: 'shape',
|
|
408
|
-
shapeType: 'rect',
|
|
409
|
-
filled: true,
|
|
410
|
-
fillColor: col,
|
|
411
|
-
strokeColor: '#cfd2d6',
|
|
412
|
-
strokeWidth: 0.5,
|
|
413
|
-
shapeStyle: ShapeStyle.General,
|
|
414
|
-
roughness: 0,
|
|
415
|
-
radius: 1,
|
|
416
|
-
xywh: new Bound(sx + i * (sw + sgap), cyRow - sw / 2, sw, sw).serialize(),
|
|
417
|
-
})
|
|
418
|
-
);
|
|
419
|
-
});
|
|
420
|
-
ids.push(
|
|
421
|
-
text(
|
|
422
|
-
grad.caption,
|
|
423
|
-
x0 + PAD + GLYPH_W + GAP,
|
|
424
|
-
cyRow - GRAD_ROW_H / 2,
|
|
425
|
-
TEXT_W,
|
|
426
|
-
GRAD_ROW_H,
|
|
427
|
-
TEXT_FS
|
|
428
|
-
)
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// 4. Group everything and select it.
|
|
433
|
-
const [, result] = std.command.exec(createGroupCommand, { elements: ids });
|
|
434
|
-
gfx.selection.set({
|
|
435
|
-
elements: [result.groupId || ids[0]],
|
|
436
|
-
editing: false,
|
|
437
|
-
});
|
|
438
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ElementRenderer,
|
|
3
|
-
ElementRendererExtension,
|
|
4
|
-
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
-
import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
|
|
6
|
-
import { type WardleyNodeElementModel, DefaultTheme } from '@formicoidea/labre-core/model';
|
|
7
|
-
|
|
8
|
-
import { ANCHOR, ECOSYSTEM, METHOD, NODE_FILL } from './consts';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Renderer for a Wardley node. The circle is drawn by REUSING the native shape
|
|
12
|
-
* renderer (so stroke width, colors and theme behave exactly like a native
|
|
13
|
-
* ellipse). On top of it, a glyph is drawn for two kinds:
|
|
14
|
-
* - `anchor` → an inscribed person (head + shoulders), clipped to the circle.
|
|
15
|
-
* - `ecosystem` → a double border at the rim + diagonal hatching confined to the
|
|
16
|
-
* inner donut + a hollow central circle.
|
|
17
|
-
* All glyph strokes use the model's (editable) stroke color; the white band and
|
|
18
|
-
* the hollow center come from the base fill, so colors stay editable.
|
|
19
|
-
*/
|
|
20
|
-
export const wardleyNode: ElementRenderer<WardleyNodeElementModel> = (
|
|
21
|
-
model,
|
|
22
|
-
ctx,
|
|
23
|
-
matrix,
|
|
24
|
-
renderer,
|
|
25
|
-
rc,
|
|
26
|
-
bound
|
|
27
|
-
) => {
|
|
28
|
-
const [, , w, h] = model.deserializedXYWH;
|
|
29
|
-
const cx = w / 2;
|
|
30
|
-
const cy = h / 2;
|
|
31
|
-
|
|
32
|
-
// Capture the element-local transform BEFORE the shape renderer mutates the
|
|
33
|
-
// matrix, so the glyph can be drawn in the same space afterwards.
|
|
34
|
-
const glyphMatrix = DOMMatrix.fromMatrix(matrix)
|
|
35
|
-
.translateSelf(cx, cy)
|
|
36
|
-
.rotateSelf(model.rotate)
|
|
37
|
-
.translateSelf(-cx, -cy);
|
|
38
|
-
|
|
39
|
-
// Native ellipse (fill / stroke / theme handled natively).
|
|
40
|
-
shapeRenderer(model, ctx, matrix, renderer, rc, bound);
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
model.kind !== 'anchor' &&
|
|
44
|
-
model.kind !== 'ecosystem' &&
|
|
45
|
-
model.kind !== 'method'
|
|
46
|
-
)
|
|
47
|
-
return;
|
|
48
|
-
|
|
49
|
-
const strokeWidth = model.strokeWidth || 1;
|
|
50
|
-
const R = Math.min(w, h) / 2 - strokeWidth / 2;
|
|
51
|
-
const color = renderer.getColorValue(
|
|
52
|
-
model.strokeColor,
|
|
53
|
-
DefaultTheme.shapeStrokeColor,
|
|
54
|
-
true
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
ctx.setTransform(glyphMatrix);
|
|
58
|
-
|
|
59
|
-
// ── Anchor: person glyph (clipped to the circle) ────────────────────
|
|
60
|
-
if (model.kind === 'anchor') {
|
|
61
|
-
ctx.save();
|
|
62
|
-
ctx.beginPath();
|
|
63
|
-
ctx.arc(cx, cy, R - strokeWidth / 2, 0, Math.PI * 2);
|
|
64
|
-
ctx.clip();
|
|
65
|
-
|
|
66
|
-
ctx.lineWidth = strokeWidth;
|
|
67
|
-
ctx.strokeStyle = color;
|
|
68
|
-
ctx.lineCap = 'round';
|
|
69
|
-
ctx.lineJoin = 'round';
|
|
70
|
-
|
|
71
|
-
// head
|
|
72
|
-
ctx.beginPath();
|
|
73
|
-
ctx.arc(cx, cy + ANCHOR.headCY * R, ANCHOR.headR * R, 0, Math.PI * 2);
|
|
74
|
-
ctx.stroke();
|
|
75
|
-
|
|
76
|
-
// rounded shoulders (extremities sit on the circle border)
|
|
77
|
-
const sx = ANCHOR.shoulderEndX * R;
|
|
78
|
-
const sy = ANCHOR.shoulderEndY * R;
|
|
79
|
-
const kx = ANCHOR.shoulderCtrlX * R;
|
|
80
|
-
const ky = ANCHOR.shoulderCtrlY * R;
|
|
81
|
-
ctx.beginPath();
|
|
82
|
-
ctx.moveTo(cx - sx, cy + sy);
|
|
83
|
-
ctx.bezierCurveTo(cx - kx, cy + ky, cx + kx, cy + ky, cx + sx, cy + sy);
|
|
84
|
-
ctx.stroke();
|
|
85
|
-
|
|
86
|
-
ctx.restore();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── Method: a white component inscribed in the colored outer circle ──
|
|
91
|
-
if (model.kind === 'method') {
|
|
92
|
-
const rInner = R * METHOD.centerRatio;
|
|
93
|
-
ctx.fillStyle = NODE_FILL;
|
|
94
|
-
ctx.lineWidth = strokeWidth;
|
|
95
|
-
ctx.strokeStyle = color;
|
|
96
|
-
ctx.beginPath();
|
|
97
|
-
ctx.arc(cx, cy, rInner, 0, Math.PI * 2);
|
|
98
|
-
ctx.fill();
|
|
99
|
-
ctx.stroke();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Ecosystem: double border + hatched inner donut + hollow center ──
|
|
104
|
-
const rBorder2 = R * ECOSYSTEM.secondBorderRatio;
|
|
105
|
-
const rCenter = R * ECOSYSTEM.centerRatio;
|
|
106
|
-
const rHatch = R * ECOSYSTEM.hatchOuterRatio;
|
|
107
|
-
|
|
108
|
-
// Hatch confined to the donut [rCenter, rHatch] (even-odd clip = annulus).
|
|
109
|
-
ctx.save();
|
|
110
|
-
ctx.beginPath();
|
|
111
|
-
ctx.arc(cx, cy, rHatch, 0, Math.PI * 2);
|
|
112
|
-
ctx.moveTo(cx + rCenter, cy);
|
|
113
|
-
ctx.arc(cx, cy, rCenter, 0, Math.PI * 2);
|
|
114
|
-
ctx.clip('evenodd');
|
|
115
|
-
|
|
116
|
-
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.6);
|
|
117
|
-
ctx.strokeStyle = color;
|
|
118
|
-
const step = R * ECOSYSTEM.hatchSpacingRatio;
|
|
119
|
-
for (let d = -2 * R; d <= 2 * R; d += step) {
|
|
120
|
-
ctx.beginPath();
|
|
121
|
-
ctx.moveTo(cx - R, cy - R + d);
|
|
122
|
-
ctx.lineTo(cx + R, cy + R + d);
|
|
123
|
-
ctx.stroke();
|
|
124
|
-
}
|
|
125
|
-
ctx.restore();
|
|
126
|
-
|
|
127
|
-
// Double border (2nd inscribed circle) + central hole border. No fill: the
|
|
128
|
-
// white band and hollow center come from the base ellipse fill.
|
|
129
|
-
ctx.lineWidth = strokeWidth;
|
|
130
|
-
ctx.strokeStyle = color;
|
|
131
|
-
ctx.beginPath();
|
|
132
|
-
ctx.arc(cx, cy, rBorder2, 0, Math.PI * 2);
|
|
133
|
-
ctx.stroke();
|
|
134
|
-
ctx.beginPath();
|
|
135
|
-
ctx.arc(cx, cy, rCenter, 0, Math.PI * 2);
|
|
136
|
-
ctx.stroke();
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
export const WardleyNodeRendererExtension = ElementRendererExtension(
|
|
140
|
-
'wardleyNode',
|
|
141
|
-
wardleyNode
|
|
142
|
-
);
|