@peter.prib/renderengine 0.15.1 → 0.15.2

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.
@@ -38083,6 +38083,67 @@
38083
38083
  }
38084
38084
  }
38085
38085
 
38086
+ /**
38087
+ * AddGroupToViewCommand — add a view-specific group to a view: its layout entry
38088
+ * is stored in view.folderLayout. Undo removes it.
38089
+ */
38090
+ class AddGroupToViewCommand extends Command {
38091
+ /**
38092
+ * @param {ArchimateView} view
38093
+ * @param {Object} [options]
38094
+ * @param {string} [options.id]
38095
+ * @param {string} [options.name]
38096
+ * @param {number} [options.x]
38097
+ * @param {number} [options.y]
38098
+ * @param {number} [options.width]
38099
+ * @param {number} [options.height]
38100
+ * @param {number} [options.zIndex]
38101
+ * @param {Object} [options.appearance]
38102
+ * @param {string} [options.parentId]
38103
+ */
38104
+ constructor(view, { id = `group-${Date.now().toString(36)}`, name = 'Group', x = 100, y = 100, width = 400, height = 140, zIndex = 0, appearance = {}, parentId = undefined } = {}) {
38105
+ super('Add group to view');
38106
+ this.view = view;
38107
+ this.groupId = id;
38108
+ this.entry = { x, y, width, height, name, zIndex, appearance, parentId };
38109
+ }
38110
+
38111
+ execute() {
38112
+ this.view.folderLayout[this.groupId] = { ...this.entry };
38113
+ }
38114
+
38115
+ undo() {
38116
+ delete this.view.folderLayout[this.groupId];
38117
+ }
38118
+ }
38119
+
38120
+ /**
38121
+ * RemoveGroupFromViewCommand — take a group off a view: its layout entry
38122
+ * goes, and what was drawn in it stays where it is (on the view, or in the
38123
+ * group around it). The model folder stays. Undo puts the same entry back.
38124
+ */
38125
+ class RemoveGroupFromViewCommand extends Command {
38126
+ /**
38127
+ * @param {ArchimateView} view
38128
+ * @param {string} folderId - The folder the group draws
38129
+ */
38130
+ constructor(view, folderId) {
38131
+ super('Remove group from view');
38132
+ this.view = view;
38133
+ this.folderId = folderId;
38134
+ this._entry = null;
38135
+ }
38136
+
38137
+ execute() {
38138
+ this._entry = this.view.folderLayout[this.folderId] ?? null;
38139
+ delete this.view.folderLayout[this.folderId];
38140
+ }
38141
+
38142
+ undo() {
38143
+ if (this._entry) this.view.folderLayout[this.folderId] = this._entry;
38144
+ }
38145
+ }
38146
+
38086
38147
  /**
38087
38148
  * Canonical, id-indexed home for ArchiMate model data (elements, relationships, views).
38088
38149
  * Replaces ad-hoc flat-array handling and fixes multi-view import flattening.
@@ -39236,27 +39297,65 @@
39236
39297
  }
39237
39298
 
39238
39299
  /**
39239
- * The folders drawn on this view as groups: those with bounds in its
39240
- * `folderLayout`, bounds applied, each after its parent. `parentId` is the
39241
- * nearest ancestor that is also drawn here (a group inside an undrawn
39242
- * folder sits on the view itself).
39300
+ * The folders drawn on this view as groups: both model repository folders
39301
+ * and view-specific groups with bounds in `folderLayout`, bounds applied,
39302
+ * each after its parent. `parentId` is the nearest ancestor also drawn here.
39243
39303
  */
39244
39304
  get placedFolders() {
39245
- const placed = this.model.allFolders().filter(f => this.folderLayout[f.id]);
39246
- const drawn = new Set(placed.map(f => f.id));
39247
- return placed.map(folder => ({
39248
- ...folder,
39249
- ...this.folderLayout[folder.id],
39250
- parentId: this._nearestPlaced(folder.parentId, drawn),
39251
- }));
39305
+ const drawn = new Set(Object.keys(this.folderLayout));
39306
+ const modelFolders = this.model.allFolders();
39307
+ const modelFolderIds = new Set(modelFolders.map(f => f.id));
39308
+
39309
+ const result = [];
39310
+ for (const folder of modelFolders.filter(f => this.folderLayout[f.id])) {
39311
+ result.push({
39312
+ ...folder,
39313
+ ...this.folderLayout[folder.id],
39314
+ name: this.folderLayout[folder.id].name ?? folder.name,
39315
+ parentId: this._nearestPlaced(folder.parentId, drawn),
39316
+ });
39317
+ }
39318
+ for (const [id, layout] of Object.entries(this.folderLayout)) {
39319
+ if (!modelFolderIds.has(id)) {
39320
+ result.push({
39321
+ id,
39322
+ name: layout.name || 'Group',
39323
+ ...layout,
39324
+ parentId: layout.parentId && drawn.has(layout.parentId) ? layout.parentId : undefined,
39325
+ });
39326
+ }
39327
+ }
39328
+ return result;
39252
39329
  }
39253
39330
 
39254
- /** The group on this view an element is drawn in: its folder, or that folder's nearest drawn ancestor. */
39331
+ /** The group on this view an element is drawn in: its explicit view parent group, or its model folder's nearest drawn ancestor. */
39255
39332
  placedFolderOf(elementId) {
39256
- const drawn = new Set(this.model.allFolders().filter(f => this.folderLayout[f.id]).map(f => f.id));
39333
+ const inst = this.elementInstancesFor(elementId)[0];
39334
+ if (inst?.archi?.parent && this.folderLayout[inst.archi.parent]) {
39335
+ return inst.archi.parent;
39336
+ }
39337
+ if (inst?.parentId && this.folderLayout[inst.parentId]) {
39338
+ return inst.parentId;
39339
+ }
39340
+ const drawn = new Set(Object.keys(this.folderLayout));
39257
39341
  return this._nearestPlaced(this.model.getElementFolder(elementId), drawn);
39258
39342
  }
39259
39343
 
39344
+ /** Add a view-specific group to this view. One undo step (AddGroupToViewCommand). */
39345
+ addGroup(spec = {}) {
39346
+ const id = spec.id || `group-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
39347
+ const cmd = new AddGroupToViewCommand(this, { id, ...spec });
39348
+ this.history.execute(cmd);
39349
+ return this.folderLayout[id];
39350
+ }
39351
+
39352
+ /** Remove a group (view-specific or placed model folder) from this view. One undo step (RemoveGroupFromViewCommand). */
39353
+ removeGroup(groupId) {
39354
+ if (!this.folderLayout[groupId]) return false;
39355
+ this.history.execute(new RemoveGroupFromViewCommand(this, groupId));
39356
+ return true;
39357
+ }
39358
+
39260
39359
  /** @private */
39261
39360
  _nearestPlaced(folderId, drawn) {
39262
39361
  for (let id = folderId; id; id = this.model.getFolder(id)?.parentId) {
@@ -40390,6 +40489,28 @@
40390
40489
  clearWaypoints() {
40391
40490
  this.waypoints = [];
40392
40491
  }
40492
+
40493
+ /** The edge as plain JSON: its endpoints, label, waypoints, side pins and style overrides. */
40494
+ toJSON() {
40495
+ return {
40496
+ id: this.id,
40497
+ sourceId: this.sourceId,
40498
+ targetId: this.targetId,
40499
+ label: this.label,
40500
+ waypoints: this.waypoints.map(w => ({ x: w.x, y: w.y })),
40501
+ sourceSide: this.sourceSide,
40502
+ targetSide: this.targetSide,
40503
+ visible: this.visible,
40504
+ ...this.styleOverrides(),
40505
+ };
40506
+ }
40507
+
40508
+ /** Reconstruct an Edge from plain JSON. */
40509
+ static fromJSON(json) {
40510
+ if (!json) throw new TypeError('Edge.fromJSON requires a JSON object');
40511
+ const { id, sourceId, targetId, ...rest } = json;
40512
+ return new Edge(id, sourceId, targetId, rest);
40513
+ }
40393
40514
  }
40394
40515
 
40395
40516
  /** Style keys the line's Archi type bits decide (dashes, arrows). */
@@ -48490,7 +48611,8 @@
48490
48611
  execute() {
48491
48612
  const { instance, parent } = this;
48492
48613
  const { parent: _was, ...rest } = instance.archi ?? {};
48493
- instance.archi = parent ? { ...rest, parent: parent.visualId } : (Object.keys(rest).length ? rest : null);
48614
+ const parentId = parent ? (parent.visualId ?? parent.id) : null;
48615
+ instance.archi = parentId ? { ...rest, parent: parentId } : (Object.keys(rest).length ? rest : null);
48494
48616
  if (parent && (instance.zIndex ?? 0) <= (parent.zIndex ?? 0)) instance.zIndex = (parent.zIndex ?? 0) + 1;
48495
48617
  }
48496
48618
 
@@ -48615,8 +48737,11 @@
48615
48737
  return this.view.model.getFolder(this.id);
48616
48738
  }
48617
48739
 
48618
- get name() { return this.folder?.name ?? 'Group'; }
48619
- set name(value) { if (this.folder) this.folder.name = value || 'Group'; }
48740
+ get name() { return this.folder?.name ?? this.layout?.name ?? 'Group'; }
48741
+ set name(value) {
48742
+ if (this.folder) this.folder.name = value || 'Group';
48743
+ if (this.layout) this.layout.name = value || 'Group';
48744
+ }
48620
48745
 
48621
48746
  // The layout keeps the top-left corner; a template is placed by its centre.
48622
48747
  get x() { return this.layout.x + this.width / 2; }
@@ -48704,33 +48829,6 @@
48704
48829
  }
48705
48830
  }
48706
48831
 
48707
- /**
48708
- * RemoveGroupFromViewCommand — take a group off a view: its layout entry
48709
- * goes, and what was drawn in it stays where it is (on the view, or in the
48710
- * group around it). The model folder stays. Undo puts the same entry back.
48711
- */
48712
- class RemoveGroupFromViewCommand extends Command {
48713
- /**
48714
- * @param {ArchimateView} view
48715
- * @param {string} folderId - The folder the group draws
48716
- */
48717
- constructor(view, folderId) {
48718
- super('Remove group from view');
48719
- this.view = view;
48720
- this.folderId = folderId;
48721
- this._entry = null;
48722
- }
48723
-
48724
- execute() {
48725
- this._entry = this.view.folderLayout[this.folderId] ?? null;
48726
- delete this.view.folderLayout[this.folderId];
48727
- }
48728
-
48729
- undo() {
48730
- if (this._entry) this.view.folderLayout[this.folderId] = this._entry;
48731
- }
48732
- }
48733
-
48734
48832
  /**
48735
48833
  * ElementFilterRule — a filter's result on an ArchiMate view: shapes of the
48736
48834
  * model elements it kept are drawn as usual, every other element dimmed.
@@ -50343,15 +50441,28 @@
50343
50441
  return element ? treeLayerOf(element) : 'other';
50344
50442
  }
50345
50443
 
50346
- /** An element's box holds any box but a group; on a canvas, a block holds boxes. */
50444
+ /** An element's box holds any box; a group holds any box or nested group; on a canvas, a block holds boxes. */
50347
50445
  canHold(parentId, childId) {
50446
+ if (!parentId || !childId || parentId === childId) return false;
50348
50447
  const parent = this.templates.get(parentId), child = this.templates.get(childId);
50349
- if (!parent?.instance || !child?.instance || parent instanceof ViewGroupTemplate || child instanceof ViewGroupTemplate) return false;
50448
+ if (!parent || !child) return false;
50449
+ if (child instanceof ViewGroupTemplate && parent instanceof ViewGroupTemplate) {
50450
+ return !this.isAncestor(childId, parentId);
50451
+ }
50452
+ if (parent instanceof ViewGroupTemplate) {
50453
+ return !this.isAncestor(childId, parentId);
50454
+ }
50455
+ if (child instanceof ViewGroupTemplate) return false;
50456
+ if (!parent.instance || !child.instance) return false;
50350
50457
  return !!this._elementOf(parentId) || parent.instance.content?.archiVisualType === 'canvas-model-block';
50351
50458
  }
50352
50459
 
50353
- /** An element dropped on another nests there when a relationship a nesting stands for (NESTING_KINDS) is allowed between them. */
50460
+ /** An element dropped on another nests there when a relationship a nesting stands for (NESTING_KINDS) is allowed between them, or when dropped into a group. */
50354
50461
  nestsOnDrop(parentId, childId) {
50462
+ const parent = this.templates.get(parentId);
50463
+ if (parent instanceof ViewGroupTemplate) {
50464
+ return this.canHold(parentId, childId);
50465
+ }
50355
50466
  if (!this._elementOf(parentId) || !this._elementOf(childId)) return false;
50356
50467
  return this.allowedKinds(parentId, childId).some(kind => NESTING_KINDS.includes(kind));
50357
50468
  }
@@ -50359,13 +50470,16 @@
50359
50470
  nestCommand(childId, parentId) {
50360
50471
  const child = this.templates.get(childId)?.instance;
50361
50472
  if (!child) return null;
50362
- return new NestInstanceCommand(child, parentId ? this.templates.get(parentId)?.instance ?? null : null);
50473
+ const parentTemplate = parentId ? this.templates.get(parentId) : null;
50474
+ const parentTarget = parentTemplate?.instance ?? (parentTemplate instanceof ViewGroupTemplate ? parentTemplate : null);
50475
+ return new NestInstanceCommand(child, parentTarget);
50363
50476
  }
50364
50477
 
50365
- /** After nesting an element in another: offer the relationship the nesting stands for (nestingKinds). */
50478
+ /** After nesting an element in another: offer the relationship the nesting stands for (nestingKinds). Groups have no relationship. */
50366
50479
  nested(changes) {
50367
50480
  let chain = Promise.resolve();
50368
50481
  for (const { childId, parentId } of changes) {
50482
+ if (this.templates.get(parentId) instanceof ViewGroupTemplate) continue;
50369
50483
  chain = chain.then(() => this.offerNestingRelationship(parentId, childId)); // taken out: nothing to offer
50370
50484
  }
50371
50485
  return chain;
@@ -54004,64 +54118,159 @@
54004
54118
  new DrawioFileFormat(),
54005
54119
  ]);
54006
54120
 
54007
- /**
54008
- * DiagramView — one diagram (tab) of a DiagramModel: its shapes, lines,
54009
- * diagram-level line style and its own undo history (from DiagramEditorState).
54010
- *
54011
- * A DiagramEditor is bound directly to `templates`/`edges`, so edits land here
54012
- * with nothing to save back, and the history survives the editor being rebuilt.
54013
- */
54014
- class DiagramView extends DiagramEditorState {
54015
- /**
54016
- * @param {Object} options
54017
- * @param {string} options.id
54018
- * @param {string} options.title
54019
- * @param {Map<string, Template>} [options.templates]
54020
- * @param {Map<string, Edge>} [options.edges]
54021
- * @param {Object} [options.edgeStyle] - Diagram-level line style defaults
54022
- * @param {Object} [options.historyOptions] - Passed to CommandHistory (e.g. onChange)
54023
- * @param {string} [options.documentation]
54024
- */
54025
- constructor({ id, title, templates = new Map(), edges = new Map(), edgeStyle = {}, historyOptions = {}, documentation = undefined, arrangement = null }) {
54026
- super({ historyOptions, arrangement });
54027
- this.historyOptions = historyOptions;
54028
- this.id = id;
54029
- this.title = title;
54030
- this.documentation = documentation;
54031
- this.templates = templates;
54032
- this.edges = edges;
54033
- this.edgeStyle = edgeStyle;
54034
- }
54035
-
54036
- /** The shapes are what stack. */
54037
- layerItems() {
54038
- return [...this.templates.values()];
54039
- }
54040
-
54041
- /** The diagram as plain JSON: its shapes (by id), lines, line style and documentation. */
54042
- toJSON() {
54043
- return {
54044
- title: this.title,
54045
- documentation: this.documentation,
54046
- templates: [...this.templates].map(([id, t]) => ({ id, type: t.constructor.name, name: t.name, data: t.serialize() })),
54047
- edges: [...this.edges.values()].map(e => JSON.parse(JSON.stringify(e))),
54048
- edgeStyle: JSON.parse(JSON.stringify(this.edgeStyle ?? {})),
54049
- };
54050
- }
54051
-
54052
- /** An independent copy: shapes, lines, style and documentation are copied; history starts empty (same options). */
54053
- copy(id, title, historyOptions = this.historyOptions) {
54054
- return new DiagramView({
54055
- id,
54056
- title,
54057
- templates: new Map([...this.templates].map(([tid, t]) => [tid, new t.constructor(tid, t.name, t.serialize())])),
54058
- edges: new Map([...this.edges].map(([eid, e]) => [eid, e.clone()])),
54059
- edgeStyle: { ...this.edgeStyle },
54060
- documentation: this.documentation,
54061
- historyOptions,
54062
- arrangement: this.arrangement.copy(), // template ids are kept
54063
- });
54064
- }
54121
+ const TEMPLATE_CLASSES = Object.freeze({
54122
+ 'rectangle': RectangleTemplate,
54123
+ 'rounded-rectangle': RoundedRectangleTemplate,
54124
+ 'stadium': StadiumTemplate,
54125
+ 'predefined-process': PredefinedProcessTemplate,
54126
+ 'class': ClassTemplate,
54127
+ 'component': ComponentTemplate,
54128
+ 'swimlane': SwimlaneTemplate,
54129
+ 'circle': CircleTemplate,
54130
+ 'final-state': FinalStateTemplate,
54131
+ 'ellipse': EllipseTemplate,
54132
+ 'diamond': DiamondTemplate,
54133
+ 'triangle': TriangleTemplate,
54134
+ 'pentagon': PentagonTemplate,
54135
+ 'hexagon': HexagonTemplate,
54136
+ 'octagon': OctagonTemplate,
54137
+ 'star': StarTemplate,
54138
+ 'parallelogram': ParallelogramTemplate,
54139
+ 'trapezoid': TrapezoidTemplate,
54140
+ 'manual-input': ManualInputTemplate,
54141
+ 'cylinder': CylinderTemplate,
54142
+ 'document': DocumentTemplate,
54143
+ 'note': NoteTemplate,
54144
+ 'cloud': CloudTemplate,
54145
+ 'actor': ActorTemplate,
54146
+ 'package': PackageTemplate,
54147
+ 'box': BoxTemplate,
54148
+ 'data-store': DataStoreTemplate,
54149
+ 'lifeline': LifelineTemplate,
54150
+ });
54151
+
54152
+ const CLASS_TO_KEY = new Map(Object.entries(TEMPLATE_CLASSES).map(([k, c]) => [c, k]));
54153
+
54154
+ // Assign static templateType to each class
54155
+ for (const [type, cls] of Object.entries(TEMPLATE_CLASSES)) {
54156
+ cls.templateType = type;
54157
+ }
54158
+
54159
+ /** Returns the stable serialized type key for a template instance or template class. */
54160
+ function templateTypeKey(templateOrClass) {
54161
+ if (!templateOrClass) return 'rectangle';
54162
+ const c = typeof templateOrClass === 'function' ? templateOrClass : templateOrClass.constructor;
54163
+ if (CLASS_TO_KEY.has(c)) return CLASS_TO_KEY.get(c);
54164
+ if (Object.hasOwn(c, 'templateType')) return c.templateType;
54165
+ const name = c.name || '';
54166
+ const withoutSuffix = name.replace(/Template$/, '');
54167
+ const kebab = withoutSuffix.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
54168
+ return TEMPLATE_CLASSES[kebab] ? kebab : 'rectangle';
54169
+ }
54170
+
54171
+ /** Resolves a template class by its stable type key or class name, defaulting to RectangleTemplate. */
54172
+ function templateClassFor(key) {
54173
+ if (!key) return RectangleTemplate;
54174
+ if (TEMPLATE_CLASSES[key]) return TEMPLATE_CLASSES[key];
54175
+ const lower = String(key).toLowerCase();
54176
+ if (TEMPLATE_CLASSES[lower]) return TEMPLATE_CLASSES[lower];
54177
+ for (const [k, c] of Object.entries(TEMPLATE_CLASSES)) {
54178
+ if (c.name === key || c.name?.toLowerCase() === lower) return c;
54179
+ }
54180
+ return RectangleTemplate;
54181
+ }
54182
+
54183
+ /**
54184
+ * DiagramView — one diagram (tab) of a DiagramModel: its shapes, lines,
54185
+ * diagram-level line style and its own undo history (from DiagramEditorState).
54186
+ *
54187
+ * A DiagramEditor is bound directly to `templates`/`edges`, so edits land here
54188
+ * with nothing to save back, and the history survives the editor being rebuilt.
54189
+ */
54190
+ class DiagramView extends DiagramEditorState {
54191
+ /**
54192
+ * @param {Object} options
54193
+ * @param {string} options.id
54194
+ * @param {string} options.title
54195
+ * @param {Map<string, Template>} [options.templates]
54196
+ * @param {Map<string, Edge>} [options.edges]
54197
+ * @param {Object} [options.edgeStyle] - Diagram-level line style defaults
54198
+ * @param {Object} [options.historyOptions] - Passed to CommandHistory (e.g. onChange)
54199
+ * @param {string} [options.documentation]
54200
+ * @param {Arrangement} [options.arrangement]
54201
+ */
54202
+ constructor({ id, title, templates = new Map(), edges = new Map(), edgeStyle = {}, historyOptions = {}, documentation = undefined, arrangement = null }) {
54203
+ super({ historyOptions, arrangement });
54204
+ this.historyOptions = historyOptions;
54205
+ this.id = id;
54206
+ this.title = title;
54207
+ this.documentation = documentation;
54208
+ this.templates = templates;
54209
+ this.edges = edges;
54210
+ this.edgeStyle = edgeStyle;
54211
+ }
54212
+
54213
+ /** The shapes are what stack. */
54214
+ layerItems() {
54215
+ return [...this.templates.values()];
54216
+ }
54217
+
54218
+ /** The diagram as plain JSON: its shapes (by id and stable type), lines, line style and documentation. */
54219
+ toJSON() {
54220
+ return {
54221
+ id: this.id,
54222
+ title: this.title,
54223
+ documentation: this.documentation,
54224
+ templates: [...this.templates].map(([id, t]) => ({
54225
+ id,
54226
+ type: templateTypeKey(t),
54227
+ name: t.name,
54228
+ data: t.serialize(),
54229
+ })),
54230
+ edges: [...this.edges.values()].map(e => (typeof e.toJSON === 'function' ? e.toJSON() : JSON.parse(JSON.stringify(e)))),
54231
+ edgeStyle: JSON.parse(JSON.stringify(this.edgeStyle ?? {})),
54232
+ ...(this.arrangement && { arrangement: this.arrangement.toJSON() }),
54233
+ };
54234
+ }
54235
+
54236
+ /** Reconstruct a DiagramView from plain JSON. */
54237
+ static fromJSON(json, historyOptions = {}) {
54238
+ if (!json) throw new TypeError('DiagramView.fromJSON requires a JSON object');
54239
+ const templates = new Map();
54240
+ for (const t of (json.templates ?? [])) {
54241
+ const Class = templateClassFor(t.type);
54242
+ templates.set(t.id, new Class(t.id, t.name, t.data ?? {}));
54243
+ }
54244
+ const edges = new Map();
54245
+ for (const e of (json.edges ?? [])) {
54246
+ edges.set(e.id, Edge.fromJSON(e));
54247
+ }
54248
+ const arrangement = json.arrangement ? Arrangement.fromJSON(json.arrangement) : null;
54249
+ return new DiagramView({
54250
+ id: json.id,
54251
+ title: json.title,
54252
+ templates,
54253
+ edges,
54254
+ edgeStyle: json.edgeStyle ?? {},
54255
+ documentation: json.documentation,
54256
+ historyOptions,
54257
+ arrangement,
54258
+ });
54259
+ }
54260
+
54261
+ /** An independent copy: shapes, lines, style and documentation are copied; history starts empty (same options). */
54262
+ copy(id, title, historyOptions = this.historyOptions) {
54263
+ return new DiagramView({
54264
+ id,
54265
+ title,
54266
+ templates: new Map([...this.templates].map(([tid, t]) => [tid, new t.constructor(tid, t.name, t.serialize())])),
54267
+ edges: new Map([...this.edges].map(([eid, e]) => [eid, e.clone()])),
54268
+ edgeStyle: { ...this.edgeStyle },
54269
+ documentation: this.documentation,
54270
+ historyOptions,
54271
+ arrangement: this.arrangement.copy(), // template ids are kept
54272
+ });
54273
+ }
54065
54274
  }
54066
54275
 
54067
54276
  /**
@@ -54119,6 +54328,75 @@
54119
54328
  setTemplateType(templateType) {
54120
54329
  this.templateType = templateType;
54121
54330
  }
54331
+
54332
+ /** The diagram model as plain JSON: its name, templateType, line style defaults, active view and views. */
54333
+ toJSON() {
54334
+ return {
54335
+ version: 1,
54336
+ name: this.name,
54337
+ templateType: this.templateType,
54338
+ edgeStyle: JSON.parse(JSON.stringify(this.edgeStyle ?? {})),
54339
+ currentViewId: this.currentViewId,
54340
+ views: this.views().map(v => v.toJSON()),
54341
+ };
54342
+ }
54343
+
54344
+ /** Reconstruct a DiagramModel from plain JSON. */
54345
+ static fromJSON(json, historyOptions = {}) {
54346
+ if (!json) throw new TypeError('DiagramModel.fromJSON requires a JSON object');
54347
+ const model = new DiagramModel({
54348
+ name: json.name ?? '',
54349
+ templateType: json.templateType ?? 'generic',
54350
+ edgeStyle: json.edgeStyle ?? {},
54351
+ historyOptions,
54352
+ });
54353
+ const views = (json.views ?? []).map(vData => DiagramView.fromJSON(vData, model.historyOptions));
54354
+ for (const view of views) {
54355
+ model._insertView(model._adopt(view), { current: view.id === json.currentViewId });
54356
+ }
54357
+ if (json.currentViewId && model._views.has(json.currentViewId)) {
54358
+ model.currentViewId = json.currentViewId;
54359
+ } else if (model._views.size > 0 && !model.currentViewId) {
54360
+ model.currentViewId = model._views.keys().next().value;
54361
+ }
54362
+ const maxNum = Math.max(0, ...model.views().map(v => {
54363
+ const m = String(v.id).match(/\d+$/);
54364
+ return m ? parseInt(m[0], 10) : 0;
54365
+ }));
54366
+ model._nextId = maxNum + 1;
54367
+ model.markSaved();
54368
+ return model;
54369
+ }
54370
+ }
54371
+
54372
+ /**
54373
+ * DiagramJsonFileFormat — a diagram model as a file (`.diagram.json` or `.json`):
54374
+ * its name, palette category, line style defaults, views with templates, edges,
54375
+ * waypoints and arrangements (DiagramModel#toJSON), read back by DiagramModel.fromJSON.
54376
+ */
54377
+ class DiagramJsonFileFormat extends ModelFileFormat {
54378
+ get label() { return '.diagram.json'; }
54379
+ get accept() { return '.diagram.json,.json'; }
54380
+ get fileName() { return 'diagrams.diagram.json'; }
54381
+ get featureName() { return 'DiagramJson'; }
54382
+
54383
+ /** JSON with a diagram model's shape: templateType and views array, or views array of diagrams. */
54384
+ recognizes(text) {
54385
+ try {
54386
+ const json = JSON.parse(text);
54387
+ return !!json && typeof json === 'object' && (Array.isArray(json.views) || typeof json.templateType === 'string');
54388
+ } catch {
54389
+ return false;
54390
+ }
54391
+ }
54392
+
54393
+ export(model) {
54394
+ return { data: JSON.stringify(model.toJSON(), null, 2), mimeType: 'application/json' };
54395
+ }
54396
+
54397
+ import(text) {
54398
+ return DiagramModel.fromJSON(JSON.parse(text));
54399
+ }
54122
54400
  }
54123
54401
 
54124
54402
  /**
@@ -55548,7 +55826,8 @@
55548
55826
  }
55549
55827
 
55550
55828
  /**
55551
- * ArchimateDiagramEditor — ArchiMate-specific diagram editor extending DiagramEditor base.
55829
+ * ArchimateDiagramEditor — legacy GraphDiagram-based ArchiMate editor extending DiagramEditor.
55830
+ * @deprecated Use ArchimateCanvasEditor (or ArchimateCanvasPart) for ArchiMate editing. RenderEngine.archimateDiagram() remains for read-only charts.
55552
55831
  *
55553
55832
  * Encapsulates: ArchiMate model, tree navigator, diagram tabs, rendering, controls wiring.
55554
55833
  * Inherits: common rendering, drag/drop, toolbar, tree, filtering from DiagramEditor.
@@ -72008,126 +72287,139 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
72008
72287
  }
72009
72288
  }
72010
72289
 
72011
- const SVG_NS$1 = 'http://www.w3.org/2000/svg';
72012
-
72013
- /**
72014
- * DiagramEditorPart — a complete diagram editor as a workspace part: menu bar,
72015
- * palette, one tab per diagram and the canvas, over a DiagramModel.
72016
- *
72017
- * Spec (JSON): `templateType` (palette category), `tabOrientation`
72018
- * ('horizontal'|'vertical'), `diagramTitle` (first diagram), `toolStyleKey`
72019
- * (localStorage key for the tool-level line style; `null` keeps it in memory).
72020
- *
72021
- * Line styles cascade line → diagram (`view.edgeStyle`) → model
72022
- * (`model.edgeStyle`) → tool. Each diagram keeps its own undo history, so undo
72023
- * survives switching tabs. The diagrams' menu (new, duplicate, rename, delete)
72024
- * comes from EditorPart. Like ChartPart it needs the engine, so it is not in
72025
- * BUILTIN_PARTS: register it explicitly.
72026
- */
72027
- class DiagramEditorPart extends EditorPart {
72028
- static type = 'diagram-editor';
72029
- static viewNoun = 'diagram';
72030
-
72031
- constructor(spec = {}) {
72032
- super(spec);
72033
- this.tabOrientation = spec.tabOrientation ?? 'horizontal';
72034
- // The editor is rebuilt with the canvas on every render, which follows every model change.
72035
- this._unsubscribe = this.model.onChange(() => { if (this.mounted) this.render(); });
72036
- }
72037
-
72038
- createModel(spec) {
72039
- const store = spec.toolStyleKey === null ? null
72040
- : new LocalJsonStore(spec.toolStyleKey ?? 'renderengine.diagramEditor.toolEdgeStyle');
72041
- this.toolEdgeStyle = store?.load({}) ?? {};
72042
- const model = new DiagramModel({
72043
- name: spec.name ?? '',
72044
- templateType: spec.templateType ?? 'generic',
72045
- // Tool-level style edits go through the diagram's undo history; persist whenever it moves.
72046
- historyOptions: { onChange: () => store?.save(this.toolEdgeStyle) },
72047
- });
72048
- model.createView(spec.diagramTitle ?? 'Diagram 1');
72049
- return model;
72050
- }
72051
-
72052
- render() {
72053
- const model = this.model;
72054
- const view = model.currentView;
72055
- const doc = this.document;
72056
- const canvasId = `diagram-canvas-${this.id}`;
72057
-
72058
- const canvas = doc.createElementNS(SVG_NS$1, 'svg');
72059
- canvas.id = canvasId;
72060
- canvas.setAttribute('viewBox', '0 0 800 600');
72061
- canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: #fff; border: 2px solid #333; box-sizing: border-box;';
72062
- const elements = doc.createElementNS(SVG_NS$1, 'g');
72063
- elements.id = 'elements';
72064
- canvas.appendChild(elements);
72065
-
72066
- const tabs = this.viewTabs({ orientation: this.tabOrientation });
72067
-
72068
- // The shared workbench layout: toolbar, palette at the side, tabs + canvas in the main area.
72069
- const { toolbar: menuBarEl, side: palette, main } = new EditorShell({ document: doc, sideWidth: '200px', layout: this.layout, labels: { side: 'Palette' } }).mount(this.container);
72070
- palette.classList.add('ws-diagram-palette');
72071
- Object.assign(palette.style, { padding: '8px', fontSize: '12px', display: 'flex', flexDirection: 'column', overflowY: 'hidden' });
72072
- main.style.flexDirection = this.tabOrientation === 'vertical' ? 'row' : 'column';
72073
- render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
72074
-
72075
- // The editor is bound to the view's own maps and history: nothing to save back on switch.
72076
- const previous = this.editor;
72077
- previous?.dispose();
72078
- this.editor = new DiagramEditor({
72079
- diagramContainerId: canvasId,
72080
- model,
72081
- templateType: model.templateType,
72082
- onTemplateTypeChange: (type) => model.setTemplateType(type),
72083
- templates: view.templates,
72084
- edges: view.edges,
72085
- history: view.history,
72086
- arrangement: view.arrangement,
72087
- edgeStyleLevels: this.edgeStyleLevels(view),
72088
- });
72089
- if (previous) {
72090
- this.editor.showGrid = previous.showGrid;
72091
- this.editor.gridSize = previous.gridSize;
72092
- this.editor.showExportArea = previous.showExportArea;
72093
- this.editor.exportOptions = previous.exportOptions;
72094
- }
72095
- this.editor.initPaletteForDiagramType(palette, model.templateType);
72096
- this.menuBar?.dispose();
72097
- this.menuBar = new MenuBar({
72098
- container: menuBarEl,
72099
- editor: this.editor,
72100
- items: [this.fileMenu(), this.editMenu(), ...this.hostMenus(), ...this.editor.menuBarItems()],
72101
- }).render();
72102
- this.editor.render();
72103
- }
72104
-
72105
- /** A diagram's line-style levels, most specific first: the diagram, the model, the tool. */
72106
- edgeStyleLevels(view) {
72107
- return [
72108
- { key: 'view', label: 'Diagram', style: view.edgeStyle },
72109
- { key: 'model', label: 'Model', style: this.model.edgeStyle },
72110
- { key: 'tool', label: 'Tool', style: this.toolEdgeStyle },
72111
- ];
72112
- }
72113
-
72114
- /** A diagram's properties: name, documentation and its line-style defaults. */
72115
- showViewProperties(view) {
72116
- return new DiagramViewPropertiesDialog({
72117
- model: this.model,
72118
- view,
72119
- document: this.document,
72120
- cascade: new EdgeStyleCascade(this.edgeStyleLevels(view)),
72121
- // The editor shows the current diagram; a style change there needs a redraw.
72122
- onStyleChange: () => { if (view === this.model.currentView) this.editor?.render(); },
72123
- }).open();
72124
- }
72125
-
72126
- dispose() {
72127
- this.menuBar?.dispose();
72128
- this._unsubscribe();
72129
- super.dispose();
72130
- }
72290
+ const SVG_NS$1 = 'http://www.w3.org/2000/svg';
72291
+
72292
+ /**
72293
+ * DiagramEditorPart — a complete diagram editor as a workspace part: menu bar,
72294
+ * palette, one tab per diagram and the canvas, over a DiagramModel.
72295
+ *
72296
+ * Spec (JSON): `templateType` (palette category), `tabOrientation`
72297
+ * ('horizontal'|'vertical'), `diagramTitle` (first diagram), `toolStyleKey`
72298
+ * (localStorage key for the tool-level line style; `null` keeps it in memory).
72299
+ *
72300
+ * Line styles cascade line → diagram (`view.edgeStyle`) → model
72301
+ * (`model.edgeStyle`) → tool. Each diagram keeps its own undo history, so undo
72302
+ * survives switching tabs. The diagrams' menu (new, duplicate, rename, delete)
72303
+ * comes from EditorPart. Like ChartPart it needs the engine, so it is not in
72304
+ * BUILTIN_PARTS: register it explicitly.
72305
+ */
72306
+ class DiagramEditorPart extends EditorPart {
72307
+ static type = 'diagram-editor';
72308
+ static viewNoun = 'diagram';
72309
+
72310
+ constructor(spec = {}) {
72311
+ super(spec);
72312
+ this.tabOrientation = spec.tabOrientation ?? 'horizontal';
72313
+ // The editor is rebuilt with the canvas on every render, which follows every model change.
72314
+ this._unsubscribe = this.model.onChange(() => { if (this.mounted) this.render(); });
72315
+ }
72316
+
72317
+ createModel(spec) {
72318
+ if (spec.model instanceof DiagramModel) return spec.model;
72319
+ const store = spec.toolStyleKey === null ? null
72320
+ : new LocalJsonStore(spec.toolStyleKey ?? 'renderengine.diagramEditor.toolEdgeStyle');
72321
+ this.toolEdgeStyle = store?.load({}) ?? {};
72322
+ const historyOptions = { onChange: () => store?.save(this.toolEdgeStyle) };
72323
+ if (spec.views) {
72324
+ return DiagramModel.fromJSON(spec, historyOptions);
72325
+ }
72326
+ const model = new DiagramModel({
72327
+ name: spec.name ?? '',
72328
+ templateType: spec.templateType ?? 'generic',
72329
+ // Tool-level style edits go through the diagram's undo history; persist whenever it moves.
72330
+ historyOptions,
72331
+ });
72332
+ model.createView(spec.diagramTitle ?? 'Diagram 1');
72333
+ return model;
72334
+ }
72335
+
72336
+ fileFormats() {
72337
+ return this.spec.fileFormats ? [new DiagramJsonFileFormat()] : [];
72338
+ }
72339
+
72340
+ saveFormat() {
72341
+ return this.fileFormats()[0];
72342
+ }
72343
+
72344
+ render() {
72345
+ const model = this.model;
72346
+ const view = model.currentView;
72347
+ const doc = this.document;
72348
+ const canvasId = `diagram-canvas-${this.id}`;
72349
+
72350
+ const canvas = doc.createElementNS(SVG_NS$1, 'svg');
72351
+ canvas.id = canvasId;
72352
+ canvas.setAttribute('viewBox', '0 0 800 600');
72353
+ canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: #fff; border: 2px solid #333; box-sizing: border-box;';
72354
+ const elements = doc.createElementNS(SVG_NS$1, 'g');
72355
+ elements.id = 'elements';
72356
+ canvas.appendChild(elements);
72357
+
72358
+ const tabs = this.viewTabs({ orientation: this.tabOrientation });
72359
+
72360
+ // The shared workbench layout: toolbar, palette at the side, tabs + canvas in the main area.
72361
+ const { toolbar: menuBarEl, side: palette, main } = new EditorShell({ document: doc, sideWidth: '200px', layout: this.layout, labels: { side: 'Palette' } }).mount(this.container);
72362
+ palette.classList.add('ws-diagram-palette');
72363
+ Object.assign(palette.style, { padding: '8px', fontSize: '12px', display: 'flex', flexDirection: 'column', overflowY: 'hidden' });
72364
+ main.style.flexDirection = this.tabOrientation === 'vertical' ? 'row' : 'column';
72365
+ render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
72366
+
72367
+ // The editor is bound to the view's own maps and history: nothing to save back on switch.
72368
+ const previous = this.editor;
72369
+ previous?.dispose();
72370
+ this.editor = new DiagramEditor({
72371
+ diagramContainerId: canvasId,
72372
+ model,
72373
+ templateType: model.templateType,
72374
+ onTemplateTypeChange: (type) => model.setTemplateType(type),
72375
+ templates: view.templates,
72376
+ edges: view.edges,
72377
+ history: view.history,
72378
+ arrangement: view.arrangement,
72379
+ edgeStyleLevels: this.edgeStyleLevels(view),
72380
+ });
72381
+ if (previous) {
72382
+ this.editor.showGrid = previous.showGrid;
72383
+ this.editor.gridSize = previous.gridSize;
72384
+ this.editor.showExportArea = previous.showExportArea;
72385
+ this.editor.exportOptions = previous.exportOptions;
72386
+ }
72387
+ this.editor.initPaletteForDiagramType(palette, model.templateType);
72388
+ this.menuBar?.dispose();
72389
+ this.menuBar = new MenuBar({
72390
+ container: menuBarEl,
72391
+ editor: this.editor,
72392
+ items: [this.fileMenu(), this.editMenu(), ...this.hostMenus(), ...this.editor.menuBarItems()],
72393
+ }).render();
72394
+ this.editor.render();
72395
+ }
72396
+
72397
+ /** A diagram's line-style levels, most specific first: the diagram, the model, the tool. */
72398
+ edgeStyleLevels(view) {
72399
+ return [
72400
+ { key: 'view', label: 'Diagram', style: view.edgeStyle },
72401
+ { key: 'model', label: 'Model', style: this.model.edgeStyle },
72402
+ { key: 'tool', label: 'Tool', style: this.toolEdgeStyle },
72403
+ ];
72404
+ }
72405
+
72406
+ /** A diagram's properties: name, documentation and its line-style defaults. */
72407
+ showViewProperties(view) {
72408
+ return new DiagramViewPropertiesDialog({
72409
+ model: this.model,
72410
+ view,
72411
+ document: this.document,
72412
+ cascade: new EdgeStyleCascade(this.edgeStyleLevels(view)),
72413
+ // The editor shows the current diagram; a style change there needs a redraw.
72414
+ onStyleChange: () => { if (view === this.model.currentView) this.editor?.render(); },
72415
+ }).open();
72416
+ }
72417
+
72418
+ dispose() {
72419
+ this.menuBar?.dispose();
72420
+ this._unsubscribe();
72421
+ super.dispose();
72422
+ }
72131
72423
  }
72132
72424
 
72133
72425
  /**
@@ -72168,8 +72460,8 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
72168
72460
  });
72169
72461
 
72170
72462
  /**
72171
- * ArchimateEditorPart — the ArchiMate modeller as a workbench part: toolbar,
72172
- * model tree, one tab per open view and the canvas, filling its pane.
72463
+ * ArchimateEditorPart — legacy GraphDiagram-based ArchiMate editor part.
72464
+ * @deprecated Use ArchimateCanvasPart for ArchiMate editing.
72173
72465
  *
72174
72466
  * Spec (JSON): `elements`, `relationships`, `views` (as ArchimateModel takes
72175
72467
  * them), `features` (toolbar controls, see DEFAULT_FEATURES), `analysis`
@@ -73023,6 +73315,7 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
73023
73315
  exports.DiagramEditor = DiagramEditor;
73024
73316
  exports.DiagramEditorPart = DiagramEditorPart;
73025
73317
  exports.DiagramEditorState = DiagramEditorState;
73318
+ exports.DiagramJsonFileFormat = DiagramJsonFileFormat;
73026
73319
  exports.DiagramModel = DiagramModel;
73027
73320
  exports.DiagramTabs = DiagramTabs;
73028
73321
  exports.DiagramView = DiagramView;
@@ -73222,6 +73515,7 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
73222
73515
  exports.StructuredType = StructuredType;
73223
73516
  exports.SurfaceChart3D = SurfaceChart3D;
73224
73517
  exports.SwimlaneTemplate = SwimlaneTemplate;
73518
+ exports.TEMPLATE_CLASSES = TEMPLATE_CLASSES;
73225
73519
  exports.THEME_VARS = THEME_VARS;
73226
73520
  exports.TRACKERS = TRACKERS;
73227
73521
  exports.TRACKER_APIS = TRACKER_APIS;
@@ -73375,6 +73669,8 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
73375
73669
  exports.stackingOrder = stackingOrder;
73376
73670
  exports.strongerEmphasis = strongerEmphasis;
73377
73671
  exports.svgMarkupToPngBlob = svgMarkupToPngBlob;
73672
+ exports.templateClassFor = templateClassFor;
73673
+ exports.templateTypeKey = templateTypeKey;
73378
73674
  exports.toArchiExport = toArchiExport;
73379
73675
  exports.toArchiFile = toArchiFile;
73380
73676
  exports.toArchimateModel = toArchimateModel;