@haikal-fikri/archimate 0.1.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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +1368 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
// src/colors.ts
|
|
2
|
+
var LAYER_COLORS = {
|
|
3
|
+
Strategy: "#F5DEAA",
|
|
4
|
+
Resource: "#F5DEAA",
|
|
5
|
+
Capability: "#F5DEAA",
|
|
6
|
+
CourseOfAction: "#F5DEAA",
|
|
7
|
+
ValueStream: "#F5DEAA",
|
|
8
|
+
Business: "#FFFFB5",
|
|
9
|
+
BusinessActor: "#FFFFB5",
|
|
10
|
+
BusinessRole: "#FFFFB5",
|
|
11
|
+
BusinessCollaboration: "#FFFFB5",
|
|
12
|
+
BusinessInterface: "#FFFFB5",
|
|
13
|
+
BusinessProcess: "#FFFFB5",
|
|
14
|
+
BusinessFunction: "#FFFFB5",
|
|
15
|
+
BusinessInteraction: "#FFFFB5",
|
|
16
|
+
BusinessEvent: "#FFFFB5",
|
|
17
|
+
BusinessService: "#FFFFB5",
|
|
18
|
+
BusinessObject: "#FFFFB5",
|
|
19
|
+
Contract: "#FFFFB5",
|
|
20
|
+
Representation: "#FFFFB5",
|
|
21
|
+
Product: "#FFFFB5",
|
|
22
|
+
Application: "#B5FFFF",
|
|
23
|
+
ApplicationComponent: "#B5FFFF",
|
|
24
|
+
ApplicationCollaboration: "#B5FFFF",
|
|
25
|
+
ApplicationInterface: "#B5FFFF",
|
|
26
|
+
ApplicationFunction: "#B5FFFF",
|
|
27
|
+
ApplicationInteraction: "#B5FFFF",
|
|
28
|
+
ApplicationProcess: "#B5FFFF",
|
|
29
|
+
ApplicationEvent: "#B5FFFF",
|
|
30
|
+
ApplicationService: "#B5FFFF",
|
|
31
|
+
DataObject: "#B5FFFF",
|
|
32
|
+
Technology: "#C9E7B7",
|
|
33
|
+
TechnologyCollaboration: "#C9E7B7",
|
|
34
|
+
TechnologyInterface: "#C9E7B7",
|
|
35
|
+
TechnologyFunction: "#C9E7B7",
|
|
36
|
+
TechnologyProcess: "#C9E7B7",
|
|
37
|
+
TechnologyInteraction: "#C9E7B7",
|
|
38
|
+
TechnologyEvent: "#C9E7B7",
|
|
39
|
+
TechnologyService: "#C9E7B7",
|
|
40
|
+
Node: "#C9E7B7",
|
|
41
|
+
Device: "#C9E7B7",
|
|
42
|
+
SystemSoftware: "#C9E7B7",
|
|
43
|
+
Path: "#C9E7B7",
|
|
44
|
+
CommunicationNetwork: "#C9E7B7",
|
|
45
|
+
Artifact: "#C9E7B7",
|
|
46
|
+
Equipment: "#C9E7B7",
|
|
47
|
+
Facility: "#C9E7B7",
|
|
48
|
+
DistributionNetwork: "#C9E7B7",
|
|
49
|
+
Material: "#C9E7B7",
|
|
50
|
+
Stakeholder: "#CCCCFF",
|
|
51
|
+
Driver: "#CCCCFF",
|
|
52
|
+
Assessment: "#CCCCFF",
|
|
53
|
+
Goal: "#CCCCFF",
|
|
54
|
+
Outcome: "#CCCCFF",
|
|
55
|
+
Principle: "#CCCCFF",
|
|
56
|
+
Requirement: "#CCCCFF",
|
|
57
|
+
Constraint: "#CCCCFF",
|
|
58
|
+
Meaning: "#CCCCFF",
|
|
59
|
+
Value: "#CCCCFF",
|
|
60
|
+
WorkPackage: "#E0FFE0",
|
|
61
|
+
Deliverable: "#E0FFE0",
|
|
62
|
+
Plateau: "#E0FFE0",
|
|
63
|
+
Gap: "#E0FFE0",
|
|
64
|
+
ImplementationEvent: "#E0FFE0",
|
|
65
|
+
AndJunction: "#000000",
|
|
66
|
+
OrJunction: "#000000",
|
|
67
|
+
Junction: "#000000"
|
|
68
|
+
};
|
|
69
|
+
var DEFAULT_FILL = "#FFFFFF";
|
|
70
|
+
var DEFAULT_STROKE = "#1A1A1A";
|
|
71
|
+
var DEFAULT_TEXT = "#1A1A1A";
|
|
72
|
+
function colorForType(elementType) {
|
|
73
|
+
if (!elementType) return DEFAULT_FILL;
|
|
74
|
+
if (LAYER_COLORS[elementType]) return LAYER_COLORS[elementType];
|
|
75
|
+
const prefixes = ["Strategy", "Business", "Application", "Technology", "Motivation", "Implementation"];
|
|
76
|
+
for (const p of prefixes) {
|
|
77
|
+
if (elementType.startsWith(p)) {
|
|
78
|
+
return LAYER_COLORS[p] ?? DEFAULT_FILL;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (elementType.endsWith("Junction")) return LAYER_COLORS.Junction;
|
|
82
|
+
return DEFAULT_FILL;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/parser.ts
|
|
86
|
+
var ACCESS_TYPES = /* @__PURE__ */ new Set(["Access", "Read", "Write", "ReadWrite"]);
|
|
87
|
+
function isJunctionType(t) {
|
|
88
|
+
if (!t) return false;
|
|
89
|
+
return t === "Junction" || t === "AndJunction" || t === "OrJunction";
|
|
90
|
+
}
|
|
91
|
+
var OPEN_EXCHANGE_NS_V3 = "http://www.opengroup.org/xsd/archimate/3.0/";
|
|
92
|
+
var OPEN_EXCHANGE_NS_V2 = "http://www.opengroup.org/xsd/archimate";
|
|
93
|
+
var OPEN_EXCHANGE_NAMESPACES = /* @__PURE__ */ new Set([
|
|
94
|
+
OPEN_EXCHANGE_NS_V3,
|
|
95
|
+
OPEN_EXCHANGE_NS_V2
|
|
96
|
+
]);
|
|
97
|
+
var NATIVE_ARCHI_NS = "http://www.archimatetool.com/archimate";
|
|
98
|
+
var XSI_NS = "http://www.w3.org/2001/XMLSchema-instance";
|
|
99
|
+
var RELATIVE_COORDINATES = false;
|
|
100
|
+
var ArchimateParseError = class extends Error {
|
|
101
|
+
kind;
|
|
102
|
+
constructor(message, kind) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.name = "ArchimateParseError";
|
|
105
|
+
this.kind = kind;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function childrenOf(el, name) {
|
|
109
|
+
return Array.from(el.children).filter((c) => c.localName === name);
|
|
110
|
+
}
|
|
111
|
+
function firstChild(el, name) {
|
|
112
|
+
for (const c of Array.from(el.children)) if (c.localName === name) return c;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function xsiType(el) {
|
|
116
|
+
return el.getAttributeNS(XSI_NS, "type") ?? el.getAttribute("xsi:type") ?? void 0;
|
|
117
|
+
}
|
|
118
|
+
function textOf(el) {
|
|
119
|
+
return (el?.textContent ?? "").trim();
|
|
120
|
+
}
|
|
121
|
+
function nameOrLabel(el) {
|
|
122
|
+
return textOf(firstChild(el, "name")) || textOf(firstChild(el, "label"));
|
|
123
|
+
}
|
|
124
|
+
function normalizeRelationshipType(raw) {
|
|
125
|
+
if (!raw) return void 0;
|
|
126
|
+
const stripped = raw.endsWith("Relationship") ? raw.slice(0, -"Relationship".length) : raw;
|
|
127
|
+
if (stripped === "Realisation") return "Realization";
|
|
128
|
+
if (stripped === "Specialisation") return "Specialization";
|
|
129
|
+
return stripped;
|
|
130
|
+
}
|
|
131
|
+
var ELEMENT_TYPE_RENAMES = {
|
|
132
|
+
InfrastructureService: "TechnologyService",
|
|
133
|
+
InfrastructureFunction: "TechnologyFunction",
|
|
134
|
+
InfrastructureInterface: "TechnologyInterface",
|
|
135
|
+
Network: "CommunicationNetwork"
|
|
136
|
+
};
|
|
137
|
+
function normalizeElementType(raw) {
|
|
138
|
+
if (!raw) return void 0;
|
|
139
|
+
return ELEMENT_TYPE_RENAMES[raw] ?? raw;
|
|
140
|
+
}
|
|
141
|
+
function attr(el, ...names) {
|
|
142
|
+
for (const n of names) {
|
|
143
|
+
const v = el.getAttribute(n);
|
|
144
|
+
if (v !== null) return v;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function rgbaFromColor(el) {
|
|
149
|
+
if (!el) return void 0;
|
|
150
|
+
const r = el.getAttribute("r");
|
|
151
|
+
const g = el.getAttribute("g");
|
|
152
|
+
const b = el.getAttribute("b");
|
|
153
|
+
const a = el.getAttribute("a");
|
|
154
|
+
if (r === null || g === null || b === null) return void 0;
|
|
155
|
+
const alpha = a === null ? 1 : Math.max(0, Math.min(1, Number(a) / 100));
|
|
156
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
157
|
+
}
|
|
158
|
+
function num(el, attr2, fallback = 0) {
|
|
159
|
+
const v = el.getAttribute(attr2);
|
|
160
|
+
if (v === null) return fallback;
|
|
161
|
+
const n = Number(v);
|
|
162
|
+
return Number.isFinite(n) ? n : fallback;
|
|
163
|
+
}
|
|
164
|
+
function parseArchimate(xml, viewId) {
|
|
165
|
+
if (typeof xml !== "string" || xml.trim().length === 0) {
|
|
166
|
+
throw new ArchimateParseError("No XML provided.", "invalid-xml");
|
|
167
|
+
}
|
|
168
|
+
let doc;
|
|
169
|
+
try {
|
|
170
|
+
doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
171
|
+
} catch (e) {
|
|
172
|
+
throw new ArchimateParseError(`Could not parse XML: ${e.message}`, "invalid-xml");
|
|
173
|
+
}
|
|
174
|
+
const errEl = doc.getElementsByTagName("parsererror")[0];
|
|
175
|
+
if (errEl) {
|
|
176
|
+
const msg = errEl.textContent?.split("\n").find((l) => l.trim().length > 0) ?? "malformed XML";
|
|
177
|
+
throw new ArchimateParseError(`Invalid XML: ${msg.trim()}`, "invalid-xml");
|
|
178
|
+
}
|
|
179
|
+
const root = doc.documentElement;
|
|
180
|
+
if (!root) {
|
|
181
|
+
throw new ArchimateParseError("XML document has no root element.", "invalid-xml");
|
|
182
|
+
}
|
|
183
|
+
if (root.namespaceURI === NATIVE_ARCHI_NS) {
|
|
184
|
+
throw new ArchimateParseError(
|
|
185
|
+
"This looks like Archi's native .archimate file. In Archi: File \u2192 Export \u2192 Open Exchange XML, then paste the exported XML here instead.",
|
|
186
|
+
"wrong-format"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (!root.namespaceURI || !OPEN_EXCHANGE_NAMESPACES.has(root.namespaceURI)) {
|
|
190
|
+
throw new ArchimateParseError(
|
|
191
|
+
`Expected ArchiMate Open Exchange XML (namespace ${OPEN_EXCHANGE_NS_V3} or ${OPEN_EXCHANGE_NS_V2}). Got ${root.namespaceURI ?? "no namespace"}.`,
|
|
192
|
+
"wrong-format"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const elementsEl = firstChild(root, "elements");
|
|
196
|
+
const elementMap = /* @__PURE__ */ new Map();
|
|
197
|
+
if (elementsEl) {
|
|
198
|
+
for (const el of childrenOf(elementsEl, "element")) {
|
|
199
|
+
const id = el.getAttribute("identifier");
|
|
200
|
+
if (!id) continue;
|
|
201
|
+
elementMap.set(id, {
|
|
202
|
+
name: nameOrLabel(el) || id,
|
|
203
|
+
type: normalizeElementType(xsiType(el))
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const relationshipsEl = firstChild(root, "relationships");
|
|
208
|
+
const relationshipMap = /* @__PURE__ */ new Map();
|
|
209
|
+
if (relationshipsEl) {
|
|
210
|
+
for (const r of childrenOf(relationshipsEl, "relationship")) {
|
|
211
|
+
const id = r.getAttribute("identifier");
|
|
212
|
+
const source = r.getAttribute("source");
|
|
213
|
+
const target = r.getAttribute("target");
|
|
214
|
+
if (!id || !source || !target) continue;
|
|
215
|
+
const rawAccess = r.getAttribute("accessType");
|
|
216
|
+
const accessType = rawAccess && ACCESS_TYPES.has(rawAccess) ? rawAccess : void 0;
|
|
217
|
+
const isDirectedAttr = r.getAttribute("isDirected");
|
|
218
|
+
const isDirected = isDirectedAttr === "true" ? true : isDirectedAttr === "false" ? false : void 0;
|
|
219
|
+
relationshipMap.set(id, {
|
|
220
|
+
source,
|
|
221
|
+
target,
|
|
222
|
+
type: normalizeRelationshipType(xsiType(r)),
|
|
223
|
+
accessType,
|
|
224
|
+
isDirected,
|
|
225
|
+
name: nameOrLabel(r) || void 0
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const viewsEl = firstChild(root, "views");
|
|
230
|
+
if (!viewsEl) throw new ArchimateParseError("Model has no <views> section.", "no-views");
|
|
231
|
+
const viewParent = firstChild(viewsEl, "diagrams") ?? viewsEl;
|
|
232
|
+
const allViews = childrenOf(viewParent, "view");
|
|
233
|
+
if (allViews.length === 0) throw new ArchimateParseError("Model contains no views.", "no-views");
|
|
234
|
+
let viewEl = null;
|
|
235
|
+
if (viewId) {
|
|
236
|
+
viewEl = allViews.find((v) => v.getAttribute("identifier") === viewId) ?? null;
|
|
237
|
+
if (!viewEl) {
|
|
238
|
+
throw new ArchimateParseError(
|
|
239
|
+
`No view with identifier "${viewId}" found in this model.`,
|
|
240
|
+
"view-not-found"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
viewEl = allViews[0];
|
|
245
|
+
}
|
|
246
|
+
const viewName = nameOrLabel(viewEl) || viewEl.getAttribute("identifier") || "Untitled view";
|
|
247
|
+
const nodes = [];
|
|
248
|
+
function walkNode(nodeEl, offsetX, offsetY) {
|
|
249
|
+
const id = nodeEl.getAttribute("identifier");
|
|
250
|
+
if (!id) return;
|
|
251
|
+
const lx = num(nodeEl, "x");
|
|
252
|
+
const ly = num(nodeEl, "y");
|
|
253
|
+
const w = num(nodeEl, "w");
|
|
254
|
+
const h = num(nodeEl, "h");
|
|
255
|
+
const x = RELATIVE_COORDINATES ? lx + offsetX : lx;
|
|
256
|
+
const y = RELATIVE_COORDINATES ? ly + offsetY : ly;
|
|
257
|
+
const elementRef = attr(nodeEl, "elementRef", "elementref");
|
|
258
|
+
const elementInfo = elementRef ? elementMap.get(elementRef) : void 0;
|
|
259
|
+
const nodeType = xsiType(nodeEl);
|
|
260
|
+
const styleEl = firstChild(nodeEl, "style");
|
|
261
|
+
const lineEl = styleEl ? firstChild(styleEl, "lineColor") : null;
|
|
262
|
+
const fontEl = styleEl ? firstChild(styleEl, "font") : null;
|
|
263
|
+
const fontColorEl = fontEl ? firstChild(fontEl, "color") : null;
|
|
264
|
+
const fontSizeAttr = fontEl?.getAttribute("size");
|
|
265
|
+
const fontSize = fontSizeAttr ? Number(fontSizeAttr) : void 0;
|
|
266
|
+
nodes.push({
|
|
267
|
+
id,
|
|
268
|
+
elementId: elementRef ?? void 0,
|
|
269
|
+
elementType: elementInfo?.type,
|
|
270
|
+
nodeType,
|
|
271
|
+
isJunction: isJunctionType(elementInfo?.type),
|
|
272
|
+
label: elementInfo?.name ?? nameOrLabel(nodeEl),
|
|
273
|
+
x,
|
|
274
|
+
y,
|
|
275
|
+
w,
|
|
276
|
+
h,
|
|
277
|
+
fill: colorForType(elementInfo?.type),
|
|
278
|
+
stroke: rgbaFromColor(lineEl) ?? DEFAULT_STROKE,
|
|
279
|
+
textColor: rgbaFromColor(fontColorEl) ?? DEFAULT_TEXT,
|
|
280
|
+
fontFamily: fontEl?.getAttribute("name") ?? void 0,
|
|
281
|
+
fontSize: fontSize && Number.isFinite(fontSize) ? fontSize : void 0
|
|
282
|
+
});
|
|
283
|
+
for (const childNode of childrenOf(nodeEl, "node")) {
|
|
284
|
+
walkNode(childNode, x, y);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const n of childrenOf(viewEl, "node")) walkNode(n, 0, 0);
|
|
288
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const n of nodes) nodeMap.set(n.id, n);
|
|
290
|
+
const connections = [];
|
|
291
|
+
for (const c of childrenOf(viewEl, "connection")) {
|
|
292
|
+
const id = c.getAttribute("identifier");
|
|
293
|
+
const source = c.getAttribute("source");
|
|
294
|
+
const target = c.getAttribute("target");
|
|
295
|
+
if (!id || !source || !target) continue;
|
|
296
|
+
if (!nodeMap.has(source) || !nodeMap.has(target)) continue;
|
|
297
|
+
const relRef = attr(c, "relationshipRef", "relationshipref");
|
|
298
|
+
const relInfo = relRef ? relationshipMap.get(relRef) : void 0;
|
|
299
|
+
const bendpoints = [];
|
|
300
|
+
for (const bp of childrenOf(c, "bendpoint")) {
|
|
301
|
+
bendpoints.push({ x: num(bp, "x"), y: num(bp, "y") });
|
|
302
|
+
}
|
|
303
|
+
const styleEl = firstChild(c, "style");
|
|
304
|
+
const lineEl = styleEl ? firstChild(styleEl, "lineColor") : null;
|
|
305
|
+
connections.push({
|
|
306
|
+
id,
|
|
307
|
+
sourceId: source,
|
|
308
|
+
targetId: target,
|
|
309
|
+
relationshipType: relInfo?.type,
|
|
310
|
+
accessType: relInfo?.accessType,
|
|
311
|
+
isDirected: relInfo?.isDirected,
|
|
312
|
+
name: relInfo?.name,
|
|
313
|
+
bendpoints,
|
|
314
|
+
stroke: rgbaFromColor(lineEl)
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
let minX = Infinity;
|
|
318
|
+
let minY = Infinity;
|
|
319
|
+
let maxX = -Infinity;
|
|
320
|
+
let maxY = -Infinity;
|
|
321
|
+
for (const n of nodes) {
|
|
322
|
+
if (n.x < minX) minX = n.x;
|
|
323
|
+
if (n.y < minY) minY = n.y;
|
|
324
|
+
if (n.x + n.w > maxX) maxX = n.x + n.w;
|
|
325
|
+
if (n.y + n.h > maxY) maxY = n.y + n.h;
|
|
326
|
+
}
|
|
327
|
+
if (!Number.isFinite(minX)) {
|
|
328
|
+
minX = 0;
|
|
329
|
+
minY = 0;
|
|
330
|
+
maxX = 100;
|
|
331
|
+
maxY = 100;
|
|
332
|
+
}
|
|
333
|
+
const pad = 40;
|
|
334
|
+
return {
|
|
335
|
+
viewId: viewEl.getAttribute("identifier") ?? "",
|
|
336
|
+
viewName,
|
|
337
|
+
viewBox: {
|
|
338
|
+
x: minX - pad,
|
|
339
|
+
y: minY - pad,
|
|
340
|
+
width: maxX - minX + pad * 2,
|
|
341
|
+
height: maxY - minY + pad * 2
|
|
342
|
+
},
|
|
343
|
+
nodes,
|
|
344
|
+
connections
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/renderer.tsx
|
|
349
|
+
import {
|
|
350
|
+
forwardRef,
|
|
351
|
+
useEffect,
|
|
352
|
+
useImperativeHandle,
|
|
353
|
+
useMemo,
|
|
354
|
+
useRef
|
|
355
|
+
} from "react";
|
|
356
|
+
|
|
357
|
+
// src/icons.ts
|
|
358
|
+
var ICONS = {
|
|
359
|
+
// ─── Active structure ──────────────────────────────────────────────────────
|
|
360
|
+
// Actor / Stakeholder — stick figure with head, body, arms, legs
|
|
361
|
+
actor: {
|
|
362
|
+
outline: [
|
|
363
|
+
"M 50 22 m -10 0 a 10 10 0 1 0 20 0 a 10 10 0 1 0 -20 0",
|
|
364
|
+
"M 50 32 L 50 60",
|
|
365
|
+
"M 22 46 L 78 46",
|
|
366
|
+
"M 50 60 L 28 88",
|
|
367
|
+
"M 50 60 L 72 88"
|
|
368
|
+
]
|
|
369
|
+
},
|
|
370
|
+
// Role — horizontal pill with a small filled dot on the right
|
|
371
|
+
role: {
|
|
372
|
+
outline: ["M 22 32 L 78 32 a 18 18 0 0 1 0 36 L 22 68 a 18 18 0 0 1 0 -36 Z"],
|
|
373
|
+
filled: ["M 70 50 m -7 0 a 7 7 0 1 0 14 0 a 7 7 0 1 0 -14 0"]
|
|
374
|
+
},
|
|
375
|
+
// Collaboration — two full overlapping circles (Venn-style, symmetric)
|
|
376
|
+
collaboration: {
|
|
377
|
+
outline: [
|
|
378
|
+
"M 36 50 m -22 0 a 22 22 0 1 0 44 0 a 22 22 0 1 0 -44 0",
|
|
379
|
+
"M 64 50 m -22 0 a 22 22 0 1 0 44 0 a 22 22 0 1 0 -44 0"
|
|
380
|
+
]
|
|
381
|
+
},
|
|
382
|
+
// Interaction — two semicircles (D-shapes) facing each other with a gap
|
|
383
|
+
interaction: {
|
|
384
|
+
outline: [
|
|
385
|
+
"M 42 22 A 28 28 0 0 0 42 78 Z",
|
|
386
|
+
"M 58 22 A 28 28 0 0 1 58 78 Z"
|
|
387
|
+
]
|
|
388
|
+
},
|
|
389
|
+
// Interface — lollipop (line on left, filled circle on right)
|
|
390
|
+
interface: {
|
|
391
|
+
outline: [
|
|
392
|
+
"M 64 50 m -16 0 a 16 16 0 1 0 32 0 a 16 16 0 1 0 -32 0",
|
|
393
|
+
"M 12 50 L 48 50"
|
|
394
|
+
]
|
|
395
|
+
},
|
|
396
|
+
// Application Component — main rectangle + two connector tabs on the left
|
|
397
|
+
component: {
|
|
398
|
+
outline: [
|
|
399
|
+
"M 30 22 L 88 22 L 88 78 L 30 78 Z",
|
|
400
|
+
"M 14 32 L 38 32 L 38 46 L 14 46 Z",
|
|
401
|
+
"M 14 54 L 38 54 L 38 68 L 14 68 Z"
|
|
402
|
+
]
|
|
403
|
+
},
|
|
404
|
+
// ─── Behavior ──────────────────────────────────────────────────────────────
|
|
405
|
+
// Process — right-pointing arrow / chevron
|
|
406
|
+
process: {
|
|
407
|
+
outline: ["M 12 35 L 56 35 L 56 22 L 88 50 L 56 78 L 56 65 L 12 65 Z"]
|
|
408
|
+
},
|
|
409
|
+
// Function — concentric chevrons matching the ArchiMate reference notation.
|
|
410
|
+
// Outer V (apex up) and inner V (apex up, lower) sharing the same bottom
|
|
411
|
+
// axis. Both outer-bottom corners and both inner-bottom corners sit on the
|
|
412
|
+
// bottom edge; the inner V's apex sits above, forming the top of the stripe.
|
|
413
|
+
// Vertices (clockwise): outer-bottom-left, outer-apex, outer-bottom-right,
|
|
414
|
+
// inner-bottom-right, inner-apex, inner-bottom-left.
|
|
415
|
+
function: {
|
|
416
|
+
outline: ["M 10 82 L 50 18 L 90 82 L 72 82 L 50 42 L 28 82 Z"]
|
|
417
|
+
},
|
|
418
|
+
// Event — D-shape with chevron notch on the left
|
|
419
|
+
event: {
|
|
420
|
+
outline: ["M 70 22 a 28 28 0 0 1 0 56 L 28 78 L 48 50 L 28 22 Z"]
|
|
421
|
+
},
|
|
422
|
+
// Service — pill / rounded rectangle
|
|
423
|
+
service: {
|
|
424
|
+
outline: ["M 30 32 L 70 32 a 18 18 0 0 1 0 36 L 30 68 a 18 18 0 0 1 0 -36 Z"]
|
|
425
|
+
},
|
|
426
|
+
// ─── Passive structure ─────────────────────────────────────────────────────
|
|
427
|
+
// Object / Data Object — rectangle with filled header bar
|
|
428
|
+
object: {
|
|
429
|
+
outline: ["M 15 22 L 85 22 L 85 78 L 15 78 Z", "M 15 36 L 85 36"],
|
|
430
|
+
filled: ["M 15 22 L 85 22 L 85 36 L 15 36 Z"]
|
|
431
|
+
},
|
|
432
|
+
// Contract / Material — rectangle with several horizontal text lines
|
|
433
|
+
contract: {
|
|
434
|
+
outline: [
|
|
435
|
+
"M 18 18 L 82 18 L 82 82 L 18 82 Z",
|
|
436
|
+
"M 28 32 L 72 32",
|
|
437
|
+
"M 28 44 L 72 44",
|
|
438
|
+
"M 28 56 L 72 56",
|
|
439
|
+
"M 28 68 L 60 68"
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
// Representation — rectangle with wavy bottom edge
|
|
443
|
+
representation: {
|
|
444
|
+
outline: ["M 18 18 L 82 18 L 82 70 Q 70 88 50 76 Q 30 64 18 82 Z"]
|
|
445
|
+
},
|
|
446
|
+
// Product — rectangle with a small tab notched on the top-left
|
|
447
|
+
product: {
|
|
448
|
+
outline: ["M 18 28 L 38 28 L 42 18 L 82 18 L 82 82 L 18 82 Z"]
|
|
449
|
+
},
|
|
450
|
+
// Artifact — page with folded upper-right corner
|
|
451
|
+
artifact: {
|
|
452
|
+
outline: ["M 22 14 L 70 14 L 86 30 L 86 86 L 22 86 Z", "M 70 14 L 70 30 L 86 30"]
|
|
453
|
+
},
|
|
454
|
+
// ─── Technology ────────────────────────────────────────────────────────────
|
|
455
|
+
// Node — 3D wireframe box
|
|
456
|
+
node: {
|
|
457
|
+
outline: [
|
|
458
|
+
"M 18 38 L 70 38 L 70 86 L 18 86 Z",
|
|
459
|
+
"M 18 38 L 32 22 L 84 22 L 70 38",
|
|
460
|
+
"M 70 38 L 84 22 L 84 70 L 70 86"
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
// Device — computer monitor with stand
|
|
464
|
+
device: {
|
|
465
|
+
outline: [
|
|
466
|
+
"M 12 22 L 88 22 L 88 64 L 12 64 Z",
|
|
467
|
+
"M 28 78 L 72 78",
|
|
468
|
+
"M 50 64 L 50 78"
|
|
469
|
+
]
|
|
470
|
+
},
|
|
471
|
+
// System Software — two asymmetrically overlapping circles (different sizes,
|
|
472
|
+
// off-centre overlap — distinct from the symmetric Venn used for Collaboration)
|
|
473
|
+
systemsoftware: {
|
|
474
|
+
outline: [
|
|
475
|
+
"M 38 58 m -28 0 a 28 28 0 1 0 56 0 a 28 28 0 1 0 -56 0",
|
|
476
|
+
"M 72 32 m -16 0 a 16 16 0 1 0 32 0 a 16 16 0 1 0 -32 0"
|
|
477
|
+
]
|
|
478
|
+
},
|
|
479
|
+
// Communication Network — parallelogram with filled circle nodes at 4 corners
|
|
480
|
+
network: {
|
|
481
|
+
outline: ["M 28 22 L 88 22 L 72 78 L 12 78 Z"],
|
|
482
|
+
filled: [
|
|
483
|
+
"M 28 22 m -5 0 a 5 5 0 1 0 10 0 a 5 5 0 1 0 -10 0",
|
|
484
|
+
"M 88 22 m -5 0 a 5 5 0 1 0 10 0 a 5 5 0 1 0 -10 0",
|
|
485
|
+
"M 72 78 m -5 0 a 5 5 0 1 0 10 0 a 5 5 0 1 0 -10 0",
|
|
486
|
+
"M 12 78 m -5 0 a 5 5 0 1 0 10 0 a 5 5 0 1 0 -10 0"
|
|
487
|
+
]
|
|
488
|
+
},
|
|
489
|
+
// Path — bidirectional arrows with a centre dot
|
|
490
|
+
path: {
|
|
491
|
+
outline: [
|
|
492
|
+
"M 12 50 L 38 50",
|
|
493
|
+
"M 22 40 L 12 50 L 22 60",
|
|
494
|
+
"M 88 50 L 62 50",
|
|
495
|
+
"M 78 40 L 88 50 L 78 60"
|
|
496
|
+
],
|
|
497
|
+
filled: ["M 50 50 m -4 0 a 4 4 0 1 0 8 0 a 4 4 0 1 0 -8 0"]
|
|
498
|
+
},
|
|
499
|
+
// Equipment — two cogs (large + small) overlapping. Each cog: circle +
|
|
500
|
+
// radial teeth at cardinal & intercardinal directions + filled hub.
|
|
501
|
+
equipment: {
|
|
502
|
+
outline: [
|
|
503
|
+
// Big cog at (38, 60), body r=18, teeth out to r=22
|
|
504
|
+
"M 38 60 m -18 0 a 18 18 0 1 0 36 0 a 18 18 0 1 0 -36 0",
|
|
505
|
+
"M 38 38 L 38 42",
|
|
506
|
+
"M 38 78 L 38 82",
|
|
507
|
+
"M 16 60 L 20 60",
|
|
508
|
+
"M 56 60 L 60 60",
|
|
509
|
+
"M 23 45 L 26 48",
|
|
510
|
+
"M 50 72 L 53 75",
|
|
511
|
+
"M 23 75 L 26 72",
|
|
512
|
+
"M 50 48 L 53 45",
|
|
513
|
+
// Small cog at (74, 32), body r=10, teeth out to r=13
|
|
514
|
+
"M 74 32 m -10 0 a 10 10 0 1 0 20 0 a 10 10 0 1 0 -20 0",
|
|
515
|
+
"M 74 19 L 74 22",
|
|
516
|
+
"M 74 42 L 74 45",
|
|
517
|
+
"M 61 32 L 64 32",
|
|
518
|
+
"M 84 32 L 87 32"
|
|
519
|
+
],
|
|
520
|
+
filled: [
|
|
521
|
+
"M 38 60 m -4 0 a 4 4 0 1 0 8 0 a 4 4 0 1 0 -8 0",
|
|
522
|
+
"M 74 32 m -2 0 a 2 2 0 1 0 4 0 a 2 2 0 1 0 -4 0"
|
|
523
|
+
]
|
|
524
|
+
},
|
|
525
|
+
// Facility — factory with sawtooth roof (3 triangular peaks)
|
|
526
|
+
facility: {
|
|
527
|
+
outline: [
|
|
528
|
+
"M 14 86 L 14 38 L 38 14 L 38 38 L 62 14 L 62 38 L 86 14 L 86 38 L 86 86 Z"
|
|
529
|
+
]
|
|
530
|
+
},
|
|
531
|
+
// Distribution Network — two parallel horizontal lines with arrowheads at
|
|
532
|
+
// both ends (a "double-arrow" mathematical symbol ⇔)
|
|
533
|
+
distributionnetwork: {
|
|
534
|
+
outline: [
|
|
535
|
+
"M 24 45 L 76 45",
|
|
536
|
+
"M 24 55 L 76 55",
|
|
537
|
+
"M 24 45 L 12 50 L 24 55",
|
|
538
|
+
"M 76 45 L 88 50 L 76 55"
|
|
539
|
+
]
|
|
540
|
+
},
|
|
541
|
+
// Material — hexagon with internal horizontal lines
|
|
542
|
+
material: {
|
|
543
|
+
outline: [
|
|
544
|
+
"M 50 14 L 84 32 L 84 68 L 50 86 L 16 68 L 16 32 Z",
|
|
545
|
+
"M 24 44 L 76 44",
|
|
546
|
+
"M 24 56 L 76 56"
|
|
547
|
+
]
|
|
548
|
+
},
|
|
549
|
+
// ─── Motivation ────────────────────────────────────────────────────────────
|
|
550
|
+
// Driver — steering wheel: circle with 4-spoke crosshair + center hub
|
|
551
|
+
driver: {
|
|
552
|
+
outline: [
|
|
553
|
+
"M 50 50 m -34 0 a 34 34 0 1 0 68 0 a 34 34 0 1 0 -68 0",
|
|
554
|
+
"M 50 16 L 50 84",
|
|
555
|
+
"M 16 50 L 84 50",
|
|
556
|
+
"M 26 26 L 74 74",
|
|
557
|
+
"M 74 26 L 26 74"
|
|
558
|
+
],
|
|
559
|
+
filled: ["M 50 50 m -7 0 a 7 7 0 1 0 14 0 a 7 7 0 1 0 -14 0"]
|
|
560
|
+
},
|
|
561
|
+
// Assessment — magnifying glass (lens + handle)
|
|
562
|
+
assessment: {
|
|
563
|
+
outline: [
|
|
564
|
+
"M 38 38 m -22 0 a 22 22 0 1 0 44 0 a 22 22 0 1 0 -44 0",
|
|
565
|
+
"M 56 56 L 86 86"
|
|
566
|
+
]
|
|
567
|
+
},
|
|
568
|
+
// Goal — concentric circles (bullseye / target)
|
|
569
|
+
goal: {
|
|
570
|
+
outline: [
|
|
571
|
+
"M 50 50 m -36 0 a 36 36 0 1 0 72 0 a 36 36 0 1 0 -72 0",
|
|
572
|
+
"M 50 50 m -22 0 a 22 22 0 1 0 44 0 a 22 22 0 1 0 -44 0"
|
|
573
|
+
],
|
|
574
|
+
filled: ["M 50 50 m -8 0 a 8 8 0 1 0 16 0 a 8 8 0 1 0 -16 0"]
|
|
575
|
+
},
|
|
576
|
+
// Outcome — bullseye with arrow stuck through it
|
|
577
|
+
outcome: {
|
|
578
|
+
outline: [
|
|
579
|
+
"M 50 50 m -28 0 a 28 28 0 1 0 56 0 a 28 28 0 1 0 -56 0",
|
|
580
|
+
"M 50 50 m -10 0 a 10 10 0 1 0 20 0 a 10 10 0 1 0 -20 0",
|
|
581
|
+
"M 78 22 L 50 50",
|
|
582
|
+
"M 66 22 L 78 22 L 78 34"
|
|
583
|
+
]
|
|
584
|
+
},
|
|
585
|
+
// Principle — tall rectangle with exclamation mark inside (a "tablet")
|
|
586
|
+
principle: {
|
|
587
|
+
outline: ["M 35 14 L 65 14 L 65 86 L 35 86 Z", "M 50 28 L 50 60"],
|
|
588
|
+
filled: ["M 50 70 m -3 0 a 3 3 0 1 0 6 0 a 3 3 0 1 0 -6 0"]
|
|
589
|
+
},
|
|
590
|
+
// Requirement — parallelogram
|
|
591
|
+
requirement: {
|
|
592
|
+
outline: ["M 30 22 L 88 22 L 70 78 L 12 78 Z"]
|
|
593
|
+
},
|
|
594
|
+
// Constraint — parallelogram with horizontal line through middle
|
|
595
|
+
constraint: {
|
|
596
|
+
outline: ["M 30 22 L 88 22 L 70 78 L 12 78 Z", "M 21 50 L 79 50"]
|
|
597
|
+
},
|
|
598
|
+
// Meaning — speech / cloud bubble with tail
|
|
599
|
+
meaning: {
|
|
600
|
+
outline: [
|
|
601
|
+
"M 30 28 Q 18 28 18 42 Q 18 56 30 58 L 25 78 L 42 60 Q 70 64 78 50 Q 86 32 64 26 Q 50 14 30 28 Z"
|
|
602
|
+
]
|
|
603
|
+
},
|
|
604
|
+
// Value — horizontal ellipse
|
|
605
|
+
value: {
|
|
606
|
+
outline: ["M 50 50 m -32 0 a 32 18 0 1 0 64 0 a 32 18 0 1 0 -64 0"]
|
|
607
|
+
},
|
|
608
|
+
// Stakeholder — pill with filled dot (same family as Role)
|
|
609
|
+
stakeholder: {
|
|
610
|
+
outline: ["M 22 32 L 78 32 a 18 18 0 0 1 0 36 L 22 68 a 18 18 0 0 1 0 -36 Z"],
|
|
611
|
+
filled: ["M 70 50 m -7 0 a 7 7 0 1 0 14 0 a 7 7 0 1 0 -14 0"]
|
|
612
|
+
},
|
|
613
|
+
// ─── Strategy ──────────────────────────────────────────────────────────────
|
|
614
|
+
// Resource — rectangle with three vertical bars (stack of cards)
|
|
615
|
+
resource: {
|
|
616
|
+
outline: [
|
|
617
|
+
"M 18 30 L 82 30 L 82 70 L 18 70 Z",
|
|
618
|
+
"M 36 38 L 36 62",
|
|
619
|
+
"M 50 38 L 50 62",
|
|
620
|
+
"M 64 38 L 64 62"
|
|
621
|
+
]
|
|
622
|
+
},
|
|
623
|
+
// Capability — stepped staircase (three blocks ascending)
|
|
624
|
+
capability: {
|
|
625
|
+
outline: [
|
|
626
|
+
"M 14 78 L 38 78 L 38 60 L 62 60 L 62 42 L 86 42 L 86 24 L 70 24",
|
|
627
|
+
"M 70 24 L 70 42 L 46 42 L 46 60 L 22 60 L 22 78"
|
|
628
|
+
]
|
|
629
|
+
},
|
|
630
|
+
// Course of Action — right-pointing arrow (similar to process)
|
|
631
|
+
courseofaction: {
|
|
632
|
+
outline: ["M 12 35 L 56 35 L 56 22 L 88 50 L 56 78 L 56 65 L 12 65 Z"]
|
|
633
|
+
},
|
|
634
|
+
// Value Stream — chevron with concave back
|
|
635
|
+
valuestream: {
|
|
636
|
+
outline: ["M 12 28 L 65 28 L 88 50 L 65 72 L 12 72 L 32 50 Z"]
|
|
637
|
+
},
|
|
638
|
+
// ─── Implementation & Migration ────────────────────────────────────────────
|
|
639
|
+
// Work Package — small rectangle (placeholder; Archi uses a banded shape)
|
|
640
|
+
workpackage: {
|
|
641
|
+
outline: ["M 18 30 L 82 30 L 82 70 L 18 70 Z", "M 18 50 L 82 50"]
|
|
642
|
+
},
|
|
643
|
+
// Deliverable — rectangle with curled / wavy bottom edge
|
|
644
|
+
deliverable: {
|
|
645
|
+
outline: ["M 18 18 L 82 18 L 82 75 Q 70 88 50 78 Q 30 68 18 82 Z"]
|
|
646
|
+
},
|
|
647
|
+
// Plateau — stack of three horizontal bars
|
|
648
|
+
plateau: {
|
|
649
|
+
outline: [
|
|
650
|
+
"M 18 22 L 82 22 L 82 32 L 18 32 Z",
|
|
651
|
+
"M 18 44 L 82 44 L 82 54 L 18 54 Z",
|
|
652
|
+
"M 18 66 L 82 66 L 82 76 L 18 76 Z"
|
|
653
|
+
]
|
|
654
|
+
},
|
|
655
|
+
// Gap — circle with a horizontal line through the middle
|
|
656
|
+
gap: {
|
|
657
|
+
outline: [
|
|
658
|
+
"M 50 50 m -32 0 a 32 32 0 1 0 64 0 a 32 32 0 1 0 -64 0",
|
|
659
|
+
"M 14 50 L 86 50"
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
function iconNameForType(elementType) {
|
|
664
|
+
if (!elementType) return null;
|
|
665
|
+
const t = elementType;
|
|
666
|
+
if (t === "BusinessActor") return "actor";
|
|
667
|
+
if (t === "BusinessRole") return "role";
|
|
668
|
+
if (t === "BusinessCollaboration" || t === "ApplicationCollaboration" || t === "TechnologyCollaboration")
|
|
669
|
+
return "collaboration";
|
|
670
|
+
if (t.endsWith("Interface")) return "interface";
|
|
671
|
+
if (t === "ApplicationComponent") return "component";
|
|
672
|
+
if (t === "BusinessProcess" || t === "ApplicationProcess" || t === "TechnologyProcess")
|
|
673
|
+
return "process";
|
|
674
|
+
if (t === "BusinessFunction" || t === "ApplicationFunction" || t === "TechnologyFunction")
|
|
675
|
+
return "function";
|
|
676
|
+
if (t.endsWith("Interaction")) return "interaction";
|
|
677
|
+
if (t.endsWith("Service")) return "service";
|
|
678
|
+
if (t.endsWith("Event")) return "event";
|
|
679
|
+
if (t === "BusinessObject" || t === "DataObject") return "object";
|
|
680
|
+
if (t === "Contract") return "contract";
|
|
681
|
+
if (t === "Representation") return "representation";
|
|
682
|
+
if (t === "Product") return "product";
|
|
683
|
+
if (t === "Artifact") return "artifact";
|
|
684
|
+
if (t === "Node") return "node";
|
|
685
|
+
if (t === "SystemSoftware") return "systemsoftware";
|
|
686
|
+
if (t === "Device") return "device";
|
|
687
|
+
if (t === "Equipment") return "equipment";
|
|
688
|
+
if (t === "Facility") return "facility";
|
|
689
|
+
if (t === "DistributionNetwork") return "distributionnetwork";
|
|
690
|
+
if (t === "CommunicationNetwork") return "network";
|
|
691
|
+
if (t === "Path") return "path";
|
|
692
|
+
if (t === "Material") return "material";
|
|
693
|
+
if (t === "Stakeholder") return "stakeholder";
|
|
694
|
+
if (t === "Driver") return "driver";
|
|
695
|
+
if (t === "Assessment") return "assessment";
|
|
696
|
+
if (t === "Goal") return "goal";
|
|
697
|
+
if (t === "Outcome") return "outcome";
|
|
698
|
+
if (t === "Principle") return "principle";
|
|
699
|
+
if (t === "Requirement") return "requirement";
|
|
700
|
+
if (t === "Constraint") return "constraint";
|
|
701
|
+
if (t === "Meaning") return "meaning";
|
|
702
|
+
if (t === "Value") return "value";
|
|
703
|
+
if (t === "Capability") return "capability";
|
|
704
|
+
if (t === "Resource") return "resource";
|
|
705
|
+
if (t === "CourseOfAction") return "courseofaction";
|
|
706
|
+
if (t === "ValueStream") return "valuestream";
|
|
707
|
+
if (t === "WorkPackage") return "workpackage";
|
|
708
|
+
if (t === "Deliverable") return "deliverable";
|
|
709
|
+
if (t === "Plateau") return "plateau";
|
|
710
|
+
if (t === "Gap") return "gap";
|
|
711
|
+
if (t === "ImplementationEvent") return "event";
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/renderer.tsx
|
|
716
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
717
|
+
var MIN_SCALE = 0.05;
|
|
718
|
+
var MAX_SCALE = 50;
|
|
719
|
+
var PAN_FRACTION = 0.2;
|
|
720
|
+
var WHEEL_FACTOR = 1.1;
|
|
721
|
+
var DASH_PATTERN = "6 4";
|
|
722
|
+
var FLOW_PATTERN = "4 3";
|
|
723
|
+
var DOT_PATTERN = "2 3";
|
|
724
|
+
var ArchimateRenderer = forwardRef(
|
|
725
|
+
function ArchimateRenderer2({ view, className, style }, ref) {
|
|
726
|
+
const svgRef = useRef(null);
|
|
727
|
+
const gRef = useRef(null);
|
|
728
|
+
const stateRef = useRef({ tx: 0, ty: 0, scale: 1 });
|
|
729
|
+
const dragRef = useRef(null);
|
|
730
|
+
const nodeMap = useMemo(() => {
|
|
731
|
+
const map = /* @__PURE__ */ new Map();
|
|
732
|
+
for (const n of view.nodes) map.set(n.id, n);
|
|
733
|
+
return map;
|
|
734
|
+
}, [view]);
|
|
735
|
+
const endpointPlan = useMemo(
|
|
736
|
+
() => planEndpoints(view.connections, nodeMap),
|
|
737
|
+
[view, nodeMap]
|
|
738
|
+
);
|
|
739
|
+
function applyTransform() {
|
|
740
|
+
const { tx, ty, scale } = stateRef.current;
|
|
741
|
+
gRef.current?.setAttribute("transform", `translate(${tx} ${ty}) scale(${scale})`);
|
|
742
|
+
}
|
|
743
|
+
function reset() {
|
|
744
|
+
stateRef.current = { tx: 0, ty: 0, scale: 1 };
|
|
745
|
+
applyTransform();
|
|
746
|
+
}
|
|
747
|
+
function zoomAroundViewBox(vbX, vbY, factor) {
|
|
748
|
+
const { tx, ty, scale } = stateRef.current;
|
|
749
|
+
const nextScale = scale * factor;
|
|
750
|
+
if (nextScale < MIN_SCALE || nextScale > MAX_SCALE) return;
|
|
751
|
+
stateRef.current = {
|
|
752
|
+
tx: vbX * (1 - factor) + tx * factor,
|
|
753
|
+
ty: vbY * (1 - factor) + ty * factor,
|
|
754
|
+
scale: nextScale
|
|
755
|
+
};
|
|
756
|
+
applyTransform();
|
|
757
|
+
}
|
|
758
|
+
function clientToViewBox(clientX, clientY) {
|
|
759
|
+
const svg = svgRef.current;
|
|
760
|
+
if (!svg) return null;
|
|
761
|
+
const pt = svg.createSVGPoint();
|
|
762
|
+
pt.x = clientX;
|
|
763
|
+
pt.y = clientY;
|
|
764
|
+
const ctm = svg.getScreenCTM();
|
|
765
|
+
if (!ctm) return null;
|
|
766
|
+
const local = pt.matrixTransform(ctm.inverse());
|
|
767
|
+
return { x: local.x, y: local.y };
|
|
768
|
+
}
|
|
769
|
+
useImperativeHandle(
|
|
770
|
+
ref,
|
|
771
|
+
() => ({
|
|
772
|
+
zoomIn: () => {
|
|
773
|
+
const cx = view.viewBox.x + view.viewBox.width / 2;
|
|
774
|
+
const cy = view.viewBox.y + view.viewBox.height / 2;
|
|
775
|
+
zoomAroundViewBox(cx, cy, 1.25);
|
|
776
|
+
},
|
|
777
|
+
zoomOut: () => {
|
|
778
|
+
const cx = view.viewBox.x + view.viewBox.width / 2;
|
|
779
|
+
const cy = view.viewBox.y + view.viewBox.height / 2;
|
|
780
|
+
zoomAroundViewBox(cx, cy, 1 / 1.25);
|
|
781
|
+
},
|
|
782
|
+
fitView: reset,
|
|
783
|
+
pan: (dirX, dirY) => {
|
|
784
|
+
stateRef.current.tx += -dirX * view.viewBox.width * PAN_FRACTION;
|
|
785
|
+
stateRef.current.ty += -dirY * view.viewBox.height * PAN_FRACTION;
|
|
786
|
+
applyTransform();
|
|
787
|
+
}
|
|
788
|
+
}),
|
|
789
|
+
[view]
|
|
790
|
+
);
|
|
791
|
+
useEffect(() => {
|
|
792
|
+
reset();
|
|
793
|
+
}, [view]);
|
|
794
|
+
useEffect(() => {
|
|
795
|
+
const svg = svgRef.current;
|
|
796
|
+
if (!svg) return;
|
|
797
|
+
const onWheel = (e) => {
|
|
798
|
+
e.preventDefault();
|
|
799
|
+
const local = clientToViewBox(e.clientX, e.clientY);
|
|
800
|
+
if (!local) return;
|
|
801
|
+
const factor = e.deltaY < 0 ? WHEEL_FACTOR : 1 / WHEEL_FACTOR;
|
|
802
|
+
zoomAroundViewBox(local.x, local.y, factor);
|
|
803
|
+
};
|
|
804
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
805
|
+
return () => svg.removeEventListener("wheel", onWheel);
|
|
806
|
+
}, []);
|
|
807
|
+
function onPointerDown(e) {
|
|
808
|
+
if (e.button !== 0) return;
|
|
809
|
+
const svg = svgRef.current;
|
|
810
|
+
if (!svg) return;
|
|
811
|
+
svg.setPointerCapture(e.pointerId);
|
|
812
|
+
dragRef.current = {
|
|
813
|
+
pointerId: e.pointerId,
|
|
814
|
+
startClientX: e.clientX,
|
|
815
|
+
startClientY: e.clientY,
|
|
816
|
+
startTx: stateRef.current.tx,
|
|
817
|
+
startTy: stateRef.current.ty
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function onPointerMove(e) {
|
|
821
|
+
const drag = dragRef.current;
|
|
822
|
+
if (!drag || drag.pointerId !== e.pointerId) return;
|
|
823
|
+
const start = clientToViewBox(drag.startClientX, drag.startClientY);
|
|
824
|
+
const now = clientToViewBox(e.clientX, e.clientY);
|
|
825
|
+
if (!start || !now) return;
|
|
826
|
+
stateRef.current.tx = drag.startTx + (now.x - start.x);
|
|
827
|
+
stateRef.current.ty = drag.startTy + (now.y - start.y);
|
|
828
|
+
applyTransform();
|
|
829
|
+
}
|
|
830
|
+
function onPointerUp(e) {
|
|
831
|
+
const drag = dragRef.current;
|
|
832
|
+
if (!drag) return;
|
|
833
|
+
svgRef.current?.releasePointerCapture(e.pointerId);
|
|
834
|
+
dragRef.current = null;
|
|
835
|
+
}
|
|
836
|
+
return /* @__PURE__ */ jsx(
|
|
837
|
+
"svg",
|
|
838
|
+
{
|
|
839
|
+
ref: svgRef,
|
|
840
|
+
className,
|
|
841
|
+
style: { touchAction: "none", cursor: "grab", ...style },
|
|
842
|
+
viewBox: `${view.viewBox.x} ${view.viewBox.y} ${view.viewBox.width} ${view.viewBox.height}`,
|
|
843
|
+
preserveAspectRatio: "xMidYMid meet",
|
|
844
|
+
onPointerDown,
|
|
845
|
+
onPointerMove,
|
|
846
|
+
onPointerUp,
|
|
847
|
+
onPointerCancel: onPointerUp,
|
|
848
|
+
children: /* @__PURE__ */ jsxs("g", { ref: gRef, children: [
|
|
849
|
+
view.nodes.map((n) => /* @__PURE__ */ jsx(NodeShape, { node: n }, n.id)),
|
|
850
|
+
view.connections.map((c) => {
|
|
851
|
+
const pts = endpointPlan.get(c.id);
|
|
852
|
+
if (!pts) return null;
|
|
853
|
+
return /* @__PURE__ */ jsx(
|
|
854
|
+
ConnectionPath,
|
|
855
|
+
{
|
|
856
|
+
edge: c,
|
|
857
|
+
sourcePoint: pts.source,
|
|
858
|
+
targetPoint: pts.target
|
|
859
|
+
},
|
|
860
|
+
c.id
|
|
861
|
+
);
|
|
862
|
+
})
|
|
863
|
+
] })
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
var MARKER_GLYPHS = {
|
|
869
|
+
"archi-arrow-filled": {
|
|
870
|
+
refX: 11,
|
|
871
|
+
refY: 5,
|
|
872
|
+
reverseAtStart: true,
|
|
873
|
+
paint: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 12 5 L 0 10 Z", fill: "currentColor" })
|
|
874
|
+
},
|
|
875
|
+
"archi-arrow-open": {
|
|
876
|
+
refX: 11,
|
|
877
|
+
refY: 5,
|
|
878
|
+
reverseAtStart: true,
|
|
879
|
+
paint: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 12 5 L 0 10", fill: "none", stroke: "currentColor", strokeWidth: "1.2" })
|
|
880
|
+
},
|
|
881
|
+
"archi-triangle-hollow": {
|
|
882
|
+
refX: 11,
|
|
883
|
+
refY: 6,
|
|
884
|
+
reverseAtStart: true,
|
|
885
|
+
paint: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 12 6 L 0 12 Z", fill: "white", stroke: "currentColor", strokeWidth: "1" })
|
|
886
|
+
},
|
|
887
|
+
"archi-diamond-filled": {
|
|
888
|
+
refX: 0,
|
|
889
|
+
refY: 5,
|
|
890
|
+
reverseAtStart: false,
|
|
891
|
+
paint: /* @__PURE__ */ jsx(
|
|
892
|
+
"path",
|
|
893
|
+
{
|
|
894
|
+
d: "M 0 5 L 7 0 L 14 5 L 7 10 Z",
|
|
895
|
+
fill: "#1A1A1A",
|
|
896
|
+
stroke: "#1A1A1A",
|
|
897
|
+
strokeWidth: "1"
|
|
898
|
+
}
|
|
899
|
+
)
|
|
900
|
+
},
|
|
901
|
+
"archi-diamond-hollow": {
|
|
902
|
+
refX: 0,
|
|
903
|
+
refY: 5,
|
|
904
|
+
reverseAtStart: false,
|
|
905
|
+
paint: /* @__PURE__ */ jsx(
|
|
906
|
+
"path",
|
|
907
|
+
{
|
|
908
|
+
d: "M 0 5 L 7 0 L 14 5 L 7 10 Z",
|
|
909
|
+
fill: "white",
|
|
910
|
+
stroke: "#1A1A1A",
|
|
911
|
+
strokeWidth: "1"
|
|
912
|
+
}
|
|
913
|
+
)
|
|
914
|
+
},
|
|
915
|
+
"archi-arrow-thin": {
|
|
916
|
+
refX: 9,
|
|
917
|
+
refY: 4,
|
|
918
|
+
reverseAtStart: true,
|
|
919
|
+
paint: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 4 L 0 8", fill: "none", stroke: "currentColor", strokeWidth: "1" })
|
|
920
|
+
},
|
|
921
|
+
"archi-dot-filled": {
|
|
922
|
+
refX: 1,
|
|
923
|
+
refY: 4,
|
|
924
|
+
reverseAtStart: false,
|
|
925
|
+
paint: /* @__PURE__ */ jsx("circle", { cx: "4", cy: "4", r: "3", fill: "currentColor" })
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
function MarkerInline({
|
|
929
|
+
glyph,
|
|
930
|
+
position,
|
|
931
|
+
pathAngleRad,
|
|
932
|
+
isStart
|
|
933
|
+
}) {
|
|
934
|
+
const spec = MARKER_GLYPHS[glyph];
|
|
935
|
+
if (!spec) return null;
|
|
936
|
+
const angle = isStart && spec.reverseAtStart ? pathAngleRad + Math.PI : pathAngleRad;
|
|
937
|
+
const angleDeg = angle * 180 / Math.PI;
|
|
938
|
+
return /* @__PURE__ */ jsx(
|
|
939
|
+
"g",
|
|
940
|
+
{
|
|
941
|
+
transform: `translate(${position.x} ${position.y}) rotate(${angleDeg}) translate(${-spec.refX} ${-spec.refY})`,
|
|
942
|
+
children: spec.paint
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
function styleForEdge(edge) {
|
|
947
|
+
switch (edge.relationshipType) {
|
|
948
|
+
case "Composition":
|
|
949
|
+
return { markerStart: "archi-diamond-filled" };
|
|
950
|
+
case "Aggregation":
|
|
951
|
+
return { markerStart: "archi-diamond-hollow" };
|
|
952
|
+
case "Assignment":
|
|
953
|
+
return { markerStart: "archi-dot-filled", markerEnd: "archi-arrow-filled" };
|
|
954
|
+
case "Realization":
|
|
955
|
+
return { dashArray: DASH_PATTERN, markerEnd: "archi-triangle-hollow" };
|
|
956
|
+
case "Serving":
|
|
957
|
+
case "UsedBy":
|
|
958
|
+
return { markerEnd: "archi-arrow-open" };
|
|
959
|
+
case "Triggering":
|
|
960
|
+
return { markerEnd: "archi-arrow-filled" };
|
|
961
|
+
case "Flow":
|
|
962
|
+
return { dashArray: FLOW_PATTERN, markerEnd: "archi-arrow-filled" };
|
|
963
|
+
case "Specialization":
|
|
964
|
+
return { markerEnd: "archi-triangle-hollow" };
|
|
965
|
+
case "Access": {
|
|
966
|
+
const at = edge.accessType ?? "Access";
|
|
967
|
+
if (at === "Read") return { dashArray: DOT_PATTERN, markerStart: "archi-arrow-thin" };
|
|
968
|
+
if (at === "Write") return { dashArray: DOT_PATTERN, markerEnd: "archi-arrow-thin" };
|
|
969
|
+
if (at === "ReadWrite")
|
|
970
|
+
return {
|
|
971
|
+
dashArray: DOT_PATTERN,
|
|
972
|
+
markerStart: "archi-arrow-thin",
|
|
973
|
+
markerEnd: "archi-arrow-thin"
|
|
974
|
+
};
|
|
975
|
+
return { dashArray: DOT_PATTERN };
|
|
976
|
+
}
|
|
977
|
+
case "Influence":
|
|
978
|
+
return { dashArray: DASH_PATTERN, markerEnd: "archi-arrow-open" };
|
|
979
|
+
case "Association":
|
|
980
|
+
return edge.isDirected ? { markerEnd: "archi-arrow-open" } : {};
|
|
981
|
+
default:
|
|
982
|
+
return { markerEnd: "archi-arrow-filled" };
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function ConnectionPath({
|
|
986
|
+
edge,
|
|
987
|
+
sourcePoint,
|
|
988
|
+
targetPoint
|
|
989
|
+
}) {
|
|
990
|
+
const allPoints = [sourcePoint, ...edge.bendpoints, targetPoint];
|
|
991
|
+
const pointsStr = allPoints.map((p) => `${p.x},${p.y}`).join(" ");
|
|
992
|
+
const style = styleForEdge(edge);
|
|
993
|
+
const stroke = edge.stroke ?? DEFAULT_STROKE;
|
|
994
|
+
const startNeighbor = allPoints[1] ?? sourcePoint;
|
|
995
|
+
const endNeighbor = allPoints[allPoints.length - 2] ?? targetPoint;
|
|
996
|
+
const startAngle = Math.atan2(
|
|
997
|
+
startNeighbor.y - sourcePoint.y,
|
|
998
|
+
startNeighbor.x - sourcePoint.x
|
|
999
|
+
);
|
|
1000
|
+
const endAngle = Math.atan2(
|
|
1001
|
+
targetPoint.y - endNeighbor.y,
|
|
1002
|
+
targetPoint.x - endNeighbor.x
|
|
1003
|
+
);
|
|
1004
|
+
return /* @__PURE__ */ jsxs("g", { style: { color: stroke }, children: [
|
|
1005
|
+
/* @__PURE__ */ jsx(
|
|
1006
|
+
"polyline",
|
|
1007
|
+
{
|
|
1008
|
+
points: pointsStr,
|
|
1009
|
+
fill: "none",
|
|
1010
|
+
stroke,
|
|
1011
|
+
strokeWidth: 1,
|
|
1012
|
+
strokeDasharray: style.dashArray
|
|
1013
|
+
}
|
|
1014
|
+
),
|
|
1015
|
+
style.markerStart ? /* @__PURE__ */ jsx(
|
|
1016
|
+
MarkerInline,
|
|
1017
|
+
{
|
|
1018
|
+
glyph: style.markerStart,
|
|
1019
|
+
position: sourcePoint,
|
|
1020
|
+
pathAngleRad: startAngle,
|
|
1021
|
+
isStart: true
|
|
1022
|
+
}
|
|
1023
|
+
) : null,
|
|
1024
|
+
style.markerEnd ? /* @__PURE__ */ jsx(
|
|
1025
|
+
MarkerInline,
|
|
1026
|
+
{
|
|
1027
|
+
glyph: style.markerEnd,
|
|
1028
|
+
position: targetPoint,
|
|
1029
|
+
pathAngleRad: endAngle,
|
|
1030
|
+
isStart: false
|
|
1031
|
+
}
|
|
1032
|
+
) : null
|
|
1033
|
+
] });
|
|
1034
|
+
}
|
|
1035
|
+
function pickSide(box, towards) {
|
|
1036
|
+
const cx = box.x + box.w / 2;
|
|
1037
|
+
const cy = box.y + box.h / 2;
|
|
1038
|
+
const dx = towards.x - cx;
|
|
1039
|
+
const dy = towards.y - cy;
|
|
1040
|
+
const ax = Math.abs(dx) / Math.max(box.w / 2, 1);
|
|
1041
|
+
const ay = Math.abs(dy) / Math.max(box.h / 2, 1);
|
|
1042
|
+
if (ax >= ay) return dx >= 0 ? "right" : "left";
|
|
1043
|
+
return dy >= 0 ? "bottom" : "top";
|
|
1044
|
+
}
|
|
1045
|
+
function pointOnSide(box, side, t) {
|
|
1046
|
+
switch (side) {
|
|
1047
|
+
case "left":
|
|
1048
|
+
return { x: box.x, y: box.y + box.h * t };
|
|
1049
|
+
case "right":
|
|
1050
|
+
return { x: box.x + box.w, y: box.y + box.h * t };
|
|
1051
|
+
case "top":
|
|
1052
|
+
return { x: box.x + box.w * t, y: box.y };
|
|
1053
|
+
case "bottom":
|
|
1054
|
+
return { x: box.x + box.w * t, y: box.y + box.h };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function planEndpoints(connections, nodeMap) {
|
|
1058
|
+
const result = /* @__PURE__ */ new Map();
|
|
1059
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1060
|
+
const addSlot = (nodeId, side, slot) => {
|
|
1061
|
+
let byNode = groups.get(nodeId);
|
|
1062
|
+
if (!byNode) groups.set(nodeId, byNode = /* @__PURE__ */ new Map());
|
|
1063
|
+
let list = byNode.get(side);
|
|
1064
|
+
if (!list) byNode.set(side, list = []);
|
|
1065
|
+
list.push(slot);
|
|
1066
|
+
};
|
|
1067
|
+
for (const edge of connections) {
|
|
1068
|
+
const s = nodeMap.get(edge.sourceId);
|
|
1069
|
+
const t = nodeMap.get(edge.targetId);
|
|
1070
|
+
if (!s || !t) continue;
|
|
1071
|
+
const initial = endpointsForConnection(s, t, edge.bendpoints);
|
|
1072
|
+
result.set(edge.id, initial);
|
|
1073
|
+
if (edge.bendpoints.length > 0) continue;
|
|
1074
|
+
if (s.isJunction || t.isJunction) continue;
|
|
1075
|
+
const tCenter = nodeCenter(t);
|
|
1076
|
+
const sCenter = nodeCenter(s);
|
|
1077
|
+
const sSide = pickSide(s, tCenter);
|
|
1078
|
+
const tSide = pickSide(t, sCenter);
|
|
1079
|
+
const sGuide = sSide === "left" || sSide === "right" ? tCenter.y : tCenter.x;
|
|
1080
|
+
const tGuide = tSide === "left" || tSide === "right" ? sCenter.y : sCenter.x;
|
|
1081
|
+
addSlot(s.id, sSide, { edgeId: edge.id, isSourceEnd: true, guideCoord: sGuide });
|
|
1082
|
+
addSlot(t.id, tSide, { edgeId: edge.id, isSourceEnd: false, guideCoord: tGuide });
|
|
1083
|
+
}
|
|
1084
|
+
for (const [nodeId, byNode] of groups) {
|
|
1085
|
+
const node = nodeMap.get(nodeId);
|
|
1086
|
+
for (const [side, slots] of byNode) {
|
|
1087
|
+
if (slots.length < 2) continue;
|
|
1088
|
+
slots.sort((a, b) => a.guideCoord - b.guideCoord);
|
|
1089
|
+
for (let i = 0; i < slots.length; i++) {
|
|
1090
|
+
const point = pointOnSide(node, side, (i + 1) / (slots.length + 1));
|
|
1091
|
+
const cur = result.get(slots[i].edgeId);
|
|
1092
|
+
result.set(
|
|
1093
|
+
slots[i].edgeId,
|
|
1094
|
+
slots[i].isSourceEnd ? { source: point, target: cur.target } : { source: cur.source, target: point }
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return result;
|
|
1100
|
+
}
|
|
1101
|
+
function endpointsForConnection(source, target, bendpoints) {
|
|
1102
|
+
if (source.isJunction && target.isJunction) {
|
|
1103
|
+
return { source: nodeCenter(source), target: nodeCenter(target) };
|
|
1104
|
+
}
|
|
1105
|
+
if (bendpoints.length > 0) {
|
|
1106
|
+
return {
|
|
1107
|
+
source: source.isJunction ? nodeCenter(source) : attachPoint(source, bendpoints[0]),
|
|
1108
|
+
target: target.isJunction ? nodeCenter(target) : attachPoint(target, bendpoints[bendpoints.length - 1])
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const sLeft = source.x;
|
|
1112
|
+
const sRight = source.x + source.w;
|
|
1113
|
+
const sTop = source.y;
|
|
1114
|
+
const sBottom = source.y + source.h;
|
|
1115
|
+
const tLeft = target.x;
|
|
1116
|
+
const tRight = target.x + target.w;
|
|
1117
|
+
const tTop = target.y;
|
|
1118
|
+
const tBottom = target.y + target.h;
|
|
1119
|
+
const yOverlap = Math.min(sBottom, tBottom) - Math.max(sTop, tTop);
|
|
1120
|
+
const xOverlap = Math.min(sRight, tRight) - Math.max(sLeft, tLeft);
|
|
1121
|
+
if (xOverlap < 0 && yOverlap > 0) {
|
|
1122
|
+
const y = (Math.max(sTop, tTop) + Math.min(sBottom, tBottom)) / 2;
|
|
1123
|
+
if (sRight <= tLeft) {
|
|
1124
|
+
return {
|
|
1125
|
+
source: source.isJunction ? nodeCenter(source) : { x: sRight, y },
|
|
1126
|
+
target: target.isJunction ? nodeCenter(target) : { x: tLeft, y }
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
source: source.isJunction ? nodeCenter(source) : { x: sLeft, y },
|
|
1131
|
+
target: target.isJunction ? nodeCenter(target) : { x: tRight, y }
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
if (yOverlap < 0 && xOverlap > 0) {
|
|
1135
|
+
const x = (Math.max(sLeft, tLeft) + Math.min(sRight, tRight)) / 2;
|
|
1136
|
+
if (sBottom <= tTop) {
|
|
1137
|
+
return {
|
|
1138
|
+
source: source.isJunction ? nodeCenter(source) : { x, y: sBottom },
|
|
1139
|
+
target: target.isJunction ? nodeCenter(target) : { x, y: tTop }
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
source: source.isJunction ? nodeCenter(source) : { x, y: sTop },
|
|
1144
|
+
target: target.isJunction ? nodeCenter(target) : { x, y: tBottom }
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
source: source.isJunction ? nodeCenter(source) : attachPoint(source, nodeCenter(target)),
|
|
1149
|
+
target: target.isJunction ? nodeCenter(target) : attachPoint(target, nodeCenter(source))
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function nodeCenter(n) {
|
|
1153
|
+
return { x: n.x + n.w / 2, y: n.y + n.h / 2 };
|
|
1154
|
+
}
|
|
1155
|
+
function attachPoint(box, towards) {
|
|
1156
|
+
const left = box.x;
|
|
1157
|
+
const right = box.x + box.w;
|
|
1158
|
+
const top = box.y;
|
|
1159
|
+
const bottom = box.y + box.h;
|
|
1160
|
+
const xOut = towards.x < left ? -1 : towards.x > right ? 1 : 0;
|
|
1161
|
+
const yOut = towards.y < top ? -1 : towards.y > bottom ? 1 : 0;
|
|
1162
|
+
if (xOut !== 0 && yOut === 0) {
|
|
1163
|
+
return { x: xOut < 0 ? left : right, y: clamp(towards.y, top, bottom) };
|
|
1164
|
+
}
|
|
1165
|
+
if (xOut === 0 && yOut !== 0) {
|
|
1166
|
+
return { x: clamp(towards.x, left, right), y: yOut < 0 ? top : bottom };
|
|
1167
|
+
}
|
|
1168
|
+
if (xOut !== 0 && yOut !== 0) {
|
|
1169
|
+
const cx = (left + right) / 2;
|
|
1170
|
+
const cy = (top + bottom) / 2;
|
|
1171
|
+
const dx = towards.x - cx;
|
|
1172
|
+
const dy = towards.y - cy;
|
|
1173
|
+
const t = Math.min(box.w / 2 / Math.abs(dx), box.h / 2 / Math.abs(dy));
|
|
1174
|
+
return { x: cx + dx * t, y: cy + dy * t };
|
|
1175
|
+
}
|
|
1176
|
+
return nodeCenter(box);
|
|
1177
|
+
}
|
|
1178
|
+
function clamp(v, lo, hi) {
|
|
1179
|
+
return Math.max(lo, Math.min(hi, v));
|
|
1180
|
+
}
|
|
1181
|
+
function NodeShape({ node }) {
|
|
1182
|
+
if (node.isJunction) return /* @__PURE__ */ jsx(JunctionShape, { node });
|
|
1183
|
+
if (node.nodeType === "Label") return /* @__PURE__ */ jsx(LabelShape, { node });
|
|
1184
|
+
const isContainer = node.nodeType === "Container" || node.nodeType === "Group" || !node.elementId && !node.nodeType;
|
|
1185
|
+
return /* @__PURE__ */ jsx(ElementShape, { node, dashed: isContainer, transparent: isContainer });
|
|
1186
|
+
}
|
|
1187
|
+
function JunctionShape({ node }) {
|
|
1188
|
+
const cx = node.x + node.w / 2;
|
|
1189
|
+
const cy = node.y + node.h / 2;
|
|
1190
|
+
const r = Math.max(4, Math.min(node.w, node.h) / 2);
|
|
1191
|
+
return /* @__PURE__ */ jsx("circle", { cx, cy, r, fill: node.stroke });
|
|
1192
|
+
}
|
|
1193
|
+
function LabelShape({ node }) {
|
|
1194
|
+
if (!node.label) return null;
|
|
1195
|
+
const fontSize = node.fontSize ?? 11;
|
|
1196
|
+
const textX = node.x + node.w / 2;
|
|
1197
|
+
const lines = wrapLabel(node.label, node.w - 8, fontSize);
|
|
1198
|
+
const lineHeight = fontSize * 1.2;
|
|
1199
|
+
const blockHeight = lineHeight * (lines.length - 1);
|
|
1200
|
+
const firstY = node.y + node.h / 2 + fontSize / 3 - blockHeight / 2;
|
|
1201
|
+
return /* @__PURE__ */ jsx(
|
|
1202
|
+
"text",
|
|
1203
|
+
{
|
|
1204
|
+
x: textX,
|
|
1205
|
+
y: firstY,
|
|
1206
|
+
textAnchor: "middle",
|
|
1207
|
+
fontFamily: node.fontFamily ?? "system-ui, -apple-system, sans-serif",
|
|
1208
|
+
fontSize,
|
|
1209
|
+
fill: node.textColor,
|
|
1210
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsx("tspan", { x: textX, dy: i === 0 ? 0 : lineHeight, children: line }, i))
|
|
1211
|
+
}
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
var MOTIVATION_TYPES = /* @__PURE__ */ new Set([
|
|
1215
|
+
"Stakeholder",
|
|
1216
|
+
"Driver",
|
|
1217
|
+
"Assessment",
|
|
1218
|
+
"Goal",
|
|
1219
|
+
"Outcome",
|
|
1220
|
+
"Principle",
|
|
1221
|
+
"Requirement",
|
|
1222
|
+
"Constraint",
|
|
1223
|
+
"Meaning",
|
|
1224
|
+
"Value"
|
|
1225
|
+
]);
|
|
1226
|
+
var STRATEGY_TYPES = /* @__PURE__ */ new Set(["Capability", "Resource", "CourseOfAction", "ValueStream"]);
|
|
1227
|
+
var IMPLEMENTATION_TYPES = /* @__PURE__ */ new Set([
|
|
1228
|
+
"WorkPackage",
|
|
1229
|
+
"Deliverable",
|
|
1230
|
+
"Plateau",
|
|
1231
|
+
"Gap",
|
|
1232
|
+
"ImplementationEvent"
|
|
1233
|
+
]);
|
|
1234
|
+
function shapeForType(elementType, h) {
|
|
1235
|
+
if (!elementType) return { kind: "rect", rx: 4 };
|
|
1236
|
+
if (elementType.endsWith("Service")) return { kind: "stadium" };
|
|
1237
|
+
if (elementType.endsWith("Process") || elementType.endsWith("Function") || elementType.endsWith("Interaction") || elementType.endsWith("Event"))
|
|
1238
|
+
return { kind: "rect", rx: Math.min(14, h / 3) };
|
|
1239
|
+
if (MOTIVATION_TYPES.has(elementType)) return { kind: "rect", rx: Math.min(18, h / 2.5) };
|
|
1240
|
+
if (STRATEGY_TYPES.has(elementType)) return { kind: "rect", rx: Math.min(12, h / 3) };
|
|
1241
|
+
if (IMPLEMENTATION_TYPES.has(elementType)) return { kind: "rect", rx: 6 };
|
|
1242
|
+
return { kind: "rect", rx: 2 };
|
|
1243
|
+
}
|
|
1244
|
+
function ElementShape({
|
|
1245
|
+
node,
|
|
1246
|
+
dashed,
|
|
1247
|
+
transparent
|
|
1248
|
+
}) {
|
|
1249
|
+
const fontSize = node.fontSize ?? 11;
|
|
1250
|
+
const fill = transparent ? "none" : node.fill;
|
|
1251
|
+
const iconName = iconNameForType(node.elementType);
|
|
1252
|
+
const showIcon = !!iconName && node.w >= 50 && node.h >= 28;
|
|
1253
|
+
const iconSize = showIcon ? Math.min(22, Math.max(14, node.w * 0.18)) : 0;
|
|
1254
|
+
const shape = shapeForType(node.elementType, node.h);
|
|
1255
|
+
const radius = shape.kind === "stadium" ? node.h / 2 : shape.rx;
|
|
1256
|
+
return /* @__PURE__ */ jsxs("g", { style: { color: node.stroke }, children: [
|
|
1257
|
+
/* @__PURE__ */ jsx(
|
|
1258
|
+
"rect",
|
|
1259
|
+
{
|
|
1260
|
+
x: node.x,
|
|
1261
|
+
y: node.y,
|
|
1262
|
+
width: node.w,
|
|
1263
|
+
height: node.h,
|
|
1264
|
+
fill,
|
|
1265
|
+
stroke: node.stroke,
|
|
1266
|
+
strokeWidth: 1,
|
|
1267
|
+
strokeDasharray: dashed ? "5 4" : void 0,
|
|
1268
|
+
rx: radius,
|
|
1269
|
+
ry: radius
|
|
1270
|
+
}
|
|
1271
|
+
),
|
|
1272
|
+
showIcon ? /* @__PURE__ */ jsx(
|
|
1273
|
+
IconBadge,
|
|
1274
|
+
{
|
|
1275
|
+
x: node.x + node.w - iconSize - 5,
|
|
1276
|
+
y: node.y + 5,
|
|
1277
|
+
size: iconSize,
|
|
1278
|
+
iconName,
|
|
1279
|
+
color: node.stroke
|
|
1280
|
+
}
|
|
1281
|
+
) : null,
|
|
1282
|
+
node.label ? (() => {
|
|
1283
|
+
const textX = node.x + node.w / 2;
|
|
1284
|
+
const textY = node.y + Math.min(16, node.h / 2 + fontSize / 2);
|
|
1285
|
+
const availWidth = node.w - (showIcon ? iconSize + 12 : 8);
|
|
1286
|
+
const lines = wrapLabel(node.label, availWidth, fontSize);
|
|
1287
|
+
return /* @__PURE__ */ jsx(
|
|
1288
|
+
"text",
|
|
1289
|
+
{
|
|
1290
|
+
x: textX,
|
|
1291
|
+
y: textY,
|
|
1292
|
+
textAnchor: "middle",
|
|
1293
|
+
fontFamily: node.fontFamily ?? "system-ui, -apple-system, sans-serif",
|
|
1294
|
+
fontSize,
|
|
1295
|
+
fill: node.textColor,
|
|
1296
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsx("tspan", { x: textX, dy: i === 0 ? 0 : fontSize * 1.2, children: line }, i))
|
|
1297
|
+
}
|
|
1298
|
+
);
|
|
1299
|
+
})() : null
|
|
1300
|
+
] });
|
|
1301
|
+
}
|
|
1302
|
+
function IconBadge({
|
|
1303
|
+
x,
|
|
1304
|
+
y,
|
|
1305
|
+
size,
|
|
1306
|
+
iconName,
|
|
1307
|
+
color
|
|
1308
|
+
}) {
|
|
1309
|
+
const def = ICONS[iconName];
|
|
1310
|
+
if (!def) return null;
|
|
1311
|
+
const scale = size / 100;
|
|
1312
|
+
return /* @__PURE__ */ jsxs(
|
|
1313
|
+
"g",
|
|
1314
|
+
{
|
|
1315
|
+
transform: `translate(${x} ${y}) scale(${scale})`,
|
|
1316
|
+
stroke: color,
|
|
1317
|
+
strokeWidth: 5,
|
|
1318
|
+
fill: "none",
|
|
1319
|
+
strokeLinejoin: "round",
|
|
1320
|
+
strokeLinecap: "round",
|
|
1321
|
+
children: [
|
|
1322
|
+
def.outline?.map((d, i) => /* @__PURE__ */ jsx("path", { d }, `o-${i}`)),
|
|
1323
|
+
def.filled?.map((d, i) => /* @__PURE__ */ jsx("path", { d, fill: color, stroke: "none" }, `f-${i}`))
|
|
1324
|
+
]
|
|
1325
|
+
}
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
function wrapLabel(label, width, fontSize, maxLines = 3) {
|
|
1329
|
+
const maxChars = Math.max(1, Math.floor(width / (fontSize * 0.55)));
|
|
1330
|
+
const words = label.split(/\s+/).filter(Boolean);
|
|
1331
|
+
const lines = [];
|
|
1332
|
+
let current = "";
|
|
1333
|
+
for (const word of words) {
|
|
1334
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
1335
|
+
if (candidate.length <= maxChars) {
|
|
1336
|
+
current = candidate;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
if (current) lines.push(current);
|
|
1340
|
+
if (word.length > maxChars) {
|
|
1341
|
+
let rest = word;
|
|
1342
|
+
while (rest.length > maxChars) {
|
|
1343
|
+
lines.push(rest.slice(0, maxChars));
|
|
1344
|
+
rest = rest.slice(maxChars);
|
|
1345
|
+
}
|
|
1346
|
+
current = rest;
|
|
1347
|
+
} else {
|
|
1348
|
+
current = word;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (current) lines.push(current);
|
|
1352
|
+
if (lines.length > maxLines) {
|
|
1353
|
+
const truncated = lines.slice(0, maxLines);
|
|
1354
|
+
const last = truncated.length - 1;
|
|
1355
|
+
truncated[last] = truncated[last].slice(0, Math.max(1, maxChars - 1)) + "\u2026";
|
|
1356
|
+
return truncated;
|
|
1357
|
+
}
|
|
1358
|
+
return lines;
|
|
1359
|
+
}
|
|
1360
|
+
export {
|
|
1361
|
+
ArchimateParseError,
|
|
1362
|
+
ArchimateRenderer,
|
|
1363
|
+
ICONS,
|
|
1364
|
+
LAYER_COLORS,
|
|
1365
|
+
colorForType,
|
|
1366
|
+
iconNameForType,
|
|
1367
|
+
parseArchimate
|
|
1368
|
+
};
|