@dxos/diagram 0.11.1 → 0.14.0

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 (171) hide show
  1. package/dist/lib/chunk-parse.mjs +9406 -0
  2. package/dist/lib/chunk-parse.mjs.map +1 -0
  3. package/dist/lib/extension.mjs +204 -0
  4. package/dist/lib/extension.mjs.map +1 -0
  5. package/dist/lib/index.mjs +497 -2846
  6. package/dist/lib/index.mjs.map +1 -1
  7. package/dist/types/src/aesthetics.d.ts +12 -0
  8. package/dist/types/src/aesthetics.d.ts.map +1 -0
  9. package/dist/types/src/aesthetics.test.d.ts +2 -0
  10. package/dist/types/src/aesthetics.test.d.ts.map +1 -0
  11. package/dist/types/src/appeal.d.ts +55 -0
  12. package/dist/types/src/appeal.d.ts.map +1 -0
  13. package/dist/types/src/appeal.test.d.ts +2 -0
  14. package/dist/types/src/appeal.test.d.ts.map +1 -0
  15. package/dist/types/src/architecture.d.ts +86 -0
  16. package/dist/types/src/architecture.d.ts.map +1 -0
  17. package/dist/types/src/architecture.test.d.ts +2 -0
  18. package/dist/types/src/architecture.test.d.ts.map +1 -0
  19. package/dist/types/src/compact.d.ts +39 -0
  20. package/dist/types/src/compact.d.ts.map +1 -0
  21. package/dist/types/src/compact.test.d.ts +2 -0
  22. package/dist/types/src/compact.test.d.ts.map +1 -0
  23. package/dist/types/src/diagnostics.d.ts +70 -3
  24. package/dist/types/src/diagnostics.d.ts.map +1 -1
  25. package/dist/types/src/dsl/cli.d.ts +2 -0
  26. package/dist/types/src/dsl/cli.d.ts.map +1 -0
  27. package/dist/types/src/dsl/compile.d.ts +12 -0
  28. package/dist/types/src/dsl/compile.d.ts.map +1 -0
  29. package/dist/types/src/dsl/convert.d.ts +45 -0
  30. package/dist/types/src/dsl/convert.d.ts.map +1 -0
  31. package/dist/types/src/dsl/convert.test.d.ts +2 -0
  32. package/dist/types/src/dsl/convert.test.d.ts.map +1 -0
  33. package/dist/types/src/dsl/dsl.test.d.ts +2 -0
  34. package/dist/types/src/dsl/dsl.test.d.ts.map +1 -0
  35. package/dist/types/src/dsl/gen/diagram.d.ts +3 -0
  36. package/dist/types/src/dsl/gen/diagram.d.ts.map +1 -0
  37. package/dist/types/src/dsl/index.d.ts +7 -0
  38. package/dist/types/src/dsl/index.d.ts.map +1 -0
  39. package/dist/types/src/dsl/parse.d.ts +49 -0
  40. package/dist/types/src/dsl/parse.d.ts.map +1 -0
  41. package/dist/types/src/dsl/print.d.ts +19 -0
  42. package/dist/types/src/dsl/print.d.ts.map +1 -0
  43. package/dist/types/src/dsl/semantic.test.d.ts +2 -0
  44. package/dist/types/src/dsl/semantic.test.d.ts.map +1 -0
  45. package/dist/types/src/dsl/vocabulary.d.ts +61 -0
  46. package/dist/types/src/dsl/vocabulary.d.ts.map +1 -0
  47. package/dist/types/src/dx-svg.d.ts +12 -0
  48. package/dist/types/src/dx-svg.d.ts.map +1 -0
  49. package/dist/types/src/dx-svg.test.d.ts +2 -0
  50. package/dist/types/src/dx-svg.test.d.ts.map +1 -0
  51. package/dist/types/src/extension/complete.d.ts +3 -0
  52. package/dist/types/src/extension/complete.d.ts.map +1 -0
  53. package/dist/types/src/extension/extension.test.d.ts +2 -0
  54. package/dist/types/src/extension/extension.test.d.ts.map +1 -0
  55. package/dist/types/src/extension/highlight.d.ts +14 -0
  56. package/dist/types/src/extension/highlight.d.ts.map +1 -0
  57. package/dist/types/src/extension/index.d.ts +6 -0
  58. package/dist/types/src/extension/index.d.ts.map +1 -0
  59. package/dist/types/src/extension/language.d.ts +13 -0
  60. package/dist/types/src/extension/language.d.ts.map +1 -0
  61. package/dist/types/src/extension/lint.d.ts +21 -0
  62. package/dist/types/src/extension/lint.d.ts.map +1 -0
  63. package/dist/types/src/extension/syntax.d.ts +3 -0
  64. package/dist/types/src/extension/syntax.d.ts.map +1 -0
  65. package/dist/types/src/index.d.ts +14 -0
  66. package/dist/types/src/index.d.ts.map +1 -1
  67. package/dist/types/src/mermaid-engine.d.ts +40 -1
  68. package/dist/types/src/mermaid-engine.d.ts.map +1 -1
  69. package/dist/types/src/mermaid.d.ts +3 -3
  70. package/dist/types/src/mermaid.d.ts.map +1 -1
  71. package/dist/types/src/nudge.d.ts +30 -0
  72. package/dist/types/src/nudge.d.ts.map +1 -0
  73. package/dist/types/src/nudge.test.d.ts +2 -0
  74. package/dist/types/src/nudge.test.d.ts.map +1 -0
  75. package/dist/types/src/objective.d.ts +4 -0
  76. package/dist/types/src/objective.d.ts.map +1 -1
  77. package/dist/types/src/ortho-router.d.ts +13 -1
  78. package/dist/types/src/ortho-router.d.ts.map +1 -1
  79. package/dist/types/src/ports.d.ts +40 -0
  80. package/dist/types/src/ports.d.ts.map +1 -0
  81. package/dist/types/src/ports.test.d.ts +2 -0
  82. package/dist/types/src/ports.test.d.ts.map +1 -0
  83. package/dist/types/src/relations.test.d.ts +2 -0
  84. package/dist/types/src/relations.test.d.ts.map +1 -0
  85. package/dist/types/src/rules.d.ts +30 -0
  86. package/dist/types/src/rules.d.ts.map +1 -0
  87. package/dist/types/src/scene.d.ts +108 -74
  88. package/dist/types/src/scene.d.ts.map +1 -1
  89. package/dist/types/src/score.d.ts +54 -0
  90. package/dist/types/src/score.d.ts.map +1 -0
  91. package/dist/types/src/score.test.d.ts +2 -0
  92. package/dist/types/src/score.test.d.ts.map +1 -0
  93. package/dist/types/src/semantic-engine.d.ts +41 -0
  94. package/dist/types/src/semantic-engine.d.ts.map +1 -0
  95. package/dist/types/src/semantic-engine.test.d.ts +2 -0
  96. package/dist/types/src/semantic-engine.test.d.ts.map +1 -0
  97. package/dist/types/src/semantic-place.d.ts +81 -0
  98. package/dist/types/src/semantic-place.d.ts.map +1 -0
  99. package/dist/types/src/semantic-route.d.ts +133 -0
  100. package/dist/types/src/semantic-route.d.ts.map +1 -0
  101. package/dist/types/src/semantic.d.ts +164 -0
  102. package/dist/types/src/semantic.d.ts.map +1 -0
  103. package/dist/types/src/uml-grid.d.ts.map +1 -1
  104. package/dist/types/src/uml.d.ts +3 -5
  105. package/dist/types/src/uml.d.ts.map +1 -1
  106. package/dist/types/src/view.d.ts +53 -0
  107. package/dist/types/src/view.d.ts.map +1 -0
  108. package/dist/types/src/view.test.d.ts +2 -0
  109. package/dist/types/src/view.test.d.ts.map +1 -0
  110. package/dist/types/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +23 -5
  112. package/rules/DIAGRAM.mdl +388 -0
  113. package/src/aesthetics.test.ts +89 -0
  114. package/src/aesthetics.ts +148 -0
  115. package/src/appeal.test.ts +144 -0
  116. package/src/appeal.ts +586 -0
  117. package/src/architecture.test.ts +94 -0
  118. package/src/architecture.ts +215 -0
  119. package/src/compact.test.ts +67 -0
  120. package/src/compact.ts +219 -0
  121. package/src/diagnostics.test.ts +207 -1
  122. package/src/diagnostics.ts +481 -37
  123. package/src/dsl/cli.ts +46 -0
  124. package/src/dsl/compile.ts +28 -0
  125. package/src/dsl/convert.test.ts +51 -0
  126. package/src/dsl/convert.ts +98 -0
  127. package/src/dsl/diagram.grammar +195 -0
  128. package/src/dsl/dsl.test.ts +286 -0
  129. package/src/dsl/gen/diagram.ts +22 -0
  130. package/src/dsl/index.ts +44 -0
  131. package/src/dsl/parse.ts +1054 -0
  132. package/src/dsl/print.ts +198 -0
  133. package/src/dsl/semantic.test.ts +216 -0
  134. package/src/dsl/vocabulary.ts +150 -0
  135. package/src/dx-svg.test.ts +44 -0
  136. package/src/dx-svg.ts +77 -0
  137. package/src/extension/complete.ts +105 -0
  138. package/src/extension/extension.test.ts +135 -0
  139. package/src/extension/highlight.ts +23 -0
  140. package/src/extension/index.ts +9 -0
  141. package/src/extension/language.ts +31 -0
  142. package/src/extension/lint.ts +60 -0
  143. package/src/extension/syntax.ts +62 -0
  144. package/src/index.ts +14 -0
  145. package/src/mermaid-engine.test.ts +6 -6
  146. package/src/mermaid-engine.ts +569 -119
  147. package/src/mermaid.test.ts +17 -1
  148. package/src/mermaid.ts +30 -18
  149. package/src/nudge.test.ts +186 -0
  150. package/src/nudge.ts +387 -0
  151. package/src/objective.test.ts +2 -0
  152. package/src/objective.ts +17 -1
  153. package/src/ortho-router.ts +240 -80
  154. package/src/ports.test.ts +157 -0
  155. package/src/ports.ts +186 -0
  156. package/src/relations.test.ts +98 -0
  157. package/src/rules.ts +117 -0
  158. package/src/scene.ts +66 -5
  159. package/src/score.test.ts +64 -0
  160. package/src/score.ts +130 -0
  161. package/src/semantic-engine.test.ts +635 -0
  162. package/src/semantic-engine.ts +891 -0
  163. package/src/semantic-place.ts +1508 -0
  164. package/src/semantic-route.ts +1136 -0
  165. package/src/semantic.ts +278 -0
  166. package/src/typings.d.ts +8 -0
  167. package/src/uml-grid.ts +6 -1
  168. package/src/uml.test.ts +5 -1
  169. package/src/uml.ts +24 -11
  170. package/src/view.test.ts +139 -0
  171. package/src/view.ts +376 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
- import { compile, parse } from './mermaid.ts';
7
+ import { compile, markers, parse } from './mermaid.ts';
8
8
  import type * as Scene from './scene.ts';
9
9
 
10
10
  /** Exercises node declarations, a labelled-blank subgraph, fan-out, and a `C <-> Y` cycle. */
@@ -55,6 +55,22 @@ describe('mermaid', () => {
55
55
  ]);
56
56
  });
57
57
 
58
+ test('reads the relation tokens as kinds with their UML markers', ({ expect }) => {
59
+ const graph = parse(
60
+ ['flowchart TB', ' B ..|> A', ' C -.-> D', ' E o--> F', ' G --{ H', ' I --|> J', ' K --> L'].join('\n'),
61
+ );
62
+ expect(graph.edges.map(({ kind }) => kind)).toEqual([
63
+ 'implements',
64
+ 'creates',
65
+ 'contains',
66
+ 'hasMany',
67
+ 'inheritance',
68
+ 'reference',
69
+ ]);
70
+ expect(markers('implements')).toEqual({ relation: 'implementation', stroke: 'dashed' });
71
+ expect(markers('creates')).toEqual({ relation: 'dependency', stroke: 'dashed' });
72
+ });
73
+
58
74
  test('compiles to one object per node plus a frame and an edge object', ({ expect }) => {
59
75
  const objects = objectsOf(compile(FLOWCHART));
60
76
 
package/src/mermaid.ts CHANGED
@@ -29,7 +29,7 @@ export type MermaidGroup = {
29
29
  };
30
30
 
31
31
  /** Relationship kind, from the edge token; drawn with the UML end markers in `markers`. */
32
- export type RelationKind = 'reference' | 'inheritance' | 'hasMany' | 'contains';
32
+ export type RelationKind = 'reference' | 'inheritance' | 'implements' | 'hasMany' | 'contains' | 'creates';
33
33
 
34
34
  export type MermaidEdge = {
35
35
  from: string;
@@ -39,23 +39,30 @@ export type MermaidEdge = {
39
39
  };
40
40
 
41
41
  /**
42
- * Edge tokens: mermaid's own arrows read as references; the classDiagram-style `--|>` (hollow
43
- * triangle) and ER-style `--{` (crow's foot) and `o-->` (circle at the source) extend the flowchart
44
- * grammar with the UML kinds, at the cost of mermaid.js rejecting those lines.
42
+ * Edge tokens: mermaid's own arrows read as references, except the dotted `-.->`, which reads as
43
+ * creation (UML's dashed «create» dependency). The classDiagram-style `--|>` (hollow triangle) and
44
+ * `..|>` (dashed hollow triangle) and ER-style `--{` (crow's foot) and `o-->` (circle at the owner)
45
+ * extend the flowchart grammar with the UML kinds, at the cost of mermaid.js rejecting those lines.
45
46
  */
46
47
  const EDGE_KINDS: Record<string, RelationKind> = {
47
48
  '-->': 'reference',
48
49
  '---': 'reference',
49
- '-.->': 'reference',
50
50
  '==>': 'reference',
51
+ '-.->': 'creates',
51
52
  '--|>': 'inheritance',
53
+ '..|>': 'implements',
52
54
  '--{': 'hasMany',
53
55
  'o-->': 'contains',
54
56
  };
55
57
 
56
58
  /** Reference-render labels for the UML kinds, used when the edge carries none of its own. */
57
- const UML_LABELS: Record<string, string> = { '--|>': 'extends', '--{': 'has many', 'o-->': 'contains' };
58
- const UML_EDGE = /^(\s*\S+)\s*(o-->|--\|>|--\{)\s*(?:\|(.*?)\|\s*)?(\S+)\s*$/;
59
+ const UML_LABELS: Record<string, string> = {
60
+ '--|>': 'extends',
61
+ '..|>': 'implements',
62
+ '--{': 'has many',
63
+ 'o-->': 'contains',
64
+ };
65
+ const UML_EDGE = /^(\s*\S+)\s*(o-->|--\|>|\.\.\|>|--\{)\s*(?:\|(.*?)\|\s*)?(\S+)\s*$/;
59
66
 
60
67
  /**
61
68
  * Rewrite the UML edge tokens into mermaid-legal labelled arrows, for rendering the source with
@@ -72,15 +79,19 @@ export const toStandard = (source: string): string =>
72
79
  })
73
80
  .join('\n');
74
81
 
75
- /** Scene arrow markers for a relationship kind. */
76
- export const markers = (kind: RelationKind): Pick<Scene.Arrow, 'head' | 'tail'> => {
82
+ /** The scene relation a relationship kind draws as, with the dash its line needs; a plain reference stays a plain arrow. */
83
+ export const markers = (kind: RelationKind): Pick<Scene.Arrow, 'relation' | 'stroke'> => {
77
84
  switch (kind) {
78
85
  case 'inheritance':
79
- return { head: 'triangle' };
86
+ return { relation: 'inheritance' };
87
+ case 'implements':
88
+ return { relation: 'implementation', stroke: 'dashed' };
80
89
  case 'hasMany':
81
- return { head: 'crowsfoot' };
90
+ return { relation: 'one-to-many' };
82
91
  case 'contains':
83
- return { tail: 'circle' };
92
+ return { relation: 'aggregation' };
93
+ case 'creates':
94
+ return { relation: 'dependency', stroke: 'dashed' };
84
95
  default:
85
96
  return {};
86
97
  }
@@ -97,8 +108,9 @@ const DIRECTIONS: Direction[] = ['TB', 'BT', 'LR', 'RL'];
97
108
 
98
109
  // `A[Label]`, `A(Label)`, `A{Label}` or a bare `A`.
99
110
  const NODE = /^([A-Za-z0-9_-]+)(?:\[(.*?)\]|\((.*?)\)|\{(.*?)\})?$/;
100
- // `A --> B`, `A-->|label|B`, `A --- B`, plus the UML kinds `B --|> A`, `X --{ Y`, `A o--> B`.
101
- const EDGE = /^(.+?)\s*(o-->|--\|>|--\{|-->|---|-\.->|==>)\s*(?:\|(.*?)\|\s*)?(.+)$/;
111
+ // `A --> B`, `A-->|label|B`, `A --- B`, `A -.-> B`, plus the UML kinds `B --|> A`, `B ..|> A`, `X --{ Y`
112
+ // and `A o--> B`.
113
+ const EDGE = /^(.+?)\s*(o-->|--\|>|\.\.\|>|--\{|-->|---|-\.->|==>)\s*(?:\|(.*?)\|\s*)?(.+)$/;
102
114
  const SUBGRAPH = /^subgraph\s+([A-Za-z0-9_-]+)(?:\s*\[(.*?)\])?\s*$/;
103
115
 
104
116
  // `%% ref A packages/core/echo` — a comment to mermaid proper, so sources stay portable.
@@ -114,7 +126,7 @@ export const parse = (source: string): MermaidGraph => {
114
126
  let direction: Direction = 'TB';
115
127
  const stack: string[] = [];
116
128
 
117
- const declare = (token: string): string | undefined => {
129
+ const declareNode = (token: string): string | undefined => {
118
130
  const match = NODE.exec(token.trim());
119
131
  if (!match) {
120
132
  return undefined;
@@ -172,8 +184,8 @@ export const parse = (source: string): MermaidGraph => {
172
184
  const edge = EDGE.exec(line);
173
185
  if (edge) {
174
186
  const [, from, token, label, to] = edge;
175
- const fromId = declare(from);
176
- const toId = declare(to);
187
+ const fromId = declareNode(from);
188
+ const toId = declareNode(to);
177
189
  if (fromId && toId) {
178
190
  edges.push({
179
191
  from: fromId,
@@ -185,7 +197,7 @@ export const parse = (source: string): MermaidGraph => {
185
197
  continue;
186
198
  }
187
199
 
188
- declare(line);
200
+ declareNode(line);
189
201
  }
190
202
 
191
203
  // Directives may precede or follow the node they name.
@@ -0,0 +1,186 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { nudge } from './nudge.ts';
8
+ import type * as Scene from './scene.ts';
9
+ import { type Rect } from './uml-grid.ts';
10
+
11
+ const orthogonal = (points: readonly Scene.Point[]) =>
12
+ points.slice(1).every((point, index) => point.x === points[index].x || point.y === points[index].y);
13
+
14
+ /** Whether two orthogonal polylines share a collinear stretch of positive length. */
15
+ const overlapping = (first: readonly Scene.Point[], second: readonly Scene.Point[]) => {
16
+ const segments = (points: readonly Scene.Point[]) =>
17
+ points.slice(1).map((end, index) => [points[index], end] as const);
18
+ return segments(first).some(([a0, a1]) =>
19
+ segments(second).some(([b0, b1]) => {
20
+ if (a0.x === a1.x && b0.x === b1.x && a0.x === b0.x) {
21
+ const lo = Math.max(Math.min(a0.y, a1.y), Math.min(b0.y, b1.y));
22
+ const hi = Math.min(Math.max(a0.y, a1.y), Math.max(b0.y, b1.y));
23
+ return hi - lo > 0;
24
+ }
25
+ if (a0.y === a1.y && b0.y === b1.y && a0.y === b0.y) {
26
+ const lo = Math.max(Math.min(a0.x, a1.x), Math.min(b0.x, b1.x));
27
+ const hi = Math.min(Math.max(a0.x, a1.x), Math.max(b0.x, b1.x));
28
+ return hi - lo > 0;
29
+ }
30
+ return false;
31
+ }),
32
+ );
33
+ };
34
+
35
+ const onBorder = (point: Scene.Point, rect: Rect) =>
36
+ ((point.y === rect.y || point.y === rect.y + rect.h) && point.x >= rect.x && point.x <= rect.x + rect.w) ||
37
+ ((point.x === rect.x || point.x === rect.x + rect.w) && point.y >= rect.y && point.y <= rect.y + rect.h);
38
+
39
+ describe('nudge', () => {
40
+ const source: Rect = { x: 0, y: 0, w: 128, h: 64 };
41
+ const left: Rect = { x: 0, y: 200, w: 128, h: 64 };
42
+ const right: Rect = { x: 200, y: 200, w: 128, h: 64 };
43
+ // A fork: both leave the same port and run down x = 64 together before one turns right.
44
+ const straight = {
45
+ points: [
46
+ { x: 64, y: 64 },
47
+ { x: 64, y: 200 },
48
+ ],
49
+ source,
50
+ target: left,
51
+ };
52
+ const turning = {
53
+ points: [
54
+ { x: 64, y: 64 },
55
+ { x: 64, y: 132 },
56
+ { x: 264, y: 132 },
57
+ { x: 264, y: 200 },
58
+ ],
59
+ source,
60
+ target: right,
61
+ };
62
+
63
+ test('two edges sharing a vertical run are separated and stay orthogonal', ({ expect }) => {
64
+ const [first, second] = nudge([straight, turning], { obstacles: [source, left, right] });
65
+ expect(orthogonal(first)).toBe(true);
66
+ expect(orthogonal(second)).toBe(true);
67
+ // The edge that turns right takes the right-hand lane, so its turn-off does not cut the other.
68
+ expect(second[0].x - first[0].x).toBeGreaterThanOrEqual(8);
69
+ expect(second[1].x).toBe(second[0].x);
70
+ expect(onBorder(first[0], source) && onBorder(second[0], source)).toBe(true);
71
+ expect(first[first.length - 1]).toEqual({ x: first[0].x, y: 200 });
72
+ expect(second[second.length - 1]).toEqual({ x: 264, y: 200 });
73
+ });
74
+
75
+ test('a fork sharing a vertical run and then a horizontal run is fully separated', ({ expect }) => {
76
+ const far: Rect = { x: 400, y: 200, w: 128, h: 64 };
77
+ const near = {
78
+ points: [
79
+ { x: 64, y: 64 },
80
+ { x: 64, y: 132 },
81
+ { x: 264, y: 132 },
82
+ { x: 264, y: 200 },
83
+ ],
84
+ source,
85
+ target: right,
86
+ };
87
+ const away = {
88
+ points: [
89
+ { x: 64, y: 64 },
90
+ { x: 64, y: 132 },
91
+ { x: 464, y: 132 },
92
+ { x: 464, y: 200 },
93
+ ],
94
+ source,
95
+ target: far,
96
+ };
97
+ const [first, second] = nudge([near, away], { obstacles: [source, right, far] });
98
+ expect(orthogonal(first) && orthogonal(second)).toBe(true);
99
+ expect(overlapping(first, second)).toBe(false);
100
+ });
101
+
102
+ test('a fixed bus never moves; the edge lying on it is pushed off', ({ expect }) => {
103
+ const bus = [
104
+ { x: 100, y: 100 },
105
+ { x: 200, y: 100 },
106
+ ];
107
+ const edge = {
108
+ points: [
109
+ { x: 64, y: 64 },
110
+ { x: 64, y: 100 },
111
+ { x: 264, y: 100 },
112
+ { x: 264, y: 200 },
113
+ ],
114
+ source,
115
+ target: right,
116
+ };
117
+ const fixed = [bus];
118
+ const [moved] = nudge([edge], { obstacles: [source, right], fixed });
119
+ expect(fixed[0]).toEqual(bus);
120
+ expect(Math.abs(moved[1].y - 100)).toBeGreaterThanOrEqual(8);
121
+ expect(moved[0]).toEqual({ x: 64, y: 64 });
122
+ expect(orthogonal(moved)).toBe(true);
123
+ });
124
+
125
+ test('ports stay put when separation would leave the box side', ({ expect }) => {
126
+ const narrow: Rect = { x: 0, y: 0, w: 16, h: 64 };
127
+ const below: Rect = { x: 0, y: 200, w: 16, h: 64 };
128
+ const path = {
129
+ points: [
130
+ { x: 8, y: 64 },
131
+ { x: 8, y: 200 },
132
+ ],
133
+ source: narrow,
134
+ target: below,
135
+ };
136
+ const result = nudge([path, path], { obstacles: [narrow, below] });
137
+ expect(result).toEqual([path.points, path.points]);
138
+ });
139
+
140
+ test('a run is never pushed through a box', ({ expect }) => {
141
+ // Both lanes beside the shared run are blocked by boxes hugging it.
142
+ const wall = (x: number): Rect => ({ x, y: 80, w: 40, h: 100 });
143
+ const edge = (y: number) => ({
144
+ points: [
145
+ { x: 64, y: 64 },
146
+ { x: 64, y: 200 },
147
+ ],
148
+ source,
149
+ target: { x: 0, y, w: 128, h: 64 },
150
+ });
151
+ const obstacles = [source, wall(20), wall(68), { x: 0, y: 200, w: 128, h: 64 }];
152
+ const result = nudge([edge(200), edge(200)], { obstacles });
153
+ expect(result.map((points) => points[0].x)).toEqual([64, 64]);
154
+ });
155
+
156
+ test('separates a shared run even when that costs one crossing', ({ expect }) => {
157
+ // Both leave the left side of one box; the line from the upper port turns off first, so either order crosses once.
158
+ const box: Rect = { x: 100, y: 0, w: 100, h: 40 };
159
+ const near: Rect = { x: 300, y: 80, w: 100, h: 40 };
160
+ const far: Rect = { x: 300, y: 180, w: 100, h: 40 };
161
+ const result = nudge([
162
+ {
163
+ points: [
164
+ { x: 100, y: 10 },
165
+ { x: 50, y: 10 },
166
+ { x: 50, y: 100 },
167
+ { x: 300, y: 100 },
168
+ ],
169
+ source: box,
170
+ target: near,
171
+ },
172
+ {
173
+ points: [
174
+ { x: 100, y: 30 },
175
+ { x: 50, y: 30 },
176
+ { x: 50, y: 200 },
177
+ { x: 300, y: 200 },
178
+ ],
179
+ source: box,
180
+ target: far,
181
+ },
182
+ ]);
183
+ expect(overlapping(result[0], result[1])).toBe(false);
184
+ expect(result.every(orthogonal)).toBe(true);
185
+ });
186
+ });