@srsergio/taptapp-ar 1.0.78 → 1.0.80

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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/compiler/controller.d.ts +22 -6
  4. package/dist/compiler/controller.js +99 -26
  5. package/dist/compiler/controller.worker.js +2 -1
  6. package/dist/compiler/estimation/refine-estimate.d.ts +2 -1
  7. package/dist/compiler/estimation/refine-estimate.js +18 -5
  8. package/dist/compiler/features/feature-base.d.ts +1 -1
  9. package/dist/compiler/features/feature-manager.d.ts +1 -1
  10. package/dist/compiler/features/feature-manager.js +2 -2
  11. package/dist/compiler/features/one-euro-filter-feature.d.ts +1 -1
  12. package/dist/compiler/features/one-euro-filter-feature.js +8 -1
  13. package/dist/compiler/matching/matching.js +1 -1
  14. package/dist/compiler/offline-compiler.d.ts +92 -8
  15. package/dist/compiler/offline-compiler.js +3 -86
  16. package/dist/compiler/simple-ar.d.ts +12 -0
  17. package/dist/compiler/simple-ar.js +46 -19
  18. package/dist/compiler/tracker/tracker.d.ts +10 -0
  19. package/dist/compiler/tracker/tracker.js +6 -4
  20. package/dist/react/TaptappAR.js +29 -2
  21. package/dist/react/use-ar.d.ts +7 -0
  22. package/dist/react/use-ar.js +16 -1
  23. package/package.json +24 -2
  24. package/src/compiler/controller.ts +112 -26
  25. package/src/compiler/controller.worker.js +2 -1
  26. package/src/compiler/estimation/refine-estimate.js +20 -3
  27. package/src/compiler/features/feature-base.ts +1 -1
  28. package/src/compiler/features/feature-manager.ts +2 -2
  29. package/src/compiler/features/one-euro-filter-feature.ts +11 -1
  30. package/src/compiler/matching/matching.js +1 -1
  31. package/src/compiler/offline-compiler.ts +3 -94
  32. package/src/compiler/simple-ar.ts +62 -20
  33. package/src/compiler/tracker/tracker.js +7 -4
  34. package/src/react/TaptappAR.tsx +41 -1
  35. package/src/react/use-ar.ts +24 -1
@@ -1,12 +1,11 @@
1
1
  import { Tracker } from "./tracker/tracker.js";
2
- import { CropDetector } from "./detector/crop-detector.js";
3
2
  import { OfflineCompiler as Compiler } from "./offline-compiler.js";
4
3
  import { InputLoader } from "./input-loader.js";
5
4
  import { FeatureManager } from "./features/feature-manager.js";
6
5
  import { OneEuroFilterFeature } from "./features/one-euro-filter-feature.js";
7
6
  import { TemporalFilterFeature } from "./features/temporal-filter-feature.js";
8
7
  import { AutoRotationFeature } from "./features/auto-rotation-feature.js";
9
- import { CropDetectionFeature } from "./features/crop-detection-feature.js";
8
+ import { DetectorLite } from "./detector/detector-lite.js";
10
9
 
11
10
  let ControllerWorker: any;
12
11
 
@@ -26,7 +25,10 @@ ControllerWorker = await getControllerWorker();
26
25
  const DEFAULT_FILTER_CUTOFF = 0.5;
27
26
  const DEFAULT_FILTER_BETA = 0.1;
28
27
  const DEFAULT_WARMUP_TOLERANCE = 2; // Instant detection
29
- const DEFAULT_MISS_TOLERANCE = 5; // More grace when partially hidden
28
+ const DEFAULT_MISS_TOLERANCE = 1; // Immediate response to tracking loss
29
+ const WORKER_TIMEOUT_MS = 1000; // Prevent worker hangs from killing the loop
30
+
31
+ let loopIdCounter = 0;
30
32
 
31
33
  export interface ControllerOptions {
32
34
  inputWidth: number;
@@ -62,6 +64,7 @@ class Controller {
62
64
  mainThreadMatcher: any;
63
65
  mainThreadEstimator: any;
64
66
  featureManager: FeatureManager;
67
+ fullDetector: DetectorLite | null = null;
65
68
 
66
69
  constructor({
67
70
  inputWidth,
@@ -89,7 +92,7 @@ class Controller {
89
92
  missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance
90
93
  ));
91
94
  this.featureManager.addFeature(new AutoRotationFeature());
92
- this.featureManager.addFeature(new CropDetectionFeature());
95
+ // User wants "sin recortes", so we don't add CropDetectionFeature
93
96
 
94
97
  this.inputLoader = new InputLoader(this.inputWidth, this.inputHeight);
95
98
  this.onUpdate = onUpdate;
@@ -97,6 +100,9 @@ class Controller {
97
100
  this.worker = worker;
98
101
  if (this.worker) this._setupWorkerListener();
99
102
 
103
+ // Moonshot: Full frame detector for better sensitivity
104
+ this.fullDetector = new DetectorLite(this.inputWidth, this.inputHeight, { useLSH: true });
105
+
100
106
  this.featureManager.init({
101
107
  inputWidth: this.inputWidth,
102
108
  inputHeight: this.inputHeight,
@@ -219,8 +225,7 @@ class Controller {
219
225
 
220
226
  dummyRun(input: any) {
221
227
  const inputData = this.inputLoader.loadInput(input);
222
- const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
223
- cropFeature?.detect(inputData, false);
228
+ this.fullDetector?.detect(inputData);
224
229
  this.tracker!.dummyRun(inputData);
225
230
  }
226
231
 
@@ -242,8 +247,7 @@ class Controller {
242
247
  }
243
248
 
244
249
  async _detectAndMatch(inputData: any, targetIndexes: number[]) {
245
- const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
246
- const { featurePoints } = cropFeature!.detect(inputData, true);
250
+ const { featurePoints } = this.fullDetector!.detect(inputData);
247
251
  const { targetIndex: matchedTargetIndex, modelViewTransform } = await this._workerMatch(
248
252
  featurePoints,
249
253
  targetIndexes,
@@ -252,22 +256,73 @@ class Controller {
252
256
  }
253
257
 
254
258
  async _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number) {
255
- const { worldCoords, screenCoords } = this.tracker!.track(
259
+ const { worldCoords, screenCoords, reliabilities, indices = [], octaveIndex = 0 } = this.tracker!.track(
256
260
  inputData,
257
261
  lastModelViewTransform,
258
262
  targetIndex,
259
263
  );
260
- if (worldCoords.length < 8) return null; // Resynced with Matcher (8 points) to allow initial detection
264
+
265
+ const state = this.trackingStates[targetIndex];
266
+ if (!state.pointStabilities) state.pointStabilities = [];
267
+ if (!state.pointStabilities[octaveIndex]) {
268
+ // Initialize stabilities for this octave if not exists
269
+ const numPoints = (this.tracker as any).prebuiltData[targetIndex][octaveIndex].px.length;
270
+ state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0.5); // Start at 0.5
271
+ }
272
+
273
+ const stabilities = state.pointStabilities[octaveIndex];
274
+ const currentStabilities: number[] = [];
275
+
276
+ // Update all points in this octave
277
+ for (let i = 0; i < stabilities.length; i++) {
278
+ const isTracked = indices.includes(i);
279
+ if (isTracked) {
280
+ stabilities[i] = Math.min(1.0, stabilities[i] + 0.35); // Fast recovery (approx 3 frames)
281
+ } else {
282
+ stabilities[i] = Math.max(0.0, stabilities[i] - 0.12); // Slightly more forgiving loss
283
+ }
284
+ }
285
+
286
+ // Collect stabilities and FILTER OUT excessive flickerers (Dead Zone)
287
+ const filteredWorldCoords = [];
288
+ const filteredScreenCoords = [];
289
+ const filteredStabilities = [];
290
+
291
+ for (let i = 0; i < indices.length; i++) {
292
+ const s = stabilities[indices[i]];
293
+ if (s > 0.3) { // Hard Cutoff: points with <30% stability are ignored
294
+ filteredWorldCoords.push(worldCoords[i]);
295
+ filteredScreenCoords.push(screenCoords[i]);
296
+ filteredStabilities.push(s);
297
+ }
298
+ }
299
+
300
+ // STRICT QUALITY CHECK: Prevent "sticky" tracking on background noise.
301
+ // We require a minimum number of high-confidence AND STABLE points.
302
+ const stableAndReliable = reliabilities.filter((r: number, idx: number) => r > 0.75 && stabilities[indices[idx]] > 0.5).length;
303
+
304
+ if (stableAndReliable < 6 || filteredWorldCoords.length < 8) {
305
+ return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [] };
306
+ }
307
+
261
308
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
262
- worldCoords,
263
- screenCoords,
309
+ worldCoords: filteredWorldCoords,
310
+ screenCoords: filteredScreenCoords,
311
+ stabilities: filteredStabilities
264
312
  });
265
- return modelViewTransform;
313
+
314
+ return {
315
+ modelViewTransform,
316
+ screenCoords: filteredScreenCoords,
317
+ reliabilities: reliabilities.filter((_, idx) => stabilities[indices[idx]] > 0.3),
318
+ stabilities: filteredStabilities
319
+ };
266
320
  }
267
321
 
268
322
  processVideo(input: any) {
269
323
  if (this.processingVideo) return;
270
324
  this.processingVideo = true;
325
+ const currentLoopId = ++loopIdCounter; // Added for ghost loop prevention
271
326
 
272
327
  this.trackingStates = [];
273
328
  for (let i = 0; i < (this.markerDimensions?.length || 0); i++) {
@@ -282,7 +337,7 @@ class Controller {
282
337
 
283
338
  const startProcessing = async () => {
284
339
  while (true) {
285
- if (!this.processingVideo) break;
340
+ if (!this.processingVideo || currentLoopId !== loopIdCounter) break;
286
341
 
287
342
  const inputData = this.inputLoader.loadInput(input);
288
343
  const nTracking = this.trackingStates.reduce((acc, s) => acc + (!!s.isTracking ? 1 : 0), 0);
@@ -309,15 +364,21 @@ class Controller {
309
364
  const trackingState = this.trackingStates[i];
310
365
 
311
366
  if (trackingState.isTracking) {
312
- let modelViewTransform = await this._trackAndUpdate(
367
+ const result = await this._trackAndUpdate(
313
368
  inputData,
314
369
  trackingState.currentModelViewTransform,
315
370
  i,
316
371
  );
317
- if (modelViewTransform === null) {
372
+ if (result === null || result.modelViewTransform === null) {
318
373
  trackingState.isTracking = false;
374
+ trackingState.screenCoords = [];
375
+ trackingState.reliabilities = [];
376
+ trackingState.stabilities = [];
319
377
  } else {
320
- trackingState.currentModelViewTransform = modelViewTransform;
378
+ trackingState.currentModelViewTransform = result.modelViewTransform;
379
+ trackingState.screenCoords = result.screenCoords;
380
+ trackingState.reliabilities = result.reliabilities;
381
+ trackingState.stabilities = result.stabilities;
321
382
  }
322
383
  }
323
384
 
@@ -332,7 +393,14 @@ class Controller {
332
393
 
333
394
  if (trackingState.showing) {
334
395
  const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
335
- const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix);
396
+
397
+ // Calculate confidence score based on point stability
398
+ const stabilities = trackingState.stabilities || [];
399
+ const avgStability = stabilities.length > 0
400
+ ? stabilities.reduce((a: number, b: number) => a + b, 0) / stabilities.length
401
+ : 0;
402
+
403
+ const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
336
404
  trackingState.trackingMatrix = filteredMatrix;
337
405
 
338
406
  let finalMatrix = [...filteredMatrix];
@@ -349,7 +417,10 @@ class Controller {
349
417
  type: "updateMatrix",
350
418
  targetIndex: i,
351
419
  worldMatrix: finalMatrix,
352
- modelViewTransform: trackingState.currentModelViewTransform
420
+ modelViewTransform: trackingState.currentModelViewTransform,
421
+ screenCoords: trackingState.screenCoords,
422
+ reliabilities: trackingState.reliabilities,
423
+ stabilities: trackingState.stabilities
353
424
  });
354
425
  }
355
426
  }
@@ -372,9 +443,8 @@ class Controller {
372
443
 
373
444
  async detect(input: any) {
374
445
  const inputData = this.inputLoader.loadInput(input);
375
- const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
376
- const { featurePoints, debugExtra } = cropFeature!.detect(inputData, false);
377
- return { featurePoints, debugExtra };
446
+ const { featurePoints } = this.fullDetector!.detect(inputData);
447
+ return { featurePoints, debugExtra: {} };
378
448
  }
379
449
 
380
450
  async match(featurePoints: any, targetIndex: number) {
@@ -397,11 +467,18 @@ class Controller {
397
467
  _workerMatch(featurePoints: any, targetIndexes: number[]): Promise<any> {
398
468
  return new Promise((resolve) => {
399
469
  if (!this.worker) {
400
- this._matchOnMainThread(featurePoints, targetIndexes).then(resolve);
470
+ this._matchOnMainThread(featurePoints, targetIndexes).then(resolve).catch(() => resolve({ targetIndex: -1 }));
401
471
  return;
402
472
  }
403
473
 
474
+ const timeout = setTimeout(() => {
475
+ this.workerMatchDone = null;
476
+ resolve({ targetIndex: -1 });
477
+ }, WORKER_TIMEOUT_MS);
478
+
404
479
  this.workerMatchDone = (data: any) => {
480
+ clearTimeout(timeout);
481
+ this.workerMatchDone = null;
405
482
  resolve({
406
483
  targetIndex: data.targetIndex,
407
484
  modelViewTransform: data.modelViewTransform,
@@ -460,19 +537,27 @@ class Controller {
460
537
  _workerTrackUpdate(modelViewTransform: number[][], trackingFeatures: any): Promise<any> {
461
538
  return new Promise((resolve) => {
462
539
  if (!this.worker) {
463
- this._trackUpdateOnMainThread(modelViewTransform, trackingFeatures).then(resolve);
540
+ this._trackUpdateOnMainThread(modelViewTransform, trackingFeatures).then(resolve).catch(() => resolve(null));
464
541
  return;
465
542
  }
466
543
 
544
+ const timeout = setTimeout(() => {
545
+ this.workerTrackDone = null;
546
+ resolve(null);
547
+ }, WORKER_TIMEOUT_MS);
548
+
467
549
  this.workerTrackDone = (data: any) => {
550
+ clearTimeout(timeout);
551
+ this.workerTrackDone = null;
468
552
  resolve(data.modelViewTransform);
469
553
  };
470
- const { worldCoords, screenCoords } = trackingFeatures;
554
+ const { worldCoords, screenCoords, stabilities } = trackingFeatures;
471
555
  this.worker.postMessage({
472
556
  type: "trackUpdate",
473
557
  modelViewTransform,
474
558
  worldCoords,
475
559
  screenCoords,
560
+ stabilities
476
561
  });
477
562
  });
478
563
  }
@@ -483,11 +568,12 @@ class Controller {
483
568
  this.mainThreadEstimator = new Estimator(this.projectionTransform);
484
569
  }
485
570
 
486
- const { worldCoords, screenCoords } = trackingFeatures;
571
+ const { worldCoords, screenCoords, stabilities } = trackingFeatures;
487
572
  return this.mainThreadEstimator.refineEstimate({
488
573
  initialModelViewTransform: modelViewTransform,
489
574
  worldCoords,
490
575
  screenCoords,
576
+ stabilities
491
577
  });
492
578
  }
493
579
 
@@ -53,11 +53,12 @@ onmessage = (msg) => {
53
53
  break;
54
54
 
55
55
  case "trackUpdate":
56
- const { modelViewTransform, worldCoords, screenCoords } = data;
56
+ const { modelViewTransform, worldCoords, screenCoords, stabilities } = data;
57
57
  const finalModelViewTransform = estimator.refineEstimate({
58
58
  initialModelViewTransform: modelViewTransform,
59
59
  worldCoords,
60
60
  screenCoords,
61
+ stabilities, // Stability-based weights
61
62
  });
62
63
  postMessage({
63
64
  type: "trackUpdateDone",
@@ -21,6 +21,7 @@ const refineEstimate = ({
21
21
  projectionTransform,
22
22
  worldCoords,
23
23
  screenCoords,
24
+ stabilities, // Stability-based weighting
24
25
  }) => {
25
26
  // Question: shall we normlize the screen coords as well?
26
27
  // Question: do we need to normlize the scale as well, i.e. make coords from -1 to 1
@@ -74,6 +75,7 @@ const refineEstimate = ({
74
75
  projectionTransform,
75
76
  worldCoords: normalizedWorldCoords,
76
77
  screenCoords,
78
+ stabilities, // Pass weights to ICP
77
79
  inlierProb: inlierProbs[i],
78
80
  });
79
81
 
@@ -113,6 +115,7 @@ const _doICP = ({
113
115
  projectionTransform,
114
116
  worldCoords,
115
117
  screenCoords,
118
+ stabilities,
116
119
  inlierProb,
117
120
  }) => {
118
121
  const isRobustMode = inlierProb < 1;
@@ -196,7 +199,13 @@ const _doICP = ({
196
199
  });
197
200
 
198
201
  if (isRobustMode) {
199
- const W = (1.0 - E[n] / K2) * (1.0 - E[n] / K2);
202
+ const robustW = (1.0 - E[n] / K2) * (1.0 - E[n] / K2);
203
+
204
+ // Log-weighted stability: suppresses vibrators aggressively but allows recovery
205
+ const s = stabilities ? stabilities[n] : 1.0;
206
+ const stabilityW = s * Math.log10(9 * s + 1);
207
+
208
+ const W = robustW * stabilityW;
200
209
 
201
210
  for (let j = 0; j < 2; j++) {
202
211
  for (let i = 0; i < 6; i++) {
@@ -206,8 +215,16 @@ const _doICP = ({
206
215
  dU.push([dxs[n] * W]);
207
216
  dU.push([dys[n] * W]);
208
217
  } else {
209
- dU.push([dxs[n]]);
210
- dU.push([dys[n]]);
218
+ const s = stabilities ? stabilities[n] : 1.0;
219
+ const W = s * Math.log10(9 * s + 1);
220
+
221
+ for (let j = 0; j < 2; j++) {
222
+ for (let i = 0; i < 6; i++) {
223
+ J_U_S[j][i] *= W;
224
+ }
225
+ }
226
+ dU.push([dxs[n] * W]);
227
+ dU.push([dys[n] * W]);
211
228
  }
212
229
 
213
230
  for (let i = 0; i < J_U_S.length; i++) {
@@ -39,7 +39,7 @@ export interface ControllerFeature extends Feature {
39
39
  /**
40
40
  * Hook to filter or modify the final world matrix
41
41
  */
42
- filterWorldMatrix?(targetIndex: number, worldMatrix: number[]): number[];
42
+ filterWorldMatrix?(targetIndex: number, worldMatrix: number[], context?: any): number[];
43
43
 
44
44
  /**
45
45
  * Hook to decide if a target should be shown
@@ -27,11 +27,11 @@ export class FeatureManager {
27
27
  }
28
28
  }
29
29
 
30
- applyWorldMatrixFilters(targetIndex: number, worldMatrix: number[]): number[] {
30
+ applyWorldMatrixFilters(targetIndex: number, worldMatrix: number[], context?: any): number[] {
31
31
  let result = worldMatrix;
32
32
  for (const feature of this.features) {
33
33
  if (feature.enabled && feature.filterWorldMatrix) {
34
- result = feature.filterWorldMatrix(targetIndex, result);
34
+ result = feature.filterWorldMatrix(targetIndex, result, context);
35
35
  }
36
36
  }
37
37
  return result;
@@ -30,9 +30,19 @@ export class OneEuroFilterFeature implements ControllerFeature {
30
30
  return this.filters[targetIndex];
31
31
  }
32
32
 
33
- filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[] {
33
+ filterWorldMatrix(targetIndex: number, worldMatrix: number[], context?: any): number[] {
34
34
  if (!this.enabled) return worldMatrix;
35
+
35
36
  const filter = this.getFilter(targetIndex);
37
+ const stability = context?.stability ?? 1.0;
38
+
39
+ // Dynamic Cutoff: If points are very stable (1.0), use higher cutoff (less responsiveness loss).
40
+ // If points are unstable (0.3), use much lower cutoff (heavy smoothing).
41
+ // We use a squared curve for even more aggressive suppression of jitter on unstable points.
42
+ const dynamicMinCutOff = this.minCutOff * (0.05 + Math.pow(stability, 2) * 0.95);
43
+ filter.minCutOff = dynamicMinCutOff;
44
+ filter.beta = this.beta;
45
+
36
46
  return filter.filter(Date.now(), worldMatrix);
37
47
  }
38
48
 
@@ -235,7 +235,7 @@ const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop
235
235
  if (dist !== minD) {
236
236
  queue.push({ node: childrenOrIndices[i], d: dist });
237
237
  } else {
238
- _query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop });
238
+ _query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1 });
239
239
  }
240
240
  }
241
241
 
@@ -11,7 +11,6 @@ import { extractTrackingFeatures } from "./tracker/extract-utils.js";
11
11
  import { DetectorLite } from "./detector/detector-lite.js";
12
12
  import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
13
13
  import * as msgpack from "@msgpack/msgpack";
14
- import { WorkerPool } from "./utils/worker-pool.js";
15
14
 
16
15
  // Detect environment
17
16
  const isNode = typeof process !== "undefined" &&
@@ -22,40 +21,9 @@ const CURRENT_VERSION = 7; // Protocol v7: Moonshot - 4-bit Packed Tracking Data
22
21
 
23
22
  export class OfflineCompiler {
24
23
  data: any = null;
25
- workerPool: WorkerPool | null = null;
26
24
 
27
25
  constructor() {
28
- // Workers only in Node.js
29
- if (!isNode) {
30
- console.log("🌐 OfflineCompiler: Browser mode (no workers)");
31
- }
32
- }
33
-
34
- async _initNodeWorkers() {
35
- try {
36
- const pathModule = "path";
37
- const urlModule = "url";
38
- const osModule = "os";
39
- const workerThreadsModule = "node:worker_threads";
40
-
41
- const [path, url, os, { Worker }] = await Promise.all([
42
- import(/* @vite-ignore */ pathModule),
43
- import(/* @vite-ignore */ urlModule),
44
- import(/* @vite-ignore */ osModule),
45
- import(/* @vite-ignore */ workerThreadsModule)
46
- ]);
47
-
48
- const __filename = url.fileURLToPath(import.meta.url);
49
- const __dirname = path.dirname(__filename);
50
- const workerPath = path.join(__dirname, "node-worker.js");
51
-
52
- // Limit workers to avoid freezing system
53
- const numWorkers = Math.min(os.cpus().length, 4);
54
-
55
- this.workerPool = new WorkerPool(workerPath, numWorkers, Worker);
56
- } catch (e) {
57
- console.log("⚡ OfflineCompiler: Running without workers (initialization failed)", e);
58
- }
26
+ console.log("⚡ OfflineCompiler: Main thread mode (no workers)");
59
27
  }
60
28
 
61
29
  async compileImageTargets(images: any[], progressCallback: (p: number) => void) {
@@ -108,25 +76,7 @@ export class OfflineCompiler {
108
76
  }
109
77
 
110
78
  async _compileTarget(targetImages: any[], progressCallback: (p: number) => void) {
111
- if (isNode) await this._initNodeWorkers();
112
-
113
- if (this.workerPool) {
114
- const progressMap = new Float32Array(targetImages.length);
115
- const wrappedPromises = targetImages.map((targetImage: any, index: number) => {
116
- return this.workerPool!.runTask({
117
- type: 'compile-all', // 🚀 MOONSHOT: Combined task
118
- targetImage,
119
- onProgress: (p: number) => {
120
- progressMap[index] = p;
121
- const sum = progressMap.reduce((a, b) => a + b, 0);
122
- progressCallback(sum / targetImages.length);
123
- }
124
- });
125
- });
126
- return Promise.all(wrappedPromises);
127
- }
128
-
129
- // Fallback or non-worker implementation: run match and track sequentially
79
+ // Run match and track sequentially to match browser behavior exactly
130
80
  const matchingResults = await this._compileMatch(targetImages, (p) => progressCallback(p * 0.5));
131
81
  const trackingResults = await this._compileTrack(targetImages, (p) => progressCallback(50 + p * 0.5));
132
82
 
@@ -140,27 +90,6 @@ export class OfflineCompiler {
140
90
  const percentPerImage = 100 / targetImages.length;
141
91
  let currentPercent = 0;
142
92
 
143
- if (isNode) await this._initNodeWorkers();
144
- if (this.workerPool) {
145
- const progressMap = new Float32Array(targetImages.length);
146
-
147
- const wrappedPromises = targetImages.map((targetImage: any, index: number) => {
148
- return this.workerPool!.runTask({
149
- type: 'match',
150
- targetImage,
151
- percentPerImage,
152
- basePercent: 0,
153
- onProgress: (p: number) => {
154
- progressMap[index] = p;
155
- const sum = progressMap.reduce((a, b) => a + b, 0);
156
- progressCallback(sum);
157
- }
158
- });
159
- });
160
-
161
- return Promise.all(wrappedPromises);
162
- }
163
-
164
93
  const results = [];
165
94
  for (let i = 0; i < targetImages.length; i++) {
166
95
  const targetImage = targetImages[i];
@@ -201,24 +130,6 @@ export class OfflineCompiler {
201
130
  const percentPerImage = 100 / targetImages.length;
202
131
  let currentPercent = 0;
203
132
 
204
- if (this.workerPool) {
205
- const progressMap = new Float32Array(targetImages.length);
206
- const wrappedPromises = targetImages.map((targetImage: any, index: number) => {
207
- return this.workerPool!.runTask({
208
- type: 'compile',
209
- targetImage,
210
- percentPerImage,
211
- basePercent: 0,
212
- onProgress: (p: number) => {
213
- progressMap[index] = p;
214
- const sum = progressMap.reduce((a, b) => a + b, 0);
215
- progressCallback(sum);
216
- }
217
- });
218
- });
219
- return Promise.all(wrappedPromises);
220
- }
221
-
222
133
  const results = [];
223
134
  for (let i = 0; i < targetImages.length; i++) {
224
135
  const targetImage = targetImages[i];
@@ -480,9 +391,7 @@ export class OfflineCompiler {
480
391
  }
481
392
 
482
393
  async destroy() {
483
- if (this.workerPool) {
484
- await this.workerPool.destroy();
485
- }
394
+ // No workers to destroy
486
395
  }
487
396
 
488
397
 
@@ -13,7 +13,13 @@ export interface SimpleAROptions {
13
13
  scale?: number;
14
14
  onFound?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
15
15
  onLost?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
16
- onUpdate?: ((data: { targetIndex: number, worldMatrix: number[] }) => void) | null;
16
+ onUpdate?: ((data: {
17
+ targetIndex: number,
18
+ worldMatrix: number[],
19
+ screenCoords?: { x: number, y: number }[],
20
+ reliabilities?: number[],
21
+ stabilities?: number[]
22
+ }) => void) | null;
17
23
  cameraConfig?: MediaStreamConstraints['video'];
18
24
  debug?: boolean;
19
25
  }
@@ -25,7 +31,13 @@ class SimpleAR {
25
31
  scaleMultiplier: number;
26
32
  onFound: ((data: { targetIndex: number }) => void | Promise<void>) | null;
27
33
  onLost: ((data: { targetIndex: number }) => void | Promise<void>) | null;
28
- onUpdateCallback: ((data: { targetIndex: number, worldMatrix: number[] }) => void) | null;
34
+ onUpdateCallback: ((data: {
35
+ targetIndex: number,
36
+ worldMatrix: number[],
37
+ screenCoords?: { x: number, y: number }[],
38
+ reliabilities?: number[],
39
+ stabilities?: number[]
40
+ }) => void) | null;
29
41
  cameraConfig: MediaStreamConstraints['video'];
30
42
  debug: boolean;
31
43
 
@@ -151,7 +163,7 @@ class SimpleAR {
151
163
  if (this.debug) this._updateDebugPanel(this.isTracking);
152
164
  }
153
165
 
154
- const { targetIndex, worldMatrix, modelViewTransform } = data;
166
+ const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
155
167
 
156
168
  if (worldMatrix) {
157
169
  if (!this.isTracking) {
@@ -162,31 +174,61 @@ class SimpleAR {
162
174
 
163
175
  this.lastMatrix = worldMatrix;
164
176
 
165
- if (!this.filters[targetIndex]) {
166
- this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 0.8, beta: 0.2 });
177
+ // We use the matrix from the controller directly (it's already filtered there)
178
+ this._positionOverlay(modelViewTransform, targetIndex);
179
+
180
+ // Project points to screen coordinates
181
+ let projectedPoints = [];
182
+ if (screenCoords && screenCoords.length > 0) {
183
+ const containerRect = this.container.getBoundingClientRect();
184
+ const videoW = this.video!.videoWidth;
185
+ const videoH = this.video!.videoHeight;
186
+ const isPortrait = containerRect.height > containerRect.width;
187
+ const isVideoLandscape = videoW > videoH;
188
+ const needsRotation = isPortrait && isVideoLandscape;
189
+ const proj = this.controller!.projectionTransform;
190
+
191
+ const vW = needsRotation ? videoH : videoW;
192
+ const vH = needsRotation ? videoW : videoH;
193
+ const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
194
+ const dW = vW * pScale;
195
+ const dH = vH * pScale;
196
+ const oX = (containerRect.width - dW) / 2;
197
+ const oY = (containerRect.height - dH) / 2;
198
+
199
+ projectedPoints = screenCoords.map((p: any) => {
200
+ let sx, sy;
201
+ if (needsRotation) {
202
+ sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
203
+ sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
204
+ } else {
205
+ sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
206
+ sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
207
+ }
208
+ return { x: sx, y: sy };
209
+ });
167
210
  }
168
211
 
169
- const flatMVT = [
170
- modelViewTransform[0][0], modelViewTransform[0][1], modelViewTransform[0][2], modelViewTransform[0][3],
171
- modelViewTransform[1][0], modelViewTransform[1][1], modelViewTransform[1][2], modelViewTransform[1][3],
172
- modelViewTransform[2][0], modelViewTransform[2][1], modelViewTransform[2][2], modelViewTransform[2][3]
173
- ];
174
- const smoothedFlat = this.filters[targetIndex].filter(Date.now(), flatMVT);
175
- const smoothedMVT = [
176
- [smoothedFlat[0], smoothedFlat[1], smoothedFlat[2], smoothedFlat[3]],
177
- [smoothedFlat[4], smoothedFlat[5], smoothedFlat[6], smoothedFlat[7]],
178
- [smoothedFlat[8], smoothedFlat[9], smoothedFlat[10], smoothedFlat[11]]
179
- ];
180
-
181
- this._positionOverlay(smoothedMVT, targetIndex);
182
- this.onUpdateCallback && this.onUpdateCallback({ targetIndex, worldMatrix });
212
+ this.onUpdateCallback && this.onUpdateCallback({
213
+ targetIndex,
214
+ worldMatrix,
215
+ screenCoords: projectedPoints,
216
+ reliabilities,
217
+ stabilities
218
+ });
183
219
 
184
220
  } else {
185
221
  if (this.isTracking) {
186
222
  this.isTracking = false;
187
- if (this.filters[targetIndex]) this.filters[targetIndex].reset();
188
223
  this.overlay && (this.overlay.style.opacity = '0');
189
224
  this.onLost && this.onLost({ targetIndex });
225
+ this.onUpdateCallback && this.onUpdateCallback({
226
+ targetIndex,
227
+ worldMatrix: null as any,
228
+ screenCoords: [],
229
+ reliabilities: [],
230
+ stabilities: []
231
+ });
190
232
  }
191
233
  }
192
234
  }