@srsergio/taptapp-ar 1.0.37 → 1.0.40

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.
@@ -65,7 +65,7 @@ export class Controller {
65
65
  dummyRun(input: any): void;
66
66
  getProjectionMatrix(): number[];
67
67
  getRotatedZ90Matrix(m: any): any[];
68
- getWorldMatrix(modelViewTransform: any, targetIndex: any): any[];
68
+ getWorldMatrix(modelViewTransform: any, targetIndex: any): any[] | null;
69
69
  _detectAndMatch(inputData: any, targetIndexes: any): Promise<{
70
70
  targetIndex: any;
71
71
  modelViewTransform: any;
@@ -73,7 +73,7 @@ export class Controller {
73
73
  _trackAndUpdate(inputData: any, lastModelViewTransform: any, targetIndex: any): Promise<{
74
74
  modelViewTransform: any;
75
75
  inliers: number;
76
- octaveIndex: any;
76
+ octaveIndex: number;
77
77
  } | null>;
78
78
  processVideo(input: any): void;
79
79
  stopProcessVideo(): void;
@@ -102,6 +102,7 @@ export class Controller {
102
102
  x: number;
103
103
  y: number;
104
104
  }[];
105
+ octaveIndex: number;
105
106
  debugExtra: {};
106
107
  }>;
107
108
  trackUpdate(modelViewTransform: any, trackFeatures: any): Promise<any>;
@@ -128,7 +129,7 @@ export class Controller {
128
129
  _workerTrackUpdate(modelViewTransform: any, trackingFeatures: any): Promise<any>;
129
130
  workerTrackDone: ((data: any) => void) | undefined;
130
131
  _trackUpdateOnMainThread(modelViewTransform: any, trackingFeatures: any): Promise<never[][] | null>;
131
- _glModelViewMatrix(modelViewTransform: any, targetIndex: any): any[];
132
+ _glModelViewMatrix(modelViewTransform: any, targetIndex: any): any[] | null;
132
133
  _glProjectionMatrix({ projectionTransform, width, height, near, far }: {
133
134
  projectionTransform: any;
134
135
  width: any;
@@ -181,17 +181,19 @@ class Controller {
181
181
  return { targetIndex: matchedTargetIndex, modelViewTransform };
182
182
  }
183
183
  async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
184
- const { worldCoords, screenCoords, debugExtra } = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
185
- if (worldCoords.length < 6)
184
+ if (!lastModelViewTransform)
185
+ return null;
186
+ const result = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
187
+ if (result.worldCoords.length < 6)
186
188
  return null; // Umbral de puntos mínimos para mantener el seguimiento
187
189
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
188
- worldCoords,
189
- screenCoords,
190
+ worldCoords: result.worldCoords,
191
+ screenCoords: result.screenCoords,
190
192
  });
191
193
  return {
192
194
  modelViewTransform,
193
- inliers: worldCoords.length,
194
- octaveIndex: debugExtra.octaveIndex
195
+ inliers: result.worldCoords.length,
196
+ octaveIndex: result.octaveIndex
195
197
  };
196
198
  }
197
199
  processVideo(input) {
@@ -219,18 +221,22 @@ class Controller {
219
221
  return acc + (!!s.isTracking ? 1 : 0);
220
222
  }, 0);
221
223
  // detect and match only if less then maxTrack
224
+ // BUG FIX: Only match if we are NOT in a "ghosting" period for a target
225
+ // to prevent the "found but immediately lost" loop that keeps opacity at 1.
222
226
  if (nTracking < this.maxTrack) {
223
227
  const matchingIndexes = [];
224
228
  for (let i = 0; i < this.trackingStates.length; i++) {
225
229
  const trackingState = this.trackingStates[i];
226
230
  if (trackingState.isTracking === true)
227
231
  continue;
232
+ if (trackingState.showing === true)
233
+ continue; // Don't try to re-detect if we are still buffers-showing the last position
228
234
  if (this.interestedTargetIndex !== -1 && this.interestedTargetIndex !== i)
229
235
  continue;
230
236
  matchingIndexes.push(i);
231
237
  }
232
238
  const { targetIndex: matchedTargetIndex, modelViewTransform } = await this._detectAndMatch(inputData, matchingIndexes);
233
- if (matchedTargetIndex !== -1) {
239
+ if (matchedTargetIndex !== -1 && modelViewTransform) {
234
240
  this.trackingStates[matchedTargetIndex].isTracking = true;
235
241
  this.trackingStates[matchedTargetIndex].currentModelViewTransform = modelViewTransform;
236
242
  }
@@ -239,72 +245,79 @@ class Controller {
239
245
  for (let i = 0; i < this.trackingStates.length; i++) {
240
246
  const trackingState = this.trackingStates[i];
241
247
  if (trackingState.isTracking) {
242
- let result = await this._trackAndUpdate(inputData, trackingState.currentModelViewTransform, i);
243
- if (result === null) {
248
+ if (!trackingState.currentModelViewTransform) {
244
249
  trackingState.isTracking = false;
245
250
  trackingState.stabilityCount = 0;
246
251
  }
247
252
  else {
248
- trackingState.currentModelViewTransform = result.modelViewTransform;
249
- // --- LIVE MODEL ADAPTATION LOGIC ---
250
- // Si el tracking es muy sólido (muchos inliers) y estable, refinamos el modelo
251
- if (result.inliers > 25) {
252
- trackingState.stabilityCount++;
253
- if (trackingState.stabilityCount > 20) { // 20 frames de estabilidad absoluta
254
- this.tracker.applyLiveFeedback(i, result.octaveIndex, 0.1); // 10% de mezcla real
255
- if (this.debugMode)
256
- console.log(`✨ Live Reification: Target ${i} (Octave ${result.octaveIndex}) updated with real-world textures.`);
257
- trackingState.stabilityCount = 0; // Reset para la siguiente actualización
258
- }
253
+ let result = await this._trackAndUpdate(inputData, trackingState.currentModelViewTransform, i);
254
+ if (result === null) {
255
+ trackingState.isTracking = false;
256
+ trackingState.stabilityCount = 0;
259
257
  }
260
258
  else {
261
- trackingState.stabilityCount = Math.max(0, trackingState.stabilityCount - 1);
259
+ trackingState.currentModelViewTransform = result.modelViewTransform;
260
+ // --- LIVE MODEL ADAPTATION LOGIC ---
261
+ // Si el tracking es muy sólido (muchos inliers) y estable, refinamos el modelo
262
+ // Requisito: > 35 inliers (muy exigente) para evitar polución por ruido
263
+ if (result.inliers > 35) {
264
+ trackingState.stabilityCount++;
265
+ if (trackingState.stabilityCount > 30) { // 30 frames (~1s) de estabilidad absoluta
266
+ this.tracker.applyLiveFeedback(i, result.octaveIndex, 0.05); // Menor alpha (5%) para ser más conservador
267
+ if (this.debugMode)
268
+ console.log(`✨ Live Reification: Target ${i} (Octave ${result.octaveIndex}) updated.`);
269
+ trackingState.stabilityCount = 0;
270
+ }
271
+ }
272
+ else {
273
+ trackingState.stabilityCount = Math.max(0, trackingState.stabilityCount - 1);
274
+ }
275
+ // -----------------------------------
262
276
  }
263
- // -----------------------------------
264
277
  }
265
- }
266
- // if not showing, then show it once it reaches warmup number of frames
267
- if (!trackingState.showing) {
268
- if (trackingState.isTracking) {
269
- trackingState.trackMiss = 0;
270
- trackingState.trackCount += 1;
271
- if (trackingState.trackCount > this.warmupTolerance) {
272
- trackingState.showing = true;
273
- trackingState.trackingMatrix = null;
274
- trackingState.filter.reset();
278
+ // if not showing, then show it once it reaches warmup number of frames
279
+ if (!trackingState.showing) {
280
+ if (trackingState.isTracking) {
281
+ trackingState.trackMiss = 0;
282
+ trackingState.trackCount += 1;
283
+ if (trackingState.trackCount > this.warmupTolerance) {
284
+ trackingState.showing = true;
285
+ trackingState.trackingMatrix = null;
286
+ trackingState.filter.reset();
287
+ }
275
288
  }
276
289
  }
277
- }
278
- // if showing, then count miss, and hide it when reaches tolerance
279
- if (trackingState.showing) {
280
- if (!trackingState.isTracking) {
281
- trackingState.trackCount = 0;
282
- trackingState.trackMiss += 1;
283
- if (trackingState.trackMiss > this.missTolerance) {
284
- trackingState.showing = false;
285
- trackingState.trackingMatrix = null;
286
- this.onUpdate &&
287
- this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
290
+ // if showing, then count miss, and hide it when reaches tolerance
291
+ if (trackingState.showing) {
292
+ if (!trackingState.isTracking) {
293
+ trackingState.trackCount = 0;
294
+ trackingState.trackMiss += 1;
295
+ if (trackingState.trackMiss > this.missTolerance) {
296
+ trackingState.showing = false;
297
+ trackingState.trackingMatrix = null;
298
+ this.onUpdate &&
299
+ this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
300
+ }
301
+ }
302
+ else {
303
+ trackingState.trackMiss = 0;
288
304
  }
289
305
  }
290
- else {
291
- trackingState.trackMiss = 0;
292
- }
293
- }
294
- // if showing, then call onUpdate, with world matrix
295
- if (trackingState.showing) {
296
- const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
297
- trackingState.trackingMatrix = trackingState.filter.filter(Date.now(), worldMatrix);
298
- let clone = [];
299
- for (let j = 0; j < trackingState.trackingMatrix.length; j++) {
300
- clone[j] = trackingState.trackingMatrix[j];
301
- }
302
- const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
303
- if (isInputRotated) {
304
- clone = this.getRotatedZ90Matrix(clone);
306
+ // if showing, then call onUpdate, with world matrix
307
+ if (trackingState.showing && trackingState.currentModelViewTransform) {
308
+ const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
309
+ trackingState.trackingMatrix = trackingState.filter.filter(Date.now(), worldMatrix);
310
+ let clone = [];
311
+ for (let j = 0; j < trackingState.trackingMatrix.length; j++) {
312
+ clone[j] = trackingState.trackingMatrix[j];
313
+ }
314
+ const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
315
+ if (isInputRotated) {
316
+ clone = this.getRotatedZ90Matrix(clone);
317
+ }
318
+ this.onUpdate &&
319
+ this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: clone, modelViewTransform: trackingState.currentModelViewTransform });
305
320
  }
306
- this.onUpdate &&
307
- this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: clone, modelViewTransform: trackingState.currentModelViewTransform });
308
321
  }
309
322
  }
310
323
  this.onUpdate && this.onUpdate({ type: "processDone" });
@@ -433,6 +446,8 @@ class Controller {
433
446
  return finalModelViewTransform;
434
447
  }
435
448
  _glModelViewMatrix(modelViewTransform, targetIndex) {
449
+ if (!modelViewTransform)
450
+ return null;
436
451
  const height = this.markerDimensions[targetIndex][1];
437
452
  const openGLWorldMatrix = [
438
453
  modelViewTransform[0][0],
@@ -20,6 +20,7 @@ export class Tracker {
20
20
  x: number;
21
21
  y: number;
22
22
  }[];
23
+ octaveIndex: number;
23
24
  debugExtra: {};
24
25
  };
25
26
  /**
@@ -46,6 +46,8 @@ class Tracker {
46
46
  }
47
47
  track(inputData, lastModelViewTransform, targetIndex) {
48
48
  let debugExtra = {};
49
+ if (!lastModelViewTransform)
50
+ return { worldCoords: [], screenCoords: [], octaveIndex: 0, debugExtra };
49
51
  // Select the best octave based on current estimated distance/scale
50
52
  // We want the octave where the marker size is closest to its projected size on screen
51
53
  const modelViewProjectionTransform = buildModelViewProjectionTransform(this.projectionTransform, lastModelViewTransform);
@@ -87,16 +89,7 @@ class Tracker {
87
89
  });
88
90
  }
89
91
  }
90
- if (this.debugMode) {
91
- debugExtra = {
92
- octaveIndex,
93
- projectedImage: Array.from(projectedImage),
94
- matchingPoints,
95
- goodTrack,
96
- trackedPoints: screenCoords,
97
- };
98
- }
99
- return { worldCoords, screenCoords, debugExtra };
92
+ return { worldCoords, screenCoords, octaveIndex, debugExtra };
100
93
  }
101
94
  /**
102
95
  * Pure JS implementation of NCC matching
@@ -239,8 +232,13 @@ class Tracker {
239
232
  * @param {number} alpha - Blending factor (e.g. 0.1 for 10% new data)
240
233
  */
241
234
  applyLiveFeedback(targetIndex, octaveIndex, alpha) {
242
- const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
243
- if (!prebuilt || !prebuilt.projectedImage)
235
+ if (targetIndex === undefined || octaveIndex === undefined)
236
+ return;
237
+ const targetPrebuilts = this.prebuiltData[targetIndex];
238
+ if (!targetPrebuilts)
239
+ return;
240
+ const prebuilt = targetPrebuilts[octaveIndex];
241
+ if (!prebuilt || !prebuilt.projectedImage || !prebuilt.data)
244
242
  return;
245
243
  const markerPixels = prebuilt.data;
246
244
  const projectedPixels = prebuilt.projectedImage;
@@ -248,8 +246,11 @@ class Tracker {
248
246
  // Blend the projected (camera-sourced) pixels into the marker reference data
249
247
  // This allows the NCC matching to adapt to real-world lighting and print quality
250
248
  for (let i = 0; i < count; i++) {
249
+ const val = projectedPixels[i];
250
+ if (isNaN(val))
251
+ continue; // Don't pollute with NaN
251
252
  // Simple linear blend
252
- markerPixels[i] = (1 - alpha) * markerPixels[i] + alpha * projectedPixels[i];
253
+ markerPixels[i] = (1 - alpha) * markerPixels[i] + alpha * val;
253
254
  }
254
255
  }
255
256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.37",
3
+ "version": "1.0.40",
4
4
  "description": "AR Compiler for Node.js and Browser",
5
5
  "repository": {
6
6
  "type": "git",
@@ -226,20 +226,21 @@ class Controller {
226
226
  return { targetIndex: matchedTargetIndex, modelViewTransform };
227
227
  }
228
228
  async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
229
- const { worldCoords, screenCoords, debugExtra } = this.tracker.track(
229
+ if (!lastModelViewTransform) return null;
230
+ const result = this.tracker.track(
230
231
  inputData,
231
232
  lastModelViewTransform,
232
233
  targetIndex,
233
234
  );
234
- if (worldCoords.length < 6) return null; // Umbral de puntos mínimos para mantener el seguimiento
235
+ if (result.worldCoords.length < 6) return null; // Umbral de puntos mínimos para mantener el seguimiento
235
236
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
236
- worldCoords,
237
- screenCoords,
237
+ worldCoords: result.worldCoords,
238
+ screenCoords: result.screenCoords,
238
239
  });
239
240
  return {
240
241
  modelViewTransform,
241
- inliers: worldCoords.length,
242
- octaveIndex: debugExtra.octaveIndex
242
+ inliers: result.worldCoords.length,
243
+ octaveIndex: result.octaveIndex
243
244
  };
244
245
  }
245
246
 
@@ -272,11 +273,14 @@ class Controller {
272
273
  }, 0);
273
274
 
274
275
  // detect and match only if less then maxTrack
276
+ // BUG FIX: Only match if we are NOT in a "ghosting" period for a target
277
+ // to prevent the "found but immediately lost" loop that keeps opacity at 1.
275
278
  if (nTracking < this.maxTrack) {
276
279
  const matchingIndexes = [];
277
280
  for (let i = 0; i < this.trackingStates.length; i++) {
278
281
  const trackingState = this.trackingStates[i];
279
282
  if (trackingState.isTracking === true) continue;
283
+ if (trackingState.showing === true) continue; // Don't try to re-detect if we are still buffers-showing the last position
280
284
  if (this.interestedTargetIndex !== -1 && this.interestedTargetIndex !== i) continue;
281
285
 
282
286
  matchingIndexes.push(i);
@@ -285,7 +289,7 @@ class Controller {
285
289
  const { targetIndex: matchedTargetIndex, modelViewTransform } =
286
290
  await this._detectAndMatch(inputData, matchingIndexes);
287
291
 
288
- if (matchedTargetIndex !== -1) {
292
+ if (matchedTargetIndex !== -1 && modelViewTransform) {
289
293
  this.trackingStates[matchedTargetIndex].isTracking = true;
290
294
  this.trackingStates[matchedTargetIndex].currentModelViewTransform = modelViewTransform;
291
295
  }
@@ -296,84 +300,89 @@ class Controller {
296
300
  const trackingState = this.trackingStates[i];
297
301
 
298
302
  if (trackingState.isTracking) {
299
- let result = await this._trackAndUpdate(
300
- inputData,
301
- trackingState.currentModelViewTransform,
302
- i,
303
- );
304
- if (result === null) {
303
+ if (!trackingState.currentModelViewTransform) {
305
304
  trackingState.isTracking = false;
306
305
  trackingState.stabilityCount = 0;
307
306
  } else {
308
- trackingState.currentModelViewTransform = result.modelViewTransform;
309
-
310
- // --- LIVE MODEL ADAPTATION LOGIC ---
311
- // Si el tracking es muy sólido (muchos inliers) y estable, refinamos el modelo
312
- if (result.inliers > 25) {
313
- trackingState.stabilityCount++;
314
- if (trackingState.stabilityCount > 20) { // 20 frames de estabilidad absoluta
315
- this.tracker.applyLiveFeedback(i, result.octaveIndex, 0.1); // 10% de mezcla real
316
- if (this.debugMode) console.log(`✨ Live Reification: Target ${i} (Octave ${result.octaveIndex}) updated with real-world textures.`);
317
- trackingState.stabilityCount = 0; // Reset para la siguiente actualización
318
- }
307
+ let result = await this._trackAndUpdate(
308
+ inputData,
309
+ trackingState.currentModelViewTransform,
310
+ i,
311
+ );
312
+ if (result === null) {
313
+ trackingState.isTracking = false;
314
+ trackingState.stabilityCount = 0;
319
315
  } else {
320
- trackingState.stabilityCount = Math.max(0, trackingState.stabilityCount - 1);
316
+ trackingState.currentModelViewTransform = result.modelViewTransform;
317
+
318
+ // --- LIVE MODEL ADAPTATION LOGIC ---
319
+ // Si el tracking es muy sólido (muchos inliers) y estable, refinamos el modelo
320
+ // Requisito: > 35 inliers (muy exigente) para evitar polución por ruido
321
+ if (result.inliers > 35) {
322
+ trackingState.stabilityCount++;
323
+ if (trackingState.stabilityCount > 30) { // 30 frames (~1s) de estabilidad absoluta
324
+ this.tracker.applyLiveFeedback(i, result.octaveIndex, 0.05); // Menor alpha (5%) para ser más conservador
325
+ if (this.debugMode) console.log(`✨ Live Reification: Target ${i} (Octave ${result.octaveIndex}) updated.`);
326
+ trackingState.stabilityCount = 0;
327
+ }
328
+ } else {
329
+ trackingState.stabilityCount = Math.max(0, trackingState.stabilityCount - 1);
330
+ }
331
+ // -----------------------------------
321
332
  }
322
- // -----------------------------------
323
333
  }
324
- }
325
334
 
326
- // if not showing, then show it once it reaches warmup number of frames
327
- if (!trackingState.showing) {
328
- if (trackingState.isTracking) {
329
- trackingState.trackMiss = 0;
330
- trackingState.trackCount += 1;
331
- if (trackingState.trackCount > this.warmupTolerance) {
332
- trackingState.showing = true;
333
- trackingState.trackingMatrix = null;
334
- trackingState.filter.reset();
335
+ // if not showing, then show it once it reaches warmup number of frames
336
+ if (!trackingState.showing) {
337
+ if (trackingState.isTracking) {
338
+ trackingState.trackMiss = 0;
339
+ trackingState.trackCount += 1;
340
+ if (trackingState.trackCount > this.warmupTolerance) {
341
+ trackingState.showing = true;
342
+ trackingState.trackingMatrix = null;
343
+ trackingState.filter.reset();
344
+ }
335
345
  }
336
346
  }
337
- }
338
347
 
339
- // if showing, then count miss, and hide it when reaches tolerance
340
- if (trackingState.showing) {
341
- if (!trackingState.isTracking) {
342
- trackingState.trackCount = 0;
343
- trackingState.trackMiss += 1;
344
-
345
- if (trackingState.trackMiss > this.missTolerance) {
346
- trackingState.showing = false;
347
- trackingState.trackingMatrix = null;
348
- this.onUpdate &&
349
- this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
348
+ // if showing, then count miss, and hide it when reaches tolerance
349
+ if (trackingState.showing) {
350
+ if (!trackingState.isTracking) {
351
+ trackingState.trackCount = 0;
352
+ trackingState.trackMiss += 1;
353
+
354
+ if (trackingState.trackMiss > this.missTolerance) {
355
+ trackingState.showing = false;
356
+ trackingState.trackingMatrix = null;
357
+ this.onUpdate &&
358
+ this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
359
+ }
360
+ } else {
361
+ trackingState.trackMiss = 0;
350
362
  }
351
- } else {
352
- trackingState.trackMiss = 0;
353
363
  }
354
- }
355
364
 
356
- // if showing, then call onUpdate, with world matrix
357
- if (trackingState.showing) {
358
- const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
359
- trackingState.trackingMatrix = trackingState.filter.filter(Date.now(), worldMatrix);
365
+ // if showing, then call onUpdate, with world matrix
366
+ if (trackingState.showing && trackingState.currentModelViewTransform) {
367
+ const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
368
+ trackingState.trackingMatrix = trackingState.filter.filter(Date.now(), worldMatrix);
360
369
 
361
- let clone = [];
362
- for (let j = 0; j < trackingState.trackingMatrix.length; j++) {
363
- clone[j] = trackingState.trackingMatrix[j];
364
- }
370
+ let clone = [];
371
+ for (let j = 0; j < trackingState.trackingMatrix.length; j++) {
372
+ clone[j] = trackingState.trackingMatrix[j];
373
+ }
365
374
 
366
- const isInputRotated =
367
- input.width === this.inputHeight && input.height === this.inputWidth;
368
- if (isInputRotated) {
369
- clone = this.getRotatedZ90Matrix(clone);
370
- }
375
+ const isInputRotated =
376
+ input.width === this.inputHeight && input.height === this.inputWidth;
377
+ if (isInputRotated) {
378
+ clone = this.getRotatedZ90Matrix(clone);
379
+ }
371
380
 
372
- this.onUpdate &&
373
- this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: clone, modelViewTransform: trackingState.currentModelViewTransform });
381
+ this.onUpdate &&
382
+ this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: clone, modelViewTransform: trackingState.currentModelViewTransform });
383
+ }
374
384
  }
375
385
  }
376
-
377
386
  this.onUpdate && this.onUpdate({ type: "processDone" });
378
387
 
379
388
  // Use requestAnimationFrame if available, otherwise just wait briefly
@@ -521,6 +530,7 @@ class Controller {
521
530
  }
522
531
 
523
532
  _glModelViewMatrix(modelViewTransform, targetIndex) {
533
+ if (!modelViewTransform) return null;
524
534
  const height = this.markerDimensions[targetIndex][1];
525
535
 
526
536
  const openGLWorldMatrix = [
@@ -61,6 +61,7 @@ class Tracker {
61
61
 
62
62
  track(inputData, lastModelViewTransform, targetIndex) {
63
63
  let debugExtra = {};
64
+ if (!lastModelViewTransform) return { worldCoords: [], screenCoords: [], octaveIndex: 0, debugExtra };
64
65
 
65
66
  // Select the best octave based on current estimated distance/scale
66
67
  // We want the octave where the marker size is closest to its projected size on screen
@@ -127,17 +128,7 @@ class Tracker {
127
128
  }
128
129
  }
129
130
 
130
- if (this.debugMode) {
131
- debugExtra = {
132
- octaveIndex,
133
- projectedImage: Array.from(projectedImage),
134
- matchingPoints,
135
- goodTrack,
136
- trackedPoints: screenCoords,
137
- };
138
- }
139
-
140
- return { worldCoords, screenCoords, debugExtra };
131
+ return { worldCoords, screenCoords, octaveIndex, debugExtra };
141
132
  }
142
133
 
143
134
  /**
@@ -308,8 +299,12 @@ class Tracker {
308
299
  * @param {number} alpha - Blending factor (e.g. 0.1 for 10% new data)
309
300
  */
310
301
  applyLiveFeedback(targetIndex, octaveIndex, alpha) {
311
- const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
312
- if (!prebuilt || !prebuilt.projectedImage) return;
302
+ if (targetIndex === undefined || octaveIndex === undefined) return;
303
+ const targetPrebuilts = this.prebuiltData[targetIndex];
304
+ if (!targetPrebuilts) return;
305
+
306
+ const prebuilt = targetPrebuilts[octaveIndex];
307
+ if (!prebuilt || !prebuilt.projectedImage || !prebuilt.data) return;
313
308
 
314
309
  const markerPixels = prebuilt.data;
315
310
  const projectedPixels = prebuilt.projectedImage;
@@ -318,8 +313,10 @@ class Tracker {
318
313
  // Blend the projected (camera-sourced) pixels into the marker reference data
319
314
  // This allows the NCC matching to adapt to real-world lighting and print quality
320
315
  for (let i = 0; i < count; i++) {
316
+ const val = projectedPixels[i];
317
+ if (isNaN(val)) continue; // Don't pollute with NaN
321
318
  // Simple linear blend
322
- markerPixels[i] = (1 - alpha) * markerPixels[i] + alpha * projectedPixels[i];
319
+ markerPixels[i] = (1 - alpha) * markerPixels[i] + alpha * val;
323
320
  }
324
321
  }
325
322
  }