visualproof 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -26
- package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
- package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/metrics.json +1 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/ttf/jost-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/jost-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans-math.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-600-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-700-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-600-italic.ttf +0 -0
- package/dist/server/agent.js +125 -92
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/history.js +39 -0
- package/dist/server/historyapi.js +2 -2
- package/dist/server/hover.js +3 -3
- package/dist/server/layoutthread.js +1 -1
- package/dist/server/live.js +6 -0
- package/dist/server/main.js +151 -11
- package/dist/server/mcptools.js +6 -4
- package/dist/server/pastdoc.js +1 -1
- package/dist/server/pipeline.js +18 -9
- package/dist/server/readonlygate.js +2 -2
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +13 -3
- package/dist/server/usage.js +2 -2
- package/dist/server/views.js +25 -1
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +99 -1
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/doc/order.js +17 -0
- package/dist/shared/doc/outline.js +45 -0
- package/dist/shared/keys.js +1 -1
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/moves.js +2 -1
- package/dist/shared/pastgraph.js +9 -1
- package/dist/shared/query.js +21 -13
- package/dist/shared/review.js +62 -5
- package/dist/shared/sharesnap.js +2 -1
- package/dist/shared/titles.js +2 -0
- package/dist/shared/viewgraph.js +18 -0
- package/dist/web/bundle/app.js +7709 -3222
- package/dist/web/bundle/edges.js +4 -6
- package/dist/web/index.html +24 -6
- package/dist/web/src/agentmark.js +14 -0
- package/dist/web/src/artwork.js +1000 -0
- package/dist/web/src/bundles.js +151 -0
- package/dist/web/src/canvas.js +3694 -0
- package/dist/web/src/drag.js +61 -0
- package/dist/web/src/edgequeue.js +204 -0
- package/dist/web/src/gl/gl.js +123 -0
- package/dist/web/src/gl/renderer.js +451 -0
- package/dist/web/src/halo.js +95 -0
- package/dist/web/src/highlight.js +108 -0
- package/dist/web/src/overview.js +393 -0
- package/dist/web/src/reviewqueue.js +250 -0
- package/dist/web/src/scene.js +17 -0
- package/dist/web/src/selection.js +272 -0
- package/dist/web/src/status.js +80 -0
- package/dist/web/src/visitors.js +318 -0
- package/dist/web/src/visits.js +174 -0
- package/dist/web/style.css +255 -12
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/following.md +2 -2
- package/docs/agent-api/history.md +2 -1
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/docs/agent-api/views.md +3 -2
- package/package.json +3 -1
- package/test/toolchains.json +2 -2
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// What the reader selects (decided 2026-10-07): a list of picks. A pick is one thing clicked under
|
|
2
|
+
// one tool — a declaration x under Selection, Uses, TCB, Direct or Used by is decl(x), uses(x),
|
|
3
|
+
// tcb(x), refs(x) or usedBy(x), and under Keys x with the key declarations in its TCB
|
|
4
|
+
// (shared/query.ts, keysQuery); a
|
|
5
|
+
// cluster is the same tools over all of its declarations — and the selection is the union of its
|
|
6
|
+
// picks, itself a query of shared/query.ts: `pick or pick or …`. What is lit is that set, evaluated
|
|
7
|
+
// on the whole graph and cut to what the board holds (shared/board.ts, lightUp), so a pick means the
|
|
8
|
+
// same declarations on every board and a view of it holds what was lit. The mode is the tool: how
|
|
9
|
+
// the next click selects.
|
|
10
|
+
//
|
|
11
|
+
// Uses, TCB, Keys and Direct are one mode, Rests on, at the granularities of what a declaration
|
|
12
|
+
// rests on (decided 2026-10-08, ViewSpec decision 20): everything it uses, proofs included, ⊇ its
|
|
13
|
+
// TCB ⊇ the key declarations in its TCB; and what it uses directly (one step, the owner's decision
|
|
14
|
+
// of 2026-10-14). Each is a tool of its own, a step of that mode.
|
|
15
|
+
//
|
|
16
|
+
// Everything here is without the page, so that it can be tested: what a pick's query is and what
|
|
17
|
+
// it says in words, reading picks back from a query (an address, a reader followed, an agent), what
|
|
18
|
+
// a click or a shift-click does to the list, and how key 2 steps through Rests on. main.ts keeps the
|
|
19
|
+
// list and the thing the panel tells of; web/src/selbar.ts shows the list.
|
|
20
|
+
import { describe, keysQuery, parseQuery, printQuery, readKeys } from "../../shared/query.js";
|
|
21
|
+
/** Every tool, as the address and presence name them (web/src/state.ts). */
|
|
22
|
+
export const TOOLS = ["plain", "deps", "tcb", "keys", "refs", "apps"];
|
|
23
|
+
const CLOSURE = { tcb: "tcb", deps: "uses", refs: "refs", apps: "usedBy" };
|
|
24
|
+
const TOOL_OF = { tcb: "tcb", uses: "deps", refs: "refs", usedBy: "apps" };
|
|
25
|
+
const EXT = "⟨external⟩";
|
|
26
|
+
const keysOf = (k) => (k === null ? "targets" : [k]);
|
|
27
|
+
/** A declaration clicked under a tool; under Keys, with the key declarations of `keys`. */
|
|
28
|
+
export function declPick(tool, name, keys = null) {
|
|
29
|
+
const q = tool === "plain" ? { op: "decl", names: [name] } : tool === "keys" ? keysQuery(name, keysOf(keys)) : { op: CLOSURE[tool], of: [name] };
|
|
30
|
+
return { tool, root: { decl: name }, q };
|
|
31
|
+
}
|
|
32
|
+
/** A cluster's set under a tool. */
|
|
33
|
+
function onSet(tool, set, keys) {
|
|
34
|
+
return tool === "plain" ? set : tool === "keys" ? keysQuery(set, keysOf(keys)) : { op: CLOSURE[tool], of: set };
|
|
35
|
+
}
|
|
36
|
+
/** The declarations a cluster holds, as a query, exactly as the cluster tree puts them (the layout,
|
|
37
|
+
* shared/layout.ts): by namespace, the project's names under its key (an outside declaration may
|
|
38
|
+
* share the namespace); by file, the modules under it; outside the project, the same among the
|
|
39
|
+
* outside world — by namespace the names under its first component, that one name itself not
|
|
40
|
+
* included (it sits in the outside world's own cluster). */
|
|
41
|
+
export function clusterSet(key, mode) {
|
|
42
|
+
const external = { op: "external" };
|
|
43
|
+
if (key === EXT)
|
|
44
|
+
return external;
|
|
45
|
+
if (!key.startsWith(EXT + "."))
|
|
46
|
+
return mode === "module" ? { op: "file", prefix: key } : { op: "except", a: { op: "ns", prefix: key }, b: external };
|
|
47
|
+
const p = key.slice(EXT.length + 1);
|
|
48
|
+
if (mode === "module")
|
|
49
|
+
return { op: "and", a: { op: "file", prefix: p }, b: external };
|
|
50
|
+
return { op: "except", a: { op: "and", a: { op: "ns", prefix: p }, b: external }, b: { op: "decl", names: [p] } };
|
|
51
|
+
}
|
|
52
|
+
/** A cluster clicked under a tool; under Keys, with the key declarations of `keys`. */
|
|
53
|
+
export function clusterPick(tool, cl, mode, keys = null) {
|
|
54
|
+
return { tool, root: { cluster: cl.key, by: cl.key === EXT ? null : mode }, q: onSet(tool, clusterSet(cl.key, mode), keys) };
|
|
55
|
+
}
|
|
56
|
+
/** The cluster a query stands for, when it is a cluster's set (or one written by hand as `ns(P)`). */
|
|
57
|
+
function clusterOf(q) {
|
|
58
|
+
if (q.op === "external")
|
|
59
|
+
return { cluster: EXT, by: null };
|
|
60
|
+
if (q.op === "ns")
|
|
61
|
+
return { cluster: q.prefix, by: "namespace" };
|
|
62
|
+
const text = printQuery(q);
|
|
63
|
+
const head = q.op === "file" ? q : q.op === "except" || q.op === "and" ? (q.a.op === "and" ? q.a.a : q.a) : null;
|
|
64
|
+
if (!head || (head.op !== "ns" && head.op !== "file"))
|
|
65
|
+
return null;
|
|
66
|
+
const mode = head.op === "file" ? "module" : "namespace";
|
|
67
|
+
for (const key of [head.prefix, `${EXT}.${head.prefix}`])
|
|
68
|
+
if (printQuery(clusterSet(key, mode)) === text)
|
|
69
|
+
return { cluster: key, by: mode };
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/** A part of a selection as a pick: a click's, when it reads as one; else a pick with no root. */
|
|
73
|
+
export function pickOf(q) {
|
|
74
|
+
if (q.op === "decl" && q.names.length === 1)
|
|
75
|
+
return { tool: "plain", root: { decl: q.names[0] }, q };
|
|
76
|
+
const k = readKeys(q);
|
|
77
|
+
if (k)
|
|
78
|
+
return { tool: "keys", root: typeof k.from === "string" ? { decl: k.from } : clusterOf(k.from), q };
|
|
79
|
+
if (q.op === "tcb" || q.op === "refs" || q.op === "usedBy" || q.op === "uses") {
|
|
80
|
+
const root = Array.isArray(q.of) ? (q.of.length === 1 ? { decl: q.of[0] } : null) : q.of === "selection" ? null : clusterOf(q.of);
|
|
81
|
+
return { tool: TOOL_OF[q.op], root, q };
|
|
82
|
+
}
|
|
83
|
+
return { tool: "plain", root: clusterOf(q), q };
|
|
84
|
+
}
|
|
85
|
+
/** The selection's query: its picks joined by `or`; null when nothing is selected. */
|
|
86
|
+
export function selectionQuery(picks) {
|
|
87
|
+
return picks.length ? picks.slice(1).reduce((a, p) => ({ op: "or", a, b: p.q }), picks[0].q) : null;
|
|
88
|
+
}
|
|
89
|
+
export function selectionText(picks) {
|
|
90
|
+
const q = selectionQuery(picks);
|
|
91
|
+
return q && printQuery(q);
|
|
92
|
+
}
|
|
93
|
+
/** The picks of a selection's query: the parts its `or`s join (left to right, as it is printed). */
|
|
94
|
+
export function picksOf(q) {
|
|
95
|
+
return q.op === "or" ? [...picksOf(q.a), pickOf(q.b)] : [pickOf(q)];
|
|
96
|
+
}
|
|
97
|
+
/** The picks a selection's text says; none when it says nothing readable. */
|
|
98
|
+
export function readSelection(text) {
|
|
99
|
+
if (!text)
|
|
100
|
+
return [];
|
|
101
|
+
try {
|
|
102
|
+
return picksOf(parseQuery(text));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Whether two picks are the same: the same query. */
|
|
109
|
+
export function samePick(a, b) {
|
|
110
|
+
return printQuery(a.q) === printQuery(b.q);
|
|
111
|
+
}
|
|
112
|
+
const isCluster = (p) => !!p.root && "cluster" in p.root;
|
|
113
|
+
/** The last part of a name or key, as a chip says it ("FLT", not "Mathlib.NumberTheory.FLT"); a part that
|
|
114
|
+
* says little alone (a number, two letters) keeps the one before it. */
|
|
115
|
+
function short(name) {
|
|
116
|
+
const parts = name.split(".");
|
|
117
|
+
const tail = parts[parts.length - 1];
|
|
118
|
+
return /^\d+$/.test(tail) || tail.length <= 2 ? parts.slice(-2).join(".") : tail;
|
|
119
|
+
}
|
|
120
|
+
/** A pick in a few words, as its chip says it: "Minimal", "TCB of not_fermat_42", "ns Fermat42". */
|
|
121
|
+
export function pickWords(p) {
|
|
122
|
+
const r = p.root;
|
|
123
|
+
let what;
|
|
124
|
+
if (!r)
|
|
125
|
+
return describe(p.q);
|
|
126
|
+
if ("decl" in r)
|
|
127
|
+
what = short(r.decl);
|
|
128
|
+
else if (r.cluster === EXT)
|
|
129
|
+
what = "the outside world";
|
|
130
|
+
else {
|
|
131
|
+
const ext = r.cluster.startsWith(EXT + ".");
|
|
132
|
+
what = `${r.by === "module" ? "file" : "ns"} ${short(ext ? r.cluster.slice(EXT.length + 1) : r.cluster)}${ext ? " (outside)" : ""}`;
|
|
133
|
+
}
|
|
134
|
+
if (p.tool === "keys")
|
|
135
|
+
return `keys ${what} rests on`;
|
|
136
|
+
return p.tool === "tcb" ? `TCB of ${what}` : p.tool === "deps" ? `what ${what} uses` : p.tool === "refs" ? `what ${what} uses directly` : p.tool === "apps" ? `what uses ${what}` : what;
|
|
137
|
+
}
|
|
138
|
+
/** What the panel tells of when a pick was the last clicked: its root. */
|
|
139
|
+
export function shownOf(p) {
|
|
140
|
+
const r = p?.root;
|
|
141
|
+
return !r ? null : "decl" in r ? { decl: r.decl } : { cluster: r.cluster };
|
|
142
|
+
}
|
|
143
|
+
/** Whether two things the panel may tell of are one. */
|
|
144
|
+
export const sameShown = (a, b) => a === b || (!!a && !!b && ("decl" in a ? "decl" in b && a.decl === b.decl : "cluster" in b && a.cluster === b.cluster));
|
|
145
|
+
/** Whether a set holds every member of another, which holds at least one. */
|
|
146
|
+
function within(a, b) {
|
|
147
|
+
let any = false;
|
|
148
|
+
for (let i = 0; i < a.length; i++) {
|
|
149
|
+
if (!a[i])
|
|
150
|
+
continue;
|
|
151
|
+
if (!b[i])
|
|
152
|
+
return false;
|
|
153
|
+
any = true;
|
|
154
|
+
}
|
|
155
|
+
return any;
|
|
156
|
+
}
|
|
157
|
+
/** What a click does to the selection (`picks`, and `shown`, what the panel tells of). Without
|
|
158
|
+
* shift, or under any tool but Selection, the selection becomes the one pick clicked. With shift,
|
|
159
|
+
* under Selection, exactly one pick changes: it goes when it is a pick, and comes otherwise — a
|
|
160
|
+
* cluster then taking the place of the picks it covers, since the union is what counts; something
|
|
161
|
+
* a cluster picked covers already changes nothing, and `covered` says which cluster. `setOf` is
|
|
162
|
+
* what a query lights up on the board (shared/board.ts, lightUp). */
|
|
163
|
+
export function click(picks, shown, p, shift, setOf) {
|
|
164
|
+
if (!shift || p.tool !== "plain")
|
|
165
|
+
return { picks: [p], shown: shownOf(p), covered: null };
|
|
166
|
+
const k = picks.findIndex((x) => samePick(x, p));
|
|
167
|
+
if (k >= 0)
|
|
168
|
+
return remove(picks, shown, k);
|
|
169
|
+
const mine = setOf(p.q);
|
|
170
|
+
const by = picks.find((x) => isCluster(x) && within(mine, setOf(x.q)));
|
|
171
|
+
if (by)
|
|
172
|
+
return { picks, shown, covered: by };
|
|
173
|
+
const rest = isCluster(p) ? picks.filter((x) => !within(setOf(x.q), mine)) : picks;
|
|
174
|
+
return { picks: [...rest, p], shown: shownOf(p), covered: null };
|
|
175
|
+
}
|
|
176
|
+
/** Take the `k`th pick out (shift-click, or its chip's ×). The panel stays where it is, unless it
|
|
177
|
+
* told of that pick: then it tells of the last pick left, if that has a root. */
|
|
178
|
+
export function remove(picks, shown, k) {
|
|
179
|
+
const rest = picks.filter((_, j) => j !== k);
|
|
180
|
+
const told = sameShown(shown, shownOf(picks[k]));
|
|
181
|
+
return { picks: rest, shown: told ? shownOf(rest[rest.length - 1]) : shown, covered: null };
|
|
182
|
+
}
|
|
183
|
+
/** A pick's root under another tool: a declaration's, or a cluster's set; null for a pick with no
|
|
184
|
+
* root (a part of a query written by hand), which no tool can be given again. Under Keys, `keys`
|
|
185
|
+
* says whose key declarations, for that root. */
|
|
186
|
+
export function withTool(p, tool, keys = () => null) {
|
|
187
|
+
const r = p.root;
|
|
188
|
+
if (!r)
|
|
189
|
+
return null;
|
|
190
|
+
if ("decl" in r)
|
|
191
|
+
return declPick(tool, r.decl, keys(r));
|
|
192
|
+
return { tool, root: r, q: onSet(tool, clusterSet(r.cluster, r.by ?? "namespace"), keys(r)) };
|
|
193
|
+
}
|
|
194
|
+
/** What the selection becomes when the tool changes (the owner's decision, 2026-10-07, after
|
|
195
|
+
* using it): the pick last clicked — the one the panel tells of, else the last — alone, under the
|
|
196
|
+
* new tool. From Selection to another tool only the last pick stays; back to Selection, only the
|
|
197
|
+
* one thing clicked; from one step of Rests on to another, the same thing at the new step. Under
|
|
198
|
+
* Keys, `keys` says whose key declarations (withTool). Unchanged (the same array) when nothing is
|
|
199
|
+
* selected, or when that pick has no root. */
|
|
200
|
+
export function retool(picks, shown, tool, keys = () => null) {
|
|
201
|
+
if (!picks.length)
|
|
202
|
+
return { picks, shown };
|
|
203
|
+
const last = picks.find((p) => sameShown(shownOf(p), shown)) ?? picks[picks.length - 1];
|
|
204
|
+
const next = withTool(last, tool, keys);
|
|
205
|
+
return next ? { picks: [next], shown: shownOf(next) } : { picks, shown };
|
|
206
|
+
}
|
|
207
|
+
/** The edges a selection draws (canvas.ts, MODE_SITES): a TCB's statement and body edges when every
|
|
208
|
+
* pick is a TCB, else every edge between what is lit. (While the tool is Keys the canvas draws the
|
|
209
|
+
* key graph's edges instead: shared/keys.ts, litKeyEdges.) */
|
|
210
|
+
export function edgeMode(picks) {
|
|
211
|
+
return picks.length && picks.every((p) => p.tool === "tcb") ? "tcb" : "plain";
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------- Rests on and its steps
|
|
214
|
+
/** The steps of Rests on, as its switch shows them: from everything used to the few key
|
|
215
|
+
* declarations, then what it uses directly (decided 2026-10-08 and 2026-10-14, ViewSpec decision 20). */
|
|
216
|
+
export const STEPS = ["deps", "tcb", "keys", "refs"];
|
|
217
|
+
/** Whether a tool is a step of Rests on. */
|
|
218
|
+
export const restsOn = (t) => STEPS.includes(t);
|
|
219
|
+
/** The step Rests on is entered at, with key 2 or its button: the step last used, TCB for a reader
|
|
220
|
+
* who never chose. */
|
|
221
|
+
export function enterStep(last) {
|
|
222
|
+
return last && restsOn(last) ? last : "tcb";
|
|
223
|
+
}
|
|
224
|
+
/** What key 2 makes the tool: Rests on at the step last used (enterStep) from any other mode; within
|
|
225
|
+
* Rests on, the next step — TCB, Keys, Uses, Direct, TCB again. */
|
|
226
|
+
export function keyTwo(now, last) {
|
|
227
|
+
if (!restsOn(now))
|
|
228
|
+
return enterStep(last);
|
|
229
|
+
const cycle = ["tcb", "keys", "deps", "refs"];
|
|
230
|
+
return cycle[(cycle.indexOf(now) + 1) % cycle.length];
|
|
231
|
+
}
|
|
232
|
+
// ---------------------------------------------------------------- clusters on the board
|
|
233
|
+
const indexes = new WeakMap();
|
|
234
|
+
/** A cluster of the layout by its key; -1 when the layout has none (another clustering, a board
|
|
235
|
+
* that does not hold it). */
|
|
236
|
+
export function clusterIndex(layout, key) {
|
|
237
|
+
let m = indexes.get(layout);
|
|
238
|
+
if (!m)
|
|
239
|
+
indexes.set(layout, (m = new Map(layout.clusters.map((c, k) => [c.key, k]))));
|
|
240
|
+
return m.get(key) ?? -1;
|
|
241
|
+
}
|
|
242
|
+
/** Where a pick's cluster is on a board of clustering `mode`: its index, or -1 when it is not a
|
|
243
|
+
* cluster there. */
|
|
244
|
+
export function clusterOn(r, layout, mode) {
|
|
245
|
+
return "cluster" in r && (r.by === null || r.by === mode) ? clusterIndex(layout, r.cluster) : -1;
|
|
246
|
+
}
|
|
247
|
+
/** The declarations a cluster holds, at any depth, in the layout's numbering. */
|
|
248
|
+
export function clusterMembers(layout, c) {
|
|
249
|
+
const out = [];
|
|
250
|
+
const depth = layout.clusters[c].depth;
|
|
251
|
+
for (let j = 0; j < layout.cluster.length; j++) {
|
|
252
|
+
let x = layout.cluster[j];
|
|
253
|
+
while (x >= 0 && layout.clusters[x].depth > depth)
|
|
254
|
+
x = layout.clusters[x].parent;
|
|
255
|
+
if (x === c)
|
|
256
|
+
out.push(j);
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
/** What a cluster is, in a few words: a namespace, a folder or a file, of the project or outside it.
|
|
261
|
+
* `isModule` says whether a key is one of the project's modules. */
|
|
262
|
+
export function clusterWhat(cl, mode, isModule, hasKids) {
|
|
263
|
+
if (cl.key === EXT)
|
|
264
|
+
return "the outside world";
|
|
265
|
+
if (mode === "namespace")
|
|
266
|
+
return cl.ext ? "namespace, outside the project" : "namespace";
|
|
267
|
+
if (cl.ext)
|
|
268
|
+
return "folder, outside the project";
|
|
269
|
+
if (!isModule(cl.key))
|
|
270
|
+
return "folder";
|
|
271
|
+
return hasKids ? "file and folder" : "file";
|
|
272
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The Status colour scheme (CanvasSpec, "Colour schemes", decided 2026-10-08): one colour per
|
|
2
|
+
// declaration for where it stands, the most urgent state winning — its file's build failed or was
|
|
3
|
+
// never built, it contains sorry, then what reviewers said (commented, stale, approved; which of a
|
|
4
|
+
// comment and an approval wins is shared/review.ts's `VERDICT_ORDER`), and last whether it needs
|
|
5
|
+
// review at all (not yet reviewed, trusted). A file being compiled shows so while it lasts. What
|
|
6
|
+
// agents advise is not among the states: advice is never counted, and the panel shows it apart
|
|
7
|
+
// (AgentSpec decision 2).
|
|
8
|
+
import { statusOf, trusted } from "../../shared/review.js";
|
|
9
|
+
/** The states, most urgent first; a declaration's code is its index here. The legend lists them in
|
|
10
|
+
* this order, and the CSS colours are `--status-<state>`. */
|
|
11
|
+
export const STATUSES = ["failed", "building", "sorry", "commented", "stale", "approved", "unreviewed", "trusted"];
|
|
12
|
+
export const ST_FAILED = 0, ST_BUILDING = 1, ST_SORRY = 2, ST_COMMENTED = 3, ST_STALE = 4, ST_APPROVED = 5, ST_UNREVIEWED = 6, ST_TRUSTED = 7;
|
|
13
|
+
/** How the legend and the hover tip name each state. */
|
|
14
|
+
export const STATUS_LABEL = {
|
|
15
|
+
failed: "failed or not built",
|
|
16
|
+
building: "building",
|
|
17
|
+
sorry: "contains sorry",
|
|
18
|
+
commented: "commented",
|
|
19
|
+
stale: "stale",
|
|
20
|
+
approved: "approved",
|
|
21
|
+
unreviewed: "not yet reviewed",
|
|
22
|
+
trusted: "trusted",
|
|
23
|
+
};
|
|
24
|
+
/** What each state means, for the legend's tooltips. */
|
|
25
|
+
export const STATUS_HINT = {
|
|
26
|
+
failed: "Its file failed to compile in the last build, or was never built: shown as it was last built",
|
|
27
|
+
building: "Its file is being compiled right now",
|
|
28
|
+
sorry: "It rests on sorry: its own, or through anything it uses",
|
|
29
|
+
commented: "A reviewer's latest word on its present version is a comment",
|
|
30
|
+
stale: "It has opinions, none on its present version",
|
|
31
|
+
approved: "A reviewer approved its present version, and no reviewer's latest word on it is a comment",
|
|
32
|
+
unreviewed: "Nobody has given an opinion on it yet, and nothing trusts it",
|
|
33
|
+
trusted: "Trusted without review (Mathlib, Lean core, a trusted prefix, the standard axioms), with no opinion on it",
|
|
34
|
+
};
|
|
35
|
+
/** Where one declaration stands. `file` is its file's build status, when the page is live.
|
|
36
|
+
*
|
|
37
|
+
* "Contains sorry" is read from the axioms it rests on (sorryAx), which Lean collects through
|
|
38
|
+
* everything it uses: a declaration that ends up depending on a sorry anywhere below it is shown
|
|
39
|
+
* so, not only the one that writes it (the owner's decision, 2026-10-08). Out of date, queued and
|
|
40
|
+
* blocked files are shown as last built, and so by their review. */
|
|
41
|
+
export function statusState(d, g, i, file, sorryAx) {
|
|
42
|
+
if (file?.state === "building")
|
|
43
|
+
return ST_BUILDING;
|
|
44
|
+
if (file?.state === "failed" || file?.state === "unbuilt")
|
|
45
|
+
return ST_FAILED;
|
|
46
|
+
if (sorryAx)
|
|
47
|
+
return ST_SORRY;
|
|
48
|
+
const v = g.nodes[i];
|
|
49
|
+
const s = statusOf(d, v.name, v.hash);
|
|
50
|
+
if (s === "commented")
|
|
51
|
+
return ST_COMMENTED;
|
|
52
|
+
if (s === "stale")
|
|
53
|
+
return ST_STALE;
|
|
54
|
+
if (s === "approved")
|
|
55
|
+
return ST_APPROVED;
|
|
56
|
+
return trusted(d, v.name, v.module, g.isExt(i)) ? ST_TRUSTED : ST_UNREVIEWED;
|
|
57
|
+
}
|
|
58
|
+
/** Every declaration of `g`'s state, as codes (indices into STATUSES). `files` holds the build
|
|
59
|
+
* status of the project's files, by module; empty in a snapshot or before the first report. */
|
|
60
|
+
export function statusCodes(d, g, files) {
|
|
61
|
+
const out = new Uint8Array(g.n);
|
|
62
|
+
// whether each set of axioms holds sorryAx, worked out once per set rather than per declaration
|
|
63
|
+
const sorrySets = new Map();
|
|
64
|
+
for (let i = 0; i < g.n; i++) {
|
|
65
|
+
const ax = g.nodes[i].ax;
|
|
66
|
+
let s = sorrySets.get(ax);
|
|
67
|
+
if (s === undefined)
|
|
68
|
+
sorrySets.set(ax, (s = g.axiomsOf(i).includes("sorryAx")));
|
|
69
|
+
out[i] = statusState(d, g, i, g.isExt(i) ? undefined : files.get(g.nodes[i].module), s);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** How many declarations are in each state, of those `counted` keeps: the legend's progress. */
|
|
74
|
+
export function statusCounts(codes, counted = () => true) {
|
|
75
|
+
const n = new Array(STATUSES.length).fill(0);
|
|
76
|
+
for (let i = 0; i < codes.length; i++)
|
|
77
|
+
if (counted(i))
|
|
78
|
+
n[codes[i]]++;
|
|
79
|
+
return n;
|
|
80
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Visitors (ViewSpec decision 27): navigation never leaves the board. Going to a declaration the
|
|
2
|
+
// board on show does not hold — from the panel, a target's review list, a link in a comment, a name
|
|
3
|
+
// in the source, the search, a step of Rests on or Used by — shows it on this board as a temporary point,
|
|
4
|
+
// a visitor, drawn dashed with its edges to what the board holds, so that one sees why one came.
|
|
5
|
+
//
|
|
6
|
+
// A visitor shows in its own place in the cluster hierarchy (the owner's decision, 2026-10-11): in
|
|
7
|
+
// its own cluster where the board draws it, else inside the deepest cluster of its path the board
|
|
8
|
+
// draws (or at the top level), in temporary clusters for the levels between — dashed circles with
|
|
9
|
+
// their names in italics, shared by the visitors they hold. Nothing of it is the view's: the board's
|
|
10
|
+
// clusters and declarations keep their places, and what made way for a visitor returns when it goes.
|
|
11
|
+
//
|
|
12
|
+
// A visitor is the reader's own: never part of the view's scope, arrangement or layout memory, and
|
|
13
|
+
// not seen by the others on the view — but those who follow the reader see the leader's visitors, as
|
|
14
|
+
// they see the leader's selection (presence carries them: PresenceView.visitors). The last few stay
|
|
15
|
+
// as a trail one steps back along; they go when the selection is cleared or the board changes. One
|
|
16
|
+
// that comes to be in the board's scope (Add to view, a key mark on a target's Key view) is a member
|
|
17
|
+
// at once, and leaves the trail.
|
|
18
|
+
//
|
|
19
|
+
// This file holds the trail, the temporary clusters a set of visitors needs and how they are packed,
|
|
20
|
+
// where the outermost of them is put in a cluster of the board, and the references a visitor's edges
|
|
21
|
+
// draw, all as pure functions; web/src/visits.ts lays them on the board's scene, web/src/main.ts
|
|
22
|
+
// keeps the trail of the board on show and web/src/canvas.ts draws it.
|
|
23
|
+
import { packEnclose, packSiblings } from "d3-hierarchy";
|
|
24
|
+
import { SITE_BODY, SITE_STMT } from "../../shared/graph.js";
|
|
25
|
+
import { EXT_KEY, pathOf as clusterPath } from "../../shared/layout.js";
|
|
26
|
+
import { LEAF_R } from "./scene.js";
|
|
27
|
+
/** How many visitors the trail keeps: the last few, which stepping back walks along. */
|
|
28
|
+
export const TRAIL = 6;
|
|
29
|
+
/** The trail with `v` its newest: one that is a visitor already keeps its place and becomes the
|
|
30
|
+
* newest; the oldest go beyond TRAIL. */
|
|
31
|
+
export function visit(trail, v) {
|
|
32
|
+
const was = trail.find((x) => x.name === v.name);
|
|
33
|
+
const rest = trail.filter((x) => x.name !== v.name);
|
|
34
|
+
return [...rest, was ?? v].slice(-TRAIL);
|
|
35
|
+
}
|
|
36
|
+
/** The trail without what is no visitor now: what the board holds (a member since: Add to view, a
|
|
37
|
+
* rescope, a key mark on a Key view) and what its graph does not have. The same array where
|
|
38
|
+
* nothing went, so that a caller can tell. */
|
|
39
|
+
export function stillVisiting(trail, visiting) {
|
|
40
|
+
const out = trail.filter((v) => visiting(v.name));
|
|
41
|
+
return out.length === trail.length ? trail : out;
|
|
42
|
+
}
|
|
43
|
+
/** Where each visitor belongs on a board (ViewSpec decision 27): its cluster path by `mode`, as the
|
|
44
|
+
* board's tree would have it — the hidden levels taken out, so that a hidden level's members are in
|
|
45
|
+
* its parent (decision 24), a structure in its own namespace where the board or a visitor has names
|
|
46
|
+
* under it (`isPrefix`) — met with the clusters the board draws (`drawn`: key → index of the
|
|
47
|
+
* layout's clusters). The deepest level the board draws holds the visitor; the levels beneath it are
|
|
48
|
+
* temporary clusters, one tree under each cluster of the board, so that visitors share the ones they
|
|
49
|
+
* have in common, and a level that holds nothing but one level is merged with it, as the board's own
|
|
50
|
+
* are (`NumberTheory.FLT`), the outside world's apart. A function of the set and the board alone, in
|
|
51
|
+
* any order of the visitors: a follower computes the same. */
|
|
52
|
+
export function planVisitors(mode, drawn, hidden, nodes, isPrefix) {
|
|
53
|
+
const level = (key, label) => ({ key, label, kids: new Map(), visitors: [] });
|
|
54
|
+
// per cluster of the board (-1: the top level), the levels under it that it does not draw
|
|
55
|
+
const under = new Map();
|
|
56
|
+
nodes.forEach((n, k) => {
|
|
57
|
+
const parts = clusterPath(mode, { name: n.name, module: n.module, ...(n.ext ? { ext: true } : {}) }, isPrefix(n.name) ? new Set([n.name]) : new Set());
|
|
58
|
+
const levels = [];
|
|
59
|
+
let key = "";
|
|
60
|
+
for (const part of parts) {
|
|
61
|
+
key = key ? `${key}.${part}` : part;
|
|
62
|
+
if (!hidden.has(key))
|
|
63
|
+
levels.push({ key, part });
|
|
64
|
+
}
|
|
65
|
+
let d = levels.length - 1;
|
|
66
|
+
while (d >= 0 && !drawn.has(levels[d].key))
|
|
67
|
+
d--;
|
|
68
|
+
const inn = d >= 0 ? drawn.get(levels[d].key) : -1;
|
|
69
|
+
let at = under.get(inn) ?? level("", "");
|
|
70
|
+
under.set(inn, at);
|
|
71
|
+
for (const lv of levels.slice(d + 1)) {
|
|
72
|
+
const next = at.kids.get(lv.key) ?? level(lv.key, lv.part === EXT_KEY ? "External" : lv.part);
|
|
73
|
+
at.kids.set(lv.key, next);
|
|
74
|
+
at = next;
|
|
75
|
+
}
|
|
76
|
+
at.visitors.push(k);
|
|
77
|
+
});
|
|
78
|
+
// a level holding nothing but one level says nothing: one circle, the labels joined
|
|
79
|
+
const merge = (lv) => {
|
|
80
|
+
for (const [key, first] of [...lv.kids]) {
|
|
81
|
+
let kid = first;
|
|
82
|
+
while (!kid.visitors.length && kid.kids.size === 1 && kid.key !== EXT_KEY) {
|
|
83
|
+
const only = [...kid.kids.values()][0];
|
|
84
|
+
only.label = `${kid.label}.${only.label}`;
|
|
85
|
+
kid = only;
|
|
86
|
+
}
|
|
87
|
+
lv.kids.set(key, kid);
|
|
88
|
+
merge(kid);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const temps = [];
|
|
92
|
+
const at = nodes.map(() => ({ in: -1, up: -1 }));
|
|
93
|
+
const walk = (lv, inn, up) => {
|
|
94
|
+
let count = lv.visitors.length;
|
|
95
|
+
for (const k of lv.visitors)
|
|
96
|
+
at[k] = { in: inn, up };
|
|
97
|
+
for (const kid of [...lv.kids.values()].sort((a, b) => cmp(a.key, b.key))) {
|
|
98
|
+
const t = temps.length;
|
|
99
|
+
temps.push({ key: kid.key, label: kid.label, in: inn, up, count: 0 });
|
|
100
|
+
temps[t].count = walk(kid, inn, t);
|
|
101
|
+
count += temps[t].count;
|
|
102
|
+
}
|
|
103
|
+
return count;
|
|
104
|
+
};
|
|
105
|
+
for (const [inn, root] of [...under].sort((a, b) => a[0] - b[0])) {
|
|
106
|
+
merge(root);
|
|
107
|
+
walk(root, inn, -1);
|
|
108
|
+
}
|
|
109
|
+
return { temps, at };
|
|
110
|
+
}
|
|
111
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
112
|
+
/** The gap between packed discs, as the layout leaves it. */
|
|
113
|
+
const GAP = 2.5;
|
|
114
|
+
/** Room between a temporary cluster's contents and its circle, for its name: a cluster's own
|
|
115
|
+
* (web/src/scene.ts, padOf), by how many visitors it holds. */
|
|
116
|
+
export function tempPad(count) {
|
|
117
|
+
return 10 + Math.min(14, Math.sqrt(count));
|
|
118
|
+
}
|
|
119
|
+
export function packPlan(plan, names) {
|
|
120
|
+
const T = plan.temps.length;
|
|
121
|
+
const out = { r: new Array(T).fill(0), x: new Array(T).fill(0), y: new Array(T).fill(0), vx: names.map(() => 0), vy: names.map(() => 0) };
|
|
122
|
+
const kids = plan.temps.map(() => []);
|
|
123
|
+
plan.temps.forEach((tc, t) => tc.up >= 0 && kids[tc.up].push({ t, k: -1, key: tc.key }));
|
|
124
|
+
plan.at.forEach((a, k) => a.up >= 0 && kids[a.up].push({ t: -1, k, key: names[k] }));
|
|
125
|
+
// inside out: a temporary cluster comes after the one it is in (planVisitors)
|
|
126
|
+
for (let t = T - 1; t >= 0; t--) {
|
|
127
|
+
const list = kids[t].sort((a, b) => cmp(a.key, b.key));
|
|
128
|
+
const discs = list.map((c) => ({ x: 0, y: 0, r: (c.t >= 0 ? out.r[c.t] : LEAF_R) + GAP / 2 }));
|
|
129
|
+
if (discs.length > 1)
|
|
130
|
+
packSiblings(discs);
|
|
131
|
+
const enc = discs.length ? packEnclose(discs) : { x: 0, y: 0, r: LEAF_R };
|
|
132
|
+
list.forEach((c, j) => {
|
|
133
|
+
const dx = discs[j].x - enc.x, dy = discs[j].y - enc.y;
|
|
134
|
+
if (c.t >= 0) {
|
|
135
|
+
out.x[c.t] = dx;
|
|
136
|
+
out.y[c.t] = dy;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
out.vx[c.k] = dx;
|
|
140
|
+
out.vy[c.k] = dy;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
out.r[t] = enc.r + tempPad(plan.temps[t].count);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** Where what a visitor brings, a disc of radius `r`, goes in a cluster of the board: the spot
|
|
148
|
+
* nearest `prefer` (in the cluster's coordinates) where it covers none of the cluster's children
|
|
149
|
+
* (`bodies`), a gap kept, within `room` of the cluster's origin — the room its contents have, its
|
|
150
|
+
* name's above it (Infinity at the top level) — and where `ok` says no name is written (the
|
|
151
|
+
* avoidance of the clusters' labels). Rings round `prefer` are tried outwards. Null where no such
|
|
152
|
+
* spot is: then it goes against the rim (rimSpot), and what it covers makes way. */
|
|
153
|
+
export function freeSpot(bodies, room, r, prefer, ok = () => true) {
|
|
154
|
+
const lim = room - r;
|
|
155
|
+
if (!(lim >= 0))
|
|
156
|
+
return null;
|
|
157
|
+
// the declarations through a grid, the larger bodies each
|
|
158
|
+
const CELL = 4 * LEAF_R;
|
|
159
|
+
const grid = new Map();
|
|
160
|
+
const big = [];
|
|
161
|
+
const cellOf = (i, j) => (i + 100000) * 200003 + (j + 100000);
|
|
162
|
+
bodies.forEach((b, j) => {
|
|
163
|
+
if (b.r > 2 * LEAF_R)
|
|
164
|
+
return void big.push(j);
|
|
165
|
+
const key = cellOf(Math.floor(b.x / CELL), Math.floor(b.y / CELL));
|
|
166
|
+
const cell = grid.get(key);
|
|
167
|
+
if (cell)
|
|
168
|
+
cell.push(j);
|
|
169
|
+
else
|
|
170
|
+
grid.set(key, [j]);
|
|
171
|
+
});
|
|
172
|
+
const near = Math.ceil((r + 2 * LEAF_R + GAP) / CELL);
|
|
173
|
+
const free = (x, y) => {
|
|
174
|
+
for (const j of big) {
|
|
175
|
+
const b = bodies[j];
|
|
176
|
+
if ((b.x - x) ** 2 + (b.y - y) ** 2 < (b.r + r + GAP) ** 2)
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const gx = Math.floor(x / CELL), gy = Math.floor(y / CELL);
|
|
180
|
+
for (let i = gx - near; i <= gx + near; i++)
|
|
181
|
+
for (let j = gy - near; j <= gy + near; j++)
|
|
182
|
+
for (const k of grid.get(cellOf(i, j)) ?? []) {
|
|
183
|
+
const b = bodies[k];
|
|
184
|
+
if ((b.x - x) ** 2 + (b.y - y) ** 2 < (b.r + r + GAP) ** 2)
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
189
|
+
// the preferred spot, brought into the room
|
|
190
|
+
let [px, py] = prefer;
|
|
191
|
+
const d0 = Math.hypot(px, py);
|
|
192
|
+
if (d0 > lim)
|
|
193
|
+
[px, py] = [(px * lim) / d0, (py * lim) / d0];
|
|
194
|
+
// how far to look: across the room, or at the top level a little past everything there is
|
|
195
|
+
let reach = lim + Math.hypot(px, py);
|
|
196
|
+
if (!Number.isFinite(reach)) {
|
|
197
|
+
reach = 2 * r + GAP;
|
|
198
|
+
for (const b of bodies)
|
|
199
|
+
reach = Math.max(reach, Math.hypot(b.x - px, b.y - py) + b.r + 2 * r + GAP);
|
|
200
|
+
}
|
|
201
|
+
const step = Math.max(r / 2, reach / 120, 1);
|
|
202
|
+
for (let rho = 0; rho <= reach + step; rho += step) {
|
|
203
|
+
const m = rho === 0 ? 1 : Math.ceil((2 * Math.PI * rho) / step);
|
|
204
|
+
for (let q = 0; q < m; q++) {
|
|
205
|
+
// from the lower right round: the clusters' names are at their tops
|
|
206
|
+
const a = Math.PI / 4 + (q * 2 * Math.PI) / m;
|
|
207
|
+
const x = px + rho * Math.cos(a), y = py + rho * Math.sin(a);
|
|
208
|
+
if (Math.hypot(x, y) > lim + 1e-9)
|
|
209
|
+
continue;
|
|
210
|
+
if (free(x, y) && ok(x, y))
|
|
211
|
+
return [x, y];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/** Where what a visitor brings goes when no spot of a cluster is free (freeSpot): against the rim of
|
|
217
|
+
* its room, at the angle where it covers least of the cluster's children (by how deep it overlaps
|
|
218
|
+
* them), the nearest `prefer`'s direction among equals (the lower right where `prefer` is the
|
|
219
|
+
* middle). What it covers makes way (web/src/scene.ts, the held room); the circle grows only where
|
|
220
|
+
* that cannot fit otherwise. */
|
|
221
|
+
export function rimSpot(bodies, room, r, prefer) {
|
|
222
|
+
const lim = Math.max(0, room - r);
|
|
223
|
+
const want = Math.hypot(prefer[0], prefer[1]) > 1e-6 ? Math.atan2(prefer[1], prefer[0]) : Math.PI / 4;
|
|
224
|
+
if (!Number.isFinite(lim))
|
|
225
|
+
return [prefer[0], prefer[1]];
|
|
226
|
+
let best = [lim * Math.cos(want), lim * Math.sin(want)];
|
|
227
|
+
let bestCost = Infinity, bestTurn = Infinity;
|
|
228
|
+
for (let q = 0; q < 72; q++) {
|
|
229
|
+
const a = want + (q * 2 * Math.PI) / 72;
|
|
230
|
+
const x = lim * Math.cos(a), y = lim * Math.sin(a);
|
|
231
|
+
let cost = 0;
|
|
232
|
+
for (const b of bodies)
|
|
233
|
+
cost += Math.max(0, b.r + r + GAP - Math.hypot(b.x - x, b.y - y));
|
|
234
|
+
const turn = Math.min(q, 72 - q);
|
|
235
|
+
if (cost < bestCost - 1e-6 || (Math.abs(cost - bestCost) <= 1e-6 && turn < bestTurn)) {
|
|
236
|
+
[best, bestCost, bestTurn] = [[x, y], cost, turn];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return best;
|
|
240
|
+
}
|
|
241
|
+
/** Whether a disc of radius `r` round a point of the world is clear of every box of `boxes` (left,
|
|
242
|
+
* top, right, bottom, four numbers a box): what a visitor's spot keeps clear of the clusters' names
|
|
243
|
+
* as they are written on the board, so that it never sits on a circle's label. */
|
|
244
|
+
export function clearOfBoxes(boxes, x, y, r) {
|
|
245
|
+
for (let j = 0; j + 3 < boxes.length; j += 4) {
|
|
246
|
+
// the nearest point of the box to the spot, within r of it or not
|
|
247
|
+
const nx = Math.max(boxes[j], Math.min(x, boxes[j + 2]));
|
|
248
|
+
const ny = Math.max(boxes[j + 1], Math.min(y, boxes[j + 3]));
|
|
249
|
+
if ((nx - x) ** 2 + (ny - y) ** 2 <= r * r)
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------------- presence
|
|
255
|
+
/** The trail as a follower is told it (PresenceView.visitors), read back with care: at most TRAIL
|
|
256
|
+
* visitors, each a name, where it was reached from and its place where one is given (a page of
|
|
257
|
+
* before 2026-10-11 gives a place of the world instead, which is not read: the follower places it). */
|
|
258
|
+
export function readVisitors(raw) {
|
|
259
|
+
if (!Array.isArray(raw))
|
|
260
|
+
return [];
|
|
261
|
+
const out = [];
|
|
262
|
+
const finite = (p) => Array.isArray(p) && p.length === 2 && p.every((n) => typeof n === "number" && Number.isFinite(n) && Math.abs(n) < 1e8);
|
|
263
|
+
for (const x of raw.slice(0, TRAIL)) {
|
|
264
|
+
const o = x && typeof x === "object" ? x : null;
|
|
265
|
+
if (!o || typeof o.decl !== "string" || !o.decl || o.decl.length > 2000)
|
|
266
|
+
continue;
|
|
267
|
+
const place = typeof o.in === "string" && o.in.length <= 2000 && finite(o.spot) ? { in: o.in, at: [o.spot[0], o.spot[1]] } : null;
|
|
268
|
+
out.push({ name: o.decl, from: typeof o.from === "string" && o.from.length <= 2000 ? o.from : null, place });
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
/** The trail as presence carries it: each visitor's declaration, where it came from, its place (the
|
|
273
|
+
* board's cluster and the spot in it), and where it is drawn in the world (`world`), which pages of
|
|
274
|
+
* before 2026-10-11 read. */
|
|
275
|
+
export function shareVisitors(trail, world = () => null) {
|
|
276
|
+
const r = (n) => Math.round(n * 10) / 10;
|
|
277
|
+
return trail.map((v, k) => {
|
|
278
|
+
const w = world(k);
|
|
279
|
+
return {
|
|
280
|
+
decl: v.name,
|
|
281
|
+
...(w ? { at: [r(w[0]), r(w[1])] } : {}),
|
|
282
|
+
...(v.from ? { from: v.from } : {}),
|
|
283
|
+
...(v.place ? { in: v.place.in, spot: [r(v.place.at[0]), r(v.place.at[1])] } : {}),
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------------- edges
|
|
288
|
+
/** The most edges a visitor draws to the board: one used everywhere would be a wall of them. */
|
|
289
|
+
export const MAX_EDGES = 400;
|
|
290
|
+
/** A visitor's references, as its edges are drawn: to and from what the board holds (`toView`: a
|
|
291
|
+
* declaration of the whole graph on the board, or -1) and the other visitors (`visitorOf`: its
|
|
292
|
+
* index in the trail, or -1). Each is [the other end, whether it uses the visitor (in) or the
|
|
293
|
+
* visitor uses it (out), whether the reference is in a statement or a body (strong) or only in a
|
|
294
|
+
* proof or on grammar]. At most MAX_EDGES to the board, the strong first. */
|
|
295
|
+
export function visitorEdges(g, v, toView, visitorOf) {
|
|
296
|
+
const strong = (site) => (site & (SITE_STMT | SITE_BODY)) !== 0;
|
|
297
|
+
const board = new Map();
|
|
298
|
+
const visitors = new Map();
|
|
299
|
+
const take = (u, incoming, site) => {
|
|
300
|
+
if (u === v)
|
|
301
|
+
return;
|
|
302
|
+
const j = toView(u);
|
|
303
|
+
const m = j >= 0 ? board : visitors;
|
|
304
|
+
const end = j >= 0 ? j : visitorOf(u);
|
|
305
|
+
if (end < 0)
|
|
306
|
+
return;
|
|
307
|
+
const key = `${end}${incoming ? "<" : ">"}`;
|
|
308
|
+
const was = m.get(key);
|
|
309
|
+
if (!was || (!was[2] && strong(site)))
|
|
310
|
+
m.set(key, [end, incoming, strong(site) || !!was?.[2]]);
|
|
311
|
+
};
|
|
312
|
+
for (let e = g.outStart[v]; e < g.outStart[v + 1]; e++)
|
|
313
|
+
take(g.outDst[e], false, g.outSite[e]);
|
|
314
|
+
for (let e = g.inStart[v]; e < g.inStart[v + 1]; e++)
|
|
315
|
+
take(g.inSrc[e], true, g.inSite[e]);
|
|
316
|
+
const all = [...board.values()].sort((a, b) => Number(b[2]) - Number(a[2]));
|
|
317
|
+
return { board: all.slice(0, MAX_EDGES), visitors: [...visitors.values()] };
|
|
318
|
+
}
|