flexdesk 0.4.7 → 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.
- package/README.md +1036 -301
- package/css/base.css +2958 -2
- package/css/flexdesk.css +2962 -4
- package/css/overrides.css +4 -2
- package/css/tokens.css +86 -0
- package/dist/canvas.js +390 -0
- package/dist/canvas.js.map +7 -0
- package/dist/charts.js +782 -782
- package/dist/{chunk-LH5TSOZW.js → chunk-6JSOVNID.js} +42 -2
- package/dist/{chunk-LH5TSOZW.js.map → chunk-6JSOVNID.js.map} +3 -3
- package/dist/{chunk-ELXVW542.js → chunk-6NG7YVBG.js} +3 -471
- package/dist/chunk-6NG7YVBG.js.map +7 -0
- package/dist/{chunk-F5FYDCOA.js → chunk-CNXP3SIN.js} +2373 -2373
- package/dist/{chunk-F5FYDCOA.js.map → chunk-CNXP3SIN.js.map} +7 -7
- package/dist/chunk-H4LT43RS.js +475 -0
- package/dist/chunk-H4LT43RS.js.map +7 -0
- package/dist/{chunk-WMGOJ3I6.js → chunk-KH2GJTKR.js} +3702 -2628
- package/dist/chunk-KH2GJTKR.js.map +7 -0
- package/dist/chunk-SN4LSOAR.js +532 -0
- package/dist/chunk-SN4LSOAR.js.map +7 -0
- package/dist/flexdesk.css +2962 -4
- package/dist/flow.js +10030 -0
- package/dist/flow.js.map +7 -0
- package/dist/tiles.js +2195 -2195
- package/dist/tokens.css +86 -0
- package/dist/widgets.js +5824 -5796
- package/dist/widgets.js.map +1 -1
- package/dist/wm.js +8077 -8074
- package/dist/wm.js.map +7 -7
- package/package.json +89 -81
- package/src/canvas/canvas_adapter.js +523 -0
- package/src/canvas/edge_router.js +392 -0
- package/src/canvas/graph/connector_router.js +189 -0
- package/src/canvas/graph/history_service.js +170 -0
- package/src/canvas/graph/node_platform.js +116 -0
- package/src/canvas/index.js +16 -0
- package/src/flow/canvas_editor/editor.js +1081 -0
- package/src/flow/canvas_editor/index.js +16 -0
- package/src/flow/canvas_editor/layout.js +118 -0
- package/src/flow/canvas_editor/strings.js +37 -0
- package/src/flow/kit/always_before.js +325 -0
- package/src/flow/kit/catalogue.js +170 -0
- package/src/flow/kit/chip_input.js +461 -0
- package/src/flow/kit/dom.js +205 -0
- package/src/flow/kit/findings.js +129 -0
- package/src/flow/kit/history.js +173 -0
- package/src/flow/kit/index.js +38 -0
- package/src/flow/kit/keys.js +127 -0
- package/src/flow/kit/logic_graph.js +210 -0
- package/src/flow/kit/references.js +199 -0
- package/src/flow/kit/settings_panel.js +357 -0
- package/src/flow/kit/settings_schema.js +175 -0
- package/src/flow/kit/step_picker.js +209 -0
- package/src/flow/kit/strings.js +102 -0
- package/src/flow/kit/value_picker.js +238 -0
- package/src/flow/kit/widgets.js +763 -0
- package/src/flow/lanes/editor.js +1469 -0
- package/src/flow/lanes/index.js +26 -0
- package/src/flow/lanes/layout.js +525 -0
- package/src/flow/lanes/pipeline.js +327 -0
- package/src/flow/lanes/preview.js +109 -0
- package/src/flow/lanes/strings.js +98 -0
- package/src/flow/outline/editor.js +1511 -0
- package/src/flow/outline/index.js +20 -0
- package/src/flow/outline/mapping.js +286 -0
- package/src/flow/outline/menu.js +154 -0
- package/src/flow/outline/operations.js +738 -0
- package/src/flow/outline/recognise.js +755 -0
- package/src/flow/outline/strings.js +135 -0
- package/src/tiling/landing_table.js +13 -5
- package/src/tiling/wm.js +3916 -3916
- package/src/ui/components/data_table.js +4205 -3294
- package/src/ui/components/row_activation.js +70 -0
- package/src/ui/components/table_values.js +423 -0
- package/dist/chunk-ELXVW542.js.map +0 -7
- package/dist/chunk-WMGOJ3I6.js.map +0 -7
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `@flexdesk/flow` exports for the LANE editor (`createLaneEditor`):
|
|
3
|
+
* a data flow drawn as lanes, left to right (36 §7).
|
|
4
|
+
*
|
|
5
|
+
* OWNED BY the lanes agent (D, 36 §2 and §7).
|
|
6
|
+
*
|
|
7
|
+
* editor.js createLaneEditor — the editor, and its compact strip
|
|
8
|
+
* layout.js layoutLanes, placeLanes — PURE: lanes, columns and wires
|
|
9
|
+
* pipeline.js the flow's JSON and every edit the editor makes to it — PURE
|
|
10
|
+
* preview.js the debounced provider runner: the newest answer wins
|
|
11
|
+
* strings.js every word it draws
|
|
12
|
+
*
|
|
13
|
+
* Every name is the lane editor's own — `lane…`, `LANE_…`, `…Pipeline` — so the
|
|
14
|
+
* three editors' `export *` in `flow.js` cannot collide; the pure edits are one
|
|
15
|
+
* namespace, `lanePipeline`, rather than a dozen loose verbs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { createLaneEditor } from './editor.js';
|
|
19
|
+
export {
|
|
20
|
+
LANE_GEOMETRY, LANE_PAD, LANE_GAP, CW as LANE_CW, CH as LANE_CH, GAP as LANE_COLUMN_GAP,
|
|
21
|
+
LAYOUT_STRINGS as LANE_LAYOUT_STRINGS, layoutLanes, placeLanes, stepPorts as laneStepPorts,
|
|
22
|
+
} from './layout.js';
|
|
23
|
+
export { LANE_ACTIONS, emptyPipeline, normalisePipeline, serialisePipeline } from './pipeline.js';
|
|
24
|
+
export * as lanePipeline from './pipeline.js';
|
|
25
|
+
export { createPreviewRunner as createLanePreviewRunner } from './preview.js';
|
|
26
|
+
export { LANE_STRINGS } from './strings.js';
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE LANE LAYOUT — a data flow drawn as lanes, left to right (36 §7.3).
|
|
3
|
+
* PURE: no DOM, no clock; runs under plain node.
|
|
4
|
+
*
|
|
5
|
+
* const layout = layoutLanes(pipeline, catalogue);
|
|
6
|
+
* const placed = placeLanes(layout, 'regular'); // or 'small', 'strip'
|
|
7
|
+
*
|
|
8
|
+
* The flow is the pipeline JSON — `{nodes, connections: [{id, sourceId,
|
|
9
|
+
* sourcePort, targetId, targetPort}], parameters}` — and the catalogue is the
|
|
10
|
+
* kit's (`createStepCatalogue`), read for each step's FLOW ports in their
|
|
11
|
+
* DECLARED order: a step's first input is its LANE input, every further input
|
|
12
|
+
* takes a lane from elsewhere (a join's second side), and its first output is
|
|
13
|
+
* its continuation.
|
|
14
|
+
*
|
|
15
|
+
* ══ THE RULES ══════════════════════════════════════════════════════════
|
|
16
|
+
*
|
|
17
|
+
* 1. ROOTS are the steps with nothing connected to their lane input — the
|
|
18
|
+
* sources, and any step left unconnected (its lane flagged), in `nodes`
|
|
19
|
+
* order.
|
|
20
|
+
* 2. A LANE starts at a root and follows the step that takes its output ON
|
|
21
|
+
* ITS LANE INPUT. A step belongs to the lane that feeds its lane input —
|
|
22
|
+
* never to whichever lane reached it first. An output that feeds several
|
|
23
|
+
* steps' lane inputs is a FORK: the first, in connection order, goes on in
|
|
24
|
+
* the lane, and each other starts a lane below. An output that feeds
|
|
25
|
+
* another step's OTHER input makes this lane a FEEDER of that join.
|
|
26
|
+
* 3. COLUMNS: a root is column 0 and every other step is one more than the
|
|
27
|
+
* latest of the steps that feed it. Then each feeder lane is SHIFTED RIGHT
|
|
28
|
+
* as a whole until the card that feeds the join sits one column before it
|
|
29
|
+
* — never left of where the first pass put it, and never so far that the
|
|
30
|
+
* join itself would move. Forks and nested feeders are re-settled until
|
|
31
|
+
* nothing moves.
|
|
32
|
+
* 4. LANE ORDER, top to bottom: the root lanes in `nodes` order, each followed
|
|
33
|
+
* directly by its feeders (by their join's column), then its forks (by
|
|
34
|
+
* their fork's column), each of those followed by its own, recursively.
|
|
35
|
+
* 5. WIRES are computed from the grid, never measured from the page: `lane`,
|
|
36
|
+
* card to next card; `join`, from the feeding card right and then up into
|
|
37
|
+
* the join's bottom; `fork`, from the forking card's bottom down and then
|
|
38
|
+
* right into the new lane's first card. A wire that would run through a
|
|
39
|
+
* card that is neither of its ends (a feeder lane that goes on past its
|
|
40
|
+
* feed, a fork drawn under a feeder) takes the gutters instead — the gap
|
|
41
|
+
* after a column and the gap beside a lane, which hold no card.
|
|
42
|
+
*
|
|
43
|
+
* ══ TWO DEFECTS OF THE EARLIER LAYOUT, LEFT BEHIND ═════════════════════
|
|
44
|
+
*
|
|
45
|
+
* The lane layout this ports gave a join to WHICHEVER LANE REACHED IT FIRST,
|
|
46
|
+
* which depended on the order of the nodes array: list the feeding source
|
|
47
|
+
* first and the join, and everything after it, moved into the feeder's lane.
|
|
48
|
+
* Rule 2 is the fix. And it measured its wires from the page after every
|
|
49
|
+
* rebuild, so nothing about them could be tested without a browser; rule 5 is
|
|
50
|
+
* the fix, and `placeLanes` is what the stylesheet is held to.
|
|
51
|
+
*
|
|
52
|
+
* ══ WHAT IT REFUSES ════════════════════════════════════════════════════
|
|
53
|
+
*
|
|
54
|
+
* Three shapes cannot be drawn honestly as lanes, and each is a `problem`
|
|
55
|
+
* that opens the editor READ-ONLY with its sentence (the graph is drawn as
|
|
56
|
+
* best it can be, and never repaired): a step whose lane input two
|
|
57
|
+
* connections feed (`lanes_two_inputs`), a cycle (`lanes_cycle`, broken for
|
|
58
|
+
* drawing at the first step of it in `nodes` order), and two steps with one id
|
|
59
|
+
* (`lanes_duplicate_step`; the second is not drawn).
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/** The three sizes a lane is drawn at — each the numbers of the mock it comes from. */
|
|
63
|
+
export const LANE_GEOMETRY = Object.freeze({
|
|
64
|
+
/** The lane editor. `x = left + column·(width + gap)`, `laneTop = lane·(height + 2·lanePad + laneGap)`. */
|
|
65
|
+
regular: Object.freeze({
|
|
66
|
+
name: 'regular', width: 200, height: 62, gap: 30, lanePad: 12, laneGap: 26, left: 12, right: 12,
|
|
67
|
+
notch: 12, tipInset: 4, wireIn: 12, radius: 10, portDot: true,
|
|
68
|
+
}),
|
|
69
|
+
/** The lane editor in a narrower host (two lines a card). */
|
|
70
|
+
small: Object.freeze({
|
|
71
|
+
name: 'small', width: 160, height: 50, gap: 24, lanePad: 8, laneGap: 18, left: 10, right: 10,
|
|
72
|
+
notch: 10, tipInset: 4, wireIn: 10, radius: 10, portDot: true,
|
|
73
|
+
}),
|
|
74
|
+
/** The compact strip: a title a card, nothing to press. */
|
|
75
|
+
strip: Object.freeze({
|
|
76
|
+
name: 'strip', width: 92, height: 26, gap: 10, lanePad: 0, laneGap: 14, left: 0, right: 0,
|
|
77
|
+
notch: 8, tipInset: 2, wireIn: 6, radius: 10, portDot: false,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** The regular card's numbers, by the names the mock gives them. */
|
|
82
|
+
export const CW = LANE_GEOMETRY.regular.width;
|
|
83
|
+
export const CH = LANE_GEOMETRY.regular.height;
|
|
84
|
+
export const GAP = LANE_GEOMETRY.regular.gap;
|
|
85
|
+
export const LANE_PAD = LANE_GEOMETRY.regular.lanePad;
|
|
86
|
+
export const LANE_GAP = LANE_GEOMETRY.regular.laneGap;
|
|
87
|
+
|
|
88
|
+
export const LAYOUT_STRINGS = Object.freeze({
|
|
89
|
+
lanes_two_inputs: (name) => `‘${name}’ takes its lane from two steps at once, so the flow cannot be drawn `
|
|
90
|
+
+ 'as lanes.',
|
|
91
|
+
lanes_cycle: (name) => `‘${name}’ leads back to itself through the steps after it, so the flow cannot be `
|
|
92
|
+
+ 'drawn as lanes.',
|
|
93
|
+
lanes_duplicate_step: (id) => `Two steps are called ‘${id}’, so the flow cannot be drawn as lanes.`,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
97
|
+
|
|
98
|
+
function unique(list) {
|
|
99
|
+
return [...new Set(list)];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A step's FLOW ports as a lane reads them: `{known, role, laneInput, inputs,
|
|
104
|
+
* continuation, outputs}`. A type the catalogue does not know is read from the
|
|
105
|
+
* connections it has, in connection order.
|
|
106
|
+
*/
|
|
107
|
+
export function stepPorts(catalogue, node, connections = []) {
|
|
108
|
+
const type = catalogue?.get?.(node?.type) ?? null;
|
|
109
|
+
const conns = Array.isArray(connections) ? connections : [];
|
|
110
|
+
let inputs;
|
|
111
|
+
let outputs;
|
|
112
|
+
if (type) {
|
|
113
|
+
inputs = catalogue.inputs(node.type, { flow: true }).map((p) => p.name);
|
|
114
|
+
outputs = catalogue.outputs(node.type, { flow: true }).map((p) => p.name);
|
|
115
|
+
} else {
|
|
116
|
+
inputs = unique(conns.filter((c) => c.targetId === node?.id).map((c) => String(c.targetPort ?? 'in')));
|
|
117
|
+
outputs = unique(conns.filter((c) => c.sourceId === node?.id).map((c) => String(c.sourcePort ?? 'out')));
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
known: Boolean(type),
|
|
121
|
+
role: type ? (type.role || 'transform') : null,
|
|
122
|
+
laneInput: inputs[0] ?? null,
|
|
123
|
+
inputs,
|
|
124
|
+
continuation: outputs[0] ?? null,
|
|
125
|
+
outputs,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A step's name for a sentence: its label, its type's label, its id. */
|
|
130
|
+
function nameOf(catalogue, node) {
|
|
131
|
+
return node?.label || catalogue?.get?.(node?.type)?.label || String(node?.id ?? '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Lay a data flow out as lanes.
|
|
136
|
+
*
|
|
137
|
+
* @param {{nodes: object[], connections: object[]}} pipeline
|
|
138
|
+
* @param {object} catalogue the kit's step catalogue
|
|
139
|
+
* @param {object} [o]
|
|
140
|
+
* @param {object} [o.strings] the problems' sentences (`LAYOUT_STRINGS` by default)
|
|
141
|
+
* @returns {{lanes: {index, steps: {id, column}[], feeds: {from, to, port}[], forkOf: string|null,
|
|
142
|
+
* unconnected: boolean, parent: number|null, kind: 'root'|'fork'|'feeder'}[],
|
|
143
|
+
* wires: {kind: 'lane'|'join'|'fork', from, to, toPort, connection, slot?, slots?, cycle?}[],
|
|
144
|
+
* at: Object<string, {lane, column, position}>, columns: number,
|
|
145
|
+
* problems: {code, node_id, message}[]}}
|
|
146
|
+
*/
|
|
147
|
+
export function layoutLanes(pipeline, catalogue, { strings = null } = {}) {
|
|
148
|
+
const S = { ...LAYOUT_STRINGS, ...(isObject(strings) ? strings : {}) };
|
|
149
|
+
const problems = [];
|
|
150
|
+
|
|
151
|
+
// ── the steps, once each ─────────────────────────────────────────────
|
|
152
|
+
const nodes = [];
|
|
153
|
+
const byId = new Map();
|
|
154
|
+
for (const n of Array.isArray(pipeline?.nodes) ? pipeline.nodes : []) {
|
|
155
|
+
if (!isObject(n)) continue;
|
|
156
|
+
const id = String(n.id);
|
|
157
|
+
if (byId.has(id)) {
|
|
158
|
+
if (!problems.some((p) => p.code === 'lanes_duplicate_step' && p.node_id === id)) {
|
|
159
|
+
problems.push({ code: 'lanes_duplicate_step', node_id: id, message: S.lanes_duplicate_step(id) });
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
byId.set(id, n);
|
|
164
|
+
nodes.push(n);
|
|
165
|
+
}
|
|
166
|
+
const order = new Map(nodes.map((n, i) => [String(n.id), i]));
|
|
167
|
+
const ids = nodes.map((n) => String(n.id));
|
|
168
|
+
|
|
169
|
+
// ── the connections whose ends are both steps ────────────────────────
|
|
170
|
+
const conns = [];
|
|
171
|
+
(Array.isArray(pipeline?.connections) ? pipeline.connections : []).forEach((c, index) => {
|
|
172
|
+
if (!isObject(c)) return;
|
|
173
|
+
const source = String(c.sourceId);
|
|
174
|
+
const target = String(c.targetId);
|
|
175
|
+
if (!byId.has(source) || !byId.has(target)) return;
|
|
176
|
+
conns.push({ index, id: c.id ?? null, source, target, sourcePort: String(c.sourcePort ?? 'out'),
|
|
177
|
+
targetPort: String(c.targetPort ?? 'in'), ignored: false });
|
|
178
|
+
});
|
|
179
|
+
const rawConns = conns.map((c) => ({ id: c.id, sourceId: c.source, targetId: c.target,
|
|
180
|
+
sourcePort: c.sourcePort, targetPort: c.targetPort }));
|
|
181
|
+
const ports = new Map(nodes.map((n) => [String(n.id), stepPorts(catalogue, n, rawConns)]));
|
|
182
|
+
|
|
183
|
+
// ── the order steps run in, breaking a cycle where it must be ────────
|
|
184
|
+
const incoming = new Map(ids.map((id) => [id, []]));
|
|
185
|
+
const outgoing = new Map(ids.map((id) => [id, []]));
|
|
186
|
+
for (const c of conns) {
|
|
187
|
+
incoming.get(c.target).push(c);
|
|
188
|
+
outgoing.get(c.source).push(c);
|
|
189
|
+
}
|
|
190
|
+
const indeg = new Map(ids.map((id) => [id, incoming.get(id).length]));
|
|
191
|
+
const topo = [];
|
|
192
|
+
const done = new Set();
|
|
193
|
+
const ready = ids.filter((id) => indeg.get(id) === 0);
|
|
194
|
+
const release = (id) => {
|
|
195
|
+
done.add(id);
|
|
196
|
+
topo.push(id);
|
|
197
|
+
for (const c of outgoing.get(id)) {
|
|
198
|
+
if (c.ignored) continue;
|
|
199
|
+
indeg.set(c.target, indeg.get(c.target) - 1);
|
|
200
|
+
if (indeg.get(c.target) === 0 && !done.has(c.target)) ready.push(c.target);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
while (topo.length < ids.length) {
|
|
204
|
+
if (ready.length) {
|
|
205
|
+
const id = ready.shift();
|
|
206
|
+
if (!done.has(id)) release(id);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// Nothing is ready: the steps left wait on each other. Break the cycle at
|
|
210
|
+
// the first of them in `nodes` order, by setting aside every line into it
|
|
211
|
+
// from a step that has not run yet.
|
|
212
|
+
const first = ids.find((id) => !done.has(id));
|
|
213
|
+
for (const c of incoming.get(first)) {
|
|
214
|
+
if (!done.has(c.source) && !c.ignored) {
|
|
215
|
+
c.ignored = true;
|
|
216
|
+
indeg.set(first, indeg.get(first) - 1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
problems.push({ code: 'lanes_cycle', node_id: first, message: S.lanes_cycle(nameOf(catalogue, byId.get(first))) });
|
|
220
|
+
if (indeg.get(first) === 0) ready.push(first);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── which line feeds each step's lane input ──────────────────────────
|
|
224
|
+
const laneFeed = new Map();
|
|
225
|
+
for (const id of ids) {
|
|
226
|
+
const lane = ports.get(id).laneInput;
|
|
227
|
+
if (lane === null) continue;
|
|
228
|
+
const into = incoming.get(id).filter((c) => c.targetPort === lane);
|
|
229
|
+
if (into.length > 1) {
|
|
230
|
+
problems.push({ code: 'lanes_two_inputs', node_id: id,
|
|
231
|
+
message: S.lanes_two_inputs(nameOf(catalogue, byId.get(id))) });
|
|
232
|
+
}
|
|
233
|
+
const feed = into.find((c) => !c.ignored);
|
|
234
|
+
if (feed) laneFeed.set(id, feed);
|
|
235
|
+
}
|
|
236
|
+
const successors = new Map(ids.map((id) => [id, []]));
|
|
237
|
+
for (const [target, c] of [...laneFeed.entries()].sort((a, b) => a[1].index - b[1].index)) {
|
|
238
|
+
successors.get(c.source).push(target);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── the lanes, in the order they are found ───────────────────────────
|
|
242
|
+
const laneOf = new Map();
|
|
243
|
+
const created = [];
|
|
244
|
+
const build = (root, forkOf) => {
|
|
245
|
+
const lane = {
|
|
246
|
+
steps: [], feeds: [], forkOf, parent: null, kind: forkOf === null ? 'root' : 'fork',
|
|
247
|
+
unconnected: ports.get(root).laneInput !== null && !laneFeed.has(root),
|
|
248
|
+
};
|
|
249
|
+
created.push(lane);
|
|
250
|
+
const forks = [];
|
|
251
|
+
let current = root;
|
|
252
|
+
while (current !== null && !laneOf.has(current)) {
|
|
253
|
+
laneOf.set(current, lane);
|
|
254
|
+
lane.steps.push(current);
|
|
255
|
+
const next = successors.get(current).filter((t) => !laneOf.has(t));
|
|
256
|
+
for (const f of next.slice(1)) forks.push([current, f]);
|
|
257
|
+
current = next[0] ?? null;
|
|
258
|
+
}
|
|
259
|
+
for (const [from, to] of forks) if (!laneOf.has(to)) build(to, from);
|
|
260
|
+
};
|
|
261
|
+
for (const id of ids) if (!laneFeed.has(id) && !laneOf.has(id)) build(id, null);
|
|
262
|
+
for (const id of ids) if (!laneOf.has(id)) build(id, null);
|
|
263
|
+
|
|
264
|
+
// ── feeds: a line into any input but the lane's ──────────────────────
|
|
265
|
+
const feedsInto = new Map();
|
|
266
|
+
for (const c of conns) {
|
|
267
|
+
if (laneFeed.get(c.target) === c) continue;
|
|
268
|
+
laneOf.get(c.source).feeds.push({ from: c.source, to: c.target, port: c.targetPort, index: c.index });
|
|
269
|
+
if (!feedsInto.has(c.target)) feedsInto.set(c.target, []);
|
|
270
|
+
feedsInto.get(c.target).push(c);
|
|
271
|
+
}
|
|
272
|
+
for (const lane of created) {
|
|
273
|
+
if (lane.forkOf !== null) {
|
|
274
|
+
lane.parent = laneOf.get(lane.forkOf);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const out = lane.feeds.find((f) => laneOf.get(f.to) !== lane);
|
|
278
|
+
if (out) {
|
|
279
|
+
lane.parent = laneOf.get(out.to);
|
|
280
|
+
lane.kind = 'feeder';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── columns: longest path, then the feeders pulled right ─────────────
|
|
285
|
+
const preds = new Map(ids.map((id) => [id, incoming.get(id).filter((c) => !c.ignored).map((c) => c.source)]));
|
|
286
|
+
const settle = (floor) => {
|
|
287
|
+
const col = new Map();
|
|
288
|
+
for (const id of topo) {
|
|
289
|
+
let c = floor.get(id) ?? 0;
|
|
290
|
+
for (const p of preds.get(id)) c = Math.max(c, col.get(p) + 1);
|
|
291
|
+
col.set(id, c);
|
|
292
|
+
}
|
|
293
|
+
return col;
|
|
294
|
+
};
|
|
295
|
+
let floor = new Map();
|
|
296
|
+
let col = settle(floor);
|
|
297
|
+
let moved = true;
|
|
298
|
+
for (let pass = 0; moved && pass <= ids.length + 1; pass += 1) {
|
|
299
|
+
moved = false;
|
|
300
|
+
for (const lane of created) {
|
|
301
|
+
const out = lane.feeds.filter((f) => laneOf.get(f.to) !== lane && !isIgnored(conns, f));
|
|
302
|
+
if (!out.length) continue;
|
|
303
|
+
let want = Infinity;
|
|
304
|
+
for (const f of out) want = Math.min(want, col.get(f.to) - 1 - col.get(f.from));
|
|
305
|
+
for (let shift = want; shift >= 1; shift -= 1) {
|
|
306
|
+
const trial = new Map(floor);
|
|
307
|
+
for (const id of lane.steps) trial.set(id, col.get(id) + shift);
|
|
308
|
+
const next = settle(trial);
|
|
309
|
+
// A feeder may move up to its join, never move the join.
|
|
310
|
+
if (out.every((f) => next.get(f.to) === col.get(f.to))) {
|
|
311
|
+
floor = trial;
|
|
312
|
+
col = next;
|
|
313
|
+
moved = true;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── lane order: each lane, then its feeders, then its forks ──────────
|
|
321
|
+
const created_ = new Map(created.map((l, i) => [l, i]));
|
|
322
|
+
const children = new Map(created.map((l) => [l, []]));
|
|
323
|
+
for (const l of created) if (l.parent && l.parent !== l) children.get(l.parent).push(l);
|
|
324
|
+
const attach = (l) => {
|
|
325
|
+
if (l.kind === 'fork') return col.get(l.forkOf);
|
|
326
|
+
const f = l.feeds.find((x) => laneOf.get(x.to) === l.parent);
|
|
327
|
+
return f ? col.get(f.to) : 0;
|
|
328
|
+
};
|
|
329
|
+
const ordered = [];
|
|
330
|
+
const placed = new Set();
|
|
331
|
+
const visit = (lane) => {
|
|
332
|
+
if (placed.has(lane)) return;
|
|
333
|
+
placed.add(lane);
|
|
334
|
+
ordered.push(lane);
|
|
335
|
+
const kids = children.get(lane).slice().sort((a, b) => ((a.kind === 'fork') - (b.kind === 'fork'))
|
|
336
|
+
|| (attach(a) - attach(b)) || (created_.get(a) - created_.get(b)));
|
|
337
|
+
for (const k of kids) visit(k);
|
|
338
|
+
};
|
|
339
|
+
for (const l of created) if (!l.parent) visit(l);
|
|
340
|
+
// A lane whose parents lead round in a circle (each feeds the other) has
|
|
341
|
+
// no top: the first of them found is put at the top.
|
|
342
|
+
for (const l of created) if (!placed.has(l)) visit(l);
|
|
343
|
+
const indexOf = new Map(ordered.map((l, i) => [l, i]));
|
|
344
|
+
|
|
345
|
+
const at = {};
|
|
346
|
+
const lanes = ordered.map((l, index) => {
|
|
347
|
+
l.steps.forEach((id, position) => { at[id] = { lane: index, column: col.get(id), position }; });
|
|
348
|
+
return {
|
|
349
|
+
index,
|
|
350
|
+
steps: l.steps.map((id) => ({ id, column: col.get(id) })),
|
|
351
|
+
feeds: l.feeds.map((f) => ({ from: f.from, to: f.to, port: f.port })),
|
|
352
|
+
forkOf: l.forkOf,
|
|
353
|
+
unconnected: l.unconnected,
|
|
354
|
+
parent: l.parent && l.parent !== l ? indexOf.get(l.parent) : null,
|
|
355
|
+
kind: l.kind,
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ── the wires, one a connection, in connection order ─────────────────
|
|
360
|
+
const wires = [];
|
|
361
|
+
for (const c of conns) {
|
|
362
|
+
const base = { from: c.source, to: c.target, toPort: c.targetPort, connection: c.id };
|
|
363
|
+
if (laneFeed.get(c.target) === c) {
|
|
364
|
+
const a = at[c.source];
|
|
365
|
+
const b = at[c.target];
|
|
366
|
+
const next = a.lane === b.lane && b.position === a.position + 1;
|
|
367
|
+
wires.push({ kind: next ? 'lane' : 'fork', ...base });
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const others = ports.get(c.target).inputs.filter((p) => p !== ports.get(c.target).laneInput);
|
|
371
|
+
for (const x of feedsInto.get(c.target) || []) if (!others.includes(x.targetPort)) others.push(x.targetPort);
|
|
372
|
+
const slot = Math.max(0, others.indexOf(c.targetPort));
|
|
373
|
+
wires.push({ kind: 'join', ...base, slot, slots: Math.max(1, others.length), ...(c.ignored ? { cycle: true } : {}) });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const columns = ids.length ? Math.max(...ids.map((id) => col.get(id))) + 1 : 0;
|
|
377
|
+
return { lanes, wires, at, columns, problems };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isIgnored(conns, feed) {
|
|
381
|
+
return conns.some((c) => c.ignored && c.index === feed.index);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── coordinates ──────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
const num = (v) => {
|
|
387
|
+
const r = Math.round(v * 10) / 10;
|
|
388
|
+
return Object.is(r, -0) ? '0' : String(r);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* An orthogonal path through `points`, each inner corner rounded by `radius`
|
|
393
|
+
* (never more than half of either segment it joins).
|
|
394
|
+
*/
|
|
395
|
+
export function roundedPath(points, radius = 10) {
|
|
396
|
+
const pts = points.filter((p, i) => i === 0 || p.x !== points[i - 1].x || p.y !== points[i - 1].y);
|
|
397
|
+
if (!pts.length) return '';
|
|
398
|
+
let d = `M${num(pts[0].x)} ${num(pts[0].y)}`;
|
|
399
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
400
|
+
const p = pts[i];
|
|
401
|
+
const prev = pts[i - 1];
|
|
402
|
+
const next = pts[i + 1];
|
|
403
|
+
if (!next) {
|
|
404
|
+
d += p.x === prev.x ? `V${num(p.y)}` : p.y === prev.y ? `H${num(p.x)}` : `L${num(p.x)} ${num(p.y)}`;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
const inLen = Math.hypot(p.x - prev.x, p.y - prev.y);
|
|
408
|
+
const outLen = Math.hypot(next.x - p.x, next.y - p.y);
|
|
409
|
+
const r = Math.min(radius, inLen / 2, outLen / 2);
|
|
410
|
+
const ux = Math.sign(p.x - prev.x);
|
|
411
|
+
const uy = Math.sign(p.y - prev.y);
|
|
412
|
+
const vx = Math.sign(next.x - p.x);
|
|
413
|
+
const vy = Math.sign(next.y - p.y);
|
|
414
|
+
const a = { x: p.x - ux * r, y: p.y - uy * r };
|
|
415
|
+
const b = { x: p.x + vx * r, y: p.y + vy * r };
|
|
416
|
+
d += a.x === prev.x ? `V${num(a.y)}` : a.y === prev.y ? `H${num(a.x)}` : `L${num(a.x)} ${num(a.y)}`;
|
|
417
|
+
d += `Q${num(p.x)} ${num(p.y)} ${num(b.x)} ${num(b.y)}`;
|
|
418
|
+
pts[i] = b; // the next segment starts where the corner ends
|
|
419
|
+
}
|
|
420
|
+
return d;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Put a layout on a grid: every band, card and wire in pixels.
|
|
425
|
+
*
|
|
426
|
+
* @param {ReturnType<typeof layoutLanes>} layout
|
|
427
|
+
* @param {'regular'|'small'|'strip'|object} [geometry] a name, or numbers over the regular ones
|
|
428
|
+
* @returns {{geometry, width, height,
|
|
429
|
+
* bands: {index, top, height, unconnected}[],
|
|
430
|
+
* cards: {id, lane, column, x, y, w, h}[],
|
|
431
|
+
* wires: {kind, from, to, toPort, connection, d, points, port: {x, y}|null, cycle?}[]}}
|
|
432
|
+
*/
|
|
433
|
+
export function placeLanes(layout, geometry = 'regular') {
|
|
434
|
+
const g = typeof geometry === 'string'
|
|
435
|
+
? (LANE_GEOMETRY[geometry] ?? LANE_GEOMETRY.regular)
|
|
436
|
+
: { ...LANE_GEOMETRY.regular, ...(isObject(geometry) ? geometry : {}) };
|
|
437
|
+
const band = g.height + 2 * g.lanePad;
|
|
438
|
+
const colX = (c) => g.left + c * (g.width + g.gap);
|
|
439
|
+
const laneTop = (l) => l * (band + g.laneGap);
|
|
440
|
+
const cardY = (l) => laneTop(l) + g.lanePad;
|
|
441
|
+
const mid = (l) => cardY(l) + g.height / 2;
|
|
442
|
+
|
|
443
|
+
const bands = layout.lanes.map((l) => ({ index: l.index, top: laneTop(l.index), height: band,
|
|
444
|
+
unconnected: l.unconnected }));
|
|
445
|
+
const cards = [];
|
|
446
|
+
for (const l of layout.lanes) {
|
|
447
|
+
for (const s of l.steps) {
|
|
448
|
+
cards.push({ id: s.id, lane: l.index, column: s.column, x: colX(s.column), y: cardY(l.index),
|
|
449
|
+
w: g.width, h: g.height });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// A straight wire may run through a card that is neither of its ends: a
|
|
453
|
+
// feeder lane that goes on past the card feeding a join, a join fed from
|
|
454
|
+
// the lane ABOVE whose own lane goes on, a fork whose lane is drawn under
|
|
455
|
+
// a feeder. Then the wire takes the GUTTERS instead — the gap between two
|
|
456
|
+
// columns and the gap between two lanes hold no card, by construction.
|
|
457
|
+
const crossesACard = (points, ends) => cards.some((c) => {
|
|
458
|
+
if (ends.includes(c.id)) return false;
|
|
459
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
460
|
+
const p = points[i - 1];
|
|
461
|
+
const q = points[i];
|
|
462
|
+
const x1 = Math.min(p.x, q.x);
|
|
463
|
+
const x2 = Math.max(p.x, q.x);
|
|
464
|
+
const y1 = Math.min(p.y, q.y);
|
|
465
|
+
const y2 = Math.max(p.y, q.y);
|
|
466
|
+
if (x2 > c.x && x1 < c.x + c.w && y2 > c.y && y1 < c.y + c.h) return true;
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
});
|
|
470
|
+
/** The middle of the gap between lane `l` and the lane beside it, below (`+1`) or above (`-1`). */
|
|
471
|
+
const gapBeside = (l, side) => (side > 0 ? laneTop(l) + band + g.laneGap / 2 : laneTop(l) - g.laneGap / 2);
|
|
472
|
+
const wires = layout.wires.map((w) => {
|
|
473
|
+
const a = layout.at[w.from];
|
|
474
|
+
const b = layout.at[w.to];
|
|
475
|
+
const ends = [w.from, w.to];
|
|
476
|
+
let points;
|
|
477
|
+
let port = null;
|
|
478
|
+
if (w.kind === 'lane') {
|
|
479
|
+
points = [{ x: colX(a.column) + g.width - g.tipInset, y: mid(a.lane) },
|
|
480
|
+
{ x: colX(b.column) + g.wireIn, y: mid(b.lane) }];
|
|
481
|
+
} else if (w.kind === 'fork') {
|
|
482
|
+
const fx = colX(a.column) + g.width / 2;
|
|
483
|
+
const down = b.lane > a.lane;
|
|
484
|
+
const fy = down ? cardY(a.lane) + g.height : cardY(a.lane);
|
|
485
|
+
points = [{ x: fx, y: fy }, { x: fx, y: mid(b.lane) }, { x: colX(b.column) + g.wireIn, y: mid(b.lane) }];
|
|
486
|
+
if (b.column > a.column && crossesACard(points, ends)) {
|
|
487
|
+
// Out of the forking card into the gap beside its lane, along it
|
|
488
|
+
// to the gutter before the new lane's first card, along that
|
|
489
|
+
// gutter to the new lane, and in.
|
|
490
|
+
const gy = gapBeside(a.lane, down ? 1 : -1);
|
|
491
|
+
const gx = colX(b.column) - g.gap / 2;
|
|
492
|
+
points = [{ x: fx, y: fy }, { x: fx, y: gy }, { x: gx, y: gy }, { x: gx, y: mid(b.lane) },
|
|
493
|
+
{ x: colX(b.column) + g.wireIn, y: mid(b.lane) }];
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
const tx = colX(b.column) + (g.width * (w.slot + 1)) / (w.slots + 1);
|
|
497
|
+
const sx = colX(a.column) + g.width - g.tipInset;
|
|
498
|
+
if (a.lane !== b.lane && tx > sx) {
|
|
499
|
+
const below = a.lane > b.lane;
|
|
500
|
+
port = { x: tx, y: below ? cardY(b.lane) + g.height : cardY(b.lane) };
|
|
501
|
+
points = [{ x: sx, y: mid(a.lane) }, { x: tx, y: mid(a.lane) }, port];
|
|
502
|
+
if (crossesACard(points, ends)) {
|
|
503
|
+
// Out of the feeding card's tip into the gutter after it, along
|
|
504
|
+
// that gutter to the gap beside the join's lane, along the gap,
|
|
505
|
+
// and into the join's port.
|
|
506
|
+
const gx = colX(a.column) + g.width + g.gap / 2;
|
|
507
|
+
const gy = gapBeside(b.lane, below ? 1 : -1);
|
|
508
|
+
points = [{ x: sx, y: mid(a.lane) }, { x: gx, y: mid(a.lane) }, { x: gx, y: gy },
|
|
509
|
+
{ x: tx, y: gy }, port];
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
// The same lane, or a line that runs back (a cycle): under the
|
|
513
|
+
// source's lane, along the gap, and up into the join's bottom.
|
|
514
|
+
const gy = laneTop(a.lane) + band + g.laneGap / 2;
|
|
515
|
+
const fx = colX(a.column) + g.width / 2;
|
|
516
|
+
port = { x: tx, y: b.lane > a.lane ? cardY(b.lane) : cardY(b.lane) + g.height };
|
|
517
|
+
points = [{ x: fx, y: cardY(a.lane) + g.height }, { x: fx, y: gy }, { x: tx, y: gy }, port];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return { ...w, points, d: roundedPath(points, g.radius), port: g.portDot ? port : null };
|
|
521
|
+
});
|
|
522
|
+
const width = layout.columns ? colX(layout.columns - 1) + g.width + g.right : 0;
|
|
523
|
+
const height = layout.lanes.length ? laneTop(layout.lanes.length - 1) + band : 0;
|
|
524
|
+
return { geometry: g, width, height, bands, cards, wires };
|
|
525
|
+
}
|