react-native-image-stitcher 0.15.2 → 0.16.1

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 (146) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/README.md +131 -5
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
  4. package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
  5. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
  6. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
  7. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
  9. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
  10. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
  11. package/cpp/crop_quad.cpp +162 -0
  12. package/cpp/crop_quad.hpp +163 -0
  13. package/cpp/keyframe_gate.cpp +54 -15
  14. package/cpp/keyframe_gate.hpp +33 -0
  15. package/cpp/stitcher.cpp +1122 -132
  16. package/cpp/stitcher.hpp +62 -0
  17. package/cpp/warp_guard.hpp +212 -0
  18. package/dist/camera/Camera.d.ts +209 -12
  19. package/dist/camera/Camera.js +575 -36
  20. package/dist/camera/CameraView.js +35 -16
  21. package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
  22. package/dist/camera/CaptureCountdownOverlay.js +239 -0
  23. package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
  24. package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
  25. package/dist/camera/CaptureMemoryPill.d.ts +24 -8
  26. package/dist/camera/CaptureMemoryPill.js +37 -12
  27. package/dist/camera/CapturePreview.js +2 -1
  28. package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
  29. package/dist/camera/CaptureStatusOverlay.js +22 -5
  30. package/dist/camera/CaptureThumbnailStrip.js +2 -1
  31. package/dist/camera/LateralMotionModal.d.ts +85 -0
  32. package/dist/camera/LateralMotionModal.js +134 -0
  33. package/dist/camera/PanHowToOverlay.d.ts +76 -0
  34. package/dist/camera/PanHowToOverlay.js +222 -0
  35. package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
  36. package/dist/camera/PanoramaBandOverlay.js +9 -3
  37. package/dist/camera/PanoramaSettings.d.ts +8 -6
  38. package/dist/camera/PanoramaSettings.js +19 -1
  39. package/dist/camera/PanoramaSettingsModal.js +4 -4
  40. package/dist/camera/RectCropPreview.d.ts +135 -0
  41. package/dist/camera/RectCropPreview.js +370 -0
  42. package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
  43. package/dist/camera/RotateToLandscapePrompt.js +138 -0
  44. package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
  45. package/dist/camera/buildPanoramaInitialSettings.js +9 -0
  46. package/dist/camera/cameraErrorMessages.d.ts +30 -1
  47. package/dist/camera/cameraErrorMessages.js +26 -10
  48. package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
  49. package/dist/camera/cameraGuidanceCopy.js +80 -0
  50. package/dist/camera/captureCountdown.d.ts +52 -0
  51. package/dist/camera/captureCountdown.js +76 -0
  52. package/dist/camera/captureWarnings.d.ts +90 -0
  53. package/dist/camera/captureWarnings.js +108 -0
  54. package/dist/camera/classifyStitchError.d.ts +30 -0
  55. package/dist/camera/classifyStitchError.js +42 -0
  56. package/dist/camera/cropGeometry.d.ts +136 -0
  57. package/dist/camera/cropGeometry.js +223 -0
  58. package/dist/camera/displayDecodeImageProps.d.ts +25 -0
  59. package/dist/camera/displayDecodeImageProps.js +29 -0
  60. package/dist/camera/guidanceGraphics.d.ts +58 -0
  61. package/dist/camera/guidanceGraphics.js +280 -0
  62. package/dist/camera/guidanceTokens.d.ts +54 -0
  63. package/dist/camera/guidanceTokens.js +58 -0
  64. package/dist/camera/panModeGate.d.ts +54 -0
  65. package/dist/camera/panModeGate.js +62 -0
  66. package/dist/camera/pickCaptureFormat.d.ts +71 -0
  67. package/dist/camera/pickCaptureFormat.js +85 -0
  68. package/dist/camera/stitchDebugInfo.d.ts +27 -0
  69. package/dist/camera/stitchDebugInfo.js +55 -0
  70. package/dist/camera/usePanMotion.d.ts +250 -0
  71. package/dist/camera/usePanMotion.js +451 -0
  72. package/dist/index.d.ts +24 -3
  73. package/dist/index.js +33 -2
  74. package/dist/stitching/computeInscribedRect.d.ts +40 -0
  75. package/dist/stitching/computeInscribedRect.js +55 -0
  76. package/dist/stitching/cropQuad.d.ts +78 -0
  77. package/dist/stitching/cropQuad.js +116 -0
  78. package/dist/stitching/incremental.d.ts +74 -0
  79. package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
  80. package/dist/stitching/useIncrementalStitcher.js +7 -1
  81. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
  82. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
  83. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
  84. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
  85. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
  86. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
  87. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
  88. package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
  89. package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
  90. package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
  91. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
  92. package/package.json +5 -1
  93. package/src/camera/Camera.tsx +945 -47
  94. package/src/camera/CameraView.tsx +48 -16
  95. package/src/camera/CaptureCountdownOverlay.tsx +272 -0
  96. package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
  97. package/src/camera/CaptureMemoryPill.tsx +50 -12
  98. package/src/camera/CapturePreview.tsx +5 -0
  99. package/src/camera/CaptureStatusOverlay.tsx +35 -7
  100. package/src/camera/CaptureThumbnailStrip.tsx +4 -0
  101. package/src/camera/LateralMotionModal.tsx +199 -0
  102. package/src/camera/PanHowToOverlay.tsx +246 -0
  103. package/src/camera/PanoramaBandOverlay.tsx +9 -1
  104. package/src/camera/PanoramaSettings.ts +27 -7
  105. package/src/camera/PanoramaSettingsModal.tsx +4 -4
  106. package/src/camera/RectCropPreview.tsx +638 -0
  107. package/src/camera/RotateToLandscapePrompt.tsx +188 -0
  108. package/src/camera/buildPanoramaInitialSettings.ts +30 -1
  109. package/src/camera/cameraErrorMessages.ts +39 -2
  110. package/src/camera/cameraGuidanceCopy.ts +145 -0
  111. package/src/camera/captureCountdown.ts +83 -0
  112. package/src/camera/captureWarnings.ts +190 -0
  113. package/src/camera/classifyStitchError.ts +68 -0
  114. package/src/camera/cropGeometry.ts +268 -0
  115. package/src/camera/displayDecodeImageProps.ts +25 -0
  116. package/src/camera/guidanceGraphics.tsx +347 -0
  117. package/src/camera/guidanceTokens.ts +57 -0
  118. package/src/camera/panModeGate.ts +81 -0
  119. package/src/camera/pickCaptureFormat.ts +130 -0
  120. package/src/camera/stitchDebugInfo.ts +71 -0
  121. package/src/camera/usePanMotion.ts +667 -0
  122. package/src/index.ts +66 -3
  123. package/src/stitching/computeInscribedRect.ts +81 -0
  124. package/src/stitching/cropQuad.ts +167 -0
  125. package/src/stitching/incremental.ts +74 -0
  126. package/src/stitching/useIncrementalStitcher.ts +13 -0
  127. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
  128. package/cpp/tests/CMakeLists.txt +0 -104
  129. package/cpp/tests/README.md +0 -86
  130. package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
  131. package/cpp/tests/pose_test.cpp +0 -74
  132. package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
  133. package/cpp/tests/stubs/jsi/jsi.h +0 -33
  134. package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
  135. package/cpp/tests/warp_guard_test.cpp +0 -48
  136. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
  137. package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
  138. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
  139. package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
  140. package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
  141. package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
  142. package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
  143. package/src/camera/__tests__/useContentRotation.test.ts +0 -89
  144. package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
  145. package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
  146. package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
@@ -14,6 +14,7 @@ import org.opencv.core.Core
14
14
  import org.opencv.core.CvType
15
15
  import org.opencv.core.Mat
16
16
  import org.opencv.core.MatOfInt
17
+ import org.opencv.core.MatOfPoint2f
17
18
  import org.opencv.core.Point
18
19
  import org.opencv.core.Rect
19
20
  import org.opencv.core.Scalar
@@ -108,8 +109,27 @@ class BatchStitcher(reactContext: ReactApplicationContext)
108
109
  // SCANS canvas size is bounded by sum-of-frames; PANORAMA
109
110
  // can diverge to multi-GB on translation-heavy input).
110
111
  stitchMode: String,
112
+ // 2026-06-15 — pipeline picker (mirrors iOS' OpenCVStitcher
113
+ // `useManualPipeline:` param). true → the MANUAL cv::detail
114
+ // pipeline (graphcut + multiband + the full memory-guard set;
115
+ // the default for batch capture). false → stock high-level
116
+ // cv::Stitcher (the on-demand HIGH-LEVEL preview tab driven by
117
+ // refinePanorama). Appended LAST to match the JNI C signature
118
+ // in image_stitcher_jni.cpp — order/count/type must line up
119
+ // exactly or it's an UnsatisfiedLinkError at runtime.
120
+ useManualPipeline: Boolean,
111
121
  ): IntArray
112
122
 
123
+ // 2026-06-15 — getter for the last successful stitch's debugSummary
124
+ // (pipe/warp/route/seam/blend). The jintArray return of
125
+ // nativeStitchFramePaths can't carry a string; this fetches the value the
126
+ // JNI stashed. Called by stitchSync right after a successful stitch.
127
+ private external fun nativeLastDebugSummary(): String
128
+
129
+ /** debugSummary of the most recent stitchSync() (empty if none/failed). */
130
+ internal var lastDebugSummary: String = ""
131
+ private set
132
+
113
133
  // ── Stitch frames → panorama ─────────────────────────────────
114
134
 
115
135
  @ReactMethod
@@ -180,6 +200,9 @@ class BatchStitcher(reactContext: ReactApplicationContext)
180
200
  seamEstimationResolMP,
181
201
  compositingResolMP,
182
202
  stitchMode,
203
+ // Direct @ReactMethod batch stitch → MANUAL pipeline
204
+ // (the memory-safe default; mirrors iOS).
205
+ true,
183
206
  )
184
207
  val duration = System.currentTimeMillis() - start
185
208
  // 2026-05-15 (D) — dims layout from native JNI:
@@ -378,6 +401,121 @@ class BatchStitcher(reactContext: ReactApplicationContext)
378
401
  }
379
402
  }
380
403
 
404
+ /**
405
+ * item-7 — free-quad perspective crop (iOS `cropToQuadAtPath` parity).
406
+ *
407
+ * `options` carries `imagePath` + the 4 IMAGE-PIXEL corners as a flat
408
+ * `quad` array of 8 numbers `[tlX,tlY,trX,trY,brX,brY,blX,blY]`
409
+ * (ordered TL→TR→BR→BL by the JS editor's `orderQuadCorners`) +
410
+ * optional `quality` (default 90). Rectifies the quadrilateral to an
411
+ * upright rectangle (Imgproc.getPerspectiveTransform +
412
+ * Imgproc.warpPerspective), overwrites in place, resolves
413
+ * `{ width, height }`. Mirrors `cropToRect`; the JS editor chooses
414
+ * this when the dragged quad isn't ~axis-aligned.
415
+ *
416
+ * The destination size (averaged opposite edges) + the convex /
417
+ * min-area / in-bounds gate + the output-canvas OOM guard are
418
+ * Kotlin ports of cpp/crop_quad.hpp (quadDstRect / isQuadAcceptable)
419
+ * and cpp/warp_guard.hpp (canvasExceedsGuard), kept in sync with iOS
420
+ * per the same "duplicate stage code" convention `cropToRect` follows.
421
+ */
422
+ @ReactMethod
423
+ fun cropToQuad(options: ReadableMap, promise: Promise) {
424
+ val imagePath = options.getString("imagePath")
425
+ ?: return promise.reject("invalid-options", "imagePath required")
426
+ val quadArr = options.getArray("quad")
427
+ if (quadArr == null || quadArr.size() != 8) {
428
+ return promise.reject(
429
+ "invalid-options",
430
+ "quad must be an array of 8 numbers [tlX,tlY,trX,trY,brX,brY,blX,blY]",
431
+ )
432
+ }
433
+ val p = DoubleArray(8) { quadArr.getDouble(it) }
434
+ val quality =
435
+ (if (options.hasKey("quality") && !options.isNull("quality")) {
436
+ options.getInt("quality")
437
+ } else {
438
+ 90
439
+ }).coerceIn(1, 100)
440
+ CoroutineScope(Dispatchers.Default).launch {
441
+ val toRelease = mutableListOf<Mat>()
442
+ try {
443
+ ensureOpenCv()
444
+ val cleaned = stripFileScheme(imagePath)
445
+ if (!File(cleaned).exists()) {
446
+ promise.reject("read-failed", "Image not found: $imagePath")
447
+ return@launch
448
+ }
449
+ val img = Imgcodecs.imread(cleaned, Imgcodecs.IMREAD_COLOR)
450
+ toRelease += img
451
+ if (img.empty()) {
452
+ promise.reject("read-failed", "Could not decode $imagePath")
453
+ return@launch
454
+ }
455
+
456
+ // Geometry gate — convex, non-degenerate, inside the image.
457
+ if (!isQuadAcceptableForCrop(p, img.cols().toDouble(), img.rows().toDouble())) {
458
+ promise.reject(
459
+ "crop-to-quad-failed",
460
+ "Crop quad is degenerate (non-convex, zero-area, or out of bounds)",
461
+ )
462
+ return@launch
463
+ }
464
+ // Destination size = avg of opposite edge lengths (rounded).
465
+ val dstW = Math.round((quadEdge(p, 0, 1) + quadEdge(p, 3, 2)) / 2.0).toInt()
466
+ val dstH = Math.round((quadEdge(p, 0, 3) + quadEdge(p, 1, 2)) / 2.0).toInt()
467
+ // Output-canvas OOM net — same 50 MP guard the stitch uses.
468
+ if (dstW <= 0 || dstH <= 0 || canvasExceedsGuard(dstW.toLong(), dstH.toLong())) {
469
+ promise.reject(
470
+ "crop-to-quad-failed",
471
+ "Crop quad output canvas is degenerate or exceeds the size guard (${dstW}x${dstH})",
472
+ )
473
+ return@launch
474
+ }
475
+
476
+ val src = MatOfPoint2f(
477
+ Point(p[0], p[1]), // TL
478
+ Point(p[2], p[3]), // TR
479
+ Point(p[4], p[5]), // BR
480
+ Point(p[6], p[7]), // BL
481
+ )
482
+ toRelease += src
483
+ val dst = MatOfPoint2f(
484
+ Point(0.0, 0.0),
485
+ Point(dstW.toDouble(), 0.0),
486
+ Point(dstW.toDouble(), dstH.toDouble()),
487
+ Point(0.0, dstH.toDouble()),
488
+ )
489
+ toRelease += dst
490
+ val transform = Imgproc.getPerspectiveTransform(src, dst)
491
+ toRelease += transform
492
+ val warped = Mat()
493
+ toRelease += warped
494
+ Imgproc.warpPerspective(
495
+ img, warped, transform, Size(dstW.toDouble(), dstH.toDouble()),
496
+ Imgproc.INTER_LINEAR,
497
+ )
498
+ if (warped.empty()) {
499
+ promise.reject("crop-to-quad-failed", "Perspective warp produced an empty image")
500
+ return@launch
501
+ }
502
+ val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, quality)
503
+ if (!Imgcodecs.imwrite(cleaned, warped, params)) {
504
+ promise.reject("write-failed", "Could not rewrite $imagePath")
505
+ return@launch
506
+ }
507
+ promise.resolve(WritableNativeMap().apply {
508
+ putInt("width", warped.cols())
509
+ putInt("height", warped.rows())
510
+ })
511
+ } catch (t: Throwable) {
512
+ promise.reject("crop-to-quad-failed", t.message, t)
513
+ } finally {
514
+ toRelease.forEach { it.release() }
515
+ }
516
+ }
517
+ }
518
+
381
519
  /** Red-tint the dropped pixels; writes `<path>.mask.jpg`. Resolves `{ maskPath, width, height, excludedPercent }`. */
382
520
  @ReactMethod
383
521
  fun debugMaskOverlay(options: ReadableMap, promise: Promise) {
@@ -546,6 +684,79 @@ class BatchStitcher(reactContext: ReactApplicationContext)
546
684
  return intArrayOf(bx, by, bw, bh)
547
685
  }
548
686
 
687
+ // ── item-7 free-quad crop geometry (ports of cpp/crop_quad.hpp +
688
+ // cpp/warp_guard.hpp; kept in sync with iOS cropToQuad) ───────
689
+
690
+ /**
691
+ * Euclidean length of the edge between corners `i` and `j` in the flat
692
+ * `[x0,y0,x1,y1,...]` quad array `p` (corner k = (p[2k], p[2k+1])).
693
+ */
694
+ private fun quadEdge(p: DoubleArray, i: Int, j: Int): Double {
695
+ val dx = p[2 * i] - p[2 * j]
696
+ val dy = p[2 * i + 1] - p[2 * j + 1]
697
+ return Math.hypot(dx, dy)
698
+ }
699
+
700
+ /**
701
+ * Port of cpp/crop_quad.hpp:isQuadAcceptable for the flat 8-number
702
+ * quad `p` ([tlX,tlY,trX,trY,brX,brY,blX,blY]). True when the quad is
703
+ * convex, has |area| ≥ `minArea` px², and every corner lies inside
704
+ * `[0..imageW]×[0..imageH]` (½-px epsilon). Mirrors the iOS gate so
705
+ * both platforms reject the same degenerate quads.
706
+ */
707
+ private fun isQuadAcceptableForCrop(
708
+ p: DoubleArray,
709
+ imageW: Double,
710
+ imageH: Double,
711
+ minArea: Double = 1.0,
712
+ ): Boolean {
713
+ // Convexity — all consecutive edge cross-products share one sign.
714
+ var sign = 0
715
+ for (i in 0 until 4) {
716
+ val ax = p[2 * i]; val ay = p[2 * i + 1]
717
+ val bx = p[2 * ((i + 1) % 4)]; val by = p[2 * ((i + 1) % 4) + 1]
718
+ val cx = p[2 * ((i + 2) % 4)]; val cy = p[2 * ((i + 2) % 4) + 1]
719
+ val cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx)
720
+ if (cross != 0.0) {
721
+ val s = if (cross > 0.0) 1 else -1
722
+ if (sign == 0) sign = s else if (s != sign) return false
723
+ }
724
+ }
725
+ // Min-area via the shoelace formula (|2A| ≥ 2·minArea).
726
+ var area2 = 0.0
727
+ for (i in 0 until 4) {
728
+ val ax = p[2 * i]; val ay = p[2 * i + 1]
729
+ val bx = p[2 * ((i + 1) % 4)]; val by = p[2 * ((i + 1) % 4) + 1]
730
+ area2 += ax * by - bx * ay
731
+ }
732
+ if (Math.abs(area2) < minArea * 2.0) return false
733
+ // In-bounds — every corner inside the decoded image (½-px slop).
734
+ if (imageW > 0.0 && imageH > 0.0) {
735
+ val eps = 0.5
736
+ for (i in 0 until 4) {
737
+ val x = p[2 * i]; val y = p[2 * i + 1]
738
+ if (x < -eps || x > imageW + eps || y < -eps || y > imageH + eps) return false
739
+ }
740
+ }
741
+ return true
742
+ }
743
+
744
+ /**
745
+ * Port of cpp/warp_guard.hpp:canvasExceedsGuard — true when a
746
+ * `width`×`height` output canvas is degenerate (non-positive) or
747
+ * strictly larger than `maxPixels` (default 50 MP, boundary inclusive).
748
+ * int64 area math matches the C++ so the same quads are rejected.
749
+ */
750
+ private fun canvasExceedsGuard(
751
+ width: Long,
752
+ height: Long,
753
+ maxPixels: Long = 50L * 1000L * 1000L,
754
+ ): Boolean {
755
+ if (width <= 0 || height <= 0) return true
756
+ if (width > 3_000_000_000L || height > 3_000_000_000L) return true
757
+ return width * height > maxPixels
758
+ }
759
+
549
760
  // ── Internals ────────────────────────────────────────────────
550
761
 
551
762
  /**
@@ -623,9 +834,15 @@ class BatchStitcher(reactContext: ReactApplicationContext)
623
834
  // translation captures safely (PANORAMA on translation can
624
835
  // diverge → multi-GB canvas → lmkd kill).
625
836
  stitchMode: String = "scans",
837
+ // 2026-06-15 — pipeline picker (mirrors iOS' OpenCVStitcher
838
+ // `useManualPipeline:`). Defaults to true (MANUAL) so the
839
+ // batch-keyframe finalize orchestrator gets the memory-safe
840
+ // manual path without re-stating it. The refine/high-level
841
+ // path passes false to drive the stock cv::Stitcher pipeline.
842
+ useManualPipeline: Boolean = true,
626
843
  ): IntArray {
627
844
  ensureNativeStitcher()
628
- return nativeStitchFramePaths(
845
+ val dims = nativeStitchFramePaths(
629
846
  framePaths,
630
847
  outputPath,
631
848
  jpegQuality,
@@ -638,7 +855,12 @@ class BatchStitcher(reactContext: ReactApplicationContext)
638
855
  seamEstimationResolMP,
639
856
  compositingResolMP,
640
857
  stitchMode,
858
+ useManualPipeline,
641
859
  )
860
+ // Capture the run's debugSummary (pipe/warp/route/seam/blend) for the
861
+ // DEV overlay; best-effort so a getter hiccup never fails the stitch.
862
+ lastDebugSummary = try { nativeLastDebugSummary() } catch (_: Throwable) { "" }
863
+ return dims
642
864
  }
643
865
 
644
866
  /**