@srsergio/taptapp-ar 1.0.77 → 1.0.79

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 (30) hide show
  1. package/dist/compiler/controller.d.ts +2 -6
  2. package/dist/compiler/controller.js +43 -44
  3. package/dist/compiler/features/auto-rotation-feature.d.ts +15 -0
  4. package/dist/compiler/features/auto-rotation-feature.js +30 -0
  5. package/dist/compiler/features/crop-detection-feature.d.ts +18 -0
  6. package/dist/compiler/features/crop-detection-feature.js +26 -0
  7. package/dist/compiler/features/feature-base.d.ts +46 -0
  8. package/dist/compiler/features/feature-base.js +1 -0
  9. package/dist/compiler/features/feature-manager.d.ts +12 -0
  10. package/dist/compiler/features/feature-manager.js +55 -0
  11. package/dist/compiler/features/one-euro-filter-feature.d.ts +15 -0
  12. package/dist/compiler/features/one-euro-filter-feature.js +37 -0
  13. package/dist/compiler/features/temporal-filter-feature.d.ts +19 -0
  14. package/dist/compiler/features/temporal-filter-feature.js +57 -0
  15. package/dist/compiler/offline-compiler.d.ts +92 -8
  16. package/dist/compiler/offline-compiler.js +3 -86
  17. package/dist/react/TaptappAR.js +3 -0
  18. package/dist/react/use-ar.js +1 -0
  19. package/package.json +1 -1
  20. package/src/compiler/FEATURES.md +40 -0
  21. package/src/compiler/controller.ts +54 -42
  22. package/src/compiler/features/auto-rotation-feature.ts +36 -0
  23. package/src/compiler/features/crop-detection-feature.ts +31 -0
  24. package/src/compiler/features/feature-base.ts +48 -0
  25. package/src/compiler/features/feature-manager.ts +65 -0
  26. package/src/compiler/features/one-euro-filter-feature.ts +44 -0
  27. package/src/compiler/features/temporal-filter-feature.ts +68 -0
  28. package/src/compiler/offline-compiler.ts +3 -94
  29. package/src/react/TaptappAR.tsx +3 -0
  30. package/src/react/use-ar.ts +1 -0
@@ -10,7 +10,6 @@ import { extractTrackingFeatures } from "./tracker/extract-utils.js";
10
10
  import { DetectorLite } from "./detector/detector-lite.js";
11
11
  import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
12
12
  import * as msgpack from "@msgpack/msgpack";
13
- import { WorkerPool } from "./utils/worker-pool.js";
14
13
  // Detect environment
15
14
  const isNode = typeof process !== "undefined" &&
16
15
  process.versions != null &&
@@ -18,35 +17,8 @@ const isNode = typeof process !== "undefined" &&
18
17
  const CURRENT_VERSION = 7; // Protocol v7: Moonshot - 4-bit Packed Tracking Data
19
18
  export class OfflineCompiler {
20
19
  data = null;
21
- workerPool = null;
22
20
  constructor() {
23
- // Workers only in Node.js
24
- if (!isNode) {
25
- console.log("🌐 OfflineCompiler: Browser mode (no workers)");
26
- }
27
- }
28
- async _initNodeWorkers() {
29
- try {
30
- const pathModule = "path";
31
- const urlModule = "url";
32
- const osModule = "os";
33
- const workerThreadsModule = "node:worker_threads";
34
- const [path, url, os, { Worker }] = await Promise.all([
35
- import(/* @vite-ignore */ pathModule),
36
- import(/* @vite-ignore */ urlModule),
37
- import(/* @vite-ignore */ osModule),
38
- import(/* @vite-ignore */ workerThreadsModule)
39
- ]);
40
- const __filename = url.fileURLToPath(import.meta.url);
41
- const __dirname = path.dirname(__filename);
42
- const workerPath = path.join(__dirname, "node-worker.js");
43
- // Limit workers to avoid freezing system
44
- const numWorkers = Math.min(os.cpus().length, 4);
45
- this.workerPool = new WorkerPool(workerPath, numWorkers, Worker);
46
- }
47
- catch (e) {
48
- console.log("⚡ OfflineCompiler: Running without workers (initialization failed)", e);
49
- }
21
+ console.log("⚡ OfflineCompiler: Main thread mode (no workers)");
50
22
  }
51
23
  async compileImageTargets(images, progressCallback) {
52
24
  console.time("⏱️ Compilación total");
@@ -86,24 +58,7 @@ export class OfflineCompiler {
86
58
  return this.data;
87
59
  }
88
60
  async _compileTarget(targetImages, progressCallback) {
89
- if (isNode)
90
- await this._initNodeWorkers();
91
- if (this.workerPool) {
92
- const progressMap = new Float32Array(targetImages.length);
93
- const wrappedPromises = targetImages.map((targetImage, index) => {
94
- return this.workerPool.runTask({
95
- type: 'compile-all', // 🚀 MOONSHOT: Combined task
96
- targetImage,
97
- onProgress: (p) => {
98
- progressMap[index] = p;
99
- const sum = progressMap.reduce((a, b) => a + b, 0);
100
- progressCallback(sum / targetImages.length);
101
- }
102
- });
103
- });
104
- return Promise.all(wrappedPromises);
105
- }
106
- // Fallback or non-worker implementation: run match and track sequentially
61
+ // Run match and track sequentially to match browser behavior exactly
107
62
  const matchingResults = await this._compileMatch(targetImages, (p) => progressCallback(p * 0.5));
108
63
  const trackingResults = await this._compileTrack(targetImages, (p) => progressCallback(50 + p * 0.5));
109
64
  return targetImages.map((_, i) => ({
@@ -114,25 +69,6 @@ export class OfflineCompiler {
114
69
  async _compileMatch(targetImages, progressCallback) {
115
70
  const percentPerImage = 100 / targetImages.length;
116
71
  let currentPercent = 0;
117
- if (isNode)
118
- await this._initNodeWorkers();
119
- if (this.workerPool) {
120
- const progressMap = new Float32Array(targetImages.length);
121
- const wrappedPromises = targetImages.map((targetImage, index) => {
122
- return this.workerPool.runTask({
123
- type: 'match',
124
- targetImage,
125
- percentPerImage,
126
- basePercent: 0,
127
- onProgress: (p) => {
128
- progressMap[index] = p;
129
- const sum = progressMap.reduce((a, b) => a + b, 0);
130
- progressCallback(sum);
131
- }
132
- });
133
- });
134
- return Promise.all(wrappedPromises);
135
- }
136
72
  const results = [];
137
73
  for (let i = 0; i < targetImages.length; i++) {
138
74
  const targetImage = targetImages[i];
@@ -165,23 +101,6 @@ export class OfflineCompiler {
165
101
  async _compileTrack(targetImages, progressCallback) {
166
102
  const percentPerImage = 100 / targetImages.length;
167
103
  let currentPercent = 0;
168
- if (this.workerPool) {
169
- const progressMap = new Float32Array(targetImages.length);
170
- const wrappedPromises = targetImages.map((targetImage, index) => {
171
- return this.workerPool.runTask({
172
- type: 'compile',
173
- targetImage,
174
- percentPerImage,
175
- basePercent: 0,
176
- onProgress: (p) => {
177
- progressMap[index] = p;
178
- const sum = progressMap.reduce((a, b) => a + b, 0);
179
- progressCallback(sum);
180
- }
181
- });
182
- });
183
- return Promise.all(wrappedPromises);
184
- }
185
104
  const results = [];
186
105
  for (let i = 0; i < targetImages.length; i++) {
187
106
  const targetImage = targetImages[i];
@@ -407,9 +326,7 @@ export class OfflineCompiler {
407
326
  };
408
327
  }
409
328
  async destroy() {
410
- if (this.workerPool) {
411
- await this.workerPool.destroy();
412
- }
329
+ // No workers to destroy
413
330
  }
414
331
  _pack4Bit(data) {
415
332
  const length = data.length;
@@ -92,6 +92,9 @@ export const TaptappAR = ({ config, className = "", showScanningOverlay = true,
92
92
  display: block;
93
93
  width: 100%;
94
94
  height: auto;
95
+ opacity: 0;
96
+ pointer-events: none;
97
+ transition: opacity 0.3s ease;
95
98
  }
96
99
  ` })] }));
97
100
  };
@@ -38,6 +38,7 @@ export const useAR = (config) => {
38
38
  targetSrc: config.targetTaarSrc,
39
39
  overlay: overlayRef.current,
40
40
  scale: config.scale,
41
+ debug: false,
41
42
  onFound: async ({ targetIndex }) => {
42
43
  console.log(`🎯 Target ${targetIndex} detected!`);
43
44
  if (!isMounted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "description": "AR Compiler for Node.js and Browser",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,40 @@
1
+ # Taptapp AR Controller Features
2
+
3
+ Este documento detalla las características modulares y mejoras de rendimiento implementadas en el controlador de Taptapp AR. Cada una de estas funcionalidades puede ser activada o desactivada rápidamente desde el código para pruebas de rendimiento o debugging.
4
+
5
+ ## 1. One Euro Filter (Smoothing)
6
+ - **Trata de:** Un filtro adaptativo de primer orden para suavizar señales ruidosas.
7
+ - **Qué aporta:** Reduce drásticamente el "jitter" (temblor) visual del objeto AR cuando el marcador está estático o se mueve lentamente. Proporciona una experiencia mucho más estable y premium.
8
+ - **Requisito / Dependencia:** Ninguno. Es independiente.
9
+ - **Archivo:** `features/one-euro-filter-feature.ts`
10
+
11
+ ## 2. Temporal Filter (Warmup & Miss Tolerance)
12
+ - **Trata de:** Lógica de persistencia temporal que gestiona estados de "calentamiento" y "pérdida".
13
+ - **Qué aporta:**
14
+ - **Warmup:** Evita falsos positivos al requerir que el marcador sea detectado consistentemente durante N cuadros antes de mostrarlo.
15
+ - **Miss Tolerance:** Mantiene el objeto visible durante breves oclusiones o fallos rápidos del tracker, evitando que el objeto parpadee o desaparezca instantáneamente.
16
+ - **Requisito / Dependencia:** Ninguno.
17
+ - **Archivo:** `features/temporal-filter-feature.ts`
18
+
19
+ ## 3. Crop Detection (Movement Optimization)
20
+ - **Trata de:** Optimización que analiza solo regiones con movimiento en lugar de procesar toda la imagen.
21
+ - **Qué aporta:** Mejora significativa en el rendimiento (FPS) y reduce el uso de CPU. Al detectar puntos de interés solo en áreas activas, se ignora el fondo estático.
22
+ - **Requisito / Dependencia:** Depende de `CropDetector`.
23
+ - **Archivo:** `features/crop-detection-feature.ts`
24
+
25
+ ## 4. Auto Rotation Matrix
26
+ - **Trata de:** Ajuste automático de la matriz de transformación basada en la orientación del video de entrada.
27
+ - **Qué aporta:** Permite que el AR funcione correctamente tanto en modo horizontal como vertical (Portrait) sin que el desarrollador tenga que rotar manualmente las coordenadas.
28
+ - **Requisito / Dependencia:** Ninguno.
29
+ - **Archivo:** `features/auto-rotation-feature.ts`
30
+
31
+ ---
32
+
33
+ ## Cómo desactivar una característica rápidamente
34
+
35
+ En `packages/taptapp-ar/src/compiler/controller.ts`, puedes comentar la línea donde se añade la característica en el constructor o configurar el flag `enabled: false` en la instancia.
36
+
37
+ ```typescript
38
+ // Ejemplo para desactivar suavizado
39
+ this.featureManager.getFeature("one-euro-filter").enabled = false;
40
+ ```
@@ -2,7 +2,11 @@ import { Tracker } from "./tracker/tracker.js";
2
2
  import { CropDetector } from "./detector/crop-detector.js";
3
3
  import { OfflineCompiler as Compiler } from "./offline-compiler.js";
4
4
  import { InputLoader } from "./input-loader.js";
5
- import { OneEuroFilter } from "../libs/one-euro-filter.js";
5
+ import { FeatureManager } from "./features/feature-manager.js";
6
+ import { OneEuroFilterFeature } from "./features/one-euro-filter-feature.js";
7
+ import { TemporalFilterFeature } from "./features/temporal-filter-feature.js";
8
+ import { AutoRotationFeature } from "./features/auto-rotation-feature.js";
9
+ import { CropDetectionFeature } from "./features/crop-detection-feature.js";
6
10
 
7
11
  let ControllerWorker: any;
8
12
 
@@ -41,11 +45,6 @@ class Controller {
41
45
  inputWidth: number;
42
46
  inputHeight: number;
43
47
  maxTrack: number;
44
- filterMinCF: number;
45
- filterBeta: number;
46
- warmupTolerance: number;
47
- missTolerance: number;
48
- cropDetector: CropDetector;
49
48
  inputLoader: InputLoader;
50
49
  markerDimensions: any[] | null = null;
51
50
  onUpdate: ((data: any) => void) | null;
@@ -62,6 +61,7 @@ class Controller {
62
61
  workerTrackDone: ((data: any) => void) | null = null;
63
62
  mainThreadMatcher: any;
64
63
  mainThreadEstimator: any;
64
+ featureManager: FeatureManager;
65
65
 
66
66
  constructor({
67
67
  inputWidth,
@@ -78,17 +78,32 @@ class Controller {
78
78
  this.inputWidth = inputWidth;
79
79
  this.inputHeight = inputHeight;
80
80
  this.maxTrack = maxTrack;
81
- this.filterMinCF = filterMinCF === null ? DEFAULT_FILTER_CUTOFF : filterMinCF;
82
- this.filterBeta = filterBeta === null ? DEFAULT_FILTER_BETA : filterBeta;
83
- this.warmupTolerance = warmupTolerance === null ? DEFAULT_WARMUP_TOLERANCE : warmupTolerance;
84
- this.missTolerance = missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance;
85
- this.cropDetector = new CropDetector(this.inputWidth, this.inputHeight, debugMode);
81
+
82
+ this.featureManager = new FeatureManager();
83
+ this.featureManager.addFeature(new OneEuroFilterFeature(
84
+ filterMinCF === null ? DEFAULT_FILTER_CUTOFF : filterMinCF,
85
+ filterBeta === null ? DEFAULT_FILTER_BETA : filterBeta
86
+ ));
87
+ this.featureManager.addFeature(new TemporalFilterFeature(
88
+ warmupTolerance === null ? DEFAULT_WARMUP_TOLERANCE : warmupTolerance,
89
+ missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance
90
+ ));
91
+ this.featureManager.addFeature(new AutoRotationFeature());
92
+ this.featureManager.addFeature(new CropDetectionFeature());
93
+
86
94
  this.inputLoader = new InputLoader(this.inputWidth, this.inputHeight);
87
95
  this.onUpdate = onUpdate;
88
96
  this.debugMode = debugMode;
89
97
  this.worker = worker;
90
98
  if (this.worker) this._setupWorkerListener();
91
99
 
100
+ this.featureManager.init({
101
+ inputWidth: this.inputWidth,
102
+ inputHeight: this.inputHeight,
103
+ projectionTransform: [], // Will be set below
104
+ debugMode: this.debugMode
105
+ });
106
+
92
107
  const near = 10;
93
108
  const far = 100000;
94
109
  const fovy = (45.0 * Math.PI) / 180;
@@ -100,6 +115,13 @@ class Controller {
100
115
  [0, 0, 1],
101
116
  ];
102
117
 
118
+ this.featureManager.init({
119
+ inputWidth: this.inputWidth,
120
+ inputHeight: this.inputHeight,
121
+ projectionTransform: this.projectionTransform,
122
+ debugMode: this.debugMode
123
+ });
124
+
103
125
  this.projectionMatrix = this._glProjectionMatrix({
104
126
  projectionTransform: this.projectionTransform,
105
127
  width: this.inputWidth,
@@ -197,7 +219,8 @@ class Controller {
197
219
 
198
220
  dummyRun(input: any) {
199
221
  const inputData = this.inputLoader.loadInput(input);
200
- this.cropDetector.detect(inputData);
222
+ const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
223
+ cropFeature?.detect(inputData, false);
201
224
  this.tracker!.dummyRun(inputData);
202
225
  }
203
226
 
@@ -219,7 +242,8 @@ class Controller {
219
242
  }
220
243
 
221
244
  async _detectAndMatch(inputData: any, targetIndexes: number[]) {
222
- const { featurePoints } = this.cropDetector.detectMoving(inputData);
245
+ const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
246
+ const { featurePoints } = cropFeature!.detect(inputData, true);
223
247
  const { targetIndex: matchedTargetIndex, modelViewTransform } = await this._workerMatch(
224
248
  featurePoints,
225
249
  targetIndexes,
@@ -253,7 +277,6 @@ class Controller {
253
277
  currentModelViewTransform: null,
254
278
  trackCount: 0,
255
279
  trackMiss: 0,
256
- filter: new OneEuroFilter({ minCutOff: this.filterMinCF, beta: this.filterBeta }),
257
280
  });
258
281
  }
259
282
 
@@ -298,46 +321,34 @@ class Controller {
298
321
  }
299
322
  }
300
323
 
301
- if (!trackingState.showing) {
302
- if (trackingState.isTracking) {
303
- trackingState.trackMiss = 0;
304
- trackingState.trackCount += 1;
305
- if (trackingState.trackCount > this.warmupTolerance) {
306
- trackingState.showing = true;
307
- trackingState.trackingMatrix = null;
308
- trackingState.filter.reset();
309
- }
310
- }
311
- }
324
+ const wasShowing = trackingState.showing;
325
+ trackingState.showing = this.featureManager.shouldShow(i, trackingState.isTracking);
312
326
 
313
- if (trackingState.showing) {
314
- if (!trackingState.isTracking) {
315
- trackingState.trackCount = 0;
316
- trackingState.trackMiss += 1;
317
- if (trackingState.trackMiss > this.missTolerance) {
318
- trackingState.showing = false;
319
- trackingState.trackingMatrix = null;
320
- this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
321
- }
322
- } else {
323
- trackingState.trackMiss = 0;
324
- }
327
+ if (wasShowing && !trackingState.showing) {
328
+ trackingState.trackingMatrix = null;
329
+ this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
330
+ this.featureManager.notifyUpdate({ type: "reset", targetIndex: i });
325
331
  }
326
332
 
327
333
  if (trackingState.showing) {
328
334
  const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
329
- trackingState.trackingMatrix = trackingState.filter.filter(Date.now(), worldMatrix);
330
- let clone = [...trackingState.trackingMatrix];
335
+ const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix);
336
+ trackingState.trackingMatrix = filteredMatrix;
337
+
338
+ let finalMatrix = [...filteredMatrix];
331
339
 
332
340
  const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
333
341
  if (isInputRotated) {
334
- clone = this.getRotatedZ90Matrix(clone);
342
+ const rotationFeature = this.featureManager.getFeature<AutoRotationFeature>("auto-rotation");
343
+ if (rotationFeature) {
344
+ finalMatrix = rotationFeature.rotate(finalMatrix);
345
+ }
335
346
  }
336
347
 
337
348
  this.onUpdate && this.onUpdate({
338
349
  type: "updateMatrix",
339
350
  targetIndex: i,
340
- worldMatrix: clone,
351
+ worldMatrix: finalMatrix,
341
352
  modelViewTransform: trackingState.currentModelViewTransform
342
353
  });
343
354
  }
@@ -361,7 +372,8 @@ class Controller {
361
372
 
362
373
  async detect(input: any) {
363
374
  const inputData = this.inputLoader.loadInput(input);
364
- const { featurePoints, debugExtra } = this.cropDetector.detect(inputData);
375
+ const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
376
+ const { featurePoints, debugExtra } = cropFeature!.detect(inputData, false);
365
377
  return { featurePoints, debugExtra };
366
378
  }
367
379
 
@@ -0,0 +1,36 @@
1
+ import { ControllerFeature } from "./feature-base.js";
2
+
3
+ export class AutoRotationFeature implements ControllerFeature {
4
+ id = "auto-rotation";
5
+ name = "Auto Rotation Matrix";
6
+ description = "Automatically adjusts the world matrix if the input video is rotated (e.g. portrait mode).";
7
+ enabled = true;
8
+
9
+ private inputWidth: number = 0;
10
+ private inputHeight: number = 0;
11
+
12
+ init(context: { inputWidth: number; inputHeight: number }) {
13
+ this.inputWidth = context.inputWidth;
14
+ this.inputHeight = context.inputHeight;
15
+ }
16
+
17
+ filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[] {
18
+ if (!this.enabled) return worldMatrix;
19
+
20
+ // Check if input is rotated (this logic might need the actual current input dimensions)
21
+ // For now, we'll assume the controller passes the 'isRotated' info or we detect it
22
+ // But since this is a matrix post-process, we can just apply it if needed.
23
+ return worldMatrix;
24
+ }
25
+
26
+ // We might need a way to pass the 'currentInput' to the feature.
27
+ // Actually, the controller can just call this if it detects rotation.
28
+ rotate(m: number[]): number[] {
29
+ return [
30
+ -m[1], m[0], m[2], m[3],
31
+ -m[5], m[4], m[6], m[7],
32
+ -m[9], m[8], m[10], m[11],
33
+ -m[13], m[12], m[14], m[15],
34
+ ];
35
+ }
36
+ }
@@ -0,0 +1,31 @@
1
+ import { ControllerFeature, FeatureContext } from "./feature-base.js";
2
+ import { CropDetector } from "../detector/crop-detector.js";
3
+
4
+ export class CropDetectionFeature implements ControllerFeature {
5
+ id = "crop-detection";
6
+ name = "Crop Detection";
7
+ description = "Optimizes detection by focusing on areas with motion, reducing CPU usage.";
8
+ enabled = true;
9
+
10
+ private cropDetector: CropDetector | null = null;
11
+ private debugMode: boolean = false;
12
+
13
+ init(context: FeatureContext) {
14
+ this.debugMode = context.debugMode;
15
+ this.cropDetector = new CropDetector(context.inputWidth, context.inputHeight, this.debugMode);
16
+ }
17
+
18
+ detect(inputData: any, isMoving: boolean = true) {
19
+ if (!this.enabled || !this.cropDetector) {
20
+ // Fallback to full detection if disabled?
21
+ // Actually CropDetector.detect is just full detection.
22
+ // We'll expose the methods here.
23
+ }
24
+
25
+ if (isMoving && this.enabled) {
26
+ return this.cropDetector!.detectMoving(inputData);
27
+ } else {
28
+ return this.cropDetector!.detect(inputData);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,48 @@
1
+ export interface FeatureContext {
2
+ inputWidth: number;
3
+ inputHeight: number;
4
+ projectionTransform: number[][];
5
+ debugMode: boolean;
6
+ }
7
+
8
+ export interface Feature {
9
+ id: string;
10
+ name: string;
11
+ description: string;
12
+ enabled: boolean;
13
+
14
+ // Lifecycle hooks
15
+ init?(context: FeatureContext): void;
16
+ onUpdate?(data: any): void;
17
+ dispose?(): void;
18
+ }
19
+
20
+ /**
21
+ * Controller feature interface for processing hooks
22
+ */
23
+ export interface ControllerFeature extends Feature {
24
+ /**
25
+ * Called before processing a frame
26
+ */
27
+ beforeProcess?(inputData: any): void;
28
+
29
+ /**
30
+ * Called after detection/matching
31
+ */
32
+ afterMatch?(result: { targetIndex: number, modelViewTransform: number[][] | null }): void;
33
+
34
+ /**
35
+ * Called after tracking update
36
+ */
37
+ afterTrack?(result: { targetIndex: number, modelViewTransform: number[][] | null }): void;
38
+
39
+ /**
40
+ * Hook to filter or modify the final world matrix
41
+ */
42
+ filterWorldMatrix?(targetIndex: number, worldMatrix: number[]): number[];
43
+
44
+ /**
45
+ * Hook to decide if a target should be shown
46
+ */
47
+ shouldShow?(targetIndex: number, isTracking: boolean): boolean;
48
+ }
@@ -0,0 +1,65 @@
1
+ import { ControllerFeature, FeatureContext } from "./feature-base.js";
2
+
3
+ export class FeatureManager {
4
+ private features: ControllerFeature[] = [];
5
+
6
+ addFeature(feature: ControllerFeature) {
7
+ this.features.push(feature);
8
+ }
9
+
10
+ getFeature<T extends ControllerFeature>(id: string): T | undefined {
11
+ return this.features.find(f => f.id === id) as T;
12
+ }
13
+
14
+ init(context: FeatureContext) {
15
+ for (const feature of this.features) {
16
+ if (feature.enabled && feature.init) {
17
+ feature.init(context);
18
+ }
19
+ }
20
+ }
21
+
22
+ beforeProcess(inputData: any) {
23
+ for (const feature of this.features) {
24
+ if (feature.enabled && feature.beforeProcess) {
25
+ feature.beforeProcess(inputData);
26
+ }
27
+ }
28
+ }
29
+
30
+ applyWorldMatrixFilters(targetIndex: number, worldMatrix: number[]): number[] {
31
+ let result = worldMatrix;
32
+ for (const feature of this.features) {
33
+ if (feature.enabled && feature.filterWorldMatrix) {
34
+ result = feature.filterWorldMatrix(targetIndex, result);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+
40
+ shouldShow(targetIndex: number, isTracking: boolean): boolean {
41
+ let show = isTracking;
42
+ for (const feature of this.features) {
43
+ if (feature.enabled && feature.shouldShow) {
44
+ show = feature.shouldShow(targetIndex, isTracking);
45
+ }
46
+ }
47
+ return show;
48
+ }
49
+
50
+ notifyUpdate(data: any) {
51
+ for (const feature of this.features) {
52
+ if (feature.enabled && feature.onUpdate) {
53
+ feature.onUpdate(data);
54
+ }
55
+ }
56
+ }
57
+
58
+ dispose() {
59
+ for (const feature of this.features) {
60
+ if (feature.dispose) {
61
+ feature.dispose();
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,44 @@
1
+ import { ControllerFeature, FeatureContext } from "./feature-base.js";
2
+ import { OneEuroFilter } from "../../libs/one-euro-filter.js";
3
+
4
+ export class OneEuroFilterFeature implements ControllerFeature {
5
+ id = "one-euro-filter";
6
+ name = "One Euro Filter";
7
+ description = "Smooths the tracking matrix to reduce jitter using a One Euro Filter.";
8
+ enabled = true;
9
+
10
+ private filters: OneEuroFilter[] = [];
11
+ private minCutOff: number;
12
+ private beta: number;
13
+
14
+ constructor(minCutOff: number = 0.5, beta: number = 0.1) {
15
+ this.minCutOff = minCutOff;
16
+ this.beta = beta;
17
+ }
18
+
19
+ init(context: FeatureContext) {
20
+ // We'll initialize filters lazily or based on target count if known
21
+ }
22
+
23
+ private getFilter(targetIndex: number): OneEuroFilter {
24
+ if (!this.filters[targetIndex]) {
25
+ this.filters[targetIndex] = new OneEuroFilter({
26
+ minCutOff: this.minCutOff,
27
+ beta: this.beta
28
+ });
29
+ }
30
+ return this.filters[targetIndex];
31
+ }
32
+
33
+ filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[] {
34
+ if (!this.enabled) return worldMatrix;
35
+ const filter = this.getFilter(targetIndex);
36
+ return filter.filter(Date.now(), worldMatrix);
37
+ }
38
+
39
+ onUpdate(data: any) {
40
+ if (data.type === "reset" && data.targetIndex !== undefined) {
41
+ this.filters[data.targetIndex]?.reset();
42
+ }
43
+ }
44
+ }