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
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The graph the layout stages agree on, and the order they start from.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
12
|
*
|
|
13
|
-
* Pure, and asserted on the
|
|
13
|
+
* Pure, and asserted on the answer itself.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
/** A box to place: an id, and the space its text needs. */
|
|
16
|
+
export interface LayoutNode {
|
|
17
|
+
id: string;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
/** An edge to draw: which two boxes it joins, and which diagram edge it came from. */
|
|
22
|
+
export interface LayoutEdge {
|
|
23
|
+
/** The index in the diagram's own edge list, so the drawing can report a lane back for it. */
|
|
24
|
+
index: number;
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
}
|
|
28
|
+
/** One level of a nested drawing: what is placed together in a single pass of the pipeline. */
|
|
29
|
+
export interface LayoutGraph {
|
|
30
|
+
nodes: LayoutNode[];
|
|
31
|
+
edges: LayoutEdge[];
|
|
32
|
+
/** The box that must stay on layer zero, when the source named one. */
|
|
33
|
+
init?: string;
|
|
34
|
+
}
|
|
16
35
|
export interface Ranked {
|
|
17
36
|
/** The state ids of each row, top to bottom, already ordered left to right. */
|
|
18
37
|
rows: string[][];
|
|
19
38
|
/** States no edge reaches from the initial one. */
|
|
20
39
|
unreachable: string[];
|
|
21
40
|
}
|
|
22
|
-
export declare function rankStates(nodes:
|
|
41
|
+
export declare function rankStates(nodes: ReadonlyArray<{
|
|
42
|
+
id: string;
|
|
43
|
+
}>, edges: ReadonlyArray<{
|
|
44
|
+
from: string;
|
|
45
|
+
to: string;
|
|
46
|
+
}>, init: string | undefined): Ranked;
|
|
47
|
+
/**
|
|
48
|
+
* The left-to-right seed the ordering stage starts from.
|
|
49
|
+
*
|
|
50
|
+
* Breadth-first discovery is what a reader expects a drawing to follow, so it is a better first
|
|
51
|
+
* guess than the order the parser collected. A state nothing reaches has no discovery order and
|
|
52
|
+
* keeps the order it was written in, after everything that was reached.
|
|
53
|
+
*/
|
|
54
|
+
export declare function seedOrder(graph: LayoutGraph): Map<string, number>;
|
|
@@ -9,18 +9,35 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/** What a node is, as far as colour is concerned. */
|
|
11
11
|
export type NodeRole = 'normal' | 'error' | 'warning' | 'muted';
|
|
12
|
+
/** The symbol a node is drawn as. UML and Mermaid both distinguish these; the notation decides which one applies. */
|
|
13
|
+
export type NodeShape = 'rect' | 'diamond' | 'cylinder' | 'stadium' | 'circle' | 'subroutine' | 'hexagon';
|
|
12
14
|
export interface DiagramNode {
|
|
13
15
|
id: string;
|
|
14
16
|
/** The text drawn inside the box. Defaults to the id when a notation labelled it. */
|
|
15
17
|
label?: string;
|
|
16
18
|
role: NodeRole;
|
|
19
|
+
/** Defaults to a rectangle: a node the source gave no shape has no shape to draw. */
|
|
20
|
+
shape?: NodeShape;
|
|
17
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* What an edge's head means, when the notation distinguished it.
|
|
24
|
+
*
|
|
25
|
+
* The painter draws the meaning: a plain arrow for an association, a hollow triangle for a generalisation or a
|
|
26
|
+
* realisation, a filled diamond at the source for a composition, a hollow one for an aggregation, and a dashed
|
|
27
|
+
* line for the two dependency-flavoured kinds.
|
|
28
|
+
*/
|
|
29
|
+
export type EdgeArrow = 'association' | 'dependency' | 'generalization' | 'realization' | 'composition' | 'aggregation';
|
|
18
30
|
export interface DiagramEdge {
|
|
19
31
|
from: string;
|
|
20
32
|
to: string;
|
|
21
33
|
label: string;
|
|
22
34
|
/** A dashed edge, for notations that distinguish one. */
|
|
23
35
|
dashed?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The arrow the notation stated. Absent means the plain arrow every edge has been drawn with: a Mermaid edge
|
|
38
|
+
* states no arrow type, so it carries no field and is drawn exactly as before.
|
|
39
|
+
*/
|
|
40
|
+
arrow?: EdgeArrow;
|
|
24
41
|
}
|
|
25
42
|
export interface DiagramGroup {
|
|
26
43
|
id: string;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
/** One edge of the ranking program: two layers, how far apart they may be, and what one layer of slack costs. */
|
|
33
|
+
export interface RankEdge {
|
|
34
|
+
from: number;
|
|
35
|
+
to: number;
|
|
36
|
+
/** The least number of layers this edge may span. One for an edge that is drawn, zero for a constraint. */
|
|
37
|
+
minLength: number;
|
|
38
|
+
/** What one extra layer on this edge costs. Zero for a constraint, which must hold but is not worth shortening. */
|
|
39
|
+
weight: number;
|
|
40
|
+
}
|
|
41
|
+
export interface RankProblem {
|
|
42
|
+
count: number;
|
|
43
|
+
edges: RankEdge[];
|
|
44
|
+
/** A feasible ranking to start from: it already gives every edge at least its minimum length. */
|
|
45
|
+
start: number[];
|
|
46
|
+
/**
|
|
47
|
+
* The node that has to sit on the topmost layer, when the source named one.
|
|
48
|
+
*
|
|
49
|
+
* It has to be a node nothing points at, or no ranking can hold it at the top - a predecessor forces it down. The
|
|
50
|
+
* constraint is dropped when the start ranking cannot satisfy it.
|
|
51
|
+
*/
|
|
52
|
+
pinned?: number;
|
|
53
|
+
}
|
|
54
|
+
export declare function minimumLength(problem: RankProblem): number[];
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
import type { Chain } from './dummy.js';
|
|
19
|
+
export interface Ordered {
|
|
20
|
+
/** The same layers, each in its final order. */
|
|
21
|
+
layers: string[][];
|
|
22
|
+
/** The number of crossings between adjacent layers left by the ordering. */
|
|
23
|
+
crossings: number;
|
|
24
|
+
}
|
|
25
|
+
/** How many pairs of edges between adjacent layers cross. */
|
|
26
|
+
export declare function countCrossings(layers: string[][], chains: Chain[]): number;
|
|
27
|
+
export declare function order(seedLayers: string[][], chains: Chain[], seed?: Map<string, number>): Ordered;
|
|
@@ -5,21 +5,39 @@
|
|
|
5
5
|
* sidebar tab - so nothing here knows about pixel size. It emits a content-sized canvas and lets the
|
|
6
6
|
* container scale it: the same numbers serve a 360px card and a 1200px whiteboard.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The drawing is placed by the layered pipeline (`place.ts`), which also says which lane a long edge reserved.
|
|
9
|
+
* Edges are drawn from where the boxes ended up, in two ways only: a long edge follows its lane as one polyline,
|
|
10
|
+
* and everything else goes through the orthogonal router. Which side of a box an edge leaves by is read off the
|
|
11
|
+
* route, not picked by comparing the boxes' centres: a route between two rows arrives vertically, so the endpoint
|
|
12
|
+
* is snapped onto the top or bottom perimeter in the direction the route approaches. Where along that side it sits
|
|
13
|
+
* follows the symbol: a rectangle spreads its ports along the side, a cylinder's top port sits on its cap curve, and a
|
|
14
|
+
* diamond or a circle has four legal points and no side to spread along at all. A side that carries one port is
|
|
15
|
+
* centred on it, and a side that carries several spreads them symmetrically about the middle; the bend budget comes
|
|
16
|
+
* first, so a port that is the end of a straight run or of a lane keeps the coordinate its route asked for.
|
|
9
17
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* whose members float somewhere else on the page is not showing containment at all.
|
|
18
|
+
* A symbol with discrete points gives each of its edges a point of its own: the point whose outward normal faces the
|
|
19
|
+
* box at the other end is the one that edge takes, and an edge only shares a point when all four are spoken for. A
|
|
20
|
+
* node with a run on its side keeps doing what it always did, because there the ports are places on the run and not
|
|
21
|
+
* points at all. Sharing is the fallback, not the rule: two labelled branches of a decision leave by two vertices.
|
|
15
22
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
23
|
+
* An edge's name is drawn on the edge, not beside it: it is anchored at the midpoint of one of the edge's own straight
|
|
24
|
+
* runs, over a rectangle in the page colour that masks the line behind the text, and it slides along that run when
|
|
25
|
+
* something is in the way. It tries every run - the longest first - because on a dense drawing the longest one can be
|
|
26
|
+
* crowded for its whole length while a shorter run of the same edge is clear. A label that can clear nothing still
|
|
27
|
+
* stays on its edge, takes the place with the fewest collisions, and records what it could not avoid. Labels are
|
|
28
|
+
* placed after every edge is drawn, because what a label has to avoid is the whole picture.
|
|
29
|
+
*
|
|
30
|
+
* Every route is checked before it is drawn, and the assembled drawing is checked once more: a segment may touch no
|
|
31
|
+
* box except the two the edge terminates at. A route is a proposal - a channel run, a lane, a trunk branch, a wrapped
|
|
32
|
+
* column's sideways read - and any proposal that would cross a box is handed to the router instead. When even the
|
|
33
|
+
* router cannot clear a box, the failure is reported on the canvas rather than hidden: a drawing that says which edge
|
|
34
|
+
* could not be routed is worth more than one that quietly draws it through a box.
|
|
18
35
|
*
|
|
19
36
|
* Pure: a diagram in, shapes out, every decision assertable.
|
|
20
37
|
*/
|
|
38
|
+
import { type Box } from './place.js';
|
|
21
39
|
import { type ColourRole, type Palette, type PaletteOptions } from './palette.js';
|
|
22
|
-
import { type Diagram, type DiagramNode } from './model.js';
|
|
40
|
+
import { type Diagram, type DiagramNode, type EdgeArrow, type NodeShape } from './model.js';
|
|
23
41
|
export interface Point {
|
|
24
42
|
x: number;
|
|
25
43
|
y: number;
|
|
@@ -30,10 +48,30 @@ export interface BoxShape {
|
|
|
30
48
|
y: number;
|
|
31
49
|
width: number;
|
|
32
50
|
height: number;
|
|
51
|
+
/** The lines to draw, already wrapped; `label` keeps the whole text for anything that reads the shape. */
|
|
33
52
|
label: string;
|
|
53
|
+
lines: string[];
|
|
54
|
+
/** The symbol to draw. Undefined means a rectangle. */
|
|
55
|
+
shape?: NodeShape;
|
|
34
56
|
colour: ColourRole;
|
|
35
57
|
initial: boolean;
|
|
36
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Why a label sits where it does.
|
|
61
|
+
*
|
|
62
|
+
* A clear label records `collisions: 0` and the run it chose. A label that could clear nothing records the collisions
|
|
63
|
+
* it could not avoid, so a reader of the drawing (or a test) can tell a crowded drawing from a placement bug. The
|
|
64
|
+
* label never leaves its own edge to earn any of these numbers.
|
|
65
|
+
*/
|
|
66
|
+
export interface LabelPlacement {
|
|
67
|
+
/** The straight run it sits on, 0 being the edge's longest. */
|
|
68
|
+
run: number;
|
|
69
|
+
/** Everything its mask overlaps: the other labels, boxes and edges it would have had to move off its line to clear. */
|
|
70
|
+
collisions: number;
|
|
71
|
+
boxes: number;
|
|
72
|
+
labels: number;
|
|
73
|
+
edges: number;
|
|
74
|
+
}
|
|
37
75
|
export interface LinkShape {
|
|
38
76
|
kind: 'link';
|
|
39
77
|
x1: number;
|
|
@@ -41,17 +79,43 @@ export interface LinkShape {
|
|
|
41
79
|
x2: number;
|
|
42
80
|
y2: number;
|
|
43
81
|
label: string;
|
|
82
|
+
/** The centre the label is drawn on, and therefore a point on the edge itself. */
|
|
44
83
|
labelX: number;
|
|
45
84
|
labelY: number;
|
|
46
85
|
colour: ColourRole;
|
|
86
|
+
/** A dashed line, when the notation distinguished one. */
|
|
87
|
+
dashed?: boolean;
|
|
88
|
+
/** The arrow the source edge stated. Undefined means the plain arrow every edge has been drawn with. */
|
|
89
|
+
arrow?: EdgeArrow;
|
|
90
|
+
/** Why the label sits where it does. Undefined on an edge with no label. */
|
|
91
|
+
labelPlacement?: LabelPlacement;
|
|
47
92
|
}
|
|
48
93
|
export interface PathShape {
|
|
49
94
|
kind: 'path';
|
|
50
95
|
points: Point[];
|
|
51
96
|
label: string;
|
|
97
|
+
/** The centre the label is drawn on, and therefore a point on the edge itself. */
|
|
52
98
|
labelX: number;
|
|
53
99
|
labelY: number;
|
|
54
100
|
colour: ColourRole;
|
|
101
|
+
dashed?: boolean;
|
|
102
|
+
arrow?: EdgeArrow;
|
|
103
|
+
/** Why the label sits where it does. Undefined on an edge with no label. */
|
|
104
|
+
labelPlacement?: LabelPlacement;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A junction dot: the place one branch of a merged bundle meets the bundle's trunk.
|
|
108
|
+
*
|
|
109
|
+
* The dot is not decoration. Two lines that meet at a right angle read as a crossing unless one of them is marked as
|
|
110
|
+
* joining the other, and a bundle whose branches are not marked reads as a set of broken lines rather than a merge.
|
|
111
|
+
*/
|
|
112
|
+
export interface JunctionShape {
|
|
113
|
+
kind: 'junction';
|
|
114
|
+
x: number;
|
|
115
|
+
y: number;
|
|
116
|
+
colour: ColourRole;
|
|
117
|
+
/** The trunk the dot belongs to: one dot per branch, one trunk id per bundle. */
|
|
118
|
+
trunk: number;
|
|
55
119
|
}
|
|
56
120
|
/** A boundary around several states: a package, a layer, a subgraph. */
|
|
57
121
|
export interface GroupShape {
|
|
@@ -79,16 +143,99 @@ export interface TitleShape {
|
|
|
79
143
|
text: string;
|
|
80
144
|
colour: ColourRole;
|
|
81
145
|
}
|
|
82
|
-
export type Shape = GroupShape | BoxShape | LinkShape | PathShape | CaptionShape | TitleShape;
|
|
146
|
+
export type Shape = GroupShape | BoxShape | LinkShape | PathShape | JunctionShape | CaptionShape | TitleShape;
|
|
147
|
+
/** One segment of a drawn route that could not be kept out of a box it does not terminate at. */
|
|
148
|
+
export interface RoutingFailure {
|
|
149
|
+
/** The edge's index in the diagram. */
|
|
150
|
+
edge: number;
|
|
151
|
+
from: string;
|
|
152
|
+
to: string;
|
|
153
|
+
/** The box the segment passes through. */
|
|
154
|
+
box: string;
|
|
155
|
+
x1: number;
|
|
156
|
+
y1: number;
|
|
157
|
+
x2: number;
|
|
158
|
+
y2: number;
|
|
159
|
+
}
|
|
83
160
|
export interface Canvas {
|
|
84
161
|
width: number;
|
|
85
162
|
height: number;
|
|
86
163
|
shapes: Shape[];
|
|
164
|
+
/**
|
|
165
|
+
* Every place a drawn segment still passes through a box it does not terminate at, after every route was checked.
|
|
166
|
+
*
|
|
167
|
+
* A drawing should carry none. The field exists for the case where no stage and no router could find a way around:
|
|
168
|
+
* a route that cannot be drawn honestly is reported here rather than silently drawn through a box.
|
|
169
|
+
*/
|
|
170
|
+
routingFailures: RoutingFailure[];
|
|
171
|
+
/** How many edge labels could not be placed clear of every box, label and other edge. */
|
|
172
|
+
labelCollisions: number;
|
|
87
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* How far apart two edges that join the same pair are drawn.
|
|
176
|
+
*
|
|
177
|
+
* Wider than the least two ports may sit: two statements about one pair have to read as two lines, and a gap the
|
|
178
|
+
* port packer would allow is a gap a reader sees as one thick line. Exported so a test can name the pattern.
|
|
179
|
+
*/
|
|
180
|
+
export declare const MULTI_EDGE_SPACING = 20;
|
|
88
181
|
/** Which colour a box earns from its role. One mapping, used by every container. */
|
|
89
182
|
export declare function roleOfNode(node: DiagramNode): ColourRole;
|
|
183
|
+
/**
|
|
184
|
+
* How tall the mask an edge label is drawn over is.
|
|
185
|
+
*
|
|
186
|
+
* Exported because two modules must agree on it: the painter reserves the rectangle when it looks for a clear
|
|
187
|
+
* place, and the writer draws the rectangle that was reserved. A label with two sizes is a label half masked.
|
|
188
|
+
*/
|
|
189
|
+
export declare const LABEL_HEIGHT = 12;
|
|
190
|
+
/** A straight piece of an edge, as the collision test needs it. */
|
|
191
|
+
interface Segment {
|
|
192
|
+
x1: number;
|
|
193
|
+
y1: number;
|
|
194
|
+
x2: number;
|
|
195
|
+
y2: number;
|
|
196
|
+
}
|
|
197
|
+
/** Where a label sits: the point it is centred on, and the rectangle its mask covers. */
|
|
198
|
+
interface LabelSpot {
|
|
199
|
+
x: number;
|
|
200
|
+
y: number;
|
|
201
|
+
width: number;
|
|
202
|
+
height: number;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The point an edge's label is drawn at.
|
|
206
|
+
*
|
|
207
|
+
* A label is not furniture parked beside the line: it names the transition, so it sits on the line - anchored on one
|
|
208
|
+
* of its own straight runs - and the writer masks the stretch of line it covers. Text beside a line can be read as
|
|
209
|
+
* belonging to whatever it happens to sit next to; text that interrupts its own line reads as part of it.
|
|
210
|
+
*
|
|
211
|
+
* The anchor then has to be earned. A mask over a box hides a border, over another label it makes both unreadable,
|
|
212
|
+
* and over a third edge's line it hides that line. So the label walks along a run in small steps, in both directions
|
|
213
|
+
* and bounded so its rectangle stays on that run, and takes the first place that is clear. It tries the longest run
|
|
214
|
+
* first and then the shorter ones: on a dense drawing the longest run can be crowded for its whole length while a
|
|
215
|
+
* shorter run of the same edge is empty, and a label that gives up after one run leaves the drawing with a collision
|
|
216
|
+
* it did not have to accept.
|
|
217
|
+
*
|
|
218
|
+
* When no run offers a clear place, the label takes the place with the fewest collisions - boxes and other labels
|
|
219
|
+
* first, because text over a border hides the border and text over text is unreadable, then other edges, which a mask
|
|
220
|
+
* merely interrupts. It never leaves its own edge: a label moved onto a neighbouring edge is a label that names the
|
|
221
|
+
* wrong thing. The choice is returned with the label, so a crowded drawing can be told from a placement bug.
|
|
222
|
+
*
|
|
223
|
+
* Exported because the choice is pure and worth asserting without a layout: a test can hand it one polyline, one box
|
|
224
|
+
* and one other route, and read which run it took and what it could not clear.
|
|
225
|
+
*/
|
|
226
|
+
export declare function placeLabel(label: string, points: Point[], boxes: Iterable<Box>, placed: LabelSpot[], otherRoutes: Segment[][]): LabelPlacement & LabelSpot;
|
|
227
|
+
/**
|
|
228
|
+
* The boxes a polyline passes through other than the two it terminates at.
|
|
229
|
+
*
|
|
230
|
+
* This is the drawing's one clearance rule, and it is deliberately written once: a channel run, a lane, a trunk
|
|
231
|
+
* branch, a wrapped column's sideways step and the router's fallback all have to obey the same test. A box is skipped
|
|
232
|
+
* only when it is one of the edge's own two ends, so a port that sits inside its own symbol (a cylinder's cap apex)
|
|
233
|
+
* is allowed and a segment that ends inside somebody else's box is not.
|
|
234
|
+
*/
|
|
235
|
+
export declare function boxesCrossedBy(points: Point[], from: Box, to: Box, boxes: Iterable<Box>): Box[];
|
|
90
236
|
export declare function paintWith(diagram: Diagram, overrides?: Partial<Palette>, options?: PaletteOptions): {
|
|
91
237
|
canvas: Canvas;
|
|
92
238
|
colours: Palette;
|
|
93
239
|
};
|
|
94
240
|
export declare function paint(diagram: Diagram, overrides?: Partial<Palette>): Canvas;
|
|
241
|
+
export {};
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where the boxes go.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The drawing is placed by the layered pipeline, in the order the mature implementations run it: cycle breaking turns
|
|
5
|
+
* the graph into a DAG, layering gives every box a row, dummy nodes give every long edge a lane, ordering reduces the
|
|
6
|
+
* crossings, and coordinate assignment decides the x of each lane and box. This module composes those stages; it does
|
|
7
|
+
* not decide any of them itself. Each stage is a pure module next door, so it can be asserted without a canvas.
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Two decompositions sit around the pipeline. A level - the top of the drawing, or the inside of a boundary - is
|
|
10
|
+
* split into weakly connected components, each laid out on its own and then packed onto the page, so a wide
|
|
11
|
+
* subgraph cannot squeeze an unrelated one. A boundary is a node in its parent's layer whose size is whatever its
|
|
12
|
+
* contents need, and its inside is laid out by the same pipeline; that is how nesting is drawn.
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* line.
|
|
14
|
+
* What comes out is geometry only: a box per node, a rectangle per boundary, and - for a long edge - the lane its
|
|
15
|
+
* dummies reserved, so the painter can draw one polyline through it instead of searching around the boxes.
|
|
14
16
|
*/
|
|
15
|
-
import type
|
|
17
|
+
import { type Measured as TextBox } from './text.js';
|
|
18
|
+
import type { Diagram, DiagramNode } from './model.js';
|
|
16
19
|
/** Geometry of one placed box, before anything is drawn. */
|
|
17
20
|
export type Box = {
|
|
18
21
|
x: number;
|
|
@@ -20,20 +23,38 @@ export type Box = {
|
|
|
20
23
|
width: number;
|
|
21
24
|
height: number;
|
|
22
25
|
};
|
|
23
|
-
/**
|
|
26
|
+
/** A point in the drawing's own coordinates. */
|
|
27
|
+
export interface Point {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
}
|
|
31
|
+
/** The lane a long edge's dummies reserved, in layout order: from the layout source down to the layout target. */
|
|
32
|
+
export interface Lane {
|
|
33
|
+
/** One point per gap the edge crosses, at the lane's own x: it keeps its own order, and the painter walks it. */
|
|
34
|
+
points: Point[];
|
|
35
|
+
/** True when the layout runs opposite to the source's arrow, so the painter walks the lane backwards. */
|
|
36
|
+
reversed: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** What a placement returns: every box, every drawn boundary, every reserved lane, and how wide the content is. */
|
|
24
39
|
export interface Placement {
|
|
25
40
|
boxes: Map<string, Box>;
|
|
26
41
|
groupRects: Map<string, Box>;
|
|
27
42
|
width: number;
|
|
43
|
+
lanes: Map<number, Lane>;
|
|
44
|
+
/**
|
|
45
|
+
* The height of the one horizontal run an edge between two adjacent rows needs.
|
|
46
|
+
*
|
|
47
|
+
* A run is measured between two rows of the level that produced it. When a row is a boundary, that strip is the space
|
|
48
|
+
* outside the boundary while the painter draws the boxes inside it, so the run is a proposal rather than a fact: the
|
|
49
|
+
* painter clears it against every box and hands the edge to the router when a leg would cross one.
|
|
50
|
+
*/
|
|
51
|
+
runs: Map<number, number>; /** Edges whose two ends ended up in different wrapped columns: they are read across, not down. */
|
|
52
|
+
across: Set<number>;
|
|
28
53
|
}
|
|
29
|
-
/** Geometry constants
|
|
54
|
+
/** Geometry constants in one place: a container with different proportions changes these, not the rules. */
|
|
30
55
|
export declare const MARGIN = 20;
|
|
31
|
-
/**
|
|
32
|
-
export declare
|
|
33
|
-
/**
|
|
34
|
-
* Place every box of a diagram.
|
|
35
|
-
*
|
|
36
|
-
* The choice of placement is a property of the source, not of the caller: a drawing that declares boundaries is
|
|
37
|
-
* read as containers and gets the tree; one that does not is read as a machine and gets the ranks.
|
|
38
|
-
*/
|
|
56
|
+
/** The text a node will draw, measured from its label - the id is what the source called it, not what it says. */
|
|
57
|
+
export declare function textOf(node: DiagramNode): TextBox;
|
|
39
58
|
export declare function place(diagram: Diagram, top: number): Placement;
|
|
59
|
+
/** The aspect ratio of a placement, for the tests that check a drawing is a page and not a strip. */
|
|
60
|
+
export declare function placementAspect(placement: Placement): number;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orthogonal edge routing around boxes.
|
|
3
|
+
*
|
|
4
|
+
* A straight line between two boxes is right until a third box sits between them; then it is a line drawn through a
|
|
5
|
+
* component, which reads as a connection that is not there. Graphviz solves this by routing along a visibility
|
|
6
|
+
* graph and then fitting splines; ELK, yFiles and mxGraph build a *sparse* grid from the boxes' own edges and run
|
|
7
|
+
* A* over it with a bend penalty. Sparse is the whole trick: a pixel grid is O(pixels²), while the candidate lines
|
|
8
|
+
* are the coordinate of every box edge plus a margin - O(boxes) per axis.
|
|
9
|
+
*
|
|
10
|
+
* So this module does exactly that, in the two steps those routers use:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Candidate lines.** Every box contributes four: one lane just outside each side. Together with the anchor
|
|
13
|
+
* coordinates they form the grid a route may travel on.
|
|
14
|
+
* 2. **A* with bends.** Moving along a candidate line costs its length; changing direction costs extra, because a
|
|
15
|
+
* route that zig-zags to save three points is harder to follow than one that goes around.
|
|
16
|
+
*
|
|
17
|
+
* A route is only computed for an edge that needs it: the cheap straight segment is tried first, and routing is the
|
|
18
|
+
* fallback. That keeps the common case - a box directly above another - at two points and no search.
|
|
19
|
+
*
|
|
20
|
+
* The layered painter mostly does not ask: an edge between two rows and an edge with a lane are both built from the
|
|
21
|
+
* channel stage's answer, because a route searched on its own cannot know which lanes the other edges already hold.
|
|
22
|
+
* What is left for this router is the drawing the layers do not describe - an edge across two wrapped columns, an
|
|
23
|
+
* edge one of whose ends is a boundary - and the fallback when a channel route would meet a box.
|
|
24
|
+
*/
|
|
25
|
+
import type { Point } from './paint.js';
|
|
26
|
+
/** A rectangle an edge may not cross. */
|
|
27
|
+
export interface Obstacle {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}
|
|
33
|
+
/** The four sides a route may leave a box from. */
|
|
34
|
+
export type Side = 'top' | 'bottom' | 'left' | 'right';
|
|
35
|
+
/** The length of the straight run an edge takes before it is allowed to turn. */
|
|
36
|
+
export declare const ROUTE_STUB = 16;
|
|
37
|
+
/** The outward normal of a side: the direction an edge leaves along. */
|
|
38
|
+
export declare function normalOf(side: Side): Point;
|
|
39
|
+
/** A segment crossing a box at all, with a little slack so a route does not shave a corner. */
|
|
40
|
+
export declare function crossesBox(x1: number, y1: number, x2: number, y2: number, box: Obstacle): boolean;
|
|
41
|
+
/** The point on a side of a box, in the middle of that side. */
|
|
42
|
+
export declare function anchor(box: Obstacle, side: Side): Point;
|
|
43
|
+
/**
|
|
44
|
+
* Which side of `from` an edge leaves by, and which side of `to` it arrives at.
|
|
45
|
+
*
|
|
46
|
+
* The side is a property of the drawing, not of the distance between two centres. A layered drawing puts its boxes
|
|
47
|
+
* in horizontal rows, and an edge between two rows approaches vertically: out of the bottom, into the top, and the
|
|
48
|
+
* other way round for an edge drawn against the flow. Only two boxes in the same row are joined sideways, and then
|
|
49
|
+
* the direction of travel decides which side.
|
|
50
|
+
*
|
|
51
|
+
* This replaces the old rule, which compared the two centres' dx and dy and picked the larger axis - so an edge
|
|
52
|
+
* between two rows left through a box's side as soon as the target was far enough sideways, which is the hook a
|
|
53
|
+
* top-down drawing is read as a defect.
|
|
54
|
+
*/
|
|
55
|
+
export declare function approachSides(from: Obstacle, to: Obstacle): {
|
|
56
|
+
exit: Side;
|
|
57
|
+
entry: Side;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Route from one box to another, going around any box in the way.
|
|
61
|
+
*
|
|
62
|
+
* Returns the polyline to draw. When nothing blocks the straight segment, or when every candidate is blocked (a
|
|
63
|
+
* single box that fills the drawing), the straight line is returned and the caller keeps a two-point shape - an
|
|
64
|
+
* unroutable edge is still an edge, and drawing it straight is more honest than dropping it.
|
|
65
|
+
*/
|
|
66
|
+
export interface Ports {
|
|
67
|
+
exit: Point;
|
|
68
|
+
entry: Point;
|
|
69
|
+
exitSide: Side;
|
|
70
|
+
entrySide: Side;
|
|
71
|
+
}
|
|
72
|
+
export declare function routeEdge(from: Obstacle, to: Obstacle, obstacles: Obstacle[], anchors?: Ports): {
|
|
73
|
+
points: Point[];
|
|
74
|
+
routed: boolean;
|
|
75
|
+
};
|
|
76
|
+
/** Drop the points in the middle of a straight run: three collinear points are one segment. */
|
|
77
|
+
export declare function simplify(points: Point[]): Point[];
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Pure, and the output is asserted as text: escaping, geometry, arrowheads, wrapped captions.
|
|
13
13
|
*/
|
|
14
|
-
import type
|
|
14
|
+
import { type Canvas } from './paint.js';
|
|
15
15
|
import type { Palette } from './palette.js';
|
|
16
16
|
export declare function toSvg(canvas: Canvas, colours: Palette, label?: string): {
|
|
17
17
|
markup: string;
|