@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 +152 -32
- 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 +68 -12
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +4 -2
- package/react-native-sceneview.podspec +35 -10
- package/src/index.tsx +90 -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,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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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` |
|
|
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
|
|
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 ->
|
|
180
|
-
|
|
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
|
-
- **`
|
|
195
|
-
|
|
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.
|
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,
|