@weasel-js/diagram 1.10.0 → 1.11.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.
- package/README.md +470 -19
- package/dist/{layout.entry-CIdRpG2i.d.ts → body-2lC7IiIA.d.ts} +248 -261
- package/dist/chunk-C4NAI6NV.js +2041 -0
- package/dist/chunk-C4NAI6NV.js.map +1 -0
- package/dist/chunk-NUMMW5BI.js +2865 -0
- package/dist/chunk-NUMMW5BI.js.map +1 -0
- package/dist/effect.d.ts +417 -0
- package/dist/effect.js +1283 -0
- package/dist/effect.js.map +1 -0
- package/dist/fromData-GKqRgPFs.d.ts +379 -0
- package/dist/index.d.ts +217 -297
- package/dist/index.js +2 -1299
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +2 -1
- package/dist/layout.entry-BoWFcR4G.d.ts +285 -0
- package/dist/layout.js +1 -1
- package/package.json +10 -6
- package/dist/chunk-BWQJYEA3.js +0 -1240
- package/dist/chunk-BWQJYEA3.js.map +0 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ flowchart box.
|
|
|
13
13
|
|
|
14
14
|
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
15
15
|
scene-graph canvas kit for React. See the
|
|
16
|
-
[API reference](https://orochi235.github.io/weasel/
|
|
16
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/diagram).
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -49,6 +49,11 @@ const { specs } = buildBody<Data, 'main', Pose>(
|
|
|
49
49
|
The rows measure a floor and the authored pose is grown to clear it, never
|
|
50
50
|
shrunk — so resize, align, distribute, snapping and undo need no special case.
|
|
51
51
|
|
|
52
|
+
A perimeter port can carry a `label`, or be named in `portLabels` (on a
|
|
53
|
+
`BodySpec` or a `DataNode`) by port id, which labels the default compass ports
|
|
54
|
+
without redeclaring them. The label is drawn outside the node beside the port's
|
|
55
|
+
wire; layout lays the node out with room for it, and a group's box takes it in.
|
|
56
|
+
|
|
52
57
|
### Making ports grabbable
|
|
53
58
|
|
|
54
59
|
`diagramPorts` returns the two halves of the connect gesture. Attach the layer
|
|
@@ -70,12 +75,18 @@ Dragging one port onto another authors an edge. Which pairs may be joined is
|
|
|
70
75
|
`canConnect`, which defaults to "a port may not join itself, and two ports that
|
|
71
76
|
both declare a `type` must declare the same one".
|
|
72
77
|
|
|
78
|
+
A port may name other places it can sit, in `sites`. An edge meeting it takes
|
|
79
|
+
whichever place faces the edge's next point — its first waypoint, or the other
|
|
80
|
+
node — so a decision's second exit can leave by its top or its bottom,
|
|
81
|
+
wherever its wire goes. `portSitesOf` resolves every place; anything else that
|
|
82
|
+
shows a port, the connect affordance included, shows it at `at`.
|
|
83
|
+
|
|
73
84
|
### Laying it out
|
|
74
85
|
|
|
75
|
-
`layered`, `tree` and `
|
|
76
|
-
the new top-left for every node that **moves** — a node already
|
|
77
|
-
the layout wants it is absent, so re-running a layout on an
|
|
78
|
-
writes nothing at all.
|
|
86
|
+
`layered`, `tree`, `force` and `chain` are plain functions of the graph. Each
|
|
87
|
+
hands back the new top-left for every node that **moves** — a node already
|
|
88
|
+
standing where the layout wants it is absent, so re-running a layout on an
|
|
89
|
+
unchanged diagram writes nothing at all.
|
|
79
90
|
|
|
80
91
|
```tsx
|
|
81
92
|
useAction(useMemo(() => createLayoutAction<Pose>({
|
|
@@ -89,9 +100,73 @@ move as one undo entry. Three rules keep a re-layout from scrambling a diagram
|
|
|
89
100
|
someone has arranged: no RNG anywhere, within-rank order seeded from where the
|
|
90
101
|
nodes already sit, and a node carrying `pinned: true` that nothing moves.
|
|
91
102
|
|
|
103
|
+
The first pinned node anchors the layout. Under `layered`, every other pin
|
|
104
|
+
keeps its place across its rank, with the rest of the rank moved clear of it,
|
|
105
|
+
past it if there is no room beside it. A pin's rank band slides or deepens to
|
|
106
|
+
cover it, the gaps toward other pinned ranks shrink to make room, and a group's
|
|
107
|
+
column moves or widens to take in its pinned members, never over a node outside
|
|
108
|
+
the group. A pin is left out only when no arrangement fits: its band would come
|
|
109
|
+
nearer another pinned rank's than the ranks between them allow with every gap
|
|
110
|
+
at a quarter of `rankGap` (plus group insets), or a group's box would cover a
|
|
111
|
+
pin outside the group. In practice that is a pin dragged level with, or past,
|
|
112
|
+
the rank beside it. Such a pin stays put without the layout being built around
|
|
113
|
+
it; `layeredPlan` names it in `unheld`.
|
|
114
|
+
|
|
92
115
|
`force` is the exception to the second half of that: it is an iterative
|
|
93
116
|
relaxation seeded from the current positions, so re-running it keeps relaxing.
|
|
94
117
|
|
|
118
|
+
`layered` turns around any edge that closes a cycle, then gives each node a
|
|
119
|
+
rank that keeps the total edge length as short as it can (`ranksByLength`,
|
|
120
|
+
network simplex), so a node with no inputs sits beside what it feeds rather
|
|
121
|
+
than in the first rank. It lines each node up with its neighbors across the
|
|
122
|
+
ranks (Brandes–Köpf), lining up the two ports an edge joins rather than the two
|
|
123
|
+
centers. An edge that spans ranks stands in each rank it crosses, so placement
|
|
124
|
+
leaves it room, and an edge whose `GraphEdge.label` gives its label's size gets
|
|
125
|
+
a rank of its own between its ends, with room beside its wire as big as the
|
|
126
|
+
label. Under `order: 'seeded'` an edge keeps its place in each rank the way a
|
|
127
|
+
node does, from where its route crosses that rank now. Under
|
|
128
|
+
`order: 'barycenter'` it also reorders each rank to cut crossings, reading each
|
|
129
|
+
edge at the port it joins, where `portsOf` puts that port on the node's
|
|
130
|
+
outline, so two wires into one node arrive in the order of its ports. A port
|
|
131
|
+
with several places is read at the one nearest where the wire's other end
|
|
132
|
+
falls among the node's wires on that side. `layeredPlan(graph, opts)`, also reachable as `layered.plan`,
|
|
133
|
+
answers with the moves plus each node's rank, each spanning edge's waypoints
|
|
134
|
+
and where among them each label's room is, for drawing those edges where their
|
|
135
|
+
room was left. `ranksOf` is the plain longest-path ranking, which `chain` uses.
|
|
136
|
+
|
|
137
|
+
On a live scene the layout action and a live `layered` run write those routes
|
|
138
|
+
too, in the same undo entry as the moves: each long edge gets its waypoints,
|
|
139
|
+
and its label, when it sits at the default middle, moves into the room left
|
|
140
|
+
beside them. Under `layered` each edge's compass ports are named for the way it
|
|
141
|
+
runs between ranks, and an end records the port layout named as `laidOut`, the
|
|
142
|
+
way a route does; layout names it again on each run while the end names no
|
|
143
|
+
port or still names that one. An edge records the waypoints layout wrote as `laidOut`, and
|
|
144
|
+
layout rewrites a route only while the edge's waypoints still match them; once
|
|
145
|
+
someone moves a bend, the route is theirs and layout leaves it and its label
|
|
146
|
+
alone, and keeps no room for it in the ranks it crosses. A layout that routes
|
|
147
|
+
nothing, `tree` or `force`, takes layered's routes back off, and anything on a
|
|
148
|
+
locked layer stays as it is. A live run writes its routes only when it arrives
|
|
149
|
+
with nothing held along the way, since a drag or a `stop` leaves the nodes
|
|
150
|
+
somewhere the routes were not laid out for. While it eases, each route it will
|
|
151
|
+
write eases with the nodes, shown as the edge's path on the override channel. A layout of your own hands routes
|
|
152
|
+
back the same way, by giving its `LayoutFn` a `plan`, which `diagramScene`
|
|
153
|
+
reads too.
|
|
154
|
+
|
|
155
|
+
`chain` stands one run of nodes on a straight line and hangs everything else
|
|
156
|
+
beside the chain node it is nearest to, counting edges in either direction. It
|
|
157
|
+
is picked like the others, by name: `layout: 'chain'` in `diagramScene`,
|
|
158
|
+
`layout('chain')` on a `DiagramView`, or `algorithm: 'chain'` in
|
|
159
|
+
`createLayoutAction`. The `chain` layout option lists the ids on the line, in
|
|
160
|
+
order; unset, or naming no node of the graph, it is the longest path through
|
|
161
|
+
the graph. `chainSide` is
|
|
162
|
+
`'after'` (default) or `'before'`: which side of the line the rest hangs on.
|
|
163
|
+
Each chain node owns a column as wide as the widest thing hung on it, so
|
|
164
|
+
neighbors never share space, and the hung nodes stand in the order of the ports
|
|
165
|
+
they connect to. An edge between a chain node and one hung beside it, both ends
|
|
166
|
+
naming ports that face each other, runs on a track of its own in the gap
|
|
167
|
+
between them, so a fan of them never crosses; the gap grows to keep tracks 12
|
|
168
|
+
apart. Groups are ignored.
|
|
169
|
+
|
|
95
170
|
### From data
|
|
96
171
|
|
|
97
172
|
`diagramScene` turns plain `{ nodes, edges }` into a laid-out scene, and
|
|
@@ -105,22 +180,76 @@ return <DiagramView specs={specs} width={360} height={600} selected={id} onSelec
|
|
|
105
180
|
Every box is a `buildBody` body, so a data node takes the same knobs a body
|
|
106
181
|
does — `outline`, `rows`, `padding`, `gap`, `verticalAlign`, `ports` — plus `pinned`, a
|
|
107
182
|
starting `at`, and a minimum `width`/`height`; `lines` is shorthand for one
|
|
108
|
-
label row each.
|
|
183
|
+
label row each. A `slot` row is reserved space, `height` tall and, with a
|
|
184
|
+
`width`, that wide and centered; one with an `id` becomes a scene node of that
|
|
185
|
+
name, `<node>/<id>`, drawn as an empty frame until you fill it by changing its
|
|
186
|
+
data — with `{ image: { src } }`, say. An edge can name `fromPort`/`toPort`, its own `router`,
|
|
109
187
|
`waypoints` and `labelPlacement`.
|
|
110
188
|
|
|
189
|
+
`outline` is `'rect'`, `'diamond'`, `'stadium'`, `'parallelogram'`,
|
|
190
|
+
`'trapezoid'` (a demultiplexer: tall at the right, short at the left),
|
|
191
|
+
`'hexagon'` (pointed at the left and right, its rows kept between the points), or a
|
|
192
|
+
`Path` drawn in a unit square — `(0, 0)` the box's top-left, `(1, 1)` its
|
|
193
|
+
bottom-right — which is stretched over the box, so it follows its node as
|
|
194
|
+
marks do.
|
|
195
|
+
|
|
196
|
+
`marks` draws more over a box than its rows. A mark with `d` is SVG path data
|
|
197
|
+
in a unit square, stretched over the node's box; it takes `fill`, `stroke`,
|
|
198
|
+
`strokeWidth`, `dash` and `opacity`. `diagramScene` throws on path data that
|
|
199
|
+
does not parse or draws nothing, naming the node and the mark's index. A mark with `text` is pinned to a point on
|
|
200
|
+
the box: `at` is that point as `{ u, v }` fractions of the box, `self` is which
|
|
201
|
+
point of the text sits on it (default `at`), and `offset` nudges it in pixels.
|
|
202
|
+
Marks move with their node. `DiagramView` registers the painters for outlines
|
|
203
|
+
and marks itself; a scene drawn on your own `SceneCanvas` calls
|
|
204
|
+
`registerDiagramShape()` once for outlines and `registerMarkShape()` once for
|
|
205
|
+
marks, each of which returns its disposer, or they do not paint:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
{ id: 'gain', marks: [
|
|
209
|
+
{ d: 'M0 1 L1 0', stroke: '#c58bff' },
|
|
210
|
+
{ text: 'gain', at: { u: 0.5, v: 1 }, self: { u: 0.5, v: 0 }, offset: { x: 0, y: 4 } },
|
|
211
|
+
] }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`DiagramView` draws ports when `onConnect` is set, or when `ports` says so:
|
|
215
|
+
`'connect'` draws them and lets one be dragged onto another, `'show'` draws
|
|
216
|
+
them and nothing more, `'none'` hides them. `portOptions` styles them: `shape`
|
|
217
|
+
is `'square'` (default) or `'circle'`, `fill` is one fill or a function of the
|
|
218
|
+
port (its `type`, say), and `showsPort` picks which of a node's ports show.
|
|
219
|
+
|
|
220
|
+
Under `layered`, an edge that names neither leaves its source by the face
|
|
221
|
+
toward the next rank and arrives at its target's face toward the last; an edge
|
|
222
|
+
running back up the ranks does the same the other way round. One that spans
|
|
223
|
+
ranks runs through the room `layeredPlan` left for it in each rank it crosses,
|
|
224
|
+
so it goes around what lies between its ends — unless an end is a pin
|
|
225
|
+
`layeredPlan` could not hold (`unheld`), when it takes the router's own path. An
|
|
226
|
+
edge whose pinned ends stand level runs straight between their facing sides, or
|
|
227
|
+
around past both when a node stands between them. An
|
|
228
|
+
author's own ports and waypoints win. A label's `offset` is the gap between the
|
|
229
|
+
route and the label's near side. A label whose edge sets no `labelPlacement`
|
|
230
|
+
of its own, or one at `'mid'`, sits in the room layout left it, placed with
|
|
231
|
+
`at: { near }`, which stations a label where its route passes nearest a point,
|
|
232
|
+
so a bend added or removed elsewhere leaves it where it was.
|
|
233
|
+
|
|
111
234
|
`groups` draws a box around some nodes, with an optional `label` inside it:
|
|
112
235
|
|
|
113
236
|
```tsx
|
|
114
237
|
diagramScene({ nodes, edges, groups: [{ id: 'ingest', members: ['fetch', 'decode'], label: 'ingest' }] });
|
|
115
238
|
```
|
|
116
239
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
240
|
+
A group may be a member of another. `layered`, `tree` and `force` keep a
|
|
241
|
+
group's members together; `chain` ignores groups but still draws them. Under
|
|
242
|
+
`layered` and `tree` each group gets a column on the cross axis that nothing
|
|
243
|
+
else enters, in every rank from its first member's to its last, and rank gaps
|
|
244
|
+
widen where a box needs the room; `force` pulls a group's members toward each
|
|
245
|
+
other and pushes groups apart. The box is an ordinary scene node that depends
|
|
246
|
+
on its members and derives its pose from theirs (`groupDerivePose`), so
|
|
247
|
+
dragging a member carries the box along. It also depends on the labels of the
|
|
248
|
+
wires with both ends inside it, so a long label stays inside the box. It is
|
|
249
|
+
never narrower than its label,
|
|
250
|
+
and a layout running down or up leaves room for that width. An edge whose
|
|
251
|
+
`from` or `to` names a group meets the box, and ranks against the group's first
|
|
252
|
+
stage. A scene built
|
|
124
253
|
any other way joins in with a node whose trait carries `group` and whose
|
|
125
254
|
`dependsOn` lists its members.
|
|
126
255
|
|
|
@@ -144,16 +273,338 @@ since survive it. Editing reports rather than writes —
|
|
|
144
273
|
```
|
|
145
274
|
|
|
146
275
|
— `onMove` turns on dragging and hears every node a drag or a layout run
|
|
147
|
-
moved
|
|
148
|
-
|
|
149
|
-
|
|
276
|
+
moved, and, as its second argument, every edge whose route a layout run
|
|
277
|
+
or ports rewrote (`{ id, waypoints, ports }`, `id` being `edgeIdOf` of the
|
|
278
|
+
data edge, `ports` the ends whose port the run picked);
|
|
279
|
+
`onConnect` makes ports grabbable and hears the edge a connect describes.
|
|
280
|
+
Store a move as the node's `at`, with `pinned: true` unless the scene uses
|
|
281
|
+
`layout: 'none'`, or the next `diagramScene` lays it out again. Store a route
|
|
282
|
+
as the edge's `routed` and its `ports` as `routedPorts`, which `edgeIdOf` does
|
|
283
|
+
not read: it is drawn until layout runs again, and stays
|
|
284
|
+
layout's to replace, its label keeping the room layout leaves it. `waypoints`
|
|
285
|
+
would make it yours, which layout never moves. A run that only rewrote routes
|
|
286
|
+
calls `onMove` with no moves.
|
|
287
|
+
`onMove` hears only what changes after the view is built. Where the build's own
|
|
288
|
+
layout put things, `diagramLayoutOf(specs)` reads off the specs, in the same
|
|
289
|
+
shape (`{ moves, routes }`): every node not pinned, and every edge whose route
|
|
290
|
+
or ports layout wrote. Store them the same way and the next `diagramScene`
|
|
291
|
+
leaves those nodes where they stand and lays out only the new ones, which keeps
|
|
292
|
+
a diagram still while its data is edited.
|
|
150
293
|
`view.current.layout('layered')` re-runs a layout animated from where the
|
|
151
|
-
nodes stand; `'force'` relaxes live.
|
|
294
|
+
nodes stand; `'force'` relaxes live. Either way the routes ease with the nodes,
|
|
295
|
+
through the ports the run will commit.
|
|
296
|
+
|
|
297
|
+
`layout()` also takes a `LayoutPlanner`, an object whose `plan(graph, opts)`
|
|
298
|
+
answers with a whole `LayoutPlan`, now or as a promise. The run asks it once,
|
|
299
|
+
with the graph as it stands, and eases to the answer the way it eases to
|
|
300
|
+
`layered`'s, reporting the same moves, routes and ports to `onMove`. While the
|
|
301
|
+
answer is on its way, a drag, a change to the data, or another `layout()` call
|
|
302
|
+
drops it, since the plan was made for a diagram that is no longer there. A
|
|
303
|
+
planner whose promise rejects starts no run, and the reason goes to
|
|
304
|
+
`live.onError`, or to `console.error` when that is unset.
|
|
305
|
+
`elkPlanner()` from `@weasel-js/diagram-elk` is one: it lays the diagram out
|
|
306
|
+
with ELK (the Eclipse Layout Kernel).
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
view.current.layout(elkPlanner({ direction: 'right' }));
|
|
310
|
+
```
|
|
152
311
|
|
|
153
312
|
Labels paint only in a registered font family, and the default is
|
|
154
313
|
sans-serif, so call `registerCanvasFont('sans-serif')` once or they render
|
|
155
|
-
blank.
|
|
156
|
-
|
|
314
|
+
blank. The view opens fitted to the diagram and fits again when `width` or
|
|
315
|
+
`height` changes or a layout run settles, until the user pans or zooms. `minScale` stops the fit from
|
|
316
|
+
shrinking a wide diagram past readable; the rest is a pan away.
|
|
317
|
+
|
|
318
|
+
### Effect charts
|
|
319
|
+
|
|
320
|
+
`@weasel-js/diagram/effect` draws an effects pipeline: the stages a signal
|
|
321
|
+
passes through, what each reads and writes, and where a decision (a stage that
|
|
322
|
+
sends the signal one of several ways) sends it.
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
import { EffectChart, type EffectPipeline } from '@weasel-js/diagram/effect';
|
|
326
|
+
|
|
327
|
+
const pipeline: EffectPipeline = {
|
|
328
|
+
stages: [
|
|
329
|
+
{ id: 'in', title: 'Input', kind: 'source', outputs: [{ id: 'out', type: 'audio' }] },
|
|
330
|
+
{
|
|
331
|
+
id: 'gate',
|
|
332
|
+
title: 'Gate',
|
|
333
|
+
inputs: [{ id: 'in', type: 'audio' }],
|
|
334
|
+
outputs: [{ id: 'out', type: 'audio' }],
|
|
335
|
+
params: [{ label: 'threshold', value: '-40 dB' }],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: 'verb',
|
|
339
|
+
title: 'Reverb',
|
|
340
|
+
inputs: [{ id: 'in', type: 'audio' }],
|
|
341
|
+
outputs: [{ id: 'out', type: 'audio' }],
|
|
342
|
+
},
|
|
343
|
+
{ id: 'out', title: 'Output', kind: 'sink', inputs: [{ id: 'in', type: 'audio' }] },
|
|
344
|
+
],
|
|
345
|
+
wires: [
|
|
346
|
+
{ from: { stage: 'in', port: 'out' }, to: { stage: 'gate', port: 'in' } },
|
|
347
|
+
{ from: { stage: 'gate', port: 'out' }, to: { stage: 'verb', port: 'in' } },
|
|
348
|
+
{ from: { stage: 'verb', port: 'out' }, to: { stage: 'out', port: 'in' } },
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
<EffectChart pipeline={pipeline} width={900} height={420} background={{ color: '#10161c' }} />
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
A pipeline is plain data and says nothing about how it is drawn:
|
|
356
|
+
|
|
357
|
+
| Field | Meaning |
|
|
358
|
+
| --- | --- |
|
|
359
|
+
| `stages` | The stages, below. |
|
|
360
|
+
| `wires` | `{ from: { stage, port }, to: { stage, port }, label? }`, from an output to an input. |
|
|
361
|
+
| `groups` | `{ id, label?, members }`: a box around stages and other groups. |
|
|
362
|
+
| `types` | `{ [type]: { control?: boolean } }`. A `control` type carries a condition or a modulation rather than the signal, and its wires draw dashed. A type not listed is plain data. |
|
|
363
|
+
|
|
364
|
+
A stage:
|
|
365
|
+
|
|
366
|
+
| Field | Meaning |
|
|
367
|
+
| --- | --- |
|
|
368
|
+
| `id`, `title` | Required. |
|
|
369
|
+
| `kind` | `'stage'` (default), `'decision'`, `'source'` or `'sink'`. Any other value draws as `'stage'`. |
|
|
370
|
+
| `inputs`, `outputs` | Ports, `{ id, label?, type }`. `type` names a value type; a wire joins ports of one type. A port's `id` is unique among the stage's inputs and outputs together. |
|
|
371
|
+
| `params` | `{ label, value }` pairs shown on the box. A decision does not show them. |
|
|
372
|
+
| `split` | On a decision that compares a number: `{ input, at }`, the input it compares and the cut points in ascending order. N cuts need N + 1 outputs, lowest band first. |
|
|
373
|
+
| `bypassed` | The stage is in the pipeline and switched off; it draws dashed and dim. |
|
|
374
|
+
| `data` | Anything else a look wants. The shipped looks do not read it. |
|
|
375
|
+
|
|
376
|
+
`EffectChart` takes:
|
|
377
|
+
|
|
378
|
+
| Prop | Meaning |
|
|
379
|
+
| --- | --- |
|
|
380
|
+
| `pipeline` | The pipeline. It is compared by its JSON, so it may be rebuilt on every render; a value JSON drops or flattens in a stage's `data`, such as a function or a `Map`, is not seen changing. |
|
|
381
|
+
| `run` | One run of the pipeline, drawn over it (below). It may change every frame. |
|
|
382
|
+
| `look`, `options` | How it is drawn, and that look's options. Default look `nodeGraph`. |
|
|
383
|
+
| `width`, `height` | The canvas size. |
|
|
384
|
+
| `minScale` | The smallest scale the chart opens at; a chart too big to fit whole at it opens at its start, the rest a pan away. Default `EFFECT_MIN_SCALE`, 0.5, which sets the shipped looks' titles at 6px. |
|
|
385
|
+
| `background` | What the canvas paints behind the chart. Unset, it is transparent; the shipped looks are drawn for a dark ground. |
|
|
386
|
+
| `selected`, `onSelect` | As on `DiagramView`, for stage ids. A pick of anything that is not a stage, such as a group box, is reported as `null`, and the canvas keeps its own highlight on it. |
|
|
387
|
+
| `onProblems` | Hears what `checkPipeline` found, on mount and each time the pipeline's content changes, whether or not the list differs. |
|
|
388
|
+
| `className` | A class for the canvas element. |
|
|
389
|
+
|
|
390
|
+
Keep `look` and `options` steady between renders: a new object for either lays
|
|
391
|
+
the chart out again and opens it fitted afresh, as a pipeline whose content
|
|
392
|
+
changes does. Ports that
|
|
393
|
+
carry a value type are drawn, colored by type. As with `DiagramView`, call
|
|
394
|
+
`registerCanvasFont('sans-serif')` once or labels render blank.
|
|
395
|
+
|
|
396
|
+
#### Runs
|
|
397
|
+
|
|
398
|
+
A run is what one pass through the pipeline did, kept beside the pipeline
|
|
399
|
+
rather than in it, keyed by stage id:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const run: EffectRun = {
|
|
403
|
+
stages: {
|
|
404
|
+
src: { cost: 0.3, values: { out: '1920×1080' } },
|
|
405
|
+
q: { taken: 'yes' }, // a decision's chosen output
|
|
406
|
+
blur: { cost: 4.2, values: { out: '6.0 px' } },
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
A stage the run has no entry for is one it never reached. The chart fades such
|
|
412
|
+
a stage, its rows and its wires, and a decision's wires to the outputs it did
|
|
413
|
+
not take, every one of them when `taken` names none; where a look draws a
|
|
414
|
+
decision as something else, as `rack` draws a skip as an arc, the look's
|
|
415
|
+
`exits` say which edges stand for which exit. It writes each stage's `cost`, in milliseconds, above its top-right
|
|
416
|
+
corner; and writes each output's value into the row a look keeps for it when
|
|
417
|
+
its `showValues` option is on, with a dash for an output the run says nothing
|
|
418
|
+
about. A `switch` symbol is drawn thrown to its first exit, and a run whose
|
|
419
|
+
`taken` is the second throws it there. `EffectChart` fades the port dots of a
|
|
420
|
+
stage the run never reached as well; `paintRun` alone cannot, since the port
|
|
421
|
+
layer draws them outside the scene. A decision a look does not draw has nowhere
|
|
422
|
+
to show its cost.
|
|
423
|
+
|
|
424
|
+
A run never lays the chart out again or refits it: `paintRun(pipeline, specs,
|
|
425
|
+
run)` edits the specs the look already built, data only, and `DiagramView`
|
|
426
|
+
writes just the fields that changed. So `showValues` keeps its rows from the
|
|
427
|
+
first draw, wide enough for a value of about nine characters, and a longer one
|
|
428
|
+
runs past the box's edge rather than resizing it.
|
|
429
|
+
|
|
430
|
+
A run can also carry a picture per output, `previews: { [port]: ImageBitmap }`
|
|
431
|
+
on a stage's entry. With a look's `showPreviews` on, each output gets a slot
|
|
432
|
+
`previewSize` big (default `PREVIEW_SIZE`, a 48-unit square; or `{ width,
|
|
433
|
+
height }`) under its value row, kept from the first draw like the value rows,
|
|
434
|
+
drawn as an empty frame until a run fills it. The preview is stretched to the
|
|
435
|
+
slot. It is an `ImageBitmap`, not a canvas, because a bitmap's pixels cannot
|
|
436
|
+
change: a run that hands back the same bitmap edits nothing and uploads
|
|
437
|
+
nothing, and new pixels are a new bitmap. A canvas you draw into becomes one
|
|
438
|
+
with `transferToImageBitmap()` (`OffscreenCanvas`) or `createImageBitmap()`.
|
|
439
|
+
`EffectChart` frees the texture of a bitmap the next run no longer shows, and
|
|
440
|
+
of every one it shows when it unmounts; closing a bitmap is still yours to do.
|
|
441
|
+
Under `paintRun` alone the slot becomes an image node whose `src` is
|
|
442
|
+
`bitmapSrc(bitmap)` from `@weasel-js/core`, and releasing the replaced
|
|
443
|
+
textures is the caller's: `releaseImageSource(old)` from
|
|
444
|
+
`@weasel-js/core/renderer`.
|
|
445
|
+
|
|
446
|
+
`checkPipeline(pipeline)` returns the problems without drawing anything, each
|
|
447
|
+
`{ kind, message, stage?, port?, wire?, group? }`, where `wire` is an index into
|
|
448
|
+
`pipeline.wires`. The shipped looks draw a pipeline with problems as far as
|
|
449
|
+
they can:
|
|
450
|
+
|
|
451
|
+
| `kind` | When | On the chart |
|
|
452
|
+
| --- | --- | --- |
|
|
453
|
+
| `missing-stage`, `missing-port` | A wire names a stage or port that does not exist. | The wire is left out. |
|
|
454
|
+
| `wrong-direction` | A wire starts at an input or ends at an output. | The wire is drawn in the warning color. |
|
|
455
|
+
| `type-mismatch` | A wire joins ports of different types. | The wire is drawn in the warning color. |
|
|
456
|
+
| `duplicate-stage` | Two stages share an id. | The first is drawn. |
|
|
457
|
+
| `duplicate-port` | Two ports on one stage share an id. | Both are listed; wires attach to the first. |
|
|
458
|
+
| `duplicate-group` | A group's id repeats an earlier group's, or is a stage's. | Only the first group with the id is drawn, and a group with a stage's id is not. |
|
|
459
|
+
| `missing-member` | A group names something that is neither a stage nor a group. | The name is ignored. |
|
|
460
|
+
| `bad-split` | A `split` names an input the stage lacks, has no cuts, has cuts out of order or one that is not a finite number, or does not have one more output than cuts. | A decision that would draw as a bar draws as a diamond. |
|
|
461
|
+
|
|
462
|
+
#### Looks
|
|
463
|
+
|
|
464
|
+
A look is how a pipeline is drawn: a function from the pipeline and its
|
|
465
|
+
options to `{ data, scene, color }`, which are `diagramScene`'s two arguments
|
|
466
|
+
and a `color(type)` for the ports. `EFFECT_LOOKS` holds `nodeGraph`, `rack`, `lanes` and `compact`.
|
|
467
|
+
|
|
468
|
+
| Look | Draws | Options |
|
|
469
|
+
| --- | --- | --- |
|
|
470
|
+
| `nodeGraph` (default) | Every port a named row on its stage, a wire from port to port, flowing left to right. | `router` (default `'bezier'`), `nodeGap`, `rankGap`, `colors`, `showParams`, `showValues`, `showPreviews`, `previewSize`, `branch` |
|
|
471
|
+
| `rack` | The main signal on one straight line, and everything else hung beside the stage on the line it is nearest to, whichever way the wire runs. | `chain`, `side`, `nodeGap`, `rankGap`, `colors`, `showParams`, `showValues`, `showPreviews`, `previewSize`, `branch`, `collapseSkips` |
|
|
472
|
+
| `lanes` | A horizontal lane per value type, and each stage standing across the lanes of the types it reads and writes, as tall as they are, its ports in their own lanes. | `lanes`, `laneHeight`, `rankGap`, `router`, `colors`, `showParams`, `showValues`, `showPreviews`, `previewSize`, `branch`, `measure` |
|
|
473
|
+
| `compact` | Each stage one line, its title alone, no ports drawn, and one line between two stages however many wires join them. For a pipeline too big to read as a node graph. | `router`, `nodeGap`, `rankGap`, `colors`, `branch` |
|
|
474
|
+
|
|
475
|
+
`colors` maps a value type to a color, over the palette; `showParams` (default
|
|
476
|
+
true) shows the `params` of ordinary stages, sources and sinks; `showValues`
|
|
477
|
+
(default false) keeps a row per output on those stages for a run's values, and
|
|
478
|
+
`showPreviews` (default false) a slot per output for its previews. `rack` runs
|
|
479
|
+
along `chain`, a list of stage ids that defaults to the longest path over data
|
|
480
|
+
wires (`mainChain(pipeline)` returns it); `side` is `'below'` (default) or
|
|
481
|
+
`'above'`.
|
|
482
|
+
|
|
483
|
+
`lanes` puts the lanes top down in the order their types first appear on the
|
|
484
|
+
pipeline's ports, after any it is given in `lanes`. A stage stands
|
|
485
|
+
`LANE_INSET` inside the first and last lane it touches and covers every lane
|
|
486
|
+
between; its inputs sit on its left edge and its outputs on its right, each in
|
|
487
|
+
its own type's lane. Stages stand in columns, each right of every stage that
|
|
488
|
+
feeds it, and a stage moves a column further right rather than share one with
|
|
489
|
+
a stage holding any of its lanes. A lane is `laneHeight` (default 48) tall, or
|
|
490
|
+
taller until each stage fits the lanes it spans, params and value rows
|
|
491
|
+
included; a source or sink stretched taller than it measured is widened by as
|
|
492
|
+
much, so its round ends leave its rows the room they had. A decision keeps its symbol's own size, centered across its lanes,
|
|
493
|
+
and brings its control inputs in from the bottom when their lanes are below
|
|
494
|
+
the rest of its ports. The lanes are nodes too, `laneId(type)`, drawn behind
|
|
495
|
+
the stages, and only stages can be picked: a click on a lane or a group box
|
|
496
|
+
selects nothing. A wire that would cross a stage in its lane runs along a lane
|
|
497
|
+
edge clear of every stage it passes, changing height only between columns, each
|
|
498
|
+
wire in a shared corridor on a track of its own; its label sits as near the
|
|
499
|
+
middle as it can without covering a stage. A decision's title and cut values
|
|
500
|
+
stay inside its lanes and column. Text is measured to cut the
|
|
501
|
+
lanes before `diagramScene` lays anything out, so pass `measure` to the look,
|
|
502
|
+
not to `diagramScene`: the look hands it on in `scene`.
|
|
503
|
+
|
|
504
|
+
`compact` shows no params and keeps no value rows or preview slots: one line
|
|
505
|
+
has no room for them, so a run fades and costs its stages but writes no values
|
|
506
|
+
and draws no previews. Its ports
|
|
507
|
+
carry no type, which is what keeps the port layer from drawing them. A
|
|
508
|
+
decision draws as a `'hexagon'` by default, the one symbol a single row holds;
|
|
509
|
+
another `branch` draws at that symbol's own size. Wires between two stages
|
|
510
|
+
draw as one line, in the color of a data wire among them, dashed only when
|
|
511
|
+
every one is a control wire, and in the warning color when any was flagged;
|
|
512
|
+
a decision keeps one line per exit, labeled, so a run can fade the exits it
|
|
513
|
+
did not take.
|
|
514
|
+
|
|
515
|
+
With `collapseSkips` (default true), `rack` draws a decision as an arc over the
|
|
516
|
+
stages it skips, not as a symbol, when all of these hold:
|
|
517
|
+
|
|
518
|
+
- it is on the line, but not first on it;
|
|
519
|
+
- it has one input and two outputs, with one wire into it and two out;
|
|
520
|
+
- the wire in comes from the stage before it on the line;
|
|
521
|
+
- one wire out goes to the next stage on the line, and the other lands on a
|
|
522
|
+
stage further along;
|
|
523
|
+
- none of the stage before it, the stage it goes on to and the stage its skip
|
|
524
|
+
lands on is a decision. A source or a sink is fine.
|
|
525
|
+
|
|
526
|
+
A decision stage draws as a branch symbol, chosen by `branch`:
|
|
527
|
+
|
|
528
|
+
| `branch` | Symbol |
|
|
529
|
+
| --- | --- |
|
|
530
|
+
| `'auto'` (default) | `'bar'` for a decision with a sound `split`, else `'diamond'`. |
|
|
531
|
+
| `'diamond'` | A flowchart diamond titled with the stage. |
|
|
532
|
+
| `'bar'` | A tall bar in bands of equal height, the cut values as labels at the ticks. |
|
|
533
|
+
| `'demux'` | A trapezoid with the exits spread down its right side. |
|
|
534
|
+
| `'switch'` | A lever thrown between two contacts. |
|
|
535
|
+
| `'hexagon'` | The stage's title inside a flat hexagon, on one line. |
|
|
536
|
+
| `'step'` | A staircase rising a step at each cut, the cut values under the risers. |
|
|
537
|
+
| `'comparator'` | A triangle pointing right, its two exits leaving by its slanted edges. With a sound one-cut `split` the high band leaves by the upper edge and the cut is written inside; otherwise the first exit leaves by the upper edge, and two inputs on its flat side are marked `+` and `−`. |
|
|
538
|
+
| `'crossover'` | One filter curve per band, lowest at the bottom, overlapping at each cut. |
|
|
539
|
+
| `'fork'` | A bare dot every exit leaves from, the conditions left to the labels on its wires. |
|
|
540
|
+
|
|
541
|
+
`'bar'`, `'step'` and `'crossover'` draw as a diamond for a stage with no `split` or a bad one, and
|
|
542
|
+
`'switch'` and `'comparator'` for a stage that does not have exactly two outputs.
|
|
543
|
+
`BRANCH_SYMBOLS`, `branchNode` and `symbolNameFor` expose the same lookup. The
|
|
544
|
+
signal enters on the left; inputs of a `control` type enter at the top, and so
|
|
545
|
+
does the input a `'bar'`, `'step'` or `'crossover'` tests, when the stage has more than one input. A symbol
|
|
546
|
+
takes `{ controlSide: 'bottom' }` to bring those inputs in at the bottom instead,
|
|
547
|
+
and `rack` passes it with `side: 'below'`, so a control wire comes straight up from
|
|
548
|
+
the stage driving it; `lanes` passes it when the control inputs' lanes are below.
|
|
549
|
+
The title then moves above the symbol, and a diamond takes control in by its
|
|
550
|
+
lower-left edge, keeping its bottom vertex for the second exit. A symbol's lines, dots and ticks are drawn in the `text` color of
|
|
551
|
+
`{ style }` (default `stageStyle(stage)`, so a bypassed decision draws dim), and a
|
|
552
|
+
`'bar'`'s bands and a `'crossover'`'s curves in `{ tints }`, lowest first; a bypassed
|
|
553
|
+
stage's default to its style's `stroke`.
|
|
554
|
+
|
|
555
|
+
A look is an `EffectLook<TOptions>`. Plain `EffectLook` is a look whose options
|
|
556
|
+
are not known, so a list of looks with different options, such as
|
|
557
|
+
`[nodeGraph, rack]`, is an `EffectLook[]`; such a look is called with no options.
|
|
558
|
+
|
|
559
|
+
To write a look, return `{ data, scene, color }`, and `exits` if it draws a
|
|
560
|
+
decision as something other than itself; `scene` takes any
|
|
561
|
+
`diagramScene` option. The shipped looks are built from these, which are
|
|
562
|
+
exported for yours:
|
|
563
|
+
|
|
564
|
+
| Helper | Gives |
|
|
565
|
+
| --- | --- |
|
|
566
|
+
| `uniqueStages(pipeline)`, `uniqueGroups(pipeline)` | The stages and the groups with any repeated id dropped, first one kept. `diagramScene` throws on a repeated id, so build nodes and groups from these. |
|
|
567
|
+
| `drawableWires(pipeline)` | The wires whose two stages and two ports exist, each as `{ wire, index, type, problem, fromStage, toStage }`: `type` is the value type it carries, and `problem` is true when `checkPipeline` reported it. |
|
|
568
|
+
| `typeColors(pipeline, colors)` | The `color(type)` the shipped looks use. |
|
|
569
|
+
| `stageStyle(stage)` | A stage's node style: dashed and dim when it is bypassed. |
|
|
570
|
+
| `wireStyle(color, control, problem)` | A wire's edge style: dashed for a control wire, and `PROBLEM_COLOR` for a flagged one. |
|
|
571
|
+
| `isControl(pipeline, type)` | Whether `types` marks a value type as `control`. |
|
|
572
|
+
| `kindOf(stage)` | The stage's `kind`, with any value the shipped looks do not know read as `'stage'`. |
|
|
573
|
+
| `portOn(stage, id)` | The stage's port with that id. |
|
|
574
|
+
| `stageRows(stage, { showParams, withPorts, showValues, showPreviews, previewSize })` | Body rows for a stage: its title, a row of its ports, per output a value row (`valueRowId(port)`, showing `NO_VALUE`) and a preview slot (`previewRowId(port)`), its params. |
|
|
575
|
+
| `wireEdges(pipeline, color)` | An edge per drawable wire, from port to port, named by `exitLabel` and styled by `wireStyle`, each as `{ edge, style }`. |
|
|
576
|
+
| `exitLabel(wire)` | What to call one of `drawableWires`' wires: its own label, or for a wire leaving a decision the exit's label or id. |
|
|
577
|
+
| `branchNode(stage, pipeline, branch, { controlSide, style, tints })` | The node a decision draws as. |
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
import {
|
|
581
|
+
drawableWires, isControl, stageStyle, typeColors, uniqueGroups, uniqueStages, wireStyle,
|
|
582
|
+
type EffectLook,
|
|
583
|
+
} from '@weasel-js/diagram/effect';
|
|
584
|
+
|
|
585
|
+
const column: EffectLook = (pipeline) => {
|
|
586
|
+
const color = typeColors(pipeline);
|
|
587
|
+
const stages = new Map(uniqueStages(pipeline).map((s) => [s.id, s]));
|
|
588
|
+
const wires = drawableWires(pipeline);
|
|
589
|
+
return {
|
|
590
|
+
data: {
|
|
591
|
+
nodes: [...stages.values()].map((s) => ({ id: s.id, lines: [s.title] })),
|
|
592
|
+
edges: wires.map((d) => ({ from: d.wire.from.stage, to: d.wire.to.stage })),
|
|
593
|
+
groups: uniqueGroups(pipeline),
|
|
594
|
+
},
|
|
595
|
+
scene: {
|
|
596
|
+
layout: 'layered',
|
|
597
|
+
layoutOptions: { direction: 'down' },
|
|
598
|
+
nodeStyle: (n) => stageStyle(stages.get(n.id)!),
|
|
599
|
+
edgeStyle: (e) => {
|
|
600
|
+
const d = wires.find((d) => d.wire.from.stage === e.from && d.wire.to.stage === e.to)!;
|
|
601
|
+
return wireStyle(color(d.type), isControl(pipeline, d.type), d.problem);
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
color,
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
```
|
|
157
608
|
|
|
158
609
|
## Design notes
|
|
159
610
|
|