@peter.prib/renderengine 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/renderengine-workspace.umd.js +6485 -3992
  2. package/dist/renderengine.umd.js +6251 -3877
  3. package/documentation/REFERENCE.md +384 -28
  4. package/package.json +1 -1
  5. package/src/archimateArchiFormat.js +114 -10
  6. package/src/archimateArchive.js +27 -4
  7. package/src/archimateExchangeFormat.js +12 -4
  8. package/src/archimateSpecializations.js +11 -1
  9. package/src/chartCatalog.js +6 -4
  10. package/src/charting/ArchimateDiagramEditor.js +7 -109
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Edge.js +22 -0
  14. package/src/charting/Legend.js +1 -2
  15. package/src/charting/Statistics.js +14 -0
  16. package/src/charting/archimate/ArchimateCanvasEditor.js +21 -5
  17. package/src/charting/archimate/ViewGroupTemplate.js +5 -2
  18. package/src/charting/charts/BarChart.js +59 -8
  19. package/src/charting/charts/HexbinChart.js +15 -0
  20. package/src/charting/charts/LollipopChart.js +42 -7
  21. package/src/charting/decision/DecisionWorkbench.js +71 -4
  22. package/src/charting/diagramModel/DiagramJsonFileFormat.js +32 -0
  23. package/src/charting/diagramModel/DiagramModel.js +39 -0
  24. package/src/charting/diagramModel/DiagramView.js +102 -61
  25. package/src/charting/diagramModel/index.js +11 -10
  26. package/src/charting/diagrams/AddGroupToViewCommand.js +35 -0
  27. package/src/charting/diagrams/ArchimateModel.js +53 -13
  28. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  29. package/src/charting/diagrams/NestInstanceCommand.js +2 -1
  30. package/src/charting/graph/GraphSource.js +72 -0
  31. package/src/charting/modelTree/NavigatorModel.js +22 -8
  32. package/src/charting/report/TemplatedReport.js +27 -377
  33. package/src/charting/report/ViewsReport.js +118 -0
  34. package/src/charting/report/reportWriters.js +496 -0
  35. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  36. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  37. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  38. package/src/charting/templates/C4PersonTemplate.js +35 -0
  39. package/src/charting/templates/C4SystemTemplate.js +32 -0
  40. package/src/charting/templates/C4Template.js +95 -0
  41. package/src/charting/templates/index.js +6 -0
  42. package/src/charting/templates/templateRegistry.js +77 -0
  43. package/src/compute/ComputeBackend.js +132 -0
  44. package/src/compute/CpuBackend.js +209 -0
  45. package/src/compute/WebGpuBackend.js +630 -0
  46. package/src/compute/index.js +5 -0
  47. package/src/compute/selectBackend.js +85 -0
  48. package/src/index.js +3403 -3370
  49. package/src/kernelExpr.js +80 -5
  50. package/src/workspace/parts/ArchimateEditorPart.js +2 -2
  51. package/src/workspace/parts/DiagramEditorPart.js +146 -131
  52. package/src/workspace/parts/ViewsPart.js +106 -0
@@ -1,6 +1,7 @@
1
1
  import { ArchimateNotation } from './archimate.js';
2
2
  import { archiElementType, typeProfileId, typeDisplayName, ARCHI_TYPE_FEATURE, ARCHI_TYPE_PROFILE_PREFIX } from './archimateArchiTypes.js';
3
- import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
3
+ import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
4
+ import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
4
5
  import './charting/diagrams/CanvasView.js'; // registers the canvas view type
5
6
  import './charting/diagrams/SketchView.js'; // registers the sketch view type
6
7
  import { ElementInstance } from './charting/diagrams/ElementInstance.js';
@@ -720,7 +721,39 @@ function _childText(el, tagName) {
720
721
  * @param {Object} archiModel - parsed Archi XML model
721
722
  * @returns {ArchimateModel} with one view per original view (elementIds track which elements appear in each view)
722
723
  * Now applies visual bounds/waypoints per view. */
723
- export function toArchimateModel(archiModel) {
724
+ /** Register any model-level Archi specializations (<profile>) with ArchimateSpecializationRegistry. @private */
725
+ function _registerModelProfiles(archiModel) {
726
+ const profilesById = new Map();
727
+ for (const e of archiModel?.archi?.elements || []) {
728
+ if (e.tag === 'profile' && !String(e.attributes?.id ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX)) {
729
+ const id = e.attributes?.id || e.attributes?.name;
730
+ const name = e.attributes?.name || e.attributes?.id;
731
+ const conceptType = e.attributes?.conceptType;
732
+ const imagePath = e.attributes?.imagePath;
733
+ const archiImagePath = e.attributes?.archiImagePath;
734
+ const profileDef = {
735
+ id,
736
+ name,
737
+ conceptType,
738
+ baseTypes: conceptType ? [_toKebabType(conceptType)] : [],
739
+ iconUrl: imagePath || undefined,
740
+ archiImagePath,
741
+ };
742
+ if (id) profilesById.set(id, profileDef);
743
+ if (name) profilesById.set(name, profileDef);
744
+ ArchimateSpecializationRegistry.register({
745
+ id,
746
+ name,
747
+ baseTypes: profileDef.baseTypes,
748
+ iconUrl: profileDef.iconUrl,
749
+ });
750
+ }
751
+ }
752
+ return profilesById;
753
+ }
754
+
755
+ export function toArchimateModel(archiModel) {
756
+ const profilesById = _registerModelProfiles(archiModel);
724
757
  const elements = [];
725
758
  const relationships = [];
726
759
  const folders = [];
@@ -731,7 +764,7 @@ export function toArchimateModel(archiModel) {
731
764
 
732
765
  // Map all elements first
733
766
  for (const el of archiModel.elements || []) {
734
- const mapped = _mapElement(el);
767
+ const mapped = _mapElement(el, profilesById);
735
768
  elementById.set(el.id, mapped);
736
769
  elements.push(mapped);
737
770
  }
@@ -978,7 +1011,8 @@ function _viewInstances(archiView, { elementById, relationshipById }) {
978
1011
  * Returns `{ elements, relationships, options }` ready for `archimateDiagram()`.
979
1012
  * If `viewId` is omitted, the first view is used.
980
1013
  * Kept as original for backward compatibility: applies visual bounds/features and handles all view visuals. */
981
- export function toRenderEngineModel(archiModel, viewId) {
1014
+ export function toRenderEngineModel(archiModel, viewId) {
1015
+ const profilesById = _registerModelProfiles(archiModel);
982
1016
  const view = viewId
983
1017
  ? archiModel.views.find(v => v.id === viewId)
984
1018
  : archiModel.views[0];
@@ -990,7 +1024,7 @@ export function toRenderEngineModel(archiModel, viewId) {
990
1024
  const relationshipById = new Map();
991
1025
 
992
1026
  for (const el of archiModel.elements || []) {
993
- const mapped = _mapElement(el);
1027
+ const mapped = _mapElement(el, profilesById);
994
1028
  elementById.set(el.id, mapped);
995
1029
  elements.push(mapped);
996
1030
  }
@@ -1020,7 +1054,7 @@ export function toRenderEngineModel(archiModel, viewId) {
1020
1054
  return { elements, relationships, folders, folderLayout, elementFolders, options };
1021
1055
  }
1022
1056
 
1023
- function _mapElement(el) {
1057
+ function _mapElement(el, profilesById = null) {
1024
1058
  const archiType = _toKebabType(el.type);
1025
1059
  const layer = ArchimateNotation.elementLayer({ type: archiType });
1026
1060
  // A type Archi has no class for was written as the nearest one, with its own kept as a feature.
@@ -1032,6 +1066,21 @@ function _mapElement(el) {
1032
1066
  layer: layer !== 'other' ? layer : undefined,
1033
1067
  documentation: el.documentation || undefined,
1034
1068
  };
1069
+ const profileAttr = el.archi?.attributes?.profiles || el.attributes?.profiles;
1070
+ if (profileAttr && !profileAttr.startsWith(ARCHI_TYPE_PROFILE_PREFIX)) {
1071
+ const firstId = profileAttr.trim().split(/\s+/)[0];
1072
+ const profileDef = profilesById?.get(firstId) || ArchimateSpecializationRegistry.resolve(firstId);
1073
+ mapped.specialization = profileDef?.name || firstId;
1074
+ if (profileDef?.iconUrl && !mapped.iconUrl) {
1075
+ mapped.iconUrl = profileDef.iconUrl;
1076
+ }
1077
+ } else if (el.specialization) {
1078
+ mapped.specialization = el.specialization;
1079
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
1080
+ if (spec?.iconUrl && !mapped.iconUrl) {
1081
+ mapped.iconUrl = spec.iconUrl;
1082
+ }
1083
+ }
1035
1084
  const props = _propertiesToTypedArray(el.properties);
1036
1085
  if (props) mapped.properties = props;
1037
1086
  if (el.archiFolder) mapped.archiFolder = el.archiFolder;
@@ -1388,15 +1437,56 @@ export function toArchiExport(model) {
1388
1437
  archi: view.archi,
1389
1438
  children: fromArchimateView(view),
1390
1439
  }));
1440
+ const archiElements = [...(model.archi?.elements ?? [])];
1441
+ const elements = model.elements().map(el => {
1442
+ if (!el.specialization) return el;
1443
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
1444
+ if (!spec) return el;
1445
+ const specId = spec.id.startsWith('id-') ? spec.id : `id-prof-${spec.id}`;
1446
+ const existing = archiElements.find(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name));
1447
+ if (!existing) {
1448
+ archiElements.push({
1449
+ tag: 'profile',
1450
+ attributes: {
1451
+ id: specId,
1452
+ name: spec.name,
1453
+ conceptType: _toPascalType(el.type),
1454
+ ...(spec.iconUrl && { imagePath: spec.iconUrl }),
1455
+ },
1456
+ });
1457
+ } else if (spec.iconUrl && !existing.attributes?.imagePath) {
1458
+ existing.attributes = { ...existing.attributes, imagePath: spec.iconUrl };
1459
+ }
1460
+ if (!el.archi?.attributes?.profiles) {
1461
+ return {
1462
+ ...el,
1463
+ archi: {
1464
+ ...(el.archi ?? {}),
1465
+ attributes: {
1466
+ ...(el.archi?.attributes ?? {}),
1467
+ profiles: specId,
1468
+ },
1469
+ },
1470
+ };
1471
+ }
1472
+ return el;
1473
+ });
1474
+
1475
+ const archi = (model.archi || archiElements.length) ? {
1476
+ ...(model.archi ?? {}),
1477
+ attributes: { ...(model.archi?.attributes ?? {}) },
1478
+ elements: archiElements,
1479
+ } : null;
1480
+
1391
1481
  return [{
1392
1482
  id: model.id || 'model-1',
1393
1483
  name: model.name || 'ArchiMate Model',
1394
1484
  version: '5.0.0',
1395
1485
  documentation: model.documentation,
1396
1486
  properties: model.properties,
1397
- archi: model.archi,
1487
+ archi,
1398
1488
  emptyViewFolders: model.emptyViewFolders ?? [],
1399
- elements: model.elements(),
1489
+ elements,
1400
1490
  relationships: model.relationships(),
1401
1491
  }, views];
1402
1492
  }
@@ -1510,6 +1600,20 @@ export function toArchiXML(model, views) {
1510
1600
  if (original && !el.archi?.attributes?.profiles) {
1511
1601
  profile = typeProfileId(original);
1512
1602
  typeProfiles.set(profile, { name: typeDisplayName(original), conceptType: _toPascalType(type) });
1603
+ } else if (el.specialization && !el.archi?.attributes?.profiles) {
1604
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
1605
+ if (spec) {
1606
+ const specId = spec.id.startsWith('id-') ? spec.id : ('id-prof-' + spec.id);
1607
+ profile = specId;
1608
+ const existing = (model.archi?.elements ?? []).find(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name));
1609
+ if (!existing && !typeProfiles.has(specId)) {
1610
+ typeProfiles.set(specId, {
1611
+ name: spec.name,
1612
+ conceptType: _toPascalType(type),
1613
+ ...(spec.iconUrl && { imagePath: spec.iconUrl }),
1614
+ });
1615
+ }
1616
+ }
1513
1617
  }
1514
1618
  byFolder.get(folder.type).push({ el, type, original, profile });
1515
1619
  }
@@ -1540,8 +1644,8 @@ export function toArchiXML(model, views) {
1540
1644
 
1541
1645
  // What else the model carried (profiles, ...), its own properties, then its documentation (Archi's "purpose").
1542
1646
  lines.push(..._extraElementLines(model.archi?.elements, ' '));
1543
- for (const [id, { name, conceptType }] of typeProfiles) {
1544
- lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"/>`);
1647
+ for (const [id, { name, conceptType, imagePath }] of typeProfiles) {
1648
+ lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"${imagePath ? ` imagePath="${_escapeAttr(imagePath)}"` : ''}/>`);
1545
1649
  }
1546
1650
  lines.push(..._propertyLines(model.properties, ' '));
1547
1651
  if (model.documentation) lines.push(` <purpose>${_escapeXml(model.documentation)}</purpose>`);
@@ -1,4 +1,5 @@
1
- import { parseArchiModel, toArchiXML } from './archimateArchiFormat.js';
1
+ import { parseArchiModel, toArchiXML, toPascalType } from './archimateArchiFormat.js';
2
+ import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
2
3
  import { ZipArchive } from './zip/ZipArchive.js';
3
4
 
4
5
  /**
@@ -95,15 +96,37 @@ export function toArchiFile(model, views) {
95
96
  _eachVisual(copies, (visual) => registerPicture(visual));
96
97
 
97
98
  let modelForXml = model;
98
- if (model?.archi?.elements?.some(e => e.attributes?.imagePath?.startsWith('data:'))) {
99
+ let archiElements = [...(model?.archi?.elements || [])];
100
+ for (const el of model?.elements || []) {
101
+ if (el.specialization) {
102
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
103
+ if (spec?.iconUrl?.startsWith('data:')) {
104
+ const specId = spec.id.startsWith('id-') ? spec.id : ('id-prof-' + spec.id);
105
+ if (!archiElements.some(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name))) {
106
+ archiElements.push({
107
+ tag: 'profile',
108
+ attributes: {
109
+ id: specId,
110
+ name: spec.name,
111
+ conceptType: toPascalType(el.type),
112
+ imagePath: spec.iconUrl,
113
+ },
114
+ });
115
+ }
116
+ }
117
+ }
118
+ }
119
+ if (archiElements.some(e => e.attributes?.imagePath?.startsWith('data:'))) {
99
120
  modelForXml = {
100
121
  ...model,
101
122
  archi: {
102
- ...model.archi,
103
- elements: model.archi.elements.map(e => {
123
+ ...(model?.archi ?? {}),
124
+ attributes: { ...(model?.archi?.attributes ?? {}) },
125
+ elements: archiElements.map(e => {
104
126
  if (!e.attributes?.imagePath?.startsWith('data:')) return e;
105
127
  const clonedAttr = { ...e.attributes };
106
128
  registerPicture(clonedAttr);
129
+ delete clonedAttr.archiImagePath;
107
130
  return { ...e, attributes: clonedAttr };
108
131
  }),
109
132
  },
@@ -144,12 +144,20 @@ function _organizationPaths(root) {
144
144
  return { paths, emptyViewFolders };
145
145
  }
146
146
 
147
+ function _asPropertyPairs(pairs) {
148
+ const list = pairs.map(p => ({ ...p }));
149
+ for (const { key, value } of pairs) {
150
+ if (!(key in list)) list[key] = value;
151
+ }
152
+ return list;
153
+ }
154
+
147
155
  /** An element as parseArchiModel gives it (type: Archi's class name). @private */
148
156
  function _parseElement(el, propertyPairs) {
149
157
  const xsiType = _xsiType(el);
150
- const properties = Object.fromEntries(propertyPairs.map(({ key, value }) => [key, value]));
151
- const ownType = properties[EXCHANGE_TYPE_PROPERTY];
152
- delete properties[EXCHANGE_TYPE_PROPERTY];
158
+ const ownType = propertyPairs.find(p => p.key === EXCHANGE_TYPE_PROPERTY)?.value;
159
+ const filteredPairs = propertyPairs.filter(p => p.key !== EXCHANGE_TYPE_PROPERTY);
160
+ const properties = _asPropertyPairs(filteredPairs);
153
161
  const attributes = {};
154
162
  const elements = [];
155
163
  if (JUNCTION_TYPES[xsiType]) attributes.type = 'or';
@@ -180,7 +188,7 @@ function _parseRelationship(el, propertyPairs) {
180
188
  target: el.getAttribute('target'),
181
189
  ...(el.getAttribute('isDirected') === 'true' && { directed: true }),
182
190
  documentation: _text(el, 'documentation'),
183
- properties: Object.fromEntries(propertyPairs.map(({ key, value }) => [key, value])),
191
+ properties: _asPropertyPairs(propertyPairs),
184
192
  ...(Object.keys(attributes).length && { archi: { attributes, elements: [] } }),
185
193
  };
186
194
  }
@@ -99,6 +99,12 @@ export class ArchimateSpecializationRegistry {
99
99
  /** Register or replace a specialization. */
100
100
  static register(spec) {
101
101
  const s = new ArchimateSpecialization(spec);
102
+ const existingByName = this.list().find(e => e.name === s.name && e.id !== s.id);
103
+ if (existingByName) {
104
+ if (s.iconUrl && !existingByName.iconUrl) existingByName.iconUrl = s.iconUrl;
105
+ if (s.icon && !existingByName.icon) existingByName.icon = s.icon;
106
+ if (s.color && !existingByName.color) existingByName.color = s.color;
107
+ }
102
108
  this._store.set(s.id, s);
103
109
  return s;
104
110
  }
@@ -116,7 +122,11 @@ export class ArchimateSpecializationRegistry {
116
122
  /** Look up by id or by exact name. */
117
123
  static resolve(idOrName) {
118
124
  if (!idOrName) return undefined;
119
- return this._store.get(idOrName) || this.list().find(s => s.name === idOrName);
125
+ const byId = this._store.get(idOrName);
126
+ if (byId) return byId;
127
+ const matches = this.list().filter(s => s.name === idOrName);
128
+ if (!matches.length) return undefined;
129
+ return matches.find(s => s.iconUrl || s.icon) || matches[0];
120
130
  }
121
131
 
122
132
  /** All registered specializations. */
@@ -91,12 +91,13 @@ export const CAMERA_MODE = {
91
91
  export const CHART_CATALOG = [
92
92
  {
93
93
  key: 'bar', label: 'Bar', method: 'barChart', category: '2d',
94
- coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
94
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.GROUPED_FIELD, camera: CAMERA_MODE.NONE,
95
95
  axes: [
96
96
  { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
97
97
  { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y_LENGTH, required: true },
98
+ { role: 'group', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
98
99
  ],
99
- dataShape: 'Array<{label, value}>',
100
+ dataShape: 'Array<{label, value}> — optional `colorCol` (field name or compiled formula) colors bars by group',
100
101
  streamShape: 'flat',
101
102
  },
102
103
  {
@@ -609,14 +610,15 @@ export const CHART_CATALOG = [
609
610
  },
610
611
  {
611
612
  key: 'lollipop', label: 'Lollipop', method: 'lollipopChart', category: '2d',
612
- coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
613
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.GROUPED_FIELD, camera: CAMERA_MODE.NONE,
613
614
  seriesCapable: true,
614
615
  axes: [
615
616
  { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
616
617
  { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y, required: true },
617
618
  { role: 'series', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
619
+ { role: 'group', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
618
620
  ],
619
- dataShape: 'Array<{label, value}> or multi-series Array<{name, values:Array<{label,value}>}> — stem-and-dot alternative to bars',
621
+ dataShape: 'Array<{label, value}> or multi-series Array<{name, values:Array<{label,value}>}> — stem-and-dot alternative to bars; `colorCol` supported on 1D data',
620
622
  streamShape: 'flat',
621
623
  },
622
624
  {
@@ -33,7 +33,8 @@ import { ExchangeFileFormat } from './archimate/ExchangeFileFormat.js';
33
33
  import { ArchimateCsvFileFormat } from './archimate/ArchimateCsvFileFormat.js';
34
34
 
35
35
  /**
36
- * ArchimateDiagramEditor — ArchiMate-specific diagram editor extending DiagramEditor base.
36
+ * ArchimateDiagramEditor — legacy GraphDiagram-based ArchiMate editor extending DiagramEditor.
37
+ * @deprecated Use ArchimateCanvasEditor (or ArchimateCanvasPart) for ArchiMate editing. RenderEngine.archimateDiagram() remains for read-only charts.
37
38
  *
38
39
  * Encapsulates: ArchiMate model, tree navigator, diagram tabs, rendering, controls wiring.
39
40
  * Inherits: common rendering, drag/drop, toolbar, tree, filtering from DiagramEditor.
@@ -140,11 +141,6 @@ export class ArchimateDiagramEditor extends DiagramEditor {
140
141
 
141
142
  // Enable tree drag-drop (delayed to allow DOM to be ready)
142
143
  setTimeout(() => this.enableTreeDragDrop(), 100);
143
-
144
- // Wire controls if not using auto-toolbar
145
- if (!this.features) {
146
- this.wireControls();
147
- }
148
144
  }
149
145
 
150
146
  /**
@@ -1365,19 +1361,10 @@ export class ArchimateDiagramEditor extends DiagramEditor {
1365
1361
  }
1366
1362
 
1367
1363
  /**
1368
- * Get a control value from the DOM.
1364
+ * Get a control value.
1369
1365
  */
1370
1366
  getOption(controlId, defaultValue) {
1371
- if (this._options?.has(controlId)) return super.getOption(controlId, defaultValue);
1372
- // Hosts that bring their own controls (wireControls) keep them in the DOM.
1373
- try {
1374
- const el = this.document.getElementById(controlId);
1375
- if (!el) return defaultValue;
1376
- if (el.type === 'checkbox') return el.checked;
1377
- return el.value || defaultValue;
1378
- } catch {
1379
- return defaultValue;
1380
- }
1367
+ return super.getOption(controlId, defaultValue);
1381
1368
  }
1382
1369
 
1383
1370
  /** Save specializations to localStorage. */
@@ -1571,98 +1558,9 @@ export class ArchimateDiagramEditor extends DiagramEditor {
1571
1558
  }
1572
1559
 
1573
1560
  /**
1574
- * Wire up all control event listeners (version, viewpoint, distinct shapes, etc.).
1575
- * Triggers render on change. Optional: provide custom handler functions.
1561
+ * @deprecated Legacy control wiring. Controls are now auto-rendered into the toolbar via `features`.
1576
1562
  */
1577
- wireControls(options = {}) {
1578
- const {
1579
- onVersionChange,
1580
- onViewpointChange,
1581
- onViewpointModeChange,
1582
- onDistinctShapesChange,
1583
- onShowDerivedChange,
1584
- onManageViewpoints,
1585
- onManageSpecializations,
1586
- populateViewpointSelect,
1587
- } = options;
1588
-
1589
- // Version selector
1590
- const versionEl = this.document.getElementById('archimate-version');
1591
- if (versionEl) {
1592
- versionEl.addEventListener('change', () => {
1593
- if (onVersionChange) onVersionChange();
1594
- this.render();
1595
- });
1596
- }
1597
-
1598
- // Viewpoint selector and mode
1599
- const viewpointEl = this.document.getElementById('archimate-viewpoint');
1600
- const viewpointModeEl = this.document.getElementById('archimate-viewpoint-mode');
1601
- if (viewpointEl) {
1602
- viewpointEl.addEventListener('change', () => {
1603
- if (onViewpointChange) onViewpointChange();
1604
- this.render();
1605
- });
1606
- }
1607
- if (viewpointModeEl) {
1608
- viewpointModeEl.addEventListener('change', () => {
1609
- if (onViewpointModeChange) onViewpointModeChange();
1610
- this.render();
1611
- });
1612
- }
1613
-
1614
- // Distinct shapes checkbox
1615
- const distinctShapesEl = this.document.getElementById('archimate-distinct-shapes');
1616
- if (distinctShapesEl) {
1617
- distinctShapesEl.addEventListener('change', () => {
1618
- if (onDistinctShapesChange) onDistinctShapesChange();
1619
- this.render();
1620
- });
1621
- }
1622
-
1623
- // Show derived relationships checkbox
1624
- const showDerivedEl = this.document.getElementById('archimate-show-derived');
1625
- if (showDerivedEl) {
1626
- showDerivedEl.addEventListener('change', () => {
1627
- if (onShowDerivedChange) onShowDerivedChange();
1628
- this.render();
1629
- });
1630
- }
1631
-
1632
- // Manage viewpoints dialog
1633
- const manageVpEl = this.document.getElementById('archimate-manage-viewpoints');
1634
- if (manageVpEl && onManageViewpoints) {
1635
- manageVpEl.addEventListener('click', onManageViewpoints);
1636
- }
1637
-
1638
- // Manage specializations dialog
1639
- const manageSpecEl = this.document.getElementById('archimate-manage-specializations');
1640
- if (manageSpecEl && onManageSpecializations) {
1641
- manageSpecEl.addEventListener('click', onManageSpecializations);
1642
- }
1643
-
1644
- // Settings dialog
1645
- const settingsEl = this.document.getElementById('archimate-settings');
1646
- if (settingsEl) {
1647
- settingsEl.addEventListener('click', () => {
1648
- this.workspace.showGlobalSettingsDialog(this.document, this.settingsManager, 'default');
1649
- });
1650
- }
1651
-
1652
- // Filter controls
1653
- const filterInput = this.document.getElementById('archimate-filter');
1654
- const filterClearEl = this.document.getElementById('archimate-filter-clear');
1655
- if (filterInput && this.currentEngine) {
1656
- const applyFilter = () => {
1657
- this.currentEngine.filterGraphNodes(filterInput.value);
1658
- };
1659
- filterInput.addEventListener('input', applyFilter);
1660
- if (filterClearEl) {
1661
- filterClearEl.addEventListener('click', () => {
1662
- filterInput.value = '';
1663
- applyFilter();
1664
- });
1665
- }
1666
- }
1563
+ wireControls(_options = {}) {
1564
+ // No-op: toolbar and controls are managed via features and MenuBar
1667
1565
  }
1668
1566
  }
@@ -12,7 +12,8 @@ import {
12
12
  EllipseTemplate, DiamondTemplate, TriangleTemplate, PentagonTemplate, HexagonTemplate,
13
13
  OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate,
14
14
  CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate,
15
- PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate,
15
+ PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate,
16
+ C4PersonTemplate, C4SystemTemplate, C4ContainerTemplate, C4ComponentTemplate, C4BoundaryTemplate,
16
17
  } from './templates/index.js';
17
18
 
18
19
  const C = {
@@ -202,6 +203,24 @@ export const DIAGRAM_TYPES = {
202
203
  ],
203
204
  },
204
205
 
206
+ 'c4-architecture': {
207
+ name: 'C4 Architecture',
208
+ group: 'Architecture',
209
+ description: 'Hierarchical software architecture: Person, System, Container, Component, and Boundaries',
210
+ templates: [
211
+ shape('c4-person', 'Person', C4PersonTemplate, { fillColor: '#08427b', textColor: '#ffffff' }, 'A human user, actor, persona, or stakeholder interacting with systems'),
212
+ shape('c4-system', 'Software System', C4SystemTemplate, { fillColor: '#1168bd', textColor: '#ffffff' }, 'The software system in focus delivering business value'),
213
+ shape('c4-system-external', 'External System', C4SystemTemplate, { external: true, fillColor: '#999999', textColor: '#ffffff' }, 'An external software system outside the boundary of change'),
214
+ shape('c4-container', 'Container', C4ContainerTemplate, { fillColor: '#438dd5', textColor: '#ffffff' }, 'An application, web service, mobile app, or microservice'),
215
+ shape('c4-database', 'Database Container', C4ContainerTemplate, { containerType: 'database', fillColor: '#438dd5', textColor: '#ffffff' }, 'A database, persistence store, cache, or datastore container'),
216
+ shape('c4-component', 'Component', C4ComponentTemplate, { fillColor: '#85bbf0', textColor: '#000000' }, 'A modular component, controller, or service within a container'),
217
+ shape('c4-system-boundary', 'System Boundary', C4BoundaryTemplate, { boundaryType: 'System', zIndex: -1 }, 'Boundary box enclosing containers belonging to a Software System'),
218
+ shape('c4-enterprise-boundary', 'Enterprise Boundary', C4BoundaryTemplate, { boundaryType: 'Enterprise', zIndex: -2 }, 'Boundary box enclosing all systems belonging to an enterprise'),
219
+ shape('c4-container-boundary', 'Container Boundary', C4BoundaryTemplate, { boundaryType: 'Container', zIndex: -1 }, 'Boundary box enclosing components belonging to a Container'),
220
+ note,
221
+ ],
222
+ },
223
+
205
224
  'er-diagram': {
206
225
  name: 'Entity-Relationship',
207
226
  group: 'Data',
@@ -2,26 +2,33 @@ import { ViewPropertiesDialog } from './ViewPropertiesDialog.js';
2
2
  import { inheritableFields, readInheritableFields } from './SettingsCascade.js';
3
3
  import { EDGE_STYLE_PROPERTIES } from './edgeStyle/EdgeStyleSchema.js';
4
4
  import { SetStyleCommand } from './edgeStyle/SetStyleCommand.js';
5
+ import { CompositeCommand } from './CommandHistory.js';
6
+
7
+ const TITLE_POSITIONS = [['none', 'None'], ['top', 'Top'], ['bottom', 'Bottom']];
8
+ const WATERMARK_ROTATIONS = [['0°', 'Horizontal (0°)'], ['-45°', 'Diagonal (-45°)'], ['-90°', 'Vertical (-90°)']];
9
+ const WATERMARK_REPEATS = [['none', 'Center (single)'], ['repeat', 'Repeated grid'], ['tile', 'Tiled pattern']];
5
10
 
6
11
  /**
7
12
  * DiagramViewPropertiesDialog — a template-editor diagram's properties plus
8
- * its line-style defaults: the diagram level of the line-style cascade
9
- * (diagram → model → tool), each property inheriting unless set. The style
10
- * change is one step on the model's undo history. Used by
11
- * DiagramEditorPart.
13
+ * its line-style defaults and visual appearance (background, title band, watermark):
14
+ * the diagram level of the line-style cascade (diagram → model → tool) and
15
+ * the diagram appearance settings. Changes are undoable on the diagram's history.
16
+ * Used by DiagramEditorPart.
12
17
  */
13
18
  export class DiagramViewPropertiesDialog extends ViewPropertiesDialog {
14
19
  /**
15
20
  * @param {Object} options - As ViewPropertiesDialog, plus:
16
21
  * @param {EdgeStyleCascade} options.cascade - The diagram's line-style levels, the diagram's own keyed `levelKey`
17
22
  * @param {string} [options.levelKey] - Which level is this diagram's
18
- * @param {() => void} [options.onStyleChange] - After the style changes (e.g. redraw)
23
+ * @param {() => void} [options.onStyleChange] - After the line style changes
24
+ * @param {() => void} [options.onSettingsChange] - After the appearance settings change
19
25
  */
20
- constructor({ cascade, levelKey = 'view', onStyleChange = () => {}, ...rest }) {
26
+ constructor({ cascade, levelKey = 'view', onStyleChange = () => {}, onSettingsChange = () => {}, ...rest }) {
21
27
  super(rest);
22
28
  this.cascade = cascade;
23
29
  this.levelKey = levelKey;
24
30
  this.onStyleChange = onStyleChange;
31
+ this.onSettingsChange = onSettingsChange;
25
32
  }
26
33
 
27
34
  /** The diagram's own line style (its level's style object). */
@@ -30,17 +37,54 @@ export class DiagramViewPropertiesDialog extends ViewPropertiesDialog {
30
37
  }
31
38
 
32
39
  sections() {
40
+ const ownSettings = this.view.viewSettings ?? {};
33
41
  return [
34
42
  { type: 'heading', label: 'Line style' },
35
43
  ...inheritableFields(EDGE_STYLE_PROPERTIES, this.style, this.cascade.inheritedBy(this.levelKey)),
44
+ { type: 'heading', label: 'Appearance' },
45
+ { key: 'background', label: 'Background', type: 'color', value: ownSettings.background || '#ffffff' },
46
+ { key: 'backgroundImage', label: 'Background image', type: 'text', value: ownSettings.backgroundImage ?? '', placeholder: 'URL (optional)' },
47
+ { key: 'bandTitle', label: 'Title band', type: 'text', value: ownSettings.title ?? '', placeholder: 'Text shown above or below the diagram' },
48
+ { key: 'titlePosition', label: 'Title position', type: 'select', value: ownSettings.titlePosition || 'none', choices: TITLE_POSITIONS },
49
+ { key: 'watermark', label: 'Watermark', type: 'text', value: ownSettings.watermark ?? '', placeholder: 'CONFIDENTIAL, DRAFT, ...' },
50
+ { key: 'watermarkRotation', label: 'Watermark rotation', type: 'select', value: ownSettings.watermarkRotation || '-45°', choices: WATERMARK_ROTATIONS },
51
+ { key: 'watermarkOpacity', label: 'Watermark opacity', type: 'number', min: 0, max: 100, step: 5, value: ownSettings.watermarkOpacity ?? 20 },
52
+ { key: 'watermarkRepeat', label: 'Watermark repeat', type: 'select', value: ownSettings.watermarkRepeat || 'none', choices: WATERMARK_REPEATS },
36
53
  ];
37
54
  }
38
55
 
39
56
  applySections(values) {
57
+ // Line style
40
58
  const patch = readInheritableFields(EDGE_STYLE_PROPERTIES, values);
41
- const changed = Object.entries(patch).some(([k, v]) => this.style[k] !== v);
42
- if (!changed) return;
43
- this.view.history.execute(new SetStyleCommand(this.style, patch, 'Diagram line style'));
44
- this.onStyleChange();
59
+ const styleChanged = Object.entries(patch).some(([k, v]) => this.style[k] !== v);
60
+
61
+ // Appearance
62
+ const ownSettings = this.view.viewSettings ?? (this.view.viewSettings = {});
63
+ const appearancePatch = {};
64
+ if ('background' in values) appearancePatch.background = values.background;
65
+ if ('backgroundImage' in values) appearancePatch.backgroundImage = values.backgroundImage?.trim() || undefined;
66
+ if ('bandTitle' in values) appearancePatch.title = values.bandTitle?.trim() || undefined;
67
+ if ('titlePosition' in values) appearancePatch.titlePosition = values.titlePosition && values.titlePosition !== 'none' ? values.titlePosition : undefined;
68
+ if ('watermark' in values) appearancePatch.watermark = values.watermark?.trim() || undefined;
69
+ if ('watermarkRotation' in values) appearancePatch.watermarkRotation = values.watermarkRotation && values.watermarkRotation !== '-45°' ? values.watermarkRotation : undefined;
70
+ if ('watermarkOpacity' in values) appearancePatch.watermarkOpacity = values.watermarkOpacity != null && Number(values.watermarkOpacity) !== 20 ? Number(values.watermarkOpacity) : undefined;
71
+ if ('watermarkRepeat' in values) appearancePatch.watermarkRepeat = values.watermarkRepeat && values.watermarkRepeat !== 'none' ? values.watermarkRepeat : undefined;
72
+
73
+ const appearanceChanged = Object.entries(appearancePatch).some(([k, v]) => ownSettings[k] !== v);
74
+
75
+ if (styleChanged && appearanceChanged) {
76
+ this.view.history.execute(new CompositeCommand('Diagram properties', [
77
+ new SetStyleCommand(this.style, patch, 'Diagram line style'),
78
+ new SetStyleCommand(ownSettings, appearancePatch, 'Diagram appearance'),
79
+ ]));
80
+ this.onStyleChange();
81
+ this.onSettingsChange();
82
+ } else if (styleChanged) {
83
+ this.view.history.execute(new SetStyleCommand(this.style, patch, 'Diagram line style'));
84
+ this.onStyleChange();
85
+ } else if (appearanceChanged) {
86
+ this.view.history.execute(new SetStyleCommand(ownSettings, appearancePatch, 'Diagram appearance'));
87
+ this.onSettingsChange();
88
+ }
45
89
  }
46
90
  }
@@ -117,4 +117,26 @@ export class Edge {
117
117
  clearWaypoints() {
118
118
  this.waypoints = [];
119
119
  }
120
+
121
+ /** The edge as plain JSON: its endpoints, label, waypoints, side pins and style overrides. */
122
+ toJSON() {
123
+ return {
124
+ id: this.id,
125
+ sourceId: this.sourceId,
126
+ targetId: this.targetId,
127
+ label: this.label,
128
+ waypoints: this.waypoints.map(w => ({ x: w.x, y: w.y })),
129
+ sourceSide: this.sourceSide,
130
+ targetSide: this.targetSide,
131
+ visible: this.visible,
132
+ ...this.styleOverrides(),
133
+ };
134
+ }
135
+
136
+ /** Reconstruct an Edge from plain JSON. */
137
+ static fromJSON(json) {
138
+ if (!json) throw new TypeError('Edge.fromJSON requires a JSON object');
139
+ const { id, sourceId, targetId, ...rest } = json;
140
+ return new Edge(id, sourceId, targetId, rest);
141
+ }
120
142
  }