visualproof 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +99 -2
- package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
- package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
- package/dist/assets/latex/visualproof.sty +308 -0
- package/dist/extractor/Extract.lean +1180 -0
- package/dist/extractor/Usage.lean +900 -0
- package/dist/mcp/visualproof-mcp.mjs +476 -0
- package/dist/server/access.js +95 -0
- package/dist/server/agent.js +3235 -0
- package/dist/server/agentdocs.js +35 -0
- package/dist/server/agentlibrary.js +202 -0
- package/dist/server/agentnotes.js +701 -0
- package/dist/server/agentviews.js +703 -0
- package/dist/server/askfollow.js +286 -0
- package/dist/server/boardspots.js +159 -0
- package/dist/server/dirs.js +175 -0
- package/dist/server/doctor.js +186 -0
- package/dist/server/events.js +198 -0
- package/dist/server/export.js +420 -0
- package/dist/server/extract.js +204 -0
- package/dist/server/fetchurl.js +321 -0
- package/dist/server/figures.js +158 -0
- package/dist/server/history.js +741 -0
- package/dist/server/historyapi.js +304 -0
- package/dist/server/hover.js +595 -0
- package/dist/server/identity.js +114 -0
- package/dist/server/invites.js +241 -0
- package/dist/server/layoutthread.js +51 -0
- package/dist/server/library.js +600 -0
- package/dist/server/libraryroute.js +211 -0
- package/dist/server/live.js +1101 -0
- package/dist/server/lostfound.js +199 -0
- package/dist/server/main.js +1394 -0
- package/dist/server/mcptools.js +497 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +156 -0
- package/dist/server/poppler.js +251 -0
- package/dist/server/presence.js +252 -0
- package/dist/server/project.js +222 -0
- package/dist/server/prompt.js +53 -0
- package/dist/server/readonly.js +152 -0
- package/dist/server/readonlygate.js +123 -0
- package/dist/server/renames.js +268 -0
- package/dist/server/review.js +334 -0
- package/dist/server/reviewapi.js +188 -0
- package/dist/server/snapshot.js +49 -0
- package/dist/server/sourcestore.js +473 -0
- package/dist/server/status.js +360 -0
- package/dist/server/talk.js +476 -0
- package/dist/server/tokens.js +221 -0
- package/dist/server/usage.js +501 -0
- package/dist/server/versions.js +82 -0
- package/dist/server/view.js +157 -0
- package/dist/server/views.js +1167 -0
- package/dist/shared/askfollow.js +46 -0
- package/dist/shared/board.js +332 -0
- package/dist/shared/build.js +2 -0
- package/dist/shared/collab.js +148 -0
- package/dist/shared/comment.js +285 -0
- package/dist/shared/context.js +157 -0
- package/dist/shared/doc/authors.js +98 -0
- package/dist/shared/doc/build.js +358 -0
- package/dist/shared/doc/latex.js +455 -0
- package/dist/shared/doc/markdown.js +159 -0
- package/dist/shared/doc/model.js +37 -0
- package/dist/shared/doc/order.js +107 -0
- package/dist/shared/doc/proof.js +272 -0
- package/dist/shared/doc/sections.js +93 -0
- package/dist/shared/doc/settings.js +75 -0
- package/dist/shared/edgecodec.js +102 -0
- package/dist/shared/editor.js +64 -0
- package/dist/shared/excerpt.js +87 -0
- package/dist/shared/graph.js +16 -0
- package/dist/shared/items.js +92 -0
- package/dist/shared/keys.js +203 -0
- package/dist/shared/layout.js +926 -0
- package/dist/shared/lexer.js +130 -0
- package/dist/shared/library.js +711 -0
- package/dist/shared/lostfound.js +150 -0
- package/dist/shared/moves.js +243 -0
- package/dist/shared/notecard.js +295 -0
- package/dist/shared/noteplace.js +505 -0
- package/dist/shared/palette.js +46 -0
- package/dist/shared/pastgraph.js +95 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +810 -0
- package/dist/shared/review.js +642 -0
- package/dist/shared/reviewqueue.js +333 -0
- package/dist/shared/ring.js +366 -0
- package/dist/shared/scope.js +255 -0
- package/dist/shared/scopeedit.js +81 -0
- package/dist/shared/search.js +191 -0
- package/dist/shared/separate.js +178 -0
- package/dist/shared/sharesnap.js +280 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +298 -0
- package/dist/shared/viewgraph.js +139 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +48733 -0
- package/dist/web/bundle/edges.js +409 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +239 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +787 -0
- package/dist/web/style.css +2312 -0
- package/docs/agent-api/README.md +91 -0
- package/docs/agent-api/advice.md +108 -0
- package/docs/agent-api/export.md +120 -0
- package/docs/agent-api/following.md +140 -0
- package/docs/agent-api/history.md +142 -0
- package/docs/agent-api/library.md +65 -0
- package/docs/agent-api/mcp.md +104 -0
- package/docs/agent-api/notes.md +93 -0
- package/docs/agent-api/reading.md +166 -0
- package/docs/agent-api/requests.md +94 -0
- package/docs/agent-api/talking.md +100 -0
- package/docs/agent-api/tcb.md +135 -0
- package/docs/agent-api/views.md +124 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- package/test/toolchains.json +5 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
// Positions for every node, clustered by namespace or by file.
|
|
2
|
+
//
|
|
3
|
+
// The layout is a tree of discs. Each cluster lays out its own children — its nodes as small
|
|
4
|
+
// circles, its sub-clusters as the discs they already computed — and reports the disc that
|
|
5
|
+
// encloses them. So a cluster's interior never depends on anything outside it, folding a cluster
|
|
6
|
+
// is just drawing its disc, nothing overlaps, and the same graph gives the same picture.
|
|
7
|
+
//
|
|
8
|
+
// Inside a cluster, what is used sits low and what uses it sits high (CanvasSpec.md, "Dependency
|
|
9
|
+
// order"): every dependency is a 1 V battery, each cluster solves the network of its children with
|
|
10
|
+
// everything outside it as the ground, and the children settle into the disc by potential, the
|
|
11
|
+
// highest on top, like liquids of different density.
|
|
12
|
+
//
|
|
13
|
+
// A board may hide levels of its tree (ViewSpec decision 24): a hidden cluster's sub-clusters and
|
|
14
|
+
// declarations sit in its parent, and the tree is laid out as it then is.
|
|
15
|
+
import { packEnclose, packSiblings } from "d3-hierarchy";
|
|
16
|
+
import { Separator } from "./separate.js";
|
|
17
|
+
const LEAF_R = 5;
|
|
18
|
+
/** gap between declarations that touch, as the page packs them */
|
|
19
|
+
const GAP = 2.5;
|
|
20
|
+
/** the outside world's cluster, at the top level of either way of clustering */
|
|
21
|
+
export const EXT_KEY = "⟨external⟩";
|
|
22
|
+
/** Lean names split on dots, except inside «guillemets». */
|
|
23
|
+
export function splitName(name) {
|
|
24
|
+
const out = [];
|
|
25
|
+
let cur = "";
|
|
26
|
+
let depth = 0;
|
|
27
|
+
for (const ch of name) {
|
|
28
|
+
if (ch === "«")
|
|
29
|
+
depth++;
|
|
30
|
+
else if (ch === "»")
|
|
31
|
+
depth--;
|
|
32
|
+
if (ch === "." && depth === 0) {
|
|
33
|
+
out.push(cur);
|
|
34
|
+
cur = "";
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
cur += ch;
|
|
38
|
+
}
|
|
39
|
+
out.push(cur);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function tnode(key, label, parent, ext) {
|
|
43
|
+
const none = new Float64Array(0);
|
|
44
|
+
return { key, label, parent, kids: new Map(), leaves: [], ext, count: 0, x: 0, y: 0, r: 0, id: -1, slot: 0, m: 0, flow: new Map(), gOut: none, gIn: none };
|
|
45
|
+
}
|
|
46
|
+
/** The parts of the path of a declaration's cluster by `mode`: its namespace or its file, the
|
|
47
|
+
* outside world under its own cluster; `prefixes` the names that are namespaces as well (a
|
|
48
|
+
* structure sits in its own namespace's cluster). A visitor's place is read from it too
|
|
49
|
+
* (web/src/visitors.ts, `planVisitors`). */
|
|
50
|
+
export function pathOf(mode, n, prefixes) {
|
|
51
|
+
if (n.ext) {
|
|
52
|
+
if (mode === "module")
|
|
53
|
+
return [EXT_KEY, ...n.module.split(".").slice(0, 2)];
|
|
54
|
+
const parts = splitName(n.name);
|
|
55
|
+
return parts.length > 1 ? [EXT_KEY, parts[0]] : [EXT_KEY];
|
|
56
|
+
}
|
|
57
|
+
if (mode === "module")
|
|
58
|
+
return n.module.split(".");
|
|
59
|
+
const parts = splitName(n.name);
|
|
60
|
+
// a structure and its namespace are one thing: `Foo` sits in the cluster of `Foo.bar`
|
|
61
|
+
return prefixes.has(n.name) ? parts : parts.slice(0, -1);
|
|
62
|
+
}
|
|
63
|
+
/** Every key a cluster of a layout of `nodes` by `mode` can have: the tree computeLayout builds
|
|
64
|
+
* before it merges chains, whose keys the merged one keeps. What a layout's memory may speak of
|
|
65
|
+
* (server/agentviews.ts checks a memory a page hands over against it). */
|
|
66
|
+
export function clusterKeys(mode, nodes) {
|
|
67
|
+
const prefixes = new Set();
|
|
68
|
+
if (mode === "namespace") {
|
|
69
|
+
for (const n of nodes) {
|
|
70
|
+
if (n.ext)
|
|
71
|
+
continue;
|
|
72
|
+
const parts = splitName(n.name);
|
|
73
|
+
for (let i = 1; i < parts.length; i++)
|
|
74
|
+
prefixes.add(parts.slice(0, i).join("."));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const keys = new Set();
|
|
78
|
+
for (const n of nodes) {
|
|
79
|
+
let key = "";
|
|
80
|
+
for (const part of pathOf(mode, n, prefixes)) {
|
|
81
|
+
key = key ? `${key}.${part}` : part;
|
|
82
|
+
keys.add(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return keys;
|
|
86
|
+
}
|
|
87
|
+
/** Each node's innermost cluster key by `mode` in that same tree, before chains are merged and
|
|
88
|
+
* levels hidden ("" for a node at the top level): the keys around it are its prefixes. A structure
|
|
89
|
+
* sits in the cluster of its namespace where `nodes` hold what is in that namespace, as a board
|
|
90
|
+
* draws it. What a note's place is read against (shared/noteplace.ts). */
|
|
91
|
+
export function innerKeys(mode, nodes) {
|
|
92
|
+
const prefixes = new Set();
|
|
93
|
+
if (mode === "namespace") {
|
|
94
|
+
for (const n of nodes) {
|
|
95
|
+
if (n.ext)
|
|
96
|
+
continue;
|
|
97
|
+
const parts = splitName(n.name);
|
|
98
|
+
for (let i = 1; i < parts.length; i++)
|
|
99
|
+
prefixes.add(parts.slice(0, i).join("."));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return nodes.map((n) => pathOf(mode, n, prefixes).join("."));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Potentials of a cluster's children (CanvasSpec.md, "Dependency order"): every dependency a → b is
|
|
106
|
+
* a 1 V battery; a pair of children is joined with conductance log(1 + its dependencies) and a
|
|
107
|
+
* battery of (a→b − b→a) / (a→b + b→a); the outside world is held at 0 V. Minimises
|
|
108
|
+
* Σ conductance · (φ_a − φ_b − battery)² by conjugate gradients on the grounded Laplacian.
|
|
109
|
+
*/
|
|
110
|
+
function potentials(c) {
|
|
111
|
+
const m = c.m;
|
|
112
|
+
const pa = [], pb = [], pc = [];
|
|
113
|
+
const rhs = new Float64Array(m);
|
|
114
|
+
const diag = new Float64Array(m);
|
|
115
|
+
const join = (a, b, ab, ba) => {
|
|
116
|
+
const tot = ab + ba;
|
|
117
|
+
const g = Math.log1p(tot);
|
|
118
|
+
const drop = (ab - ba) / tot;
|
|
119
|
+
rhs[a] += g * drop;
|
|
120
|
+
diag[a] += g;
|
|
121
|
+
if (b < m) {
|
|
122
|
+
rhs[b] -= g * drop;
|
|
123
|
+
diag[b] += g;
|
|
124
|
+
pa.push(a);
|
|
125
|
+
pb.push(b);
|
|
126
|
+
pc.push(g);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
for (const [key, ab] of c.flow) {
|
|
130
|
+
const a = Math.floor(key / (m + 1)), b = key - a * (m + 1);
|
|
131
|
+
const ba = c.flow.get(b * (m + 1) + a) ?? 0;
|
|
132
|
+
if (a < b || ba === 0)
|
|
133
|
+
join(a, b, ab, ba);
|
|
134
|
+
}
|
|
135
|
+
for (let a = 0; a < m; a++)
|
|
136
|
+
if (c.gOut[a] + c.gIn[a] > 0)
|
|
137
|
+
join(a, m, c.gOut[a], c.gIn[a]);
|
|
138
|
+
// a child joined to nothing rests at the ground; the small term keeps the system definite
|
|
139
|
+
const EPS = 1e-4;
|
|
140
|
+
const mul = (x, y) => {
|
|
141
|
+
for (let k = 0; k < m; k++)
|
|
142
|
+
y[k] = (diag[k] + EPS) * x[k];
|
|
143
|
+
for (let e = 0; e < pa.length; e++) {
|
|
144
|
+
const a = pa[e], b = pb[e], g = pc[e];
|
|
145
|
+
y[a] -= g * x[b];
|
|
146
|
+
y[b] -= g * x[a];
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const phi = new Float64Array(m);
|
|
150
|
+
const r = rhs.slice();
|
|
151
|
+
const p = r.slice();
|
|
152
|
+
const Ap = new Float64Array(m);
|
|
153
|
+
let rr = r.reduce((s, v) => s + v * v, 0);
|
|
154
|
+
const stop = rr * 1e-12;
|
|
155
|
+
for (let it = 0; it < 1000 && rr > stop && rr > 0; it++) {
|
|
156
|
+
mul(p, Ap);
|
|
157
|
+
let pAp = 0;
|
|
158
|
+
for (let k = 0; k < m; k++)
|
|
159
|
+
pAp += p[k] * Ap[k];
|
|
160
|
+
const alpha = rr / pAp;
|
|
161
|
+
let next = 0;
|
|
162
|
+
for (let k = 0; k < m; k++) {
|
|
163
|
+
phi[k] += alpha * p[k];
|
|
164
|
+
r[k] -= alpha * Ap[k];
|
|
165
|
+
next += r[k] * r[k];
|
|
166
|
+
}
|
|
167
|
+
const beta = next / rr;
|
|
168
|
+
rr = next;
|
|
169
|
+
for (let k = 0; k < m; k++)
|
|
170
|
+
p[k] = r[k] + beta * p[k];
|
|
171
|
+
}
|
|
172
|
+
return phi;
|
|
173
|
+
}
|
|
174
|
+
const separator = new Separator(2 * LEAF_R + GAP);
|
|
175
|
+
/** declarations sit on a hexagonal lattice this far apart: touching, with room for rounding */
|
|
176
|
+
const PITCH = 2 * LEAF_R + 0.3;
|
|
177
|
+
/**
|
|
178
|
+
* The children settle into a disc by potential, the highest on top. Each one's height is where the
|
|
179
|
+
* area of everything above it ends (the share of a disc above a chord). Sub-clusters, largest
|
|
180
|
+
* first, take the free spot nearest their height, sideways being cheap; declarations then fill the
|
|
181
|
+
* hexagonal lattice cells nearest the centre that the sub-clusters leave free, the highest
|
|
182
|
+
* potential in the highest cell.
|
|
183
|
+
*/
|
|
184
|
+
function settle(items, phi) {
|
|
185
|
+
const n = items.length;
|
|
186
|
+
const order = [...items.keys()].sort((p, q) => phi[q] - phi[p] || p - q);
|
|
187
|
+
let total = 0;
|
|
188
|
+
for (const it of items)
|
|
189
|
+
total += Math.PI * (it.r + 0.3) ** 2;
|
|
190
|
+
const R = Math.sqrt(total / (Math.PI * 0.78));
|
|
191
|
+
// the share of a unit disc's area above the chord at height t
|
|
192
|
+
const above = (t) => (Math.acos(t) - t * Math.sqrt(1 - t * t)) / Math.PI;
|
|
193
|
+
const heightOf = (f) => {
|
|
194
|
+
let lo = -1, hi = 1;
|
|
195
|
+
for (let k = 0; k < 40; k++) {
|
|
196
|
+
const mid = (lo + hi) / 2;
|
|
197
|
+
if (above(mid) > f)
|
|
198
|
+
lo = mid;
|
|
199
|
+
else
|
|
200
|
+
hi = mid;
|
|
201
|
+
}
|
|
202
|
+
return (lo + hi) / 2;
|
|
203
|
+
};
|
|
204
|
+
const share = new Float64Array(n);
|
|
205
|
+
let acc = 0;
|
|
206
|
+
for (const k of order) {
|
|
207
|
+
const a = Math.PI * (items[k].r + 0.3) ** 2;
|
|
208
|
+
share[k] = heightOf((acc + a / 2) / total);
|
|
209
|
+
acc += a;
|
|
210
|
+
}
|
|
211
|
+
const big = [...items.keys()].filter((k) => items[k].sub).sort((p, q) => items[q].r - items[p].r || p - q);
|
|
212
|
+
const leaves = order.filter((k) => !items[k].sub);
|
|
213
|
+
if (big.length === 0) {
|
|
214
|
+
// declarations only: all of one size, so any compact packing will do — pack them tightly,
|
|
215
|
+
// then hand the places out from the top down by potential
|
|
216
|
+
const spots = packSiblings(leaves.map(() => ({ x: 0, y: 0, r: LEAF_R + 0.15 })));
|
|
217
|
+
spots.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
218
|
+
leaves.forEach((k, j) => {
|
|
219
|
+
items[k].x = spots[j].x;
|
|
220
|
+
items[k].y = spots[j].y;
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// placed as tall as the estimate says, then stretched or squeezed until it comes out round
|
|
225
|
+
let scale = R;
|
|
226
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
227
|
+
place(share.map((t) => -t * scale));
|
|
228
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
229
|
+
for (const it of items) {
|
|
230
|
+
x0 = Math.min(x0, it.x - it.r);
|
|
231
|
+
x1 = Math.max(x1, it.x + it.r);
|
|
232
|
+
y0 = Math.min(y0, it.y - it.r);
|
|
233
|
+
y1 = Math.max(y1, it.y + it.r);
|
|
234
|
+
}
|
|
235
|
+
const aspect = (x1 - x0) / Math.max(1e-6, y1 - y0);
|
|
236
|
+
if (Math.abs(aspect - 1) < 0.06 || big.length === 0)
|
|
237
|
+
break;
|
|
238
|
+
scale *= Math.sqrt(aspect);
|
|
239
|
+
}
|
|
240
|
+
finish();
|
|
241
|
+
function place(ty) {
|
|
242
|
+
// 1. sub-clusters, largest first
|
|
243
|
+
const px = [], py = [], pr = [];
|
|
244
|
+
const free = (x, y, r) => {
|
|
245
|
+
for (let j = 0; j < px.length; j++) {
|
|
246
|
+
const need = r + pr[j] + 0.3;
|
|
247
|
+
if ((x - px[j]) ** 2 + (y - py[j]) ** 2 < need * need)
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
};
|
|
252
|
+
const cost = (x, y, t) => (y - t) ** 2 + 0.3 * x * x;
|
|
253
|
+
const ANGLES = 24;
|
|
254
|
+
for (const k of big) {
|
|
255
|
+
const r = items[k].r;
|
|
256
|
+
const t = ty[k];
|
|
257
|
+
let bx = 0, by = t, best = free(0, t, r) ? 0 : Infinity;
|
|
258
|
+
if (best > 0) {
|
|
259
|
+
// touching one of the placed discs nearest the target
|
|
260
|
+
const near = px.map((_, j) => j).sort((a, b) => Math.hypot(px[a], py[a] - t) - pr[a] - (Math.hypot(px[b], py[b] - t) - pr[b])).slice(0, 12);
|
|
261
|
+
for (const j of near) {
|
|
262
|
+
const d = r + pr[j] + 0.3;
|
|
263
|
+
for (let s = 0; s < ANGLES; s++) {
|
|
264
|
+
const a = (2 * Math.PI * s) / ANGLES;
|
|
265
|
+
const x = px[j] + d * Math.cos(a), y = py[j] + d * Math.sin(a);
|
|
266
|
+
const c = cost(x, y, t);
|
|
267
|
+
if (c < best && free(x, y, r)) {
|
|
268
|
+
best = c;
|
|
269
|
+
bx = x;
|
|
270
|
+
by = y;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// nothing free beside them: further out along the height
|
|
275
|
+
for (let step = 1; best === Infinity; step++) {
|
|
276
|
+
for (const x of [step * r, -step * r])
|
|
277
|
+
if (best === Infinity && free(x, t, r))
|
|
278
|
+
[best, bx, by] = [0, x, t];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
items[k].x = bx;
|
|
282
|
+
items[k].y = by;
|
|
283
|
+
px.push(bx);
|
|
284
|
+
py.push(by);
|
|
285
|
+
pr.push(r);
|
|
286
|
+
}
|
|
287
|
+
// close the gaps the one-by-one placement left: drawn towards the middle, held at their heights
|
|
288
|
+
if (big.length > 1) {
|
|
289
|
+
const D = {
|
|
290
|
+
n: big.length,
|
|
291
|
+
x: Float64Array.from(px),
|
|
292
|
+
y: Float64Array.from(py),
|
|
293
|
+
r: Float64Array.from(pr, (r) => r + 0.3),
|
|
294
|
+
pinned: new Uint8Array(big.length),
|
|
295
|
+
seq: new Float64Array(big.length),
|
|
296
|
+
big: Int32Array.from(big.keys()),
|
|
297
|
+
small: new Int32Array(0),
|
|
298
|
+
};
|
|
299
|
+
for (let it = 0; it < 80; it++) {
|
|
300
|
+
for (let j = 0; j < big.length; j++) {
|
|
301
|
+
D.x[j] *= 0.93;
|
|
302
|
+
D.y[j] += (ty[big[j]] - D.y[j]) * 0.1;
|
|
303
|
+
D.y[j] *= 0.985;
|
|
304
|
+
}
|
|
305
|
+
separator.separate(D, 4, null);
|
|
306
|
+
}
|
|
307
|
+
for (let guard = 0; guard < 20 && separator.overlaps(D); guard++)
|
|
308
|
+
separator.separate(D, 50, null);
|
|
309
|
+
big.forEach((k, j) => {
|
|
310
|
+
items[k].x = px[j] = D.x[j];
|
|
311
|
+
items[k].y = py[j] = D.y[j];
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// 2. declarations on the lattice cells nearest the centre that the discs leave free
|
|
315
|
+
if (leaves.length) {
|
|
316
|
+
const rowH = (PITCH * Math.sqrt(3)) / 2;
|
|
317
|
+
let Rl = Math.sqrt(leaves.length) * PITCH * 0.55 + 2 * PITCH;
|
|
318
|
+
let cells = [];
|
|
319
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
320
|
+
cells = [];
|
|
321
|
+
const rows = Math.ceil(Rl / rowH);
|
|
322
|
+
for (let j = -rows; j <= rows; j++) {
|
|
323
|
+
const y = j * rowH;
|
|
324
|
+
const off = j & 1 ? PITCH / 2 : 0;
|
|
325
|
+
const cols = Math.ceil(Rl / PITCH) + 1;
|
|
326
|
+
for (let i = -cols; i <= cols; i++) {
|
|
327
|
+
const x = i * PITCH + off;
|
|
328
|
+
const d2 = x * x + y * y;
|
|
329
|
+
if (d2 > Rl * Rl)
|
|
330
|
+
continue;
|
|
331
|
+
let ok = true;
|
|
332
|
+
for (let q = 0; q < px.length && ok; q++) {
|
|
333
|
+
const need = pr[q] + LEAF_R + 0.3;
|
|
334
|
+
if ((x - px[q]) ** 2 + (y - py[q]) ** 2 < need * need)
|
|
335
|
+
ok = false;
|
|
336
|
+
}
|
|
337
|
+
if (ok)
|
|
338
|
+
cells.push([x, y, d2]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (cells.length >= leaves.length)
|
|
342
|
+
break;
|
|
343
|
+
Rl *= 1.08;
|
|
344
|
+
}
|
|
345
|
+
cells.sort((a, b) => a[2] - b[2] || a[1] - b[1] || a[0] - b[0]);
|
|
346
|
+
const chosen = cells.slice(0, leaves.length).sort((a, b) => a[1] - b[1] || a[0] - b[0]);
|
|
347
|
+
leaves.forEach((k, j) => {
|
|
348
|
+
items[k].x = chosen[j][0];
|
|
349
|
+
items[k].y = chosen[j][1];
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function finish() {
|
|
354
|
+
// nothing should touch now; if anything does, it is pushed apart
|
|
355
|
+
const B = {
|
|
356
|
+
n,
|
|
357
|
+
x: Float64Array.from(items, (it) => it.x),
|
|
358
|
+
y: Float64Array.from(items, (it) => it.y),
|
|
359
|
+
r: Float64Array.from(items, (it) => it.r + 0.3),
|
|
360
|
+
pinned: new Uint8Array(n),
|
|
361
|
+
seq: new Float64Array(n),
|
|
362
|
+
big: Int32Array.from(big),
|
|
363
|
+
small: Int32Array.from(leaves),
|
|
364
|
+
};
|
|
365
|
+
if (separator.overlaps(B)) {
|
|
366
|
+
for (let guard = 0; guard < 20 && separator.overlaps(B); guard++)
|
|
367
|
+
separator.separate(B, 50, null);
|
|
368
|
+
items.forEach((it, k) => {
|
|
369
|
+
it.x = B.x[k];
|
|
370
|
+
it.y = B.y[k];
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function layoutCluster(c, pos) {
|
|
376
|
+
for (const k of c.kids.values())
|
|
377
|
+
layoutCluster(k, pos);
|
|
378
|
+
// children in slot order: sub-clusters, then declarations
|
|
379
|
+
const items = [];
|
|
380
|
+
for (const k of c.kids.values())
|
|
381
|
+
items.push({ key: "c:" + k.key, r: k.r, x: 0, y: 0, sub: k });
|
|
382
|
+
for (const n of c.leaves)
|
|
383
|
+
items.push({ key: "n:" + n, r: LEAF_R, x: 0, y: 0, leaf: n });
|
|
384
|
+
c.count = c.leaves.length + [...c.kids.values()].reduce((a, k) => a + k.count, 0);
|
|
385
|
+
if (items.length === 1) {
|
|
386
|
+
items[0].x = 0;
|
|
387
|
+
items[0].y = 0;
|
|
388
|
+
}
|
|
389
|
+
else if (items.length > 1) {
|
|
390
|
+
settle(items, potentials(c));
|
|
391
|
+
}
|
|
392
|
+
// the enclosing disc, with room for the label
|
|
393
|
+
const enc = items.length ? packEnclose(items.map((i) => ({ x: i.x, y: i.y, r: i.r }))) : { x: 0, y: 0, r: 0 };
|
|
394
|
+
const pad = c.parent ? 10 + Math.min(14, Math.sqrt(c.count)) : 0;
|
|
395
|
+
c.r = (enc?.r ?? 0) + pad;
|
|
396
|
+
for (const it of items) {
|
|
397
|
+
const x = it.x - (enc?.x ?? 0);
|
|
398
|
+
const y = it.y - (enc?.y ?? 0);
|
|
399
|
+
if (it.sub) {
|
|
400
|
+
it.sub.x = x;
|
|
401
|
+
it.sub.y = y;
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
pos[2 * it.leaf] = x;
|
|
405
|
+
pos[2 * it.leaf + 1] = y;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** The cluster tree of `nodes` by `mode` as a board shows it, each node in its cluster (`home`): a
|
|
410
|
+
* cluster holding nothing but one cluster merged into it (one circle, the labels joined), and the
|
|
411
|
+
* levels `hidden` names taken out (ViewSpec decision 24) — what a hidden cluster holds, its
|
|
412
|
+
* sub-clusters and its declarations, sits in its parent, the top level's at the top. A key is a
|
|
413
|
+
* full path, so whatever moves up keeps one no other cluster has: two hidden siblings' declarations
|
|
414
|
+
* simply become siblings. A cluster that moved up keeps its label, unless a sibling reads the same;
|
|
415
|
+
* then it says which hidden levels it came through (`Proof.Basic` beside `Basic`). */
|
|
416
|
+
function buildTree(mode, nodes, hidden) {
|
|
417
|
+
const prefixes = new Set();
|
|
418
|
+
if (mode === "namespace") {
|
|
419
|
+
for (const n of nodes) {
|
|
420
|
+
if (n.ext)
|
|
421
|
+
continue;
|
|
422
|
+
const parts = splitName(n.name);
|
|
423
|
+
for (let i = 1; i < parts.length; i++)
|
|
424
|
+
prefixes.add(parts.slice(0, i).join("."));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const root = tnode("", "", null, false);
|
|
428
|
+
const home = new Array(nodes.length);
|
|
429
|
+
nodes.forEach((n, i) => {
|
|
430
|
+
let cur = root;
|
|
431
|
+
for (const part of pathOf(mode, n, prefixes)) {
|
|
432
|
+
const key = cur.key ? `${cur.key}.${part}` : part;
|
|
433
|
+
let next = cur.kids.get(part);
|
|
434
|
+
if (!next) {
|
|
435
|
+
next = tnode(key, part === EXT_KEY ? "External" : part, cur, cur.ext || part === EXT_KEY);
|
|
436
|
+
cur.kids.set(part, next);
|
|
437
|
+
}
|
|
438
|
+
cur = next;
|
|
439
|
+
}
|
|
440
|
+
cur.leaves.push(i);
|
|
441
|
+
});
|
|
442
|
+
// a cluster holding nothing but one cluster says nothing: merge the chain into one label
|
|
443
|
+
const collapse = (c) => {
|
|
444
|
+
for (const [part, k] of [...c.kids]) {
|
|
445
|
+
let kid = k;
|
|
446
|
+
while (kid.leaves.length === 0 && kid.kids.size === 1 && kid.key !== EXT_KEY) {
|
|
447
|
+
const only = [...kid.kids.values()][0];
|
|
448
|
+
only.label = `${kid.label}.${only.label}`;
|
|
449
|
+
only.parent = c;
|
|
450
|
+
kid = only;
|
|
451
|
+
}
|
|
452
|
+
c.kids.set(part, kid);
|
|
453
|
+
collapse(kid);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
collapse(root);
|
|
457
|
+
// the hidden levels, bottom-up so that one inside another moves up twice; a merged chain is
|
|
458
|
+
// hidden by its key, the deepest of its levels, which is the circle's. Children are known by
|
|
459
|
+
// their keys from here on: two that moved up can share a last part, never a key.
|
|
460
|
+
const via = new Map();
|
|
461
|
+
const lift = (c) => {
|
|
462
|
+
const kids = new Map();
|
|
463
|
+
for (const k of c.kids.values()) {
|
|
464
|
+
lift(k);
|
|
465
|
+
if (!hidden.has(k.key)) {
|
|
466
|
+
kids.set(k.key, k);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
for (const kk of k.kids.values()) {
|
|
470
|
+
kk.parent = c;
|
|
471
|
+
via.set(kk, [k.label, ...(via.get(kk) ?? [])]);
|
|
472
|
+
kids.set(kk.key, kk);
|
|
473
|
+
}
|
|
474
|
+
c.leaves.push(...k.leaves);
|
|
475
|
+
}
|
|
476
|
+
c.kids = kids;
|
|
477
|
+
// labels are told apart where they are drawn: never inside a level that goes
|
|
478
|
+
if (hidden.has(c.key))
|
|
479
|
+
return;
|
|
480
|
+
const seen = new Map();
|
|
481
|
+
for (const k of kids.values())
|
|
482
|
+
seen.set(k.label, (seen.get(k.label) ?? 0) + 1);
|
|
483
|
+
for (const k of kids.values())
|
|
484
|
+
if (seen.get(k.label) > 1 && via.has(k))
|
|
485
|
+
k.label = [...via.get(k), k.label].join(".");
|
|
486
|
+
};
|
|
487
|
+
if (hidden.size)
|
|
488
|
+
lift(root);
|
|
489
|
+
const reindex = (c) => {
|
|
490
|
+
for (const n of c.leaves)
|
|
491
|
+
home[n] = c;
|
|
492
|
+
for (const k of c.kids.values())
|
|
493
|
+
reindex(k);
|
|
494
|
+
};
|
|
495
|
+
reindex(root);
|
|
496
|
+
return { root, home };
|
|
497
|
+
}
|
|
498
|
+
/** The tree as a layout: clusters parents first (each with its depth), every node in its cluster;
|
|
499
|
+
* places from `rel` (each node relative to its cluster, each cluster to its parent, as
|
|
500
|
+
* layoutCluster leaves them), or all at the origin. */
|
|
501
|
+
function flatten(root, n, rel) {
|
|
502
|
+
const clusters = [];
|
|
503
|
+
const x = new Array(n).fill(0);
|
|
504
|
+
const y = new Array(n).fill(0);
|
|
505
|
+
const cluster = new Array(n).fill(-1);
|
|
506
|
+
const place = (c, ox, oy, parentId, depth) => {
|
|
507
|
+
const cx = ox + c.x;
|
|
508
|
+
const cy = oy + c.y;
|
|
509
|
+
let id = parentId;
|
|
510
|
+
if (c.parent) {
|
|
511
|
+
id = clusters.length;
|
|
512
|
+
c.id = id;
|
|
513
|
+
clusters.push({
|
|
514
|
+
key: c.key, label: c.label, parent: parentId, depth,
|
|
515
|
+
x: round(cx), y: round(cy), lx: 0, ly: 0, r: round(c.r), count: c.count, ext: c.ext,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
for (const i of c.leaves) {
|
|
519
|
+
if (rel) {
|
|
520
|
+
x[i] = round(cx + rel[2 * i]);
|
|
521
|
+
y[i] = round(cy + rel[2 * i + 1]);
|
|
522
|
+
}
|
|
523
|
+
cluster[i] = id;
|
|
524
|
+
}
|
|
525
|
+
for (const k of c.kids.values())
|
|
526
|
+
place(k, cx, cy, id, c.parent ? depth + 1 : 0);
|
|
527
|
+
};
|
|
528
|
+
place(root, 0, 0, -1, 0);
|
|
529
|
+
return withLocal({ x, y, lx: [], ly: [], cluster, clusters });
|
|
530
|
+
}
|
|
531
|
+
/** The cluster tree a layout of `nodes` by `mode` with the levels `hidden` hidden has, everything at
|
|
532
|
+
* the origin: a board's structure — its clusters, their parents, depths, labels and counts, each
|
|
533
|
+
* node's cluster, in the order computeLayout gives them — without the cost of laying it out. What
|
|
534
|
+
* a layout's memory is recalled or regrouped onto (shared/board.ts), and what the exported
|
|
535
|
+
* document's sections follow (shared/doc/sections.ts). */
|
|
536
|
+
export function clusterTree(mode, nodes, hidden = []) {
|
|
537
|
+
const { root } = buildTree(mode, nodes, new Set(hidden));
|
|
538
|
+
const count = (c) => (c.count = c.leaves.length + [...c.kids.values()].reduce((a, k) => a + count(k), 0));
|
|
539
|
+
count(root);
|
|
540
|
+
return flatten(root, nodes.length, null);
|
|
541
|
+
}
|
|
542
|
+
export function computeLayout(mode, nodes, edges, hidden = []) {
|
|
543
|
+
// 1. the tree
|
|
544
|
+
const { root, home } = buildTree(mode, nodes, new Set(hidden));
|
|
545
|
+
// every cluster numbers its children: sub-clusters first, then declarations
|
|
546
|
+
const leafSlot = new Int32Array(nodes.length);
|
|
547
|
+
const number = (c) => {
|
|
548
|
+
let s = 0;
|
|
549
|
+
for (const k of c.kids.values()) {
|
|
550
|
+
k.slot = s++;
|
|
551
|
+
number(k);
|
|
552
|
+
}
|
|
553
|
+
for (const n of c.leaves)
|
|
554
|
+
leafSlot[n] = s++;
|
|
555
|
+
c.m = s;
|
|
556
|
+
c.gOut = new Float64Array(s);
|
|
557
|
+
c.gIn = new Float64Array(s);
|
|
558
|
+
};
|
|
559
|
+
number(root);
|
|
560
|
+
// 2. edges: a dependency counts in the cluster where its two ends meet, between the children
|
|
561
|
+
// holding them; in every cluster below that, the other end is outside — the ground
|
|
562
|
+
const chain = (c) => {
|
|
563
|
+
const out = [];
|
|
564
|
+
for (let x = c; x; x = x.parent)
|
|
565
|
+
out.push(x);
|
|
566
|
+
return out.reverse();
|
|
567
|
+
};
|
|
568
|
+
const chains = new Map();
|
|
569
|
+
const chainOf = (c) => {
|
|
570
|
+
let ch = chains.get(c);
|
|
571
|
+
if (!ch)
|
|
572
|
+
chains.set(c, (ch = chain(c)));
|
|
573
|
+
return ch;
|
|
574
|
+
};
|
|
575
|
+
for (let e = 0; e < edges.length; e += 3) {
|
|
576
|
+
const u = edges[e];
|
|
577
|
+
const v = edges[e + 1];
|
|
578
|
+
if (u === v)
|
|
579
|
+
continue;
|
|
580
|
+
const cu = chainOf(home[u]);
|
|
581
|
+
const cv = chainOf(home[v]);
|
|
582
|
+
let i = 0;
|
|
583
|
+
while (i < cu.length && i < cv.length && cu[i] === cv[i])
|
|
584
|
+
i++;
|
|
585
|
+
const meet = cu[i - 1];
|
|
586
|
+
const a = i < cu.length ? cu[i].slot : leafSlot[u];
|
|
587
|
+
const b = i < cv.length ? cv[i].slot : leafSlot[v];
|
|
588
|
+
if (a !== b) {
|
|
589
|
+
const key = a * (meet.m + 1) + b;
|
|
590
|
+
meet.flow.set(key, (meet.flow.get(key) ?? 0) + 1);
|
|
591
|
+
}
|
|
592
|
+
for (let j = i; j < cu.length; j++)
|
|
593
|
+
cu[j].gOut[j + 1 < cu.length ? cu[j + 1].slot : leafSlot[u]]++;
|
|
594
|
+
for (let j = i; j < cv.length; j++)
|
|
595
|
+
cv[j].gIn[j + 1 < cv.length ? cv[j + 1].slot : leafSlot[v]]++;
|
|
596
|
+
}
|
|
597
|
+
// 3. bottom-up discs, then top-down absolute positions
|
|
598
|
+
const rel = new Float64Array(nodes.length * 2);
|
|
599
|
+
layoutCluster(root, rel);
|
|
600
|
+
return flatten(root, nodes.length, rel);
|
|
601
|
+
}
|
|
602
|
+
/** Fill in the local coordinates from the world ones: a cluster relative to its parent, a node
|
|
603
|
+
* relative to its cluster. The scene graph on the page works in these. */
|
|
604
|
+
export function withLocal(L) {
|
|
605
|
+
for (const c of L.clusters) {
|
|
606
|
+
const p = c.parent >= 0 ? L.clusters[c.parent] : null;
|
|
607
|
+
c.lx = round(c.x - (p ? p.x : 0));
|
|
608
|
+
c.ly = round(c.y - (p ? p.y : 0));
|
|
609
|
+
}
|
|
610
|
+
L.lx = L.x.map((x, i) => round(x - (L.cluster[i] >= 0 ? L.clusters[L.cluster[i]].x : 0)));
|
|
611
|
+
L.ly = L.y.map((y, i) => round(y - (L.cluster[i] >= 0 ? L.clusters[L.cluster[i]].y : 0)));
|
|
612
|
+
return L;
|
|
613
|
+
}
|
|
614
|
+
function round(v) {
|
|
615
|
+
return Math.round(v * 10) / 10;
|
|
616
|
+
}
|
|
617
|
+
export function memoryOf(layout, nodes) {
|
|
618
|
+
const mem = { nodes: {}, clusters: {} };
|
|
619
|
+
layout.clusters.forEach((c) => (mem.clusters[c.key] = [c.x, c.y, c.r]));
|
|
620
|
+
nodes.forEach((n, i) => {
|
|
621
|
+
const c = layout.cluster[i];
|
|
622
|
+
mem.nodes[n.name] = [layout.x[i], layout.y[i], c >= 0 ? layout.clusters[c].key : ""];
|
|
623
|
+
});
|
|
624
|
+
return mem;
|
|
625
|
+
}
|
|
626
|
+
const SPACING = 2 * LEAF_R;
|
|
627
|
+
/** Points already taken, for finding free room near a target. */
|
|
628
|
+
class Occupancy {
|
|
629
|
+
cells = new Map();
|
|
630
|
+
key(x, y) {
|
|
631
|
+
return `${Math.floor(x / SPACING)},${Math.floor(y / SPACING)}`;
|
|
632
|
+
}
|
|
633
|
+
add(x, y) {
|
|
634
|
+
const k = this.key(x, y);
|
|
635
|
+
const c = this.cells.get(k);
|
|
636
|
+
if (c)
|
|
637
|
+
c.push([x, y]);
|
|
638
|
+
else
|
|
639
|
+
this.cells.set(k, [[x, y]]);
|
|
640
|
+
}
|
|
641
|
+
free(x, y) {
|
|
642
|
+
const gx = Math.floor(x / SPACING);
|
|
643
|
+
const gy = Math.floor(y / SPACING);
|
|
644
|
+
for (let i = gx - 1; i <= gx + 1; i++) {
|
|
645
|
+
for (let j = gy - 1; j <= gy + 1; j++) {
|
|
646
|
+
for (const [px, py] of this.cells.get(`${i},${j}`) ?? []) {
|
|
647
|
+
if ((px - x) ** 2 + (py - y) ** 2 < SPACING * SPACING)
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** The free spot nearest to (x, y), searched outward on a hexagonal spiral; `discs` are
|
|
656
|
+
* sub-clusters the spot must stay out of. */
|
|
657
|
+
function nearestFree(occ, x, y, discs = []) {
|
|
658
|
+
const ok = (px, py) => occ.free(px, py) && discs.every((d) => (d.x - px) ** 2 + (d.y - py) ** 2 >= (d.r + LEAF_R) ** 2);
|
|
659
|
+
if (ok(x, y))
|
|
660
|
+
return [x, y];
|
|
661
|
+
for (let ring = 1; ring < 400; ring++) {
|
|
662
|
+
const n = 6 * ring;
|
|
663
|
+
for (let k = 0; k < n; k++) {
|
|
664
|
+
const a = (2 * Math.PI * k) / n;
|
|
665
|
+
const px = x + Math.cos(a) * ring * SPACING;
|
|
666
|
+
const py = y + Math.sin(a) * ring * SPACING;
|
|
667
|
+
if (ok(px, py))
|
|
668
|
+
return [px, py];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return [x, y];
|
|
672
|
+
}
|
|
673
|
+
/** `fresh` with everything `mem` remembers kept in place, relative to its cluster, unless its
|
|
674
|
+
* height in the cluster (the dependency order) moved by more than a tenth of the cluster's
|
|
675
|
+
* radius: then it goes where the fresh layout puts it. Returns the fresh layout unchanged when
|
|
676
|
+
* there is no memory or too little of the graph survived for it to mean anything. */
|
|
677
|
+
export function stabilize(fresh, nodes, mem) {
|
|
678
|
+
if (!mem)
|
|
679
|
+
return fresh;
|
|
680
|
+
const survivors = nodes.filter((n) => mem.nodes[n.name]).length;
|
|
681
|
+
if (survivors < nodes.length * 0.5)
|
|
682
|
+
return fresh;
|
|
683
|
+
const MOVE = 0.1;
|
|
684
|
+
// the top level has no circle: heights there are measured against the extent of everything
|
|
685
|
+
const extent = (xs) => Math.max(1, ...xs.map(([x, y, r]) => Math.hypot(x, y) + r));
|
|
686
|
+
const freshTop = extent(fresh.clusters.filter((c) => c.parent < 0).map((c) => [c.x, c.y, c.r]));
|
|
687
|
+
const oldTop = extent(Object.values(mem.clusters));
|
|
688
|
+
/** an offset in a frame of radius `r`, kept if its height did not move much */
|
|
689
|
+
const keeps = (fy, fr, oy, or) => Math.abs(fy / fr - oy / or) <= MOVE;
|
|
690
|
+
const cs = fresh.clusters.map((c) => ({ ...c }));
|
|
691
|
+
// parents first: each keeps its old offset in its parent (so a subtree follows its parent), or
|
|
692
|
+
// takes the fresh one
|
|
693
|
+
for (let i = 0; i < cs.length; i++) {
|
|
694
|
+
const c = cs[i];
|
|
695
|
+
const p = c.parent;
|
|
696
|
+
const fp = p >= 0 ? fresh.clusters[p] : null;
|
|
697
|
+
const old = mem.clusters[c.key];
|
|
698
|
+
const oldP = p >= 0 ? mem.clusters[cs[p].key] : null;
|
|
699
|
+
const px = p >= 0 ? cs[p].x : 0, py = p >= 0 ? cs[p].y : 0;
|
|
700
|
+
const fx = fresh.clusters[i].x - (fp ? fp.x : 0), fy = fresh.clusters[i].y - (fp ? fp.y : 0);
|
|
701
|
+
if (old && (p < 0 || oldP)) {
|
|
702
|
+
const ox = old[0] - (oldP ? oldP[0] : 0), oy = old[1] - (oldP ? oldP[1] : 0);
|
|
703
|
+
if (keeps(fy, fp ? fp.r : freshTop, oy, oldP ? oldP[2] : oldTop)) {
|
|
704
|
+
c.x = round(px + ox);
|
|
705
|
+
c.y = round(py + oy);
|
|
706
|
+
c.r = old[2];
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
c.x = round(px + fx);
|
|
711
|
+
c.y = round(py + fy);
|
|
712
|
+
}
|
|
713
|
+
const x = [...fresh.x];
|
|
714
|
+
const y = [...fresh.y];
|
|
715
|
+
const occ = new Occupancy();
|
|
716
|
+
const placed = new Uint8Array(nodes.length);
|
|
717
|
+
nodes.forEach((n, i) => {
|
|
718
|
+
const old = mem.nodes[n.name];
|
|
719
|
+
const c = fresh.cluster[i];
|
|
720
|
+
const key = c >= 0 ? cs[c].key : "";
|
|
721
|
+
if (!old || old[2] !== key)
|
|
722
|
+
return;
|
|
723
|
+
const oldC = c >= 0 ? mem.clusters[key] : null;
|
|
724
|
+
if (c >= 0 && !oldC)
|
|
725
|
+
return;
|
|
726
|
+
const ox = old[0] - (oldC ? oldC[0] : 0), oy = old[1] - (oldC ? oldC[1] : 0);
|
|
727
|
+
const fc = c >= 0 ? fresh.clusters[c] : null;
|
|
728
|
+
const fy = fresh.y[i] - (fc ? fc.y : 0);
|
|
729
|
+
if (!keeps(fy, fc ? fc.r : freshTop, oy, oldC ? oldC[2] : oldTop))
|
|
730
|
+
return;
|
|
731
|
+
x[i] = round((c >= 0 ? cs[c].x : 0) + ox);
|
|
732
|
+
y[i] = round((c >= 0 ? cs[c].y : 0) + oy);
|
|
733
|
+
occ.add(x[i], y[i]);
|
|
734
|
+
placed[i] = 1;
|
|
735
|
+
});
|
|
736
|
+
// the rest (new, or moved in the order) at the spot the fresh layout suggests in its cluster
|
|
737
|
+
nodes.forEach((_, i) => {
|
|
738
|
+
if (placed[i])
|
|
739
|
+
return;
|
|
740
|
+
const c = fresh.cluster[i];
|
|
741
|
+
let tx = fresh.x[i];
|
|
742
|
+
let ty = fresh.y[i];
|
|
743
|
+
if (c >= 0) {
|
|
744
|
+
tx = cs[c].x + (fresh.x[i] - fresh.clusters[c].x);
|
|
745
|
+
ty = cs[c].y + (fresh.y[i] - fresh.clusters[c].y);
|
|
746
|
+
}
|
|
747
|
+
// stay out of the sub-clusters of its own cluster, as they stand now
|
|
748
|
+
const discs = cs.filter((k) => k.parent === c).map((k) => ({ x: k.x, y: k.y, r: k.r }));
|
|
749
|
+
[x[i], y[i]] = nearestFree(occ, tx, ty, discs).map(round);
|
|
750
|
+
occ.add(x[i], y[i]);
|
|
751
|
+
// a disc grows to hold what was placed outside it, and so do the discs around it
|
|
752
|
+
for (let k = c; k >= 0; k = cs[k].parent) {
|
|
753
|
+
const d = Math.hypot(x[i] - cs[k].x, y[i] - cs[k].y) + LEAF_R + 4;
|
|
754
|
+
if (d > cs[k].r)
|
|
755
|
+
cs[k].r = round(d);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
return relax(withLocal({ x, y, lx: [], ly: [], cluster: fresh.cluster, clusters: cs }));
|
|
759
|
+
}
|
|
760
|
+
/** The layout of `tree` (clusterTree) for the same declarations `mem` was made for, under other
|
|
761
|
+
* hidden levels (ViewSpec decision 24): nothing moves. Every declaration and every cluster `mem`
|
|
762
|
+
* places stays where it was on the board — a level hidden leaves what it held in place, inside the
|
|
763
|
+
* circle that holds it now — and a level shown again, which `mem` has no place for, is drawn round
|
|
764
|
+
* what it holds, where that is; the siblings it would cover make way (`relax`, in its parent
|
|
765
|
+
* alone: elsewhere a layout may hold overlaps of its own, which are left as they are). A
|
|
766
|
+
* declaration `mem` does not place (none, when the set is the same) waits at its cluster's
|
|
767
|
+
* centre. `at`: the places of the declarations by index, where a layout of the same nodes is at
|
|
768
|
+
* hand (the whole-project board's base layout), read directly rather than looked up by name. */
|
|
769
|
+
export function regroup(tree, nodes, mem, at) {
|
|
770
|
+
const cs = tree.clusters.map((c) => ({ ...c }));
|
|
771
|
+
const x = new Array(nodes.length).fill(0);
|
|
772
|
+
const y = new Array(nodes.length).fill(0);
|
|
773
|
+
const placed = new Uint8Array(nodes.length);
|
|
774
|
+
nodes.forEach((n, i) => {
|
|
775
|
+
const m = at ? [at.x[i], at.y[i]] : mem.nodes[n.name];
|
|
776
|
+
if (!m)
|
|
777
|
+
return;
|
|
778
|
+
x[i] = m[0];
|
|
779
|
+
y[i] = m[1];
|
|
780
|
+
placed[i] = 1;
|
|
781
|
+
});
|
|
782
|
+
const kidsC = cs.map(() => []);
|
|
783
|
+
const kidsN = cs.map(() => []);
|
|
784
|
+
cs.forEach((c, k) => c.parent >= 0 && kidsC[c.parent].push(k));
|
|
785
|
+
tree.cluster.forEach((c, i) => c >= 0 && kidsN[c].push(i));
|
|
786
|
+
/** the parents of the clusters drawn anew: where something may have to make way */
|
|
787
|
+
const anew = new Set();
|
|
788
|
+
// children before their parents: a cluster comes after its parent in the tree's order
|
|
789
|
+
for (let k = cs.length - 1; k >= 0; k--) {
|
|
790
|
+
const c = cs[k];
|
|
791
|
+
const m = mem.clusters[c.key];
|
|
792
|
+
if (m) {
|
|
793
|
+
[c.x, c.y, c.r] = m;
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
anew.add(c.parent);
|
|
797
|
+
const discs = [...kidsC[k].map((j) => ({ x: cs[j].x, y: cs[j].y, r: cs[j].r })), ...kidsN[k].filter((i) => placed[i]).map((i) => ({ x: x[i], y: y[i], r: LEAF_R }))];
|
|
798
|
+
const enc = discs.length ? packEnclose(discs) : null;
|
|
799
|
+
c.x = round(enc?.x ?? 0);
|
|
800
|
+
c.y = round(enc?.y ?? 0);
|
|
801
|
+
c.r = round((enc?.r ?? 0) + 10 + Math.min(14, Math.sqrt(c.count)));
|
|
802
|
+
}
|
|
803
|
+
nodes.forEach((_, i) => {
|
|
804
|
+
const c = tree.cluster[i];
|
|
805
|
+
if (placed[i] || c < 0)
|
|
806
|
+
return;
|
|
807
|
+
x[i] = cs[c].x;
|
|
808
|
+
y[i] = cs[c].y;
|
|
809
|
+
});
|
|
810
|
+
const L = withLocal({ x, y, lx: [], ly: [], cluster: tree.cluster, clusters: cs });
|
|
811
|
+
return anew.size ? relax(L, anew) : L;
|
|
812
|
+
}
|
|
813
|
+
/** Clusters that grew in place (new members) or arrived (new clusters) can overlap their
|
|
814
|
+
* siblings. Push siblings apart in each parent's local frame, smallest moving most, bottom-up so
|
|
815
|
+
* a parent sees its children's final sizes; then let each parent grow to hold its children and
|
|
816
|
+
* recompute world positions from the local ones. Nothing already apart moves. `only`: the parents
|
|
817
|
+
* (-1 the top) whose children are looked at, and each one's parent once it grew; every one where
|
|
818
|
+
* it is left out. */
|
|
819
|
+
export function relax(L, only) {
|
|
820
|
+
const cs = L.clusters;
|
|
821
|
+
const kidsC = cs.map(() => []);
|
|
822
|
+
const kidsN = cs.map(() => []);
|
|
823
|
+
const rootC = [];
|
|
824
|
+
const rootN = [];
|
|
825
|
+
cs.forEach((c, i) => (c.parent >= 0 ? kidsC[c.parent] : rootC).push(i));
|
|
826
|
+
L.cluster.forEach((c, i) => (c >= 0 ? kidsN[c] : rootN).push(i));
|
|
827
|
+
// how far each item moved in its parent's frame; world positions change by exactly the sum
|
|
828
|
+
// of these along the chain, so whatever did not move keeps its coordinates to the last digit
|
|
829
|
+
const dcx = new Float64Array(cs.length), dcy = new Float64Array(cs.length);
|
|
830
|
+
const dnx = new Float64Array(L.x.length), dny = new Float64Array(L.x.length);
|
|
831
|
+
const separate = (kc, kn) => {
|
|
832
|
+
if (!kc.length)
|
|
833
|
+
return 0;
|
|
834
|
+
const items = [...kc.map((c) => ({ c, i: -1, r: cs[c].r })), ...kn.map((i) => ({ c: -1, i, r: LEAF_R }))];
|
|
835
|
+
const pos = (it) => (it.c >= 0 ? [cs[it.c].lx, cs[it.c].ly] : [L.lx[it.i], L.ly[it.i]]);
|
|
836
|
+
const shift = (it, dx, dy) => {
|
|
837
|
+
if (it.c >= 0) {
|
|
838
|
+
cs[it.c].lx += dx;
|
|
839
|
+
cs[it.c].ly += dy;
|
|
840
|
+
dcx[it.c] += dx;
|
|
841
|
+
dcy[it.c] += dy;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
L.lx[it.i] += dx;
|
|
845
|
+
L.ly[it.i] += dy;
|
|
846
|
+
dnx[it.i] += dx;
|
|
847
|
+
dny[it.i] += dy;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
let moved = 0;
|
|
851
|
+
for (let iter = 0; iter < 40; iter++) {
|
|
852
|
+
let any = false;
|
|
853
|
+
// only pairs with a cluster in them: declarations among themselves were placed apart
|
|
854
|
+
for (let a = 0; a < kc.length; a++) {
|
|
855
|
+
for (let b = a + 1; b < items.length; b++) {
|
|
856
|
+
const A = items[a], B = items[b];
|
|
857
|
+
const [ax, ay] = pos(A), [bx, by] = pos(B);
|
|
858
|
+
let dx = bx - ax, dy = by - ay;
|
|
859
|
+
let d = Math.hypot(dx, dy);
|
|
860
|
+
// packed discs touch exactly: only a real overlap counts
|
|
861
|
+
const need = A.r + B.r;
|
|
862
|
+
if (d >= need - 0.5)
|
|
863
|
+
continue;
|
|
864
|
+
if (d < 1e-6) {
|
|
865
|
+
dx = 1;
|
|
866
|
+
dy = 0;
|
|
867
|
+
d = 1;
|
|
868
|
+
}
|
|
869
|
+
const push = need - d;
|
|
870
|
+
const wa = (B.r * B.r) / (A.r * A.r + B.r * B.r); // the smaller one moves more
|
|
871
|
+
shift(A, (-dx / d) * push * wa, (-dy / d) * push * wa);
|
|
872
|
+
shift(B, (dx / d) * push * (1 - wa), (dy / d) * push * (1 - wa));
|
|
873
|
+
any = true;
|
|
874
|
+
moved++;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (!any)
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
return moved;
|
|
881
|
+
};
|
|
882
|
+
const look = only ? new Set(only) : null;
|
|
883
|
+
for (let c = cs.length - 1; c >= 0; c--) {
|
|
884
|
+
if (look && !look.has(c))
|
|
885
|
+
continue;
|
|
886
|
+
if (separate(kidsC[c], kidsN[c]) === 0)
|
|
887
|
+
continue;
|
|
888
|
+
// grow to hold what moved
|
|
889
|
+
let reach = 0;
|
|
890
|
+
for (const k of kidsC[c])
|
|
891
|
+
reach = Math.max(reach, Math.hypot(cs[k].lx, cs[k].ly) + cs[k].r);
|
|
892
|
+
for (const i of kidsN[c])
|
|
893
|
+
reach = Math.max(reach, Math.hypot(L.lx[i], L.ly[i]) + LEAF_R);
|
|
894
|
+
const was = cs[c].r;
|
|
895
|
+
cs[c].r = round(Math.max(cs[c].r, reach + 6));
|
|
896
|
+
// a parent that grew may now cover its siblings: its own parent is looked at in its turn
|
|
897
|
+
if (look && cs[c].r !== was)
|
|
898
|
+
look.add(cs[c].parent);
|
|
899
|
+
}
|
|
900
|
+
if (!look || look.has(-1))
|
|
901
|
+
separate(rootC, rootN);
|
|
902
|
+
// world positions move by the accumulated deltas, parents first
|
|
903
|
+
const wdx = new Float64Array(cs.length), wdy = new Float64Array(cs.length);
|
|
904
|
+
cs.forEach((c, i) => {
|
|
905
|
+
wdx[i] = (c.parent >= 0 ? wdx[c.parent] : 0) + dcx[i];
|
|
906
|
+
wdy[i] = (c.parent >= 0 ? wdy[c.parent] : 0) + dcy[i];
|
|
907
|
+
if (wdx[i] || wdy[i]) {
|
|
908
|
+
c.x = round(c.x + wdx[i]);
|
|
909
|
+
c.y = round(c.y + wdy[i]);
|
|
910
|
+
c.lx = round(c.lx);
|
|
911
|
+
c.ly = round(c.ly);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
for (let i = 0; i < L.x.length; i++) {
|
|
915
|
+
const c = L.cluster[i];
|
|
916
|
+
const dx = (c >= 0 ? wdx[c] : 0) + dnx[i];
|
|
917
|
+
const dy = (c >= 0 ? wdy[c] : 0) + dny[i];
|
|
918
|
+
if (dx || dy) {
|
|
919
|
+
L.x[i] = round(L.x[i] + dx);
|
|
920
|
+
L.y[i] = round(L.y[i] + dy);
|
|
921
|
+
L.lx[i] = round(L.lx[i]);
|
|
922
|
+
L.ly[i] = round(L.ly[i]);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return L;
|
|
926
|
+
}
|