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,238 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * PanoramaSettingsBridge — JS-side adapters that convert the v0.4
4
+ * typed `PanoramaSettings` / `SlitscanSettings` / `HybridSettings`
5
+ * shape into the flat `configOverrides` dictionary the native
6
+ * bridges read.
7
+ *
8
+ * Why this file exists
9
+ * ────────────────────
10
+ *
11
+ * The v0.4 types use hierarchical sub-trees (`stitcher`,
12
+ * `frameSelection.flow`, `painting`, `registration.ncc1d`,
13
+ * `registration.ncc2d.emaSmoothing`, `plane`, …) to give consumers
14
+ * a clean, ergonomic settings surface that mirrors the native
15
+ * engine's domain. But the native bridges (iOS Swift's
16
+ * `applyConfigOverrides`, Android Kotlin's `IncrementalStitcher.start`)
17
+ * read a FLAT dictionary of native-named keys (e.g. `nccSearchRadius1d`,
18
+ * `enable1dNcc`, `ncc2dEmaAlpha`, `flowMaxTranslationCm`).
19
+ *
20
+ * Two semantic gaps to bridge:
21
+ *
22
+ * 1. **Naming.** JS `registration.ncc1d.searchRadius` →
23
+ * native `nccSearchRadius1d`. JS `painting.paintMode` →
24
+ * native `paintMode` (same). Etc.
25
+ *
26
+ * 2. **Presence-as-enable.** The native side reads explicit
27
+ * `enable1dNcc`, `enable2dNcc`, `enableNcc2dEmaSmoothing`,
28
+ * `enableNcc2dPanAxisLock` booleans. JS models these as
29
+ * optional sub-objects (sub-object present ⇒ enabled). This
30
+ * adapter flattens the booleans for the wire.
31
+ *
32
+ * 3. **Skipped engine defaults.** Hybrid engine presets internally
33
+ * clobber most fields (see HybridSettings JSDoc), so we don't
34
+ * send overrides that would be ignored — just the small useful
35
+ * surface.
36
+ *
37
+ * The Camera component calls `panoramaSettingsToNativeConfig` once
38
+ * per capture start to produce the value passed as
39
+ * `incremental.start({ config: … })`. Layer 2 callers building
40
+ * SlitscanSettings or HybridSettings call the matching adapter
41
+ * before reaching `incremental.start()`.
42
+ */
43
+
44
+ import {
45
+ DEFAULT_FLOW_GATE_SETTINGS,
46
+ type PanoramaSettings,
47
+ type SlitscanSettings,
48
+ type HybridSettings,
49
+ } from './PanoramaSettings';
50
+
51
+
52
+ /**
53
+ * Flat config dictionary type — what the native bridges expect.
54
+ * Indexed by the native-side key name; values are platform-
55
+ * marshallable (booleans / numbers / strings). Keep this type
56
+ * loose: native validates each key individually, and silently
57
+ * ignores keys it doesn't recognise.
58
+ */
59
+ export type NativeConfigDict = Record<string, boolean | number | string>;
60
+
61
+
62
+ /**
63
+ * Convert a v0.4 PanoramaSettings tree into the flat dict the
64
+ * batch-keyframe native side reads. Maps every consumed field
65
+ * exactly once and skips fields the engine doesn't reach.
66
+ *
67
+ * Verified against:
68
+ * - iOS `IncrementalStitcher.swift:810-960` (batch path)
69
+ * - Android `IncrementalStitcher.kt:280-430` (batch path)
70
+ */
71
+ export function panoramaSettingsToNativeConfig(
72
+ s: PanoramaSettings,
73
+ ): NativeConfigDict {
74
+ const cfg: NativeConfigDict = {
75
+ // ── Cross-cutting ────────────────────────────────────────────
76
+ captureSource: s.captureSource,
77
+
78
+ // ── BatchStitcherSettings → cv::Stitcher knobs ───────────────
79
+ stitchMode: s.stitcher.stitchMode,
80
+ warperType: s.stitcher.warperType,
81
+ blenderType: s.stitcher.blenderType,
82
+ seamFinderType: s.stitcher.seamFinderType,
83
+ enableMaxInscribedRectCrop: s.stitcher.enableMaxInscribedRectCrop,
84
+
85
+ // ── FrameSelectionSettings → KeyframeGate knobs ──────────────
86
+ frameSelectionMode: s.frameSelection.mode,
87
+ keyframeMaxCount: s.frameSelection.maxKeyframes,
88
+ keyframeOverlapThreshold: s.frameSelection.overlapThreshold,
89
+ };
90
+
91
+ // Flow strategy knobs — always serialised, regardless of
92
+ // `frameSelection.mode`. Two reasons:
93
+ //
94
+ // 1. Mode-flip-mid-session: hosts can change `mode` without
95
+ // restarting capture; consistent flow serialisation means
96
+ // `'time-based' → 'flow-based'` mid-session doesn't slip
97
+ // back to stale native-side defaults. Native ignores these
98
+ // keys when the active mode doesn't use them.
99
+ //
100
+ // 2. **Native compiled-in defaults disagree with the JS
101
+ // defaults.** Specifically: native sets `flowMaxTranslationCm
102
+ // = 0` and `flowEvalEveryNFrames = 1` when the keys are
103
+ // missing (iOS `IncrementalStitcher.swift:1003-1029`,
104
+ // Android `IncrementalStitcher.kt:419-445`), whereas the JS
105
+ // `DEFAULT_PANORAMA_SETTINGS.frameSelection.flow` values are
106
+ // `50` and `5`. Hosts who write sparse settings literals
107
+ // (omitted `flow` sub-tree, legal per the optional `?`)
108
+ // would silently get IMU translation gate disabled and
109
+ // ~5× CPU on flow evaluation — a v0.3-style behaviour
110
+ // regression on the wire that the type system can't catch.
111
+ // Filling from `DEFAULT_FLOW_GATE_SETTINGS` here closes the
112
+ // gap; the JS defaults become the canonical defaults across
113
+ // both layers.
114
+ //
115
+ // See the F10 Phase 2 review (B1 + N3 + N6) for the full
116
+ // discussion of why this matters.
117
+ const f = s.frameSelection.flow ?? DEFAULT_FLOW_GATE_SETTINGS;
118
+ cfg.flowNoveltyPercentile = f.noveltyPercentile;
119
+ cfg.flowEvalEveryNFrames = f.evalEveryNFrames;
120
+ cfg.flowMaxTranslationCm = f.maxTranslationCm;
121
+ cfg.flowMaxCorners = f.maxCorners;
122
+ cfg.flowQualityLevel = f.qualityLevel;
123
+ cfg.flowMinDistance = f.minDistance;
124
+
125
+ return cfg;
126
+ }
127
+
128
+
129
+ /**
130
+ * Convert a v0.4 SlitscanSettings tree into the flat dict the
131
+ * slit-scan / firstwins native engines read. Handles the
132
+ * "presence-as-enable" boolean expansion: a non-undefined
133
+ * `registration.ncc1d` means `enable1dNcc: true` on the wire,
134
+ * with the sub-object's `searchRadius` carried alongside.
135
+ *
136
+ * Verified against:
137
+ * - iOS `IncrementalStitcher.swift:1006-1100` (applyConfigOverrides)
138
+ * - iOS `OpenCVSlitScanStitcher.mm` (all numbered references in
139
+ * the audit ground-truth matrix)
140
+ */
141
+ export function slitscanSettingsToNativeConfig(
142
+ s: SlitscanSettings,
143
+ ): NativeConfigDict {
144
+ const cfg: NativeConfigDict = {
145
+ captureSource: s.captureSource,
146
+ // The native side reads `engine: 'slitscan-…'` at start time
147
+ // from a separate top-level field, NOT from configOverrides.
148
+ // We still serialise the variant here for hosts that want to
149
+ // round-trip a single settings object through both surfaces.
150
+ engineVariant: s.variant,
151
+
152
+ // ── Painting ─────────────────────────────────────────────────
153
+ paintMode: s.painting.paintMode,
154
+ sliverPosition: s.painting.sliverPosition,
155
+ firstFrameFullFrame: s.painting.firstFrameFullFrame,
156
+
157
+ // ── Registration (explicit booleans) ─────────────────────────
158
+ enableTriangulation: s.registration.enableTriangulation,
159
+ enableTriAccumulator: s.registration.enableTriAccumulator,
160
+ enableRansacHomography: s.registration.enableRansacHomography,
161
+
162
+ // ── Plane projection ─────────────────────────────────────────
163
+ planeSource: s.plane.source,
164
+ };
165
+
166
+ // ── 1D NCC: presence-as-enable ─────────────────────────────────
167
+ if (s.registration.ncc1d) {
168
+ cfg.enable1dNcc = true;
169
+ cfg.nccSearchRadius1d = s.registration.ncc1d.searchRadius;
170
+ } else {
171
+ cfg.enable1dNcc = false;
172
+ }
173
+
174
+ // ── 2D NCC: presence-as-enable + nested optionals ──────────────
175
+ if (s.registration.ncc2d) {
176
+ const n2 = s.registration.ncc2d;
177
+ cfg.enable2dNcc = true;
178
+ cfg.nccSearchMargin2d = n2.searchMargin;
179
+ cfg.nccConfidenceThreshold2d = n2.confidenceThreshold;
180
+ if (n2.emaSmoothing) {
181
+ cfg.enableNcc2dEmaSmoothing = true;
182
+ cfg.ncc2dEmaAlpha = n2.emaSmoothing.alpha;
183
+ } else {
184
+ cfg.enableNcc2dEmaSmoothing = false;
185
+ }
186
+ if (n2.panAxisLock) {
187
+ cfg.enableNcc2dPanAxisLock = true;
188
+ cfg.ncc2dCrossAxisLockPx = n2.panAxisLock.crossAxisLockPx;
189
+ } else {
190
+ cfg.enableNcc2dPanAxisLock = false;
191
+ }
192
+ } else {
193
+ cfg.enable2dNcc = false;
194
+ }
195
+
196
+ // ── Plane optionals ────────────────────────────────────────────
197
+ // Only emit when `source` actually consumes the field. Native
198
+ // tolerates unsolicited keys but the modal also walks the dict
199
+ // to decide which sliders to render — extra keys would mislead.
200
+ if (s.plane.source !== 'Disabled' && s.plane.projectionStyle !== undefined) {
201
+ cfg.planeProjectionStyle = s.plane.projectionStyle;
202
+ }
203
+ if (s.plane.source === 'Virtual' && s.plane.virtualDepthMeters !== undefined) {
204
+ cfg.virtualPlaneDepthMeters = s.plane.virtualDepthMeters;
205
+ }
206
+ if (s.plane.source === 'ARKitDetected' && s.plane.alignmentThreshold !== undefined) {
207
+ cfg.arkitPlaneAlignmentThreshold = s.plane.alignmentThreshold;
208
+ }
209
+
210
+ // ── Advanced motion knobs (only emit if explicitly set) ────────
211
+ if (s.advanced?.panAxisFractionRect !== undefined) {
212
+ cfg.kPanAxisFractionRect = s.advanced.panAxisFractionRect;
213
+ }
214
+ if (s.advanced?.minAcceptDeltaPx !== undefined) {
215
+ cfg.kMinAcceptDeltaPx = s.advanced.minAcceptDeltaPx;
216
+ }
217
+
218
+ return cfg;
219
+ }
220
+
221
+
222
+ /**
223
+ * Convert a v0.4 HybridSettings tree into the flat dict the hybrid
224
+ * engine reads. Minimal surface — hybrid presets internally clobber
225
+ * almost everything; see HybridSettings JSDoc for context.
226
+ *
227
+ * Verified against:
228
+ * - iOS `OpenCVIncrementalStitcher.mm:139-180` (preset paths)
229
+ * - iOS `IncrementalStitcher.swift:1034-1040` (hybridProjection override)
230
+ */
231
+ export function hybridSettingsToNativeConfig(
232
+ s: HybridSettings,
233
+ ): NativeConfigDict {
234
+ return {
235
+ captureSource: s.captureSource,
236
+ hybridProjection: s.projection,
237
+ };
238
+ }