@dxos/diagram 0.11.1
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.
- package/LICENSE +105 -0
- package/README.md +10 -0
- package/dist/lib/index.mjs +3975 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +68 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/content.d.ts +60 -0
- package/dist/types/src/content.d.ts.map +1 -0
- package/dist/types/src/diagnostics.d.ts +56 -0
- package/dist/types/src/diagnostics.d.ts.map +1 -0
- package/dist/types/src/diagnostics.test.d.ts +2 -0
- package/dist/types/src/diagnostics.test.d.ts.map +1 -0
- package/dist/types/src/dialect.d.ts +19 -0
- package/dist/types/src/dialect.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +16 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/layout.d.ts +26 -0
- package/dist/types/src/layout.d.ts.map +1 -0
- package/dist/types/src/mermaid-engine.d.ts +83 -0
- package/dist/types/src/mermaid-engine.d.ts.map +1 -0
- package/dist/types/src/mermaid-engine.test.d.ts +2 -0
- package/dist/types/src/mermaid-engine.test.d.ts.map +1 -0
- package/dist/types/src/mermaid.d.ts +56 -0
- package/dist/types/src/mermaid.d.ts.map +1 -0
- package/dist/types/src/mermaid.test.d.ts +3 -0
- package/dist/types/src/mermaid.test.d.ts.map +1 -0
- package/dist/types/src/objective.d.ts +75 -0
- package/dist/types/src/objective.d.ts.map +1 -0
- package/dist/types/src/objective.test.d.ts +2 -0
- package/dist/types/src/objective.test.d.ts.map +1 -0
- package/dist/types/src/ortho-router.d.ts +13 -0
- package/dist/types/src/ortho-router.d.ts.map +1 -0
- package/dist/types/src/scene.d.ts +1006 -0
- package/dist/types/src/scene.d.ts.map +1 -0
- package/dist/types/src/scene.test.d.ts +2 -0
- package/dist/types/src/scene.test.d.ts.map +1 -0
- package/dist/types/src/svg-handler.d.ts +5 -0
- package/dist/types/src/svg-handler.d.ts.map +1 -0
- package/dist/types/src/svg-handler.test.d.ts +2 -0
- package/dist/types/src/svg-handler.test.d.ts.map +1 -0
- package/dist/types/src/testing.d.ts +11 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/dist/types/src/ui.d.ts +67 -0
- package/dist/types/src/ui.d.ts.map +1 -0
- package/dist/types/src/ui.test.d.ts +2 -0
- package/dist/types/src/ui.test.d.ts.map +1 -0
- package/dist/types/src/uml-engine.d.ts +14 -0
- package/dist/types/src/uml-engine.d.ts.map +1 -0
- package/dist/types/src/uml-engine.test.d.ts +2 -0
- package/dist/types/src/uml-engine.test.d.ts.map +1 -0
- package/dist/types/src/uml-grid.d.ts +99 -0
- package/dist/types/src/uml-grid.d.ts.map +1 -0
- package/dist/types/src/uml-grid.test.d.ts +2 -0
- package/dist/types/src/uml-grid.test.d.ts.map +1 -0
- package/dist/types/src/uml-rules.d.ts +56 -0
- package/dist/types/src/uml-rules.d.ts.map +1 -0
- package/dist/types/src/uml-rules.test.d.ts +2 -0
- package/dist/types/src/uml-rules.test.d.ts.map +1 -0
- package/dist/types/src/uml-search.d.ts +26 -0
- package/dist/types/src/uml-search.d.ts.map +1 -0
- package/dist/types/src/uml-search.test.d.ts +2 -0
- package/dist/types/src/uml-search.test.d.ts.map +1 -0
- package/dist/types/src/uml.d.ts +60 -0
- package/dist/types/src/uml.d.ts.map +1 -0
- package/dist/types/src/uml.test.d.ts +2 -0
- package/dist/types/src/uml.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/src/__snapshots__/diagnostics.test.ts.snap +36 -0
- package/src/__snapshots__/mermaid-engine.test.ts.snap +22 -0
- package/src/content.ts +180 -0
- package/src/diagnostics.test.ts +173 -0
- package/src/diagnostics.ts +393 -0
- package/src/dialect.ts +53 -0
- package/src/index.ts +19 -0
- package/src/layout.ts +99 -0
- package/src/mermaid-engine.test.ts +244 -0
- package/src/mermaid-engine.ts +863 -0
- package/src/mermaid.test.ts +138 -0
- package/src/mermaid.ts +326 -0
- package/src/objective.test.ts +82 -0
- package/src/objective.ts +157 -0
- package/src/ortho-router.ts +372 -0
- package/src/scene.test.ts +36 -0
- package/src/scene.ts +281 -0
- package/src/svg-handler.test.ts +75 -0
- package/src/svg-handler.ts +119 -0
- package/src/testing.ts +68 -0
- package/src/ui.test.ts +183 -0
- package/src/ui.ts +398 -0
- package/src/uml-engine.test.ts +63 -0
- package/src/uml-engine.ts +146 -0
- package/src/uml-grid.test.ts +175 -0
- package/src/uml-grid.ts +559 -0
- package/src/uml-rules.test.ts +109 -0
- package/src/uml-rules.ts +319 -0
- package/src/uml-search.test.ts +108 -0
- package/src/uml-search.ts +266 -0
- package/src/uml.test.ts +172 -0
- package/src/uml.ts +390 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// Engine-backed placement for the grid UML dialect: dagre or ELK replace the hand-rolled
|
|
7
|
+
// lane/column pass (proper crossing minimization and straightness-seeking coordinate
|
|
8
|
+
// assignment), while parsing, cell measurement, grid snapping, routing, and emission stay
|
|
9
|
+
// shared with `uml-grid.ts`. See `docs/DESIGN.md`.
|
|
10
|
+
//
|
|
11
|
+
|
|
12
|
+
import { layout as dagreLayout, graphlib } from '@dagrejs/dagre';
|
|
13
|
+
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
14
|
+
|
|
15
|
+
import type * as Scene from './scene.ts';
|
|
16
|
+
import {
|
|
17
|
+
type Cell,
|
|
18
|
+
GRID,
|
|
19
|
+
type CompileOptions as GridCompileOptions,
|
|
20
|
+
type Rect,
|
|
21
|
+
emit,
|
|
22
|
+
measureCell,
|
|
23
|
+
} from './uml-grid.ts';
|
|
24
|
+
import { type Direction, type UmlModel, parse, relationRanks } from './uml.ts';
|
|
25
|
+
|
|
26
|
+
export type Engine = 'dagre' | 'elk';
|
|
27
|
+
|
|
28
|
+
export type CompileOptions = Omit<GridCompileOptions, 'route'> & {
|
|
29
|
+
/** Placement engine (default 'elk'). */
|
|
30
|
+
engine?: Engine;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const GAP_MAIN = GRID * 4;
|
|
34
|
+
const GAP_CROSS = GRID * 4;
|
|
35
|
+
|
|
36
|
+
const snap = (value: number) => Math.round(value / GRID) * GRID;
|
|
37
|
+
|
|
38
|
+
type PlaceOptions = {
|
|
39
|
+
cell: Cell;
|
|
40
|
+
gapMain: number;
|
|
41
|
+
gapCross: number;
|
|
42
|
+
direction: Direction;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DAGRE_DIRECTION: Record<Direction, string> = { TB: 'TB', BT: 'BT', LR: 'LR', RL: 'RL' };
|
|
46
|
+
const ELK_DIRECTION: Record<Direction, string> = { TB: 'DOWN', BT: 'UP', LR: 'RIGHT', RL: 'LEFT' };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Edges oriented for layering: inheritance, realization, and dependency point at abstractions,
|
|
50
|
+
* which rank ABOVE their sources — so those edges are reversed for the engine (which lays out
|
|
51
|
+
* source-above-target), matching `relationRanks`.
|
|
52
|
+
*/
|
|
53
|
+
const orientedEdges = (model: UmlModel): { from: string; to: string }[] =>
|
|
54
|
+
model.relations.map((relation) =>
|
|
55
|
+
relation.kind === 'inheritance' || relation.kind === 'realization' || relation.kind === 'dependency'
|
|
56
|
+
? { from: relation.to, to: relation.from }
|
|
57
|
+
: { from: relation.from, to: relation.to },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/** Snap engine output to the document grid and normalize the top-left to (0, 0). */
|
|
61
|
+
const snapRects = (rects: Map<string, Rect>): Map<string, Rect> => {
|
|
62
|
+
const snapped = new Map([...rects].map(([id, rect]) => [id, { ...rect, x: snap(rect.x), y: snap(rect.y) }]));
|
|
63
|
+
const minX = Math.min(...[...snapped.values()].map((rect) => rect.x), 0);
|
|
64
|
+
const minY = Math.min(...[...snapped.values()].map((rect) => rect.y), 0);
|
|
65
|
+
return new Map([...snapped].map(([id, rect]) => [id, { ...rect, x: rect.x - minX, y: rect.y - minY }]));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** dagre: network-simplex layering + median-sweep crossing minimization. Sync. */
|
|
69
|
+
const dagrePlace = (model: UmlModel, { cell, gapMain, gapCross, direction }: PlaceOptions): Map<string, Rect> => {
|
|
70
|
+
const graph = new graphlib.Graph();
|
|
71
|
+
graph.setGraph({
|
|
72
|
+
rankdir: DAGRE_DIRECTION[direction],
|
|
73
|
+
ranker: 'network-simplex',
|
|
74
|
+
ranksep: gapMain,
|
|
75
|
+
nodesep: gapCross,
|
|
76
|
+
edgesep: gapCross / 2,
|
|
77
|
+
});
|
|
78
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
79
|
+
for (const entry of model.classes) {
|
|
80
|
+
graph.setNode(entry.id, { width: cell.w, height: cell.h });
|
|
81
|
+
}
|
|
82
|
+
for (const edge of orientedEdges(model)) {
|
|
83
|
+
graph.setEdge(edge.from, edge.to);
|
|
84
|
+
}
|
|
85
|
+
dagreLayout(graph);
|
|
86
|
+
|
|
87
|
+
// dagre reports node centers.
|
|
88
|
+
const rects = new Map<string, Rect>(
|
|
89
|
+
model.classes.map((entry) => {
|
|
90
|
+
const node = graph.node(entry.id);
|
|
91
|
+
return [entry.id, { x: node.x - cell.w / 2, y: node.y - cell.h / 2, w: cell.w, h: cell.h }];
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
return snapRects(rects);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** ELK layered: layer-sweep crossing minimization + Brandes-Köpf placement. Async. */
|
|
98
|
+
const elkPlace = async (
|
|
99
|
+
model: UmlModel,
|
|
100
|
+
{ cell, gapMain, gapCross, direction }: PlaceOptions,
|
|
101
|
+
): Promise<Map<string, Rect>> => {
|
|
102
|
+
const elk = new ELK();
|
|
103
|
+
const result = await elk.layout({
|
|
104
|
+
id: 'root',
|
|
105
|
+
layoutOptions: {
|
|
106
|
+
'elk.algorithm': 'layered',
|
|
107
|
+
'elk.direction': ELK_DIRECTION[direction],
|
|
108
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': String(gapMain),
|
|
109
|
+
'elk.spacing.nodeNode': String(gapCross),
|
|
110
|
+
// Favors long straight chains over balanced centering — fewer jagged connectors.
|
|
111
|
+
'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
|
|
112
|
+
// Keeps declaration order among otherwise-unconstrained siblings, so peers stay gathered.
|
|
113
|
+
'elk.layered.considerModelOrder.strategy': 'PREFER_NODES',
|
|
114
|
+
},
|
|
115
|
+
children: model.classes.map((entry) => ({ id: entry.id, width: cell.w, height: cell.h })),
|
|
116
|
+
edges: orientedEdges(model).map((edge, index) => ({
|
|
117
|
+
id: `edge-${index}`,
|
|
118
|
+
sources: [edge.from],
|
|
119
|
+
targets: [edge.to],
|
|
120
|
+
})),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const rects = new Map<string, Rect>(
|
|
124
|
+
(result.children ?? []).map((child) => [child.id, { x: child.x ?? 0, y: child.y ?? 0, w: cell.w, h: cell.h }]),
|
|
125
|
+
);
|
|
126
|
+
return snapRects(rects);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compile a mermaid class diagram with engine-backed placement; identical output structure to
|
|
131
|
+
* `UmlGrid.compile` (equal-size grid cells, orthogonal Z-routed connectors), but node positions
|
|
132
|
+
* come from dagre/ELK. Async because ELK is.
|
|
133
|
+
*/
|
|
134
|
+
export const compile = async (source: string, options: CompileOptions = {}): Promise<Scene.Command[]> => {
|
|
135
|
+
const { origin, scale, maxWidth = GRID * 6, engine = 'elk' } = options;
|
|
136
|
+
const model = parse(source);
|
|
137
|
+
const cell = measureCell(model, { maxWidth, cell: options.cell, titleHeight: options.titleHeight });
|
|
138
|
+
const place: PlaceOptions = {
|
|
139
|
+
cell,
|
|
140
|
+
gapMain: snap(options.gapMain ?? GAP_MAIN),
|
|
141
|
+
gapCross: snap(options.gapCross ?? GAP_CROSS),
|
|
142
|
+
direction: model.direction,
|
|
143
|
+
};
|
|
144
|
+
const rects = engine === 'dagre' ? dagrePlace(model, place) : await elkPlace(model, place);
|
|
145
|
+
return emit({ model, cell, rects, ranks: relationRanks(model) }, { origin, scale });
|
|
146
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import type * as Scene from './scene.ts';
|
|
8
|
+
import { CLASS_DIAGRAM } from './testing.ts';
|
|
9
|
+
import { GRID, type Router, compile } from './uml-grid.ts';
|
|
10
|
+
|
|
11
|
+
const objectsOf = (commands: Scene.Command[]) =>
|
|
12
|
+
commands.flatMap((command) => (command.op === 'upsert-object' ? [command.object] : []));
|
|
13
|
+
|
|
14
|
+
const nodesOf = (objects: Scene.WorldObject[]) => objects.filter((object) => object.id !== 'edges');
|
|
15
|
+
|
|
16
|
+
const frameOf = (node: Scene.WorldObject) => node.elements.find((element) => element.id === 'frame') as Scene.Box;
|
|
17
|
+
|
|
18
|
+
describe('uml-grid', () => {
|
|
19
|
+
test('every class renders as an equal-size frame with a top-rounded header', ({ expect }) => {
|
|
20
|
+
const nodes = nodesOf(objectsOf(compile(CLASS_DIAGRAM)));
|
|
21
|
+
|
|
22
|
+
expect(nodes).toHaveLength(6);
|
|
23
|
+
const sizes = new Set(
|
|
24
|
+
nodes.map((node) => {
|
|
25
|
+
const [frame, title] = node.elements as Scene.Box[];
|
|
26
|
+
expect(frame.id).toBe('frame');
|
|
27
|
+
expect(title.id).toBe('title');
|
|
28
|
+
expect(title.corners).toBe('top');
|
|
29
|
+
expect(title.w).toBe(frame.w);
|
|
30
|
+
// Members render as free text inside the frame, not a third bordered rect.
|
|
31
|
+
const body = node.elements.find((element) => element.id === 'body');
|
|
32
|
+
if (body) {
|
|
33
|
+
expect(body.kind).toBe('text');
|
|
34
|
+
}
|
|
35
|
+
return `${frame.w}x${frame.h}`;
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
expect(sizes.size).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('cells and positions align to the grid', ({ expect }) => {
|
|
42
|
+
const nodes = nodesOf(objectsOf(compile(CLASS_DIAGRAM)));
|
|
43
|
+
|
|
44
|
+
for (const node of nodes) {
|
|
45
|
+
expect(node.origin!.x % GRID).toBe(0);
|
|
46
|
+
expect(node.origin!.y % GRID).toBe(0);
|
|
47
|
+
const frame = frameOf(node);
|
|
48
|
+
expect(frame.w % GRID).toBe(0);
|
|
49
|
+
expect(frame.h % GRID).toBe(0);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('connectors are orthogonal: every segment is axis-aligned', ({ expect }) => {
|
|
54
|
+
const edges = objectsOf(compile(CLASS_DIAGRAM)).find((object) => object.id === 'edges')!;
|
|
55
|
+
|
|
56
|
+
const segments: [Scene.Point, Scene.Point][] = [];
|
|
57
|
+
for (const element of edges.elements) {
|
|
58
|
+
if (element.kind === 'line') {
|
|
59
|
+
element.points.forEach((point, index) => {
|
|
60
|
+
if (index > 0) {
|
|
61
|
+
segments.push([element.points[index - 1], point]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
} else if (element.kind === 'arrow') {
|
|
65
|
+
segments.push([element.start!, element.end!]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(segments.length).toBeGreaterThan(0);
|
|
70
|
+
for (const [head, tail] of segments) {
|
|
71
|
+
expect(head.x === tail.x || head.y === tail.y).toBe(true);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('arrowheads terminate on the target cell edge and labels float alongside', ({ expect }) => {
|
|
76
|
+
const objects = objectsOf(compile(CLASS_DIAGRAM));
|
|
77
|
+
const edges = objects.find((object) => object.id === 'edges')!;
|
|
78
|
+
const animal = objects.find((object) => object.id === 'Animal')!;
|
|
79
|
+
const frame = frameOf(animal);
|
|
80
|
+
|
|
81
|
+
// `Animal <|-- Dog` renders upward: the head lands on Animal's bottom edge.
|
|
82
|
+
const inheritance = edges.elements.find(
|
|
83
|
+
(element): element is Scene.Arrow => element.kind === 'arrow' && element.id.startsWith('Dog-Animal'),
|
|
84
|
+
)!;
|
|
85
|
+
expect(inheritance.end!.y).toBe(animal.origin!.y + frame.h);
|
|
86
|
+
|
|
87
|
+
const labels = edges.elements.filter((element) => element.kind === 'text');
|
|
88
|
+
expect(labels.map((element) => (element as Scene.Text).text)).toEqual(
|
|
89
|
+
expect.arrayContaining(['1 owns *', 'chews']),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('dashed styling carries onto routed paths', ({ expect }) => {
|
|
94
|
+
const edges = objectsOf(compile(CLASS_DIAGRAM)).find((object) => object.id === 'edges')!;
|
|
95
|
+
const realization = edges.elements.find(
|
|
96
|
+
(element): element is Scene.Arrow => element.kind === 'arrow' && element.id.startsWith('Dog-Serializable'),
|
|
97
|
+
)!;
|
|
98
|
+
expect(realization.stroke).toBe('dashed');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a custom router controls the path', ({ expect }) => {
|
|
102
|
+
const route: Router = ({ from, to }) => [
|
|
103
|
+
{ x: from.x, y: from.y },
|
|
104
|
+
{ x: from.x, y: -100 },
|
|
105
|
+
{ x: to.x, y: -100 },
|
|
106
|
+
{ x: to.x, y: to.y },
|
|
107
|
+
];
|
|
108
|
+
const edges = objectsOf(compile(CLASS_DIAGRAM, { route })).find((object) => object.id === 'edges')!;
|
|
109
|
+
const paths = edges.elements.filter((element): element is Scene.Polyline => element.kind === 'line');
|
|
110
|
+
|
|
111
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
112
|
+
for (const path of paths) {
|
|
113
|
+
expect(path.points.some((point) => point.y === -100)).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('default height clamps to the GRID-derived bounds', ({ expect }) => {
|
|
118
|
+
const members = Array.from({ length: 30 }, (_, index) => ` +field${index}: string`);
|
|
119
|
+
const source = ['classDiagram', 'class Big {', ...members, '}'].join('\n');
|
|
120
|
+
const [big] = nodesOf(objectsOf(compile(source)));
|
|
121
|
+
|
|
122
|
+
const frame = frameOf(big);
|
|
123
|
+
// Far shorter than the unclamped member stack, whatever the tuned MAX_H is.
|
|
124
|
+
expect(frame.h).toBeLessThan(30 * 26);
|
|
125
|
+
expect(frame.h % GRID).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('cell and titleHeight options fix the node size', ({ expect }) => {
|
|
129
|
+
const snap = (value: number) => Math.ceil(value / GRID) * GRID;
|
|
130
|
+
const nodes = nodesOf(objectsOf(compile(CLASS_DIAGRAM, { cell: { w: 256, h: 480 }, titleHeight: 48 })));
|
|
131
|
+
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
const [frame, title] = node.elements as Scene.Box[];
|
|
134
|
+
expect(frame.w).toBe(snap(256));
|
|
135
|
+
expect(frame.h).toBe(snap(480));
|
|
136
|
+
expect(title.h).toBe(48);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('aligned columns straighten connectors', ({ expect }) => {
|
|
141
|
+
const source = ['classDiagram', 'class A', 'class B', 'A <|-- B'].join('\n');
|
|
142
|
+
const objects = objectsOf(compile(source));
|
|
143
|
+
const edges = objects.find((object) => object.id === 'edges')!;
|
|
144
|
+
|
|
145
|
+
// The sole subtype sits directly below its supertype, so the arrow runs straight: a single
|
|
146
|
+
// two-point segment, no polyline waypoints.
|
|
147
|
+
expect(edges.elements.filter((element) => element.kind === 'line')).toHaveLength(0);
|
|
148
|
+
const [arrow] = edges.elements as Scene.Arrow[];
|
|
149
|
+
expect(arrow.start!.x).toBe(arrow.end!.x);
|
|
150
|
+
const [a, b] = ['A', 'B'].map((id) => objects.find((object) => object.id === id)!);
|
|
151
|
+
expect(a.origin!.x).toBe(b.origin!.x);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('parallel relations between the same lanes take separate channels', ({ expect }) => {
|
|
155
|
+
const source = [
|
|
156
|
+
'classDiagram',
|
|
157
|
+
'class A',
|
|
158
|
+
'class B',
|
|
159
|
+
'class C',
|
|
160
|
+
'class D',
|
|
161
|
+
'A --> C',
|
|
162
|
+
'A --> D',
|
|
163
|
+
'B --> C',
|
|
164
|
+
'B --> D',
|
|
165
|
+
].join('\n');
|
|
166
|
+
const edges = objectsOf(compile(source)).find((object) => object.id === 'edges')!;
|
|
167
|
+
const paths = edges.elements.filter((element): element is Scene.Polyline => element.kind === 'line');
|
|
168
|
+
|
|
169
|
+
// Column alignment can straighten at most one of the crossing edges; the rest bend through
|
|
170
|
+
// the same gutter and their channel runs must not share a coordinate.
|
|
171
|
+
expect(paths.length).toBeGreaterThanOrEqual(2);
|
|
172
|
+
const channels = paths.map((path) => path.points[1].y);
|
|
173
|
+
expect(new Set(channels).size).toBe(paths.length);
|
|
174
|
+
});
|
|
175
|
+
});
|