@theoremjs/studio 0.3.4-next.482.g7e608f2 → 0.3.4-next.508.geda6db0
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 +3 -3
- package/compile.d.ts +1 -1
- package/draft.d.ts +1 -1
- package/example.js +5 -2
- package/map-line.d.ts +12 -0
- package/map-line.js +98 -0
- package/map.d.ts +1 -1
- package/map.js +0 -1
- package/package.json +4 -4
- package/page/assets/{Banner-CrUhb26_.js → Banner-BcHZFaJb.js} +2 -2
- package/page/assets/{ConsoleFrame-6cEEvO4i.js → ConsoleFrame-Byq_hSpu.js} +3 -3
- package/page/assets/{ContextMenu-WnsgC14_.js → ContextMenu-C6S-GYrc.js} +2 -2
- package/page/assets/{ToolResultCharts-BEhuo0Z4.js → ToolResultCharts-CkEf5ub5.js} +1 -1
- package/page/assets/{TraceInspector-k1w1q0XO.js → TraceInspector-BsYHHhGv.js} +1 -1
- package/page/assets/{TraceOverview-Crxd-sTS.js → TraceOverview-CMFfbfGM.js} +2 -2
- package/page/assets/{_virtual_studio-type-sources-DAabvYbE.js → _virtual_studio-type-sources-CTIGQ210.js} +56 -48
- package/page/assets/browser-engine-DjYv346h.js +11 -0
- package/page/assets/browser-transport-Bm3uY9Fj.js +47 -0
- package/page/assets/{crypto-iT_LZ8Et.js → crypto-DAIn4qnM.js} +1 -1
- package/page/assets/{egress-automata-B5ZGE1mT.js → egress-automata-SiHQTP1i.js} +1 -1
- package/page/assets/{encode-files-8AFMw7Er.js → encode-files-oYzzs_iG.js} +1 -1
- package/page/assets/{export-agent-BedkRp13.js → export-agent-BeUTYowB.js} +3 -3
- package/page/assets/{guardrail-tester-XwnZWNsg.js → guardrail-tester-DLT99rnO.js} +2 -2
- package/page/assets/index-C9rhrCGT.css +1 -0
- package/page/assets/{index-BVHT-9mR.js → index-DNA-Y8Mp.js} +69 -69
- package/page/assets/{labels-provider-BSiD6Lrb.js → labels-provider-Gus3heEH.js} +1 -1
- package/page/assets/{live-messages-B65rUlNM.js → live-messages-BnBioidx.js} +1 -1
- package/page/assets/reader-BFYIhA73.js +7 -0
- package/page/assets/shaped-data-1Qn2111E.js +16 -0
- package/page/assets/{stub-DG5wxM-A.js → stub-B3GersQP.js} +2 -2
- package/page/assets/{studio-code-pane-p4fjtQhL.js → studio-code-pane-CCi7-lyE.js} +3 -3
- package/page/assets/{studio-console-BtZY4aUM.js → studio-console-CJT5FkWm.js} +1 -1
- package/page/assets/{studio-decision-BovTBIwV.js → studio-decision-DhHdHb7Z.js} +1 -1
- package/page/assets/{studio-embedding-BY6UQk0k.js → studio-embedding-B8vBhuZU.js} +1 -1
- package/page/assets/studio-host-console-CE_5_tEn.js +2 -0
- package/page/assets/{studio-labels-C3yQoEd0.js → studio-labels-C2SI_T2p.js} +1 -1
- package/page/assets/{studio-live-B_cVDiqs.js → studio-live-CKSSyYZj.js} +3 -3
- package/page/assets/studio-map-CKm_FCd9.js +1 -0
- package/page/assets/studio-trace-replay-BRMD6pex.js +2 -0
- package/page/assets/{studio-type-checker-Bljo3amv.js → studio-type-checker-DoaPHYNy.js} +1 -1
- package/page/assets/tool-schema-DXYwL5aE.js +18 -0
- package/page/assets/{trace-record-D4xAUHLZ.js → trace-record-DWxIfO7D.js} +1 -1
- package/page/assets/tree-Chi6mqzx.js +3 -0
- package/page/assets/{turn-client-CBWNoFgo.js → turn-client-DF0ije7A.js} +2 -2
- package/page/assets/types-2I4NfLiS.js +1 -0
- package/page/assets/{use-one-call-rh-5EjBB.js → use-one-call-Bhnpve1Y.js} +1 -1
- package/page/assets/{workspace-kAEf4Zn4.js → workspace-U3w2ELtN.js} +1 -1
- package/page/index.html +17 -17
- package/run-payload.d.ts +1 -1
- package/runtime.d.ts +1 -1
- package/runtime.js +1 -1
- package/server/draft-decide.d.ts +1 -4
- package/server/draft-decide.js +1 -1
- package/server/draft-replies.js +2 -2
- package/source.js +1 -1
- package/ui/brand-icons.d.ts +2 -2
- package/ui/brand-icons.js +6 -4
- package/ui/lib/tree-map-layout.d.ts +120 -18
- package/ui/lib/tree-map-layout.js +1292 -190
- package/ui/map-token.css +25 -0
- package/ui/node-icon.js +1 -4
- package/ui/profile-editor.js +1 -1
- package/ui/read-map.css +1 -24
- package/ui/read-map.js +52 -4
- package/ui/studio-map.js +1 -3
- package/ui/tree-map.css +9 -0
- package/ui/tree-map.d.ts +27 -6
- package/ui/tree-map.js +215 -75
- package/page/assets/browser-engine-C78edv3x.js +0 -11
- package/page/assets/browser-transport-mGxdvtBL.js +0 -47
- package/page/assets/index-Cbs7B46A.css +0 -1
- package/page/assets/reader-XebgyCzF.js +0 -7
- package/page/assets/shaped-data-Bc6u4pow.js +0 -16
- package/page/assets/studio-host-console-D9NFWXxV.js +0 -2
- package/page/assets/studio-map-iN6-MOzK.js +0 -1
- package/page/assets/studio-trace-replay-B3k21TSg.js +0 -2
- package/page/assets/tool-schema-BmScISma.js +0 -18
- package/page/assets/tree-BiZScKs7.js +0 -3
- package/page/assets/types-zRlFhC9U.js +0 -1
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { workspaceTree } from "../../mod.js";
|
|
10
10
|
/** A row's height closed; grown, the least and the most it stretches to, to fill the box it fits. */
|
|
11
11
|
const ROW = 32;
|
|
12
|
+
/** Closed, the room before each agent after the first. */
|
|
13
|
+
const AGENT_GAP = 16;
|
|
12
14
|
const GROWN_ROW = 28;
|
|
13
15
|
const TALLEST_ROW = 40;
|
|
14
16
|
/** How far in each level of the tree starts. */
|
|
@@ -16,53 +18,116 @@ export const INDENT = 28;
|
|
|
16
18
|
/** A token's half height. */
|
|
17
19
|
export const HALF = 11;
|
|
18
20
|
export const PAD = 16;
|
|
21
|
+
/** Grown, how much further in from the box's edges the map stands. */
|
|
22
|
+
const INSET = 16;
|
|
23
|
+
/** Grown, the room over the columns their headings take: a title, the line under it, and a gap. */
|
|
24
|
+
export const HEADING = 56;
|
|
25
|
+
/** Grown, each column's heading, in order: the tree, the registries, the runner. */
|
|
26
|
+
export const HEADINGS = [
|
|
27
|
+
{ label: 'Profile', note: 'The contract you define' },
|
|
28
|
+
{ label: 'Registry', note: 'The pieces you provide' },
|
|
29
|
+
{ label: 'Runner', note: 'What runs your contract' },
|
|
30
|
+
];
|
|
19
31
|
/** Closed, between the tree's widest row and the feeds; grown, the least and most between columns. */
|
|
20
32
|
const GAP = 72;
|
|
21
33
|
const LEAST_GAP = 56;
|
|
22
|
-
const MOST_GAP =
|
|
23
|
-
/** The
|
|
34
|
+
const MOST_GAP = 200;
|
|
35
|
+
/** The least room between two lanes. */
|
|
36
|
+
const LANE = 9;
|
|
37
|
+
/** Closed, how far left of the feeds the lines from a row run down to them, as one. */
|
|
38
|
+
const TRUNK_GAP = 24;
|
|
39
|
+
/** The types of tool the map draws, in the order it draws them. */
|
|
24
40
|
const TOOL_TYPES = ['function', 'http', 'mcp', 'agent'];
|
|
25
|
-
/**
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
/** The tools a remote call reaches the network for. */
|
|
42
|
+
const REMOTE_TYPES = ['http', 'mcp'];
|
|
43
|
+
/** By how far grown the map's frame has eased to its grown size and place. */
|
|
44
|
+
const MOVE_TO = 0.2;
|
|
45
|
+
/** What the still shows and growing takes away is gone by this far grown. */
|
|
46
|
+
export const LEAVE_TO = 0.2;
|
|
47
|
+
/** How far grown the drawing starts from the tree's agents: once the frame has settled. */
|
|
48
|
+
const DRAW_FROM = MOVE_TO;
|
|
49
|
+
/**
|
|
50
|
+
* How long nodes of the still take to glide into place, as a length of line: they go as the
|
|
51
|
+
* drawing comes to the node they are reached from, and its lines to them wait for them.
|
|
52
|
+
*/
|
|
53
|
+
const GLIDE = 280;
|
|
54
|
+
/** How long a node growing adds takes to show once its line has reached it, as a length of line. */
|
|
55
|
+
const SHOW = 180;
|
|
56
|
+
/** How far clear of a node of the still what shows near it stands. */
|
|
57
|
+
const ROOM = 12;
|
|
58
|
+
/** How much of its glide a node of the still has done when what would land on it starts to show. */
|
|
59
|
+
const AHEAD = 0.35;
|
|
60
|
+
/** How far a node of the still that made room glides on once its line reaches it, as a share of a glide. */
|
|
61
|
+
const LAND = 0.3;
|
|
62
|
+
/** How much of its showing a node has done as its line reaches it, so the line meets it there. */
|
|
63
|
+
const MET = 0.35;
|
|
64
|
+
/** How much of its showing a node has done when its lines start to leave it. */
|
|
65
|
+
const SHOWN = 0.4;
|
|
66
|
+
/** How far a node growing adds drifts in along its line as it shows. */
|
|
67
|
+
const DRIFT = 14;
|
|
68
|
+
/** How long the drawing rests at each node before its lines leave it, as a length of line. */
|
|
69
|
+
const PAUSE = 40;
|
|
36
70
|
/** Space kept clear round a token. */
|
|
37
71
|
const CLEAR = 2;
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Of a still node's glide into a slot, the share of it the node goes across before it starts down
|
|
74
|
+
* or up, and the share it has left to go across once it has.
|
|
75
|
+
*/
|
|
76
|
+
const SINK = 0.3;
|
|
77
|
+
export const TOOL_RUNTIME = 'tool-runtime';
|
|
78
|
+
export const GUARDRAIL_RUNTIME = 'guardrail-runtime';
|
|
79
|
+
export const TRACE_DESTINATIONS = 'trace-destinations';
|
|
80
|
+
export const PROVIDER_STREAM = 'provider-stream';
|
|
81
|
+
export const KEY_VAULT = 'key-vault';
|
|
82
|
+
export const TURN_LOOP = 'turn-loop';
|
|
40
83
|
/**
|
|
41
84
|
* What the runner does in a turn, in order: the steps of the kernel contract's turn lifecycle
|
|
42
|
-
* (docs/contracts/kernel.md, "Turn lifecycle") that do work.
|
|
85
|
+
* (docs/contracts/kernel.md, "Turn lifecycle") that do work, and the profile sections each reads.
|
|
43
86
|
* why: The canary bind runs only while a canary detector is on, and pre_turn, before_end and
|
|
44
|
-
* post_turn are hooks for the host's stages, so the map leaves those four out
|
|
87
|
+
* post_turn are hooks for the host's stages, so the map leaves those four out; the structured
|
|
88
|
+
* reply's validation is the outputs' own, not a story of the runner's, so it is left out too.
|
|
89
|
+
* Sanitize and egress are named for what they guard, the input and the reply, and drawn from them.
|
|
45
90
|
*/
|
|
46
91
|
export const TURN_STEPS = [
|
|
47
|
-
{ id: '
|
|
48
|
-
{ id: '
|
|
49
|
-
{ id: '
|
|
50
|
-
{ id: 'stream', label: 'Provider stream' },
|
|
51
|
-
{ id: 'tools', label: 'Tool loop' },
|
|
52
|
-
{ id: 'validation', label: 'Validation' },
|
|
53
|
-
{ id: 'egress', label: 'Egress' },
|
|
54
|
-
{ id: 'trace', label: 'Trace' },
|
|
92
|
+
{ id: 'sanitize', label: 'Input guard', reads: ['inputs'] },
|
|
93
|
+
{ id: 'egress', label: 'Reply guard', reads: ['outputs'] },
|
|
94
|
+
{ id: 'trace', label: 'Trace exporter', reads: ['observability'] },
|
|
55
95
|
];
|
|
56
96
|
/**
|
|
57
|
-
* What the tool runner
|
|
58
|
-
*
|
|
97
|
+
* What the tool runner does with every call (`executeRegisteredTool`): its permission, the MCP
|
|
98
|
+
* session an MCP call runs in, and its sign-in.
|
|
99
|
+
* why: The sessions stand before the sign-in, level with the third tool, so an MCP tool there has a
|
|
100
|
+
* straight line to them.
|
|
59
101
|
*/
|
|
60
102
|
export const TOOL_CHECKS = [
|
|
61
103
|
{ id: 'permission', label: 'Permission gate' },
|
|
62
|
-
{ id: '
|
|
63
|
-
{ id: '
|
|
64
|
-
|
|
104
|
+
{ id: 'sessions', label: 'MCP sessions' },
|
|
105
|
+
{ id: 'sign-in', label: 'Authentication' },
|
|
106
|
+
];
|
|
107
|
+
/** What the guard runner does with a call: the network a remote call reaches, and the guard on its result. */
|
|
108
|
+
const GUARDRAIL_CHECKS = [
|
|
109
|
+
{ id: 'network', label: 'Network guard' },
|
|
110
|
+
{ id: 'result', label: 'Tool result guard' },
|
|
111
|
+
];
|
|
112
|
+
/**
|
|
113
|
+
* The order of an agent's rows: what it runs on, how it turns, its tools, its guardrails, then what
|
|
114
|
+
* goes in and out of it, level with the guards on them; any other section after, observability last.
|
|
115
|
+
*/
|
|
116
|
+
const ROW_ORDER = [
|
|
117
|
+
'identity',
|
|
118
|
+
'models',
|
|
119
|
+
'decision',
|
|
120
|
+
'image',
|
|
121
|
+
'speech',
|
|
122
|
+
'live',
|
|
123
|
+
'turnBehaviour',
|
|
124
|
+
'tools',
|
|
125
|
+
'guardrails',
|
|
126
|
+
'inputs',
|
|
127
|
+
'outputs',
|
|
65
128
|
];
|
|
129
|
+
/** Closed, the map shows only these beside the tree: each agent's models and its tools. */
|
|
130
|
+
const CLOSED = ['model', 'tool'];
|
|
66
131
|
/** A row's token id: the tree's ids repeat from one agent to the next. */
|
|
67
132
|
export function rowId(row) {
|
|
68
133
|
return `${row.agent}/${row.node.id}`;
|
|
@@ -78,8 +143,18 @@ function rowsOf(workspace, only) {
|
|
|
78
143
|
...(parent ? { parent: rowId(parent) } : {}),
|
|
79
144
|
};
|
|
80
145
|
rows.push(row);
|
|
81
|
-
|
|
146
|
+
// why: The models are named once, under their providers beside the tree; the Models row links to
|
|
147
|
+
// them. Observability comes last, level with the trace destinations and the trace exporter at
|
|
148
|
+
// the foot of the map.
|
|
149
|
+
const children = node.children.filter((child) => child.ref.facet !== 'modelBinding');
|
|
150
|
+
const rank = (child) => child.ref.facet === 'observability'
|
|
151
|
+
? ROW_ORDER.length + 1
|
|
152
|
+
: ROW_ORDER.includes(child.ref.facet)
|
|
153
|
+
? ROW_ORDER.indexOf(child.ref.facet)
|
|
154
|
+
: ROW_ORDER.length;
|
|
155
|
+
for (const child of children.toSorted((a, b) => rank(a) - rank(b))) {
|
|
82
156
|
walk(child, agent, depth + 1, row);
|
|
157
|
+
}
|
|
83
158
|
};
|
|
84
159
|
workspaceTree(workspace).agents.forEach((root, index) => {
|
|
85
160
|
const key = workspace.agents[index]?.key ?? '';
|
|
@@ -88,127 +163,287 @@ function rowsOf(workspace, only) {
|
|
|
88
163
|
});
|
|
89
164
|
return rows;
|
|
90
165
|
}
|
|
166
|
+
/** A tool's feed, with a line from each row in `from`, and an agent tool's agent under it. */
|
|
167
|
+
function toolFeeds(workspace, id, spec, from) {
|
|
168
|
+
const tool = {
|
|
169
|
+
id,
|
|
170
|
+
label: spec.toolName,
|
|
171
|
+
kind: 'tool',
|
|
172
|
+
of: spec.toolType,
|
|
173
|
+
from,
|
|
174
|
+
column: 1,
|
|
175
|
+
group: 'tools',
|
|
176
|
+
};
|
|
177
|
+
const agent = workspace.agents.find((each) => each.key === spec.agentKey);
|
|
178
|
+
if (spec.toolType !== 'agent' || !agent)
|
|
179
|
+
return [tool];
|
|
180
|
+
return [
|
|
181
|
+
tool,
|
|
182
|
+
{
|
|
183
|
+
id: `${id}/agent`,
|
|
184
|
+
label: agent.identity.agentId || agent.identity.handle,
|
|
185
|
+
kind: 'subagent',
|
|
186
|
+
of: agent.identity.profileType,
|
|
187
|
+
from: [],
|
|
188
|
+
under: id,
|
|
189
|
+
column: 1,
|
|
190
|
+
group: 'tools',
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
}
|
|
91
194
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
195
|
+
* Each provider with the sources of each of its protocols, and each model once under its provider,
|
|
196
|
+
* with a line from each source that runs on it.
|
|
197
|
+
*/
|
|
198
|
+
function modelsOf(modelled) {
|
|
199
|
+
const providers = new Map();
|
|
200
|
+
// why: Agents that run on the same model share it, as they share a tool: one model, with a line
|
|
201
|
+
// from each Models row.
|
|
202
|
+
const models = new Map();
|
|
203
|
+
for (const { agent, source } of modelled) {
|
|
204
|
+
for (const binding of agent.modelBindings) {
|
|
205
|
+
const protocols = providers.get(binding.provider) ?? new Map();
|
|
206
|
+
protocols.set(binding.protocol, [...(protocols.get(binding.protocol) ?? []), source]);
|
|
207
|
+
providers.set(binding.provider, protocols);
|
|
208
|
+
const id = `model:${binding.provider}:${binding.apiId}:${binding.modelId}`;
|
|
209
|
+
const shared = models.get(id);
|
|
210
|
+
if (shared) {
|
|
211
|
+
if (!shared.from.includes(source))
|
|
212
|
+
shared.from.push(source);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
models.set(id, {
|
|
216
|
+
id,
|
|
217
|
+
label: binding.modelId,
|
|
218
|
+
kind: 'model',
|
|
219
|
+
of: binding.apiId,
|
|
220
|
+
from: [source],
|
|
221
|
+
under: `provider:${binding.provider}`,
|
|
222
|
+
column: 1,
|
|
223
|
+
group: 'providers',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { providers, models };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Beside the tree, shared by every agent drawn, two columns. First, the registries: each tool the
|
|
231
|
+
* agents allow, by type, with a line from each Tools row that allows it, and an agent tool's
|
|
232
|
+
* agent under it; each provider, with its models under it, each with a line from its Models row; and the trace
|
|
233
|
+
* destinations, with a line from the observability rows. Then the runner: the provider protocols, a
|
|
234
|
+
* head over each provider's protocols, each with a line from its provider; the tool runner, with a line from
|
|
235
|
+
* each tool, and what it does with each call under it; the guard runner, with a line from the
|
|
236
|
+
* guardrails, and what it does under it; and the trace exporter, with a line from the trace
|
|
237
|
+
* destinations.
|
|
98
238
|
*/
|
|
99
239
|
function feedsOf(workspace, rows) {
|
|
100
240
|
const drawn = workspace.agents.filter((agent) => rows.some((row) => row.agent === agent.key));
|
|
101
241
|
const rowOf = (agent, facet, key) => rows.find((row) => row.agent === agent &&
|
|
102
242
|
row.node.ref.facet === facet &&
|
|
103
243
|
(key === undefined || ('key' in row.node.ref && row.node.ref.key === key)));
|
|
244
|
+
const sections = (facets) => rows.filter((row) => facets.includes(row.node.ref.facet)).map(rowId);
|
|
104
245
|
const feeds = [];
|
|
105
|
-
const providers = new Map();
|
|
106
|
-
for (const agent of drawn) {
|
|
107
|
-
for (const binding of agent.modelBindings) {
|
|
108
|
-
const row = rowOf(agent.key, 'modelBinding', binding.key);
|
|
109
|
-
if (!row)
|
|
110
|
-
continue;
|
|
111
|
-
const provider = providers.get(binding.provider) ?? { into: [], protocols: new Set() };
|
|
112
|
-
provider.into.push(rowId(row));
|
|
113
|
-
provider.protocols.add(binding.protocol);
|
|
114
|
-
providers.set(binding.provider, provider);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for (const [provider, { into, protocols }] of providers) {
|
|
118
|
-
const id = `provider:${provider}`;
|
|
119
|
-
feeds.push({ id, label: provider, kind: 'provider', into, column: 1, group: 'providers' });
|
|
120
|
-
for (const protocol of protocols) {
|
|
121
|
-
const nested = { id: `${id}/${protocol}`, label: protocol, kind: 'protocol' };
|
|
122
|
-
feeds.push({ ...nested, into: [], under: id, column: 1, group: 'providers' });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
246
|
const allowing = (key) => drawn.flatMap((agent) => {
|
|
126
247
|
const row = agent.tools.allow.includes(key) ? rowOf(agent.key, 'tools') : undefined;
|
|
127
248
|
return row ? [rowId(row)] : [];
|
|
128
249
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
250
|
+
// The tools the agents allow, by type, each type's in the workspace's order.
|
|
251
|
+
const allowed = TOOL_TYPES.flatMap((type) => workspace.toolSpecs.filter((each) => each.toolType === type && allowing(each.key).length > 0));
|
|
252
|
+
// why: An agent tool's agent runs on a model of its own, so its models stand under their
|
|
253
|
+
// providers too, each with a line from the agent under the tool, when the tree does not draw it.
|
|
254
|
+
const asked = allowed.flatMap((tool) => {
|
|
255
|
+
const agent = drawn.some((each) => each.key === tool.agentKey)
|
|
256
|
+
? undefined
|
|
257
|
+
: workspace.agents.find((each) => each.key === tool.agentKey);
|
|
258
|
+
return agent ? [{ agent, source: `tool:${tool.key}/agent` }] : [];
|
|
259
|
+
});
|
|
260
|
+
const modelled = [
|
|
261
|
+
...drawn.flatMap((agent) => {
|
|
262
|
+
const section = rowOf(agent.key, 'models');
|
|
263
|
+
return section ? [{ agent, source: rowId(section) }] : [];
|
|
264
|
+
}),
|
|
265
|
+
...asked,
|
|
266
|
+
];
|
|
267
|
+
const { providers, models } = modelsOf(modelled);
|
|
268
|
+
const protocols = [];
|
|
269
|
+
for (const [provider, byProtocol] of providers) {
|
|
270
|
+
const id = `provider:${provider}`;
|
|
135
271
|
feeds.push({
|
|
136
272
|
id,
|
|
137
|
-
label:
|
|
138
|
-
kind: '
|
|
139
|
-
|
|
140
|
-
|
|
273
|
+
label: provider,
|
|
274
|
+
kind: 'provider',
|
|
275
|
+
// why: The Models rows link to the models under it, not to the provider.
|
|
276
|
+
from: [],
|
|
141
277
|
column: 1,
|
|
142
|
-
group: '
|
|
278
|
+
group: 'providers',
|
|
143
279
|
});
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
id: `${id}/agent`,
|
|
154
|
-
label,
|
|
155
|
-
kind: 'subagent',
|
|
156
|
-
of: agent.identity.profileType,
|
|
157
|
-
joins: RUNNER,
|
|
280
|
+
for (const protocol of byProtocol.keys()) {
|
|
281
|
+
protocols.push({
|
|
282
|
+
id: `${id}/${protocol}`,
|
|
283
|
+
label: protocol,
|
|
284
|
+
kind: 'protocol',
|
|
285
|
+
from: [id],
|
|
286
|
+
under: PROVIDER_STREAM,
|
|
287
|
+
column: 2,
|
|
288
|
+
group: 'protocols',
|
|
158
289
|
});
|
|
159
290
|
}
|
|
160
291
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
292
|
+
// why: The key vault holds the keys the protocols call providers with and the tools sign in
|
|
293
|
+
// with, so it stands between the providers and the tools.
|
|
294
|
+
if (protocols.length > 0 || allowed.length > 0) {
|
|
295
|
+
feeds.push({
|
|
296
|
+
id: KEY_VAULT,
|
|
297
|
+
label: 'Key vault',
|
|
298
|
+
kind: 'vault',
|
|
299
|
+
from: [],
|
|
300
|
+
column: 1,
|
|
301
|
+
group: 'vault',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const tools = allowed.map((spec) => ({ id: `tool:${spec.key}`, spec }));
|
|
305
|
+
for (const { id, spec } of tools)
|
|
306
|
+
feeds.push(...toolFeeds(workspace, id, spec, allowing(spec.key)));
|
|
307
|
+
const stepOf = (id) => TURN_STEPS.find((step) => step.id === id) ?? { id, label: id, reads: [] };
|
|
308
|
+
const guardrails = sections(['guardrails']);
|
|
309
|
+
const observability = sections(['observability']);
|
|
310
|
+
// Registries: each provider, its models under it, and the trace destinations.
|
|
311
|
+
feeds.push(...models.values());
|
|
312
|
+
feeds.push({
|
|
313
|
+
id: TRACE_DESTINATIONS,
|
|
314
|
+
label: 'Trace destinations',
|
|
315
|
+
kind: 'destinations',
|
|
316
|
+
from: observability,
|
|
317
|
+
column: 1,
|
|
318
|
+
group: 'destinations',
|
|
165
319
|
});
|
|
166
|
-
|
|
167
|
-
|
|
320
|
+
// The runner: the provider protocols, one under another, then the turn loop, the tool runner and
|
|
321
|
+
// the guard runner with what each does under it, then the trace exporter.
|
|
322
|
+
if (protocols.length > 0) {
|
|
168
323
|
feeds.push({
|
|
169
|
-
id:
|
|
170
|
-
label:
|
|
324
|
+
id: PROVIDER_STREAM,
|
|
325
|
+
label: 'Provider protocols',
|
|
326
|
+
kind: 'stream',
|
|
327
|
+
// why: The protocols call each provider with the key the vault holds for it.
|
|
328
|
+
from: [KEY_VAULT],
|
|
329
|
+
column: 2,
|
|
330
|
+
group: 'protocols',
|
|
331
|
+
});
|
|
332
|
+
feeds.push(...protocols);
|
|
333
|
+
}
|
|
334
|
+
// why: The turn loop runs the steps of a turn as the turn behaviour sets them: where the step
|
|
335
|
+
// limit stops it, and whether a cut-off reply continues or the user steers it.
|
|
336
|
+
const turnBehaviour = sections(['turnBehaviour']);
|
|
337
|
+
if (turnBehaviour.length > 0) {
|
|
338
|
+
feeds.push({
|
|
339
|
+
id: TURN_LOOP,
|
|
340
|
+
label: 'Turn loop',
|
|
171
341
|
kind: 'step',
|
|
172
|
-
of:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
group: 'runner',
|
|
177
|
-
...(step.id === 'tools' ? { joins: TOOL_RUNNER } : {}),
|
|
342
|
+
of: 'loop',
|
|
343
|
+
from: turnBehaviour,
|
|
344
|
+
column: 2,
|
|
345
|
+
group: 'turn-loop',
|
|
178
346
|
});
|
|
179
347
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
348
|
+
const toolsOf = (types) => tools.filter(({ spec }) => types.includes(spec.toolType)).map(({ id }) => id);
|
|
349
|
+
const checkFrom = {
|
|
350
|
+
'sign-in': [
|
|
351
|
+
...tools.filter(({ spec }) => (spec.authType ?? 'none') !== 'none').map(({ id }) => id),
|
|
352
|
+
// why: A tool's sign-in reads its key or its resolver from the vault.
|
|
353
|
+
...(tools.length > 0 ? [KEY_VAULT] : []),
|
|
354
|
+
],
|
|
355
|
+
network: toolsOf(REMOTE_TYPES),
|
|
356
|
+
sessions: toolsOf(['mcp']),
|
|
357
|
+
// why: The tool runner hands each result to its guard before it goes back to the model.
|
|
358
|
+
result: tools.length > 0 ? [TOOL_RUNTIME] : [],
|
|
359
|
+
};
|
|
360
|
+
feeds.push({
|
|
361
|
+
id: TOOL_RUNTIME,
|
|
362
|
+
label: 'Tool runner',
|
|
363
|
+
kind: 'tool-runtime',
|
|
364
|
+
from: tools.map(({ id }) => id),
|
|
365
|
+
column: 2,
|
|
366
|
+
group: 'tool-runtime',
|
|
367
|
+
});
|
|
182
368
|
for (const check of TOOL_CHECKS) {
|
|
183
|
-
const nested = { id: `${TOOL_RUNNER}/${check.id}`, label: check.label, of: check.id };
|
|
184
369
|
feeds.push({
|
|
185
|
-
|
|
370
|
+
id: `${TOOL_RUNTIME}/${check.id}`,
|
|
371
|
+
label: check.label,
|
|
372
|
+
kind: check.id === 'sessions' ? 'branch' : 'check',
|
|
373
|
+
of: check.id,
|
|
374
|
+
from: checkFrom[check.id] ?? [],
|
|
375
|
+
under: TOOL_RUNTIME,
|
|
376
|
+
column: 2,
|
|
377
|
+
group: 'tool-runtime',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
feeds.push({
|
|
381
|
+
id: GUARDRAIL_RUNTIME,
|
|
382
|
+
label: 'Guard runner',
|
|
383
|
+
kind: 'guardrail-runtime',
|
|
384
|
+
from: guardrails,
|
|
385
|
+
column: 2,
|
|
386
|
+
group: 'guardrail-runtime',
|
|
387
|
+
});
|
|
388
|
+
for (const id of ['sanitize', 'egress']) {
|
|
389
|
+
const step = stepOf(id);
|
|
390
|
+
feeds.push({
|
|
391
|
+
id: `${GUARDRAIL_RUNTIME}/${id}`,
|
|
392
|
+
label: step.label,
|
|
393
|
+
kind: 'step',
|
|
394
|
+
of: id,
|
|
395
|
+
from: sections(step.reads),
|
|
396
|
+
under: GUARDRAIL_RUNTIME,
|
|
397
|
+
column: 2,
|
|
398
|
+
group: 'guardrail-runtime',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
for (const check of GUARDRAIL_CHECKS) {
|
|
402
|
+
feeds.push({
|
|
403
|
+
id: `${GUARDRAIL_RUNTIME}/${check.id}`,
|
|
404
|
+
label: check.label,
|
|
186
405
|
kind: 'check',
|
|
187
|
-
|
|
188
|
-
|
|
406
|
+
of: check.id,
|
|
407
|
+
from: checkFrom[check.id] ?? [],
|
|
408
|
+
under: GUARDRAIL_RUNTIME,
|
|
189
409
|
column: 2,
|
|
190
|
-
group: '
|
|
410
|
+
group: 'guardrail-runtime',
|
|
191
411
|
});
|
|
192
412
|
}
|
|
413
|
+
feeds.push({
|
|
414
|
+
id: 'telemetry',
|
|
415
|
+
label: stepOf('trace').label,
|
|
416
|
+
kind: 'step',
|
|
417
|
+
of: 'trace',
|
|
418
|
+
from: [TRACE_DESTINATIONS],
|
|
419
|
+
column: 2,
|
|
420
|
+
group: 'telemetry',
|
|
421
|
+
});
|
|
193
422
|
return feeds;
|
|
194
423
|
}
|
|
195
424
|
const clamp = (value) => Math.min(Math.max(value, 0), 1);
|
|
425
|
+
/** Slow out, slow in: how far a move or a draw has gone at `at` of its time, both from 0 to 1. */
|
|
426
|
+
/** A gentle ease in and out, for what moves far: it never rushes through the middle. */
|
|
427
|
+
export const soft = (at) => (1 - Math.cos(Math.PI * clamp(at))) / 2;
|
|
428
|
+
/** An ease that comes in quickly and settles slowly, for what drifts the last of its way. */
|
|
429
|
+
export const settling = (at) => 1 - (1 - clamp(at)) ** 3;
|
|
430
|
+
/** How far the map's frame has settled this far grown, 0 to 1: its size and place, and what holds it. */
|
|
431
|
+
export const settledAt = (grow) => soft(grow / MOVE_TO);
|
|
432
|
+
export const ease = (at) => {
|
|
433
|
+
const t = clamp(at);
|
|
434
|
+
return t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
|
|
435
|
+
};
|
|
196
436
|
const between = (from, to, at) => from + (to - from) * at;
|
|
197
|
-
/** A
|
|
198
|
-
function slotsOf(feeds
|
|
437
|
+
/** A node under a feed a level in, a row down for each before it. */
|
|
438
|
+
function slotsOf(feeds) {
|
|
199
439
|
const slots = new Map();
|
|
200
|
-
const steps = feeds.filter((feed) => feed.kind === 'step');
|
|
201
|
-
const stepColumn = Math.max(0, ...steps.map((step) => width(step.id))) + STEP_GAP;
|
|
202
440
|
const counts = new Map();
|
|
203
441
|
for (const feed of feeds) {
|
|
204
442
|
if (feed.under === undefined)
|
|
205
443
|
continue;
|
|
206
444
|
const index = counts.get(feed.under) ?? 0;
|
|
207
445
|
counts.set(feed.under, index + 1);
|
|
208
|
-
|
|
209
|
-
? { across: Math.floor(index / STEPS_DOWN) * stepColumn, down: (index % STEPS_DOWN) + 1 }
|
|
210
|
-
: { across: 0, down: index + 1 };
|
|
211
|
-
slots.set(feed.id, { holder: feed.under, across: INDENT + slot.across, down: slot.down });
|
|
446
|
+
slots.set(feed.id, { holder: feed.under, across: INDENT, down: index + 1 });
|
|
212
447
|
}
|
|
213
448
|
return slots;
|
|
214
449
|
}
|
|
@@ -237,18 +472,48 @@ function stacksOf(rows, feeds, slots) {
|
|
|
237
472
|
});
|
|
238
473
|
return { held, columns: [stack(tree), ...columns] };
|
|
239
474
|
}
|
|
475
|
+
/** The tallest row a grown map can take that `fits`, else the least. */
|
|
476
|
+
function fittingRow(fits) {
|
|
477
|
+
for (let step = TALLEST_ROW; step >= GROWN_ROW; step -= 1) {
|
|
478
|
+
if (fits(step))
|
|
479
|
+
return step;
|
|
480
|
+
}
|
|
481
|
+
return GROWN_ROW;
|
|
482
|
+
}
|
|
240
483
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* between groups widen, so the map fills `box`; without one, rows and gaps are at their least.
|
|
484
|
+
* Moves the last row and `foot` down by `shift`, and the rows before it with no line of their own
|
|
485
|
+
* to stand a `step` apart over it.
|
|
244
486
|
*/
|
|
245
|
-
function
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
487
|
+
function liftTail(rows, lined, lastRow, ys, foot, shift, step) {
|
|
488
|
+
const tail = [lastRow];
|
|
489
|
+
for (const each of rows.toReversed().slice(1)) {
|
|
490
|
+
if (lined.has(rowId(each)) || each.agent !== lastRow.agent)
|
|
491
|
+
break;
|
|
492
|
+
tail.push(each);
|
|
493
|
+
}
|
|
494
|
+
const last = (ys.get(rowId(lastRow)) ?? 0) + Math.max(0, shift);
|
|
495
|
+
for (const [index, each] of tail.entries())
|
|
496
|
+
ys.set(rowId(each), last - index * step);
|
|
497
|
+
if (shift > 0)
|
|
498
|
+
for (const block of foot)
|
|
499
|
+
block.top += shift;
|
|
500
|
+
}
|
|
501
|
+
/** Opens `spare` room between the agents, each a zone of its own. */
|
|
502
|
+
function spreadAgents(rows, tree, ys, blocks, spare) {
|
|
503
|
+
const agents = [...new Set(rows.map((each) => each.agent))];
|
|
504
|
+
if (agents.length <= 1 || spare <= 0)
|
|
505
|
+
return;
|
|
506
|
+
const by = (agent) => (spare * agents.indexOf(agent)) / (agents.length - 1);
|
|
507
|
+
for (const each of rows)
|
|
508
|
+
ys.set(rowId(each), (ys.get(rowId(each)) ?? 0) + by(each.agent));
|
|
509
|
+
for (const block of blocks) {
|
|
510
|
+
const anchor = block.anchor && tree.get(block.anchor.row);
|
|
511
|
+
if (anchor)
|
|
512
|
+
block.top += by(anchor.agent);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/** Where each column starts: the tree, the registries, the runner. */
|
|
516
|
+
function columnLefts(rows, feeds, slots, width, box) {
|
|
252
517
|
const right = (id, x) => x + width(id);
|
|
253
518
|
const treeRight = Math.max(0, ...rows.map((each) => right(rowId(each), each.x)));
|
|
254
519
|
const columnWidth = (column) => Math.max(0, ...feeds
|
|
@@ -258,84 +523,913 @@ function grownPlaces(rows, feeds, slots, width, box) {
|
|
|
258
523
|
return right(feed.id, slot ? slot.across : 0);
|
|
259
524
|
}));
|
|
260
525
|
const [first, second] = [columnWidth(1), columnWidth(2)];
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
526
|
+
const taken = treeRight - PAD + first + second;
|
|
527
|
+
// A gap is wide enough for a lane into each feed of the column after it, and the spare is shared.
|
|
528
|
+
const gaps = [1, 2].map((column) => {
|
|
529
|
+
const lanes = feeds.filter((feed) => feed.column === column && feed.from.length + (feed.late?.length ?? 0) > 0).length;
|
|
530
|
+
return Math.max(LEAST_GAP, (lanes + 1) * LANE);
|
|
531
|
+
});
|
|
532
|
+
const [one = 0, two = 0] = gaps;
|
|
533
|
+
const spare = box ? Math.max(0, box.width - 2 * (PAD + INSET) - taken - one - two) / 2 : 0;
|
|
534
|
+
const widen = (gap) => Math.max(gap, Math.min(gap + spare, MOST_GAP));
|
|
535
|
+
return [0, treeRight + widen(one), treeRight + widen(one) + first + widen(two)];
|
|
536
|
+
}
|
|
537
|
+
/** Stands each block as near its `want` as it can, the blocks in order a row apart, inside the map. */
|
|
538
|
+
function keepInside(steps, start, end, row) {
|
|
539
|
+
let floor = start;
|
|
540
|
+
for (const block of steps) {
|
|
541
|
+
block.top = Math.max(block.want, floor);
|
|
542
|
+
floor = block.top + block.size + 2 * row;
|
|
543
|
+
}
|
|
544
|
+
let ceiling = end;
|
|
545
|
+
for (const block of steps.toReversed()) {
|
|
546
|
+
block.top = Math.max(Math.min(block.top, ceiling - block.size), start);
|
|
547
|
+
ceiling = block.top - 2 * row;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* The map grown: the tree, each row with lines into a registry level with it; beside it the
|
|
552
|
+
* registries, then the runner, each block centred on its sources. Rows stretch, up to a most, and
|
|
553
|
+
* the gaps between columns and between groups widen, so the map fills `box`; without one, rows and
|
|
554
|
+
* gaps are at their least.
|
|
555
|
+
*/
|
|
556
|
+
function grownPlaces(rows, feeds, slots, width, box) {
|
|
557
|
+
const { held, columns } = stacksOf(rows, feeds, slots);
|
|
558
|
+
const [, registries, runner] = columns;
|
|
559
|
+
const start = PAD + INSET + HEADING + HALF;
|
|
560
|
+
const room = box ? box.height - 2 * (PAD + INSET + HALF) - HEADING : 0;
|
|
561
|
+
const byId = new Map(feeds.map((feed) => [feed.id, feed]));
|
|
562
|
+
const sourcesOf = (id) => {
|
|
563
|
+
const feed = byId.get(id);
|
|
564
|
+
return feed ? [...feed.from, ...(feed.late ?? [])] : [];
|
|
565
|
+
};
|
|
265
566
|
const tree = new Map(rows.map((each) => [rowId(each), each]));
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
567
|
+
const lined = new Set(feeds.flatMap((feed) => sourcesOf(feed.id)));
|
|
568
|
+
// The rows with lines straight into the runner, each run of them level with the block they feed.
|
|
569
|
+
const toRunner = new Set(feeds.filter((feed) => feed.column === 2).flatMap((feed) => sourcesOf(feed.id)));
|
|
570
|
+
const opens = new Set(rows
|
|
571
|
+
.filter((each, index) => {
|
|
572
|
+
const before = rows[index - 1];
|
|
573
|
+
return toRunner.has(rowId(each)) && !(before && toRunner.has(rowId(before)));
|
|
574
|
+
})
|
|
575
|
+
.map(rowId));
|
|
576
|
+
/** A column's groups, each a block of rows `step` apart, its heads' nested feeds under them. */
|
|
577
|
+
const blocksOf = (stack, step) => (stack?.ids ?? []).map((group) => {
|
|
578
|
+
const offsets = new Map();
|
|
579
|
+
let size = 0;
|
|
580
|
+
for (const id of group) {
|
|
581
|
+
offsets.set(id, size);
|
|
582
|
+
for (const [nested, slot] of slots) {
|
|
583
|
+
if (slot.holder === id)
|
|
584
|
+
offsets.set(nested, size + slot.down * step);
|
|
276
585
|
}
|
|
277
|
-
|
|
586
|
+
size += step * (1 + (held.get(id) ?? 0));
|
|
278
587
|
}
|
|
588
|
+
// A registry block stands level with the first tree row any of its feeds has a line from.
|
|
589
|
+
const anchor = [...offsets]
|
|
590
|
+
.toSorted((a, b) => a[1] - b[1])
|
|
591
|
+
.flatMap(([id, offset]) => {
|
|
592
|
+
const row = sourcesOf(id).find((source) => tree.has(source));
|
|
593
|
+
return row ? [{ row, offset }] : [];
|
|
594
|
+
})[0];
|
|
595
|
+
return { group, offsets, size: size - step, top: start, anchor };
|
|
279
596
|
});
|
|
280
|
-
|
|
597
|
+
/**
|
|
598
|
+
* The tree and the registries, together down the map: a row with lines into a registry block is
|
|
599
|
+
* level with the first feed it has a line to, and the row after it stands a row below that block.
|
|
600
|
+
* `extra` opens above each run of rows with lines into the runner; `reserving`, the foot keeps
|
|
601
|
+
* clear of the runner block each run feeds.
|
|
602
|
+
*/
|
|
603
|
+
const lay = (step, extra = 0, reserving = true) => {
|
|
604
|
+
const ys = new Map();
|
|
605
|
+
const blocks = blocksOf(registries, step);
|
|
606
|
+
const runs = blocksOf(runner, step);
|
|
607
|
+
let y = start;
|
|
608
|
+
let floor = start;
|
|
609
|
+
// A row after a run of rows into the runner stands below the runner block the run feeds, in
|
|
610
|
+
// its agent.
|
|
611
|
+
let reserve = start;
|
|
612
|
+
let last;
|
|
613
|
+
const placed = new Set();
|
|
614
|
+
for (const row of rows) {
|
|
615
|
+
const id = rowId(row);
|
|
616
|
+
if (last && last.agent !== row.agent) {
|
|
617
|
+
y += 2 * step;
|
|
618
|
+
reserve = start;
|
|
619
|
+
}
|
|
620
|
+
if (opens.has(id))
|
|
621
|
+
y += extra;
|
|
622
|
+
if (reserving && lined.has(id) && !toRunner.has(id))
|
|
623
|
+
y = Math.max(y, reserve);
|
|
624
|
+
if (opens.has(id)) {
|
|
625
|
+
for (const run of runs) {
|
|
626
|
+
const fed = [...run.offsets].find(([feed]) => sourcesOf(feed).includes(id));
|
|
627
|
+
if (fed)
|
|
628
|
+
reserve = Math.max(reserve, y - fed[1] + run.size + 2 * step);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
let below = y;
|
|
632
|
+
for (const block of blocks.filter((each) => each.anchor?.row === id)) {
|
|
633
|
+
const offset = block.anchor?.offset ?? 0;
|
|
634
|
+
block.top = Math.max(floor, y - offset);
|
|
635
|
+
y = block.top + offset;
|
|
636
|
+
floor = block.top + block.size + 2 * step;
|
|
637
|
+
below = Math.max(below, block.top + block.size + step);
|
|
638
|
+
// A block with no line from a row stands after the block before it.
|
|
639
|
+
for (const next of blocks.slice(blocks.indexOf(block) + 1)) {
|
|
640
|
+
if (next.anchor || placed.has(next))
|
|
641
|
+
break;
|
|
642
|
+
next.top = floor;
|
|
643
|
+
floor = next.top + next.size + 2 * step;
|
|
644
|
+
below = Math.max(below, next.top + next.size + step);
|
|
645
|
+
placed.add(next);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
ys.set(id, y);
|
|
649
|
+
y = Math.max(y + step, below);
|
|
650
|
+
last = row;
|
|
651
|
+
}
|
|
652
|
+
for (const block of blocks.filter((each) => !each.anchor && !placed.has(each))) {
|
|
653
|
+
block.top = floor;
|
|
654
|
+
floor = block.top + block.size + 2 * step;
|
|
655
|
+
}
|
|
656
|
+
const bottom = Math.max(...ys.values(), ...blocks.map((block) => block.top + block.size));
|
|
657
|
+
return { ys, blocks, bottom };
|
|
658
|
+
};
|
|
659
|
+
const runnerRows = (runner?.rows ?? 1) + 2 * ((runner?.ids.length ?? 1) - 1) - 1;
|
|
660
|
+
const fits = (step) => {
|
|
661
|
+
const laid = lay(step);
|
|
662
|
+
return laid.bottom - start <= room && runnerRows * step <= room;
|
|
663
|
+
};
|
|
664
|
+
const row = box ? fittingRow(fits) : GROWN_ROW;
|
|
665
|
+
const plain = lay(row);
|
|
666
|
+
const height = Math.max(room, plain.bottom - start, runnerRows * row);
|
|
667
|
+
// why: The last row's registry stands at the foot of the map, with the rows before it that have
|
|
668
|
+
// no line of their own; the map's spare room is shared between the gap above them and the gap
|
|
669
|
+
// above each run of rows into the runner, so the rows into the registries stay close. A run goes
|
|
670
|
+
// no lower than keeps its runner block clear of the foot's.
|
|
671
|
+
const lastRow = rows.at(-1);
|
|
672
|
+
const atFoot = (block) => lastRow !== undefined && block.anchor?.row === rowId(lastRow);
|
|
673
|
+
const breaks = opens.size + 1;
|
|
674
|
+
const footOf = (blocks) => Math.max(...blocks.filter(atFoot).map((block) => block.top + block.size));
|
|
675
|
+
const share = Math.min(Math.max(0, start + height - footOf(lay(row, 0, false).blocks)) / breaks, Math.max(0, start + height - footOf(plain.blocks)) / Math.max(1, opens.size));
|
|
676
|
+
const { ys, blocks, bottom } = plain.blocks.some(atFoot) ? lay(row, share) : plain;
|
|
677
|
+
const end = start + height;
|
|
678
|
+
const foot = blocks.filter(atFoot);
|
|
679
|
+
if (lastRow && foot.length > 0) {
|
|
680
|
+
liftTail(rows, lined, lastRow, ys, foot, end - footOf(foot), row);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
spreadAgents(rows, tree, ys, blocks, end - bottom);
|
|
684
|
+
}
|
|
685
|
+
const lefts = columnLefts(rows, feeds, slots, width, box);
|
|
686
|
+
const places = new Map();
|
|
687
|
+
for (const [id, y] of ys)
|
|
688
|
+
places.set(id, { x: INSET + (tree.get(id)?.x ?? 0), y });
|
|
689
|
+
for (const block of blocks) {
|
|
690
|
+
for (const id of block.group) {
|
|
691
|
+
places.set(id, { x: INSET + (lefts[1] ?? 0), y: block.top + (block.offsets.get(id) ?? 0) });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const yOf = (id) => {
|
|
695
|
+
const slot = slots.get(id);
|
|
696
|
+
if (!slot)
|
|
697
|
+
return places.get(id)?.y;
|
|
698
|
+
const holder = yOf(slot.holder);
|
|
699
|
+
return holder === undefined ? undefined : holder + slot.down * row;
|
|
700
|
+
};
|
|
701
|
+
// The runner: each block's head level with the first row it has a line from, so what stands under
|
|
702
|
+
// it is level with the rows after; a head without lines, its block centred on the rows its feeds
|
|
703
|
+
// have lines from.
|
|
704
|
+
const ysOf = (sources) => sources.flatMap((source) => {
|
|
705
|
+
const y = yOf(source);
|
|
706
|
+
return y === undefined ? [] : [y];
|
|
707
|
+
});
|
|
708
|
+
const steps = blocksOf(runner, row).map((block) => {
|
|
709
|
+
const [head] = block.group;
|
|
710
|
+
// why: A source with no line of its own, the key vault, does not place a head; its line finds
|
|
711
|
+
// the head where it stands.
|
|
712
|
+
const heads = ysOf((head === undefined ? [] : sourcesOf(head)).filter((source) => tree.has(source) || sourcesOf(source).length > 0));
|
|
713
|
+
const at = ysOf([...block.offsets.keys()].flatMap(sourcesOf));
|
|
714
|
+
const want = heads.length > 0
|
|
715
|
+
? Math.min(...heads)
|
|
716
|
+
: at.length > 0
|
|
717
|
+
? (Math.min(...at) + Math.max(...at)) / 2 - block.size / 2
|
|
718
|
+
: start;
|
|
719
|
+
return { ...block, want };
|
|
720
|
+
});
|
|
721
|
+
keepInside(steps, start, end, row);
|
|
722
|
+
for (const block of steps) {
|
|
723
|
+
for (const id of block.group) {
|
|
724
|
+
places.set(id, { x: INSET + (lefts[2] ?? 0), y: block.top + (block.offsets.get(id) ?? 0) });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const headings = HEADINGS.map((heading, index) => ({
|
|
728
|
+
...heading,
|
|
729
|
+
x: INSET + (index === 0 ? PAD : (lefts[index] ?? 0)),
|
|
730
|
+
y: PAD + INSET,
|
|
731
|
+
}));
|
|
732
|
+
return { places, row, headings };
|
|
733
|
+
}
|
|
734
|
+
/** How far what the still shows and growing takes away has gone, 0 to 1. */
|
|
735
|
+
export function goneAt(grow) {
|
|
736
|
+
return ease(grow / LEAVE_TO);
|
|
281
737
|
}
|
|
282
|
-
/**
|
|
738
|
+
/** The key under which the line into a feed under another draws, for a feed that was a row. */
|
|
739
|
+
export const underKey = (feed) => `${feed}^`;
|
|
740
|
+
/** Where a node stands grown: a node under a feed, in its slot under it. */
|
|
741
|
+
function grownPlace(layout, id) {
|
|
742
|
+
const slot = layout.slots.get(id);
|
|
743
|
+
if (!slot)
|
|
744
|
+
return layout.grown.get(id) ?? { x: 0, y: 0 };
|
|
745
|
+
const holder = grownPlace(layout, slot.holder);
|
|
746
|
+
return { x: holder.x + slot.across, y: holder.y + slot.down * layout.row };
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Where a node stands this far grown. A node of the still glides to where it stands grown in one
|
|
750
|
+
* eased move, landing as the drawing reaches it. A node growing adds stands where it stands from
|
|
751
|
+
* the node it grows from, and moves with it, drifting in along its line as it shows.
|
|
752
|
+
*/
|
|
283
753
|
export function placeAt(layout, id, grow) {
|
|
284
|
-
|
|
754
|
+
return standAt(layout, id, grow, true);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Where a node stands this far grown, drifting in or not. What grows from a node keeps to where it
|
|
758
|
+
* stands, not to its drift, so a node's drift moves nothing else.
|
|
759
|
+
*/
|
|
760
|
+
function standAt(layout, id, grow, drifts) {
|
|
761
|
+
const moved = glidedAt(layout, id, grow);
|
|
762
|
+
const glide = (from, to, at) => ({
|
|
763
|
+
x: between(from.x, to.x, at),
|
|
764
|
+
y: between(from.y, to.y, at),
|
|
765
|
+
});
|
|
285
766
|
const slot = layout.slots.get(id);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
767
|
+
const anchor = layout.anchors.get(id);
|
|
768
|
+
if (anchor !== undefined) {
|
|
769
|
+
// why: It keeps to the node it grows from, which may still be on its way, and comes the last
|
|
770
|
+
// little way along the line that reached it, as a pen's mark settles.
|
|
771
|
+
const from = standAt(layout, anchor, grow, false);
|
|
772
|
+
const [own, its] = [grownPlace(layout, id), grownPlace(layout, anchor)];
|
|
773
|
+
const source = layout.reachedFrom.get(id);
|
|
774
|
+
const back = source === undefined ? { x: 0, y: 0 } : grownPlace(layout, source);
|
|
775
|
+
const [dx, dy] = [own.x - back.x, own.y - back.y];
|
|
776
|
+
const drift = drifts ? DRIFT * (1 - settling(drawnAt(layout, id, grow) ?? 1)) : 0;
|
|
777
|
+
const away = drift / (Math.hypot(dx, dy) || 1);
|
|
778
|
+
return { x: from.x + own.x - its.x - dx * away, y: from.y + own.y - its.y - dy * away };
|
|
779
|
+
}
|
|
780
|
+
// A row of the still that growing takes away folds into its agent as it goes, and an agent going
|
|
781
|
+
// is carried down with the tree's last row above it, so no row passes through it.
|
|
782
|
+
const leaving = layout.leaving.rows.find((row) => rowId(row) === id);
|
|
783
|
+
if (leaving?.parent !== undefined) {
|
|
784
|
+
const parent = placeAt(layout, layout.becomes.get(leaving.parent) ?? leaving.parent, grow);
|
|
785
|
+
return glide(leaving, parent, goneAt(grow));
|
|
786
|
+
}
|
|
787
|
+
if (leaving) {
|
|
788
|
+
const above = layout.rows
|
|
789
|
+
.filter((row) => (layout.closed.get(rowId(row))?.y ?? Infinity) < leaving.y)
|
|
790
|
+
.at(-1);
|
|
791
|
+
const from = above && layout.closed.get(rowId(above));
|
|
792
|
+
const by = above && from ? placeAt(layout, rowId(above), grow).y - from.y : 0;
|
|
793
|
+
return { x: leaving.x, y: leaving.y + by };
|
|
794
|
+
}
|
|
795
|
+
const from = layout.closed.get(id);
|
|
796
|
+
if (slot && from) {
|
|
797
|
+
// why: A node of the still that moves into a slot goes across first, out of the tree's way,
|
|
798
|
+
// and turns down or up to its slot on the way, in one curve.
|
|
799
|
+
const to = grownPlace(layout, id);
|
|
800
|
+
const t = glideAt(layout, id, grow);
|
|
801
|
+
return {
|
|
802
|
+
x: between(from.x, to.x, soft(t / (1 - SINK))),
|
|
803
|
+
y: between(from.y, to.y, soft((t - SINK) / (1 - SINK))),
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const to = grownPlace(layout, id);
|
|
807
|
+
return glide(from ?? to, to, moved);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Whether a node stands still this far grown: closed, what the still shows; then each node once it
|
|
811
|
+
* is out and in its zone, and what it comes out from too. What growing takes away is on its way.
|
|
812
|
+
*/
|
|
813
|
+
export function atRest(layout, id, grow) {
|
|
814
|
+
if (layout.leaving.rows.some((row) => rowId(row) === id))
|
|
815
|
+
return grow <= 0;
|
|
816
|
+
if (layout.leaving.feeds.some((feed) => feed.id === id))
|
|
817
|
+
return grow <= 0;
|
|
818
|
+
const anchor = layout.anchors.get(id);
|
|
819
|
+
if (anchor === undefined) {
|
|
820
|
+
const glided = glideAt(layout, id, grow);
|
|
821
|
+
return glided === 0 || glided === 1;
|
|
290
822
|
}
|
|
291
|
-
|
|
292
|
-
const from = layout.closed.get(id) ?? to;
|
|
293
|
-
return { x: between(from.x, to.x, moved), y: between(from.y, to.y, moved) };
|
|
823
|
+
return (drawnAt(layout, id, grow) ?? 1) >= 1 && atRest(layout, anchor, grow);
|
|
294
824
|
}
|
|
295
|
-
/**
|
|
825
|
+
/**
|
|
826
|
+
* How far a node of the still has gone on its glide into place, 0 to 1: it goes once the drawing
|
|
827
|
+
* reaches it, or with the frame where the drawing does not reach it.
|
|
828
|
+
*/
|
|
829
|
+
function glideAt(layout, id, grow) {
|
|
830
|
+
const start = layout.gathers.get(id);
|
|
831
|
+
if (start === undefined)
|
|
832
|
+
return clamp(grow / MOVE_TO);
|
|
833
|
+
const land = layout.lands.get(id) ?? start + GLIDE * layout.pace;
|
|
834
|
+
return clamp((grow - start) / (land - start));
|
|
835
|
+
}
|
|
836
|
+
/** How far a node of the still has glided, eased. */
|
|
837
|
+
function glidedAt(layout, id, grow) {
|
|
838
|
+
return soft(glideAt(layout, id, grow));
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* How far a node growing adds has shown, 0 to 1, once the drawing has reached it; undefined for
|
|
842
|
+
* what the map shows closed.
|
|
843
|
+
*/
|
|
296
844
|
export function drawnAt(layout, id, grow) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
845
|
+
if (!layout.anchors.has(id))
|
|
846
|
+
return undefined;
|
|
847
|
+
const reach = layout.reach.get(id) ?? DRAW_FROM;
|
|
848
|
+
return clamp((grow - reach) / (SHOW * layout.pace) + MET);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* A line's length as the drawing runs it: across, then down, from where its source stands grown to
|
|
852
|
+
* where its target stands when the pen comes, which for a node of the still is where the still holds it.
|
|
853
|
+
*/
|
|
854
|
+
function lineLength(layout, source, target) {
|
|
855
|
+
const from = grownPlace(layout, source);
|
|
856
|
+
const to = (glides(layout, target) && layout.closed.get(target)) || grownPlace(layout, target);
|
|
857
|
+
return Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* How far the line from `source` to `target` has drawn, 0 to 1: it runs out from its source at the
|
|
861
|
+
* drawing's pace once its source stands shown and still, easing out of it and into its end. What the still draws closed is drawn.
|
|
862
|
+
*/
|
|
863
|
+
export function lineAt(layout, source, target, grow) {
|
|
864
|
+
if (layout.drawnClosed.has(lineKey(source, target)))
|
|
865
|
+
return 1;
|
|
866
|
+
const start = layout.leaves.get(source);
|
|
867
|
+
if (start === undefined)
|
|
868
|
+
return 1;
|
|
869
|
+
const span = lineLength(layout, source, target) * layout.pace;
|
|
870
|
+
return span > 0 ? soft((grow - start) / span) : grow >= start ? 1 : 0;
|
|
871
|
+
}
|
|
872
|
+
/** At most this many lanes are tried in every order. */
|
|
873
|
+
const EVERY_ORDER = 7;
|
|
874
|
+
/** Every order of `items`, the given one first. */
|
|
875
|
+
function* orders(items) {
|
|
876
|
+
if (items.length <= 1) {
|
|
877
|
+
yield items;
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
for (const [index, first] of items.entries()) {
|
|
881
|
+
const rest = items.filter((_, other) => other !== index);
|
|
882
|
+
for (const tail of orders(rest))
|
|
883
|
+
yield [first, ...tail];
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Where two lines' runs cross, one's level runs over the other's lane. A line whose source has a
|
|
888
|
+
* line down the other lane meets it there, where its own line joins, so it does not cross it.
|
|
889
|
+
*/
|
|
890
|
+
function crossings(a, b) {
|
|
891
|
+
const across = (level, down) => level.reduce((count, line) => count +
|
|
892
|
+
down.filter((other) => !down.some((each) => each.source === line.source) &&
|
|
893
|
+
[
|
|
894
|
+
[line.from.y, line.from.x, line.lane],
|
|
895
|
+
[line.to.y, line.lane, line.to.x],
|
|
896
|
+
].some(([y = 0, left = 0, right = 0]) => other.lane > Math.min(left, right) &&
|
|
897
|
+
other.lane < Math.max(left, right) &&
|
|
898
|
+
y > Math.min(other.from.y, other.to.y) &&
|
|
899
|
+
y < Math.max(other.from.y, other.to.y))).length, 0);
|
|
900
|
+
return across(a, b) + across(b, a);
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Each feed's lane, in the gap left of its column: lines into one feed share it, and the lanes go
|
|
904
|
+
* in the order that crosses fewest.
|
|
905
|
+
*/
|
|
906
|
+
function lanesOf(layout, width) {
|
|
907
|
+
const lanes = new Map();
|
|
908
|
+
/** Per column, where a line steps aside in the gap before it: left of every lane there. */
|
|
909
|
+
const steps = new Map();
|
|
910
|
+
const grown = (id) => placeAt(layout, id, 1);
|
|
911
|
+
const root = (id) => {
|
|
912
|
+
const slot = layout.slots.get(id);
|
|
913
|
+
return slot ? root(slot.holder) : id;
|
|
914
|
+
};
|
|
915
|
+
const rights = (ids) => Math.max(0, ...ids.map((id) => grown(id).x + width(id)));
|
|
916
|
+
[1, 2].forEach((column) => {
|
|
917
|
+
const inColumn = layout.feeds.filter((feed) => feed.column === column);
|
|
918
|
+
const head = inColumn.find((feed) => !layout.slots.has(feed.id));
|
|
919
|
+
if (!head)
|
|
920
|
+
return;
|
|
921
|
+
const left = grown(head.id).x;
|
|
922
|
+
const before = column === 1
|
|
923
|
+
? layout.rows.map(rowId)
|
|
924
|
+
: layout.feeds.filter((feed) => feed.column === column - 1).map((feed) => feed.id);
|
|
925
|
+
const space = left - rights(before);
|
|
926
|
+
// why: As in the studio's map, the lines that share an end run as one: the end most lines share
|
|
927
|
+
// takes them as its hub, then the end most of the rest share, a feed before a source when even.
|
|
928
|
+
const links = inColumn.flatMap((feed) => [...feed.from, ...(feed.late ?? [])]
|
|
929
|
+
.filter((source) => !beside(layout, source, feed))
|
|
930
|
+
// why: A line level with its feed runs straight across and needs no lane of its own.
|
|
931
|
+
.filter((source) => Math.abs(grown(source).y - grown(feed.id).y) >= 1)
|
|
932
|
+
.map((source) => ({ source, feed: feed.id })));
|
|
933
|
+
const hubs = new Map();
|
|
934
|
+
let open = links;
|
|
935
|
+
while (open.length > 0) {
|
|
936
|
+
const ends = new Map();
|
|
937
|
+
for (const link of open) {
|
|
938
|
+
for (const end of [`>${link.feed}`, `${link.source}>`]) {
|
|
939
|
+
ends.set(end, [...(ends.get(end) ?? []), link]);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const feedEnd = (end) => end.startsWith('>');
|
|
943
|
+
const [hub, taken] = [...ends].reduce((most, each) => each[1].length > most[1].length ||
|
|
944
|
+
(each[1].length === most[1].length && feedEnd(each[0]) && !feedEnd(most[0]))
|
|
945
|
+
? each
|
|
946
|
+
: most);
|
|
947
|
+
hubs.set(hub, taken);
|
|
948
|
+
open = open.filter((link) => !taken.includes(link));
|
|
949
|
+
}
|
|
950
|
+
const middle = (group) => group.reduce((sum, link) => sum + grown(link.feed).y, 0) / group.length;
|
|
951
|
+
const runs = (group, lane) => group.map(({ source, feed }) => ({
|
|
952
|
+
source,
|
|
953
|
+
from: { x: grown(source).x + width(source), y: grown(source).y },
|
|
954
|
+
to: grown(feed),
|
|
955
|
+
lane,
|
|
956
|
+
}));
|
|
957
|
+
// The first lane is the nearest its column; to start, the top hub's.
|
|
958
|
+
const order = [...hubs.values()].toSorted((a, b) => middle(a) - middle(b));
|
|
959
|
+
const laneAt = (index) => left - (space * (index + 1)) / (order.length + 1);
|
|
960
|
+
const cost = () => order.reduce((sum, feed, index) => sum +
|
|
961
|
+
order
|
|
962
|
+
.slice(index + 1)
|
|
963
|
+
.reduce((more, other, offset) => more +
|
|
964
|
+
crossings(runs(feed, laneAt(index)), runs(other, laneAt(index + 1 + offset))), 0), 0);
|
|
965
|
+
let best = cost();
|
|
966
|
+
// why: Few lanes are tried in every order, since swapping neighbours stops where a lane must
|
|
967
|
+
// pass two others to cross fewer; many, by swapping neighbours.
|
|
968
|
+
if (order.length <= EVERY_ORDER) {
|
|
969
|
+
const start = [...order];
|
|
970
|
+
for (const each of orders(start)) {
|
|
971
|
+
order.splice(0, order.length, ...each);
|
|
972
|
+
const next = cost();
|
|
973
|
+
if (next < best) {
|
|
974
|
+
best = next;
|
|
975
|
+
start.splice(0, start.length, ...each);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
order.splice(0, order.length, ...start);
|
|
979
|
+
}
|
|
980
|
+
for (let pass = 0, swapped = true; swapped && pass < order.length; pass += 1) {
|
|
981
|
+
swapped = false;
|
|
982
|
+
for (let index = 0; index + 1 < order.length; index += 1) {
|
|
983
|
+
const [a, b] = [order[index], order[index + 1]];
|
|
984
|
+
if (!a || !b)
|
|
985
|
+
continue;
|
|
986
|
+
order[index] = b;
|
|
987
|
+
order[index + 1] = a;
|
|
988
|
+
const next = cost();
|
|
989
|
+
if (next < best) {
|
|
990
|
+
best = next;
|
|
991
|
+
swapped = true;
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
order[index] = a;
|
|
995
|
+
order[index + 1] = b;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
order.forEach((group, index) => {
|
|
1000
|
+
const of = root(group[0]?.feed ?? head.id);
|
|
1001
|
+
for (const { source, feed } of group) {
|
|
1002
|
+
lanes.set(lineKey(source, feed), { of, by: grown(of).x - laneAt(index) });
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
steps.set(column, { of: head.id, by: space - space / (order.length + 1) / 2 });
|
|
1006
|
+
});
|
|
1007
|
+
return { lanes, jogs: jogsOf(layout, width, lanes, steps) };
|
|
1008
|
+
}
|
|
1009
|
+
/** Whether `source` stands in `feed`'s own column: its line runs out to the right of the column and back. */
|
|
1010
|
+
export function beside(layout, source, feed) {
|
|
1011
|
+
return layout.feeds.some((each) => each.id === source && each.column === feed.column);
|
|
1012
|
+
}
|
|
1013
|
+
/** The key of the line from `source` into `feed`. */
|
|
1014
|
+
export const lineKey = (source, feed) => `${source}>${feed}`;
|
|
1015
|
+
/**
|
|
1016
|
+
* Where a line would run level behind a token, or along another feed's last run, it steps half a
|
|
1017
|
+
* row aside in the gap before it and runs on between the rows.
|
|
1018
|
+
*/
|
|
1019
|
+
function jogsOf(layout, width, lanes, steps) {
|
|
1020
|
+
const grown = (id) => placeAt(layout, id, 1);
|
|
1021
|
+
const laneX = (lane) => grown(lane.of).x - lane.by;
|
|
1022
|
+
const columnOf = new Map(layout.feeds.map((feed) => [feed.id, feed.column]));
|
|
1023
|
+
const band = HALF + CLEAR;
|
|
1024
|
+
const nodes = [...layout.rows.map(rowId), ...layout.feeds.map((feed) => feed.id)];
|
|
1025
|
+
const jogs = new Map();
|
|
1026
|
+
for (const feed of layout.feeds) {
|
|
1027
|
+
const end = grown(feed.id);
|
|
1028
|
+
// What a level run can't pass: other tokens, and the last runs into the feeds beside this one,
|
|
1029
|
+
// but a run from its own source, which it shares.
|
|
1030
|
+
const blocks = [
|
|
1031
|
+
...nodes.map((id) => ({
|
|
1032
|
+
id,
|
|
1033
|
+
from: undefined,
|
|
1034
|
+
left: grown(id).x,
|
|
1035
|
+
right: grown(id).x + width(id),
|
|
1036
|
+
reach: band,
|
|
1037
|
+
})),
|
|
1038
|
+
...layout.feeds
|
|
1039
|
+
.filter((other) => other.column === feed.column && other.id !== feed.id)
|
|
1040
|
+
.flatMap((other) => [...other.from, ...(other.late ?? [])].flatMap((source) => {
|
|
1041
|
+
const its = lanes.get(lineKey(source, other.id));
|
|
1042
|
+
return its
|
|
1043
|
+
? [
|
|
1044
|
+
{
|
|
1045
|
+
id: other.id,
|
|
1046
|
+
from: source,
|
|
1047
|
+
left: laneX(its),
|
|
1048
|
+
right: grown(other.id).x,
|
|
1049
|
+
reach: HALF,
|
|
1050
|
+
},
|
|
1051
|
+
]
|
|
1052
|
+
: [];
|
|
1053
|
+
})),
|
|
1054
|
+
];
|
|
1055
|
+
for (const source of [...feed.from, ...(feed.late ?? [])]) {
|
|
1056
|
+
if (beside(layout, source, feed))
|
|
1057
|
+
continue;
|
|
1058
|
+
const lane = lanes.get(lineKey(source, feed.id));
|
|
1059
|
+
if (!lane)
|
|
1060
|
+
continue;
|
|
1061
|
+
const rail = laneX(lane);
|
|
1062
|
+
const aside = [];
|
|
1063
|
+
let x = grown(source).x + width(source);
|
|
1064
|
+
let y = grown(source).y;
|
|
1065
|
+
for (let tries = 0; tries < 3; tries += 1) {
|
|
1066
|
+
const hit = blocks
|
|
1067
|
+
.filter((block) => block.id !== source &&
|
|
1068
|
+
block.id !== feed.id &&
|
|
1069
|
+
block.from !== source &&
|
|
1070
|
+
block.right > x &&
|
|
1071
|
+
block.left < rail &&
|
|
1072
|
+
Math.abs(grown(block.id).y - y) < block.reach)
|
|
1073
|
+
.toSorted((a, b) => a.left - b.left)[0];
|
|
1074
|
+
if (!hit)
|
|
1075
|
+
break;
|
|
1076
|
+
const step = steps.get(columnOf.get(hit.id) ?? 0);
|
|
1077
|
+
if (!step || laneX(step) <= x)
|
|
1078
|
+
break;
|
|
1079
|
+
const side = end.y >= grown(hit.id).y ? 1 : -1;
|
|
1080
|
+
aside.push({ lane: step, near: hit.id, side });
|
|
1081
|
+
x = laneX(step);
|
|
1082
|
+
y = grown(hit.id).y + (side * layout.row) / 2;
|
|
1083
|
+
}
|
|
1084
|
+
if (aside.length > 0)
|
|
1085
|
+
jogs.set(lineKey(source, feed.id), aside);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return jogs;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* What each node growing adds comes out from under: a row, the row above it; a feed under another,
|
|
1092
|
+
* the one above it there, else that one; any other feed, the first it has a line from, else the
|
|
1093
|
+
* first under it (or, when that comes out too, the first that one has a line from), else the feed
|
|
1094
|
+
* before it.
|
|
1095
|
+
*/
|
|
1096
|
+
function anchorsOf(rows, feeds, slots, unfolds, few) {
|
|
1097
|
+
const anchors = new Map();
|
|
1098
|
+
const ids = rows.map(rowId);
|
|
1099
|
+
for (const id of unfolds) {
|
|
1100
|
+
const above = ids[ids.indexOf(id) - 1];
|
|
1101
|
+
if (above !== undefined)
|
|
1102
|
+
anchors.set(id, above);
|
|
1103
|
+
}
|
|
1104
|
+
for (const [index, feed] of feeds.entries()) {
|
|
1105
|
+
if (few.has(feed.id))
|
|
1106
|
+
continue;
|
|
1107
|
+
const under = [...slots].find(([, slot]) => slot.holder === feed.id)?.[0];
|
|
1108
|
+
const slot = slots.get(feed.id);
|
|
1109
|
+
const above = slot &&
|
|
1110
|
+
[...slots].find(([, each]) => each.holder === slot.holder && each.down === slot.down - 1)?.[0];
|
|
1111
|
+
const first = under === undefined || few.has(under)
|
|
1112
|
+
? under
|
|
1113
|
+
: feeds.find((each) => each.id === under)?.from[0];
|
|
1114
|
+
const anchor = above ?? slot?.holder ?? feed.from[0] ?? first ?? feeds[index - 1]?.id ?? ids[0];
|
|
1115
|
+
if (anchor !== undefined)
|
|
1116
|
+
anchors.set(feed.id, anchor);
|
|
1117
|
+
}
|
|
1118
|
+
return anchors;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Closed, the still: the rows down the map, a gap before each agent after the first, and beside
|
|
1122
|
+
* them, at `x`, the feeds it shows: the runs one under another, a row's gap between them, centred
|
|
1123
|
+
* on the rows.
|
|
1124
|
+
*/
|
|
1125
|
+
function stillPlaces(rows, few, x) {
|
|
1126
|
+
const ids = new Set(rows.map(rowId));
|
|
1127
|
+
const runs = new Map();
|
|
1128
|
+
for (const feed of few) {
|
|
1129
|
+
const source = feed.from.find((id) => ids.has(id));
|
|
1130
|
+
if (source !== undefined)
|
|
1131
|
+
runs.set(source, [...(runs.get(source) ?? []), feed]);
|
|
1132
|
+
}
|
|
1133
|
+
let y = PAD + HALF;
|
|
1134
|
+
const placed = rows.map((row, index) => {
|
|
1135
|
+
if (index > 0 && row.parent === undefined)
|
|
1136
|
+
y += AGENT_GAP;
|
|
1137
|
+
const at = { ...row, y };
|
|
1138
|
+
y += ROW;
|
|
1139
|
+
return at;
|
|
1140
|
+
});
|
|
1141
|
+
const own = placed.flatMap((row) => {
|
|
1142
|
+
const run = runs.get(rowId(row)) ?? [];
|
|
1143
|
+
return run.length > 0 ? [run] : [];
|
|
1144
|
+
});
|
|
1145
|
+
// The runs one under another, a row's gap between them, their middle level with the rows' middle.
|
|
1146
|
+
const slots = own.reduce((sum, run) => sum + run.length + 1, -1);
|
|
1147
|
+
const [first, last] = [placed[0]?.y ?? 0, placed.at(-1)?.y ?? 0];
|
|
1148
|
+
let top = (first + last) / 2 - ((slots - 1) * ROW) / 2;
|
|
1149
|
+
const feeds = new Map();
|
|
1150
|
+
for (const run of own) {
|
|
1151
|
+
for (const [step, feed] of run.entries())
|
|
1152
|
+
feeds.set(feed.id, { x, y: top + step * ROW });
|
|
1153
|
+
top += (run.length + 1) * ROW;
|
|
1154
|
+
}
|
|
1155
|
+
return { rows: placed, feeds };
|
|
1156
|
+
}
|
|
1157
|
+
/** Closed, the rows and feeds given a place, each where it is given. */
|
|
1158
|
+
function givenPlaces(rows, feeds, given) {
|
|
1159
|
+
return {
|
|
1160
|
+
rows: rows.map((row) => ({ ...row, ...given.get(rowId(row)) })),
|
|
1161
|
+
feeds: new Map(feeds.flatMap((feed) => {
|
|
1162
|
+
const place = given.get(feed.id);
|
|
1163
|
+
return place ? [[feed.id, place]] : [];
|
|
1164
|
+
})),
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Closed, the x each row's lines run down to its feeds, together: left of the feeds, a lane
|
|
1169
|
+
* further left for each trunk from a row above it that it would run beside, so two rows' lines meet
|
|
1170
|
+
* only at a feed both have.
|
|
1171
|
+
*/
|
|
1172
|
+
function trunksOf(lines, closed, x) {
|
|
1173
|
+
const spans = new Map();
|
|
1174
|
+
for (const { source, feed } of lines) {
|
|
1175
|
+
const ys = [closed.get(source)?.y, closed.get(feed)?.y].filter((y) => y !== undefined);
|
|
1176
|
+
const [top, bottom] = spans.get(source) ?? [Infinity, -Infinity];
|
|
1177
|
+
spans.set(source, [Math.min(top, ...ys), Math.max(bottom, ...ys)]);
|
|
1178
|
+
}
|
|
1179
|
+
const taken = [];
|
|
1180
|
+
const trunks = new Map();
|
|
1181
|
+
// why: A lower row's trunk stands further left, so its line in runs short of the trunks above.
|
|
1182
|
+
const fromTop = [...spans].sort(([, [a]], [, [b]]) => a - b);
|
|
1183
|
+
for (const [source, [top, bottom]] of fromTop) {
|
|
1184
|
+
let lane = 0;
|
|
1185
|
+
while (taken.some((each) => each.lane === lane && each.top <= bottom && top <= each.bottom))
|
|
1186
|
+
lane += 1;
|
|
1187
|
+
taken.push({ lane, top, bottom });
|
|
1188
|
+
trunks.set(source, x - TRUNK_GAP - lane * LANE);
|
|
1189
|
+
}
|
|
1190
|
+
return trunks;
|
|
1191
|
+
}
|
|
1192
|
+
/** Every line the map draws, from the node it leaves to the node at its end. */
|
|
1193
|
+
function edgesOf(rows, feeds) {
|
|
1194
|
+
return [
|
|
1195
|
+
...rows.flatMap((each) => (each.parent === undefined ? [] : [[each.parent, rowId(each)]])),
|
|
1196
|
+
...feeds.flatMap((feed) => [
|
|
1197
|
+
...[...feed.from, ...(feed.late ?? [])].map((source) => [source, feed.id]),
|
|
1198
|
+
...(feed.under === undefined ? [] : [[feed.under, feed.id]]),
|
|
1199
|
+
]),
|
|
1200
|
+
];
|
|
1201
|
+
}
|
|
1202
|
+
/** Whether a node is of the still and goes somewhere growing, so it glides when the drawing comes. */
|
|
1203
|
+
function glides(layout, id) {
|
|
1204
|
+
const closed = layout.closed.get(id);
|
|
1205
|
+
if (!closed || layout.anchors.has(id))
|
|
1206
|
+
return false;
|
|
1207
|
+
const grown = grownPlace(layout, id);
|
|
1208
|
+
return Math.hypot(grown.x - closed.x, grown.y - closed.y) > 1;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Settles a node the drawing has reached: when it stands ready, and when its lines leave. A node
|
|
1212
|
+
* growing adds is well on its way to shown; a node of the still that moves glides into place once
|
|
1213
|
+
* its line has reached it, the line coming with it.
|
|
1214
|
+
*/
|
|
1215
|
+
function settle(layout, runs, id) {
|
|
1216
|
+
const ran = runs.ran.get(id) ?? 0;
|
|
1217
|
+
let ready = ran + (layout.anchors.has(id) ? SHOW * (SHOWN - MET) : 0);
|
|
1218
|
+
if (runs.from.has(id) && glides(layout, id)) {
|
|
1219
|
+
runs.gather.set(id, ran);
|
|
1220
|
+
ready = ran + GLIDE;
|
|
1221
|
+
}
|
|
1222
|
+
runs.ready.set(id, ready);
|
|
1223
|
+
runs.leave.set(id, ready + PAUSE);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The pen's runs to every node, by the shortest run of line from the tree's agents.
|
|
1227
|
+
*/
|
|
1228
|
+
function runsOf(layout, edges) {
|
|
1229
|
+
const runs = {
|
|
1230
|
+
ran: new Map(),
|
|
1231
|
+
from: new Map(),
|
|
1232
|
+
ready: new Map(),
|
|
1233
|
+
leave: new Map(),
|
|
1234
|
+
gather: new Map(),
|
|
1235
|
+
land: new Map(),
|
|
1236
|
+
};
|
|
1237
|
+
const queue = layout.rows.filter((each) => each.parent === undefined).map(rowId);
|
|
1238
|
+
for (const id of queue)
|
|
1239
|
+
runs.ran.set(id, 0);
|
|
1240
|
+
while (queue.length > 0) {
|
|
1241
|
+
queue.sort((a, b) => (runs.ran.get(a) ?? 0) - (runs.ran.get(b) ?? 0));
|
|
1242
|
+
const id = queue.shift();
|
|
1243
|
+
if (runs.leave.has(id))
|
|
1244
|
+
continue;
|
|
1245
|
+
settle(layout, runs, id);
|
|
1246
|
+
for (const [source, target] of edges.filter(([each]) => each === id)) {
|
|
1247
|
+
const to = (runs.leave.get(id) ?? 0) + lineLength(layout, source, target);
|
|
1248
|
+
if (to >= (runs.ran.get(target) ?? Infinity))
|
|
1249
|
+
continue;
|
|
1250
|
+
runs.ran.set(target, to);
|
|
1251
|
+
runs.from.set(target, source);
|
|
1252
|
+
queue.push(target);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
reachUnlined(layout, runs);
|
|
1256
|
+
return runs;
|
|
1257
|
+
}
|
|
1258
|
+
/** What the pen still has not reached is reached from what it grows from, once that node's lines leave. */
|
|
1259
|
+
function reachUnlined(layout, runs) {
|
|
1260
|
+
for (const id of layout.anchors.keys()) {
|
|
1261
|
+
if (runs.ran.has(id))
|
|
1262
|
+
continue;
|
|
1263
|
+
let anchor = layout.anchors.get(id);
|
|
1264
|
+
while (anchor !== undefined && !runs.ran.has(anchor))
|
|
1265
|
+
anchor = layout.anchors.get(anchor);
|
|
1266
|
+
if (anchor !== undefined)
|
|
1267
|
+
runs.from.set(id, anchor);
|
|
1268
|
+
const leaves = anchor === undefined ? 0 : (runs.leave.get(anchor) ?? 0);
|
|
1269
|
+
runs.ran.set(id, leaves + (anchor === undefined ? 0 : lineLength(layout, anchor, id)));
|
|
1270
|
+
settle(layout, runs, id);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Nodes of the still a line reaches together glide as one, once their line reaches them, or sooner
|
|
1275
|
+
* where something would show where one of them stands: they are on their way as it comes, and their
|
|
1276
|
+
* line catches them up.
|
|
1277
|
+
*/
|
|
1278
|
+
function makeRoom(layout, runs, width) {
|
|
1279
|
+
const box = (id, place) => ({
|
|
1280
|
+
left: place.x - ROOM,
|
|
1281
|
+
right: place.x + width(id) + ROOM,
|
|
1282
|
+
top: place.y - HALF - CLEAR,
|
|
1283
|
+
bottom: place.y + HALF + CLEAR,
|
|
1284
|
+
});
|
|
1285
|
+
const meet = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
|
1286
|
+
const comes = (id) => runs.gather.get(id) ?? runs.ran.get(id) ?? 0;
|
|
1287
|
+
const coming = [...new Set([...layout.anchors.keys(), ...layout.closed.keys()])].map((id) => ({
|
|
1288
|
+
id,
|
|
1289
|
+
at: comes(id) - (layout.anchors.has(id) ? SHOW * MET : 0),
|
|
1290
|
+
edges: box(id, grownPlace(layout, id)),
|
|
1291
|
+
}));
|
|
1292
|
+
const groups = Map.groupBy(runs.gather.keys(), (id) => runs.from.get(id) ?? id);
|
|
1293
|
+
for (const members of groups.values()) {
|
|
1294
|
+
const own = Math.min(...members.map(comes));
|
|
1295
|
+
const before = members.flatMap((id) => {
|
|
1296
|
+
const stands = box(id, layout.closed.get(id));
|
|
1297
|
+
return coming
|
|
1298
|
+
.filter((each) => !members.includes(each.id) && each.at < own && meet(each.edges, stands))
|
|
1299
|
+
.map((each) => each.at - GLIDE * AHEAD);
|
|
1300
|
+
});
|
|
1301
|
+
const start = Math.min(own, ...before);
|
|
1302
|
+
// why: What would land on it with the frame is made room for with the frame. Gone sooner, it
|
|
1303
|
+
// goes slower, and lands as its line reaches it.
|
|
1304
|
+
for (const id of members) {
|
|
1305
|
+
const reached = runs.ran.get(id) ?? 0;
|
|
1306
|
+
if (start <= 0)
|
|
1307
|
+
runs.gather.delete(id);
|
|
1308
|
+
else
|
|
1309
|
+
runs.gather.set(id, start);
|
|
1310
|
+
if (start < own)
|
|
1311
|
+
runs.land.set(id, Math.max(start + GLIDE, reached + GLIDE * LAND));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* When the drawing reaches each node, when each node's lines leave it, and when each node of the
|
|
1317
|
+
* still glides into place.
|
|
1318
|
+
*
|
|
1319
|
+
* why: The map draws out from the tree's agents as one pen would, at an even pace, and nothing
|
|
1320
|
+
* moves before the pen comes to it: down the tree to each row, and along each line to what it
|
|
1321
|
+
* feeds, so a node is reached by the shortest run of line to it. A node shows as its line reaches
|
|
1322
|
+
* it; what the still holds stands until its line reaches it, then glides into place with it.
|
|
1323
|
+
*/
|
|
1324
|
+
function drawingOf(layout, width) {
|
|
1325
|
+
const edges = edgesOf(layout.rows, layout.feeds);
|
|
1326
|
+
// why: What no line runs into is reached from what it grows from, as the pen goes, so the lines
|
|
1327
|
+
// it sends out run in their turn with the rest.
|
|
1328
|
+
const lined = new Set(edges.map(([, target]) => target));
|
|
1329
|
+
const jumps = [...layout.anchors]
|
|
1330
|
+
.filter(([id]) => !lined.has(id))
|
|
1331
|
+
.map(([id, anchor]) => [anchor, id]);
|
|
1332
|
+
const runs = runsOf(layout, [...edges, ...jumps]);
|
|
1333
|
+
makeRoom(layout, runs, width);
|
|
1334
|
+
const ends = edges
|
|
1335
|
+
.filter(([source]) => runs.leave.has(source))
|
|
1336
|
+
.map(([source, target]) => (runs.leave.get(source) ?? 0) + lineLength(layout, source, target));
|
|
1337
|
+
const farthest = Math.max(1, ...[...runs.ran.values()].map((at) => at + SHOW), ...ends);
|
|
1338
|
+
const pace = (1 - DRAW_FROM) / Math.max(farthest, ...runs.ready.values());
|
|
1339
|
+
const grown = (runs) => new Map([...runs].map(([id, run]) => [id, DRAW_FROM + run * pace]));
|
|
1340
|
+
return {
|
|
1341
|
+
reach: grown(runs.ran),
|
|
1342
|
+
reachedFrom: runs.from,
|
|
1343
|
+
leaves: grown(runs.leave),
|
|
1344
|
+
gathers: grown(runs.gather),
|
|
1345
|
+
lands: grown(runs.land),
|
|
1346
|
+
pace,
|
|
1347
|
+
};
|
|
316
1348
|
}
|
|
317
1349
|
/**
|
|
318
1350
|
* The tree and what feeds it, closed and grown, for tokens this wide: grown, it fills `box` when
|
|
319
1351
|
* one is given.
|
|
320
1352
|
*/
|
|
321
|
-
export function treeMapLayout(workspace, agents, width, box
|
|
1353
|
+
export function treeMapLayout(workspace, agents, width, box,
|
|
1354
|
+
/** The agents the map shows closed, when not the tree's. */
|
|
1355
|
+
still,
|
|
1356
|
+
/** Where the tokens the map shows closed stand, by id, when not where the still puts them. */
|
|
1357
|
+
given) {
|
|
322
1358
|
const rows = rowsOf(workspace, agents);
|
|
323
1359
|
const feeds = feedsOf(workspace, rows);
|
|
324
|
-
// Closed, the
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
1360
|
+
// Closed, the still: what is given, else its own agents and rows, or the tree's, and what feeds them.
|
|
1361
|
+
const stillRows = given
|
|
1362
|
+
? rows.filter((row) => given.has(rowId(row)))
|
|
1363
|
+
: still
|
|
1364
|
+
? rowsOf(workspace, still)
|
|
1365
|
+
: rows;
|
|
1366
|
+
const stillFeeds = given
|
|
1367
|
+
? feeds.filter((feed) => given.has(feed.id))
|
|
1368
|
+
: still
|
|
1369
|
+
? feedsOf(workspace, stillRows)
|
|
1370
|
+
: feeds;
|
|
1371
|
+
const grownRows = new Set(rows.map(rowId));
|
|
1372
|
+
const grownFeeds = new Set(feeds.map((feed) => feed.id));
|
|
1373
|
+
// why: An agent of the still that the tree draws as an agent tool's agent glides into it.
|
|
1374
|
+
const becomes = new Map();
|
|
1375
|
+
for (const row of stillRows) {
|
|
1376
|
+
if (row.parent !== undefined || grownRows.has(rowId(row)))
|
|
1377
|
+
continue;
|
|
1378
|
+
const spec = workspace.toolSpecs.find((each) => each.agentKey === row.agent);
|
|
1379
|
+
const feed = spec ? `tool:${spec.key}/agent` : undefined;
|
|
1380
|
+
if (feed && grownFeeds.has(feed))
|
|
1381
|
+
becomes.set(rowId(row), feed);
|
|
1382
|
+
}
|
|
1383
|
+
// Closed, each agent's models and tools stand beside the tree, those with a line from a row of it.
|
|
1384
|
+
const feedX = Math.max(0, ...stillRows.map((row) => row.x + width(rowId(row)))) + GAP;
|
|
1385
|
+
const placed = given
|
|
1386
|
+
? givenPlaces(stillRows, stillFeeds, given)
|
|
1387
|
+
: stillPlaces(stillRows, CLOSED.flatMap((kind) => stillFeeds.filter((feed) => feed.kind === kind)), feedX);
|
|
1388
|
+
const few = new Set([...placed.feeds.keys(), ...becomes.values()]);
|
|
1389
|
+
const closed = new Map(placed.rows.map((row) => [rowId(row), row]));
|
|
1390
|
+
for (const [row, feed] of becomes) {
|
|
1391
|
+
const place = closed.get(row);
|
|
1392
|
+
if (place)
|
|
1393
|
+
closed.set(feed, place);
|
|
1394
|
+
}
|
|
1395
|
+
// why: A row the still does not show unfolds from under the row above it, so it moves with the
|
|
1396
|
+
// tree and shows as it comes apart, not into a gap opened for it.
|
|
1397
|
+
const unfolds = rows.map(rowId).filter((id) => !closed.has(id));
|
|
1398
|
+
rows.forEach((row, index) => {
|
|
1399
|
+
if (closed.has(rowId(row)))
|
|
1400
|
+
return;
|
|
1401
|
+
const above = rows
|
|
1402
|
+
.slice(0, index)
|
|
1403
|
+
.map((each) => closed.get(rowId(each)))
|
|
1404
|
+
.findLast((place) => place !== undefined);
|
|
1405
|
+
if (above)
|
|
1406
|
+
closed.set(rowId(row), { x: above.x, y: above.y });
|
|
1407
|
+
});
|
|
1408
|
+
for (const [id, place] of placed.feeds)
|
|
1409
|
+
closed.set(id, place);
|
|
1410
|
+
const slots = slotsOf(feeds);
|
|
1411
|
+
const { places, row, headings } = grownPlaces(rows, feeds, slots, width, box);
|
|
1412
|
+
// What the still shows and the tree does not: it stays where it stood closed as it goes.
|
|
1413
|
+
const shownClosed = (feed) => few.has(feed.id);
|
|
1414
|
+
const stillIds = new Set(stillRows.map(rowId));
|
|
1415
|
+
const stillLines = stillFeeds
|
|
1416
|
+
.filter(shownClosed)
|
|
1417
|
+
.flatMap((feed) => feed.from
|
|
1418
|
+
.filter((source) => stillIds.has(source))
|
|
1419
|
+
.map((source) => ({ source, feed: feed.id })));
|
|
1420
|
+
const grownLines = new Set(feeds.flatMap((feed) => feed.from.map((source) => lineKey(source, feed.id))));
|
|
1421
|
+
const leaving = {
|
|
1422
|
+
rows: placed.rows.filter((each) => !grownRows.has(rowId(each)) && !becomes.has(rowId(each))),
|
|
1423
|
+
feeds: stillFeeds.filter((feed) => few.has(feed.id) && !grownFeeds.has(feed.id)),
|
|
1424
|
+
lines: stillLines.filter((line) => !grownLines.has(lineKey(line.source, line.feed))),
|
|
1425
|
+
};
|
|
1426
|
+
for (const each of leaving.rows)
|
|
1427
|
+
places.set(rowId(each), closed.get(rowId(each)) ?? each);
|
|
1428
|
+
for (const feed of leaving.feeds) {
|
|
1429
|
+
const place = closed.get(feed.id);
|
|
1430
|
+
if (place)
|
|
1431
|
+
places.set(feed.id, place);
|
|
1432
|
+
}
|
|
339
1433
|
const layout = {
|
|
340
1434
|
rows,
|
|
341
1435
|
feeds,
|
|
@@ -343,15 +1437,23 @@ export function treeMapLayout(workspace, agents, width, box) {
|
|
|
343
1437
|
closed,
|
|
344
1438
|
grown: places,
|
|
345
1439
|
row,
|
|
1440
|
+
headings,
|
|
346
1441
|
slots,
|
|
347
|
-
|
|
1442
|
+
reach: new Map(),
|
|
1443
|
+
reachedFrom: new Map(),
|
|
1444
|
+
leaves: new Map(),
|
|
1445
|
+
gathers: new Map(),
|
|
1446
|
+
lands: new Map(),
|
|
1447
|
+
pace: 0,
|
|
1448
|
+
drawnClosed: new Set(stillLines.map((line) => lineKey(line.source, line.feed))),
|
|
1449
|
+
anchors: anchorsOf(rows, feeds, slots, unfolds, few),
|
|
1450
|
+
lanes: new Map(),
|
|
1451
|
+
jogs: new Map(),
|
|
1452
|
+
leaving,
|
|
1453
|
+
becomes,
|
|
1454
|
+
trunks: trunksOf(stillLines, closed, feedX),
|
|
348
1455
|
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const ids = [...rows.map(rowId), ...feeds.map((feed) => feed.id)];
|
|
352
|
-
order.forEach((feed, index) => {
|
|
353
|
-
const turn = DRAW_FROM + ((DRAW_TO - DRAW_SPAN - DRAW_FROM) * index) / Math.max(order.length - 1, 1);
|
|
354
|
-
layout.starts.set(feed.id, Math.max(turn, clearFrom(layout, feed.id, ids, width)));
|
|
355
|
-
});
|
|
1456
|
+
Object.assign(layout, lanesOf(layout, width));
|
|
1457
|
+
Object.assign(layout, drawingOf(layout, width));
|
|
356
1458
|
return layout;
|
|
357
1459
|
}
|