@formicoidea/labre-framework-wardley 0.23.0 → 0.23.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.
Files changed (61) hide show
  1. package/dist/consts.d.ts +72 -0
  2. package/{src/consts.ts → dist/consts.js} +63 -72
  3. package/dist/descriptor.d.ts +7 -0
  4. package/{src/descriptor.ts → dist/descriptor.js} +1 -1
  5. package/dist/effects.d.ts +10 -0
  6. package/dist/effects.js +7 -0
  7. package/dist/element-renderer.d.ts +15 -0
  8. package/dist/element-renderer.js +160 -0
  9. package/dist/element-view.d.ts +21 -0
  10. package/dist/element-view.js +122 -0
  11. package/dist/gradient.d.ts +18 -0
  12. package/dist/gradient.js +112 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +2 -0
  15. package/dist/label-layout.d.ts +21 -0
  16. package/dist/label-layout.js +73 -0
  17. package/dist/legend.d.ts +12 -0
  18. package/dist/legend.js +333 -0
  19. package/dist/node/consts.d.ts +107 -0
  20. package/{src/node/consts.ts → dist/node/consts.js} +12 -20
  21. package/dist/node/label-editor.d.ts +28 -0
  22. package/dist/node/label-editor.js +216 -0
  23. package/dist/node/node-renderer.d.ts +17 -0
  24. package/dist/node/node-renderer.js +106 -0
  25. package/{src/node/node-view.ts → dist/node/node-view.d.ts} +3 -3
  26. package/dist/node/node-view.js +10 -0
  27. package/dist/templates/index.d.ts +3 -0
  28. package/dist/templates/index.js +172 -0
  29. package/dist/templates/maps.d.ts +3 -0
  30. package/dist/templates/maps.js +247 -0
  31. package/dist/toolbar/config.d.ts +75 -0
  32. package/dist/toolbar/config.js +206 -0
  33. package/dist/toolbar/icons.d.ts +31 -0
  34. package/{src/toolbar/icons.ts → dist/toolbar/icons.js} +51 -66
  35. package/dist/toolbar/node-config.d.ts +2 -0
  36. package/{src/toolbar/node-config.ts → dist/toolbar/node-config.js} +7 -14
  37. package/dist/toolbar/senior-tool.d.ts +2 -0
  38. package/{src/toolbar/senior-tool.ts → dist/toolbar/senior-tool.js} +5 -5
  39. package/dist/toolbar/wardley-menu.d.ts +53 -0
  40. package/dist/toolbar/wardley-menu.js +408 -0
  41. package/dist/toolbar/wardley-senior-button.d.ts +18 -0
  42. package/dist/toolbar/wardley-senior-button.js +146 -0
  43. package/dist/toolbar/wardley-tool-button.d.ts +10 -0
  44. package/dist/toolbar/wardley-tool-button.js +123 -0
  45. package/dist/view.d.ts +7 -0
  46. package/dist/view.js +36 -0
  47. package/package.json +15 -6
  48. package/src/effects.ts +0 -17
  49. package/src/element-renderer.ts +0 -242
  50. package/src/element-view.ts +0 -143
  51. package/src/gradient.ts +0 -137
  52. package/src/index.ts +0 -1
  53. package/src/label-layout.ts +0 -126
  54. package/src/legend.ts +0 -438
  55. package/src/node/node-renderer.ts +0 -142
  56. package/src/templates/index.ts +0 -236
  57. package/src/templates/maps.ts +0 -283
  58. package/src/toolbar/config.ts +0 -280
  59. package/src/toolbar/wardley-menu.ts +0 -552
  60. package/src/toolbar/wardley-senior-button.ts +0 -154
  61. package/src/view.ts +0 -39
@@ -7,33 +7,29 @@
7
7
  * pre-formatted defaults at creation. The person glyph (anchor) is expressed as
8
8
  * ratios of the circle radius R so it stays proportional at any size.
9
9
  */
10
-
11
10
  /** Default node diameter (= the map label font size, 18). White fill, thin border. */
12
11
  export const NODE_SIZE = 18;
13
12
  export const NODE_FILL = '#ffffff';
14
13
  export const NODE_STROKE = '#1f2328';
15
14
  /** Thin border, matching the link / silhouette line weight. */
16
15
  export const NODE_STROKE_WIDTH = 1;
17
-
18
16
  /**
19
17
  * Person glyph (`kind: 'anchor'`) ratios of R, mirroring the validated icon
20
18
  * ANCH-B (circle r6 → head r1.8 at cy-2.6 ; shoulders cubic touching the
21
19
  * border at ±(4.6, 3.9) with controls at ±(3.8, 0.1)).
22
20
  */
23
21
  export const ANCHOR = {
24
- headR: 0.3,
25
- headCY: -0.433,
26
- shoulderEndX: 0.767,
27
- shoulderEndY: 0.65,
28
- shoulderCtrlX: 0.633,
29
- shoulderCtrlY: 0.017,
22
+ headR: 0.3,
23
+ headCY: -0.433,
24
+ shoulderEndX: 0.767,
25
+ shoulderEndY: 0.65,
26
+ shoulderCtrlX: 0.633,
27
+ shoulderCtrlY: 0.017,
30
28
  };
31
-
32
29
  /** Native text label, same family as the axis labels, size 18. */
33
30
  export const LABEL_FONT_SIZE = 18;
34
31
  export const LABEL_GAP = 8;
35
32
  export const LABEL_DEFAULT = { component: 'Component', anchor: 'Anchor' };
36
-
37
33
  /**
38
34
  * Pipeline defaults. The body is a wide, thin native rect (≈ 1.4× the node
39
35
  * diameter tall) with a white slightly-transparent fill and a node-weight
@@ -48,14 +44,12 @@ export const PIPELINE_FILL = '#ffffff99';
48
44
  /** Handle square = node diameter. */
49
45
  export const HANDLE_SIZE = NODE_SIZE;
50
46
  export const PIPELINE_LABEL = 'Pipeline';
51
-
52
47
  /**
53
48
  * Connector line width. Connectors are constrained to the LineWidth enum
54
49
  * {2,4,6,8,10,12}, so the thinnest available (2) is used — as close as possible
55
50
  * to the 1px node border.
56
51
  */
57
52
  export const LINK_STROKE_WIDTH = 2;
58
-
59
53
  /** Wardley red ("future"/evolution) — matches the validated arrow icon. */
60
54
  export const WARDLEY_RED = '#d6455d';
61
55
  /** Dependency link grey. */
@@ -63,7 +57,6 @@ export const LINK_GREY = '#666666';
63
57
  /** Inertia bar color + size. */
64
58
  export const INERTIA_COLOR = '#1f2328';
65
59
  export const INERTIA_SIZE = { w: 8, h: 44 };
66
-
67
60
  /**
68
61
  * Market node (composite): a large thin-bordered circle containing 3 small
69
62
  * thick-bordered component nodes wired into a triangle by native connectors.
@@ -78,7 +71,6 @@ export const MARKET_DOT_STROKE_WIDTH = 2;
78
71
  export const MARKET_LINK_WIDTH = 1;
79
72
  export const MARKET_LINK_COLOR = NODE_STROKE;
80
73
  export const MARKET_LABEL = 'Market';
81
-
82
74
  /**
83
75
  * Ecosystem node: a single connectable circle drawn as a GLYPH — a double border
84
76
  * at the rim (outer circle + a 2nd inscribed circle with a thin blank band
@@ -88,13 +80,12 @@ export const MARKET_LABEL = 'Market';
88
80
  */
89
81
  export const ECOSYSTEM_SIZE = 40;
90
82
  export const ECOSYSTEM = {
91
- secondBorderRatio: 0.88, // 2nd (inscribed) border
92
- centerRatio: 0.43, // central hollow circle
93
- hatchOuterRatio: 0.86, // hatch stays just inside the 2nd border
94
- hatchSpacingRatio: 0.15, // gap between hatch lines
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
95
87
  };
96
88
  export const ECOSYSTEM_LABEL = 'Ecosystem';
97
-
98
89
  /**
99
90
  * Method node: a component inscribed in a slightly larger outer circle whose
100
91
  * FILL color encodes the chosen method (editable via the toolbar). Glyph = the
@@ -104,6 +95,7 @@ export const ECOSYSTEM_LABEL = 'Ecosystem';
104
95
  export const METHOD_SIZE = 35;
105
96
  export const METHOD_FILL = '#d9d9d9';
106
97
  export const METHOD = {
107
- centerRatio: 0.5, // inner white component radius / R
98
+ centerRatio: 0.5, // inner white component radius / R
108
99
  };
109
100
  export const METHOD_LABEL = 'Component';
101
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1,28 @@
1
+ import { WardleyNodeElementModel } from '@blocksuite/affine-model';
2
+ import type { RichText } from '@blocksuite/affine-rich-text';
3
+ import { type BlockComponent, type BlockStdScope, ShadowlessElement } from '@blocksuite/std';
4
+ import { nothing } from 'lit';
5
+ /**
6
+ * Mount an inline editor over a Wardley node's label. A trimmed clone of
7
+ * `EdgelessShapeTextEditor`, bound to {@link WardleyNodeElementModel.text} and
8
+ * positioned at the label spot (right of the circle + `labelOffset`).
9
+ */
10
+ export declare function mountWardleyNodeLabelEditor(node: WardleyNodeElementModel, edgeless: BlockComponent): void;
11
+ declare const EdgelessWardleyNodeLabelEditor_base: typeof ShadowlessElement & import("@blocksuite/global/utils").Constructor<import("@blocksuite/global/lit").DisposableClass>;
12
+ export declare class EdgelessWardleyNodeLabelEditor extends EdgelessWardleyNodeLabelEditor_base {
13
+ get inlineEditor(): import("@blocksuite/affine-shared/types").AffineInlineEditor | null;
14
+ get crud(): import("@blocksuite/affine-block-surface").EdgelessCRUDExtension;
15
+ get gfx(): import("@blocksuite/std/gfx").GfxController;
16
+ get selection(): import("@blocksuite/std/gfx").GfxSelectionManager;
17
+ get inlineEditorContainer(): import("@blocksuite/std/inline").InlineRootElement<import("@blocksuite/affine-shared/types").AffineTextAttributes> | null | undefined;
18
+ private _unmount;
19
+ connectedCallback(): void;
20
+ firstUpdated(): void;
21
+ getUpdateComplete(): Promise<boolean>;
22
+ render(): typeof nothing | import("lit-html").TemplateResult<1>;
23
+ accessor element: WardleyNodeElementModel;
24
+ accessor std: BlockStdScope;
25
+ accessor richText: RichText;
26
+ }
27
+ export {};
28
+ //# sourceMappingURL=label-editor.d.ts.map
@@ -0,0 +1,216 @@
1
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) {
3
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
4
+ return f;
5
+ }
6
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
7
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
8
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
9
+ var _, done = false;
10
+ for (var i = decorators.length - 1; i >= 0; i--) {
11
+ var context = {};
12
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
13
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
14
+ context.addInitializer = function(f) {
15
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
16
+ extraInitializers.push(accept(f || null));
17
+ };
18
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
19
+ if (kind === "accessor") {
20
+ if (result === void 0) continue;
21
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
22
+ if (_ = accept(result.get)) descriptor.get = _;
23
+ if (_ = accept(result.set)) descriptor.set = _;
24
+ if (_ = accept(result.init)) initializers.unshift(_);
25
+ } else if (_ = accept(result)) {
26
+ if (kind === "field") initializers.unshift(_);
27
+ else descriptor[key] = _;
28
+ }
29
+ }
30
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
31
+ done = true;
32
+ };
33
+ var __runInitializers = function(thisArg, initializers, value) {
34
+ var useValue = arguments.length > 2;
35
+ for (var i = 0; i < initializers.length; i++) {
36
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
37
+ }
38
+ return useValue ? value : void 0;
39
+ };
40
+ import { DefaultTool, EdgelessCRUDIdentifier, TextUtils } from "@blocksuite/affine-block-surface";
41
+ import { WardleyNodeElementModel } from "@blocksuite/affine-model";
42
+ import { BlockSuiteError, ErrorCode } from "@blocksuite/global/exceptions";
43
+ import { WithDisposable } from "@blocksuite/global/lit";
44
+ import { ShadowlessElement, stdContext } from "@blocksuite/std";
45
+ import { GfxControllerIdentifier } from "@blocksuite/std/gfx";
46
+ import { RANGE_SYNC_EXCLUDE_ATTR } from "@blocksuite/std/inline";
47
+ import { consume } from "@lit/context";
48
+ import { html, nothing } from "lit";
49
+ import { property, query } from "lit/decorators.js";
50
+ import { styleMap } from "lit/directives/style-map.js";
51
+ import * as Y from "yjs";
52
+ import { LABEL } from "./consts";
53
+ export function mountWardleyNodeLabelEditor(node, edgeless) {
54
+ const mountElm = edgeless.querySelector(".edgeless-mount-point");
55
+ if (!mountElm) {
56
+ throw new BlockSuiteError(ErrorCode.ValueNotExists, "edgeless block's mount point does not exist");
57
+ }
58
+ const gfx = edgeless.std.get(GfxControllerIdentifier);
59
+ const crud = edgeless.std.get(EdgelessCRUDIdentifier);
60
+ const updated = crud.getElementById(node.id);
61
+ if (!(updated instanceof WardleyNodeElementModel)) {
62
+ console.error("Cannot mount label editor on a non-wardley-node element");
63
+ return;
64
+ }
65
+ gfx.tool.setTool(DefaultTool);
66
+ gfx.selection.set({ elements: [node.id], editing: true });
67
+ if (!node.text) {
68
+ crud.updateElement(node.id, { text: new Y.Text() });
69
+ }
70
+ const editor = new EdgelessWardleyNodeLabelEditor();
71
+ editor.element = crud.getElementById(node.id);
72
+ mountElm.append(editor);
73
+ }
74
+ let EdgelessWardleyNodeLabelEditor = (() => {
75
+ let _classSuper = WithDisposable(ShadowlessElement);
76
+ let _element_decorators;
77
+ let _element_initializers = [];
78
+ let _element_extraInitializers = [];
79
+ let _std_decorators;
80
+ let _std_initializers = [];
81
+ let _std_extraInitializers = [];
82
+ let _richText_decorators;
83
+ let _richText_initializers = [];
84
+ let _richText_extraInitializers = [];
85
+ return class EdgelessWardleyNodeLabelEditor extends _classSuper {
86
+ static {
87
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
88
+ _element_decorators = [property({ attribute: false })];
89
+ _std_decorators = [consume({ context: stdContext })];
90
+ _richText_decorators = [query("rich-text")];
91
+ __esDecorate(this, null, _element_decorators, { kind: "accessor", name: "element", static: false, private: false, access: { has: (obj) => "element" in obj, get: (obj) => obj.element, set: (obj, value) => {
92
+ obj.element = value;
93
+ } }, metadata: _metadata }, _element_initializers, _element_extraInitializers);
94
+ __esDecorate(this, null, _std_decorators, { kind: "accessor", name: "std", static: false, private: false, access: { has: (obj) => "std" in obj, get: (obj) => obj.std, set: (obj, value) => {
95
+ obj.std = value;
96
+ } }, metadata: _metadata }, _std_initializers, _std_extraInitializers);
97
+ __esDecorate(this, null, _richText_decorators, { kind: "accessor", name: "richText", static: false, private: false, access: { has: (obj) => "richText" in obj, get: (obj) => obj.richText, set: (obj, value) => {
98
+ obj.richText = value;
99
+ } }, metadata: _metadata }, _richText_initializers, _richText_extraInitializers);
100
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
101
+ }
102
+ get inlineEditor() {
103
+ return this.richText.inlineEditor;
104
+ }
105
+ get crud() {
106
+ return this.std.get(EdgelessCRUDIdentifier);
107
+ }
108
+ get gfx() {
109
+ return this.std.get(GfxControllerIdentifier);
110
+ }
111
+ get selection() {
112
+ return this.gfx.selection;
113
+ }
114
+ get inlineEditorContainer() {
115
+ return this.inlineEditor?.rootElement;
116
+ }
117
+ _unmount() {
118
+ if (this.element.text) {
119
+ const text = this.element.text.toString();
120
+ const trimmed = text.trim();
121
+ if (trimmed.length === 0) {
122
+ this.crud.updateElement(this.element.id, { text: void 0 });
123
+ } else if (trimmed.length < text.length) {
124
+ this.crud.updateElement(this.element.id, {
125
+ text: new Y.Text(trimmed)
126
+ });
127
+ }
128
+ }
129
+ this.remove();
130
+ this.selection.set({ elements: [], editing: false });
131
+ }
132
+ connectedCallback() {
133
+ super.connectedCallback();
134
+ this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, "true");
135
+ }
136
+ firstUpdated() {
137
+ this.disposables.add(this.gfx.viewport.viewportUpdated.subscribe(() => this.requestUpdate()));
138
+ this.updateComplete.then(() => {
139
+ if (!this.inlineEditor)
140
+ return;
141
+ this.inlineEditor.focusEnd();
142
+ if (!this.inlineEditorContainer)
143
+ return;
144
+ this.disposables.addFromEvent(this.inlineEditorContainer, "blur", () => this._unmount());
145
+ }).catch(console.error);
146
+ this.disposables.addFromEvent(this, "keydown", (evt) => {
147
+ if (evt.key === "Escape") {
148
+ this.ownerDocument.activeElement?.blur();
149
+ }
150
+ });
151
+ }
152
+ async getUpdateComplete() {
153
+ const result = await super.getUpdateComplete();
154
+ await this.richText?.updateComplete;
155
+ return result;
156
+ }
157
+ render() {
158
+ if (!this.element.text)
159
+ return nothing;
160
+ const [, , w, h] = this.element.deserializedXYWH;
161
+ const R = Math.min(w, h) / 2;
162
+ const [ox, oy] = this.element.labelOffset ?? [0, 0];
163
+ const modelX = this.element.x + w / 2 + R + LABEL.gap + ox;
164
+ const modelY = this.element.y + h / 2 + oy - LABEL.font / 2;
165
+ const [x, y] = this.gfx.viewport.toViewCoord(modelX, modelY);
166
+ const zoom = this.gfx.viewport.zoom;
167
+ const style = styleMap({
168
+ position: "absolute",
169
+ left: `${x}px`,
170
+ top: `${y}px`,
171
+ minWidth: "8px",
172
+ fontSize: `${LABEL.font}px`,
173
+ fontFamily: TextUtils.wrapFontFamily(LABEL.family),
174
+ lineHeight: "normal",
175
+ color: LABEL.color,
176
+ outline: "none",
177
+ whiteSpace: "nowrap",
178
+ transform: `scale(${zoom}, ${zoom})`,
179
+ transformOrigin: "top left",
180
+ zIndex: "1"
181
+ });
182
+ return html`<rich-text
183
+ .yText=${this.element.text}
184
+ .enableFormat=${false}
185
+ .enableAutoScrollHorizontally=${false}
186
+ style=${style}
187
+ ></rich-text>`;
188
+ }
189
+ #element = __runInitializers(this, _element_initializers, void 0);
190
+ get element() {
191
+ return this.#element;
192
+ }
193
+ set element(_) {
194
+ this.#element = _;
195
+ }
196
+ #std = (__runInitializers(this, _element_extraInitializers), __runInitializers(this, _std_initializers, void 0));
197
+ get std() {
198
+ return this.#std;
199
+ }
200
+ set std(_) {
201
+ this.#std = _;
202
+ }
203
+ #richText = (__runInitializers(this, _std_extraInitializers), __runInitializers(this, _richText_initializers, void 0));
204
+ get richText() {
205
+ return this.#richText;
206
+ }
207
+ set richText(_) {
208
+ this.#richText = _;
209
+ }
210
+ constructor() {
211
+ super(...arguments);
212
+ __runInitializers(this, _richText_extraInitializers);
213
+ }
214
+ };
215
+ })();
216
+ export { EdgelessWardleyNodeLabelEditor };
@@ -0,0 +1,17 @@
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/global/di").ServiceIdentifier<ElementRenderer<WardleyNodeElementModel>>;
16
+ };
17
+ //# sourceMappingURL=node-renderer.d.ts.map
@@ -0,0 +1,106 @@
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);
106
+ //# sourceMappingURL=node-renderer.js.map
@@ -1,11 +1,11 @@
1
1
  import type { WardleyNodeElementModel } from '@formicoidea/labre-core/model';
2
2
  import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
3
-
4
3
  /**
5
4
  * View for a Wardley node. Registering it ensures `gfx.view.get(model)` returns
6
5
  * a view (required so move / select / connector interactions work). The label
7
6
  * is a separate native text element, so no custom editing is needed here.
8
7
  */
9
- export class WardleyNodeView extends GfxElementModelView<WardleyNodeElementModel> {
10
- static override type: string = 'wardleyNode';
8
+ export declare class WardleyNodeView extends GfxElementModelView<WardleyNodeElementModel> {
9
+ static type: string;
11
10
  }
11
+ //# sourceMappingURL=node-view.d.ts.map
@@ -0,0 +1,10 @@
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
+ }
10
+ //# sourceMappingURL=node-view.js.map
@@ -0,0 +1,3 @@
1
+ import { type TemplateCategory } from '@formicoidea/labre-core/gfx/template';
2
+ export declare const wardleyTemplateCategory: TemplateCategory;
3
+ //# sourceMappingURL=index.d.ts.map