@peter.prib/renderengine 0.14.0 → 0.15.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.
Files changed (109) hide show
  1. package/README.md +45 -19
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49391 -39748
  4. package/dist/renderengine.umd.js +25366 -15650
  5. package/documentation/REFERENCE.md +38 -0
  6. package/package.json +6 -1
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +219 -0
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +3 -0
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +114 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/charts/AlluvialDiagram.js +311 -0
  34. package/src/charting/charts/AreaChart.js +52 -15
  35. package/src/charting/charts/BarChart.js +38 -3
  36. package/src/charting/charts/BarChart3D.js +5 -3
  37. package/src/charting/charts/BarChartBase.js +26 -6
  38. package/src/charting/charts/BeeswarmChart.js +164 -0
  39. package/src/charting/charts/BoxPlotChart.js +23 -1
  40. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  41. package/src/charting/charts/CandlestickChart.js +172 -4
  42. package/src/charting/charts/ChordDiagram.js +105 -2
  43. package/src/charting/charts/CirclePackChart.js +81 -0
  44. package/src/charting/charts/ComboChart.js +233 -0
  45. package/src/charting/charts/ContourChart.js +310 -0
  46. package/src/charting/charts/DonutChart3D.js +13 -0
  47. package/src/charting/charts/DumbbellChart.js +217 -0
  48. package/src/charting/charts/EcdfChart.js +150 -0
  49. package/src/charting/charts/FunnelChart.js +4 -1
  50. package/src/charting/charts/GeoMapChart.js +132 -11
  51. package/src/charting/charts/GroupedBarChart.js +44 -3
  52. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  53. package/src/charting/charts/HeatmapChart.js +2 -2
  54. package/src/charting/charts/HexbinChart.js +137 -0
  55. package/src/charting/charts/HierarchyChart.js +77 -3
  56. package/src/charting/charts/HorizonChart.js +105 -0
  57. package/src/charting/charts/KagiChart.js +167 -0
  58. package/src/charting/charts/LineChart.js +74 -18
  59. package/src/charting/charts/OhlcChart.js +79 -0
  60. package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
  61. package/src/charting/charts/PieChart.js +195 -17
  62. package/src/charting/charts/PieChart3D.js +137 -0
  63. package/src/charting/charts/PointAndFigureChart.js +198 -0
  64. package/src/charting/charts/PolarAreaChart.js +189 -0
  65. package/src/charting/charts/RadarChart.js +7 -1
  66. package/src/charting/charts/RadialBarChart.js +5 -2
  67. package/src/charting/charts/RenkoChart.js +178 -0
  68. package/src/charting/charts/RidgelineChart.js +113 -0
  69. package/src/charting/charts/SankeyDiagram.js +127 -4
  70. package/src/charting/charts/ScatterChart.js +13 -0
  71. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  72. package/src/charting/charts/SlopegraphChart.js +205 -0
  73. package/src/charting/charts/SparklineTableChart.js +166 -0
  74. package/src/charting/charts/StackedBarChart.js +48 -5
  75. package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
  76. package/src/charting/charts/StreamGraphChart.js +10 -3
  77. package/src/charting/charts/SunburstChart.js +27 -7
  78. package/src/charting/charts/SurfaceChart3D.js +157 -0
  79. package/src/charting/charts/TreemapChart.js +28 -8
  80. package/src/charting/charts/ViolinChart.js +18 -0
  81. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  82. package/src/charting/datamodel/DataLineage.js +150 -0
  83. package/src/charting/datamodel/DataModel.js +83 -1
  84. package/src/charting/datamodel/ModelMapping.js +87 -0
  85. package/src/charting/datamodel/SqlDdl.js +110 -85
  86. package/src/charting/datamodel/index.js +3 -0
  87. package/src/charting/decision/DecisionWorkbench.js +295 -0
  88. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  89. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  90. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  91. package/src/charting/diagrams/SketchView.js +121 -0
  92. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  93. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  94. package/src/charting/modelTree/NavigatorModel.js +410 -0
  95. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  96. package/src/charting/pivot/PivotChart.js +109 -4
  97. package/src/charting/pivot/PivotGridChart.js +321 -33
  98. package/src/charting/pivot/PivotView.js +82 -0
  99. package/src/charting/report/TemplatedReport.js +547 -0
  100. package/src/charting/templates/BoxTemplate.js +28 -27
  101. package/src/charting/templates/PackageTemplate.js +26 -25
  102. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  103. package/src/charting/templates/Template.js +219 -212
  104. package/src/cli.js +146 -0
  105. package/src/index.js +3370 -2995
  106. package/src/workspace/Workspace.js +126 -7
  107. package/src/workspace/archimateServices.js +4 -0
  108. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  109. package/src/workspace/styles.js +36 -1
@@ -1,231 +1,235 @@
1
- import { TextStyle, TEXT_STYLE_KEYS } from '../TextStyle.js';
2
- import { GlyphTemplate } from '../templates/GlyphTemplate.js';
3
- import { Glyph } from '../glyphs/Glyph.js';
4
- import { glyphFor } from '../glyphs/index.js';
5
- import { ArchimateDiagram } from '../diagrams/ArchimateDiagram.js';
6
- import { LabelHelper } from '../diagrams/LabelHelper.js';
7
-
8
- /** Paint a view can set on one occurrence: template property → appearance key (same names). */
9
- const APPEARANCE = ['fillColor', 'fillOpacity', 'strokeColor', 'strokeWidth', 'textColor', ...TEXT_STYLE_KEYS];
10
-
11
- /**
12
- * ArchimateElementTemplate — one element (or note, or image) on an ArchiMate
13
- * view, as an editor template. It keeps no copy of its own: position, size,
14
- * stacking and paint are read from and written to its ElementInstance, and
15
- * what it looks like comes from the model element through the same
16
- * enrichment the graph engine uses (ArchimateDiagram.buildNode: layer colour,
17
- * corner style, notation shape, type badge, label position). So every editor
18
- * command that moves, resizes, restacks or repaints a template edits the view
19
- * itself, and its undo does too.
20
- *
21
- * The shape is the node's glyph: a box (sharp, rounded or chamfered by
22
- * aspect) with the type's glyph as a corner badge, or the full notation shape
23
- * when distinct shapes are on; notes and images draw their own glyph.
24
- */
25
- export class ArchimateElementTemplate extends GlyphTemplate {
26
- /** ArchiMate notation is not rotated. */
27
- static rotatable = false;
28
-
29
- /** The format painter copies its appearance (what the view's instance holds). */
30
- static formatKeys = Object.freeze([...APPEARANCE]);
31
-
32
- /**
33
- * @param {ElementInstance} instance - What this template shows, and where
34
- * @param {ArchimateModel} model
35
- * @param {Object} [options]
36
- * @param {Object} [options.nodeOptions] - ArchimateDiagram.buildNode options
37
- * (archimateVersion, useDistinctShapes, layerColors, elementWidth, ...)
38
- */
39
- constructor(instance, model, { nodeOptions = {} } = {}) {
40
- super(instance.id, undefined, {});
41
- this.instance = instance;
42
- this.model = model;
43
- this.nodeOptions = nodeOptions;
44
- /** A host's live status, `{text, color}` (the editor's elementStatus), or null: drawn as a pill. */
45
- this.status = null;
46
- }
47
-
48
- /** State lives on the instance and the model element, not here. @protected */
49
- _initState() {}
50
-
51
- /** The element as the ArchiMate notation draws it on this view. */
52
- get node() {
53
- const projected = this.instance.project(this.model) ?? { id: this.instance.id, name: this.instance.elementId };
54
- return ArchimateDiagram.buildNode(projected, this.nodeOptions);
55
- }
56
-
57
- get name() { return this.instance.getDisplayName(this.model); }
58
- /** Renaming one occurrence labels it on this view; the element's own name is left alone. */
59
- set name(value) {
60
- const own = this.instance.resolve(this.model)?.name;
61
- this.instance.labelOverride = value && value !== own ? value : null;
62
- }
63
-
64
- get x() { return this.instance.x; }
65
- set x(value) { this.instance.x = value; }
66
- get y() { return this.instance.y; }
67
- set y(value) { this.instance.y = value; }
68
- get zIndex() { return this.instance.zIndex; }
69
- set zIndex(value) { this.instance.zIndex = value ?? 0; }
70
- get visible() { return this.instance.visible; }
71
- set visible(value) { this.instance.visible = value !== false; }
72
-
73
- get width() { return this.node.width; }
74
- set width(value) { this._setSize('width', value); }
75
- get height() { return this.node.height; }
76
- set height(value) { this._setSize('height', value); }
77
-
78
- get fillColor() { return this.node.color; }
79
- set fillColor(value) { this._setAppearance('fillColor', value); }
80
- get fillOpacity() { return this.instance.appearance.fillOpacity ?? 1; }
81
- set fillOpacity(value) { this._setAppearance('fillOpacity', value); }
82
- get strokeColor() { return this.node.strokeColor; }
83
- set strokeColor(value) { this._setAppearance('strokeColor', value); }
84
- get strokeWidth() { return this.node.strokeWidth; }
85
- set strokeWidth(value) { this._setAppearance('strokeWidth', value); }
86
- get textColor() { return this.instance.appearance.textColor ?? '#222'; }
87
- set textColor(value) { this._setAppearance('textColor', value); }
88
- get fontFamily() { return this.instance.appearance.fontFamily; }
89
- set fontFamily(value) { this._setAppearance('fontFamily', value); }
90
- get fontSize() { return this.instance.appearance.fontSize; }
91
- set fontSize(value) { this._setAppearance('fontSize', value); }
92
- get fontWeight() { return this.instance.appearance.fontWeight; }
93
- set fontWeight(value) { this._setAppearance('fontWeight', value); }
94
- get fontStyle() { return this.instance.appearance.fontStyle; }
95
- set fontStyle(value) { this._setAppearance('fontStyle', value); }
96
- get textDecoration() { return this.instance.appearance.textDecoration; }
97
- set textDecoration(value) { this._setAppearance('textDecoration', value); }
98
- get textAlign() { return this.instance.appearance.textAlign; }
99
- set textAlign(value) { this._setAppearance('textAlign', value); }
100
- get textPosition() { return this.instance.appearance.textPosition; }
101
- set textPosition(value) { this._setAppearance('textPosition', value); }
102
-
103
- /**
104
- * What this occurrence sets itself for a property (undefined when it shows
105
- * the element's own), for commands that must undo to "unset".
106
- */
107
- ownProperty(key) {
108
- if (APPEARANCE.includes(key)) return this.instance.appearance[key];
109
- if (key === 'width' || key === 'height') return this.instance.size[key];
110
- if (key === 'name') return this.instance.labelOverride ?? undefined;
111
- return this[key];
112
- }
113
-
114
- /** undefined puts the occurrence back on the element's own value. @private */
115
- _setSize(key, value) {
116
- if (value === undefined) delete this.instance.size[key];
117
- else this.instance.size = { ...this.instance.size, [key]: value };
118
- }
119
-
120
- /** @private */
121
- _setAppearance(key, value) {
122
- if (!APPEARANCE.includes(key)) throw new TypeError(`Not an appearance property: ${key}`);
123
- if (value === undefined) delete this.instance.appearance[key];
124
- else this.instance.appearance = { ...this.instance.appearance, [key]: value };
125
- }
126
-
127
- glyph() {
128
- return glyphFor(this.node.shape);
129
- }
130
-
131
- glyphContext() {
132
- const node = this.node;
133
- return Glyph.context({
134
- R: Math.max(node.width, node.height) / 2,
135
- size: { w: node.width, h: node.height },
136
- color: node.color,
137
- cornerStyle: node.cornerStyle,
138
- strokeColor: node.strokeColor,
139
- strokeWidth: node.strokeWidth,
140
- icon: node.icon,
141
- iconUrl: node.iconUrl,
142
- ownFill: this.instance.appearance.fillColor ?? null,
143
- });
144
- }
145
-
146
- /**
147
- * The badge's size: the same whatever the box's size (as Archi draws it), so
148
- * resizing a box, or a parent growing to hold what is dropped in it, leaves
149
- * it as it was; only a box too small for it gets a smaller one.
150
- */
151
- static badgeSize(w, h) {
152
- return Math.max(8, Math.min(ArchimateElementTemplate.BADGE_SIZE, Math.round(Math.min(w, h) * 0.4)));
153
- }
154
-
155
- /**
156
- * The type badge in the top-right corner: its glyph, or an icon (text or
157
- * image), a fixed size (badgeSize). As in the graph engine, a notation shape
158
- * gets no glyph badge (buildNode leaves iconShape unset) but keeps an icon the element sets.
159
- */
160
- decorations(ctx) {
161
- return [...this._badge(ctx), ...this.statusPill(ctx)];
162
- }
163
-
164
- /**
165
- * The host's status (`status`), as a small pill inside the box's bottom
166
- * right corner: its text, on its colour. Nothing without one.
167
- */
168
- statusPill({ w, h }) {
169
- const text = this.status?.text;
170
- if (!text) return [];
171
- const label = String(text);
172
- const ph = 12, pw = Math.min(w - 6, label.length * 5.5 + 10);
173
- const x = w / 2 - pw - 3, y = h / 2 - ph - 3;
174
- return [Glyph.group([
175
- Glyph.rect(x, y, pw, ph, this.status.color || '#455a64', null, { rx: 6, ry: 6 }),
176
- Glyph.text(label, x + pw / 2, y + ph / 2, { fill: '#ffffff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: '8px' }),
177
- ], { 'pointer-events': 'none', class: 'element-status' })];
178
- }
179
-
180
- /** The type badge (decorations). @private */
181
- _badge(ctx) {
182
- const node = this.node;
183
- const { w, h } = ctx;
184
- const fixed = ArchimateElementTemplate.badgeSize(w, h);
185
- if (node.iconShape) {
186
- const badge = fixed;
187
- const inset = badge / 2 + 3;
188
- const parts = glyphFor(node.iconShape).parts(Glyph.context({
189
- R: badge / 2, size: { w: badge, h: badge * 0.78 }, color: 'none', strokeColor: node.strokeColor || '#000000', strokeWidth: 1,
190
- }));
191
- return [Glyph.group([parts], { transform: `translate(${w / 2 - inset},${-h / 2 + inset})`, 'pointer-events': 'none' })];
192
- }
193
- if (node.icon || node.iconUrl) {
194
- const size = fixed;
195
- const bx = w / 2 - (size / 2 + 2), by = -h / 2 + (size / 2 + 2);
196
- return [node.iconUrl
197
- ? Glyph.image(node.iconUrl, bx - size / 2, by - size / 2, size, size, { 'pointer-events': 'none' })
198
- : Glyph.text(node.icon, bx, by, { fill: node.strokeColor || '#fff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: `${size}px`, 'pointer-events': 'none' })];
199
- }
200
- return [];
201
- }
202
-
203
- /** The name inside the box (wrapped, top-left) or below a notation shape; images have none. */
204
- _renderLabel(g, doc) {
205
- const node = this.node;
206
- if (node.shape === 'image') return;
207
- const text = (x, y, content, attrs) => {
208
- const t = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
209
- for (const [k, v] of Object.entries({ x, y, 'pointer-events': 'none', 'font-family': 'sans-serif', ...attrs })) t.setAttribute(k, String(v));
210
- t.textContent = content;
211
- g.appendChild(t);
212
- };
213
- const { width: w, height: h } = node;
214
- const inside = (node.labelPosition || (node.shape === 'note' ? 'inside' : 'below')) === 'inside';
215
- if (inside) {
216
- const font = TextStyle.svgAttributes(this.textStyle, { defaultPixels: 10 });
217
- const PAD = 6, FONT_SIZE = font['font-size'], LINE_HEIGHT = Math.round(FONT_SIZE * 1.2);
218
- const lines = LabelHelper.wrapLabelLines(this.name, w - PAD * 2, FONT_SIZE, h - PAD * 2, LINE_HEIGHT);
219
- // Top left unless the occurrence says otherwise, as in Archi.
220
- const at = TextStyle.block(this.textStyle, { left: this.x - w / 2, right: this.x + w / 2, top: this.y - h / 2, bottom: this.y + h / 2 },
221
- lines.length, { pixels: FONT_SIZE, lineHeight: LINE_HEIGHT, pad: PAD });
222
- lines.forEach((line, i) => text(at.x, at.baseline + i * LINE_HEIGHT, line,
223
- { ...font, fill: this.textColor, 'font-size': `${FONT_SIZE}px`, 'text-anchor': at.textAnchor }));
224
- } else {
225
- text(this.x, this.y + h / 2 + 9, this.name, { fill: '#333', 'font-size': '9px', 'text-anchor': 'middle' });
226
- }
227
- }
228
- }
229
-
230
- /** The corner badge's size in pixels (badgeSize). */
231
- ArchimateElementTemplate.BADGE_SIZE = 16;
1
+ import { TextStyle, TEXT_STYLE_KEYS } from '../TextStyle.js';
2
+ import { GlyphTemplate } from '../templates/GlyphTemplate.js';
3
+ import { Glyph } from '../glyphs/Glyph.js';
4
+ import { glyphFor } from '../glyphs/index.js';
5
+ import { ArchimateDiagram } from '../diagrams/ArchimateDiagram.js';
6
+ import { LabelHelper } from '../diagrams/LabelHelper.js';
7
+
8
+ /** Paint a view can set on one occurrence: template property → appearance key (same names). */
9
+ const APPEARANCE = ['fillColor', 'fillOpacity', 'strokeColor', 'strokeWidth', 'strokeOpacity', 'gradient', 'textColor', ...TEXT_STYLE_KEYS];
10
+
11
+ /**
12
+ * ArchimateElementTemplate — one element (or note, or image) on an ArchiMate
13
+ * view, as an editor template. It keeps no copy of its own: position, size,
14
+ * stacking and paint are read from and written to its ElementInstance, and
15
+ * what it looks like comes from the model element through the same
16
+ * enrichment the graph engine uses (ArchimateDiagram.buildNode: layer colour,
17
+ * corner style, notation shape, type badge, label position). So every editor
18
+ * command that moves, resizes, restacks or repaints a template edits the view
19
+ * itself, and its undo does too.
20
+ *
21
+ * The shape is the node's glyph: a box (sharp, rounded or chamfered by
22
+ * aspect) with the type's glyph as a corner badge, or the full notation shape
23
+ * when distinct shapes are on; notes and images draw their own glyph.
24
+ */
25
+ export class ArchimateElementTemplate extends GlyphTemplate {
26
+ /** ArchiMate notation is not rotated. */
27
+ static rotatable = false;
28
+
29
+ /** The format painter copies its appearance (what the view's instance holds). */
30
+ static formatKeys = Object.freeze([...APPEARANCE]);
31
+
32
+ /**
33
+ * @param {ElementInstance} instance - What this template shows, and where
34
+ * @param {ArchimateModel} model
35
+ * @param {Object} [options]
36
+ * @param {Object} [options.nodeOptions] - ArchimateDiagram.buildNode options
37
+ * (archimateVersion, useDistinctShapes, layerColors, elementWidth, ...)
38
+ */
39
+ constructor(instance, model, { nodeOptions = {} } = {}) {
40
+ super(instance.id, undefined, {});
41
+ this.instance = instance;
42
+ this.model = model;
43
+ this.nodeOptions = nodeOptions;
44
+ /** A host's live status, `{text, color}` (the editor's elementStatus), or null: drawn as a pill. */
45
+ this.status = null;
46
+ }
47
+
48
+ /** State lives on the instance and the model element, not here. @protected */
49
+ _initState() {}
50
+
51
+ /** The element as the ArchiMate notation draws it on this view. */
52
+ get node() {
53
+ const projected = this.instance.project(this.model) ?? { id: this.instance.id, name: this.instance.elementId };
54
+ return ArchimateDiagram.buildNode(projected, this.nodeOptions);
55
+ }
56
+
57
+ get name() { return this.instance.getDisplayName(this.model); }
58
+ /** Renaming one occurrence labels it on this view; the element's own name is left alone. */
59
+ set name(value) {
60
+ const own = this.instance.resolve(this.model)?.name;
61
+ this.instance.labelOverride = value && value !== own ? value : null;
62
+ }
63
+
64
+ get x() { return this.instance.x; }
65
+ set x(value) { this.instance.x = value; }
66
+ get y() { return this.instance.y; }
67
+ set y(value) { this.instance.y = value; }
68
+ get zIndex() { return this.instance.zIndex; }
69
+ set zIndex(value) { this.instance.zIndex = value ?? 0; }
70
+ get visible() { return this.instance.visible; }
71
+ set visible(value) { this.instance.visible = value !== false; }
72
+
73
+ get width() { return this.node.width; }
74
+ set width(value) { this._setSize('width', value); }
75
+ get height() { return this.node.height; }
76
+ set height(value) { this._setSize('height', value); }
77
+
78
+ get fillColor() { return this.node.color; }
79
+ set fillColor(value) { this._setAppearance('fillColor', value); }
80
+ get fillOpacity() { return this.instance.appearance.fillOpacity ?? 1; }
81
+ set fillOpacity(value) { this._setAppearance('fillOpacity', value); }
82
+ get strokeColor() { return this.node.strokeColor; }
83
+ set strokeColor(value) { this._setAppearance('strokeColor', value); }
84
+ get strokeWidth() { return this.node.strokeWidth; }
85
+ set strokeWidth(value) { this._setAppearance('strokeWidth', value); }
86
+ get strokeOpacity() { return this.instance.appearance.strokeOpacity ?? 1; }
87
+ set strokeOpacity(value) { this._setAppearance('strokeOpacity', value); }
88
+ get gradient() { return this.instance.appearance.gradient; }
89
+ set gradient(value) { this._setAppearance('gradient', value); }
90
+ get textColor() { return this.instance.appearance.textColor ?? '#222'; }
91
+ set textColor(value) { this._setAppearance('textColor', value); }
92
+ get fontFamily() { return this.instance.appearance.fontFamily; }
93
+ set fontFamily(value) { this._setAppearance('fontFamily', value); }
94
+ get fontSize() { return this.instance.appearance.fontSize; }
95
+ set fontSize(value) { this._setAppearance('fontSize', value); }
96
+ get fontWeight() { return this.instance.appearance.fontWeight; }
97
+ set fontWeight(value) { this._setAppearance('fontWeight', value); }
98
+ get fontStyle() { return this.instance.appearance.fontStyle; }
99
+ set fontStyle(value) { this._setAppearance('fontStyle', value); }
100
+ get textDecoration() { return this.instance.appearance.textDecoration; }
101
+ set textDecoration(value) { this._setAppearance('textDecoration', value); }
102
+ get textAlign() { return this.instance.appearance.textAlign; }
103
+ set textAlign(value) { this._setAppearance('textAlign', value); }
104
+ get textPosition() { return this.instance.appearance.textPosition; }
105
+ set textPosition(value) { this._setAppearance('textPosition', value); }
106
+
107
+ /**
108
+ * What this occurrence sets itself for a property (undefined when it shows
109
+ * the element's own), for commands that must undo to "unset".
110
+ */
111
+ ownProperty(key) {
112
+ if (APPEARANCE.includes(key)) return this.instance.appearance[key];
113
+ if (key === 'width' || key === 'height') return this.instance.size[key];
114
+ if (key === 'name') return this.instance.labelOverride ?? undefined;
115
+ return this[key];
116
+ }
117
+
118
+ /** undefined puts the occurrence back on the element's own value. @private */
119
+ _setSize(key, value) {
120
+ if (value === undefined) delete this.instance.size[key];
121
+ else this.instance.size = { ...this.instance.size, [key]: value };
122
+ }
123
+
124
+ /** @private */
125
+ _setAppearance(key, value) {
126
+ if (!APPEARANCE.includes(key)) throw new TypeError(`Not an appearance property: ${key}`);
127
+ if (value === undefined) delete this.instance.appearance[key];
128
+ else this.instance.appearance = { ...this.instance.appearance, [key]: value };
129
+ }
130
+
131
+ glyph() {
132
+ return glyphFor(this.node.shape);
133
+ }
134
+
135
+ glyphContext() {
136
+ const node = this.node;
137
+ return Glyph.context({
138
+ R: Math.max(node.width, node.height) / 2,
139
+ size: { w: node.width, h: node.height },
140
+ color: node.color,
141
+ cornerStyle: node.cornerStyle,
142
+ strokeColor: node.strokeColor,
143
+ strokeWidth: node.strokeWidth,
144
+ icon: node.icon,
145
+ iconUrl: node.iconUrl,
146
+ ownFill: this.instance.appearance.fillColor ?? null,
147
+ });
148
+ }
149
+
150
+ /**
151
+ * The badge's size: the same whatever the box's size (as Archi draws it), so
152
+ * resizing a box, or a parent growing to hold what is dropped in it, leaves
153
+ * it as it was; only a box too small for it gets a smaller one.
154
+ */
155
+ static badgeSize(w, h) {
156
+ return Math.max(8, Math.min(ArchimateElementTemplate.BADGE_SIZE, Math.round(Math.min(w, h) * 0.4)));
157
+ }
158
+
159
+ /**
160
+ * The type badge in the top-right corner: its glyph, or an icon (text or
161
+ * image), a fixed size (badgeSize). As in the graph engine, a notation shape
162
+ * gets no glyph badge (buildNode leaves iconShape unset) but keeps an icon the element sets.
163
+ */
164
+ decorations(ctx) {
165
+ return [...this._badge(ctx), ...this.statusPill(ctx)];
166
+ }
167
+
168
+ /**
169
+ * The host's status (`status`), as a small pill inside the box's bottom
170
+ * right corner: its text, on its colour. Nothing without one.
171
+ */
172
+ statusPill({ w, h }) {
173
+ const text = this.status?.text;
174
+ if (!text) return [];
175
+ const label = String(text);
176
+ const ph = 12, pw = Math.min(w - 6, label.length * 5.5 + 10);
177
+ const x = w / 2 - pw - 3, y = h / 2 - ph - 3;
178
+ return [Glyph.group([
179
+ Glyph.rect(x, y, pw, ph, this.status.color || '#455a64', null, { rx: 6, ry: 6 }),
180
+ Glyph.text(label, x + pw / 2, y + ph / 2, { fill: '#ffffff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: '8px' }),
181
+ ], { 'pointer-events': 'none', class: 'element-status' })];
182
+ }
183
+
184
+ /** The type badge (decorations). @private */
185
+ _badge(ctx) {
186
+ const node = this.node;
187
+ const { w, h } = ctx;
188
+ const fixed = ArchimateElementTemplate.badgeSize(w, h);
189
+ if (node.iconShape) {
190
+ const badge = fixed;
191
+ const inset = badge / 2 + 3;
192
+ const parts = glyphFor(node.iconShape).parts(Glyph.context({
193
+ R: badge / 2, size: { w: badge, h: badge * 0.78 }, color: 'none', strokeColor: node.strokeColor || '#000000', strokeWidth: 1,
194
+ }));
195
+ return [Glyph.group([parts], { transform: `translate(${w / 2 - inset},${-h / 2 + inset})`, 'pointer-events': 'none' })];
196
+ }
197
+ if (node.icon || node.iconUrl) {
198
+ const size = fixed;
199
+ const bx = w / 2 - (size / 2 + 2), by = -h / 2 + (size / 2 + 2);
200
+ return [node.iconUrl
201
+ ? Glyph.image(node.iconUrl, bx - size / 2, by - size / 2, size, size, { 'pointer-events': 'none' })
202
+ : Glyph.text(node.icon, bx, by, { fill: node.strokeColor || '#fff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: `${size}px`, 'pointer-events': 'none' })];
203
+ }
204
+ return [];
205
+ }
206
+
207
+ /** The name inside the box (wrapped, top-left) or below a notation shape; images have none. */
208
+ _renderLabel(g, doc) {
209
+ const node = this.node;
210
+ if (node.shape === 'image') return;
211
+ const text = (x, y, content, attrs) => {
212
+ const t = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
213
+ for (const [k, v] of Object.entries({ x, y, 'pointer-events': 'none', 'font-family': 'sans-serif', ...attrs })) t.setAttribute(k, String(v));
214
+ t.textContent = content;
215
+ g.appendChild(t);
216
+ };
217
+ const { width: w, height: h } = node;
218
+ const inside = (node.labelPosition || (node.shape === 'note' ? 'inside' : 'below')) === 'inside';
219
+ if (inside) {
220
+ const font = TextStyle.svgAttributes(this.textStyle, { defaultPixels: 10 });
221
+ const PAD = 6, FONT_SIZE = font['font-size'], LINE_HEIGHT = Math.round(FONT_SIZE * 1.2);
222
+ const lines = LabelHelper.wrapLabelLines(this.name, w - PAD * 2, FONT_SIZE, h - PAD * 2, LINE_HEIGHT);
223
+ // Top left unless the occurrence says otherwise, as in Archi.
224
+ const at = TextStyle.block(this.textStyle, { left: this.x - w / 2, right: this.x + w / 2, top: this.y - h / 2, bottom: this.y + h / 2 },
225
+ lines.length, { pixels: FONT_SIZE, lineHeight: LINE_HEIGHT, pad: PAD });
226
+ lines.forEach((line, i) => text(at.x, at.baseline + i * LINE_HEIGHT, line,
227
+ { ...font, fill: this.textColor, 'font-size': `${FONT_SIZE}px`, 'text-anchor': at.textAnchor }));
228
+ } else {
229
+ text(this.x, this.y + h / 2 + 9, this.name, { fill: '#333', 'font-size': '9px', 'text-anchor': 'middle' });
230
+ }
231
+ }
232
+ }
233
+
234
+ /** The corner badge's size in pixels (badgeSize). */
235
+ ArchimateElementTemplate.BADGE_SIZE = 16;