@peter.prib/renderengine 0.2.3 → 0.2.5
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/REFERENCE.md +2231 -2205
- package/dist/renderengine-workspace.umd.js +7965 -7306
- package/dist/renderengine.umd.js +9674 -9211
- package/package.json +1 -1
- package/src/charting/DiagramEditor.js +3330 -3330
- package/src/charting/DiagramTypeRegistry.js +361 -326
- package/src/charting/FormDialog.js +1 -1
- package/src/charting/ModelComparisonDialog.js +122 -0
- package/src/charting/Palette.js +119 -119
- package/src/charting/TemplateCatalog.js +61 -59
- package/src/charting/diagramModel/ComparedKind.js +67 -0
- package/src/charting/diagramModel/DiagramView.js +11 -0
- package/src/charting/diagramModel/MergeModelCommand.js +37 -0
- package/src/charting/diagramModel/ModelComparison.js +100 -0
- package/src/charting/diagramModel/ViewSetModel.js +260 -243
- package/src/charting/diagramModel/ViewsCompared.js +39 -0
- package/src/charting/diagramModel/index.js +10 -6
- package/src/charting/diagrams/ArchimateModel.js +1170 -1155
- package/src/charting/diagrams/ArchimateViewsCompared.js +70 -0
- package/src/charting/diagrams/ConceptsCompared.js +76 -0
- package/src/charting/modelTree/ModelTreePanel.js +886 -914
- package/src/charting/modelTree/viewFolders.js +31 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +345 -345
- package/src/workspace/parts/DiagramEditorPart.js +135 -135
- package/src/workspace/parts/EditorPart.js +26 -7
- package/src/workspace/parts/ModelsPart.js +58 -1
|
@@ -109,7 +109,7 @@ export class FormDialog {
|
|
|
109
109
|
style: { overflowY: 'auto', flex: '1 1 auto', minHeight: '0', paddingRight: '4px' },
|
|
110
110
|
ref: (n) => { this.body = n; },
|
|
111
111
|
children: [
|
|
112
|
-
this.description && { text: this.description, style: { fontSize: '11px', color: '#666', margin: '0 0 10px' } },
|
|
112
|
+
this.description && { text: this.description, style: { fontSize: '11px', color: '#666', margin: '0 0 10px', whiteSpace: 'pre-line' } },
|
|
113
113
|
{ style: { display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '8px 12px', alignItems: 'center' }, children: rows.flat() },
|
|
114
114
|
{ attrs: { role: 'alert' }, style: { color: '#c0392b', fontSize: '12px', marginTop: '8px', display: 'none' }, ref: (n) => { this._error = n; } },
|
|
115
115
|
].filter(Boolean),
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { FormDialog } from './FormDialog.js';
|
|
2
|
+
|
|
3
|
+
/** What a difference is listed under, for a model called `mine` compared with `theirs`. */
|
|
4
|
+
const GROUP_TEXT = {
|
|
5
|
+
added: (kind, mine, theirs) => `${kind.label} only in "${theirs}"`,
|
|
6
|
+
changed: (kind) => `${kind.label} changed`,
|
|
7
|
+
removed: (kind, mine) => `${kind.label} only in "${mine}"`,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** The order of a kind's groups. */
|
|
11
|
+
const CHANGE_ORDER = ['added', 'changed', 'removed'];
|
|
12
|
+
|
|
13
|
+
/** 'elementInstances' reads 'element instances'. */
|
|
14
|
+
const words = (field) => field.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ModelCompareDialog — how one model differs from another (a
|
|
18
|
+
* ModelComparison), shown: how many things of each kind only one has or
|
|
19
|
+
* both have unalike, then each of them, grouped. With anything to merge, its
|
|
20
|
+
* button opens the merge (`onMerge`).
|
|
21
|
+
*/
|
|
22
|
+
export class ModelCompareDialog {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {ModelComparison} options.comparison
|
|
26
|
+
* @param {string} options.mineName - What the model compared is called
|
|
27
|
+
* @param {string} options.theirsName - What the other is called
|
|
28
|
+
* @param {() => void} [options.onMerge] - Merge from the other (offered when there is something to merge)
|
|
29
|
+
* @param {Document} [options.document]
|
|
30
|
+
*/
|
|
31
|
+
constructor({ comparison, mineName, theirsName, onMerge = null, document: doc = globalThis.document }) {
|
|
32
|
+
this.comparison = comparison;
|
|
33
|
+
this.mineName = mineName;
|
|
34
|
+
this.theirsName = theirsName;
|
|
35
|
+
this.onMerge = onMerge;
|
|
36
|
+
this.document = doc;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get title() {
|
|
40
|
+
return `Compare "${this.mineName}" with "${this.theirsName}"`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One line per kind that differs: how many only one has, and how many changed. */
|
|
44
|
+
summaryText() {
|
|
45
|
+
if (this.comparison.same) return 'The models are alike.';
|
|
46
|
+
return this.comparison.summary()
|
|
47
|
+
.filter(s => s.added || s.changed || s.removed)
|
|
48
|
+
.map(({ kind, added, changed, removed }) => `${kind.label}: ${[
|
|
49
|
+
added && `${added} only in "${this.theirsName}"`,
|
|
50
|
+
changed && `${changed} changed`,
|
|
51
|
+
removed && `${removed} only in "${this.mineName}"`,
|
|
52
|
+
].filter(Boolean).join(', ')}`)
|
|
53
|
+
.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The differences listed: [value, text, group] for a checklist, kind by kind, each kind's groups in CHANGE_ORDER. */
|
|
57
|
+
choices(differences) {
|
|
58
|
+
const kinds = this.comparison.kinds;
|
|
59
|
+
const rank = (d) => kinds.indexOf(d.kind) * CHANGE_ORDER.length + CHANGE_ORDER.indexOf(d.change);
|
|
60
|
+
return [...differences].sort((a, b) => rank(a) - rank(b)).map(d => [
|
|
61
|
+
d.key,
|
|
62
|
+
d.change === 'changed' ? `${d.name} (${d.fields.map(words).join(', ')})` : d.name,
|
|
63
|
+
GROUP_TEXT[d.change](d.kind, this.mineName, this.theirsName),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The dialog's fields: every difference, to read. */
|
|
68
|
+
fields() {
|
|
69
|
+
const all = this.comparison.differences();
|
|
70
|
+
return all.length ? [{ key: 'items', label: '', type: 'checklist', readOnly: true, value: [], choices: this.choices(all) }] : [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
open() {
|
|
74
|
+
const canMerge = !!this.onMerge && this.comparison.mergeable().length > 0;
|
|
75
|
+
return new FormDialog({
|
|
76
|
+
title: this.title,
|
|
77
|
+
description: this.summaryText(),
|
|
78
|
+
fields: this.fields(),
|
|
79
|
+
submitLabel: canMerge ? 'Merge…' : 'Close',
|
|
80
|
+
cancelLabel: 'Close',
|
|
81
|
+
idPrefix: 'model-compare-',
|
|
82
|
+
document: this.document,
|
|
83
|
+
onApply: () => { if (canMerge) this.onMerge(); },
|
|
84
|
+
}).open();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* ModelMergeDialog — choose what to bring from the other model: what only it
|
|
90
|
+
* has and its version of what changed, each ticked to start with; what this
|
|
91
|
+
* model alone has stays. Merging hands the command (ModelComparison#mergeCommand,
|
|
92
|
+
* with what the chosen need) to `onMerge`.
|
|
93
|
+
*/
|
|
94
|
+
export class ModelMergeDialog extends ModelCompareDialog {
|
|
95
|
+
get title() {
|
|
96
|
+
return `Merge from "${this.theirsName}" into "${this.mineName}"`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fields() {
|
|
100
|
+
const mergeable = this.comparison.mergeable();
|
|
101
|
+
return mergeable.length
|
|
102
|
+
? [{ key: 'items', label: '', type: 'checklist', value: this.choices(mergeable).map(([key]) => key), choices: this.choices(mergeable) }]
|
|
103
|
+
: [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
open() {
|
|
107
|
+
const nothing = !this.comparison.mergeable().length;
|
|
108
|
+
return new FormDialog({
|
|
109
|
+
title: this.title,
|
|
110
|
+
description: nothing ? `"${this.mineName}" has everything "${this.theirsName}" has.` : `${this.summaryText()}\n\nWhat the chosen need (a view's elements, a relationship's ends) comes too.`,
|
|
111
|
+
fields: this.fields(),
|
|
112
|
+
submitLabel: nothing ? 'Close' : 'Merge',
|
|
113
|
+
idPrefix: 'model-merge-',
|
|
114
|
+
document: this.document,
|
|
115
|
+
validate: ({ items }) => (nothing || items?.length ? null : 'Choose something to merge'),
|
|
116
|
+
onApply: ({ items }) => {
|
|
117
|
+
const command = nothing ? null : this.comparison.mergeCommand(items);
|
|
118
|
+
if (command) this.onMerge(command);
|
|
119
|
+
},
|
|
120
|
+
}).open();
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/charting/Palette.js
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
import { render } from '../workspace/dom.js';
|
|
2
|
-
import { CreateTemplateCommand } from './DiagramEditorCommands.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Palette — category picker plus a grid of draggable shapes.
|
|
6
|
-
*
|
|
7
|
-
* Previews are drawn by the templates themselves (Template#renderPreview), so a
|
|
8
|
-
* palette tile always matches what lands on the canvas.
|
|
9
|
-
*/
|
|
10
|
-
export class Palette {
|
|
11
|
-
/**
|
|
12
|
-
* @param {Object} options
|
|
13
|
-
* @param {HTMLElement} options.container - Element the palette renders into (contents replaced)
|
|
14
|
-
* @param {TemplateCatalog} options.catalog - Shapes offered in the current category
|
|
15
|
-
* @param {DiagramEditor} options.editor - Editor receiving drops
|
|
16
|
-
* @param {Array<{group: string, types: Array<{key: string, name: string, description?: string}>}>} [options.categories]
|
|
17
|
-
* Grouped categories for the picker; omit to hide the picker
|
|
18
|
-
* @param {string} [options.category] - Selected category key
|
|
19
|
-
* @param {(key: string) => void} [options.onCategoryChange]
|
|
20
|
-
*/
|
|
21
|
-
constructor(options = {}) {
|
|
22
|
-
this.container = options.container;
|
|
23
|
-
this.catalog = options.catalog;
|
|
24
|
-
this.editor = options.editor;
|
|
25
|
-
this.categories = options.categories ?? [];
|
|
26
|
-
this.category = options.category ?? null;
|
|
27
|
-
this.onCategoryChange = options.onCategoryChange ?? (() => {});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
render() {
|
|
31
|
-
if (!this.container) return;
|
|
32
|
-
const doc = this.container.ownerDocument;
|
|
33
|
-
render(this.container, [
|
|
34
|
-
this.categories.length ? this._pickerSpec() : '',
|
|
35
|
-
{
|
|
36
|
-
style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '6px', overflowY: 'auto', flex: '1 1 auto', alignContent: 'start' },
|
|
37
|
-
children: this.catalog.getAll().map(entry => this._tileSpec(entry, doc)),
|
|
38
|
-
},
|
|
39
|
-
], doc);
|
|
40
|
-
Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', gap: '8px', minHeight: '0' });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
_pickerSpec() {
|
|
44
|
-
const selected = this.categories.flatMap(g => g.types).find(t => t.key === this.category);
|
|
45
|
-
return {
|
|
46
|
-
tag: 'select',
|
|
47
|
-
attrs: { 'aria-label': 'Diagram type', title: selected?.description },
|
|
48
|
-
style: { width: '100%', padding: '6px', fontSize: '12px', flex: '0 0 auto' },
|
|
49
|
-
on: { change: (e) => this.onCategoryChange(e.target.value) },
|
|
50
|
-
children: this.categories.map(({ group, types }) => ({
|
|
51
|
-
tag: 'optgroup',
|
|
52
|
-
attrs: { label: group },
|
|
53
|
-
children: types.map(t => ({
|
|
54
|
-
tag: 'option',
|
|
55
|
-
attrs: { value: t.key, selected: t.key === this.category, title: t.description },
|
|
56
|
-
text: t.name,
|
|
57
|
-
})),
|
|
58
|
-
})),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
_tileSpec(entry, doc) {
|
|
63
|
-
return {
|
|
64
|
-
attrs: { draggable: 'true', title: `Drag to add ${entry.name}` },
|
|
65
|
-
style: {
|
|
66
|
-
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px',
|
|
67
|
-
padding: '6px 4px', background: '#fff', border: '1px solid #ddd', borderRadius: '4px',
|
|
68
|
-
cursor: 'grab', userSelect: 'none',
|
|
69
|
-
},
|
|
70
|
-
on: { dragstart: (e) => this._startDrag(e, entry) },
|
|
71
|
-
children: [
|
|
72
|
-
entry.factory('__preview', 0, 0).renderPreview(40, doc),
|
|
73
|
-
{
|
|
74
|
-
text: entry.name,
|
|
75
|
-
style: { fontSize: '11px', color: '#333', textAlign: 'center', lineHeight: '1.2', overflowWrap: 'anywhere' },
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
_startDrag(e, entry) {
|
|
82
|
-
if (!e.dataTransfer) return;
|
|
83
|
-
e.dataTransfer.effectAllowed = 'copy';
|
|
84
|
-
e.dataTransfer.setData('templateType', entry.type);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Create the dragged shape at a drop point given in SVG viewBox coordinates,
|
|
89
|
-
* snapping to the grid when it is shown.
|
|
90
|
-
*/
|
|
91
|
-
handleDrop(e, x, y) {
|
|
92
|
-
const templateType = e.dataTransfer.getData('templateType');
|
|
93
|
-
if (!templateType) return false;
|
|
94
|
-
|
|
95
|
-
let snapX = x, snapY = y;
|
|
96
|
-
if (this.editor.showGrid && this.editor.gridSize) {
|
|
97
|
-
const gridSize = this.editor.gridSize;
|
|
98
|
-
snapX = Math.round(x / gridSize) * gridSize;
|
|
99
|
-
snapY = Math.round(y / gridSize) * gridSize;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const template = this.catalog.create(templateType, this._generateId(templateType), snapX, snapY);
|
|
103
|
-
if (!template) return false;
|
|
104
|
-
|
|
105
|
-
if (this.editor.history) {
|
|
106
|
-
this.editor.history.execute(new CreateTemplateCommand(this.editor, template));
|
|
107
|
-
} else {
|
|
108
|
-
this.editor.addTemplate(template);
|
|
109
|
-
}
|
|
110
|
-
this.editor.render();
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
_generateId(templateType) {
|
|
115
|
-
let counter = 1;
|
|
116
|
-
while (this.editor.getTemplate(`${templateType}_${counter}`)) counter++;
|
|
117
|
-
return `${templateType}_${counter}`;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
1
|
+
import { render } from '../workspace/dom.js';
|
|
2
|
+
import { CreateTemplateCommand } from './DiagramEditorCommands.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Palette — category picker plus a grid of draggable shapes.
|
|
6
|
+
*
|
|
7
|
+
* Previews are drawn by the templates themselves (Template#renderPreview), so a
|
|
8
|
+
* palette tile always matches what lands on the canvas.
|
|
9
|
+
*/
|
|
10
|
+
export class Palette {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {HTMLElement} options.container - Element the palette renders into (contents replaced)
|
|
14
|
+
* @param {TemplateCatalog} options.catalog - Shapes offered in the current category
|
|
15
|
+
* @param {DiagramEditor} options.editor - Editor receiving drops
|
|
16
|
+
* @param {Array<{group: string, types: Array<{key: string, name: string, description?: string}>}>} [options.categories]
|
|
17
|
+
* Grouped categories for the picker; omit to hide the picker
|
|
18
|
+
* @param {string} [options.category] - Selected category key
|
|
19
|
+
* @param {(key: string) => void} [options.onCategoryChange]
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.container = options.container;
|
|
23
|
+
this.catalog = options.catalog;
|
|
24
|
+
this.editor = options.editor;
|
|
25
|
+
this.categories = options.categories ?? [];
|
|
26
|
+
this.category = options.category ?? null;
|
|
27
|
+
this.onCategoryChange = options.onCategoryChange ?? (() => {});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
if (!this.container) return;
|
|
32
|
+
const doc = this.container.ownerDocument;
|
|
33
|
+
render(this.container, [
|
|
34
|
+
this.categories.length ? this._pickerSpec() : '',
|
|
35
|
+
{
|
|
36
|
+
style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '6px', overflowY: 'auto', flex: '1 1 auto', alignContent: 'start' },
|
|
37
|
+
children: this.catalog.getAll().map(entry => this._tileSpec(entry, doc)),
|
|
38
|
+
},
|
|
39
|
+
], doc);
|
|
40
|
+
Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', gap: '8px', minHeight: '0' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_pickerSpec() {
|
|
44
|
+
const selected = this.categories.flatMap(g => g.types).find(t => t.key === this.category);
|
|
45
|
+
return {
|
|
46
|
+
tag: 'select',
|
|
47
|
+
attrs: { 'aria-label': 'Diagram type', title: selected?.description },
|
|
48
|
+
style: { width: '100%', padding: '6px', fontSize: '12px', flex: '0 0 auto' },
|
|
49
|
+
on: { change: (e) => this.onCategoryChange(e.target.value) },
|
|
50
|
+
children: this.categories.map(({ group, types }) => ({
|
|
51
|
+
tag: 'optgroup',
|
|
52
|
+
attrs: { label: group },
|
|
53
|
+
children: types.map(t => ({
|
|
54
|
+
tag: 'option',
|
|
55
|
+
attrs: { value: t.key, selected: t.key === this.category, title: t.description },
|
|
56
|
+
text: t.name,
|
|
57
|
+
})),
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_tileSpec(entry, doc) {
|
|
63
|
+
return {
|
|
64
|
+
attrs: { draggable: 'true', title: entry.description ? `${entry.name}: ${entry.description}\nDrag to add` : `Drag to add ${entry.name}` },
|
|
65
|
+
style: {
|
|
66
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px',
|
|
67
|
+
padding: '6px 4px', background: '#fff', border: '1px solid #ddd', borderRadius: '4px',
|
|
68
|
+
cursor: 'grab', userSelect: 'none',
|
|
69
|
+
},
|
|
70
|
+
on: { dragstart: (e) => this._startDrag(e, entry) },
|
|
71
|
+
children: [
|
|
72
|
+
entry.factory('__preview', 0, 0).renderPreview(40, doc),
|
|
73
|
+
{
|
|
74
|
+
text: entry.name,
|
|
75
|
+
style: { fontSize: '11px', color: '#333', textAlign: 'center', lineHeight: '1.2', overflowWrap: 'anywhere' },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_startDrag(e, entry) {
|
|
82
|
+
if (!e.dataTransfer) return;
|
|
83
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
84
|
+
e.dataTransfer.setData('templateType', entry.type);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create the dragged shape at a drop point given in SVG viewBox coordinates,
|
|
89
|
+
* snapping to the grid when it is shown.
|
|
90
|
+
*/
|
|
91
|
+
handleDrop(e, x, y) {
|
|
92
|
+
const templateType = e.dataTransfer.getData('templateType');
|
|
93
|
+
if (!templateType) return false;
|
|
94
|
+
|
|
95
|
+
let snapX = x, snapY = y;
|
|
96
|
+
if (this.editor.showGrid && this.editor.gridSize) {
|
|
97
|
+
const gridSize = this.editor.gridSize;
|
|
98
|
+
snapX = Math.round(x / gridSize) * gridSize;
|
|
99
|
+
snapY = Math.round(y / gridSize) * gridSize;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const template = this.catalog.create(templateType, this._generateId(templateType), snapX, snapY);
|
|
103
|
+
if (!template) return false;
|
|
104
|
+
|
|
105
|
+
if (this.editor.history) {
|
|
106
|
+
this.editor.history.execute(new CreateTemplateCommand(this.editor, template));
|
|
107
|
+
} else {
|
|
108
|
+
this.editor.addTemplate(template);
|
|
109
|
+
}
|
|
110
|
+
this.editor.render();
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_generateId(templateType) {
|
|
115
|
+
let counter = 1;
|
|
116
|
+
while (this.editor.getTemplate(`${templateType}_${counter}`)) counter++;
|
|
117
|
+
return `${templateType}_${counter}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1,59 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TemplateCatalog — registry of template types that can be added to a diagram.
|
|
3
|
-
* Each entry describes a template type and how to create instances.
|
|
4
|
-
*/
|
|
5
|
-
export class TemplateCatalog {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.entries = new Map(); // type -> { name, icon, factory, properties }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Register a template type.
|
|
12
|
-
* @param {string} type - Unique type identifier
|
|
13
|
-
* @param {Object} spec - Template specification
|
|
14
|
-
* @param {string} spec.name - Display name
|
|
15
|
-
* @param {string} spec.icon - Icon/emoji (e.g., "📦")
|
|
16
|
-
* @param {Function} spec.factory - Factory function: (id, x, y) => Template instance
|
|
17
|
-
* @param {Object} spec.properties - Default properties for this type
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* @param {
|
|
51
|
-
* @param {
|
|
52
|
-
* @
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
/**
|
|
2
|
+
* TemplateCatalog — registry of template types that can be added to a diagram.
|
|
3
|
+
* Each entry describes a template type and how to create instances.
|
|
4
|
+
*/
|
|
5
|
+
export class TemplateCatalog {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.entries = new Map(); // type -> { name, icon, factory, properties }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Register a template type.
|
|
12
|
+
* @param {string} type - Unique type identifier
|
|
13
|
+
* @param {Object} spec - Template specification
|
|
14
|
+
* @param {string} spec.name - Display name
|
|
15
|
+
* @param {string} spec.icon - Icon/emoji (e.g., "📦")
|
|
16
|
+
* @param {Function} spec.factory - Factory function: (id, x, y) => Template instance
|
|
17
|
+
* @param {Object} spec.properties - Default properties for this type
|
|
18
|
+
* @param {string} [spec.description] - What it is (the palette shows it on hover)
|
|
19
|
+
*/
|
|
20
|
+
register(type, spec) {
|
|
21
|
+
if (!spec.factory) {
|
|
22
|
+
throw new Error(`TemplateCatalog.register: factory required for type '${type}'`);
|
|
23
|
+
}
|
|
24
|
+
this.entries.set(type, {
|
|
25
|
+
type,
|
|
26
|
+
name: spec.name || type,
|
|
27
|
+
icon: spec.icon || '■',
|
|
28
|
+
factory: spec.factory,
|
|
29
|
+
properties: spec.properties || {},
|
|
30
|
+
...(spec.description && { description: spec.description }),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a template type entry.
|
|
36
|
+
*/
|
|
37
|
+
get(type) {
|
|
38
|
+
return this.entries.get(type);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all entries.
|
|
43
|
+
*/
|
|
44
|
+
getAll() {
|
|
45
|
+
return Array.from(this.entries.values());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a template instance of given type.
|
|
50
|
+
* @param {string} type
|
|
51
|
+
* @param {string} id - Instance id
|
|
52
|
+
* @param {number} x - X position
|
|
53
|
+
* @param {number} y - Y position
|
|
54
|
+
* @returns {Template|null}
|
|
55
|
+
*/
|
|
56
|
+
create(type, id, x, y) {
|
|
57
|
+
const entry = this.get(type);
|
|
58
|
+
if (!entry) return null;
|
|
59
|
+
return entry.factory(id, x, y);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComparedKind — one kind of thing two models are compared on and merged by
|
|
3
|
+
* (their views; a notation's elements and relationships). A subclass per kind
|
|
4
|
+
* says how to list them, what of one counts when two are compared
|
|
5
|
+
* (`content`), what one needs to be brought into another model (the elements
|
|
6
|
+
* a view draws: `requires`), and how to bring it (`bring`, undoable).
|
|
7
|
+
* Things are matched by id, as two versions of one model (an Archi file and
|
|
8
|
+
* its later copy) keep them.
|
|
9
|
+
*/
|
|
10
|
+
export class ComparedKind {
|
|
11
|
+
constructor() {
|
|
12
|
+
if (new.target === ComparedKind) throw new TypeError('ComparedKind is abstract — subclass it');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What a difference of this kind is keyed by ('view', 'element'). @abstract */
|
|
16
|
+
get key() { throw new Error(`${this.constructor.name} must define key`); }
|
|
17
|
+
|
|
18
|
+
/** What they are called in lists ('Views', 'Elements'). @abstract */
|
|
19
|
+
get label() { throw new Error(`${this.constructor.name} must define label`); }
|
|
20
|
+
|
|
21
|
+
/** Every one of them in a model, in order. @abstract */
|
|
22
|
+
items(model) {
|
|
23
|
+
throw new Error(`${this.constructor.name} must implement items()`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One of them by id, or null. */
|
|
27
|
+
get(model, id) {
|
|
28
|
+
return this.items(model).find(item => this.idOf(item) === id) ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
idOf(item) {
|
|
32
|
+
return item.id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What it is called: its name, else its id. */
|
|
36
|
+
nameOf(item) {
|
|
37
|
+
return item.name || item.title || this.idOf(item);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What of it counts when two are compared: plain JSON, nothing generated anew each time a model is read. @abstract */
|
|
41
|
+
content(item) {
|
|
42
|
+
throw new Error(`${this.constructor.name} must implement content()`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The fields of `content` that differ between two of them (none: they are the same). */
|
|
46
|
+
changedFields(mine, theirs) {
|
|
47
|
+
const a = this.content(mine);
|
|
48
|
+
const b = this.content(theirs);
|
|
49
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])]
|
|
50
|
+
.filter(k => JSON.stringify(a[k]) !== JSON.stringify(b[k]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What else it needs in a model to be brought there from `from`: [[kind key, id]] (none here). */
|
|
54
|
+
requires(item, from) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Put a copy of `item` (from `from`) in `model`, in place of the one with
|
|
60
|
+
* its id if there is one. Returns what undoes it.
|
|
61
|
+
* @abstract
|
|
62
|
+
* @returns {() => void}
|
|
63
|
+
*/
|
|
64
|
+
bring(model, item, from) {
|
|
65
|
+
throw new Error(`${this.constructor.name} must implement bring()`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -34,6 +34,17 @@ export class DiagramView extends DiagramEditorState {
|
|
|
34
34
|
return [...this.templates.values()];
|
|
35
35
|
}
|
|
36
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
|
+
|
|
37
48
|
/** An independent copy: shapes, lines, style and documentation are copied; history starts empty (same options). */
|
|
38
49
|
copy(id, title, historyOptions = this.historyOptions) {
|
|
39
50
|
return new DiagramView({
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command } from '../CommandHistory.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MergeModelCommand — things from another model brought into this one
|
|
5
|
+
* (ModelComparison#mergeCommand): each step a ComparedKind's `bring`, in the
|
|
6
|
+
* order given (what is needed first). One undo step; undo takes each back,
|
|
7
|
+
* last first.
|
|
8
|
+
*/
|
|
9
|
+
export class MergeModelCommand extends Command {
|
|
10
|
+
/** A merge is no edit on one view: undoing it shows no other. */
|
|
11
|
+
viewId = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ViewSetModel} model - Merged into
|
|
15
|
+
* @param {ViewSetModel} from - Merged from
|
|
16
|
+
* @param {Array<{kind: ComparedKind, item: Object}>} steps
|
|
17
|
+
* @param {string} [label]
|
|
18
|
+
*/
|
|
19
|
+
constructor(model, from, steps, label = `Merge ${steps.length} item${steps.length === 1 ? '' : 's'} from "${from.name || 'another model'}"`) {
|
|
20
|
+
super(label);
|
|
21
|
+
this.model = model;
|
|
22
|
+
this.from = from;
|
|
23
|
+
this.steps = steps;
|
|
24
|
+
this._undos = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
execute() {
|
|
28
|
+
this._undos = this.steps.map(({ kind, item }) => kind.bring(this.model, item, this.from));
|
|
29
|
+
this.model._notify();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
undo() {
|
|
33
|
+
for (const undo of [...this._undos].reverse()) undo();
|
|
34
|
+
this._undos = [];
|
|
35
|
+
this.model._notify();
|
|
36
|
+
}
|
|
37
|
+
}
|