dsh-model-everything 0.4.1 → 0.5.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/README.en-US.md +75 -19
- package/README.md +377 -345
- package/cordis.patch.yml +9 -0
- package/diagram/package.json +5 -0
- package/lib/client.js +3494 -0
- package/lib/diagram-diff-tool.js +90 -0
- package/lib/diagram-render-tool.js +102 -0
- package/lib/diagram-review-tool.js +99 -0
- package/lib/diagram.js +43 -0
- package/lib/gate.js +14 -12
- package/lib/presentation.js +10 -0
- package/lib/render/budget.js +99 -0
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +155 -0
- 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/diff.js +138 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +235 -0
- package/lib/render/from-plantuml.js +430 -0
- package/lib/render/index.js +42 -0
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +68 -0
- package/lib/render/model.js +17 -0
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +891 -0
- package/lib/render/palette.js +127 -0
- package/lib/render/place.js +482 -0
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +229 -0
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/viewmodel.js +126 -0
- package/lib/render/wrap.js +87 -0
- package/lib/render/write.js +14 -0
- package/lib/scan.js +248 -8
- package/lib/structure-tool.js +2 -2
- package/lib/types/client.d.ts +83 -0
- package/lib/types/diagram-diff-tool.d.ts +8 -0
- package/lib/types/diagram-render-tool.d.ts +8 -0
- package/lib/types/diagram-review-tool.d.ts +8 -0
- package/lib/types/diagram.d.ts +34 -0
- package/lib/types/presentation.d.ts +7 -0
- package/lib/types/render/budget.d.ts +55 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/collapse.d.ts +23 -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/diff.d.ts +44 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-mermaid.d.ts +27 -0
- package/lib/types/render/from-plantuml.d.ts +33 -0
- package/lib/types/render/index.d.ts +38 -0
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +54 -0
- package/lib/types/render/model.d.ts +73 -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 +127 -0
- package/lib/types/render/palette.d.ts +41 -0
- package/lib/types/render/place.d.ts +55 -0
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +19 -0
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/viewmodel.d.ts +20 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/lib/types/render/write.d.ts +14 -0
- package/lib/types/scan.d.ts +10 -2
- package/lib/types/structure.d.ts +1 -0
- package/package.json +148 -132
- package/skills/model-everything/SKILL.md +454 -454
- package/skills/model-everything/references/architecture-domains.md +137 -0
- package/skills/model-everything/references/architecture-views.md +119 -0
- package/skills/model-everything-diagram/SKILL.md +84 -0
- package/src/client.ts +307 -0
- package/src/diagram-diff-tool.ts +94 -0
- package/src/diagram-render-tool.ts +108 -0
- package/src/diagram-review-tool.ts +109 -0
- package/src/diagram.ts +61 -0
- package/src/gate.ts +14 -12
- package/src/presentation.ts +12 -0
- package/src/render/budget.ts +135 -0
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -0
- 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/diff.ts +162 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -0
- package/src/render/from-plantuml.ts +469 -0
- package/src/render/index.ts +73 -0
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -0
- package/src/render/model.ts +86 -0
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1006 -0
- package/src/render/palette.ts +164 -0
- package/src/render/place.ts +527 -0
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +270 -0
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/viewmodel.ts +136 -0
- package/src/render/wrap.ts +103 -0
- package/src/render/write.ts +17 -0
- package/src/scan.ts +807 -564
- package/src/structure-tool.ts +2 -2
- package/src/structure.ts +1 -1
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +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
|
+
/** 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
|
+
}
|
|
@@ -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
|
+
}
|