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,82 +1,85 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: model-everything-diagram
|
|
3
|
-
description: "Use when drawing a diagram from its source text or checking its structure: Mermaid and PlantUML subsets, structure budgets, and containment diffs. Every line it cannot draw is refused by number. The drawing proves layout, not design. Doctrine: the model-everything skill.""
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Model Everything — Drawing
|
|
7
|
-
|
|
8
|
-
A drawing is how a design is read, and a drawing that quietly drops a lifeline is worse than no drawing, because
|
|
9
|
-
it looks complete. This skill covers the drawing half: it renders the subset it can actually draw, refuses the rest
|
|
10
|
-
**by line**, measures the structure against budgets, and compares two drawings by what contains what.
|
|
11
|
-
|
|
12
|
-
The doctrine it inherits — say what a result does and does not prove, read `verdict` and never `ok` — lives in the
|
|
13
|
-
`model-everything` skill. Load that one when the question is whether a design is *correct*; load this one when the
|
|
14
|
-
question is what the picture shows.
|
|
15
|
-
|
|
16
|
-
## When This Skill Applies
|
|
17
|
-
|
|
18
|
-
| The task involves… | What it does |
|
|
19
|
-
|---|---|
|
|
20
|
-
| "draw this mermaid", "render this PlantUML", "make a diagram of this flow" | `diagram_render`: source text to SVG, laid out and painted here, with `out` to write a file and `scheme` for the palette |
|
|
21
|
-
| "is this diagram too dense", "how deep do the packages nest" | `diagram_review`: group width, nesting depth and node count against budgets you set, plus **what it measured** |
|
|
22
|
-
| "what changed between these two diagrams", "did this package move" | `diagram_diff`: boundaries added, removed, renamed or re-parented, and nodes that changed owner |
|
|
23
|
-
| A source in a notation the plugin does not draw | Refused with the line number; `diagram_render` still draws everything else and reports what it skipped |
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- **It does not
|
|
80
|
-
|
|
81
|
-
- **It does not
|
|
82
|
-
|
|
1
|
+
---
|
|
2
|
+
name: model-everything-diagram
|
|
3
|
+
description: "Use when drawing a diagram from its source text or checking its structure: Mermaid and PlantUML subsets, structure budgets, and containment diffs. Every line it cannot draw is refused by number. The drawing proves layout, not design. Doctrine: the model-everything skill.""
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Model Everything — Drawing
|
|
7
|
+
|
|
8
|
+
A drawing is how a design is read, and a drawing that quietly drops a lifeline is worse than no drawing, because
|
|
9
|
+
it looks complete. This skill covers the drawing half: it renders the subset it can actually draw, refuses the rest
|
|
10
|
+
**by line**, measures the structure against budgets, and compares two drawings by what contains what.
|
|
11
|
+
|
|
12
|
+
The doctrine it inherits — say what a result does and does not prove, read `verdict` and never `ok` — lives in the
|
|
13
|
+
`model-everything` skill. Load that one when the question is whether a design is *correct*; load this one when the
|
|
14
|
+
question is what the picture shows.
|
|
15
|
+
|
|
16
|
+
## When This Skill Applies
|
|
17
|
+
|
|
18
|
+
| The task involves… | What it does |
|
|
19
|
+
|---|---|
|
|
20
|
+
| "draw this mermaid", "render this PlantUML", "make a diagram of this flow" | `diagram_render`: source text to SVG, laid out and painted here, with `out` to write a file and `scheme` for the palette |
|
|
21
|
+
| "is this diagram too dense", "how deep do the packages nest" | `diagram_review`: group width, nesting depth and node count against budgets you set, plus **what it measured** |
|
|
22
|
+
| "what changed between these two diagrams", "did this package move" | `diagram_diff`: boundaries added, removed, renamed or re-parented, and nodes that changed owner |
|
|
23
|
+
| A source in a notation the plugin does not draw | Refused with the line number; `diagram_render` still draws everything else and reports what it skipped |
|
|
24
|
+
| An edge whose straight line would cross a component | Routed around it on a sparse grid (candidate lines from each box edge, A* with a bend penalty), so a line never reads as a connection that is not there |
|
|
25
|
+
| A batch of arrows into one component from the same side | Drawn as one **bundle**: a straight run outside the target, one shared entry, and a junction dot where each branch joins. Only ends agreeing on target, entry side and arrow meaning merge, from distinct sources, and three is the least that does - so a decision's two branches stay two lines and two relations between one pair stay apart |
|
|
26
|
+
| A label with a hard line break, or one too long for its box | `<br>`, `<br/>` and a real newline end a line; anything longer than the drawing's line width wraps. The box is sized from the lines it draws. An edge label is drawn on its edge - centred on the middle of the edge's longest straight run, over a mask in the page colour - and slides along that run to clear a box, another label or another edge's line |
|
|
27
|
+
| A diagram already in a file, to link from a document | `diagram_render` with `out`, then link the SVG |
|
|
28
|
+
|
|
29
|
+
## The two notations, and where each one stops
|
|
30
|
+
|
|
31
|
+
| Notation | Drawn | Refused by name |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| Mermaid | `graph`/`flowchart` (TD/TB/LR), node shapes `[..]`, `(..)`, `{..}`, `((..))`, `[(..)]`, `[[..]]`, `<<..>>`; edges `-->`, `---`, `-.->`, `==>`, labels `-->\|text\|` and `-- text -->`; `stateDiagram`/`-v2` with `[*]` start and end markers and `state "text" as id`; `subgraph … end` including nesting | `sequenceDiagram`, `classDiagram`, `erDiagram`, `gantt`, `pie`, `journey`, `mindmap`, `timeline`, `quadrantChart`, `gitGraph`, `C4Context`, and the `click`/`style`/`classDef` directives |
|
|
34
|
+
| PlantUML | `component`, `package`, `node`, `folder`, `cloud`, `database`, `state`, `artifact`, `rectangle` blocks and `[bracketed]` components; `-->`, `--->`, `..>`, either direction, `: label`; `[*] -->` start markers; `skinparam`/`!theme` ignored | `actor`, `participant`, `usecase`, `class`, `object`, `note`, `legend`, and the `alt`/`opt`/`loop`/`par` fragments |
|
|
35
|
+
|
|
36
|
+
A refusal is one line in `refused`, phrased `line N: …`. The drawing you get does not show the refused lines, and
|
|
37
|
+
the tool says so rather than leaving you to notice.
|
|
38
|
+
|
|
39
|
+
## What the structure review reports
|
|
40
|
+
|
|
41
|
+
| Code | Severity | What it means |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `B1_GROUP_TOO_WIDE` | error | A boundary holds more direct children than the budget. A package with twenty boxes has stopped reading as a package. |
|
|
44
|
+
| `B2_TOO_DEEP` | error | Nesting past the budget. Beyond three levels a reader needs the source, not the picture. |
|
|
45
|
+
| `B3_EMPTY_GROUP` | error | A boundary with nothing drawable inside it: it claims a containment the drawing cannot show. |
|
|
46
|
+
| `B4_SINGLE_MEMBER` | warning | A boundary around one thing. Often a layer that was never split, sometimes exactly right. |
|
|
47
|
+
| `B5_TOO_MANY_NODES` | error | The whole drawing past its node budget. |
|
|
48
|
+
| `D0_NO_SOURCE` | error | Nothing to review. |
|
|
49
|
+
|
|
50
|
+
`measured` is printed next to `findings` on purpose: an empty finding list from a source that never parsed would
|
|
51
|
+
otherwise read as a clean bill of health.
|
|
52
|
+
|
|
53
|
+
## What the diff reports
|
|
54
|
+
|
|
55
|
+
| Code | Kind | What changed |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `H1_GROUP_ADDED` | info | A boundary that did not exist, saying what contains it when it is nested |
|
|
58
|
+
| `H2_GROUP_REMOVED` | info | A boundary that is gone |
|
|
59
|
+
| `H3_GROUP_RENAMED` | info | Same id, different text |
|
|
60
|
+
| `H4_GROUP_MOVED` | warning | A boundary that changed parent, both sides named |
|
|
61
|
+
| `H5_NODE_ADDED` / `H6_NODE_REMOVED` | info | A node that appeared or disappeared |
|
|
62
|
+
| `H7_NODE_REGROUPED` | warning | A node that changed which group holds it |
|
|
63
|
+
| `D1_SIDE_UNREADABLE` | error | One side produced no drawing, so there was nothing to compare |
|
|
64
|
+
|
|
65
|
+
A rename is only a rename when the id survived. A vanished id beside a new one is an unrelated removal and
|
|
66
|
+
addition, because the ids are what a caller tracks across versions.
|
|
67
|
+
|
|
68
|
+
**It judges movement, not merit.** Whether a component belongs in its new package is the reader's call; the diff
|
|
69
|
+
reports that the move happened and nothing more.
|
|
70
|
+
|
|
71
|
+
## The whiteboard
|
|
72
|
+
|
|
73
|
+
The right sidebar has a **Diagram** tab: paste a source, get the drawing. It scales to the pane, switches colour
|
|
74
|
+
scheme (`default`, `github`, `high-contrast`, `print`), follows the page's light or dark surface, and folds a group
|
|
75
|
+
into a single box when you click it. It reads no tool result, so it keeps working whatever changes on the tool side.
|
|
76
|
+
|
|
77
|
+
## What this does not do
|
|
78
|
+
|
|
79
|
+
- **It does not verify behaviour.** A drawing of a machine says nothing about whether the machine is safe; that is
|
|
80
|
+
`model_verify` on the model the drawing came from.
|
|
81
|
+
- **It does not read a diagram back into a model.** `uml_parse` does that for the families it owns.
|
|
82
|
+
- **It does not audit dependencies.** `uml_structure_verify` reads a component diagram as a dependency graph and
|
|
83
|
+
judges the edges; `diagram_review` only measures the shape.
|
|
84
|
+
- **It does not draw everything.** A notation outside the subset above is refused by line; a picture that looks
|
|
85
|
+
complete and quietly lost half its arrows is the failure this whole subset exists to avoid.
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel assignment: where the vertical lanes and the horizontal runs between two rows go.
|
|
3
|
+
*
|
|
4
|
+
* The stages before this one decide where the boxes are, and the painter decides what a box looks like. What is left
|
|
5
|
+
* is the space between two rows, and an edge that decides that space alone will collide with the next one: every long
|
|
6
|
+
* edge has to cross a row somewhere, and every edge whose two ends are not vertically aligned needs one horizontal run
|
|
7
|
+
* inside the gap it crosses. Chosen independently, two runs land on one height and read as a junction, and a run
|
|
8
|
+
* sweeps across a vertical another edge is already using.
|
|
9
|
+
*
|
|
10
|
+
* So this stage assigns both, after the boxes have settled and before anything is drawn:
|
|
11
|
+
*
|
|
12
|
+
* - **A lane's x.** A long edge owns one vertical lane through the rows it crosses. The x is a position no box
|
|
13
|
+
* occupies in any of those rows, and lanes keep the order their dummies have in the ordering, so two lanes never
|
|
14
|
+
* share an x inside one gap. Alignment is worth a detour: a lane that lands inside a box's own port span turns a
|
|
15
|
+
* four-bend staircase into a straight drop, so alignment comes before distance from the proposed position.
|
|
16
|
+
* - **A run's y.** The horizontal runs inside one gap are stacked at distinct heights - Graphviz's virtual nodes - so
|
|
17
|
+
* two runs never lie on one line, and a nested run stays near the box it leaves.
|
|
18
|
+
*
|
|
19
|
+
* Pure: rows, chains and coordinates in; lane positions and run heights out.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Chain } from './dummy.js'
|
|
23
|
+
|
|
24
|
+
/** A box, as far as routing is concerned: only a box can move a lane. */
|
|
25
|
+
export interface ChannelBox {
|
|
26
|
+
x: number
|
|
27
|
+
y: number
|
|
28
|
+
width: number
|
|
29
|
+
height: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChannelInput {
|
|
33
|
+
/** The ordered rows, dummies included: a lane keeps its dummy's order. */
|
|
34
|
+
layers: string[][]
|
|
35
|
+
/** Every edge's chain, from the layout source to the layout target. */
|
|
36
|
+
chains: Chain[]
|
|
37
|
+
/** A dummy instance, and the lane it belongs to. */
|
|
38
|
+
chainOf: ReadonlyMap<string, string>
|
|
39
|
+
/** The boxes that block a lane. A dummy has none. */
|
|
40
|
+
boxes: ReadonlyMap<string, ChannelBox>
|
|
41
|
+
/** The top of each row, and how tall it is. */
|
|
42
|
+
layerY: number[]
|
|
43
|
+
layerHeight: number[]
|
|
44
|
+
/** What coordinate assignment proposed for each lane. */
|
|
45
|
+
laneX: ReadonlyMap<string, number>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ChannelOptions {
|
|
49
|
+
/** How far a lane stays from a box in a row it crosses. */
|
|
50
|
+
clearance: number
|
|
51
|
+
/** The narrowest two lanes sharing a gap may be. */
|
|
52
|
+
spacing: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One long edge's lane: the x of its vertical run, and the height of a run in each gap it crosses. */
|
|
56
|
+
export interface LaneChannel {
|
|
57
|
+
x: number
|
|
58
|
+
y: number[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ChannelPlan {
|
|
62
|
+
/** By diagram edge index: the lane a long edge reserved, with one corridor height per gap it crosses. */
|
|
63
|
+
lanes: Map<number, LaneChannel>
|
|
64
|
+
/** By diagram edge index: the height of the one run an edge between two adjacent rows needs. */
|
|
65
|
+
runs: Map<number, number>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** How far a port sits from a box's corner. The painter places ports from this same number. */
|
|
69
|
+
export const PORT_INSET = 10
|
|
70
|
+
|
|
71
|
+
/** An x-interval, open at either end when nothing bounds it. */
|
|
72
|
+
interface Span {
|
|
73
|
+
lo: number
|
|
74
|
+
hi: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The x's a port may sit at on a box's top or bottom side. */
|
|
78
|
+
function spanOf(box: ChannelBox): Span {
|
|
79
|
+
const lo = box.x + PORT_INSET
|
|
80
|
+
const hi = box.x + box.width - PORT_INSET
|
|
81
|
+
return hi > lo ? { lo, hi } : { lo: box.x + box.width / 2, hi: box.x + box.width / 2 }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clamp(value: number, span: Span): number {
|
|
85
|
+
return Math.min(span.hi, Math.max(span.lo, value))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inside(value: number, span: Span): boolean {
|
|
89
|
+
return value >= span.lo - 0.001 && value <= span.hi + 0.001
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The intervals a blocked strip cuts out of the free ones. */
|
|
93
|
+
function cut(free: Span[], blocked: Span): Span[] {
|
|
94
|
+
const out: Span[] = []
|
|
95
|
+
for (const span of free) {
|
|
96
|
+
if (blocked.hi <= span.lo || blocked.lo >= span.hi) {
|
|
97
|
+
out.push(span)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (blocked.lo > span.lo) out.push({ lo: span.lo, hi: blocked.lo })
|
|
101
|
+
if (blocked.hi < span.hi) out.push({ lo: blocked.hi, hi: span.hi })
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The nearest free x at or after `value`: a lane pushed right never lands inside a box. */
|
|
107
|
+
function atOrAfter(value: number, free: Span[]): number {
|
|
108
|
+
let found = value
|
|
109
|
+
for (const span of free) {
|
|
110
|
+
if (span.hi < value) continue
|
|
111
|
+
found = Math.max(value, span.lo)
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
return found
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One long edge, and the space its lane may use. */
|
|
118
|
+
interface Long {
|
|
119
|
+
edge: number
|
|
120
|
+
lane: string
|
|
121
|
+
/** The ids it passes through, dummies included. */
|
|
122
|
+
path: string[]
|
|
123
|
+
/** The rows it starts in and ends in. */
|
|
124
|
+
from: number
|
|
125
|
+
to: number
|
|
126
|
+
/** The x-intervals free of boxes in every row it crosses. */
|
|
127
|
+
free: Span[]
|
|
128
|
+
source: Span
|
|
129
|
+
target: Span
|
|
130
|
+
/** What coordinate assignment proposed, and what was finally chosen. */
|
|
131
|
+
proposal: number
|
|
132
|
+
x: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The x a lane takes: aligned with a box's own port if it can be, then the proposed position, then the detour. */
|
|
136
|
+
function choose(entry: Long): number {
|
|
137
|
+
const candidates: number[] = [entry.proposal, entry.source.lo, entry.source.hi, entry.target.lo, entry.target.hi]
|
|
138
|
+
candidates.push((clamp(entry.proposal, entry.source) + clamp(entry.proposal, entry.target)) / 2)
|
|
139
|
+
// Ordered by what the drawing wants: no detour at either end first, then the position coordinate assignment
|
|
140
|
+
// proposed, then the shorter detour. A free x is never this stage's to invent - the corridor is already there.
|
|
141
|
+
let best = entry.proposal
|
|
142
|
+
let bestBends = Infinity
|
|
143
|
+
let bestFar = Infinity
|
|
144
|
+
let bestReach = Infinity
|
|
145
|
+
for (const span of entry.free) {
|
|
146
|
+
for (const value of candidates) {
|
|
147
|
+
const x = clamp(value, span)
|
|
148
|
+
const bends = (inside(x, entry.source) ? 0 : 1) + (inside(x, entry.target) ? 0 : 1)
|
|
149
|
+
const far = Math.abs(x - entry.proposal)
|
|
150
|
+
const reach = Math.abs(x - clamp(x, entry.source)) + Math.abs(x - clamp(x, entry.target))
|
|
151
|
+
const better =
|
|
152
|
+
bends < bestBends ||
|
|
153
|
+
(bends === bestBends && (far < bestFar - 0.001 || (Math.abs(far - bestFar) <= 0.001 && reach < bestReach)))
|
|
154
|
+
if (!better) continue
|
|
155
|
+
best = x
|
|
156
|
+
bestBends = bends
|
|
157
|
+
bestFar = far
|
|
158
|
+
bestReach = reach
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return best
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The two ports the painter will place for an edge between two adjacent rows.
|
|
166
|
+
*
|
|
167
|
+
* The painter reads the side off the rows and the position off the box the edge points at; this repeats that rule so
|
|
168
|
+
* the run's height can be stacked against the runs it will actually share a gap with.
|
|
169
|
+
*/
|
|
170
|
+
function adjacentPorts(source: ChannelBox, target: ChannelBox): { from: number; to: number } {
|
|
171
|
+
const fromMiddle = source.x + source.width / 2
|
|
172
|
+
const toMiddle = target.x + target.width / 2
|
|
173
|
+
const fromSpan = spanOf(source)
|
|
174
|
+
const toSpan = spanOf(target)
|
|
175
|
+
const chosen = inside(fromMiddle, toSpan) ? fromMiddle : clamp(toMiddle, fromSpan)
|
|
176
|
+
return { from: clamp(chosen, fromSpan), to: clamp(chosen, toSpan) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function assignChannels(input: ChannelInput, options: ChannelOptions): ChannelPlan {
|
|
180
|
+
const { layers, chains, chainOf, boxes, layerY, layerHeight, laneX } = input
|
|
181
|
+
const rowOf = new Map<string, number>()
|
|
182
|
+
layers.forEach((row, depth) => {
|
|
183
|
+
for (const id of row) rowOf.set(id, depth)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// A lane's x: free in every row it crosses, and reachable from both ends.
|
|
187
|
+
const long: Long[] = []
|
|
188
|
+
for (const chain of chains) {
|
|
189
|
+
if (chain.path.length <= 2) continue
|
|
190
|
+
const head = chain.path[0] as string
|
|
191
|
+
const tail = chain.path[chain.path.length - 1] as string
|
|
192
|
+
const from = rowOf.get(head)
|
|
193
|
+
const to = rowOf.get(tail)
|
|
194
|
+
if (from === undefined || to === undefined || to <= from) continue
|
|
195
|
+
const lane = chainOf.get(chain.path[1] as string)
|
|
196
|
+
const source = boxes.get(head)
|
|
197
|
+
const target = boxes.get(tail)
|
|
198
|
+
// A lane into a boundary has no box to arrive at: that edge is the router's.
|
|
199
|
+
if (lane === undefined || source === undefined || target === undefined) continue
|
|
200
|
+
let free: Span[] = [{ lo: -Infinity, hi: Infinity }]
|
|
201
|
+
for (let depth = from + 1; depth < to; depth += 1) {
|
|
202
|
+
for (const id of layers[depth] as string[]) {
|
|
203
|
+
const box = boxes.get(id)
|
|
204
|
+
if (box === undefined) continue
|
|
205
|
+
free = cut(free, { lo: box.x - options.clearance, hi: box.x + box.width + options.clearance })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const centre = (source.x + source.width / 2 + target.x + target.width / 2) / 2
|
|
209
|
+
long.push({
|
|
210
|
+
edge: chain.index,
|
|
211
|
+
lane,
|
|
212
|
+
path: chain.path,
|
|
213
|
+
from,
|
|
214
|
+
to,
|
|
215
|
+
free,
|
|
216
|
+
source: spanOf(source),
|
|
217
|
+
target: spanOf(target),
|
|
218
|
+
proposal: laneX.get(lane) ?? centre,
|
|
219
|
+
x: 0,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
for (const entry of long) entry.x = choose(entry)
|
|
223
|
+
|
|
224
|
+
// The order inside a gap: two lanes never share an x, and the left one is the one the ordering put left.
|
|
225
|
+
const byGap = new Map<number, Array<{ entry: Long; index: number }>>()
|
|
226
|
+
for (const entry of long) {
|
|
227
|
+
for (let gap = entry.from; gap < entry.to; gap += 1) {
|
|
228
|
+
// The dummy in the lower row of the gap; the last gap has none, and its upper row's dummy orders it.
|
|
229
|
+
const row = gap + 1 < entry.to ? gap + 1 : gap
|
|
230
|
+
const id = entry.path[row - entry.from] as string
|
|
231
|
+
const index = (layers[row] as string[]).indexOf(id)
|
|
232
|
+
byGap.set(gap, [...(byGap.get(gap) ?? []), { entry, index }])
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const [, list] of byGap) list.sort((left, right) => left.index - right.index)
|
|
236
|
+
for (let round = 0; round < 4; round += 1) {
|
|
237
|
+
let moved = false
|
|
238
|
+
for (const [, list] of byGap) {
|
|
239
|
+
let previous = -Infinity
|
|
240
|
+
for (const item of list) {
|
|
241
|
+
const x = item.entry.x < previous + options.spacing ? atOrAfter(previous + options.spacing, item.entry.free) : item.entry.x
|
|
242
|
+
if (x !== item.entry.x) {
|
|
243
|
+
item.entry.x = x
|
|
244
|
+
moved = true
|
|
245
|
+
}
|
|
246
|
+
previous = item.entry.x
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!moved) break
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The runs: one per edge end that does not land on its lane, stacked at distinct heights inside the gap.
|
|
253
|
+
const stacks = new Map<number, Array<{ edge: number; gap: number; left: number }>>()
|
|
254
|
+
const want = (gap: number, edge: number, left: number): void => {
|
|
255
|
+
stacks.set(gap, [...(stacks.get(gap) ?? []), { edge, gap, left }])
|
|
256
|
+
}
|
|
257
|
+
for (const entry of long) {
|
|
258
|
+
const head = clamp(entry.x, entry.source)
|
|
259
|
+
const tail = clamp(entry.x, entry.target)
|
|
260
|
+
if (Math.abs(head - entry.x) > 0.5) want(entry.from, entry.edge, Math.min(head, entry.x))
|
|
261
|
+
if (Math.abs(tail - entry.x) > 0.5) want(entry.to - 1, entry.edge, Math.min(tail, entry.x))
|
|
262
|
+
}
|
|
263
|
+
for (const chain of chains) {
|
|
264
|
+
if (chain.path.length > 2) continue
|
|
265
|
+
const head = chain.path[0] as string
|
|
266
|
+
const tail = chain.path[chain.path.length - 1] as string
|
|
267
|
+
const from = rowOf.get(head)
|
|
268
|
+
const to = rowOf.get(tail)
|
|
269
|
+
if (from === undefined || to === undefined || Math.abs(from - to) !== 1) continue
|
|
270
|
+
const source = boxes.get(head)
|
|
271
|
+
const target = boxes.get(tail)
|
|
272
|
+
if (source === undefined || target === undefined) continue
|
|
273
|
+
const ports = adjacentPorts(source, target)
|
|
274
|
+
want(Math.min(from, to), chain.index, Math.min(ports.from, ports.to))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// A gap always has a row below it, so its strip is bounded: one stack per gap, inside that strip.
|
|
278
|
+
const midOf = (gap: number): number => (layerY[gap] + layerHeight[gap] + layerY[gap + 1]) / 2
|
|
279
|
+
const heightOf = new Map<number, Map<number, number>>()
|
|
280
|
+
for (const [gap, list] of stacks) {
|
|
281
|
+
const top = layerY[gap] + layerHeight[gap]
|
|
282
|
+
const bottom = layerY[gap + 1]
|
|
283
|
+
const pad = Math.min(8, Math.max(1, (bottom - top) / 3))
|
|
284
|
+
const lo = top + pad
|
|
285
|
+
const hi = Math.max(lo, bottom - pad)
|
|
286
|
+
// Left to right, so a run that stays near its box sits near the row it left.
|
|
287
|
+
const sorted = [...list].sort((left, right) => left.left - right.left || left.edge - right.edge)
|
|
288
|
+
sorted.forEach((item, index) => {
|
|
289
|
+
const y = sorted.length === 1 ? (lo + hi) / 2 : lo + ((hi - lo) * (index + 1)) / (sorted.length + 1)
|
|
290
|
+
const heights = heightOf.get(item.edge) ?? new Map<number, number>()
|
|
291
|
+
heights.set(item.gap, y)
|
|
292
|
+
heightOf.set(item.edge, heights)
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const lanes = new Map<number, LaneChannel>()
|
|
297
|
+
for (const entry of long) {
|
|
298
|
+
const y: number[] = []
|
|
299
|
+
for (let gap = entry.from; gap < entry.to; gap += 1) y.push(heightOf.get(entry.edge)?.get(gap) ?? midOf(gap))
|
|
300
|
+
lanes.set(entry.edge, { x: entry.x, y })
|
|
301
|
+
}
|
|
302
|
+
const runs = new Map<number, number>()
|
|
303
|
+
for (const [edge, heights] of heightOf) {
|
|
304
|
+
// A long edge's heights belong to its lane; an edge between two rows has the one run it asked for.
|
|
305
|
+
if (lanes.has(edge)) continue
|
|
306
|
+
runs.set(edge, [...heights.values()][0])
|
|
307
|
+
}
|
|
308
|
+
return { lanes, runs }
|
|
309
|
+
}
|