dsh-model-everything 0.5.0 → 0.5.2

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 (65) hide show
  1. package/README.en-US.md +12 -0
  2. package/README.md +4 -0
  3. package/lib/client.js +2829 -224
  4. package/lib/render/channels.js +243 -0
  5. package/lib/render/collapse.js +12 -20
  6. package/lib/render/components.js +99 -0
  7. package/lib/render/coords.js +288 -0
  8. package/lib/render/cycle-break.js +147 -0
  9. package/lib/render/dummy.js +43 -0
  10. package/lib/render/from-mermaid.js +34 -10
  11. package/lib/render/from-plantuml.js +318 -46
  12. package/lib/render/layer.js +91 -0
  13. package/lib/render/layout.js +27 -11
  14. package/lib/render/network-simplex.js +272 -0
  15. package/lib/render/order.js +122 -0
  16. package/lib/render/paint.js +1227 -69
  17. package/lib/render/place.js +393 -121
  18. package/lib/render/route.js +225 -0
  19. package/lib/render/svg.js +146 -21
  20. package/lib/render/symbol.js +108 -0
  21. package/lib/render/text.js +74 -0
  22. package/lib/render/trunk.js +213 -0
  23. package/lib/render/wrap.js +87 -0
  24. package/lib/types/render/channels.d.ts +63 -0
  25. package/lib/types/render/components.d.ts +57 -0
  26. package/lib/types/render/coords.d.ts +64 -0
  27. package/lib/types/render/cycle-break.d.ts +45 -0
  28. package/lib/types/render/dummy.d.ts +32 -0
  29. package/lib/types/render/from-plantuml.d.ts +12 -6
  30. package/lib/types/render/layer.d.ts +28 -0
  31. package/lib/types/render/layout.d.ts +43 -11
  32. package/lib/types/render/model.d.ts +17 -0
  33. package/lib/types/render/network-simplex.d.ts +54 -0
  34. package/lib/types/render/order.d.ts +27 -0
  35. package/lib/types/render/paint.d.ts +157 -10
  36. package/lib/types/render/place.d.ts +40 -19
  37. package/lib/types/render/route.d.ts +77 -0
  38. package/lib/types/render/svg.d.ts +1 -1
  39. package/lib/types/render/symbol.d.ts +73 -0
  40. package/lib/types/render/text.d.ts +38 -0
  41. package/lib/types/render/trunk.d.ts +104 -0
  42. package/lib/types/render/wrap.d.ts +27 -0
  43. package/package.json +148 -148
  44. package/skills/model-everything-diagram/SKILL.md +85 -82
  45. package/src/render/channels.ts +309 -0
  46. package/src/render/collapse.ts +150 -155
  47. package/src/render/components.ts +126 -0
  48. package/src/render/coords.ts +328 -0
  49. package/src/render/cycle-break.ts +164 -0
  50. package/src/render/dummy.ts +65 -0
  51. package/src/render/from-mermaid.ts +261 -236
  52. package/src/render/from-plantuml.ts +469 -178
  53. package/src/render/layer.ts +98 -0
  54. package/src/render/layout.ts +101 -62
  55. package/src/render/model.ts +86 -67
  56. package/src/render/network-simplex.ts +290 -0
  57. package/src/render/order.ts +139 -0
  58. package/src/render/paint.ts +1570 -290
  59. package/src/render/place.ts +532 -234
  60. package/src/render/route.ts +269 -0
  61. package/src/render/svg.ts +274 -127
  62. package/src/render/symbol.ts +137 -0
  63. package/src/render/text.ts +86 -0
  64. package/src/render/trunk.ts +275 -0
  65. package/src/render/wrap.ts +103 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Minimum-length ranking: the network simplex of Gansner, Koutsofios, North and Vo (1993).
3
+ *
4
+ * The ranking problem: give every node a layer such that each edge spans at least its minimum length, and make the
5
+ * total of all the slack - the layers every edge spans beyond its minimum - as small as it can be. Slack is what a
6
+ * reader pays for: an edge that spans three layers has to be drawn through two of them.
7
+ *
8
+ * Longest paths are one feasible answer and they are not the best one. They put every node as high as it can
9
+ * possibly go, which leaves a node whose edges leave it with nothing to do above the boxes it points at. The best
10
+ * answer is the solution of a linear program, and network simplex is how the mature layering implementations solve
11
+ * it: hold a *feasible tree* - a spanning tree every edge of which is tight - and a ranking that tightens it, attach
12
+ * a *cut value* to each tree edge (the weight flowing one way across that cut minus the weight flowing the other),
13
+ * and while some tree edge has a negative cut value, take it out, let the cheapest edge across its cut in, and move
14
+ * the ranks so the new edge is tight. The cut value is the rate at which the total length falls as the tree edge is
15
+ * left out, so every exchange shortens the drawing, and a tree with no negative cut value is the optimum: no single
16
+ * exchange is left that would shorten it, and for this program that is the definition of optimal.
17
+ *
18
+ * Three things are done differently from the paper, each because this graph is not the paper's graph:
19
+ *
20
+ * - **Components are solved one at a time.** The ranking is translation invariant, so two components that share no
21
+ * edge can be laid out apart from each other; each is solved on its own and its top row is then numbered zero.
22
+ * - **A node may be pinned to the top.** A state machine has an initial state and a reader expects it first, so the
23
+ * stage can name one node that must be on the topmost layer: a zero-length, zero-weight constraint edge runs from
24
+ * it to every other node of its component (`rank(v) >= rank(pinned)`), and the ranking is minimised subject to that.
25
+ * The constraint costs length when the free optimum would have put the pinned node below another source, and that
26
+ * price is what "the initial state is first" means.
27
+ * - **The iteration is capped.** A degenerate exchange (one that moves nothing) is legal and in theory can go round
28
+ * in circles. The cap turns that into a ranking that is feasible and no worse than where it started.
29
+ *
30
+ * Pure, and generic in the ranking program: no ids, no sizes, no drawing.
31
+ */
32
+
33
+ /** One edge of the ranking program: two layers, how far apart they may be, and what one layer of slack costs. */
34
+ export interface RankEdge {
35
+ from: number
36
+ to: number
37
+ /** The least number of layers this edge may span. One for an edge that is drawn, zero for a constraint. */
38
+ minLength: number
39
+ /** What one extra layer on this edge costs. Zero for a constraint, which must hold but is not worth shortening. */
40
+ weight: number
41
+ }
42
+
43
+ export interface RankProblem {
44
+ count: number
45
+ edges: RankEdge[]
46
+ /** A feasible ranking to start from: it already gives every edge at least its minimum length. */
47
+ start: number[]
48
+ /**
49
+ * The node that has to sit on the topmost layer, when the source named one.
50
+ *
51
+ * It has to be a node nothing points at, or no ranking can hold it at the top - a predecessor forces it down. The
52
+ * constraint is dropped when the start ranking cannot satisfy it.
53
+ */
54
+ pinned?: number
55
+ }
56
+
57
+ /** How many layers this edge spans beyond its minimum: at least zero in a feasible ranking. */
58
+ function slackOf(edge: RankEdge, rank: number[]): number {
59
+ return rank[edge.to] - rank[edge.from] - edge.minLength
60
+ }
61
+
62
+ /** The nodes of each weakly connected component, in index order, each list in index order. */
63
+ function componentsOf(count: number, edges: RankEdge[]): number[][] {
64
+ const neighbours: number[][] = []
65
+ for (let node = 0; node < count; node += 1) neighbours.push([])
66
+ for (const edge of edges) {
67
+ neighbours[edge.from].push(edge.to)
68
+ neighbours[edge.to].push(edge.from)
69
+ }
70
+ const seen = new Array<boolean>(count).fill(false)
71
+ const components: number[][] = []
72
+ for (let node = 0; node < count; node += 1) {
73
+ if (seen[node]) continue
74
+ seen[node] = true
75
+ const members = [node]
76
+ for (let head = 0; head < members.length; head += 1) {
77
+ for (const next of neighbours[members[head]]) {
78
+ if (seen[next]) continue
79
+ seen[next] = true
80
+ members.push(next)
81
+ }
82
+ }
83
+ members.sort((left, right) => left - right)
84
+ components.push(members)
85
+ }
86
+ return components
87
+ }
88
+
89
+ /**
90
+ * A spanning tree of tight edges, built one edge at a time.
91
+ *
92
+ * A tight edge is one of the tree's to grow on: it already spans exactly its minimum, so it constrains the ranking
93
+ * without costing anything. When no tight edge reaches out of the tree, no tree can be grown from here without
94
+ * moving something: the nodes outside the tree are shifted as one block until a crossing edge is tight. The block
95
+ * moves by the largest amount that keeps every crossing edge feasible, which is the smaller of the two bounds the
96
+ * crossing edges impose - one per direction - so one edge becomes tight and none goes negative.
97
+ */
98
+ function buildTightTree(members: number[], edges: RankEdge[], rank: number[], tree: Set<number>): void {
99
+ const inside = new Set<number>([members[0]])
100
+ while (inside.size < members.length) {
101
+ let tight = -1
102
+ for (const [index, edge] of edges.entries()) {
103
+ if (tree.has(index)) continue
104
+ if (inside.has(edge.from) === inside.has(edge.to)) continue
105
+ if (slackOf(edge, rank) === 0) {
106
+ tight = index
107
+ break
108
+ }
109
+ }
110
+ if (tight >= 0) {
111
+ tree.add(tight)
112
+ inside.add(edges[tight].from)
113
+ inside.add(edges[tight].to)
114
+ continue
115
+ }
116
+ // Nothing tight leaves the tree: slide everything outside it until something does. An edge leaving the tree
117
+ // gains a layer of slack per unit of the slide and an edge entering it loses one, so the feasible slides are the
118
+ // interval between the two bounds, and the bound that is not zero is where an edge becomes tight.
119
+ let lowest = -Infinity
120
+ let highest = Infinity
121
+ for (const edge of edges) {
122
+ const from = inside.has(edge.from)
123
+ const to = inside.has(edge.to)
124
+ if (from === to) continue
125
+ const slack = slackOf(edge, rank)
126
+ if (from) lowest = Math.max(lowest, -slack)
127
+ else highest = Math.min(highest, slack)
128
+ }
129
+ // A connected component has at least one edge across the tree's cut, so one of the two bounds is finite.
130
+ const step = lowest > -Infinity ? lowest : highest
131
+ for (const node of members) if (!inside.has(node)) rank[node] += step
132
+ }
133
+ }
134
+
135
+ /** The tree's parents and subtree extents, read off one depth-first walk. */
136
+ interface TreeWalk {
137
+ /** For each node, its parent, or -1 for the root. */
138
+ parent: Map<number, number>
139
+ /** The preorder position of each node, and how many nodes its subtree holds. */
140
+ first: Map<number, number>
141
+ size: Map<number, number>
142
+ order: number[]
143
+ }
144
+
145
+ function walkTree(members: number[], edges: RankEdge[], tree: Set<number>): TreeWalk {
146
+ const adjacency = new Map<number, Array<{ node: number }>>()
147
+ const link = (node: number, other: number): void => {
148
+ adjacency.set(node, [...(adjacency.get(node) ?? []), { node: other }])
149
+ }
150
+ for (const index of tree) {
151
+ const edge = edges[index]
152
+ link(edge.from, edge.to)
153
+ link(edge.to, edge.from)
154
+ }
155
+ const parent = new Map<number, number>()
156
+ const first = new Map<number, number>()
157
+ const size = new Map<number, number>()
158
+ const order: number[] = []
159
+ const seen = new Set<number>()
160
+ const root = members[0]
161
+ const stack = [root]
162
+ seen.add(root)
163
+ parent.set(root, -1)
164
+ while (stack.length > 0) {
165
+ const node = stack.pop() as number
166
+ first.set(node, order.length)
167
+ order.push(node)
168
+ for (const step of adjacency.get(node) as Array<{ node: number }>) {
169
+ if (seen.has(step.node)) continue
170
+ seen.add(step.node)
171
+ parent.set(step.node, node)
172
+ stack.push(step.node)
173
+ }
174
+ }
175
+ for (let index = order.length - 1; index >= 0; index -= 1) {
176
+ const node = order[index]
177
+ size.set(node, (size.get(node) ?? 0) + 1)
178
+ const above = parent.get(node)
179
+ if (above !== undefined && above >= 0) size.set(above, (size.get(above) ?? 0) + (size.get(node) as number))
180
+ }
181
+ return { parent, first, size, order }
182
+ }
183
+
184
+ /**
185
+ * Solve one component in place: grow a tight tree, exchange until no cut value is negative, then number its top row
186
+ * zero.
187
+ */
188
+ function solveComponent(size: number, members: number[], edges: RankEdge[], rank: number[]): void {
189
+ if (members.length < 2) {
190
+ if (members.length === 1) rank[members[0]] = 0
191
+ return
192
+ }
193
+ const tree = new Set<number>()
194
+ buildTightTree(members, edges, rank, tree)
195
+ // A tree that does not span cannot be exchanged on; the start ranking is still feasible, so it is what is kept.
196
+ const spanning = tree.size === members.length - 1
197
+ if (spanning) improve(size, members, edges, tree, rank)
198
+
199
+ let top = Infinity
200
+ for (const node of members) top = Math.min(top, rank[node])
201
+ for (const node of members) rank[node] -= top
202
+ }
203
+
204
+ function improve(size: number, members: number[], edges: RankEdge[], tree: Set<number>, rank: number[]): void {
205
+ const incoming = new Float64Array(size)
206
+ const outgoing = new Float64Array(size)
207
+ for (const edge of edges) {
208
+ outgoing[edge.from] += edge.weight
209
+ incoming[edge.to] += edge.weight
210
+ }
211
+ const cap = Math.max(50, 8 * members.length + 4 * edges.length)
212
+ for (let iteration = 0; iteration < cap; iteration += 1) {
213
+ const walk = walkTree(members, edges, tree)
214
+ // A subtree's cut value is what enters it minus what leaves it, or the other way round: which of the two is the
215
+ // tail side of the tree edge decides the sign. Removing a tree edge splits the tree in two, and the side holding
216
+ // the edge's tail is the one the value is read from.
217
+ const entering = new Map<number, number>()
218
+ const leaving = new Map<number, number>()
219
+ for (let index = walk.order.length - 1; index >= 0; index -= 1) {
220
+ const node = walk.order[index]
221
+ entering.set(node, incoming[node] + (entering.get(node) ?? 0))
222
+ leaving.set(node, outgoing[node] + (leaving.get(node) ?? 0))
223
+ const above = walk.parent.get(node)
224
+ if (above === undefined || above < 0) continue
225
+ entering.set(above, (entering.get(above) ?? 0) + (entering.get(node) as number))
226
+ leaving.set(above, (leaving.get(above) ?? 0) + (leaving.get(node) as number))
227
+ }
228
+ const holds = (node: number, child: number): boolean => {
229
+ const start = walk.first.get(child) as number
230
+ const at = walk.first.get(node) as number
231
+ return at >= start && at < start + (walk.size.get(child) as number)
232
+ }
233
+
234
+ let worst = -1
235
+ let worstEdge = -1
236
+ let worstChild = -1
237
+ for (const index of tree) {
238
+ const edge = edges[index]
239
+ const child = walk.parent.get(edge.from) === edge.to ? edge.from : edge.to
240
+ const tailInside = holds(edge.from, child)
241
+ const value = tailInside
242
+ ? (leaving.get(child) as number) - (entering.get(child) as number)
243
+ : (entering.get(child) as number) - (leaving.get(child) as number)
244
+ if (value < 0 && (worstEdge < 0 || value < worst)) {
245
+ worst = value
246
+ worstEdge = index
247
+ worstChild = child
248
+ }
249
+ }
250
+ if (worstEdge < 0) return
251
+
252
+ // The head side of the leaving edge is moved down, which shortens every edge running from it to the tail side.
253
+ // The move is as far as the first of those edges allows, and that edge becomes the one that enters the tree.
254
+ const leavingEdge = edges[worstEdge]
255
+ const headInside = !holds(leavingEdge.from, worstChild)
256
+ const inHead = (node: number): boolean => (headInside ? holds(node, worstChild) : !holds(node, worstChild))
257
+ let enteringEdge = -1
258
+ let step = Infinity
259
+ for (const [index, edge] of edges.entries()) {
260
+ if (tree.has(index)) continue
261
+ if (!inHead(edge.from) || inHead(edge.to)) continue
262
+ const slack = slackOf(edge, rank)
263
+ if (slack < step || (slack === step && (enteringEdge < 0 || index < enteringEdge))) {
264
+ step = slack
265
+ enteringEdge = index
266
+ }
267
+ }
268
+ if (enteringEdge < 0 || step === Infinity) return
269
+ if (step !== 0) for (const node of members) if (inHead(node)) rank[node] += step
270
+ tree.delete(worstEdge)
271
+ tree.add(enteringEdge)
272
+ }
273
+ }
274
+
275
+ export function minimumLength(problem: RankProblem): number[] {
276
+ const rank = problem.start.slice()
277
+ for (const members of componentsOf(problem.count, problem.edges)) {
278
+ const inside = new Set(members)
279
+ const local = problem.edges.filter((edge) => inside.has(edge.from) && inside.has(edge.to))
280
+ const pinned = problem.pinned !== undefined && inside.has(problem.pinned) ? problem.pinned : undefined
281
+ // The pin is a set of zero-length constraints from the pinned node to the rest of its component. It is only
282
+ // feasible when the start ranking already holds the pinned node at the top of its component.
283
+ const pinnedRank = pinned === undefined ? undefined : rank[pinned]
284
+ if (pinned !== undefined && pinnedRank !== undefined && members.every((node) => rank[node] >= pinnedRank)) {
285
+ for (const node of members) if (node !== pinned) local.push({ from: pinned, to: node, minLength: 0, weight: 0 })
286
+ }
287
+ solveComponent(problem.count, members, local, rank)
288
+ }
289
+ return rank
290
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Ordering: the left-to-right order of each layer.
3
+ *
4
+ * The layer a box is in is fixed by now; the only freedom left is the order inside each layer, and that order decides
5
+ * how many edges cross. A crossing is counted on a pair of adjacent layers: two edges between the same two layers
6
+ * cross when their end points are in opposite order on the top and the bottom.
7
+ *
8
+ * The method is the one every layered layout uses. A median sweep takes each layer in turn and sorts it by the median
9
+ * position of its neighbours in the layer just visited - down the drawing, then up. A down sweep alone settles each
10
+ * layer against the one above it and can be undone by the next; alternating is what makes the passes converge.
11
+ *
12
+ * The median is a good guess, not a search. Greedy switch is the search: try swapping two neighbours in a layer, keep
13
+ * the swap if the total crossing count falls, and stop when a whole pass finds nothing. That is where a crossing the
14
+ * median cannot see gets removed.
15
+ *
16
+ * Pure: layers of ids in, layers of ids out, plus the crossing count that justified them.
17
+ */
18
+
19
+ import type { Chain } from './dummy.js'
20
+
21
+ export interface Ordered {
22
+ /** The same layers, each in its final order. */
23
+ layers: string[][]
24
+ /** The number of crossings between adjacent layers left by the ordering. */
25
+ crossings: number
26
+ }
27
+
28
+ /** Where each id sits, by layer and by position within it. */
29
+ function locate(layers: string[][]): Map<string, { layer: number; index: number }> {
30
+ const found = new Map<string, { layer: number; index: number }>()
31
+ layers.forEach((layer, depth) => layer.forEach((id, index) => found.set(id, { layer: depth, index })))
32
+ return found
33
+ }
34
+
35
+ /** The median position of an id's neighbours in the layer that was just visited; its own when it has none there. */
36
+ function medianOf(
37
+ id: string,
38
+ neighbours: Map<string, string[]>,
39
+ reference: number,
40
+ positions: Map<string, { layer: number; index: number }>,
41
+ ): number {
42
+ const found = (neighbours.get(id) ?? [])
43
+ .filter((other) => positions.get(other)?.layer === reference)
44
+ .map((other) => (positions.get(other) as { index: number }).index)
45
+ .sort((left, right) => left - right)
46
+ if (found.length === 0) return (positions.get(id) as { index: number }).index
47
+ const middle = Math.floor(found.length / 2)
48
+ return found.length % 2 === 1 ? found[middle] : (found[middle - 1] + found[middle]) / 2
49
+ }
50
+
51
+ /** How many pairs of edges between adjacent layers cross. */
52
+ export function countCrossings(layers: string[][], chains: Chain[]): number {
53
+ const positions = locate(layers)
54
+ const segments = new Map<number, Array<{ top: number; bottom: number }>>()
55
+ for (const chain of chains) {
56
+ for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
57
+ const top = positions.get(chain.path[cursor])
58
+ const bottom = positions.get(chain.path[cursor + 1])
59
+ if (top === undefined || bottom === undefined || bottom.layer !== top.layer + 1) continue
60
+ const list = segments.get(top.layer) ?? []
61
+ list.push({ top: top.index, bottom: bottom.index })
62
+ segments.set(top.layer, list)
63
+ }
64
+ }
65
+ let crossings = 0
66
+ for (const list of segments.values()) {
67
+ for (let left = 0; left < list.length; left += 1) {
68
+ for (let right = left + 1; right < list.length; right += 1) {
69
+ if ((list[left].top - list[right].top) * (list[left].bottom - list[right].bottom) < 0) crossings += 1
70
+ }
71
+ }
72
+ }
73
+ return crossings
74
+ }
75
+
76
+ export function order(seedLayers: string[][], chains: Chain[], seed?: Map<string, number>): Ordered {
77
+ // The first guess: the order the source's own reading suggests. A dummy has no such order and starts last in its
78
+ // layer; the median sweep is what moves it to the lane its edge crosses.
79
+ const layers = seedLayers.map((layer) =>
80
+ layer
81
+ .map((id, index) => ({ id, index }))
82
+ .sort((left, right) => (seed?.get(left.id) ?? Infinity) - (seed?.get(right.id) ?? Infinity) || left.index - right.index)
83
+ .map((entry) => entry.id),
84
+ )
85
+ const down = new Map<string, string[]>()
86
+ const up = new Map<string, string[]>()
87
+ for (const chain of chains) {
88
+ for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
89
+ const top = chain.path[cursor]
90
+ const bottom = chain.path[cursor + 1]
91
+ down.set(top, [...(down.get(top) ?? []), bottom])
92
+ up.set(bottom, [...(up.get(bottom) ?? []), top])
93
+ }
94
+ }
95
+
96
+ let positions = locate(layers)
97
+ for (let round = 0; round < 4; round += 1) {
98
+ for (let depth = 1; depth < layers.length; depth += 1) {
99
+ const layer = layers[depth]
100
+ const sorted = layer
101
+ .map((id, index) => ({ id, index, median: medianOf(id, up, depth - 1, positions) }))
102
+ .sort((left, right) => left.median - right.median || left.index - right.index)
103
+ .map((entry) => entry.id)
104
+ layers[depth] = sorted
105
+ positions = locate(layers)
106
+ }
107
+ for (let depth = layers.length - 2; depth >= 0; depth -= 1) {
108
+ const layer = layers[depth]
109
+ const sorted = layer
110
+ .map((id, index) => ({ id, index, median: medianOf(id, down, depth + 1, positions) }))
111
+ .sort((left, right) => left.median - right.median || left.index - right.index)
112
+ .map((entry) => entry.id)
113
+ layers[depth] = sorted
114
+ positions = locate(layers)
115
+ }
116
+ }
117
+
118
+ let crossings = countCrossings(layers, chains)
119
+ for (let pass = 0; pass < 4; pass += 1) {
120
+ let improved = false
121
+ for (const layer of layers) {
122
+ for (let index = 0; index + 1 < layer.length; index += 1) {
123
+ const before = layer[index]
124
+ layer[index] = layer[index + 1]
125
+ layer[index + 1] = before
126
+ const next = countCrossings(layers, chains)
127
+ if (next < crossings) {
128
+ crossings = next
129
+ improved = true
130
+ } else {
131
+ layer[index + 1] = layer[index]
132
+ layer[index] = before
133
+ }
134
+ }
135
+ }
136
+ if (!improved) break
137
+ }
138
+ return { layers, crossings }
139
+ }