@peter.prib/renderengine 0.15.1 → 0.15.3

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 (52) hide show
  1. package/dist/renderengine-workspace.umd.js +6485 -3992
  2. package/dist/renderengine.umd.js +6251 -3877
  3. package/documentation/REFERENCE.md +384 -28
  4. package/package.json +1 -1
  5. package/src/archimateArchiFormat.js +114 -10
  6. package/src/archimateArchive.js +27 -4
  7. package/src/archimateExchangeFormat.js +12 -4
  8. package/src/archimateSpecializations.js +11 -1
  9. package/src/chartCatalog.js +6 -4
  10. package/src/charting/ArchimateDiagramEditor.js +7 -109
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Edge.js +22 -0
  14. package/src/charting/Legend.js +1 -2
  15. package/src/charting/Statistics.js +14 -0
  16. package/src/charting/archimate/ArchimateCanvasEditor.js +21 -5
  17. package/src/charting/archimate/ViewGroupTemplate.js +5 -2
  18. package/src/charting/charts/BarChart.js +59 -8
  19. package/src/charting/charts/HexbinChart.js +15 -0
  20. package/src/charting/charts/LollipopChart.js +42 -7
  21. package/src/charting/decision/DecisionWorkbench.js +71 -4
  22. package/src/charting/diagramModel/DiagramJsonFileFormat.js +32 -0
  23. package/src/charting/diagramModel/DiagramModel.js +39 -0
  24. package/src/charting/diagramModel/DiagramView.js +102 -61
  25. package/src/charting/diagramModel/index.js +11 -10
  26. package/src/charting/diagrams/AddGroupToViewCommand.js +35 -0
  27. package/src/charting/diagrams/ArchimateModel.js +53 -13
  28. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  29. package/src/charting/diagrams/NestInstanceCommand.js +2 -1
  30. package/src/charting/graph/GraphSource.js +72 -0
  31. package/src/charting/modelTree/NavigatorModel.js +22 -8
  32. package/src/charting/report/TemplatedReport.js +27 -377
  33. package/src/charting/report/ViewsReport.js +118 -0
  34. package/src/charting/report/reportWriters.js +496 -0
  35. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  36. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  37. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  38. package/src/charting/templates/C4PersonTemplate.js +35 -0
  39. package/src/charting/templates/C4SystemTemplate.js +32 -0
  40. package/src/charting/templates/C4Template.js +95 -0
  41. package/src/charting/templates/index.js +6 -0
  42. package/src/charting/templates/templateRegistry.js +77 -0
  43. package/src/compute/ComputeBackend.js +132 -0
  44. package/src/compute/CpuBackend.js +209 -0
  45. package/src/compute/WebGpuBackend.js +630 -0
  46. package/src/compute/index.js +5 -0
  47. package/src/compute/selectBackend.js +85 -0
  48. package/src/index.js +3403 -3370
  49. package/src/kernelExpr.js +80 -5
  50. package/src/workspace/parts/ArchimateEditorPart.js +2 -2
  51. package/src/workspace/parts/DiagramEditorPart.js +146 -131
  52. package/src/workspace/parts/ViewsPart.js +106 -0
@@ -1,61 +1,102 @@
1
- import { DiagramEditorState } from '../DiagramEditorState.js';
2
-
3
- /**
4
- * DiagramView — one diagram (tab) of a DiagramModel: its shapes, lines,
5
- * diagram-level line style and its own undo history (from DiagramEditorState).
6
- *
7
- * A DiagramEditor is bound directly to `templates`/`edges`, so edits land here
8
- * with nothing to save back, and the history survives the editor being rebuilt.
9
- */
10
- export class DiagramView extends DiagramEditorState {
11
- /**
12
- * @param {Object} options
13
- * @param {string} options.id
14
- * @param {string} options.title
15
- * @param {Map<string, Template>} [options.templates]
16
- * @param {Map<string, Edge>} [options.edges]
17
- * @param {Object} [options.edgeStyle] - Diagram-level line style defaults
18
- * @param {Object} [options.historyOptions] - Passed to CommandHistory (e.g. onChange)
19
- * @param {string} [options.documentation]
20
- */
21
- constructor({ id, title, templates = new Map(), edges = new Map(), edgeStyle = {}, historyOptions = {}, documentation = undefined, arrangement = null }) {
22
- super({ historyOptions, arrangement });
23
- this.historyOptions = historyOptions;
24
- this.id = id;
25
- this.title = title;
26
- this.documentation = documentation;
27
- this.templates = templates;
28
- this.edges = edges;
29
- this.edgeStyle = edgeStyle;
30
- }
31
-
32
- /** The shapes are what stack. */
33
- layerItems() {
34
- return [...this.templates.values()];
35
- }
36
-
37
- /** The diagram as plain JSON: its shapes (by id), lines, line style and documentation. */
38
- toJSON() {
39
- return {
40
- title: this.title,
41
- documentation: this.documentation,
42
- templates: [...this.templates].map(([id, t]) => ({ id, type: t.constructor.name, name: t.name, data: t.serialize() })),
43
- edges: [...this.edges.values()].map(e => JSON.parse(JSON.stringify(e))),
44
- edgeStyle: JSON.parse(JSON.stringify(this.edgeStyle ?? {})),
45
- };
46
- }
47
-
48
- /** An independent copy: shapes, lines, style and documentation are copied; history starts empty (same options). */
49
- copy(id, title, historyOptions = this.historyOptions) {
50
- return new DiagramView({
51
- id,
52
- title,
53
- templates: new Map([...this.templates].map(([tid, t]) => [tid, new t.constructor(tid, t.name, t.serialize())])),
54
- edges: new Map([...this.edges].map(([eid, e]) => [eid, e.clone()])),
55
- edgeStyle: { ...this.edgeStyle },
56
- documentation: this.documentation,
57
- historyOptions,
58
- arrangement: this.arrangement.copy(), // template ids are kept
59
- });
60
- }
61
- }
1
+ import { DiagramEditorState } from '../DiagramEditorState.js';
2
+ import { Edge } from '../Edge.js';
3
+ import { Arrangement } from '../arrangement/Arrangement.js';
4
+ import { templateClassFor, templateTypeKey } from '../templates/templateRegistry.js';
5
+
6
+ /**
7
+ * DiagramView — one diagram (tab) of a DiagramModel: its shapes, lines,
8
+ * diagram-level line style and its own undo history (from DiagramEditorState).
9
+ *
10
+ * A DiagramEditor is bound directly to `templates`/`edges`, so edits land here
11
+ * with nothing to save back, and the history survives the editor being rebuilt.
12
+ */
13
+ export class DiagramView extends DiagramEditorState {
14
+ /**
15
+ * @param {Object} options
16
+ * @param {string} options.id
17
+ * @param {string} options.title
18
+ * @param {Map<string, Template>} [options.templates]
19
+ * @param {Map<string, Edge>} [options.edges]
20
+ * @param {Object} [options.edgeStyle] - Diagram-level line style defaults
21
+ * @param {Object} [options.viewSettings] - Diagram appearance: background, watermark, title band
22
+ * @param {Object} [options.historyOptions] - Passed to CommandHistory (e.g. onChange)
23
+ * @param {string} [options.documentation]
24
+ * @param {Arrangement} [options.arrangement]
25
+ */
26
+ constructor({ id, title, templates = new Map(), edges = new Map(), edgeStyle = {}, viewSettings = {}, historyOptions = {}, documentation = undefined, arrangement = null }) {
27
+ super({ historyOptions, arrangement });
28
+ this.historyOptions = historyOptions;
29
+ this.id = id;
30
+ this.title = title;
31
+ this.documentation = documentation;
32
+ this.templates = templates;
33
+ this.edges = edges;
34
+ this.edgeStyle = edgeStyle;
35
+ this.viewSettings = viewSettings;
36
+ }
37
+
38
+ /** The shapes are what stack. */
39
+ layerItems() {
40
+ return [...this.templates.values()];
41
+ }
42
+
43
+ /** The diagram as plain JSON: its shapes (by id and stable type), lines, line style and documentation. */
44
+ toJSON() {
45
+ return {
46
+ id: this.id,
47
+ title: this.title,
48
+ documentation: this.documentation,
49
+ templates: [...this.templates].map(([id, t]) => ({
50
+ id,
51
+ type: templateTypeKey(t),
52
+ name: t.name,
53
+ data: t.serialize(),
54
+ })),
55
+ edges: [...this.edges.values()].map(e => (typeof e.toJSON === 'function' ? e.toJSON() : JSON.parse(JSON.stringify(e)))),
56
+ edgeStyle: JSON.parse(JSON.stringify(this.edgeStyle ?? {})),
57
+ ...(this.viewSettings && Object.keys(this.viewSettings).length ? { viewSettings: JSON.parse(JSON.stringify(this.viewSettings)) } : {}),
58
+ ...(this.arrangement && { arrangement: this.arrangement.toJSON() }),
59
+ };
60
+ }
61
+
62
+ /** Reconstruct a DiagramView from plain JSON. */
63
+ static fromJSON(json, historyOptions = {}) {
64
+ if (!json) throw new TypeError('DiagramView.fromJSON requires a JSON object');
65
+ const templates = new Map();
66
+ for (const t of (json.templates ?? [])) {
67
+ const Class = templateClassFor(t.type);
68
+ templates.set(t.id, new Class(t.id, t.name, t.data ?? {}));
69
+ }
70
+ const edges = new Map();
71
+ for (const e of (json.edges ?? [])) {
72
+ edges.set(e.id, Edge.fromJSON(e));
73
+ }
74
+ const arrangement = json.arrangement ? Arrangement.fromJSON(json.arrangement) : null;
75
+ return new DiagramView({
76
+ id: json.id,
77
+ title: json.title,
78
+ templates,
79
+ edges,
80
+ edgeStyle: json.edgeStyle ?? {},
81
+ viewSettings: json.viewSettings ? JSON.parse(JSON.stringify(json.viewSettings)) : {},
82
+ documentation: json.documentation,
83
+ historyOptions,
84
+ arrangement,
85
+ });
86
+ }
87
+
88
+ /** An independent copy: shapes, lines, style and documentation are copied; history starts empty (same options). */
89
+ copy(id, title, historyOptions = this.historyOptions) {
90
+ return new DiagramView({
91
+ id,
92
+ title,
93
+ templates: new Map([...this.templates].map(([tid, t]) => [tid, new t.constructor(tid, t.name, t.serialize())])),
94
+ edges: new Map([...this.edges].map(([eid, e]) => [eid, e.clone()])),
95
+ edgeStyle: { ...this.edgeStyle },
96
+ viewSettings: { ...(this.viewSettings ?? {}) },
97
+ documentation: this.documentation,
98
+ historyOptions,
99
+ arrangement: this.arrangement?.copy() ?? null, // template ids are kept
100
+ });
101
+ }
102
+ }
@@ -1,10 +1,11 @@
1
- export { DiagramModel } from './DiagramModel.js';
2
- export { DiagramView } from './DiagramView.js';
3
- export { ViewSetModel } from './ViewSetModel.js';
4
- export { ModelSearch, SEARCH_FIELDS } from './ModelSearch.js';
5
- export { DiagramModelSearch } from './DiagramModelSearch.js';
6
- export { ReplaceTextCommand } from './ReplaceTextCommand.js';
7
- export { ModelComparison } from './ModelComparison.js';
8
- export { ComparedKind } from './ComparedKind.js';
9
- export { ViewsCompared } from './ViewsCompared.js';
10
- export { MergeModelCommand } from './MergeModelCommand.js';
1
+ export { DiagramModel } from './DiagramModel.js';
2
+ export { DiagramView } from './DiagramView.js';
3
+ export { ViewSetModel } from './ViewSetModel.js';
4
+ export { ModelSearch, SEARCH_FIELDS } from './ModelSearch.js';
5
+ export { DiagramModelSearch } from './DiagramModelSearch.js';
6
+ export { ReplaceTextCommand } from './ReplaceTextCommand.js';
7
+ export { ModelComparison } from './ModelComparison.js';
8
+ export { ComparedKind } from './ComparedKind.js';
9
+ export { ViewsCompared } from './ViewsCompared.js';
10
+ export { MergeModelCommand } from './MergeModelCommand.js';
11
+ export { DiagramJsonFileFormat } from './DiagramJsonFileFormat.js';
@@ -0,0 +1,35 @@
1
+ import { Command } from '../CommandHistory.js';
2
+
3
+ /**
4
+ * AddGroupToViewCommand — add a view-specific group to a view: its layout entry
5
+ * is stored in view.folderLayout. Undo removes it.
6
+ */
7
+ export class AddGroupToViewCommand extends Command {
8
+ /**
9
+ * @param {ArchimateView} view
10
+ * @param {Object} [options]
11
+ * @param {string} [options.id]
12
+ * @param {string} [options.name]
13
+ * @param {number} [options.x]
14
+ * @param {number} [options.y]
15
+ * @param {number} [options.width]
16
+ * @param {number} [options.height]
17
+ * @param {number} [options.zIndex]
18
+ * @param {Object} [options.appearance]
19
+ * @param {string} [options.parentId]
20
+ */
21
+ constructor(view, { id = `group-${Date.now().toString(36)}`, name = 'Group', x = 100, y = 100, width = 400, height = 140, zIndex = 0, appearance = {}, parentId = undefined } = {}) {
22
+ super('Add group to view');
23
+ this.view = view;
24
+ this.groupId = id;
25
+ this.entry = { x, y, width, height, name, zIndex, appearance, parentId };
26
+ }
27
+
28
+ execute() {
29
+ this.view.folderLayout[this.groupId] = { ...this.entry };
30
+ }
31
+
32
+ undo() {
33
+ delete this.view.folderLayout[this.groupId];
34
+ }
35
+ }
@@ -17,6 +17,8 @@ import { DIAGRAM_BOX_KINDS } from '../canvas/diagramBoxKinds.js';
17
17
  import { RemoveLineCommand } from './RemoveLineCommand.js';
18
18
  import { CompositeCommand } from '../CommandHistory.js';
19
19
  import { AddToViewCommand, DeleteElementCommand, AddRelationshipToViewCommand, RemoveRelationshipFromViewCommand } from './ArchimateCommands.js';
20
+ import { AddGroupToViewCommand } from './AddGroupToViewCommand.js';
21
+ import { RemoveGroupFromViewCommand } from './RemoveGroupFromViewCommand.js';
20
22
 
21
23
  /**
22
24
  * Canonical, id-indexed home for ArchiMate model data (elements, relationships, views).
@@ -1171,27 +1173,65 @@ export class ArchimateView extends DiagramEditorState {
1171
1173
  }
1172
1174
 
1173
1175
  /**
1174
- * The folders drawn on this view as groups: those with bounds in its
1175
- * `folderLayout`, bounds applied, each after its parent. `parentId` is the
1176
- * nearest ancestor that is also drawn here (a group inside an undrawn
1177
- * folder sits on the view itself).
1176
+ * The folders drawn on this view as groups: both model repository folders
1177
+ * and view-specific groups with bounds in `folderLayout`, bounds applied,
1178
+ * each after its parent. `parentId` is the nearest ancestor also drawn here.
1178
1179
  */
1179
1180
  get placedFolders() {
1180
- const placed = this.model.allFolders().filter(f => this.folderLayout[f.id]);
1181
- const drawn = new Set(placed.map(f => f.id));
1182
- return placed.map(folder => ({
1183
- ...folder,
1184
- ...this.folderLayout[folder.id],
1185
- parentId: this._nearestPlaced(folder.parentId, drawn),
1186
- }));
1181
+ const drawn = new Set(Object.keys(this.folderLayout));
1182
+ const modelFolders = this.model.allFolders();
1183
+ const modelFolderIds = new Set(modelFolders.map(f => f.id));
1184
+
1185
+ const result = [];
1186
+ for (const folder of modelFolders.filter(f => this.folderLayout[f.id])) {
1187
+ result.push({
1188
+ ...folder,
1189
+ ...this.folderLayout[folder.id],
1190
+ name: this.folderLayout[folder.id].name ?? folder.name,
1191
+ parentId: this._nearestPlaced(folder.parentId, drawn),
1192
+ });
1193
+ }
1194
+ for (const [id, layout] of Object.entries(this.folderLayout)) {
1195
+ if (!modelFolderIds.has(id)) {
1196
+ result.push({
1197
+ id,
1198
+ name: layout.name || 'Group',
1199
+ ...layout,
1200
+ parentId: layout.parentId && drawn.has(layout.parentId) ? layout.parentId : undefined,
1201
+ });
1202
+ }
1203
+ }
1204
+ return result;
1187
1205
  }
1188
1206
 
1189
- /** The group on this view an element is drawn in: its folder, or that folder's nearest drawn ancestor. */
1207
+ /** The group on this view an element is drawn in: its explicit view parent group, or its model folder's nearest drawn ancestor. */
1190
1208
  placedFolderOf(elementId) {
1191
- const drawn = new Set(this.model.allFolders().filter(f => this.folderLayout[f.id]).map(f => f.id));
1209
+ const inst = this.elementInstancesFor(elementId)[0];
1210
+ if (inst?.archi?.parent && this.folderLayout[inst.archi.parent]) {
1211
+ return inst.archi.parent;
1212
+ }
1213
+ if (inst?.parentId && this.folderLayout[inst.parentId]) {
1214
+ return inst.parentId;
1215
+ }
1216
+ const drawn = new Set(Object.keys(this.folderLayout));
1192
1217
  return this._nearestPlaced(this.model.getElementFolder(elementId), drawn);
1193
1218
  }
1194
1219
 
1220
+ /** Add a view-specific group to this view. One undo step (AddGroupToViewCommand). */
1221
+ addGroup(spec = {}) {
1222
+ const id = spec.id || `group-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
1223
+ const cmd = new AddGroupToViewCommand(this, { id, ...spec });
1224
+ this.history.execute(cmd);
1225
+ return this.folderLayout[id];
1226
+ }
1227
+
1228
+ /** Remove a group (view-specific or placed model folder) from this view. One undo step (RemoveGroupFromViewCommand). */
1229
+ removeGroup(groupId) {
1230
+ if (!this.folderLayout[groupId]) return false;
1231
+ this.history.execute(new RemoveGroupFromViewCommand(this, groupId));
1232
+ return true;
1233
+ }
1234
+
1195
1235
  /** @private */
1196
1236
  _nearestPlaced(folderId, drawn) {
1197
1237
  for (let id = folderId; id; id = this.model.getFolder(id)?.parentId) {
@@ -149,4 +149,19 @@ export class GraphDiagram3D extends Diagram3D {
149
149
  }
150
150
  return pos;
151
151
  }
152
+
153
+ /**
154
+ * Asynchronous 3D force layout simulation, offloading to GPU when backend provided.
155
+ * @param {Array<{id: string, x?: number, y?: number, z?: number}>} nodes
156
+ * @param {Array<{source: string, target: string}>} edges
157
+ * @param {object} [options]
158
+ * @param {import('../../compute/ComputeBackend.js').ComputeBackend} [options.backend]
159
+ * @returns {Promise<Map<string, {x: number, y: number, z: number, vx: number, vy: number, vz: number}>>}
160
+ */
161
+ static async layoutForce3DAsync(nodes, edges, options = {}) {
162
+ if (options.backend) {
163
+ return options.backend.layoutForce3DAsync(nodes, edges, options);
164
+ }
165
+ return GraphDiagram3D.layoutForce3D(nodes, edges, options);
166
+ }
152
167
  }
@@ -23,7 +23,8 @@ export class NestInstanceCommand extends Command {
23
23
  execute() {
24
24
  const { instance, parent } = this;
25
25
  const { parent: _was, ...rest } = instance.archi ?? {};
26
- instance.archi = parent ? { ...rest, parent: parent.visualId } : (Object.keys(rest).length ? rest : null);
26
+ const parentId = parent ? (parent.visualId ?? parent.id) : null;
27
+ instance.archi = parentId ? { ...rest, parent: parentId } : (Object.keys(rest).length ? rest : null);
27
28
  if (parent && (instance.zIndex ?? 0) <= (parent.zIndex ?? 0)) instance.zIndex = (parent.zIndex ?? 0) + 1;
28
29
  }
29
30
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * GraphSource — what a graph walker reads, whatever the graph is.
3
+ *
4
+ * `NavigatorModel` (step-by-step exploration, `findPaths`, `traceImpact`) asks a graph four questions:
5
+ *
6
+ * - `getElement(id)` — the element, `{ id, name, type, ... }`, or null;
7
+ * - `relationshipsFor(id)` — `{ incoming, outgoing }`, each relationship `{ id, source, target, kind }`;
8
+ * - `viewsContaining(id)` — the views that draw it (none, for a graph that has no views);
9
+ * - `layerOf(element)` — the band it belongs to, for grouping and colour.
10
+ *
11
+ * Why an interface. Traversal is the same for any graph; only these reads differ. So there is one walker,
12
+ * and each kind of graph answers the four questions: `ArchimateModel` answers them already (its layers
13
+ * from `treeLayerOf`), and `DocumentGraph` answers them for a graph document — `{ nodes, edges }`, the
14
+ * shape sqlengine stores graphs in and decisionengine scores — so a portfolio, a dependency graph or any
15
+ * stored graph is explored the way an ArchiMate model is, without a second walker.
16
+ *
17
+ * A subclass implements the four reads; this base says what is missing rather than failing obscurely.
18
+ */
19
+ export class GraphSource {
20
+ getElement() { throw new Error(`${this.constructor.name} must implement getElement(id)`); }
21
+ relationshipsFor() { throw new Error(`${this.constructor.name} must implement relationshipsFor(id)`); }
22
+ viewsContaining() { return []; }
23
+ layerOf(element) { return element?.type ?? 'other'; }
24
+ }
25
+
26
+ /**
27
+ * DocumentGraph — a graph document (`{ nodes: [{ id, labels, props }], edges: [{ id?, from, to, type, props }] }`)
28
+ * as a GraphSource. A node's type is its first label and its name `props.name` (else its id); an edge's
29
+ * kind is its `type`, its id its own or one made from its ends. Its layer is its type, so a portfolio's
30
+ * capabilities, epics and objectives band apart. Indexed once, read many.
31
+ */
32
+ export class DocumentGraph extends GraphSource {
33
+ /** True for something shaped like a graph document. */
34
+ static isDocument(doc) {
35
+ return Boolean(doc) && Array.isArray(doc.nodes) && (doc.edges === undefined || Array.isArray(doc.edges));
36
+ }
37
+
38
+ constructor(doc) {
39
+ super();
40
+ if (!DocumentGraph.isDocument(doc)) throw new Error('A graph document has a nodes list (and an edges list)');
41
+ this.elements = new Map();
42
+ for (const node of doc.nodes) {
43
+ if (node?.id == null) continue;
44
+ this.elements.set(node.id, Object.freeze({
45
+ id: node.id, name: node.props?.name ?? String(node.id), type: node.labels?.[0] ?? 'Node',
46
+ labels: node.labels ?? [], props: node.props ?? {},
47
+ }));
48
+ }
49
+ this.incoming = new Map();
50
+ this.outgoing = new Map();
51
+ (doc.edges ?? []).forEach((edge, i) => {
52
+ if (!this.elements.has(edge?.from) || !this.elements.has(edge?.to)) return;
53
+ const relationship = Object.freeze({
54
+ id: edge.id ?? `${edge.from}-${edge.type}-${edge.to}-${i}`, source: edge.from, target: edge.to,
55
+ kind: edge.type, name: edge.props?.name ?? edge.type, props: edge.props ?? {},
56
+ });
57
+ push(this.outgoing, edge.from, relationship);
58
+ push(this.incoming, edge.to, relationship);
59
+ });
60
+ }
61
+
62
+ getElement(id) { return this.elements.get(id) ?? null; }
63
+
64
+ relationshipsFor(id) {
65
+ return { incoming: this.incoming.get(id) ?? [], outgoing: this.outgoing.get(id) ?? [] };
66
+ }
67
+ }
68
+
69
+ function push(map, key, value) {
70
+ if (!map.has(key)) map.set(key, []);
71
+ map.get(key).push(value);
72
+ }
@@ -1,7 +1,11 @@
1
1
  import { treeLayerOf } from './ModelTreePanel.js';
2
+ import { DocumentGraph } from '../graph/GraphSource.js';
2
3
 
3
4
  /**
4
- * NavigatorModel — pure, DOM-free graph traversal and navigation engine for an ArchiMate model.
5
+ * NavigatorModel — pure, DOM-free graph traversal and navigation engine for any graph: an ArchiMate
6
+ * model, or any GraphSource (`DocumentGraph` for a `{ nodes, edges }` graph document — see
7
+ * `NavigatorModel.forGraph`). It reads the graph only through `getElement`, `relationshipsFor`,
8
+ * `viewsContaining` and `layerOf` (see `GraphSource` for why).
5
9
  *
6
10
  * Allows exploring relationships step-by-step from any focused root element to arbitrary depth,
7
11
  * with cycle detection, navigation history (back/forward), direction filters, path-finding
@@ -9,12 +13,17 @@ import { treeLayerOf } from './ModelTreePanel.js';
9
13
  */
10
14
  export class NavigatorModel {
11
15
  /**
12
- * @param {ArchimateModel} model
16
+ * @param {ArchimateModel|import('../graph/GraphSource.js').GraphSource} model
13
17
  * @param {Object} [options]
14
18
  * @param {string} [options.rootId] - Initial focused element id
15
19
  * @param {'both'|'outgoing'|'incoming'} [options.direction='both']
16
20
  * @param {Iterable<string>} [options.allowedKinds] - Optional relationship kinds to include
17
21
  */
22
+ /** A navigator over a graph document (`{ nodes, edges }`): the same walker, through a DocumentGraph. */
23
+ static forGraph(doc, options) {
24
+ return new NavigatorModel(new DocumentGraph(doc), options);
25
+ }
26
+
18
27
  constructor(model, { rootId = null, direction = 'both', allowedKinds = null } = {}) {
19
28
  this.model = model;
20
29
  this.direction = direction;
@@ -39,6 +48,11 @@ export class NavigatorModel {
39
48
  return () => this._listeners.delete(listener);
40
49
  }
41
50
 
51
+ /** An element's layer: the graph's own answer, or ArchiMate's (an ArchimateModel has none of its own). */
52
+ layerOf(element) {
53
+ return typeof this.model.layerOf === 'function' ? this.model.layerOf(element) : treeLayerOf(element);
54
+ }
55
+
42
56
  _notify() {
43
57
  for (const listener of this._listeners) {
44
58
  try {
@@ -209,7 +223,7 @@ export class NavigatorModel {
209
223
  direction: dir,
210
224
  relationship: rel,
211
225
  element: otherElement,
212
- layer: treeLayerOf(otherElement),
226
+ layer: this.layerOf(otherElement),
213
227
  cycle,
214
228
  expanded,
215
229
  children,
@@ -227,8 +241,8 @@ export class NavigatorModel {
227
241
  return {
228
242
  pathKey,
229
243
  element,
230
- layer: treeLayerOf(element),
231
- views: this.model.viewsContaining(element.id),
244
+ layer: this.layerOf(element),
245
+ views: typeof this.model.viewsContaining === 'function' ? this.model.viewsContaining(element.id) : [],
232
246
  incoming: incomingBranches,
233
247
  outgoing: outgoingBranches,
234
248
  };
@@ -355,7 +369,7 @@ export class NavigatorModel {
355
369
  const depths = new Map([[elementId, 0]]);
356
370
  const elementsMap = new Map();
357
371
  const relsMap = new Map();
358
- const layers = new Set([treeLayerOf(root)]);
372
+ const layers = new Set([this.layerOf(root)]);
359
373
 
360
374
  const queue = [{ id: elementId, depth: 0 }];
361
375
 
@@ -376,7 +390,7 @@ export class NavigatorModel {
376
390
  if (!depths.has(target.id)) {
377
391
  depths.set(target.id, nextDepth);
378
392
  elementsMap.set(target.id, target);
379
- layers.add(treeLayerOf(target));
393
+ layers.add(this.layerOf(target));
380
394
  queue.push({ id: target.id, depth: nextDepth });
381
395
  }
382
396
  }
@@ -392,7 +406,7 @@ export class NavigatorModel {
392
406
  if (!depths.has(source.id)) {
393
407
  depths.set(source.id, nextDepth);
394
408
  elementsMap.set(source.id, source);
395
- layers.add(treeLayerOf(source));
409
+ layers.add(this.layerOf(source));
396
410
  queue.push({ id: source.id, depth: nextDepth });
397
411
  }
398
412
  }