@sceneview-sdk/react-native 4.25.0 → 4.27.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.
package/README.md CHANGED
@@ -7,8 +7,14 @@
7
7
  React Native bindings for [SceneView](https://sceneview.github.io) — 3D and AR scenes powered by Filament (Android) and RealityKit (iOS).
8
8
 
9
9
  > **Status:** Alpha — 3D model loading works on both platforms. AR scene is
10
- > functional on Android. The iOS native bridge compiles against the real
11
- > `SceneViewSwift` API and is CI-verified (`.github/workflows/rn-ios-compile.yml`).
10
+ > functional on Android. The iOS native bridge is CI-verified against the real
11
+ > `SceneViewSwift` API **and the real React Native API**
12
+ > (`.github/workflows/rn-ios-compile.yml`): both modules come from an actual
13
+ > build — SceneViewSwift from SwiftPM, React from the demo's `pod install` —
14
+ > and the job proves each import is load-bearing before trusting the result.
15
+ > It type-checks `ios/*.swift` only: it does not link, does not run, and does
16
+ > not compile the Obj-C `RCT_EXTERN_MODULE` glue in `ios/RNSceneViewManager.m`
17
+ > that exposes the module to JS.
12
18
 
13
19
  ## Features
14
20
 
@@ -21,25 +27,17 @@ React Native bindings for [SceneView](https://sceneview.github.io) — 3D and AR
21
27
 
22
28
  ## Installation
23
29
 
24
- > **Status note (May 2026):** the npm registry currently has
25
- > `@sceneview-sdk/react-native@3.6.1` as `latest`. The 4.0.x line of this
26
- > bridge — including the type-safe `unlit` prop and the v4 SceneView
27
- > bindings — has not yet been published to npm. Tracked in
28
- > [#924](https://github.com/sceneview/sceneview/issues/924) and
29
- > [#962](https://github.com/sceneview/sceneview/issues/962) (publish-rn
30
- > GitHub Actions job lands automatically on the next `v4.0.x` tag).
31
- > To pin against the latest source today, prefer the GitHub-package
32
- > snippet at the bottom of this section.
33
-
34
30
  ```sh
35
31
  npm install @sceneview-sdk/react-native
36
32
  ```
37
33
 
38
- …or to ride main directly until the 4.0.x publish lands:
34
+ The `publish-rn` job publishes this bridge on every `vX.Y.Z` tag, so `latest`
35
+ on npm tracks the current SceneView release (#924, #962).
39
36
 
40
- ```sh
41
- npm install github:sceneview/sceneview#v4.0.9 --save # path includes react-native/react-native-sceneview/
42
- ```
37
+ There is no GitHub-install fallback: this is a monorepo with no root
38
+ `package.json`, so `npm install github:sceneview/sceneview` cannot resolve the
39
+ module at `react-native/react-native-sceneview/`. To build against unreleased
40
+ source, clone the repo and `npm install <path-to-clone>/react-native/react-native-sceneview`.
43
41
 
44
42
  ### iOS
45
43
 
@@ -47,15 +45,32 @@ npm install github:sceneview/sceneview#v4.0.9 --save # path includes react-nati
47
45
  cd ios && pod install
48
46
  ```
49
47
 
50
- Requires iOS 17+ and Xcode 15+.
48
+ Requires iOS 18+ and Xcode 16+ — `SceneViewSwift`'s own floor is iOS 18.0
49
+ (`SceneViewSwift/Package.swift`), so the host app's `Podfile` must declare
50
+ `platform :ios, '18.0'` rather than React Native's `min_ios_version_supported`
51
+ (13.4), or `pod install` fails to resolve this module.
51
52
 
52
53
  **The host app must add `SceneViewSwift` via Swift Package Manager.**
53
54
  `SceneViewSwift` ships as a SwiftPM package only (no CocoaPods spec), so this
54
55
  module's podspec deliberately does **not** declare it as a `s.dependency` —
55
56
  add it once in Xcode (*File ▸ Add Package Dependencies…*):
56
57
 
57
- - URL: `https://github.com/sceneview/SceneViewSwift`
58
- - Version: `4.14.0` (or *Up to Next Major*)
58
+ - URL: `https://github.com/sceneview/sceneview`
59
+ - Version: `4.27.0` (or *Up to Next Major*)
60
+
61
+ Unlike the Flutter plugin's pub.dev range, this version tracks the SDK release
62
+ directly: SwiftPM resolves against a git tag in this same repository, which the
63
+ release creates, so it is never ahead of something that does not exist yet.
64
+
65
+ > **Keep the two versions in step.** npm `4.27.0`'s Swift requires SPM tag
66
+ > `v4.27.0` or newer: this module's `ios/*.swift` builds on
67
+ > `SceneViewerHostView` — which landed *after* `v4.26.0`, so no `SceneViewer*`
68
+ > type exists at that tag or any earlier one — and it calls the two-parameter
69
+ > `onTapEntity` (the tapped model root is the second parameter). A mismatch is a
70
+ > loud Swift compile error at build time, not a runtime surprise, but no CI job
71
+ > here will catch it for you: `rn-ios-compile.yml` type-checks against the
72
+ > `SceneViewSwift` sources *in this repo*, not against the tag your app
73
+ > resolves.
59
74
 
60
75
  The module's `ios/*.swift` `import SceneViewSwift` resolves against that
61
76
  app-level package at build time.
@@ -84,7 +99,7 @@ import { SceneView } from '@sceneview-sdk/react-native';
84
99
  style={{ flex: 1 }}
85
100
  environment="environments/studio_small.hdr"
86
101
  modelNodes={[{ src: 'models/damaged_helmet.glb', position: [0, 0, -2] }]}
87
- cameraOrbit
102
+ cameraControlMode="orbit"
88
103
  />
89
104
  ```
90
105
 
@@ -109,15 +124,23 @@ import { ARSceneView } from '@sceneview-sdk/react-native';
109
124
  | `modelNodes` | `ModelNode[]` | `[]` | Models to render |
110
125
  | `geometryNodes` | `GeometryNode[]` | `[]` | Geometry nodes (forward-compatible) |
111
126
  | `lightNodes` | `LightNode[]` | `[]` | Light nodes (forward-compatible) |
112
- | `cameraOrbit` | `boolean` | `true` | Enable orbit camera controls |
127
+ | `cameraOrbit` | `boolean` | `true` | **Deprecated**, inert on iOS — use `cameraControlMode` |
113
128
  | `cameraControlMode` | `CameraControlMode` | `'orbit'` | Camera mode (v4.3.0). `'pan'`/`'firstPerson'` are iOS-only |
114
129
  | `autoCenterContent` | `boolean` | `true` | Auto-centre content on first frame (v4.3.0, iOS-first) |
115
- | `onTap` | `function` | — | Tap callback (event pending) |
130
+ | `onTap` | `function` | — | Tap callback — `{ x, y, z, nodeName }` |
116
131
 
117
132
  `cameraControlMode` `'pan'` and `'firstPerson'` are iOS-only in v4.3.0; on
118
133
  Android they fall back to orbit. `autoCenterContent` is iOS-first — the
119
134
  Android side is tracked in issue #1051.
120
135
 
136
+ `cameraOrbit` is **deprecated and inert on iOS**. It predates
137
+ `cameraControlMode`, and the two contradict each other — nothing can say which
138
+ should win for `cameraOrbit: false, cameraControlMode: 'orbit'` — so the iOS
139
+ bridge reads only `cameraControlMode`. Note this leaves **no** way to freeze the
140
+ camera from this bridge on iOS: `SceneViewSwift` has `cameraGesturesEnabled`,
141
+ but the React Native surface does not expose it yet. `cameraOrbit: false` still
142
+ works on Android.
143
+
121
144
  ### Props — ARSceneView (extends SceneView)
122
145
 
123
146
  | Prop | Type | Default | Description |
@@ -176,8 +199,12 @@ requireNativeComponent('RNSceneView' / 'RNARSceneView')
176
199
  +---> Android: ViewManager -> ComposeView -> SceneView { } / ARSceneView { }
177
200
  | (Filament, SceneView SDK)
178
201
  |
179
- +---> iOS: RCTViewManager -> UIHostingController -> SceneView / ARSceneView
180
- (RealityKit, SceneViewSwift)
202
+ +---> iOS 3D: RCTViewManager -> SceneViewerHostView -> SceneView
203
+ | (RealityKit, SceneViewSwift — the same host
204
+ | the Flutter plugin and sceneview-compose use)
205
+ |
206
+ +---> iOS AR: RCTViewManager -> UIHostingController -> ARSceneView
207
+ (RealityKit, SceneViewSwift)
181
208
  ```
182
209
 
183
210
  Props are mapped from the React Native bridge to native view parameters on each platform.
@@ -191,8 +218,22 @@ coverage map (tracked in [#909](https://github.com/sceneview/sceneview/issues/90
191
218
  the iOS RealityKit port is not yet bridged.
192
219
  - **`depthOcclusion` / `instantPlacement`** — declared as props but **not
193
220
  configured natively** on either platform. Setting them has no effect today.
194
- - **`onTap` / `onPlaneDetected`** — declared, but the native side does not yet
195
- dispatch these events, so the callbacks never fire.
221
+ - **`onPlaneDetected`** — dispatched on **Android** only; SceneViewSwift's
222
+ `ARSceneView` exposes no plane-detection callback, so it never fires on iOS.
223
+ - **`onTap`** — dispatched on both platforms and on both views. On `SceneView`
224
+ (3D) it carries the tapped model's world position and its file base name
225
+ without extension as `nodeName`, identically on Android and iOS. On
226
+ `ARSceneView` *what a hit reports* differs: **Android** hit-tests the AR
227
+ scene, so a tap on a model reports that model's file base name just as
228
+ `SceneView` does and a plane hit or a miss reports `null`; **iOS** always
229
+ reports `null`, because SceneViewSwift's `ARSceneView` exposes no entity
230
+ hit-test hook and its tap can only resolve the surface point
231
+ ([#2051](https://github.com/sceneview/sceneview/issues/2051)). Every dispatch
232
+ path still writes the key, so `nodeName == null` is the single "the tap hit no
233
+ model" test — it is never `undefined`. *How often* it dispatches differs
234
+ though: on iOS a tap that hits no entity fires no `onTap` at all (RealityKit's
235
+ gesture is entity-targeted), where Android dispatches a `0, 0, 0` miss — so
236
+ the two platforms do not deliver the same number of tap events.
196
237
  - **`environment` on `ARSceneView`** — AR scenes use the camera feed; the HDR
197
238
  environment is accepted but not applied.
198
239
  - **`ModelNode.scale`** — parsed as a uniform float; the per-axis `[x, y, z]`
@@ -208,6 +249,38 @@ coverage map (tracked in [#909](https://github.com/sceneview/sceneview/issues/90
208
249
 
209
250
  See [CONTRIBUTING.md](https://github.com/sceneview/sceneview/blob/main/.github/CONTRIBUTING.md).
210
251
 
252
+ ### Local checks
253
+
254
+ From this directory, after `npm ci`:
255
+
256
+ ```bash
257
+ npm run lint # Biome (repo-root biome.json) — lint + format + import assists
258
+ npm run lint:fix # same, applying the safe fixes
259
+ npm run typescript # tsc --noEmit
260
+ npm test # jest
261
+ ```
262
+
263
+ All four run on every PR that touches this package's TypeScript, via
264
+ [`.github/workflows/rn-ts-check.yml`](https://github.com/sceneview/sceneview/blob/main/.github/workflows/rn-ts-check.yml).
265
+ They do not all cover the same files:
266
+
267
+ | check | covers |
268
+ |---|---|
269
+ | `lint` | `src/`, `__tests__/`, `example/src/` |
270
+ | `typescript` | `src/` only — that is `tsconfig.json`'s `include` |
271
+ | `test` | `__tests__/` |
272
+
273
+ The linter is **Biome**, not ESLint — the repo has a single JS/TS rule set in
274
+ the root `biome.json`, and those three directories are listed in its
275
+ `files.includes`. The `lint` scripts `cd` to the repo root before invoking
276
+ Biome, mirroring `mcp/package.json`'s script; Biome also walks up and finds the
277
+ root config on its own, so calling `./node_modules/.bin/biome check .` from
278
+ this directory works too.
279
+
280
+ Note that most rules in `biome.json` are **warning** severity, so `npm run
281
+ lint` can exit 0 with findings still printed. Read its output, don't just read
282
+ its exit code.
283
+
211
284
  ## License
212
285
 
213
286
  Apache-2.0 — see [LICENSE](LICENSE) for details.
@@ -18,11 +18,32 @@ android {
18
18
  minSdk = 24
19
19
  }
20
20
 
21
+ // Explicit and REQUIRED — not boilerplate. Without it AGP defaults Java to
22
+ // 1.8 while the Kotlin plugin picks the toolchain JDK, and the build dies
23
+ // with "Inconsistent JVM-target compatibility detected for tasks
24
+ // 'compileReleaseJavaWithJavac' (1.8) and 'compileReleaseKotlin' (22)".
25
+ // A host RN app does NOT fix this for us: React Native's Gradle plugin
26
+ // supplies plugin versions, never a library module's JVM target. 17 matches
27
+ // the Flutter plugin (`flutter/sceneview_flutter/android/build.gradle`) and
28
+ // React Native's own JDK 17 requirement. Caught by the standalone compile
29
+ // gate (`tools/rn-android-compile`, #3042) the first time this module was
30
+ // ever compiled by CI.
31
+ compileOptions {
32
+ sourceCompatibility = JavaVersion.VERSION_17
33
+ targetCompatibility = JavaVersion.VERSION_17
34
+ }
35
+
21
36
  buildFeatures {
22
37
  compose = true
23
38
  }
24
39
  }
25
40
 
41
+ kotlin {
42
+ compilerOptions {
43
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
44
+ }
45
+ }
46
+
26
47
  dependencies {
27
48
  // SceneView — track the last PUBLISHED release on Maven Central (4.7.0).
28
49
  // The bridge Kotlin already targets the 4.x API surface (the `SceneView { }`
@@ -191,6 +191,11 @@ class ARSceneViewManager : SimpleViewManager<FrameLayout>() {
191
191
  autoAnimate = model.animate,
192
192
  position = model.position,
193
193
  rotation = model.rotation,
194
+ apply = {
195
+ // The tap payload's `nodeName` — see
196
+ // [ModelNodeData.nodeName] (PR #3037).
197
+ name = model.nodeName()
198
+ },
194
199
  )
195
200
  }
196
201
  }
@@ -13,8 +13,14 @@ import io.github.sceneview.node.Node
13
13
  * RN Fabric event delivered to the JS `onTap` prop of `<SceneView>` / `<ARSceneView>`.
14
14
  *
15
15
  * Payload mirrors the TypeScript `TapEvent` interface in `src/index.tsx`:
16
- * `{ x, y, z, nodeName? }`. Wired in [SceneViewManager] / [ARSceneViewManager]
17
- * via `getExportedCustomDirectEventTypeConstants` (issue #2053).
16
+ * `{ x, y, z, nodeName }` — the key is never optional. [getEventData] writes it
17
+ * on every dispatch, `putNull` when no node was hit, so JS sees `null` and never
18
+ * `undefined`. Wired in [SceneViewManager] / [ARSceneViewManager] via
19
+ * `getExportedCustomDirectEventTypeConstants` (issue #2053).
20
+ *
21
+ * Both managers share this event, so an AR tap that lands on a model reports
22
+ * that model's name exactly as the 3D view does. iOS AR cannot: its
23
+ * `ARSceneView` has no entity hit-test hook and always reports `null` (#2051).
18
24
  */
19
25
  class TapEvent(
20
26
  surfaceId: Int,
@@ -151,6 +151,13 @@ class SceneViewManager : SimpleViewManager<FrameLayout>() {
151
151
  autoAnimate = model.animate,
152
152
  position = model.position,
153
153
  rotation = model.rotation,
154
+ apply = {
155
+ // The tap payload's `nodeName`. Without a name the
156
+ // hit-tested ModelNode reports `null` for every model
157
+ // tap, while iOS reports the model file's base name —
158
+ // so name the node the same way here (PR #3037).
159
+ name = model.nodeName()
160
+ },
154
161
  )
155
162
  }
156
163
  }
@@ -383,7 +390,28 @@ data class ModelNodeData(
383
390
  val animate: Boolean = true,
384
391
  val position: Position = Position(x = 0f),
385
392
  val rotation: Rotation = Rotation(x = 0f),
386
- )
393
+ ) {
394
+ /**
395
+ * The name reported as the tap payload's `nodeName`: the model file's base
396
+ * name without extension, matching the iOS bridge (which names each loaded
397
+ * model root after its file and strips the extension on tap).
398
+ *
399
+ * [src] is documented as an "asset path **or URL**", and `ModelLoader`
400
+ * really does load `https://` sources, so the query and fragment are
401
+ * stripped FIRST. Cutting at the last `.` on a raw URL only works when the
402
+ * extension is the last dot in the whole string: for
403
+ * `https://cdn/robot.glb?sig=SIG&v=1.2` it yields
404
+ * `robot.glb?sig=SIG&v=1` — a CDN signature leaking into a payload that
405
+ * apps routinely put in a label or an analytics event.
406
+ *
407
+ * `null` for a path with no usable base name, so the payload stays
408
+ * `nodeName: null` rather than an empty string.
409
+ */
410
+ fun nodeName(): String? =
411
+ src.substringBefore('?').substringBefore('#')
412
+ .substringAfterLast('/').substringBeforeLast('.')
413
+ .takeIf { it.isNotEmpty() }
414
+ }
387
415
 
388
416
  data class GeometryNodeData(
389
417
  val type: String,
@@ -18,6 +18,48 @@ struct RNModelData: Identifiable, Equatable {
18
18
  }
19
19
  }
20
20
 
21
+ // MARK: - Tap payload
22
+
23
+ /// Builds the JS `onTap` payload — the iOS counterpart of Android's single
24
+ /// `TapEvent.getEventData()`.
25
+ ///
26
+ /// Both iOS views go through here so `nodeName` is written on *every* dispatch
27
+ /// path, never merely on the ones that happen to have a name: a missed tap
28
+ /// reports `NSNull()`, which crosses the bridge as the same JS `null` Android
29
+ /// emits via `putNull`. That is what lets the public `TapEvent.nodeName` be
30
+ /// typed `string | null` instead of `string | null | undefined` — a consumer
31
+ /// writes one `nodeName == null` guard.
32
+ ///
33
+ /// This iOS `ARSceneView` can only resolve a surface point, never an entity, so
34
+ /// it always passes `nil`. That is a platform limitation, not the contract:
35
+ /// Android's AR view hit-tests the scene and does name the tapped model. Naming
36
+ /// it here needs an entity hit-test hook on `SceneViewSwift.ARSceneView`, which
37
+ /// does not exist yet (#2051).
38
+ ///
39
+ /// Keeping this a single function (rather than a comment asking each call site
40
+ /// to remember the key) is the guard: a new tap source cannot omit `nodeName`
41
+ /// without skipping the only payload builder in the bridge.
42
+ func rnTapPayload(worldPosition: SIMD3<Float>, nodeName: String?) -> [String: Any] {
43
+ // Seeded with the null sentinel, then overwritten — so the key exists
44
+ // before any branch runs and no path can drop it.
45
+ var payload: [String: Any] = [
46
+ "x": worldPosition.x,
47
+ "y": worldPosition.y,
48
+ "z": worldPosition.z,
49
+ "nodeName": NSNull(),
50
+ ]
51
+ // An empty name is "no model", not a model called "". Android already
52
+ // collapses it (`SceneViewManager.nodeName()` ends in
53
+ // `takeIf { it.isNotEmpty() }`); collapsing it here rather than at the call
54
+ // sites is what keeps the two platforms on ONE sentinel, for the same
55
+ // reason the key is seeded above — a future tap source cannot reintroduce
56
+ // the empty string without going around the only payload builder.
57
+ if let nodeName, !nodeName.isEmpty {
58
+ payload["nodeName"] = nodeName
59
+ }
60
+ return payload
61
+ }
62
+
21
63
  // MARK: - SceneView (3D)
22
64
 
23
65
  /// RCTViewManager subclass that bridges React Native's `<RNSceneView>`
@@ -34,35 +76,43 @@ class RNSceneViewManager: RCTViewManager {
34
76
  }
35
77
  }
36
78
 
37
- /// Maps the wire string sent from JS to a `CameraControlMode` (v4.3.0).
38
- /// Unknown values fall back to `.orbit`.
39
- func rnCameraControlMode(_ raw: String?) -> CameraControlMode {
40
- switch raw {
41
- case "pan": return .pan
42
- case "firstPerson": return .firstPerson
43
- default: return .orbit
44
- }
45
- }
46
-
47
- /// Observable state model shared between React props and SwiftUI view.
79
+ /// State model backing the React props.
80
+ ///
81
+ /// Still an `ObservableObject` for shape consistency with `RNARSceneState`, which
82
+ /// genuinely drives SwiftUI; nothing observes this one since the 3D scene moved
83
+ /// onto `SceneViewerHostView`.
48
84
  @MainActor
49
85
  class RNSceneState: ObservableObject {
50
86
  @Published var models: [RNModelData] = []
51
87
  @Published var environmentPath: String?
88
+
89
+ /// Accepted, stored, and deliberately not applied — as before this bridge
90
+ /// moved onto the shared host.
91
+ ///
92
+ /// `cameraControlMode` supersedes it and the two would contradict each other
93
+ /// (which wins for `cameraOrbit: false, cameraControlMode: "orbit"`?). Wiring
94
+ /// it up here would be a behaviour change dressed as a refactor, so it stays
95
+ /// inert; the TypeScript surface deprecates it.
52
96
  @Published var cameraOrbit: Bool = true
53
- @Published var cameraControlMode: CameraControlMode = .orbit
54
- @Published var autoCenterContent: Bool = true
55
97
 
56
- /// Invoked from the SwiftUI content's `onEntityTapped` modifier so the
57
- /// wrapper can forward the tap to React Native's `onTap` prop (issue #2053).
58
- /// Not `@Published` — it is plumbing, not rendered state.
59
- var onTap: ((Entity) -> Void)?
98
+ /// Wire name as it arrives from JS: `"orbit"`, `"pan"` or `"firstPerson"`.
99
+ /// `SceneViewerConfiguration` normalises an unrecognised value to `"orbit"`,
100
+ /// which is what the local mapping this replaces did.
101
+ @Published var cameraControlMode: String = "orbit"
102
+
103
+ @Published var autoCenterContent: Bool = true
60
104
  }
61
105
 
62
- /// UIView wrapper that hosts a SwiftUI `SceneView` via UIHostingController.
106
+ /// UIView wrapper hosting the shared `SceneViewerHostView`.
107
+ ///
108
+ /// This class owns the React prop bag; the scene itself — hosting SwiftUI in
109
+ /// UIKit, loading models, reconciling them — belongs to `SceneViewerHostView`
110
+ /// and is shared with the Flutter plugin and `sceneview-compose`. The AR wrapper
111
+ /// below keeps its own SwiftUI content: `ARSceneView` is anchor-driven and
112
+ /// shares nothing with the 3D viewer.
63
113
  class RNSceneViewWrapper: UIView {
64
114
 
65
- private var hostingController: UIHostingController<RNSceneViewContent>?
115
+ private let hostView = SceneViewerHostView()
66
116
  private let sceneState = RNSceneState()
67
117
 
68
118
  /// Event callback for tap events.
@@ -70,16 +120,21 @@ class RNSceneViewWrapper: UIView {
70
120
  didSet {
71
121
  let block = onTap
72
122
  Task { @MainActor in
73
- sceneState.onTap = { entity in
123
+ // `onTapEntity` rather than the host's flattened `onTap`: this
124
+ // prop reports the tapped *model* — its own origin and its file
125
+ // base name — where the flattened callback carries the hit's
126
+ // bounds centre and no name at all.
127
+ //
128
+ // The model root is what the host resolves, so a tap inside a
129
+ // USDZ that names its meshes reports the model, not `skin0`.
130
+ hostView.onTapEntity = { _, modelRoot in
74
131
  // Mirrors the Android `TapEvent` payload: world-space
75
- // coordinates of the tapped entity + its node name.
76
- let p = entity.position(relativeTo: nil)
77
- block?([
78
- "x": p.x,
79
- "y": p.y,
80
- "z": p.z,
81
- "nodeName": entity.name,
82
- ])
132
+ // coordinates of the tapped model (`node.worldPosition`
133
+ // there) + its node name, `null` when nothing was hit.
134
+ block?(rnTapPayload(
135
+ worldPosition: modelRoot?.position(relativeTo: nil) ?? .zero,
136
+ nodeName: modelRoot.map { ($0.name as NSString).deletingPathExtension }
137
+ ))
83
138
  }
84
139
  }
85
140
  }
@@ -96,12 +151,55 @@ class RNSceneViewWrapper: UIView {
96
151
  }
97
152
 
98
153
  private func setupView() {
99
- let content = RNSceneViewContent(state: sceneState)
100
- let hosting = UIHostingController(rootView: content)
101
- hosting.view.frame = bounds
102
- hosting.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
103
- addSubview(hosting.view)
104
- hostingController = hosting
154
+ hostView.frame = bounds
155
+ hostView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
156
+ addSubview(hostView)
157
+ }
158
+
159
+ /// Pushes the current props into the host view.
160
+ ///
161
+ /// Called after every prop change. `applyConfiguration` compares field by
162
+ /// field and touches only what changed, so re-sending everything on each
163
+ /// prop update costs nothing.
164
+ @MainActor
165
+ private func applyState() {
166
+ let configuration = SceneViewerConfiguration()
167
+
168
+ configuration.models = sceneState.models.map { data in
169
+ let model = SceneViewerModel()
170
+ model.assetPath = data.path
171
+ // The per-entry id minted when the prop was decoded. A new
172
+ // `modelNodes` value mints new ids, which is what gives this prop
173
+ // its replace-the-whole-list semantics.
174
+ model.identity = data.id.uuidString
175
+ model.setScale(data.scale.x, data.scale.y, data.scale.z)
176
+ model.setPosition(data.position.x, data.position.y, data.position.z)
177
+ model.animationName = data.animation
178
+ // No named animation means play them all, which is this bridge's
179
+ // behaviour and not the Flutter one's.
180
+ model.autoPlayAllAnimations = true
181
+ // The file's name — with its extension, which the tap report
182
+ // strips. This is the only entity the bridge names, and the host
183
+ // resolves a tap to exactly it, so it is what JS receives as
184
+ // `nodeName`. Matches Android's `ModelNodeData.nodeName()`.
185
+ model.nodeName = sceneViewerModelFileName(data.path)
186
+ return model
187
+ }
188
+
189
+ configuration.cameraControlMode = sceneState.cameraControlMode
190
+ configuration.autoCenterContent = sceneState.autoCenterContent
191
+ // This bridge exposes no camera prop, so it authors no pose: without
192
+ // this, every prop change would re-assert the configuration's default
193
+ // pose and snap the camera back out of the auto-centre framing and away
194
+ // from wherever the user had orbited to.
195
+ configuration.cameraPoseAuthored = false
196
+
197
+ if let path = sceneState.environmentPath, !path.isEmpty {
198
+ configuration.environmentKind = "hdr"
199
+ configuration.environmentHdrPath = path
200
+ }
201
+
202
+ hostView.applyConfiguration(configuration)
105
203
  }
106
204
 
107
205
  // MARK: - React props
@@ -110,6 +208,7 @@ class RNSceneViewWrapper: UIView {
110
208
  didSet {
111
209
  Task { @MainActor in
112
210
  sceneState.environmentPath = environment
211
+ applyState()
113
212
  }
114
213
  }
115
214
  }
@@ -136,10 +235,12 @@ class RNSceneViewWrapper: UIView {
136
235
  let animation = dict["animation"] as? String
137
236
  return RNModelData(path: src, scale: scale, position: position, animation: animation)
138
237
  } ?? []
238
+ applyState()
139
239
  }
140
240
  }
141
241
  }
142
242
 
243
+ /// **Deprecated.** Superseded by `cameraControlMode`; see `RNSceneState.cameraOrbit`.
143
244
  @objc var cameraOrbit: Bool = true {
144
245
  didSet {
145
246
  Task { @MainActor in
@@ -151,9 +252,10 @@ class RNSceneViewWrapper: UIView {
151
252
  /// Camera interaction mode (v4.3.0, issue #1053).
152
253
  @objc var cameraControlMode: String? {
153
254
  didSet {
154
- let mode = rnCameraControlMode(cameraControlMode)
255
+ let raw = cameraControlMode
155
256
  Task { @MainActor in
156
- sceneState.cameraControlMode = mode
257
+ sceneState.cameraControlMode = raw ?? "orbit"
258
+ applyState()
157
259
  }
158
260
  }
159
261
  }
@@ -163,78 +265,7 @@ class RNSceneViewWrapper: UIView {
163
265
  didSet {
164
266
  Task { @MainActor in
165
267
  sceneState.autoCenterContent = autoCenterContent
166
- }
167
- }
168
- }
169
- }
170
-
171
- /// SwiftUI content view rendering `SceneViewSwift.SceneView` (issue #2067).
172
- ///
173
- /// `SceneViewSwift` loads models **asynchronously** (`ModelNode.load(_:)` is
174
- /// `async throws`) — there is no synchronous `ModelNode(String)` initialiser,
175
- /// and the `@NodeBuilder` content builder composes static `EntityProvider`
176
- /// values, not a SwiftUI `ForEach`. So the bridge loads each `RNModelData`
177
- /// off the main actor's `Task`, wraps the resulting `ModelEntity`s in a
178
- /// stable holder entity, and feeds that holder to `SceneView`'s **imperative**
179
- /// `init(_ content: (Entity) -> Void)`.
180
- ///
181
- /// `SceneView`'s content closure runs once when the underlying `RealityView`
182
- /// is created, so the holder entity is built up front and re-populated in a
183
- /// `.task(id:)` whenever the JS `modelNodes` prop changes — RealityKit picks
184
- /// up the new children on its next render frame without recreating the view.
185
- struct RNSceneViewContent: View {
186
- @ObservedObject var state: RNSceneState
187
-
188
- /// Stable parent entity handed to `SceneView`. Its children are the
189
- /// loaded model entities; rebuilt by `loadModels()` on every prop change.
190
- @State private var modelRoot = Entity()
191
-
192
- var body: some View {
193
- SceneView { root in
194
- root.addChild(modelRoot)
195
- }
196
- .cameraControls(state.cameraControlMode)
197
- .autoCenterContent(state.autoCenterContent)
198
- // Forward entity taps to React Native's `onTap` prop (issue #2053).
199
- .onEntityTapped { entity in
200
- state.onTap?(entity)
201
- }
202
- // Reload whenever the JS `modelNodes` prop changes. Keyed on the
203
- // model identities so an unrelated re-render does not re-download.
204
- .task(id: state.models.map(\.id)) {
205
- await loadModels()
206
- }
207
- }
208
-
209
- /// Loads every model in `state.models` and replaces `modelRoot`'s
210
- /// children with the freshly loaded entities. `ModelNode.load(_:)` is
211
- /// `@MainActor`-isolated, so this runs on the main actor as required by
212
- /// RealityKit. A failed load is skipped (the others still render).
213
- @MainActor
214
- private func loadModels() async {
215
- // Clear previous content before reloading.
216
- for child in modelRoot.children {
217
- child.removeFromParent()
218
- }
219
- for model in state.models {
220
- do {
221
- let node = try await ModelNode.load(model.path)
222
- // `.task(id:)` cancels this task when the `modelNodes` prop
223
- // changes mid-load. A cancelled task still resumes past the
224
- // `await`, so bail out before mutating the scene — otherwise a
225
- // superseded load leaks a stale model into `modelRoot`.
226
- guard !Task.isCancelled else { return }
227
- node.position(model.position)
228
- node.scale(model.scale)
229
- if let animation = model.animation {
230
- node.playAnimation(named: animation)
231
- } else if node.animationCount > 0 {
232
- node.playAllAnimations()
233
- }
234
- modelRoot.addChild(node.entity)
235
- } catch {
236
- // A single bad path must not break the whole scene.
237
- print("[RNSceneView] Failed to load model '\(model.path)': \(error)")
268
+ applyState()
238
269
  }
239
270
  }
240
271
  }
@@ -284,12 +315,14 @@ class RNARSceneViewWrapper: UIView {
284
315
  sceneState.onTap = { worldPosition in
285
316
  // Mirrors the Android `TapEvent` payload. `ARSceneView`'s
286
317
  // `onTapOnPlane` reports the tapped surface point, not a
287
- // node, so `nodeName` is left absent.
288
- block?([
289
- "x": worldPosition.x,
290
- "y": worldPosition.y,
291
- "z": worldPosition.z,
292
- ])
318
+ // node, so `nodeName` is always `null` here — the same JS
319
+ // `null` Android emits via `putNull` and the 3D view emits
320
+ // for a tap that hit no model. It used to omit the key,
321
+ // which made JS see `undefined` on this one view.
322
+ //
323
+ // Android's AR view names the tapped model instead; this
324
+ // side cannot until #2051 adds an entity hit-test hook.
325
+ block?(rnTapPayload(worldPosition: worldPosition, nodeName: nil))
293
326
  }
294
327
  }
295
328
  }
@@ -7,6 +7,13 @@ exports.SceneView = exports.ARSceneView = exports.ARRecorder = void 0;
7
7
  var _react = _interopRequireDefault(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
+ // `tsconfig.json` sets `"jsx": "react"` — the CLASSIC runtime — so every JSX
11
+ // element below compiles to `React.createElement(...)` (verified in the
12
+ // published `lib/commonjs/index.js`). Biome's `useImportType` only sees the
13
+ // `React.FC` type annotations, not the JSX lowering, so it offers a "safe fix"
14
+ // that would erase this import and break the shipped bundle at runtime.
15
+ // biome-ignore lint/style/useImportType: React is a VALUE import — see above.
16
+
10
17
  // ---------------------------------------------------------------------------
11
18
  // Node type interfaces
12
19
  // ---------------------------------------------------------------------------
@@ -55,9 +62,9 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
55
62
  // Native components (only available on Android and iOS)
56
63
  // ---------------------------------------------------------------------------
57
64
 
58
- const isNativeAvailable = _reactNative.Platform.OS === 'android' || _reactNative.Platform.OS === 'ios';
59
- const NativeSceneView = isNativeAvailable ? (0, _reactNative.requireNativeComponent)('RNSceneView') : null;
60
- const NativeARSceneView = isNativeAvailable ? (0, _reactNative.requireNativeComponent)('RNARSceneView') : null;
65
+ const isNativeAvailable = _reactNative.Platform.OS === "android" || _reactNative.Platform.OS === "ios";
66
+ const NativeSceneView = isNativeAvailable ? (0, _reactNative.requireNativeComponent)("RNSceneView") : null;
67
+ const NativeARSceneView = isNativeAvailable ? (0, _reactNative.requireNativeComponent)("RNARSceneView") : null;
61
68
 
62
69
  // ---------------------------------------------------------------------------
63
70
  // Fallback for unsupported platforms
@@ -73,12 +80,12 @@ const UnsupportedView = ({
73
80
  const fallbackStyles = _reactNative.StyleSheet.create({
74
81
  container: {
75
82
  flex: 1,
76
- justifyContent: 'center',
77
- alignItems: 'center',
78
- backgroundColor: '#1a1a2e'
83
+ justifyContent: "center",
84
+ alignItems: "center",
85
+ backgroundColor: "#1a1a2e"
79
86
  },
80
87
  text: {
81
- color: '#aaa',
88
+ color: "#aaa",
82
89
  fontSize: 16
83
90
  }
84
91
  });
@@ -158,10 +165,10 @@ const NativeARRecorder = _reactNative.NativeModules.RNARRecorder;
158
165
  class ARRecorder {
159
166
  /** `true` when {@link ARRecorder} is supported on the current platform. */
160
167
  static get isSupported() {
161
- return _reactNative.Platform.OS === 'ios' && NativeARRecorder != null;
168
+ return _reactNative.Platform.OS === "ios" && NativeARRecorder != null;
162
169
  }
163
170
  rejectUnsupported() {
164
- return Promise.reject(new Error('ARRecorder is currently only supported on iOS. Android AR session ' + 'recording is tracked in issue #1051.'));
171
+ return Promise.reject(new Error("ARRecorder is currently only supported on iOS. Android AR session " + "recording is tracked in issue #1051."));
165
172
  }
166
173
 
167
174
  /** Starts an AR session recording. */
@@ -1 +1 @@
1
- {"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","e","__esModule","default","isNativeAvailable","Platform","OS","NativeSceneView","requireNativeComponent","NativeARSceneView","UnsupportedView","name","createElement","View","style","fallbackStyles","container","Text","text","StyleSheet","create","flex","justifyContent","alignItems","backgroundColor","color","fontSize","SceneView","props","exports","ARSceneView","NativeARRecorder","NativeModules","RNARRecorder","ARRecorder","isSupported","rejectUnsupported","Promise","reject","Error","start","stop","outputPath","saveToPhotoLibrary","movPath"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","e","__esModule","default","isNativeAvailable","Platform","OS","NativeSceneView","requireNativeComponent","NativeARSceneView","UnsupportedView","name","createElement","View","style","fallbackStyles","container","Text","text","StyleSheet","create","flex","justifyContent","alignItems","backgroundColor","color","fontSize","SceneView","props","exports","ARSceneView","NativeARRecorder","NativeModules","RNARRecorder","ARRecorder","isSupported","rejectUnsupported","Promise","reject","Error","start","stop","outputPath","saveToPhotoLibrary","movPath"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;;;;;AAMA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AASsB,SAAAD,uBAAAG,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAhBtB;AACA;AACA;AACA;AACA;AACA;;AAaA;AACA;AACA;;AAEA;;AAiBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAmBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AASA;AACA;AACA;;AA+BA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAwHA;AACA;AACA;;AAEA,MAAMG,iBAAiB,GAAGC,qBAAQ,CAACC,EAAE,KAAK,SAAS,IAAID,qBAAQ,CAACC,EAAE,KAAK,KAAK;AAE5E,MAAMC,eAAe,GAAGH,iBAAiB,GACrC,IAAAI,mCAAsB,EAAiB,aAAa,CAAC,GACrD,IAAI;AAER,MAAMC,iBAAiB,GAAGL,iBAAiB,GACvC,IAAAI,mCAAsB,EAAmB,eAAe,CAAC,GACzD,IAAI;;AAER;AACA;AACA;;AAEA,MAAME,eAA2C,GAAGA,CAAC;EAAEC;AAAK,CAAC,kBAC3Dd,MAAA,CAAAM,OAAA,CAAAS,aAAA,CAACZ,YAAA,CAAAa,IAAI;EAACC,KAAK,EAAEC,cAAc,CAACC;AAAU,gBACpCnB,MAAA,CAAAM,OAAA,CAAAS,aAAA,CAACZ,YAAA,CAAAiB,IAAI;EAACH,KAAK,EAAEC,cAAc,CAACG;AAAK,GAAEP,IAAI,EAAC,oCAAwC,CAC5E,CACP;AAED,MAAMI,cAAc,GAAGI,uBAAU,CAACC,MAAM,CAAC;EACvCJ,SAAS,EAAE;IACTK,IAAI,EAAE,CAAC;IACPC,cAAc,EAAE,QAAQ;IACxBC,UAAU,EAAE,QAAQ;IACpBC,eAAe,EAAE;EACnB,CAAC;EACDN,IAAI,EAAE;IACJO,KAAK,EAAE,MAAM;IACbC,QAAQ,EAAE;EACZ;AACF,CAAC,CAAC;;AAEF;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMC,SAAmC,GAAIC,KAAK,IAAK;EAC5D,IAAI,CAACrB,eAAe,EAAE;IACpB,oBAAOV,MAAA,CAAAM,OAAA,CAAAS,aAAA,CAACF,eAAe;MAACC,IAAI,EAAC;IAAW,CAAE,CAAC;EAC7C;EACA,oBAAOd,MAAA,CAAAM,OAAA,CAAAS,aAAA,CAACL,eAAe,EAAKqB,KAAQ,CAAC;AACvC,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AATAC,OAAA,CAAAF,SAAA,GAAAA,SAAA;AAUO,MAAMG,WAAuC,GAAIF,KAAK,IAAK;EAChE,IAAI,CAACnB,iBAAiB,EAAE;IACtB,oBAAOZ,MAAA,CAAAM,OAAA,CAAAS,aAAA,CAACF,eAAe;MAACC,IAAI,EAAC;IAAa,CAAE,CAAC;EAC/C;EACA,oBAAOd,MAAA,CAAAM,OAAA,CAAAS,aAAA,CAACH,iBAAiB,EAAKmB,KAAQ,CAAC;AACzC,CAAC;;AAED;AACA;AACA;;AAEA;AAAAC,OAAA,CAAAC,WAAA,GAAAA,WAAA;AAOA,MAAMC,gBAAgD,GAAGC,0BAAa,CAACC,YAAY;;AAEnF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMC,UAAU,CAAC;EACtB;EACA,WAAWC,WAAWA,CAAA,EAAY;IAChC,OAAO9B,qBAAQ,CAACC,EAAE,KAAK,KAAK,IAAIyB,gBAAgB,IAAI,IAAI;EAC1D;EAEQK,iBAAiBA,CAAA,EAAmB;IAC1C,OAAOC,OAAO,CAACC,MAAM,CACnB,IAAIC,KAAK,CACP,oEAAoE,GAClE,sCACJ,CACF,CAAC;EACH;;EAEA;EACAC,KAAKA,CAAA,EAAkB;IACrB,IAAI,CAACN,UAAU,CAACC,WAAW,IAAI,CAACJ,gBAAgB,EAAE;MAChD,OAAO,IAAI,CAACK,iBAAiB,CAAC,CAAC;IACjC;IACA,OAAOL,gBAAgB,CAACS,KAAK,CAAC,CAAC;EACjC;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,IAAIA,CAACC,UAAmB,EAAmB;IACzC,IAAI,CAACR,UAAU,CAACC,WAAW,IAAI,CAACJ,gBAAgB,EAAE;MAChD,OAAO,IAAI,CAACK,iBAAiB,CAAC,CAAC;IACjC;IACA,OAAOL,gBAAgB,CAACU,IAAI,CAACC,UAAU,IAAI,IAAI,CAAC;EAClD;;EAEA;EACAC,kBAAkBA,CAACC,OAAe,EAAiB;IACjD,IAAI,CAACV,UAAU,CAACC,WAAW,IAAI,CAACJ,gBAAgB,EAAE;MAChD,OAAO,IAAI,CAACK,iBAAiB,CAAC,CAAC;IACjC;IACA,OAAOL,gBAAgB,CAACY,kBAAkB,CAACC,OAAO,CAAC;EACrD;AACF;AAACf,OAAA,CAAAK,UAAA,GAAAA,UAAA","ignoreList":[]}
@@ -1,5 +1,11 @@
1
- import React from 'react';
2
- import { requireNativeComponent, NativeModules, Platform, View, Text, StyleSheet } from 'react-native';
1
+ // `tsconfig.json` sets `"jsx": "react"` — the CLASSIC runtime — so every JSX
2
+ // element below compiles to `React.createElement(...)` (verified in the
3
+ // published `lib/commonjs/index.js`). Biome's `useImportType` only sees the
4
+ // `React.FC` type annotations, not the JSX lowering, so it offers a "safe fix"
5
+ // that would erase this import and break the shipped bundle at runtime.
6
+ // biome-ignore lint/style/useImportType: React is a VALUE import — see above.
7
+ import React from "react";
8
+ import { NativeModules, Platform, requireNativeComponent, StyleSheet, Text, View } from "react-native";
3
9
 
4
10
  // ---------------------------------------------------------------------------
5
11
  // Node type interfaces
@@ -49,9 +55,9 @@ import { requireNativeComponent, NativeModules, Platform, View, Text, StyleSheet
49
55
  // Native components (only available on Android and iOS)
50
56
  // ---------------------------------------------------------------------------
51
57
 
52
- const isNativeAvailable = Platform.OS === 'android' || Platform.OS === 'ios';
53
- const NativeSceneView = isNativeAvailable ? requireNativeComponent('RNSceneView') : null;
54
- const NativeARSceneView = isNativeAvailable ? requireNativeComponent('RNARSceneView') : null;
58
+ const isNativeAvailable = Platform.OS === "android" || Platform.OS === "ios";
59
+ const NativeSceneView = isNativeAvailable ? requireNativeComponent("RNSceneView") : null;
60
+ const NativeARSceneView = isNativeAvailable ? requireNativeComponent("RNARSceneView") : null;
55
61
 
56
62
  // ---------------------------------------------------------------------------
57
63
  // Fallback for unsupported platforms
@@ -67,12 +73,12 @@ const UnsupportedView = ({
67
73
  const fallbackStyles = StyleSheet.create({
68
74
  container: {
69
75
  flex: 1,
70
- justifyContent: 'center',
71
- alignItems: 'center',
72
- backgroundColor: '#1a1a2e'
76
+ justifyContent: "center",
77
+ alignItems: "center",
78
+ backgroundColor: "#1a1a2e"
73
79
  },
74
80
  text: {
75
- color: '#aaa',
81
+ color: "#aaa",
76
82
  fontSize: 16
77
83
  }
78
84
  });
@@ -151,10 +157,10 @@ const NativeARRecorder = NativeModules.RNARRecorder;
151
157
  export class ARRecorder {
152
158
  /** `true` when {@link ARRecorder} is supported on the current platform. */
153
159
  static get isSupported() {
154
- return Platform.OS === 'ios' && NativeARRecorder != null;
160
+ return Platform.OS === "ios" && NativeARRecorder != null;
155
161
  }
156
162
  rejectUnsupported() {
157
- return Promise.reject(new Error('ARRecorder is currently only supported on iOS. Android AR session ' + 'recording is tracked in issue #1051.'));
163
+ return Promise.reject(new Error("ARRecorder is currently only supported on iOS. Android AR session " + "recording is tracked in issue #1051."));
158
164
  }
159
165
 
160
166
  /** Starts an AR session recording. */
@@ -1 +1 @@
1
- {"version":3,"names":["React","requireNativeComponent","NativeModules","Platform","View","Text","StyleSheet","isNativeAvailable","OS","NativeSceneView","NativeARSceneView","UnsupportedView","name","createElement","style","fallbackStyles","container","text","create","flex","justifyContent","alignItems","backgroundColor","color","fontSize","SceneView","props","ARSceneView","NativeARRecorder","RNARRecorder","ARRecorder","isSupported","rejectUnsupported","Promise","reject","Error","start","stop","outputPath","saveToPhotoLibrary","movPath"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SACEC,sBAAsB,EACtBC,aAAa,EACbC,QAAQ,EAGRC,IAAI,EACJC,IAAI,EACJC,UAAU,QACL,cAAc;;AAErB;AACA;AACA;;AAEA;;AAiBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAmBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AASA;AACA;AACA;;AAkBA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAyFA;AACA;AACA;;AAEA,MAAMC,iBAAiB,GAAGJ,QAAQ,CAACK,EAAE,KAAK,SAAS,IAAIL,QAAQ,CAACK,EAAE,KAAK,KAAK;AAE5E,MAAMC,eAAe,GAAGF,iBAAiB,GACrCN,sBAAsB,CAAiB,aAAa,CAAC,GACrD,IAAI;AAER,MAAMS,iBAAiB,GAAGH,iBAAiB,GACvCN,sBAAsB,CAAmB,eAAe,CAAC,GACzD,IAAI;;AAER;AACA;AACA;;AAEA,MAAMU,eAA2C,GAAGA,CAAC;EAAEC;AAAK,CAAC,kBAC3DZ,KAAA,CAAAa,aAAA,CAACT,IAAI;EAACU,KAAK,EAAEC,cAAc,CAACC;AAAU,gBACpChB,KAAA,CAAAa,aAAA,CAACR,IAAI;EAACS,KAAK,EAAEC,cAAc,CAACE;AAAK,GAAEL,IAAI,EAAC,oCAAwC,CAC5E,CACP;AAED,MAAMG,cAAc,GAAGT,UAAU,CAACY,MAAM,CAAC;EACvCF,SAAS,EAAE;IACTG,IAAI,EAAE,CAAC;IACPC,cAAc,EAAE,QAAQ;IACxBC,UAAU,EAAE,QAAQ;IACpBC,eAAe,EAAE;EACnB,CAAC;EACDL,IAAI,EAAE;IACJM,KAAK,EAAE,MAAM;IACbC,QAAQ,EAAE;EACZ;AACF,CAAC,CAAC;;AAEF;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,SAAmC,GAAIC,KAAK,IAAK;EAC5D,IAAI,CAACjB,eAAe,EAAE;IACpB,oBAAOT,KAAA,CAAAa,aAAA,CAACF,eAAe;MAACC,IAAI,EAAC;IAAW,CAAE,CAAC;EAC7C;EACA,oBAAOZ,KAAA,CAAAa,aAAA,CAACJ,eAAe,EAAKiB,KAAQ,CAAC;AACvC,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,WAAuC,GAAID,KAAK,IAAK;EAChE,IAAI,CAAChB,iBAAiB,EAAE;IACtB,oBAAOV,KAAA,CAAAa,aAAA,CAACF,eAAe;MAACC,IAAI,EAAC;IAAa,CAAE,CAAC;EAC/C;EACA,oBAAOZ,KAAA,CAAAa,aAAA,CAACH,iBAAiB,EAAKgB,KAAQ,CAAC;AACzC,CAAC;;AAED;AACA;AACA;;AAEA;;AAOA,MAAME,gBAAgD,GACpD1B,aAAa,CAAC2B,YAAY;;AAE5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,UAAU,CAAC;EACtB;EACA,WAAWC,WAAWA,CAAA,EAAY;IAChC,OAAO5B,QAAQ,CAACK,EAAE,KAAK,KAAK,IAAIoB,gBAAgB,IAAI,IAAI;EAC1D;EAEQI,iBAAiBA,CAAA,EAAmB;IAC1C,OAAOC,OAAO,CAACC,MAAM,CACnB,IAAIC,KAAK,CACP,oEAAoE,GAClE,sCACJ,CACF,CAAC;EACH;;EAEA;EACAC,KAAKA,CAAA,EAAkB;IACrB,IAAI,CAACN,UAAU,CAACC,WAAW,IAAI,CAACH,gBAAgB,EAAE;MAChD,OAAO,IAAI,CAACI,iBAAiB,CAAC,CAAC;IACjC;IACA,OAAOJ,gBAAgB,CAACQ,KAAK,CAAC,CAAC;EACjC;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;EACEC,IAAIA,CAACC,UAAmB,EAAmB;IACzC,IAAI,CAACR,UAAU,CAACC,WAAW,IAAI,CAACH,gBAAgB,EAAE;MAChD,OAAO,IAAI,CAACI,iBAAiB,CAAC,CAAC;IACjC;IACA,OAAOJ,gBAAgB,CAACS,IAAI,CAACC,UAAU,IAAI,IAAI,CAAC;EAClD;;EAEA;EACAC,kBAAkBA,CAACC,OAAe,EAAiB;IACjD,IAAI,CAACV,UAAU,CAACC,WAAW,IAAI,CAACH,gBAAgB,EAAE;MAChD,OAAO,IAAI,CAACI,iBAAiB,CAAC,CAAC;IACjC;IACA,OAAOJ,gBAAgB,CAACW,kBAAkB,CAACC,OAAO,CAAC;EACrD;AACF","ignoreList":[]}
1
+ {"version":3,"names":["React","NativeModules","Platform","requireNativeComponent","StyleSheet","Text","View","isNativeAvailable","OS","NativeSceneView","NativeARSceneView","UnsupportedView","name","createElement","style","fallbackStyles","container","text","create","flex","justifyContent","alignItems","backgroundColor","color","fontSize","SceneView","props","ARSceneView","NativeARRecorder","RNARRecorder","ARRecorder","isSupported","rejectUnsupported","Promise","reject","Error","start","stop","outputPath","saveToPhotoLibrary","movPath"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
- import { type ViewStyle, type NativeSyntheticEvent } from 'react-native';
1
+ import React from "react";
2
+ import { type NativeSyntheticEvent, type ViewStyle } from "react-native";
3
3
  /** A 3D model loaded from a .glb / .gltf file. */
4
4
  export interface ModelNode {
5
5
  /** Asset path or URL to the glTF/GLB model. */
@@ -26,7 +26,7 @@ export interface ModelNode {
26
26
  * cross-platform bridge-parity umbrella (#909). Use `modelNodes` on iOS.
27
27
  */
28
28
  export interface GeometryNode {
29
- type: 'box' | 'cube' | 'sphere' | 'cylinder' | 'plane';
29
+ type: "box" | "cube" | "sphere" | "cylinder" | "plane";
30
30
  size?: [number, number, number];
31
31
  position?: [number, number, number];
32
32
  rotation?: [number, number, number];
@@ -52,7 +52,7 @@ export interface GeometryNode {
52
52
  * cross-platform bridge-parity umbrella (#909).
53
53
  */
54
54
  export interface LightNode {
55
- type: 'directional' | 'point' | 'spot';
55
+ type: "directional" | "point" | "spot";
56
56
  intensity?: number;
57
57
  color?: string;
58
58
  position?: [number, number, number];
@@ -63,12 +63,25 @@ export interface TapEvent {
63
63
  x: number;
64
64
  y: number;
65
65
  z: number;
66
- /** Name of the tapped node, if any. */
67
- nodeName?: string;
66
+ /**
67
+ * Name of the tapped model: its file's base name without extension
68
+ * (`models/robot.glb` → `robot`), identical on Android and iOS. Never an
69
+ * asset-internal mesh name — a tap inside a model always reports the model.
70
+ *
71
+ * `null` — never `undefined` — when the tap hit no model: an untitled
72
+ * geometry node or nothing at all on Android, a plane or a miss in Android
73
+ * AR, and *every* `ARSceneView` tap on iOS, where `SceneViewSwift`'s
74
+ * `ARSceneView` exposes no entity hit-test hook and so can only resolve the
75
+ * surface point (#2051).
76
+ *
77
+ * Every native dispatch path writes the key, so `nodeName == null` is the one
78
+ * guard that covers both views on both platforms.
79
+ */
80
+ nodeName: string | null;
68
81
  }
69
82
  export interface PlaneDetectedEvent {
70
83
  id: string;
71
- type: 'horizontal' | 'vertical';
84
+ type: "horizontal" | "vertical";
72
85
  center: [number, number, number];
73
86
  extent: [number, number];
74
87
  }
@@ -81,7 +94,7 @@ export interface PlaneDetectedEvent {
81
94
  * back to orbit (the per-mode switch is an iOS-first v4.3.0 addition —
82
95
  * the Android side is tracked in issue #1051).
83
96
  */
84
- export type CameraControlMode = 'orbit' | 'pan' | 'firstPerson';
97
+ export type CameraControlMode = "orbit" | "pan" | "firstPerson";
85
98
  export interface SceneViewProps {
86
99
  style?: ViewStyle;
87
100
  /** HDR environment asset path (e.g. "environments/studio.hdr"). */
@@ -100,7 +113,17 @@ export interface SceneViewProps {
100
113
  * **iOS:** acknowledged but not yet rendered — see {@link LightNode}.
101
114
  */
102
115
  lightNodes?: LightNode[];
103
- /** Enable default orbit camera controls. Default: true. */
116
+ /**
117
+ * Enable default orbit camera controls. Default: `true`.
118
+ *
119
+ * @deprecated Superseded by {@link SceneViewProps.cameraControlMode}, and
120
+ * **inert on iOS**: the two would contradict each other — nothing can say
121
+ * which wins for `cameraOrbit: false, cameraControlMode: 'orbit'` — so the
122
+ * iOS bridge deliberately reads only `cameraControlMode`. Still honoured on
123
+ * Android. There is currently no way to freeze the camera from this bridge
124
+ * on iOS; `SceneViewSwift` has `cameraGesturesEnabled` but it is not exposed
125
+ * here yet.
126
+ */
104
127
  cameraOrbit?: boolean;
105
128
  /**
106
129
  * Camera interaction mode (v4.3.0). Default: `'orbit'`.
@@ -118,9 +141,30 @@ export interface SceneViewProps {
118
141
  /**
119
142
  * Called when the user taps inside the scene.
120
143
  *
121
- * The event payload carries the world-space tap coordinates. On Android the
122
- * tapped node's `nodeName` is included when the tap hits a node; on iOS AR
123
- * the tap reports the surface point only, so `nodeName` is absent there.
144
+ * The event payload carries the world-space position of the tapped model and
145
+ * its `nodeName` (the model file's base name without extension) on both
146
+ * Android and iOS. A tap that hits no model reports `nodeName: null` — with
147
+ * the tapped node's real world position on Android when it landed on an
148
+ * (unnamed) geometry node, and `0, 0, 0` when it hit nothing at all. That
149
+ * `0, 0, 0` miss is **in practice Android-only**: iOS resolves the tap
150
+ * through RealityKit's entity-targeted gesture, which fires only on a hit,
151
+ * so a tap on empty space dispatches no `onTap` event at all — count taps
152
+ * and you will see fewer events on iOS, not a `0, 0, 0` one. (iOS emits
153
+ * `0, 0, 0` only if a hit entity resolves outside every loaded model, which
154
+ * the content root's contents make unreachable today.)
155
+ *
156
+ * On {@link ARSceneViewProps | `ARSceneView`} *what a hit reports* differs by
157
+ * platform:
158
+ * - **Android**: the AR view hit-tests the scene, so a tap on a model reports
159
+ * that model's file base name exactly as `SceneView` does; a tap on a plane
160
+ * or on nothing reports `null`.
161
+ * - **iOS**: always `null` — `SceneViewSwift.ARSceneView` exposes no entity
162
+ * hit-test hook, so the AR tap can only resolve the surface point. Tracked
163
+ * under {@link https://github.com/sceneview/sceneview/issues/2051 | #2051}.
164
+ *
165
+ * The *key* is written on every dispatch path of both views on both
166
+ * platforms, so one `nodeName == null` check remains the correct "the tap hit
167
+ * no model" test everywhere; it is never omitted and never `undefined`.
124
168
  */
125
169
  onTap?: (event: NativeSyntheticEvent<TapEvent>) => void;
126
170
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sceneview-sdk/react-native",
3
- "version": "4.25.0",
3
+ "version": "4.27.0",
4
4
  "description": "React Native bindings for SceneView — 3D and AR scenes powered by Filament (Android) and RealityKit (iOS)",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -17,7 +17,8 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "typescript": "tsc --noEmit",
20
- "lint": "eslint \"src/**/*.{ts,tsx}\"",
20
+ "lint": "cd ../.. && react-native/react-native-sceneview/node_modules/.bin/biome check react-native/react-native-sceneview/src react-native/react-native-sceneview/__tests__ react-native/react-native-sceneview/example/src",
21
+ "lint:fix": "cd ../.. && react-native/react-native-sceneview/node_modules/.bin/biome check --write react-native/react-native-sceneview/src react-native/react-native-sceneview/__tests__ react-native/react-native-sceneview/example/src",
21
22
  "test": "jest",
22
23
  "build": "bob build",
23
24
  "prepare": "bob build"
@@ -51,6 +52,7 @@
51
52
  "react-native": ">=0.72.0"
52
53
  },
53
54
  "devDependencies": {
55
+ "@biomejs/biome": "^2.0.0",
54
56
  "@types/jest": "^29.5.0",
55
57
  "@types/react": "^18.2.0",
56
58
  "@types/react-native": "^0.72.0",
@@ -6,11 +6,18 @@ Pod::Spec.new do |s|
6
6
  s.name = "react-native-sceneview"
7
7
  s.version = package["version"]
8
8
  s.summary = package["description"]
9
- s.homepage = package["repository"]
9
+ # `package["repository"]` is an OBJECT ({type, url, directory}), and CocoaPods
10
+ # rejects a Hash here ("Unacceptable type `Hash` for `homepage`"), which made
11
+ # `pod install` fail outright for any host app consuming this module. Use the
12
+ # string `homepage` field instead.
13
+ s.homepage = package["homepage"]
10
14
  s.license = package["license"]
11
15
  s.authors = "SceneView contributors"
12
16
 
13
- s.platforms = { :ios => "17.0" }
17
+ # Must match `SceneViewSwift/Package.swift`'s `.iOS("18.0")` floor — the
18
+ # bridge's `ios/*.swift` imports SceneViewSwift, so a lower value here is a
19
+ # lie CocoaPods cannot catch until a host app fails to link.
20
+ s.platforms = { :ios => "18.0" }
14
21
  s.source = { :git => "https://github.com/sceneview/sceneview.git", :tag => "v#{s.version}" }
15
22
  s.source_files = "ios/**/*.{swift,m}"
16
23
 
@@ -20,7 +27,7 @@ Pod::Spec.new do |s|
20
27
  # no published CocoaPods spec for it — so it CANNOT be declared as a
21
28
  # `s.dependency` here (CocoaPods would fail `pod install` resolving it).
22
29
  # The host app must add it once via Xcode's SwiftPM integration:
23
- # File ▸ Add Package Dependencies… ▸ https://github.com/sceneview/SceneViewSwift
30
+ # File ▸ Add Package Dependencies… ▸ https://github.com/sceneview/sceneview
24
31
  # The module's `ios/*.swift` `import SceneViewSwift` then resolves at the
25
32
  # app build, exactly like any RN native module with a SwiftPM dependency.
26
33
  # See this module's README "iOS" section.
package/src/index.tsx CHANGED
@@ -1,14 +1,20 @@
1
- import React from 'react';
1
+ // `tsconfig.json` sets `"jsx": "react"` — the CLASSIC runtime — so every JSX
2
+ // element below compiles to `React.createElement(...)` (verified in the
3
+ // published `lib/commonjs/index.js`). Biome's `useImportType` only sees the
4
+ // `React.FC` type annotations, not the JSX lowering, so it offers a "safe fix"
5
+ // that would erase this import and break the shipped bundle at runtime.
6
+ // biome-ignore lint/style/useImportType: React is a VALUE import — see above.
7
+ import React from "react";
2
8
  import {
3
- requireNativeComponent,
4
9
  NativeModules,
5
- Platform,
6
- type ViewStyle,
7
10
  type NativeSyntheticEvent,
8
- View,
9
- Text,
11
+ Platform,
12
+ requireNativeComponent,
10
13
  StyleSheet,
11
- } from 'react-native';
14
+ Text,
15
+ View,
16
+ type ViewStyle,
17
+ } from "react-native";
12
18
 
13
19
  // ---------------------------------------------------------------------------
14
20
  // Node type interfaces
@@ -41,7 +47,7 @@ export interface ModelNode {
41
47
  * cross-platform bridge-parity umbrella (#909). Use `modelNodes` on iOS.
42
48
  */
43
49
  export interface GeometryNode {
44
- type: 'box' | 'cube' | 'sphere' | 'cylinder' | 'plane';
50
+ type: "box" | "cube" | "sphere" | "cylinder" | "plane";
45
51
  size?: [number, number, number];
46
52
  position?: [number, number, number];
47
53
  rotation?: [number, number, number];
@@ -68,7 +74,7 @@ export interface GeometryNode {
68
74
  * cross-platform bridge-parity umbrella (#909).
69
75
  */
70
76
  export interface LightNode {
71
- type: 'directional' | 'point' | 'spot';
77
+ type: "directional" | "point" | "spot";
72
78
  intensity?: number;
73
79
  color?: string;
74
80
  position?: [number, number, number];
@@ -84,13 +90,26 @@ export interface TapEvent {
84
90
  x: number;
85
91
  y: number;
86
92
  z: number;
87
- /** Name of the tapped node, if any. */
88
- nodeName?: string;
93
+ /**
94
+ * Name of the tapped model: its file's base name without extension
95
+ * (`models/robot.glb` → `robot`), identical on Android and iOS. Never an
96
+ * asset-internal mesh name — a tap inside a model always reports the model.
97
+ *
98
+ * `null` — never `undefined` — when the tap hit no model: an untitled
99
+ * geometry node or nothing at all on Android, a plane or a miss in Android
100
+ * AR, and *every* `ARSceneView` tap on iOS, where `SceneViewSwift`'s
101
+ * `ARSceneView` exposes no entity hit-test hook and so can only resolve the
102
+ * surface point (#2051).
103
+ *
104
+ * Every native dispatch path writes the key, so `nodeName == null` is the one
105
+ * guard that covers both views on both platforms.
106
+ */
107
+ nodeName: string | null;
89
108
  }
90
109
 
91
110
  export interface PlaneDetectedEvent {
92
111
  id: string;
93
- type: 'horizontal' | 'vertical';
112
+ type: "horizontal" | "vertical";
94
113
  center: [number, number, number];
95
114
  extent: [number, number];
96
115
  }
@@ -108,7 +127,7 @@ export interface PlaneDetectedEvent {
108
127
  * back to orbit (the per-mode switch is an iOS-first v4.3.0 addition —
109
128
  * the Android side is tracked in issue #1051).
110
129
  */
111
- export type CameraControlMode = 'orbit' | 'pan' | 'firstPerson';
130
+ export type CameraControlMode = "orbit" | "pan" | "firstPerson";
112
131
 
113
132
  export interface SceneViewProps {
114
133
  style?: ViewStyle;
@@ -131,7 +150,17 @@ export interface SceneViewProps {
131
150
  */
132
151
  lightNodes?: LightNode[];
133
152
 
134
- /** Enable default orbit camera controls. Default: true. */
153
+ /**
154
+ * Enable default orbit camera controls. Default: `true`.
155
+ *
156
+ * @deprecated Superseded by {@link SceneViewProps.cameraControlMode}, and
157
+ * **inert on iOS**: the two would contradict each other — nothing can say
158
+ * which wins for `cameraOrbit: false, cameraControlMode: 'orbit'` — so the
159
+ * iOS bridge deliberately reads only `cameraControlMode`. Still honoured on
160
+ * Android. There is currently no way to freeze the camera from this bridge
161
+ * on iOS; `SceneViewSwift` has `cameraGesturesEnabled` but it is not exposed
162
+ * here yet.
163
+ */
135
164
  cameraOrbit?: boolean;
136
165
 
137
166
  /**
@@ -152,9 +181,30 @@ export interface SceneViewProps {
152
181
  /**
153
182
  * Called when the user taps inside the scene.
154
183
  *
155
- * The event payload carries the world-space tap coordinates. On Android the
156
- * tapped node's `nodeName` is included when the tap hits a node; on iOS AR
157
- * the tap reports the surface point only, so `nodeName` is absent there.
184
+ * The event payload carries the world-space position of the tapped model and
185
+ * its `nodeName` (the model file's base name without extension) on both
186
+ * Android and iOS. A tap that hits no model reports `nodeName: null` — with
187
+ * the tapped node's real world position on Android when it landed on an
188
+ * (unnamed) geometry node, and `0, 0, 0` when it hit nothing at all. That
189
+ * `0, 0, 0` miss is **in practice Android-only**: iOS resolves the tap
190
+ * through RealityKit's entity-targeted gesture, which fires only on a hit,
191
+ * so a tap on empty space dispatches no `onTap` event at all — count taps
192
+ * and you will see fewer events on iOS, not a `0, 0, 0` one. (iOS emits
193
+ * `0, 0, 0` only if a hit entity resolves outside every loaded model, which
194
+ * the content root's contents make unreachable today.)
195
+ *
196
+ * On {@link ARSceneViewProps | `ARSceneView`} *what a hit reports* differs by
197
+ * platform:
198
+ * - **Android**: the AR view hit-tests the scene, so a tap on a model reports
199
+ * that model's file base name exactly as `SceneView` does; a tap on a plane
200
+ * or on nothing reports `null`.
201
+ * - **iOS**: always `null` — `SceneViewSwift.ARSceneView` exposes no entity
202
+ * hit-test hook, so the AR tap can only resolve the surface point. Tracked
203
+ * under {@link https://github.com/sceneview/sceneview/issues/2051 | #2051}.
204
+ *
205
+ * The *key* is written on every dispatch path of both views on both
206
+ * platforms, so one `nodeName == null` check remains the correct "the tap hit
207
+ * no model" test everywhere; it is never omitted and never `undefined`.
158
208
  */
159
209
  onTap?: (event: NativeSyntheticEvent<TapEvent>) => void;
160
210
  }
@@ -200,14 +250,14 @@ export interface ARSceneViewProps extends SceneViewProps {
200
250
  // Native components (only available on Android and iOS)
201
251
  // ---------------------------------------------------------------------------
202
252
 
203
- const isNativeAvailable = Platform.OS === 'android' || Platform.OS === 'ios';
253
+ const isNativeAvailable = Platform.OS === "android" || Platform.OS === "ios";
204
254
 
205
255
  const NativeSceneView = isNativeAvailable
206
- ? requireNativeComponent<SceneViewProps>('RNSceneView')
256
+ ? requireNativeComponent<SceneViewProps>("RNSceneView")
207
257
  : null;
208
258
 
209
259
  const NativeARSceneView = isNativeAvailable
210
- ? requireNativeComponent<ARSceneViewProps>('RNARSceneView')
260
+ ? requireNativeComponent<ARSceneViewProps>("RNARSceneView")
211
261
  : null;
212
262
 
213
263
  // ---------------------------------------------------------------------------
@@ -223,12 +273,12 @@ const UnsupportedView: React.FC<{ name: string }> = ({ name }) => (
223
273
  const fallbackStyles = StyleSheet.create({
224
274
  container: {
225
275
  flex: 1,
226
- justifyContent: 'center',
227
- alignItems: 'center',
228
- backgroundColor: '#1a1a2e',
276
+ justifyContent: "center",
277
+ alignItems: "center",
278
+ backgroundColor: "#1a1a2e",
229
279
  },
230
280
  text: {
231
- color: '#aaa',
281
+ color: "#aaa",
232
282
  fontSize: 16,
233
283
  },
234
284
  });
@@ -282,8 +332,7 @@ interface RNARRecorderModule {
282
332
  saveToPhotoLibrary(movPath: string): Promise<void>;
283
333
  }
284
334
 
285
- const NativeARRecorder: RNARRecorderModule | undefined =
286
- NativeModules.RNARRecorder;
335
+ const NativeARRecorder: RNARRecorderModule | undefined = NativeModules.RNARRecorder;
287
336
 
288
337
  /**
289
338
  * Records an AR session to a video file (v4.3.0).
@@ -309,14 +358,14 @@ const NativeARRecorder: RNARRecorderModule | undefined =
309
358
  export class ARRecorder {
310
359
  /** `true` when {@link ARRecorder} is supported on the current platform. */
311
360
  static get isSupported(): boolean {
312
- return Platform.OS === 'ios' && NativeARRecorder != null;
361
+ return Platform.OS === "ios" && NativeARRecorder != null;
313
362
  }
314
363
 
315
364
  private rejectUnsupported(): Promise<never> {
316
365
  return Promise.reject(
317
366
  new Error(
318
- 'ARRecorder is currently only supported on iOS. Android AR session ' +
319
- 'recording is tracked in issue #1051.'
367
+ "ARRecorder is currently only supported on iOS. Android AR session " +
368
+ "recording is tracked in issue #1051."
320
369
  )
321
370
  );
322
371
  }