@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/dist/lib/chunk-parse.mjs +20 -10
- package/dist/lib/chunk-parse.mjs.map +1 -1
- package/dist/types/src/nudge.d.ts.map +1 -1
- package/dist/types/src/scene.d.ts +9 -9
- package/dist/types/src/scene.d.ts.map +1 -1
- package/dist/types/src/semantic-engine.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/dsl/semantic.test.ts +16 -0
- package/src/nudge.test.ts +31 -0
- package/src/nudge.ts +4 -2
- package/src/scene.ts +2 -2
- package/src/semantic-engine.ts +9 -6
- package/src/view.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/diagram",
|
|
3
|
-
"version": "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.
|
|
51
|
-
"@dxos/invariant": "0.
|
|
52
|
-
"@dxos/util": "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",
|
package/src/dsl/semantic.test.ts
CHANGED
|
@@ -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 >=
|
|
335
|
-
crossings
|
|
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:
|
package/src/semantic-engine.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 + (
|
|
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: (
|
|
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']) => {
|