@desert-ant-labs/shapes 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/dist/fit.js ADDED
@@ -0,0 +1,393 @@
1
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
2
+ const add = (a, b) => [a[0] + b[0], a[1] + b[1]];
3
+ const mul = (a, s) => [a[0] * s, a[1] * s];
4
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1];
5
+ const len = (a) => Math.hypot(a[0], a[1]);
6
+ const rot = (v, ang) => {
7
+ const c = Math.cos(ang), s = Math.sin(ang);
8
+ return [c * v[0] - s * v[1], s * v[0] + c * v[1]];
9
+ };
10
+ function centroid(p) {
11
+ let x = 0, y = 0;
12
+ for (const q of p) {
13
+ x += q[0];
14
+ y += q[1];
15
+ }
16
+ return [x / p.length, y / p.length];
17
+ }
18
+ /** Larger/smaller eigenvalue + eigenvectors of the symmetric [[a,b],[b,c]]. */
19
+ function symEig(a, b, c) {
20
+ const tr = a + c;
21
+ const disc = Math.hypot((a - c) / 2, b);
22
+ const l1 = tr / 2 + disc;
23
+ const vec = (l) => {
24
+ const v = Math.abs(b) > 1e-15 ? [l - c, b] : a >= c ? [1, 0] : [0, 1];
25
+ const n = len(v);
26
+ return n > 0 ? [v[0] / n, v[1] / n] : [1, 0];
27
+ };
28
+ const v1 = vec(l1);
29
+ return { v1, v2: [-v1[1], v1[0]] };
30
+ }
31
+ function resampleUniform(p, count) {
32
+ if (p.length <= 2)
33
+ return p;
34
+ const cum = [0];
35
+ for (let i = 1; i < p.length; i++)
36
+ cum.push(cum[i - 1] + len(sub(p[i], p[i - 1])));
37
+ const total = cum[cum.length - 1];
38
+ if (total <= 0)
39
+ return p;
40
+ const out = [];
41
+ let j = 0;
42
+ for (let i = 0; i < count; i++) {
43
+ const target = (total * i) / (count - 1);
44
+ while (j < p.length - 2 && cum[j + 1] < target)
45
+ j++;
46
+ const seg = cum[j + 1] - cum[j];
47
+ const t = seg > 0 ? (target - cum[j]) / seg : 0;
48
+ out.push(add(p[j], mul(sub(p[j + 1], p[j]), t)));
49
+ }
50
+ return out;
51
+ }
52
+ function bboxDiag(p) {
53
+ let lo = [p[0][0], p[0][1]];
54
+ let hi = [p[0][0], p[0][1]];
55
+ for (const q of p) {
56
+ lo = [Math.min(lo[0], q[0]), Math.min(lo[1], q[1])];
57
+ hi = [Math.max(hi[0], q[0]), Math.max(hi[1], q[1])];
58
+ }
59
+ const d = len(sub(hi, lo));
60
+ return d > 0 ? d : 1;
61
+ }
62
+ function ptSegDist(q, a, b) {
63
+ const ab = sub(b, a);
64
+ const l2 = dot(ab, ab);
65
+ if (l2 === 0)
66
+ return len(sub(q, a));
67
+ const t = Math.max(0, Math.min(1, dot(sub(q, a), ab) / l2));
68
+ return len(sub(q, add(a, mul(ab, t))));
69
+ }
70
+ function residual(stroke, poly, closed) {
71
+ const ring = closed ? [...poly, poly[0]] : poly;
72
+ let sumSq = 0;
73
+ for (const q of stroke) {
74
+ let best = Infinity;
75
+ for (let i = 0; i < ring.length - 1; i++)
76
+ best = Math.min(best, ptSegDist(q, ring[i], ring[i + 1]));
77
+ sumSq += best * best;
78
+ }
79
+ return Math.sqrt(sumSq / stroke.length) / bboxDiag(stroke);
80
+ }
81
+ function convexHull(points) {
82
+ const seen = new Set();
83
+ const pts = [];
84
+ for (const p of points) {
85
+ const key = `${Math.round(p[0] * 1e6)},${Math.round(p[1] * 1e6)}`;
86
+ if (!seen.has(key)) {
87
+ seen.add(key);
88
+ pts.push([p[0], p[1]]);
89
+ }
90
+ }
91
+ if (pts.length <= 2)
92
+ return pts;
93
+ pts.sort((a, b) => (a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]));
94
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
95
+ const lower = [];
96
+ for (const p of pts) {
97
+ while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0)
98
+ lower.pop();
99
+ lower.push(p);
100
+ }
101
+ const upper = [];
102
+ for (let i = pts.length - 1; i >= 0; i--) {
103
+ const p = pts[i];
104
+ while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0)
105
+ upper.pop();
106
+ upper.push(p);
107
+ }
108
+ return lower.slice(0, -1).concat(upper.slice(0, -1));
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // Per-kind fitters
112
+ // ---------------------------------------------------------------------------
113
+ function fitLine(pts) {
114
+ const c = centroid(pts);
115
+ let sxx = 0, sxy = 0, syy = 0;
116
+ for (const p of pts) {
117
+ const d = sub(p, c);
118
+ sxx += d[0] * d[0];
119
+ sxy += d[0] * d[1];
120
+ syy += d[1] * d[1];
121
+ }
122
+ const dir = symEig(sxx, sxy, syy).v1;
123
+ let lo = Infinity, hi = -Infinity;
124
+ for (const p of pts) {
125
+ const t = dot(sub(p, c), dir);
126
+ lo = Math.min(lo, t);
127
+ hi = Math.max(hi, t);
128
+ }
129
+ const a = add(c, mul(dir, lo));
130
+ const b = add(c, mul(dir, hi));
131
+ return { shape: { type: "line", from: a, to: b }, residual: residual(pts, [a, b], false) };
132
+ }
133
+ function fitRectangle(pts) {
134
+ const hull = convexHull(pts);
135
+ if (hull.length < 3)
136
+ return fitLine(pts);
137
+ let best = null;
138
+ for (let i = 0; i < hull.length; i++) {
139
+ const edge = sub(hull[(i + 1) % hull.length], hull[i]);
140
+ const ang = Math.atan2(edge[1], edge[0]);
141
+ let loX = Infinity, loY = Infinity, hiX = -Infinity, hiY = -Infinity;
142
+ for (const h of hull) {
143
+ const r = rot(h, -ang);
144
+ loX = Math.min(loX, r[0]);
145
+ loY = Math.min(loY, r[1]);
146
+ hiX = Math.max(hiX, r[0]);
147
+ hiY = Math.max(hiY, r[1]);
148
+ }
149
+ const area = (hiX - loX) * (hiY - loY);
150
+ if (!best || area < best.area) {
151
+ const cr = [[loX, loY], [hiX, loY], [hiX, hiY], [loX, hiY]];
152
+ best = { area, corners: cr.map((p) => rot(p, ang)) };
153
+ }
154
+ }
155
+ const corners = best.corners;
156
+ return { shape: { type: "rectangle", corners }, residual: residual(pts, corners, true) };
157
+ }
158
+ function fitTriangle(pts) {
159
+ let hull = convexHull(pts);
160
+ if (hull.length < 3)
161
+ return fitLine(pts);
162
+ if (hull.length > 36) {
163
+ const idx = Array.from({ length: 36 }, (_, i) => Math.round((i * (hull.length - 1)) / 35));
164
+ const seen = new Set();
165
+ hull = idx.filter((k) => (seen.has(k) ? false : (seen.add(k), true))).map((k) => hull[k]);
166
+ }
167
+ let bestArea = -1;
168
+ let tri = [hull[0], hull[1], hull[2]];
169
+ for (let i = 0; i < hull.length; i++)
170
+ for (let j = i + 1; j < hull.length; j++)
171
+ for (let k = j + 1; k < hull.length; k++) {
172
+ const ab = sub(hull[j], hull[i]);
173
+ const ac = sub(hull[k], hull[i]);
174
+ const area = Math.abs(ab[0] * ac[1] - ab[1] * ac[0]) * 0.5;
175
+ if (area > bestArea) {
176
+ bestArea = area;
177
+ tri = [hull[i], hull[j], hull[k]];
178
+ }
179
+ }
180
+ return { shape: { type: "triangle", vertices: tri }, residual: residual(pts, tri, true) };
181
+ }
182
+ function ellipseOutline(c, major, minor, rotation) {
183
+ const cc = Math.cos(rotation), ss = Math.sin(rotation);
184
+ const out = [];
185
+ for (let i = 0; i < 160; i++) {
186
+ const t = (2 * Math.PI * i) / 160;
187
+ const x = major * Math.cos(t), y = minor * Math.sin(t);
188
+ out.push([c[0] + x * cc - y * ss, c[1] + x * ss + y * cc]);
189
+ }
190
+ return out;
191
+ }
192
+ function fitEllipse(pts) {
193
+ const c = centroid(pts);
194
+ let sxx = 0, sxy = 0, syy = 0;
195
+ for (const p of pts) {
196
+ const d = sub(p, c);
197
+ sxx += d[0] * d[0];
198
+ sxy += d[0] * d[1];
199
+ syy += d[1] * d[1];
200
+ }
201
+ const u = symEig(sxx, sxy, syy).v1;
202
+ const v = [-u[1], u[0]];
203
+ let uLo = Infinity, uHi = -Infinity, vLo = Infinity, vHi = -Infinity;
204
+ for (const p of pts) {
205
+ const tu = dot(sub(p, c), u), tv = dot(sub(p, c), v);
206
+ uLo = Math.min(uLo, tu);
207
+ uHi = Math.max(uHi, tu);
208
+ vLo = Math.min(vLo, tv);
209
+ vHi = Math.max(vHi, tv);
210
+ }
211
+ const center = add(add(c, mul(u, (uLo + uHi) / 2)), mul(v, (vLo + vHi) / 2));
212
+ const major = (uHi - uLo) / 2, minor = (vHi - vLo) / 2;
213
+ const rotation = Math.atan2(u[1], u[0]);
214
+ if (!(major > 0 && minor > 0 && isFinite(major) && isFinite(minor))) {
215
+ const r = pts.reduce((acc, p) => acc + len(sub(p, c)), 0) / pts.length;
216
+ return {
217
+ shape: { type: "ellipse", center: c, semiMajor: r, semiMinor: r, rotation: 0 },
218
+ residual: residual(pts, ellipseOutline(c, r, r, 0), true),
219
+ };
220
+ }
221
+ return {
222
+ shape: { type: "ellipse", center, semiMajor: major, semiMinor: minor, rotation },
223
+ residual: residual(pts, ellipseOutline(center, major, minor, rotation), true),
224
+ };
225
+ }
226
+ function starVertices(center, outer, inner, rotation) {
227
+ const out = [];
228
+ for (let i = 0; i < 10; i++) {
229
+ const a = rotation - Math.PI / 2 + (i * Math.PI) / 5;
230
+ const r = i % 2 === 0 ? outer : inner;
231
+ out.push([center[0] + r * Math.cos(a), center[1] + r * Math.sin(a)]);
232
+ }
233
+ return out;
234
+ }
235
+ function fitStar(pts) {
236
+ const center = centroid(pts);
237
+ const radii = pts.map((p) => len(sub(p, center)));
238
+ let sc = 0, ss = 0;
239
+ for (let i = 0; i < pts.length; i++) {
240
+ const a = Math.atan2(pts[i][1] - center[1], pts[i][0] - center[0]);
241
+ const w = radii[i] * radii[i];
242
+ sc += w * Math.cos(5 * a);
243
+ ss += w * Math.sin(5 * a);
244
+ }
245
+ const rot0 = Math.atan2(ss, sc) / 5 + Math.PI / 2;
246
+ const sorted = [...radii].sort((a, b) => a - b);
247
+ const top = Math.max(1, Math.floor(sorted.length / 5));
248
+ const outer = sorted.slice(sorted.length - top).reduce((a, b) => a + b, 0) / top;
249
+ const inner = outer * 0.4;
250
+ let best = { res: Infinity, angle: rot0 };
251
+ for (const angle of [rot0, rot0 + Math.PI / 5]) {
252
+ const res = residual(pts, starVertices(center, outer, inner, angle), true);
253
+ if (res < best.res)
254
+ best = { res, angle };
255
+ }
256
+ return {
257
+ shape: { type: "star", center, outerRadius: outer, innerRadius: inner, rotation: best.angle, pointCount: 5 },
258
+ residual: best.res,
259
+ };
260
+ }
261
+ // ---------------------------------------------------------------------------
262
+ // Snapping (regularize to clean axes / angles / circles / squares)
263
+ // ---------------------------------------------------------------------------
264
+ const LINE_AXIS_DEG = 5;
265
+ const ELLIPSE_CIRCLE_RATIO = 0.25;
266
+ const ELLIPSE_ROT_DEG = 15;
267
+ const RECT_SQUARE_RATIO = 0.25;
268
+ const RECT_ROT_DEG = 15;
269
+ const TRI_AXIS_DEG = 5;
270
+ const TRI_EQUI_RATIO = 0.25;
271
+ const TRI_ISO_RATIO = 0.25;
272
+ const snapIncrement = (a, incDeg) => incDeg <= 0 ? a : Math.round(a / (incDeg * Math.PI / 180)) * (incDeg * Math.PI / 180);
273
+ function snapAxis(a, thrDeg) {
274
+ if (thrDeg <= 0)
275
+ return null;
276
+ const q = Math.PI / 2;
277
+ const nearest = Math.round(a / q) * q;
278
+ return Math.abs(a - nearest) <= (thrDeg * Math.PI) / 180 ? nearest : null;
279
+ }
280
+ function snap(shape) {
281
+ switch (shape.type) {
282
+ case "line": {
283
+ const a = shape.from, b = shape.to;
284
+ const ang = Math.atan2(b[1] - a[1], b[0] - a[0]);
285
+ const snapped = snapAxis(ang, LINE_AXIS_DEG);
286
+ if (snapped == null)
287
+ return shape;
288
+ const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
289
+ const half = len(sub(b, a)) / 2;
290
+ const d = [Math.cos(snapped), Math.sin(snapped)];
291
+ return { type: "line", from: sub(mid, mul(d, half)), to: add(mid, mul(d, half)) };
292
+ }
293
+ case "ellipse": {
294
+ const hi = Math.max(shape.semiMajor, shape.semiMinor);
295
+ const lo = Math.min(shape.semiMajor, shape.semiMinor);
296
+ if (ELLIPSE_CIRCLE_RATIO > 0 && hi > 0 && lo / hi >= 1 - ELLIPSE_CIRCLE_RATIO) {
297
+ const r = (shape.semiMajor + shape.semiMinor) / 2;
298
+ return { ...shape, semiMajor: r, semiMinor: r, rotation: 0 };
299
+ }
300
+ return { ...shape, rotation: snapIncrement(shape.rotation, ELLIPSE_ROT_DEG) };
301
+ }
302
+ case "rectangle": {
303
+ const p = shape.corners;
304
+ if (p.length !== 4)
305
+ return shape;
306
+ const center = [(p[0][0] + p[1][0] + p[2][0] + p[3][0]) / 4, (p[0][1] + p[1][1] + p[2][1] + p[3][1]) / 4];
307
+ let w = len(sub(p[1], p[0]));
308
+ let h = len(sub(p[3], p[0]));
309
+ let ang = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);
310
+ const hi = Math.max(w, h), lo = Math.min(w, h);
311
+ if (RECT_SQUARE_RATIO > 0 && hi > 0 && lo / hi >= 1 - RECT_SQUARE_RATIO) {
312
+ const s = (w + h) / 2;
313
+ w = s;
314
+ h = s;
315
+ }
316
+ ang = snapIncrement(ang, RECT_ROT_DEG);
317
+ const hw = w / 2, hh = h / 2;
318
+ const local = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]];
319
+ return { type: "rectangle", corners: local.map((q) => add(center, rot(q, ang))) };
320
+ }
321
+ case "triangle":
322
+ return snapTriangle(shape.vertices);
323
+ case "star":
324
+ return shape;
325
+ }
326
+ }
327
+ const rel = (a, b) => {
328
+ const m = Math.max(a, b);
329
+ return m > 0 ? Math.abs(a - b) / m : 0;
330
+ };
331
+ function snapTriangle(verts) {
332
+ let v = verts.map((p) => [p[0], p[1]]);
333
+ const c = [(v[0][0] + v[1][0] + v[2][0]) / 3, (v[0][1] + v[1][1] + v[2][1]) / 3];
334
+ const lAB = len(sub(v[0], v[1])), lBC = len(sub(v[1], v[2])), lCA = len(sub(v[2], v[0]));
335
+ const sides = [lAB, lBC, lCA];
336
+ const mx = Math.max(...sides), mn = Math.min(...sides);
337
+ if (TRI_EQUI_RATIO > 0 && mx > 0 && (mx - mn) / mx <= TRI_EQUI_RATIO) {
338
+ const r = (len(sub(v[0], c)) + len(sub(v[1], c)) + len(sub(v[2], c))) / 3;
339
+ let sx = 0, sy = 0;
340
+ for (let i = 0; i < 3; i++) {
341
+ const a = Math.atan2(v[i][1] - c[1], v[i][0] - c[0]) - (i * 2 * Math.PI) / 3;
342
+ sx += Math.cos(a);
343
+ sy += Math.sin(a);
344
+ }
345
+ const base = Math.atan2(sy, sx);
346
+ v = [0, 1, 2].map((i) => [c[0] + r * Math.cos(base + (i * 2 * Math.PI) / 3), c[1] + r * Math.sin(base + (i * 2 * Math.PI) / 3)]);
347
+ }
348
+ else if (TRI_ISO_RATIO > 0) {
349
+ const cand = [[0, 1, 2, lAB, lCA], [1, 0, 2, lAB, lBC], [2, 0, 1, lCA, lBC]];
350
+ let bi = 0;
351
+ for (let i = 1; i < 3; i++)
352
+ if (rel(cand[i][3], cand[i][4]) < rel(cand[bi][3], cand[bi][4]))
353
+ bi = i;
354
+ const best = cand[bi];
355
+ if (rel(best[3], best[4]) <= TRI_ISO_RATIO) {
356
+ const apex = v[best[0]];
357
+ const avg = (best[3] + best[4]) / 2;
358
+ const leg = (to) => {
359
+ const d = sub(v[to], apex);
360
+ const n = len(d);
361
+ return add(apex, mul(n > 0 ? [d[0] / n, d[1] / n] : d, avg));
362
+ };
363
+ v[best[1]] = leg(best[1]);
364
+ v[best[2]] = leg(best[2]);
365
+ }
366
+ }
367
+ // axis-align the longest edge
368
+ const edges = [[v[0], v[1]], [v[1], v[2]], [v[2], v[0]]];
369
+ let longest = edges[0];
370
+ for (const e of edges)
371
+ if (len(sub(e[1], e[0])) > len(sub(longest[1], longest[0])))
372
+ longest = e;
373
+ const ang = Math.atan2(longest[1][1] - longest[0][1], longest[1][0] - longest[0][0]);
374
+ const snapped = snapAxis(ang, TRI_AXIS_DEG);
375
+ if (snapped == null)
376
+ return { type: "triangle", vertices: v };
377
+ const cen = [(v[0][0] + v[1][0] + v[2][0]) / 3, (v[0][1] + v[1][1] + v[2][1]) / 3];
378
+ const delta = snapped - ang;
379
+ return { type: "triangle", vertices: v.map((p) => add(cen, rot(sub(p, cen), delta))) };
380
+ }
381
+ const FITTERS = {
382
+ line: fitLine,
383
+ rectangle: fitRectangle,
384
+ triangle: fitTriangle,
385
+ ellipse: fitEllipse,
386
+ star: fitStar,
387
+ };
388
+ /** Fit `kind` to the raw stroke and snap it. Returns the clean shape + residual. */
389
+ export function fit(kind, points) {
390
+ const pts = resampleUniform(points, 256);
391
+ const { shape, residual: res } = FITTERS[kind](pts);
392
+ return { shape: snap(shape), residual: res };
393
+ }
package/dist/hub.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { ShapesModel } from "./model.js";
2
+ export declare const DEFAULT_HOST = "https://huggingface.co";
3
+ export declare const DEFAULT_REPO = "desert-ant-labs/shapes";
4
+ /** Pinned revision of the model repo (a commit SHA). */
5
+ export declare const DEFAULT_REVISION = "2a45f451571c17cfd86d52e063d98e724036881d";
6
+ /** Resolution + caching configuration (mutate the exported `env` to change defaults). */
7
+ export interface ShapesEnv {
8
+ /** Hugging Face host serving the model repo. */
9
+ host: string;
10
+ /** Model repo id, e.g. `"desert-ant-labs/shapes"`. */
11
+ repo: string;
12
+ /** Pinned revision (commit SHA, tag, or branch). */
13
+ revision: string;
14
+ /** Allow fetching from the Hugging Face Hub. Set `false` to require a local copy. */
15
+ allowRemote: boolean;
16
+ /** Cache downloaded files (filesystem on Node, Cache Storage in the browser). */
17
+ useCache: boolean;
18
+ /** Directory of pre-downloaded model files to use instead of the Hub (Node). */
19
+ localModelPath?: string;
20
+ /** Filesystem cache directory (Node). */
21
+ cacheDir?: string;
22
+ /** Optional Hugging Face access token (Node) — needed while the repo is private. */
23
+ token?: string;
24
+ }
25
+ /** A key/value store for cached file bytes, keyed by resolve URL. */
26
+ export interface FileCache {
27
+ get(key: string): Promise<Uint8Array | null>;
28
+ put(key: string, data: Uint8Array): Promise<void>;
29
+ }
30
+ /** Resolves all model files (local dir → cache → Hub) and builds a {@link ShapesModel}. */
31
+ export declare function loadModel(env: ShapesEnv, cache: FileCache | null, readLocal?: (name: string) => Promise<Uint8Array | null>): Promise<ShapesModel>;
package/dist/hub.js ADDED
@@ -0,0 +1,42 @@
1
+ import { createShapes } from "./model.js";
2
+ export const DEFAULT_HOST = "https://huggingface.co";
3
+ export const DEFAULT_REPO = "desert-ant-labs/shapes";
4
+ /** Pinned revision of the model repo (a commit SHA). */
5
+ export const DEFAULT_REVISION = "2a45f451571c17cfd86d52e063d98e724036881d";
6
+ const FILES = ["shapes.safetensors", "shapes_meta.json"];
7
+ function resolveUrl(env, name) {
8
+ return `${env.host}/${env.repo}/resolve/${env.revision}/${name}`;
9
+ }
10
+ async function fetchFile(env, name, cache, readLocal) {
11
+ if (readLocal) {
12
+ const local = await readLocal(name);
13
+ if (local)
14
+ return local;
15
+ }
16
+ const url = resolveUrl(env, name);
17
+ if (cache && env.useCache) {
18
+ const hit = await cache.get(url);
19
+ if (hit)
20
+ return hit;
21
+ }
22
+ if (!env.allowRemote)
23
+ throw new Error(`shapes: ${name} unavailable locally and remote loading is disabled`);
24
+ const res = await fetch(url, env.token ? { headers: { Authorization: `Bearer ${env.token}` } } : undefined);
25
+ if (!res.ok)
26
+ throw new Error(`shapes: failed to fetch ${name} from ${url} (${res.status} ${res.statusText})`);
27
+ const data = new Uint8Array(await res.arrayBuffer());
28
+ if (cache && env.useCache) {
29
+ try {
30
+ await cache.put(url, data);
31
+ }
32
+ catch {
33
+ /* caching is best-effort */
34
+ }
35
+ }
36
+ return data;
37
+ }
38
+ /** Resolves all model files (local dir → cache → Hub) and builds a {@link ShapesModel}. */
39
+ export async function loadModel(env, cache, readLocal) {
40
+ const [weights, meta] = await Promise.all(FILES.map((name) => fetchFile(env, name, cache, readLocal)));
41
+ return createShapes({ weights, meta: JSON.parse(new TextDecoder().decode(meta)) });
42
+ }
@@ -0,0 +1,18 @@
1
+ import { type ShapesEnv } from "./hub.js";
2
+ import { type ShapesModel } from "./model.js";
3
+ import type { Point, Shape } from "./shape.js";
4
+ export { createShapes, ShapesModel, type ShapesMeta } from "./model.js";
5
+ export { outline, type Point, type Shape, type ShapeKind } from "./shape.js";
6
+ export { type PreprocessConfig } from "./preprocess.js";
7
+ export { loadModel, type ShapesEnv, type FileCache } from "./hub.js";
8
+ /** Loading configuration. Mutate before the first call, or pass overrides to {@link load}. */
9
+ export declare const env: ShapesEnv;
10
+ /** Loads the model from the Hugging Face Hub (cached in Cache Storage). */
11
+ export declare function load(options?: Partial<ShapesEnv>): Promise<ShapesModel>;
12
+ /**
13
+ * Recognizes a single stroke (ordered `[x, y]` points) as a clean {@link Shape},
14
+ * or `null` if rejected. The model is loaded (and cached) lazily on first call.
15
+ */
16
+ export declare function recognize(points: Point[]): Promise<Shape | null>;
17
+ /** Clears the memoized model so the next {@link recognize} call re-reads `env`. */
18
+ export declare function reset(): void;
@@ -0,0 +1,36 @@
1
+ import { webCache } from "./cache-web.js";
2
+ import { DEFAULT_HOST, DEFAULT_REPO, DEFAULT_REVISION, loadModel } from "./hub.js";
3
+ export { createShapes, ShapesModel } from "./model.js";
4
+ export { outline } from "./shape.js";
5
+ export { loadModel } from "./hub.js";
6
+ /** Loading configuration. Mutate before the first call, or pass overrides to {@link load}. */
7
+ export const env = {
8
+ host: DEFAULT_HOST,
9
+ repo: DEFAULT_REPO,
10
+ revision: DEFAULT_REVISION,
11
+ allowRemote: true,
12
+ useCache: true,
13
+ };
14
+ /** Loads the model from the Hugging Face Hub (cached in Cache Storage). */
15
+ export async function load(options = {}) {
16
+ const e = { ...env, ...options };
17
+ return loadModel(e, e.useCache ? webCache() : null);
18
+ }
19
+ let modelPromise = null;
20
+ /**
21
+ * Recognizes a single stroke (ordered `[x, y]` points) as a clean {@link Shape},
22
+ * or `null` if rejected. The model is loaded (and cached) lazily on first call.
23
+ */
24
+ export async function recognize(points) {
25
+ if (!modelPromise) {
26
+ modelPromise = load().catch((err) => {
27
+ modelPromise = null;
28
+ throw err;
29
+ });
30
+ }
31
+ return (await modelPromise).recognize(points);
32
+ }
33
+ /** Clears the memoized model so the next {@link recognize} call re-reads `env`. */
34
+ export function reset() {
35
+ modelPromise = null;
36
+ }
@@ -0,0 +1,18 @@
1
+ import { type ShapesEnv } from "./hub.js";
2
+ import { type ShapesModel } from "./model.js";
3
+ import type { Point, Shape } from "./shape.js";
4
+ export { createShapes, ShapesModel, type ShapesMeta } from "./model.js";
5
+ export { outline, type Point, type Shape, type ShapeKind } from "./shape.js";
6
+ export { type PreprocessConfig } from "./preprocess.js";
7
+ export { loadModel, type ShapesEnv, type FileCache } from "./hub.js";
8
+ /** Loading configuration. Mutate before the first call, or pass overrides to {@link load}. */
9
+ export declare const env: ShapesEnv;
10
+ /** Loads the model: a local dir if configured, else the Hugging Face Hub (cached to disk). */
11
+ export declare function load(options?: Partial<ShapesEnv>): Promise<ShapesModel>;
12
+ /**
13
+ * Recognizes a single stroke (ordered `[x, y]` points) as a clean {@link Shape},
14
+ * or `null` if rejected. The model is loaded (and cached) lazily on first call.
15
+ */
16
+ export declare function recognize(points: Point[]): Promise<Shape | null>;
17
+ /** Clears the memoized model so the next {@link recognize} call re-reads `env`. */
18
+ export declare function reset(): void;
@@ -0,0 +1,43 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { fsCache, localReader } from "./cache-node.js";
4
+ import { DEFAULT_HOST, DEFAULT_REPO, DEFAULT_REVISION, loadModel } from "./hub.js";
5
+ export { createShapes, ShapesModel } from "./model.js";
6
+ export { outline } from "./shape.js";
7
+ export { loadModel } from "./hub.js";
8
+ /** Loading configuration. Mutate before the first call, or pass overrides to {@link load}. */
9
+ export const env = {
10
+ host: DEFAULT_HOST,
11
+ repo: DEFAULT_REPO,
12
+ revision: DEFAULT_REVISION,
13
+ allowRemote: true,
14
+ useCache: true,
15
+ cacheDir: process.env.SHAPES_CACHE_DIR ?? join(homedir(), ".cache", "shapes"),
16
+ localModelPath: process.env.SHAPES_LOCAL_PATH,
17
+ token: process.env.HF_TOKEN ?? process.env.HUGGING_FACE_HUB_TOKEN,
18
+ };
19
+ /** Loads the model: a local dir if configured, else the Hugging Face Hub (cached to disk). */
20
+ export async function load(options = {}) {
21
+ const e = { ...env, ...options };
22
+ const cache = e.useCache && e.cacheDir ? fsCache(e.cacheDir) : null;
23
+ const readLocal = e.localModelPath ? localReader(e.localModelPath) : undefined;
24
+ return loadModel(e, cache, readLocal);
25
+ }
26
+ let modelPromise = null;
27
+ /**
28
+ * Recognizes a single stroke (ordered `[x, y]` points) as a clean {@link Shape},
29
+ * or `null` if rejected. The model is loaded (and cached) lazily on first call.
30
+ */
31
+ export async function recognize(points) {
32
+ if (!modelPromise) {
33
+ modelPromise = load().catch((err) => {
34
+ modelPromise = null;
35
+ throw err;
36
+ });
37
+ }
38
+ return (await modelPromise).recognize(points);
39
+ }
40
+ /** Clears the memoized model so the next {@link recognize} call re-reads `env`. */
41
+ export function reset() {
42
+ modelPromise = null;
43
+ }
@@ -0,0 +1,48 @@
1
+ import { type PreprocessConfig } from "./preprocess.js";
2
+ import type { Point, Shape } from "./shape.js";
3
+ export type { Point, Shape, ShapeKind } from "./shape.js";
4
+ export { outline } from "./shape.js";
5
+ /** Parsed `shapes_meta.json`: classes, gates, preprocessing constants, dims. */
6
+ export interface ShapesMeta {
7
+ classes: string[];
8
+ gates: Record<string, {
9
+ conf: number;
10
+ resid: number;
11
+ }>;
12
+ preprocess: PreprocessConfig;
13
+ model: {
14
+ width: number;
15
+ heads: number;
16
+ layers: number;
17
+ in_channels: number;
18
+ pool_factor: number;
19
+ };
20
+ }
21
+ /** Loads the portable model and runs stroke recognition. Construct once and reuse. */
22
+ export declare class ShapesModel {
23
+ readonly meta: ShapesMeta;
24
+ private readonly t;
25
+ private readonly D;
26
+ private readonly H;
27
+ private readonly dh;
28
+ private readonly scale;
29
+ private readonly layers;
30
+ constructor(weights: Uint8Array, meta: ShapesMeta);
31
+ private get;
32
+ /**
33
+ * Recognize a single stroke (ordered `[x, y]` points). Returns the snapped
34
+ * {@link Shape}, or `null` when the stroke is rejected or degenerate.
35
+ */
36
+ recognize(points: Point[]): Shape | null;
37
+ private classify;
38
+ private conv1d;
39
+ private relu;
40
+ private pool;
41
+ private linear;
42
+ private encoderLayer;
43
+ }
44
+ /** Creates a {@link ShapesModel} from raw buffers (lowest-level entry). */
45
+ export declare function createShapes(buffers: {
46
+ weights: Uint8Array;
47
+ meta: ShapesMeta;
48
+ }): ShapesModel;