dsh-model-everything 0.5.1 → 0.6.0

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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * An annotation: the reader points at a node in a drawing and says something about it.
3
+ *
4
+ * The note has to reach the session as an ordinary message, because the session log is the only record a
5
+ * replay, a fork or a resumed agent can rebuild from. Two transports can carry it, and they consume the
6
+ * same text: a host command that turns the note into a follow-up (planned; the interface is
7
+ * `annotationMessage`), and the composer, which hands the text to the reader's own input box (works today).
8
+ *
9
+ * Splitting the text from the transport is what keeps the two interchangeable, and what lets this module be
10
+ * tested without a browser.
11
+ */
12
+ /** What the reader said, and where. */
13
+ export interface Annotation {
14
+ /** The node's id, as it appears in the markup the renderer wrote. */
15
+ nodeId: string;
16
+ /** The node's label, when it differs from the id: a reader points at words, not at identifiers. */
17
+ label?: string;
18
+ /** The note itself. Empty is meaningful: the reader marked the node and said nothing. */
19
+ note?: string;
20
+ /** Which drawing, when the reader is looking at more than one. A single source line is enough. */
21
+ caption?: string;
22
+ }
23
+ /**
24
+ * The text that travels to the session: what was marked, and what was said, and nothing invented.
25
+ *
26
+ * A blank note is not dropped - it says the node was marked - because "the reader pointed here" and "the
27
+ * reader pointed here and said this" are different messages, and only one of them survives a silent trim.
28
+ */
29
+ /**
30
+ * The text that travels to the session: what was marked, what was said, and nothing invented.
31
+ *
32
+ * One message for N notes, with a fixed opening so both the reader and the model know what they are looking
33
+ * at: a note arrives without the picture, so the block says what it is about, which drawing, and that every
34
+ * numbered item is a separate answer. The shape follows the annotation plugin this harness already ships -
35
+ * an opening sentence, numbered items, a question line - and it stays English: this is protocol between
36
+ * reader and model, not interface chrome that follows the page's language.
37
+ *
38
+ * Plain lines, not a markdown quote: the reader's bubble is rendered as one text node, so a quote never
39
+ * becomes a quote there - it would only show its own `>` characters. The bubble is folded afterwards.
40
+ * A blank note is not dropped: 'marked, no note' and 'marked with this note' are different messages.
41
+ */
42
+ export declare function annotationBlock(annotations: Annotation[]): string;
43
+ /** One annotation, in the same block shape: a caller with a single note should not get a second format. */
44
+ export declare function annotationMessage(annotation: Annotation): string;
45
+ /** The parts of the page this module touches, so the client bundle needs no DOM lib and a test can fake them. */
46
+ export interface ElementLike {
47
+ value?: unknown;
48
+ querySelector?: (selector: string) => unknown;
49
+ textContent?: unknown;
50
+ focus?: () => void;
51
+ dispatchEvent?: (event: unknown) => boolean;
52
+ getAttribute?: (name: string) => string | null;
53
+ tagName?: unknown;
54
+ }
55
+ export interface DocumentLike {
56
+ querySelector: (selector: string) => unknown;
57
+ querySelectorAll?: (selector: string) => ArrayLike<unknown>;
58
+ }
59
+ export declare function findComposer(doc: DocumentLike | undefined): ElementLike | undefined;
60
+ /**
61
+ * What the page has, for a status line: a probe with no console needs one place to report from.
62
+ *
63
+ * Counts only. A reader's draft is never read into the report.
64
+ */
65
+ export declare function pageCensus(doc: DocumentLike | undefined): string;
66
+ /** What a write did, so the caller can say something true instead of something hopeful. */
67
+ export type Placed = 'filled' | 'no-composer';
68
+ /**
69
+ * Add the text to the composer, above whatever the reader has already typed.
70
+ *
71
+ * Appending rather than replacing is the only behavior that cannot lose work: a reader who was halfway
72
+ * through a message and clicked an annotation should find both halves in the box, not the annotation alone.
73
+ */
74
+ export declare function fillComposer(doc: DocumentLike | undefined, text: string): Placed;
75
+ /** Whether the page can take a Return key for the composer at all, so a button may promise it. */
76
+ export declare function submitComposer(doc: DocumentLike | undefined): boolean;
77
+ /**
78
+ * One note, two actions.
79
+ *
80
+ * Returns the sentence to show the reader: `sent` and `the box was not there` are different outcomes, and a
81
+ * button that reports the wrong one is worse than no button. The text is appended above a half-written draft,
82
+ * never in place of it.
83
+ */
84
+ export declare function pushNote(document: DocumentLike | undefined, picked: {
85
+ id: string;
86
+ } | null, note: string, caption: string, submit: boolean): string;
87
+ /**
88
+ * The session a page is showing, read from its own address.
89
+ *
90
+ * The client services expose no "current session", and a slot's props are not guaranteed to carry one, but the
91
+ * page has to name the session in its URL to survive a reload - so the address is the one source that is always
92
+ * there. Both spellings are accepted: the `session-<uuid>` id this Harness mints and a bare uuid.
93
+ */
94
+ export declare function sessionFromLocation(location: {
95
+ pathname?: string;
96
+ search?: string;
97
+ hash?: string;
98
+ } | undefined): string | undefined;
99
+ /**
100
+ * Put a whole annotation block in the composer, and say what happened.
101
+ *
102
+ * The block is one message for N notes: the model is told to answer each item, and the reader's input box sees
103
+ * the structured text rather than N raw fragments. The same failure vocabulary as a single note.
104
+ */
105
+ export declare function pushBlock(document: DocumentLike | undefined, annotations: Annotation[], caption: string, submit: boolean): string;
106
+ export interface Rect {
107
+ left: number;
108
+ top: number;
109
+ width: number;
110
+ height: number;
111
+ }
112
+ /** Where the reader's input box is, for a widget that has to sit right above it. */
113
+ export declare function composerRect(doc: DocumentLike | undefined): Rect | undefined;
114
+ /**
115
+ * Put the block above what the reader has already typed, and say what happened.
116
+ *
117
+ * Above, because the annotations introduce the message: the reader's own words are the point, and a block
118
+ * appended after them reads as an afterthought. A rich editor has no portable way to insert at the start, so
119
+ * it takes the same path as an empty box - the block becomes the message and the reader's text follows it.
120
+ */
121
+ export declare function prependBlock(document: DocumentLike | undefined, annotations: Annotation[], caption: string): string;
122
+ /**
123
+ * The block without its question line, for a message that has no question.
124
+ *
125
+ * The line is an invitation for the reader's own words; with the box empty it is the last thing the bubble shows
126
+ * and the only thing a chip would be folded away from, so it goes.
127
+ */
128
+ export declare function withoutEmptyQuestion(block: string): string;
@@ -12,9 +12,11 @@
12
12
  * compiled output in `window.__ModuleLoader__.load(...)`, which is the loader contract itself.
13
13
  */
14
14
  import { type Palette, type PaletteMode } from './render/palette.js';
15
+ import { type DocumentLike } from './annotation.js';
15
16
  export interface ReactLike {
16
17
  createElement: (type: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown;
17
- useState?: <T>(initial: T) => [T, (next: T) => void];
18
+ useState?: <T>(initial: T) => [T, (next: T | ((previous: T) => T)) => void];
19
+ useEffect?: (effect: () => void | (() => void), deps?: ReadonlyArray<unknown>) => void;
18
20
  }
19
21
  export interface SlotContext {
20
22
  slots: {
@@ -29,7 +31,6 @@ export interface PluginShape {
29
31
  /** The whiteboard mount, with its scheduler injectable. `apply` calls this with the default. */
30
32
  mount: (ctx: SlotContext, options?: MountOptions) => unknown;
31
33
  }
32
- /** A settled tool call, as the host hands it to a view. */
33
34
  export interface ToolCallProps {
34
35
  phase?: string;
35
36
  toolName?: string;
@@ -53,12 +54,93 @@ export declare const WHITEBOARD_ID = "model-everything-diagram";
53
54
  /** What the whiteboard opens with: a diagram the reader can edit rather than an empty box. */
54
55
  export declare const WHITEBOARD_SAMPLE: string;
55
56
  /**
56
- * The view for one tool name.
57
+ * The card for a drawing: the source the call carried, drawn by the renderer that wrote the file.
57
58
  *
58
- * The drawing is emitted as markup, so React receives it through `dangerouslySetInnerHTML`. That is safe
59
- * only because the writer escapes every value that came from a report or a model, which the render suite
60
- * pins with an injection case.
59
+ * Drawing it here rather than replaying the SVG means the card and the file are one picture from one
60
+ * engine, and it costs the result nothing. Lines the renderer refused are listed under the drawing: a
61
+ * card that hid them would show a picture that is missing something, which is the failure the tool
62
+ * exists to prevent.
61
63
  */
64
+ /** A create-element function with this package's calling convention, so a shared builder fits both seats. */
65
+ type H = (type: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown;
66
+ /** What an annotation editor shows, shared by the two surfaces that offer one. */
67
+ export interface AnnotationState {
68
+ picked: {
69
+ id: string;
70
+ } | null;
71
+ note: string;
72
+ pending: Array<{
73
+ nodeId: string;
74
+ note: string;
75
+ }>;
76
+ status: string;
77
+ /** Whether the pending list is expanded under the count. */
78
+ open?: boolean;
79
+ /** Where the editor sits, in the drawing's own coordinates: beside the node the reader picked. */
80
+ anchor?: {
81
+ left: number;
82
+ top: number;
83
+ };
84
+ /** The correction the dock applied after measuring where the chip actually landed. */
85
+ dockFix?: {
86
+ dx: number;
87
+ dy: number;
88
+ };
89
+ }
90
+ export interface AnnotationActions {
91
+ onNote: (value: string) => void;
92
+ onAdd: () => void;
93
+ onAddAndSend: () => void;
94
+ onSend: () => void;
95
+ onClear: () => void;
96
+ onCancel: () => void;
97
+ onToggle: () => void;
98
+ onRemove: (index: number) => void;
99
+ }
100
+ /**
101
+ * The annotation surface: a floating editor over the drawing, and a count of what is waiting.
102
+ *
103
+ * Floating rather than inline, because the reader is looking at a picture and the editor must not move it.
104
+ * The notes stay in a list until one message is composed from them: the reader's input box is theirs, and N raw
105
+ * fragments in it would be exactly the mess this feature is meant to avoid. Colours come from the host's tokens,
106
+ * so light and dark themes need no separate code.
107
+ */
108
+ export declare function annotationPanel(h: H, state: AnnotationState, actions: AnnotationActions, options?: {
109
+ floating?: boolean;
110
+ document?: DocumentLike;
111
+ dockFix?: {
112
+ dx: number;
113
+ dy: number;
114
+ };
115
+ dockChip?: boolean;
116
+ }): unknown;
117
+ /** Remember what would leave with the next message, and start watching for that message. */
118
+ export declare function setPendingBlock(text: string): void;
119
+ /**
120
+ * The dock, as a layer on `document.body`.
121
+ *
122
+ * A `position: fixed` node still positions against the nearest transformed ancestor, and the composer sits
123
+ * inside one - which is why every coordinate this plugin computed landed inside the box. The shipped
124
+ * annotation plugin sidesteps it the same way: its chip is a body-level layer, so fixed means the viewport.
125
+ * Size and place come from the composer card's own rectangle, measured after every layout change.
126
+ */
127
+ interface DockHandlers {
128
+ onToggle: () => void;
129
+ onSend: () => void;
130
+ onRemove: (index: number) => void;
131
+ }
132
+ /** Keep the docked count in step with the pending list. Called from a render path, so it cannot throw. */
133
+ export declare function updateDockLayer(items: Array<{
134
+ nodeId: string;
135
+ note: string;
136
+ }>, open: boolean, handlers: DockHandlers): void;
137
+ /**
138
+ * Tell this module when a message leaves, by watching the one signal the host gives: the draft empties.
139
+ *
140
+ * A note that was committed is in the draft, so after the send there is nothing left to keep - and a chip still
141
+ * counting a note that has already gone out is the plugin lying about its own state.
142
+ */
143
+ export declare function onMessageSent(listener: () => void): () => void;
62
144
  export declare function viewFor(React: ReactLike, overrides?: Partial<Palette>): (props: ToolCallProps) => unknown;
63
145
  /**
64
146
  * The whiteboard: paste a diagram, get a drawing, zoom it.
@@ -81,3 +163,4 @@ export interface MountOptions {
81
163
  */
82
164
  export declare function mountWhiteboard(React: ReactLike, ctx: SlotContext, options?: MountOptions): unknown;
83
165
  export default function factory(require: (name: string) => unknown): PluginShape;
166
+ export {};
@@ -31,4 +31,11 @@ export interface ComponentContext {
31
31
  };
32
32
  }
33
33
  export declare function apply(ctx: ComponentContext, config: Config): void;
34
+ /**
35
+ * Offer an annotation as a session command, so a page can send one without a composer.
36
+ *
37
+ * Optional on purpose, and failure-tolerant on purpose: a host without the commands service keeps the
38
+ * composer path, and a registration that throws must not take the drawing tools down with it.
39
+ */
40
+ export declare function registerAnnotateCommand(ctx: unknown): void;
34
41
  export declare const inject: string[];
@@ -10,13 +10,15 @@
10
10
  *
11
11
  * Why it is a separate row. The plugin's doctrine is that a capability is discoverable
12
12
  * through its own tool description or skill text. Injection departs from that, so it is
13
- * opt-in and mounted as its own component: install it, disable it, or never enable it,
14
- * without touching the tools or the skills. `enabled` is a live switch, so the Web Plugins
15
- * page flips it inside a running host.
13
+ * mounted as its own component: the bundle's patch enables it, and the row can be turned off
14
+ * or removed without touching the tools or the skills. `enabled` is a live switch, so the Web
15
+ * Plugins page flips it inside a running host.
16
16
  *
17
- * Why it is off by default. It costs tokens in every session that turns it on, to fix a
18
- * problem text-only routing was measured not to have once the session engages. A deployment
19
- * that wants it says so; the default pays nothing.
17
+ * Why the bundle turns it on. Measured: a session that never decides to load a skill is never
18
+ * routed, and the catalog line is cut at 500 characters, so a domain the request names can be
19
+ * invisible at the moment of choosing. The cost is a fixed message in every session, which is
20
+ * why the switch is live and documented: turn it off where the bundle's default is wrong. The
21
+ * component's own `enabled` script default is off, so a bare `app.add(gate)` injects nothing.
20
22
  *
21
23
  * What it does not do. It routes; it checks nothing. It cannot make a skill load, and it
22
24
  * does not restate the doctrine - the skill bodies carry that, and duplicating them here
@@ -11,20 +11,31 @@
11
11
  * route, not picked by comparing the boxes' centres: a route between two rows arrives vertically, so the endpoint
12
12
  * is snapped onto the top or bottom perimeter in the direction the route approaches. Where along that side it sits
13
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.
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.
15
17
  *
16
18
  * A symbol with discrete points gives each of its edges a point of its own: the point whose outward normal faces the
17
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
18
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
19
21
  * points at all. Sharing is the fallback, not the rule: two labelled branches of a decision leave by two vertices.
20
22
  *
21
- * An edge's name is drawn on the edge, not beside it: it is anchored at the midpoint of the edge's longest straight
22
- * run, over a rectangle in the page colour that masks the line behind the text, and it slides along that run when
23
- * something is in the way. Labels are placed after every edge is drawn, because what a label has to avoid is the
24
- * whole picture.
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.
25
35
  *
26
36
  * Pure: a diagram in, shapes out, every decision assertable.
27
37
  */
38
+ import { type Box } from './place.js';
28
39
  import { type ColourRole, type Palette, type PaletteOptions } from './palette.js';
29
40
  import { type Diagram, type DiagramNode, type EdgeArrow, type NodeShape } from './model.js';
30
41
  export interface Point {
@@ -33,6 +44,8 @@ export interface Point {
33
44
  }
34
45
  export interface BoxShape {
35
46
  kind: 'box';
47
+ /** Which node this box is. It travels into the markup, so a reader can target one node by delegation. */
48
+ id: string;
36
49
  x: number;
37
50
  y: number;
38
51
  width: number;
@@ -45,6 +58,22 @@ export interface BoxShape {
45
58
  colour: ColourRole;
46
59
  initial: boolean;
47
60
  }
61
+ /**
62
+ * Why a label sits where it does.
63
+ *
64
+ * A clear label records `collisions: 0` and the run it chose. A label that could clear nothing records the collisions
65
+ * it could not avoid, so a reader of the drawing (or a test) can tell a crowded drawing from a placement bug. The
66
+ * label never leaves its own edge to earn any of these numbers.
67
+ */
68
+ export interface LabelPlacement {
69
+ /** The straight run it sits on, 0 being the edge's longest. */
70
+ run: number;
71
+ /** Everything its mask overlaps: the other labels, boxes and edges it would have had to move off its line to clear. */
72
+ collisions: number;
73
+ boxes: number;
74
+ labels: number;
75
+ edges: number;
76
+ }
48
77
  export interface LinkShape {
49
78
  kind: 'link';
50
79
  x1: number;
@@ -60,6 +89,8 @@ export interface LinkShape {
60
89
  dashed?: boolean;
61
90
  /** The arrow the source edge stated. Undefined means the plain arrow every edge has been drawn with. */
62
91
  arrow?: EdgeArrow;
92
+ /** Why the label sits where it does. Undefined on an edge with no label. */
93
+ labelPlacement?: LabelPlacement;
63
94
  }
64
95
  export interface PathShape {
65
96
  kind: 'path';
@@ -71,6 +102,22 @@ export interface PathShape {
71
102
  colour: ColourRole;
72
103
  dashed?: boolean;
73
104
  arrow?: EdgeArrow;
105
+ /** Why the label sits where it does. Undefined on an edge with no label. */
106
+ labelPlacement?: LabelPlacement;
107
+ }
108
+ /**
109
+ * A junction dot: the place one branch of a merged bundle meets the bundle's trunk.
110
+ *
111
+ * The dot is not decoration. Two lines that meet at a right angle read as a crossing unless one of them is marked as
112
+ * joining the other, and a bundle whose branches are not marked reads as a set of broken lines rather than a merge.
113
+ */
114
+ export interface JunctionShape {
115
+ kind: 'junction';
116
+ x: number;
117
+ y: number;
118
+ colour: ColourRole;
119
+ /** The trunk the dot belongs to: one dot per branch, one trunk id per bundle. */
120
+ trunk: number;
74
121
  }
75
122
  /** A boundary around several states: a package, a layer, a subgraph. */
76
123
  export interface GroupShape {
@@ -98,11 +145,33 @@ export interface TitleShape {
98
145
  text: string;
99
146
  colour: ColourRole;
100
147
  }
101
- export type Shape = GroupShape | BoxShape | LinkShape | PathShape | CaptionShape | TitleShape;
148
+ export type Shape = GroupShape | BoxShape | LinkShape | PathShape | JunctionShape | CaptionShape | TitleShape;
149
+ /** One segment of a drawn route that could not be kept out of a box it does not terminate at. */
150
+ export interface RoutingFailure {
151
+ /** The edge's index in the diagram. */
152
+ edge: number;
153
+ from: string;
154
+ to: string;
155
+ /** The box the segment passes through. */
156
+ box: string;
157
+ x1: number;
158
+ y1: number;
159
+ x2: number;
160
+ y2: number;
161
+ }
102
162
  export interface Canvas {
103
163
  width: number;
104
164
  height: number;
105
165
  shapes: Shape[];
166
+ /**
167
+ * Every place a drawn segment still passes through a box it does not terminate at, after every route was checked.
168
+ *
169
+ * A drawing should carry none. The field exists for the case where no stage and no router could find a way around:
170
+ * a route that cannot be drawn honestly is reported here rather than silently drawn through a box.
171
+ */
172
+ routingFailures: RoutingFailure[];
173
+ /** How many edge labels could not be placed clear of every box, label and other edge. */
174
+ labelCollisions: number;
106
175
  }
107
176
  /**
108
177
  * How far apart two edges that join the same pair are drawn.
@@ -120,8 +189,55 @@ export declare function roleOfNode(node: DiagramNode): ColourRole;
120
189
  * place, and the writer draws the rectangle that was reserved. A label with two sizes is a label half masked.
121
190
  */
122
191
  export declare const LABEL_HEIGHT = 12;
192
+ /** A straight piece of an edge, as the collision test needs it. */
193
+ interface Segment {
194
+ x1: number;
195
+ y1: number;
196
+ x2: number;
197
+ y2: number;
198
+ }
199
+ /** Where a label sits: the point it is centred on, and the rectangle its mask covers. */
200
+ interface LabelSpot {
201
+ x: number;
202
+ y: number;
203
+ width: number;
204
+ height: number;
205
+ }
206
+ /**
207
+ * The point an edge's label is drawn at.
208
+ *
209
+ * A label is not furniture parked beside the line: it names the transition, so it sits on the line - anchored on one
210
+ * of its own straight runs - and the writer masks the stretch of line it covers. Text beside a line can be read as
211
+ * belonging to whatever it happens to sit next to; text that interrupts its own line reads as part of it.
212
+ *
213
+ * The anchor then has to be earned. A mask over a box hides a border, over another label it makes both unreadable,
214
+ * and over a third edge's line it hides that line. So the label walks along a run in small steps, in both directions
215
+ * and bounded so its rectangle stays on that run, and takes the first place that is clear. It tries the longest run
216
+ * first and then the shorter ones: on a dense drawing the longest run can be crowded for its whole length while a
217
+ * shorter run of the same edge is empty, and a label that gives up after one run leaves the drawing with a collision
218
+ * it did not have to accept.
219
+ *
220
+ * When no run offers a clear place, the label takes the place with the fewest collisions - boxes and other labels
221
+ * first, because text over a border hides the border and text over text is unreadable, then other edges, which a mask
222
+ * merely interrupts. It never leaves its own edge: a label moved onto a neighbouring edge is a label that names the
223
+ * wrong thing. The choice is returned with the label, so a crowded drawing can be told from a placement bug.
224
+ *
225
+ * Exported because the choice is pure and worth asserting without a layout: a test can hand it one polyline, one box
226
+ * and one other route, and read which run it took and what it could not clear.
227
+ */
228
+ export declare function placeLabel(label: string, points: Point[], boxes: Iterable<Box>, placed: LabelSpot[], otherRoutes: Segment[][]): LabelPlacement & LabelSpot;
229
+ /**
230
+ * The boxes a polyline passes through other than the two it terminates at.
231
+ *
232
+ * This is the drawing's one clearance rule, and it is deliberately written once: a channel run, a lane, a trunk
233
+ * branch, a wrapped column's sideways step and the router's fallback all have to obey the same test. A box is skipped
234
+ * 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)
235
+ * is allowed and a segment that ends inside somebody else's box is not.
236
+ */
237
+ export declare function boxesCrossedBy(points: Point[], from: Box, to: Box, boxes: Iterable<Box>): Box[];
123
238
  export declare function paintWith(diagram: Diagram, overrides?: Partial<Palette>, options?: PaletteOptions): {
124
239
  canvas: Canvas;
125
240
  colours: Palette;
126
241
  };
127
242
  export declare function paint(diagram: Diagram, overrides?: Partial<Palette>): Canvas;
243
+ export {};
@@ -41,9 +41,14 @@ export interface Placement {
41
41
  groupRects: Map<string, Box>;
42
42
  width: number;
43
43
  lanes: Map<number, Lane>;
44
- /** The height of the one horizontal run an edge between two adjacent rows needs. */
45
- runs: Map<number, number>;
46
- /** Edges whose two ends ended up in different wrapped columns: they are read across, not down. */
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. */
47
52
  across: Set<number>;
48
53
  }
49
54
  /** Geometry constants in one place: a container with different proportions changes these, not the rules. */
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Fan-in trunks: a batch of arrows into one box from one side, drawn as one bundle.
3
+ *
4
+ * A node that four relations point at is drawn as four ports along its side and four lines converging on it. Each
5
+ * line is right on its own, and the picture is wrong: the bundle reads as a comb, and a reader counts ports to count
6
+ * arrows. The mature layered implementations solve this by merging the edges - ELK's *Merge Edges* with junction
7
+ * points, Graphviz's `concentrate` - and the shape they produce is one straight run outside the node, with a branch
8
+ * joining it wherever a separate edge arrives, and one shared stub into the node.
9
+ *
10
+ * This module decides only which ends may merge, where the shared run sits and which of the branches becomes the
11
+ * shared entry. It is pure geometry over boxes and ports, so the rules can be asserted without a canvas; the painter
12
+ * draws the answer.
13
+ *
14
+ * The rules, and why each one exists:
15
+ *
16
+ * - **One target, one entry side.** Edges that arrive from different sides are travelling in different directions,
17
+ * and a trunk that changed axis would be two trunks.
18
+ * - **The exit faces the trunk.** A branch arrives at the trunk along the direction its own port leaves by, so the
19
+ * trunk is outside the source as well as outside the target. An end that does not face the trunk is not merged: it
20
+ * would have to turn before it got there, which is the bend the merge was supposed to save.
21
+ * - **Distinct sources, three or more.** Two merged edges read as one thick line. A pair of relations about one pair
22
+ * has its own, older rule - they stay visibly apart - and that rule is the distinct-source rule here: two
23
+ * statements about one pair share their source, so they can never both be a branch of one trunk.
24
+ * - **One arrow semantics.** A composition and an association are different statements about the relation; drawn on
25
+ * one trunk they would be read as one statement. Ends that disagree on their arrow are not merged with each other.
26
+ * - **Clear of every box.** A trunk that crosses a box is the same defect as a route that does, and it is worse here
27
+ * because it is drawn once for the whole bundle.
28
+ * - **One trunk per line.** Two groups whose trunks would be drawn on one line would read as one bundle between two
29
+ * unrelated targets, so the later group is refused.
30
+ *
31
+ * The shared entry is the middle of the target's side. A bundle enters the target through one port, and a side that
32
+ * carries one port is centred on it, so the trunk runs to the middle and every branch joins it there: the branches
33
+ * that are not in the middle bend, which is the price of one entry that reads as centred rather than one that leans
34
+ * towards whichever branch happened to arrive straightest.
35
+ *
36
+ * Merging forces every branch but one to bend twice, so an unmerged drawing of `k` straight branches costs
37
+ * `2 * (n - k)` bends and a merged one costs `2 * (n - 1)`; the branch nearest the middle is the lead because it is
38
+ * the only one that can still reach the shared port without a horizontal leg, and on a target whose middle happens
39
+ * to fall on a branch's own line that branch is free, as before.
40
+ *
41
+ * What this module does not decide: the colour of a branch, the text of its label, and how a branch's own route
42
+ * reaches the trunk. Those belong to the painter.
43
+ */
44
+ import { type Side } from './route.js';
45
+ import type { Box } from './place.js';
46
+ import type { EdgeArrow } from './model.js';
47
+ /** One edge end, as the trunk planner reads it. */
48
+ export interface TrunkEnd {
49
+ /** The edge's index in the diagram, so the painter can key its answer back to the edge. */
50
+ index: number;
51
+ from: string;
52
+ to: string;
53
+ /** The side the edge's port leaves by, and the side it arrives at, as the port stage decided them. */
54
+ exit: Side;
55
+ entry: Side;
56
+ /** The arrow the notation stated. Ends merge only when they agree on it; two unset arrows agree. */
57
+ arrow?: EdgeArrow;
58
+ /** The height of the run the channel stage gave the edge. An edge without one is not drawn by a run. */
59
+ runY?: number;
60
+ }
61
+ /** A port in the drawing's coordinates. Structural, so this module needs no import from the painter. */
62
+ export interface TrunkPoint {
63
+ x: number;
64
+ y: number;
65
+ }
66
+ export interface TrunkInput {
67
+ ends: TrunkEnd[];
68
+ /** The box of a node, and nothing else: a boundary is not a trunk target, because an arrow points past it. */
69
+ boxOf: (id: string) => Box | undefined;
70
+ /** Every box the trunk and its branches must keep clear of. A boundary is not an obstacle, as on any route. */
71
+ boxes: Iterable<Box>;
72
+ /** Where an edge's own exit port sits, when it has one. The branch joins the trunk at that port's coordinate. */
73
+ exitPortOf: (index: number) => TrunkPoint | undefined;
74
+ }
75
+ /** One accepted bundle: what it merges, where its run sits, and which branch the target is entered by. */
76
+ export interface TrunkGroup {
77
+ /** Position in `groups`, which is also the id a junction dot carries. */
78
+ id: number;
79
+ target: string;
80
+ side: Side;
81
+ /** The coordinate of the trunk line: a y when the trunk runs across, an x when it runs down. */
82
+ at: number;
83
+ /** The coordinate of the one port the bundle enters by: the middle of the target's side. */
84
+ entryAlong: number;
85
+ /** The two ends of the run, along the axis it moves on. A branch joins it between them. */
86
+ from: number;
87
+ to: number;
88
+ /** The member whose own exit is nearest the shared entry, and therefore the only branch that may still be a straight drop. */
89
+ lead: number;
90
+ /** The edges in the bundle, in the order the source wrote them. */
91
+ members: number[];
92
+ }
93
+ export interface TrunkPlan {
94
+ groups: TrunkGroup[];
95
+ /** By edge index: the id of the group it joined. An edge not in here travels alone. */
96
+ groupOf: Map<number, number>;
97
+ }
98
+ /**
99
+ * Decide which ends merge, and where each accepted trunk runs.
100
+ *
101
+ * Ends that cannot merge are simply absent from the plan; the painter draws them as it always has. Groups are
102
+ * ordered by the first member the source wrote, so the answer does not depend on map iteration order.
103
+ */
104
+ export declare function planTrunks(input: TrunkInput): TrunkPlan;