react-native-image-stitcher 0.3.0 → 0.4.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.
@@ -0,0 +1,605 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * PanoramaSettings (v0.4) — engine-discriminated, hierarchical
4
+ * settings types.
5
+ *
6
+ * Background
7
+ * ──────────
8
+ *
9
+ * Pre-v0.4 the lib exported a single flat `PanoramaSettings`
10
+ * interface with 45+ fields covering three unrelated stitching
11
+ * engines (batch-keyframe, hybrid, slit-scan). The 2026-05-22
12
+ * audit (CHANGELOG entry for v0.3.0) traced every field's actual
13
+ * native consumer and proved:
14
+ *
15
+ * • batch-keyframe and the live engines (hybrid + slit-scan)
16
+ * share **zero** settings — they read disjoint subsets of
17
+ * the flat interface.
18
+ * • ~10 fields had no native consumer at all (dead surface).
19
+ * • The <Camera> public component hardcodes `engine:
20
+ * 'batch-keyframe'` and never reaches the slit-scan / hybrid
21
+ * branches.
22
+ *
23
+ * v0.4 splits the flat interface into three engine-specific types:
24
+ *
25
+ * • `PanoramaSettings` — what <Camera> consumes (batch-keyframe).
26
+ * • `SlitscanSettings` — for Layer 2 hosts using the slit-scan
27
+ * engine (incremental.start({ engine:
28
+ * 'slitscan-*', ... })).
29
+ * • `HybridSettings` — for the RetaiLens-specific hybrid live
30
+ * engine. Exported for completeness;
31
+ * most consumers won't touch it.
32
+ *
33
+ * Each type carries only the fields its target engine actually
34
+ * reads. Sub-objects (`stitcher`, `frameSelection`, `flow`,
35
+ * `painting`, `registration`, `plane`, `ncc2d`, `emaSmoothing`,
36
+ * `panAxisLock`) group related knobs so the modal can render
37
+ * collapsible sections that match the type tree.
38
+ *
39
+ * Migration
40
+ * ─────────
41
+ *
42
+ * No automated migration helper. v0.4 is a clean break; the
43
+ * v0.3 `PanoramaSettings` type is deleted. Consumers (notably
44
+ * `retailens-camera-sdk`) update their settings literals to match
45
+ * the new shape. See the v0.4.0 CHANGELOG entry for the field-
46
+ * by-field mapping.
47
+ */
48
+
49
+
50
+ // ═════════════════════════════════════════════════════════════════════
51
+ // CaptureBaseSettings — fields common to ALL engine-specific types.
52
+ // Extracted in response to a code-reviewer DRY flag (3-way duplication
53
+ // across PanoramaSettings / SlitscanSettings / HybridSettings).
54
+ // ═════════════════════════════════════════════════════════════════════
55
+
56
+ export interface CaptureBaseSettings {
57
+ /**
58
+ * Which camera + tracking source feeds the engine:
59
+ *
60
+ * • `'ar'` — ARKit (iOS) / ARCore (Android) session. Rich
61
+ * pose with real translation. Required for
62
+ * plane-projected slit-scan; recommended for
63
+ * batch-keyframe whenever the device supports
64
+ * it (the auto-resolver's translation signal
65
+ * comes from AR pose).
66
+ * • `'non-ar'` — vision-camera fallback. Gyro-integrated yaw
67
+ * + pitch only; no translation from pose. The
68
+ * JS-side IMU translation gate fills in the
69
+ * translation signal. Required on devices
70
+ * without ARKit/ARCore support.
71
+ */
72
+ captureSource: 'ar' | 'non-ar';
73
+
74
+ /**
75
+ * Show the lib's built-in diagnostic overlay (memory pill,
76
+ * keyframe pill, orientation pill, stitch-stats toast, detailed
77
+ * metrics block). Default `false` so end-users don't see them.
78
+ * Hosts that compose their own debug surface can leave this off
79
+ * and mount the individual `Capture*Pill` components themselves.
80
+ */
81
+ debug: boolean;
82
+ }
83
+
84
+
85
+ // ═════════════════════════════════════════════════════════════════════
86
+ // PanoramaSettings — what <Camera> uses (batch-keyframe engine).
87
+ // ═════════════════════════════════════════════════════════════════════
88
+
89
+ /**
90
+ * Top-level settings for the standard panorama capture flow
91
+ * exposed by <Camera>. Engine is fixed to batch-keyframe internally;
92
+ * the only mode choice exposed at this level is `captureSource`
93
+ * (AR-backed vs vision-camera fallback) and the `stitcher` /
94
+ * `frameSelection` sub-trees.
95
+ */
96
+ export interface PanoramaSettings extends CaptureBaseSettings {
97
+ /** cv::Stitcher pipeline configuration (applied at finalize). */
98
+ stitcher: BatchStitcherSettings;
99
+
100
+ /** Per-frame keyframe-selection gate configuration. */
101
+ frameSelection: FrameSelectionSettings;
102
+ }
103
+
104
+
105
+ /**
106
+ * cv::Stitcher tuning — these knobs reach the C++ stitcher at
107
+ * `finalize()` time, after all keyframes are collected. They have
108
+ * no effect on per-frame selection.
109
+ */
110
+ export interface BatchStitcherSettings {
111
+ /**
112
+ * cv::Stitcher pipeline mode.
113
+ *
114
+ * • `'auto'` (default) — engine looks at the
115
+ * translation/rotation ratio between first + last accepted
116
+ * keyframe poses (and, in non-AR mode, the IMU translation
117
+ * accumulator) and picks `'panorama'` or `'scans'` at
118
+ * finalize.
119
+ * • `'panorama'` — rotation-only pipeline (ORB + BA-Ray +
120
+ * SphericalWarper). Best for "rotate phone in place" pans.
121
+ * Diverges on translation-heavy input.
122
+ * • `'scans'` — affine pipeline (Affine matcher + BA-Affine +
123
+ * PlaneWarper). Best for "walk past a shelf" captures.
124
+ * Slight quality drop on pure rotation, never diverges.
125
+ *
126
+ * Both platforms now honour this and the auto-resolver. Both
127
+ * also retry with the OPPOSITE mode if the configured mode
128
+ * produces degenerate camera params (warpRoi too large).
129
+ */
130
+ stitchMode: 'auto' | 'panorama' | 'scans';
131
+
132
+ /**
133
+ * Output projection. PANORAMA mode uses this directly; SCANS
134
+ * hard-wires PlaneWarper internally and ignores this field.
135
+ */
136
+ warperType: 'plane' | 'cylindrical' | 'spherical';
137
+
138
+ /**
139
+ * Pixel blender for the warped frames. `'multiband'` produces
140
+ * cleaner seams but holds all warped frames in memory; `'feather'`
141
+ * streams and uses less peak memory.
142
+ */
143
+ blenderType: 'multiband' | 'feather';
144
+
145
+ /**
146
+ * Seam-finder strategy. `'graphcut'` finds optimal seams before
147
+ * blending (pair with multiband); `'skip'` streams warp+feed (pair
148
+ * with feather for the lowest-memory configuration).
149
+ */
150
+ seamFinderType: 'graphcut' | 'skip';
151
+
152
+ /**
153
+ * Output crop strategy. `false` (default) crops to the bounding
154
+ * rectangle of non-black pixels. `true` runs the
155
+ * max-inscribed-rectangle + morph-close pipeline — cleaner output
156
+ * with no black corners, more CPU at finalize.
157
+ */
158
+ enableMaxInscribedRectCrop: boolean;
159
+ }
160
+
161
+
162
+ /**
163
+ * KeyframeGate tuning — these knobs control which incoming frames
164
+ * become keyframes. The mode selects the strategy (passthrough,
165
+ * plane-overlap, or sparse optical flow); the `flow` sub-tree is
166
+ * only consulted when `mode === 'flow-based'`.
167
+ */
168
+ export interface FrameSelectionSettings {
169
+ /**
170
+ * Frame selection strategy:
171
+ *
172
+ * • `'time-based'` — gate disabled. Every JS-driver / AR
173
+ * frame becomes a keyframe up to
174
+ * `maxKeyframes`. Useful for testing or
175
+ * when the host wants to do its own
176
+ * keyframe selection upstream.
177
+ * • `'pose-based'` — plane-overlap novelty (when a plane is
178
+ * latched) or angular-delta fallback (no
179
+ * plane). Cheap to evaluate but conservative
180
+ * about pure-rotation motion.
181
+ * • `'flow-based'` — sparse Shi-Tomasi corners + KLT tracking.
182
+ * More expensive (~3–5 ms per AR frame on
183
+ * a Galaxy A35) but accurate for translation.
184
+ * The default for v0.3+.
185
+ */
186
+ mode: 'time-based' | 'pose-based' | 'flow-based';
187
+
188
+ /**
189
+ * Hard cap on accepted keyframes per capture. Clamped to
190
+ * `[3, 10]` natively. Higher is rarely useful: cv::Stitcher
191
+ * convergence degrades past ~8-10 frames, and the per-keyframe
192
+ * disk + memory cost adds up fast at 4K+ resolutions.
193
+ */
194
+ maxKeyframes: number;
195
+
196
+ /**
197
+ * Required NEW-content fraction (0..1) for a candidate frame to
198
+ * be accepted. Default 0.20 = 20% novel content per accept.
199
+ * Lower = more frames accepted, larger panoramas. Higher = fewer
200
+ * frames, faster captures but more conservative about coverage.
201
+ * Clamped to `[0.10, 0.80]` natively
202
+ * (`IncrementalStitcher.swift:962`).
203
+ */
204
+ overlapThreshold: number;
205
+
206
+ /**
207
+ * Sparse-optical-flow strategy tunables. Consulted only when
208
+ * `mode === 'flow-based'`; safe to omit otherwise. Defaults
209
+ * track [DEFAULT_PANORAMA_SETTINGS.frameSelection.flow].
210
+ */
211
+ flow?: FlowGateSettings;
212
+ }
213
+
214
+
215
+ /**
216
+ * Sparse-flow KLT tuning for the gate. All ranges are enforced
217
+ * (clamped silently) at the native boundary.
218
+ */
219
+ export interface FlowGateSettings {
220
+ /**
221
+ * Percentile used to aggregate the per-feature absolute
222
+ * displacements into a single per-axis novelty estimate. Default
223
+ * 0.85 (V16 change from the pre-V16 median of 0.50). Higher
224
+ * percentile picks up leading-edge motion sooner; lower is more
225
+ * conservative. Clamped to `[0.50, 0.99]`.
226
+ */
227
+ noveltyPercentile: number;
228
+
229
+ /**
230
+ * Caller-side throttle: evaluate the Flow strategy every Nth
231
+ * frame instead of every frame. Default 5 (≈ 6 Hz at 30 Hz
232
+ * ARCore). Pure CPU savings; doesn't change WHICH frames are
233
+ * accepted. Clamped to `[1, 10]`.
234
+ */
235
+ evalEveryNFrames: number;
236
+
237
+ /**
238
+ * Translation budget in centimetres. When > 0, the gate
239
+ * force-accepts the next frame after the operator has translated
240
+ * more than this distance since the last accepted keyframe — even
241
+ * when novelty is below `overlapThreshold`. Bounds parallax
242
+ * between adjacent keyframes so the stitcher's matcher sees
243
+ * inputs it can handle. Default 50. `0` disables. Clamped
244
+ * to `[0, 100]`.
245
+ */
246
+ maxTranslationCm: number;
247
+
248
+ /**
249
+ * Shi-Tomasi corner count. Default 150; clamped to `[50, 300]`.
250
+ * Higher = more robust median, slower detect (~15-25 ms at 150
251
+ * on Galaxy A35).
252
+ */
253
+ maxCorners: number;
254
+
255
+ /**
256
+ * Shi-Tomasi quality level. Default 0.01; clamped to
257
+ * `[0.005, 0.05]`. Lower lets weaker corners in (more candidate
258
+ * points, more KLT noise); higher demands stronger corners.
259
+ */
260
+ qualityLevel: number;
261
+
262
+ /**
263
+ * Shi-Tomasi minimum distance between detected corners, in
264
+ * working-resolution pixels (the gate downscales the input
265
+ * internally to a 720-px-longest-side working frame). Default
266
+ * 10; clamped to `[1, 50]`. Higher = more spatially-spread
267
+ * features = more representative median.
268
+ */
269
+ minDistance: number;
270
+ }
271
+
272
+
273
+ /**
274
+ * Canonical FlowGateSettings defaults, exported as a standalone
275
+ * constant so consumers (the bridge, the modal, prop translators)
276
+ * can reach the values WITHOUT typing
277
+ * `DEFAULT_PANORAMA_SETTINGS.frameSelection.flow!.X` — the
278
+ * non-null-assertion form is brittle (will start crashing at
279
+ * runtime the moment someone "cleans up" the default tree and
280
+ * makes `flow` undefined in `DEFAULT_PANORAMA_SETTINGS`). Lifted
281
+ * out 2026-05-22 in the F10 Phase 2 review (NIT-4).
282
+ *
283
+ * Numerical values mirror the v0.3 defaults; they're verified
284
+ * against the native engine's compiled-in fallback values
285
+ * (`IncrementalStitcher.swift:1003-1029`, `IncrementalStitcher.kt:419-445`)
286
+ * — discrepancies are flagged in the v0.3.0 audit and resolved by
287
+ * the bridge always-emitting these on the wire (see
288
+ * `PanoramaSettingsBridge.ts:panoramaSettingsToNativeConfig`).
289
+ */
290
+ export const DEFAULT_FLOW_GATE_SETTINGS: FlowGateSettings = {
291
+ noveltyPercentile: 0.85,
292
+ evalEveryNFrames: 5,
293
+ maxTranslationCm: 50,
294
+ maxCorners: 150,
295
+ qualityLevel: 0.01,
296
+ minDistance: 10,
297
+ };
298
+
299
+
300
+ export const DEFAULT_PANORAMA_SETTINGS: PanoramaSettings = {
301
+ captureSource: 'ar',
302
+ debug: false,
303
+ stitcher: {
304
+ stitchMode: 'auto',
305
+ warperType: 'plane',
306
+ blenderType: 'multiband',
307
+ seamFinderType: 'graphcut',
308
+ enableMaxInscribedRectCrop: false,
309
+ },
310
+ frameSelection: {
311
+ mode: 'flow-based',
312
+ maxKeyframes: 6,
313
+ overlapThreshold: 0.20,
314
+ flow: DEFAULT_FLOW_GATE_SETTINGS,
315
+ },
316
+ };
317
+
318
+
319
+ // ═════════════════════════════════════════════════════════════════════
320
+ // SlitscanSettings — Layer 2 hosts using the slit-scan engine.
321
+ // ═════════════════════════════════════════════════════════════════════
322
+
323
+ /**
324
+ * Settings for slit-scan stitching engines (`slitscan-rotate`,
325
+ * `slitscan-both`, `firstwins-rectilinear`). Reached via
326
+ * `incremental.start({ engine: '<variant>', config: { ... } })`,
327
+ * NOT via <Camera> (which always uses batch-keyframe). Each
328
+ * sub-tree corresponds to a section of the native `RLISStitcherConfig`
329
+ * the slit-scan engine reads at start.
330
+ *
331
+ * Field-by-field native consumer references are documented in
332
+ * `OpenCVSlitScanStitcher.mm` / `OpenCVIncrementalStitcher.h`.
333
+ */
334
+ export interface SlitscanSettings extends CaptureBaseSettings {
335
+ /**
336
+ * Which slit-scan variant the engine runs. All three share the
337
+ * same painting + registration + plane configuration; they differ
338
+ * in their internal motion model (rotation-only vs combined
339
+ * translation+rotation, and slit position).
340
+ *
341
+ * • `'slitscan-rotate'` — preferred name; rotation-only
342
+ * motion model.
343
+ * • `'slitscan-both'` — combined translation + rotation
344
+ * motion model.
345
+ * • `'firstwins-rectilinear'` — legacy alias of
346
+ * `'slitscan-rotate'` (V13.0a naming). Accepted natively
347
+ * but new code should prefer the canonical name.
348
+ */
349
+ variant: 'slitscan-rotate' | 'slitscan-both' | 'firstwins-rectilinear';
350
+
351
+ /** Where the per-accept slit is taken from + how it's blended. */
352
+ painting: SlitscanPaintingSettings;
353
+
354
+ /** Frame-to-frame registration (NCC + RANSAC + triangulation). */
355
+ registration: SlitscanRegistrationSettings;
356
+
357
+ /** Plane projection (ARKit-detected, virtual, or disabled). */
358
+ plane: PlaneProjectionSettings;
359
+
360
+ /**
361
+ * Advanced motion-tuning knobs that the v0.3 modal never exposed.
362
+ * Both are read by the native side
363
+ * (`IncrementalStitcher.swift:1074, 1077`) and have sensible
364
+ * defaults; most consumers can leave this field undefined.
365
+ */
366
+ advanced?: SlitscanAdvancedSettings;
367
+ }
368
+
369
+
370
+ export interface SlitscanAdvancedSettings {
371
+ /**
372
+ * Fraction of the pan-axis sensor extent used to compute the
373
+ * per-frame slit width. Range `[0.05, 0.90]`, default 0.70
374
+ * (engine internal). Higher = wider slits = fewer accepts per
375
+ * pan. Set this only if you know what the slit-scan motion
376
+ * model needs for your specific capture geometry.
377
+ * Native key: `kPanAxisFractionRect`.
378
+ */
379
+ panAxisFractionRect?: number;
380
+
381
+ /**
382
+ * Minimum pan-axis delta (in canvas pixels) between consecutive
383
+ * accepted strips. Acts as a hard floor below which subsequent
384
+ * frames are rejected regardless of NCC scores. Range
385
+ * `[0, 500]`, default 0 (no floor). Native key:
386
+ * `kMinAcceptDeltaPx`.
387
+ */
388
+ minAcceptDeltaPx?: number;
389
+ }
390
+
391
+
392
+ export interface SlitscanPaintingSettings {
393
+ /**
394
+ * How new strips are blended into already-painted canvas pixels.
395
+ *
396
+ * • `'FirstPaintedWins'` (default) — preserve the first frame's
397
+ * content at any pixel; later strips don't overwrite.
398
+ * • `'FeatherBlend'` — alpha-blend new strips into
399
+ * already-painted areas at slit boundaries. Smooths visible
400
+ * seams when many narrow slits stack.
401
+ */
402
+ paintMode: 'FirstPaintedWins' | 'FeatherBlend';
403
+
404
+ /**
405
+ * Where on the camera frame the per-accept slit is sampled from.
406
+ * For a typical landscape vertical pan tilting DOWN, the leading
407
+ * edge (new content) is at the BOTTOM of the camera frame; for
408
+ * upward tilt, it's at the TOP. `'Center'` is the V13.x default.
409
+ */
410
+ sliverPosition: 'Center' | 'Bottom' | 'Top';
411
+
412
+ /**
413
+ * When `true`, the very first frame's FULL frame is painted onto
414
+ * the canvas (not just the configured slit clip). Default
415
+ * `true` — gives the panorama a wider initial anchor that
416
+ * subsequent slits extend from. Set false if you want strict
417
+ * slit-only behaviour even on the first frame.
418
+ */
419
+ firstFrameFullFrame: boolean;
420
+ }
421
+
422
+
423
+ export interface SlitscanRegistrationSettings {
424
+ /**
425
+ * 3D triangulation step. Cross-references features across
426
+ * multiple frames to estimate scene depth. Default `false` (off);
427
+ * adds latency, useful for parallax-heavy captures.
428
+ */
429
+ enableTriangulation: boolean;
430
+
431
+ /**
432
+ * Triangulation accumulator — when `enableTriangulation` is on,
433
+ * keeps a running pose graph across the whole capture. Default
434
+ * `false` (off); needed for multi-shot fusion.
435
+ */
436
+ enableTriAccumulator: boolean;
437
+
438
+ /**
439
+ * RANSAC homography fit per pair. Adds robustness to feature
440
+ * matching at the cost of a few ms per frame. Default `false`.
441
+ */
442
+ enableRansacHomography: boolean;
443
+
444
+ /**
445
+ * 1D NCC strip alignment. Present iff enabled. Default
446
+ * undefined (disabled); engine uses pure feature matching.
447
+ */
448
+ ncc1d?: Ncc1dSettings;
449
+
450
+ /**
451
+ * 2D NCC strip alignment. Present iff enabled. More expensive
452
+ * than 1D NCC; needed for shelf-scan captures with vertical
453
+ * misalignment. Default undefined (disabled).
454
+ */
455
+ ncc2d?: Ncc2dSettings;
456
+ }
457
+
458
+
459
+ export interface Ncc1dSettings {
460
+ /**
461
+ * Search radius in working-resolution pixels (along the pan axis).
462
+ * Clamped to `[5, 60]`. Default 15 when the field is set.
463
+ */
464
+ searchRadius: number;
465
+ }
466
+
467
+
468
+ export interface Ncc2dSettings {
469
+ /**
470
+ * 2D search margin in pixels (rectangular region around the
471
+ * predicted strip position). Clamped to `[4, 60]`. Default 12.
472
+ */
473
+ searchMargin: number;
474
+
475
+ /**
476
+ * Minimum NCC score to accept a match. Below this the engine
477
+ * falls back to the predicted (pose-only) position. Clamped
478
+ * to `[0.30, 0.99]`. Default 0.99 (only accept very strong
479
+ * matches; the canvas falls back to pose-only quickly).
480
+ */
481
+ confidenceThreshold: number;
482
+
483
+ /**
484
+ * EMA smoothing of the NCC-derived offset across consecutive
485
+ * strips. Present iff enabled. Default undefined. Useful
486
+ * for jittery captures.
487
+ */
488
+ emaSmoothing?: { alpha: number };
489
+
490
+ /**
491
+ * Pan-axis-lock — when enabled, the NCC offset is constrained
492
+ * to the dominant pan axis (cross-axis movement bounded by
493
+ * `crossAxisLockPx`). Useful when the operator's hand wobble
494
+ * introduces unwanted cross-axis motion. Present iff enabled.
495
+ */
496
+ panAxisLock?: { crossAxisLockPx: number };
497
+ }
498
+
499
+
500
+ export interface PlaneProjectionSettings {
501
+ /**
502
+ * Where the plane the slit-scan projects onto comes from.
503
+ *
504
+ * • `'Disabled'` — no plane projection; engine runs
505
+ * its baseline slit-scan path.
506
+ * • `'ARKitDetected'` — use the first vertical plane that
507
+ * ARKit/ARCore finds AND whose normal
508
+ * aligns with the camera (filtered by
509
+ * `alignmentThreshold`). Requires
510
+ * `captureSource === 'ar'`.
511
+ * • `'Virtual'` — synthesise a plane at a fixed depth
512
+ * (`virtualDepthMeters`) in front of the
513
+ * camera at first-frame pose. No
514
+ * ARKit dependency.
515
+ */
516
+ source: 'Disabled' | 'ARKitDetected' | 'Virtual';
517
+
518
+ /**
519
+ * How frames are warped onto the plane. Only consulted when
520
+ * `source !== 'Disabled'`. Default `'Rectified'` for slit-scan.
521
+ */
522
+ projectionStyle?: 'Trapezoidal' | 'Rectified';
523
+
524
+ /**
525
+ * Depth in metres for `source === 'Virtual'`. Range `[0.3, 5.0]`,
526
+ * default 1.5. Set close to the actual shelf distance for the
527
+ * cleanest projection.
528
+ */
529
+ virtualDepthMeters?: number;
530
+
531
+ /**
532
+ * Minimum `|planeNormal · cameraForward|` for an ARKit-detected
533
+ * plane to be accepted (when `source === 'ARKitDetected'`).
534
+ * Range `[0, 1]`, default 0.6 (≈ 53° max off-axis). Higher =
535
+ * stricter, only accept very-on-axis planes.
536
+ */
537
+ alignmentThreshold?: number;
538
+ }
539
+
540
+
541
+ export const DEFAULT_SLITSCAN_SETTINGS: SlitscanSettings = {
542
+ captureSource: 'ar',
543
+ debug: false,
544
+ variant: 'slitscan-rotate',
545
+ painting: {
546
+ paintMode: 'FirstPaintedWins',
547
+ sliverPosition: 'Bottom',
548
+ firstFrameFullFrame: true,
549
+ },
550
+ registration: {
551
+ enableTriangulation: false,
552
+ enableTriAccumulator: false,
553
+ enableRansacHomography: false,
554
+ // ncc1d / ncc2d omitted — both disabled by default.
555
+ },
556
+ plane: {
557
+ source: 'ARKitDetected',
558
+ projectionStyle: 'Rectified',
559
+ virtualDepthMeters: 1.5,
560
+ alignmentThreshold: 0.6,
561
+ },
562
+ };
563
+
564
+
565
+ // ═════════════════════════════════════════════════════════════════════
566
+ // HybridSettings — RetaiLens-specific live engine.
567
+ // ═════════════════════════════════════════════════════════════════════
568
+
569
+ /**
570
+ * Settings for the hybrid live-compositing engine
571
+ * (`incremental.start({ engine: 'hybrid', ... })`). Most consumers
572
+ * won't touch this — the hybrid engine is RetaiLens-specific and
573
+ * the public lib's batch-keyframe pipeline is a better fit for
574
+ * general-purpose captures. Exported here for completeness.
575
+ *
576
+ * Important: the hybrid engine has internal preset paths
577
+ * (`OpenCVIncrementalStitcher.mm:139-180`) that hard-set
578
+ * `enableTriangulation`, `enable2dNcc`, `enableRansacHomography`,
579
+ * `planeSource = Disabled`, etc. Code-reviewer flagged that
580
+ * exposing those fields would be misleading — the engine clobbers
581
+ * any overrides. So this type is intentionally minimal: only
582
+ * `projection` is reliably operator-tunable. Hosts that need to
583
+ * reach deeper-level hybrid knobs can pass a raw config dict to
584
+ * `incremental.start()` directly (Layer 2 escape hatch).
585
+ */
586
+ export interface HybridSettings extends CaptureBaseSettings {
587
+ /**
588
+ * Internal projection during real-time compositing. Independent
589
+ * from the panorama-stitcher's warperType (which doesn't apply
590
+ * to the hybrid engine — its output is the live canvas directly).
591
+ *
592
+ * Note: only effective in the rotation-only preset path (hybrid
593
+ * preset 1). In the other hybrid presets the engine forces
594
+ * Planar internally regardless of this setting. Native source:
595
+ * `OpenCVIncrementalStitcher.mm:146,161,180`.
596
+ */
597
+ projection: 'Cylindrical' | 'Planar';
598
+ }
599
+
600
+
601
+ export const DEFAULT_HYBRID_SETTINGS: HybridSettings = {
602
+ captureSource: 'ar',
603
+ debug: false,
604
+ projection: 'Planar',
605
+ };