@dxos/diagram 0.13.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/diagram",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Renderer-neutral scene DSL, diagram dialects, and layout engines.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -47,9 +47,9 @@
47
47
  "@lezer/highlight": "^1.2.3",
48
48
  "@lezer/lr": "^1.4.10",
49
49
  "elkjs": "^0.12.0",
50
- "@dxos/effect": "0.13.0",
51
- "@dxos/invariant": "0.13.0",
52
- "@dxos/util": "0.13.0"
50
+ "@dxos/effect": "0.14.0",
51
+ "@dxos/invariant": "0.14.0",
52
+ "@dxos/util": "0.14.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@lezer/generator": "^1.8.0",
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
+ import * as Scene from '../scene.ts';
7
8
  import { parse, read } from './parse.ts';
8
9
  import { formatId } from './print.ts';
9
10
 
@@ -96,6 +97,21 @@ describe('semantic statements', () => {
96
97
  ]);
97
98
  });
98
99
 
100
+ test('edges: `tail=arrow` is one connector headed at both ends', ({ expect }) => {
101
+ const { diagram, problems } = read(`
102
+ node A node B
103
+ edge A -> B "syncs" tail=arrow
104
+ `);
105
+ expect(problems).toEqual([]);
106
+ expect(diagram?.edges).toHaveLength(1);
107
+ expect(diagram?.edges[0]).toMatchObject({ label: 'syncs', tail: 'arrow' });
108
+ expect(Scene.markersOf({ tail: 'arrow' })).toEqual({
109
+ start: 'arrow',
110
+ end: 'arrow',
111
+ dashed: false,
112
+ });
113
+ });
114
+
99
115
  test('buses: a fan with `bus`, and named buses across statements', ({ expect }) => {
100
116
  const { diagram, problems } = read(`
101
117
  node H node A node B node C
package/src/nudge.test.ts CHANGED
@@ -152,4 +152,35 @@ describe('nudge', () => {
152
152
  const result = nudge([edge(200), edge(200)], { obstacles });
153
153
  expect(result.map((points) => points[0].x)).toEqual([64, 64]);
154
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
+ });
155
186
  });
package/src/nudge.ts CHANGED
@@ -330,9 +330,11 @@ const separate = (
330
330
  }
331
331
  const overlaps = pairCount(candidate, changed, shared, true);
332
332
  const crossings = pairCount(candidate, changed, crosses);
333
+ const overlapsBefore = pairCount(lines, changed, shared, true);
334
+ // A crossing still reads as two lines and a shared run does not, so each overlap resolved may cost one crossing.
333
335
  if (
334
- overlaps >= pairCount(lines, changed, shared, true) ||
335
- crossings > pairCount(lines, changed, crosses) ||
336
+ overlaps >= overlapsBefore ||
337
+ crossings - pairCount(lines, changed, crosses) > overlapsBefore - overlaps ||
336
338
  obstructed(candidate, changed) > obstructed(lines, changed)
337
339
  ) {
338
340
  continue;
package/src/scene.ts CHANGED
@@ -144,7 +144,7 @@ export type Text = Schema.Schema.Type<typeof Text>;
144
144
  /** UML-style end markers: the head sits at the target, the tail at the source. */
145
145
  export const ArrowHead = Schema.Literals(['arrow', 'triangle', 'crowsfoot', 'none']);
146
146
  export type ArrowHead = Schema.Schema.Type<typeof ArrowHead>;
147
- export const ArrowTail = Schema.Literals(['none', 'circle']);
147
+ export const ArrowTail = Schema.Literals(['none', 'circle', 'arrow']);
148
148
  export type ArrowTail = Schema.Schema.Type<typeof ArrowTail>;
149
149
 
150
150
  /**
@@ -213,7 +213,7 @@ export const Arrow = Schema.Struct({
213
213
  description: 'Marker at the target end (default arrow): triangle for inheritance, crowsfoot for has-many.',
214
214
  }),
215
215
  tail: Schema.optional(ArrowTail).annotate({
216
- description: 'Marker at the source end (default none): circle for containment.',
216
+ description: 'Marker at the source end (default none): circle for containment, arrow for a two-way connector.',
217
217
  }),
218
218
  relation: Schema.optional(Relation).annotate({
219
219
  description:
@@ -29,8 +29,9 @@ const MAX_W = GRID * 6;
29
29
  const MIN_H = GRID * 3;
30
30
  const PAD_X = GRID;
31
31
  const PAD_Y = GRID / 2;
32
- /** Frame inset around a group's members; the top band also holds the title. */
32
+ /** Frame inset around a group's members, the same on every side; the title sits in the top inset. */
33
33
  const FRAME_PAD = GRID;
34
+ /** Room a frame's title takes above its members, when sizing the default pitch. */
34
35
  const FRAME_LABEL_H = GRID;
35
36
  /** Extra gutter where two frames face each other across a row gutter, so packages read as separate. */
36
37
  const FRAME_STACK_GAP = GRID * 2;
@@ -212,15 +213,17 @@ const geometry = (
212
213
  });
213
214
  return Math.max(stacked ? FRAME_STACK_GAP : 0, ...gaps);
214
215
  };
216
+ // An explicit grid is a lattice the boxes must stay on (a canvas lattice, say): extra space is whole cells.
217
+ const whole = (extra: number, step: number) => (diagram.grid && extra > 0 ? Math.ceil(extra / step) * step : extra);
215
218
  const colX = new Map<number, number>();
216
219
  for (let col = minCol, x = minCol * pitch.w; col <= maxCol; col++) {
217
220
  colX.set(col, x);
218
- x += pitch.w + extraAfterCol(col);
221
+ x += pitch.w + whole(extraAfterCol(col), pitch.w);
219
222
  }
220
223
  const rowY = new Map<number, number>();
221
224
  for (let row = minRow, y = minRow * pitch.h; row <= maxRow; row++) {
222
225
  rowY.set(row, y);
223
- y += pitch.h + extraAfterRow(row);
226
+ y += pitch.h + whole(extraAfterRow(row), pitch.h);
224
227
  }
225
228
  const at = (lookup: Map<number, number>, min: number, max: number, step: number, value: number) =>
226
229
  value < min
@@ -259,7 +262,7 @@ const geometry = (
259
262
  continue;
260
263
  }
261
264
  const x = Math.min(...members.map((rect) => rect.x)) - FRAME_PAD;
262
- const y = Math.min(...members.map((rect) => rect.y)) - FRAME_PAD - FRAME_LABEL_H;
265
+ const y = Math.min(...members.map((rect) => rect.y)) - FRAME_PAD;
263
266
  const right = Math.max(...members.map((rect) => rect.x + rect.w)) + FRAME_PAD;
264
267
  const bottom = Math.max(...members.map((rect) => rect.y + rect.h)) + FRAME_PAD;
265
268
  frames.set(group.id, { x, y, w: right - x, h: bottom - y });
@@ -273,7 +276,7 @@ const geometry = (
273
276
  ? [
274
277
  {
275
278
  x: frame.x + FRAME_PAD / 2,
276
- y: frame.y + (FRAME_LABEL_H - LABEL_FONT.lineH) / 2,
279
+ y: frame.y + (FRAME_PAD - LABEL_FONT.lineH) / 2,
277
280
  w: group.label.length * LABEL_FONT.charW,
278
281
  h: LABEL_FONT.lineH,
279
282
  },
@@ -424,7 +427,7 @@ const draw = (
424
427
  kind: 'text',
425
428
  id: 'label',
426
429
  x: FRAME_PAD / 2,
427
- y: (FRAME_LABEL_H - LABEL_FONT.lineH) / 2,
430
+ y: (FRAME_PAD - LABEL_FONT.lineH) / 2,
428
431
  text: group.label,
429
432
  weight: 's',
430
433
  color: 'grey',
package/src/view.ts CHANGED
@@ -162,7 +162,7 @@ const HEADS: Record<string, string> = {
162
162
  crowsfoot: "crow's foot head",
163
163
  none: 'no head',
164
164
  };
165
- const TAILS: Record<string, string> = { circle: 'dot at the start' };
165
+ const TAILS: Record<string, string> = { circle: 'dot at the start', arrow: 'arrowhead at the start' };
166
166
 
167
167
  /** A path's markers in words, e.g. "dashed, hollow triangle head", or undefined for a plain arrow. */
168
168
  const describe = (style?: Path['style']) => {