@peter.prib/renderengine 0.2.4 → 0.2.6

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 (35) hide show
  1. package/README.md +215 -213
  2. package/REFERENCE.md +99 -7
  3. package/dist/renderengine-workspace.umd.js +13168 -11966
  4. package/dist/renderengine.umd.js +7439 -6472
  5. package/examples/data-model.html +324 -0
  6. package/package.json +70 -68
  7. package/src/charting/CheckboxTreeSelector.js +373 -304
  8. package/src/charting/DiagramEditor.js +3372 -3330
  9. package/src/charting/DiagramTypeRegistry.js +365 -326
  10. package/src/charting/Palette.js +119 -119
  11. package/src/charting/TemplateCatalog.js +61 -59
  12. package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
  13. package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
  14. package/src/charting/datamodel/DataModel.js +299 -0
  15. package/src/charting/datamodel/DataModelContext.js +108 -0
  16. package/src/charting/datamodel/DataModelPerspective.js +84 -0
  17. package/src/charting/datamodel/LevelMapping.js +38 -0
  18. package/src/charting/datamodel/LevelSchema.js +41 -0
  19. package/src/charting/datamodel/index.js +5 -0
  20. package/src/charting/diagrams/DataModelDiagram.js +160 -86
  21. package/src/charting/diagrams/DataModelRelationshipStrategy.js +86 -38
  22. package/src/charting/diagrams/nesting.js +31 -0
  23. package/src/charting/modelTree/ModelTree.js +202 -164
  24. package/src/charting/modelTree/ModelTreePanel.js +36 -3
  25. package/src/charting/modelTree/TreeViewMode.js +131 -110
  26. package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
  27. package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
  28. package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
  29. package/src/datamodel.js +132 -122
  30. package/src/diagramCatalog.js +35 -30
  31. package/src/index.js +6 -1
  32. package/src/workspace/index.js +1 -0
  33. package/src/workspace/parts/ArchimateCanvasPart.js +5 -2
  34. package/src/workspace/parts/ChartPart.js +6 -1
  35. package/src/workspace/parts/DataModelPart.js +234 -0
@@ -1,86 +1,160 @@
1
- import { Diagram } from '../Diagram.js';
2
- import { DataModelNotation } from '../../datamodel.js';
3
- import { RelationshipAsEdgeStrategy, RelationshipAsNodeStrategy } from './DataModelRelationshipStrategy.js';
4
-
5
- /**
6
- * DataModelDiagram — builds entity-relationship nodes/edges and renders them
7
- * through the shared GraphDiagram engine.
8
- */
9
- export class DataModelDiagram extends Diagram {
10
- static diagramType = 'dataModel';
11
-
12
- constructor(renderer, entities, relationships, options = {}, engine = null) {
13
- super(renderer, { entities, relationships }, options);
14
- this.engine = engine;
15
- }
16
-
17
- static _strategyFor(notation) {
18
- return notation.relationshipAsNode
19
- ? new RelationshipAsNodeStrategy()
20
- : new RelationshipAsEdgeStrategy();
21
- }
22
-
23
- render() {
24
- const { entities, relationships } = this.data;
25
- const options = this.options;
26
- const { level = 'logical', notation: notationName = 'crowsfoot', ...rest } = options;
27
- const notation = DataModelNotation.notation(notationName);
28
- const nodes = entities.map(entity => DataModelDiagram.buildNode(entity, { level, notation }));
29
- const strategy = DataModelDiagram._strategyFor(notation);
30
- const { nodes: relNodes, edges } = strategy.apply(relationships, {
31
- buildEdge: DataModelDiagram.buildEdge,
32
- notation,
33
- });
34
- nodes.push(...relNodes);
35
- this.engine.graphDiagram(nodes, edges, { edgeColor: '#666666', ...rest });
36
- }
37
-
38
- static typeGroup(type) {
39
- if (!type) return 'UNKNOWN';
40
- const t = String(type).toUpperCase();
41
- if (/^(INT|INTEGER|BIGINT|SMALLINT|TINYINT|DECIMAL|NUMERIC|FLOAT|DOUBLE|REAL|NUMBER|SERIAL)/.test(t)) return 'NUMBER';
42
- if (/^(VARCHAR|CHAR|TEXT|STRING|CLOB|UUID)/.test(t)) return 'TEXT';
43
- if (/^(TIMESTAMP|DATE|DATETIME|TIME)/.test(t)) return 'TIMESTAMP';
44
- if (/^(BOOL|BOOLEAN)/.test(t)) return 'BOOLEAN';
45
- if (/^(JSON|JSONB)/.test(t)) return 'JSON';
46
- return 'UNKNOWN';
47
- }
48
-
49
- static buildNode(entity, { level, notation }) {
50
- const levelData = entity[level] || {};
51
- const attributes = levelData.attributes;
52
- if (notation.entityShape === 'table' && attributes?.length) {
53
- return {
54
- id: entity.id,
55
- label: levelData.tableName || entity.name,
56
- shape: 'table',
57
- x: entity.x,
58
- y: entity.y,
59
- color: entity.color,
60
- columns: attributes.map(a => ({ name: a.name, typeGroup: DataModelDiagram.typeGroup(a.type), isPK: !!a.isPK, isFK: !!a.isFK })),
61
- };
62
- }
63
- return {
64
- id: entity.id,
65
- label: entity.name,
66
- shape: entity.shape || 'rect',
67
- x: entity.x,
68
- y: entity.y,
69
- color: entity.color,
70
- description: entity.description,
71
- labelPosition: entity.labelPosition || 'inside',
72
- };
73
- }
74
-
75
- static buildEdge(relationship, notation) {
76
- return {
77
- ...relationship,
78
- label: relationship.name,
79
- lineStyle: relationship.identifying === false ? 'dashed' : 'solid',
80
- arrowheadStart: notation.cardinalityArrowhead(relationship.sourceCardinality) || 'none',
81
- arrowhead: notation.cardinalityArrowhead(relationship.targetCardinality) || 'none',
82
- sourceLabel: notation.cardinalityLabel(relationship.sourceCardinality),
83
- targetLabel: notation.cardinalityLabel(relationship.targetCardinality),
84
- };
85
- }
86
- }
1
+ import { Diagram } from '../Diagram.js';
2
+ import { DataModelNotation } from '../../datamodel.js';
3
+ import { RelationshipAsEdgeStrategy, RelationshipAsNodeStrategy } from './DataModelRelationshipStrategy.js';
4
+ import { NodeGeometry } from './NodeGeometry.js';
5
+
6
+ /**
7
+ * DataModelDiagram — builds entity-relationship nodes/edges and renders them
8
+ * through the shared GraphDiagram engine.
9
+ */
10
+ export class DataModelDiagram extends Diagram {
11
+ static diagramType = 'dataModel';
12
+
13
+ /** The size of an entity drawn as a box with its name inside (an entity's own width/height win). */
14
+ static ENTITY_BOX = Object.freeze({ width: 140, height: 56 });
15
+
16
+ /** The size of an annotation's note. */
17
+ static NOTE = Object.freeze({ width: 180, height: 64 });
18
+
19
+ /** The size of a shape with no size of its own (a person, a pin), and the room its name takes below it. */
20
+ static SHAPE_SIZE = 50;
21
+ static LABEL_BELOW = 18;
22
+
23
+ constructor(renderer, entities, relationships, options = {}, engine = null) {
24
+ super(renderer, { entities, relationships }, options);
25
+ this.engine = engine;
26
+ }
27
+
28
+ static _strategyFor(notation) {
29
+ return notation.relationshipAsNode
30
+ ? new RelationshipAsNodeStrategy()
31
+ : new RelationshipAsEdgeStrategy();
32
+ }
33
+
34
+ render() {
35
+ const { entities, relationships } = this.data;
36
+ const options = this.options;
37
+ const { level = 'logical', notation: notationName = 'crowsfoot', annotations = [], links = [], ...rest } = options;
38
+ const notation = DataModelNotation.notation(notationName);
39
+ const nodes = entities.map(entity => DataModelDiagram.buildNode(entity, { level, notation }));
40
+ const strategy = DataModelDiagram._strategyFor(notation);
41
+ const byId = new Map(entities.map(e => [e.id, e]));
42
+ const notes = annotations.map(annotation => DataModelDiagram.buildAnnotation(annotation, byId));
43
+ const { nodes: relNodes, edges } = strategy.apply(relationships, {
44
+ buildEdge: DataModelDiagram.buildEdge,
45
+ notation,
46
+ entity: (id) => byId.get(id),
47
+ occupied: [...nodes, ...notes.map(n => n.node)].map(DataModelDiagram.boxOf).filter(Boolean),
48
+ });
49
+ nodes.push(...relNodes);
50
+ for (const { node, edges: lines } of notes) {
51
+ nodes.push(node);
52
+ edges.push(...lines);
53
+ }
54
+ edges.push(...links.map(link => DataModelDiagram.buildLink(link)));
55
+ this.engine.graphDiagram(nodes, edges, { edgeColor: '#666666', ...rest });
56
+ }
57
+
58
+ /**
59
+ * Where a built node is drawn, as a box {x0, y0, x1, y1} (a table as tall
60
+ * as its columns; a shape with its name below, the name too), or null
61
+ * when it has no place yet.
62
+ */
63
+ static boxOf(node) {
64
+ if (!Number.isFinite(node?.x) || !Number.isFinite(node?.y)) return null;
65
+ const { w, h } = node.shape === 'table'
66
+ ? NodeGeometry.tableNodeSize(node)
67
+ : { w: node.width ?? DataModelDiagram.SHAPE_SIZE, h: (node.height ?? DataModelDiagram.SHAPE_SIZE) + (node.labelPosition === 'inside' ? 0 : DataModelDiagram.LABEL_BELOW) };
68
+ return { x0: node.x - w / 2, x1: node.x + w / 2, y0: node.y - h / 2, y1: node.y + h / 2 };
69
+ }
70
+
71
+ /**
72
+ * A note on the diagram (a rule, a remark) and a dotted line to each entity
73
+ * it is about, the same in every notation. Without its own place it sits
74
+ * above the first entity it is about.
75
+ * @param {{id: string, text: string, attachTo?: string[], x?: number, y?: number, color?: string}} annotation
76
+ * @param {Map<string, Object>} entities - By id (where they are)
77
+ */
78
+ static buildAnnotation(annotation, entities) {
79
+ const attachTo = annotation.attachTo ?? [];
80
+ const first = entities.get(attachTo[0]);
81
+ const place = Number.isFinite(annotation.x) ? { x: annotation.x, y: annotation.y }
82
+ : Number.isFinite(first?.x) ? { x: first.x, y: first.y - 110 } : {};
83
+ return {
84
+ node: {
85
+ id: annotation.id, label: annotation.text, shape: 'note', labelPosition: 'inside',
86
+ color: annotation.color ?? '#fff8c5', ...DataModelDiagram.NOTE, ...place,
87
+ },
88
+ edges: attachTo.filter(id => entities.has(id)).map(id => DataModelDiagram.buildLink({ source: annotation.id, target: id })),
89
+ };
90
+ }
91
+
92
+ /**
93
+ * A plain dotted line between two boxes, the same in every notation: a
94
+ * note to what it is about, or (the `links` option) a mapping between levels.
95
+ * @param {{id?: string, source: string, target: string, label?: string}} link
96
+ */
97
+ static buildLink({ id, source, target, label }) {
98
+ return { ...(id && { id }), source, target, ...(label && { label }), lineStyle: 'dotted', arrowhead: 'none', arrowheadStart: 'none' };
99
+ }
100
+
101
+ static typeGroup(type) {
102
+ if (!type) return 'UNKNOWN';
103
+ const t = String(type).toUpperCase();
104
+ if (/^(INT|INTEGER|BIGINT|SMALLINT|TINYINT|DECIMAL|NUMERIC|FLOAT|DOUBLE|REAL|NUMBER|SERIAL)/.test(t)) return 'NUMBER';
105
+ if (/^(VARCHAR|CHAR|TEXT|STRING|CLOB|UUID)/.test(t)) return 'TEXT';
106
+ if (/^(TIMESTAMP|DATE|DATETIME|TIME)/.test(t)) return 'TIMESTAMP';
107
+ if (/^(BOOL|BOOLEAN)/.test(t)) return 'BOOLEAN';
108
+ if (/^(JSON|JSONB)/.test(t)) return 'JSON';
109
+ return 'UNKNOWN';
110
+ }
111
+
112
+ /** An entity drawn at a level: its own `level` (a view of several levels) else the diagram's. */
113
+ static buildNode(entity, { level, notation }) {
114
+ const levelData = entity[entity.level ?? level] || {};
115
+ const attributes = levelData.attributes;
116
+ if (notation.entityShape === 'table' && attributes?.length) {
117
+ return {
118
+ id: entity.id,
119
+ label: levelData.tableName || entity.name,
120
+ shape: 'table',
121
+ x: entity.x,
122
+ y: entity.y,
123
+ color: entity.color,
124
+ description: entity.description,
125
+ ...entity.details,
126
+ columns: attributes.map(a => ({ name: a.name, typeGroup: DataModelDiagram.typeGroup(a.type), isPK: !!a.isPK, isFK: !!a.isFK })),
127
+ };
128
+ }
129
+ const labelPosition = entity.labelPosition || 'inside';
130
+ // A name inside the box needs room; a shape with its name beside it keeps its own size.
131
+ const box = labelPosition === 'inside' ? DataModelDiagram.ENTITY_BOX : {};
132
+ const width = entity.width ?? box.width;
133
+ const height = entity.height ?? box.height;
134
+ return {
135
+ id: entity.id,
136
+ label: entity.name,
137
+ shape: entity.shape || 'rect',
138
+ x: entity.x,
139
+ y: entity.y,
140
+ color: entity.color,
141
+ description: entity.description,
142
+ ...entity.details,
143
+ labelPosition,
144
+ ...(width !== undefined && { width }),
145
+ ...(height !== undefined && { height }),
146
+ };
147
+ }
148
+
149
+ static buildEdge(relationship, notation) {
150
+ return {
151
+ ...relationship,
152
+ label: relationship.name,
153
+ lineStyle: relationship.identifying === false ? 'dashed' : 'solid',
154
+ arrowheadStart: notation.cardinalityArrowhead(relationship.sourceCardinality) || 'none',
155
+ arrowhead: notation.cardinalityArrowhead(relationship.targetCardinality) || 'none',
156
+ sourceLabel: notation.cardinalityLabel(relationship.sourceCardinality),
157
+ targetLabel: notation.cardinalityLabel(relationship.targetCardinality),
158
+ };
159
+ }
160
+ }
@@ -1,38 +1,86 @@
1
- /**
2
- * Strategy interface for turning data-model relationships into diagram nodes
3
- * and edges. Crow's Foot and UML keep relationships as direct edges; Chen
4
- * turns each relationship into a diamond node plus two connecting edges.
5
- */
6
- export class DataModelRelationshipStrategy {
7
- /**
8
- * @param {Array<object>} relationships
9
- * @param {object} context
10
- * @param {Function} context.buildEdge
11
- * @returns {{nodes: Array<object>, edges: Array<object>}}
12
- */
13
- apply(relationships, context) {
14
- throw new Error(`${this.constructor.name} must implement apply()`);
15
- }
16
- }
17
-
18
- export class RelationshipAsEdgeStrategy extends DataModelRelationshipStrategy {
19
- apply(relationships, { buildEdge, notation }) {
20
- const edges = relationships.map(r => buildEdge(r, notation));
21
- return { nodes: [], edges };
22
- }
23
- }
24
-
25
- export class RelationshipAsNodeStrategy extends DataModelRelationshipStrategy {
26
- apply(relationships, { notation }) {
27
- const nodes = [];
28
- const edges = [];
29
- relationships.forEach((relationship, i) => {
30
- const relId = relationship.id || `${relationship.source}__${relationship.target}__rel${i}`;
31
- const lineStyle = relationship.identifying === false ? 'dashed' : 'solid';
32
- nodes.push({ id: relId, label: relationship.name || '', shape: 'diamond', color: relationship.color || '#f6c344' });
33
- edges.push({ source: relationship.source, target: relId, label: notation.cardinalityLabel(relationship.sourceCardinality), lineStyle });
34
- edges.push({ source: relId, target: relationship.target, label: notation.cardinalityLabel(relationship.targetCardinality), lineStyle });
35
- });
36
- return { nodes, edges };
37
- }
38
- }
1
+ /**
2
+ * Strategy interface for turning data-model relationships into diagram nodes
3
+ * and edges. Crow's Foot and UML keep relationships as direct edges; Chen
4
+ * turns each relationship into a diamond node plus two connecting edges.
5
+ */
6
+ export class DataModelRelationshipStrategy {
7
+ /**
8
+ * @param {Array<object>} relationships
9
+ * @param {object} context
10
+ * @param {Function} context.buildEdge
11
+ * @param {(id: string) => object|undefined} [context.entity] - An entity by id (where it is)
12
+ * @param {Array<{x0, y0, x1, y1}>} [context.occupied] - What is already drawn where (boxes, notes), to keep clear of
13
+ * @returns {{nodes: Array<object>, edges: Array<object>}}
14
+ */
15
+ apply(relationships, context) {
16
+ throw new Error(`${this.constructor.name} must implement apply()`);
17
+ }
18
+ }
19
+
20
+ export class RelationshipAsEdgeStrategy extends DataModelRelationshipStrategy {
21
+ apply(relationships, { buildEdge, notation }) {
22
+ const edges = relationships.map(r => buildEdge(r, notation));
23
+ return { nodes: [], edges };
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Chen: each relationship a diamond holding its name, between its two entities
29
+ * when both have a place (else laid out with the rest): midway, or when
30
+ * something is drawn there already, the nearest clear spot to one side of the
31
+ * line (spotFor). Joined to each by a plain line (no arrowhead) labelled with
32
+ * that end's cardinality.
33
+ */
34
+ export class RelationshipAsNodeStrategy extends DataModelRelationshipStrategy {
35
+ /** A relationship diamond's size. */
36
+ static DIAMOND = Object.freeze({ width: 110, height: 60 });
37
+
38
+ /** How far each try steps to the side of the line, and how many tries each way. */
39
+ static STEP = 45;
40
+ static TRIES = 6;
41
+
42
+ /**
43
+ * Where a diamond between two places goes: their midpoint when it is clear
44
+ * of everything in `occupied`, else the nearest clear spot stepping out
45
+ * across the line, one side then the other; the midpoint when none is clear.
46
+ * @returns {{x: number, y: number}}
47
+ */
48
+ static spotFor(from, to, occupied) {
49
+ const { width, height } = RelationshipAsNodeStrategy.DIAMOND;
50
+ const mid = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
51
+ const length = Math.hypot(to.x - from.x, to.y - from.y) || 1;
52
+ const across = { x: -(to.y - from.y) / length, y: (to.x - from.x) / length };
53
+ const clear = ({ x, y }) => occupied.every(o => x + width / 2 <= o.x0 || o.x1 <= x - width / 2 || y + height / 2 <= o.y0 || o.y1 <= y - height / 2);
54
+ for (let k = 0; k <= RelationshipAsNodeStrategy.TRIES; k++) {
55
+ for (const side of k ? [1, -1] : [0]) {
56
+ const at = { x: mid.x + across.x * side * k * RelationshipAsNodeStrategy.STEP, y: mid.y + across.y * side * k * RelationshipAsNodeStrategy.STEP };
57
+ if (clear(at)) return at;
58
+ }
59
+ }
60
+ return mid;
61
+ }
62
+
63
+ apply(relationships, { notation, entity = () => undefined, occupied = [] }) {
64
+ const nodes = [];
65
+ const edges = [];
66
+ const taken = [...occupied];
67
+ const { width, height } = RelationshipAsNodeStrategy.DIAMOND;
68
+ relationships.forEach((relationship, i) => {
69
+ const relId = relationship.id || `${relationship.source}__${relationship.target}__rel${i}`;
70
+ const lineStyle = relationship.identifying === false ? 'dashed' : 'solid';
71
+ const [from, to] = [entity(relationship.source), entity(relationship.target)];
72
+ const placed = [from, to].every(e => Number.isFinite(e?.x) && Number.isFinite(e?.y));
73
+ const at = placed ? RelationshipAsNodeStrategy.spotFor(from, to, taken) : null;
74
+ if (at) taken.push({ x0: at.x - width / 2, x1: at.x + width / 2, y0: at.y - height / 2, y1: at.y + height / 2 });
75
+ nodes.push({
76
+ id: relId, label: relationship.name || '', shape: 'diamond', color: relationship.color || '#f6c344',
77
+ labelPosition: 'inside', ...RelationshipAsNodeStrategy.DIAMOND,
78
+ ...(at && { x: at.x, y: at.y }),
79
+ });
80
+ const line = { lineStyle, arrowhead: 'none', arrowheadStart: 'none' };
81
+ edges.push({ source: relationship.source, target: relId, label: notation.cardinalityLabel(relationship.sourceCardinality), ...line });
82
+ edges.push({ source: relId, target: relationship.target, label: notation.cardinalityLabel(relationship.targetCardinality), ...line });
83
+ });
84
+ return { nodes, edges };
85
+ }
86
+ }
@@ -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
+ }