@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/model.js ADDED
@@ -0,0 +1,342 @@
1
+ import { fit } from "./fit.js";
2
+ import { preprocess } from "./preprocess.js";
3
+ export { outline } from "./shape.js";
4
+ class Tensor {
5
+ shape;
6
+ floats;
7
+ packed;
8
+ palette;
9
+ length;
10
+ constructor(shape, floats, packed, palette) {
11
+ this.shape = shape;
12
+ this.floats = floats;
13
+ this.packed = packed;
14
+ this.palette = palette;
15
+ this.length = shape.reduce((a, b) => a * b, 1);
16
+ }
17
+ get(i) {
18
+ const f = this.floats;
19
+ if (f)
20
+ return f[i];
21
+ const byte = this.packed[i >> 1];
22
+ return this.palette[(i & 1) ? (byte >> 4) & 0xf : byte & 0xf];
23
+ }
24
+ }
25
+ // Minimal safetensors reader: u64 little-endian header length, JSON header, then
26
+ // raw F32 tensors or packed 4-bit k-means palette tensors (`name` U8 indices +
27
+ // `name.palette` F32 centroids, with logical shape in __metadata__).
28
+ function parseSafetensors(bytes) {
29
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
30
+ const headerLen = Number(dv.getBigUint64(0, true));
31
+ const header = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + headerLen)));
32
+ const dataStart = 8 + headerLen;
33
+ const meta = header.__metadata__ ?? {};
34
+ const out = new Map();
35
+ const slice = (name) => {
36
+ const e = header[name];
37
+ if (!e)
38
+ throw new Error(`shapes: missing tensor ${name}`);
39
+ const [a, b] = e.data_offsets;
40
+ const buf = bytes.slice(dataStart + a, dataStart + b);
41
+ return { shape: e.shape, bytes: buf };
42
+ };
43
+ const f32 = (name) => {
44
+ const s = slice(name);
45
+ return new Float32Array(s.bytes.buffer, s.bytes.byteOffset, s.bytes.byteLength / 4);
46
+ };
47
+ for (const name of Object.keys(header)) {
48
+ if (name === "__metadata__" || name.endsWith(".palette"))
49
+ continue;
50
+ if (header[name + ".palette"]) {
51
+ const shape = String(meta["shape." + name]).split(",").map(Number);
52
+ out.set(name, new Tensor(shape, null, slice(name).bytes, f32(name + ".palette")));
53
+ }
54
+ else {
55
+ const s = slice(name);
56
+ out.set(name, new Tensor(s.shape, new Float32Array(s.bytes.buffer, s.bytes.byteOffset, s.bytes.byteLength / 4), null, null));
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ function erf(x) {
62
+ const t = 1 / (1 + 0.3275911 * Math.abs(x));
63
+ const y = 1 -
64
+ (((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) *
65
+ t *
66
+ Math.exp(-x * x));
67
+ return x >= 0 ? y : -y;
68
+ }
69
+ const gelu = (x) => 0.5 * x * (1 + erf(x / Math.SQRT2));
70
+ function layerNorm(v, w, b) {
71
+ const n = v.length;
72
+ let mean = 0;
73
+ for (let i = 0; i < n; i++)
74
+ mean += v[i];
75
+ mean /= n;
76
+ let varr = 0;
77
+ for (let i = 0; i < n; i++)
78
+ varr += (v[i] - mean) * (v[i] - mean);
79
+ varr /= n;
80
+ const inv = 1 / Math.sqrt(varr + 1e-5);
81
+ const out = new Float32Array(n);
82
+ for (let i = 0; i < n; i++)
83
+ out[i] = (v[i] - mean) * inv * w.get(i) + b.get(i);
84
+ return out;
85
+ }
86
+ /** Loads the portable model and runs stroke recognition. Construct once and reuse. */
87
+ export class ShapesModel {
88
+ meta;
89
+ t;
90
+ D;
91
+ H;
92
+ dh;
93
+ scale;
94
+ layers;
95
+ constructor(weights, meta) {
96
+ this.meta = meta;
97
+ this.t = parseSafetensors(weights);
98
+ this.D = meta.model.width;
99
+ this.H = meta.model.heads;
100
+ this.dh = this.D / this.H;
101
+ this.scale = 1 / Math.sqrt(this.dh);
102
+ this.layers = meta.model.layers;
103
+ }
104
+ get(name) {
105
+ const t = this.t.get(name);
106
+ if (!t)
107
+ throw new Error(`shapes: missing tensor ${name}`);
108
+ return t;
109
+ }
110
+ /**
111
+ * Recognize a single stroke (ordered `[x, y]` points). Returns the snapped
112
+ * {@link Shape}, or `null` when the stroke is rejected or degenerate.
113
+ */
114
+ recognize(points) {
115
+ const feats = preprocess(points, this.meta.preprocess);
116
+ if (!feats)
117
+ return null;
118
+ const probs = this.classify(feats);
119
+ let best = 0;
120
+ for (let i = 1; i < probs.length; i++)
121
+ if (probs[i] > probs[best])
122
+ best = i;
123
+ const kind = this.meta.classes[best];
124
+ if (kind === "none")
125
+ return null;
126
+ const gate = this.meta.gates[kind];
127
+ if (gate && probs[best] < gate.conf)
128
+ return null;
129
+ const { shape, residual } = fit(kind, points);
130
+ if (gate && residual > gate.resid)
131
+ return null;
132
+ return shape;
133
+ }
134
+ // Conv1d stem -> Transformer encoder -> mean-pool -> MLP head -> softmax.
135
+ classify(feats) {
136
+ const N = feats.length;
137
+ // channel-major input [in_channels][N]
138
+ let h = [];
139
+ const cin = this.meta.model.in_channels;
140
+ for (let c = 0; c < cin; c++) {
141
+ const row = new Float32Array(N);
142
+ for (let t = 0; t < N; t++)
143
+ row[t] = feats[t][c];
144
+ h.push(row);
145
+ }
146
+ h = this.pool(this.relu(this.conv1d(h, "stem.conv1", cin, 32)));
147
+ h = this.pool(this.relu(this.conv1d(h, "stem.conv2", 32, 64)));
148
+ h = this.relu(this.conv1d(h, "stem.conv3", 64, 128));
149
+ const T = h[0].length;
150
+ const pe = this.get("encoder.pe");
151
+ // tokens [T][D]
152
+ const tok = [];
153
+ for (let t = 0; t < T; t++) {
154
+ const row = new Float32Array(this.D);
155
+ for (let d = 0; d < this.D; d++)
156
+ row[d] = h[d][t] + pe.get(t * this.D + d);
157
+ tok.push(row);
158
+ }
159
+ let layer = tok;
160
+ for (let l = 0; l < this.layers; l++)
161
+ layer = this.encoderLayer(layer, l);
162
+ const pooled = new Float32Array(this.D);
163
+ for (const row of layer)
164
+ for (let d = 0; d < this.D; d++)
165
+ pooled[d] += row[d];
166
+ for (let d = 0; d < this.D; d++)
167
+ pooled[d] /= layer.length;
168
+ const h0 = this.linear(pooled, "head.0", 64, this.D);
169
+ for (let i = 0; i < 64; i++)
170
+ h0[i] = Math.max(0, h0[i]);
171
+ const logits = this.linear(h0, "head.3", this.meta.classes.length, 64);
172
+ return softmax(logits);
173
+ }
174
+ conv1d(input, name, cin, cout) {
175
+ const W = this.get(name + ".weight");
176
+ const B = this.get(name + ".bias");
177
+ const T = input[0].length;
178
+ const out = [];
179
+ for (let o = 0; o < cout; o++) {
180
+ const row = new Float32Array(T);
181
+ const wbase = o * cin * 3;
182
+ for (let t = 0; t < T; t++) {
183
+ let acc = B.get(o);
184
+ for (let c = 0; c < cin; c++) {
185
+ const ib = input[c];
186
+ const wb = wbase + c * 3;
187
+ if (t - 1 >= 0)
188
+ acc += W.get(wb) * ib[t - 1];
189
+ acc += W.get(wb + 1) * ib[t];
190
+ if (t + 1 < T)
191
+ acc += W.get(wb + 2) * ib[t + 1];
192
+ }
193
+ row[t] = acc;
194
+ }
195
+ out.push(row);
196
+ }
197
+ return out;
198
+ }
199
+ relu(x) {
200
+ for (const row of x)
201
+ for (let i = 0; i < row.length; i++)
202
+ if (row[i] < 0)
203
+ row[i] = 0;
204
+ return x;
205
+ }
206
+ pool(x) {
207
+ const T2 = Math.floor(x[0].length / 2);
208
+ return x.map((row) => {
209
+ const out = new Float32Array(T2);
210
+ for (let i = 0; i < T2; i++)
211
+ out[i] = Math.max(row[2 * i], row[2 * i + 1]);
212
+ return out;
213
+ });
214
+ }
215
+ linear(x, name, out, inDim) {
216
+ const W = this.get(name + ".weight");
217
+ const B = this.get(name + ".bias");
218
+ const y = new Float32Array(out);
219
+ for (let o = 0; o < out; o++) {
220
+ let acc = B.get(o);
221
+ const base = o * inDim;
222
+ for (let i = 0; i < inDim; i++)
223
+ acc += W.get(base + i) * x[i];
224
+ y[o] = acc;
225
+ }
226
+ return y;
227
+ }
228
+ encoderLayer(tok, l) {
229
+ const p = `encoder.enc.layers.${l}.`;
230
+ const inW = this.get(p + "self_attn.in_proj_weight"); // [3D, D]
231
+ const inB = this.get(p + "self_attn.in_proj_bias");
232
+ const T = tok.length;
233
+ const D = this.D, H = this.H, dh = this.dh;
234
+ // q/k/v: [T][3D]
235
+ const q = [], k = [], v = [];
236
+ for (let t = 0; t < T; t++) {
237
+ const qkv = new Float32Array(3 * D);
238
+ for (let o = 0; o < 3 * D; o++) {
239
+ let acc = inB.get(o);
240
+ const base = o * D;
241
+ for (let i = 0; i < D; i++)
242
+ acc += inW.get(base + i) * tok[t][i];
243
+ qkv[o] = acc;
244
+ }
245
+ q.push(qkv.subarray(0, D));
246
+ k.push(qkv.subarray(D, 2 * D));
247
+ v.push(qkv.subarray(2 * D, 3 * D));
248
+ }
249
+ // per-head scaled dot-product attention
250
+ const attn = [];
251
+ for (let t = 0; t < T; t++)
252
+ attn.push(new Float32Array(D));
253
+ for (let head = 0; head < H; head++) {
254
+ const off = head * dh;
255
+ for (let i = 0; i < T; i++) {
256
+ const scores = new Float32Array(T);
257
+ let mx = -Infinity;
258
+ for (let j = 0; j < T; j++) {
259
+ let s = 0;
260
+ for (let d = 0; d < dh; d++)
261
+ s += q[i][off + d] * k[j][off + d];
262
+ s *= this.scale;
263
+ scores[j] = s;
264
+ if (s > mx)
265
+ mx = s;
266
+ }
267
+ let sum = 0;
268
+ for (let j = 0; j < T; j++) {
269
+ scores[j] = Math.exp(scores[j] - mx);
270
+ sum += scores[j];
271
+ }
272
+ const ai = attn[i];
273
+ for (let j = 0; j < T; j++) {
274
+ const a = scores[j] / sum;
275
+ for (let d = 0; d < dh; d++)
276
+ ai[off + d] += a * v[j][off + d];
277
+ }
278
+ }
279
+ }
280
+ // out projection + residual + norm1
281
+ const outW = this.get(p + "self_attn.out_proj.weight");
282
+ const outB = this.get(p + "self_attn.out_proj.bias");
283
+ const n1w = this.get(p + "norm1.weight"), n1b = this.get(p + "norm1.bias");
284
+ const x1 = [];
285
+ for (let t = 0; t < T; t++) {
286
+ const a = new Float32Array(D);
287
+ for (let o = 0; o < D; o++) {
288
+ let acc = outB.get(o);
289
+ const base = o * D;
290
+ for (let i = 0; i < D; i++)
291
+ acc += outW.get(base + i) * attn[t][i];
292
+ a[o] = acc + tok[t][o];
293
+ }
294
+ x1.push(layerNorm(a, n1w, n1b));
295
+ }
296
+ // FFN + residual + norm2
297
+ const l1w = this.get(p + "linear1.weight"), l1b = this.get(p + "linear1.bias");
298
+ const l2w = this.get(p + "linear2.weight"), l2b = this.get(p + "linear2.bias");
299
+ const ffn = l1b.length;
300
+ const n2w = this.get(p + "norm2.weight"), n2b = this.get(p + "norm2.bias");
301
+ const out = [];
302
+ for (let t = 0; t < T; t++) {
303
+ const hidden = new Float32Array(ffn);
304
+ for (let o = 0; o < ffn; o++) {
305
+ let acc = l1b.get(o);
306
+ const base = o * D;
307
+ for (let i = 0; i < D; i++)
308
+ acc += l1w.get(base + i) * x1[t][i];
309
+ hidden[o] = gelu(acc);
310
+ }
311
+ const y = new Float32Array(D);
312
+ for (let o = 0; o < D; o++) {
313
+ let acc = l2b.get(o);
314
+ const base = o * ffn;
315
+ for (let i = 0; i < ffn; i++)
316
+ acc += l2w.get(base + i) * hidden[i];
317
+ y[o] = acc + x1[t][o];
318
+ }
319
+ out.push(layerNorm(y, n2w, n2b));
320
+ }
321
+ return out;
322
+ }
323
+ }
324
+ function softmax(logits) {
325
+ let mx = -Infinity;
326
+ for (const x of logits)
327
+ if (x > mx)
328
+ mx = x;
329
+ let sum = 0;
330
+ const out = new Float32Array(logits.length);
331
+ for (let i = 0; i < logits.length; i++) {
332
+ out[i] = Math.exp(logits[i] - mx);
333
+ sum += out[i];
334
+ }
335
+ for (let i = 0; i < out.length; i++)
336
+ out[i] /= sum;
337
+ return out;
338
+ }
339
+ /** Creates a {@link ShapesModel} from raw buffers (lowest-level entry). */
340
+ export function createShapes(buffers) {
341
+ return new ShapesModel(buffers.weights, buffers.meta);
342
+ }
@@ -0,0 +1,14 @@
1
+ import type { Point } from "./shape.js";
2
+ /** Frozen preprocessing constants (from the model's `shapes_meta.json`). */
3
+ export interface PreprocessConfig {
4
+ spacing: number;
5
+ dist_mean: number;
6
+ dist_std: number;
7
+ add_curvature: boolean;
8
+ }
9
+ /**
10
+ * Port of the shared `preprocess.py`: dedupe → normalize → arc-length resample →
11
+ * `[dist, cos, sin]` features. Returns the `N × C` feature rows, or `null` for a
12
+ * degenerate stroke (too few points / too small).
13
+ */
14
+ export declare function preprocess(points: Point[], cfg: PreprocessConfig): number[][] | null;
@@ -0,0 +1,111 @@
1
+ const MIN_POINTS = 2;
2
+ const MIN_TOTAL_LENGTH = 1e-6;
3
+ const DEDUPE_EPSILON = 1e-9;
4
+ /**
5
+ * Port of the shared `preprocess.py`: dedupe → normalize → arc-length resample →
6
+ * `[dist, cos, sin]` features. Returns the `N × C` feature rows, or `null` for a
7
+ * degenerate stroke (too few points / too small).
8
+ */
9
+ export function preprocess(points, cfg) {
10
+ const cleaned = dedupe(points, DEDUPE_EPSILON);
11
+ if (cleaned.length < MIN_POINTS || totalLength(cleaned) < MIN_TOTAL_LENGTH)
12
+ return null;
13
+ const normalized = normalize(cleaned);
14
+ const resampled = resample(normalized, cfg.spacing);
15
+ return features(resampled, cfg.dist_mean, cfg.dist_std);
16
+ }
17
+ function dedupe(points, epsilon) {
18
+ if (points.length === 0)
19
+ return [];
20
+ const eps2 = epsilon * epsilon;
21
+ const out = [[points[0][0], points[0][1]]];
22
+ for (let i = 1; i < points.length; i++) {
23
+ const [px, py] = out[out.length - 1];
24
+ const dx = points[i][0] - px;
25
+ const dy = points[i][1] - py;
26
+ if (dx * dx + dy * dy > eps2)
27
+ out.push([points[i][0], points[i][1]]);
28
+ }
29
+ return out;
30
+ }
31
+ function totalLength(p) {
32
+ let total = 0;
33
+ for (let i = 1; i < p.length; i++)
34
+ total += Math.hypot(p[i][0] - p[i - 1][0], p[i][1] - p[i - 1][1]);
35
+ return total;
36
+ }
37
+ function normalize(p) {
38
+ let minX = p[0][0], maxX = p[0][0], minY = p[0][1], maxY = p[0][1];
39
+ for (const [x, y] of p) {
40
+ if (x < minX)
41
+ minX = x;
42
+ if (x > maxX)
43
+ maxX = x;
44
+ if (y < minY)
45
+ minY = y;
46
+ if (y > maxY)
47
+ maxY = y;
48
+ }
49
+ const cx = (minX + maxX) * 0.5;
50
+ const cy = (minY + maxY) * 0.5;
51
+ const longer = Math.max(maxX - minX, maxY - minY);
52
+ const scale = longer > 0 ? 1 / longer : 1;
53
+ return p.map(([x, y]) => [(x - cx) * scale, (y - cy) * scale]);
54
+ }
55
+ function resample(p, spacing) {
56
+ const n = p.length;
57
+ if (n <= 1)
58
+ return p.map(([x, y]) => [x, y]);
59
+ const out = [[p[0][0], p[0][1]]];
60
+ let [prevX, prevY] = p[0];
61
+ let since = 0;
62
+ let i = 1;
63
+ while (i < n) {
64
+ const dx = p[i][0] - prevX;
65
+ const dy = p[i][1] - prevY;
66
+ const segLen = Math.hypot(dx, dy);
67
+ if (segLen <= 0) {
68
+ prevX = p[i][0];
69
+ prevY = p[i][1];
70
+ i++;
71
+ continue;
72
+ }
73
+ const needed = spacing - since;
74
+ if (needed <= segLen) {
75
+ const t = needed / segLen;
76
+ prevX += dx * t;
77
+ prevY += dy * t;
78
+ out.push([prevX, prevY]);
79
+ since = 0;
80
+ }
81
+ else {
82
+ since += segLen;
83
+ prevX = p[i][0];
84
+ prevY = p[i][1];
85
+ i++;
86
+ }
87
+ }
88
+ const last = p[n - 1];
89
+ const [rx, ry] = out[out.length - 1];
90
+ if ((last[0] - rx) ** 2 + (last[1] - ry) ** 2 > 1e-18)
91
+ out.push([last[0], last[1]]);
92
+ return out;
93
+ }
94
+ function features(p, distMean, distStd) {
95
+ const std = distStd > 0 ? distStd : 1;
96
+ const out = [];
97
+ for (let i = 0; i < p.length; i++) {
98
+ let dist = 0, cos = 0, sin = 0;
99
+ if (i > 0) {
100
+ const dx = p[i][0] - p[i - 1][0];
101
+ const dy = p[i][1] - p[i - 1][1];
102
+ dist = Math.hypot(dx, dy);
103
+ if (dist > 0) {
104
+ cos = dx / dist;
105
+ sin = dy / dist;
106
+ }
107
+ }
108
+ out.push([(dist - distMean) / std, cos, sin]);
109
+ }
110
+ return out;
111
+ }
@@ -0,0 +1,34 @@
1
+ /** A 2-D point, `[x, y]`, in the same coordinate space as the input stroke. */
2
+ export type Point = [number, number];
3
+ /** A recognized, fitted shape. */
4
+ export type Shape = {
5
+ type: "line";
6
+ from: Point;
7
+ to: Point;
8
+ } | {
9
+ type: "rectangle";
10
+ corners: Point[];
11
+ } | {
12
+ type: "triangle";
13
+ vertices: Point[];
14
+ } | {
15
+ type: "ellipse";
16
+ center: Point;
17
+ semiMajor: number;
18
+ semiMinor: number;
19
+ rotation: number;
20
+ } | {
21
+ type: "star";
22
+ center: Point;
23
+ outerRadius: number;
24
+ innerRadius: number;
25
+ rotation: number;
26
+ pointCount: number;
27
+ };
28
+ /** The semantic kinds a stroke can be recognized as. */
29
+ export type ShapeKind = Shape["type"];
30
+ /**
31
+ * A closed (or, for a line, open) polyline outline of `shape`, suitable for
32
+ * rendering. `samples` controls the smoothness of the ellipse.
33
+ */
34
+ export declare function outline(shape: Shape, samples?: number): Point[];
package/dist/shape.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A closed (or, for a line, open) polyline outline of `shape`, suitable for
3
+ * rendering. `samples` controls the smoothness of the ellipse.
4
+ */
5
+ export function outline(shape, samples = 96) {
6
+ switch (shape.type) {
7
+ case "line":
8
+ return [shape.from, shape.to];
9
+ case "rectangle":
10
+ return shape.corners;
11
+ case "triangle":
12
+ return shape.vertices;
13
+ case "ellipse": {
14
+ const [cx, cy] = shape.center;
15
+ const c = Math.cos(shape.rotation);
16
+ const s = Math.sin(shape.rotation);
17
+ const out = [];
18
+ for (let i = 0; i < samples; i++) {
19
+ const t = (2 * Math.PI * i) / samples;
20
+ const x = shape.semiMajor * Math.cos(t);
21
+ const y = shape.semiMinor * Math.sin(t);
22
+ out.push([cx + x * c - y * s, cy + x * s + y * c]);
23
+ }
24
+ return out;
25
+ }
26
+ case "star": {
27
+ const [cx, cy] = shape.center;
28
+ const out = [];
29
+ const steps = shape.pointCount * 2;
30
+ for (let i = 0; i < steps; i++) {
31
+ const a = shape.rotation - Math.PI / 2 + (i * Math.PI) / shape.pointCount;
32
+ const r = i % 2 === 0 ? shape.outerRadius : shape.innerRadius;
33
+ out.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
34
+ }
35
+ return out;
36
+ }
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@desert-ant-labs/shapes",
3
+ "version": "0.1.0",
4
+ "description": "On-device single-stroke shape recognition.",
5
+ "type": "module",
6
+ "main": "./dist/index.node.js",
7
+ "types": "./dist/index.node.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "browser": {
11
+ "types": "./dist/index.browser.d.ts",
12
+ "default": "./dist/index.browser.js"
13
+ },
14
+ "worker": {
15
+ "types": "./dist/index.browser.d.ts",
16
+ "default": "./dist/index.browser.js"
17
+ },
18
+ "workerd": {
19
+ "types": "./dist/index.browser.d.ts",
20
+ "default": "./dist/index.browser.js"
21
+ },
22
+ "edge-light": {
23
+ "types": "./dist/index.browser.d.ts",
24
+ "default": "./dist/index.browser.js"
25
+ },
26
+ "node": {
27
+ "types": "./dist/index.node.d.ts",
28
+ "default": "./dist/index.node.js"
29
+ },
30
+ "default": {
31
+ "types": "./dist/index.node.d.ts",
32
+ "default": "./dist/index.node.js"
33
+ }
34
+ },
35
+ "./core": {
36
+ "types": "./dist/model.d.ts",
37
+ "default": "./dist/model.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsc",
46
+ "test": "node --import tsx --test test/*.test.ts",
47
+ "prepare": "npm run build",
48
+ "lint:publint": "publint --strict",
49
+ "lint:attw": "attw --pack . --profile esm-only",
50
+ "check:pkg": "npm run build && npm run lint:publint && npm run lint:attw"
51
+ },
52
+ "keywords": [
53
+ "shapes",
54
+ "shape-recognition",
55
+ "sketch",
56
+ "pen-input",
57
+ "pencilkit",
58
+ "on-device"
59
+ ],
60
+ "license": "SEE LICENSE IN LICENSE.md",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/Desert-Ant-Labs/shapes-js.git"
64
+ },
65
+ "homepage": "https://github.com/Desert-Ant-Labs/shapes-js",
66
+ "engines": {
67
+ "node": ">=18"
68
+ },
69
+ "sideEffects": false,
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
73
+ "devDependencies": {
74
+ "@arethetypeswrong/cli": "^0.18.4",
75
+ "@types/node": "^22",
76
+ "publint": "^0.3.21",
77
+ "tsx": "^4",
78
+ "typescript": "^5"
79
+ }
80
+ }