react-native-image-stitcher 0.5.1 → 0.6.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.
@@ -365,12 +365,12 @@ public final class IncrementalStitcher: NSObject {
365
365
  /// F8.3 — gate for `consumeFrameFromPlugin` (the vision-camera
366
366
  /// Frame Processor producer-thread entry point). TRUE only when
367
367
  /// the current capture was started with
368
- /// `frameSourceMode == "frameProcessor"`. In any other mode
369
- /// (especially the legacy `"jsDriver"` path which feeds via
370
- /// `processFrameAtPath`), the plugin would double-feed the
371
- /// engine — pixel buffers from the producer thread + JPEG paths
372
- /// from the JS interval, racing on the same workQueue — so we
373
- /// drop the producer-thread call.
368
+ /// `frameSourceMode == "frameProcessor"`. In AR mode
369
+ /// (`frameSourceMode == "arSession"`) the plugin would double-feed
370
+ /// the engine alongside ARKit's `consumeFrame` delegate path
371
+ /// pixel buffers from the producer thread + pixel buffers from the
372
+ /// ARSession delegate, racing on the same workQueue — so we drop
373
+ /// the producer-thread call.
374
374
  ///
375
375
  /// Set under `stateLock` in `start()`, cleared under `stateLock`
376
376
  /// in `cancel()` and `finalize()`, ALSO read under `stateLock`
@@ -747,11 +747,13 @@ public final class IncrementalStitcher: NSObject {
747
747
  engineMode: String,
748
748
  captureOrientation: String = "portrait",
749
749
  configOverrides: [String: Any] = [:],
750
- // 2026-05-18 (Issue #2 regression fix): "arSession" (default,
751
- // legacy) registers as the ARSession's frame consumer.
752
- // "jsDriver" skips that registration — frames will come in
753
- // via processFrameAtPath instead. Used by iOS non-AR
754
- // captures (the vision-camera + gyro driver path).
750
+ // 2026-05-18 (Issue #2 regression fix): "arSession" (default)
751
+ // registers as the ARSession's frame consumer.
752
+ // "frameProcessor" skips that registration — frames come in
753
+ // via the vision-camera Frame Processor plugin's
754
+ // `consumeFrameFromPlugin` path instead. The pre-v0.6
755
+ // "jsDriver" mode (push frames in from JS via
756
+ // processFrameAtPath) has been removed.
755
757
  frameSourceMode: String = "arSession"
756
758
  ) {
757
759
  stateLock.lock()
@@ -947,9 +949,9 @@ public final class IncrementalStitcher: NSObject {
947
949
  }
948
950
  self.isRunning = true
949
951
  // F8.3 — enable the Frame Processor plugin's producer-thread
950
- // ingest only for the new "frameProcessor" mode. Any other
951
- // mode (arSession, jsDriver) keeps it OFF; see the ivar's
952
- // declaration comment for why.
952
+ // ingest only for the new "frameProcessor" mode. AR mode
953
+ // ("arSession") keeps it OFF; see the ivar's declaration
954
+ // comment for why.
953
955
  self.frameProcessorIngestEnabled = (frameSourceMode == "frameProcessor")
954
956
  self.snapshotJpegQuality = max(1, min(100, snapshotJpegQuality))
955
957
  self.snapshotEveryNAccepts = max(1, snapshotEveryNAccepts)
@@ -1099,9 +1101,10 @@ public final class IncrementalStitcher: NSObject {
1099
1101
  // `stateLock.try()` and corrupting
1100
1102
  // the gate's novelty math.
1101
1103
  // (Adversarial-review C1.)
1102
- // * `jsDriver` — DO NOT register. Legacy path uses
1103
- // `processFrameAtPath`; bypasses
1104
- // consumeFrame entirely.
1104
+ //
1105
+ // The pre-v0.6 `jsDriver` mode (which pushed frames via
1106
+ // `processFrameAtPath` and also skipped registration) has
1107
+ // been removed.
1105
1108
  if frameSourceMode == "arSession" {
1106
1109
  RNSARSession.shared.incrementalConsumer = self
1107
1110
  }
@@ -2149,191 +2152,6 @@ public final class IncrementalStitcher: NSObject {
2149
2152
  self.keyframeGate.acceptedCount, self.keyframeGate.maxCount)
2150
2153
  }
2151
2154
 
2152
- /// Whether the engine is currently in batch-keyframe mode.
2153
- /// Bridge reads this to decide whether the JS-driven
2154
- /// `processFrameAtPath` path can use the lightweight
2155
- /// `addBatchKeyframePath` (path-only) entry below.
2156
- @objc public var isBatchKeyframeMode: Bool {
2157
- stateLock.lock()
2158
- defer { stateLock.unlock() }
2159
- return batchKeyframeMode
2160
- }
2161
-
2162
- /// 2026-05-18 (Issue #2 v2) — JS-driver entry-point for
2163
- /// batch-keyframe captures. Mirrors Android's behaviour: the
2164
- /// caller (JS side via the IncrementalStitcherBridge) hands us a
2165
- /// JPEG file path that already exists on disk (saved by
2166
- /// vision-camera's takeSnapshot), plus a synthetic pose derived
2167
- /// from gyro integration. We:
2168
- ///
2169
- /// 1. Validate state (running + batchKeyframeMode).
2170
- /// 2. Ask the shared C++ KeyframeGate whether to accept this
2171
- /// frame. Pass `latchedPlane: nil` — non-AR captures have
2172
- /// no plane; the C++ gate falls back to a pose-only
2173
- /// angular-delta strategy. We do NOT pass a pixel buffer:
2174
- /// the Pose strategy doesn't need one, and avoiding the
2175
- /// JPEG → CVPixelBuffer round-trip dodges the iOS
2176
- /// orientation bugs that broke Issue 2 v1
2177
- /// (UIImage/CGContext Y-flip + EXIF-vs-CGImage dimension
2178
- /// mismatch — see the symptom in 2026-05-18 user report).
2179
- /// 3. If accepted, append the existing path + pose to the
2180
- /// finalize-time lists. No JPEG re-encode — the file on
2181
- /// disk IS the keyframe. `retailens::stitchFramePaths()`
2182
- /// at finalize uses `cv::imread` which natively handles
2183
- /// EXIF orientation, so the output panorama reads upright.
2184
- /// 4. Emit the same state-event the AR delegate path emits so
2185
- /// the JS live band populates identically.
2186
- ///
2187
- /// Architecture note: this is structurally parallel to Android's
2188
- /// `IncrementalStitcher.kt::processFrameAtPath`
2189
- /// `batchKeyframeMode` branch (lines 573-627). A follow-up
2190
- /// should extract the dispatch (gate-eval + path-append + emit)
2191
- /// into shared cpp/ so both platforms become 5-line wrappers
2192
- /// around a single C++ entry point.
2193
- @objc public func addBatchKeyframePath(
2194
- path: String,
2195
- pose: RNSARFramePose
2196
- ) -> Bool {
2197
- stateLock.lock()
2198
- guard self.isRunning, self.batchKeyframeMode else {
2199
- stateLock.unlock()
2200
- return false
2201
- }
2202
- stateLock.unlock()
2203
-
2204
- // 2026-05-21 (v0.3) — pixel-aware Flow-strategy evaluation.
2205
- // Pre-0.3 this was `evaluate(pose:latchedPlane:)` with no pixel
2206
- // buffer, which forced the C++ gate to silently fall back to
2207
- // Pose strategy (same bug as Android non-AR; both fixed in
2208
- // v0.3). We now decode the JPEG snapshot at `path` to a
2209
- // single-channel grayscale CVPixelBuffer and pass it through,
2210
- // so the gate's Flow strategy actually runs sparse-flow
2211
- // novelty on real image content.
2212
- //
2213
- // CGImageSource → CGContext into a OneComponent8 CVPixelBuffer.
2214
- // ~10-20 ms per snapshot on iPhone 13/16 Pro; well under the
2215
- // ~250 ms non-AR snapshot interval (~4 FPS cadence). v0.4
2216
- // will replace this path entirely by moving non-AR capture to
2217
- // vision-camera's Frame Processor API (tracked at issue #11).
2218
- let decision: KeyframeGateDecision
2219
- if let grayBuffer = Self.decodeJpegToGrayscalePixelBuffer(path: path) {
2220
- decision = self.keyframeGate.evaluate(
2221
- pose: pose,
2222
- latchedPlane: nil,
2223
- pixelBuffer: grayBuffer
2224
- )
2225
- } else {
2226
- // JPEG decode failed (corrupt file, OOM, etc.). Fall back
2227
- // to pose-only so the capture doesn't lock up — matches
2228
- // the C++ side's defensive grayData==nullptr handling
2229
- // inside evaluateWithFrame.
2230
- decision = self.keyframeGate.evaluate(
2231
- pose: pose,
2232
- latchedPlane: nil
2233
- )
2234
- }
2235
- if !decision.accept {
2236
- self.emitKeyframeRejectState(decision: decision)
2237
- return false
2238
- }
2239
-
2240
- // Append path + pose to the finalize lists. Take the lock
2241
- // briefly — these mutate state read by `finalize()`.
2242
- stateLock.lock()
2243
- self.keyframePaths.append(path)
2244
- self.keyframePoses.append(pose.asDictionary())
2245
- // 2026-05-22 (audit F2) — track first + last pose for the
2246
- // stitchMode auto-resolver. iOS parity: Android records
2247
- // these in IncrementalStitcher.kt at the same accept points.
2248
- let poseArr = [pose.tx, pose.ty, pose.tz,
2249
- pose.qx, pose.qy, pose.qz, pose.qw]
2250
- if self.batchFirstAcceptedPose == nil { self.batchFirstAcceptedPose = poseArr }
2251
- self.batchLastAcceptedPose = poseArr
2252
- let count = self.keyframePaths.count
2253
- stateLock.unlock()
2254
- os_log(.fault, log: Self.diagLog,
2255
- "[V16-batch-keyframe.js] accepted path #%d → %{public}@",
2256
- Int32(count), path)
2257
- self.emitBatchKeyframeAcceptedState(
2258
- thumbnailPath: path,
2259
- keyframeIndex: count - 1,
2260
- keyframeCount: count,
2261
- keyframeMax: self.keyframeGate.maxCount,
2262
- isLandscape: pose.imageWidth >= pose.imageHeight
2263
- )
2264
- return true
2265
- }
2266
-
2267
- /// 2026-05-21 (v0.3) — decode a JPEG file at the given path into a
2268
- /// single-channel grayscale CVPixelBuffer (`kCVPixelFormatType_-
2269
- /// OneComponent8`) suitable for feeding into the C++ KeyframeGate's
2270
- /// Flow-strategy evaluate path. The bridge's `evaluatePixelBuffer:`
2271
- /// has explicit OneComponent8 handling (added in v0.3) that reads
2272
- /// the base address as the Y plane directly, so no extra conversion
2273
- /// happens on the C++ side.
2274
- ///
2275
- /// Used by `addBatchKeyframePath` (the JS-driver non-AR path) so the
2276
- /// Flow strategy actually runs on real pixel data — pre-0.3 this
2277
- /// path called `evaluate(pose:latchedPlane:)` with no buffer and
2278
- /// the C++ side silently fell back to Pose strategy.
2279
- ///
2280
- /// Performance: ~10-20 ms for a 1920×1080 JPEG on iPhone 13/16 Pro.
2281
- /// Well under the ~250 ms non-AR snapshot interval (~4 FPS).
2282
- /// v0.4 will replace this path entirely via Frame Processor — see
2283
- /// issue #11.
2284
- ///
2285
- /// Returns nil on any failure (file missing, corrupt JPEG, OOM
2286
- /// on the CVPixelBufferCreate). Callers fall back to the
2287
- /// pose-only evaluate so the capture doesn't lock up.
2288
- private static func decodeJpegToGrayscalePixelBuffer(
2289
- path: String
2290
- ) -> CVPixelBuffer? {
2291
- let url = URL(fileURLWithPath: path)
2292
- guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
2293
- let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
2294
- else {
2295
- return nil
2296
- }
2297
- let width = cgImage.width
2298
- let height = cgImage.height
2299
-
2300
- var pixelBuffer: CVPixelBuffer?
2301
- let attrs: NSDictionary = [
2302
- kCVPixelBufferIOSurfacePropertiesKey: NSDictionary(),
2303
- ]
2304
- let status = CVPixelBufferCreate(
2305
- kCFAllocatorDefault,
2306
- width, height,
2307
- kCVPixelFormatType_OneComponent8,
2308
- attrs,
2309
- &pixelBuffer
2310
- )
2311
- guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
2312
- return nil
2313
- }
2314
-
2315
- CVPixelBufferLockBaseAddress(buffer, [])
2316
- defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
2317
- guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else {
2318
- return nil
2319
- }
2320
- let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
2321
- let colorSpace = CGColorSpaceCreateDeviceGray()
2322
- guard let context = CGContext(
2323
- data: baseAddress,
2324
- width: width,
2325
- height: height,
2326
- bitsPerComponent: 8,
2327
- bytesPerRow: bytesPerRow,
2328
- space: colorSpace,
2329
- bitmapInfo: CGImageAlphaInfo.none.rawValue
2330
- ) else {
2331
- return nil
2332
- }
2333
- context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
2334
- return buffer
2335
- }
2336
-
2337
2155
  /// V16 Phase 1 — emit a state event when a batch-keyframe is
2338
2156
  /// saved. Carries the on-disk thumbnail path so JS can render it
2339
2157
  /// in LiveFrameStrip + advance the "Keyframes: N/M" pill.
@@ -3119,14 +2937,15 @@ extension IncrementalStitcher: ARFrameConsumer {}
3119
2937
  // * `pixelBuffer` from `frame.buffer` (vision-camera YUV biplanar)
3120
2938
  // * `tx`/`ty`/`tz` = 0 (no AR translation; gyro only gives rotation)
3121
2939
  // * `qx,qy,qz,qw` from JS-thread gyro-integrated yaw+pitch (synthesised
3122
- // as `q = q_yaw * q_pitch` same convention as
3123
- // `useIncrementalJSDriver`'s pose synthesis)
2940
+ // as `q = q_yaw * q_pitch`). `useFrameProcessorDriver` and (pre-v0.6)
2941
+ // `useIncrementalJSDriver` both produced quaternions with this layout.
3124
2942
  // * `fx`/`fy` from frame dims + assumed FoV
3125
2943
  // * `cx`/`cy` at image centre
3126
2944
  // * `trackingStateRaw = 2` (= `.tracking`) — non-AR captures don't have
3127
2945
  // a real ARKit tracking-quality signal; reporting `.tracking` keeps
3128
- // the engine's `trackingPoor` path inactive, matching the legacy
3129
- // `useIncrementalJSDriver` contract.
2946
+ // the engine's `trackingPoor` path inactive. Both the v0.6+
2947
+ // `useFrameProcessorDriver` and the pre-v0.6 `useIncrementalJSDriver`
2948
+ // follow(ed) this contract.
3130
2949
  extension IncrementalStitcher {
3131
2950
  @objc public func consumeFrameFromPlugin(
3132
2951
  pixelBuffer: CVPixelBuffer,
@@ -65,14 +65,6 @@ RCT_EXTERN_METHOD(refinePanorama:(NSDictionary *)options
65
65
  resolver:(RCTPromiseResolveBlock)resolver
66
66
  rejecter:(RCTPromiseRejectBlock)rejecter)
67
67
 
68
- // 2026-05-17 (Issue #2) — iOS non-AR frame ingestion. JS-side driver
69
- // hands snapshot file paths + gyro-derived pose to the engine so the
70
- // live band populates in non-AR mode (parity with Android). See JS
71
- // `useIncrementalVisionCameraDriver`.
72
- RCT_EXTERN_METHOD(processFrameAtPath:(NSDictionary *)options
73
- resolver:(RCTPromiseResolveBlock)resolver
74
- rejecter:(RCTPromiseRejectBlock)rejecter)
75
-
76
68
  // 2026-05-18 (Iss 3) — keyframe storage management. cleanupKeyframes
77
69
  // GCs stale per-session directories under Library/Application Support/
78
70
  // Captures; getKeyframeDir returns the active capture's session dir.
@@ -20,7 +20,6 @@
20
20
  import Foundation
21
21
  import React
22
22
  import os.log
23
- import ImageIO // CGImageSource + kCGImagePropertyOrientation for EXIF read in processFrameAtPath
24
23
 
25
24
  @objc(IncrementalStitcherBridge)
26
25
  public final class IncrementalStitcherBridge: RCTEventEmitter {
@@ -75,10 +74,12 @@ public final class IncrementalStitcherBridge: RCTEventEmitter {
75
74
  /// Resolves with `{ ok: true }`. Rejects when `frameSourceMode`
76
75
  /// (options dict) is 'arSession' (the default) AND the AR session
77
76
  /// isn't running — that path needs ARKit to deliver frames.
78
- /// When `frameSourceMode` is 'jsDriver' the AR-session check is
79
- /// skipped and the engine expects JS to feed frames via
80
- /// `processFrameAtPath` (used by iOS non-AR captures since
81
- /// 2026-05-18 / Issue #2 regression fix).
77
+ /// When `frameSourceMode` is 'frameProcessor' the AR-session check
78
+ /// is skipped and the engine expects the vision-camera Frame
79
+ /// Processor plugin (`CvFlowGateFrameProcessor`) to feed frames
80
+ /// via `consumeFrameFromPlugin`. The pre-v0.6 'jsDriver' mode
81
+ /// (push frames in from JS via `processFrameAtPath`) has been
82
+ /// removed.
82
83
  @objc(start:resolver:rejecter:)
83
84
  public func start(
84
85
  options: NSDictionary,
@@ -251,127 +252,6 @@ public final class IncrementalStitcherBridge: RCTEventEmitter {
251
252
  resolver(["ok": true])
252
253
  }
253
254
 
254
- /// 2026-05-18 (Issue #2 v2) — JS-driven frame ingestion for iOS
255
- /// non-AR mode. Mirrors Android's `processFrameAtPath` exactly:
256
- /// the JPEG at `path` is already saved on disk by vision-camera
257
- /// in its native EXIF-correct orientation. We DO NOT decode the
258
- /// image here. Instead:
259
- ///
260
- /// - Build a synthetic `RNSARFramePose` from the
261
- /// JS-supplied quaternion + intrinsics (no translation;
262
- /// non-AR captures don't have it).
263
- /// - Hand the path + pose to
264
- /// `IncrementalStitcher.addBatchKeyframePath`, which
265
- /// evaluates the shared-C++ KeyframeGate and (if accepted)
266
- /// records the path in the finalize-time keyframe list +
267
- /// emits the same state event the AR-delegate path emits.
268
- /// - `cv::imread` at finalize handles EXIF orientation
269
- /// natively, so the output panorama reads upright with no
270
- /// iOS-specific orientation handling needed in this bridge.
271
- ///
272
- /// History: Issue #2 v1 (commit 0e40f17) tried to decode the
273
- /// JPEG into a CVPixelBuffer and reuse the existing AR
274
- /// `consumeFrame(pixelBuffer:pose:)` path. That introduced two
275
- /// orientation bugs (CGContext Y-flip + UIImage.size vs
276
- /// cgImage.width dim swap) → upside-down output AND canvas-
277
- /// dimension overflow → OOM crashes (user-reported 2026-05-18).
278
- /// Architecturally Android never decoded the image either, so
279
- /// the right fix was to mirror that.
280
- ///
281
- /// `options` keys:
282
- /// - path (NSString, required) — local file path (no file://)
283
- /// - qx, qy, qz, qw (Double, required) — quaternion, JS-side
284
- /// gyro-integrated
285
- /// - fx, fy, cx, cy (Double, required) — intrinsics in sensor px
286
- /// - imageWidth, imageHeight (Int, required)
287
- /// - trackingPoor (Bool, optional, default false)
288
- /// - timestampMs (Double, optional, default = now)
289
- ///
290
- /// Only batch-keyframe captures are supported on this path right
291
- /// now — other engines (hybrid / firstwins) need real pixel data
292
- /// during the live phase, which isn't trivially derivable from a
293
- /// JPEG path. Reject with `E_NOT_BATCH_KEYFRAME` so the JS host
294
- /// can fall back to the legacy stitchVideo path if needed.
295
- @objc(processFrameAtPath:resolver:rejecter:)
296
- public func processFrameAtPath(
297
- options: NSDictionary,
298
- resolver: @escaping RCTPromiseResolveBlock,
299
- rejecter: @escaping RCTPromiseRejectBlock
300
- ) {
301
- guard let pathRaw = options["path"] as? String, !pathRaw.isEmpty else {
302
- rejecter("E_NO_PATH", "processFrameAtPath: missing 'path'", nil)
303
- return
304
- }
305
- // Strip optional file:// prefix — JS callers sometimes send
306
- // file URIs, native APIs want filesystem paths.
307
- let cleanPath = pathRaw.hasPrefix("file://")
308
- ? String(pathRaw.dropFirst("file://".count))
309
- : pathRaw
310
-
311
- let engine = IncrementalStitcher.shared
312
- guard engine.isBatchKeyframeMode else {
313
- rejecter("E_NOT_BATCH_KEYFRAME",
314
- "processFrameAtPath only supports batch-keyframe "
315
- + "engine mode on iOS. Configure "
316
- + "incrementalEngine='batch-keyframe' in start() "
317
- + "options, or fall back to the stitchVideo path.",
318
- nil)
319
- return
320
- }
321
-
322
- let qx = (options["qx"] as? Double) ?? 0
323
- let qy = (options["qy"] as? Double) ?? 0
324
- let qz = (options["qz"] as? Double) ?? 0
325
- let qw = (options["qw"] as? Double) ?? 1 // identity quat default
326
- let fx = (options["fx"] as? Double) ?? 1000.0
327
- let fy = (options["fy"] as? Double) ?? 1000.0
328
- let cx = (options["cx"] as? Double) ?? 540.0
329
- let cy = (options["cy"] as? Double) ?? 960.0
330
- let imageWidth = (options["imageWidth"] as? Int) ?? 1080
331
- let imageHeight = (options["imageHeight"] as? Int) ?? 1920
332
- let trackingPoor = (options["trackingPoor"] as? Bool) ?? false
333
- let timestampMs = (options["timestampMs"] as? Double)
334
- ?? (Date().timeIntervalSince1970 * 1000.0)
335
- let trackingState: RNSARTrackingState =
336
- trackingPoor ? .limited : .tracking
337
-
338
- let pose = RNSARFramePose(
339
- tx: 0, ty: 0, tz: 0, // no translation in non-AR
340
- qx: qx, qy: qy, qz: qz, qw: qw,
341
- fx: fx, fy: fy, cx: cx, cy: cy,
342
- imageWidth: imageWidth, imageHeight: imageHeight,
343
- timestampMs: timestampMs,
344
- trackingState: trackingState
345
- )
346
-
347
- // 2026-05-18 (Iss #1 diag) — read EXIF Orientation tag from the
348
- // keyframe JPEG before handing it to the engine. vision-camera
349
- // writes a JPEG with an EXIF tag matching the physical capture
350
- // orientation (1=no rotation, 3=180°, 6=90°CW, 8=90°CCW). The
351
- // bake-rotation table in cpp/stitcher.cpp assumes the post-imread
352
- // Mat is in user-view orientation (post-EXIF apply). If the EXIF
353
- // tag isn't what we expect for a given physical orientation, the
354
- // input Mat to cv::Stitcher will be a different shape than the AR
355
- // path produces (AR keyframes hardcode EXIF=6, commit 7b828f1) —
356
- // which would explain why iOS non-AR landscape captures stitch
357
- // but bake the wrong way. CGImageSource is cheap (metadata-only;
358
- // no decode).
359
- var exifOrientation: Int = -1
360
- if let src = CGImageSourceCreateWithURL(
361
- URL(fileURLWithPath: cleanPath) as CFURL, nil
362
- ),
363
- let props = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as? [CFString: Any],
364
- let o = props[kCGImagePropertyOrientation] as? Int {
365
- exifOrientation = o
366
- }
367
- os_log(.fault, log: OSLog(subsystem: "com.tiger.retailens",
368
- category: "stitcher.diag"),
369
- "[V16-batch-keyframe.js] processFrameAtPath EXIF=%d imageW=%d imageH=%d path=%{public}@",
370
- Int32(exifOrientation), Int32(imageWidth), Int32(imageHeight), cleanPath)
371
-
372
- let accepted = engine.addBatchKeyframePath(path: cleanPath, pose: pose)
373
- resolver(["ok": true, "accepted": accepted])
374
- }
375
255
 
376
256
  /// 2026-05-18 (Iss 3) — bridge for `cleanupKeyframes`. See the
377
257
  /// Swift method's docstring for behaviour. Options dict keys:
@@ -140,13 +140,13 @@ static NSInteger kg_argInt(NSDictionary* args, NSString* key, NSInteger defaultV
140
140
  // Pose from worklet args. Defaults are safe non-AR values:
141
141
  // * tx/ty/tz = 0 (no translation in non-AR; gyro only gives rot)
142
142
  // * qw = 1 (identity quaternion if JS hasn't supplied rotation)
143
- // * fx/fy/cx/cy = 0 → JS-driver caller MUST supply these (the
144
- // engine derives FoV from intrinsics; 0 would yield NaN FoV).
145
- // We default the principal point to image centre as a safer
146
- // fallback if only fx/fy are missing.
143
+ // * fx/fy/cx/cy = 0 → the Frame Processor worklet caller MUST
144
+ // supply these (the engine derives FoV from intrinsics; 0 would
145
+ // yield NaN FoV). We default the principal point to image
146
+ // centre as a safer fallback if only fx/fy are missing.
147
147
  // * trackingStateRaw = 2 → `.tracking` (non-AR captures don't
148
- // have a real tracking-quality signal; engine's `trackingPoor`
149
- // path stays inactive, matching legacy `useIncrementalJSDriver`).
148
+ // have a real tracking-quality signal; reporting `.tracking`
149
+ // keeps the engine's `trackingPoor` path inactive).
150
150
  double tx = kg_argDouble(arguments, @"tx", 0.0);
151
151
  double ty = kg_argDouble(arguments, @"ty", 0.0);
152
152
  double tz = kg_argDouble(arguments, @"tz", 0.0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-image-stitcher",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",