@peter.prib/renderengine 0.2.5 → 0.2.7

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 (67) hide show
  1. package/README.md +219 -213
  2. package/REFERENCE.md +272 -6
  3. package/dist/renderengine-workspace.umd.js +11980 -7421
  4. package/dist/renderengine.umd.js +8991 -5784
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +534 -0
  7. package/package.json +70 -68
  8. package/src/bian.js +182 -0
  9. package/src/charting/CheckboxTreeSelector.js +373 -304
  10. package/src/charting/DiagramEditor.js +53 -32
  11. package/src/charting/DiagramTypeRegistry.js +13 -3
  12. package/src/charting/ModelCheckDialog.js +63 -0
  13. package/src/charting/ModelChecker.js +26 -0
  14. package/src/charting/ViewTabStrip.js +94 -81
  15. package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
  16. package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
  17. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  18. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  19. package/src/charting/datamodel/DataModel.js +645 -0
  20. package/src/charting/datamodel/DataModelChecker.js +92 -0
  21. package/src/charting/datamodel/DataModelCompared.js +127 -0
  22. package/src/charting/datamodel/DataModelContext.js +108 -0
  23. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  24. package/src/charting/datamodel/DataModelPerspective.js +129 -0
  25. package/src/charting/datamodel/DataType.js +108 -0
  26. package/src/charting/datamodel/EnumerationType.js +51 -0
  27. package/src/charting/datamodel/LevelMapping.js +38 -0
  28. package/src/charting/datamodel/LevelSchema.js +41 -0
  29. package/src/charting/datamodel/ServiceDomain.js +177 -0
  30. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  31. package/src/charting/datamodel/StructuredType.js +63 -0
  32. package/src/charting/datamodel/index.js +15 -0
  33. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  34. package/src/charting/diagramModel/OpenViewList.js +75 -0
  35. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  36. package/src/charting/diagrams/DataModelDiagram.js +160 -86
  37. package/src/charting/diagrams/DataModelRelationshipStrategy.js +94 -38
  38. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  39. package/src/charting/diagrams/GraphDiagram.js +11 -1
  40. package/src/charting/diagrams/NodeGeometry.js +169 -158
  41. package/src/charting/diagrams/nesting.js +31 -0
  42. package/src/charting/glyphs/PersonGlyph.js +24 -23
  43. package/src/charting/modelTree/ModelTree.js +202 -164
  44. package/src/charting/modelTree/ModelTreePanel.js +36 -3
  45. package/src/charting/modelTree/TreeViewMode.js +131 -110
  46. package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
  47. package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
  48. package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
  49. package/src/datamodel.js +132 -122
  50. package/src/diagramCatalog.js +35 -30
  51. package/src/index.js +2666 -2659
  52. package/src/workspace/EditorShell.js +166 -67
  53. package/src/workspace/PanelLayout.js +56 -0
  54. package/src/workspace/index.js +1 -0
  55. package/src/workspace/layout/TabsNode.js +205 -191
  56. package/src/workspace/parts/ArchimateCanvasPart.js +10 -9
  57. package/src/workspace/parts/ChartPart.js +6 -1
  58. package/src/workspace/parts/DataModelPart.js +544 -0
  59. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  60. package/src/workspace/parts/EditorPart.js +461 -407
  61. package/src/workspace/parts/ModelFile.js +216 -187
  62. package/src/workspace/parts/ModelsPart.js +483 -192
  63. package/src/workspace/storage/BackupFolder.js +145 -0
  64. package/src/workspace/storage/KeyValueStore.js +92 -0
  65. package/src/workspace/storage/ModelLibrary.js +65 -0
  66. package/src/workspace/workArea.js +417 -413
  67. package/src/yaml/YamlReader.js +372 -0
@@ -24,6 +24,16 @@ import {
24
24
  const EDGE_HANDLE_ATTR = 'data-edge-handle';
25
25
 
26
26
  export class GraphDiagram extends Diagram {
27
+ /**
28
+ * Pure. Whether a node without a picture shows centred text: its icon text,
29
+ * or (standing in for a name drawn below the shape) its initials. A node
30
+ * whose name is written inside it needs no initials.
31
+ */
32
+ static showsInitials(node, defaultLabelPosition = 'below') {
33
+ if (node.icon) return true;
34
+ return (node.labelPosition || (node.shape === 'note' ? 'inside' : defaultLabelPosition)) !== 'inside';
35
+ }
36
+
27
37
  static diagramType = 'graph';
28
38
 
29
39
  constructor(renderer, nodes, edges, options = {}, engine = null, folders = []) {
@@ -522,7 +532,7 @@ export class GraphDiagram extends Diagram {
522
532
  if (node.iconUrl) {
523
533
  const iconSize = nodeR * 1.3;
524
534
  g.appendChild(this.renderer.image(node.iconUrl, -iconSize / 2, -iconSize / 2, iconSize, iconSize, { 'pointer-events': 'none' }));
525
- } else {
535
+ } else if (GraphDiagram.showsInitials(node, defaultLabelPosition)) {
526
536
  const centerText = node.icon || resolvedLabel.slice(0, 2).toUpperCase() || (node.id ?? '').slice(0, 2).toUpperCase();
527
537
  const fontSize = node.icon ? Math.max(12, Math.round(nodeR * 1.1)) : 10;
528
538
  g.appendChild(this.renderer.text(centerText, 0, 0, { fill: '#fff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: `${fontSize}px`, fontWeight: node.icon ? 'normal' : 'bold', 'pointer-events': 'none' }));
@@ -1,158 +1,169 @@
1
- import { Glyph, glyphFor, drawGlyph } from '../glyphs/index.js';
2
- import { GraphMetrics } from './GraphMetrics.js';
3
- import { LabelHelper } from './LabelHelper.js';
4
-
5
- /**
6
- * NodeGeometry — pure, stateless helpers for graph node sizing, corners,
7
- * resize-handle math, and table-node geometry.
8
- *
9
- * These used to live on RenderEngine; they are collected here so diagram
10
- * classes can compute node geometry without going through the engine façade.
11
- */
12
- export class NodeGeometry {
13
- static TABLE_NODE = { width: 170, headerH: 26, rowH: 18, pad: 6 };
14
-
15
- /** Bounding box a node occupies, centred on its (x,y) — {w:2R,h:2R} for
16
- * the plain default (sized by GraphMetrics.effectiveNodeRadius when a
17
- * `sizeDomain` is supplied), the real card size for a `shape:'table'`
18
- * node (see NodeGeometry.tableNodeSize), or, for any OTHER shape, an
19
- * explicit `node.width`/`node.height` override for a wider-than-tall box. */
20
- static nodeSize(node, R, sizeDomain, sizeOptions) {
21
- if (node?.shape === 'table') return NodeGeometry.tableNodeSize(node);
22
- const radius = GraphMetrics ? GraphMetrics.effectiveNodeRadius(node, R, sizeDomain, sizeOptions) : R;
23
- if (Number.isFinite(node?.width) || Number.isFinite(node?.height)) {
24
- return {
25
- w: Number.isFinite(node.width) ? node.width : radius * 2,
26
- h: Number.isFinite(node.height) ? node.height : radius * 2,
27
- };
28
- }
29
- return { w: radius * 2, h: radius * 2 };
30
- }
31
-
32
- /** Size of a `shape:'table'` node — column-count-driven, never affected
33
- * by node.size/sizeDomain. */
34
- static tableNodeSize(node) {
35
- const { width, headerH, rowH, pad } = NodeGeometry.TABLE_NODE;
36
- const cols = node.columns?.length ?? 0;
37
- return { w: node.width || width, h: headerH + cols * rowH + pad };
38
- }
39
-
40
- /** World-space Y of a named column's row within a table node, for edge anchoring. */
41
- static tableColumnY(node, posEntry, size, colName) {
42
- const { headerH, rowH } = NodeGeometry.TABLE_NODE;
43
- const idx = Math.max(0, (node.columns ?? []).findIndex(c => c.name === colName));
44
- return posEntry.y - size.h / 2 + headerH + idx * rowH + rowH / 2;
45
- }
46
-
47
- /** Draws a table-card node's header + one row per column into wrapper `g`. */
48
- static buildTableNodeContent(renderer, g, node) {
49
- const { headerH, rowH } = NodeGeometry.TABLE_NODE;
50
- const { w, h } = NodeGeometry.tableNodeSize(node);
51
- const cols = node.columns ?? [];
52
- const x0 = -w / 2, y0 = -h / 2;
53
- const borderColor = node.selected ? '#4f8ef7' : '#cfd8dc';
54
- const borderWidth = node.selected ? 2 : 1;
55
-
56
- const TYPE_COLORS = { NUMBER: '#74b9ff', TEXT: '#a29bfe', TIMESTAMP: '#55efc4', BOOLEAN: '#ffeaa7', JSON: '#fd79a8', UNKNOWN: '#dfe6e9' };
57
- g.appendChild(renderer.rect(x0, y0, w, h, '#ffffff', { color: borderColor, width: borderWidth }));
58
- g.appendChild(renderer.rect(x0, y0, w, headerH, '#1976d2', null));
59
- g.appendChild(renderer.text(LabelHelper.resolveNodeLabel(node), x0 + 8, y0 + headerH / 2, {
60
- fill: '#fff', textAnchor: 'start', verticalAlign: 'middle', fontSize: '11px', fontWeight: 'bold', 'pointer-events': 'none',
61
- }));
62
- if (node.rowCount != null) {
63
- g.appendChild(renderer.text(String(node.rowCount), x0 + w - 8, y0 + headerH / 2, {
64
- fill: '#cfe3fb', textAnchor: 'end', verticalAlign: 'middle', fontSize: '9px', 'pointer-events': 'none',
65
- }));
66
- }
67
-
68
- cols.forEach((col, i) => {
69
- const rowY = y0 + headerH + i * rowH;
70
- g.appendChild(renderer.circle(x0 + 12, rowY + rowH / 2, 3.5, TYPE_COLORS[col.typeGroup] ?? TYPE_COLORS.UNKNOWN, null));
71
- g.appendChild(renderer.text(col.name, x0 + 20, rowY + rowH / 2, {
72
- fill: '#333', textAnchor: 'start', verticalAlign: 'middle', fontSize: '10px', 'pointer-events': 'none',
73
- }));
74
- let badgeX = x0 + w - 8;
75
- if (col.isFK) {
76
- g.appendChild(renderer.text('FK', badgeX, rowY + rowH / 2, {
77
- fill: '#e67e22', textAnchor: 'end', verticalAlign: 'middle', fontSize: '8px', fontWeight: 'bold', 'pointer-events': 'none',
78
- }));
79
- badgeX -= 18;
80
- }
81
- if (col.isPK) {
82
- g.appendChild(renderer.text('PK', badgeX, rowY + rowH / 2, {
83
- fill: '#00b894', textAnchor: 'end', verticalAlign: 'middle', fontSize: '8px', fontWeight: 'bold', 'pointer-events': 'none',
84
- }));
85
- }
86
- });
87
- }
88
-
89
- /** Pure. The point at a given corner of a `size` (`{w,h}`) box centred on
90
- * `pos` — shared by the resize handle's screen positioning and a node's
91
- * icon-badge placement. */
92
- static cornerPoint(pos, size, corner) {
93
- return {
94
- x: pos.x + (corner.includes('right') ? size.w / 2 : -size.w / 2),
95
- y: pos.y + (corner.includes('bottom') ? size.h / 2 : -size.h / 2),
96
- };
97
- }
98
-
99
- /** Pure. Generic 3-tier corner resolution — 'top-left'|'top-right'|
100
- * 'bottom-left'|'bottom-right': a per-node `node[nodeKey]` override,
101
- * then a per-shape entry in `byShape` (keyed by `node.shape`, default
102
- * shape 'circle'), then `defaultCorner`. */
103
- static resolveNodeCorner(node, nodeKey, byShape, defaultCorner) {
104
- if (node?.[nodeKey]) return node[nodeKey];
105
- const shapeCorner = byShape?.[node?.shape || 'circle'];
106
- return shapeCorner || defaultCorner;
107
- }
108
-
109
- /** Pure. Which corner a node's resize handle sits at/drags from. */
110
- static nodeResizeCorner(node, resizeCornerByShape, defaultCorner = 'top-right') {
111
- return NodeGeometry.resolveNodeCorner(node, 'resizeCorner', resizeCornerByShape, defaultCorner);
112
- }
113
-
114
- /** Pure. Which corner a node's icon badge sits at when `iconPosition`
115
- * is `'corner'`. */
116
- static nodeIconCorner(node, iconCornerByShape, defaultCorner = 'top-right') {
117
- return NodeGeometry.resolveNodeCorner(node, 'iconCorner', iconCornerByShape, defaultCorner);
118
- }
119
-
120
- /** Pure. What dragging a node's resize handle to `mousePos` should set on
121
- * the node. */
122
- static resizedNodeValue(node, corner, center, mousePos) {
123
- const signX = corner.includes('right') ? 1 : -1;
124
- const signY = corner.includes('bottom') ? 1 : -1;
125
- if (node?.shape === 'table') {
126
- const MIN_TABLE_WIDTH = 80;
127
- return { width: Math.max(MIN_TABLE_WIDTH, Math.round(Math.abs(mousePos.x - center.x) * 2)) };
128
- }
129
- if (node?.shape === 'rect') {
130
- const MIN_RECT_SIZE = 24;
131
- const width = Math.max(MIN_RECT_SIZE, Math.round((mousePos.x - center.x) * signX * 2));
132
- const height = Math.max(MIN_RECT_SIZE, Math.round((mousePos.y - center.y) * signY * 2));
133
- return { width, height };
134
- }
135
- const MIN_RADIUS = 4;
136
- const projected = ((mousePos.x - center.x) * signX + (mousePos.y - center.y) * signY) / 2;
137
- return { radius: Math.max(MIN_RADIUS, Math.round(projected)) };
138
- }
139
-
140
- /** Pure. SVG path `d` for a `w`x`h` rectangle centred on the origin with
141
- * all four corners cut at 45° (an octagon) — see nodeShapeEl's
142
- * 'chamfered' cornerStyle. `chamfer` (the cut's size) defaults to a
143
- * fraction of the box's smaller dimension so it scales sensibly with
144
- * the box rather than needing its own per-node configuration. */
145
- static chamferedRectPath(w, h, chamfer) {
146
- return Glyph.chamferedRectPath(w, h, chamfer);
147
- }
148
-
149
- /** Build the SVG element for a node's shape. Returns `{el, suppressInner}`
150
- * where `suppressInner` tells the caller not to draw a centred icon or
151
- * initials on top of busy distinct shapes. The shapes themselves are
152
- * Glyphs (src/charting/glyphs), shared with the editor's templates. */
153
- static nodeShapeEl(renderer, shape, R, color, size, cornerStyle, strokeColor = '#fff', strokeWidth = 2, iconUrl = null, icon = null) {
154
- const glyph = glyphFor(shape);
155
- const ctx = Glyph.context({ R, size, color, cornerStyle, strokeColor, strokeWidth, iconUrl, icon });
156
- return { el: drawGlyph(renderer, glyph.parts(ctx)), suppressInner: glyph.constructor.ownsInterior };
157
- }
158
- }
1
+ import { Glyph, glyphFor, drawGlyph } from '../glyphs/index.js';
2
+ import { GraphMetrics } from './GraphMetrics.js';
3
+ import { LabelHelper } from './LabelHelper.js';
4
+
5
+ /**
6
+ * NodeGeometry — pure, stateless helpers for graph node sizing, corners,
7
+ * resize-handle math, and table-node geometry.
8
+ *
9
+ * These used to live on RenderEngine; they are collected here so diagram
10
+ * classes can compute node geometry without going through the engine façade.
11
+ */
12
+ export class NodeGeometry {
13
+ static TABLE_NODE = { width: 170, headerH: 26, rowH: 18, pad: 6 };
14
+
15
+ /** Bounding box a node occupies, centred on its (x,y) — {w:2R,h:2R} for
16
+ * the plain default (sized by GraphMetrics.effectiveNodeRadius when a
17
+ * `sizeDomain` is supplied), the real card size for a `shape:'table'`
18
+ * node (see NodeGeometry.tableNodeSize), or, for any OTHER shape, an
19
+ * explicit `node.width`/`node.height` override for a wider-than-tall box. */
20
+ static nodeSize(node, R, sizeDomain, sizeOptions) {
21
+ if (node?.shape === 'table') return NodeGeometry.tableNodeSize(node);
22
+ const radius = GraphMetrics ? GraphMetrics.effectiveNodeRadius(node, R, sizeDomain, sizeOptions) : R;
23
+ if (Number.isFinite(node?.width) || Number.isFinite(node?.height)) {
24
+ return {
25
+ w: Number.isFinite(node.width) ? node.width : radius * 2,
26
+ h: Number.isFinite(node.height) ? node.height : radius * 2,
27
+ };
28
+ }
29
+ return { w: radius * 2, h: radius * 2 };
30
+ }
31
+
32
+ /**
33
+ * A shape's visible outline for a node of `size`, as a closed polygon centred
34
+ * on (0, 0) (its glyph's `outline`: a curve sampled, a box its box), or null
35
+ * for a shape no glyph draws.
36
+ */
37
+ static outline(shape, size) {
38
+ const glyph = glyphFor(shape);
39
+ if (!glyph || glyph.constructor.key !== shape) return null; // glyphFor falls back to a circle for one it lacks
40
+ return glyph.outline(Glyph.context({ R: Math.max(size.w, size.h) / 2, size }));
41
+ }
42
+
43
+ /** Size of a `shape:'table'` node — column-count-driven, never affected
44
+ * by node.size/sizeDomain. */
45
+ static tableNodeSize(node) {
46
+ const { width, headerH, rowH, pad } = NodeGeometry.TABLE_NODE;
47
+ const cols = node.columns?.length ?? 0;
48
+ return { w: node.width || width, h: headerH + cols * rowH + pad };
49
+ }
50
+
51
+ /** World-space Y of a named column's row within a table node, for edge anchoring. */
52
+ static tableColumnY(node, posEntry, size, colName) {
53
+ const { headerH, rowH } = NodeGeometry.TABLE_NODE;
54
+ const idx = Math.max(0, (node.columns ?? []).findIndex(c => c.name === colName));
55
+ return posEntry.y - size.h / 2 + headerH + idx * rowH + rowH / 2;
56
+ }
57
+
58
+ /** Draws a table-card node's header + one row per column into wrapper `g`. */
59
+ static buildTableNodeContent(renderer, g, node) {
60
+ const { headerH, rowH } = NodeGeometry.TABLE_NODE;
61
+ const { w, h } = NodeGeometry.tableNodeSize(node);
62
+ const cols = node.columns ?? [];
63
+ const x0 = -w / 2, y0 = -h / 2;
64
+ const borderColor = node.selected ? '#4f8ef7' : '#cfd8dc';
65
+ const borderWidth = node.selected ? 2 : 1;
66
+
67
+ const TYPE_COLORS = { NUMBER: '#74b9ff', TEXT: '#a29bfe', TIMESTAMP: '#55efc4', BOOLEAN: '#ffeaa7', JSON: '#fd79a8', UNKNOWN: '#dfe6e9' };
68
+ g.appendChild(renderer.rect(x0, y0, w, h, '#ffffff', { color: borderColor, width: borderWidth }));
69
+ g.appendChild(renderer.rect(x0, y0, w, headerH, '#1976d2', null));
70
+ g.appendChild(renderer.text(LabelHelper.resolveNodeLabel(node), x0 + 8, y0 + headerH / 2, {
71
+ fill: '#fff', textAnchor: 'start', verticalAlign: 'middle', fontSize: '11px', fontWeight: 'bold', 'pointer-events': 'none',
72
+ }));
73
+ if (node.rowCount != null) {
74
+ g.appendChild(renderer.text(String(node.rowCount), x0 + w - 8, y0 + headerH / 2, {
75
+ fill: '#cfe3fb', textAnchor: 'end', verticalAlign: 'middle', fontSize: '9px', 'pointer-events': 'none',
76
+ }));
77
+ }
78
+
79
+ cols.forEach((col, i) => {
80
+ const rowY = y0 + headerH + i * rowH;
81
+ g.appendChild(renderer.circle(x0 + 12, rowY + rowH / 2, 3.5, TYPE_COLORS[col.typeGroup] ?? TYPE_COLORS.UNKNOWN, null));
82
+ g.appendChild(renderer.text(col.name, x0 + 20, rowY + rowH / 2, {
83
+ fill: '#333', textAnchor: 'start', verticalAlign: 'middle', fontSize: '10px', 'pointer-events': 'none',
84
+ }));
85
+ let badgeX = x0 + w - 8;
86
+ if (col.isFK) {
87
+ g.appendChild(renderer.text('FK', badgeX, rowY + rowH / 2, {
88
+ fill: '#e67e22', textAnchor: 'end', verticalAlign: 'middle', fontSize: '8px', fontWeight: 'bold', 'pointer-events': 'none',
89
+ }));
90
+ badgeX -= 18;
91
+ }
92
+ if (col.isPK) {
93
+ g.appendChild(renderer.text('PK', badgeX, rowY + rowH / 2, {
94
+ fill: '#00b894', textAnchor: 'end', verticalAlign: 'middle', fontSize: '8px', fontWeight: 'bold', 'pointer-events': 'none',
95
+ }));
96
+ }
97
+ });
98
+ }
99
+
100
+ /** Pure. The point at a given corner of a `size` (`{w,h}`) box centred on
101
+ * `pos` — shared by the resize handle's screen positioning and a node's
102
+ * icon-badge placement. */
103
+ static cornerPoint(pos, size, corner) {
104
+ return {
105
+ x: pos.x + (corner.includes('right') ? size.w / 2 : -size.w / 2),
106
+ y: pos.y + (corner.includes('bottom') ? size.h / 2 : -size.h / 2),
107
+ };
108
+ }
109
+
110
+ /** Pure. Generic 3-tier corner resolution — 'top-left'|'top-right'|
111
+ * 'bottom-left'|'bottom-right': a per-node `node[nodeKey]` override,
112
+ * then a per-shape entry in `byShape` (keyed by `node.shape`, default
113
+ * shape 'circle'), then `defaultCorner`. */
114
+ static resolveNodeCorner(node, nodeKey, byShape, defaultCorner) {
115
+ if (node?.[nodeKey]) return node[nodeKey];
116
+ const shapeCorner = byShape?.[node?.shape || 'circle'];
117
+ return shapeCorner || defaultCorner;
118
+ }
119
+
120
+ /** Pure. Which corner a node's resize handle sits at/drags from. */
121
+ static nodeResizeCorner(node, resizeCornerByShape, defaultCorner = 'top-right') {
122
+ return NodeGeometry.resolveNodeCorner(node, 'resizeCorner', resizeCornerByShape, defaultCorner);
123
+ }
124
+
125
+ /** Pure. Which corner a node's icon badge sits at when `iconPosition`
126
+ * is `'corner'`. */
127
+ static nodeIconCorner(node, iconCornerByShape, defaultCorner = 'top-right') {
128
+ return NodeGeometry.resolveNodeCorner(node, 'iconCorner', iconCornerByShape, defaultCorner);
129
+ }
130
+
131
+ /** Pure. What dragging a node's resize handle to `mousePos` should set on
132
+ * the node. */
133
+ static resizedNodeValue(node, corner, center, mousePos) {
134
+ const signX = corner.includes('right') ? 1 : -1;
135
+ const signY = corner.includes('bottom') ? 1 : -1;
136
+ if (node?.shape === 'table') {
137
+ const MIN_TABLE_WIDTH = 80;
138
+ return { width: Math.max(MIN_TABLE_WIDTH, Math.round(Math.abs(mousePos.x - center.x) * 2)) };
139
+ }
140
+ if (node?.shape === 'rect') {
141
+ const MIN_RECT_SIZE = 24;
142
+ const width = Math.max(MIN_RECT_SIZE, Math.round((mousePos.x - center.x) * signX * 2));
143
+ const height = Math.max(MIN_RECT_SIZE, Math.round((mousePos.y - center.y) * signY * 2));
144
+ return { width, height };
145
+ }
146
+ const MIN_RADIUS = 4;
147
+ const projected = ((mousePos.x - center.x) * signX + (mousePos.y - center.y) * signY) / 2;
148
+ return { radius: Math.max(MIN_RADIUS, Math.round(projected)) };
149
+ }
150
+
151
+ /** Pure. SVG path `d` for a `w`x`h` rectangle centred on the origin with
152
+ * all four corners cut at 45° (an octagon) — see nodeShapeEl's
153
+ * 'chamfered' cornerStyle. `chamfer` (the cut's size) defaults to a
154
+ * fraction of the box's smaller dimension so it scales sensibly with
155
+ * the box rather than needing its own per-node configuration. */
156
+ static chamferedRectPath(w, h, chamfer) {
157
+ return Glyph.chamferedRectPath(w, h, chamfer);
158
+ }
159
+
160
+ /** Build the SVG element for a node's shape. Returns `{el, suppressInner}`
161
+ * where `suppressInner` tells the caller not to draw a centred icon or
162
+ * initials on top of busy distinct shapes. The shapes themselves are
163
+ * Glyphs (src/charting/glyphs), shared with the editor's templates. */
164
+ static nodeShapeEl(renderer, shape, R, color, size, cornerStyle, strokeColor = '#fff', strokeWidth = 2, iconUrl = null, icon = null) {
165
+ const glyph = glyphFor(shape);
166
+ const ctx = Glyph.context({ R, size, color, cornerStyle, strokeColor, strokeWidth, iconUrl, icon });
167
+ return { el: drawGlyph(renderer, glyph.parts(ctx)), suppressInner: glyph.constructor.ownsInterior };
168
+ }
169
+ }
@@ -28,3 +28,34 @@ export function viewNestings(view) {
28
28
  export function nestingRelationships(model, parentId, childId) {
29
29
  return model.relationships().filter(r => r.source === parentId && r.target === childId && NESTING_KINDS.includes(r.kind));
30
30
  }
31
+
32
+ /**
33
+ * Whether a box holds another on a view at any depth: the other is drawn in
34
+ * it, or in a box drawn in it, and so on.
35
+ * @param {ArchimateView} view
36
+ * @param {Object} ancestor - An element instance
37
+ * @param {Object} descendant - An element instance
38
+ */
39
+ export function holdsNested(view, ancestor, descendant) {
40
+ if (!ancestor || !descendant || ancestor === descendant) return false;
41
+ const byVisualId = new Map([...(view.elementInstances?.values() ?? [])].map(i => [i.visualId, i]));
42
+ const seen = new Set();
43
+ for (let at = byVisualId.get(descendant.nestedIn); at && !seen.has(at); at = byVisualId.get(at.nestedIn)) {
44
+ if (at === ancestor) return true;
45
+ seen.add(at);
46
+ }
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Whether a relationship's line goes undrawn on a view, as Archi leaves it: a
52
+ * kind a nesting stands for (NESTING_KINDS), from a box to one nested in it
53
+ * (at any depth: in a box in it too). The nesting shows the relationship.
54
+ * @param {ArchimateView} view
55
+ * @param {Object} relationship
56
+ * @param {Object} sourceInstance - The line's source box
57
+ * @param {Object} targetInstance - The line's target box
58
+ */
59
+ export function hiddenByNesting(view, relationship, sourceInstance, targetInstance) {
60
+ return NESTING_KINDS.includes(relationship?.kind) && holdsNested(view, sourceInstance, targetInstance);
61
+ }
@@ -1,23 +1,24 @@
1
- import { Glyph } from './Glyph.js';
2
-
3
- /** A disc with a white head-and-shoulders silhouette. */
4
- export class PersonGlyph extends Glyph {
5
- static key = 'person';
6
- static ownsInterior = true;
7
-
8
- parts(ctx) {
9
- const G = Glyph, st = (k) => this.stroke(ctx, k);
10
- const r = ctx.circleR;
11
- const headR = r * 0.30;
12
- const headY = -(r * 0.28);
13
- const neckY = headY + headR + r * 0.05;
14
- const bodyW = r * 0.55;
15
- const bodyH = r * 0.88;
16
- const bodyPath = `M ${-bodyW},${bodyH} Q ${-bodyW},${neckY} 0,${neckY} Q ${bodyW},${neckY} ${bodyW},${bodyH} Z`;
17
- return G.group([
18
- G.circle(0, 0, r, ctx.color, st()),
19
- G.circle(0, headY, headR, '#fff', null, { opacity: 0.92 }),
20
- G.path(bodyPath, '#fff', null, { opacity: 0.92 }),
21
- ]);
22
- }
23
- }
1
+ import { Glyph } from './Glyph.js';
2
+ import { CircleGlyph } from './CircleGlyph.js';
3
+
4
+ /** A disc with a white head-and-shoulders silhouette: a circle, its outline the circle's. */
5
+ export class PersonGlyph extends CircleGlyph {
6
+ static key = 'person';
7
+ static ownsInterior = true;
8
+
9
+ parts(ctx) {
10
+ const G = Glyph, st = (k) => this.stroke(ctx, k);
11
+ const r = ctx.circleR;
12
+ const headR = r * 0.30;
13
+ const headY = -(r * 0.28);
14
+ const neckY = headY + headR + r * 0.05;
15
+ const bodyW = r * 0.55;
16
+ const bodyH = r * 0.88;
17
+ const bodyPath = `M ${-bodyW},${bodyH} Q ${-bodyW},${neckY} 0,${neckY} Q ${bodyW},${neckY} ${bodyW},${bodyH} Z`;
18
+ return G.group([
19
+ G.circle(0, 0, r, ctx.color, st()),
20
+ G.circle(0, headY, headR, '#fff', null, { opacity: 0.92 }),
21
+ G.path(bodyPath, '#fff', null, { opacity: 0.92 }),
22
+ ]);
23
+ }
24
+ }