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,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
|
+
}
|