@srsergio/taptapp-ar 1.0.92 → 1.0.94

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 (52) hide show
  1. package/README.md +16 -14
  2. package/dist/compiler/offline-compiler.d.ts +3 -3
  3. package/dist/compiler/offline-compiler.js +50 -33
  4. package/dist/core/constants.d.ts +2 -0
  5. package/dist/core/constants.js +4 -1
  6. package/dist/core/detector/detector-lite.d.ts +6 -5
  7. package/dist/core/detector/detector-lite.js +46 -16
  8. package/dist/core/image-list.d.ts +24 -6
  9. package/dist/core/image-list.js +4 -4
  10. package/dist/core/matching/matcher.d.ts +1 -1
  11. package/dist/core/matching/matcher.js +7 -4
  12. package/dist/core/matching/matching.d.ts +2 -1
  13. package/dist/core/matching/matching.js +43 -11
  14. package/dist/core/perception/bio-inspired-engine.d.ts +130 -0
  15. package/dist/core/perception/bio-inspired-engine.js +232 -0
  16. package/dist/core/perception/foveal-attention.d.ts +142 -0
  17. package/dist/core/perception/foveal-attention.js +280 -0
  18. package/dist/core/perception/index.d.ts +6 -0
  19. package/dist/core/perception/index.js +17 -0
  20. package/dist/core/perception/predictive-coding.d.ts +92 -0
  21. package/dist/core/perception/predictive-coding.js +278 -0
  22. package/dist/core/perception/saccadic-controller.d.ts +126 -0
  23. package/dist/core/perception/saccadic-controller.js +269 -0
  24. package/dist/core/perception/saliency-map.d.ts +74 -0
  25. package/dist/core/perception/saliency-map.js +254 -0
  26. package/dist/core/perception/scale-orchestrator.d.ts +28 -0
  27. package/dist/core/perception/scale-orchestrator.js +68 -0
  28. package/dist/core/protocol.d.ts +14 -1
  29. package/dist/core/protocol.js +33 -1
  30. package/dist/runtime/bio-inspired-controller.d.ts +135 -0
  31. package/dist/runtime/bio-inspired-controller.js +358 -0
  32. package/dist/runtime/controller.d.ts +11 -2
  33. package/dist/runtime/controller.js +20 -8
  34. package/dist/runtime/controller.worker.js +2 -2
  35. package/package.json +1 -1
  36. package/src/compiler/offline-compiler.ts +56 -36
  37. package/src/core/constants.ts +5 -1
  38. package/src/core/detector/detector-lite.js +46 -16
  39. package/src/core/image-list.js +4 -4
  40. package/src/core/matching/matcher.js +8 -4
  41. package/src/core/matching/matching.js +51 -12
  42. package/src/core/perception/bio-inspired-engine.js +275 -0
  43. package/src/core/perception/foveal-attention.js +306 -0
  44. package/src/core/perception/index.js +18 -0
  45. package/src/core/perception/predictive-coding.js +327 -0
  46. package/src/core/perception/saccadic-controller.js +303 -0
  47. package/src/core/perception/saliency-map.js +296 -0
  48. package/src/core/perception/scale-orchestrator.js +80 -0
  49. package/src/core/protocol.ts +38 -1
  50. package/src/runtime/bio-inspired-controller.ts +448 -0
  51. package/src/runtime/controller.ts +22 -7
  52. package/src/runtime/controller.worker.js +2 -1
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Saccadic Controller
3
+ *
4
+ * Mimics human eye saccades - rapid movements that redirect foveal attention
5
+ * to areas of interest. The human eye makes 3-4 saccades per second to
6
+ * build a complete picture of the visual scene.
7
+ *
8
+ * Strategy:
9
+ * 1. Compute saliency map to find "interesting" regions
10
+ * 2. Use tracking state to predict where features should be
11
+ * 3. Generate priority-ordered list of "glance" targets
12
+ * 4. Limit saccades per frame to balance coverage vs. efficiency
13
+ */
14
+
15
+ /**
16
+ * A saccade target representing where attention should be directed
17
+ * @typedef {Object} SaccadeTarget
18
+ * @property {number} x - X coordinate
19
+ * @property {number} y - Y coordinate
20
+ * @property {number} priority - Priority (0 = highest)
21
+ * @property {string} reason - Why this target was selected
22
+ * @property {number} saliency - Saliency score at this location
23
+ */
24
+
25
+ class SaccadicController {
26
+ /**
27
+ * @param {number} width - Image width
28
+ * @param {number} height - Image height
29
+ * @param {Object} config - Configuration
30
+ */
31
+ constructor(width, height, config) {
32
+ this.width = width;
33
+ this.height = height;
34
+ this.config = config;
35
+
36
+ // Saccade history for inhibition of return
37
+ this.recentTargets = [];
38
+ this.inhibitionRadius = Math.min(width, height) * 0.1;
39
+
40
+ // Movement prediction
41
+ this.velocityHistory = [];
42
+ this.lastCenter = { x: width / 2, y: height / 2 };
43
+
44
+ // Grid for systematic coverage
45
+ this.gridCells = this._buildCoverageGrid(3, 3);
46
+ this.lastVisitedCell = 4; // Center
47
+
48
+ // State
49
+ this.lastSaccadeTime = 0;
50
+ this.saccadeCount = 0;
51
+ }
52
+
53
+ /**
54
+ * Build a grid for systematic coverage during tracking loss
55
+ * @private
56
+ */
57
+ _buildCoverageGrid(rows, cols) {
58
+ const cells = [];
59
+ const cellW = this.width / cols;
60
+ const cellH = this.height / rows;
61
+
62
+ for (let r = 0; r < rows; r++) {
63
+ for (let c = 0; c < cols; c++) {
64
+ cells.push({
65
+ x: cellW * (c + 0.5),
66
+ y: cellH * (r + 0.5),
67
+ index: r * cols + c,
68
+ lastVisit: 0,
69
+ });
70
+ }
71
+ }
72
+ return cells;
73
+ }
74
+
75
+ /**
76
+ * Compute saccade targets based on current state
77
+ *
78
+ * @param {Object} saliency - Saliency map result
79
+ * @param {Object} currentFovea - Current fovea center {x, y}
80
+ * @param {Object} trackingState - Current tracking state (optional)
81
+ * @returns {SaccadeTarget[]} Priority-ordered list of targets
82
+ */
83
+ computeTargets(saliency, currentFovea, trackingState = null) {
84
+ const targets = [];
85
+ const maxTargets = this.config.MAX_SACCADES_PER_FRAME;
86
+
87
+ // Strategy 1: Follow tracking prediction (highest priority)
88
+ if (trackingState && trackingState.isTracking) {
89
+ const predicted = this._predictTrackingCenter(trackingState);
90
+ if (predicted) {
91
+ targets.push({
92
+ x: predicted.x,
93
+ y: predicted.y,
94
+ priority: 0,
95
+ reason: 'tracking_prediction',
96
+ saliency: 1.0,
97
+ });
98
+ }
99
+ }
100
+
101
+ // Strategy 2: High saliency regions
102
+ if (saliency && saliency.peaks) {
103
+ for (const peak of saliency.peaks) {
104
+ if (targets.length >= maxTargets) break;
105
+
106
+ // Skip if too close to existing targets (inhibition of return)
107
+ if (this._isInhibited(peak.x, peak.y, targets)) continue;
108
+
109
+ if (peak.value > this.config.SALIENCY_THRESHOLD) {
110
+ targets.push({
111
+ x: peak.x,
112
+ y: peak.y,
113
+ priority: targets.length,
114
+ reason: 'saliency_peak',
115
+ saliency: peak.value,
116
+ });
117
+ }
118
+ }
119
+ }
120
+
121
+ // Strategy 3: Systematic grid search (when not tracking)
122
+ if (!trackingState?.isTracking && targets.length < maxTargets) {
123
+ const gridTarget = this._getNextGridCell();
124
+ if (gridTarget && !this._isInhibited(gridTarget.x, gridTarget.y, targets)) {
125
+ targets.push({
126
+ x: gridTarget.x,
127
+ y: gridTarget.y,
128
+ priority: targets.length,
129
+ reason: 'grid_search',
130
+ saliency: 0.5,
131
+ });
132
+ }
133
+ }
134
+
135
+ // Strategy 4: Stay at current center if no better options
136
+ if (targets.length === 0) {
137
+ targets.push({
138
+ x: currentFovea.x,
139
+ y: currentFovea.y,
140
+ priority: 0,
141
+ reason: 'maintain_position',
142
+ saliency: 0.3,
143
+ });
144
+ }
145
+
146
+ // Update history
147
+ this._updateHistory(targets);
148
+
149
+ return targets;
150
+ }
151
+
152
+ /**
153
+ * Predict center of tracking based on current state and velocity
154
+ * @private
155
+ */
156
+ _predictTrackingCenter(trackingState) {
157
+ if (!trackingState.worldMatrix) return null;
158
+
159
+ // Extract center from world matrix
160
+ const matrix = trackingState.worldMatrix;
161
+ const cx = matrix[12] || this.width / 2;
162
+ const cy = matrix[13] || this.height / 2;
163
+
164
+ // Apply velocity-based prediction
165
+ if (this.velocityHistory.length >= 2) {
166
+ const vx = this._computeAverageVelocity('x');
167
+ const vy = this._computeAverageVelocity('y');
168
+
169
+ // Predict 1 frame ahead
170
+ return {
171
+ x: Math.max(0, Math.min(this.width - 1, cx + vx)),
172
+ y: Math.max(0, Math.min(this.height - 1, cy + vy)),
173
+ };
174
+ }
175
+
176
+ return { x: cx, y: cy };
177
+ }
178
+
179
+ /**
180
+ * Compute average velocity from history
181
+ * @private
182
+ */
183
+ _computeAverageVelocity(axis) {
184
+ if (this.velocityHistory.length < 2) return 0;
185
+
186
+ let sum = 0;
187
+ for (let i = 1; i < this.velocityHistory.length; i++) {
188
+ sum += this.velocityHistory[i][axis] - this.velocityHistory[i - 1][axis];
189
+ }
190
+ return sum / (this.velocityHistory.length - 1);
191
+ }
192
+
193
+ /**
194
+ * Check if a location is inhibited (too close to recent targets)
195
+ * @private
196
+ */
197
+ _isInhibited(x, y, currentTargets) {
198
+ const r2 = this.inhibitionRadius ** 2;
199
+
200
+ // Check against current frame targets
201
+ for (const t of currentTargets) {
202
+ const dx = x - t.x;
203
+ const dy = y - t.y;
204
+ if (dx * dx + dy * dy < r2) return true;
205
+ }
206
+
207
+ // Check against recent history
208
+ for (const t of this.recentTargets) {
209
+ const dx = x - t.x;
210
+ const dy = y - t.y;
211
+ if (dx * dx + dy * dy < r2) return true;
212
+ }
213
+
214
+ return false;
215
+ }
216
+
217
+ /**
218
+ * Get next grid cell for systematic search
219
+ * @private
220
+ */
221
+ _getNextGridCell() {
222
+ // Find least recently visited cell
223
+ let oldest = this.gridCells[0];
224
+ let oldestTime = Infinity;
225
+
226
+ for (const cell of this.gridCells) {
227
+ if (cell.lastVisit < oldestTime) {
228
+ oldestTime = cell.lastVisit;
229
+ oldest = cell;
230
+ }
231
+ }
232
+
233
+ oldest.lastVisit = Date.now();
234
+ return oldest;
235
+ }
236
+
237
+ /**
238
+ * Update history with new targets
239
+ * @private
240
+ */
241
+ _updateHistory(targets) {
242
+ // Add to recent targets for inhibition of return
243
+ this.recentTargets.push(...targets);
244
+
245
+ // Keep only last N targets
246
+ const maxHistory = this.config.MOTION_HISTORY_FRAMES * this.config.MAX_SACCADES_PER_FRAME;
247
+ while (this.recentTargets.length > maxHistory) {
248
+ this.recentTargets.shift();
249
+ }
250
+
251
+ // Update velocity history
252
+ if (targets.length > 0) {
253
+ this.velocityHistory.push({ x: targets[0].x, y: targets[0].y });
254
+ while (this.velocityHistory.length > this.config.MOTION_HISTORY_FRAMES) {
255
+ this.velocityHistory.shift();
256
+ }
257
+ this.lastCenter = { x: targets[0].x, y: targets[0].y };
258
+ }
259
+
260
+ this.saccadeCount += targets.length;
261
+ this.lastSaccadeTime = Date.now();
262
+ }
263
+
264
+ /**
265
+ * Get the most likely location of interest based on history
266
+ * @returns {Object} {x, y} of predicted location
267
+ */
268
+ getPredictedLocation() {
269
+ if (this.velocityHistory.length >= 2) {
270
+ const vx = this._computeAverageVelocity('x');
271
+ const vy = this._computeAverageVelocity('y');
272
+ return {
273
+ x: Math.max(0, Math.min(this.width - 1, this.lastCenter.x + vx)),
274
+ y: Math.max(0, Math.min(this.height - 1, this.lastCenter.y + vy)),
275
+ };
276
+ }
277
+ return this.lastCenter;
278
+ }
279
+
280
+ /**
281
+ * Reset controller state
282
+ */
283
+ reset() {
284
+ this.recentTargets = [];
285
+ this.velocityHistory = [];
286
+ this.lastCenter = { x: this.width / 2, y: this.height / 2 };
287
+ this.saccadeCount = 0;
288
+
289
+ for (const cell of this.gridCells) {
290
+ cell.lastVisit = 0;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Update configuration
296
+ */
297
+ configure(config) {
298
+ this.config = { ...this.config, ...config };
299
+ this.inhibitionRadius = Math.min(this.width, this.height) * 0.1;
300
+ }
301
+ }
302
+
303
+ export { SaccadicController };
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Saliency Map Computation
3
+ *
4
+ * Computes visual saliency - regions that "pop out" and attract attention.
5
+ * Used to guide saccadic attention to visually important areas.
6
+ *
7
+ * Implements a simplified Itti-Koch saliency model:
8
+ * - Intensity contrast
9
+ * - Edge density
10
+ * - Local complexity
11
+ *
12
+ * For AR tracking, high-saliency regions often contain:
13
+ * - Corners and edges (good for feature detection)
14
+ * - High-contrast areas (robust to lighting changes)
15
+ * - Texture-rich regions (distinctive for matching)
16
+ */
17
+
18
+ class SaliencyMap {
19
+ /**
20
+ * @param {number} width - Image width
21
+ * @param {number} height - Image height
22
+ */
23
+ constructor(width, height) {
24
+ this.width = width;
25
+ this.height = height;
26
+
27
+ // Downsampled dimensions for efficiency
28
+ this.scale = 8; // Process at 1/8 resolution
29
+ this.scaledW = Math.ceil(width / this.scale);
30
+ this.scaledH = Math.ceil(height / this.scale);
31
+
32
+ // Pre-allocate buffers
33
+ this.intensityMap = new Float32Array(this.scaledW * this.scaledH);
34
+ this.contrastMap = new Float32Array(this.scaledW * this.scaledH);
35
+ this.edgeMap = new Float32Array(this.scaledW * this.scaledH);
36
+ this.saliencyBuffer = new Float32Array(this.scaledW * this.scaledH);
37
+
38
+ // Peak detection parameters
39
+ this.maxPeaks = 5;
40
+ this.suppressionRadius = Math.max(this.scaledW, this.scaledH) * 0.15;
41
+ }
42
+
43
+ /**
44
+ * Compute saliency map for input image
45
+ *
46
+ * @param {Uint8Array} inputData - Grayscale input image
47
+ * @returns {Object} Saliency result with peaks
48
+ */
49
+ compute(inputData) {
50
+ // Step 1: Downsample and compute intensity
51
+ this._downsample(inputData);
52
+
53
+ // Step 2: Compute features
54
+ this._computeContrast();
55
+ this._computeEdges();
56
+
57
+ // Step 3: Combine into saliency map
58
+ this._combineSaliency();
59
+
60
+ // Step 4: Find peaks
61
+ const peaks = this._findPeaks();
62
+
63
+ return {
64
+ map: this.saliencyBuffer,
65
+ width: this.scaledW,
66
+ height: this.scaledH,
67
+ peaks,
68
+ maxSaliency: peaks.length > 0 ? peaks[0].value : 0,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Downsample input to working resolution
74
+ * @private
75
+ */
76
+ _downsample(inputData) {
77
+ const s = this.scale;
78
+ const w = this.width;
79
+
80
+ for (let sy = 0; sy < this.scaledH; sy++) {
81
+ const yStart = sy * s;
82
+ const yEnd = Math.min(yStart + s, this.height);
83
+
84
+ for (let sx = 0; sx < this.scaledW; sx++) {
85
+ const xStart = sx * s;
86
+ const xEnd = Math.min(xStart + s, this.width);
87
+
88
+ let sum = 0;
89
+ let count = 0;
90
+
91
+ for (let y = yStart; y < yEnd; y++) {
92
+ const rowOffset = y * w;
93
+ for (let x = xStart; x < xEnd; x++) {
94
+ sum += inputData[rowOffset + x];
95
+ count++;
96
+ }
97
+ }
98
+
99
+ this.intensityMap[sy * this.scaledW + sx] = sum / count / 255;
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Compute local contrast map
106
+ * @private
107
+ */
108
+ _computeContrast() {
109
+ const w = this.scaledW;
110
+ const h = this.scaledH;
111
+ const intensity = this.intensityMap;
112
+ const contrast = this.contrastMap;
113
+
114
+ // 3x3 local contrast using center-surround
115
+ for (let y = 1; y < h - 1; y++) {
116
+ for (let x = 1; x < w - 1; x++) {
117
+ const idx = y * w + x;
118
+ const center = intensity[idx];
119
+
120
+ // Compute average of 8 neighbors
121
+ let surround = 0;
122
+ surround += intensity[(y - 1) * w + (x - 1)];
123
+ surround += intensity[(y - 1) * w + x];
124
+ surround += intensity[(y - 1) * w + (x + 1)];
125
+ surround += intensity[y * w + (x - 1)];
126
+ surround += intensity[y * w + (x + 1)];
127
+ surround += intensity[(y + 1) * w + (x - 1)];
128
+ surround += intensity[(y + 1) * w + x];
129
+ surround += intensity[(y + 1) * w + (x + 1)];
130
+ surround /= 8;
131
+
132
+ // Contrast is absolute difference
133
+ contrast[idx] = Math.abs(center - surround);
134
+ }
135
+ }
136
+
137
+ // Handle borders
138
+ for (let y = 0; y < h; y++) {
139
+ contrast[y * w] = 0;
140
+ contrast[y * w + w - 1] = 0;
141
+ }
142
+ for (let x = 0; x < w; x++) {
143
+ contrast[x] = 0;
144
+ contrast[(h - 1) * w + x] = 0;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Compute edge density map using Sobel-like operator
150
+ * @private
151
+ */
152
+ _computeEdges() {
153
+ const w = this.scaledW;
154
+ const h = this.scaledH;
155
+ const intensity = this.intensityMap;
156
+ const edges = this.edgeMap;
157
+
158
+ for (let y = 1; y < h - 1; y++) {
159
+ for (let x = 1; x < w - 1; x++) {
160
+ // Simplified Sobel
161
+ const gx =
162
+ -intensity[(y - 1) * w + (x - 1)] + intensity[(y - 1) * w + (x + 1)] +
163
+ -2 * intensity[y * w + (x - 1)] + 2 * intensity[y * w + (x + 1)] +
164
+ -intensity[(y + 1) * w + (x - 1)] + intensity[(y + 1) * w + (x + 1)];
165
+
166
+ const gy =
167
+ -intensity[(y - 1) * w + (x - 1)] - 2 * intensity[(y - 1) * w + x] - intensity[(y - 1) * w + (x + 1)] +
168
+ intensity[(y + 1) * w + (x - 1)] + 2 * intensity[(y + 1) * w + x] + intensity[(y + 1) * w + (x + 1)];
169
+
170
+ edges[y * w + x] = Math.sqrt(gx * gx + gy * gy) / 4; // Normalize
171
+ }
172
+ }
173
+
174
+ // Handle borders
175
+ for (let y = 0; y < h; y++) {
176
+ edges[y * w] = 0;
177
+ edges[y * w + w - 1] = 0;
178
+ }
179
+ for (let x = 0; x < w; x++) {
180
+ edges[x] = 0;
181
+ edges[(h - 1) * w + x] = 0;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Combine features into final saliency map
187
+ * @private
188
+ */
189
+ _combineSaliency() {
190
+ const n = this.saliencyBuffer.length;
191
+ const contrast = this.contrastMap;
192
+ const edges = this.edgeMap;
193
+ const saliency = this.saliencyBuffer;
194
+
195
+ // Weight: 60% contrast, 40% edges
196
+ for (let i = 0; i < n; i++) {
197
+ saliency[i] = contrast[i] * 0.6 + edges[i] * 0.4;
198
+ }
199
+
200
+ // Normalize to [0, 1]
201
+ let max = 0;
202
+ for (let i = 0; i < n; i++) {
203
+ max = Math.max(max, saliency[i]);
204
+ }
205
+
206
+ if (max > 0) {
207
+ for (let i = 0; i < n; i++) {
208
+ saliency[i] /= max;
209
+ }
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Find peaks in saliency map using non-maximum suppression
215
+ * @private
216
+ */
217
+ _findPeaks() {
218
+ const w = this.scaledW;
219
+ const h = this.scaledH;
220
+ const saliency = this.saliencyBuffer;
221
+ const peaks = [];
222
+ const r = this.suppressionRadius;
223
+ const r2 = r * r;
224
+
225
+ // Find all local maxima
226
+ const candidates = [];
227
+ for (let y = 1; y < h - 1; y++) {
228
+ for (let x = 1; x < w - 1; x++) {
229
+ const idx = y * w + x;
230
+ const val = saliency[idx];
231
+
232
+ // Check if local maximum (8-connected)
233
+ if (val > saliency[(y - 1) * w + (x - 1)] &&
234
+ val > saliency[(y - 1) * w + x] &&
235
+ val > saliency[(y - 1) * w + (x + 1)] &&
236
+ val > saliency[y * w + (x - 1)] &&
237
+ val > saliency[y * w + (x + 1)] &&
238
+ val > saliency[(y + 1) * w + (x - 1)] &&
239
+ val > saliency[(y + 1) * w + x] &&
240
+ val > saliency[(y + 1) * w + (x + 1)]) {
241
+ candidates.push({ x, y, value: val });
242
+ }
243
+ }
244
+ }
245
+
246
+ // Sort by value descending
247
+ candidates.sort((a, b) => b.value - a.value);
248
+
249
+ // Non-maximum suppression
250
+ for (const cand of candidates) {
251
+ if (peaks.length >= this.maxPeaks) break;
252
+
253
+ // Check if too close to existing peaks
254
+ let suppress = false;
255
+ for (const peak of peaks) {
256
+ const dx = cand.x - peak.x;
257
+ const dy = cand.y - peak.y;
258
+ if (dx * dx + dy * dy < r2) {
259
+ suppress = true;
260
+ break;
261
+ }
262
+ }
263
+
264
+ if (!suppress) {
265
+ // Convert to original image coordinates
266
+ peaks.push({
267
+ x: (cand.x + 0.5) * this.scale,
268
+ y: (cand.y + 0.5) * this.scale,
269
+ value: cand.value,
270
+ });
271
+ }
272
+ }
273
+
274
+ return peaks;
275
+ }
276
+
277
+ /**
278
+ * Get saliency value at a specific location
279
+ *
280
+ * @param {number} x - X coordinate in original image
281
+ * @param {number} y - Y coordinate in original image
282
+ * @returns {number} Saliency value (0-1)
283
+ */
284
+ getSaliencyAt(x, y) {
285
+ const sx = Math.floor(x / this.scale);
286
+ const sy = Math.floor(y / this.scale);
287
+
288
+ if (sx < 0 || sx >= this.scaledW || sy < 0 || sy >= this.scaledH) {
289
+ return 0;
290
+ }
291
+
292
+ return this.saliencyBuffer[sy * this.scaledW + sx];
293
+ }
294
+ }
295
+
296
+ export { SaliencyMap };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Scale Orchestrator
3
+ *
4
+ * Manages which octaves should be processed based on the current tracking state.
5
+ * Implements temporal consistency and interleave strategies to optimize performance.
6
+ */
7
+ export class ScaleOrchestrator {
8
+ constructor(numOctaves, options = {}) {
9
+ this.numOctaves = numOctaves;
10
+ this.options = {
11
+ interleaveInterval: 10,
12
+ hysteresis: 1, // Number of adjacent octaves to keep
13
+ ...options
14
+ };
15
+
16
+ this.frameCount = 0;
17
+ this.lastActiveOctave = -1;
18
+ this.interleaveOctave = 0;
19
+ }
20
+
21
+ /**
22
+ * Determine which octaves should be processed in the current frame
23
+ *
24
+ * @param {Object} trackingState - Current state of tracking
25
+ * @returns {number[]} Array of octave indices to process
26
+ */
27
+ getOctavesToProcess(trackingState = null) {
28
+ this.frameCount++;
29
+
30
+ // Case 1: No tracking or lost tracking -> Process all octaves
31
+ if (!trackingState || !trackingState.isTracking || trackingState.activeOctave === undefined) {
32
+ this.lastActiveOctave = -1;
33
+ return Array.from({ length: this.numOctaves }, (_, i) => i);
34
+ }
35
+
36
+ const activeScale = trackingState.activeOctave;
37
+ this.lastActiveOctave = activeScale;
38
+
39
+ // Case 2: Active tracking -> Focus on current scale and neighbors
40
+ const octaves = new Set();
41
+
42
+ // Add current and adjacent scales (Hysteresis)
43
+ for (let i = -this.options.hysteresis; i <= this.options.hysteresis; i++) {
44
+ const octave = activeScale + i;
45
+ if (octave >= 0 && octave < this.numOctaves) {
46
+ octaves.add(octave);
47
+ }
48
+ }
49
+
50
+ // Case 3: Interleave - Periodically check a distant octave to ensure we don't "drift"
51
+ if (this.frameCount % this.options.interleaveInterval === 0) {
52
+ this.interleaveOctave = (this.interleaveOctave + 1) % this.numOctaves;
53
+ // If the interleave octave is already being processed, pick the next one
54
+ if (octaves.has(this.interleaveOctave)) {
55
+ this.interleaveOctave = (this.interleaveOctave + 1) % this.numOctaves;
56
+ }
57
+ octaves.add(this.interleaveOctave);
58
+
59
+ if (this.options.debug) {
60
+ console.log(`[ScaleOrchestrator] Interleave check on octave ${this.interleaveOctave}`);
61
+ }
62
+ }
63
+
64
+ const result = Array.from(octaves).sort((a, b) => a - b);
65
+
66
+ if (this.options.debug) {
67
+ console.log(`[ScaleOrchestrator] Active: ${activeScale}, Processing: [${result.join(', ')}]`);
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Reset orchestrator state
75
+ */
76
+ reset() {
77
+ this.frameCount = 0;
78
+ this.lastActiveOctave = -1;
79
+ }
80
+ }