gridstamp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.cursorrules +74 -0
  2. package/CLAUDE.md +61 -0
  3. package/LICENSE +190 -0
  4. package/README.md +107 -0
  5. package/dist/index.js +194 -0
  6. package/package.json +84 -0
  7. package/src/antispoofing/detector.ts +509 -0
  8. package/src/antispoofing/index.ts +7 -0
  9. package/src/gamification/badges.ts +429 -0
  10. package/src/gamification/fleet-leaderboard.ts +293 -0
  11. package/src/gamification/index.ts +44 -0
  12. package/src/gamification/streaks.ts +243 -0
  13. package/src/gamification/trust-tiers.ts +393 -0
  14. package/src/gamification/zone-mastery.ts +256 -0
  15. package/src/index.ts +341 -0
  16. package/src/memory/index.ts +9 -0
  17. package/src/memory/place-cells.ts +279 -0
  18. package/src/memory/spatial-memory.ts +375 -0
  19. package/src/navigation/index.ts +1 -0
  20. package/src/navigation/pathfinding.ts +403 -0
  21. package/src/perception/camera.ts +249 -0
  22. package/src/perception/index.ts +2 -0
  23. package/src/types/index.ts +416 -0
  24. package/src/utils/crypto.ts +94 -0
  25. package/src/utils/index.ts +2 -0
  26. package/src/utils/math.ts +204 -0
  27. package/src/verification/index.ts +9 -0
  28. package/src/verification/spatial-proof.ts +442 -0
  29. package/tests/antispoofing/detector.test.ts +196 -0
  30. package/tests/gamification/badges.test.ts +163 -0
  31. package/tests/gamification/fleet-leaderboard.test.ts +181 -0
  32. package/tests/gamification/streaks.test.ts +158 -0
  33. package/tests/gamification/trust-tiers.test.ts +165 -0
  34. package/tests/gamification/zone-mastery.test.ts +143 -0
  35. package/tests/memory/place-cells.test.ts +128 -0
  36. package/tests/stress/load.test.ts +499 -0
  37. package/tests/stress/security.test.ts +378 -0
  38. package/tests/stress/simulation.test.ts +361 -0
  39. package/tests/utils/crypto.test.ts +115 -0
  40. package/tests/utils/math.test.ts +195 -0
  41. package/tests/verification/spatial-proof.test.ts +299 -0
  42. package/tsconfig.json +26 -0
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Navigation — A* and RRT* pathfinding in 3D space
3
+ *
4
+ * A*: Optimal for grid-based environments (warehouses, structured spaces)
5
+ * RRT*: Optimal for continuous spaces with complex obstacles (outdoor, unstructured)
6
+ *
7
+ * Both integrate with place cell activations for biologically-plausible navigation.
8
+ */
9
+ import type {
10
+ Vec3,
11
+ Path,
12
+ Waypoint,
13
+ PathAlgorithm,
14
+ AABB,
15
+ } from '../types/index.js';
16
+ import { vec3Distance, vec3Sub, vec3Add, vec3Scale, vec3Normalize } from '../utils/math.js';
17
+ import { generateNonce } from '../utils/crypto.js';
18
+
19
+ // ============================================================
20
+ // OCCUPANCY GRID (collision checking)
21
+ // ============================================================
22
+
23
+ /** 3D occupancy grid for collision detection */
24
+ export class OccupancyGrid {
25
+ private grid: Uint8Array;
26
+ readonly resolution: number; // meters per cell
27
+ readonly sizeX: number;
28
+ readonly sizeY: number;
29
+ readonly sizeZ: number;
30
+ readonly origin: Vec3;
31
+
32
+ constructor(
33
+ bounds: AABB,
34
+ resolution: number = 0.1,
35
+ ) {
36
+ this.resolution = resolution;
37
+ this.origin = bounds.min;
38
+ this.sizeX = Math.ceil((bounds.max.x - bounds.min.x) / resolution);
39
+ this.sizeY = Math.ceil((bounds.max.y - bounds.min.y) / resolution);
40
+ this.sizeZ = Math.ceil((bounds.max.z - bounds.min.z) / resolution);
41
+ this.grid = new Uint8Array(this.sizeX * this.sizeY * this.sizeZ);
42
+ }
43
+
44
+ /** Convert world position to grid index */
45
+ private toIndex(pos: Vec3): number | null {
46
+ const ix = Math.floor((pos.x - this.origin.x) / this.resolution);
47
+ const iy = Math.floor((pos.y - this.origin.y) / this.resolution);
48
+ const iz = Math.floor((pos.z - this.origin.z) / this.resolution);
49
+ if (ix < 0 || ix >= this.sizeX || iy < 0 || iy >= this.sizeY || iz < 0 || iz >= this.sizeZ) {
50
+ return null;
51
+ }
52
+ return ix + iy * this.sizeX + iz * this.sizeX * this.sizeY;
53
+ }
54
+
55
+ /** Mark a position as occupied */
56
+ setOccupied(pos: Vec3): void {
57
+ const idx = this.toIndex(pos);
58
+ if (idx !== null) this.grid[idx] = 1;
59
+ }
60
+
61
+ /** Check if a position is free */
62
+ isFree(pos: Vec3): boolean {
63
+ const idx = this.toIndex(pos);
64
+ if (idx === null) return false; // out of bounds = not free
65
+ return this.grid[idx] === 0;
66
+ }
67
+
68
+ /** Check if a straight line between two points is collision-free */
69
+ isLineFree(from: Vec3, to: Vec3, stepSize: number = 0.05): boolean {
70
+ const dist = vec3Distance(from, to);
71
+ const steps = Math.ceil(dist / stepSize);
72
+ for (let i = 0; i <= steps; i++) {
73
+ const t = i / steps;
74
+ const point: Vec3 = {
75
+ x: from.x + (to.x - from.x) * t,
76
+ y: from.y + (to.y - from.y) * t,
77
+ z: from.z + (to.z - from.z) * t,
78
+ };
79
+ if (!this.isFree(point)) return false;
80
+ }
81
+ return true;
82
+ }
83
+ }
84
+
85
+ // ============================================================
86
+ // A* PATHFINDING
87
+ // ============================================================
88
+
89
+ interface AStarNode {
90
+ position: Vec3;
91
+ g: number; // cost from start
92
+ h: number; // heuristic to goal
93
+ f: number; // g + h
94
+ parent: AStarNode | null;
95
+ key: string;
96
+ }
97
+
98
+ function positionKey(pos: Vec3, resolution: number): string {
99
+ const x = Math.round(pos.x / resolution);
100
+ const y = Math.round(pos.y / resolution);
101
+ const z = Math.round(pos.z / resolution);
102
+ return `${x},${y},${z}`;
103
+ }
104
+
105
+ /**
106
+ * A* pathfinding in 3D space
107
+ * Uses 26-connected grid (all diagonal neighbors)
108
+ */
109
+ export function aStarPath(
110
+ start: Vec3,
111
+ goal: Vec3,
112
+ grid: OccupancyGrid,
113
+ maxIterations: number = 100_000,
114
+ ): Vec3[] | null {
115
+ const resolution = grid.resolution;
116
+ const startKey = positionKey(start, resolution);
117
+
118
+ const startNode: AStarNode = {
119
+ position: start,
120
+ g: 0,
121
+ h: vec3Distance(start, goal),
122
+ f: vec3Distance(start, goal),
123
+ parent: null,
124
+ key: startKey,
125
+ };
126
+
127
+ const openSet = new Map<string, AStarNode>();
128
+ const closedSet = new Set<string>();
129
+ openSet.set(startKey, startNode);
130
+
131
+ // 26-connected neighbors (3D)
132
+ const neighbors: Vec3[] = [];
133
+ for (let dx = -1; dx <= 1; dx++) {
134
+ for (let dy = -1; dy <= 1; dy++) {
135
+ for (let dz = -1; dz <= 1; dz++) {
136
+ if (dx === 0 && dy === 0 && dz === 0) continue;
137
+ neighbors.push({
138
+ x: dx * resolution,
139
+ y: dy * resolution,
140
+ z: dz * resolution,
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ let iterations = 0;
147
+ while (openSet.size > 0 && iterations < maxIterations) {
148
+ iterations++;
149
+
150
+ // Find node with lowest f in open set
151
+ let current: AStarNode | null = null;
152
+ for (const node of openSet.values()) {
153
+ if (!current || node.f < current.f) {
154
+ current = node;
155
+ }
156
+ }
157
+ if (!current) break;
158
+
159
+ // Check if we reached the goal
160
+ if (vec3Distance(current.position, goal) < resolution * 1.5) {
161
+ return reconstructPath(current);
162
+ }
163
+
164
+ openSet.delete(current.key);
165
+ closedSet.add(current.key);
166
+
167
+ // Explore neighbors
168
+ for (const offset of neighbors) {
169
+ const neighborPos = vec3Add(current.position, offset);
170
+ const neighborKey = positionKey(neighborPos, resolution);
171
+
172
+ if (closedSet.has(neighborKey)) continue;
173
+ if (!grid.isFree(neighborPos)) continue;
174
+
175
+ const g = current.g + vec3Distance(current.position, neighborPos);
176
+ const existing = openSet.get(neighborKey);
177
+
178
+ if (!existing || g < existing.g) {
179
+ const h = vec3Distance(neighborPos, goal);
180
+ const node: AStarNode = {
181
+ position: neighborPos,
182
+ g,
183
+ h,
184
+ f: g + h,
185
+ parent: current,
186
+ key: neighborKey,
187
+ };
188
+ openSet.set(neighborKey, node);
189
+ }
190
+ }
191
+ }
192
+
193
+ return null; // no path found
194
+ }
195
+
196
+ function reconstructPath(node: AStarNode): Vec3[] {
197
+ const path: Vec3[] = [];
198
+ let current: AStarNode | null = node;
199
+ while (current) {
200
+ path.unshift(current.position);
201
+ current = current.parent;
202
+ }
203
+ return path;
204
+ }
205
+
206
+ // ============================================================
207
+ // RRT* PATHFINDING
208
+ // ============================================================
209
+
210
+ interface RRTNode {
211
+ position: Vec3;
212
+ parent: RRTNode | null;
213
+ cost: number;
214
+ children: RRTNode[];
215
+ }
216
+
217
+ /**
218
+ * RRT* (Rapidly-exploring Random Tree Star)
219
+ * Asymptotically optimal for continuous spaces
220
+ */
221
+ export function rrtStarPath(
222
+ start: Vec3,
223
+ goal: Vec3,
224
+ grid: OccupancyGrid,
225
+ bounds: AABB,
226
+ options: {
227
+ maxIterations?: number;
228
+ stepSize?: number;
229
+ goalBias?: number;
230
+ rewireRadius?: number;
231
+ } = {},
232
+ ): Vec3[] | null {
233
+ const {
234
+ maxIterations = 5000,
235
+ stepSize = 0.3,
236
+ goalBias = 0.1,
237
+ rewireRadius = 0.5,
238
+ } = options;
239
+
240
+ const root: RRTNode = { position: start, parent: null, cost: 0, children: [] };
241
+ const nodes: RRTNode[] = [root];
242
+ let bestGoalNode: RRTNode | null = null;
243
+
244
+ for (let i = 0; i < maxIterations; i++) {
245
+ // Sample random point (with goal bias)
246
+ const sample = Math.random() < goalBias
247
+ ? goal
248
+ : randomPoint(bounds);
249
+
250
+ // Find nearest node in tree
251
+ const nearest = findNearest(nodes, sample);
252
+ if (!nearest) continue;
253
+
254
+ // Steer towards sample
255
+ const direction = vec3Normalize(vec3Sub(sample, nearest.position));
256
+ const newPos = vec3Add(nearest.position, vec3Scale(direction, stepSize));
257
+
258
+ // Check collision
259
+ if (!grid.isFree(newPos)) continue;
260
+ if (!grid.isLineFree(nearest.position, newPos)) continue;
261
+
262
+ // Find nearby nodes for potential rewiring
263
+ const nearby = findNearby(nodes, newPos, rewireRadius);
264
+
265
+ // Choose best parent (lowest cost path)
266
+ let bestParent = nearest;
267
+ let bestCost = nearest.cost + vec3Distance(nearest.position, newPos);
268
+
269
+ for (const candidate of nearby) {
270
+ const candidateCost = candidate.cost + vec3Distance(candidate.position, newPos);
271
+ if (candidateCost < bestCost && grid.isLineFree(candidate.position, newPos)) {
272
+ bestParent = candidate;
273
+ bestCost = candidateCost;
274
+ }
275
+ }
276
+
277
+ // Add new node
278
+ const newNode: RRTNode = {
279
+ position: newPos,
280
+ parent: bestParent,
281
+ cost: bestCost,
282
+ children: [],
283
+ };
284
+ bestParent.children.push(newNode);
285
+ nodes.push(newNode);
286
+
287
+ // Rewire nearby nodes through new node if cheaper
288
+ for (const candidate of nearby) {
289
+ const newCandidateCost = newNode.cost + vec3Distance(newNode.position, candidate.position);
290
+ if (newCandidateCost < candidate.cost && grid.isLineFree(newNode.position, candidate.position)) {
291
+ // Remove from old parent's children
292
+ if (candidate.parent) {
293
+ const idx = candidate.parent.children.indexOf(candidate);
294
+ if (idx >= 0) candidate.parent.children.splice(idx, 1);
295
+ }
296
+ candidate.parent = newNode;
297
+ candidate.cost = newCandidateCost;
298
+ newNode.children.push(candidate);
299
+ propagateCostUpdate(candidate);
300
+ }
301
+ }
302
+
303
+ // Check if we reached the goal
304
+ if (vec3Distance(newPos, goal) < stepSize * 2) {
305
+ if (!bestGoalNode || newNode.cost < bestGoalNode.cost) {
306
+ bestGoalNode = newNode;
307
+ }
308
+ }
309
+ }
310
+
311
+ if (!bestGoalNode) return null;
312
+ return reconstructRRTPath(bestGoalNode);
313
+ }
314
+
315
+ function randomPoint(bounds: AABB): Vec3 {
316
+ return {
317
+ x: bounds.min.x + Math.random() * (bounds.max.x - bounds.min.x),
318
+ y: bounds.min.y + Math.random() * (bounds.max.y - bounds.min.y),
319
+ z: bounds.min.z + Math.random() * (bounds.max.z - bounds.min.z),
320
+ };
321
+ }
322
+
323
+ function findNearest(nodes: RRTNode[], target: Vec3): RRTNode | null {
324
+ let best: RRTNode | null = null;
325
+ let bestDist = Infinity;
326
+ for (const node of nodes) {
327
+ const dist = vec3Distance(node.position, target);
328
+ if (dist < bestDist) {
329
+ bestDist = dist;
330
+ best = node;
331
+ }
332
+ }
333
+ return best;
334
+ }
335
+
336
+ function findNearby(nodes: RRTNode[], target: Vec3, radius: number): RRTNode[] {
337
+ return nodes.filter(n => vec3Distance(n.position, target) <= radius);
338
+ }
339
+
340
+ function propagateCostUpdate(node: RRTNode): void {
341
+ for (const child of node.children) {
342
+ child.cost = node.cost + vec3Distance(node.position, child.position);
343
+ propagateCostUpdate(child);
344
+ }
345
+ }
346
+
347
+ function reconstructRRTPath(node: RRTNode): Vec3[] {
348
+ const path: Vec3[] = [];
349
+ let current: RRTNode | null = node;
350
+ while (current) {
351
+ path.unshift(current.position);
352
+ current = current.parent;
353
+ }
354
+ return path;
355
+ }
356
+
357
+ // ============================================================
358
+ // PATH PLANNING (unified interface)
359
+ // ============================================================
360
+
361
+ /**
362
+ * Plan a path using the specified algorithm
363
+ */
364
+ export function planPath(
365
+ start: Vec3,
366
+ goal: Vec3,
367
+ grid: OccupancyGrid,
368
+ bounds: AABB,
369
+ algorithm: PathAlgorithm = 'a-star' as PathAlgorithm,
370
+ ): Path | null {
371
+ let points: Vec3[] | null;
372
+
373
+ if (algorithm === 'rrt-star') {
374
+ points = rrtStarPath(start, goal, grid, bounds);
375
+ } else {
376
+ points = aStarPath(start, goal, grid);
377
+ }
378
+
379
+ if (!points || points.length === 0) return null;
380
+
381
+ // Convert points to waypoints
382
+ const waypoints: Waypoint[] = points.map(pos => ({
383
+ position: pos,
384
+ tolerance: grid.resolution * 2,
385
+ }));
386
+
387
+ // Calculate total distance
388
+ let totalDistance = 0;
389
+ for (let i = 1; i < points.length; i++) {
390
+ totalDistance += vec3Distance(points[i - 1]!, points[i]!);
391
+ }
392
+
393
+ return {
394
+ id: generateNonce(16),
395
+ waypoints,
396
+ algorithm,
397
+ totalDistance,
398
+ estimatedTime: totalDistance / 0.5, // assume 0.5 m/s robot speed
399
+ cost: totalDistance,
400
+ collisionFree: true,
401
+ createdAt: Date.now(),
402
+ };
403
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Camera abstraction layer
3
+ * Supports Luxonis OAK-D, RealSense, ZED, and simulated cameras
4
+ * Handles frame capture, depth fusion, and HMAC signing at capture time
5
+ */
6
+ import type {
7
+ CameraConfig,
8
+ CameraFrame,
9
+ CameraType,
10
+ DepthMap,
11
+ Pose,
12
+ StereoConfig,
13
+ } from '../types/index.js';
14
+ import { signFrame, generateNonce } from '../utils/crypto.js';
15
+ import { stereoDepth } from '../utils/math.js';
16
+
17
+ /** Camera driver interface — implement per hardware */
18
+ export interface CameraDriver {
19
+ readonly type: CameraType;
20
+ initialize(): Promise<void>;
21
+ captureRGB(): Promise<{ data: Uint8Array; width: number; height: number }>;
22
+ captureDepth(): Promise<DepthMap>;
23
+ getPose(): Promise<Pose | undefined>;
24
+ shutdown(): Promise<void>;
25
+ }
26
+
27
+ /**
28
+ * Frame capture pipeline with integrity guarantees
29
+ *
30
+ * Security properties:
31
+ * - Every frame is HMAC-SHA256 signed at capture time
32
+ * - Monotonic sequence numbers detect frame drops/injection
33
+ * - Timestamps are validated against system clock (±50ms tolerance)
34
+ */
35
+ export class FrameCapture {
36
+ private sequenceNumber = 0;
37
+ private lastTimestamp = 0;
38
+ private readonly maxClockDrift = 50; // ms
39
+
40
+ constructor(
41
+ private readonly driver: CameraDriver,
42
+ private readonly config: CameraConfig,
43
+ private readonly hmacSecret: string,
44
+ ) {
45
+ if (!hmacSecret || hmacSecret.length < 32) {
46
+ throw new Error('HMAC secret must be at least 32 characters');
47
+ }
48
+ }
49
+
50
+ async initialize(): Promise<void> {
51
+ await this.driver.initialize();
52
+ }
53
+
54
+ /**
55
+ * Capture a signed frame with depth and pose
56
+ * Returns a CameraFrame with HMAC signature and sequence number
57
+ */
58
+ async capture(): Promise<CameraFrame> {
59
+ const timestamp = Date.now();
60
+
61
+ // Validate timestamp monotonicity (prevents replay of old frames)
62
+ if (timestamp < this.lastTimestamp) {
63
+ throw new Error(
64
+ `Clock went backwards: ${timestamp} < ${this.lastTimestamp}. ` +
65
+ 'Possible clock manipulation attack.',
66
+ );
67
+ }
68
+
69
+ // Capture RGB + depth in parallel
70
+ const [rgbResult, depthResult, pose] = await Promise.all([
71
+ this.driver.captureRGB(),
72
+ this.driver.captureDepth(),
73
+ this.driver.getPose(),
74
+ ]);
75
+
76
+ // Increment sequence number (monotonic, never resets)
77
+ this.sequenceNumber++;
78
+ this.lastTimestamp = timestamp;
79
+
80
+ // Sign the frame at capture time
81
+ const hmac = signFrame(
82
+ rgbResult.data,
83
+ timestamp,
84
+ this.sequenceNumber,
85
+ this.hmacSecret,
86
+ );
87
+
88
+ const frame: CameraFrame = {
89
+ id: generateNonce(16),
90
+ timestamp,
91
+ rgb: rgbResult.data,
92
+ width: rgbResult.width,
93
+ height: rgbResult.height,
94
+ depth: depthResult.data,
95
+ pose,
96
+ hmac,
97
+ sequenceNumber: this.sequenceNumber,
98
+ };
99
+
100
+ return frame;
101
+ }
102
+
103
+ /** Get current sequence number (for external monitoring) */
104
+ getSequenceNumber(): number {
105
+ return this.sequenceNumber;
106
+ }
107
+
108
+ async shutdown(): Promise<void> {
109
+ await this.driver.shutdown();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Dual-camera system (foveal + peripheral)
115
+ * Foveal: OAK-D Pro (20cm-8m, high detail)
116
+ * Peripheral: OAK-D Long Range (1-35m, wide coverage)
117
+ */
118
+ export class DualCameraSystem {
119
+ private foveal: FrameCapture | undefined;
120
+ private peripheral: FrameCapture | undefined;
121
+
122
+ constructor(
123
+ private readonly fovealDriver: CameraDriver,
124
+ private readonly peripheralDriver: CameraDriver,
125
+ private readonly fovealConfig: CameraConfig,
126
+ private readonly peripheralConfig: CameraConfig,
127
+ private readonly hmacSecret: string,
128
+ ) {}
129
+
130
+ async initialize(): Promise<void> {
131
+ this.foveal = new FrameCapture(this.fovealDriver, this.fovealConfig, this.hmacSecret);
132
+ this.peripheral = new FrameCapture(this.peripheralDriver, this.peripheralConfig, this.hmacSecret);
133
+ await Promise.all([
134
+ this.foveal.initialize(),
135
+ this.peripheral.initialize(),
136
+ ]);
137
+ }
138
+
139
+ /** Capture from both cameras simultaneously */
140
+ async captureStereo(): Promise<{ foveal: CameraFrame; peripheral: CameraFrame }> {
141
+ if (!this.foveal || !this.peripheral) {
142
+ throw new Error('Dual camera system not initialized');
143
+ }
144
+ const [foveal, peripheral] = await Promise.all([
145
+ this.foveal.capture(),
146
+ this.peripheral.capture(),
147
+ ]);
148
+ return { foveal, peripheral };
149
+ }
150
+
151
+ async shutdown(): Promise<void> {
152
+ await Promise.all([
153
+ this.foveal?.shutdown(),
154
+ this.peripheral?.shutdown(),
155
+ ]);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Depth fusion — merge depth maps from multiple cameras
161
+ * Uses confidence-weighted averaging for overlapping regions
162
+ */
163
+ export function fuseDepthMaps(
164
+ maps: readonly DepthMap[],
165
+ ): DepthMap {
166
+ if (maps.length === 0) throw new Error('No depth maps to fuse');
167
+ if (maps.length === 1) return maps[0]!;
168
+
169
+ const reference = maps[0]!;
170
+ const width = reference.width;
171
+ const height = reference.height;
172
+ const fused = new Float32Array(width * height);
173
+ const totalWeight = new Float32Array(width * height);
174
+ let minDepth = Infinity;
175
+ let maxDepth = -Infinity;
176
+
177
+ for (const map of maps) {
178
+ if (map.width !== width || map.height !== height) {
179
+ throw new Error('Depth map dimensions must match for fusion');
180
+ }
181
+ for (let i = 0; i < map.data.length; i++) {
182
+ const depth = map.data[i]!;
183
+ if (!isFinite(depth) || depth <= 0) continue;
184
+
185
+ const confidence = map.confidence?.[i] ?? 0.5;
186
+ fused[i]! += depth * confidence;
187
+ totalWeight[i]! += confidence;
188
+
189
+ if (depth < minDepth) minDepth = depth;
190
+ if (depth > maxDepth) maxDepth = depth;
191
+ }
192
+ }
193
+
194
+ // Normalize by total weight
195
+ for (let i = 0; i < fused.length; i++) {
196
+ if (totalWeight[i]! > 0) {
197
+ fused[i] = fused[i]! / totalWeight[i]!;
198
+ }
199
+ }
200
+
201
+ return {
202
+ data: fused,
203
+ width,
204
+ height,
205
+ minDepth: isFinite(minDepth) ? minDepth : 0,
206
+ maxDepth: isFinite(maxDepth) ? maxDepth : 0,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Compute depth from stereo disparity map
212
+ * Z = f * B / d (fundamental stereo equation)
213
+ */
214
+ export function disparityToDepth(
215
+ disparity: Float32Array,
216
+ config: StereoConfig,
217
+ width: number,
218
+ height: number,
219
+ ): DepthMap {
220
+ const depth = new Float32Array(disparity.length);
221
+ const confidence = new Float32Array(disparity.length);
222
+
223
+ for (let i = 0; i < disparity.length; i++) {
224
+ const d = disparity[i]!;
225
+ if (d <= 0) {
226
+ depth[i] = 0;
227
+ confidence[i] = 0;
228
+ continue;
229
+ }
230
+ const z = stereoDepth(config.intrinsics.fx, config.baseline, d);
231
+ if (z >= config.minDepth && z <= config.maxDepth) {
232
+ depth[i] = z;
233
+ // Confidence decreases with distance (depth noise ∝ z²/fB)
234
+ confidence[i] = Math.max(0, 1 - (z - config.minDepth) / (config.maxDepth - config.minDepth));
235
+ } else {
236
+ depth[i] = 0;
237
+ confidence[i] = 0;
238
+ }
239
+ }
240
+
241
+ return {
242
+ data: depth,
243
+ width,
244
+ height,
245
+ minDepth: config.minDepth,
246
+ maxDepth: config.maxDepth,
247
+ confidence,
248
+ };
249
+ }
@@ -0,0 +1,2 @@
1
+ export { FrameCapture, DualCameraSystem, fuseDepthMaps, disparityToDepth } from './camera.js';
2
+ export type { CameraDriver } from './camera.js';