@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.
- package/dist/renderengine-workspace.umd.js +6485 -3992
- package/dist/renderengine.umd.js +6251 -3877
- package/documentation/REFERENCE.md +384 -28
- package/package.json +1 -1
- package/src/archimateArchiFormat.js +114 -10
- package/src/archimateArchive.js +27 -4
- package/src/archimateExchangeFormat.js +12 -4
- package/src/archimateSpecializations.js +11 -1
- package/src/chartCatalog.js +6 -4
- package/src/charting/ArchimateDiagramEditor.js +7 -109
- package/src/charting/DiagramTypeRegistry.js +20 -1
- package/src/charting/DiagramViewPropertiesDialog.js +54 -10
- package/src/charting/Edge.js +22 -0
- package/src/charting/Legend.js +1 -2
- package/src/charting/Statistics.js +14 -0
- package/src/charting/archimate/ArchimateCanvasEditor.js +21 -5
- package/src/charting/archimate/ViewGroupTemplate.js +5 -2
- package/src/charting/charts/BarChart.js +59 -8
- package/src/charting/charts/HexbinChart.js +15 -0
- package/src/charting/charts/LollipopChart.js +42 -7
- package/src/charting/decision/DecisionWorkbench.js +71 -4
- package/src/charting/diagramModel/DiagramJsonFileFormat.js +32 -0
- package/src/charting/diagramModel/DiagramModel.js +39 -0
- package/src/charting/diagramModel/DiagramView.js +102 -61
- package/src/charting/diagramModel/index.js +11 -10
- package/src/charting/diagrams/AddGroupToViewCommand.js +35 -0
- package/src/charting/diagrams/ArchimateModel.js +53 -13
- package/src/charting/diagrams/GraphDiagram3D.js +15 -0
- package/src/charting/diagrams/NestInstanceCommand.js +2 -1
- package/src/charting/graph/GraphSource.js +72 -0
- package/src/charting/modelTree/NavigatorModel.js +22 -8
- package/src/charting/report/TemplatedReport.js +27 -377
- package/src/charting/report/ViewsReport.js +118 -0
- package/src/charting/report/reportWriters.js +496 -0
- package/src/charting/templates/C4BoundaryTemplate.js +34 -0
- package/src/charting/templates/C4ComponentTemplate.js +30 -0
- package/src/charting/templates/C4ContainerTemplate.js +49 -0
- package/src/charting/templates/C4PersonTemplate.js +35 -0
- package/src/charting/templates/C4SystemTemplate.js +32 -0
- package/src/charting/templates/C4Template.js +95 -0
- package/src/charting/templates/index.js +6 -0
- package/src/charting/templates/templateRegistry.js +77 -0
- package/src/compute/ComputeBackend.js +132 -0
- package/src/compute/CpuBackend.js +209 -0
- package/src/compute/WebGpuBackend.js +630 -0
- package/src/compute/index.js +5 -0
- package/src/compute/selectBackend.js +85 -0
- package/src/index.js +3403 -3370
- package/src/kernelExpr.js +80 -5
- package/src/workspace/parts/ArchimateEditorPart.js +2 -2
- package/src/workspace/parts/DiagramEditorPart.js +146 -131
- package/src/workspace/parts/ViewsPart.js +106 -0
|
@@ -1,61 +1,102 @@
|
|
|
1
|
-
import { DiagramEditorState } from '../DiagramEditorState.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {string} [options.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
arrangement: this.arrangement.
|
|
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:
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
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
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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([
|
|
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(
|
|
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(
|
|
409
|
+
layers.add(this.layerOf(source));
|
|
396
410
|
queue.push({ id: source.id, depth: nextDepth });
|
|
397
411
|
}
|
|
398
412
|
}
|