@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/src/across.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { BAND_MARGIN, bandOfCode, type Layering } from './graph.ts';
|
|
2
|
+
|
|
3
|
+
/* Rounds that pull every unit towards the units it is joined to, alternating the direction of the sweep. */
|
|
4
|
+
const ROUNDS = 8;
|
|
5
|
+
|
|
6
|
+
function clamp(value: number, low: number, high: number): number {
|
|
7
|
+
return Math.min(high, Math.max(low, value));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Where every unit sits across the flow, as the top of its box, with the order of `orderLayers` kept.
|
|
12
|
+
* A group's block has one top in every layer it spans and its members sit inside it. It starts from
|
|
13
|
+
* the middle of the tightest packing towards either end, which is always a valid place, and then
|
|
14
|
+
* moves every unit towards the median of its neighbors as far as its own neighbors in the layer let
|
|
15
|
+
* it, so a chain lines up straight. Every step is clamped to whole numbers.
|
|
16
|
+
*/
|
|
17
|
+
export function placeAcross(layering: Layering): number[] {
|
|
18
|
+
const { units, bands, lines, items } = layering;
|
|
19
|
+
const unitCount = units.length;
|
|
20
|
+
const variableCount = unitCount + bands.length;
|
|
21
|
+
// A free unit is its own variable; a band is one variable for all its layers, its members sit at an offset from it.
|
|
22
|
+
const top = new Array<number>(variableCount).fill(0);
|
|
23
|
+
const local = new Array<number>(unitCount).fill(0);
|
|
24
|
+
|
|
25
|
+
const variableOf = (code: number): number => (code >= 0 ? code : unitCount + bandOfCode(code));
|
|
26
|
+
const codeOf = (variable: number): number => (variable < unitCount ? variable : -(variable - unitCount + 1));
|
|
27
|
+
const sizeOf = (code: number): number => (code >= 0 ? units[code]!.h : bands[bandOfCode(code)]!.height);
|
|
28
|
+
const marginOf = (code: number): number => (code >= 0 ? units[code]!.margin : BAND_MARGIN);
|
|
29
|
+
const topOf = (code: number): number => top[variableOf(code)]!;
|
|
30
|
+
const absoluteTop = (unit: number): number => {
|
|
31
|
+
const band = units[unit]!.band;
|
|
32
|
+
return band >= 0 ? top[unitCount + band]! + local[unit]! : top[unit]!;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const successors: number[][] = Array.from({ length: variableCount }, () => []);
|
|
36
|
+
const incoming = new Array<number>(variableCount).fill(0);
|
|
37
|
+
const present = new Array<boolean>(variableCount).fill(false);
|
|
38
|
+
for (const row of items) {
|
|
39
|
+
row.forEach((code, index) => {
|
|
40
|
+
present[variableOf(code)] = true;
|
|
41
|
+
if (index > 0) {
|
|
42
|
+
successors[variableOf(row[index - 1]!)]!.push(code);
|
|
43
|
+
incoming[variableOf(code)]! += 1;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The band order is the same in every layer, so these constraints never form a cycle.
|
|
49
|
+
const lowest = new Array<number>(variableCount).fill(0);
|
|
50
|
+
const queue: number[] = [];
|
|
51
|
+
for (let variable = 0; variable < variableCount; variable++) {
|
|
52
|
+
if (present[variable] && incoming[variable] === 0) {
|
|
53
|
+
queue.push(variable);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (let i = 0; i < queue.length; i++) {
|
|
57
|
+
const variable = queue[i]!;
|
|
58
|
+
const code = codeOf(variable);
|
|
59
|
+
for (const next of successors[variable]!) {
|
|
60
|
+
const target = variableOf(next);
|
|
61
|
+
lowest[target] = Math.max(lowest[target]!, lowest[variable]! + sizeOf(code) + marginOf(code) + marginOf(next));
|
|
62
|
+
incoming[target]! -= 1;
|
|
63
|
+
if (incoming[target] === 0) {
|
|
64
|
+
queue.push(target);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const tail = new Array<number>(variableCount).fill(0);
|
|
69
|
+
let total = 0;
|
|
70
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
71
|
+
const variable = queue[i]!;
|
|
72
|
+
const code = codeOf(variable);
|
|
73
|
+
let length = sizeOf(code);
|
|
74
|
+
for (const next of successors[variable]!) {
|
|
75
|
+
length = Math.max(length, sizeOf(code) + marginOf(code) + marginOf(next) + tail[variableOf(next)]!);
|
|
76
|
+
}
|
|
77
|
+
tail[variable] = length;
|
|
78
|
+
total = Math.max(total, lowest[variable]! + length);
|
|
79
|
+
}
|
|
80
|
+
for (const variable of queue) {
|
|
81
|
+
top[variable] = Math.floor((lowest[variable]! + total - tail[variable]!) / 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const band of bands) {
|
|
85
|
+
for (const slot of band.members) {
|
|
86
|
+
const low: number[] = [];
|
|
87
|
+
let at = band.before;
|
|
88
|
+
slot.forEach((unit, index) => {
|
|
89
|
+
if (index > 0) {
|
|
90
|
+
const previous = units[slot[index - 1]!]!;
|
|
91
|
+
at += previous.h + previous.margin + units[unit]!.margin;
|
|
92
|
+
}
|
|
93
|
+
low.push(at);
|
|
94
|
+
});
|
|
95
|
+
let high = band.height - band.after;
|
|
96
|
+
for (let index = slot.length - 1; index >= 0; index--) {
|
|
97
|
+
const unit = units[slot[index]!]!;
|
|
98
|
+
if (index < slot.length - 1) {
|
|
99
|
+
high -= unit.margin + units[slot[index + 1]!]!.margin;
|
|
100
|
+
}
|
|
101
|
+
high -= unit.h;
|
|
102
|
+
local[slot[index]!] = Math.floor((low[index]! + high) / 2);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const medianOf = (values: number[]): number => {
|
|
108
|
+
values.sort((left, right) => left - right);
|
|
109
|
+
const middle = values.length >> 1;
|
|
110
|
+
return values.length % 2 === 1 ? values[middle]! : (values[middle - 1]! + values[middle]!) / 2;
|
|
111
|
+
};
|
|
112
|
+
const desiredTop = (unit: number): number | null => {
|
|
113
|
+
const self = units[unit]!;
|
|
114
|
+
if (self.line >= 0) {
|
|
115
|
+
// The points of one long edge pull towards each other rather than their neighbors, so the edge runs straight
|
|
116
|
+
// instead of stepping a little in every layer it passes.
|
|
117
|
+
const line = lines[self.line]!;
|
|
118
|
+
const ends = [units[line[0]!]!.up[0]!, units[line.at(-1)!]!.down[0]!].map((end) => absoluteTop(end) + units[end]!.h / 2);
|
|
119
|
+
if (line.length === 1) {
|
|
120
|
+
const here = absoluteTop(unit);
|
|
121
|
+
return Math.round(Math.abs(ends[1]! - here) < Math.abs(ends[0]! - here) ? ends[1]! : ends[0]!);
|
|
122
|
+
}
|
|
123
|
+
return Math.round(medianOf([...line.filter((other) => other !== unit).map(absoluteTop), ...ends]));
|
|
124
|
+
}
|
|
125
|
+
const neighbors = [...self.up, ...self.down];
|
|
126
|
+
if (neighbors.length === 0) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return Math.round(medianOf(neighbors.map((neighbor) => absoluteTop(neighbor) + units[neighbor]!.h / 2)) - self.h / 2);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
133
|
+
for (let step = 0; step < items.length; step++) {
|
|
134
|
+
const layer = round % 2 === 0 ? step : items.length - 1 - step;
|
|
135
|
+
const row = items[layer]!;
|
|
136
|
+
row.forEach((code, index) => {
|
|
137
|
+
if (code >= 0) {
|
|
138
|
+
const want = desiredTop(code);
|
|
139
|
+
if (want === null) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const previous = row[index - 1];
|
|
143
|
+
const next = row[index + 1];
|
|
144
|
+
const low = previous === undefined ? -Infinity : topOf(previous) + sizeOf(previous) + marginOf(previous) + marginOf(code);
|
|
145
|
+
const high = next === undefined ? Infinity : topOf(next) - marginOf(next) - marginOf(code) - sizeOf(code);
|
|
146
|
+
top[code] = clamp(want, low, high);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const bandIndex = bandOfCode(code);
|
|
150
|
+
const band = bands[bandIndex]!;
|
|
151
|
+
const slot = band.members[layer - band.first]!;
|
|
152
|
+
slot.forEach((unit, position) => {
|
|
153
|
+
const want = desiredTop(unit);
|
|
154
|
+
if (want === null) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const self = units[unit]!;
|
|
158
|
+
const previous = slot[position - 1];
|
|
159
|
+
const next = slot[position + 1];
|
|
160
|
+
const low = previous === undefined ? band.before : local[previous]! + units[previous]!.h + units[previous]!.margin + self.margin;
|
|
161
|
+
const high = next === undefined ? band.height - band.after - self.h : local[next]! - units[next]!.margin - self.margin - self.h;
|
|
162
|
+
local[unit] = clamp(want - top[unitCount + bandIndex]!, low, high);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
bands.forEach((band, bandIndex) => {
|
|
168
|
+
const shifts: number[] = [];
|
|
169
|
+
for (const unit of band.members.flat()) {
|
|
170
|
+
const want = desiredTop(unit);
|
|
171
|
+
if (want !== null) {
|
|
172
|
+
shifts.push(want - absoluteTop(unit));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (shifts.length === 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const code = -(bandIndex + 1);
|
|
179
|
+
let low = -Infinity;
|
|
180
|
+
let high = Infinity;
|
|
181
|
+
for (let layer = band.first; layer <= band.last; layer++) {
|
|
182
|
+
const row = items[layer]!;
|
|
183
|
+
const index = row.indexOf(code);
|
|
184
|
+
const previous = row[index - 1];
|
|
185
|
+
const next = row[index + 1];
|
|
186
|
+
if (previous !== undefined) {
|
|
187
|
+
low = Math.max(low, topOf(previous) + sizeOf(previous) + marginOf(previous) + BAND_MARGIN);
|
|
188
|
+
}
|
|
189
|
+
if (next !== undefined) {
|
|
190
|
+
high = Math.min(high, topOf(next) - marginOf(next) - BAND_MARGIN - band.height);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const shift = Math.round(shifts.reduce((sum, value) => sum + value, 0) / shifts.length);
|
|
194
|
+
top[unitCount + bandIndex] = clamp(top[unitCount + bandIndex]! + shift, low, high);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let least = Infinity;
|
|
199
|
+
for (const row of items) {
|
|
200
|
+
for (const code of row) {
|
|
201
|
+
least = Math.min(least, topOf(code));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (Number.isFinite(least)) {
|
|
205
|
+
for (let variable = 0; variable < variableCount; variable++) {
|
|
206
|
+
top[variable] = top[variable]! - least || 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
bands.forEach((band, index) => {
|
|
210
|
+
band.top = top[unitCount + index]!;
|
|
211
|
+
});
|
|
212
|
+
return units.map((_, unit) => absoluteTop(unit));
|
|
213
|
+
}
|
package/src/graph.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The graph the layout works on once every edge that skips a layer has a point per layer it passes
|
|
3
|
+
* through. Internal to the package: the coordinates it ends in are what `layoutOf` hands out.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* A box or a point a long edge passes through, in the frame of a diagram that runs to the right. */
|
|
7
|
+
export interface Unit {
|
|
8
|
+
layer: number;
|
|
9
|
+
/* The index of the node in the file, or -1 for a point a long edge passes through. */
|
|
10
|
+
node: number;
|
|
11
|
+
/* The band of the group that holds it, or -1. */
|
|
12
|
+
band: number;
|
|
13
|
+
w: number;
|
|
14
|
+
h: number;
|
|
15
|
+
/* Room kept free across the flow on either side, so a neighbor stays this far off. */
|
|
16
|
+
margin: number;
|
|
17
|
+
/* For a point a long edge passes through, the index of that edge's line among `Layering.lines`; -1 for a box. */
|
|
18
|
+
line: number;
|
|
19
|
+
/* Units in the layer before and after that a hop joins this one to. */
|
|
20
|
+
up: number[];
|
|
21
|
+
down: number[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* One stretch of an edge, from a unit in one layer to a unit in the next. */
|
|
25
|
+
export interface Hop {
|
|
26
|
+
edge: number;
|
|
27
|
+
from: number;
|
|
28
|
+
to: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* A group seen across the flow: one block per layer it spans, at the same place in every one of them. */
|
|
32
|
+
export interface Band {
|
|
33
|
+
/* The index of the group in the file. */
|
|
34
|
+
group: number;
|
|
35
|
+
first: number;
|
|
36
|
+
last: number;
|
|
37
|
+
/* The units inside, per layer from `first`, in order. */
|
|
38
|
+
members: number[][];
|
|
39
|
+
top: number;
|
|
40
|
+
height: number;
|
|
41
|
+
/* Room inside the border before the first member and after the last, across the flow. */
|
|
42
|
+
before: number;
|
|
43
|
+
after: number;
|
|
44
|
+
/* Where the band stands among the others; the same in every layer, so no two bands swap places. */
|
|
45
|
+
rank: number;
|
|
46
|
+
labelWidth: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Layering {
|
|
50
|
+
units: Unit[];
|
|
51
|
+
hops: Hop[];
|
|
52
|
+
bands: Band[];
|
|
53
|
+
/* The points of every long edge, first to last. */
|
|
54
|
+
lines: number[][];
|
|
55
|
+
/* Per layer the top-level entries in order: a unit, or `bandCode(band)` for the block of a group. */
|
|
56
|
+
items: number[][];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function bandCode(band: number): number {
|
|
60
|
+
return -(band + 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function bandOfCode(code: number): number {
|
|
64
|
+
return -code - 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const NODE_MARGIN = 16;
|
|
68
|
+
export const DUMMY_MARGIN = 8;
|
|
69
|
+
/* A node beside a group is this plus its own margin away from the border. */
|
|
70
|
+
export const BAND_MARGIN = 16;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type NodeBox,
|
|
3
|
+
type GroupBox,
|
|
4
|
+
type EdgeLabelBox,
|
|
5
|
+
type EdgeRoute,
|
|
6
|
+
type DiagramLayout,
|
|
7
|
+
NODE_MAX_WIDTH,
|
|
8
|
+
CYLINDER_LID,
|
|
9
|
+
EDGE_LABEL_MAX_WIDTH,
|
|
10
|
+
EDGE_LABEL_PADDING,
|
|
11
|
+
GROUP_PADDING,
|
|
12
|
+
GROUP_LABEL_BAND,
|
|
13
|
+
GROUP_LABEL_INSET,
|
|
14
|
+
type NodeSize,
|
|
15
|
+
sizeOfNode,
|
|
16
|
+
sizeOfEdgeLabel,
|
|
17
|
+
layersOf,
|
|
18
|
+
layoutOf,
|
|
19
|
+
LABEL_LINE,
|
|
20
|
+
LABEL_SIZE,
|
|
21
|
+
SUB_LINE,
|
|
22
|
+
SUB_SIZE,
|
|
23
|
+
estimateTextWidth,
|
|
24
|
+
wrapText,
|
|
25
|
+
type Point,
|
|
26
|
+
type Rect
|
|
27
|
+
} from './layout.ts';
|
|
28
|
+
export { readingOrder } from './reading-order.ts';
|
|
29
|
+
export { type ShapePaths, shapePaths, type TextLine, textLinesOf, edgeLabelLinesOf, edgePath, arrowHeadPath, dashOf } from './shapes.ts';
|
|
30
|
+
export { type DiagramSvgOptions, DEFAULT_DIAGRAM_MARGIN, DEFAULT_NODE_TONE, DEFAULT_GROUP_TONE, DEFAULT_EDGE_TONE, toSvg } from './svg.ts';
|