@peter.prib/renderengine 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49641 -39747
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +206 -0
- package/examples/decision-workbench.html +26 -0
- package/package.json +7 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/index.js +3 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
- package/src/workspace/parts/ViewsPart.js +196 -0
- package/src/workspace/styles.js +36 -1
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { RectangleTemplate } from './RectangleTemplate.js';
|
|
2
|
-
import { svgEl } from './svg.js';
|
|
3
|
-
|
|
4
|
-
/** SwimlaneTemplate — lane / pool / partition: large box with a title band. */
|
|
5
|
-
export class SwimlaneTemplate extends RectangleTemplate {
|
|
6
|
-
static
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
1
|
+
import { RectangleTemplate } from './RectangleTemplate.js';
|
|
2
|
+
import { svgEl } from './svg.js';
|
|
3
|
+
|
|
4
|
+
/** SwimlaneTemplate — lane / pool / partition: large box with a title band. */
|
|
5
|
+
export class SwimlaneTemplate extends RectangleTemplate {
|
|
6
|
+
static isContainer = true;
|
|
7
|
+
static defaults = { width: 420, height: 160, fillColor: '#f8fafc' };
|
|
8
|
+
|
|
9
|
+
get cornerRadius() { return 0; }
|
|
10
|
+
|
|
11
|
+
get headerHeight() { return Math.min(26, this.height / 4); }
|
|
12
|
+
|
|
13
|
+
get labelAnchor() { return { x: this.x, y: this.top + this.headerHeight / 2 }; }
|
|
14
|
+
|
|
15
|
+
shapeElements(doc) {
|
|
16
|
+
const y = this.top + this.headerHeight;
|
|
17
|
+
return [...super.shapeElements(doc), svgEl('line', this.line({ x1: this.left, y1: y, x2: this.right, y2: y }), doc)];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,212 +1,219 @@
|
|
|
1
|
-
import { svgEl } from './svg.js';
|
|
2
|
-
import { rayExitDistance, rotateVector } from './outline.js';
|
|
3
|
-
import { TextStyle, TEXT_STYLE_KEYS } from '../TextStyle.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Template — abstract base for diagram shapes.
|
|
7
|
-
*
|
|
8
|
-
* Owns everything shapes have in common: geometry/paint state, the element
|
|
9
|
-
* group, rotation, label and editor wiring. A subclass supplies only its
|
|
10
|
-
* outline via shapeElements(), so the canvas and the palette preview are
|
|
11
|
-
* drawn by the same code and cannot drift apart.
|
|
12
|
-
*
|
|
13
|
-
* Geometry is centre-based: (x, y) is the centre of a width x height box.
|
|
14
|
-
*/
|
|
15
|
-
export class Template {
|
|
16
|
-
/** Per-shape construction defaults; subclasses override. */
|
|
17
|
-
static defaults = { width: 120, height: 60 };
|
|
18
|
-
|
|
19
|
-
/** Whether the editor offers a rotate handle for this kind of shape. */
|
|
20
|
-
static rotatable = true;
|
|
21
|
-
|
|
22
|
-
/** What the format painter copies from a shape and gives to another: its paint and text style. */
|
|
23
|
-
static formatKeys = Object.freeze(['fillColor', 'strokeColor', 'strokeWidth', 'textColor', ...TEXT_STYLE_KEYS]);
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The shape's format (`formatKeys`), as it holds it: a value it takes from
|
|
27
|
-
* elsewhere (an ArchiMate shape's layer colour) is left unset, so pasting
|
|
28
|
-
* it gives the other shape its own default too.
|
|
29
|
-
* @returns {Object}
|
|
30
|
-
*/
|
|
31
|
-
format() {
|
|
32
|
-
const own = typeof this.ownProperty === 'function' ? (k) => this.ownProperty(k) : (k) => this[k];
|
|
33
|
-
return Object.fromEntries(this.constructor.formatKeys.map(k => [k, own(k)]));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
constructor(id, name, options = {}) {
|
|
37
|
-
if (new.target === Template) throw new TypeError('Template is abstract — subclass it');
|
|
38
|
-
this.id = id;
|
|
39
|
-
this.rotation = options.rotation ?? 0;
|
|
40
|
-
this.connections = options.connections || [];
|
|
41
|
-
this._initState(name, options);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Set the name, placement and paint this shape keeps as its own fields. A
|
|
46
|
-
* subclass that keeps them elsewhere (an ArchiMate element keeps them on
|
|
47
|
-
* its view instance and model element) overrides this.
|
|
48
|
-
* @protected
|
|
49
|
-
*/
|
|
50
|
-
_initState(name, options) {
|
|
51
|
-
const d = this.constructor.defaults;
|
|
52
|
-
this.name = name;
|
|
53
|
-
this.x = options.x ?? 0;
|
|
54
|
-
this.y = options.y ?? 0;
|
|
55
|
-
this.zIndex = options.zIndex ?? 0;
|
|
56
|
-
this.visible = options.visible !== false;
|
|
57
|
-
this.width = options.width ?? d.width;
|
|
58
|
-
this.height = options.height ?? d.height;
|
|
59
|
-
this.fillColor = options.fillColor ?? d.fillColor ?? '#e8f4f8';
|
|
60
|
-
this.fillOpacity = options.fillOpacity ?? d.fillOpacity ?? 1;
|
|
61
|
-
this.strokeColor = options.strokeColor ?? d.strokeColor ?? '#333';
|
|
62
|
-
this.strokeWidth = options.strokeWidth ?? d.strokeWidth ?? 2;
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
get
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
1
|
+
import { svgEl } from './svg.js';
|
|
2
|
+
import { rayExitDistance, rotateVector } from './outline.js';
|
|
3
|
+
import { TextStyle, TEXT_STYLE_KEYS } from '../TextStyle.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Template — abstract base for diagram shapes.
|
|
7
|
+
*
|
|
8
|
+
* Owns everything shapes have in common: geometry/paint state, the element
|
|
9
|
+
* group, rotation, label and editor wiring. A subclass supplies only its
|
|
10
|
+
* outline via shapeElements(), so the canvas and the palette preview are
|
|
11
|
+
* drawn by the same code and cannot drift apart.
|
|
12
|
+
*
|
|
13
|
+
* Geometry is centre-based: (x, y) is the centre of a width x height box.
|
|
14
|
+
*/
|
|
15
|
+
export class Template {
|
|
16
|
+
/** Per-shape construction defaults; subclasses override. */
|
|
17
|
+
static defaults = { width: 120, height: 60 };
|
|
18
|
+
|
|
19
|
+
/** Whether the editor offers a rotate handle for this kind of shape. */
|
|
20
|
+
static rotatable = true;
|
|
21
|
+
|
|
22
|
+
/** What the format painter copies from a shape and gives to another: its paint and text style. */
|
|
23
|
+
static formatKeys = Object.freeze(['fillColor', 'fillOpacity', 'strokeColor', 'strokeWidth', 'strokeOpacity', 'gradient', 'textColor', ...TEXT_STYLE_KEYS]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The shape's format (`formatKeys`), as it holds it: a value it takes from
|
|
27
|
+
* elsewhere (an ArchiMate shape's layer colour) is left unset, so pasting
|
|
28
|
+
* it gives the other shape its own default too.
|
|
29
|
+
* @returns {Object}
|
|
30
|
+
*/
|
|
31
|
+
format() {
|
|
32
|
+
const own = typeof this.ownProperty === 'function' ? (k) => this.ownProperty(k) : (k) => this[k];
|
|
33
|
+
return Object.fromEntries(this.constructor.formatKeys.map(k => [k, own(k)]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
constructor(id, name, options = {}) {
|
|
37
|
+
if (new.target === Template) throw new TypeError('Template is abstract — subclass it');
|
|
38
|
+
this.id = id;
|
|
39
|
+
this.rotation = options.rotation ?? 0;
|
|
40
|
+
this.connections = options.connections || [];
|
|
41
|
+
this._initState(name, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set the name, placement and paint this shape keeps as its own fields. A
|
|
46
|
+
* subclass that keeps them elsewhere (an ArchiMate element keeps them on
|
|
47
|
+
* its view instance and model element) overrides this.
|
|
48
|
+
* @protected
|
|
49
|
+
*/
|
|
50
|
+
_initState(name, options) {
|
|
51
|
+
const d = this.constructor.defaults;
|
|
52
|
+
this.name = name;
|
|
53
|
+
this.x = options.x ?? 0;
|
|
54
|
+
this.y = options.y ?? 0;
|
|
55
|
+
this.zIndex = options.zIndex ?? 0;
|
|
56
|
+
this.visible = options.visible !== false;
|
|
57
|
+
this.width = options.width ?? d.width;
|
|
58
|
+
this.height = options.height ?? d.height;
|
|
59
|
+
this.fillColor = options.fillColor ?? d.fillColor ?? '#e8f4f8';
|
|
60
|
+
this.fillOpacity = options.fillOpacity ?? d.fillOpacity ?? 1;
|
|
61
|
+
this.strokeColor = options.strokeColor ?? d.strokeColor ?? '#333';
|
|
62
|
+
this.strokeWidth = options.strokeWidth ?? d.strokeWidth ?? 2;
|
|
63
|
+
this.strokeOpacity = options.strokeOpacity ?? d.strokeOpacity ?? 1;
|
|
64
|
+
this.gradient = options.gradient ?? d.gradient ?? null;
|
|
65
|
+
this.textColor = options.textColor ?? d.textColor ?? '#333';
|
|
66
|
+
this.parentId = options.parentId ?? null;
|
|
67
|
+
this.isContainer = options.isContainer ?? this.constructor.isContainer ?? false;
|
|
68
|
+
// Text style (TextStyle): unset ones are left off, so the default applies.
|
|
69
|
+
for (const key of TEXT_STYLE_KEYS) {
|
|
70
|
+
const value = options[key] ?? d[key];
|
|
71
|
+
if (value !== undefined && value !== null && value !== '') this[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The text style this shape's name is drawn in (TextStyle). */
|
|
76
|
+
get textStyle() {
|
|
77
|
+
return Object.fromEntries(TEXT_STYLE_KEYS.map(k => [k, this[k]]).filter(([, v]) => v !== undefined));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get left() { return this.x - this.width / 2; }
|
|
81
|
+
get right() { return this.x + this.width / 2; }
|
|
82
|
+
get top() { return this.y - this.height / 2; }
|
|
83
|
+
get bottom() { return this.y + this.height / 2; }
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The visible outline as a closed polygon in unrotated canvas coordinates
|
|
87
|
+
* (curves sampled). Edges attach to it, so it must match shapeElements().
|
|
88
|
+
*/
|
|
89
|
+
outline() {
|
|
90
|
+
return [[this.left, this.top], [this.right, this.top], [this.right, this.bottom], [this.left, this.bottom]];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Where a line heading out from the centre in screen direction (dx, dy)
|
|
95
|
+
* meets the outline, with rotation applied.
|
|
96
|
+
* @returns {{x: number, y: number}}
|
|
97
|
+
*/
|
|
98
|
+
boundaryPoint(dx, dy) {
|
|
99
|
+
const len = Math.hypot(dx, dy);
|
|
100
|
+
if (!len) return { x: this.x, y: this.y };
|
|
101
|
+
const ux = dx / len, uy = dy / len;
|
|
102
|
+
// Rotation preserves length, so the exit distance found in the shape's own
|
|
103
|
+
// frame is the distance along the on-screen direction too.
|
|
104
|
+
const [lx, ly] = rotateVector(ux, uy, -this.rotation);
|
|
105
|
+
const t = rayExitDistance(this.outline(), this.x, this.y, lx, ly);
|
|
106
|
+
return { x: this.x + ux * t, y: this.y + uy * t };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Where a line from (px, py), a point inside the shape, heading in
|
|
111
|
+
* direction (dx, dy) leaves its outline (the shape unrotated).
|
|
112
|
+
* @returns {{x: number, y: number}}
|
|
113
|
+
*/
|
|
114
|
+
exitPoint(px, py, dx, dy) {
|
|
115
|
+
const t = rayExitDistance(this.outline(), px, py, dx, dy);
|
|
116
|
+
return { x: px + dx * t, y: py + dy * t };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Centre of the name text; the z-index line sits just below it. */
|
|
120
|
+
get labelAnchor() { return { x: this.x, y: this.y - 8 }; }
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* SVG nodes forming the outline, positioned in canvas coordinates.
|
|
124
|
+
* @param {Document} doc
|
|
125
|
+
* @returns {SVGElement[]}
|
|
126
|
+
*/
|
|
127
|
+
shapeElements(doc) {
|
|
128
|
+
throw new Error(`${this.constructor.name} must implement shapeElements()`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Fill/stroke attributes for the main outline, merged with `extra`. */
|
|
132
|
+
paint(extra = {}) {
|
|
133
|
+
return {
|
|
134
|
+
fill: this.fillColor,
|
|
135
|
+
'fill-opacity': this.fillOpacity === 1 ? null : this.fillOpacity,
|
|
136
|
+
stroke: this.strokeColor,
|
|
137
|
+
'stroke-width': this.strokeWidth,
|
|
138
|
+
'stroke-opacity': this.strokeOpacity === 1 ? null : this.strokeOpacity,
|
|
139
|
+
...extra,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Stroke-only attributes for interior detail lines. */
|
|
144
|
+
line(extra = {}) {
|
|
145
|
+
return { fill: 'none', stroke: this.strokeColor, 'stroke-width': Math.max(1, this.strokeWidth - 0.5), ...extra };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Draw into the editor's element group and wire interaction. */
|
|
149
|
+
render({ group, editor }) {
|
|
150
|
+
const g = this.renderElement(group.ownerDocument);
|
|
151
|
+
editor.attachElementHandlers(this.id, g);
|
|
152
|
+
group.appendChild(g);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Build the complete element group (outline + label) without attaching it. */
|
|
156
|
+
renderElement(doc = document) {
|
|
157
|
+
const g = svgEl('g', { 'data-id': this.id, 'data-template-id': this.id }, doc);
|
|
158
|
+
if (this.rotation) g.setAttribute('transform', `rotate(${this.rotation} ${this.x} ${this.y})`);
|
|
159
|
+
for (const node of this.shapeElements(doc)) g.appendChild(node);
|
|
160
|
+
this._renderLabel(g, doc);
|
|
161
|
+
return g;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Standalone thumbnail of the outline, scaled to fit a size x size box. */
|
|
165
|
+
renderPreview(size = 40, doc = document) {
|
|
166
|
+
const pad = this.strokeWidth + 2;
|
|
167
|
+
const svg = svgEl('svg', {
|
|
168
|
+
width: size,
|
|
169
|
+
height: size,
|
|
170
|
+
viewBox: `${this.left - pad} ${this.top - pad} ${this.width + pad * 2} ${this.height + pad * 2}`,
|
|
171
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
172
|
+
}, doc);
|
|
173
|
+
for (const node of this.shapeElements(doc)) svg.appendChild(node);
|
|
174
|
+
return svg;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_renderLabel(g, doc) {
|
|
178
|
+
const { x, y } = this.labelAnchor;
|
|
179
|
+
const text = (attrs, content) => {
|
|
180
|
+
const t = svgEl('text', {
|
|
181
|
+
'text-anchor': 'middle', 'dominant-baseline': 'middle', 'pointer-events': 'none', ...attrs,
|
|
182
|
+
}, doc);
|
|
183
|
+
t.textContent = content;
|
|
184
|
+
g.appendChild(t);
|
|
185
|
+
};
|
|
186
|
+
const style = this.textStyle;
|
|
187
|
+
const font = TextStyle.svgAttributes(style, { defaultPixels: 12 });
|
|
188
|
+
if (TextStyle.placesText(style)) {
|
|
189
|
+
// Placed in the shape's box by its alignment and position (centred, middle where unset).
|
|
190
|
+
const at = TextStyle.block(style, this, 1, { pixels: font['font-size'], defaults: { textAlign: 'center', textPosition: 'middle' } });
|
|
191
|
+
const placed = { 'text-anchor': at.textAnchor, 'dominant-baseline': 'auto' };
|
|
192
|
+
if (this.name) text({ x: at.x, y: at.baseline, ...font, ...placed, fill: this.textColor }, this.name);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.name) text({ x, y, ...font, fill: this.textColor }, this.name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
serialize() {
|
|
199
|
+
return {
|
|
200
|
+
id: this.id,
|
|
201
|
+
name: this.name,
|
|
202
|
+
x: this.x,
|
|
203
|
+
y: this.y,
|
|
204
|
+
zIndex: this.zIndex,
|
|
205
|
+
visible: this.visible,
|
|
206
|
+
width: this.width,
|
|
207
|
+
height: this.height,
|
|
208
|
+
rotation: this.rotation,
|
|
209
|
+
fillColor: this.fillColor,
|
|
210
|
+
fillOpacity: this.fillOpacity,
|
|
211
|
+
strokeColor: this.strokeColor,
|
|
212
|
+
strokeWidth: this.strokeWidth,
|
|
213
|
+
textColor: this.textColor,
|
|
214
|
+
...(this.parentId ? { parentId: this.parentId } : {}),
|
|
215
|
+
...(this.isContainer ? { isContainer: true } : {}),
|
|
216
|
+
...this.textStyle,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { parseArchiFile, toArchiFile } from './archimateArchive.js';
|
|
2
|
+
import { toArchimateModel, toArchiExport } from './archimateArchiFormat.js';
|
|
3
|
+
import { parseExchangeModel, toExchangeXML } from './archimateExchangeFormat.js';
|
|
4
|
+
import { ArchimateModelChecker } from './charting/diagrams/ArchimateModelChecker.js';
|
|
5
|
+
import { reportHtml } from './charting/report/reportHtml.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CLI and headless automation helpers: load, save, check and report
|
|
9
|
+
* ArchiMate models without a browser.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load an ArchiMate model from a file path or in-memory bytes/string.
|
|
14
|
+
* Supports .archimate (XML and zip archives with pictures) and
|
|
15
|
+
* Open Group Exchange Format (.xml).
|
|
16
|
+
*
|
|
17
|
+
* @param {string|Uint8Array|ArrayBuffer} input - File path, XML string, or archive bytes
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {'auto'|'archi'|'exchange'} [options.format='auto']
|
|
20
|
+
* @returns {ArchimateModel}
|
|
21
|
+
*/
|
|
22
|
+
export function loadModel(input, { format = 'auto' } = {}) {
|
|
23
|
+
let data = input;
|
|
24
|
+
if (typeof input === 'string' && !input.includes('<') && !input.includes('{')) {
|
|
25
|
+
try {
|
|
26
|
+
const fs = globalThis.process?.getBuiltinModule ? globalThis.process.getBuiltinModule('node:fs') : null;
|
|
27
|
+
if (fs && fs.existsSync(input)) {
|
|
28
|
+
data = fs.readFileSync(input);
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isExchange = format === 'exchange' || (format === 'auto' && typeof data === 'string'
|
|
34
|
+
? /opengroup\.org\/xsd\/archimate/.test(data)
|
|
35
|
+
: (data instanceof Uint8Array || Buffer.isBuffer(data)) && /opengroup\.org\/xsd\/archimate/.test(new TextDecoder().decode(data.slice(0, 4000))));
|
|
36
|
+
|
|
37
|
+
if (isExchange) {
|
|
38
|
+
const text = typeof data === 'string' ? data : new TextDecoder().decode(data);
|
|
39
|
+
return toArchimateModel(parseExchangeModel(text));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return toArchimateModel(parseArchiFile(data));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run model validation checks over an ArchiMate model.
|
|
47
|
+
*
|
|
48
|
+
* @param {ArchimateModel} model
|
|
49
|
+
* @param {Object} [options]
|
|
50
|
+
* @param {string} [options.severity] - Filter by severity ('error', 'warning', 'info')
|
|
51
|
+
* @returns {{ findings: Array<Object>, summary: { total: number, errors: number, warnings: number, infos: number } }}
|
|
52
|
+
*/
|
|
53
|
+
export function checkModel(model, { severity = null } = {}) {
|
|
54
|
+
const checker = new ArchimateModelChecker(model);
|
|
55
|
+
let findings = checker.run();
|
|
56
|
+
|
|
57
|
+
if (severity) {
|
|
58
|
+
findings = findings.filter(f => f.severity === severity);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const summary = {
|
|
62
|
+
total: findings.length,
|
|
63
|
+
errors: findings.filter(f => f.severity === 'error').length,
|
|
64
|
+
warnings: findings.filter(f => f.severity === 'warning').length,
|
|
65
|
+
infos: findings.filter(f => f.severity === 'info' || !f.severity).length,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return { findings, summary };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Save an ArchiMate model to Archi format, Open Group Exchange format, or JSON.
|
|
73
|
+
*
|
|
74
|
+
* @param {ArchimateModel} model
|
|
75
|
+
* @param {Object} [options]
|
|
76
|
+
* @param {'archi'|'exchange'|'json'} [options.format='archi']
|
|
77
|
+
* @returns {{ data: string|Uint8Array, mimeType: string, archive: boolean }}
|
|
78
|
+
*/
|
|
79
|
+
export function saveModel(model, { format = 'archi' } = {}) {
|
|
80
|
+
const fmt = String(format).toLowerCase();
|
|
81
|
+
if (fmt === 'exchange' || fmt === 'xml-exchange') {
|
|
82
|
+
const xml = toExchangeXML(...toArchiExport(model));
|
|
83
|
+
return { data: xml, mimeType: 'application/xml', archive: false };
|
|
84
|
+
}
|
|
85
|
+
if (fmt === 'json') {
|
|
86
|
+
const payload = typeof model.toJSON === 'function'
|
|
87
|
+
? model.toJSON()
|
|
88
|
+
: {
|
|
89
|
+
id: model.id,
|
|
90
|
+
name: model.name,
|
|
91
|
+
documentation: model.documentation,
|
|
92
|
+
elements: typeof model.elements === 'function' ? model.elements() : model.elements,
|
|
93
|
+
relationships: typeof model.relationships === 'function' ? model.relationships() : model.relationships,
|
|
94
|
+
views: typeof model.views === 'function'
|
|
95
|
+
? model.views().map(v => (typeof v.toJSON === 'function' ? v.toJSON() : v))
|
|
96
|
+
: model.views,
|
|
97
|
+
};
|
|
98
|
+
const json = JSON.stringify(payload, null, 2);
|
|
99
|
+
return { data: json, mimeType: 'application/json', archive: false };
|
|
100
|
+
}
|
|
101
|
+
// Default: Archi format (.archimate: plain XML or zip archive with pictures)
|
|
102
|
+
return toArchiFile(...toArchiExport(model));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate a standalone HTML report of the model.
|
|
107
|
+
*
|
|
108
|
+
* @param {ArchimateModel} model
|
|
109
|
+
* @param {Object} [options]
|
|
110
|
+
* @param {string} [options.title]
|
|
111
|
+
* @returns {string} HTML content
|
|
112
|
+
*/
|
|
113
|
+
export function reportModel(model, { title = model.name || 'Model Report' } = {}) {
|
|
114
|
+
const reportData = model.report({ title }).data();
|
|
115
|
+
return reportHtml(reportData);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Format check findings into a human-readable string.
|
|
120
|
+
*
|
|
121
|
+
* @param {Array<Object>} findings
|
|
122
|
+
* @param {Object} [summary]
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
export function formatFindings(findings, summary = null) {
|
|
126
|
+
if (!findings.length) {
|
|
127
|
+
return 'No issues found. Model is clean.';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const lines = [];
|
|
131
|
+
const iconOf = (s) => (s === 'error' ? '✖ [ERROR]' : s === 'warning' ? '▲ [WARN] ' : 'ℹ [INFO] ');
|
|
132
|
+
|
|
133
|
+
for (const f of findings) {
|
|
134
|
+
const sev = iconOf(f.severity);
|
|
135
|
+
const code = f.code ? ` (${f.code})` : '';
|
|
136
|
+
const ids = f.ids?.length ? ` [${f.ids.join(', ')}]` : '';
|
|
137
|
+
lines.push(`${sev}${code} ${f.message}${ids}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (summary) {
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push(`Total issues: ${summary.total} (${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos)`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|