@sceneview-sdk/react-native 4.26.0 → 4.28.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,18 +45,64 @@ 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
- **The host app must add `SceneViewSwift` via Swift Package Manager.**
53
- `SceneViewSwift` ships as a SwiftPM package only (no CocoaPods spec), so this
54
- module's podspec deliberately does **not** declare it as a `s.dependency` —
55
- add it once in Xcode (*File ▸ Add Package Dependencies…*):
53
+ **`SceneViewSwift` must arrive as a pod, and your `Podfile` must give it a
54
+ coordinate.** This module's podspec declares `s.dependency "SceneViewSwift",
55
+ "~> 4.27"`, but `SceneViewSwift` is **not published on the CocoaPods trunk**,
56
+ so CocoaPods cannot resolve that name on its own. Add one of these lines to
57
+ your `Podfile`:
56
58
 
57
- - URL: `https://github.com/sceneview/SceneViewSwift`
58
- - Version: `4.14.0` (or *Up to Next Major*)
59
+ ```ruby
60
+ # Released consumers — no monorepo clone
61
+ pod 'SceneViewSwift',
62
+ :podspec => 'https://raw.githubusercontent.com/sceneview/sceneview/main/SceneViewSwift.podspec'
59
63
 
60
- The module's `ios/*.swift` `import SceneViewSwift` resolves against that
61
- app-level package at build time.
64
+ # In-repo consumers — resolve from a local checkout
65
+ pod 'SceneViewSwift', :path => '<repo-root>'
66
+ ```
67
+
68
+ `samples/react-native-demo/ios/Podfile` takes the `:path` route, exactly as
69
+ `samples/flutter-demo/ios/Podfile` does.
70
+
71
+ > **`main`, not a tag — for now.** A tagged coordinate would be the
72
+ > reproducible one, but `SceneViewSwift.podspec` lives at the repo *root* and
73
+ > landed after `v4.26.0` was cut, so no tag that currently exists carries it:
74
+ > `pod install` against `.../v4.27.0/SceneViewSwift.podspec` is a 404, verified
75
+ > with `git cat-file -e v4.27.0:SceneViewSwift.podspec`. The Flutter plugin
76
+ > documents `main` for the same reason. Both move to
77
+ > `:git => …, :tag => 'vX.Y.Z'` the first time a release is cut with that file
78
+ > in it.
79
+
80
+ > **Always supply the coordinate.** The `SceneViewSwift` name is unclaimed on
81
+ > the CocoaPods trunk, so a `Podfile` that omits the line does not fail closed
82
+ > forever — it would resolve to whatever someone else publishes under that
83
+ > name. The explicit `:podspec` / `:path` line is what makes the source
84
+ > unambiguous.
85
+
86
+ > **Swift Package Manager does not work for this module.** Adding
87
+ > `SceneViewSwift` to the host *project* in Xcode (*File ▸ Add Package
88
+ > Dependencies…*) leaves it invisible here: this module's `ios/*.swift`
89
+ > compiles inside the generated `Pods.xcodeproj`, which does not see the host
90
+ > project's SwiftPM packages, and `import SceneViewSwift` fails with *"Unable
91
+ > to find module dependency: 'SceneViewSwift'"*. The README used to document
92
+ > the SwiftPM route as the supported one; it had never been built, and the
93
+ > first real `xcodebuild` against it failed exactly that way. This closes the
94
+ > React Native half of
95
+ > [#3072](https://github.com/sceneview/sceneview/issues/3072).
96
+
97
+ > **Keep the two versions in step.** npm `4.27.0`'s Swift requires
98
+ > `SceneViewSwift` at tag `v4.27.0` or newer: this module's `ios/*.swift`
99
+ > builds on `SceneViewerHostView` — which landed *after* `v4.26.0`, so no
100
+ > `SceneViewer*` type exists at that tag or any earlier one — and it calls the
101
+ > two-parameter `onTapEntity` (the tapped model root is the second parameter).
102
+ > A mismatch is a loud Swift compile error at build time, not a runtime
103
+ > surprise, but no CI job here will catch it for you: `rn-ios-compile.yml`
104
+ > type-checks against the `SceneViewSwift` sources *in this repo*, not against
105
+ > the tag your app resolves.
62
106
 
63
107
  ### Android
64
108
 
@@ -84,7 +128,7 @@ import { SceneView } from '@sceneview-sdk/react-native';
84
128
  style={{ flex: 1 }}
85
129
  environment="environments/studio_small.hdr"
86
130
  modelNodes={[{ src: 'models/damaged_helmet.glb', position: [0, 0, -2] }]}
87
- cameraOrbit
131
+ cameraControlMode="orbit"
88
132
  />
89
133
  ```
90
134
 
@@ -109,15 +153,23 @@ import { ARSceneView } from '@sceneview-sdk/react-native';
109
153
  | `modelNodes` | `ModelNode[]` | `[]` | Models to render |
110
154
  | `geometryNodes` | `GeometryNode[]` | `[]` | Geometry nodes (forward-compatible) |
111
155
  | `lightNodes` | `LightNode[]` | `[]` | Light nodes (forward-compatible) |
112
- | `cameraOrbit` | `boolean` | `true` | Enable orbit camera controls |
156
+ | `cameraOrbit` | `boolean` | `true` | **Deprecated**, inert on iOS — use `cameraControlMode` |
113
157
  | `cameraControlMode` | `CameraControlMode` | `'orbit'` | Camera mode (v4.3.0). `'pan'`/`'firstPerson'` are iOS-only |
114
158
  | `autoCenterContent` | `boolean` | `true` | Auto-centre content on first frame (v4.3.0, iOS-first) |
115
- | `onTap` | `function` | — | Tap callback (event pending) |
159
+ | `onTap` | `function` | — | Tap callback — `{ x, y, z, nodeName }` |
116
160
 
117
161
  `cameraControlMode` `'pan'` and `'firstPerson'` are iOS-only in v4.3.0; on
118
162
  Android they fall back to orbit. `autoCenterContent` is iOS-first — the
119
163
  Android side is tracked in issue #1051.
120
164
 
165
+ `cameraOrbit` is **deprecated and inert on iOS**. It predates
166
+ `cameraControlMode`, and the two contradict each other — nothing can say which
167
+ should win for `cameraOrbit: false, cameraControlMode: 'orbit'` — so the iOS
168
+ bridge reads only `cameraControlMode`. Note this leaves **no** way to freeze the
169
+ camera from this bridge on iOS: `SceneViewSwift` has `cameraGesturesEnabled`,
170
+ but the React Native surface does not expose it yet. `cameraOrbit: false` still
171
+ works on Android.
172
+
121
173
  ### Props — ARSceneView (extends SceneView)
122
174
 
123
175
  | Prop | Type | Default | Description |
@@ -176,8 +228,12 @@ requireNativeComponent('RNSceneView' / 'RNARSceneView')
176
228
  +---> Android: ViewManager -> ComposeView -> SceneView { } / ARSceneView { }
177
229
  | (Filament, SceneView SDK)
178
230
  |
179
- +---> iOS: RCTViewManager -> UIHostingController -> SceneView / ARSceneView
180
- (RealityKit, SceneViewSwift)
231
+ +---> iOS 3D: RCTViewManager -> SceneViewerHostView -> SceneView
232
+ | (RealityKit, SceneViewSwift — the same host
233
+ | the Flutter plugin and sceneview-compose use)
234
+ |
235
+ +---> iOS AR: RCTViewManager -> UIHostingController -> ARSceneView
236
+ (RealityKit, SceneViewSwift)
181
237
  ```
182
238
 
183
239
  Props are mapped from the React Native bridge to native view parameters on each platform.
@@ -191,8 +247,40 @@ coverage map (tracked in [#909](https://github.com/sceneview/sceneview/issues/90
191
247
  the iOS RealityKit port is not yet bridged.
192
248
  - **`depthOcclusion` / `instantPlacement`** — declared as props but **not
193
249
  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.
250
+ - **`onPlaneDetected`** — dispatched on **Android** only; SceneViewSwift's
251
+ `ARSceneView` exposes no plane-detection callback, so it never fires on iOS.
252
+ - **`onTap`** — dispatched on **Android and iOS** on both views. On `SceneView`
253
+ (3D) it carries the tapped model's world position and its file base name
254
+ without extension as `nodeName`.
255
+ > **Measured, not inferred ([#3086](https://github.com/sceneview/sceneview/issues/3086)).**
256
+ > The iOS 3D tap was run on an iPhone 17 Pro Max simulator with the Fox USDZ
257
+ > rendering: 5 taps on the model, 5 dispatches, `nodeName` `khronos_fox` every
258
+ > time; a tap on empty space dispatched nothing, as documented below. The path
259
+ > was instrumented at all four stages — gesture arrival, entity resolution,
260
+ > `SceneViewerHostView.reportTap`, and this module's `onTapEntity` block — and
261
+ > every stage fired.
262
+ >
263
+ > This does **not** clear the sibling bridge: Flutter's 3D `onTap` still never
264
+ > fires on iOS ([#3045](https://github.com/sceneview/sceneview/issues/3045)).
265
+ > The same run measured both hosts back to back against the *same*
266
+ > SceneViewSwift build and the same entity graph (11 entities, 1 collision
267
+ > shape, 9 input targets in each): 6 taps on the model under Flutter resolved
268
+ > no entity at all, while the plain, untargeted gesture arrived every time. So
269
+ > #3045 is a property of Flutter's platform-view touch delivery, not of
270
+ > RealityKit's entity-targeted hit test — which is what its write-up claims.
271
+
272
+ On
273
+ `ARSceneView` *what a hit reports* differs: **Android** hit-tests the AR
274
+ scene, so a tap on a model reports that model's file base name just as
275
+ `SceneView` does and a plane hit or a miss reports `null`; **iOS** always
276
+ reports `null`, because SceneViewSwift's `ARSceneView` exposes no entity
277
+ hit-test hook and its tap can only resolve the surface point
278
+ ([#2051](https://github.com/sceneview/sceneview/issues/2051)). Every dispatch
279
+ path still writes the key, so `nodeName == null` is the single "the tap hit no
280
+ model" test — it is never `undefined`. *How often* it dispatches differs
281
+ though: on iOS a tap that hits no entity fires no `onTap` at all (RealityKit's
282
+ gesture is entity-targeted), where Android dispatches a `0, 0, 0` miss — so
283
+ the two platforms do not deliver the same number of tap events.
196
284
  - **`environment` on `ARSceneView`** — AR scenes use the camera feed; the HDR
197
285
  environment is accepted but not applied.
198
286
  - **`ModelNode.scale`** — parsed as a uniform float; the per-axis `[x, y, z]`
@@ -208,6 +296,38 @@ coverage map (tracked in [#909](https://github.com/sceneview/sceneview/issues/90
208
296
 
209
297
  See [CONTRIBUTING.md](https://github.com/sceneview/sceneview/blob/main/.github/CONTRIBUTING.md).
210
298
 
299
+ ### Local checks
300
+
301
+ From this directory, after `npm ci`:
302
+
303
+ ```bash
304
+ npm run lint # Biome (repo-root biome.json) — lint + format + import assists
305
+ npm run lint:fix # same, applying the safe fixes
306
+ npm run typescript # tsc --noEmit
307
+ npm test # jest
308
+ ```
309
+
310
+ All four run on every PR that touches this package's TypeScript, via
311
+ [`.github/workflows/rn-ts-check.yml`](https://github.com/sceneview/sceneview/blob/main/.github/workflows/rn-ts-check.yml).
312
+ They do not all cover the same files:
313
+
314
+ | check | covers |
315
+ |---|---|
316
+ | `lint` | `src/`, `__tests__/`, `example/src/` |
317
+ | `typescript` | `src/` only — that is `tsconfig.json`'s `include` |
318
+ | `test` | `__tests__/` |
319
+
320
+ The linter is **Biome**, not ESLint — the repo has a single JS/TS rule set in
321
+ the root `biome.json`, and those three directories are listed in its
322
+ `files.includes`. The `lint` scripts `cd` to the repo root before invoking
323
+ Biome, mirroring `mcp/package.json`'s script; Biome also walks up and finds the
324
+ root config on its own, so calling `./node_modules/.bin/biome check .` from
325
+ this directory works too.
326
+
327
+ Note that most rules in `biome.json` are **warning** severity, so `npm run
328
+ lint` can exit 0 with findings still printed. Read its output, don't just read
329
+ its exit code.
330
+
211
331
  ## License
212
332
 
213
333
  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,