@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,1701 +1,1735 @@
1
- import { ArchimateNotation } from './archimate.js';
2
- import { archiElementType, typeProfileId, typeDisplayName, ARCHI_TYPE_FEATURE, ARCHI_TYPE_PROFILE_PREFIX } from './archimateArchiTypes.js';
3
- import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
4
- import './charting/diagrams/CanvasView.js'; // registers the canvas view type
5
- import { ElementInstance } from './charting/diagrams/ElementInstance.js';
6
- import { ViewObjectInstance } from './charting/diagrams/ViewObjectInstance.js';
7
- import { RelationshipInstance } from './charting/diagrams/RelationshipInstance.js';
8
- import { ViewConnectionInstance } from './charting/diagrams/ViewConnectionInstance.js';
9
-
10
- /**
11
- * ArchimateArchiFormat — pure import/export helpers for Archi’s native
12
- * `.archimate` XML format (not the Open Group Model Exchange File Format).
13
- *
14
- * The module maps Archi model elements, relationships, and view/visual
15
- * children to renderengine’s `archimateDiagram` inputs, and back again.
16
- */
17
-
18
- export const ARCHI_VISUAL_OBJECTS = Object.freeze([
19
- 'diagram-model-note',
20
- 'diagram-model-group',
21
- 'diagram-model-connection',
22
- 'diagram-model-image',
23
- 'diagram-model-reference',
24
- 'sketch-model-sticky',
25
- 'sketch-model-actor',
26
- 'canvas-model-block',
27
- 'canvas-model-sticky',
28
- 'canvas-model-image',
29
- ]);
30
-
31
- export const ARCHI_VIEW_TYPES = Object.freeze([
32
- 'archimate-diagram-model',
33
- 'sketch-model',
34
- 'canvas-model',
35
- ]);
36
-
37
- const ARCHI_NAMESPACE = 'http://www.archimatetool.com/archimate';
38
- /** Archi's canvas classes live in their own namespace. */
39
- const ARCHI_CANVAS_NAMESPACE = 'http://www.archimatetool.com/archimate/canvas';
40
-
41
- /** Lightweight XML element descriptor used by both the DOMParser wrapper and
42
- * the Node fallback parser. Mirrors the small subset of DOM we need. */
43
- export class ArchiXMLElement {
44
- constructor(tagName, attributes = {}) {
45
- this.tagName = tagName;
46
- this.attributes = attributes;
47
- this.children = [];
48
- this.textContent = '';
49
- }
50
-
51
- getAttribute(name) {
52
- return this.attributes[name] ?? null;
53
- }
54
-
55
- querySelectorAll(tagName) {
56
- const out = [];
57
- for (const child of this.children) {
58
- if (child.tagName === tagName) out.push(child);
59
- out.push(...child.querySelectorAll(tagName));
60
- }
61
- return out;
62
- }
63
-
64
- querySelector(tagName) {
65
- for (const child of this.children) {
66
- if (child.tagName === tagName) return child;
67
- const deep = child.querySelector(tagName);
68
- if (deep) return deep;
69
- }
70
- return null;
71
- }
72
- }
73
-
74
- /** Parse an XML string into an `ArchiXMLElement` tree. Uses DOMParser when
75
- * available (browser), otherwise a minimal dependency-free fallback. */
76
- export function parseXML(xml) {
77
- if (typeof DOMParser !== 'undefined') {
78
- const doc = new DOMParser().parseFromString(xml, 'application/xml');
79
- const parserError = doc.querySelector('parsererror');
80
- if (parserError) throw new Error('XML parse error');
81
- return _fromDOMNode(doc.documentElement);
82
- }
83
- return _parseXMLFallback(xml);
84
- }
85
-
86
- function _fromDOMNode(node) {
87
- const attrs = {};
88
- for (const attr of node.attributes || []) {
89
- attrs[attr.name] = attr.value;
90
- }
91
- const el = new ArchiXMLElement(node.tagName, attrs);
92
- for (const child of node.childNodes || []) {
93
- if (child.nodeType === 1) { // ELEMENT_NODE
94
- el.children.push(_fromDOMNode(child));
95
- } else if (child.nodeType === 3) { // TEXT_NODE
96
- el.textContent += child.textContent;
97
- }
98
- }
99
- if (!el.textContent.trim()) el.textContent = ''; // indentation between elements, not text
100
- return el;
101
- }
102
-
103
- function _parseXMLFallback(xml) {
104
- // XML 1.0 (2.11): a parser sees every line break as a newline; a CR written as 
 survives (decoded later).
105
- const trimmed = xml.replace(/\r\n?/g, '\n').trim();
106
- const root = _parseNode(trimmed, { pos: 0 });
107
- if (!root) throw new Error('XML parse error: no root element');
108
- return root;
109
- }
110
-
111
- function _parseNode(xml, state) {
112
- _skipWhitespace(xml, state);
113
- if (state.pos >= xml.length || xml[state.pos] !== '<') return null;
114
-
115
- // Skip XML declarations, processing instructions and comments.
116
- if (xml.slice(state.pos, state.pos + 2) === '<?') {
117
- const end = xml.indexOf('?>', state.pos);
118
- state.pos = end === -1 ? xml.length : end + 2;
119
- return _parseNode(xml, state);
120
- }
121
- if (xml.startsWith('<!--', state.pos)) {
122
- const end = xml.indexOf('-->', state.pos);
123
- state.pos = end === -1 ? xml.length : end + 3;
124
- return _parseNode(xml, state);
125
- }
126
-
127
- // A quoted attribute value may hold '>' or the other quote character.
128
- const tagMatch = xml.slice(state.pos).match(/^<([^>\s/]+)((?:[^>"']|"[^"]*"|'[^']*')*)>/);
129
- if (!tagMatch) throw new Error('XML parse error: malformed tag');
130
- const tagName = tagMatch[1];
131
- const attrText = tagMatch[2];
132
- state.pos += tagMatch[0].length;
133
-
134
- const isSelfClosing = attrText.trimEnd().endsWith('/');
135
- const attributes = _parseAttributes(attrText);
136
- const node = new ArchiXMLElement(tagName, attributes);
137
-
138
- if (isSelfClosing) {
139
- return node;
140
- }
141
-
142
- // Whitespace is text: a content's leading spaces are its own (Archi uses them to clear a picture);
143
- // indentation between elements is dropped below, once the element is read.
144
- while (state.pos < xml.length) {
145
- if (xml.slice(state.pos, state.pos + 2) === '</') {
146
- const closeMatch = xml.slice(state.pos).match(/^<\/([^>]+)>/);
147
- if (!closeMatch || closeMatch[1] !== tagName) {
148
- throw new Error(`XML parse error: mismatched closing tag for ${tagName}`);
149
- }
150
- state.pos += closeMatch[0].length;
151
- if (!node.textContent.trim()) node.textContent = ''; // indentation between elements, not text
152
- return node;
153
- }
154
- if (xml.startsWith('<![CDATA[', state.pos)) {
155
- const end = xml.indexOf(']]>', state.pos);
156
- node.textContent += xml.slice(state.pos + 9, end === -1 ? xml.length : end);
157
- state.pos = end === -1 ? xml.length : end + 3;
158
- } else if (xml.startsWith('<!--', state.pos)) {
159
- const end = xml.indexOf('-->', state.pos);
160
- state.pos = end === -1 ? xml.length : end + 3;
161
- } else if (xml[state.pos] === '<') {
162
- const child = _parseNode(xml, state);
163
- if (child) node.children.push(child);
164
- } else {
165
- const textEnd = xml.indexOf('<', state.pos);
166
- node.textContent += _unescapeXml(xml.slice(state.pos, textEnd === -1 ? xml.length : textEnd));
167
- state.pos = textEnd === -1 ? xml.length : textEnd;
168
- }
169
- }
170
- if (!node.textContent.trim()) node.textContent = ''; // indentation between elements, not text
171
- return node;
172
- }
173
-
174
- function _skipWhitespace(xml, state) {
175
- while (state.pos < xml.length && /\s/.test(xml[state.pos])) state.pos++;
176
- }
177
-
178
- function _parseAttributes(text) {
179
- const attrs = {};
180
- const regex = /([^\s=/]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
181
- let m;
182
- while ((m = regex.exec(text)) !== null) {
183
- attrs[m[1]] = _unescapeXml(m[2] ?? m[3]);
184
- }
185
- return attrs;
186
- }
187
-
188
- /** Decode the predefined entities and character references (the fallback parser's half of _escapeXml). */
189
- function _unescapeXml(str) {
190
- return str.replace(/&(#x[0-9a-f]+|#\d+|lt|gt|quot|apos|amp);/gi, (whole, ref) => {
191
- const r = ref.toLowerCase();
192
- if (r[0] === '#') {
193
- const code = r[1] === 'x' ? parseInt(r.slice(2), 16) : parseInt(r.slice(1), 10);
194
- return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
195
- }
196
- return { lt: '<', gt: '>', quot: '"', apos: "'", amp: '&' }[r];
197
- });
198
- }
199
-
200
- /** Parse an Archi native XML model into plain JavaScript structures. */
201
- export function parseArchiModel(xml) {
202
- const root = typeof xml === 'string' ? parseXML(xml) : xml;
203
- const model = {
204
- id: root.getAttribute('id'),
205
- name: root.getAttribute('name'),
206
- version: root.getAttribute('version'),
207
- elements: [],
208
- relationships: [],
209
- views: [],
210
- // The Views sub-folders that hold no view (each a path of folder steps); a view carries its own.
211
- emptyViewFolders: [],
212
- // The model's own documentation (Archi's "purpose") and properties, in order (keys may repeat).
213
- documentation: _childText(root, 'purpose'),
214
- properties: _propertyPairs(root),
215
- archi: _extrasOrNull(_archiExtras(root, ARCHI_MODEL_HANDLED)),
216
- };
217
-
218
- for (const folder of root.children.filter(c => c.tagName === 'folder')) {
219
- const folderType = folder.getAttribute('type');
220
- if (folderType === 'diagrams') {
221
- _walkViewFolder(folder, model);
222
- continue;
223
- }
224
-
225
- // Recursively walk folder structure (handles nested sub-folders)
226
- _walkFolder(folder, folderType, model);
227
- }
228
-
229
- return model;
230
- }
231
-
232
- /** A sub-folder as one step of a folder path: its id, name, documentation and properties. @private */
233
- function _folderStep(folder) {
234
- const documentation = _childText(folder, 'documentation');
235
- const properties = _propertyPairs(folder);
236
- return {
237
- id: folder.getAttribute('id'),
238
- name: folder.getAttribute('name') ?? '',
239
- ...(documentation && { documentation }),
240
- ...(properties.length && { properties }),
241
- };
242
- }
243
-
244
- /** Every view in the Views folder, sub-folders included, in file order; each
245
- * remembers its sub-folder path (`archiFolder`). A sub-folder with nothing in
246
- * it is kept as its path (`emptyViewFolders`), as Archi shows it. @private */
247
- function _walkViewFolder(folder, model, path = []) {
248
- if (path.length && !folder.children.some(c => c.tagName === 'element' || c.tagName === 'folder')) model.emptyViewFolders.push(path);
249
- for (const child of folder.children) {
250
- if (child.tagName === 'element') {
251
- const view = _parseView(child);
252
- if (path.length) view.archiFolder = path;
253
- model.views.push(view);
254
- } else if (child.tagName === 'folder') {
255
- _walkViewFolder(child, model, [...path, _folderStep(child)]);
256
- }
257
- }
258
- }
259
-
260
- /** Every element (or relationship) in a top-level folder, sub-folders included;
261
- * each remembers its sub-folder path (`archiFolder`). @private */
262
- function _walkFolder(folder, folderType, model, path = []) {
263
- for (const child of folder.children) {
264
- if (child.tagName === 'element') {
265
- const parsed = _parseModelElement(child);
266
- if (path.length) parsed.archiFolder = path;
267
- if (folderType === 'relations') {
268
- model.relationships.push(parsed);
269
- } else {
270
- model.elements.push(parsed);
271
- }
272
- } else if (child.tagName === 'folder') {
273
- _walkFolder(child, folderType, model, [...path, _folderStep(child)]);
274
- }
275
- }
276
- }
277
-
278
- function _parseModelElement(el) {
279
- const xsiType = el.getAttribute('xsi:type') || '';
280
- const type = xsiType.replace(/^archimate:/, '');
281
- const element = {
282
- id: el.getAttribute('id'),
283
- name: el.getAttribute('name'),
284
- type,
285
- documentation: _childText(el, 'documentation'),
286
- properties: _parseProperties(el),
287
- };
288
- const source = el.getAttribute('source');
289
- const target = el.getAttribute('target');
290
- if (source) element.source = source;
291
- if (target) element.target = target;
292
- if (el.getAttribute('directed') === 'true') element.directed = true;
293
- const archi = _extrasOrNull(_archiExtras(el, ARCHI_CONCEPT_HANDLED));
294
- if (archi) element.archi = archi;
295
- return element;
296
- }
297
-
298
- function _parseView(el) {
299
- const xsiType = el.getAttribute('xsi:type') || '';
300
- const view = {
301
- id: el.getAttribute('id'),
302
- name: el.getAttribute('name'),
303
- type: _viewTypeFromXSI(xsiType),
304
- documentation: _childText(el, 'documentation'),
305
- properties: _propertyPairs(el),
306
- archi: _extrasOrNull(_archiExtras(el, ARCHI_VIEW_HANDLED)),
307
- children: [],
308
- };
309
- for (const child of el.children.filter(c => c.tagName === 'child')) {
310
- view.children.push(_parseVisual(child));
311
- }
312
- _relativizeLocatedBendpoints(view);
313
- return view;
314
- }
315
-
316
- /** Archi's bendpoint: offsets from the source's and the target's centre. */
317
- const BENDPOINT_KEYS = Object.freeze(['startX', 'startY', 'endX', 'endY']);
318
-
319
- /** Archi's size for a visual saved at its default size (width/height -1). @private */
320
- const ARCHI_DEFAULT_SIZE = Object.freeze({
321
- 'diagram-model-group': { width: 400, height: 140 },
322
- 'diagram-model-note': { width: 185, height: 80 },
323
- });
324
- const ARCHI_ELEMENT_SIZE = Object.freeze({ width: 120, height: 55 });
325
-
326
- /** Every visual's absolute bounds in a view (or visual tree), by visual id. @private */
327
- function _visualBoxes(root) {
328
- const boxes = new Map();
329
- const walk = (visual, origin) => {
330
- const bounds = _absoluteBounds(visual, origin);
331
- if (bounds && visual.id) boxes.set(visual.id, bounds);
332
- for (const child of visual.children || []) walk(child, bounds ?? origin);
333
- };
334
- walk(root, VIEW_ORIGIN);
335
- return boxes;
336
- }
337
-
338
- /** A box's anchor point as Archi (GEF's Rectangle#getCenter) takes it: integer halves. @private */
339
- const _centre = (b) => b && { x: b.x + Math.floor(b.width / 2), y: b.y + Math.floor(b.height / 2) };
340
-
341
- /**
342
- * Where a connection's bendpoints are on the view. Archi places bendpoint i of n
343
- * between its two offsets, weighted by w = (i + 1) / (n + 1) from source to target:
344
- * (1 - w)(sourceCentre + start) + w(targetCentre + end). A connection to another
345
- * connection has no box at that end; the other end's offset is used alone.
346
- * @private
347
- */
348
- function _bendpointLocations(sc, boxes) {
349
- const src = _centre(boxes.get(sc.source));
350
- const tgt = _centre(boxes.get(sc.target));
351
- const n = sc.bendpoints.length;
352
- return sc.bendpoints.flatMap((bp, i) => {
353
- const fromSource = src && { x: src.x + bp.startX, y: src.y + bp.startY };
354
- const fromTarget = tgt && { x: tgt.x + bp.endX, y: tgt.y + bp.endY };
355
- if (!fromSource || !fromTarget) return fromSource || fromTarget ? [fromSource || fromTarget] : [];
356
- const w = (i + 1) / (n + 1);
357
- return [{ x: (1 - w) * fromSource.x + w * fromTarget.x, y: (1 - w) * fromSource.y + w * fromTarget.y }];
358
- });
359
- }
360
-
361
- /** Archi bendpoints for points on the view, between two boxes (whole pixels, as Archi keeps them). @private */
362
- function _relativeBendpoints(points, sourceBox, targetBox) {
363
- const src = _centre(sourceBox) ?? VIEW_ORIGIN;
364
- const tgt = _centre(targetBox) ?? VIEW_ORIGIN;
365
- return points.map(p => ({
366
- startX: Math.round(p.x - src.x), startY: Math.round(p.y - src.y),
367
- endX: Math.round(p.x - tgt.x), endY: Math.round(p.y - tgt.y),
368
- }));
369
- }
370
-
371
- /** Convert a parsed view's x/y bendpoints (an older export) to Archi's form. @private */
372
- function _relativizeLocatedBendpoints(view) {
373
- const boxes = _visualBoxes(view);
374
- const walk = (visual) => {
375
- for (const sc of visual.sourceConnections || []) {
376
- if (sc.bendpoints?.some(bp => 'x' in bp)) {
377
- sc.bendpoints = _relativeBendpoints(sc.bendpoints, boxes.get(sc.source), boxes.get(sc.target));
378
- }
379
- }
380
- for (const child of visual.children || []) walk(child);
381
- };
382
- walk(view);
383
- }
384
-
385
- function _viewTypeFromXSI(xsiType) {
386
- const map = {
387
- 'archimate:ArchimateDiagramModel': 'archimate-diagram-model',
388
- 'archimate:SketchModel': 'sketch-model',
389
- 'archimate:CanvasModel': 'canvas-model',
390
- 'canvas:CanvasModel': 'canvas-model', // Archi writes canvases in their own namespace
391
- };
392
- return map[xsiType] || xsiType.replace(/^archimate:/, '');
393
- }
394
-
395
- /**
396
- * Archi's XML names for the visual classes it renames (EMF extended metadata in
397
- * Archi's model): xsi:type -> our visual type. Every other class is written
398
- * under its own name (DiagramModelImage, DiagramModelReference, ...).
399
- */
400
- const ARCHI_VISUAL_XML_NAMES = Object.freeze({
401
- Group: 'diagram-model-group',
402
- Note: 'diagram-model-note',
403
- DiagramObject: 'diagram-object',
404
- Connection: 'diagram-model-archimate-connection',
405
- });
406
-
407
- function _parseVisual(el) {
408
- const xsiType = el.getAttribute('xsi:type') || '';
409
- const className = xsiType.replace(/^[a-z]+:/, ''); // archimate:, canvas:, ...
410
- const type = ARCHI_VISUAL_XML_NAMES[className] ?? className.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
411
- const visual = {
412
- id: el.getAttribute('id'),
413
- type,
414
- archimateElement: el.getAttribute('archimateElement') || null,
415
- referencedModel: el.getAttribute('referencedModel') || null,
416
- imagePath: el.getAttribute('imagePath') || null,
417
- name: el.getAttribute('name') || null,
418
- text: _childText(el, 'content') || el.getAttribute('text') || null,
419
- notes: _childText(el, 'notes') || null,
420
- bounds: _parseBounds(el),
421
- style: _parseStyle(el),
422
- archi: _archiExtras(el),
423
- features: _parseFeatures(el),
424
- properties: _parseProperties(el),
425
- sourceConnections: [],
426
- children: [],
427
- };
428
-
429
- for (const sc of el.children.filter(c => c.tagName === 'sourceConnection')) {
430
- const bendpoints = [];
431
- for (const bp of sc.children.filter(c => c.tagName === 'bendpoint')) {
432
- bendpoints.push(bp.getAttribute('x') !== null && bp.getAttribute('startX') === null
433
- ? { x: Number(bp.getAttribute('x')), y: Number(bp.getAttribute('y')) }
434
- : Object.fromEntries(BENDPOINT_KEYS.map(k => [k, Number(bp.getAttribute(k) ?? 0)])));
435
- }
436
- visual.sourceConnections.push({
437
- id: sc.getAttribute('id'),
438
- source: sc.getAttribute('source'),
439
- target: sc.getAttribute('target'),
440
- // Archi writes archimateRelationship; 'relationship' is what older exports of ours wrote.
441
- relationship: sc.getAttribute('archimateRelationship') || sc.getAttribute('relationship') || null,
442
- type: (sc.getAttribute('xsi:type') || '').replace(/^archimate:/, ''),
443
- // A plain line's class (no xsi:type is Archi's default, DiagramModelConnection) and its look (type bits).
444
- archiType: _kebab((sc.getAttribute('xsi:type') || 'DiagramModelConnection').replace(/^[a-z]+:/, '')),
445
- connectionType: Number(sc.getAttribute('type')) || 0,
446
- archi: _archiExtras(sc, ARCHI_LINE_HANDLED), // a line's lock passes through as written
447
- name: sc.getAttribute('name') || '',
448
- style: _parseStyle(sc),
449
- features: _parseFeatures(sc),
450
- ...(bendpoints.length && { bendpoints }),
451
- });
452
- }
453
-
454
- for (const child of el.children.filter(c => c.tagName === 'child')) {
455
- visual.children.push(_parseVisual(child));
456
- }
457
-
458
- return visual;
459
- }
460
-
461
- function _parseBounds(el) {
462
- const bounds = el.querySelector('bounds');
463
- if (!bounds) return null;
464
- return {
465
- x: Number(bounds.getAttribute('x')),
466
- y: Number(bounds.getAttribute('y')),
467
- width: Number(bounds.getAttribute('width')),
468
- height: Number(bounds.getAttribute('height')),
469
- };
470
- }
471
-
472
- const _kebab = (className) => className.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
473
-
474
- /** Attributes the model reads or writes itself; anything else on a visual or line is passed through. */
475
- const ARCHI_HANDLED_ATTRIBUTES = new Set([
476
- 'xsi:type', 'id', 'archimateElement', 'archimateRelationship', 'source', 'target', 'targetConnections',
477
- 'name', 'imagePath', 'type', 'text', 'locked',
478
- ]);
479
-
480
- /** The feature that names the logical group (ItemGroup) a box is in: Archi has none of its own. */
481
- export const ARCHI_GROUP_FEATURE = 'renderengine.group';
482
- /** The feature that names the kind of that group when it is not a plain one ('background': a template's, pinned behind). */
483
- export const ARCHI_GROUP_KIND_FEATURE = 'renderengine.groupKind';
484
- /** The feature that fixes a box in place where Archi has no lock of its own (anything but a canvas box). */
485
- export const ARCHI_LOCK_FEATURE = 'renderengine.locked';
486
- /** The feature that marks a view as a template (kept to copy from): Archi has no such flag. */
487
- export const ARCHI_TEMPLATE_FEATURE = 'renderengine.template';
488
-
489
- /** The visuals Archi itself can lock (ILockable: its canvas classes). */
490
- const ARCHI_LOCKABLE = new Set(['canvas-model-block', 'canvas-model-sticky', 'canvas-model-image', 'canvas-model-connection']);
491
- /** A line's lock is not the arrangement's (which holds boxes): it passes through. */
492
- const ARCHI_LINE_HANDLED = {
493
- attributes: new Set(['xsi:type', 'id', 'archimateElement', 'archimateRelationship', 'source', 'target', 'targetConnections', 'name', 'imagePath', 'type', 'text']),
494
- elements: null, // set below, once ARCHI_HANDLED_ELEMENTS exists
495
- style: true,
496
- };
497
- /** Child elements the model reads itself. */
498
- const ARCHI_HANDLED_ELEMENTS = new Set(['child', 'sourceConnection', 'bounds', 'content', 'feature', 'property', 'bendpoint', 'documentation']);
499
- ARCHI_LINE_HANDLED.elements = ARCHI_HANDLED_ELEMENTS;
500
-
501
- /**
502
- * What a visual or line carries that the model does not use, kept to write back:
503
- * other attributes (locked, hintTitle, imagePosition, alpha, ...), other child
504
- * elements (hintContent, notes, ...) and the raw style attributes.
505
- * @private
506
- */
507
- function _archiExtras(el, handled = { attributes: ARCHI_HANDLED_ATTRIBUTES, elements: ARCHI_HANDLED_ELEMENTS, style: true }) {
508
- const attributes = {};
509
- const locked = handled.attributes === ARCHI_HANDLED_ATTRIBUTES && el.getAttribute('locked') === 'true';
510
- for (const [name, value] of Object.entries(el.attributes ?? {})) {
511
- if (handled.attributes.has(name) || name.startsWith('xmlns')) continue;
512
- if (handled.style && ARCHI_STYLE_ATTRIBUTES.includes(name)) continue;
513
- attributes[name] = value;
514
- }
515
- const elements = el.children
516
- .filter(c => !handled.elements.has(c.tagName))
517
- .map(c => ({ tag: c.tagName, text: c.textContent, attributes: { ...(c.attributes ?? {}) } }));
518
- return { attributes, elements, ...(handled.style && { style: _parseStyle(el) }), ...(locked && { locked: true }) };
519
- }
520
-
521
- /** What a concept (element or relationship) carries that the model does not read: accessType,
522
- * strength, profiles, ...; null when nothing. Written back on export. @private */
523
- const ARCHI_CONCEPT_HANDLED = {
524
- attributes: new Set(['xsi:type', 'id', 'name', 'source', 'target', 'directed']),
525
- elements: new Set(['documentation', 'property']),
526
- };
527
- /** The same for a view (hintTitle, viewpoint, connectionRouterType, hintContent, ...). */
528
- const ARCHI_VIEW_HANDLED = {
529
- attributes: new Set(['xsi:type', 'id', 'name']),
530
- elements: new Set(['documentation', 'property', 'child']),
531
- };
532
- /** The same for the model (its profiles, metadata, ...). */
533
- const ARCHI_MODEL_HANDLED = {
534
- attributes: new Set(['id', 'name', 'version']),
535
- elements: new Set(['folder', 'purpose', 'property']),
536
- };
537
-
538
- /** Extras with nothing in them are null. @private */
539
- function _extrasOrNull(extras) {
540
- return Object.keys(extras.attributes).length || extras.elements.length ? extras : null;
541
- }
542
-
543
- /** Child elements kept as {tag, text, attributes}, as lines. @private */
544
- function _extraElementLines(elements = [], indent) {
545
- return elements.map(({ tag, text = '', attributes = {} }) => {
546
- const attrText = Object.entries(attributes).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
547
- return text ? `${indent}<${tag}${attrText}>${_escapeXml(text)}</${tag}>` : `${indent}<${tag}${attrText}/>`;
548
- });
549
- }
550
-
551
- /** An instance's `archi` record from its parsed visual and its appearance as imported
552
- * (null when there is nothing to keep). @private */
553
- function _archiRecord(visual, appearance) {
554
- const record = {
555
- ...(visual.id && { id: visual.id }), // Archi's id for the visual, kept on export
556
- ...(visual.archiImagePath && { imagePath: visual.archiImagePath }), // its picture's path in the archive
557
- ...visual.archi,
558
- ...(visual.name && { name: visual.name }),
559
- ...(Object.keys(visual.features ?? {}).length && { features: { ...visual.features } }),
560
- };
561
- return { ...record, appearance: { ...appearance } };
562
- }
563
-
564
- /** Archi keeps a box's line transparency as a feature, not an attribute. */
565
- const ARCHI_LINE_ALPHA_FEATURE = 'lineAlpha';
566
-
567
- /** Which appearance fields each Archi style attribute sets. */
568
- const STYLE_FIELDS = Object.freeze({
569
- fillColor: ['fillColor'], lineColor: ['strokeColor'], borderColor: ['strokeColor'], fontColor: ['textColor'],
570
- font: ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle'], lineWidth: ['strokeWidth'],
571
- textAlignment: ['textAlign'], textPosition: ['textPosition'],
572
- // Transparency: no fill is alpha 0, no line is the lineAlpha feature at 0.
573
- alpha: ['fillColor'], [ARCHI_LINE_ALPHA_FEATURE]: ['strokeColor'],
574
- });
575
-
576
- /**
577
- * Archi's style attributes for an appearance: the original value of each one
578
- * whose meaning is unchanged since import (so values the model does not model,
579
- * and a font's full SWT string, survive), and the appearance's own value for
580
- * any changed. Without a record, what the appearance says.
581
- * @private
582
- */
583
- function _exportStyle(appearance = {}, record = null, { border = false, box = true } = {}) {
584
- const original = record?.style ?? {};
585
- const before = record?.appearance ?? _appearanceFromStyle(original, { box });
586
- const mapped = _styleFromAppearance(appearance, { border, box });
587
- const style = {};
588
- for (const [attribute, fields] of Object.entries(STYLE_FIELDS)) {
589
- const unchanged = fields.every(f => before[f] === appearance[f]);
590
- if (unchanged && original[attribute] !== undefined) style[attribute] = original[attribute];
591
- else if (!unchanged && mapped[attribute] !== undefined) style[attribute] = mapped[attribute];
592
- }
593
- return style;
594
- }
595
-
596
- /** Archi's style attributes on a box or line, as written (colours are "#rrggbb"). */
597
- const ARCHI_STYLE_ATTRIBUTES = Object.freeze([
598
- 'fillColor', 'lineColor', 'borderColor', 'fontColor', 'font', 'lineWidth', 'textAlignment', 'textPosition', 'alpha',
599
- ]);
600
-
601
- /** The style attributes an element carries. @private */
602
- function _parseStyle(el) {
603
- const style = {};
604
- for (const name of ARCHI_STYLE_ATTRIBUTES) {
605
- const value = el.getAttribute(name);
606
- if (value !== null && value !== undefined && value !== '') style[name] = value;
607
- }
608
- const lineAlpha = el.children?.find(c => c.tagName === 'feature' && c.getAttribute('name') === ARCHI_LINE_ALPHA_FEATURE);
609
- if (lineAlpha) style[ARCHI_LINE_ALPHA_FEATURE] = lineAlpha.getAttribute('value');
610
- return style;
611
- }
612
-
613
- /** Archi's text alignment (draw2d PositionConstants) and our names for it. */
614
- const TEXT_ALIGN = Object.freeze({ 1: 'left', 2: 'center', 4: 'right' });
615
- /** Archi's text position (top, centre, bottom of the box). */
616
- const TEXT_POSITION = Object.freeze({ 0: 'top', 1: 'middle', 2: 'bottom' });
617
-
618
- /**
619
- * An instance's appearance (ElementInstance#appearance) from Archi's style:
620
- * fill, line (Archi's lineColor, or borderColor on groups and notes), text
621
- * colour, line width, font, and text alignment and position.
622
- * @private
623
- */
624
- function _appearanceFromStyle(style = {}, { box = true } = {}) {
625
- const a = {};
626
- if (style.fillColor) a.fillColor = style.fillColor;
627
- const line = style.lineColor ?? style.borderColor;
628
- if (line) a.strokeColor = line;
629
- if (style.fontColor) a.textColor = style.fontColor;
630
- if (style.lineWidth) a.strokeWidth = Number(style.lineWidth);
631
- if (box && style.alpha === '0') a.fillColor = 'none';
632
- if (box && style[ARCHI_LINE_ALPHA_FEATURE] === '0') a.strokeColor = 'none';
633
- Object.assign(a, _parseFontFeature(style.font));
634
- // Archi centres a box's text unless the box says otherwise.
635
- if (TEXT_ALIGN[style.textAlignment] || box) a.textAlign = TEXT_ALIGN[style.textAlignment] ?? 'center';
636
- if (TEXT_POSITION[style.textPosition]) a.textPosition = TEXT_POSITION[style.textPosition];
637
- return a;
638
- }
639
-
640
- /** Archi's style attributes back from an appearance (the inverse of _appearanceFromStyle). @private */
641
- function _styleFromAppearance(a = {}, { border = false, box = true } = {}) {
642
- const style = {};
643
- if (a.fillColor === 'none') style.alpha = '0';
644
- else if (a.fillColor) style.fillColor = a.fillColor;
645
- if (box && a.strokeColor === 'none') style[ARCHI_LINE_ALPHA_FEATURE] = '0';
646
- if (a.strokeColor && a.strokeColor !== 'none') style[border ? 'borderColor' : 'lineColor'] = a.strokeColor;
647
- if (a.textColor) style.fontColor = a.textColor;
648
- if (a.strokeWidth !== undefined) style.lineWidth = String(a.strokeWidth);
649
- if (a.fontFamily || a.fontSize || a.fontWeight || a.fontStyle) style.font = _fontString(a);
650
- const align = Object.entries(TEXT_ALIGN).find(([, name]) => name === a.textAlign)?.[0];
651
- if (align) style.textAlignment = align;
652
- const position = Object.entries(TEXT_POSITION).find(([, name]) => name === a.textPosition)?.[0];
653
- if (position) style.textPosition = position;
654
- return style;
655
- }
656
-
657
- function _parseFeatures(el) {
658
- const features = {};
659
- for (const f of el.children.filter(c => c.tagName === 'feature')) {
660
- if (f.getAttribute('name') === ARCHI_LINE_ALPHA_FEATURE) continue; // style (see _parseStyle)
661
- features[f.getAttribute('name')] = f.getAttribute('value');
662
- }
663
- return features;
664
- }
665
-
666
- /** An element's own properties as [{key, value}], in file order (Archi allows a key more than once). @private */
667
- function _propertyPairs(el) {
668
- return el.children.filter(c => c.tagName === 'property').map(p => ({ key: p.getAttribute('key') ?? '', value: p.getAttribute('value') ?? '' }));
669
- }
670
-
671
- function _parseProperties(el) {
672
- const props = {};
673
- for (const p of el.children.filter(c => c.tagName === 'property')) {
674
- props[p.getAttribute('key')] = p.getAttribute('value');
675
- }
676
- return props;
677
- }
678
-
679
- /** Archi native format stores flat string properties. RenderEngine stores a
680
- * typed array `{name, type, value}`. Import converts everything to 'string'
681
- * by default; callers can retype in the UI. */
682
- function _propertiesToTypedArray(props) {
683
- if (!props || Object.keys(props).length === 0) return undefined;
684
- return Object.entries(props).map(([name, value]) => ({ name, type: 'string', value }));
685
- }
686
-
687
- /** Export: stringify typed values back to Archi's flat key/value strings. */
688
- function _typedArrayToProperties(arr) {
689
- const out = {};
690
- for (const { name, value } of arr || []) {
691
- if (!name) continue;
692
- out[name] = value == null ? '' : String(value);
693
- }
694
- return out;
695
- }
696
-
697
- /** The text of an element's own child `tagName` (not a nested element's: a canvas block holds stickies with their own content). @private */
698
- function _childText(el, tagName) {
699
- const child = el.children.find(c => c.tagName === tagName);
700
- return child ? child.textContent : null;
701
- }
702
-
703
- /** Convert a parsed Archi model into an ArchimateModel (with views).
704
- * @param {Object} archiModel - parsed Archi XML model
705
- * @returns {ArchimateModel} with one view per original view (elementIds track which elements appear in each view)
706
- * Now applies visual bounds/waypoints per view. */
707
- export function toArchimateModel(archiModel) {
708
- const elements = [];
709
- const relationships = [];
710
- const folders = [];
711
- const elementFolders = [];
712
- const views = [];
713
- const elementById = new Map();
714
- const relationshipById = new Map();
715
-
716
- // Map all elements first
717
- for (const el of archiModel.elements || []) {
718
- const mapped = _mapElement(el);
719
- elementById.set(el.id, mapped);
720
- elements.push(mapped);
721
- }
722
-
723
- // Map all relationships
724
- for (const rel of archiModel.relationships || []) {
725
- const mapped = _mapRelationship(rel);
726
- relationshipById.set(rel.id, mapped);
727
- relationships.push(mapped);
728
- }
729
-
730
- // Each view: one instance per box Archi draws, and one per line.
731
- for (const archiView of archiModel.views || []) {
732
- const v = _viewInstances(archiView, { elementById, relationshipById });
733
- // Our template flag is a feature on the view; the rest of its extras pass through.
734
- const isTemplateFeature = (e) => e.tag === 'feature' && e.attributes?.name === ARCHI_TEMPLATE_FEATURE;
735
- const template = !!archiView.archi?.elements?.some(e => isTemplateFeature(e) && e.attributes?.value === 'true');
736
- if (archiView.archi?.elements) archiView.archi.elements = archiView.archi.elements.filter(e => !isTemplateFeature(e));
737
- views.push({
738
- id: archiView.id,
739
- name: archiView.name,
740
- ...(archiView.documentation && { documentation: archiView.documentation }),
741
- elementIds: v.elementIds,
742
- elementInstances: v.elementInstances,
743
- relationshipInstances: v.relationshipInstances,
744
- viewConnections: v.viewConnections,
745
- arrangement: v.arrangement,
746
- archiViewType: archiView.type,
747
- ...(template && { template }),
748
- ...(archiView.properties?.length && { properties: archiView.properties }),
749
- ...(archiView.archiFolder && { archiFolder: archiView.archiFolder }),
750
- ...(archiView.archi && { archi: archiView.archi }),
751
- ...(v.folders.length && { folders: v.folders, folderLayout: v.folderLayout }),
752
- });
753
- folders.push(...v.folders);
754
- elementFolders.push(...v.elementFolders);
755
- }
756
-
757
- return new ArchimateModel({
758
- id: archiModel.id ?? undefined,
759
- name: archiModel.name ?? '', elements, relationships, folders, elementFolders, views,
760
- documentation: archiModel.documentation || undefined, properties: archiModel.properties ?? [],
761
- emptyViewFolders: archiModel.emptyViewFolders ?? [],
762
- // The specializations written for element types Archi lacks are not the model's own.
763
- archi: archiModel.archi ? {
764
- ...archiModel.archi,
765
- elements: (archiModel.archi.elements ?? []).filter(e => !(e.tag === 'profile' && String(e.attributes?.id ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX))),
766
- } : null,
767
- });
768
- }
769
-
770
- /** Where a visual sits on the view. Archi stores a nested visual's bounds
771
- * relative to its parent (a group, or an element drawn inside another), so
772
- * they are offset by the parent's absolute position. Null without bounds. @private */
773
- function _absoluteBounds(visual, origin) {
774
- if (!visual.bounds) return null;
775
- const { x, y, width, height } = visual.bounds;
776
- const size = ARCHI_DEFAULT_SIZE[visual.type] ?? ARCHI_ELEMENT_SIZE;
777
- return {
778
- x: origin.x + x, y: origin.y + y,
779
- width: width > 0 ? width : size.width,
780
- height: height > 0 ? height : size.height,
781
- };
782
- }
783
-
784
- const VIEW_ORIGIN = Object.freeze({ x: 0, y: 0 });
785
-
786
- /**
787
- * Boxes on canvas and sketch views: no model element behind them, drawn as a
788
- * shape with text (and, for a canvas sticky, maybe a picture). Archi type ->
789
- * the renderer's shape.
790
- */
791
- const VIEW_BOX_SHAPES = Object.freeze({
792
- 'canvas-model-block': 'rect',
793
- 'canvas-model-sticky': 'rect',
794
- 'sketch-model-sticky': 'rect',
795
- 'sketch-model-actor': 'stick-figure',
796
- });
797
-
798
- /** What a view-only box draws (a ViewObjectInstance's content), from its Archi visual. @private */
799
- function _viewObjectContent(visual) {
800
- return _viewObjectDrawing(visual);
801
- }
802
-
803
- /** @private */
804
- function _viewObjectDrawing(visual) {
805
- const text = visual.text || visual.name || '';
806
- if (visual.type === 'diagram-model-note') {
807
- // A note's text is its own content; an empty one shows its name, if any (Archi's legend note).
808
- return { id: visual.id, name: visual.text || visual.name || '', label: visual.text || '', shape: 'note' };
809
- }
810
- if (visual.type === 'diagram-model-image' || visual.type === 'canvas-model-image') {
811
- return {
812
- id: visual.id, name: visual.name || 'Image', shape: 'image', iconUrl: visual.imagePath,
813
- ...(visual.properties?.href && { href: visual.properties.href }),
814
- ...(visual.type === 'canvas-model-image' && { archiVisualType: visual.type }),
815
- };
816
- }
817
- const shape = VIEW_BOX_SHAPES[visual.type];
818
- if (!shape) return null;
819
- return {
820
- id: visual.id, name: text, label: text, shape, // an empty box shows no text
821
- archiVisualType: visual.type,
822
- ...(visual.imagePath && { iconUrl: visual.imagePath }),
823
- ...(visual.notes && { documentation: visual.notes }),
824
- };
825
- }
826
-
827
- /** Where a box is drawn: its centre, as the renderer places nodes (Archi keeps the top-left corner). @private */
828
- const _centreOf = (bounds) => ({ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 });
829
-
830
- /**
831
- * One Archi view as instances: an ElementInstance per box that shows a model
832
- * element (so the same element twice is two instances), a ViewObjectInstance
833
- * per note or image, each centred on its box with that box's size and style;
834
- * a RelationshipInstance per line, joining the two instances it joins, with its
835
- * bend points (none is a straight line); groups as folders with their bounds.
836
- * @private
837
- */
838
- function _viewInstances(archiView, { elementById, relationshipById }) {
839
- const boxes = _visualBoxes(archiView);
840
- const elementInstances = new Map();
841
- const instanceOfVisual = new Map(); // Archi visual id -> instance id
842
- const elementIds = new Set();
843
- const folders = [];
844
- const folderLayout = {};
845
- const elementFolders = [];
846
- const lines = [];
847
-
848
- let order = 0; // document order is Archi's drawing order
849
- const locked = [];
850
- const groupMembers = new Map(); // group id -> instance ids
851
- const groupKinds = new Map(); // group id -> its kind, when not a plain group
852
- const place = (inst, bounds, style, type, visual) => {
853
- inst.size = { width: bounds.width, height: bounds.height };
854
- inst.zIndex = order++;
855
- inst.appearance = _appearanceFromStyle(style);
856
- // Archi centres a canvas sticky's text vertically unless it says otherwise.
857
- if (type === 'canvas-model-sticky' && !TEXT_POSITION[style?.textPosition]) inst.appearance.textPosition = 'middle';
858
- // Archi draws a canvas box without a border colour with no border.
859
- if (type?.startsWith('canvas-model-') && !style?.borderColor && !style?.lineColor) inst.appearance.strokeColor = 'none';
860
- inst.archi = _archiRecord(visual, inst.appearance);
861
- // Archi's lock, and our logical group (kept as a feature), go to the view's arrangement.
862
- const features = inst.archi?.features ?? {};
863
- if (visual.archi?.locked || features[ARCHI_LOCK_FEATURE] === 'true') locked.push(inst.id);
864
- const groupId = features[ARCHI_GROUP_FEATURE];
865
- if (groupId) (groupMembers.get(groupId) ?? groupMembers.set(groupId, []).get(groupId)).push(inst.id);
866
- if (groupId && features[ARCHI_GROUP_KIND_FEATURE]) groupKinds.set(groupId, features[ARCHI_GROUP_KIND_FEATURE]);
867
- if (inst.archi?.features) {
868
- delete inst.archi.features[ARCHI_GROUP_FEATURE];
869
- delete inst.archi.features[ARCHI_GROUP_KIND_FEATURE];
870
- delete inst.archi.features[ARCHI_LOCK_FEATURE];
871
- if (!Object.keys(inst.archi.features).length) delete inst.archi.features;
872
- }
873
- elementInstances.set(inst.id, inst);
874
- return inst;
875
- };
876
- const walk = (visual, parentFolderId, origin, parentBoxId = null) => {
877
- const bounds = _absoluteBounds(visual, origin);
878
- const instanceCount = elementInstances.size;
879
- if (bounds) {
880
- const { x, y } = _centreOf(bounds);
881
- if (visual.archimateElement && elementById.has(visual.archimateElement)) {
882
- const inst = place(new ElementInstance(visual.archimateElement, x, y), bounds, visual.style, visual.type, visual);
883
- instanceOfVisual.set(visual.id, inst.id);
884
- elementIds.add(visual.archimateElement);
885
- if (parentFolderId) elementFolders.push([visual.archimateElement, parentFolderId]);
886
- } else {
887
- // Notes, images, and canvas and sketch boxes: drawn on this view only.
888
- const content = _viewObjectContent(visual);
889
- if (content) {
890
- const inst = place(new ViewObjectInstance({ ...content, width: bounds.width, height: bounds.height }, x, y), bounds, visual.style, visual.type, visual);
891
- instanceOfVisual.set(visual.id, inst.id);
892
- }
893
- }
894
- }
895
- if (visual.type === 'diagram-model-group') {
896
- folders.push({
897
- id: visual.id,
898
- name: visual.name || 'Group',
899
- documentation: visual.documentation || undefined,
900
- parentId: parentFolderId || undefined,
901
- children: [],
902
- });
903
- if (parentFolderId) folders.find(f => f.id === parentFolderId)?.children.push(visual.id);
904
- if (bounds) {
905
- // Its paint as an instance keeps it (the canvas editor draws and edits it), and the record to write back.
906
- const appearance = _appearanceFromStyle(visual.style);
907
- folderLayout[visual.id] = {
908
- x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, collapsed: false, zIndex: order++,
909
- appearance, archi: _archiRecord(visual, appearance),
910
- };
911
- }
912
- instanceOfVisual.set(visual.id, visual.id); // a plain line may end at a group
913
- }
914
- // A box drawn inside another box, or a note or picture inside a group (an element's group
915
- // is its folder): kept so the canvas moves it with its parent and export nests it there again.
916
- const isBox = elementInstances.size > instanceCount;
917
- const parent = parentBoxId ?? (visual.archimateElement ? null : parentFolderId);
918
- if (isBox && parent) {
919
- const inst = [...elementInstances.values()].at(-1);
920
- inst.archi = { ...(inst.archi ?? {}), parent };
921
- }
922
- lines.push(...(visual.sourceConnections || []));
923
- const folderId = visual.type === 'diagram-model-group' ? visual.id : parentFolderId;
924
- const boxId = isBox ? visual.id : (visual.type === 'diagram-model-group' ? null : parentBoxId);
925
- for (const child of visual.children || []) walk(child, folderId, bounds ?? origin, boxId);
926
- };
927
- for (const child of archiView.children || []) walk(child, null, VIEW_ORIGIN);
928
-
929
- const relationshipInstances = new Map();
930
- const viewConnections = [];
931
- for (const sc of lines) {
932
- const source = instanceOfVisual.get(sc.source);
933
- const target = instanceOfVisual.get(sc.target);
934
- if (!source || !target) continue;
935
- if (!sc.relationship) {
936
- // A plain line: Archi's connection between any two boxes, or a canvas connection.
937
- const connection = new ViewConnectionInstance({
938
- id: sc.id, sourceInstanceId: source, targetInstanceId: target,
939
- waypoints: sc.bendpoints?.length ? _bendpointLocations(sc, boxes) : [],
940
- type: sc.connectionType, label: sc.name, appearance: _appearanceFromStyle(sc.style, { box: false }), archiType: sc.archiType,
941
- });
942
- connection.archi = _archiRecord(sc, connection.appearance);
943
- viewConnections.push(connection);
944
- continue;
945
- }
946
- if (!relationshipById.has(sc.relationship)) continue;
947
- const inst = new RelationshipInstance(sc.relationship, sc.bendpoints?.length ? _bendpointLocations(sc, boxes) : [], source, target);
948
- inst.appearance = _appearanceFromStyle(sc.style, { box: false });
949
- inst.archi = _archiRecord(sc, inst.appearance);
950
- relationshipInstances.set(inst.id, inst);
951
- }
952
- const arrangement = {
953
- // A plain group needs two members; a kind of group (a background) may have one.
954
- groups: [...groupMembers].filter(([id, ids]) => ids.length > 1 || groupKinds.has(id))
955
- .map(([id, memberIds]) => ({ id, memberIds, ...(groupKinds.has(id) && { kind: groupKinds.get(id) }) })),
956
- locked,
957
- };
958
- return { elementIds: [...elementIds], elementInstances, relationshipInstances, viewConnections, folders, folderLayout, elementFolders, arrangement };
959
- }
960
-
961
- /** Convert a parsed Archi model into renderengine inputs.
962
- * Returns `{ elements, relationships, options }` ready for `archimateDiagram()`.
963
- * If `viewId` is omitted, the first view is used.
964
- * Kept as original for backward compatibility: applies visual bounds/features and handles all view visuals. */
965
- export function toRenderEngineModel(archiModel, viewId) {
966
- const view = viewId
967
- ? archiModel.views.find(v => v.id === viewId)
968
- : archiModel.views[0];
969
-
970
- const elements = [];
971
- const relationships = [];
972
- const annotations = [];
973
- const elementById = new Map();
974
- const relationshipById = new Map();
975
-
976
- for (const el of archiModel.elements || []) {
977
- const mapped = _mapElement(el);
978
- elementById.set(el.id, mapped);
979
- elements.push(mapped);
980
- }
981
-
982
- for (const rel of archiModel.relationships || []) {
983
- const mapped = _mapRelationship(rel);
984
- relationshipById.set(rel.id, mapped);
985
- relationships.push(mapped);
986
- }
987
-
988
- const folders = [];
989
- const folderLayout = {};
990
- const elementFolders = []; // Array of [elementId, folderId] pairs
991
-
992
- if (view) {
993
- const boxes = _visualBoxes(view);
994
- for (const visual of view.children || []) {
995
- _mapVisual(visual, { elementById, relationshipById, elements, relationships, annotations, folders, folderLayout, elementFolders, boxes });
996
- }
997
- }
998
-
999
- const options = {
1000
- annotations,
1001
- archimateVersion: '4',
1002
- };
1003
-
1004
- return { elements, relationships, folders, folderLayout, elementFolders, options };
1005
- }
1006
-
1007
- function _mapElement(el) {
1008
- const archiType = _toKebabType(el.type);
1009
- const layer = ArchimateNotation.elementLayer({ type: archiType });
1010
- // A type Archi has no class for was written as the nearest one, with its own kept as a feature.
1011
- const ownType = el.archi?.elements?.find(e => e.tag === 'feature' && e.attributes?.name === ARCHI_TYPE_FEATURE)?.attributes?.value;
1012
- const mapped = {
1013
- id: el.id,
1014
- name: el.name,
1015
- type: ownType || archiType,
1016
- layer: layer !== 'other' ? layer : undefined,
1017
- documentation: el.documentation || undefined,
1018
- };
1019
- const props = _propertiesToTypedArray(el.properties);
1020
- if (props) mapped.properties = props;
1021
- if (el.archiFolder) mapped.archiFolder = el.archiFolder;
1022
- if (el.archi) mapped.archi = ownType ? _withoutTypeMarks(el.archi) : el.archi;
1023
- return mapped;
1024
- }
1025
-
1026
- /** An element's archi record without the feature and specialization written for its own type. @private */
1027
- function _withoutTypeMarks(archi) {
1028
- const attributes = { ...(archi.attributes ?? {}) };
1029
- if (String(attributes.profiles ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX)) delete attributes.profiles;
1030
- return {
1031
- ...archi,
1032
- attributes,
1033
- elements: (archi.elements ?? []).filter(e => !(e.tag === 'feature' && e.attributes?.name === ARCHI_TYPE_FEATURE)),
1034
- };
1035
- }
1036
-
1037
- function _mapRelationship(rel) {
1038
- const base = _toKebabType(rel.type).replace(/-relationship$/, '');
1039
- const kind = base === 'association' && rel.directed ? 'association-directed' : base;
1040
- const mapped = {
1041
- id: rel.id,
1042
- source: rel.source,
1043
- target: rel.target,
1044
- kind,
1045
- label: rel.name || undefined,
1046
- documentation: rel.documentation || undefined,
1047
- };
1048
- const props = _propertiesToTypedArray(rel.properties);
1049
- if (props) mapped.properties = props;
1050
- if (rel.archiFolder) mapped.archiFolder = rel.archiFolder;
1051
- if (rel.archi) mapped.archi = rel.archi;
1052
- return mapped;
1053
- }
1054
-
1055
- function _mapVisual(visual, ctx, parentBounds = null, parentFolderId = null, origin = VIEW_ORIGIN) {
1056
- const own = _absoluteBounds(visual, origin);
1057
- const bounds = own || parentBounds || { x: 0, y: 0, width: 120, height: 55 };
1058
- // Children are placed relative to this visual if it has bounds, else as it is placed.
1059
- const childOrigin = own ?? origin;
1060
- // Placed by its centre, as the renderer draws nodes; styled under the renderer's names.
1061
- const { x, y } = _centreOf(bounds);
1062
- const { fillColor, ...appearance } = _appearanceFromStyle(visual.style);
1063
- const visualProps = {
1064
- x, y,
1065
- width: bounds.width,
1066
- height: bounds.height,
1067
- ...appearance,
1068
- ...(fillColor && { color: fillColor }),
1069
- };
1070
-
1071
- if (visual.archimateElement) {
1072
- const element = ctx.elementById.get(visual.archimateElement);
1073
- if (element) {
1074
- Object.assign(element, visualProps);
1075
- // A DiagramObject may carry a per-view text override.
1076
- if (visual.text) element.label = visual.text;
1077
- }
1078
- // Track which folder contains this element (if any)
1079
- if (parentFolderId && ctx.elementFolders) {
1080
- ctx.elementFolders.push([visual.archimateElement, parentFolderId]);
1081
- }
1082
- // On to its connections and to the elements drawn inside it (below).
1083
- }
1084
-
1085
- const base = { id: visual.id, ...visualProps };
1086
-
1087
- if (visual.type === 'diagram-model-note') {
1088
- ctx.annotations.push({
1089
- ...base,
1090
- label: visual.text || 'Note',
1091
- shape: 'note',
1092
- });
1093
- return;
1094
- }
1095
-
1096
- if (visual.type === 'diagram-model-group') {
1097
- const folder = {
1098
- id: visual.id,
1099
- name: visual.name || 'Group',
1100
- documentation: visual.documentation || undefined,
1101
- parentId: parentFolderId || undefined,
1102
- children: [],
1103
- };
1104
- ctx.folders.push(folder);
1105
- ctx.folderLayout[visual.id] = {
1106
- x: bounds.x,
1107
- y: bounds.y,
1108
- width: visualProps.width,
1109
- height: visualProps.height,
1110
- collapsed: false,
1111
- };
1112
- // Recurse into children with this folder as parent
1113
- for (const child of visual.children || []) {
1114
- _mapVisual(child, ctx, bounds, visual.id, childOrigin);
1115
- }
1116
- return;
1117
- }
1118
-
1119
- if (visual.type === 'diagram-model-image') {
1120
- ctx.annotations.push({
1121
- ...base,
1122
- label: visual.name || '',
1123
- iconUrl: visual.imagePath,
1124
- shape: 'image',
1125
- href: visual.properties?.href,
1126
- });
1127
- return;
1128
- }
1129
-
1130
- if (visual.type === 'diagram-model-reference') {
1131
- ctx.annotations.push({
1132
- ...base,
1133
- label: visual.name || 'Reference',
1134
- reference: visual.referencedModel,
1135
- shape: 'note',
1136
- });
1137
- return;
1138
- }
1139
-
1140
- if ([
1141
- 'sketch-model-sticky', 'sketch-model-actor',
1142
- 'canvas-model-block', 'canvas-model-sticky', 'canvas-model-image',
1143
- ].includes(visual.type)) {
1144
- ctx.annotations.push({
1145
- ...base,
1146
- label: visual.text || visual.name || visual.type,
1147
- archiVisualType: visual.type,
1148
- shape: visual.type.endsWith('-image') ? 'rect' : 'note',
1149
- });
1150
- return;
1151
- }
1152
-
1153
- // An element's connections, then the visuals nested in it.
1154
- for (const sc of visual.sourceConnections || []) {
1155
- if (sc.relationship && sc.bendpoints?.length) {
1156
- // Apply bendpoints from the visual connection to the relationship model
1157
- const rel = ctx.relationshipById?.get(sc.relationship);
1158
- if (rel) {
1159
- rel.waypoints = _bendpointLocations(sc, ctx.boxes);
1160
- }
1161
- }
1162
- }
1163
-
1164
- for (const child of visual.children || []) {
1165
- _mapVisual(child, ctx, bounds, parentFolderId, childOrigin);
1166
- }
1167
- }
1168
-
1169
- function _toKebabType(name) {
1170
- if (!name) return '';
1171
- return name
1172
- .replace(/^archimate:/, '')
1173
- .replace(/([a-z])([A-Z])/g, '$1-$2')
1174
- .toLowerCase();
1175
- }
1176
-
1177
- /**
1178
- * Archi's font: SWT FontData, "version|name|height in points|style|platform|...",
1179
- * style bit 1 bold, bit 2 italic. The size is kept in points ("9pt"), which SVG
1180
- * draws as Archi does.
1181
- * @private
1182
- */
1183
- function _parseFontFeature(fontString) {
1184
- if (!fontString) return {};
1185
- const [, family, height, style] = fontString.split('|');
1186
- const result = {};
1187
- if (family) result.fontFamily = family;
1188
- if (height && Number(height) > 0) result.fontSize = `${Number(height)}pt`;
1189
- const bits = Number(style) || 0;
1190
- if (bits & 1) result.fontWeight = 'bold';
1191
- if (bits & 2) result.fontStyle = 'italic';
1192
- return result;
1193
- }
1194
-
1195
- /** A font as SWT FontData's leading fields, which Archi reads (inverse of _parseFontFeature). @private */
1196
- function _fontString({ fontFamily = 'Segoe UI', fontSize = '9pt', fontWeight, fontStyle } = {}) {
1197
- const points = String(fontSize).endsWith('px') ? parseFloat(fontSize) * 0.75 : parseFloat(fontSize);
1198
- const bits = (fontWeight === 'bold' ? 1 : 0) | (fontStyle === 'italic' ? 2 : 0);
1199
- return `1|${fontFamily}|${points}|${bits}`;
1200
- }
1201
-
1202
- /** Convert an ArchimateView to Archi's visual-tree shape, ready for toArchiXML.
1203
- * Returns array of visual child objects with bounds, sourceConnections (including bendpoints), and nested children.
1204
- * This is the export-side bridge: ArchimateView → Archi visual tree → XML. */
1205
- export function fromArchimateView(view) {
1206
- const visuals = [];
1207
- const relationshipsBySource = new Map();
1208
- const folderById = new Map();
1209
-
1210
- // Index relationships by source visual ID for quick lookup
1211
- for (const rel of view.relationships || []) {
1212
- if (rel.viewConnection) continue; // plain lines are written from view.viewConnections (below)
1213
- if (!relationshipsBySource.has(rel.source)) {
1214
- relationshipsBySource.set(rel.source, []);
1215
- }
1216
- relationshipsBySource.get(rel.source).push(rel);
1217
- }
1218
-
1219
- // Groups: an ArchimateView's placedFolders (each after its parent, parentId the
1220
- // nearest group on the view); a plain view object's `folders` are all top level.
1221
- const groups = view.placedFolders ?? (view.folders || []).map(f => ({ ...f, parentId: undefined }));
1222
- const absolute = new Map(); // visual -> its bounds on the view
1223
- const stacking = new Map(); // visual -> its zIndex (Infinity: on top, in the order placed)
1224
- /** Put a visual on the view, or inside the group `groupId` with bounds relative to it. */
1225
- const placedById = new Map(); // visual id -> visual, for nesting a box in its box
1226
- const place = (visual, groupId, zIndex = Infinity, parentBoxId = null) => {
1227
- absolute.set(visual, visual.bounds);
1228
- stacking.set(visual, zIndex);
1229
- placedById.set(visual.id, visual);
1230
- const group = (parentBoxId && placedById.get(parentBoxId)) || (groupId && folderById.get(groupId));
1231
- if (!group) { visuals.push(visual); return; }
1232
- const origin = absolute.get(group);
1233
- visual.bounds = { ...visual.bounds, x: visual.bounds.x - origin.x, y: visual.bounds.y - origin.y };
1234
- (group.children ??= []).push(visual); // a box gets its first child here
1235
- };
1236
- for (const folder of groups) {
1237
- const { x = 0, y = 0, width = 200, height = 150 } = view.folderLayout?.[folder.id] ?? folder;
1238
- const folderVisual = {
1239
- id: folder.id,
1240
- type: 'diagram-model-group',
1241
- name: folder.name,
1242
- bounds: { x, y, width, height },
1243
- children: [],
1244
- sourceConnections: [],
1245
- };
1246
- // Its paint as the view has it now: the original attributes where unchanged (as for a box).
1247
- _applyArchiRecord(folderVisual, folder.archi, folder.appearance ? _exportStyle(folder.appearance, folder.archi, { border: true }) : (folder.archi?.style ?? {}));
1248
- const folderZ = view.folderLayout?.[folder.id]?.zIndex ?? folder.zIndex;
1249
- if (folder.documentation) {
1250
- folderVisual.documentation = folder.documentation;
1251
- }
1252
- folderById.set(folder.id, folderVisual);
1253
- place(folderVisual, folder.parentId, folderZ ?? Infinity);
1254
- }
1255
-
1256
- /** The visual id for a drawn item: Archi's own when it was imported, else one made from the instance id. */
1257
- const visualIdOf = (instanceId) => view.getElementInstance?.(instanceId)?.visualId ?? `diag-${instanceId}`;
1258
-
1259
- // One visual per drawn item. A view's items carry their instance id as `id`
1260
- // and the model element's as `elementId` (a plain {id} item is both).
1261
- // Each item is placed by its centre, as the renderer draws it; Archi keeps the
1262
- // top-left corner, in whole pixels. A box nested in another is placed after it
1263
- // (whichever was added first), so it goes inside it.
1264
- const byVisualId = new Map((view.elements || []).map(el => [visualIdOf(el.id), el]));
1265
- const depthOf = (el, seen = new Set()) => {
1266
- const parent = byVisualId.get(view.getElementInstance?.(el.id)?.nestedIn);
1267
- return parent && !seen.has(parent) ? 1 + depthOf(parent, seen.add(el)) : 0;
1268
- };
1269
- const inPlacingOrder = (view.elements || []).map((el, i) => [el, depthOf(el), i]).sort((a, b) => a[1] - b[1] || a[2] - b[2]).map(([el]) => el);
1270
- for (const el of inPlacingOrder) {
1271
- const width = el.width || ARCHI_ELEMENT_SIZE.width;
1272
- const height = el.height || ARCHI_ELEMENT_SIZE.height;
1273
- const bounds = { x: Math.round((el.x || 0) - width / 2), y: Math.round((el.y || 0) - height / 2), width, height };
1274
- const visual = el.viewObject
1275
- ? { ..._viewObjectVisual(el, bounds), id: visualIdOf(el.id) }
1276
- : {
1277
- id: visualIdOf(el.id),
1278
- type: 'diagram-model-object',
1279
- archimateElement: el.elementId ?? el.id,
1280
- bounds,
1281
- sourceConnections: [],
1282
- };
1283
- // This view's style for the box (an ArchimateView keeps it on the instance), and what the file said besides.
1284
- const instance = view.getElementInstance?.(el.id);
1285
- _applyArchiRecord(visual, instance?.archi, _exportStyle(instance?.appearance, instance?.archi, { border: !!el.viewObject }));
1286
- // Fixed in place (on its own or in a locked group): Archi's lock where Archi has one (canvas
1287
- // boxes), else a feature Archi keeps. Its logical group: a feature.
1288
- if (view.arrangement?.isLocked(el.id)) {
1289
- if (ARCHI_LOCKABLE.has(visual.type)) visual.locked = true;
1290
- else visual.features = { ...(visual.features ?? {}), [ARCHI_LOCK_FEATURE]: 'true' };
1291
- }
1292
- const itemGroup = view.arrangement?.groupOf(el.id);
1293
- if (itemGroup) {
1294
- const kind = itemGroup.constructor.kind;
1295
- visual.features = { ...(visual.features ?? {}), [ARCHI_GROUP_FEATURE]: itemGroup.id, ...(kind !== 'group' && { [ARCHI_GROUP_KIND_FEATURE]: kind }) };
1296
- }
1297
-
1298
- // Add connections from this element to others
1299
- const rels = relationshipsBySource.get(el.id) || [];
1300
- for (const rel of rels) {
1301
- const sc = {
1302
- id: view.relationshipInstances?.get?.(rel.id)?.archi?.id ?? `conn-${rel.id}`,
1303
- source: visual.id,
1304
- target: visualIdOf(rel.target),
1305
- relationship: rel.relationshipId ?? rel.id,
1306
- type: 'archimate:Connection',
1307
- };
1308
- const line = view.relationshipInstances?.get?.(rel.id);
1309
- _applyArchiRecord(sc, line?.archi, _exportStyle(line?.appearance, line?.archi, { box: false }));
1310
- if (rel.waypoints?.length) {
1311
- // Points on the view for now; made relative once every box is known (below).
1312
- sc.bendpoints = rel.waypoints.map(wp => ({ x: wp.x, y: wp.y }));
1313
- }
1314
- visual.sourceConnections.push(sc);
1315
- }
1316
-
1317
- if (instance?.archi?.imagePath) visual.archiImagePath = instance.archi.imagePath;
1318
- place(visual, el.viewObject ? undefined : view.placedFolderOf?.(el.elementId ?? el.id), el.zIndex ?? Infinity, instance?.archi?.parent);
1319
- }
1320
-
1321
- // Plain lines (no relationship): on their source box, with their own class and type bits.
1322
- const visualOf = new Map();
1323
- const index = (v) => { visualOf.set(v.id, v); (v.children || []).forEach(index); };
1324
- visuals.forEach(index);
1325
- for (const connection of view.viewConnections?.values?.() ?? []) {
1326
- // An end is a box (exported as "diag-<instance id>") or a group (under its own id).
1327
- const visualFor = (end) => visualOf.get(visualIdOf(end)) ?? visualOf.get(end);
1328
- const source = visualFor(connection.sourceInstanceId);
1329
- const target = visualFor(connection.targetInstanceId);
1330
- if (!source || !target) continue;
1331
- const sc = {
1332
- id: connection.id,
1333
- source: source.id,
1334
- target: target.id,
1335
- archiType: connection.archiType,
1336
- connectionType: connection.type,
1337
- name: connection.label || '',
1338
- ...(connection.waypoints.length && { bendpoints: connection.waypoints.map(wp => ({ x: wp.x, y: wp.y })) }),
1339
- };
1340
- _applyArchiRecord(sc, connection.archi, _exportStyle(connection.appearance, connection.archi, { box: false }));
1341
- source.sourceConnections.push(sc);
1342
- }
1343
-
1344
- // Each level in stacking order (Archi draws in document order); ties keep their order.
1345
- const byStacking = (list) => {
1346
- list.sort((a, b) => stacking.get(a) - stacking.get(b));
1347
- list.forEach(v => byStacking(v.children ?? []));
1348
- };
1349
- byStacking(visuals);
1350
-
1351
- _relativizeLocatedBendpoints({ children: visuals });
1352
- return visuals;
1353
- }
1354
-
1355
- /**
1356
- * A model and its views as the writers take them (toArchiXML, toArchiFile,
1357
- * toExchangeXML): the model's data, and each view as an Archi view tree.
1358
- * @param {ArchimateModel} model
1359
- * @returns {[Object, Array<Object>]} `[modelData, views]`, to spread into a writer
1360
- */
1361
- export function toArchiExport(model) {
1362
- const views = model.views().map(view => ({
1363
- id: view.id,
1364
- name: view.name,
1365
- type: view.archiViewType,
1366
- template: view.template,
1367
- documentation: view.documentation,
1368
- properties: view.properties,
1369
- archiFolder: view.archiFolder,
1370
- archi: view.archi,
1371
- children: fromArchimateView(view),
1372
- }));
1373
- return [{
1374
- id: model.id || 'model-1',
1375
- name: model.name || 'ArchiMate Model',
1376
- version: '5.0.0',
1377
- documentation: model.documentation,
1378
- properties: model.properties,
1379
- archi: model.archi,
1380
- emptyViewFolders: model.emptyViewFolders ?? [],
1381
- elements: model.elements(),
1382
- relationships: model.relationships(),
1383
- }, views];
1384
- }
1385
-
1386
- /**
1387
- * Put what the file said about a box or line back on its export visual: the
1388
- * style to write, its name and features (when it has none of its own), and the
1389
- * attributes and child elements the model does not use.
1390
- * @private
1391
- */
1392
- function _applyArchiRecord(target, record, style) {
1393
- target.style = style;
1394
- if (!record) return;
1395
- if (record.name && !target.name) target.name = record.name;
1396
- if (record.features) target.features = { ...record.features, ...(target.features ?? {}) };
1397
- if (Object.keys(record.attributes ?? {}).length) target.extraAttributes = { ...record.attributes };
1398
- if (record.elements?.length) target.extraElements = record.elements.map(e => ({ ...e }));
1399
- }
1400
-
1401
- /** <property> lines for [{key, value}] pairs (a model's or a view's). @private */
1402
- function _propertyLines(pairs, indent) {
1403
- return (Array.isArray(pairs) ? pairs : []).map(({ key, value }) => `${indent}<property key="${_escapeAttr(key)}" value="${_escapeAttr(value)}"/>`);
1404
- }
1405
-
1406
- /**
1407
- * Items written inside a top-level folder, each in the sub-folders its
1408
- * `archiFolder` path names (rebuilt as Archi had them: ids, names,
1409
- * documentation, properties); items without a path directly in the folder.
1410
- * `write(item, indent)` gives an item's lines.
1411
- * @private
1412
- */
1413
- function _inSubFolders(items, write, indent = ' ') {
1414
- const lines = [];
1415
- // An empty folder comes as {archiFolder, folderOnly}: it makes its folders and writes nothing itself.
1416
- const direct = items.filter(item => !item.archiFolder?.length && !item.folderOnly);
1417
- // Sub-folders in the order they are first named, each holding the items one level deeper.
1418
- const groups = new Map();
1419
- for (const item of items.filter(item => item.archiFolder?.length)) {
1420
- const [step, ...rest] = item.archiFolder;
1421
- if (!groups.has(step.id)) groups.set(step.id, { step, items: [] });
1422
- groups.get(step.id).items.push({ ...item, archiFolder: rest });
1423
- }
1424
- for (const { step, items: inner } of groups.values()) {
1425
- lines.push(`${indent}<folder name="${_escapeAttr(step.name)}" id="${_escapeAttr(step.id)}">`);
1426
- if (step.documentation) lines.push(`${indent} <documentation>${_escapeXml(step.documentation)}</documentation>`);
1427
- lines.push(..._inSubFolders(inner, write, indent + ' '));
1428
- lines.push(..._propertyLines(step.properties, indent + ' '));
1429
- lines.push(`${indent}</folder>`);
1430
- }
1431
- for (const item of direct) lines.push(...write(item, indent));
1432
- return lines;
1433
- }
1434
-
1435
- /** One element or relationship: its attributes, documentation and properties. @private */
1436
- function _serializeConcept(xsiType, attrs, concept, indent, extraLines = []) {
1437
- const all = { ...attrs, ...(concept.archi?.attributes ?? {}) };
1438
- const attrText = Object.entries(all).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
1439
- const body = [];
1440
- if (concept.documentation) body.push(`${indent} <documentation>${_escapeXml(concept.documentation)}</documentation>`);
1441
- body.push(..._extraElementLines(concept.archi?.elements, `${indent} `));
1442
- body.push(...extraLines);
1443
- for (const [k, v] of Object.entries(_typedArrayToProperties(concept.properties))) {
1444
- body.push(`${indent} <property key="${_escapeAttr(k)}" value="${_escapeAttr(v)}"/>`);
1445
- }
1446
- if (!body.length) return [`${indent}<element xsi:type="${xsiType}"${attrText}/>`];
1447
- return [`${indent}<element xsi:type="${xsiType}"${attrText}>`, ...body, `${indent}</element>`];
1448
- }
1449
-
1450
- /** A view-only note or image as its Archi visual. @private */
1451
- function _viewObjectVisual(el, bounds) {
1452
- const base = { id: `diag-${el.id}`, bounds, sourceConnections: [] };
1453
- if (el.archiVisualType) {
1454
- // A canvas or sketch box: its own class, text and picture.
1455
- return { ...base, type: el.archiVisualType, text: el.label ?? '', ...(el.iconUrl && { imagePath: el.iconUrl }) };
1456
- }
1457
- if (el.shape === 'image') {
1458
- return { ...base, type: el.archiVisualType === 'canvas-model-image' ? 'canvas-model-image' : 'diagram-model-image', name: el.name || '', imagePath: el.iconUrl, ...(el.href && { properties: [{ name: 'href', type: 'string', value: el.href }] }) };
1459
- }
1460
- return { ...base, type: 'diagram-model-note', text: el.label ?? '' };
1461
- }
1462
-
1463
- /** Reverse mapping: produce an Archi native XML string from renderengine data. */
1464
- export function toArchiXML(model, views) {
1465
- const { elements = [], relationships = [] } = model;
1466
- const viewArray = views || [];
1467
-
1468
- const lines = [
1469
- '<?xml version="1.0" encoding="UTF-8"?>',
1470
- `<archimate:model xmlns:archimate="${ARCHI_NAMESPACE}" xmlns:canvas="${ARCHI_CANVAS_NAMESPACE}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="${_escapeAttr(model.name || 'Untitled')}" id="${_escapeAttr(model.id || _newId('id'))}" version="${_escapeAttr(model.version || '5.0')}"${Object.entries(model.archi?.attributes ?? {}).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('')}>`,
1471
- ];
1472
-
1473
- // Archi always has its nine top-level folders, and fails to open a model
1474
- // that lacks some of them while it has a view: write them all, in its order.
1475
- const byFolder = new Map(ARCHI_FOLDERS.map(folder => [folder.type, []]));
1476
- // Specializations written for element types Archi lacks, by id.
1477
- const typeProfiles = new Map();
1478
- for (const el of elements) {
1479
- // Archi knows ArchiMate 3.2 only: a version 4 type goes back to its 3.2 one, and a type
1480
- // Archi has no class for (an IT type, a custom type) to the nearest one.
1481
- const { folder, type, original } = archiFolderOf(el);
1482
- let profile = null;
1483
- // An element Archi already gave a specialization keeps that one.
1484
- if (original && !el.archi?.attributes?.profiles) {
1485
- profile = typeProfileId(original);
1486
- typeProfiles.set(profile, { name: typeDisplayName(original), conceptType: _toPascalType(type) });
1487
- }
1488
- byFolder.get(folder.type).push({ el, type, original, profile });
1489
- }
1490
- for (const folder of ARCHI_FOLDERS) {
1491
- const items = byFolder.get(folder.type);
1492
- const body = [];
1493
- if (folder.type === 'relations') {
1494
- body.push(..._inSubFolders(relationships, (rel, indent) => {
1495
- const name = rel.label ?? rel.name;
1496
- const directed = rel.kind === 'association-directed';
1497
- const kind = directed ? 'association' : (rel.kind || 'association');
1498
- return _serializeConcept(`archimate:${_toPascalType(kind)}Relationship`,
1499
- { id: rel.id, ...(name && { name }), source: rel.source, target: rel.target, ...(directed && { directed: 'true' }) }, rel, indent);
1500
- }));
1501
- } else if (folder.type === 'diagrams') {
1502
- const emptyFolders = (model.emptyViewFolders ?? []).map(archiFolder => ({ archiFolder, folderOnly: true }));
1503
- body.push(..._inSubFolders([...viewArray, ...emptyFolders], (view, indent) => _serializeView(view).map(line => indent.slice(4) + line)));
1504
- } else {
1505
- body.push(..._inSubFolders(items.map(item => ({ ...item, archiFolder: item.el.archiFolder })),
1506
- ({ el, type, original, profile }, indent) => _serializeConcept(`archimate:${_toPascalType(type)}`,
1507
- { id: el.id, name: el.name || '', ...(profile && { profiles: profile }) }, el, indent,
1508
- original ? [`${indent} <feature name="${ARCHI_TYPE_FEATURE}" value="${_escapeAttr(original)}"/>`] : [])));
1509
- }
1510
- const open = ` <folder name="${_escapeAttr(folder.name)}" id="${folder.type}-folder" type="${folder.type}"`;
1511
- if (body.length) lines.push(`${open}>`, ...body.map(line => ` ${line}`), ' </folder>');
1512
- else lines.push(`${open}/>`);
1513
- }
1514
-
1515
- // What else the model carried (profiles, ...), its own properties, then its documentation (Archi's "purpose").
1516
- lines.push(..._extraElementLines(model.archi?.elements, ' '));
1517
- for (const [id, { name, conceptType }] of typeProfiles) {
1518
- lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"/>`);
1519
- }
1520
- lines.push(..._propertyLines(model.properties, ' '));
1521
- if (model.documentation) lines.push(` <purpose>${_escapeXml(model.documentation)}</purpose>`);
1522
- lines.push('</archimate:model>');
1523
- return lines.join('\n');
1524
- }
1525
-
1526
- function _serializeView(view) {
1527
- const xsiType = {
1528
- 'archimate-diagram-model': 'archimate:ArchimateDiagramModel',
1529
- 'sketch-model': 'archimate:SketchModel',
1530
- 'canvas-model': 'canvas:CanvasModel',
1531
- }[view.type] || 'archimate:ArchimateDiagramModel';
1532
-
1533
- const extra = Object.entries(view.archi?.attributes ?? {}).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
1534
- const lines = [` <element xsi:type="${xsiType}" name="${_escapeAttr(view.name || 'View')}" id="${view.id}"${extra}>`];
1535
- // Archi lists each visual's incoming connections on it (targetConnections); without
1536
- // them it draws a line only when its target happens to be created before its source.
1537
- const incoming = new Map();
1538
- const collect = (visual) => {
1539
- for (const sc of visual.sourceConnections || []) {
1540
- if (!incoming.has(sc.target)) incoming.set(sc.target, []);
1541
- incoming.get(sc.target).push(sc.id);
1542
- }
1543
- (visual.children || []).forEach(collect);
1544
- };
1545
- (view.children || []).forEach(collect);
1546
- for (const visual of view.children || []) {
1547
- lines.push(..._serializeVisual(visual, ' ', incoming));
1548
- }
1549
- // After the boxes, as Archi writes a view: its documentation, other elements, properties.
1550
- if (view.documentation) lines.push(` <documentation>${_escapeXml(view.documentation)}</documentation>`);
1551
- lines.push(..._extraElementLines(view.archi?.elements, ' '));
1552
- if (view.template) lines.push(` <feature name="${ARCHI_TEMPLATE_FEATURE}" value="true"/>`);
1553
- lines.push(..._propertyLines(view.properties, ' '));
1554
- lines.push(' </element>');
1555
- return lines;
1556
- }
1557
-
1558
- function _serializeVisual(visual, indent, incoming = new Map()) {
1559
- const xsiType = _visualTypeToXSI(visual.type);
1560
- const attrs = [`xsi:type="${xsiType}"`, `id="${visual.id}"`];
1561
- if (incoming.get(visual.id)?.length) attrs.push(`targetConnections="${incoming.get(visual.id).join(' ')}"`);
1562
- if (visual.archimateElement) attrs.push(`archimateElement="${visual.archimateElement}"`);
1563
- if (visual.referencedModel) attrs.push(`referencedModel="${visual.referencedModel}"`);
1564
- if (visual.imagePath) attrs.push(`imagePath="${_escapeAttr(visual.imagePath)}"`);
1565
- if (visual.name) attrs.push(`name="${_escapeAttr(visual.name)}"`);
1566
- if (visual.locked) attrs.push('locked="true"');
1567
- const { [ARCHI_LINE_ALPHA_FEATURE]: lineAlpha, ...attributeStyle } = visual.style || {};
1568
- for (const [name, value] of Object.entries(attributeStyle)) attrs.push(`${name}="${_escapeAttr(value)}"`);
1569
- for (const [name, value] of Object.entries(visual.extraAttributes || {})) attrs.push(`${name}="${_escapeAttr(value)}"`);
1570
-
1571
- const lines = [`${indent}<child ${attrs.join(' ')}>`];
1572
- if (visual.bounds) {
1573
- const b = visual.bounds;
1574
- lines.push(`${indent} <bounds x="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}"/>`);
1575
- }
1576
- // A note's text: Archi keeps it in a <content> element.
1577
- if (visual.text) lines.push(`${indent} <content>${_escapeXml(visual.text)}</content>`);
1578
- for (const [name, value] of Object.entries(visual.features || {})) {
1579
- lines.push(`${indent} <feature name="${_escapeAttr(name)}" value="${_escapeAttr(String(value))}"/>`);
1580
- }
1581
- if (lineAlpha !== undefined) lines.push(`${indent} <feature name="${ARCHI_LINE_ALPHA_FEATURE}" value="${_escapeAttr(String(lineAlpha))}"/>`);
1582
- for (const [key, value] of Object.entries(_typedArrayToProperties(visual.properties))) {
1583
- lines.push(`${indent} <property key="${_escapeAttr(key)}" value="${_escapeAttr(value)}"/>`);
1584
- }
1585
- lines.push(..._extraElementLines(visual.extraElements, `${indent} `));
1586
- for (const sc of visual.sourceConnections || []) {
1587
- // A relationship's line is archimate:Connection; a plain line keeps its own class.
1588
- const xsiType = sc.relationship ? 'archimate:Connection' : _visualTypeToXSI(sc.archiType ?? 'diagram-model-connection');
1589
- const scAttrs = [
1590
- // Archi's default class for a line needs no type, and Archi writes none.
1591
- ...(xsiType === 'archimate:DiagramModelConnection' ? [] : [`xsi:type="${xsiType}"`]),
1592
- `id="${sc.id}"`,
1593
- `source="${sc.source}"`,
1594
- `target="${sc.target}"`,
1595
- ];
1596
- if (sc.relationship) scAttrs.push(`archimateRelationship="${sc.relationship}"`);
1597
- if (!sc.relationship && sc.name) scAttrs.push(`name="${_escapeAttr(sc.name)}"`);
1598
- if (!sc.relationship && sc.connectionType) scAttrs.push(`type="${sc.connectionType}"`);
1599
- for (const [name, value] of Object.entries(sc.style || {})) scAttrs.push(`${name}="${_escapeAttr(value)}"`);
1600
- for (const [name, value] of Object.entries(sc.extraAttributes || {})) scAttrs.push(`${name}="${_escapeAttr(value)}"`);
1601
-
1602
- if (sc.bendpoints?.length) {
1603
- lines.push(`${indent} <sourceConnection ${scAttrs.join(' ')}>`);
1604
- for (const bp of sc.bendpoints) {
1605
- lines.push(`${indent} <bendpoint ${BENDPOINT_KEYS.map(k => `${k}="${bp[k]}"`).join(' ')}/>`);
1606
- }
1607
- lines.push(`${indent} </sourceConnection>`);
1608
- } else {
1609
- lines.push(`${indent} <sourceConnection ${scAttrs.join(' ')}/>`);
1610
- }
1611
- }
1612
- for (const child of visual.children || []) {
1613
- lines.push(..._serializeVisual(child, `${indent} `, incoming));
1614
- }
1615
- lines.push(`${indent}</child>`);
1616
- return lines;
1617
- }
1618
-
1619
- /** Archi's top-level folders, in its order, with the layers whose elements each holds. */
1620
- export const ARCHI_FOLDERS = Object.freeze([
1621
- { type: 'strategy', name: 'Strategy', layers: ['strategy'] },
1622
- { type: 'business', name: 'Business', layers: ['business'] },
1623
- { type: 'application', name: 'Application', layers: ['application'] },
1624
- { type: 'technology', name: 'Technology & Physical', layers: ['technology', 'physical'] },
1625
- { type: 'motivation', name: 'Motivation', layers: ['motivation'] },
1626
- { type: 'implementation_migration', name: 'Implementation & Migration', layers: ['implementation'] },
1627
- { type: 'other', name: 'Other', layers: ['composite', 'other'] },
1628
- { type: 'relations', name: 'Relations' },
1629
- { type: 'diagrams', name: 'Views' },
1630
- ]);
1631
-
1632
- /**
1633
- * Where Archi keeps an element: the top-level folder of its ArchiMate 3.2
1634
- * layer, with the 3.2 type it is written as and, for a type Archi lacks, its
1635
- * own (archiElementType).
1636
- * @param {Object} el - A model element
1637
- * @returns {{folder: Object, type: string, original: string|undefined}}
1638
- */
1639
- export function archiFolderOf(el) {
1640
- const { type, original } = archiElementType(el);
1641
- const layer = ArchimateNotation.elementLayer({ type }, '3.2');
1642
- const folder = ARCHI_FOLDERS.find(x => x.layers?.includes(layer !== 'other' ? layer : el.layer)) ?? ARCHI_FOLDERS.find(x => x.type === 'other');
1643
- return { folder, type, original };
1644
- }
1645
-
1646
- function _visualTypeToXSI(type) {
1647
- const map = {
1648
- 'diagram-model-note': 'archimate:Note',
1649
- 'diagram-model-group': 'archimate:Group',
1650
- 'diagram-model-connection': 'archimate:DiagramModelConnection',
1651
- 'canvas-model-connection': 'canvas:CanvasModelConnection',
1652
- 'diagram-model-image': 'archimate:DiagramModelImage',
1653
- 'diagram-model-reference': 'archimate:DiagramModelReference',
1654
- 'sketch-model-sticky': 'archimate:SketchModelSticky',
1655
- 'sketch-model-actor': 'archimate:SketchModelActor',
1656
- 'canvas-model-block': 'canvas:CanvasModelBlock',
1657
- 'canvas-model-sticky': 'canvas:CanvasModelSticky',
1658
- 'canvas-model-image': 'canvas:CanvasModelImage',
1659
- };
1660
- return map[type] || 'archimate:DiagramObject';
1661
- }
1662
-
1663
- function _toPascalType(kebab) {
1664
- if (!kebab) return '';
1665
- return kebab.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
1666
- }
1667
-
1668
- function _newId(prefix = 'id') {
1669
- return `${prefix}_${Math.random().toString(36).slice(2, 9)}`;
1670
- }
1671
-
1672
- /** Text for element content. A CR is written as a reference: a parser turns a literal one into a newline. */
1673
- function _escapeXml(str) {
1674
- return String(str)
1675
- .replace(/&/g, '&amp;')
1676
- .replace(/</g, '&lt;')
1677
- .replace(/>/g, '&gt;')
1678
- .replace(/"/g, '&quot;')
1679
- .replace(/\r/g, '&#xD;');
1680
- }
1681
-
1682
- /** Text for an attribute value: a parser also turns a literal newline or tab there into a space. */
1683
- function _escapeAttr(str) {
1684
- return _escapeXml(str).replace(/\n/g, '&#xA;').replace(/\t/g, '&#x9;');
1685
- }
1686
-
1687
- /*
1688
- * Shared with the Open Group Exchange Format (archimateExchangeFormat.js), which
1689
- * reads and writes the same parsed model and view trees as this module.
1690
- */
1691
- export {
1692
- _visualBoxes as archiVisualBoxes,
1693
- _bendpointLocations as archiBendpointLocations,
1694
- _relativizeLocatedBendpoints as relativizeArchiBendpoints,
1695
- _parseFontFeature as parseArchiFont,
1696
- _fontString as archiFontString,
1697
- _typedArrayToProperties as typedArrayToProperties,
1698
- _toPascalType as toPascalType,
1699
- _escapeXml as escapeXml,
1700
- _escapeAttr as escapeAttr,
1701
- };
1
+ import { ArchimateNotation } from './archimate.js';
2
+ import { archiElementType, typeProfileId, typeDisplayName, ARCHI_TYPE_FEATURE, ARCHI_TYPE_PROFILE_PREFIX } from './archimateArchiTypes.js';
3
+ import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
4
+ import './charting/diagrams/CanvasView.js'; // registers the canvas view type
5
+ import './charting/diagrams/SketchView.js'; // registers the sketch view type
6
+ import { ElementInstance } from './charting/diagrams/ElementInstance.js';
7
+ import { ViewObjectInstance } from './charting/diagrams/ViewObjectInstance.js';
8
+ import { RelationshipInstance } from './charting/diagrams/RelationshipInstance.js';
9
+ import { ViewConnectionInstance } from './charting/diagrams/ViewConnectionInstance.js';
10
+
11
+ /**
12
+ * ArchimateArchiFormat — pure import/export helpers for Archi’s native
13
+ * `.archimate` XML format (not the Open Group Model Exchange File Format).
14
+ *
15
+ * The module maps Archi model elements, relationships, and view/visual
16
+ * children to renderengine’s `archimateDiagram` inputs, and back again.
17
+ */
18
+
19
+ export const ARCHI_VISUAL_OBJECTS = Object.freeze([
20
+ 'diagram-model-note',
21
+ 'diagram-model-group',
22
+ 'diagram-model-connection',
23
+ 'diagram-model-image',
24
+ 'diagram-model-reference',
25
+ 'sketch-model-sticky',
26
+ 'sketch-model-actor',
27
+ 'canvas-model-block',
28
+ 'canvas-model-sticky',
29
+ 'canvas-model-image',
30
+ ]);
31
+
32
+ export const ARCHI_VIEW_TYPES = Object.freeze([
33
+ 'archimate-diagram-model',
34
+ 'sketch-model',
35
+ 'canvas-model',
36
+ ]);
37
+
38
+ const ARCHI_NAMESPACE = 'http://www.archimatetool.com/archimate';
39
+ /** Archi's canvas classes live in their own namespace. */
40
+ const ARCHI_CANVAS_NAMESPACE = 'http://www.archimatetool.com/archimate/canvas';
41
+
42
+ /** Lightweight XML element descriptor used by both the DOMParser wrapper and
43
+ * the Node fallback parser. Mirrors the small subset of DOM we need. */
44
+ export class ArchiXMLElement {
45
+ constructor(tagName, attributes = {}) {
46
+ this.tagName = tagName;
47
+ this.attributes = attributes;
48
+ this.children = [];
49
+ this.textContent = '';
50
+ }
51
+
52
+ getAttribute(name) {
53
+ return this.attributes[name] ?? null;
54
+ }
55
+
56
+ querySelectorAll(tagName) {
57
+ const out = [];
58
+ for (const child of this.children) {
59
+ if (child.tagName === tagName) out.push(child);
60
+ out.push(...child.querySelectorAll(tagName));
61
+ }
62
+ return out;
63
+ }
64
+
65
+ querySelector(tagName) {
66
+ for (const child of this.children) {
67
+ if (child.tagName === tagName) return child;
68
+ const deep = child.querySelector(tagName);
69
+ if (deep) return deep;
70
+ }
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /** Parse an XML string into an `ArchiXMLElement` tree. Uses DOMParser when
76
+ * available (browser), otherwise a minimal dependency-free fallback. */
77
+ export function parseXML(xml) {
78
+ if (typeof DOMParser !== 'undefined') {
79
+ const doc = new DOMParser().parseFromString(xml, 'application/xml');
80
+ const parserError = doc.querySelector('parsererror');
81
+ if (parserError) throw new Error('XML parse error');
82
+ return _fromDOMNode(doc.documentElement);
83
+ }
84
+ return _parseXMLFallback(xml);
85
+ }
86
+
87
+ function _fromDOMNode(node) {
88
+ const attrs = {};
89
+ for (const attr of node.attributes || []) {
90
+ attrs[attr.name] = attr.value;
91
+ }
92
+ const el = new ArchiXMLElement(node.tagName, attrs);
93
+ for (const child of node.childNodes || []) {
94
+ if (child.nodeType === 1) { // ELEMENT_NODE
95
+ el.children.push(_fromDOMNode(child));
96
+ } else if (child.nodeType === 3) { // TEXT_NODE
97
+ el.textContent += child.textContent;
98
+ }
99
+ }
100
+ if (!el.textContent.trim()) el.textContent = ''; // indentation between elements, not text
101
+ return el;
102
+ }
103
+
104
+ function _parseXMLFallback(xml) {
105
+ // XML 1.0 (2.11): a parser sees every line break as a newline; a CR written as &#xD; survives (decoded later).
106
+ const trimmed = xml.replace(/\r\n?/g, '\n').trim();
107
+ const root = _parseNode(trimmed, { pos: 0 });
108
+ if (!root) throw new Error('XML parse error: no root element');
109
+ return root;
110
+ }
111
+
112
+ function _parseNode(xml, state) {
113
+ _skipWhitespace(xml, state);
114
+ if (state.pos >= xml.length || xml[state.pos] !== '<') return null;
115
+
116
+ // Skip XML declarations, processing instructions and comments.
117
+ if (xml.slice(state.pos, state.pos + 2) === '<?') {
118
+ const end = xml.indexOf('?>', state.pos);
119
+ state.pos = end === -1 ? xml.length : end + 2;
120
+ return _parseNode(xml, state);
121
+ }
122
+ if (xml.startsWith('<!--', state.pos)) {
123
+ const end = xml.indexOf('-->', state.pos);
124
+ state.pos = end === -1 ? xml.length : end + 3;
125
+ return _parseNode(xml, state);
126
+ }
127
+
128
+ // A quoted attribute value may hold '>' or the other quote character.
129
+ const tagMatch = xml.slice(state.pos).match(/^<([^>\s/]+)((?:[^>"']|"[^"]*"|'[^']*')*)>/);
130
+ if (!tagMatch) throw new Error('XML parse error: malformed tag');
131
+ const tagName = tagMatch[1];
132
+ const attrText = tagMatch[2];
133
+ state.pos += tagMatch[0].length;
134
+
135
+ const isSelfClosing = attrText.trimEnd().endsWith('/');
136
+ const attributes = _parseAttributes(attrText);
137
+ const node = new ArchiXMLElement(tagName, attributes);
138
+
139
+ if (isSelfClosing) {
140
+ return node;
141
+ }
142
+
143
+ // Whitespace is text: a content's leading spaces are its own (Archi uses them to clear a picture);
144
+ // indentation between elements is dropped below, once the element is read.
145
+ while (state.pos < xml.length) {
146
+ if (xml.slice(state.pos, state.pos + 2) === '</') {
147
+ const closeMatch = xml.slice(state.pos).match(/^<\/([^>]+)>/);
148
+ if (!closeMatch || closeMatch[1] !== tagName) {
149
+ throw new Error(`XML parse error: mismatched closing tag for ${tagName}`);
150
+ }
151
+ state.pos += closeMatch[0].length;
152
+ if (!node.textContent.trim()) node.textContent = ''; // indentation between elements, not text
153
+ return node;
154
+ }
155
+ if (xml.startsWith('<![CDATA[', state.pos)) {
156
+ const end = xml.indexOf(']]>', state.pos);
157
+ node.textContent += xml.slice(state.pos + 9, end === -1 ? xml.length : end);
158
+ state.pos = end === -1 ? xml.length : end + 3;
159
+ } else if (xml.startsWith('<!--', state.pos)) {
160
+ const end = xml.indexOf('-->', state.pos);
161
+ state.pos = end === -1 ? xml.length : end + 3;
162
+ } else if (xml[state.pos] === '<') {
163
+ const child = _parseNode(xml, state);
164
+ if (child) node.children.push(child);
165
+ } else {
166
+ const textEnd = xml.indexOf('<', state.pos);
167
+ node.textContent += _unescapeXml(xml.slice(state.pos, textEnd === -1 ? xml.length : textEnd));
168
+ state.pos = textEnd === -1 ? xml.length : textEnd;
169
+ }
170
+ }
171
+ if (!node.textContent.trim()) node.textContent = ''; // indentation between elements, not text
172
+ return node;
173
+ }
174
+
175
+ function _skipWhitespace(xml, state) {
176
+ while (state.pos < xml.length && /\s/.test(xml[state.pos])) state.pos++;
177
+ }
178
+
179
+ function _parseAttributes(text) {
180
+ const attrs = {};
181
+ const regex = /([^\s=/]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
182
+ let m;
183
+ while ((m = regex.exec(text)) !== null) {
184
+ attrs[m[1]] = _unescapeXml(m[2] ?? m[3]);
185
+ }
186
+ return attrs;
187
+ }
188
+
189
+ /** Decode the predefined entities and character references (the fallback parser's half of _escapeXml). */
190
+ function _unescapeXml(str) {
191
+ return str.replace(/&(#x[0-9a-f]+|#\d+|lt|gt|quot|apos|amp);/gi, (whole, ref) => {
192
+ const r = ref.toLowerCase();
193
+ if (r[0] === '#') {
194
+ const code = r[1] === 'x' ? parseInt(r.slice(2), 16) : parseInt(r.slice(1), 10);
195
+ return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
196
+ }
197
+ return { lt: '<', gt: '>', quot: '"', apos: "'", amp: '&' }[r];
198
+ });
199
+ }
200
+
201
+ /** Parse an Archi native XML model into plain JavaScript structures. */
202
+ export function parseArchiModel(xml) {
203
+ const root = typeof xml === 'string' ? parseXML(xml) : xml;
204
+ const model = {
205
+ id: root.getAttribute('id'),
206
+ name: root.getAttribute('name'),
207
+ version: root.getAttribute('version'),
208
+ elements: [],
209
+ relationships: [],
210
+ views: [],
211
+ // The Views sub-folders that hold no view (each a path of folder steps); a view carries its own.
212
+ emptyViewFolders: [],
213
+ // The model's own documentation (Archi's "purpose") and properties, in order (keys may repeat).
214
+ documentation: _childText(root, 'purpose'),
215
+ properties: _propertyPairs(root),
216
+ archi: _extrasOrNull(_archiExtras(root, ARCHI_MODEL_HANDLED)),
217
+ };
218
+
219
+ for (const folder of root.children.filter(c => c.tagName === 'folder')) {
220
+ const folderType = folder.getAttribute('type');
221
+ if (folderType === 'diagrams') {
222
+ _walkViewFolder(folder, model);
223
+ continue;
224
+ }
225
+
226
+ // Recursively walk folder structure (handles nested sub-folders)
227
+ _walkFolder(folder, folderType, model);
228
+ }
229
+
230
+ return model;
231
+ }
232
+
233
+ /** A sub-folder as one step of a folder path: its id, name, documentation and properties. @private */
234
+ function _folderStep(folder) {
235
+ const documentation = _childText(folder, 'documentation');
236
+ const properties = _propertyPairs(folder);
237
+ return {
238
+ id: folder.getAttribute('id'),
239
+ name: folder.getAttribute('name') ?? '',
240
+ ...(documentation && { documentation }),
241
+ ...(properties.length && { properties }),
242
+ };
243
+ }
244
+
245
+ /** Every view in the Views folder, sub-folders included, in file order; each
246
+ * remembers its sub-folder path (`archiFolder`). A sub-folder with nothing in
247
+ * it is kept as its path (`emptyViewFolders`), as Archi shows it. @private */
248
+ function _walkViewFolder(folder, model, path = []) {
249
+ if (path.length && !folder.children.some(c => c.tagName === 'element' || c.tagName === 'folder')) model.emptyViewFolders.push(path);
250
+ for (const child of folder.children) {
251
+ if (child.tagName === 'element') {
252
+ const view = _parseView(child);
253
+ if (path.length) view.archiFolder = path;
254
+ model.views.push(view);
255
+ } else if (child.tagName === 'folder') {
256
+ _walkViewFolder(child, model, [...path, _folderStep(child)]);
257
+ }
258
+ }
259
+ }
260
+
261
+ /** Every element (or relationship) in a top-level folder, sub-folders included;
262
+ * each remembers its sub-folder path (`archiFolder`). @private */
263
+ function _walkFolder(folder, folderType, model, path = []) {
264
+ for (const child of folder.children) {
265
+ if (child.tagName === 'element') {
266
+ const parsed = _parseModelElement(child);
267
+ if (path.length) parsed.archiFolder = path;
268
+ if (folderType === 'relations') {
269
+ model.relationships.push(parsed);
270
+ } else {
271
+ model.elements.push(parsed);
272
+ }
273
+ } else if (child.tagName === 'folder') {
274
+ _walkFolder(child, folderType, model, [...path, _folderStep(child)]);
275
+ }
276
+ }
277
+ }
278
+
279
+ function _parseModelElement(el) {
280
+ const xsiType = el.getAttribute('xsi:type') || '';
281
+ const type = xsiType.replace(/^archimate:/, '');
282
+ const element = {
283
+ id: el.getAttribute('id'),
284
+ name: el.getAttribute('name'),
285
+ type,
286
+ documentation: _childText(el, 'documentation'),
287
+ properties: _parseProperties(el),
288
+ };
289
+ const source = el.getAttribute('source');
290
+ const target = el.getAttribute('target');
291
+ if (source) element.source = source;
292
+ if (target) element.target = target;
293
+ if (el.getAttribute('directed') === 'true') element.directed = true;
294
+ const archi = _extrasOrNull(_archiExtras(el, ARCHI_CONCEPT_HANDLED));
295
+ if (archi) element.archi = archi;
296
+ return element;
297
+ }
298
+
299
+ function _parseView(el) {
300
+ const xsiType = el.getAttribute('xsi:type') || '';
301
+ const view = {
302
+ id: el.getAttribute('id'),
303
+ name: el.getAttribute('name'),
304
+ type: _viewTypeFromXSI(xsiType),
305
+ documentation: _childText(el, 'documentation'),
306
+ properties: _propertyPairs(el),
307
+ archi: _extrasOrNull(_archiExtras(el, ARCHI_VIEW_HANDLED)),
308
+ children: [],
309
+ };
310
+ for (const child of el.children.filter(c => c.tagName === 'child')) {
311
+ view.children.push(_parseVisual(child));
312
+ }
313
+ _relativizeLocatedBendpoints(view);
314
+ return view;
315
+ }
316
+
317
+ /** Archi's bendpoint: offsets from the source's and the target's centre. */
318
+ const BENDPOINT_KEYS = Object.freeze(['startX', 'startY', 'endX', 'endY']);
319
+
320
+ /** Archi's size for a visual saved at its default size (width/height -1). @private */
321
+ const ARCHI_DEFAULT_SIZE = Object.freeze({
322
+ 'diagram-model-group': { width: 400, height: 140 },
323
+ 'diagram-model-note': { width: 185, height: 80 },
324
+ });
325
+ const ARCHI_ELEMENT_SIZE = Object.freeze({ width: 120, height: 55 });
326
+
327
+ /** Every visual's absolute bounds in a view (or visual tree), by visual id. @private */
328
+ function _visualBoxes(root) {
329
+ const boxes = new Map();
330
+ const walk = (visual, origin) => {
331
+ const bounds = _absoluteBounds(visual, origin);
332
+ if (bounds && visual.id) boxes.set(visual.id, bounds);
333
+ for (const child of visual.children || []) walk(child, bounds ?? origin);
334
+ };
335
+ walk(root, VIEW_ORIGIN);
336
+ return boxes;
337
+ }
338
+
339
+ /** A box's anchor point as Archi (GEF's Rectangle#getCenter) takes it: integer halves. @private */
340
+ const _centre = (b) => b && { x: b.x + Math.floor(b.width / 2), y: b.y + Math.floor(b.height / 2) };
341
+
342
+ /**
343
+ * Where a connection's bendpoints are on the view. Archi places bendpoint i of n
344
+ * between its two offsets, weighted by w = (i + 1) / (n + 1) from source to target:
345
+ * (1 - w)(sourceCentre + start) + w(targetCentre + end). A connection to another
346
+ * connection has no box at that end; the other end's offset is used alone.
347
+ * @private
348
+ */
349
+ function _bendpointLocations(sc, boxes) {
350
+ const src = _centre(boxes.get(sc.source));
351
+ const tgt = _centre(boxes.get(sc.target));
352
+ const n = sc.bendpoints.length;
353
+ return sc.bendpoints.flatMap((bp, i) => {
354
+ const fromSource = src && { x: src.x + bp.startX, y: src.y + bp.startY };
355
+ const fromTarget = tgt && { x: tgt.x + bp.endX, y: tgt.y + bp.endY };
356
+ if (!fromSource || !fromTarget) return fromSource || fromTarget ? [fromSource || fromTarget] : [];
357
+ const w = (i + 1) / (n + 1);
358
+ return [{ x: (1 - w) * fromSource.x + w * fromTarget.x, y: (1 - w) * fromSource.y + w * fromTarget.y }];
359
+ });
360
+ }
361
+
362
+ /** Archi bendpoints for points on the view, between two boxes (whole pixels, as Archi keeps them). @private */
363
+ function _relativeBendpoints(points, sourceBox, targetBox) {
364
+ const src = _centre(sourceBox) ?? VIEW_ORIGIN;
365
+ const tgt = _centre(targetBox) ?? VIEW_ORIGIN;
366
+ return points.map(p => ({
367
+ startX: Math.round(p.x - src.x), startY: Math.round(p.y - src.y),
368
+ endX: Math.round(p.x - tgt.x), endY: Math.round(p.y - tgt.y),
369
+ }));
370
+ }
371
+
372
+ /** Convert a parsed view's x/y bendpoints (an older export) to Archi's form. @private */
373
+ function _relativizeLocatedBendpoints(view) {
374
+ const boxes = _visualBoxes(view);
375
+ const walk = (visual) => {
376
+ for (const sc of visual.sourceConnections || []) {
377
+ if (sc.bendpoints?.some(bp => 'x' in bp)) {
378
+ sc.bendpoints = _relativeBendpoints(sc.bendpoints, boxes.get(sc.source), boxes.get(sc.target));
379
+ }
380
+ }
381
+ for (const child of visual.children || []) walk(child);
382
+ };
383
+ walk(view);
384
+ }
385
+
386
+ function _viewTypeFromXSI(xsiType) {
387
+ const map = {
388
+ 'archimate:ArchimateDiagramModel': 'archimate-diagram-model',
389
+ 'archimate:SketchModel': 'sketch-model',
390
+ 'archimate:CanvasModel': 'canvas-model',
391
+ 'canvas:CanvasModel': 'canvas-model', // Archi writes canvases in their own namespace
392
+ };
393
+ return map[xsiType] || xsiType.replace(/^archimate:/, '');
394
+ }
395
+
396
+ /**
397
+ * Archi's XML names for the visual classes it renames (EMF extended metadata in
398
+ * Archi's model): xsi:type -> our visual type. Every other class is written
399
+ * under its own name (DiagramModelImage, DiagramModelReference, ...).
400
+ */
401
+ const ARCHI_VISUAL_XML_NAMES = Object.freeze({
402
+ Group: 'diagram-model-group',
403
+ Note: 'diagram-model-note',
404
+ DiagramObject: 'diagram-object',
405
+ Connection: 'diagram-model-archimate-connection',
406
+ });
407
+
408
+ function _parseVisual(el) {
409
+ const xsiType = el.getAttribute('xsi:type') || '';
410
+ const className = xsiType.replace(/^[a-z]+:/, ''); // archimate:, canvas:, ...
411
+ const type = ARCHI_VISUAL_XML_NAMES[className] ?? className.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
412
+ const visual = {
413
+ id: el.getAttribute('id'),
414
+ type,
415
+ archimateElement: el.getAttribute('archimateElement') || null,
416
+ referencedModel: el.getAttribute('referencedModel') || null,
417
+ imagePath: el.getAttribute('imagePath') || null,
418
+ name: el.getAttribute('name') || null,
419
+ text: _childText(el, 'content') || el.getAttribute('text') || null,
420
+ notes: _childText(el, 'notes') || null,
421
+ bounds: _parseBounds(el),
422
+ style: _parseStyle(el),
423
+ archi: _archiExtras(el),
424
+ features: _parseFeatures(el),
425
+ properties: _parseProperties(el),
426
+ sourceConnections: [],
427
+ children: [],
428
+ };
429
+
430
+ for (const sc of el.children.filter(c => c.tagName === 'sourceConnection')) {
431
+ const bendpoints = [];
432
+ for (const bp of sc.children.filter(c => c.tagName === 'bendpoint')) {
433
+ bendpoints.push(bp.getAttribute('x') !== null && bp.getAttribute('startX') === null
434
+ ? { x: Number(bp.getAttribute('x')), y: Number(bp.getAttribute('y')) }
435
+ : Object.fromEntries(BENDPOINT_KEYS.map(k => [k, Number(bp.getAttribute(k) ?? 0)])));
436
+ }
437
+ visual.sourceConnections.push({
438
+ id: sc.getAttribute('id'),
439
+ source: sc.getAttribute('source'),
440
+ target: sc.getAttribute('target'),
441
+ // Archi writes archimateRelationship; 'relationship' is what older exports of ours wrote.
442
+ relationship: sc.getAttribute('archimateRelationship') || sc.getAttribute('relationship') || null,
443
+ type: (sc.getAttribute('xsi:type') || '').replace(/^archimate:/, ''),
444
+ // A plain line's class (no xsi:type is Archi's default, DiagramModelConnection) and its look (type bits).
445
+ archiType: _kebab((sc.getAttribute('xsi:type') || 'DiagramModelConnection').replace(/^[a-z]+:/, '')),
446
+ connectionType: Number(sc.getAttribute('type')) || 0,
447
+ archi: _archiExtras(sc, ARCHI_LINE_HANDLED), // a line's lock passes through as written
448
+ name: sc.getAttribute('name') || '',
449
+ style: _parseStyle(sc),
450
+ features: _parseFeatures(sc),
451
+ ...(bendpoints.length && { bendpoints }),
452
+ });
453
+ }
454
+
455
+ for (const child of el.children.filter(c => c.tagName === 'child')) {
456
+ visual.children.push(_parseVisual(child));
457
+ }
458
+
459
+ return visual;
460
+ }
461
+
462
+ function _parseBounds(el) {
463
+ const bounds = el.querySelector('bounds');
464
+ if (!bounds) return null;
465
+ return {
466
+ x: Number(bounds.getAttribute('x')),
467
+ y: Number(bounds.getAttribute('y')),
468
+ width: Number(bounds.getAttribute('width')),
469
+ height: Number(bounds.getAttribute('height')),
470
+ };
471
+ }
472
+
473
+ const _kebab = (className) => className.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
474
+
475
+ /** Attributes the model reads or writes itself; anything else on a visual or line is passed through. */
476
+ const ARCHI_HANDLED_ATTRIBUTES = new Set([
477
+ 'xsi:type', 'id', 'archimateElement', 'archimateRelationship', 'source', 'target', 'targetConnections',
478
+ 'name', 'imagePath', 'type', 'text', 'locked',
479
+ ]);
480
+
481
+ /** The feature that names the logical group (ItemGroup) a box is in: Archi has none of its own. */
482
+ export const ARCHI_GROUP_FEATURE = 'renderengine.group';
483
+ /** The feature that names the kind of that group when it is not a plain one ('background': a template's, pinned behind). */
484
+ export const ARCHI_GROUP_KIND_FEATURE = 'renderengine.groupKind';
485
+ /** The feature that fixes a box in place where Archi has no lock of its own (anything but a canvas box). */
486
+ export const ARCHI_LOCK_FEATURE = 'renderengine.locked';
487
+ /** The feature that marks a view as a template (kept to copy from): Archi has no such flag. */
488
+ export const ARCHI_TEMPLATE_FEATURE = 'renderengine.template';
489
+
490
+ /** The visuals Archi itself can lock (ILockable: its canvas classes). */
491
+ const ARCHI_LOCKABLE = new Set(['canvas-model-block', 'canvas-model-sticky', 'canvas-model-image', 'canvas-model-connection']);
492
+ /** A line's lock is not the arrangement's (which holds boxes): it passes through. */
493
+ const ARCHI_LINE_HANDLED = {
494
+ attributes: new Set(['xsi:type', 'id', 'archimateElement', 'archimateRelationship', 'source', 'target', 'targetConnections', 'name', 'imagePath', 'type', 'text']),
495
+ elements: null, // set below, once ARCHI_HANDLED_ELEMENTS exists
496
+ style: true,
497
+ };
498
+ /** Child elements the model reads itself. */
499
+ const ARCHI_HANDLED_ELEMENTS = new Set(['child', 'sourceConnection', 'bounds', 'content', 'feature', 'property', 'bendpoint', 'documentation']);
500
+ ARCHI_LINE_HANDLED.elements = ARCHI_HANDLED_ELEMENTS;
501
+
502
+ /**
503
+ * What a visual or line carries that the model does not use, kept to write back:
504
+ * other attributes (locked, hintTitle, imagePosition, alpha, ...), other child
505
+ * elements (hintContent, notes, ...) and the raw style attributes.
506
+ * @private
507
+ */
508
+ function _archiExtras(el, handled = { attributes: ARCHI_HANDLED_ATTRIBUTES, elements: ARCHI_HANDLED_ELEMENTS, style: true }) {
509
+ const attributes = {};
510
+ const locked = handled.attributes === ARCHI_HANDLED_ATTRIBUTES && el.getAttribute('locked') === 'true';
511
+ for (const [name, value] of Object.entries(el.attributes ?? {})) {
512
+ if (handled.attributes.has(name) || name.startsWith('xmlns')) continue;
513
+ if (handled.style && ARCHI_STYLE_ATTRIBUTES.includes(name)) continue;
514
+ attributes[name] = value;
515
+ }
516
+ const elements = el.children
517
+ .filter(c => !handled.elements.has(c.tagName))
518
+ .map(c => ({ tag: c.tagName, text: c.textContent, attributes: { ...(c.attributes ?? {}) } }));
519
+ return { attributes, elements, ...(handled.style && { style: _parseStyle(el) }), ...(locked && { locked: true }) };
520
+ }
521
+
522
+ /** What a concept (element or relationship) carries that the model does not read: accessType,
523
+ * strength, profiles, ...; null when nothing. Written back on export. @private */
524
+ const ARCHI_CONCEPT_HANDLED = {
525
+ attributes: new Set(['xsi:type', 'id', 'name', 'source', 'target', 'directed']),
526
+ elements: new Set(['documentation', 'property']),
527
+ };
528
+ /** The same for a view (hintTitle, viewpoint, connectionRouterType, hintContent, ...). */
529
+ const ARCHI_VIEW_HANDLED = {
530
+ attributes: new Set(['xsi:type', 'id', 'name']),
531
+ elements: new Set(['documentation', 'property', 'child']),
532
+ };
533
+ /** The same for the model (its profiles, metadata, ...). */
534
+ const ARCHI_MODEL_HANDLED = {
535
+ attributes: new Set(['id', 'name', 'version']),
536
+ elements: new Set(['folder', 'purpose', 'property']),
537
+ };
538
+
539
+ /** Extras with nothing in them are null. @private */
540
+ function _extrasOrNull(extras) {
541
+ return Object.keys(extras.attributes).length || extras.elements.length ? extras : null;
542
+ }
543
+
544
+ /** Child elements kept as {tag, text, attributes}, as lines. @private */
545
+ function _extraElementLines(elements = [], indent) {
546
+ return elements.map(({ tag, text = '', attributes = {} }) => {
547
+ const attrText = Object.entries(attributes).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
548
+ return text ? `${indent}<${tag}${attrText}>${_escapeXml(text)}</${tag}>` : `${indent}<${tag}${attrText}/>`;
549
+ });
550
+ }
551
+
552
+ /** An instance's `archi` record from its parsed visual and its appearance as imported
553
+ * (null when there is nothing to keep). @private */
554
+ function _archiRecord(visual, appearance) {
555
+ const record = {
556
+ ...(visual.id && { id: visual.id }), // Archi's id for the visual, kept on export
557
+ ...(visual.archiImagePath && { imagePath: visual.archiImagePath }), // its picture's path in the archive
558
+ ...visual.archi,
559
+ ...(visual.name && { name: visual.name }),
560
+ ...(Object.keys(visual.features ?? {}).length && { features: { ...visual.features } }),
561
+ };
562
+ return { ...record, appearance: { ...appearance } };
563
+ }
564
+
565
+ /** Archi keeps a box's line transparency as a feature, not an attribute. */
566
+ const ARCHI_LINE_ALPHA_FEATURE = 'lineAlpha';
567
+
568
+ /** Which appearance fields each Archi style attribute sets. */
569
+ const STYLE_FIELDS = Object.freeze({
570
+ fillColor: ['fillColor'], lineColor: ['strokeColor'], borderColor: ['strokeColor'], fontColor: ['textColor'],
571
+ font: ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle'], lineWidth: ['strokeWidth'],
572
+ textAlignment: ['textAlign'], textPosition: ['textPosition'],
573
+ alpha: ['fillColor', 'fillOpacity'], [ARCHI_LINE_ALPHA_FEATURE]: ['strokeColor', 'strokeOpacity'],
574
+ gradient: ['gradient'],
575
+ });
576
+
577
+ /**
578
+ * Archi's style attributes for an appearance: the original value of each one
579
+ * whose meaning is unchanged since import (so values the model does not model,
580
+ * and a font's full SWT string, survive), and the appearance's own value for
581
+ * any changed. Without a record, what the appearance says.
582
+ * @private
583
+ */
584
+ function _exportStyle(appearance = {}, record = null, { border = false, box = true } = {}) {
585
+ const original = record?.style ?? {};
586
+ const before = record?.appearance ?? _appearanceFromStyle(original, { box });
587
+ const mapped = _styleFromAppearance(appearance, { border, box });
588
+ const style = {};
589
+ for (const [attribute, fields] of Object.entries(STYLE_FIELDS)) {
590
+ const unchanged = fields.every(f => before[f] === appearance[f]);
591
+ if (unchanged && original[attribute] !== undefined) style[attribute] = original[attribute];
592
+ else if (!unchanged && mapped[attribute] !== undefined) style[attribute] = mapped[attribute];
593
+ }
594
+ return style;
595
+ }
596
+
597
+ /** Archi's style attributes on a box or line, as written (colours are "#rrggbb"). */
598
+ const ARCHI_STYLE_ATTRIBUTES = Object.freeze([
599
+ 'fillColor', 'lineColor', 'borderColor', 'fontColor', 'font', 'lineWidth', 'textAlignment', 'textPosition', 'alpha', 'gradient',
600
+ ]);
601
+
602
+ /** The style attributes an element carries. @private */
603
+ function _parseStyle(el) {
604
+ const style = {};
605
+ for (const name of ARCHI_STYLE_ATTRIBUTES) {
606
+ const value = el.getAttribute(name);
607
+ if (value !== null && value !== undefined && value !== '') style[name] = value;
608
+ }
609
+ const lineAlpha = el.children?.find(c => c.tagName === 'feature' && c.getAttribute('name') === ARCHI_LINE_ALPHA_FEATURE);
610
+ if (lineAlpha) style[ARCHI_LINE_ALPHA_FEATURE] = lineAlpha.getAttribute('value');
611
+ return style;
612
+ }
613
+
614
+ /** Archi's text alignment (draw2d PositionConstants) and our names for it. */
615
+ const TEXT_ALIGN = Object.freeze({ 1: 'left', 2: 'center', 4: 'right' });
616
+ /** Archi's text position (top, centre, bottom of the box). */
617
+ const TEXT_POSITION = Object.freeze({ 0: 'top', 1: 'middle', 2: 'bottom' });
618
+
619
+ /**
620
+ * An instance's appearance (ElementInstance#appearance) from Archi's style:
621
+ * fill, line (Archi's lineColor, or borderColor on groups and notes), text
622
+ * colour, line width, font, and text alignment and position.
623
+ * @private
624
+ */
625
+ function _appearanceFromStyle(style = {}, { box = true } = {}) {
626
+ const a = {};
627
+ if (style.fillColor) a.fillColor = style.fillColor;
628
+ const line = style.lineColor ?? style.borderColor;
629
+ if (line) a.strokeColor = line;
630
+ if (style.fontColor) a.textColor = style.fontColor;
631
+ if (style.lineWidth) a.strokeWidth = Number(style.lineWidth);
632
+ if (box && style.alpha === '0') a.fillColor = 'none';
633
+ else if (box && style.alpha !== undefined && style.alpha !== '') a.fillOpacity = Number(style.alpha) / 255;
634
+ if (box && style[ARCHI_LINE_ALPHA_FEATURE] === '0') a.strokeColor = 'none';
635
+ else if (box && style[ARCHI_LINE_ALPHA_FEATURE] !== undefined && style[ARCHI_LINE_ALPHA_FEATURE] !== '') a.strokeOpacity = Number(style[ARCHI_LINE_ALPHA_FEATURE]) / 255;
636
+ if (style.gradient !== undefined && style.gradient !== '') a.gradient = Number(style.gradient);
637
+ Object.assign(a, _parseFontFeature(style.font));
638
+ // Archi centres a box's text unless the box says otherwise.
639
+ if (TEXT_ALIGN[style.textAlignment] || box) a.textAlign = TEXT_ALIGN[style.textAlignment] ?? 'center';
640
+ if (TEXT_POSITION[style.textPosition]) a.textPosition = TEXT_POSITION[style.textPosition];
641
+ return a;
642
+ }
643
+
644
+ /** Archi's style attributes back from an appearance (the inverse of _appearanceFromStyle). @private */
645
+ function _styleFromAppearance(a = {}, { border = false, box = true } = {}) {
646
+ const style = {};
647
+ if (a.fillColor === 'none') style.alpha = '0';
648
+ else if (a.fillColor) style.fillColor = a.fillColor;
649
+ if (a.fillOpacity !== undefined && a.fillColor !== 'none') style.alpha = String(Math.round(a.fillOpacity * 255));
650
+ if (box && a.strokeColor === 'none') style[ARCHI_LINE_ALPHA_FEATURE] = '0';
651
+ if (a.strokeOpacity !== undefined && a.strokeColor !== 'none') style[ARCHI_LINE_ALPHA_FEATURE] = String(Math.round(a.strokeOpacity * 255));
652
+ if (a.gradient !== undefined && a.gradient !== null) style.gradient = String(a.gradient);
653
+ if (a.strokeColor && a.strokeColor !== 'none') style[border ? 'borderColor' : 'lineColor'] = a.strokeColor;
654
+ if (a.textColor) style.fontColor = a.textColor;
655
+ if (a.strokeWidth !== undefined) style.lineWidth = String(a.strokeWidth);
656
+ if (a.fontFamily || a.fontSize || a.fontWeight || a.fontStyle) style.font = _fontString(a);
657
+ const align = Object.entries(TEXT_ALIGN).find(([, name]) => name === a.textAlign)?.[0];
658
+ if (align) style.textAlignment = align;
659
+ const position = Object.entries(TEXT_POSITION).find(([, name]) => name === a.textPosition)?.[0];
660
+ if (position) style.textPosition = position;
661
+ return style;
662
+ }
663
+
664
+ function _parseFeatures(el) {
665
+ const features = {};
666
+ for (const f of el.children.filter(c => c.tagName === 'feature')) {
667
+ if (f.getAttribute('name') === ARCHI_LINE_ALPHA_FEATURE) continue; // style (see _parseStyle)
668
+ features[f.getAttribute('name')] = f.getAttribute('value');
669
+ }
670
+ return features;
671
+ }
672
+
673
+ /** An element's own properties as [{key, value}], in file order (Archi allows a key more than once). @private */
674
+ function _propertyPairs(el) {
675
+ return el.children.filter(c => c.tagName === 'property').map(p => ({ key: p.getAttribute('key') ?? '', value: p.getAttribute('value') ?? '' }));
676
+ }
677
+
678
+ function _parseProperties(el) {
679
+ const pairs = _propertyPairs(el);
680
+ for (const { key, value } of pairs) {
681
+ if (!(key in pairs)) pairs[key] = value;
682
+ }
683
+ return pairs;
684
+ }
685
+
686
+ /** Archi native format stores flat string properties. RenderEngine stores a
687
+ * typed array `{name, type, value}`. Import converts everything to 'string'
688
+ * by default; callers can retype in the UI. */
689
+ function _propertiesToTypedArray(props) {
690
+ if (!props) return undefined;
691
+ if (Array.isArray(props)) {
692
+ if (!props.length) return undefined;
693
+ return props.map(p => ({
694
+ name: p.name ?? p.key ?? '',
695
+ type: p.type ?? 'string',
696
+ value: p.value == null ? '' : String(p.value),
697
+ }));
698
+ }
699
+ if (Object.keys(props).length === 0) return undefined;
700
+ return Object.entries(props).map(([name, value]) => ({ name, type: 'string', value }));
701
+ }
702
+
703
+ /** Export: stringify typed values back to Archi's flat key/value strings. */
704
+ function _typedArrayToProperties(arr) {
705
+ const out = {};
706
+ for (const { name, value } of arr || []) {
707
+ if (!name) continue;
708
+ out[name] = value == null ? '' : String(value);
709
+ }
710
+ return out;
711
+ }
712
+
713
+ /** The text of an element's own child `tagName` (not a nested element's: a canvas block holds stickies with their own content). @private */
714
+ function _childText(el, tagName) {
715
+ const child = el.children.find(c => c.tagName === tagName);
716
+ return child ? child.textContent : null;
717
+ }
718
+
719
+ /** Convert a parsed Archi model into an ArchimateModel (with views).
720
+ * @param {Object} archiModel - parsed Archi XML model
721
+ * @returns {ArchimateModel} with one view per original view (elementIds track which elements appear in each view)
722
+ * Now applies visual bounds/waypoints per view. */
723
+ export function toArchimateModel(archiModel) {
724
+ const elements = [];
725
+ const relationships = [];
726
+ const folders = [];
727
+ const elementFolders = [];
728
+ const views = [];
729
+ const elementById = new Map();
730
+ const relationshipById = new Map();
731
+
732
+ // Map all elements first
733
+ for (const el of archiModel.elements || []) {
734
+ const mapped = _mapElement(el);
735
+ elementById.set(el.id, mapped);
736
+ elements.push(mapped);
737
+ }
738
+
739
+ // Map all relationships
740
+ for (const rel of archiModel.relationships || []) {
741
+ const mapped = _mapRelationship(rel);
742
+ relationshipById.set(rel.id, mapped);
743
+ relationships.push(mapped);
744
+ }
745
+
746
+ // Each view: one instance per box Archi draws, and one per line.
747
+ for (const archiView of archiModel.views || []) {
748
+ const v = _viewInstances(archiView, { elementById, relationshipById });
749
+ // Our template flag is a feature on the view; the rest of its extras pass through.
750
+ const isTemplateFeature = (e) => e.tag === 'feature' && e.attributes?.name === ARCHI_TEMPLATE_FEATURE;
751
+ const template = !!archiView.archi?.elements?.some(e => isTemplateFeature(e) && e.attributes?.value === 'true');
752
+ if (archiView.archi?.elements) archiView.archi.elements = archiView.archi.elements.filter(e => !isTemplateFeature(e));
753
+ views.push({
754
+ id: archiView.id,
755
+ name: archiView.name,
756
+ ...(archiView.documentation && { documentation: archiView.documentation }),
757
+ elementIds: v.elementIds,
758
+ elementInstances: v.elementInstances,
759
+ relationshipInstances: v.relationshipInstances,
760
+ viewConnections: v.viewConnections,
761
+ arrangement: v.arrangement,
762
+ archiViewType: archiView.type,
763
+ ...(template && { template }),
764
+ ...(archiView.properties?.length && { properties: archiView.properties }),
765
+ ...(archiView.archiFolder && { archiFolder: archiView.archiFolder }),
766
+ ...(archiView.archi && { archi: archiView.archi }),
767
+ ...(v.folders.length && { folders: v.folders, folderLayout: v.folderLayout }),
768
+ });
769
+ folders.push(...v.folders);
770
+ elementFolders.push(...v.elementFolders);
771
+ }
772
+
773
+ return new ArchimateModel({
774
+ id: archiModel.id ?? undefined,
775
+ name: archiModel.name ?? '', elements, relationships, folders, elementFolders, views,
776
+ documentation: archiModel.documentation || undefined, properties: archiModel.properties ?? [],
777
+ emptyViewFolders: archiModel.emptyViewFolders ?? [],
778
+ // The specializations written for element types Archi lacks are not the model's own.
779
+ archi: archiModel.archi ? {
780
+ ...archiModel.archi,
781
+ elements: (archiModel.archi.elements ?? []).filter(e => !(e.tag === 'profile' && String(e.attributes?.id ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX))),
782
+ } : null,
783
+ });
784
+ }
785
+
786
+ /** Where a visual sits on the view. Archi stores a nested visual's bounds
787
+ * relative to its parent (a group, or an element drawn inside another), so
788
+ * they are offset by the parent's absolute position. Null without bounds. @private */
789
+ function _absoluteBounds(visual, origin) {
790
+ if (!visual.bounds) return null;
791
+ const { x, y, width, height } = visual.bounds;
792
+ const size = ARCHI_DEFAULT_SIZE[visual.type] ?? ARCHI_ELEMENT_SIZE;
793
+ return {
794
+ x: origin.x + x, y: origin.y + y,
795
+ width: width > 0 ? width : size.width,
796
+ height: height > 0 ? height : size.height,
797
+ };
798
+ }
799
+
800
+ const VIEW_ORIGIN = Object.freeze({ x: 0, y: 0 });
801
+
802
+ /**
803
+ * Boxes on canvas and sketch views: no model element behind them, drawn as a
804
+ * shape with text (and, for a canvas sticky, maybe a picture). Archi type ->
805
+ * the renderer's shape.
806
+ */
807
+ const VIEW_BOX_SHAPES = Object.freeze({
808
+ 'canvas-model-block': 'rect',
809
+ 'canvas-model-sticky': 'rect',
810
+ 'sketch-model-sticky': 'rect',
811
+ 'sketch-model-actor': 'stick-figure',
812
+ });
813
+
814
+ /** What a view-only box draws (a ViewObjectInstance's content), from its Archi visual. @private */
815
+ function _viewObjectContent(visual) {
816
+ return _viewObjectDrawing(visual);
817
+ }
818
+
819
+ /** @private */
820
+ function _viewObjectDrawing(visual) {
821
+ const text = visual.text || visual.name || '';
822
+ if (visual.type === 'diagram-model-note') {
823
+ // A note's text is its own content; an empty one shows its name, if any (Archi's legend note).
824
+ return { id: visual.id, name: visual.text || visual.name || '', label: visual.text || '', shape: 'note' };
825
+ }
826
+ if (visual.type === 'diagram-model-image' || visual.type === 'canvas-model-image') {
827
+ return {
828
+ id: visual.id, name: visual.name || 'Image', shape: 'image', iconUrl: visual.imagePath,
829
+ ...(visual.properties?.href && { href: visual.properties.href }),
830
+ ...(visual.type === 'canvas-model-image' && { archiVisualType: visual.type }),
831
+ };
832
+ }
833
+ const shape = VIEW_BOX_SHAPES[visual.type];
834
+ if (!shape) return null;
835
+ return {
836
+ id: visual.id, name: text, label: text, shape, // an empty box shows no text
837
+ archiVisualType: visual.type,
838
+ ...(visual.imagePath && { iconUrl: visual.imagePath }),
839
+ ...(visual.notes && { documentation: visual.notes }),
840
+ };
841
+ }
842
+
843
+ /** Where a box is drawn: its centre, as the renderer places nodes (Archi keeps the top-left corner). @private */
844
+ const _centreOf = (bounds) => ({ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 });
845
+
846
+ /**
847
+ * One Archi view as instances: an ElementInstance per box that shows a model
848
+ * element (so the same element twice is two instances), a ViewObjectInstance
849
+ * per note or image, each centred on its box with that box's size and style;
850
+ * a RelationshipInstance per line, joining the two instances it joins, with its
851
+ * bend points (none is a straight line); groups as folders with their bounds.
852
+ * @private
853
+ */
854
+ function _viewInstances(archiView, { elementById, relationshipById }) {
855
+ const boxes = _visualBoxes(archiView);
856
+ const elementInstances = new Map();
857
+ const instanceOfVisual = new Map(); // Archi visual id -> instance id
858
+ const elementIds = new Set();
859
+ const folders = [];
860
+ const folderLayout = {};
861
+ const elementFolders = [];
862
+ const lines = [];
863
+
864
+ let order = 0; // document order is Archi's drawing order
865
+ const locked = [];
866
+ const groupMembers = new Map(); // group id -> instance ids
867
+ const groupKinds = new Map(); // group id -> its kind, when not a plain group
868
+ const place = (inst, bounds, style, type, visual) => {
869
+ inst.size = { width: bounds.width, height: bounds.height };
870
+ inst.zIndex = order++;
871
+ inst.appearance = _appearanceFromStyle(style);
872
+ // Archi centres a canvas sticky's text vertically unless it says otherwise.
873
+ if (type === 'canvas-model-sticky' && !TEXT_POSITION[style?.textPosition]) inst.appearance.textPosition = 'middle';
874
+ // Archi draws a canvas box without a border colour with no border.
875
+ if (type?.startsWith('canvas-model-') && !style?.borderColor && !style?.lineColor) inst.appearance.strokeColor = 'none';
876
+ inst.archi = _archiRecord(visual, inst.appearance);
877
+ // Archi's lock, and our logical group (kept as a feature), go to the view's arrangement.
878
+ const features = inst.archi?.features ?? {};
879
+ if (visual.archi?.locked || features[ARCHI_LOCK_FEATURE] === 'true') locked.push(inst.id);
880
+ const groupId = features[ARCHI_GROUP_FEATURE];
881
+ if (groupId) (groupMembers.get(groupId) ?? groupMembers.set(groupId, []).get(groupId)).push(inst.id);
882
+ if (groupId && features[ARCHI_GROUP_KIND_FEATURE]) groupKinds.set(groupId, features[ARCHI_GROUP_KIND_FEATURE]);
883
+ if (inst.archi?.features) {
884
+ delete inst.archi.features[ARCHI_GROUP_FEATURE];
885
+ delete inst.archi.features[ARCHI_GROUP_KIND_FEATURE];
886
+ delete inst.archi.features[ARCHI_LOCK_FEATURE];
887
+ if (!Object.keys(inst.archi.features).length) delete inst.archi.features;
888
+ }
889
+ elementInstances.set(inst.id, inst);
890
+ return inst;
891
+ };
892
+ const walk = (visual, parentFolderId, origin, parentBoxId = null) => {
893
+ const bounds = _absoluteBounds(visual, origin);
894
+ const instanceCount = elementInstances.size;
895
+ if (bounds) {
896
+ const { x, y } = _centreOf(bounds);
897
+ if (visual.archimateElement && elementById.has(visual.archimateElement)) {
898
+ const inst = place(new ElementInstance(visual.archimateElement, x, y), bounds, visual.style, visual.type, visual);
899
+ instanceOfVisual.set(visual.id, inst.id);
900
+ elementIds.add(visual.archimateElement);
901
+ if (parentFolderId) elementFolders.push([visual.archimateElement, parentFolderId]);
902
+ } else {
903
+ // Notes, images, and canvas and sketch boxes: drawn on this view only.
904
+ const content = _viewObjectContent(visual);
905
+ if (content) {
906
+ const inst = place(new ViewObjectInstance({ ...content, width: bounds.width, height: bounds.height }, x, y), bounds, visual.style, visual.type, visual);
907
+ instanceOfVisual.set(visual.id, inst.id);
908
+ }
909
+ }
910
+ }
911
+ if (visual.type === 'diagram-model-group') {
912
+ folders.push({
913
+ id: visual.id,
914
+ name: visual.name || 'Group',
915
+ documentation: visual.documentation || undefined,
916
+ parentId: parentFolderId || undefined,
917
+ children: [],
918
+ });
919
+ if (parentFolderId) folders.find(f => f.id === parentFolderId)?.children.push(visual.id);
920
+ if (bounds) {
921
+ // Its paint as an instance keeps it (the canvas editor draws and edits it), and the record to write back.
922
+ const appearance = _appearanceFromStyle(visual.style);
923
+ folderLayout[visual.id] = {
924
+ x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, collapsed: false, zIndex: order++,
925
+ appearance, archi: _archiRecord(visual, appearance),
926
+ };
927
+ }
928
+ instanceOfVisual.set(visual.id, visual.id); // a plain line may end at a group
929
+ }
930
+ // A box drawn inside another box, or a note or picture inside a group (an element's group
931
+ // is its folder): kept so the canvas moves it with its parent and export nests it there again.
932
+ const isBox = elementInstances.size > instanceCount;
933
+ const parent = parentBoxId ?? (visual.archimateElement ? null : parentFolderId);
934
+ if (isBox && parent) {
935
+ const inst = [...elementInstances.values()].at(-1);
936
+ inst.archi = { ...(inst.archi ?? {}), parent };
937
+ }
938
+ lines.push(...(visual.sourceConnections || []));
939
+ const folderId = visual.type === 'diagram-model-group' ? visual.id : parentFolderId;
940
+ const boxId = isBox ? visual.id : (visual.type === 'diagram-model-group' ? null : parentBoxId);
941
+ for (const child of visual.children || []) walk(child, folderId, bounds ?? origin, boxId);
942
+ };
943
+ for (const child of archiView.children || []) walk(child, null, VIEW_ORIGIN);
944
+
945
+ const relationshipInstances = new Map();
946
+ const viewConnections = [];
947
+ for (const sc of lines) {
948
+ const source = instanceOfVisual.get(sc.source);
949
+ const target = instanceOfVisual.get(sc.target);
950
+ if (!source || !target) continue;
951
+ if (!sc.relationship) {
952
+ // A plain line: Archi's connection between any two boxes, or a canvas connection.
953
+ const connection = new ViewConnectionInstance({
954
+ id: sc.id, sourceInstanceId: source, targetInstanceId: target,
955
+ waypoints: sc.bendpoints?.length ? _bendpointLocations(sc, boxes) : [],
956
+ type: sc.connectionType, label: sc.name, appearance: _appearanceFromStyle(sc.style, { box: false }), archiType: sc.archiType,
957
+ });
958
+ connection.archi = _archiRecord(sc, connection.appearance);
959
+ viewConnections.push(connection);
960
+ continue;
961
+ }
962
+ if (!relationshipById.has(sc.relationship)) continue;
963
+ const inst = new RelationshipInstance(sc.relationship, sc.bendpoints?.length ? _bendpointLocations(sc, boxes) : [], source, target);
964
+ inst.appearance = _appearanceFromStyle(sc.style, { box: false });
965
+ inst.archi = _archiRecord(sc, inst.appearance);
966
+ relationshipInstances.set(inst.id, inst);
967
+ }
968
+ const arrangement = {
969
+ // A plain group needs two members; a kind of group (a background) may have one.
970
+ groups: [...groupMembers].filter(([id, ids]) => ids.length > 1 || groupKinds.has(id))
971
+ .map(([id, memberIds]) => ({ id, memberIds, ...(groupKinds.has(id) && { kind: groupKinds.get(id) }) })),
972
+ locked,
973
+ };
974
+ return { elementIds: [...elementIds], elementInstances, relationshipInstances, viewConnections, folders, folderLayout, elementFolders, arrangement };
975
+ }
976
+
977
+ /** Convert a parsed Archi model into renderengine inputs.
978
+ * Returns `{ elements, relationships, options }` ready for `archimateDiagram()`.
979
+ * If `viewId` is omitted, the first view is used.
980
+ * Kept as original for backward compatibility: applies visual bounds/features and handles all view visuals. */
981
+ export function toRenderEngineModel(archiModel, viewId) {
982
+ const view = viewId
983
+ ? archiModel.views.find(v => v.id === viewId)
984
+ : archiModel.views[0];
985
+
986
+ const elements = [];
987
+ const relationships = [];
988
+ const annotations = [];
989
+ const elementById = new Map();
990
+ const relationshipById = new Map();
991
+
992
+ for (const el of archiModel.elements || []) {
993
+ const mapped = _mapElement(el);
994
+ elementById.set(el.id, mapped);
995
+ elements.push(mapped);
996
+ }
997
+
998
+ for (const rel of archiModel.relationships || []) {
999
+ const mapped = _mapRelationship(rel);
1000
+ relationshipById.set(rel.id, mapped);
1001
+ relationships.push(mapped);
1002
+ }
1003
+
1004
+ const folders = [];
1005
+ const folderLayout = {};
1006
+ const elementFolders = []; // Array of [elementId, folderId] pairs
1007
+
1008
+ if (view) {
1009
+ const boxes = _visualBoxes(view);
1010
+ for (const visual of view.children || []) {
1011
+ _mapVisual(visual, { elementById, relationshipById, elements, relationships, annotations, folders, folderLayout, elementFolders, boxes });
1012
+ }
1013
+ }
1014
+
1015
+ const options = {
1016
+ annotations,
1017
+ archimateVersion: '4',
1018
+ };
1019
+
1020
+ return { elements, relationships, folders, folderLayout, elementFolders, options };
1021
+ }
1022
+
1023
+ function _mapElement(el) {
1024
+ const archiType = _toKebabType(el.type);
1025
+ const layer = ArchimateNotation.elementLayer({ type: archiType });
1026
+ // A type Archi has no class for was written as the nearest one, with its own kept as a feature.
1027
+ const ownType = el.archi?.elements?.find(e => e.tag === 'feature' && e.attributes?.name === ARCHI_TYPE_FEATURE)?.attributes?.value;
1028
+ const mapped = {
1029
+ id: el.id,
1030
+ name: el.name,
1031
+ type: ownType || archiType,
1032
+ layer: layer !== 'other' ? layer : undefined,
1033
+ documentation: el.documentation || undefined,
1034
+ };
1035
+ const props = _propertiesToTypedArray(el.properties);
1036
+ if (props) mapped.properties = props;
1037
+ if (el.archiFolder) mapped.archiFolder = el.archiFolder;
1038
+ if (el.archi) mapped.archi = ownType ? _withoutTypeMarks(el.archi) : el.archi;
1039
+ return mapped;
1040
+ }
1041
+
1042
+ /** An element's archi record without the feature and specialization written for its own type. @private */
1043
+ function _withoutTypeMarks(archi) {
1044
+ const attributes = { ...(archi.attributes ?? {}) };
1045
+ if (String(attributes.profiles ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX)) delete attributes.profiles;
1046
+ return {
1047
+ ...archi,
1048
+ attributes,
1049
+ elements: (archi.elements ?? []).filter(e => !(e.tag === 'feature' && e.attributes?.name === ARCHI_TYPE_FEATURE)),
1050
+ };
1051
+ }
1052
+
1053
+ function _mapRelationship(rel) {
1054
+ const base = _toKebabType(rel.type).replace(/-relationship$/, '');
1055
+ const kind = base === 'association' && rel.directed ? 'association-directed' : base;
1056
+ const mapped = {
1057
+ id: rel.id,
1058
+ source: rel.source,
1059
+ target: rel.target,
1060
+ kind,
1061
+ label: rel.name || undefined,
1062
+ documentation: rel.documentation || undefined,
1063
+ };
1064
+ const props = _propertiesToTypedArray(rel.properties);
1065
+ if (props) mapped.properties = props;
1066
+ if (rel.archiFolder) mapped.archiFolder = rel.archiFolder;
1067
+ if (rel.archi) mapped.archi = rel.archi;
1068
+ return mapped;
1069
+ }
1070
+
1071
+ function _mapVisual(visual, ctx, parentBounds = null, parentFolderId = null, origin = VIEW_ORIGIN) {
1072
+ const own = _absoluteBounds(visual, origin);
1073
+ const bounds = own || parentBounds || { x: 0, y: 0, width: 120, height: 55 };
1074
+ // Children are placed relative to this visual if it has bounds, else as it is placed.
1075
+ const childOrigin = own ?? origin;
1076
+ // Placed by its centre, as the renderer draws nodes; styled under the renderer's names.
1077
+ const { x, y } = _centreOf(bounds);
1078
+ const { fillColor, ...appearance } = _appearanceFromStyle(visual.style);
1079
+ const visualProps = {
1080
+ x, y,
1081
+ width: bounds.width,
1082
+ height: bounds.height,
1083
+ ...appearance,
1084
+ ...(fillColor && { color: fillColor }),
1085
+ };
1086
+
1087
+ if (visual.archimateElement) {
1088
+ const element = ctx.elementById.get(visual.archimateElement);
1089
+ if (element) {
1090
+ Object.assign(element, visualProps);
1091
+ // A DiagramObject may carry a per-view text override.
1092
+ if (visual.text) element.label = visual.text;
1093
+ }
1094
+ // Track which folder contains this element (if any)
1095
+ if (parentFolderId && ctx.elementFolders) {
1096
+ ctx.elementFolders.push([visual.archimateElement, parentFolderId]);
1097
+ }
1098
+ // On to its connections and to the elements drawn inside it (below).
1099
+ }
1100
+
1101
+ const base = { id: visual.id, ...visualProps };
1102
+
1103
+ if (visual.type === 'diagram-model-note') {
1104
+ ctx.annotations.push({
1105
+ ...base,
1106
+ label: visual.text || 'Note',
1107
+ shape: 'note',
1108
+ });
1109
+ return;
1110
+ }
1111
+
1112
+ if (visual.type === 'diagram-model-group') {
1113
+ const folder = {
1114
+ id: visual.id,
1115
+ name: visual.name || 'Group',
1116
+ documentation: visual.documentation || undefined,
1117
+ parentId: parentFolderId || undefined,
1118
+ children: [],
1119
+ };
1120
+ ctx.folders.push(folder);
1121
+ ctx.folderLayout[visual.id] = {
1122
+ x: bounds.x,
1123
+ y: bounds.y,
1124
+ width: visualProps.width,
1125
+ height: visualProps.height,
1126
+ collapsed: false,
1127
+ };
1128
+ // Recurse into children with this folder as parent
1129
+ for (const child of visual.children || []) {
1130
+ _mapVisual(child, ctx, bounds, visual.id, childOrigin);
1131
+ }
1132
+ return;
1133
+ }
1134
+
1135
+ if (visual.type === 'diagram-model-image') {
1136
+ ctx.annotations.push({
1137
+ ...base,
1138
+ label: visual.name || '',
1139
+ iconUrl: visual.imagePath,
1140
+ shape: 'image',
1141
+ href: visual.properties?.href,
1142
+ });
1143
+ return;
1144
+ }
1145
+
1146
+ if (visual.type === 'diagram-model-reference') {
1147
+ ctx.annotations.push({
1148
+ ...base,
1149
+ label: visual.name || 'Reference',
1150
+ reference: visual.referencedModel,
1151
+ referencedModel: visual.referencedModel,
1152
+ isViewReference: true,
1153
+ shape: 'note',
1154
+ });
1155
+ return;
1156
+ }
1157
+
1158
+ if ([
1159
+ 'sketch-model-sticky', 'sketch-model-actor',
1160
+ 'canvas-model-block', 'canvas-model-sticky', 'canvas-model-image',
1161
+ ].includes(visual.type)) {
1162
+ ctx.annotations.push({
1163
+ ...base,
1164
+ label: visual.text || visual.name || visual.type,
1165
+ archiVisualType: visual.type,
1166
+ shape: visual.type.endsWith('-image') ? 'rect' : 'note',
1167
+ });
1168
+ return;
1169
+ }
1170
+
1171
+ // An element's connections, then the visuals nested in it.
1172
+ for (const sc of visual.sourceConnections || []) {
1173
+ if (sc.relationship && sc.bendpoints?.length) {
1174
+ // Apply bendpoints from the visual connection to the relationship model
1175
+ const rel = ctx.relationshipById?.get(sc.relationship);
1176
+ if (rel) {
1177
+ rel.waypoints = _bendpointLocations(sc, ctx.boxes);
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ for (const child of visual.children || []) {
1183
+ _mapVisual(child, ctx, bounds, parentFolderId, childOrigin);
1184
+ }
1185
+ }
1186
+
1187
+ function _toKebabType(name) {
1188
+ if (!name) return '';
1189
+ return name
1190
+ .replace(/^archimate:/, '')
1191
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
1192
+ .toLowerCase();
1193
+ }
1194
+
1195
+ /**
1196
+ * Archi's font: SWT FontData, "version|name|height in points|style|platform|...",
1197
+ * style bit 1 bold, bit 2 italic. The size is kept in points ("9pt"), which SVG
1198
+ * draws as Archi does.
1199
+ * @private
1200
+ */
1201
+ function _parseFontFeature(fontString) {
1202
+ if (!fontString) return {};
1203
+ const [, family, height, style] = fontString.split('|');
1204
+ const result = {};
1205
+ if (family) result.fontFamily = family;
1206
+ if (height && Number(height) > 0) result.fontSize = `${Number(height)}pt`;
1207
+ const bits = Number(style) || 0;
1208
+ if (bits & 1) result.fontWeight = 'bold';
1209
+ if (bits & 2) result.fontStyle = 'italic';
1210
+ return result;
1211
+ }
1212
+
1213
+ /** A font as SWT FontData's leading fields, which Archi reads (inverse of _parseFontFeature). @private */
1214
+ function _fontString({ fontFamily = 'Segoe UI', fontSize = '9pt', fontWeight, fontStyle } = {}) {
1215
+ const points = String(fontSize).endsWith('px') ? parseFloat(fontSize) * 0.75 : parseFloat(fontSize);
1216
+ const bits = (fontWeight === 'bold' ? 1 : 0) | (fontStyle === 'italic' ? 2 : 0);
1217
+ return `1|${fontFamily}|${points}|${bits}`;
1218
+ }
1219
+
1220
+ /** Convert an ArchimateView to Archi's visual-tree shape, ready for toArchiXML.
1221
+ * Returns array of visual child objects with bounds, sourceConnections (including bendpoints), and nested children.
1222
+ * This is the export-side bridge: ArchimateView → Archi visual tree → XML. */
1223
+ export function fromArchimateView(view) {
1224
+ const visuals = [];
1225
+ const relationshipsBySource = new Map();
1226
+ const folderById = new Map();
1227
+
1228
+ // Index relationships by source visual ID for quick lookup
1229
+ for (const rel of view.relationships || []) {
1230
+ if (rel.viewConnection) continue; // plain lines are written from view.viewConnections (below)
1231
+ if (!relationshipsBySource.has(rel.source)) {
1232
+ relationshipsBySource.set(rel.source, []);
1233
+ }
1234
+ relationshipsBySource.get(rel.source).push(rel);
1235
+ }
1236
+
1237
+ // Groups: an ArchimateView's placedFolders (each after its parent, parentId the
1238
+ // nearest group on the view); a plain view object's `folders` are all top level.
1239
+ const groups = view.placedFolders ?? (view.folders || []).map(f => ({ ...f, parentId: undefined }));
1240
+ const absolute = new Map(); // visual -> its bounds on the view
1241
+ const stacking = new Map(); // visual -> its zIndex (Infinity: on top, in the order placed)
1242
+ /** Put a visual on the view, or inside the group `groupId` with bounds relative to it. */
1243
+ const placedById = new Map(); // visual id -> visual, for nesting a box in its box
1244
+ const place = (visual, groupId, zIndex = Infinity, parentBoxId = null) => {
1245
+ absolute.set(visual, visual.bounds);
1246
+ stacking.set(visual, zIndex);
1247
+ placedById.set(visual.id, visual);
1248
+ const group = (parentBoxId && placedById.get(parentBoxId)) || (groupId && folderById.get(groupId));
1249
+ if (!group) { visuals.push(visual); return; }
1250
+ const origin = absolute.get(group);
1251
+ visual.bounds = { ...visual.bounds, x: visual.bounds.x - origin.x, y: visual.bounds.y - origin.y };
1252
+ (group.children ??= []).push(visual); // a box gets its first child here
1253
+ };
1254
+ for (const folder of groups) {
1255
+ const { x = 0, y = 0, width = 200, height = 150 } = view.folderLayout?.[folder.id] ?? folder;
1256
+ const folderVisual = {
1257
+ id: folder.id,
1258
+ type: 'diagram-model-group',
1259
+ name: folder.name,
1260
+ bounds: { x, y, width, height },
1261
+ children: [],
1262
+ sourceConnections: [],
1263
+ };
1264
+ // Its paint as the view has it now: the original attributes where unchanged (as for a box).
1265
+ _applyArchiRecord(folderVisual, folder.archi, folder.appearance ? _exportStyle(folder.appearance, folder.archi, { border: true }) : (folder.archi?.style ?? {}));
1266
+ const folderZ = view.folderLayout?.[folder.id]?.zIndex ?? folder.zIndex;
1267
+ if (folder.documentation) {
1268
+ folderVisual.documentation = folder.documentation;
1269
+ }
1270
+ folderById.set(folder.id, folderVisual);
1271
+ place(folderVisual, folder.parentId, folderZ ?? Infinity);
1272
+ }
1273
+
1274
+ /** The visual id for a drawn item: Archi's own when it was imported, else one made from the instance id. */
1275
+ const visualIdOf = (instanceId) => view.getElementInstance?.(instanceId)?.visualId ?? `diag-${instanceId}`;
1276
+
1277
+ // One visual per drawn item. A view's items carry their instance id as `id`
1278
+ // and the model element's as `elementId` (a plain {id} item is both).
1279
+ // Each item is placed by its centre, as the renderer draws it; Archi keeps the
1280
+ // top-left corner, in whole pixels. A box nested in another is placed after it
1281
+ // (whichever was added first), so it goes inside it.
1282
+ const byVisualId = new Map((view.elements || []).map(el => [visualIdOf(el.id), el]));
1283
+ const depthOf = (el, seen = new Set()) => {
1284
+ const parent = byVisualId.get(view.getElementInstance?.(el.id)?.nestedIn);
1285
+ return parent && !seen.has(parent) ? 1 + depthOf(parent, seen.add(el)) : 0;
1286
+ };
1287
+ const inPlacingOrder = (view.elements || []).map((el, i) => [el, depthOf(el), i]).sort((a, b) => a[1] - b[1] || a[2] - b[2]).map(([el]) => el);
1288
+ for (const el of inPlacingOrder) {
1289
+ const width = el.width || ARCHI_ELEMENT_SIZE.width;
1290
+ const height = el.height || ARCHI_ELEMENT_SIZE.height;
1291
+ const bounds = { x: Math.round((el.x || 0) - width / 2), y: Math.round((el.y || 0) - height / 2), width, height };
1292
+ const visual = el.viewObject
1293
+ ? { ..._viewObjectVisual(el, bounds), id: visualIdOf(el.id) }
1294
+ : {
1295
+ id: visualIdOf(el.id),
1296
+ type: 'diagram-model-object',
1297
+ archimateElement: el.elementId ?? el.id,
1298
+ bounds,
1299
+ sourceConnections: [],
1300
+ };
1301
+ // This view's style for the box (an ArchimateView keeps it on the instance), and what the file said besides.
1302
+ const instance = view.getElementInstance?.(el.id);
1303
+ _applyArchiRecord(visual, instance?.archi, _exportStyle(instance?.appearance, instance?.archi, { border: !!el.viewObject }));
1304
+ // Fixed in place (on its own or in a locked group): Archi's lock where Archi has one (canvas
1305
+ // boxes), else a feature Archi keeps. Its logical group: a feature.
1306
+ if (view.arrangement?.isLocked(el.id)) {
1307
+ if (ARCHI_LOCKABLE.has(visual.type)) visual.locked = true;
1308
+ else visual.features = { ...(visual.features ?? {}), [ARCHI_LOCK_FEATURE]: 'true' };
1309
+ }
1310
+ const itemGroup = view.arrangement?.groupOf(el.id);
1311
+ if (itemGroup) {
1312
+ const kind = itemGroup.constructor.kind;
1313
+ visual.features = { ...(visual.features ?? {}), [ARCHI_GROUP_FEATURE]: itemGroup.id, ...(kind !== 'group' && { [ARCHI_GROUP_KIND_FEATURE]: kind }) };
1314
+ }
1315
+
1316
+ // Add connections from this element to others
1317
+ const rels = relationshipsBySource.get(el.id) || [];
1318
+ for (const rel of rels) {
1319
+ const sc = {
1320
+ id: view.relationshipInstances?.get?.(rel.id)?.archi?.id ?? `conn-${rel.id}`,
1321
+ source: visual.id,
1322
+ target: visualIdOf(rel.target),
1323
+ relationship: rel.relationshipId ?? rel.id,
1324
+ type: 'archimate:Connection',
1325
+ };
1326
+ const line = view.relationshipInstances?.get?.(rel.id);
1327
+ _applyArchiRecord(sc, line?.archi, _exportStyle(line?.appearance, line?.archi, { box: false }));
1328
+ if (rel.waypoints?.length) {
1329
+ // Points on the view for now; made relative once every box is known (below).
1330
+ sc.bendpoints = rel.waypoints.map(wp => ({ x: wp.x, y: wp.y }));
1331
+ }
1332
+ visual.sourceConnections.push(sc);
1333
+ }
1334
+
1335
+ if (instance?.archi?.imagePath) visual.archiImagePath = instance.archi.imagePath;
1336
+ place(visual, el.viewObject ? undefined : view.placedFolderOf?.(el.elementId ?? el.id), el.zIndex ?? Infinity, instance?.archi?.parent);
1337
+ }
1338
+
1339
+ // Plain lines (no relationship): on their source box, with their own class and type bits.
1340
+ const visualOf = new Map();
1341
+ const index = (v) => { visualOf.set(v.id, v); (v.children || []).forEach(index); };
1342
+ visuals.forEach(index);
1343
+ for (const connection of view.viewConnections?.values?.() ?? []) {
1344
+ // An end is a box (exported as "diag-<instance id>") or a group (under its own id).
1345
+ const visualFor = (end) => visualOf.get(visualIdOf(end)) ?? visualOf.get(end);
1346
+ const source = visualFor(connection.sourceInstanceId);
1347
+ const target = visualFor(connection.targetInstanceId);
1348
+ if (!source || !target) continue;
1349
+ const sc = {
1350
+ id: connection.id,
1351
+ source: source.id,
1352
+ target: target.id,
1353
+ archiType: connection.archiType,
1354
+ connectionType: connection.type,
1355
+ name: connection.label || '',
1356
+ ...(connection.waypoints.length && { bendpoints: connection.waypoints.map(wp => ({ x: wp.x, y: wp.y })) }),
1357
+ };
1358
+ _applyArchiRecord(sc, connection.archi, _exportStyle(connection.appearance, connection.archi, { box: false }));
1359
+ source.sourceConnections.push(sc);
1360
+ }
1361
+
1362
+ // Each level in stacking order (Archi draws in document order); ties keep their order.
1363
+ const byStacking = (list) => {
1364
+ list.sort((a, b) => stacking.get(a) - stacking.get(b));
1365
+ list.forEach(v => byStacking(v.children ?? []));
1366
+ };
1367
+ byStacking(visuals);
1368
+
1369
+ _relativizeLocatedBendpoints({ children: visuals });
1370
+ return visuals;
1371
+ }
1372
+
1373
+ /**
1374
+ * A model and its views as the writers take them (toArchiXML, toArchiFile,
1375
+ * toExchangeXML): the model's data, and each view as an Archi view tree.
1376
+ * @param {ArchimateModel} model
1377
+ * @returns {[Object, Array<Object>]} `[modelData, views]`, to spread into a writer
1378
+ */
1379
+ export function toArchiExport(model) {
1380
+ const views = model.views().map(view => ({
1381
+ id: view.id,
1382
+ name: view.name,
1383
+ type: view.archiViewType,
1384
+ template: view.template,
1385
+ documentation: view.documentation,
1386
+ properties: view.properties,
1387
+ archiFolder: view.archiFolder,
1388
+ archi: view.archi,
1389
+ children: fromArchimateView(view),
1390
+ }));
1391
+ return [{
1392
+ id: model.id || 'model-1',
1393
+ name: model.name || 'ArchiMate Model',
1394
+ version: '5.0.0',
1395
+ documentation: model.documentation,
1396
+ properties: model.properties,
1397
+ archi: model.archi,
1398
+ emptyViewFolders: model.emptyViewFolders ?? [],
1399
+ elements: model.elements(),
1400
+ relationships: model.relationships(),
1401
+ }, views];
1402
+ }
1403
+
1404
+ /**
1405
+ * Put what the file said about a box or line back on its export visual: the
1406
+ * style to write, its name and features (when it has none of its own), and the
1407
+ * attributes and child elements the model does not use.
1408
+ * @private
1409
+ */
1410
+ function _applyArchiRecord(target, record, style) {
1411
+ target.style = style;
1412
+ if (!record) return;
1413
+ if (record.name && !target.name) target.name = record.name;
1414
+ if (record.features) target.features = { ...record.features, ...(target.features ?? {}) };
1415
+ if (Object.keys(record.attributes ?? {}).length) target.extraAttributes = { ...record.attributes };
1416
+ if (record.elements?.length) target.extraElements = record.elements.map(e => ({ ...e }));
1417
+ }
1418
+
1419
+ /** <property> lines for [{key, value}] pairs (a model's or a view's). @private */
1420
+ function _propertyLines(pairs, indent) {
1421
+ return (Array.isArray(pairs) ? pairs : []).map(({ key, value }) => `${indent}<property key="${_escapeAttr(key)}" value="${_escapeAttr(value)}"/>`);
1422
+ }
1423
+
1424
+ /**
1425
+ * Items written inside a top-level folder, each in the sub-folders its
1426
+ * `archiFolder` path names (rebuilt as Archi had them: ids, names,
1427
+ * documentation, properties); items without a path directly in the folder.
1428
+ * `write(item, indent)` gives an item's lines.
1429
+ * @private
1430
+ */
1431
+ function _inSubFolders(items, write, indent = ' ') {
1432
+ const lines = [];
1433
+ // An empty folder comes as {archiFolder, folderOnly}: it makes its folders and writes nothing itself.
1434
+ const direct = items.filter(item => !item.archiFolder?.length && !item.folderOnly);
1435
+ // Sub-folders in the order they are first named, each holding the items one level deeper.
1436
+ const groups = new Map();
1437
+ for (const item of items.filter(item => item.archiFolder?.length)) {
1438
+ const [step, ...rest] = item.archiFolder;
1439
+ if (!groups.has(step.id)) groups.set(step.id, { step, items: [] });
1440
+ groups.get(step.id).items.push({ ...item, archiFolder: rest });
1441
+ }
1442
+ for (const { step, items: inner } of groups.values()) {
1443
+ lines.push(`${indent}<folder name="${_escapeAttr(step.name)}" id="${_escapeAttr(step.id)}">`);
1444
+ if (step.documentation) lines.push(`${indent} <documentation>${_escapeXml(step.documentation)}</documentation>`);
1445
+ lines.push(..._inSubFolders(inner, write, indent + ' '));
1446
+ lines.push(..._propertyLines(step.properties, indent + ' '));
1447
+ lines.push(`${indent}</folder>`);
1448
+ }
1449
+ for (const item of direct) lines.push(...write(item, indent));
1450
+ return lines;
1451
+ }
1452
+
1453
+ /** One element or relationship: its attributes, documentation and properties. @private */
1454
+ function _serializeConcept(xsiType, attrs, concept, indent, extraLines = []) {
1455
+ const all = { ...attrs, ...(concept.archi?.attributes ?? {}) };
1456
+ const attrText = Object.entries(all).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
1457
+ const body = [];
1458
+ if (concept.documentation) body.push(`${indent} <documentation>${_escapeXml(concept.documentation)}</documentation>`);
1459
+ body.push(..._extraElementLines(concept.archi?.elements, `${indent} `));
1460
+ body.push(...extraLines);
1461
+ if (Array.isArray(concept.properties)) {
1462
+ for (const p of concept.properties) {
1463
+ const k = p.name ?? p.key ?? '';
1464
+ const v = p.value == null ? '' : String(p.value);
1465
+ if (k) body.push(`${indent} <property key="${_escapeAttr(k)}" value="${_escapeAttr(v)}"/>`);
1466
+ }
1467
+ } else if (concept.properties) {
1468
+ for (const [k, v] of Object.entries(_typedArrayToProperties(concept.properties))) {
1469
+ body.push(`${indent} <property key="${_escapeAttr(k)}" value="${_escapeAttr(v)}"/>`);
1470
+ }
1471
+ }
1472
+ if (!body.length) return [`${indent}<element xsi:type="${xsiType}"${attrText}/>`];
1473
+ return [`${indent}<element xsi:type="${xsiType}"${attrText}>`, ...body, `${indent}</element>`];
1474
+ }
1475
+
1476
+ /** A view-only note or image as its Archi visual. @private */
1477
+ function _viewObjectVisual(el, bounds) {
1478
+ const base = { id: `diag-${el.id}`, bounds, sourceConnections: [] };
1479
+ if (el.archiVisualType) {
1480
+ // A canvas or sketch box: its own class, text and picture.
1481
+ return { ...base, type: el.archiVisualType, text: el.label ?? '', ...(el.iconUrl && { imagePath: el.iconUrl }) };
1482
+ }
1483
+ if (el.shape === 'image') {
1484
+ return { ...base, type: el.archiVisualType === 'canvas-model-image' ? 'canvas-model-image' : 'diagram-model-image', name: el.name || '', imagePath: el.iconUrl, ...(el.href && { properties: [{ name: 'href', type: 'string', value: el.href }] }) };
1485
+ }
1486
+ return { ...base, type: 'diagram-model-note', text: el.label ?? '' };
1487
+ }
1488
+
1489
+ /** Reverse mapping: produce an Archi native XML string from renderengine data. */
1490
+ export function toArchiXML(model, views) {
1491
+ const { elements = [], relationships = [] } = model;
1492
+ const viewArray = views || [];
1493
+
1494
+ const lines = [
1495
+ '<?xml version="1.0" encoding="UTF-8"?>',
1496
+ `<archimate:model xmlns:archimate="${ARCHI_NAMESPACE}" xmlns:canvas="${ARCHI_CANVAS_NAMESPACE}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="${_escapeAttr(model.name || 'Untitled')}" id="${_escapeAttr(model.id || _newId('id'))}" version="${_escapeAttr(model.version || '5.0')}"${Object.entries(model.archi?.attributes ?? {}).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('')}>`,
1497
+ ];
1498
+
1499
+ // Archi always has its nine top-level folders, and fails to open a model
1500
+ // that lacks some of them while it has a view: write them all, in its order.
1501
+ const byFolder = new Map(ARCHI_FOLDERS.map(folder => [folder.type, []]));
1502
+ // Specializations written for element types Archi lacks, by id.
1503
+ const typeProfiles = new Map();
1504
+ for (const el of elements) {
1505
+ // Archi knows ArchiMate 3.2 only: a version 4 type goes back to its 3.2 one, and a type
1506
+ // Archi has no class for (an IT type, a custom type) to the nearest one.
1507
+ const { folder, type, original } = archiFolderOf(el);
1508
+ let profile = null;
1509
+ // An element Archi already gave a specialization keeps that one.
1510
+ if (original && !el.archi?.attributes?.profiles) {
1511
+ profile = typeProfileId(original);
1512
+ typeProfiles.set(profile, { name: typeDisplayName(original), conceptType: _toPascalType(type) });
1513
+ }
1514
+ byFolder.get(folder.type).push({ el, type, original, profile });
1515
+ }
1516
+ for (const folder of ARCHI_FOLDERS) {
1517
+ const items = byFolder.get(folder.type);
1518
+ const body = [];
1519
+ if (folder.type === 'relations') {
1520
+ body.push(..._inSubFolders(relationships, (rel, indent) => {
1521
+ const name = rel.label ?? rel.name;
1522
+ const directed = rel.kind === 'association-directed';
1523
+ const kind = directed ? 'association' : (rel.kind || 'association');
1524
+ return _serializeConcept(`archimate:${_toPascalType(kind)}Relationship`,
1525
+ { id: rel.id, ...(name && { name }), source: rel.source, target: rel.target, ...(directed && { directed: 'true' }) }, rel, indent);
1526
+ }));
1527
+ } else if (folder.type === 'diagrams') {
1528
+ const emptyFolders = (model.emptyViewFolders ?? []).map(archiFolder => ({ archiFolder, folderOnly: true }));
1529
+ body.push(..._inSubFolders([...viewArray, ...emptyFolders], (view, indent) => _serializeView(view).map(line => indent.slice(4) + line)));
1530
+ } else {
1531
+ body.push(..._inSubFolders(items.map(item => ({ ...item, archiFolder: item.el.archiFolder })),
1532
+ ({ el, type, original, profile }, indent) => _serializeConcept(`archimate:${_toPascalType(type)}`,
1533
+ { id: el.id, name: el.name || '', ...(profile && { profiles: profile }) }, el, indent,
1534
+ original ? [`${indent} <feature name="${ARCHI_TYPE_FEATURE}" value="${_escapeAttr(original)}"/>`] : [])));
1535
+ }
1536
+ const open = ` <folder name="${_escapeAttr(folder.name)}" id="${folder.type}-folder" type="${folder.type}"`;
1537
+ if (body.length) lines.push(`${open}>`, ...body.map(line => ` ${line}`), ' </folder>');
1538
+ else lines.push(`${open}/>`);
1539
+ }
1540
+
1541
+ // What else the model carried (profiles, ...), its own properties, then its documentation (Archi's "purpose").
1542
+ lines.push(..._extraElementLines(model.archi?.elements, ' '));
1543
+ for (const [id, { name, conceptType }] of typeProfiles) {
1544
+ lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"/>`);
1545
+ }
1546
+ lines.push(..._propertyLines(model.properties, ' '));
1547
+ if (model.documentation) lines.push(` <purpose>${_escapeXml(model.documentation)}</purpose>`);
1548
+ lines.push('</archimate:model>');
1549
+ return lines.join('\n');
1550
+ }
1551
+
1552
+ function _serializeView(view) {
1553
+ const xsiType = {
1554
+ 'archimate-diagram-model': 'archimate:ArchimateDiagramModel',
1555
+ 'sketch-model': 'archimate:SketchModel',
1556
+ 'canvas-model': 'canvas:CanvasModel',
1557
+ }[view.type] || 'archimate:ArchimateDiagramModel';
1558
+
1559
+ const extra = Object.entries(view.archi?.attributes ?? {}).map(([k, v]) => ` ${k}="${_escapeAttr(v)}"`).join('');
1560
+ const lines = [` <element xsi:type="${xsiType}" name="${_escapeAttr(view.name || 'View')}" id="${view.id}"${extra}>`];
1561
+ // Archi lists each visual's incoming connections on it (targetConnections); without
1562
+ // them it draws a line only when its target happens to be created before its source.
1563
+ const incoming = new Map();
1564
+ const collect = (visual) => {
1565
+ for (const sc of visual.sourceConnections || []) {
1566
+ if (!incoming.has(sc.target)) incoming.set(sc.target, []);
1567
+ incoming.get(sc.target).push(sc.id);
1568
+ }
1569
+ (visual.children || []).forEach(collect);
1570
+ };
1571
+ (view.children || []).forEach(collect);
1572
+ for (const visual of view.children || []) {
1573
+ lines.push(..._serializeVisual(visual, ' ', incoming));
1574
+ }
1575
+ // After the boxes, as Archi writes a view: its documentation, other elements, properties.
1576
+ if (view.documentation) lines.push(` <documentation>${_escapeXml(view.documentation)}</documentation>`);
1577
+ lines.push(..._extraElementLines(view.archi?.elements, ' '));
1578
+ if (view.template) lines.push(` <feature name="${ARCHI_TEMPLATE_FEATURE}" value="true"/>`);
1579
+ lines.push(..._propertyLines(view.properties, ' '));
1580
+ lines.push(' </element>');
1581
+ return lines;
1582
+ }
1583
+
1584
+ function _serializeVisual(visual, indent, incoming = new Map()) {
1585
+ const xsiType = _visualTypeToXSI(visual.type);
1586
+ const attrs = [`xsi:type="${xsiType}"`, `id="${visual.id}"`];
1587
+ if (incoming.get(visual.id)?.length) attrs.push(`targetConnections="${incoming.get(visual.id).join(' ')}"`);
1588
+ if (visual.archimateElement) attrs.push(`archimateElement="${visual.archimateElement}"`);
1589
+ if (visual.referencedModel) attrs.push(`referencedModel="${visual.referencedModel}"`);
1590
+ if (visual.imagePath) attrs.push(`imagePath="${_escapeAttr(visual.imagePath)}"`);
1591
+ if (visual.name) attrs.push(`name="${_escapeAttr(visual.name)}"`);
1592
+ if (visual.locked) attrs.push('locked="true"');
1593
+ const { [ARCHI_LINE_ALPHA_FEATURE]: lineAlpha, ...attributeStyle } = visual.style || {};
1594
+ for (const [name, value] of Object.entries(attributeStyle)) attrs.push(`${name}="${_escapeAttr(value)}"`);
1595
+ for (const [name, value] of Object.entries(visual.extraAttributes || {})) attrs.push(`${name}="${_escapeAttr(value)}"`);
1596
+
1597
+ const lines = [`${indent}<child ${attrs.join(' ')}>`];
1598
+ if (visual.bounds) {
1599
+ const b = visual.bounds;
1600
+ lines.push(`${indent} <bounds x="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}"/>`);
1601
+ }
1602
+ // A note's text: Archi keeps it in a <content> element.
1603
+ if (visual.text) lines.push(`${indent} <content>${_escapeXml(visual.text)}</content>`);
1604
+ for (const [name, value] of Object.entries(visual.features || {})) {
1605
+ lines.push(`${indent} <feature name="${_escapeAttr(name)}" value="${_escapeAttr(String(value))}"/>`);
1606
+ }
1607
+ if (lineAlpha !== undefined) lines.push(`${indent} <feature name="${ARCHI_LINE_ALPHA_FEATURE}" value="${_escapeAttr(String(lineAlpha))}"/>`);
1608
+ if (Array.isArray(visual.properties)) {
1609
+ for (const p of visual.properties) {
1610
+ const k = p.name ?? p.key ?? '';
1611
+ const v = p.value == null ? '' : String(p.value);
1612
+ if (k) lines.push(`${indent} <property key="${_escapeAttr(k)}" value="${_escapeAttr(v)}"/>`);
1613
+ }
1614
+ } else if (visual.properties) {
1615
+ for (const [key, value] of Object.entries(_typedArrayToProperties(visual.properties))) {
1616
+ lines.push(`${indent} <property key="${_escapeAttr(key)}" value="${_escapeAttr(value)}"/>`);
1617
+ }
1618
+ }
1619
+ lines.push(..._extraElementLines(visual.extraElements, `${indent} `));
1620
+ for (const sc of visual.sourceConnections || []) {
1621
+ // A relationship's line is archimate:Connection; a plain line keeps its own class.
1622
+ const xsiType = sc.relationship ? 'archimate:Connection' : _visualTypeToXSI(sc.archiType ?? 'diagram-model-connection');
1623
+ const scAttrs = [
1624
+ // Archi's default class for a line needs no type, and Archi writes none.
1625
+ ...(xsiType === 'archimate:DiagramModelConnection' ? [] : [`xsi:type="${xsiType}"`]),
1626
+ `id="${sc.id}"`,
1627
+ `source="${sc.source}"`,
1628
+ `target="${sc.target}"`,
1629
+ ];
1630
+ if (sc.relationship) scAttrs.push(`archimateRelationship="${sc.relationship}"`);
1631
+ if (!sc.relationship && sc.name) scAttrs.push(`name="${_escapeAttr(sc.name)}"`);
1632
+ if (!sc.relationship && sc.connectionType) scAttrs.push(`type="${sc.connectionType}"`);
1633
+ for (const [name, value] of Object.entries(sc.style || {})) scAttrs.push(`${name}="${_escapeAttr(value)}"`);
1634
+ for (const [name, value] of Object.entries(sc.extraAttributes || {})) scAttrs.push(`${name}="${_escapeAttr(value)}"`);
1635
+
1636
+ if (sc.bendpoints?.length) {
1637
+ lines.push(`${indent} <sourceConnection ${scAttrs.join(' ')}>`);
1638
+ for (const bp of sc.bendpoints) {
1639
+ lines.push(`${indent} <bendpoint ${BENDPOINT_KEYS.map(k => `${k}="${bp[k]}"`).join(' ')}/>`);
1640
+ }
1641
+ lines.push(`${indent} </sourceConnection>`);
1642
+ } else {
1643
+ lines.push(`${indent} <sourceConnection ${scAttrs.join(' ')}/>`);
1644
+ }
1645
+ }
1646
+ for (const child of visual.children || []) {
1647
+ lines.push(..._serializeVisual(child, `${indent} `, incoming));
1648
+ }
1649
+ lines.push(`${indent}</child>`);
1650
+ return lines;
1651
+ }
1652
+
1653
+ /** Archi's top-level folders, in its order, with the layers whose elements each holds. */
1654
+ export const ARCHI_FOLDERS = Object.freeze([
1655
+ { type: 'strategy', name: 'Strategy', layers: ['strategy'] },
1656
+ { type: 'business', name: 'Business', layers: ['business'] },
1657
+ { type: 'application', name: 'Application', layers: ['application'] },
1658
+ { type: 'technology', name: 'Technology & Physical', layers: ['technology', 'physical'] },
1659
+ { type: 'motivation', name: 'Motivation', layers: ['motivation'] },
1660
+ { type: 'implementation_migration', name: 'Implementation & Migration', layers: ['implementation'] },
1661
+ { type: 'other', name: 'Other', layers: ['composite', 'other'] },
1662
+ { type: 'relations', name: 'Relations' },
1663
+ { type: 'diagrams', name: 'Views' },
1664
+ ]);
1665
+
1666
+ /**
1667
+ * Where Archi keeps an element: the top-level folder of its ArchiMate 3.2
1668
+ * layer, with the 3.2 type it is written as and, for a type Archi lacks, its
1669
+ * own (archiElementType).
1670
+ * @param {Object} el - A model element
1671
+ * @returns {{folder: Object, type: string, original: string|undefined}}
1672
+ */
1673
+ export function archiFolderOf(el) {
1674
+ const { type, original } = archiElementType(el);
1675
+ const layer = ArchimateNotation.elementLayer({ type }, '3.2');
1676
+ const folder = ARCHI_FOLDERS.find(x => x.layers?.includes(layer !== 'other' ? layer : el.layer)) ?? ARCHI_FOLDERS.find(x => x.type === 'other');
1677
+ return { folder, type, original };
1678
+ }
1679
+
1680
+ function _visualTypeToXSI(type) {
1681
+ const map = {
1682
+ 'diagram-model-note': 'archimate:Note',
1683
+ 'diagram-model-group': 'archimate:Group',
1684
+ 'diagram-model-connection': 'archimate:DiagramModelConnection',
1685
+ 'canvas-model-connection': 'canvas:CanvasModelConnection',
1686
+ 'diagram-model-image': 'archimate:DiagramModelImage',
1687
+ 'diagram-model-reference': 'archimate:DiagramModelReference',
1688
+ 'sketch-model-sticky': 'archimate:SketchModelSticky',
1689
+ 'sketch-model-actor': 'archimate:SketchModelActor',
1690
+ 'canvas-model-block': 'canvas:CanvasModelBlock',
1691
+ 'canvas-model-sticky': 'canvas:CanvasModelSticky',
1692
+ 'canvas-model-image': 'canvas:CanvasModelImage',
1693
+ };
1694
+ return map[type] || 'archimate:DiagramObject';
1695
+ }
1696
+
1697
+ function _toPascalType(kebab) {
1698
+ if (!kebab) return '';
1699
+ return kebab.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
1700
+ }
1701
+
1702
+ function _newId(prefix = 'id') {
1703
+ return `${prefix}_${Math.random().toString(36).slice(2, 9)}`;
1704
+ }
1705
+
1706
+ /** Text for element content. A CR is written as a reference: a parser turns a literal one into a newline. */
1707
+ function _escapeXml(str) {
1708
+ return String(str)
1709
+ .replace(/&/g, '&amp;')
1710
+ .replace(/</g, '&lt;')
1711
+ .replace(/>/g, '&gt;')
1712
+ .replace(/"/g, '&quot;')
1713
+ .replace(/\r/g, '&#xD;');
1714
+ }
1715
+
1716
+ /** Text for an attribute value: a parser also turns a literal newline or tab there into a space. */
1717
+ function _escapeAttr(str) {
1718
+ return _escapeXml(str).replace(/\n/g, '&#xA;').replace(/\t/g, '&#x9;');
1719
+ }
1720
+
1721
+ /*
1722
+ * Shared with the Open Group Exchange Format (archimateExchangeFormat.js), which
1723
+ * reads and writes the same parsed model and view trees as this module.
1724
+ */
1725
+ export {
1726
+ _visualBoxes as archiVisualBoxes,
1727
+ _bendpointLocations as archiBendpointLocations,
1728
+ _relativizeLocatedBendpoints as relativizeArchiBendpoints,
1729
+ _parseFontFeature as parseArchiFont,
1730
+ _fontString as archiFontString,
1731
+ _typedArrayToProperties as typedArrayToProperties,
1732
+ _toPascalType as toPascalType,
1733
+ _escapeXml as escapeXml,
1734
+ _escapeAttr as escapeAttr,
1735
+ };