slidemind 0.3.19 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/engine-slides.mjs +1 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// A drawn figure as a STRUCTURAL SKETCH, compiled to exact geometry.
|
|
2
|
+
//
|
|
3
|
+
// The concept image is a sketch; tracing it copies its wobble. A figure is
|
|
4
|
+
// instead described by what it is: a closed outline of a few points where the
|
|
5
|
+
// outline changes character (corners, arc ends, extremes and inflections of a
|
|
6
|
+
// smooth curve), each edge typed — a straight line, an arc on a named circle,
|
|
7
|
+
// or a smooth curve — plus relations (a mirrored half, a copy of another
|
|
8
|
+
// figure). Errors in those few numbers move or resize a feature; they cannot
|
|
9
|
+
// put a dent into an edge, and a repeated or mirrored figure cannot drift.
|
|
10
|
+
//
|
|
11
|
+
// The same module compiles the sketch in the slide page (`<Figure>`, shipped
|
|
12
|
+
// by the add-in as a browser script — so preview = export) and expands the
|
|
13
|
+
// reader's sketch in the transcriber. It must stay self-contained: no imports,
|
|
14
|
+
// only top-level `export function` / `export const` declarations.
|
|
15
|
+
//
|
|
16
|
+
// Figure props (slot px, absolute in the content container):
|
|
17
|
+
// circles { id: [cx, cy, r] }
|
|
18
|
+
// outline [{ x, y, smooth?, tangent?: "h"|"v", to?: "line"|"arc"|"curve", on?: circleId, dir?: "cw"|"ccw" }]
|
|
19
|
+
// — `to` is the edge from this point to the next (default line); arcs lie on `on`,
|
|
20
|
+
// running clockwise (cw) or counter-clockwise (ccw) on screen around its centre.
|
|
21
|
+
// mirror { axis: "vertical"|"horizontal", at } | { axis: "line", through: [x, y], angle }
|
|
22
|
+
// (0° = vertical); `outline` is then the half from the axis back to the axis.
|
|
23
|
+
// holes [outline, …]
|
|
24
|
+
|
|
25
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
|
|
26
|
+
const add = (a, b) => [a[0] + b[0], a[1] + b[1]];
|
|
27
|
+
const mul = (a, s) => [a[0] * s, a[1] * s];
|
|
28
|
+
const len = (a) => Math.hypot(a[0], a[1]);
|
|
29
|
+
const nrm = (a) => { const l = len(a) || 1; return [a[0] / l, a[1] / l]; };
|
|
30
|
+
const circleOf = (circles, id) => {
|
|
31
|
+
const c = id != null && circles ? circles[id] : null;
|
|
32
|
+
return Array.isArray(c) && c.length >= 3 && c.every(Number.isFinite) && c[2] > 0 ? { cx: c[0], cy: c[1], r: c[2] } : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function reflector(mirror) {
|
|
36
|
+
let px = 0, py = 0, ux = 0, uy = 1;
|
|
37
|
+
if (mirror.axis === "vertical") px = Number(mirror.at);
|
|
38
|
+
else if (mirror.axis === "horizontal") { py = Number(mirror.at); ux = 1; uy = 0; }
|
|
39
|
+
else {
|
|
40
|
+
const through = Array.isArray(mirror.through) ? mirror.through : [mirror.through?.x, mirror.through?.y];
|
|
41
|
+
const a = (Number(mirror.angle ?? 0) * Math.PI) / 180;
|
|
42
|
+
px = Number(through[0]); py = Number(through[1]); ux = Math.sin(a); uy = Math.cos(a);
|
|
43
|
+
}
|
|
44
|
+
const foot = (p) => { const t = (p[0] - px) * ux + (p[1] - py) * uy; return [px + t * ux, py + t * uy]; };
|
|
45
|
+
return { foot, reflect: (p) => { const f = foot(p); return [2 * f[0] - p[0], 2 * f[1] - p[1]]; } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The full outline of a mirrored half: its first and last points sit on the
|
|
50
|
+
* axis; the other half is reflected and traversed back. A reflection AND a
|
|
51
|
+
* reversed traversal leave each arc's turning direction as it was.
|
|
52
|
+
*/
|
|
53
|
+
export function mirrorOutline(half, mirror, circles) {
|
|
54
|
+
const { foot, reflect } = reflector(mirror);
|
|
55
|
+
const m = half.length - 1;
|
|
56
|
+
if (m < 1) return { outline: half, circles };
|
|
57
|
+
const out = { ...circles };
|
|
58
|
+
const pts = half.map((p, i) => {
|
|
59
|
+
if (i !== 0 && i !== m) return { ...p };
|
|
60
|
+
const [x, y] = foot([p.x, p.y]);
|
|
61
|
+
return { ...p, x, y };
|
|
62
|
+
});
|
|
63
|
+
const mirroredCircle = (id) => {
|
|
64
|
+
const c = circleOf(out, id);
|
|
65
|
+
if (!c) return id;
|
|
66
|
+
const [cx, cy] = reflect([c.cx, c.cy]);
|
|
67
|
+
if (Math.hypot(cx - c.cx, cy - c.cy) < 0.5) return id;
|
|
68
|
+
const nid = `${id}~m`;
|
|
69
|
+
out[nid] ??= [cx, cy, c.r];
|
|
70
|
+
return nid;
|
|
71
|
+
};
|
|
72
|
+
const edge = (p) => ({ to: p.to, on: p.to === "arc" ? mirroredCircle(p.on) : undefined, dir: p.dir });
|
|
73
|
+
const full = pts.slice(0, m);
|
|
74
|
+
full.push({ ...pts[m], ...edge(pts[m - 1]) });
|
|
75
|
+
for (let k = m - 1; k >= 1; k--) {
|
|
76
|
+
const [x, y] = reflect([pts[k].x, pts[k].y]);
|
|
77
|
+
full.push({ ...pts[k], x, y, ...edge(pts[k - 1]) });
|
|
78
|
+
}
|
|
79
|
+
return { outline: full, circles: out };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function gauss(A, b) {
|
|
83
|
+
const n = b.length, m = A.map((r, i) => [...r, b[i]]);
|
|
84
|
+
for (let c = 0; c < n; c++) {
|
|
85
|
+
let p = c;
|
|
86
|
+
for (let r = c + 1; r < n; r++) if (Math.abs(m[r][c]) > Math.abs(m[p][c])) p = r;
|
|
87
|
+
[m[c], m[p]] = [m[p], m[c]];
|
|
88
|
+
const d = m[c][c] || 1e-12;
|
|
89
|
+
for (let r = 0; r < n; r++) if (r !== c) { const f = m[r][c] / d; for (let k = c; k <= n; k++) m[r][k] -= f * m[c][k]; }
|
|
90
|
+
}
|
|
91
|
+
return m.map((r, i) => r[n] / (r[i] || 1e-12));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A C2 spline through a few designer points (chord-length parameters), ends
|
|
95
|
+
// clamped to a unit tangent where one is known, natural otherwise.
|
|
96
|
+
function splineOpen(Q, t0, t1) {
|
|
97
|
+
const k = Q.length - 1, h = [];
|
|
98
|
+
for (let i = 0; i < k; i++) h.push(Math.max(1e-6, len(sub(Q[i + 1], Q[i]))));
|
|
99
|
+
const M = [0, 1].map((ax) => {
|
|
100
|
+
const y = Q.map((q) => q[ax]), n = k + 1;
|
|
101
|
+
const A = Array.from({ length: n }, () => new Array(n).fill(0)), b = new Array(n).fill(0);
|
|
102
|
+
for (let i = 1; i < k; i++) { A[i][i - 1] = h[i - 1]; A[i][i] = 2 * (h[i - 1] + h[i]); A[i][i + 1] = h[i]; b[i] = 6 * ((y[i + 1] - y[i]) / h[i] - (y[i] - y[i - 1]) / h[i - 1]); }
|
|
103
|
+
if (t0) { A[0][0] = 2 * h[0]; A[0][1] = h[0]; b[0] = 6 * ((y[1] - y[0]) / h[0] - t0[ax]); } else A[0][0] = 1;
|
|
104
|
+
if (t1) { A[k][k - 1] = h[k - 1]; A[k][k] = 2 * h[k - 1]; b[k] = 6 * (t1[ax] - (y[k] - y[k - 1]) / h[k - 1]); } else A[k][k] = 1;
|
|
105
|
+
return gauss(A, b);
|
|
106
|
+
});
|
|
107
|
+
return cubicsFrom(Q, h, M, false);
|
|
108
|
+
}
|
|
109
|
+
function splineClosed(Q) {
|
|
110
|
+
const k = Q.length, h = [];
|
|
111
|
+
for (let i = 0; i < k; i++) h.push(Math.max(1e-6, len(sub(Q[(i + 1) % k], Q[i]))));
|
|
112
|
+
const M = [0, 1].map((ax) => {
|
|
113
|
+
const y = Q.map((q) => q[ax]), A = Array.from({ length: k }, () => new Array(k).fill(0)), b = new Array(k).fill(0);
|
|
114
|
+
for (let i = 0; i < k; i++) { const p = (i - 1 + k) % k, nx = (i + 1) % k; A[i][p] += h[p]; A[i][i] += 2 * (h[p] + h[i]); A[i][nx] += h[i]; b[i] = 6 * ((y[nx] - y[i]) / h[i] - (y[i] - y[p]) / h[p]); }
|
|
115
|
+
return gauss(A, b);
|
|
116
|
+
});
|
|
117
|
+
return cubicsFrom(Q, h, M, true);
|
|
118
|
+
}
|
|
119
|
+
function cubicsFrom(Q, h, M, closed) {
|
|
120
|
+
const n = closed ? Q.length : Q.length - 1, segs = [];
|
|
121
|
+
for (let i = 0; i < n; i++) {
|
|
122
|
+
const j = (i + 1) % Q.length;
|
|
123
|
+
const d0 = [0, 1].map((ax) => (Q[j][ax] - Q[i][ax]) / h[i] - (h[i] * (2 * M[ax][i] + M[ax][j])) / 6);
|
|
124
|
+
const d1 = [0, 1].map((ax) => (Q[j][ax] - Q[i][ax]) / h[i] + (h[i] * (M[ax][i] + 2 * M[ax][j])) / 6);
|
|
125
|
+
segs.push({ t: "C", p0: Q[i], c1: add(Q[i], mul(d0, h[i] / 3)), c2: sub(Q[j], mul(d1, h[i] / 3)), p1: Q[j] });
|
|
126
|
+
}
|
|
127
|
+
return segs;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function circleCircle(a, b, near) {
|
|
131
|
+
const d = Math.hypot(b.cx - a.cx, b.cy - a.cy);
|
|
132
|
+
if (d < 1e-6 || d > a.r + b.r || d < Math.abs(a.r - b.r)) return null;
|
|
133
|
+
const t = (a.r ** 2 - b.r ** 2 + d * d) / (2 * d), hh = Math.sqrt(Math.max(0, a.r ** 2 - t * t));
|
|
134
|
+
const mx = a.cx + (t * (b.cx - a.cx)) / d, my = a.cy + (t * (b.cy - a.cy)) / d;
|
|
135
|
+
const c1 = [mx + (hh * (b.cy - a.cy)) / d, my - (hh * (b.cx - a.cx)) / d], c2 = [mx - (hh * (b.cy - a.cy)) / d, my + (hh * (b.cx - a.cx)) / d];
|
|
136
|
+
return len(sub(c1, near)) <= len(sub(c2, near)) ? c1 : c2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Screen coordinates (y down): a positive angle turns clockwise.
|
|
140
|
+
function arcSweep(c, a, b, dir) {
|
|
141
|
+
let da = Math.atan2(b[1] - c.cy, b[0] - c.cx) - Math.atan2(a[1] - c.cy, a[0] - c.cx);
|
|
142
|
+
while (da <= -Math.PI) da += 2 * Math.PI;
|
|
143
|
+
while (da > Math.PI) da -= 2 * Math.PI;
|
|
144
|
+
if (dir === "cw" && da < 0) da += 2 * Math.PI;
|
|
145
|
+
else if (dir === "ccw" && da > 0) da -= 2 * Math.PI;
|
|
146
|
+
return da;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether an outline has points enough to enclose anything: 3, or 2 joined by
|
|
151
|
+
* an arc (a lens, a disc from two half circles). A mirrored half runs from the
|
|
152
|
+
* axis to the axis; its 2 points enclose only when their edge is an arc.
|
|
153
|
+
*/
|
|
154
|
+
export function enoughPoints(outline, mirrored = false) {
|
|
155
|
+
const n = outline?.length ?? 0;
|
|
156
|
+
if (n >= 3) return true;
|
|
157
|
+
return n === 2 && outline.some((p, i) => p?.to === "arc" && (!mirrored || i === 0));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One closed outline -> segments (L / A / C). */
|
|
161
|
+
export function compileOutline(outline, circles) {
|
|
162
|
+
const issues = [];
|
|
163
|
+
const pts = (outline ?? []).filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y));
|
|
164
|
+
const n = pts.length;
|
|
165
|
+
if (!enoughPoints(pts)) return { segs: [], issues: ["an outline needs 3 points, or 2 joined by an arc"] };
|
|
166
|
+
const P = pts.map((p) => [p.x, p.y]);
|
|
167
|
+
const kind = (i) => { const t = pts[i].to; return t === "arc" || t === "curve" ? t : "line"; };
|
|
168
|
+
const arcCircle = (i) => { if (kind(i) !== "arc") return null; const c = circleOf(circles, pts[i].on); if (!c) issues.push(`point ${i}: arc on unknown circle "${pts[i].on}"`); return c; };
|
|
169
|
+
// 1. every arc end lies on its circle; a corner between two circles is their intersection
|
|
170
|
+
for (let i = 0; i < n; i++) {
|
|
171
|
+
const cin = arcCircle((i - 1 + n) % n), cout = arcCircle(i);
|
|
172
|
+
if (cin && cout && (cin.cx !== cout.cx || cin.cy !== cout.cy || cin.r !== cout.r)) { const x = circleCircle(cin, cout, P[i]); if (x) { P[i] = x; continue; } }
|
|
173
|
+
const c = cin ?? cout;
|
|
174
|
+
if (c) { const v = nrm(sub(P[i], [c.cx, c.cy])); P[i] = [c.cx + v[0] * c.r, c.cy + v[1] * c.r]; }
|
|
175
|
+
}
|
|
176
|
+
const tangentAt = (i, atEnd) => {
|
|
177
|
+
const a = P[i], b = P[(i + 1) % n], c = kind(i) === "arc" ? circleOf(circles, pts[i].on) : null;
|
|
178
|
+
if (c) { const q = atEnd ? b : a, r = sub(q, [c.cx, c.cy]); return nrm(arcSweep(c, a, b, pts[i].dir) > 0 ? [-r[1], r[0]] : [r[1], -r[0]]); }
|
|
179
|
+
return nrm(sub(b, a));
|
|
180
|
+
};
|
|
181
|
+
const hint = (i, chord) => (pts[i].tangent === "h" ? [Math.sign(chord[0]) || 1, 0] : pts[i].tangent === "v" ? [0, Math.sign(chord[1]) || 1] : null);
|
|
182
|
+
const breaks = (i) => !pts[i].smooth || !!pts[i].tangent;
|
|
183
|
+
const segs = new Array(n).fill(null);
|
|
184
|
+
// 2. smooth curves: one spline per run of curve edges between breaks
|
|
185
|
+
if (pts.every((_, i) => kind(i) === "curve") && !pts.some((_, i) => breaks(i))) {
|
|
186
|
+
splineClosed(P).forEach((s, i) => (segs[i] = s));
|
|
187
|
+
} else {
|
|
188
|
+
for (let s = 0; s < n; s++) {
|
|
189
|
+
if (kind(s) !== "curve" || (kind((s - 1 + n) % n) === "curve" && !breaks(s))) continue;
|
|
190
|
+
const idx = [s];
|
|
191
|
+
let j = s;
|
|
192
|
+
do { j = (j + 1) % n; idx.push(j); } while (kind(j) === "curve" && !breaks(j) && idx.length <= n);
|
|
193
|
+
const Q = idx.map((i) => P[i]), first = idx[0], last = idx[idx.length - 1];
|
|
194
|
+
let t0 = hint(first, sub(Q[1], Q[0])), t1 = hint(last, sub(Q[Q.length - 1], Q[Q.length - 2]));
|
|
195
|
+
if (!t0 && pts[first].smooth && kind((first - 1 + n) % n) !== "curve") t0 = tangentAt((first - 1 + n) % n, true);
|
|
196
|
+
if (!t1 && pts[last].smooth && kind(last) !== "curve") t1 = tangentAt(last, false);
|
|
197
|
+
const cs = Q.length === 2 && !t0 && !t1 ? [{ t: "L", p0: Q[0], p1: Q[1] }] : splineOpen(Q, t0, t1);
|
|
198
|
+
idx.slice(0, -1).forEach((i, k) => (segs[i] = cs[k]));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 3. lines and arcs
|
|
202
|
+
for (let i = 0; i < n; i++) {
|
|
203
|
+
if (segs[i]) continue;
|
|
204
|
+
const a = P[i], b = P[(i + 1) % n], c = kind(i) === "arc" ? circleOf(circles, pts[i].on) : null;
|
|
205
|
+
segs[i] = c ? { t: "A", c: [c.cx, c.cy], r: c.r, a0: Math.atan2(a[1] - c.cy, a[0] - c.cx), da: arcSweep(c, a, b, pts[i].dir), p0: a, p1: b } : { t: "L", p0: a, p1: b };
|
|
206
|
+
}
|
|
207
|
+
return { segs, issues };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function flattenSegs(segs) {
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const s of segs) {
|
|
213
|
+
if (s.t === "L") out.push(s.p0);
|
|
214
|
+
else if (s.t === "A") { const k = Math.max(2, Math.ceil(Math.abs(s.da) / 0.05)); for (let i = 0; i < k; i++) { const a = s.a0 + (s.da * i) / k; out.push([s.c[0] + s.r * Math.cos(a), s.c[1] + s.r * Math.sin(a)]); } }
|
|
215
|
+
else for (let i = 0; i < 16; i++) { const t = i / 16, u = 1 - t; out.push([0, 1].map((ax) => u * u * u * s.p0[ax] + 3 * u * u * t * s.c1[ax] + 3 * u * t * t * s.c2[ax] + t * t * t * s.p1[ax])); }
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Crossings between non-adjacent edges of a closed polyline. */
|
|
221
|
+
export function selfIntersections(poly) {
|
|
222
|
+
const n = poly.length;
|
|
223
|
+
const o = (p, q, r) => Math.sign((q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]));
|
|
224
|
+
let hits = 0;
|
|
225
|
+
for (let i = 0; i < n; i++) for (let j = i + 2; j < n; j++) {
|
|
226
|
+
if (i === 0 && j === n - 1) continue;
|
|
227
|
+
const a = poly[i], b = poly[(i + 1) % n], c = poly[j], d = poly[(j + 1) % n];
|
|
228
|
+
if (o(a, b, c) * o(a, b, d) < 0 && o(c, d, a) * o(c, d, b) < 0) hits++;
|
|
229
|
+
}
|
|
230
|
+
return hits;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const fmt = (v) => String(Math.round(v * 100) / 100);
|
|
234
|
+
export function segsToPath(segs) {
|
|
235
|
+
if (!segs.length) return "";
|
|
236
|
+
let d = `M ${fmt(segs[0].p0[0])} ${fmt(segs[0].p0[1])}`;
|
|
237
|
+
for (const s of segs) {
|
|
238
|
+
if (s.t === "L") d += ` L ${fmt(s.p1[0])} ${fmt(s.p1[1])}`;
|
|
239
|
+
else if (s.t === "A") d += ` A ${fmt(s.r)} ${fmt(s.r)} 0 ${Math.abs(s.da) > Math.PI ? 1 : 0} ${s.da > 0 ? 1 : 0} ${fmt(s.p1[0])} ${fmt(s.p1[1])}`;
|
|
240
|
+
else d += ` C ${fmt(s.c1[0])} ${fmt(s.c1[1])} ${fmt(s.c2[0])} ${fmt(s.c2[1])} ${fmt(s.p1[0])} ${fmt(s.p1[1])}`;
|
|
241
|
+
}
|
|
242
|
+
return `${d} Z`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Figure props -> the path to draw (slot px), its box, and what is wrong with
|
|
247
|
+
* it. A figure that cannot be drawn exactly still draws its points as a
|
|
248
|
+
* polygon, and names why.
|
|
249
|
+
*/
|
|
250
|
+
export function compileFigure(props) {
|
|
251
|
+
let circles = props?.circles ?? {};
|
|
252
|
+
let outline = props?.outline ?? [];
|
|
253
|
+
if (props?.mirror) ({ outline, circles } = mirrorOutline(outline, props.mirror, circles));
|
|
254
|
+
const main = compileOutline(outline, circles);
|
|
255
|
+
const holes = (props?.holes ?? []).map((h) => compileOutline(h, circles));
|
|
256
|
+
const issues = [...main.issues, ...holes.flatMap((h) => h.issues)];
|
|
257
|
+
let segs = main.segs;
|
|
258
|
+
if (!segs.length && outline.length >= 2) segs = outline.map((p, i) => ({ t: "L", p0: [p.x, p.y], p1: [outline[(i + 1) % outline.length].x, outline[(i + 1) % outline.length].y] }));
|
|
259
|
+
const flat = flattenSegs(segs);
|
|
260
|
+
const crossings = flat.length ? selfIntersections(flat) : 0;
|
|
261
|
+
if (crossings) issues.push(`the outline crosses itself (${crossings}×)`);
|
|
262
|
+
const all = [flat, ...holes.map((h) => flattenSegs(h.segs))].flat();
|
|
263
|
+
const xs = all.map((p) => p[0]), ys = all.map((p) => p[1]);
|
|
264
|
+
const bbox = all.length ? { x: Math.min(...xs), y: Math.min(...ys), w: Math.max(...xs) - Math.min(...xs), h: Math.max(...ys) - Math.min(...ys) } : { x: 0, y: 0, w: 0, h: 0 };
|
|
265
|
+
const d = [segs, ...holes.map((h) => h.segs)].filter((s) => s.length).map(segsToPath).join(" ");
|
|
266
|
+
return { d, bbox, issues };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---- the reader's sketch -> per-figure props (transcriber side) ----------
|
|
270
|
+
|
|
271
|
+
const round1 = (v) => Math.round(v * 2) / 2;
|
|
272
|
+
|
|
273
|
+
function rotatePoint(p, pivot, deg, dx, dy) {
|
|
274
|
+
const t = (deg * Math.PI) / 180, co = Math.cos(t), si = Math.sin(t);
|
|
275
|
+
const x = p[0] - pivot[0], y = p[1] - pivot[1];
|
|
276
|
+
return [pivot[0] + x * co - y * si + dx, pivot[1] + x * si + y * co + dy];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The reader's sketch (slot px: `{ circles: [{ id, cx, cy, r }], figures: [{ id,
|
|
281
|
+
* outline: [{ x, y, kind, tangent, next, circle, dir }], mirror, copyOf, holes }] }`)
|
|
282
|
+
* as literal Figure props per figure id. A copy is written out as its own
|
|
283
|
+
* outline (a rotated/shifted source), so every figure's props stand alone.
|
|
284
|
+
* Returns `{ figures: Map<id, props>, issues: string[] }`.
|
|
285
|
+
*/
|
|
286
|
+
export function expandSketch(sketch) {
|
|
287
|
+
const circlesAll = {};
|
|
288
|
+
for (const c of sketch?.circles ?? []) {
|
|
289
|
+
if (c && typeof c.id === "string" && [c.cx, c.cy, c.r].every(Number.isFinite) && c.r > 0) circlesAll[c.id] = [c.cx, c.cy, c.r];
|
|
290
|
+
}
|
|
291
|
+
const readPoint = (p) => {
|
|
292
|
+
const out = { x: round1(p.x), y: round1(p.y) };
|
|
293
|
+
if (p.kind === "smooth") out.smooth = true;
|
|
294
|
+
if (p.tangent === "h" || p.tangent === "v") out.tangent = p.tangent;
|
|
295
|
+
if (p.next === "arc" || p.next === "curve") out.to = p.next;
|
|
296
|
+
if (p.next === "arc") { out.on = p.circle; if (p.dir === "cw" || p.dir === "ccw") out.dir = p.dir; }
|
|
297
|
+
return out;
|
|
298
|
+
};
|
|
299
|
+
const byId = new Map((sketch?.figures ?? []).filter((f) => f && typeof f.id === "string").map((f) => [f.id, f]));
|
|
300
|
+
const out = new Map();
|
|
301
|
+
const issues = [];
|
|
302
|
+
const usedCircles = (outlines, pool) => {
|
|
303
|
+
const ids = new Set(outlines.flat().filter((p) => p.to === "arc").map((p) => p.on));
|
|
304
|
+
return Object.fromEntries([...ids].filter((id) => pool[id]).map((id) => [id, pool[id].map(round1)]));
|
|
305
|
+
};
|
|
306
|
+
const build = (id, stack) => {
|
|
307
|
+
if (out.has(id)) return out.get(id);
|
|
308
|
+
const f = byId.get(id);
|
|
309
|
+
if (!f) return null;
|
|
310
|
+
if (f.copyOf) {
|
|
311
|
+
if (stack.includes(f.copyOf.id)) { issues.push(`${id}: copyOf forms a cycle`); return null; }
|
|
312
|
+
const src = build(f.copyOf.id, [...stack, id]);
|
|
313
|
+
if (!src) { issues.push(`${id}: copy of unknown figure ${f.copyOf.id}`); return null; }
|
|
314
|
+
// Mirror first, then rotate the whole: a copy has no relation of its own.
|
|
315
|
+
const full = src.mirror ? mirrorOutline(src.outline, src.mirror, src.circles) : { outline: src.outline, circles: src.circles };
|
|
316
|
+
const piv = circlesAll[f.copyOf.about]
|
|
317
|
+
? circlesAll[f.copyOf.about].slice(0, 2)
|
|
318
|
+
: (() => { const xs = full.outline.map((p) => p.x), ys = full.outline.map((p) => p.y); return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2]; })();
|
|
319
|
+
const rot = Number(f.copyOf.rotate) || 0, dx = Number(f.copyOf.dx) || 0, dy = Number(f.copyOf.dy) || 0;
|
|
320
|
+
const circles = Object.fromEntries(Object.entries(full.circles).map(([cid, c]) => { const [x, y] = rotatePoint([c[0], c[1]], piv, rot, dx, dy); return [cid, [round1(x), round1(y), round1(c[2])]]; }));
|
|
321
|
+
const outline = full.outline.map((p) => { const [x, y] = rotatePoint([p.x, p.y], piv, rot, dx, dy); return { ...p, x: round1(x), y: round1(y) }; });
|
|
322
|
+
const holes = (src.holes ?? []).map((h) => h.map((p) => { const [x, y] = rotatePoint([p.x, p.y], piv, rot, dx, dy); return { ...p, x: round1(x), y: round1(y) }; }));
|
|
323
|
+
const props = { circles: usedCircles([outline, ...holes], circles), outline, ...(holes.length ? { holes } : {}), copyOf: f.copyOf.id };
|
|
324
|
+
out.set(id, props);
|
|
325
|
+
return props;
|
|
326
|
+
}
|
|
327
|
+
const outline = (f.outline ?? []).filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y)).map(readPoint);
|
|
328
|
+
if (!enoughPoints(outline, !!f.mirror)) { issues.push(`${id}: too few points to draw`); return null; }
|
|
329
|
+
const holes = (f.holes ?? []).map((h) => (h ?? []).filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y)).map(readPoint)).filter((h) => enoughPoints(h));
|
|
330
|
+
const mirror = f.mirror?.axis === "vertical" || f.mirror?.axis === "horizontal"
|
|
331
|
+
? { axis: f.mirror.axis, at: round1(Number(f.mirror.at)) }
|
|
332
|
+
: f.mirror?.axis === "line" && f.mirror.through
|
|
333
|
+
? { axis: "line", through: [round1(Number(f.mirror.through.x ?? f.mirror.through[0])), round1(Number(f.mirror.through.y ?? f.mirror.through[1]))], angle: Math.round(Number(f.mirror.angle) * 10) / 10 }
|
|
334
|
+
: null;
|
|
335
|
+
const props = { circles: usedCircles([outline, ...holes], circlesAll), outline, ...(mirror ? { mirror } : {}), ...(holes.length ? { holes } : {}) };
|
|
336
|
+
const compiled = compileFigure(props);
|
|
337
|
+
if (compiled.issues.length) issues.push(...compiled.issues.map((m) => `${id}: ${m}`));
|
|
338
|
+
out.set(id, props);
|
|
339
|
+
return props;
|
|
340
|
+
};
|
|
341
|
+
for (const id of byId.keys()) build(id, []);
|
|
342
|
+
return { figures: out, issues };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const lit = (v) => JSON.stringify(v).replace(/"([A-Za-z_][A-Za-z0-9_]*)":/g, "$1: ");
|
|
346
|
+
/** Figure props as JSX attributes (object literals only — the preservation reader reads them). */
|
|
347
|
+
export function figureJsxAttrs(props) {
|
|
348
|
+
const attrs = [`circles={${lit(props.circles ?? {})}}`, `outline={${lit(props.outline)}}`];
|
|
349
|
+
if (props.mirror) attrs.push(`mirror={${lit(props.mirror)}}`);
|
|
350
|
+
if (props.holes?.length) attrs.push(`holes={${lit(props.holes)}}`);
|
|
351
|
+
return attrs.join(" ");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* A figure that reaches outside the content slot (the export refuses one) is
|
|
356
|
+
* shifted in — and shrunk uniformly only when it is larger than the slot —
|
|
357
|
+
* never distorted. Returns `{ props, moved }`.
|
|
358
|
+
*/
|
|
359
|
+
export function fitFigureToSlot(props, slot) {
|
|
360
|
+
const { bbox } = compileFigure(props);
|
|
361
|
+
if (!(bbox.w > 0 && bbox.h > 0)) return { props, moved: false };
|
|
362
|
+
const inside = bbox.x >= -1 && bbox.y >= -1 && bbox.x + bbox.w <= slot.w + 1 && bbox.y + bbox.h <= slot.h + 1;
|
|
363
|
+
if (inside) return { props, moved: false };
|
|
364
|
+
const k = Math.min(1, slot.w / bbox.w, slot.h / bbox.h);
|
|
365
|
+
const tx = Math.max(0, Math.min(slot.w - bbox.w * k, bbox.x)), ty = Math.max(0, Math.min(slot.h - bbox.h * k, bbox.y));
|
|
366
|
+
const T = (x, y) => [Math.round((tx + (x - bbox.x) * k) * 2) / 2, Math.round((ty + (y - bbox.y) * k) * 2) / 2];
|
|
367
|
+
const point = (p) => { const [x, y] = T(p.x, p.y); return { ...p, x, y }; };
|
|
368
|
+
const circles = Object.fromEntries(Object.entries(props.circles ?? {}).map(([id, c]) => { const [x, y] = T(c[0], c[1]); return [id, [x, y, Math.round(c[2] * k * 2) / 2]]; }));
|
|
369
|
+
let mirror = props.mirror ?? null;
|
|
370
|
+
if (mirror?.axis === "vertical") mirror = { ...mirror, at: T(mirror.at, 0)[0] };
|
|
371
|
+
else if (mirror?.axis === "horizontal") mirror = { ...mirror, at: T(0, mirror.at)[1] };
|
|
372
|
+
else if (mirror?.axis === "line") mirror = { ...mirror, through: T(mirror.through[0], mirror.through[1]) };
|
|
373
|
+
const out = { ...props, circles, outline: props.outline.map(point), ...(mirror ? { mirror } : {}), ...(props.holes?.length ? { holes: props.holes.map((h) => h.map(point)) } : {}) };
|
|
374
|
+
return { props: out, moved: true };
|
|
375
|
+
}
|