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.
- package/README.en-US.md +12 -0
- package/README.md +4 -0
- package/lib/client.js +2829 -224
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +12 -20
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +34 -10
- package/lib/render/from-plantuml.js +318 -46
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +27 -11
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +1227 -69
- package/lib/render/place.js +393 -121
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +146 -21
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/trunk.js +213 -0
- package/lib/render/wrap.js +87 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-plantuml.d.ts +12 -6
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +43 -11
- package/lib/types/render/model.d.ts +17 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +157 -10
- package/lib/types/render/place.d.ts +40 -19
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +1 -1
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/trunk.d.ts +104 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/package.json +148 -148
- package/skills/model-everything-diagram/SKILL.md +85 -82
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -155
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -236
- package/src/render/from-plantuml.ts +469 -178
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -62
- package/src/render/model.ts +86 -67
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1570 -290
- package/src/render/place.ts +532 -234
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +274 -127
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/trunk.ts +275 -0
- package/src/render/wrap.ts +103 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layering: which row each box is in.
|
|
3
|
+
*
|
|
4
|
+
* The graph is acyclic by the time it gets here. A layered drawing needs every edge to span at least one layer, so it
|
|
5
|
+
* always travels downwards, and it wants the total of the layers every edge spans beyond one to be as small as
|
|
6
|
+
* possible: those are the edges that have to be drawn through a row of placeholders instead of going straight down a
|
|
7
|
+
* gap.
|
|
8
|
+
*
|
|
9
|
+
* Longest paths are the first guess: every source on the top row, every other node one layer below its latest
|
|
10
|
+
* predecessor. It is feasible and it is not optimal, because it puts every node as high as it can go - a node whose
|
|
11
|
+
* edges leave it with slack above the boxes it points at is a node that could have moved down and shortened them.
|
|
12
|
+
* `network-simplex.ts` solves the program the mature layering implementations solve: the ranking that minimises the
|
|
13
|
+
* total length, found by exchanging edges in a tight tree until no exchange is left that shortens it.
|
|
14
|
+
*
|
|
15
|
+
* The initial state is layer zero. It is a node with no predecessor - cycle breaking has turned around every edge
|
|
16
|
+
* that ran into it - so a source, and every source is a candidate for the top row. The pin is passed to the solver as
|
|
17
|
+
* a constraint, so the search never trades it away: an initial state that moved down the page would be a machine
|
|
18
|
+
* diagram read from the wrong end.
|
|
19
|
+
*
|
|
20
|
+
* Pure: the graph in, one layer per node out.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { LayoutEdge, LayoutGraph } from './layout.js'
|
|
24
|
+
import { minimumLength, type RankEdge } from './network-simplex.js'
|
|
25
|
+
|
|
26
|
+
export interface Layering {
|
|
27
|
+
/** The ids of each layer, top to bottom. A layer holds at least one node. */
|
|
28
|
+
layers: string[][]
|
|
29
|
+
layerOf: Map<string, number>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The longest-path ranking: the highest layer each node can possibly take.
|
|
34
|
+
*
|
|
35
|
+
* Every source starts on layer zero and every other node one below its latest predecessor, which is the shortest
|
|
36
|
+
* ranking that is feasible at all - and, because no node can then move up, the starting point the exchange search
|
|
37
|
+
* needs. A node the walk never reaches is part of a cycle and is put on layer zero: the stage is defined on a DAG,
|
|
38
|
+
* and a graph that arrived cyclic is laid out as far as it can be rather than losing the boxes of the cycle.
|
|
39
|
+
*/
|
|
40
|
+
function longestPath(ids: string[], edges: LayoutEdge[], index: Map<string, number>): number[] {
|
|
41
|
+
const rank = new Array<number>(ids.length).fill(0)
|
|
42
|
+
const pending = new Array<number>(ids.length).fill(0)
|
|
43
|
+
const outgoing: number[][] = ids.map(() => [])
|
|
44
|
+
for (const edge of edges) {
|
|
45
|
+
outgoing[index.get(edge.from) as number].push(index.get(edge.to) as number)
|
|
46
|
+
pending[index.get(edge.to) as number] += 1
|
|
47
|
+
}
|
|
48
|
+
const queue: number[] = []
|
|
49
|
+
for (let node = 0; node < ids.length; node += 1) if (pending[node] === 0) queue.push(node)
|
|
50
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
51
|
+
const node = queue[head]
|
|
52
|
+
for (const target of outgoing[node]) {
|
|
53
|
+
rank[target] = Math.max(rank[target], rank[node] + 1)
|
|
54
|
+
pending[target] -= 1
|
|
55
|
+
if (pending[target] === 0) queue.push(target)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return rank
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function layer(graph: LayoutGraph, edges: LayoutEdge[]): Layering {
|
|
62
|
+
// A model is data: the same box may be written twice, and an edge may name a box this level does not have.
|
|
63
|
+
const ids: string[] = []
|
|
64
|
+
const seen = new Set<string>()
|
|
65
|
+
for (const node of graph.nodes) {
|
|
66
|
+
if (seen.has(node.id)) continue
|
|
67
|
+
seen.add(node.id)
|
|
68
|
+
ids.push(node.id)
|
|
69
|
+
}
|
|
70
|
+
const index = new Map(ids.map((id, offset) => [id, offset] as const))
|
|
71
|
+
const forward = edges.filter((edge) => index.has(edge.from) && index.has(edge.to) && edge.from !== edge.to)
|
|
72
|
+
const rankEdges: RankEdge[] = forward.map((edge) => ({
|
|
73
|
+
from: index.get(edge.from) as number,
|
|
74
|
+
to: index.get(edge.to) as number,
|
|
75
|
+
minLength: 1,
|
|
76
|
+
weight: 1,
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
// The pin only means something for a node that could be on the top row. A node with a predecessor cannot be: it is
|
|
80
|
+
// forced below it, and no ranking puts it first while keeping the edge that points at it.
|
|
81
|
+
let pinned: number | undefined
|
|
82
|
+
if (graph.init !== undefined && index.has(graph.init)) {
|
|
83
|
+
const start = index.get(graph.init) as number
|
|
84
|
+
const forced = forward.some((edge) => edge.to === graph.init)
|
|
85
|
+
if (!forced) pinned = start
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rank = minimumLength({ count: ids.length, edges: rankEdges, start: longestPath(ids, forward, index), ...(pinned === undefined ? {} : { pinned }) })
|
|
89
|
+
|
|
90
|
+
const layers: string[][] = []
|
|
91
|
+
for (const [offset, id] of ids.entries()) {
|
|
92
|
+
const depth = rank[offset]
|
|
93
|
+
while (layers.length <= depth) layers.push([])
|
|
94
|
+
layers[depth].push(id)
|
|
95
|
+
}
|
|
96
|
+
const layerOf = new Map(ids.map((id, offset) => [id, rank[offset]] as const))
|
|
97
|
+
return { layers, layerOf }
|
|
98
|
+
}
|
package/src/render/layout.ts
CHANGED
|
@@ -1,62 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Pure, and asserted on the
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The graph the layout stages agree on, and the order they start from.
|
|
3
|
+
*
|
|
4
|
+
* A layout stage has no business knowing what a diagram is: it sees boxes with a size and edges
|
|
5
|
+
* between them. That is what `LayoutGraph` is, and it is the whole vocabulary the pipeline shares -
|
|
6
|
+
* cycle breaking reads it, layering writes layers, ordering and coordinate assignment read those.
|
|
7
|
+
*
|
|
8
|
+
* `rankStates` stays: it is the breadth-first order a source's own reading suggests, and it seeds
|
|
9
|
+
* the ordering stage with the discovery order instead of whatever order the parser happened to
|
|
10
|
+
* collect. It no longer decides which row a state is in - `layer.ts` does that now - so the rows it
|
|
11
|
+
* returns are only a first guess at the left-to-right order.
|
|
12
|
+
*
|
|
13
|
+
* Pure, and asserted on the answer itself.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A box to place: an id, and the space its text needs. */
|
|
17
|
+
export interface LayoutNode {
|
|
18
|
+
id: string
|
|
19
|
+
width: number
|
|
20
|
+
height: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** An edge to draw: which two boxes it joins, and which diagram edge it came from. */
|
|
24
|
+
export interface LayoutEdge {
|
|
25
|
+
/** The index in the diagram's own edge list, so the drawing can report a lane back for it. */
|
|
26
|
+
index: number
|
|
27
|
+
from: string
|
|
28
|
+
to: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One level of a nested drawing: what is placed together in a single pass of the pipeline. */
|
|
32
|
+
export interface LayoutGraph {
|
|
33
|
+
nodes: LayoutNode[]
|
|
34
|
+
edges: LayoutEdge[]
|
|
35
|
+
/** The box that must stay on layer zero, when the source named one. */
|
|
36
|
+
init?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Ranked {
|
|
40
|
+
/** The state ids of each row, top to bottom, already ordered left to right. */
|
|
41
|
+
rows: string[][]
|
|
42
|
+
/** States no edge reaches from the initial one. */
|
|
43
|
+
unreachable: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function rankStates(
|
|
47
|
+
nodes: ReadonlyArray<{ id: string }>,
|
|
48
|
+
edges: ReadonlyArray<{ from: string; to: string }>,
|
|
49
|
+
init: string | undefined,
|
|
50
|
+
): Ranked {
|
|
51
|
+
const ids = nodes.map((node) => node.id)
|
|
52
|
+
const known = new Set(ids)
|
|
53
|
+
const start = init !== undefined && known.has(init) ? init : ids[0]
|
|
54
|
+
|
|
55
|
+
const outgoing = new Map<string, string[]>()
|
|
56
|
+
for (const edge of edges) {
|
|
57
|
+
if (!known.has(edge.from) || !known.has(edge.to)) continue
|
|
58
|
+
outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const depth = new Map<string, number>()
|
|
62
|
+
if (start !== undefined) {
|
|
63
|
+
depth.set(start, 0)
|
|
64
|
+
const queue: string[] = [start]
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
const id = queue.shift() as string
|
|
67
|
+
const level = depth.get(id) as number
|
|
68
|
+
for (const target of outgoing.get(id) ?? []) {
|
|
69
|
+
if (depth.has(target)) continue
|
|
70
|
+
depth.set(target, level + 1)
|
|
71
|
+
queue.push(target)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rows: string[][] = []
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const level = depth.get(id)
|
|
79
|
+
if (level === undefined) continue
|
|
80
|
+
while (rows.length <= level) rows.push([])
|
|
81
|
+
rows[level].push(id)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { rows, unreachable: ids.filter((id) => !depth.has(id)) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The left-to-right seed the ordering stage starts from.
|
|
89
|
+
*
|
|
90
|
+
* Breadth-first discovery is what a reader expects a drawing to follow, so it is a better first
|
|
91
|
+
* guess than the order the parser collected. A state nothing reaches has no discovery order and
|
|
92
|
+
* keeps the order it was written in, after everything that was reached.
|
|
93
|
+
*/
|
|
94
|
+
export function seedOrder(graph: LayoutGraph): Map<string, number> {
|
|
95
|
+
const ranked = rankStates(graph.nodes, graph.edges, graph.init)
|
|
96
|
+
const order = new Map<string, number>()
|
|
97
|
+
let index = 0
|
|
98
|
+
for (const row of ranked.rows) for (const id of row) order.set(id, index++)
|
|
99
|
+
for (const id of ranked.unreachable) order.set(id, index++)
|
|
100
|
+
return order
|
|
101
|
+
}
|
package/src/render/model.ts
CHANGED
|
@@ -1,67 +1,86 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The diagram a renderer draws, in the notation it came from being nobody's business.
|
|
3
|
-
*
|
|
4
|
-
* Every source - a verification report, a Mermaid file, a PlantUML component diagram, a dependency scan -
|
|
5
|
-
* is reduced to this one shape before anything is laid out. A renderer that understood notations would have
|
|
6
|
-
* to be changed for each of them; a renderer that understands this does not.
|
|
7
|
-
*
|
|
8
|
-
* Pure data, no functions: a model can be serialised, compared and asserted.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/** What a node is, as far as colour is concerned. */
|
|
12
|
-
export type NodeRole = 'normal' | 'error' | 'warning' | 'muted'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The diagram a renderer draws, in the notation it came from being nobody's business.
|
|
3
|
+
*
|
|
4
|
+
* Every source - a verification report, a Mermaid file, a PlantUML component diagram, a dependency scan -
|
|
5
|
+
* is reduced to this one shape before anything is laid out. A renderer that understood notations would have
|
|
6
|
+
* to be changed for each of them; a renderer that understands this does not.
|
|
7
|
+
*
|
|
8
|
+
* Pure data, no functions: a model can be serialised, compared and asserted.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** What a node is, as far as colour is concerned. */
|
|
12
|
+
export type NodeRole = 'normal' | 'error' | 'warning' | 'muted'
|
|
13
|
+
|
|
14
|
+
/** The symbol a node is drawn as. UML and Mermaid both distinguish these; the notation decides which one applies. */
|
|
15
|
+
export type NodeShape = 'rect' | 'diamond' | 'cylinder' | 'stadium' | 'circle' | 'subroutine' | 'hexagon'
|
|
16
|
+
|
|
17
|
+
export interface DiagramNode {
|
|
18
|
+
id: string
|
|
19
|
+
/** The text drawn inside the box. Defaults to the id when a notation labelled it. */
|
|
20
|
+
label?: string
|
|
21
|
+
role: NodeRole
|
|
22
|
+
/** Defaults to a rectangle: a node the source gave no shape has no shape to draw. */
|
|
23
|
+
shape?: NodeShape
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What an edge's head means, when the notation distinguished it.
|
|
28
|
+
*
|
|
29
|
+
* The painter draws the meaning: a plain arrow for an association, a hollow triangle for a generalisation or a
|
|
30
|
+
* realisation, a filled diamond at the source for a composition, a hollow one for an aggregation, and a dashed
|
|
31
|
+
* line for the two dependency-flavoured kinds.
|
|
32
|
+
*/
|
|
33
|
+
export type EdgeArrow = 'association' | 'dependency' | 'generalization' | 'realization' | 'composition' | 'aggregation'
|
|
34
|
+
|
|
35
|
+
export interface DiagramEdge {
|
|
36
|
+
from: string
|
|
37
|
+
to: string
|
|
38
|
+
label: string
|
|
39
|
+
/** A dashed edge, for notations that distinguish one. */
|
|
40
|
+
dashed?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* The arrow the notation stated. Absent means the plain arrow every edge has been drawn with: a Mermaid edge
|
|
43
|
+
* states no arrow type, so it carries no field and is drawn exactly as before.
|
|
44
|
+
*/
|
|
45
|
+
arrow?: EdgeArrow
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DiagramGroup {
|
|
49
|
+
id: string
|
|
50
|
+
label: string
|
|
51
|
+
/**
|
|
52
|
+
* The nodes declared directly inside it: a group is a box around boxes, and its own members are the ones a
|
|
53
|
+
* source named there. A node inside a nested group is that group's member, not this one's.
|
|
54
|
+
*/
|
|
55
|
+
members: string[]
|
|
56
|
+
/**
|
|
57
|
+
* The group that contains this one, when the source said so.
|
|
58
|
+
*
|
|
59
|
+
* Only a parser can know this - nesting is a property of where the text put the boundary - so the layout does
|
|
60
|
+
* not guess it. Left absent, the painter falls back to inferring containment from member sets, which is the
|
|
61
|
+
* best a hand-written model can offer.
|
|
62
|
+
*/
|
|
63
|
+
parent?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface Diagram {
|
|
67
|
+
kind: 'machine' | 'graph' | 'none'
|
|
68
|
+
/** The state the machine starts in, when the source declared one. */
|
|
69
|
+
init?: string
|
|
70
|
+
title: string
|
|
71
|
+
nodes: DiagramNode[]
|
|
72
|
+
edges: DiagramEdge[]
|
|
73
|
+
groups?: DiagramGroup[]
|
|
74
|
+
/** One line under the drawing: what was decided, in words the drawing cannot carry. */
|
|
75
|
+
caption: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** An empty drawing, for a source that produced no nodes. */
|
|
79
|
+
export function emptyDiagram(title: string, caption: string): Diagram {
|
|
80
|
+
return { kind: 'none', title, nodes: [], edges: [], caption }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The label to draw for a node: its own label when the notation gave one, else its id. */
|
|
84
|
+
export function labelOf(node: DiagramNode): string {
|
|
85
|
+
return node.label !== undefined && node.label !== '' ? node.label : node.id
|
|
86
|
+
}
|