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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -2
  3. package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
  4. package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
  5. package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
  6. package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
  7. package/dist/assets/latex/visualproof.sty +308 -0
  8. package/dist/extractor/Extract.lean +1180 -0
  9. package/dist/extractor/Usage.lean +900 -0
  10. package/dist/mcp/visualproof-mcp.mjs +476 -0
  11. package/dist/server/access.js +95 -0
  12. package/dist/server/agent.js +3235 -0
  13. package/dist/server/agentdocs.js +35 -0
  14. package/dist/server/agentlibrary.js +202 -0
  15. package/dist/server/agentnotes.js +701 -0
  16. package/dist/server/agentviews.js +703 -0
  17. package/dist/server/askfollow.js +286 -0
  18. package/dist/server/boardspots.js +159 -0
  19. package/dist/server/dirs.js +175 -0
  20. package/dist/server/doctor.js +186 -0
  21. package/dist/server/events.js +198 -0
  22. package/dist/server/export.js +420 -0
  23. package/dist/server/extract.js +204 -0
  24. package/dist/server/fetchurl.js +321 -0
  25. package/dist/server/figures.js +158 -0
  26. package/dist/server/history.js +741 -0
  27. package/dist/server/historyapi.js +304 -0
  28. package/dist/server/hover.js +595 -0
  29. package/dist/server/identity.js +114 -0
  30. package/dist/server/invites.js +241 -0
  31. package/dist/server/layoutthread.js +51 -0
  32. package/dist/server/library.js +600 -0
  33. package/dist/server/libraryroute.js +211 -0
  34. package/dist/server/live.js +1101 -0
  35. package/dist/server/lostfound.js +199 -0
  36. package/dist/server/main.js +1394 -0
  37. package/dist/server/mcptools.js +497 -0
  38. package/dist/server/pastdoc.js +73 -0
  39. package/dist/server/pipeline.js +156 -0
  40. package/dist/server/poppler.js +251 -0
  41. package/dist/server/presence.js +252 -0
  42. package/dist/server/project.js +222 -0
  43. package/dist/server/prompt.js +53 -0
  44. package/dist/server/readonly.js +152 -0
  45. package/dist/server/readonlygate.js +123 -0
  46. package/dist/server/renames.js +268 -0
  47. package/dist/server/review.js +334 -0
  48. package/dist/server/reviewapi.js +188 -0
  49. package/dist/server/snapshot.js +49 -0
  50. package/dist/server/sourcestore.js +473 -0
  51. package/dist/server/status.js +360 -0
  52. package/dist/server/talk.js +476 -0
  53. package/dist/server/tokens.js +221 -0
  54. package/dist/server/usage.js +501 -0
  55. package/dist/server/versions.js +82 -0
  56. package/dist/server/view.js +157 -0
  57. package/dist/server/views.js +1167 -0
  58. package/dist/shared/askfollow.js +46 -0
  59. package/dist/shared/board.js +332 -0
  60. package/dist/shared/build.js +2 -0
  61. package/dist/shared/collab.js +148 -0
  62. package/dist/shared/comment.js +285 -0
  63. package/dist/shared/context.js +157 -0
  64. package/dist/shared/doc/authors.js +98 -0
  65. package/dist/shared/doc/build.js +358 -0
  66. package/dist/shared/doc/latex.js +455 -0
  67. package/dist/shared/doc/markdown.js +159 -0
  68. package/dist/shared/doc/model.js +37 -0
  69. package/dist/shared/doc/order.js +107 -0
  70. package/dist/shared/doc/proof.js +272 -0
  71. package/dist/shared/doc/sections.js +93 -0
  72. package/dist/shared/doc/settings.js +75 -0
  73. package/dist/shared/edgecodec.js +102 -0
  74. package/dist/shared/editor.js +64 -0
  75. package/dist/shared/excerpt.js +87 -0
  76. package/dist/shared/graph.js +16 -0
  77. package/dist/shared/items.js +92 -0
  78. package/dist/shared/keys.js +203 -0
  79. package/dist/shared/layout.js +926 -0
  80. package/dist/shared/lexer.js +130 -0
  81. package/dist/shared/library.js +711 -0
  82. package/dist/shared/lostfound.js +150 -0
  83. package/dist/shared/moves.js +243 -0
  84. package/dist/shared/notecard.js +295 -0
  85. package/dist/shared/noteplace.js +505 -0
  86. package/dist/shared/palette.js +46 -0
  87. package/dist/shared/pastgraph.js +95 -0
  88. package/dist/shared/places.js +80 -0
  89. package/dist/shared/projectnotes.js +244 -0
  90. package/dist/shared/query.js +810 -0
  91. package/dist/shared/review.js +642 -0
  92. package/dist/shared/reviewqueue.js +333 -0
  93. package/dist/shared/ring.js +366 -0
  94. package/dist/shared/scope.js +255 -0
  95. package/dist/shared/scopeedit.js +81 -0
  96. package/dist/shared/search.js +191 -0
  97. package/dist/shared/separate.js +178 -0
  98. package/dist/shared/sharesnap.js +280 -0
  99. package/dist/shared/sticky.js +501 -0
  100. package/dist/shared/timeline.js +73 -0
  101. package/dist/shared/titles.js +298 -0
  102. package/dist/shared/viewgraph.js +139 -0
  103. package/dist/shared/views.js +859 -0
  104. package/dist/web/bundle/app.js +48733 -0
  105. package/dist/web/bundle/edges.js +409 -0
  106. package/dist/web/bundle/layout.js +1010 -0
  107. package/dist/web/bundle/pdfworker.js +48322 -0
  108. package/dist/web/index.html +239 -0
  109. package/dist/web/src/motion.js +430 -0
  110. package/dist/web/src/past.js +224 -0
  111. package/dist/web/src/scene.js +787 -0
  112. package/dist/web/style.css +2312 -0
  113. package/docs/agent-api/README.md +91 -0
  114. package/docs/agent-api/advice.md +108 -0
  115. package/docs/agent-api/export.md +120 -0
  116. package/docs/agent-api/following.md +140 -0
  117. package/docs/agent-api/history.md +142 -0
  118. package/docs/agent-api/library.md +65 -0
  119. package/docs/agent-api/mcp.md +104 -0
  120. package/docs/agent-api/notes.md +93 -0
  121. package/docs/agent-api/reading.md +166 -0
  122. package/docs/agent-api/requests.md +94 -0
  123. package/docs/agent-api/talking.md +100 -0
  124. package/docs/agent-api/tcb.md +135 -0
  125. package/docs/agent-api/views.md +124 -0
  126. package/docs/agent-api/words.md +56 -0
  127. package/package.json +39 -4
  128. package/test/toolchains.json +5 -0
@@ -0,0 +1,295 @@
1
+ // A note on the canvas (CanvasSpec, "Notes", the owner's decisions of 2026-10-11): a small round icon
2
+ // at its place — the spot its place records — that shows what the note carries, and, expanded, its card
3
+ // (its text, what it carries, who wrote it) at an offset from the icon, joined to it by a thin leader
4
+ // line. The card keeps its size on the screen whatever the zoom, so its offset is kept in pixels of
5
+ // the screen (shared/sticky.ts, NoteCard), per note and shared as its open state is; it is never the
6
+ // note's place, which only a drag on the icon changes. This file is the geometry and the rules the
7
+ // page draws by (web/src/sticky.ts), the same in the tests: boxes are in pixels of the screen, y down.
8
+ //
9
+ // Icons scale with what they belong to (the owner's decisions of 2026-10-12): a note in a cluster, or
10
+ // attached to a declaration, is its clickable icon only while its cluster — for an attached note the
11
+ // cluster that holds its declaration — is large enough on the screen to hold a card, and below that a
12
+ // dot the size of a declaration's point, coloured by what it carries, shrinking with the zoom as
13
+ // points do; between the two it is a blend over a short range of zoom, never a jump in time. One rule
14
+ // for every note that belongs to something on the board. An attached note's offset from its
15
+ // declaration is a place on the board, in its units, so it comes closer on the screen as the zoom
16
+ // goes out. On the ring and on the canvas an icon keeps its size whatever the zoom.
17
+ //
18
+ // While the icon of a note attached to a declaration is dragged, a circle round the declaration shows
19
+ // where it stays attached (the owner's decision of 2026-10-12): its reach on the board, held on the
20
+ // screen between 48 and 160 px whatever the zoom, so that zoomed out a short move does not leave it
21
+ // and zoomed in a short move does; out of it, the note is dragged as a free note is, and another
22
+ // declaration under it takes it wherever it is.
23
+ //
24
+ // The notes layer stacks, bottom to top (NOTE_Z): the leaders, the icons and dots with the folded
25
+ // clusters' markers, the cards — the one pressed or opened last in front —, what would hold a dragged
26
+ // note, and the icon being dragged. An expanded note's card covers the icons under it; its own it
27
+ // never covers, since a card hangs clear of its icon.
28
+ import { AUTO_COLLAPSE_PX, CARD_REACH, DECL_AT, DECL_REACH, keptDecl, noteLook, ZONE_MAX_PX, ZONE_MIN_PX } from "./sticky.js";
29
+ /** the icon's diameter on the screen, in pixels: something to point at and click, small enough to
30
+ * sit in a cluster without covering what is around its place */
31
+ export const ICON_PX = 24;
32
+ export const ICON_R = ICON_PX / 2;
33
+ /** where a card hangs by default: its corner nearest the icon this far to the right of and above the
34
+ * icon's centre, which leaves a short leader between the two */
35
+ export const CARD_GAP = 20;
36
+ /** on the ring, how far out from the icon's centre the side of its card that faces the middle is */
37
+ const RING_HANG = ICON_R + 14;
38
+ /** the least room between a card and its icon: a card never covers its own icon */
39
+ const ICON_CLEAR = ICON_R + 4;
40
+ /** The icon of a note that carries `carries` (`fileOf`: the library's record of a file, null where it
41
+ * has none, which counts as a paper). */
42
+ export function iconKind(carries, fileOf) {
43
+ const cs = carries ?? [];
44
+ if (!cs.length)
45
+ return "note";
46
+ return cs.every((c) => !c.region && (fileOf(c.file)?.type ?? "pdf") !== "pdf") ? "image" : "paper";
47
+ }
48
+ /** The count on a note's icon: how many things it carries — files and clips alike, as its tooltip
49
+ * lists them — where that is more than one; null for one or none. */
50
+ export function carryCount(carries) {
51
+ const n = carries?.length ?? 0;
52
+ return n > 1 ? n : null;
53
+ }
54
+ /** What of a note shows (CanvasSpec, "Notes"): nothing in a folded cluster or in one the board does
55
+ * not draw (`hidden`: a fold counts it); else its icon, always, and its card — "open" — where the
56
+ * note is expanded and its cluster is large enough on the screen to hold the card (`clusterPx`, its
57
+ * radius in pixels; null on the canvas, never too small: shared/sticky.ts, noteLook). Zoomed out,
58
+ * the card is hidden and the icon stays — "shut", as a note collapsed by its own state shows. */
59
+ export function noteShows(open, clusterPx, hidden = false) {
60
+ return hidden ? "hidden" : noteLook(open, clusterPx);
61
+ }
62
+ // ---------------------------------------------------------------- icons and dots
63
+ /** A declaration's point across, in board units (web/src/scene.ts, LEAF_R, twice), and how large it
64
+ * must be on the screen to be pointed at (web/src/canvas.ts, NODE_MIN_PX): the canvas's rule for
65
+ * pointing at declarations, from a zoom of 0.6, which a note is attached by a drop under. */
66
+ export const POINT_UNITS = 10;
67
+ export const POINT_MIN_PX = 6;
68
+ /** A declaration's point's radius on the screen at zoom `k`, as the canvas draws it (web/src/canvas.ts,
69
+ * dotPx): smaller as the zoom goes out, never under 1.3 px nor over 5. A note's dot is as large. */
70
+ export const pointPx = (k) => Math.max(1.3, Math.min(5, 3 * Math.sqrt(k)));
71
+ /** Whether declarations can be pointed at on the screen at zoom `k` (the canvas's rule). */
72
+ export const declsPointable = (k) => POINT_UNITS * k >= POINT_MIN_PX;
73
+ /** Over how much zoom a note's icon turns into its dot, in doublings of the zoom: from the zoom at
74
+ * which what it belongs to is clearly visible down to that zoom over √2, about 41 % — short enough to
75
+ * read as one change, long enough that it is never a jump. */
76
+ export const BLEND_OCTAVES = 0.5;
77
+ /** What a note belongs to on the board (the owner's decision of 2026-10-12: one rule for every note
78
+ * that belongs to something there): a note in a cluster, its cluster; a note attached to a
79
+ * declaration, the cluster that holds the declaration — at the top level the whole board, whose
80
+ * extent the ring's radius is (`top`; null: none, and the note keeps its icon) —; on the ring or the
81
+ * canvas, nothing. `c` is the cluster (-1 for none), `radius` its radius drawn. */
82
+ export function levelOf(at, c, radius, top) {
83
+ if (at === "ring" || at === "world")
84
+ return null;
85
+ if (c >= 0)
86
+ return { r: radius(c) };
87
+ return at === "decl" && top !== null ? { r: top } : null;
88
+ }
89
+ /** How much of a note's icon shows at zoom `k`, from 0 (its dot) to 1 (its icon): 1 while the circle
90
+ * it belongs to is at least AUTO_COLLAPSE_PX round on the screen, the threshold that already decides
91
+ * whether a card can show, and on the ring or the canvas; 0 at BLEND_OCTAVES of zoom below that and
92
+ * further out; between, in proportion to the zoom's logarithm, so that it follows the zoom as the
93
+ * edges do. */
94
+ export function iconShare(level, k) {
95
+ if (!level)
96
+ return 1;
97
+ const px = level.r * k;
98
+ if (px >= AUTO_COLLAPSE_PX)
99
+ return 1;
100
+ if (!(px > 0))
101
+ return 0;
102
+ return Math.max(0, Math.min(1, 1 + Math.log2(px / AUTO_COLLAPSE_PX) / BLEND_OCTAVES));
103
+ }
104
+ /** A note's mark's radius on the screen at zoom `k`, `t` of its icon showing (iconShare): its icon's,
105
+ * ICON_R, at 1; a declaration's point's at 0 (pointPx); between, in proportion. */
106
+ export const markR = (t, k) => pointPx(k) + (ICON_R - pointPx(k)) * t;
107
+ /** Where a note attached to a declaration is in the board's world: its offset (NoteDecl.at, in the
108
+ * board's units) from the declaration's point at `point` — a place on the board, as a node's is, so
109
+ * that its distance on the screen is the offset times the zoom. Its icon and its dot alike are drawn
110
+ * there. */
111
+ export const declSpot = (point, at) => [point[0] + at[0], point[1] + at[1]];
112
+ /** What a click on a note's mark does: below its icon (`t` < 1, a dot or turning into one) it flies to
113
+ * the note, near enough to read it; on the icon it opens or collapses the note — but where its card
114
+ * is hidden by the zoom though it is expanded (`auto`: on the ring), it flies there too. */
115
+ export const markClick = (t, auto) => (t < 1 || auto ? "fly" : "toggle");
116
+ /** How a note shows at zoom `k` (the owner's decision of 2026-10-12): `t`, how much of its icon
117
+ * (iconShare), and `look` — hidden where a fold or the board hides it; its card where it is
118
+ * expanded and its icon shows whole (a card shows only while its icon does); else its mark alone. */
119
+ export function markLook(open, level, k, hidden = false) {
120
+ const t = iconShare(level, k);
121
+ return { look: hidden ? "hidden" : open && t >= 1 ? "open" : "shut", t };
122
+ }
123
+ /** How far from the point, on the screen, a note let go on a declaration sits at least: room for the
124
+ * icon (12 px round), the point (at most 5 px round), the diamond on its rim and a leader between
125
+ * them long enough to read as one, at the zoom it was let go at */
126
+ export const DECL_CLEAR_PX = 40;
127
+ /** The radius on the screen, in pixels, at zoom `k`, of the circle round a declaration in which a
128
+ * note attached to it stays attached while its icon is dragged (the owner's decision of 2026-10-12):
129
+ * the reach, DECL_REACH units, as the zoom draws it, held between ZONE_MIN_PX and ZONE_MAX_PX — so
130
+ * that zoomed in, a short move takes the note off its declaration, and zoomed out, the circle still
131
+ * shows round the icon and a short move does not. 48 px below a zoom of 0.75, 64 × the zoom up to
132
+ * 2.5, 160 px above. */
133
+ export const zonePx = (k) => Math.max(ZONE_MIN_PX, Math.min(ZONE_MAX_PX, DECL_REACH * k));
134
+ /** Whether an icon at `icon`, in the board's world, is inside the circle round the point at `point`
135
+ * at zoom `k` (zonePx). */
136
+ export const inZone = (icon, point, k) => Math.hypot(icon[0] - point[0], icon[1] - point[1]) * k <= zonePx(k);
137
+ /** The declaration a note is attached to once its icon is let go at `icon`, in the board's world, at
138
+ * zoom `k` (the owner's decisions of 2026-10-12): another declaration under it (`under`: the canvas's
139
+ * own hit, where declarations can be pointed at), wherever that is, which wins over the circle and
140
+ * over the cluster around it; else the one it is attached to already (`own`), where the icon is
141
+ * inside the circle round its point (inZone) — there it stays (`stay`); else none, and it is placed
142
+ * as a free note is (a cluster, the ring, the canvas). Each is given with its point in the world. The
143
+ * declaration, and the offset the note keeps from it in the board's units (keptDecl): where it was
144
+ * let go, on that side of the point, pushed out to DECL_CLEAR_PX on the screen at this zoom so that
145
+ * it clears the point as the reader sees it, DECL_NEAR at least and never further out than
146
+ * DECL_REACH. An icon let go inside the circle is kept where it was let go, or pushed out to clear
147
+ * the point and still inside: the circle is never smaller than the push nor larger than the farthest
148
+ * offset kept (shared/sticky.ts, DECL_FAR). */
149
+ export function declDrop(icon, under, own, k) {
150
+ // its own point under the icon is inside its circle: it stays
151
+ const other = under && under.j !== own?.j ? under : null;
152
+ const pick = other ?? (own && (under || inZone(icon, own.at, k)) ? own : null);
153
+ if (!pick)
154
+ return null;
155
+ // let go right on the point, it has no side: up and to the right, as by default, pushed out as far
156
+ // as any drop, so that it is inside the circle zoomed in too, where the default offset is not
157
+ const dx = icon[0] - pick.at[0], dy = icon[1] - pick.at[1];
158
+ const d = Math.hypot(dx, dy) >= 0.1 ? [dx, dy] : [DECL_AT[0] / Math.hypot(...DECL_AT), DECL_AT[1] / Math.hypot(...DECL_AT)];
159
+ return { j: pick.j, d: keptDecl(d, DECL_CLEAR_PX / k), stay: pick === own };
160
+ }
161
+ /** The leader of a note attached to a declaration, and the mark at the declaration's end: a thin
162
+ * dashed line from the icon's edge (`r`, its radius now) toward the point, ending at a small diamond
163
+ * that sits on the point's rim (`pr`, the point's radius), so that the point keeps its colour. The
164
+ * diamond and the room around it scale with the icon (`s`, its scale: shared with the line's width
165
+ * by the page), so that they shrink as the icon turns into its dot. All on the screen: the point at
166
+ * `p`, the icon at `i`. Null where they are too near for a line. */
167
+ export function declLeader(p, i, r, pr, s = 1) {
168
+ const dx = i[0] - p[0], dy = i[1] - p[1];
169
+ const len = Math.hypot(dx, dy);
170
+ const ux = dx / len, uy = dy / len;
171
+ const dr = DIAMOND_R * s;
172
+ const m = pr + dr + s;
173
+ if (!(len > r + m + dr + 2 * s))
174
+ return null;
175
+ return { line: [i[0] - ux * r, i[1] - uy * r, p[0] + ux * (m + dr), p[1] + uy * (m + dr)], mark: [p[0] + ux * m, p[1] + uy * m] };
176
+ }
177
+ /** the diamond's half-diagonal at the declaration's end of an attached note's leader, in pixels, at
178
+ * the icon's full size */
179
+ export const DIAMOND_R = 3.5;
180
+ // ---------------------------------------------------------------- stacking
181
+ /** How the notes layer stacks, bottom to top (the owner's decision of 2026-10-12: an expanded note's
182
+ * card covers icons; web/style.css holds the same numbers): the leaders — a card's, an attached
183
+ * note's to its declaration, and its diamond —, under every mark so that no line crosses an icon;
184
+ * the icons and dots, and the folded clusters' note markers, which stand on the board as the icons
185
+ * do; the cards, those pressed or opened last in front of the others (`card` + their rank, up to
186
+ * STACK_MOST); what would hold a dragged note, lit over the cards so that a declaration or a circle
187
+ * under one still shows; and the icon being dragged, over everything, so that it stays in sight. */
188
+ export const NOTE_Z = { leader: 1, mark: 2, card: 3, target: 68, dragged: 69 };
189
+ /** how many cards keep their place in front, the last pressed or opened first; one pressed longer ago
190
+ * goes back among the others. The frontmost card's z-index, NOTE_Z.card + STACK_MOST, stays under
191
+ * NOTE_Z.target. */
192
+ export const STACK_MOST = 64;
193
+ /** The cards' order in front after note `id` is pressed or opened (`order`: the notes in front, the
194
+ * frontmost last): `id` to the front, the oldest beyond STACK_MOST let go. */
195
+ export function raiseCard(order, id) {
196
+ const out = order.filter((x) => x !== id);
197
+ out.push(id);
198
+ return out.length > STACK_MOST ? out.slice(out.length - STACK_MOST) : out;
199
+ }
200
+ /** A card's place in the stack (its z-index on the layer): NOTE_Z.card for one never brought to the
201
+ * front (among them, the order the layer holds them in), above that by its rank in `order`. */
202
+ export const cardZ = (order, id) => NOTE_Z.card + order.indexOf(id) + 1;
203
+ /** The box of a card `w` × `h` hanging at offset `off` from an icon at (x, y). */
204
+ export const cardBox = (x, y, off, w, h) => ({ x0: x + off[0] - w / 2, y0: y + off[1] - h / 2, x1: x + off[0] + w / 2, y1: y + off[1] + h / 2 });
205
+ /** Where a card hangs by default: up and to the right of its icon, its nearest corner CARD_GAP from
206
+ * the icon's centre each way, so that it grows away from the icon as what it shows grows. */
207
+ export const defaultCard = (w, h) => [CARD_GAP + w / 2, -(CARD_GAP + h / 2)];
208
+ /** Where a card hangs by default from an icon on the ring at angle `a` (radians, in the board's
209
+ * coordinates, whose y grows downwards as the screen's does): outward, the side that faces the
210
+ * middle touching a point a little beyond the icon on the ray, as a note's box on the ring always
211
+ * touched the ring (shared/ring.ts, ringBox). */
212
+ export const ringCard = (a, w, h) => [(RING_HANG + w / 2) * Math.cos(a), (RING_HANG + h / 2) * Math.sin(a)];
213
+ /** An offset moved outwards along its own direction until the card is clear of its icon: a card
214
+ * never covers the icon that is its note's place. */
215
+ export function clearOfIcon(off, w, h) {
216
+ const [dx, dy] = off;
217
+ const nx = w / 2 + ICON_CLEAR, ny = h / 2 + ICON_CLEAR;
218
+ if (Math.abs(dx) >= nx || Math.abs(dy) >= ny)
219
+ return off;
220
+ if (!dx && !dy)
221
+ return defaultCard(w, h);
222
+ const t = Math.min(dx ? nx / Math.abs(dx) : Infinity, dy ? ny / Math.abs(dy) : Infinity);
223
+ return [dx * t, dy * t];
224
+ }
225
+ /** The offset a card dragged to centre `centre` keeps from its icon at `icon`: clear of the icon,
226
+ * within CARD_REACH, in whole pixels. */
227
+ export function keptCard(icon, centre, w, h) {
228
+ const [dx, dy] = clearOfIcon([centre[0] - icon[0], centre[1] - icon[1]], w, h);
229
+ const within = (v) => Math.round(Math.max(-CARD_REACH, Math.min(CARD_REACH, v)));
230
+ return [within(dx), within(dy)];
231
+ }
232
+ const area = (a, b) => Math.max(0, Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0)) * Math.max(0, Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0));
233
+ /** The ways a card can hang from its icon: as its offset says, mirrored across, up and down, both. */
234
+ const FLIPS = [
235
+ [1, 1],
236
+ [-1, 1],
237
+ [1, -1],
238
+ [-1, -1],
239
+ ];
240
+ /** Where a card `w` × `h` hangs from an icon at `icon` with offset `off`: as the offset says, or
241
+ * mirrored across, up and down, or both — the first of those that lies wholly in `room` (the screen
242
+ * the bars and panels leave), and, where `avoid` is given (the icon's own cluster's name, for a card
243
+ * hanging as by default), the first of those that also keeps off it; where none lies wholly in the
244
+ * room, the one that has the most of it there. Its box, and which way it hangs (0–3, as FLIPS). */
245
+ export function hangCard(icon, off, w, h, room, avoid = null) {
246
+ let best = null;
247
+ for (let f = 0; f < FLIPS.length; f++) {
248
+ const box = cardBox(icon[0], icon[1], [off[0] * FLIPS[f][0], off[1] * FLIPS[f][1]], w, h);
249
+ // what lies outside the room, under half a pixel being none
250
+ const outside = w * h - area(box, room);
251
+ const out = outside > 0.5 ? outside : 0;
252
+ const covers = !!avoid && area(box, avoid) > 0;
253
+ if (!best || out < best.out || (out === best.out && best.covers && !covers))
254
+ best = { box, flip: f, out, covers };
255
+ if (!out && !covers)
256
+ break;
257
+ }
258
+ return { box: best.box, flip: best.flip };
259
+ }
260
+ /** The leader between an icon at (x, y) and its card's box: from the icon's edge to the point of the
261
+ * card nearest the icon's centre; null where they touch. */
262
+ export function leaderLine(x, y, b) {
263
+ const px = Math.max(b.x0, Math.min(b.x1, x)), py = Math.max(b.y0, Math.min(b.y1, y));
264
+ const d = Math.hypot(px - x, py - y);
265
+ if (d <= ICON_R + 1)
266
+ return null;
267
+ return [x + ((px - x) / d) * ICON_R, y + ((py - y) / d) * ICON_R, px, py];
268
+ }
269
+ export function dropTarget(to, stands, index) {
270
+ if (!to)
271
+ return null;
272
+ if (to.kind === "project") {
273
+ if (!("in" in to.place))
274
+ return { at: "ring" };
275
+ const c = stands(to.place.in[0][0]);
276
+ return c !== undefined && c >= 0 ? { at: "cluster", c } : { at: "ring" };
277
+ }
278
+ const key = to.place.in[0]?.[0];
279
+ const c = key === undefined ? -1 : index(key);
280
+ return c >= 0 ? { at: "cluster", c } : null;
281
+ }
282
+ /** What Fit to screen holds of the notes at zoom `k`: each mark — its icon where the icon shows at
283
+ * that zoom, else its dot — and with `cards` each card that shows at that zoom, as it hangs
284
+ * unflipped: a view that holds it leaves it on the screen as it is. */
285
+ export function noteBoxes(notes, k, cards) {
286
+ const out = [];
287
+ for (const n of notes) {
288
+ const { d, r } = n.mark ? n.mark(k) : { d: [0, 0], r: ICON_R };
289
+ out.push({ x: n.x, y: n.y, dx0: d[0] - r, dy0: d[1] - r, dx1: d[0] + r, dy1: d[1] + r });
290
+ const c = cards && n.card ? n.card(k) : null;
291
+ if (c)
292
+ out.push({ x: n.x, y: n.y, dx0: d[0] + c.off[0] - c.w / 2, dy0: d[1] + c.off[1] - c.h / 2, dx1: d[0] + c.off[0] + c.w / 2, dy1: d[1] + c.off[1] + c.h / 2 });
293
+ }
294
+ return out;
295
+ }