@adecore/diagram 0.0.1 → 0.17.0-beta.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 +110 -0
- package/README.md +35 -0
- package/dist/across.d.ts +2 -0
- package/dist/across.js +202 -0
- package/dist/graph.d.ts +40 -0
- package/dist/graph.js +14 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/layout.d.ts +55 -0
- package/dist/layout.js +668 -0
- package/dist/order.d.ts +2 -0
- package/dist/order.js +224 -0
- package/dist/protocol.d.ts +254 -0
- package/dist/protocol.js +111 -0
- package/dist/reading-order.d.ts +2 -0
- package/dist/reading-order.js +23 -0
- package/dist/route.d.ts +33 -0
- package/dist/route.js +206 -0
- package/dist/shapes.d.ts +20 -0
- package/dist/shapes.js +95 -0
- package/dist/svg.d.ts +16 -0
- package/dist/svg.js +67 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.js +86 -0
- package/package.json +41 -2
- package/src/across.ts +213 -0
- package/src/graph.ts +70 -0
- package/src/index.ts +30 -0
- package/src/layout.ts +778 -0
- package/src/order.ts +235 -0
- package/src/protocol.ts +135 -0
- package/src/reading-order.ts +26 -0
- package/src/route.ts +266 -0
- package/src/shapes.ts +119 -0
- package/src/svg.ts +103 -0
- package/src/text.ts +92 -0
package/dist/order.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { bandCode, bandOfCode } from './graph.js';
|
|
2
|
+
/* Sweeps of the barycenter heuristic, alternating down and up; the best order any of them reached is kept. */
|
|
3
|
+
const SWEEPS = 12;
|
|
4
|
+
/*
|
|
5
|
+
* The order inside every layer, in place. A unit moves to the average position of the units it is
|
|
6
|
+
* joined to in the layer it was compared with, a tie keeps the order it had (file order to begin
|
|
7
|
+
* with), and the members of a group stay together as one block. The blocks of groups keep one order
|
|
8
|
+
* in every layer, and a unit that is not in a group stays on the same side of a group from one layer
|
|
9
|
+
* to the next inside it, so no hop has to cross a group it does not belong to.
|
|
10
|
+
*/
|
|
11
|
+
export function orderLayers(layering) {
|
|
12
|
+
const { units, hops, bands, items } = layering;
|
|
13
|
+
const position = new Array(units.length).fill(0);
|
|
14
|
+
const bandPosition = bands.map((band) => new Array(band.last - band.first + 1).fill(0));
|
|
15
|
+
const hopsByLayer = items.map(() => []);
|
|
16
|
+
hops.forEach((hop, index) => {
|
|
17
|
+
hopsByLayer[units[hop.from].layer].push(index);
|
|
18
|
+
});
|
|
19
|
+
const flatten = (layer) => {
|
|
20
|
+
let at = 0;
|
|
21
|
+
for (const code of items[layer]) {
|
|
22
|
+
if (code >= 0) {
|
|
23
|
+
position[code] = at++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const band = bands[bandOfCode(code)];
|
|
27
|
+
const members = band.members[layer - band.first];
|
|
28
|
+
bandPosition[bandOfCode(code)][layer - band.first] = at;
|
|
29
|
+
if (members.length === 0) {
|
|
30
|
+
at++;
|
|
31
|
+
}
|
|
32
|
+
for (const member of members) {
|
|
33
|
+
position[member] = at++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const enforceBandOrder = (layer) => {
|
|
38
|
+
const row = items[layer];
|
|
39
|
+
const slots = [];
|
|
40
|
+
const present = [];
|
|
41
|
+
row.forEach((code, index) => {
|
|
42
|
+
if (code < 0) {
|
|
43
|
+
slots.push(index);
|
|
44
|
+
present.push(bandOfCode(code));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
present.sort((left, right) => bands[left].rank - bands[right].rank);
|
|
48
|
+
slots.forEach((slot, index) => {
|
|
49
|
+
row[slot] = bandCode(present[index]);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const sortLayer = (layer, towardsUp) => {
|
|
53
|
+
const keyOf = (unit) => {
|
|
54
|
+
const neighbors = towardsUp ? units[unit].up : units[unit].down;
|
|
55
|
+
if (neighbors.length === 0) {
|
|
56
|
+
return position[unit];
|
|
57
|
+
}
|
|
58
|
+
let sum = 0;
|
|
59
|
+
for (const neighbor of neighbors) {
|
|
60
|
+
sum += position[neighbor];
|
|
61
|
+
}
|
|
62
|
+
return sum / neighbors.length;
|
|
63
|
+
};
|
|
64
|
+
const keyed = items[layer].map((code, index) => {
|
|
65
|
+
if (code >= 0) {
|
|
66
|
+
return { code, index, key: keyOf(code) };
|
|
67
|
+
}
|
|
68
|
+
const band = bands[bandOfCode(code)];
|
|
69
|
+
const slot = layer - band.first;
|
|
70
|
+
const members = band.members[slot].map((unit) => ({ unit, key: keyOf(unit), at: position[unit] }));
|
|
71
|
+
members.sort((left, right) => left.key - right.key || left.at - right.at);
|
|
72
|
+
band.members[slot] = members.map((member) => member.unit);
|
|
73
|
+
const key = members.length > 0 ? members.reduce((sum, member) => sum + member.key, 0) / members.length : bandPosition[bandOfCode(code)][slot];
|
|
74
|
+
return { code, index, key };
|
|
75
|
+
});
|
|
76
|
+
keyed.sort((left, right) => left.key - right.key || left.index - right.index);
|
|
77
|
+
items[layer] = keyed.map((entry) => entry.code);
|
|
78
|
+
enforceBandOrder(layer);
|
|
79
|
+
flatten(layer);
|
|
80
|
+
};
|
|
81
|
+
const updateRanks = () => {
|
|
82
|
+
const scores = bands.map((band, index) => {
|
|
83
|
+
let sum = 0;
|
|
84
|
+
for (let layer = band.first; layer <= band.last; layer++) {
|
|
85
|
+
const row = items[layer];
|
|
86
|
+
sum += row.indexOf(bandCode(index)) / row.length;
|
|
87
|
+
}
|
|
88
|
+
return sum / (band.last - band.first + 1);
|
|
89
|
+
});
|
|
90
|
+
const order = bands.map((_, index) => index).sort((left, right) => scores[left] - scores[right] || bands[left].rank - bands[right].rank);
|
|
91
|
+
order.forEach((band, rank) => {
|
|
92
|
+
bands[band].rank = rank;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
// A free unit takes the side of every group its neighbors are on, as far as they agree.
|
|
96
|
+
const keepSides = () => {
|
|
97
|
+
for (let layer = 0; layer < items.length; layer++) {
|
|
98
|
+
if (!items[layer].some((code) => code < 0)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const previousIndex = new Map();
|
|
102
|
+
(items[layer - 1] ?? []).forEach((code, index) => previousIndex.set(code, index));
|
|
103
|
+
const free = items[layer].filter((code) => code >= 0);
|
|
104
|
+
for (const unit of free) {
|
|
105
|
+
const row = items[layer];
|
|
106
|
+
const present = row.filter((code) => code < 0).map(bandOfCode);
|
|
107
|
+
let lower = 0;
|
|
108
|
+
let upper = present.length;
|
|
109
|
+
const narrow = (source, sourceLayer) => {
|
|
110
|
+
let low = 0;
|
|
111
|
+
let high = present.length;
|
|
112
|
+
present.forEach((band, slot) => {
|
|
113
|
+
const target = bands[band];
|
|
114
|
+
const sourceBand = units[source].band;
|
|
115
|
+
if (sourceLayer < target.first || sourceLayer > target.last || sourceBand === band) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const before = sourceBand >= 0 ? bands[sourceBand].rank < target.rank : previousIndex.get(source) < previousIndex.get(bandCode(band));
|
|
119
|
+
if (before) {
|
|
120
|
+
high = Math.min(high, slot);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
low = Math.max(low, slot + 1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (Math.max(lower, low) <= Math.min(upper, high)) {
|
|
127
|
+
lower = Math.max(lower, low);
|
|
128
|
+
upper = Math.min(upper, high);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
for (const source of units[unit].up) {
|
|
132
|
+
narrow(source, layer - 1);
|
|
133
|
+
}
|
|
134
|
+
// A member of a group in the next layer cannot move, so the free unit follows it instead.
|
|
135
|
+
for (const target of units[unit].down) {
|
|
136
|
+
if (units[target].band >= 0) {
|
|
137
|
+
narrow(target, layer + 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const at = row.indexOf(unit);
|
|
141
|
+
const slot = row.slice(0, at).filter((code) => code < 0).length;
|
|
142
|
+
if (slot < lower) {
|
|
143
|
+
row.splice(at, 1);
|
|
144
|
+
row.splice(row.indexOf(bandCode(present[lower - 1])) + 1, 0, unit);
|
|
145
|
+
}
|
|
146
|
+
else if (slot > upper) {
|
|
147
|
+
row.splice(at, 1);
|
|
148
|
+
row.splice(row.indexOf(bandCode(present[upper])), 0, unit);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const crossings = () => {
|
|
154
|
+
let total = 0;
|
|
155
|
+
for (let layer = 0; layer + 1 < items.length; layer++) {
|
|
156
|
+
const pairs = hopsByLayer[layer].map((index) => [position[hops[index].from], position[hops[index].to]]);
|
|
157
|
+
if (pairs.length < 2) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
pairs.sort((left, right) => left[0] - right[0] || left[1] - right[1]);
|
|
161
|
+
// A Fenwick tree over the positions of the next layer counts the pairs that come in out of order.
|
|
162
|
+
const size = pairs.reduce((largest, pair) => Math.max(largest, pair[1]), 0) + 2;
|
|
163
|
+
const tree = new Array(size + 1).fill(0);
|
|
164
|
+
pairs.forEach(([, to], seen) => {
|
|
165
|
+
let atMost = 0;
|
|
166
|
+
for (let i = to + 1; i > 0; i -= i & -i) {
|
|
167
|
+
atMost += tree[i];
|
|
168
|
+
}
|
|
169
|
+
total += seen - atMost;
|
|
170
|
+
for (let i = to + 1; i <= size; i += i & -i) {
|
|
171
|
+
tree[i] += 1;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return total;
|
|
176
|
+
};
|
|
177
|
+
const settle = () => {
|
|
178
|
+
items.forEach((_, layer) => enforceBandOrder(layer));
|
|
179
|
+
keepSides();
|
|
180
|
+
items.forEach((_, layer) => flatten(layer));
|
|
181
|
+
return crossings();
|
|
182
|
+
};
|
|
183
|
+
const snapshot = () => ({
|
|
184
|
+
items: items.map((row) => row.slice()),
|
|
185
|
+
members: bands.map((band) => band.members.map((slot) => slot.slice())),
|
|
186
|
+
ranks: bands.map((band) => band.rank)
|
|
187
|
+
});
|
|
188
|
+
items.forEach((_, layer) => flatten(layer));
|
|
189
|
+
// A band's first layer always holds a member, which is what made it the first.
|
|
190
|
+
const firstSeen = bands.map((band, index) => [band.first, position[band.members[0][0]], index]);
|
|
191
|
+
firstSeen
|
|
192
|
+
.slice()
|
|
193
|
+
.sort((left, right) => left[0] - right[0] || left[1] - right[1] || left[2] - right[2])
|
|
194
|
+
.forEach(([, , band], rank) => {
|
|
195
|
+
bands[band].rank = rank;
|
|
196
|
+
});
|
|
197
|
+
let bestCount = settle();
|
|
198
|
+
let best = snapshot();
|
|
199
|
+
for (let sweep = 0; sweep < SWEEPS && bestCount > 0; sweep++) {
|
|
200
|
+
if (sweep % 2 === 0) {
|
|
201
|
+
for (let layer = 1; layer < items.length; layer++) {
|
|
202
|
+
sortLayer(layer, true);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
for (let layer = items.length - 2; layer >= 0; layer--) {
|
|
207
|
+
sortLayer(layer, false);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
updateRanks();
|
|
211
|
+
const count = settle();
|
|
212
|
+
if (count < bestCount) {
|
|
213
|
+
bestCount = count;
|
|
214
|
+
best = snapshot();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
best.items.forEach((row, layer) => {
|
|
218
|
+
items[layer] = row;
|
|
219
|
+
});
|
|
220
|
+
bands.forEach((band, index) => {
|
|
221
|
+
band.members = best.members[index];
|
|
222
|
+
band.rank = best.ranks[index];
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const DiagramShapeSchema: z.ZodEnum<{
|
|
3
|
+
cylinder: "cylinder";
|
|
4
|
+
diamond: "diamond";
|
|
5
|
+
pill: "pill";
|
|
6
|
+
rect: "rect";
|
|
7
|
+
round: "round";
|
|
8
|
+
}>;
|
|
9
|
+
export type DiagramShape = z.infer<typeof DiagramShapeSchema>;
|
|
10
|
+
export declare const DIAGRAM_SHAPES: ("cylinder" | "diamond" | "pill" | "rect" | "round")[];
|
|
11
|
+
export declare const DiagramDirectionSchema: z.ZodEnum<{
|
|
12
|
+
down: "down";
|
|
13
|
+
right: "right";
|
|
14
|
+
}>;
|
|
15
|
+
export type DiagramDirection = z.infer<typeof DiagramDirectionSchema>;
|
|
16
|
+
export declare const DiagramEdgeStyleSchema: z.ZodEnum<{
|
|
17
|
+
dashed: "dashed";
|
|
18
|
+
dotted: "dotted";
|
|
19
|
+
solid: "solid";
|
|
20
|
+
}>;
|
|
21
|
+
export type DiagramEdgeStyle = z.infer<typeof DiagramEdgeStyleSchema>;
|
|
22
|
+
export declare const DiagramNodeSchema: z.ZodObject<{
|
|
23
|
+
id: z.ZodString;
|
|
24
|
+
label: z.ZodString;
|
|
25
|
+
sub: z.ZodOptional<z.ZodString>;
|
|
26
|
+
shape: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
cylinder: "cylinder";
|
|
28
|
+
diamond: "diamond";
|
|
29
|
+
pill: "pill";
|
|
30
|
+
rect: "rect";
|
|
31
|
+
round: "round";
|
|
32
|
+
}>>;
|
|
33
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
34
|
+
accent: "accent";
|
|
35
|
+
blue: "blue";
|
|
36
|
+
green: "green";
|
|
37
|
+
ink: "ink";
|
|
38
|
+
muted: "muted";
|
|
39
|
+
orange: "orange";
|
|
40
|
+
pink: "pink";
|
|
41
|
+
purple: "purple";
|
|
42
|
+
red: "red";
|
|
43
|
+
yellow: "yellow";
|
|
44
|
+
}>>;
|
|
45
|
+
pos: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type DiagramNode = z.infer<typeof DiagramNodeSchema>;
|
|
48
|
+
export declare const DiagramGroupSchema: z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
label: z.ZodString;
|
|
51
|
+
wraps: z.ZodArray<z.ZodString>;
|
|
52
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
53
|
+
accent: "accent";
|
|
54
|
+
blue: "blue";
|
|
55
|
+
green: "green";
|
|
56
|
+
ink: "ink";
|
|
57
|
+
muted: "muted";
|
|
58
|
+
orange: "orange";
|
|
59
|
+
pink: "pink";
|
|
60
|
+
purple: "purple";
|
|
61
|
+
red: "red";
|
|
62
|
+
yellow: "yellow";
|
|
63
|
+
}>>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
export type DiagramGroup = z.infer<typeof DiagramGroupSchema>;
|
|
66
|
+
export declare const DiagramEdgeSchema: z.ZodObject<{
|
|
67
|
+
from: z.ZodString;
|
|
68
|
+
to: z.ZodString;
|
|
69
|
+
label: z.ZodOptional<z.ZodString>;
|
|
70
|
+
style: z.ZodOptional<z.ZodEnum<{
|
|
71
|
+
dashed: "dashed";
|
|
72
|
+
dotted: "dotted";
|
|
73
|
+
solid: "solid";
|
|
74
|
+
}>>;
|
|
75
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
76
|
+
accent: "accent";
|
|
77
|
+
blue: "blue";
|
|
78
|
+
green: "green";
|
|
79
|
+
ink: "ink";
|
|
80
|
+
muted: "muted";
|
|
81
|
+
orange: "orange";
|
|
82
|
+
pink: "pink";
|
|
83
|
+
purple: "purple";
|
|
84
|
+
red: "red";
|
|
85
|
+
yellow: "yellow";
|
|
86
|
+
}>>;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
export type DiagramEdge = z.infer<typeof DiagramEdgeSchema>;
|
|
89
|
+
export declare const DiagramMetaSchema: z.ZodObject<{
|
|
90
|
+
title: z.ZodString;
|
|
91
|
+
direction: z.ZodEnum<{
|
|
92
|
+
down: "down";
|
|
93
|
+
right: "right";
|
|
94
|
+
}>;
|
|
95
|
+
}, z.core.$strip>;
|
|
96
|
+
export type DiagramMeta = z.infer<typeof DiagramMetaSchema>;
|
|
97
|
+
export declare const DIAGRAM_VERSION = 1;
|
|
98
|
+
export declare const DIAGRAM_LIMITS: {
|
|
99
|
+
nodes: number;
|
|
100
|
+
edges: number;
|
|
101
|
+
};
|
|
102
|
+
export declare const DiagramContentSchema: z.ZodObject<{
|
|
103
|
+
meta: z.ZodObject<{
|
|
104
|
+
title: z.ZodString;
|
|
105
|
+
direction: z.ZodEnum<{
|
|
106
|
+
down: "down";
|
|
107
|
+
right: "right";
|
|
108
|
+
}>;
|
|
109
|
+
}, z.core.$strip>;
|
|
110
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
111
|
+
id: z.ZodString;
|
|
112
|
+
label: z.ZodString;
|
|
113
|
+
sub: z.ZodOptional<z.ZodString>;
|
|
114
|
+
shape: z.ZodOptional<z.ZodEnum<{
|
|
115
|
+
cylinder: "cylinder";
|
|
116
|
+
diamond: "diamond";
|
|
117
|
+
pill: "pill";
|
|
118
|
+
rect: "rect";
|
|
119
|
+
round: "round";
|
|
120
|
+
}>>;
|
|
121
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
122
|
+
accent: "accent";
|
|
123
|
+
blue: "blue";
|
|
124
|
+
green: "green";
|
|
125
|
+
ink: "ink";
|
|
126
|
+
muted: "muted";
|
|
127
|
+
orange: "orange";
|
|
128
|
+
pink: "pink";
|
|
129
|
+
purple: "purple";
|
|
130
|
+
red: "red";
|
|
131
|
+
yellow: "yellow";
|
|
132
|
+
}>>;
|
|
133
|
+
pos: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
|
|
134
|
+
}, z.core.$strip>>;
|
|
135
|
+
groups: z.ZodArray<z.ZodObject<{
|
|
136
|
+
id: z.ZodString;
|
|
137
|
+
label: z.ZodString;
|
|
138
|
+
wraps: z.ZodArray<z.ZodString>;
|
|
139
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
140
|
+
accent: "accent";
|
|
141
|
+
blue: "blue";
|
|
142
|
+
green: "green";
|
|
143
|
+
ink: "ink";
|
|
144
|
+
muted: "muted";
|
|
145
|
+
orange: "orange";
|
|
146
|
+
pink: "pink";
|
|
147
|
+
purple: "purple";
|
|
148
|
+
red: "red";
|
|
149
|
+
yellow: "yellow";
|
|
150
|
+
}>>;
|
|
151
|
+
}, z.core.$strip>>;
|
|
152
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
153
|
+
from: z.ZodString;
|
|
154
|
+
to: z.ZodString;
|
|
155
|
+
label: z.ZodOptional<z.ZodString>;
|
|
156
|
+
style: z.ZodOptional<z.ZodEnum<{
|
|
157
|
+
dashed: "dashed";
|
|
158
|
+
dotted: "dotted";
|
|
159
|
+
solid: "solid";
|
|
160
|
+
}>>;
|
|
161
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
162
|
+
accent: "accent";
|
|
163
|
+
blue: "blue";
|
|
164
|
+
green: "green";
|
|
165
|
+
ink: "ink";
|
|
166
|
+
muted: "muted";
|
|
167
|
+
orange: "orange";
|
|
168
|
+
pink: "pink";
|
|
169
|
+
purple: "purple";
|
|
170
|
+
red: "red";
|
|
171
|
+
yellow: "yellow";
|
|
172
|
+
}>>;
|
|
173
|
+
}, z.core.$strip>>;
|
|
174
|
+
}, z.core.$strip>;
|
|
175
|
+
export type DiagramContent = z.infer<typeof DiagramContentSchema>;
|
|
176
|
+
export declare const DiagramDocumentSchema: z.ZodObject<{
|
|
177
|
+
meta: z.ZodObject<{
|
|
178
|
+
title: z.ZodString;
|
|
179
|
+
direction: z.ZodEnum<{
|
|
180
|
+
down: "down";
|
|
181
|
+
right: "right";
|
|
182
|
+
}>;
|
|
183
|
+
}, z.core.$strip>;
|
|
184
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
185
|
+
id: z.ZodString;
|
|
186
|
+
label: z.ZodString;
|
|
187
|
+
sub: z.ZodOptional<z.ZodString>;
|
|
188
|
+
shape: z.ZodOptional<z.ZodEnum<{
|
|
189
|
+
cylinder: "cylinder";
|
|
190
|
+
diamond: "diamond";
|
|
191
|
+
pill: "pill";
|
|
192
|
+
rect: "rect";
|
|
193
|
+
round: "round";
|
|
194
|
+
}>>;
|
|
195
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
196
|
+
accent: "accent";
|
|
197
|
+
blue: "blue";
|
|
198
|
+
green: "green";
|
|
199
|
+
ink: "ink";
|
|
200
|
+
muted: "muted";
|
|
201
|
+
orange: "orange";
|
|
202
|
+
pink: "pink";
|
|
203
|
+
purple: "purple";
|
|
204
|
+
red: "red";
|
|
205
|
+
yellow: "yellow";
|
|
206
|
+
}>>;
|
|
207
|
+
pos: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
|
|
208
|
+
}, z.core.$strip>>;
|
|
209
|
+
groups: z.ZodArray<z.ZodObject<{
|
|
210
|
+
id: z.ZodString;
|
|
211
|
+
label: z.ZodString;
|
|
212
|
+
wraps: z.ZodArray<z.ZodString>;
|
|
213
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
214
|
+
accent: "accent";
|
|
215
|
+
blue: "blue";
|
|
216
|
+
green: "green";
|
|
217
|
+
ink: "ink";
|
|
218
|
+
muted: "muted";
|
|
219
|
+
orange: "orange";
|
|
220
|
+
pink: "pink";
|
|
221
|
+
purple: "purple";
|
|
222
|
+
red: "red";
|
|
223
|
+
yellow: "yellow";
|
|
224
|
+
}>>;
|
|
225
|
+
}, z.core.$strip>>;
|
|
226
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
227
|
+
from: z.ZodString;
|
|
228
|
+
to: z.ZodString;
|
|
229
|
+
label: z.ZodOptional<z.ZodString>;
|
|
230
|
+
style: z.ZodOptional<z.ZodEnum<{
|
|
231
|
+
dashed: "dashed";
|
|
232
|
+
dotted: "dotted";
|
|
233
|
+
solid: "solid";
|
|
234
|
+
}>>;
|
|
235
|
+
tone: z.ZodOptional<z.ZodEnum<{
|
|
236
|
+
accent: "accent";
|
|
237
|
+
blue: "blue";
|
|
238
|
+
green: "green";
|
|
239
|
+
ink: "ink";
|
|
240
|
+
muted: "muted";
|
|
241
|
+
orange: "orange";
|
|
242
|
+
pink: "pink";
|
|
243
|
+
purple: "purple";
|
|
244
|
+
red: "red";
|
|
245
|
+
yellow: "yellow";
|
|
246
|
+
}>>;
|
|
247
|
+
}, z.core.$strip>>;
|
|
248
|
+
version: z.ZodLiteral<1>;
|
|
249
|
+
rev: z.ZodNumber;
|
|
250
|
+
}, z.core.$strip>;
|
|
251
|
+
export type DiagramDocument = z.infer<typeof DiagramDocumentSchema>;
|
|
252
|
+
export declare const EMPTY_DIAGRAM: DiagramDocument;
|
|
253
|
+
export declare function migrateDiagram(value: unknown): DiagramDocument | null;
|
|
254
|
+
export declare function diagramProblemIn(content: Pick<DiagramContent, 'nodes' | 'groups' | 'edges'>): string | null;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { DrawingColorSchema, DrawingStrokeStyleSchema } from '@adecore/drawing/protocol';
|
|
3
|
+
/*
|
|
4
|
+
* A diagram is a directed graph with groups, written rather than drawn: the file says what is
|
|
5
|
+
* connected to what and the layout is computed from that. Colors are the drawing palette's names,
|
|
6
|
+
* so a diagram follows the theme the way a drawing does.
|
|
7
|
+
*/
|
|
8
|
+
export const DiagramShapeSchema = z.enum(['rect', 'round', 'pill', 'diamond', 'cylinder']);
|
|
9
|
+
export const DIAGRAM_SHAPES = DiagramShapeSchema.options;
|
|
10
|
+
export const DiagramDirectionSchema = z.enum(['right', 'down']);
|
|
11
|
+
export const DiagramEdgeStyleSchema = DrawingStrokeStyleSchema;
|
|
12
|
+
export const DiagramNodeSchema = z.object({
|
|
13
|
+
id: z.string().min(1),
|
|
14
|
+
label: z.string(),
|
|
15
|
+
sub: z.string().optional(),
|
|
16
|
+
// Absent reads as 'rect'.
|
|
17
|
+
shape: DiagramShapeSchema.optional(),
|
|
18
|
+
tone: DrawingColorSchema.optional(),
|
|
19
|
+
// Only on a node a person dragged; every other node goes where the layout puts it.
|
|
20
|
+
pos: z.tuple([z.number(), z.number()]).optional()
|
|
21
|
+
});
|
|
22
|
+
export const DiagramGroupSchema = z.object({
|
|
23
|
+
id: z.string().min(1),
|
|
24
|
+
label: z.string(),
|
|
25
|
+
// Node ids, never group ids: a group is a box around nodes and does not nest.
|
|
26
|
+
wraps: z.array(z.string().min(1)),
|
|
27
|
+
tone: DrawingColorSchema.optional()
|
|
28
|
+
});
|
|
29
|
+
export const DiagramEdgeSchema = z.object({
|
|
30
|
+
from: z.string().min(1),
|
|
31
|
+
to: z.string().min(1),
|
|
32
|
+
label: z.string().optional(),
|
|
33
|
+
// Absent reads as 'solid'.
|
|
34
|
+
style: DiagramEdgeStyleSchema.optional(),
|
|
35
|
+
tone: DrawingColorSchema.optional()
|
|
36
|
+
});
|
|
37
|
+
export const DiagramMetaSchema = z.object({
|
|
38
|
+
title: z.string(),
|
|
39
|
+
direction: DiagramDirectionSchema
|
|
40
|
+
});
|
|
41
|
+
export const DIAGRAM_VERSION = 1;
|
|
42
|
+
// Edges spanning many layers make layout costs grow quickly, so validation caps graph size.
|
|
43
|
+
export const DIAGRAM_LIMITS = {
|
|
44
|
+
nodes: 100,
|
|
45
|
+
edges: 120
|
|
46
|
+
};
|
|
47
|
+
export const DiagramContentSchema = z.object({
|
|
48
|
+
meta: DiagramMetaSchema,
|
|
49
|
+
// In file order, which is also the order inside a layer of the layout.
|
|
50
|
+
nodes: z.array(DiagramNodeSchema),
|
|
51
|
+
groups: z.array(DiagramGroupSchema),
|
|
52
|
+
edges: z.array(DiagramEdgeSchema)
|
|
53
|
+
});
|
|
54
|
+
export const DiagramDocumentSchema = DiagramContentSchema.extend({
|
|
55
|
+
version: z.literal(DIAGRAM_VERSION),
|
|
56
|
+
// Goes up by one on every write; a save that names an older rev is a conflict.
|
|
57
|
+
rev: z.number().int().nonnegative()
|
|
58
|
+
});
|
|
59
|
+
export const EMPTY_DIAGRAM = { version: 1, rev: 0, meta: { title: '', direction: 'right' }, nodes: [], groups: [], edges: [] };
|
|
60
|
+
/* One version so far, so reading is a parse. A file that is not a diagram at all reads as null. */
|
|
61
|
+
export function migrateDiagram(value) {
|
|
62
|
+
const parsed = DiagramDocumentSchema.safeParse(value);
|
|
63
|
+
return parsed.success ? parsed.data : null;
|
|
64
|
+
}
|
|
65
|
+
/*
|
|
66
|
+
* The first rule of the graph the content breaks, as a sentence that names the id, or null. The
|
|
67
|
+
* schema cannot say this, and an agent that wrote the file needs the id to repair it: a silent drop
|
|
68
|
+
* would leave it believing the edge is there.
|
|
69
|
+
*/
|
|
70
|
+
export function diagramProblemIn(content) {
|
|
71
|
+
if (content.nodes.length > DIAGRAM_LIMITS.nodes) {
|
|
72
|
+
return `A diagram holds at most ${DIAGRAM_LIMITS.nodes} nodes, and this one has ${content.nodes.length}`;
|
|
73
|
+
}
|
|
74
|
+
if (content.edges.length > DIAGRAM_LIMITS.edges) {
|
|
75
|
+
return `A diagram holds at most ${DIAGRAM_LIMITS.edges} edges, and this one has ${content.edges.length}`;
|
|
76
|
+
}
|
|
77
|
+
// Nodes and groups share one namespace, so an id always says which box it means.
|
|
78
|
+
const nodes = new Set();
|
|
79
|
+
for (const node of content.nodes) {
|
|
80
|
+
if (nodes.has(node.id)) {
|
|
81
|
+
return `Two nodes share the id "${node.id}"`;
|
|
82
|
+
}
|
|
83
|
+
nodes.add(node.id);
|
|
84
|
+
}
|
|
85
|
+
const groups = new Set();
|
|
86
|
+
const groupOf = new Map();
|
|
87
|
+
for (const group of content.groups) {
|
|
88
|
+
if (nodes.has(group.id) || groups.has(group.id)) {
|
|
89
|
+
return `The group id "${group.id}" is already taken`;
|
|
90
|
+
}
|
|
91
|
+
groups.add(group.id);
|
|
92
|
+
for (const id of group.wraps) {
|
|
93
|
+
if (!nodes.has(id)) {
|
|
94
|
+
return `The group "${group.id}" wraps "${id}", which is not a node`;
|
|
95
|
+
}
|
|
96
|
+
const other = groupOf.get(id);
|
|
97
|
+
if (other !== undefined) {
|
|
98
|
+
return `The node "${id}" is in two groups, "${other}" and "${group.id}"`;
|
|
99
|
+
}
|
|
100
|
+
groupOf.set(id, group.id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const edge of content.edges) {
|
|
104
|
+
for (const end of [edge.from, edge.to]) {
|
|
105
|
+
if (!nodes.has(end)) {
|
|
106
|
+
return `The edge from "${edge.from}" to "${edge.to}" names "${end}", which is not a node`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { layersOf } from './layout.js';
|
|
2
|
+
function nameOf(node, id) {
|
|
3
|
+
return node && node.label.trim() !== '' ? node.label : id;
|
|
4
|
+
}
|
|
5
|
+
/*
|
|
6
|
+
* A diagram as lines an agent can read: every node layer by layer (file order inside a layer), then
|
|
7
|
+
* every edge as the two names it joins, then what each group holds. Unlike a drawing nothing has to
|
|
8
|
+
* be guessed, since the file already says what points at what.
|
|
9
|
+
*/
|
|
10
|
+
export function readingOrder(document) {
|
|
11
|
+
const byId = new Map(document.nodes.map((node) => [node.id, node]));
|
|
12
|
+
const layers = layersOf(document.nodes, document.edges);
|
|
13
|
+
const nodes = document.nodes
|
|
14
|
+
.map((node, index) => ({ node, index, layer: layers.get(node.id) ?? 0 }))
|
|
15
|
+
.sort((left, right) => left.layer - right.layer || left.index - right.index)
|
|
16
|
+
.map(({ node }) => (node.sub ? `${nameOf(node, node.id)} (${node.sub})` : nameOf(node, node.id)));
|
|
17
|
+
const edges = document.edges.map((edge) => {
|
|
18
|
+
const line = `${nameOf(byId.get(edge.from), edge.from)} -> ${nameOf(byId.get(edge.to), edge.to)}`;
|
|
19
|
+
return edge.label ? `${line}: ${edge.label}` : line;
|
|
20
|
+
});
|
|
21
|
+
const groups = document.groups.map((group) => `${group.label || group.id} wraps: ${group.wraps.map((id) => nameOf(byId.get(id), id)).join(', ')}`);
|
|
22
|
+
return [...nodes, ...edges, ...groups];
|
|
23
|
+
}
|
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Point, Rect } from './layout.ts';
|
|
2
|
+
export declare const TRACK_GAP = 12;
|
|
3
|
+
export declare const MIN_CHANNEL = 64;
|
|
4
|
+
export interface ChannelHop {
|
|
5
|
+
hop: number;
|
|
6
|
+
ya: number;
|
|
7
|
+
yb: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ChannelLabel {
|
|
10
|
+
edge: number;
|
|
11
|
+
hop: number;
|
|
12
|
+
y: number;
|
|
13
|
+
along: number;
|
|
14
|
+
across: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ChannelInput {
|
|
17
|
+
start: number;
|
|
18
|
+
clearBefore: number;
|
|
19
|
+
clearAfter: number;
|
|
20
|
+
hops: ChannelHop[];
|
|
21
|
+
labels: ChannelLabel[];
|
|
22
|
+
obstacles: {
|
|
23
|
+
y: number;
|
|
24
|
+
h: number;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
export interface ChannelResult {
|
|
28
|
+
width: number;
|
|
29
|
+
trackX: Map<number, number>;
|
|
30
|
+
labels: Map<number, Rect>;
|
|
31
|
+
}
|
|
32
|
+
export declare function conflictsOf(left: readonly Point[], right: readonly Point[]): number;
|
|
33
|
+
export declare function routeChannel(input: ChannelInput): ChannelResult;
|