@dxos/diagram 0.11.1 → 0.13.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 +9396 -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 +99 -65
  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 +200 -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 +155 -0
  150. package/src/nudge.ts +385 -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 +64 -3
  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 +888 -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 { analyze, errors } from './diagnostics.ts';
7
+ import { analyze, arrowPoints, bindTargets, errors, layoutLabel, routes } from './diagnostics.ts';
8
8
  import type * as Scene from './scene.ts';
9
9
  import { CLASS_DIAGRAM } from './testing.ts';
10
10
  import * as UmlEngine from './uml-engine.ts';
@@ -95,6 +95,40 @@ describe('diagnostics', () => {
95
95
  expect(fanIn.metrics.crossings).toBe(0);
96
96
  });
97
97
 
98
+ test('detects labels overlapping each other or a box, not a frame', ({ expect }) => {
99
+ const label = (id: string, x: number, y: number, text: string): Scene.Element => ({ kind: 'text', id, x, y, text });
100
+ const report = analyze([
101
+ box('a', 0, 0),
102
+ box('frame', -32, -32, 400, 400),
103
+ { id: 'edges', elements: [label('one', 100, 100, 'spawn child'), label('two', 110, 110, 'per process')] },
104
+ { id: 'more', elements: [label('three', 10, 10, 'on a box'), label('clear', 300, 300, 'x')] },
105
+ ]);
106
+ expect(report.metrics.textOverlaps).toBe(2);
107
+ expect(report.diagnostics.filter(({ code }) => code === 'text-overlap').map(({ refs }) => refs)).toEqual([
108
+ ['edges/one', 'edges/two'],
109
+ ['more/three', 'a/frame'],
110
+ ]);
111
+ });
112
+
113
+ test('detects connectors running along the same line, not ones that only cross or share a port', ({ expect }) => {
114
+ const report = analyze([
115
+ {
116
+ id: 'edges',
117
+ elements: [
118
+ arrow('trunk', { x: 0, y: 0 }, { x: 200, y: 0 }),
119
+ arrow('shares', { x: 100, y: 0 }, { x: 300, y: 0 }),
120
+ arrow('crosses', { x: 50, y: -50 }, { x: 50, y: 50 }),
121
+ arrow('port', { x: 0, y: 0 }, { x: 0, y: 100 }),
122
+ ],
123
+ },
124
+ ]);
125
+ expect(report.metrics.edgeOverlaps).toBe(1);
126
+ expect(report.diagnostics.find(({ code }) => code === 'edge-overlap')?.refs).toEqual([
127
+ 'edges/trunk',
128
+ 'edges/shares',
129
+ ]);
130
+ });
131
+
98
132
  test('rejoins a routed path with its arrow head and counts its bends', ({ expect }) => {
99
133
  const report = analyze([
100
134
  {
@@ -143,6 +177,178 @@ describe('diagnostics', () => {
143
177
  });
144
178
  });
145
179
 
180
+ describe('connector geometry', () => {
181
+ const line = (id: string, points: Scene.Point[]): Scene.Element => ({ kind: 'line', id, points });
182
+
183
+ test('a bound arrow is measured along the clipped segment the renderer draws', ({ expect }) => {
184
+ const bound: Scene.WorldObject = {
185
+ id: 'edges',
186
+ elements: [{ kind: 'arrow', id: 'a-b', from: 'a/frame', to: 'b/frame' }],
187
+ };
188
+ const report = analyze([box('a', 0, 0), box('blocker', 0, 128), box('b', 0, 320), bound]);
189
+ expect(report.metrics.connectors).toBe(1);
190
+ expect(report.metrics.length).toBe(256);
191
+ expect(report.diagnostics.map(({ code }) => code)).toEqual(['route-through-node']);
192
+
193
+ const objects = [box('a', 0, 0), box('b', 200, 200), bound];
194
+ expect(
195
+ arrowPoints(bound, { kind: 'arrow', id: 'a-b', from: 'a/frame', to: 'b/frame' }, bindTargets(objects)),
196
+ ).toEqual([
197
+ { x: 64, y: 64 },
198
+ { x: 200, y: 200 },
199
+ ]);
200
+ });
201
+
202
+ test('a T where one connector ends on another at a port is not a crossing', ({ expect }) => {
203
+ const report = analyze([
204
+ box('port', 40, 50, 20, 20),
205
+ {
206
+ id: 'edges',
207
+ elements: [
208
+ arrow('along', { x: 0, y: 50 }, { x: 100, y: 50 }),
209
+ arrow('into', { x: 50, y: 100 }, { x: 50, y: 50 }),
210
+ ],
211
+ },
212
+ ]);
213
+ expect(report.metrics.crossings).toBe(0);
214
+ });
215
+
216
+ test('a bus trunk and its spokes are one connector, traced once per spoke', ({ expect }) => {
217
+ const objects: Scene.WorldObject[] = [
218
+ box('hub', 68, 0),
219
+ box('left', -12, 200),
220
+ box('middle', 28, 200),
221
+ box('other', 300, 100),
222
+ {
223
+ id: 'edges',
224
+ elements: [
225
+ line('hub-bus-0-trunk', [
226
+ { x: 100, y: 64 },
227
+ { x: 100, y: 120 },
228
+ ]),
229
+ // Both spokes leave the junction along one run before they part.
230
+ line('hub-left-path', [
231
+ { x: 100, y: 120 },
232
+ { x: 60, y: 120 },
233
+ { x: 20, y: 120 },
234
+ ]),
235
+ arrow('hub-left', { x: 20, y: 120 }, { x: 20, y: 200 }),
236
+ line('hub-middle-path', [
237
+ { x: 100, y: 120 },
238
+ { x: 60, y: 120 },
239
+ ]),
240
+ arrow('hub-middle', { x: 60, y: 120 }, { x: 60, y: 200 }),
241
+ // One unrelated connector crosses the trunk and both spokes' shared run.
242
+ line('stray-path', [
243
+ { x: 332, y: 100 },
244
+ { x: 332, y: 90 },
245
+ { x: 80, y: 90 },
246
+ ]),
247
+ arrow('stray', { x: 80, y: 90 }, { x: 80, y: 150 }),
248
+ ],
249
+ },
250
+ ];
251
+ const report = analyze(objects, { maxBends: 1 });
252
+ expect(report.metrics).toMatchObject({ connectors: 3, crossings: 1, edgeOverlaps: 0 });
253
+ // A spoke is traced from the hub; the turn at the junction is the bus's shape, its own corner a bend.
254
+ expect(report.diagnostics.filter(({ code }) => code === 'excessive-bends').map(({ refs }) => refs)).toEqual([
255
+ ['edges/stray'],
256
+ ]);
257
+ expect(routes(objects).find(({ ref }) => ref === 'edges/hub-left')).toMatchObject({ bends: 1 });
258
+ expect(routes(objects).find(({ ref }) => ref === 'edges/hub-left')?.points).toEqual([
259
+ { x: 100, y: 64 },
260
+ { x: 100, y: 120 },
261
+ { x: 20, y: 120 },
262
+ { x: 20, y: 200 },
263
+ ]);
264
+ });
265
+
266
+ test('stubs meeting an inheritance bus mid-line do not cross it', ({ expect }) => {
267
+ const report = analyze([
268
+ box('base', 68, 0),
269
+ box('first', 0, 200),
270
+ box('second', 68, 200),
271
+ box('third', 136, 200),
272
+ {
273
+ id: 'edges',
274
+ elements: [
275
+ line('base-bus', [
276
+ { x: 32, y: 132 },
277
+ { x: 168, y: 132 },
278
+ ]),
279
+ ...[32, 100, 168].map((x, index) =>
280
+ line(`sub-${index}-base-stub-${index}`, [
281
+ { x, y: 200 },
282
+ { x, y: 132 },
283
+ ]),
284
+ ),
285
+ {
286
+ kind: 'arrow',
287
+ id: 'base-inherit',
288
+ start: { x: 100, y: 132 },
289
+ end: { x: 100, y: 64 },
290
+ relation: 'inheritance',
291
+ },
292
+ ],
293
+ },
294
+ ]);
295
+ expect(report.metrics).toMatchObject({ connectors: 3, crossings: 0, edgeOverlaps: 0 });
296
+ });
297
+
298
+ test('a self-loop is drawn off its box and measures without NaN or a crossing', ({ expect }) => {
299
+ const objects: Scene.WorldObject[] = [
300
+ box('a', 0, 0),
301
+ box('b', 0, 200),
302
+ {
303
+ id: 'edges',
304
+ elements: [
305
+ { kind: 'arrow', id: 'a-a', from: 'a/frame', to: 'a/frame' },
306
+ { kind: 'arrow', id: 'a-b', from: 'a/frame', to: 'b/frame' },
307
+ ],
308
+ },
309
+ ];
310
+ const { metrics, diagnostics } = analyze(objects);
311
+ expect(Object.values(metrics).every((value) => !Number.isNaN(value))).toBe(true);
312
+ expect(metrics).toMatchObject({ connectors: 2, crossings: 0, routesThroughNodes: 0, bends: 3 });
313
+ expect(diagnostics).toEqual([]);
314
+ });
315
+
316
+ test('a connector with an empty path is skipped rather than bused', ({ expect }) => {
317
+ const objects: Scene.WorldObject[] = [
318
+ box('a', 0, 0),
319
+ box('b', 0, 200),
320
+ {
321
+ id: 'edges',
322
+ elements: [{ kind: 'line', id: 'stray-path', points: [] }, arrow('a-b', { x: 32, y: 64 }, { x: 32, y: 200 })],
323
+ },
324
+ ];
325
+ expect(analyze(objects).metrics).toMatchObject({ connectors: 1, crossings: 0 });
326
+ expect(routes(objects).map(({ ref }) => ref)).toEqual(['edges/a-b']);
327
+ });
328
+ });
329
+
330
+ describe('box labels', () => {
331
+ test('a long label wraps at camel case, and one too long to fit is ellipsized at a smaller size', ({ expect }) => {
332
+ const wrapped = layoutLabel('RemoteProcessHandleWithLongName', 'm', { w: 192, h: 160 });
333
+ expect(wrapped).toMatchObject({ lines: ['RemoteProcess', 'HandleWithLong', 'Name'], size: 18, overflow: false });
334
+
335
+ const squeezed = layoutLabel('RemoteProcessHandleWithLongName', 'm', { w: 192, h: 64 });
336
+ expect(squeezed.size).toBeLessThan(18);
337
+ expect(squeezed.overflow).toBe(false);
338
+ expect(squeezed.lines.join('')).toBe('RemoteProcessHandleWithLongName');
339
+
340
+ const truncated = layoutLabel('RemoteProcessHandleWithLongName and then some more words', 'm', { w: 192, h: 40 });
341
+ expect(truncated.overflow).toBe(true);
342
+ expect(truncated.lines).toHaveLength(1);
343
+ expect(truncated.lines[0].endsWith('…')).toBe(true);
344
+ });
345
+
346
+ test('label-overflow reports what the renderer cannot show, not text it wraps', ({ expect }) => {
347
+ const report = analyze([box('a', 0, 0, 192, 160, 'RemoteProcessHandleWithLongName')]);
348
+ expect(report.metrics.labelOverflows).toBe(0);
349
+ });
350
+ });
351
+
146
352
  //
147
353
  // Tier 1: every placement strategy must produce a scene free of hard defects for the shared
148
354
  // fixture. Soft metrics are recorded in the snapshot so a layout change reads as a diff.