@reactvision/react-viro 2.57.2 → 2.57.4
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 +8 -0
- package/android/react_viro/react_viro-release.aar +0 -0
- package/android/viro_renderer/viro_renderer-release.aar +0 -0
- package/components/AR/ViroARSceneNavigator.tsx +11 -3
- package/components/Studio/StudioARScene.tsx +153 -4
- package/components/Studio/StudioSceneErrorBoundary.tsx +48 -0
- package/components/Studio/StudioSceneNavigator.tsx +186 -41
- package/components/Studio/VRTStudioModule.ts +26 -0
- package/components/Studio/domain/dragConfiguration.ts +6 -4
- package/components/Studio/domain/materialConfig.ts +75 -42
- package/components/Studio/domain/physicsConfig.ts +75 -32
- package/components/Studio/domain/viroNodeFactory.tsx +53 -14
- package/components/Studio/index.ts +4 -0
- package/components/Studio/types.ts +3 -8
- package/components/Utilities/ViroVersion.ts +1 -1
- package/components/ViroXRSceneNavigator.tsx +2 -0
- package/dist/components/AR/ViroARSceneNavigator.d.ts +11 -3
- package/dist/components/Studio/StudioARScene.d.ts +6 -0
- package/dist/components/Studio/StudioARScene.js +101 -6
- package/dist/components/Studio/StudioSceneErrorBoundary.d.ts +28 -0
- package/dist/components/Studio/StudioSceneErrorBoundary.js +31 -0
- package/dist/components/Studio/StudioSceneNavigator.d.ts +28 -3
- package/dist/components/Studio/StudioSceneNavigator.js +84 -20
- package/dist/components/Studio/VRTStudioModule.d.ts +17 -0
- package/dist/components/Studio/VRTStudioModule.js +14 -0
- package/dist/components/Studio/domain/dragConfiguration.d.ts +5 -3
- package/dist/components/Studio/domain/dragConfiguration.js +5 -3
- package/dist/components/Studio/domain/materialConfig.d.ts +0 -1
- package/dist/components/Studio/domain/materialConfig.js +12 -5
- package/dist/components/Studio/domain/physicsConfig.d.ts +0 -1
- package/dist/components/Studio/domain/physicsConfig.js +18 -6
- package/dist/components/Studio/domain/viroNodeFactory.d.ts +2 -2
- package/dist/components/Studio/domain/viroNodeFactory.js +31 -18
- package/dist/components/Studio/index.d.ts +1 -0
- package/dist/components/Studio/types.d.ts +1 -0
- package/dist/components/Utilities/ViroVersion.d.ts +1 -1
- package/dist/components/Utilities/ViroVersion.js +1 -1
- package/dist/components/ViroXRSceneNavigator.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/index.ts +2 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +8 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSessioniOS.h +14 -3
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROFrontCameraProvider.h +34 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROViewAR.h +3 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
- package/ios/dist/include/RVStudioWatermarkState.h +25 -0
- package/ios/dist/lib/libViroReact.a +0 -0
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -92,6 +92,14 @@ You can get a Studio account, which includes Platform access, for free at <https
|
|
|
92
92
|
- Cloud Anchors guide: <https://viro-community.readme.io/docs/cloud-anchors>
|
|
93
93
|
- Geospatial Anchors guide: <https://viro-community.readme.io/docs/geospatial-anchors>
|
|
94
94
|
|
|
95
|
+
## Optional Packages
|
|
96
|
+
|
|
97
|
+
Two capabilities are shipped as separate, opt-in packages rather than in core — install them only if you need them:
|
|
98
|
+
|
|
99
|
+
- **[`@reactvision/react-viro-onnx`](https://www.npmjs.com/package/@reactvision/react-viro-onnx)** — on-device object detection. It provides the ONNX Runtime inference engine behind [`ViroObjectDetector`](https://github.com/ReactVision/viro/blob/main/docs/ViroObjectDetector.md) (model inference, NMS, class-name decoding). Without it, `ViroObjectDetector` returns no detections. Keeping it separate avoids bundling ONNX Runtime (~tens of MB) into apps that don't use detection.
|
|
100
|
+
|
|
101
|
+
- **[`@reactvision/react-viro-face-tracking`](https://www.npmjs.com/package/@reactvision/react-viro-face-tracking)** — front-camera (ARKit face-tracking) AR. It backs the `frontCameraEnabled` prop on `ViroARSceneNavigator`. On iOS ARKit reaches the front camera only through the **TrueDepth** API, which Apple statically scans for under App Store review Guideline 2.5.1 — so it's kept out of the core binary. Apps that don't use front-camera AR stay TrueDepth-free and pass review with no configuration; install this package (and declare TrueDepth) only when you need it. For a selfie *feed* without face tracking, use [`ViroCameraTexture`](https://github.com/ReactVision/viro/blob/main/docs/ViroCameraTexture.md) with `cameraPosition="front"` — that's in core and uses AVFoundation, no TrueDepth.
|
|
102
|
+
|
|
95
103
|
## Studio
|
|
96
104
|
|
|
97
105
|
[ReactVision Studio](https://studio.reactvision.xyz) is our browser-based visual scene editor for AR and VR. Build scenes visually in Studio, then embed them inside your own ViroReact app with a single component. Designers and developers can iterate on a scene in Studio and have it update in production without a new app build.
|
|
Binary file
|
|
Binary file
|
|
@@ -208,9 +208,17 @@ type Props = ViewProps & {
|
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
210
|
* Use the front (selfie) camera as the AR session background.
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
211
|
+
*
|
|
212
|
+
* Requires the optional `@reactvision/react-viro-face-tracking` package to be
|
|
213
|
+
* installed — it provides the native front-camera AR configuration and, on
|
|
214
|
+
* iOS, declares TrueDepth usage. Without it, this prop has no effect.
|
|
215
|
+
*
|
|
216
|
+
* On iOS the package uses the front TrueDepth camera; on Android it uses
|
|
217
|
+
* ARCore Augmented Faces mode. World tracking, plane detection, and LiDAR are
|
|
218
|
+
* unavailable in this mode.
|
|
219
|
+
*
|
|
220
|
+
* For a plain selfie feed without face tracking (no TrueDepth), use
|
|
221
|
+
* `ViroCameraTexture` with `cameraPosition="front"` instead.
|
|
214
222
|
*
|
|
215
223
|
* @default false
|
|
216
224
|
* @platform ios, android
|
|
@@ -10,6 +10,7 @@ import { ViroScene } from "../ViroScene";
|
|
|
10
10
|
import { ViroText } from "../ViroText";
|
|
11
11
|
import { ViroController } from "../ViroController";
|
|
12
12
|
import { isQuest } from "../Utilities/ViroPlatform";
|
|
13
|
+
import type { ViroAnchor } from "../Types/ViroEvents";
|
|
13
14
|
import { registerSceneAnimations } from "./domain/animationRegistry";
|
|
14
15
|
import { createPlacementCollisionHandler } from "./domain/collisionBindingsRuntime";
|
|
15
16
|
import { collisionPairKey } from "./domain/collisionPairKey";
|
|
@@ -51,6 +52,12 @@ interface StudioARSceneProps {
|
|
|
51
52
|
onReady?: () => void;
|
|
52
53
|
onError?: (err: Error) => void;
|
|
53
54
|
onSceneChange?: (sceneId: string, sceneName: string) => void;
|
|
55
|
+
/** Fired on first AR plane detection (AUTOMATIC) / plane accept (MANUAL). */
|
|
56
|
+
onPlaneDetected?: () => void;
|
|
57
|
+
/** Fired when the user taps to select a plane (MANUAL mode). */
|
|
58
|
+
onPlaneSelected?: () => void;
|
|
59
|
+
/** Text shown when the scene has no assets. Defaults to "No assets to display". */
|
|
60
|
+
noAssetsMessage?: string;
|
|
54
61
|
/** Session-scoped store owned by the navigator; survives scene pushes. */
|
|
55
62
|
variableStore?: StudioVariableStore;
|
|
56
63
|
}
|
|
@@ -73,8 +80,16 @@ interface StudioARSceneInnerProps extends StudioARSceneProps {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
76
|
-
const {
|
|
77
|
-
|
|
83
|
+
const {
|
|
84
|
+
sceneNavigator,
|
|
85
|
+
sceneData,
|
|
86
|
+
onReady,
|
|
87
|
+
onSceneChange,
|
|
88
|
+
onPlaneDetected,
|
|
89
|
+
onPlaneSelected,
|
|
90
|
+
noAssetsMessage,
|
|
91
|
+
variableStore,
|
|
92
|
+
} = props;
|
|
78
93
|
const { scene, assets, animations, collision_bindings, functions } =
|
|
79
94
|
sceneData;
|
|
80
95
|
|
|
@@ -193,6 +208,49 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
193
208
|
{}
|
|
194
209
|
);
|
|
195
210
|
|
|
211
|
+
// ─── Drag-active state (debounced) ────────────────────────────────────────
|
|
212
|
+
// Viro's onDrag fires per-frame. Track a Record<assetId, true> cleared 220ms
|
|
213
|
+
// after the last drag event; the node factory reads isDragActive to pass
|
|
214
|
+
// kinematicDragOverride so Dynamic-physics bodies don't fight the gesture.
|
|
215
|
+
// The onDrag callback's mere presence is also what unlocks native drag.
|
|
216
|
+
const [dragActiveByAssetId, setDragActiveByAssetId] = useState<
|
|
217
|
+
Record<string, true>
|
|
218
|
+
>({});
|
|
219
|
+
const dragTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
|
|
220
|
+
new Map()
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const notifyPhysicsDrag = useCallback((assetId: string) => {
|
|
224
|
+
setDragActiveByAssetId((prev) =>
|
|
225
|
+
prev[assetId] ? prev : { ...prev, [assetId]: true }
|
|
226
|
+
);
|
|
227
|
+
const existing = dragTimersRef.current.get(assetId);
|
|
228
|
+
if (existing) clearTimeout(existing);
|
|
229
|
+
const t = setTimeout(() => {
|
|
230
|
+
setDragActiveByAssetId((prev) => {
|
|
231
|
+
if (!prev[assetId]) return prev;
|
|
232
|
+
const next = { ...prev };
|
|
233
|
+
delete next[assetId];
|
|
234
|
+
return next;
|
|
235
|
+
});
|
|
236
|
+
dragTimersRef.current.delete(assetId);
|
|
237
|
+
}, 220);
|
|
238
|
+
dragTimersRef.current.set(assetId, t);
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
const isDragActive = useCallback(
|
|
242
|
+
(assetId: string) => !!dragActiveByAssetId[assetId],
|
|
243
|
+
[dragActiveByAssetId]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
const timers = dragTimersRef.current;
|
|
248
|
+
return () => {
|
|
249
|
+
timers.forEach((timer) => clearTimeout(timer));
|
|
250
|
+
timers.clear();
|
|
251
|
+
};
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
196
254
|
const handleAssetLoaded = useCallback((assetId: string) => {
|
|
197
255
|
setLoadedAssetIds((prev) =>
|
|
198
256
|
prev[assetId] ? prev : { ...prev, [assetId]: true }
|
|
@@ -429,6 +487,8 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
429
487
|
animationStates,
|
|
430
488
|
handleAssetLoaded,
|
|
431
489
|
getCollisionHandler(asset.id),
|
|
490
|
+
isDragActive,
|
|
491
|
+
notifyPhysicsDrag,
|
|
432
492
|
handleSceneChange,
|
|
433
493
|
runtimeCtx
|
|
434
494
|
);
|
|
@@ -441,6 +501,8 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
441
501
|
animationStates,
|
|
442
502
|
handleAssetLoaded,
|
|
443
503
|
getCollisionHandler,
|
|
504
|
+
isDragActive,
|
|
505
|
+
notifyPhysicsDrag,
|
|
444
506
|
maxModels,
|
|
445
507
|
handleSceneChange,
|
|
446
508
|
runtimeCtx,
|
|
@@ -461,6 +523,8 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
461
523
|
animationStates,
|
|
462
524
|
handleAssetLoaded,
|
|
463
525
|
getCollisionHandler(asset.id),
|
|
526
|
+
isDragActive,
|
|
527
|
+
notifyPhysicsDrag,
|
|
464
528
|
handleSceneChange,
|
|
465
529
|
runtimeCtx
|
|
466
530
|
);
|
|
@@ -480,6 +544,8 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
480
544
|
animationStates,
|
|
481
545
|
handleAssetLoaded,
|
|
482
546
|
getCollisionHandler,
|
|
547
|
+
isDragActive,
|
|
548
|
+
notifyPhysicsDrag,
|
|
483
549
|
handleSceneChange,
|
|
484
550
|
runtimeCtx,
|
|
485
551
|
]);
|
|
@@ -490,6 +556,76 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
490
556
|
).toUpperCase();
|
|
491
557
|
const planeAlignment = (scene.plane_direction ?? "Horizontal") as any;
|
|
492
558
|
|
|
559
|
+
// Native plane anchor types for ViroARScene (lowercase matches Viro defaults).
|
|
560
|
+
const anchorDetectionTypes = useMemo((): string[] | undefined => {
|
|
561
|
+
if (planeDetectionMode !== "AUTOMATIC" && planeDetectionMode !== "MANUAL") {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
const dir = (scene.plane_direction ?? "Horizontal").toLowerCase();
|
|
565
|
+
if (dir === "vertical") return ["planesVertical"];
|
|
566
|
+
if (dir.includes("horizontal")) return ["planesHorizontal"];
|
|
567
|
+
return ["planesHorizontal", "planesVertical"];
|
|
568
|
+
}, [planeDetectionMode, scene.plane_direction]);
|
|
569
|
+
|
|
570
|
+
// ViroARPlaneSelector (react-viro 2.54+) no longer receives scene anchors
|
|
571
|
+
// automatically; ViroARScene forwards them here via ref. Also surfaces
|
|
572
|
+
// onPlaneDetected / onPlaneSelected to the host.
|
|
573
|
+
const planeSelectorRef = useRef<InstanceType<
|
|
574
|
+
typeof ViroARPlaneSelector
|
|
575
|
+
> | null>(null);
|
|
576
|
+
|
|
577
|
+
const handleAnchorFound = useCallback(
|
|
578
|
+
(anchor: ViroAnchor) => {
|
|
579
|
+
try {
|
|
580
|
+
if (planeDetectionMode === "MANUAL") {
|
|
581
|
+
planeSelectorRef.current?.handleAnchorFound(anchor);
|
|
582
|
+
}
|
|
583
|
+
if (planeDetectionMode === "AUTOMATIC" && anchor?.type === "plane") {
|
|
584
|
+
onPlaneDetected?.();
|
|
585
|
+
}
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error("[Studio] handleAnchorFound failed:", error);
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
[planeDetectionMode, onPlaneDetected]
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const handleAnchorUpdated = useCallback(
|
|
594
|
+
(anchor: ViroAnchor) => {
|
|
595
|
+
try {
|
|
596
|
+
if (planeDetectionMode === "MANUAL") {
|
|
597
|
+
planeSelectorRef.current?.handleAnchorUpdated(anchor);
|
|
598
|
+
}
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error("[Studio] handleAnchorUpdated failed:", error);
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
[planeDetectionMode]
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const handleAnchorRemoved = useCallback(
|
|
607
|
+
(anchor?: ViroAnchor) => {
|
|
608
|
+
try {
|
|
609
|
+
if (planeDetectionMode === "MANUAL" && anchor) {
|
|
610
|
+
planeSelectorRef.current?.handleAnchorRemoved(anchor);
|
|
611
|
+
}
|
|
612
|
+
} catch (error) {
|
|
613
|
+
console.error("[Studio] handleAnchorRemoved failed:", error);
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
[planeDetectionMode]
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
const handlePlaneSelected = useCallback(() => {
|
|
620
|
+
onPlaneSelected?.();
|
|
621
|
+
}, [onPlaneSelected]);
|
|
622
|
+
|
|
623
|
+
// ViroARPlaneSelector.onPlaneDetected must return a boolean (accept the plane).
|
|
624
|
+
const handlePlaneDetectedForSelector = useCallback(() => {
|
|
625
|
+
onPlaneDetected?.();
|
|
626
|
+
return true;
|
|
627
|
+
}, [onPlaneDetected]);
|
|
628
|
+
|
|
493
629
|
const renderAssets = () => {
|
|
494
630
|
if (isQuest) {
|
|
495
631
|
if (planeDetectionMode !== "NONE") {
|
|
@@ -510,9 +646,12 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
510
646
|
if (planeDetectionMode === "MANUAL") {
|
|
511
647
|
return (
|
|
512
648
|
<ViroARPlaneSelector
|
|
649
|
+
ref={planeSelectorRef}
|
|
513
650
|
minHeight={0.1}
|
|
514
651
|
minWidth={0.1}
|
|
515
652
|
alignment={planeAlignment}
|
|
653
|
+
onPlaneDetected={handlePlaneDetectedForSelector}
|
|
654
|
+
onPlaneSelected={handlePlaneSelected}
|
|
516
655
|
>
|
|
517
656
|
{renderedPlaneAssets}
|
|
518
657
|
</ViroARPlaneSelector>
|
|
@@ -543,7 +682,7 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
543
682
|
<StudioSounds manager={soundManagerRef.current!} />
|
|
544
683
|
{assets.length === 0 && (
|
|
545
684
|
<ViroText
|
|
546
|
-
text="No assets to display"
|
|
685
|
+
text={noAssetsMessage ?? "No assets to display"}
|
|
547
686
|
position={[0, 0, -2]}
|
|
548
687
|
style={{
|
|
549
688
|
fontFamily: "Arial",
|
|
@@ -559,5 +698,15 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
559
698
|
if (isQuest) {
|
|
560
699
|
return <ViroScene {...physicsProps}>{children}</ViroScene>;
|
|
561
700
|
}
|
|
562
|
-
return
|
|
701
|
+
return (
|
|
702
|
+
<ViroARScene
|
|
703
|
+
{...physicsProps}
|
|
704
|
+
{...(anchorDetectionTypes != null ? { anchorDetectionTypes } : {})}
|
|
705
|
+
onAnchorFound={handleAnchorFound}
|
|
706
|
+
onAnchorUpdated={handleAnchorUpdated}
|
|
707
|
+
onAnchorRemoved={handleAnchorRemoved}
|
|
708
|
+
>
|
|
709
|
+
{children}
|
|
710
|
+
</ViroARScene>
|
|
711
|
+
);
|
|
563
712
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component, ErrorInfo, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
onError?: (error: Error) => void;
|
|
6
|
+
renderError?: (error: Error) => ReactNode;
|
|
7
|
+
sceneId?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface State {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Catches render/lifecycle errors in the Studio scene tree so a bad asset or a
|
|
17
|
+
* node-factory throw can't take down the host app. Always reports via onError;
|
|
18
|
+
* renders renderError(error) if provided, else nothing (no built-in UI).
|
|
19
|
+
*
|
|
20
|
+
* AR-only in effect: on Quest, ViroXRSceneNavigator forwards to a separate
|
|
21
|
+
* React root (VRActivity), so scene errors there are outside this boundary.
|
|
22
|
+
* Native and async errors are also out of a React boundary's reach; async
|
|
23
|
+
* scene-load failures route through onError in StudioSceneNavigator instead.
|
|
24
|
+
*/
|
|
25
|
+
export class StudioSceneErrorBoundary extends Component<Props, State> {
|
|
26
|
+
public state: State = { hasError: false, error: null };
|
|
27
|
+
|
|
28
|
+
public static getDerivedStateFromError(error: Error): State {
|
|
29
|
+
return { hasError: true, error };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
33
|
+
this.props.onError?.(error);
|
|
34
|
+
console.error(
|
|
35
|
+
"[Studio] Scene render error:",
|
|
36
|
+
error,
|
|
37
|
+
this.props.sceneId,
|
|
38
|
+
errorInfo.componentStack
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public render() {
|
|
43
|
+
if (this.state.hasError && this.state.error) {
|
|
44
|
+
return this.props.renderError?.(this.state.error) ?? null;
|
|
45
|
+
}
|
|
46
|
+
return this.props.children;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
forwardRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
3
10
|
import { ActivityIndicator, StyleSheet, View, ViewStyle } from "react-native";
|
|
4
11
|
import { ViroARScene } from "../AR/ViroARScene";
|
|
5
12
|
import { ViroScene } from "../ViroScene";
|
|
@@ -9,23 +16,55 @@ import { registerSceneAnimations } from "./domain/animationRegistry";
|
|
|
9
16
|
import { registerStudioMaterialsForAssets } from "./domain/studioMaterials";
|
|
10
17
|
import { StudioVariableStore } from "./domain/variableStore";
|
|
11
18
|
import { StudioARScene } from "./StudioARScene";
|
|
19
|
+
import { StudioSceneErrorBoundary } from "./StudioSceneErrorBoundary";
|
|
12
20
|
import { StudioProjectApiResponse, StudioSceneResponse } from "./types";
|
|
13
21
|
import { VRTStudioModule } from "./VRTStudioModule";
|
|
14
22
|
|
|
15
|
-
function LoadingARScene() {
|
|
16
|
-
|
|
23
|
+
function LoadingARScene() {
|
|
24
|
+
return <ViroARScene />;
|
|
25
|
+
}
|
|
26
|
+
function LoadingVRScene() {
|
|
27
|
+
return <ViroScene />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ViroOcclusionMode = "peopleOnly" | "depthBased" | undefined;
|
|
31
|
+
|
|
32
|
+
function mapOcclusionMode(
|
|
33
|
+
dbValue: string | null | undefined
|
|
34
|
+
): ViroOcclusionMode {
|
|
35
|
+
switch (dbValue) {
|
|
36
|
+
case "PEOPLEONLY":
|
|
37
|
+
return "peopleOnly";
|
|
38
|
+
case "DEPTHBASED":
|
|
39
|
+
return "depthBased";
|
|
40
|
+
default:
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
17
44
|
|
|
18
45
|
const styles = StyleSheet.create({
|
|
19
46
|
loader: {
|
|
20
47
|
position: "absolute",
|
|
21
|
-
top: 0,
|
|
48
|
+
top: 0,
|
|
49
|
+
left: 0,
|
|
50
|
+
right: 0,
|
|
51
|
+
bottom: 0,
|
|
22
52
|
justifyContent: "center",
|
|
23
53
|
alignItems: "center",
|
|
24
54
|
backgroundColor: "#000000",
|
|
25
55
|
},
|
|
26
56
|
});
|
|
27
57
|
|
|
28
|
-
|
|
58
|
+
/** Imperative handle exposed via ref. */
|
|
59
|
+
export interface StudioSceneNavigatorHandle {
|
|
60
|
+
/** Screenshots the AR renderer. Resolves `{ success: false }` (no-op) on Quest. */
|
|
61
|
+
takeScreenshot: (
|
|
62
|
+
fileName: string,
|
|
63
|
+
saveToCameraRoll: boolean
|
|
64
|
+
) => Promise<{ success: boolean; url?: string; errorCode?: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface StudioSceneNavigatorProps {
|
|
29
68
|
/**
|
|
30
69
|
* UUID of a specific scene to load. If omitted, the navigator fetches the
|
|
31
70
|
* project configured in the app manifest and uses its opening scene.
|
|
@@ -38,6 +77,22 @@ interface StudioSceneNavigatorProps {
|
|
|
38
77
|
onError?: (err: Error) => void;
|
|
39
78
|
onSceneChange?: (sceneId: string, sceneName: string) => void;
|
|
40
79
|
onExitViro?: () => void;
|
|
80
|
+
/** Fired after the scene is fetched and parsed, before it is pushed. */
|
|
81
|
+
onSceneLoaded?: (sceneData: StudioSceneResponse) => void;
|
|
82
|
+
/** Threaded to the initial scene's StudioARScene (initial scene only). */
|
|
83
|
+
onPlaneDetected?: () => void;
|
|
84
|
+
onPlaneSelected?: () => void;
|
|
85
|
+
noAssetsMessage?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Opt-in overlay shown until the scene mounts. Omit to render nothing on AR
|
|
88
|
+
* during load (the camera feed); Quest falls back to a built-in spinner.
|
|
89
|
+
*/
|
|
90
|
+
loadingView?: React.ReactNode;
|
|
91
|
+
/**
|
|
92
|
+
* Opt-in UI for a caught render error. The boundary always catches and calls
|
|
93
|
+
* `onError`; when this is omitted it renders nothing.
|
|
94
|
+
*/
|
|
95
|
+
renderError?: (error: Error) => React.ReactNode;
|
|
41
96
|
}
|
|
42
97
|
|
|
43
98
|
/**
|
|
@@ -53,19 +108,33 @@ interface StudioSceneNavigatorProps {
|
|
|
53
108
|
* ready. This means VRActivity always launches with the actual content scene
|
|
54
109
|
* as its initial scene, avoiding the LoadingVRScene → replace timing race.
|
|
55
110
|
*/
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
111
|
+
export const StudioSceneNavigator = forwardRef<
|
|
112
|
+
StudioSceneNavigatorHandle,
|
|
113
|
+
StudioSceneNavigatorProps
|
|
114
|
+
>(function StudioSceneNavigator(
|
|
115
|
+
{
|
|
116
|
+
sceneId,
|
|
117
|
+
worldAlignment = "Gravity",
|
|
118
|
+
autofocus = true,
|
|
119
|
+
style,
|
|
120
|
+
onSceneReady,
|
|
121
|
+
onError,
|
|
122
|
+
onSceneChange,
|
|
123
|
+
onExitViro,
|
|
124
|
+
onSceneLoaded,
|
|
125
|
+
onPlaneDetected,
|
|
126
|
+
onPlaneSelected,
|
|
127
|
+
noAssetsMessage,
|
|
128
|
+
loadingView,
|
|
129
|
+
renderError,
|
|
130
|
+
},
|
|
131
|
+
ref
|
|
132
|
+
) {
|
|
66
133
|
const navigatorRef = useRef<any>(null);
|
|
67
134
|
const loadedSceneIdRef = useRef<string | null>(null);
|
|
68
135
|
|
|
136
|
+
const [isSceneReady, setIsSceneReady] = useState(false);
|
|
137
|
+
|
|
69
138
|
// Session-scoped variable store: outlives every scene push, resets when the
|
|
70
139
|
// navigator (= the AR/VR session) unmounts.
|
|
71
140
|
const variableStoreRef = useRef<StudioVariableStore | null>(null);
|
|
@@ -82,13 +151,55 @@ export function StudioSceneNavigator({
|
|
|
82
151
|
const onSceneReadyRef = useRef(onSceneReady);
|
|
83
152
|
const onErrorRef = useRef(onError);
|
|
84
153
|
const onSceneChangeRef = useRef(onSceneChange);
|
|
154
|
+
const onSceneLoadedRef = useRef(onSceneLoaded);
|
|
155
|
+
const onPlaneDetectedRef = useRef(onPlaneDetected);
|
|
156
|
+
const onPlaneSelectedRef = useRef(onPlaneSelected);
|
|
157
|
+
const noAssetsMessageRef = useRef(noAssetsMessage);
|
|
85
158
|
onSceneReadyRef.current = onSceneReady;
|
|
86
159
|
onErrorRef.current = onError;
|
|
87
160
|
onSceneChangeRef.current = onSceneChange;
|
|
161
|
+
onSceneLoadedRef.current = onSceneLoaded;
|
|
162
|
+
onPlaneDetectedRef.current = onPlaneDetected;
|
|
163
|
+
onPlaneSelectedRef.current = onPlaneSelected;
|
|
164
|
+
noAssetsMessageRef.current = noAssetsMessage;
|
|
165
|
+
|
|
166
|
+
// Stable so passProps stays referentially steady across renders. Idempotent,
|
|
167
|
+
// so StrictMode's dev double-invoke of StudioARScene's onReady effect is safe.
|
|
168
|
+
const handleSceneReady = useCallback(() => {
|
|
169
|
+
setIsSceneReady(true);
|
|
170
|
+
onSceneReadyRef.current?.();
|
|
171
|
+
}, []);
|
|
88
172
|
|
|
89
173
|
// On Quest: holds the resolved scene entry. ViroXRSceneNavigator is not
|
|
90
174
|
// rendered until this is non-null, so VRActivity always launches into content.
|
|
91
|
-
const [vrSceneEntry, setVrSceneEntry] = useState<{
|
|
175
|
+
const [vrSceneEntry, setVrSceneEntry] = useState<{
|
|
176
|
+
scene: any;
|
|
177
|
+
passProps?: any;
|
|
178
|
+
} | null>(null);
|
|
179
|
+
|
|
180
|
+
// Host config derived from the loaded scene; native setters apply post-mount,
|
|
181
|
+
// so setting these after the navigator mounts is fine.
|
|
182
|
+
const [occlusionMode, setOcclusionMode] =
|
|
183
|
+
useState<ViroOcclusionMode>(undefined);
|
|
184
|
+
const [numberOfTrackedImages, setNumberOfTrackedImages] = useState<
|
|
185
|
+
number | undefined
|
|
186
|
+
>(undefined);
|
|
187
|
+
|
|
188
|
+
useImperativeHandle(
|
|
189
|
+
ref,
|
|
190
|
+
(): StudioSceneNavigatorHandle => ({
|
|
191
|
+
takeScreenshot: (fileName, saveToCameraRoll) => {
|
|
192
|
+
// On AR the handle is the ViroARSceneNavigator instance (has
|
|
193
|
+
// arSceneNavigator.takeScreenshot); on Quest it's a bridge without it.
|
|
194
|
+
const nav = navigatorRef.current?.arSceneNavigator;
|
|
195
|
+
if (typeof nav?.takeScreenshot !== "function") {
|
|
196
|
+
return Promise.resolve({ success: false });
|
|
197
|
+
}
|
|
198
|
+
return nav.takeScreenshot(fileName, saveToCameraRoll);
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
[]
|
|
202
|
+
);
|
|
92
203
|
|
|
93
204
|
const resolveSceneId = useCallback(async (): Promise<string> => {
|
|
94
205
|
if (sceneId) return sceneId;
|
|
@@ -101,7 +212,9 @@ export function StudioSceneNavigator({
|
|
|
101
212
|
throw new Error("rvGetProject returned no data");
|
|
102
213
|
}
|
|
103
214
|
|
|
104
|
-
const { project } = JSON.parse(
|
|
215
|
+
const { project } = JSON.parse(
|
|
216
|
+
projectResult.data
|
|
217
|
+
) as StudioProjectApiResponse;
|
|
105
218
|
|
|
106
219
|
if (project.opening_scene?.id) {
|
|
107
220
|
return project.opening_scene.id;
|
|
@@ -114,7 +227,9 @@ export function StudioSceneNavigator({
|
|
|
114
227
|
|
|
115
228
|
const loadScene = useCallback(
|
|
116
229
|
async (isCancelled: () => boolean) => {
|
|
117
|
-
await new Promise<void>((resolve) =>
|
|
230
|
+
await new Promise<void>((resolve) =>
|
|
231
|
+
requestAnimationFrame(() => resolve())
|
|
232
|
+
);
|
|
118
233
|
if (isCancelled()) return;
|
|
119
234
|
|
|
120
235
|
const resolvedSceneId = await resolveSceneId();
|
|
@@ -136,11 +251,20 @@ export function StudioSceneNavigator({
|
|
|
136
251
|
|
|
137
252
|
loadedSceneIdRef.current = resolvedSceneId;
|
|
138
253
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
254
|
+
const triggerImageCount = sceneData.assets.filter(
|
|
255
|
+
(a) => !!a.trigger_image_url
|
|
256
|
+
).length;
|
|
257
|
+
setNumberOfTrackedImages(
|
|
258
|
+
triggerImageCount > 0 ? Math.min(triggerImageCount, 5) : undefined
|
|
259
|
+
);
|
|
260
|
+
setOcclusionMode(mapOcclusionMode(sceneData.project?.occlusion_mode));
|
|
261
|
+
|
|
262
|
+
onSceneLoadedRef.current?.(sceneData);
|
|
263
|
+
|
|
264
|
+
// On Quest, pre-register animations and materials before VRActivity
|
|
265
|
+
// launches so the native registrations land before any Viro component
|
|
266
|
+
// mounts; otherwise registerAnimations/createMaterials races the Fabric
|
|
267
|
+
// commit that creates those components.
|
|
144
268
|
if (isQuest) {
|
|
145
269
|
registerSceneAnimations(sceneData.animations);
|
|
146
270
|
registerStudioMaterialsForAssets(sceneData.assets);
|
|
@@ -150,21 +274,24 @@ export function StudioSceneNavigator({
|
|
|
150
274
|
scene: StudioARScene,
|
|
151
275
|
passProps: {
|
|
152
276
|
sceneData,
|
|
153
|
-
onReady:
|
|
277
|
+
onReady: handleSceneReady,
|
|
154
278
|
onSceneChange: onSceneChangeRef.current,
|
|
279
|
+
onPlaneDetected: onPlaneDetectedRef.current,
|
|
280
|
+
onPlaneSelected: onPlaneSelectedRef.current,
|
|
281
|
+
noAssetsMessage: noAssetsMessageRef.current,
|
|
155
282
|
variableStore: variableStoreRef.current,
|
|
156
283
|
},
|
|
157
284
|
};
|
|
158
285
|
|
|
159
286
|
if (isQuest) {
|
|
160
|
-
//
|
|
161
|
-
//
|
|
287
|
+
// Setting vrSceneEntry mounts ViroXRSceneNavigator with StudioARScene as
|
|
288
|
+
// vrInitialScene, so VRActivity launches straight into content.
|
|
162
289
|
setVrSceneEntry(entry);
|
|
163
290
|
} else {
|
|
164
291
|
navigatorRef.current?.arSceneNavigator?.push(entry);
|
|
165
292
|
}
|
|
166
293
|
},
|
|
167
|
-
[resolveSceneId]
|
|
294
|
+
[resolveSceneId, handleSceneReady]
|
|
168
295
|
);
|
|
169
296
|
|
|
170
297
|
useEffect(() => {
|
|
@@ -179,28 +306,46 @@ export function StudioSceneNavigator({
|
|
|
179
306
|
else console.error("[Studio] Failed to load scene:", err);
|
|
180
307
|
});
|
|
181
308
|
|
|
182
|
-
return () => {
|
|
309
|
+
return () => {
|
|
310
|
+
cancelled = true;
|
|
311
|
+
};
|
|
183
312
|
}, [sceneId, loadScene]);
|
|
184
313
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
314
|
+
// Quest has no camera passthrough, so during load it always needs something
|
|
315
|
+
// on screen: the caller's loadingView, else a built-in spinner. (AR shows the
|
|
316
|
+
// live camera, so its overlay stays opt-in.)
|
|
187
317
|
if (isQuest && !vrSceneEntry) {
|
|
188
318
|
return (
|
|
189
319
|
<View style={styles.loader}>
|
|
190
|
-
<ActivityIndicator size="large" color="#ffffff" />
|
|
320
|
+
{loadingView ?? <ActivityIndicator size="large" color="#ffffff" />}
|
|
191
321
|
</View>
|
|
192
322
|
);
|
|
193
323
|
}
|
|
194
324
|
|
|
195
325
|
return (
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
326
|
+
<StudioSceneErrorBoundary
|
|
327
|
+
sceneId={sceneId}
|
|
328
|
+
onError={onError}
|
|
329
|
+
renderError={renderError}
|
|
330
|
+
>
|
|
331
|
+
<View style={style ?? StyleSheet.absoluteFill}>
|
|
332
|
+
<ViroXRSceneNavigator
|
|
333
|
+
ref={navigatorRef}
|
|
334
|
+
arInitialScene={{ scene: LoadingARScene }}
|
|
335
|
+
vrInitialScene={vrSceneEntry ?? { scene: LoadingVRScene }}
|
|
336
|
+
worldAlignment={worldAlignment}
|
|
337
|
+
autofocus={autofocus}
|
|
338
|
+
numberOfTrackedImages={numberOfTrackedImages}
|
|
339
|
+
occlusionMode={occlusionMode}
|
|
340
|
+
onExitViro={onExitViro}
|
|
341
|
+
style={StyleSheet.absoluteFill}
|
|
342
|
+
/>
|
|
343
|
+
{/* Absolutely filled so the overlay covers the navigator instead of
|
|
344
|
+
taking flow space beneath it. */}
|
|
345
|
+
{!isSceneReady && loadingView && (
|
|
346
|
+
<View style={StyleSheet.absoluteFill}>{loadingView}</View>
|
|
347
|
+
)}
|
|
348
|
+
</View>
|
|
349
|
+
</StudioSceneErrorBoundary>
|
|
205
350
|
);
|
|
206
|
-
}
|
|
351
|
+
});
|