@myzonerocks/gosslens 0.9.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.
@@ -0,0 +1,493 @@
1
+ // The web face tracker: the tracking module compiled to wasm, run inside a
2
+ // Worker so inference never touches the main thread. The module speaks
3
+ // wasi for its clock and random imports; the tiny shim below covers
4
+ // exactly what it reaches for, no filesystem, no sockets. Frames go in as
5
+ // RGBA pixels, the frozen result struct comes back parsed.
6
+
7
+ export const GOSS_FACE_LANDMARK_COUNT = 478;
8
+ export const GOSS_FACE_BLENDSHAPE_COUNT = 52;
9
+
10
+ export interface GossFaceResult {
11
+ frameSerial: bigint;
12
+ timestampUs: bigint;
13
+ presence: number;
14
+ landmarkCount: number;
15
+ /** x, y frame pixels and z, three floats per landmark. */
16
+ landmarks: Float32Array;
17
+ blendshapes: Float32Array;
18
+ }
19
+
20
+ interface TrackingExports {
21
+ memory: WebAssembly.Memory;
22
+ goss_tracking_alloc(size: number): number;
23
+ goss_tracking_free(ptr: number, size: number): void;
24
+ goss_tracking_result_size(): number;
25
+ goss_tracking_create(taskPtr: number, taskLen: number): number;
26
+ goss_tracking_destroy(instance: number): void;
27
+ goss_tracking_process(instance: number, rgba: number, width: number, height: number, timestampUs: bigint): number;
28
+ goss_tracking_result(instance: number, out: number): number;
29
+ }
30
+
31
+ /** The wasi surface the module actually imports: a monotonic clock for
32
+ * profiling counters, randomness for hashing seeds, and exit/write stubs
33
+ * the standard library references but this module never drives. */
34
+ function wasiImports(getMemory: () => WebAssembly.Memory): WebAssembly.ModuleImports {
35
+ return {
36
+ clock_time_get: (_clock: number, _precision: bigint, outPtr: number): number => {
37
+ const now = BigInt(Math.round(performance.now() * 1e6));
38
+ new DataView(getMemory().buffer).setBigUint64(outPtr, now, true);
39
+ return 0;
40
+ },
41
+ random_get: (ptr: number, len: number): number => {
42
+ const bytes = new Uint8Array(getMemory().buffer, ptr, len);
43
+ crypto.getRandomValues(bytes);
44
+ return 0;
45
+ },
46
+ fd_write: (fd: number, iovs: number, iovsLen: number, writtenPtr: number): number => {
47
+ // Consume every byte or the writer retries forever; log lines
48
+ // surface on the console where they help.
49
+ const view = new DataView(getMemory().buffer);
50
+ let total = 0;
51
+ let text = "";
52
+ for (let at = 0; at < iovsLen; at += 1) {
53
+ const ptr = view.getUint32(iovs + at * 8, true);
54
+ const len = view.getUint32(iovs + at * 8 + 4, true);
55
+ total += len;
56
+ if (len > 0 && fd <= 2) {
57
+ text += new TextDecoder().decode(new Uint8Array(getMemory().buffer, ptr, Math.min(len, 4096)));
58
+ }
59
+ }
60
+ if (text.trim().length > 0) console.log(`tracking module: ${text.trim()}`);
61
+ view.setUint32(writtenPtr, total, true);
62
+ return 0;
63
+ },
64
+ fd_close: (): number => 0,
65
+ fd_seek: (): number => 0,
66
+ fd_fdstat_get: (): number => 8,
67
+ proc_exit: (code: number): never => {
68
+ throw new Error(`tracking module exited ${code}`);
69
+ },
70
+ environ_sizes_get: (countPtr: number, sizePtr: number): number => {
71
+ const view = new DataView(getMemory().buffer);
72
+ view.setUint32(countPtr, 0, true);
73
+ view.setUint32(sizePtr, 0, true);
74
+ return 0;
75
+ },
76
+ environ_get: (): number => 0,
77
+ args_sizes_get: (countPtr: number, sizePtr: number): number => {
78
+ const view = new DataView(getMemory().buffer);
79
+ view.setUint32(countPtr, 0, true);
80
+ view.setUint32(sizePtr, 0, true);
81
+ return 0;
82
+ },
83
+ args_get: (): number => 0,
84
+ };
85
+ }
86
+
87
+ /// Anything else the module's startup declares but never drives answers
88
+ /// with the not-supported code, so instantiation is total without hiding
89
+ /// a call that matters.
90
+ function totalWasi(getMemory: () => WebAssembly.Memory): WebAssembly.ModuleImports {
91
+ const implemented = wasiImports(getMemory);
92
+ return new Proxy(implemented, {
93
+ get(target, property: string) {
94
+ return target[property] ?? (() => 52);
95
+ },
96
+ });
97
+ }
98
+
99
+ /// Parses the frozen goss_face_result layout out of module memory at ptr:
100
+ /// frameSerial u64, timestampUs i64, presence f32, landmarkCount u32, then
101
+ /// the landmark and blendshape floats. Pure over the buffer so it is testable
102
+ /// without the wasm module.
103
+ export function parseFaceResult(buffer: ArrayBuffer, ptr: number): GossFaceResult {
104
+ const view = new DataView(buffer, ptr);
105
+ const floats = new Float32Array(buffer, ptr + 24, GOSS_FACE_LANDMARK_COUNT * 3 + GOSS_FACE_BLENDSHAPE_COUNT);
106
+ return {
107
+ frameSerial: view.getBigUint64(0, true),
108
+ timestampUs: view.getBigInt64(8, true),
109
+ presence: view.getFloat32(16, true),
110
+ landmarkCount: view.getUint32(20, true),
111
+ landmarks: floats.slice(0, GOSS_FACE_LANDMARK_COUNT * 3),
112
+ blendshapes: floats.slice(GOSS_FACE_LANDMARK_COUNT * 3),
113
+ };
114
+ }
115
+
116
+ export class GossFaceTracker {
117
+ private constructor(
118
+ private exports: TrackingExports,
119
+ private instance: number,
120
+ private resultPtr: number,
121
+ private framePtr: number,
122
+ private frameCapacity: number,
123
+ ) {}
124
+
125
+ /** Instantiates the module and stands the engines up from the task
126
+ * bundle bytes. Call inside a Worker: creation parses three models and
127
+ * takes real time, and process runs inference synchronously. */
128
+ /** Optional stage reporting for hosts that want startup visibility. */
129
+ static onStage: ((stage: string) => void) | null = null;
130
+
131
+ static async create(moduleBytes: ArrayBuffer, taskBundle: Uint8Array): Promise<GossFaceTracker> {
132
+ let memory: WebAssembly.Memory | undefined;
133
+ GossFaceTracker.onStage?.("instantiating");
134
+ const { instance } = await WebAssembly.instantiate(moduleBytes, {
135
+ wasi_snapshot_preview1: totalWasi(() => memory!),
136
+ });
137
+ GossFaceTracker.onStage?.("instantiated");
138
+ const exports = instance.exports as unknown as TrackingExports;
139
+ memory = exports.memory;
140
+
141
+ GossFaceTracker.onStage?.("engines starting");
142
+ const taskPtr = exports.goss_tracking_alloc(taskBundle.length);
143
+ if (taskPtr === 0) throw new Error("tracking module allocation failed");
144
+ new Uint8Array(exports.memory.buffer, taskPtr, taskBundle.length).set(taskBundle);
145
+ GossFaceTracker.onStage?.("bundle staged");
146
+ const handle = exports.goss_tracking_create(taskPtr, taskBundle.length);
147
+ GossFaceTracker.onStage?.("engines returned");
148
+ exports.goss_tracking_free(taskPtr, taskBundle.length);
149
+ if (handle === 0) throw new Error("tracking bundle rejected");
150
+
151
+ const resultPtr = exports.goss_tracking_alloc(exports.goss_tracking_result_size());
152
+ if (resultPtr === 0) throw new Error("tracking module allocation failed");
153
+ return new GossFaceTracker(exports, handle, resultPtr, 0, 0);
154
+ }
155
+
156
+ /** Runs the pipeline over one RGBA frame; returns the parsed result, or
157
+ * null while nothing has been published yet. */
158
+ process(rgba: Uint8Array, width: number, height: number, timestampUs: bigint): GossFaceResult | null {
159
+ const needed = width * height * 4;
160
+ if (rgba.length < needed) throw new Error("frame shorter than its dimensions");
161
+ if (this.frameCapacity < needed) {
162
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
163
+ this.framePtr = this.exports.goss_tracking_alloc(needed);
164
+ if (this.framePtr === 0) throw new Error("tracking module allocation failed");
165
+ this.frameCapacity = needed;
166
+ }
167
+ new Uint8Array(this.exports.memory.buffer, this.framePtr, needed).set(rgba.subarray(0, needed));
168
+ if (this.exports.goss_tracking_process(this.instance, this.framePtr, width, height, timestampUs) !== 0) {
169
+ throw new Error("tracking process refused the frame");
170
+ }
171
+ return this.latest();
172
+ }
173
+
174
+ /** The newest published result, parsed out of the frozen layout. */
175
+ latest(): GossFaceResult | null {
176
+ if (this.exports.goss_tracking_result(this.instance, this.resultPtr) !== 0) return null;
177
+ return parseFaceResult(this.exports.memory.buffer, this.resultPtr);
178
+ }
179
+
180
+ destroy(): void {
181
+ this.exports.goss_tracking_destroy(this.instance);
182
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
183
+ this.exports.goss_tracking_free(this.resultPtr, this.exports.goss_tracking_result_size());
184
+ }
185
+ }
186
+
187
+ export const GOSS_SEGMENTATION_MASK_SIDE = 256;
188
+
189
+ interface SegmentationExports {
190
+ memory: WebAssembly.Memory;
191
+ goss_tracking_alloc(size: number): number;
192
+ goss_tracking_free(ptr: number, size: number): void;
193
+ goss_segmentation_mask_side(): number;
194
+ goss_segmentation_create(modelPtr: number, modelLen: number, threads: number): number;
195
+ goss_segmentation_destroy(core: number): void;
196
+ goss_segmentation_process(core: number, rgba: number, width: number, height: number): number;
197
+ goss_segmentation_read_mask(core: number, out: number): number;
198
+ goss_segmentation_class_count(core: number): number;
199
+ goss_segmentation_read_class_mask(core: number, classIndex: number, out: number): number;
200
+ }
201
+
202
+ /// The web segmenter: the same wasm module's segmentation core, run in a
203
+ /// Worker. A single .tflite model in, a mask_side x mask_side subject mask
204
+ /// out. Feed the mask to a session with setSegmentationMask.
205
+ export class GossSegmenter {
206
+ private constructor(
207
+ private exports: SegmentationExports,
208
+ private core: number,
209
+ private maskPtr: number,
210
+ private maskSide: number,
211
+ private framePtr: number,
212
+ private frameCapacity: number,
213
+ ) {}
214
+
215
+ static async create(moduleBytes: ArrayBuffer, modelBytes: Uint8Array): Promise<GossSegmenter> {
216
+ let memory: WebAssembly.Memory | undefined;
217
+ const { instance } = await WebAssembly.instantiate(moduleBytes, {
218
+ wasi_snapshot_preview1: totalWasi(() => memory!),
219
+ });
220
+ const exports = instance.exports as unknown as SegmentationExports;
221
+ memory = exports.memory;
222
+
223
+ const modelPtr = exports.goss_tracking_alloc(modelBytes.length);
224
+ if (modelPtr === 0) throw new Error("segmentation module allocation failed");
225
+ new Uint8Array(exports.memory.buffer, modelPtr, modelBytes.length).set(modelBytes);
226
+ const core = exports.goss_segmentation_create(modelPtr, modelBytes.length, 1);
227
+ exports.goss_tracking_free(modelPtr, modelBytes.length);
228
+ if (core === 0) throw new Error("segmentation model rejected");
229
+
230
+ const side = exports.goss_segmentation_mask_side();
231
+ const maskPtr = exports.goss_tracking_alloc(side * side * 4);
232
+ if (maskPtr === 0) throw new Error("segmentation module allocation failed");
233
+ return new GossSegmenter(exports, core, maskPtr, side, 0, 0);
234
+ }
235
+
236
+ /** Runs the segmenter over one RGBA frame; returns the subject mask
237
+ * (mask_side x mask_side floats), or null before the first result. */
238
+ process(rgba: Uint8Array, width: number, height: number): Float32Array | null {
239
+ const needed = width * height * 4;
240
+ if (rgba.length < needed) throw new Error("frame shorter than its dimensions");
241
+ if (this.frameCapacity < needed) {
242
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
243
+ this.framePtr = this.exports.goss_tracking_alloc(needed);
244
+ if (this.framePtr === 0) throw new Error("segmentation module allocation failed");
245
+ this.frameCapacity = needed;
246
+ }
247
+ new Uint8Array(this.exports.memory.buffer, this.framePtr, needed).set(rgba.subarray(0, needed));
248
+ if (this.exports.goss_segmentation_process(this.core, this.framePtr, width, height) !== 0) {
249
+ throw new Error("segmentation process refused the frame");
250
+ }
251
+ if (this.exports.goss_segmentation_read_mask(this.core, this.maskPtr) !== 0) return null;
252
+ const count = this.maskSide * this.maskSide;
253
+ return new Float32Array(this.exports.memory.buffer, this.maskPtr, count).slice(0, count);
254
+ }
255
+
256
+ /** How many classes the model publishes: one for the selfie/hair
257
+ * segmenters, more for the multiclass model behind per-class channels. */
258
+ get classCount(): number {
259
+ return this.exports.goss_segmentation_class_count(this.core);
260
+ }
261
+
262
+ /** One class channel (mask_side x mask_side floats) from the last
263
+ * processed frame, or null before the first result. classIndex runs the
264
+ * model's own label order; channel N of the mask channels reads N - 1. */
265
+ classMask(classIndex: number): Float32Array | null {
266
+ if (this.exports.goss_segmentation_read_class_mask(this.core, classIndex, this.maskPtr) !== 0) return null;
267
+ const count = this.maskSide * this.maskSide;
268
+ return new Float32Array(this.exports.memory.buffer, this.maskPtr, count).slice(0, count);
269
+ }
270
+
271
+ destroy(): void {
272
+ this.exports.goss_segmentation_destroy(this.core);
273
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
274
+ this.exports.goss_tracking_free(this.maskPtr, this.maskSide * this.maskSide * 4);
275
+ }
276
+ }
277
+
278
+ export const GOSS_POSE_LANDMARK_COUNT = 33;
279
+
280
+ export interface GossPoseResult {
281
+ frameSerial: bigint;
282
+ timestampUs: bigint;
283
+ presence: number;
284
+ landmarkCount: number;
285
+ /** x, y frame pixels and z, three floats per landmark. */
286
+ landmarks: Float32Array;
287
+ visibilities: Float32Array;
288
+ presences: Float32Array;
289
+ }
290
+
291
+ interface PoseExports {
292
+ memory: WebAssembly.Memory;
293
+ goss_tracking_alloc(size: number): number;
294
+ goss_tracking_free(ptr: number, size: number): void;
295
+ goss_pose_result_size(): number;
296
+ goss_pose_create(taskPtr: number, taskLen: number): number;
297
+ goss_pose_destroy(instance: number): void;
298
+ goss_pose_process(instance: number, rgba: number, width: number, height: number, timestampUs: bigint): number;
299
+ goss_pose_result(instance: number, out: number): number;
300
+ }
301
+
302
+ /// The web pose tracker: the wasm module's pose pipeline, run in a Worker.
303
+ /// A pose task bundle in, the 33-landmark pose result out per frame.
304
+ /// Parses the frozen goss_pose_result layout: the header, then the 33-point
305
+ /// landmark floats, the visibility scores, and the presence scores. Pure over
306
+ /// the buffer so it is testable without the wasm module.
307
+ export function parsePoseResult(buffer: ArrayBuffer, ptr: number): GossPoseResult {
308
+ const view = new DataView(buffer, ptr);
309
+ return {
310
+ frameSerial: view.getBigUint64(0, true),
311
+ timestampUs: view.getBigInt64(8, true),
312
+ presence: view.getFloat32(16, true),
313
+ landmarkCount: view.getUint32(20, true),
314
+ landmarks: new Float32Array(buffer, ptr + 24, GOSS_POSE_LANDMARK_COUNT * 3).slice(),
315
+ visibilities: new Float32Array(buffer, ptr + 24 + GOSS_POSE_LANDMARK_COUNT * 3 * 4, GOSS_POSE_LANDMARK_COUNT).slice(),
316
+ presences: new Float32Array(buffer, ptr + 24 + GOSS_POSE_LANDMARK_COUNT * 4 * 4, GOSS_POSE_LANDMARK_COUNT).slice(),
317
+ };
318
+ }
319
+
320
+ export class GossPoseTracker {
321
+ private constructor(
322
+ private exports: PoseExports,
323
+ private instance: number,
324
+ private resultPtr: number,
325
+ private framePtr: number,
326
+ private frameCapacity: number,
327
+ ) {}
328
+
329
+ static async create(moduleBytes: ArrayBuffer, taskBundle: Uint8Array): Promise<GossPoseTracker> {
330
+ let memory: WebAssembly.Memory | undefined;
331
+ const { instance } = await WebAssembly.instantiate(moduleBytes, {
332
+ wasi_snapshot_preview1: totalWasi(() => memory!),
333
+ });
334
+ const exports = instance.exports as unknown as PoseExports;
335
+ memory = exports.memory;
336
+
337
+ const taskPtr = exports.goss_tracking_alloc(taskBundle.length);
338
+ if (taskPtr === 0) throw new Error("pose module allocation failed");
339
+ new Uint8Array(exports.memory.buffer, taskPtr, taskBundle.length).set(taskBundle);
340
+ const handle = exports.goss_pose_create(taskPtr, taskBundle.length);
341
+ exports.goss_tracking_free(taskPtr, taskBundle.length);
342
+ if (handle === 0) throw new Error("pose bundle rejected");
343
+
344
+ const resultPtr = exports.goss_tracking_alloc(exports.goss_pose_result_size());
345
+ if (resultPtr === 0) throw new Error("pose module allocation failed");
346
+ return new GossPoseTracker(exports, handle, resultPtr, 0, 0);
347
+ }
348
+
349
+ process(rgba: Uint8Array, width: number, height: number, timestampUs: bigint): GossPoseResult | null {
350
+ const needed = width * height * 4;
351
+ if (rgba.length < needed) throw new Error("frame shorter than its dimensions");
352
+ if (this.frameCapacity < needed) {
353
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
354
+ this.framePtr = this.exports.goss_tracking_alloc(needed);
355
+ if (this.framePtr === 0) throw new Error("pose module allocation failed");
356
+ this.frameCapacity = needed;
357
+ }
358
+ new Uint8Array(this.exports.memory.buffer, this.framePtr, needed).set(rgba.subarray(0, needed));
359
+ if (this.exports.goss_pose_process(this.instance, this.framePtr, width, height, timestampUs) !== 0) {
360
+ throw new Error("pose process refused the frame");
361
+ }
362
+ return this.latest();
363
+ }
364
+
365
+ latest(): GossPoseResult | null {
366
+ if (this.exports.goss_pose_result(this.instance, this.resultPtr) !== 0) return null;
367
+ return parsePoseResult(this.exports.memory.buffer, this.resultPtr);
368
+ }
369
+
370
+ destroy(): void {
371
+ this.exports.goss_pose_destroy(this.instance);
372
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
373
+ this.exports.goss_tracking_free(this.resultPtr, this.exports.goss_pose_result_size());
374
+ }
375
+ }
376
+
377
+ export const GOSS_HAND_LANDMARK_COUNT = 21;
378
+ export const GOSS_MAX_HANDS = 2;
379
+ const HAND_STRIDE = 16 + GOSS_HAND_LANDMARK_COUNT * 3 * 4; // one GossHand, bytes
380
+
381
+ export interface GossHand {
382
+ presence: number;
383
+ /** the model's score that this is a right hand. */
384
+ handedness: number;
385
+ /** index into the canned gesture set; 0 when none/unavailable. */
386
+ gesture: number;
387
+ gestureScore: number;
388
+ landmarks: Float32Array;
389
+ }
390
+
391
+ export interface GossHandResult {
392
+ frameSerial: bigint;
393
+ timestampUs: bigint;
394
+ handCount: number;
395
+ hands: GossHand[];
396
+ }
397
+
398
+ interface HandExports {
399
+ memory: WebAssembly.Memory;
400
+ goss_tracking_alloc(size: number): number;
401
+ goss_tracking_free(ptr: number, size: number): void;
402
+ goss_hand_result_size(): number;
403
+ goss_hand_create(taskPtr: number, taskLen: number): number;
404
+ goss_hand_destroy(instance: number): void;
405
+ goss_hand_process(instance: number, rgba: number, width: number, height: number, timestampUs: bigint): number;
406
+ goss_hand_result(instance: number, out: number): number;
407
+ }
408
+
409
+ /// The web hand tracker: the wasm module's hand pipeline, run in a Worker.
410
+ /// A hand landmarker or gesture-recognizer bundle in, up to two tracked
411
+ /// hands out - landmarks, handedness, and a canned gesture when present.
412
+ /// Parses the frozen goss_hand_result layout: the header with hand_count at
413
+ /// offset 16, then up to GOSS_MAX_HANDS records at offset 24, each a
414
+ /// HAND_STRIDE-byte GossHand. Pure over the buffer so it is testable without
415
+ /// the wasm module.
416
+ export function parseHandResult(buffer: ArrayBuffer, ptr: number): GossHandResult {
417
+ const view = new DataView(buffer, ptr);
418
+ const handCount = view.getUint32(16, true);
419
+ const hands: GossHand[] = [];
420
+ for (let h = 0; h < handCount && h < GOSS_MAX_HANDS; h += 1) {
421
+ const base = 24 + h * HAND_STRIDE;
422
+ hands.push({
423
+ presence: view.getFloat32(base, true),
424
+ handedness: view.getFloat32(base + 4, true),
425
+ gesture: view.getUint32(base + 8, true),
426
+ gestureScore: view.getFloat32(base + 12, true),
427
+ landmarks: new Float32Array(buffer, ptr + base + 16, GOSS_HAND_LANDMARK_COUNT * 3).slice(),
428
+ });
429
+ }
430
+ return {
431
+ frameSerial: view.getBigUint64(0, true),
432
+ timestampUs: view.getBigInt64(8, true),
433
+ handCount,
434
+ hands,
435
+ };
436
+ }
437
+
438
+ export class GossHandTracker {
439
+ private constructor(
440
+ private exports: HandExports,
441
+ private instance: number,
442
+ private resultPtr: number,
443
+ private framePtr: number,
444
+ private frameCapacity: number,
445
+ ) {}
446
+
447
+ static async create(moduleBytes: ArrayBuffer, taskBundle: Uint8Array): Promise<GossHandTracker> {
448
+ let memory: WebAssembly.Memory | undefined;
449
+ const { instance } = await WebAssembly.instantiate(moduleBytes, {
450
+ wasi_snapshot_preview1: totalWasi(() => memory!),
451
+ });
452
+ const exports = instance.exports as unknown as HandExports;
453
+ memory = exports.memory;
454
+
455
+ const taskPtr = exports.goss_tracking_alloc(taskBundle.length);
456
+ if (taskPtr === 0) throw new Error("hand module allocation failed");
457
+ new Uint8Array(exports.memory.buffer, taskPtr, taskBundle.length).set(taskBundle);
458
+ const handle = exports.goss_hand_create(taskPtr, taskBundle.length);
459
+ exports.goss_tracking_free(taskPtr, taskBundle.length);
460
+ if (handle === 0) throw new Error("hand bundle rejected");
461
+
462
+ const resultPtr = exports.goss_tracking_alloc(exports.goss_hand_result_size());
463
+ if (resultPtr === 0) throw new Error("hand module allocation failed");
464
+ return new GossHandTracker(exports, handle, resultPtr, 0, 0);
465
+ }
466
+
467
+ process(rgba: Uint8Array, width: number, height: number, timestampUs: bigint): GossHandResult | null {
468
+ const needed = width * height * 4;
469
+ if (rgba.length < needed) throw new Error("frame shorter than its dimensions");
470
+ if (this.frameCapacity < needed) {
471
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
472
+ this.framePtr = this.exports.goss_tracking_alloc(needed);
473
+ if (this.framePtr === 0) throw new Error("hand module allocation failed");
474
+ this.frameCapacity = needed;
475
+ }
476
+ new Uint8Array(this.exports.memory.buffer, this.framePtr, needed).set(rgba.subarray(0, needed));
477
+ if (this.exports.goss_hand_process(this.instance, this.framePtr, width, height, timestampUs) !== 0) {
478
+ throw new Error("hand process refused the frame");
479
+ }
480
+ return this.latest();
481
+ }
482
+
483
+ latest(): GossHandResult | null {
484
+ if (this.exports.goss_hand_result(this.instance, this.resultPtr) !== 0) return null;
485
+ return parseHandResult(this.exports.memory.buffer, this.resultPtr);
486
+ }
487
+
488
+ destroy(): void {
489
+ this.exports.goss_hand_destroy(this.instance);
490
+ if (this.framePtr !== 0) this.exports.goss_tracking_free(this.framePtr, this.frameCapacity);
491
+ this.exports.goss_tracking_free(this.resultPtr, this.exports.goss_hand_result_size());
492
+ }
493
+ }
package/src/world.ts ADDED
@@ -0,0 +1,108 @@
1
+ /// The WebXR world backend: reads the viewer pose, projection, detected
2
+ /// planes, and tracked anchors off each XR frame and feeds them into
3
+ /// the session. The page owns the XR session; this source only reads.
4
+
5
+ import type { GossSession, GossWorldPlane, GossWorldAnchorInput } from "./index";
6
+
7
+ // WebXR's types live outside the DOM lib; the narrow slice this source
8
+ // reads is declared here so no type package enters the build.
9
+ interface XRRigidTransformLike {
10
+ matrix: Float32Array;
11
+ }
12
+
13
+ interface XRViewLike {
14
+ projectionMatrix: Float32Array;
15
+ }
16
+
17
+ interface XRViewerPoseLike {
18
+ transform: XRRigidTransformLike;
19
+ views: XRViewLike[];
20
+ emulatedPosition?: boolean;
21
+ }
22
+
23
+ interface XRPlaneLike {
24
+ planeSpace: unknown;
25
+ }
26
+
27
+ interface XRAnchorLike {
28
+ anchorSpace: unknown;
29
+ }
30
+
31
+ interface XRPoseLike {
32
+ transform: XRRigidTransformLike;
33
+ }
34
+
35
+ export interface GossXRFrameLike {
36
+ getViewerPose(referenceSpace: unknown): XRViewerPoseLike | null;
37
+ getPose(space: unknown, referenceSpace: unknown): XRPoseLike | null;
38
+ detectedPlanes?: Set<XRPlaneLike>;
39
+ trackedAnchors?: Set<XRAnchorLike>;
40
+ }
41
+
42
+ export class GossWebXRWorldSource {
43
+ private planeIds = new Map<XRPlaneLike, number>();
44
+ private anchorIds = new Map<XRAnchorLike, number>();
45
+ private nextId = 1;
46
+
47
+ constructor(private readonly session: GossSession) {}
48
+
49
+ /// Call once per XR animation frame with the frame and the reference
50
+ /// space the page renders against.
51
+ onFrame(frame: GossXRFrameLike, referenceSpace: unknown, timestampUs: number): void {
52
+ const viewer = frame.getViewerPose(referenceSpace);
53
+ if (!viewer || viewer.views.length === 0) {
54
+ this.session.submitWorld({
55
+ trackingState: 1,
56
+ worldFromCamera: identity16,
57
+ projection: identity16,
58
+ timestampUs,
59
+ });
60
+ return;
61
+ }
62
+
63
+ const planes: GossWorldPlane[] = [];
64
+ if (frame.detectedPlanes) {
65
+ for (const plane of frame.detectedPlanes) {
66
+ const pose = frame.getPose(plane.planeSpace, referenceSpace);
67
+ if (!pose) continue;
68
+ planes.push({
69
+ id: this.idFor(this.planeIds, plane),
70
+ pose: pose.transform.matrix,
71
+ extentX: 0,
72
+ extentZ: 0,
73
+ classification: 0,
74
+ });
75
+ }
76
+ }
77
+ const anchors: GossWorldAnchorInput[] = [];
78
+ if (frame.trackedAnchors) {
79
+ for (const anchor of frame.trackedAnchors) {
80
+ const pose = frame.getPose(anchor.anchorSpace, referenceSpace);
81
+ if (!pose) continue;
82
+ anchors.push({ id: this.idFor(this.anchorIds, anchor), pose: pose.transform.matrix });
83
+ }
84
+ }
85
+
86
+ this.session.submitWorld(
87
+ {
88
+ trackingState: viewer.emulatedPosition ? 3 : 2,
89
+ worldFromCamera: viewer.transform.matrix,
90
+ projection: viewer.views[0].projectionMatrix,
91
+ timestampUs,
92
+ },
93
+ planes,
94
+ anchors,
95
+ );
96
+ }
97
+
98
+ private idFor<K>(map: Map<K, number>, key: K): number {
99
+ const existing = map.get(key);
100
+ if (existing !== undefined) return existing;
101
+ const id = this.nextId;
102
+ this.nextId += 1;
103
+ map.set(key, id);
104
+ return id;
105
+ }
106
+ }
107
+
108
+ const identity16 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];