@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 +99 -26
- package/android/build.gradle.kts +21 -0
- package/android/src/main/java/io/github/sceneview/reactnative/ARSceneViewManager.kt +5 -0
- package/android/src/main/java/io/github/sceneview/reactnative/SceneViewEvents.kt +8 -2
- package/android/src/main/java/io/github/sceneview/reactnative/SceneViewManager.kt +29 -1
- package/ios/SceneViewModule.swift +147 -114
- package/lib/commonjs/index.js +16 -9
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +17 -11
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +56 -12
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +4 -2
- package/react-native-sceneview.podspec +10 -3
- package/src/index.tsx +78 -29
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
|
|
11
|
-
> `SceneViewSwift` API and
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
npm install github:sceneview/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
|
|
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/
|
|
58
|
-
- Version: `4.
|
|
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
|
-
|
|
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` |
|
|
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
|
|
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 ->
|
|
180
|
-
|
|
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
|
-
- **`
|
|
195
|
-
|
|
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.
|
package/android/build.gradle.kts
CHANGED
|
@@ -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
|
|
17
|
-
*
|
|
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
|
-
///
|
|
38
|
-
///
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
///
|
|
57
|
-
///
|
|
58
|
-
///
|
|
59
|
-
var
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
block?(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
255
|
+
let raw = cameraControlMode
|
|
155
256
|
Task { @MainActor in
|
|
156
|
-
sceneState.cameraControlMode =
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
}
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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 ===
|
|
59
|
-
const NativeSceneView = isNativeAvailable ? (0, _reactNative.requireNativeComponent)(
|
|
60
|
-
const NativeARSceneView = isNativeAvailable ? (0, _reactNative.requireNativeComponent)(
|
|
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:
|
|
77
|
-
alignItems:
|
|
78
|
-
backgroundColor:
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
alignItems: "center",
|
|
85
|
+
backgroundColor: "#1a1a2e"
|
|
79
86
|
},
|
|
80
87
|
text: {
|
|
81
|
-
color:
|
|
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 ===
|
|
168
|
+
return _reactNative.Platform.OS === "ios" && NativeARRecorder != null;
|
|
162
169
|
}
|
|
163
170
|
rejectUnsupported() {
|
|
164
|
-
return Promise.reject(new Error(
|
|
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":";;;;;;
|
|
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":[]}
|
package/lib/module/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 ===
|
|
53
|
-
const NativeSceneView = isNativeAvailable ? requireNativeComponent(
|
|
54
|
-
const NativeARSceneView = isNativeAvailable ? requireNativeComponent(
|
|
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:
|
|
71
|
-
alignItems:
|
|
72
|
-
backgroundColor:
|
|
76
|
+
justifyContent: "center",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
backgroundColor: "#1a1a2e"
|
|
73
79
|
},
|
|
74
80
|
text: {
|
|
75
|
-
color:
|
|
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 ===
|
|
160
|
+
return Platform.OS === "ios" && NativeARRecorder != null;
|
|
155
161
|
}
|
|
156
162
|
rejectUnsupported() {
|
|
157
|
-
return Promise.reject(new Error(
|
|
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. */
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","
|
|
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
|
|
2
|
-
import { type
|
|
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:
|
|
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:
|
|
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
|
-
/**
|
|
67
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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":"
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
11
|
+
Platform,
|
|
12
|
+
requireNativeComponent,
|
|
10
13
|
StyleSheet,
|
|
11
|
-
|
|
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:
|
|
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:
|
|
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
|
-
/**
|
|
88
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
|
156
|
-
*
|
|
157
|
-
*
|
|
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 ===
|
|
253
|
+
const isNativeAvailable = Platform.OS === "android" || Platform.OS === "ios";
|
|
204
254
|
|
|
205
255
|
const NativeSceneView = isNativeAvailable
|
|
206
|
-
? requireNativeComponent<SceneViewProps>(
|
|
256
|
+
? requireNativeComponent<SceneViewProps>("RNSceneView")
|
|
207
257
|
: null;
|
|
208
258
|
|
|
209
259
|
const NativeARSceneView = isNativeAvailable
|
|
210
|
-
? requireNativeComponent<ARSceneViewProps>(
|
|
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:
|
|
227
|
-
alignItems:
|
|
228
|
-
backgroundColor:
|
|
276
|
+
justifyContent: "center",
|
|
277
|
+
alignItems: "center",
|
|
278
|
+
backgroundColor: "#1a1a2e",
|
|
229
279
|
},
|
|
230
280
|
text: {
|
|
231
|
-
color:
|
|
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 ===
|
|
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
|
-
|
|
319
|
-
|
|
367
|
+
"ARRecorder is currently only supported on iOS. Android AR session " +
|
|
368
|
+
"recording is tracked in issue #1051."
|
|
320
369
|
)
|
|
321
370
|
);
|
|
322
371
|
}
|