@peter.prib/renderengine 0.2.2 → 0.2.4

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.
@@ -1,135 +1,135 @@
1
- import { render } from '../dom.js';
2
- import { EditorShell } from '../EditorShell.js';
3
- import { EditorPart } from './EditorPart.js';
4
- import { DiagramEditor } from '../../charting/DiagramEditor.js';
5
- import { MenuBar } from '../../charting/MenuBar.js';
6
- import { DiagramModel } from '../../charting/diagramModel/index.js';
7
- import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
8
- import { LocalJsonStore } from '../../charting/storage/LocalJsonStore.js';
9
- import { EdgeStyleCascade } from '../../charting/edgeStyle/index.js';
10
- import { DiagramViewPropertiesDialog } from '../../charting/DiagramViewPropertiesDialog.js';
11
-
12
- const SVG_NS = 'http://www.w3.org/2000/svg';
13
-
14
- /**
15
- * DiagramEditorPart — a complete diagram editor as a workspace part: menu bar,
16
- * palette, one tab per diagram and the canvas, over a DiagramModel.
17
- *
18
- * Spec (JSON): `templateType` (palette category), `tabOrientation`
19
- * ('horizontal'|'vertical'), `diagramTitle` (first diagram), `toolStyleKey`
20
- * (localStorage key for the tool-level line style; `null` keeps it in memory).
21
- *
22
- * Line styles cascade line → diagram (`view.edgeStyle`) → model
23
- * (`model.edgeStyle`) → tool. Each diagram keeps its own undo history, so undo
24
- * survives switching tabs. The diagrams' menu (new, duplicate, rename, delete)
25
- * comes from EditorPart. Like ChartPart it needs the engine, so it is not in
26
- * BUILTIN_PARTS: register it explicitly.
27
- */
28
- export class DiagramEditorPart extends EditorPart {
29
- static type = 'diagram-editor';
30
- static viewNoun = 'diagram';
31
-
32
- constructor(spec = {}) {
33
- super(spec);
34
- this.tabOrientation = spec.tabOrientation ?? 'horizontal';
35
- // The editor is rebuilt with the canvas on every render, which follows every model change.
36
- this._unsubscribe = this.model.onChange(() => { if (this.mounted) this.render(); });
37
- }
38
-
39
- createModel(spec) {
40
- const store = spec.toolStyleKey === null ? null
41
- : new LocalJsonStore(spec.toolStyleKey ?? 'renderengine.diagramEditor.toolEdgeStyle');
42
- this.toolEdgeStyle = store?.load({}) ?? {};
43
- const model = new DiagramModel({
44
- name: spec.name ?? '',
45
- templateType: spec.templateType ?? 'generic',
46
- // Tool-level style edits go through the diagram's undo history; persist whenever it moves.
47
- historyOptions: { onChange: () => store?.save(this.toolEdgeStyle) },
48
- });
49
- model.createView(spec.diagramTitle ?? 'Diagram 1');
50
- return model;
51
- }
52
-
53
- render() {
54
- const model = this.model;
55
- const view = model.currentView;
56
- const doc = this.document;
57
- const canvasId = `diagram-canvas-${this.id}`;
58
-
59
- const canvas = doc.createElementNS(SVG_NS, 'svg');
60
- canvas.id = canvasId;
61
- canvas.setAttribute('viewBox', '0 0 800 600');
62
- canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: #fff; border: 2px solid #333; box-sizing: border-box;';
63
- const elements = doc.createElementNS(SVG_NS, 'g');
64
- elements.id = 'elements';
65
- canvas.appendChild(elements);
66
-
67
- const tabs = new ViewTabStrip({
68
- orientation: this.tabOrientation,
69
- document: doc,
70
- onSelect: (id) => model.switchTo(id),
71
- menuItems: (id) => this.viewMenu.items(id),
72
- }).render(model.views(), model.currentViewId);
73
-
74
- // The shared workbench layout: toolbar, palette at the side, tabs + canvas in the main area.
75
- const { toolbar: menuBarEl, side: palette, main } = new EditorShell({ document: doc, sideWidth: '200px' }).mount(this.container);
76
- palette.classList.add('ws-diagram-palette');
77
- Object.assign(palette.style, { padding: '8px', fontSize: '12px', display: 'flex', flexDirection: 'column', overflowY: 'hidden' });
78
- main.style.flexDirection = this.tabOrientation === 'vertical' ? 'row' : 'column';
79
- render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
80
-
81
- // The editor is bound to the view's own maps and history: nothing to save back on switch.
82
- const previous = this.editor;
83
- previous?.dispose();
84
- this.editor = new DiagramEditor({
85
- diagramContainerId: canvasId,
86
- model,
87
- templateType: model.templateType,
88
- onTemplateTypeChange: (type) => model.setTemplateType(type),
89
- templates: view.templates,
90
- edges: view.edges,
91
- history: view.history,
92
- arrangement: view.arrangement,
93
- edgeStyleLevels: this.edgeStyleLevels(view),
94
- });
95
- if (previous) {
96
- this.editor.showGrid = previous.showGrid;
97
- this.editor.gridSize = previous.gridSize;
98
- }
99
- this.editor.initPaletteForDiagramType(palette, model.templateType);
100
- this.menuBar?.dispose();
101
- this.menuBar = new MenuBar({
102
- container: menuBarEl,
103
- editor: this.editor,
104
- items: [this.fileMenu(), this.editMenu(), ...this.editor.menuBarItems()],
105
- }).render();
106
- this.editor.render();
107
- }
108
-
109
- /** A diagram's line-style levels, most specific first: the diagram, the model, the tool. */
110
- edgeStyleLevels(view) {
111
- return [
112
- { key: 'view', label: 'Diagram', style: view.edgeStyle },
113
- { key: 'model', label: 'Model', style: this.model.edgeStyle },
114
- { key: 'tool', label: 'Tool', style: this.toolEdgeStyle },
115
- ];
116
- }
117
-
118
- /** A diagram's properties: name, documentation and its line-style defaults. */
119
- showViewProperties(view) {
120
- return new DiagramViewPropertiesDialog({
121
- model: this.model,
122
- view,
123
- document: this.document,
124
- cascade: new EdgeStyleCascade(this.edgeStyleLevels(view)),
125
- // The editor shows the current diagram; a style change there needs a redraw.
126
- onStyleChange: () => { if (view === this.model.currentView) this.editor?.render(); },
127
- }).open();
128
- }
129
-
130
- dispose() {
131
- this.menuBar?.dispose();
132
- this._unsubscribe();
133
- super.dispose();
134
- }
135
- }
1
+ import { render } from '../dom.js';
2
+ import { EditorShell } from '../EditorShell.js';
3
+ import { EditorPart } from './EditorPart.js';
4
+ import { DiagramEditor } from '../../charting/DiagramEditor.js';
5
+ import { MenuBar } from '../../charting/MenuBar.js';
6
+ import { DiagramModel } from '../../charting/diagramModel/index.js';
7
+ import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
8
+ import { LocalJsonStore } from '../../charting/storage/LocalJsonStore.js';
9
+ import { EdgeStyleCascade } from '../../charting/edgeStyle/index.js';
10
+ import { DiagramViewPropertiesDialog } from '../../charting/DiagramViewPropertiesDialog.js';
11
+
12
+ const SVG_NS = 'http://www.w3.org/2000/svg';
13
+
14
+ /**
15
+ * DiagramEditorPart — a complete diagram editor as a workspace part: menu bar,
16
+ * palette, one tab per diagram and the canvas, over a DiagramModel.
17
+ *
18
+ * Spec (JSON): `templateType` (palette category), `tabOrientation`
19
+ * ('horizontal'|'vertical'), `diagramTitle` (first diagram), `toolStyleKey`
20
+ * (localStorage key for the tool-level line style; `null` keeps it in memory).
21
+ *
22
+ * Line styles cascade line → diagram (`view.edgeStyle`) → model
23
+ * (`model.edgeStyle`) → tool. Each diagram keeps its own undo history, so undo
24
+ * survives switching tabs. The diagrams' menu (new, duplicate, rename, delete)
25
+ * comes from EditorPart. Like ChartPart it needs the engine, so it is not in
26
+ * BUILTIN_PARTS: register it explicitly.
27
+ */
28
+ export class DiagramEditorPart extends EditorPart {
29
+ static type = 'diagram-editor';
30
+ static viewNoun = 'diagram';
31
+
32
+ constructor(spec = {}) {
33
+ super(spec);
34
+ this.tabOrientation = spec.tabOrientation ?? 'horizontal';
35
+ // The editor is rebuilt with the canvas on every render, which follows every model change.
36
+ this._unsubscribe = this.model.onChange(() => { if (this.mounted) this.render(); });
37
+ }
38
+
39
+ createModel(spec) {
40
+ const store = spec.toolStyleKey === null ? null
41
+ : new LocalJsonStore(spec.toolStyleKey ?? 'renderengine.diagramEditor.toolEdgeStyle');
42
+ this.toolEdgeStyle = store?.load({}) ?? {};
43
+ const model = new DiagramModel({
44
+ name: spec.name ?? '',
45
+ templateType: spec.templateType ?? 'generic',
46
+ // Tool-level style edits go through the diagram's undo history; persist whenever it moves.
47
+ historyOptions: { onChange: () => store?.save(this.toolEdgeStyle) },
48
+ });
49
+ model.createView(spec.diagramTitle ?? 'Diagram 1');
50
+ return model;
51
+ }
52
+
53
+ render() {
54
+ const model = this.model;
55
+ const view = model.currentView;
56
+ const doc = this.document;
57
+ const canvasId = `diagram-canvas-${this.id}`;
58
+
59
+ const canvas = doc.createElementNS(SVG_NS, 'svg');
60
+ canvas.id = canvasId;
61
+ canvas.setAttribute('viewBox', '0 0 800 600');
62
+ canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: #fff; border: 2px solid #333; box-sizing: border-box;';
63
+ const elements = doc.createElementNS(SVG_NS, 'g');
64
+ elements.id = 'elements';
65
+ canvas.appendChild(elements);
66
+
67
+ const tabs = new ViewTabStrip({
68
+ orientation: this.tabOrientation,
69
+ document: doc,
70
+ onSelect: (id) => model.switchTo(id),
71
+ menuItems: (id) => this.viewMenu.items(id),
72
+ }).render(model.views(), model.currentViewId);
73
+
74
+ // The shared workbench layout: toolbar, palette at the side, tabs + canvas in the main area.
75
+ const { toolbar: menuBarEl, side: palette, main } = new EditorShell({ document: doc, sideWidth: '200px' }).mount(this.container);
76
+ palette.classList.add('ws-diagram-palette');
77
+ Object.assign(palette.style, { padding: '8px', fontSize: '12px', display: 'flex', flexDirection: 'column', overflowY: 'hidden' });
78
+ main.style.flexDirection = this.tabOrientation === 'vertical' ? 'row' : 'column';
79
+ render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
80
+
81
+ // The editor is bound to the view's own maps and history: nothing to save back on switch.
82
+ const previous = this.editor;
83
+ previous?.dispose();
84
+ this.editor = new DiagramEditor({
85
+ diagramContainerId: canvasId,
86
+ model,
87
+ templateType: model.templateType,
88
+ onTemplateTypeChange: (type) => model.setTemplateType(type),
89
+ templates: view.templates,
90
+ edges: view.edges,
91
+ history: view.history,
92
+ arrangement: view.arrangement,
93
+ edgeStyleLevels: this.edgeStyleLevels(view),
94
+ });
95
+ if (previous) {
96
+ this.editor.showGrid = previous.showGrid;
97
+ this.editor.gridSize = previous.gridSize;
98
+ }
99
+ this.editor.initPaletteForDiagramType(palette, model.templateType);
100
+ this.menuBar?.dispose();
101
+ this.menuBar = new MenuBar({
102
+ container: menuBarEl,
103
+ editor: this.editor,
104
+ items: [this.fileMenu(), this.editMenu(), ...this.hostMenus(), ...this.editor.menuBarItems()],
105
+ }).render();
106
+ this.editor.render();
107
+ }
108
+
109
+ /** A diagram's line-style levels, most specific first: the diagram, the model, the tool. */
110
+ edgeStyleLevels(view) {
111
+ return [
112
+ { key: 'view', label: 'Diagram', style: view.edgeStyle },
113
+ { key: 'model', label: 'Model', style: this.model.edgeStyle },
114
+ { key: 'tool', label: 'Tool', style: this.toolEdgeStyle },
115
+ ];
116
+ }
117
+
118
+ /** A diagram's properties: name, documentation and its line-style defaults. */
119
+ showViewProperties(view) {
120
+ return new DiagramViewPropertiesDialog({
121
+ model: this.model,
122
+ view,
123
+ document: this.document,
124
+ cascade: new EdgeStyleCascade(this.edgeStyleLevels(view)),
125
+ // The editor shows the current diagram; a style change there needs a redraw.
126
+ onStyleChange: () => { if (view === this.model.currentView) this.editor?.render(); },
127
+ }).open();
128
+ }
129
+
130
+ dispose() {
131
+ this.menuBar?.dispose();
132
+ this._unsubscribe();
133
+ super.dispose();
134
+ }
135
+ }
@@ -29,7 +29,9 @@ import { NAME_TRANSFORMS } from '../../charting/diagramModel/nameTransforms.js';
29
29
  *
30
30
  * Hosted by a ModelsPart (`ctx.models`: several models open at once, a tab
31
31
  * each), its File menu also offers New model, Open… in a new tab and Close
32
- * model, and its tab follows its file's name and saved state.
32
+ * model, an import opens in a new tab rather than replacing this model, a
33
+ * Tools menu compares and merges models (ModelsPart#toolItems), and its tab
34
+ * follows its file's name and saved state.
33
35
  */
34
36
  export class EditorPart extends Part {
35
37
  /** What a view is called in this part's menus ('view', 'diagram'). */
@@ -113,6 +115,11 @@ export class EditorPart extends Part {
113
115
  return new ToolbarMenu('File', () => this.fileMenuItems());
114
116
  }
115
117
 
118
+ /** The menus beside File and Edit that work across models: Tools, with a ModelsPart. None without. */
119
+ hostMenus() {
120
+ return this.modelsHost ? [new ToolbarMenu('Tools', () => this.modelsHost.toolItems(this))] : [];
121
+ }
122
+
116
123
  /** Edit menu: undo and redo (the editor's), then Find and replace… over the whole model. */
117
124
  editMenu() {
118
125
  return new ToolbarMenu('Edit', () => this.editMenuItems());
@@ -132,8 +139,8 @@ export class EditorPart extends Part {
132
139
  ];
133
140
  }
134
141
 
135
- /** Run a model-wide edit (null: nothing to do) as one undo step, then redraw. @protected */
136
- _runModelEdit(command) {
142
+ /** Run a model-wide edit (null: nothing to do; a rename, a merge) as one undo step, then redraw. */
143
+ runModelEdit(command) {
137
144
  if (!command) return null;
138
145
  (this.editor?.history ?? this.model.history).execute(command);
139
146
  if (this.mounted) this.render();
@@ -162,7 +169,7 @@ export class EditorPart extends Part {
162
169
 
163
170
  /** Transform names (ModelSearch#transformCommand), one undo step. */
164
171
  transformNames(transform, text = '') {
165
- return this._runModelEdit(this.model.search().transformCommand(transform, text));
172
+ return this.runModelEdit(this.model.search().transformCommand(transform, text));
166
173
  }
167
174
 
168
175
  /** Rename property…: a property's name, everywhere in the model. One undo step. */
@@ -184,7 +191,7 @@ export class EditorPart extends Part {
184
191
 
185
192
  /** Rename a property everywhere (ModelSearch#renamePropertyCommand), one undo step. */
186
193
  renameProperty(from, to) {
187
- return this._runModelEdit(this.model.search().renamePropertyCommand(from, to));
194
+ return this.runModelEdit(this.model.search().renamePropertyCommand(from, to));
188
195
  }
189
196
 
190
197
  /**
@@ -260,18 +267,30 @@ export class EditorPart extends Part {
260
267
  ]);
261
268
  }
262
269
 
263
- /** File > Import's items: each format that imports (it replaces the model), then the subclass's own (ownImportItems). */
270
+ /**
271
+ * File > Import's items: each format that imports, then the subclass's own
272
+ * (ownImportItems). An imported model opens in a new tab with a ModelsPart
273
+ * (onImported), else replaces this one.
274
+ */
264
275
  importItems() {
265
276
  return EditorPart.tidyMenu([
266
277
  ...modelFileMenuItems(this.fileFormats(), {
267
278
  model: () => this.model, only: 'import',
268
- onImport: (model) => { this.replaceModel(model); this.file.forget(); },
279
+ onImport: (model) => this.onImported(model),
269
280
  document: () => this.document,
270
281
  }),
271
282
  ...this.ownImportItems(),
272
283
  ]);
273
284
  }
274
285
 
286
+ /** A model imported from a file: in a new tab with a ModelsPart, else in place of this one. Returns the part showing it. */
287
+ onImported(model) {
288
+ if (this.modelsHost) return this.modelsHost.openModel(model);
289
+ this.replaceModel(model);
290
+ this.file.forget();
291
+ return this;
292
+ }
293
+
275
294
  /** File > Export's items: each format, the subclass's own (ownExportItems), then the HTML report and the pictures. */
276
295
  exportItems() {
277
296
  return EditorPart.tidyMenu([
@@ -1,6 +1,7 @@
1
1
  import { Part } from '../Part.js';
2
2
  import { WorkArea } from '../workArea.js';
3
3
  import { el } from '../dom.js';
4
+ import { ModelCompareDialog, ModelMergeDialog } from '../../charting/ModelComparisonDialog.js';
4
5
 
5
6
  /**
6
7
  * ModelsPart — several models open at once (as Archi's model tree holds
@@ -10,7 +11,10 @@ import { el } from '../dom.js';
10
11
  * them as between any two editors.
11
12
  *
12
13
  * The editor parts it hosts find it as `ctx.models` and offer, in their File
13
- * menu, New model, Open… in a new tab and Close model. A tab shows its file's
14
+ * menu, New model, Open… in a new tab and Close model; a model imported from
15
+ * a file opens in a tab of its own. Their Tools menu works across the open
16
+ * models (toolItems): compare one with another, and merge from another into
17
+ * it (ModelComparison). A tab shows its file's
14
18
  * name and • while it has unsaved changes; closing one with unsaved changes
15
19
  * asks first; the page title follows the model in front. The last model
16
20
  * closed leaves a new, empty one.
@@ -74,6 +78,59 @@ export class ModelsPart extends Part {
74
78
  return part;
75
79
  }
76
80
 
81
+ /**
82
+ * Open a model made elsewhere (one imported from a file) in a new tab,
83
+ * as it is: nothing to undo, not yet saved to a file. Returns the part.
84
+ */
85
+ openModel(model) {
86
+ const part = this.newModel();
87
+ part.replaceModel(model);
88
+ this.work.focus(part.id);
89
+ this.fileStateChanged(part);
90
+ return part;
91
+ }
92
+
93
+ /**
94
+ * The Tools menu's items for one open model (`part`): Compare with and
95
+ * Merge from, each a submenu of the other open models.
96
+ */
97
+ toolItems(part) {
98
+ const others = this.models().filter(m => m !== part);
99
+ const each = (act) => (others.length
100
+ ? others.map(other => ({ label: this.labelOf(other), action: () => act(part, other) }))
101
+ : [{ label: 'Open another model first', disabled: true }]);
102
+ return [
103
+ { label: 'Compare with', items: each((a, b) => this.compare(a, b)) },
104
+ { label: 'Merge from', items: each((a, b) => this.merge(a, b)) },
105
+ ];
106
+ }
107
+
108
+ /** How `part`'s model differs from `other`'s, shown (ModelCompareDialog); its Merge… goes on to merge. */
109
+ compare(part, other) {
110
+ return new ModelCompareDialog({
111
+ ...this._comparing(part, other),
112
+ onMerge: () => this.merge(part, other),
113
+ }).open();
114
+ }
115
+
116
+ /** Choose what to bring from `other`'s model into `part`'s, then bring it: one undo step there. */
117
+ merge(part, other) {
118
+ return new ModelMergeDialog({
119
+ ...this._comparing(part, other),
120
+ onMerge: (command) => part.runModelEdit(command),
121
+ }).open();
122
+ }
123
+
124
+ /** @private */
125
+ _comparing(part, other) {
126
+ return {
127
+ comparison: part.model.compareWith(other.model),
128
+ mineName: part.model.name || this.labelOf(part),
129
+ theirsName: other.model.name || this.labelOf(other),
130
+ document: this.container?.ownerDocument ?? globalThis.document,
131
+ };
132
+ }
133
+
77
134
  /**
78
135
  * Ask for a file (as `from`'s Open… does) and open it in a new tab,
79
136
  * leaving `from`'s model as it is. Returns the new part, or null.