@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
|
@@ -149,7 +149,7 @@ export interface StudioApiRequestOutcome {
|
|
|
149
149
|
*/
|
|
150
150
|
export type StudioApiRequestExecutor = (
|
|
151
151
|
functionId: string,
|
|
152
|
-
variables: Record<string, boolean | number | string
|
|
152
|
+
variables: Record<string, boolean | number | string>
|
|
153
153
|
) => Promise<StudioApiRequestOutcome>;
|
|
154
154
|
|
|
155
155
|
export interface StudioSceneFunction {
|
|
@@ -267,13 +267,7 @@ export interface StudioAnimation {
|
|
|
267
267
|
properties: Record<string, unknown>; // Viro keyframe format
|
|
268
268
|
duration_ms: number | null;
|
|
269
269
|
delay_ms: number | null;
|
|
270
|
-
easing:
|
|
271
|
-
| "Linear"
|
|
272
|
-
| "EaseIn"
|
|
273
|
-
| "EaseOut"
|
|
274
|
-
| "EaseInEaseOut"
|
|
275
|
-
| "Bounce"
|
|
276
|
-
| null;
|
|
270
|
+
easing: "Linear" | "EaseIn" | "EaseOut" | "EaseInEaseOut" | "Bounce" | null;
|
|
277
271
|
loop: boolean;
|
|
278
272
|
interruptible: boolean;
|
|
279
273
|
on_start_function: string | null;
|
|
@@ -326,6 +320,7 @@ export interface StudioSceneResponse {
|
|
|
326
320
|
functions: StudioSceneFunction[];
|
|
327
321
|
/** Absent in responses from backends predating the Variables feature. */
|
|
328
322
|
variables?: StudioSceneVariable[];
|
|
323
|
+
is_free_tier?: boolean;
|
|
329
324
|
meta: { request_id: string };
|
|
330
325
|
}
|
|
331
326
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VIRO_VERSION = "2.57.
|
|
1
|
+
export const VIRO_VERSION = "2.57.4";
|
|
@@ -75,6 +75,8 @@ type Props = ViewProps & {
|
|
|
75
75
|
autofocus?: boolean;
|
|
76
76
|
videoQuality?: "High" | "Low";
|
|
77
77
|
numberOfTrackedImages?: number;
|
|
78
|
+
/** AR depth/people occlusion. Flows via ...rest to ViroARSceneNavigator. */
|
|
79
|
+
occlusionMode?: "peopleOnly" | "depthBased";
|
|
78
80
|
|
|
79
81
|
// ── Forwarded to ViroVRSceneNavigator (Quest path via bridge) ──────────────
|
|
80
82
|
vrModeEnabled?: boolean;
|
|
@@ -149,9 +149,17 @@ type Props = ViewProps & {
|
|
|
149
149
|
monocularDepthTargetFPS?: number;
|
|
150
150
|
/**
|
|
151
151
|
* Use the front (selfie) camera as the AR session background.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
152
|
+
*
|
|
153
|
+
* Requires the optional `@reactvision/react-viro-face-tracking` package to be
|
|
154
|
+
* installed — it provides the native front-camera AR configuration and, on
|
|
155
|
+
* iOS, declares TrueDepth usage. Without it, this prop has no effect.
|
|
156
|
+
*
|
|
157
|
+
* On iOS the package uses the front TrueDepth camera; on Android it uses
|
|
158
|
+
* ARCore Augmented Faces mode. World tracking, plane detection, and LiDAR are
|
|
159
|
+
* unavailable in this mode.
|
|
160
|
+
*
|
|
161
|
+
* For a plain selfie feed without face tracking (no TrueDepth), use
|
|
162
|
+
* `ViroCameraTexture` with `cameraPosition="front"` instead.
|
|
155
163
|
*
|
|
156
164
|
* @default false
|
|
157
165
|
* @platform ios, android
|
|
@@ -7,6 +7,12 @@ interface StudioARSceneProps {
|
|
|
7
7
|
onReady?: () => void;
|
|
8
8
|
onError?: (err: Error) => void;
|
|
9
9
|
onSceneChange?: (sceneId: string, sceneName: string) => void;
|
|
10
|
+
/** Fired on first AR plane detection (AUTOMATIC) / plane accept (MANUAL). */
|
|
11
|
+
onPlaneDetected?: () => void;
|
|
12
|
+
/** Fired when the user taps to select a plane (MANUAL mode). */
|
|
13
|
+
onPlaneSelected?: () => void;
|
|
14
|
+
/** Text shown when the scene has no assets. Defaults to "No assets to display". */
|
|
15
|
+
noAssetsMessage?: string;
|
|
10
16
|
/** Session-scoped store owned by the navigator; survives scene pushes. */
|
|
11
17
|
variableStore?: StudioVariableStore;
|
|
12
18
|
}
|
|
@@ -75,7 +75,7 @@ const StudioARScene = (props) => {
|
|
|
75
75
|
};
|
|
76
76
|
exports.StudioARScene = StudioARScene;
|
|
77
77
|
const StudioARSceneInner = (props) => {
|
|
78
|
-
const { sceneNavigator, sceneData, onReady, onSceneChange, variableStore } = props;
|
|
78
|
+
const { sceneNavigator, sceneData, onReady, onSceneChange, onPlaneDetected, onPlaneSelected, noAssetsMessage, variableStore, } = props;
|
|
79
79
|
const { scene, assets, animations, collision_bindings, functions } = sceneData;
|
|
80
80
|
// ─── Sequence scheduler ───────────────────────────────────────────────────
|
|
81
81
|
// One per scene. Drives WAIT steps; cancelled on unmount and on navigation so
|
|
@@ -169,6 +169,38 @@ const StudioARSceneInner = (props) => {
|
|
|
169
169
|
// ─── Animation runtime state ──────────────────────────────────────────────
|
|
170
170
|
const [animOverrides, setAnimOverrides] = (0, react_1.useState)({});
|
|
171
171
|
const [loadedAssetIds, setLoadedAssetIds] = (0, react_1.useState)({});
|
|
172
|
+
// ─── Drag-active state (debounced) ────────────────────────────────────────
|
|
173
|
+
// Viro's onDrag fires per-frame. Track a Record<assetId, true> cleared 220ms
|
|
174
|
+
// after the last drag event; the node factory reads isDragActive to pass
|
|
175
|
+
// kinematicDragOverride so Dynamic-physics bodies don't fight the gesture.
|
|
176
|
+
// The onDrag callback's mere presence is also what unlocks native drag.
|
|
177
|
+
const [dragActiveByAssetId, setDragActiveByAssetId] = (0, react_1.useState)({});
|
|
178
|
+
const dragTimersRef = (0, react_1.useRef)(new Map());
|
|
179
|
+
const notifyPhysicsDrag = (0, react_1.useCallback)((assetId) => {
|
|
180
|
+
setDragActiveByAssetId((prev) => prev[assetId] ? prev : { ...prev, [assetId]: true });
|
|
181
|
+
const existing = dragTimersRef.current.get(assetId);
|
|
182
|
+
if (existing)
|
|
183
|
+
clearTimeout(existing);
|
|
184
|
+
const t = setTimeout(() => {
|
|
185
|
+
setDragActiveByAssetId((prev) => {
|
|
186
|
+
if (!prev[assetId])
|
|
187
|
+
return prev;
|
|
188
|
+
const next = { ...prev };
|
|
189
|
+
delete next[assetId];
|
|
190
|
+
return next;
|
|
191
|
+
});
|
|
192
|
+
dragTimersRef.current.delete(assetId);
|
|
193
|
+
}, 220);
|
|
194
|
+
dragTimersRef.current.set(assetId, t);
|
|
195
|
+
}, []);
|
|
196
|
+
const isDragActive = (0, react_1.useCallback)((assetId) => !!dragActiveByAssetId[assetId], [dragActiveByAssetId]);
|
|
197
|
+
(0, react_1.useEffect)(() => {
|
|
198
|
+
const timers = dragTimersRef.current;
|
|
199
|
+
return () => {
|
|
200
|
+
timers.forEach((timer) => clearTimeout(timer));
|
|
201
|
+
timers.clear();
|
|
202
|
+
};
|
|
203
|
+
}, []);
|
|
172
204
|
const handleAssetLoaded = (0, react_1.useCallback)((assetId) => {
|
|
173
205
|
setLoadedAssetIds((prev) => prev[assetId] ? prev : { ...prev, [assetId]: true });
|
|
174
206
|
}, []);
|
|
@@ -334,7 +366,7 @@ const StudioARSceneInner = (props) => {
|
|
|
334
366
|
return null;
|
|
335
367
|
}
|
|
336
368
|
}
|
|
337
|
-
return (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), handleSceneChange, runtimeCtx);
|
|
369
|
+
return (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), isDragActive, notifyPhysicsDrag, handleSceneChange, runtimeCtx);
|
|
338
370
|
})
|
|
339
371
|
.filter(Boolean);
|
|
340
372
|
}, [
|
|
@@ -344,6 +376,8 @@ const StudioARSceneInner = (props) => {
|
|
|
344
376
|
animationStates,
|
|
345
377
|
handleAssetLoaded,
|
|
346
378
|
getCollisionHandler,
|
|
379
|
+
isDragActive,
|
|
380
|
+
notifyPhysicsDrag,
|
|
347
381
|
maxModels,
|
|
348
382
|
handleSceneChange,
|
|
349
383
|
runtimeCtx,
|
|
@@ -356,7 +390,7 @@ const StudioARSceneInner = (props) => {
|
|
|
356
390
|
const targetName = urlToTargetName.get(asset.trigger_image_url);
|
|
357
391
|
if (!targetName)
|
|
358
392
|
return null;
|
|
359
|
-
const node = (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), handleSceneChange, runtimeCtx);
|
|
393
|
+
const node = (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), isDragActive, notifyPhysicsDrag, handleSceneChange, runtimeCtx);
|
|
360
394
|
if (!node)
|
|
361
395
|
return null;
|
|
362
396
|
return (<ViroARImageMarker_1.ViroARImageMarker key={asset.id} target={targetName}>
|
|
@@ -372,12 +406,71 @@ const StudioARSceneInner = (props) => {
|
|
|
372
406
|
animationStates,
|
|
373
407
|
handleAssetLoaded,
|
|
374
408
|
getCollisionHandler,
|
|
409
|
+
isDragActive,
|
|
410
|
+
notifyPhysicsDrag,
|
|
375
411
|
handleSceneChange,
|
|
376
412
|
runtimeCtx,
|
|
377
413
|
]);
|
|
378
414
|
// ─── Plane detection (AR only) ────────────────────────────────────────────
|
|
379
415
|
const planeDetectionMode = (scene.plane_detection ?? "NONE").toUpperCase();
|
|
380
416
|
const planeAlignment = (scene.plane_direction ?? "Horizontal");
|
|
417
|
+
// Native plane anchor types for ViroARScene (lowercase matches Viro defaults).
|
|
418
|
+
const anchorDetectionTypes = (0, react_1.useMemo)(() => {
|
|
419
|
+
if (planeDetectionMode !== "AUTOMATIC" && planeDetectionMode !== "MANUAL") {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
const dir = (scene.plane_direction ?? "Horizontal").toLowerCase();
|
|
423
|
+
if (dir === "vertical")
|
|
424
|
+
return ["planesVertical"];
|
|
425
|
+
if (dir.includes("horizontal"))
|
|
426
|
+
return ["planesHorizontal"];
|
|
427
|
+
return ["planesHorizontal", "planesVertical"];
|
|
428
|
+
}, [planeDetectionMode, scene.plane_direction]);
|
|
429
|
+
// ViroARPlaneSelector (react-viro 2.54+) no longer receives scene anchors
|
|
430
|
+
// automatically; ViroARScene forwards them here via ref. Also surfaces
|
|
431
|
+
// onPlaneDetected / onPlaneSelected to the host.
|
|
432
|
+
const planeSelectorRef = (0, react_1.useRef)(null);
|
|
433
|
+
const handleAnchorFound = (0, react_1.useCallback)((anchor) => {
|
|
434
|
+
try {
|
|
435
|
+
if (planeDetectionMode === "MANUAL") {
|
|
436
|
+
planeSelectorRef.current?.handleAnchorFound(anchor);
|
|
437
|
+
}
|
|
438
|
+
if (planeDetectionMode === "AUTOMATIC" && anchor?.type === "plane") {
|
|
439
|
+
onPlaneDetected?.();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
console.error("[Studio] handleAnchorFound failed:", error);
|
|
444
|
+
}
|
|
445
|
+
}, [planeDetectionMode, onPlaneDetected]);
|
|
446
|
+
const handleAnchorUpdated = (0, react_1.useCallback)((anchor) => {
|
|
447
|
+
try {
|
|
448
|
+
if (planeDetectionMode === "MANUAL") {
|
|
449
|
+
planeSelectorRef.current?.handleAnchorUpdated(anchor);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
console.error("[Studio] handleAnchorUpdated failed:", error);
|
|
454
|
+
}
|
|
455
|
+
}, [planeDetectionMode]);
|
|
456
|
+
const handleAnchorRemoved = (0, react_1.useCallback)((anchor) => {
|
|
457
|
+
try {
|
|
458
|
+
if (planeDetectionMode === "MANUAL" && anchor) {
|
|
459
|
+
planeSelectorRef.current?.handleAnchorRemoved(anchor);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
console.error("[Studio] handleAnchorRemoved failed:", error);
|
|
464
|
+
}
|
|
465
|
+
}, [planeDetectionMode]);
|
|
466
|
+
const handlePlaneSelected = (0, react_1.useCallback)(() => {
|
|
467
|
+
onPlaneSelected?.();
|
|
468
|
+
}, [onPlaneSelected]);
|
|
469
|
+
// ViroARPlaneSelector.onPlaneDetected must return a boolean (accept the plane).
|
|
470
|
+
const handlePlaneDetectedForSelector = (0, react_1.useCallback)(() => {
|
|
471
|
+
onPlaneDetected?.();
|
|
472
|
+
return true;
|
|
473
|
+
}, [onPlaneDetected]);
|
|
381
474
|
const renderAssets = () => {
|
|
382
475
|
if (ViroPlatform_1.isQuest) {
|
|
383
476
|
if (planeDetectionMode !== "NONE") {
|
|
@@ -391,7 +484,7 @@ const StudioARSceneInner = (props) => {
|
|
|
391
484
|
</ViroARPlane_1.ViroARPlane>);
|
|
392
485
|
}
|
|
393
486
|
if (planeDetectionMode === "MANUAL") {
|
|
394
|
-
return (<ViroARPlaneSelector_1.ViroARPlaneSelector minHeight={0.1} minWidth={0.1} alignment={planeAlignment}>
|
|
487
|
+
return (<ViroARPlaneSelector_1.ViroARPlaneSelector ref={planeSelectorRef} minHeight={0.1} minWidth={0.1} alignment={planeAlignment} onPlaneDetected={handlePlaneDetectedForSelector} onPlaneSelected={handlePlaneSelected}>
|
|
395
488
|
{renderedPlaneAssets}
|
|
396
489
|
</ViroARPlaneSelector_1.ViroARPlaneSelector>);
|
|
397
490
|
}
|
|
@@ -412,7 +505,7 @@ const StudioARSceneInner = (props) => {
|
|
|
412
505
|
{renderAssets()}
|
|
413
506
|
{renderedImageTriggeredAssets}
|
|
414
507
|
<StudioSounds_1.StudioSounds manager={soundManagerRef.current}/>
|
|
415
|
-
{assets.length === 0 && (<ViroText_1.ViroText text="No assets to display" position={[0, 0, -2]} style={{
|
|
508
|
+
{assets.length === 0 && (<ViroText_1.ViroText text={noAssetsMessage ?? "No assets to display"} position={[0, 0, -2]} style={{
|
|
416
509
|
fontFamily: "Arial",
|
|
417
510
|
fontSize: 16,
|
|
418
511
|
color: "#CCCCCC",
|
|
@@ -422,5 +515,7 @@ const StudioARSceneInner = (props) => {
|
|
|
422
515
|
if (ViroPlatform_1.isQuest) {
|
|
423
516
|
return <ViroScene_1.ViroScene {...physicsProps}>{children}</ViroScene_1.ViroScene>;
|
|
424
517
|
}
|
|
425
|
-
return <ViroARScene_1.ViroARScene {...physicsProps}
|
|
518
|
+
return (<ViroARScene_1.ViroARScene {...physicsProps} {...(anchorDetectionTypes != null ? { anchorDetectionTypes } : {})} onAnchorFound={handleAnchorFound} onAnchorUpdated={handleAnchorUpdated} onAnchorRemoved={handleAnchorRemoved}>
|
|
519
|
+
{children}
|
|
520
|
+
</ViroARScene_1.ViroARScene>);
|
|
426
521
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Component, ErrorInfo, ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
onError?: (error: Error) => void;
|
|
5
|
+
renderError?: (error: Error) => ReactNode;
|
|
6
|
+
sceneId?: string;
|
|
7
|
+
}
|
|
8
|
+
interface State {
|
|
9
|
+
hasError: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Catches render/lifecycle errors in the Studio scene tree so a bad asset or a
|
|
14
|
+
* node-factory throw can't take down the host app. Always reports via onError;
|
|
15
|
+
* renders renderError(error) if provided, else nothing (no built-in UI).
|
|
16
|
+
*
|
|
17
|
+
* AR-only in effect: on Quest, ViroXRSceneNavigator forwards to a separate
|
|
18
|
+
* React root (VRActivity), so scene errors there are outside this boundary.
|
|
19
|
+
* Native and async errors are also out of a React boundary's reach; async
|
|
20
|
+
* scene-load failures route through onError in StudioSceneNavigator instead.
|
|
21
|
+
*/
|
|
22
|
+
export declare class StudioSceneErrorBoundary extends Component<Props, State> {
|
|
23
|
+
state: State;
|
|
24
|
+
static getDerivedStateFromError(error: Error): State;
|
|
25
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
26
|
+
render(): ReactNode;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StudioSceneErrorBoundary = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Catches render/lifecycle errors in the Studio scene tree so a bad asset or a
|
|
7
|
+
* node-factory throw can't take down the host app. Always reports via onError;
|
|
8
|
+
* renders renderError(error) if provided, else nothing (no built-in UI).
|
|
9
|
+
*
|
|
10
|
+
* AR-only in effect: on Quest, ViroXRSceneNavigator forwards to a separate
|
|
11
|
+
* React root (VRActivity), so scene errors there are outside this boundary.
|
|
12
|
+
* Native and async errors are also out of a React boundary's reach; async
|
|
13
|
+
* scene-load failures route through onError in StudioSceneNavigator instead.
|
|
14
|
+
*/
|
|
15
|
+
class StudioSceneErrorBoundary extends react_1.Component {
|
|
16
|
+
state = { hasError: false, error: null };
|
|
17
|
+
static getDerivedStateFromError(error) {
|
|
18
|
+
return { hasError: true, error };
|
|
19
|
+
}
|
|
20
|
+
componentDidCatch(error, errorInfo) {
|
|
21
|
+
this.props.onError?.(error);
|
|
22
|
+
console.error("[Studio] Scene render error:", error, this.props.sceneId, errorInfo.componentStack);
|
|
23
|
+
}
|
|
24
|
+
render() {
|
|
25
|
+
if (this.state.hasError && this.state.error) {
|
|
26
|
+
return this.props.renderError?.(this.state.error) ?? null;
|
|
27
|
+
}
|
|
28
|
+
return this.props.children;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.StudioSceneErrorBoundary = StudioSceneErrorBoundary;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { ViewStyle } from "react-native";
|
|
3
|
-
|
|
3
|
+
import { StudioSceneResponse } from "./types";
|
|
4
|
+
/** Imperative handle exposed via ref. */
|
|
5
|
+
export interface StudioSceneNavigatorHandle {
|
|
6
|
+
/** Screenshots the AR renderer. Resolves `{ success: false }` (no-op) on Quest. */
|
|
7
|
+
takeScreenshot: (fileName: string, saveToCameraRoll: boolean) => Promise<{
|
|
8
|
+
success: boolean;
|
|
9
|
+
url?: string;
|
|
10
|
+
errorCode?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export interface StudioSceneNavigatorProps {
|
|
4
14
|
/**
|
|
5
15
|
* UUID of a specific scene to load. If omitted, the navigator fetches the
|
|
6
16
|
* project configured in the app manifest and uses its opening scene.
|
|
@@ -13,6 +23,22 @@ interface StudioSceneNavigatorProps {
|
|
|
13
23
|
onError?: (err: Error) => void;
|
|
14
24
|
onSceneChange?: (sceneId: string, sceneName: string) => void;
|
|
15
25
|
onExitViro?: () => void;
|
|
26
|
+
/** Fired after the scene is fetched and parsed, before it is pushed. */
|
|
27
|
+
onSceneLoaded?: (sceneData: StudioSceneResponse) => void;
|
|
28
|
+
/** Threaded to the initial scene's StudioARScene (initial scene only). */
|
|
29
|
+
onPlaneDetected?: () => void;
|
|
30
|
+
onPlaneSelected?: () => void;
|
|
31
|
+
noAssetsMessage?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Opt-in overlay shown until the scene mounts. Omit to render nothing on AR
|
|
34
|
+
* during load (the camera feed); Quest falls back to a built-in spinner.
|
|
35
|
+
*/
|
|
36
|
+
loadingView?: React.ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Opt-in UI for a caught render error. The boundary always catches and calls
|
|
39
|
+
* `onError`; when this is omitted it renders nothing.
|
|
40
|
+
*/
|
|
41
|
+
renderError?: (error: Error) => React.ReactNode;
|
|
16
42
|
}
|
|
17
43
|
/**
|
|
18
44
|
* Cross-reality Studio scene navigator. Renders a Studio-authored scene on
|
|
@@ -27,5 +53,4 @@ interface StudioSceneNavigatorProps {
|
|
|
27
53
|
* ready. This means VRActivity always launches with the actual content scene
|
|
28
54
|
* as its initial scene, avoiding the LoadingVRScene → replace timing race.
|
|
29
55
|
*/
|
|
30
|
-
export declare
|
|
31
|
-
export {};
|
|
56
|
+
export declare const StudioSceneNavigator: React.ForwardRefExoticComponent<StudioSceneNavigatorProps & React.RefAttributes<StudioSceneNavigatorHandle>>;
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.StudioSceneNavigator =
|
|
36
|
+
exports.StudioSceneNavigator = void 0;
|
|
37
37
|
const React = __importStar(require("react"));
|
|
38
38
|
const react_1 = require("react");
|
|
39
39
|
const react_native_1 = require("react-native");
|
|
@@ -45,13 +45,31 @@ const animationRegistry_1 = require("./domain/animationRegistry");
|
|
|
45
45
|
const studioMaterials_1 = require("./domain/studioMaterials");
|
|
46
46
|
const variableStore_1 = require("./domain/variableStore");
|
|
47
47
|
const StudioARScene_1 = require("./StudioARScene");
|
|
48
|
+
const StudioSceneErrorBoundary_1 = require("./StudioSceneErrorBoundary");
|
|
48
49
|
const VRTStudioModule_1 = require("./VRTStudioModule");
|
|
49
|
-
function LoadingARScene() {
|
|
50
|
-
|
|
50
|
+
function LoadingARScene() {
|
|
51
|
+
return <ViroARScene_1.ViroARScene />;
|
|
52
|
+
}
|
|
53
|
+
function LoadingVRScene() {
|
|
54
|
+
return <ViroScene_1.ViroScene />;
|
|
55
|
+
}
|
|
56
|
+
function mapOcclusionMode(dbValue) {
|
|
57
|
+
switch (dbValue) {
|
|
58
|
+
case "PEOPLEONLY":
|
|
59
|
+
return "peopleOnly";
|
|
60
|
+
case "DEPTHBASED":
|
|
61
|
+
return "depthBased";
|
|
62
|
+
default:
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
51
66
|
const styles = react_native_1.StyleSheet.create({
|
|
52
67
|
loader: {
|
|
53
68
|
position: "absolute",
|
|
54
|
-
top: 0,
|
|
69
|
+
top: 0,
|
|
70
|
+
left: 0,
|
|
71
|
+
right: 0,
|
|
72
|
+
bottom: 0,
|
|
55
73
|
justifyContent: "center",
|
|
56
74
|
alignItems: "center",
|
|
57
75
|
backgroundColor: "#000000",
|
|
@@ -70,9 +88,10 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
70
88
|
* ready. This means VRActivity always launches with the actual content scene
|
|
71
89
|
* as its initial scene, avoiding the LoadingVRScene → replace timing race.
|
|
72
90
|
*/
|
|
73
|
-
function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus = true, style, onSceneReady, onError, onSceneChange, onExitViro, }) {
|
|
91
|
+
exports.StudioSceneNavigator = (0, react_1.forwardRef)(function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus = true, style, onSceneReady, onError, onSceneChange, onExitViro, onSceneLoaded, onPlaneDetected, onPlaneSelected, noAssetsMessage, loadingView, renderError, }, ref) {
|
|
74
92
|
const navigatorRef = (0, react_1.useRef)(null);
|
|
75
93
|
const loadedSceneIdRef = (0, react_1.useRef)(null);
|
|
94
|
+
const [isSceneReady, setIsSceneReady] = (0, react_1.useState)(false);
|
|
76
95
|
// Session-scoped variable store: outlives every scene push, resets when the
|
|
77
96
|
// navigator (= the AR/VR session) unmounts.
|
|
78
97
|
const variableStoreRef = (0, react_1.useRef)(null);
|
|
@@ -88,12 +107,41 @@ function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus =
|
|
|
88
107
|
const onSceneReadyRef = (0, react_1.useRef)(onSceneReady);
|
|
89
108
|
const onErrorRef = (0, react_1.useRef)(onError);
|
|
90
109
|
const onSceneChangeRef = (0, react_1.useRef)(onSceneChange);
|
|
110
|
+
const onSceneLoadedRef = (0, react_1.useRef)(onSceneLoaded);
|
|
111
|
+
const onPlaneDetectedRef = (0, react_1.useRef)(onPlaneDetected);
|
|
112
|
+
const onPlaneSelectedRef = (0, react_1.useRef)(onPlaneSelected);
|
|
113
|
+
const noAssetsMessageRef = (0, react_1.useRef)(noAssetsMessage);
|
|
91
114
|
onSceneReadyRef.current = onSceneReady;
|
|
92
115
|
onErrorRef.current = onError;
|
|
93
116
|
onSceneChangeRef.current = onSceneChange;
|
|
117
|
+
onSceneLoadedRef.current = onSceneLoaded;
|
|
118
|
+
onPlaneDetectedRef.current = onPlaneDetected;
|
|
119
|
+
onPlaneSelectedRef.current = onPlaneSelected;
|
|
120
|
+
noAssetsMessageRef.current = noAssetsMessage;
|
|
121
|
+
// Stable so passProps stays referentially steady across renders. Idempotent,
|
|
122
|
+
// so StrictMode's dev double-invoke of StudioARScene's onReady effect is safe.
|
|
123
|
+
const handleSceneReady = (0, react_1.useCallback)(() => {
|
|
124
|
+
setIsSceneReady(true);
|
|
125
|
+
onSceneReadyRef.current?.();
|
|
126
|
+
}, []);
|
|
94
127
|
// On Quest: holds the resolved scene entry. ViroXRSceneNavigator is not
|
|
95
128
|
// rendered until this is non-null, so VRActivity always launches into content.
|
|
96
129
|
const [vrSceneEntry, setVrSceneEntry] = (0, react_1.useState)(null);
|
|
130
|
+
// Host config derived from the loaded scene; native setters apply post-mount,
|
|
131
|
+
// so setting these after the navigator mounts is fine.
|
|
132
|
+
const [occlusionMode, setOcclusionMode] = (0, react_1.useState)(undefined);
|
|
133
|
+
const [numberOfTrackedImages, setNumberOfTrackedImages] = (0, react_1.useState)(undefined);
|
|
134
|
+
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
135
|
+
takeScreenshot: (fileName, saveToCameraRoll) => {
|
|
136
|
+
// On AR the handle is the ViroARSceneNavigator instance (has
|
|
137
|
+
// arSceneNavigator.takeScreenshot); on Quest it's a bridge without it.
|
|
138
|
+
const nav = navigatorRef.current?.arSceneNavigator;
|
|
139
|
+
if (typeof nav?.takeScreenshot !== "function") {
|
|
140
|
+
return Promise.resolve({ success: false });
|
|
141
|
+
}
|
|
142
|
+
return nav.takeScreenshot(fileName, saveToCameraRoll);
|
|
143
|
+
},
|
|
144
|
+
}), []);
|
|
97
145
|
const resolveSceneId = (0, react_1.useCallback)(async () => {
|
|
98
146
|
if (sceneId)
|
|
99
147
|
return sceneId;
|
|
@@ -135,11 +183,14 @@ function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus =
|
|
|
135
183
|
if (isCancelled())
|
|
136
184
|
return;
|
|
137
185
|
loadedSceneIdRef.current = resolvedSceneId;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
186
|
+
const triggerImageCount = sceneData.assets.filter((a) => !!a.trigger_image_url).length;
|
|
187
|
+
setNumberOfTrackedImages(triggerImageCount > 0 ? Math.min(triggerImageCount, 5) : undefined);
|
|
188
|
+
setOcclusionMode(mapOcclusionMode(sceneData.project?.occlusion_mode));
|
|
189
|
+
onSceneLoadedRef.current?.(sceneData);
|
|
190
|
+
// On Quest, pre-register animations and materials before VRActivity
|
|
191
|
+
// launches so the native registrations land before any Viro component
|
|
192
|
+
// mounts; otherwise registerAnimations/createMaterials races the Fabric
|
|
193
|
+
// commit that creates those components.
|
|
143
194
|
if (ViroPlatform_1.isQuest) {
|
|
144
195
|
(0, animationRegistry_1.registerSceneAnimations)(sceneData.animations);
|
|
145
196
|
(0, studioMaterials_1.registerStudioMaterialsForAssets)(sceneData.assets);
|
|
@@ -148,20 +199,23 @@ function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus =
|
|
|
148
199
|
scene: StudioARScene_1.StudioARScene,
|
|
149
200
|
passProps: {
|
|
150
201
|
sceneData,
|
|
151
|
-
onReady:
|
|
202
|
+
onReady: handleSceneReady,
|
|
152
203
|
onSceneChange: onSceneChangeRef.current,
|
|
204
|
+
onPlaneDetected: onPlaneDetectedRef.current,
|
|
205
|
+
onPlaneSelected: onPlaneSelectedRef.current,
|
|
206
|
+
noAssetsMessage: noAssetsMessageRef.current,
|
|
153
207
|
variableStore: variableStoreRef.current,
|
|
154
208
|
},
|
|
155
209
|
};
|
|
156
210
|
if (ViroPlatform_1.isQuest) {
|
|
157
|
-
//
|
|
158
|
-
//
|
|
211
|
+
// Setting vrSceneEntry mounts ViroXRSceneNavigator with StudioARScene as
|
|
212
|
+
// vrInitialScene, so VRActivity launches straight into content.
|
|
159
213
|
setVrSceneEntry(entry);
|
|
160
214
|
}
|
|
161
215
|
else {
|
|
162
216
|
navigatorRef.current?.arSceneNavigator?.push(entry);
|
|
163
217
|
}
|
|
164
|
-
}, [resolveSceneId]);
|
|
218
|
+
}, [resolveSceneId, handleSceneReady]);
|
|
165
219
|
(0, react_1.useEffect)(() => {
|
|
166
220
|
let cancelled = false;
|
|
167
221
|
const isCancelled = () => cancelled;
|
|
@@ -175,14 +229,24 @@ function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus =
|
|
|
175
229
|
else
|
|
176
230
|
console.error("[Studio] Failed to load scene:", err);
|
|
177
231
|
});
|
|
178
|
-
return () => {
|
|
232
|
+
return () => {
|
|
233
|
+
cancelled = true;
|
|
234
|
+
};
|
|
179
235
|
}, [sceneId, loadScene]);
|
|
180
|
-
//
|
|
181
|
-
//
|
|
236
|
+
// Quest has no camera passthrough, so during load it always needs something
|
|
237
|
+
// on screen: the caller's loadingView, else a built-in spinner. (AR shows the
|
|
238
|
+
// live camera, so its overlay stays opt-in.)
|
|
182
239
|
if (ViroPlatform_1.isQuest && !vrSceneEntry) {
|
|
183
240
|
return (<react_native_1.View style={styles.loader}>
|
|
184
|
-
<react_native_1.ActivityIndicator size="large" color="#ffffff"/>
|
|
241
|
+
{loadingView ?? <react_native_1.ActivityIndicator size="large" color="#ffffff"/>}
|
|
185
242
|
</react_native_1.View>);
|
|
186
243
|
}
|
|
187
|
-
return (<
|
|
188
|
-
}
|
|
244
|
+
return (<StudioSceneErrorBoundary_1.StudioSceneErrorBoundary sceneId={sceneId} onError={onError} renderError={renderError}>
|
|
245
|
+
<react_native_1.View style={style ?? react_native_1.StyleSheet.absoluteFill}>
|
|
246
|
+
<ViroXRSceneNavigator_1.ViroXRSceneNavigator ref={navigatorRef} arInitialScene={{ scene: LoadingARScene }} vrInitialScene={vrSceneEntry ?? { scene: LoadingVRScene }} worldAlignment={worldAlignment} autofocus={autofocus} numberOfTrackedImages={numberOfTrackedImages} occlusionMode={occlusionMode} onExitViro={onExitViro} style={react_native_1.StyleSheet.absoluteFill}/>
|
|
247
|
+
{/* Absolutely filled so the overlay covers the navigator instead of
|
|
248
|
+
taking flow space beneath it. */}
|
|
249
|
+
{!isSceneReady && loadingView && (<react_native_1.View style={react_native_1.StyleSheet.absoluteFill}>{loadingView}</react_native_1.View>)}
|
|
250
|
+
</react_native_1.View>
|
|
251
|
+
</StudioSceneErrorBoundary_1.StudioSceneErrorBoundary>);
|
|
252
|
+
});
|
|
@@ -3,6 +3,16 @@ export interface StudioModuleResult {
|
|
|
3
3
|
data?: string;
|
|
4
4
|
error?: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* @internal — session auth for first-party apps (e.g. StudioGo). Deliberately
|
|
8
|
+
* not exported from the package root; the server's org-membership check is the
|
|
9
|
+
* real boundary.
|
|
10
|
+
*/
|
|
11
|
+
export interface StudioSessionConfig {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
accessToken: string;
|
|
14
|
+
clientTag?: string;
|
|
15
|
+
}
|
|
6
16
|
export declare const VRTStudioModule: {
|
|
7
17
|
rvGetScene: (sceneId: string) => Promise<StudioModuleResult>;
|
|
8
18
|
/**
|
|
@@ -18,4 +28,11 @@ export declare const VRTStudioModule: {
|
|
|
18
28
|
* `data` is the proxy's outcome envelope JSON.
|
|
19
29
|
*/
|
|
20
30
|
rvStudioApiRequest: (bodyJson: string) => Promise<StudioModuleResult>;
|
|
31
|
+
/**
|
|
32
|
+
* @internal — sets/clears an internal session so the fetch methods use
|
|
33
|
+
* `${baseUrl}/functions/v1/...` with `Authorization: Bearer` (and no
|
|
34
|
+
* `x-api-key`) instead of the manifest RVApiKey. Pass null to revert to
|
|
35
|
+
* API-key mode. Session auth wins over any manifest key.
|
|
36
|
+
*/
|
|
37
|
+
rvSetStudioSession: (config: StudioSessionConfig | null) => Promise<void>;
|
|
21
38
|
};
|
|
@@ -38,4 +38,18 @@ exports.VRTStudioModule = {
|
|
|
38
38
|
return Promise.resolve(NOT_AVAILABLE);
|
|
39
39
|
return native.rvStudioApiRequest(bodyJson);
|
|
40
40
|
},
|
|
41
|
+
/**
|
|
42
|
+
* @internal — sets/clears an internal session so the fetch methods use
|
|
43
|
+
* `${baseUrl}/functions/v1/...` with `Authorization: Bearer` (and no
|
|
44
|
+
* `x-api-key`) instead of the manifest RVApiKey. Pass null to revert to
|
|
45
|
+
* API-key mode. Session auth wins over any manifest key.
|
|
46
|
+
*/
|
|
47
|
+
rvSetStudioSession: (config) => {
|
|
48
|
+
// Method-level guard, not just `!native`: JS can OTA ahead of the native
|
|
49
|
+
// binary, so an older build has VRTStudio present but without this (newer)
|
|
50
|
+
// method. No-op then, and viro stays in manifest API-key mode.
|
|
51
|
+
if (typeof native?.rvSetStudioSession !== "function")
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
return native.rvSetStudioSession(config);
|
|
54
|
+
},
|
|
41
55
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StudioAsset, StudioSceneMeta } from "../types";
|
|
2
|
-
export type DragType = "
|
|
2
|
+
export type DragType = "FixedToPlane" | "FixedDistance" | undefined;
|
|
3
3
|
export type DragPlane = {
|
|
4
4
|
planePoint: [number, number, number];
|
|
5
5
|
planeNormal: [number, number, number];
|
|
@@ -7,8 +7,10 @@ export type DragPlane = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare class DragConfiguration {
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* FixedToPlane when the scene uses plane detection; otherwise FixedDistance,
|
|
11
|
+
* which keeps the object at its grab distance and follows the finger.
|
|
12
|
+
* (FixedToWorld raycast-snapped objects toward the camera on drag start, so
|
|
13
|
+
* they appeared to grow and were hard to place.) undefined if not draggable.
|
|
12
14
|
*/
|
|
13
15
|
static getDragType(asset: StudioAsset, scene: StudioSceneMeta | null): DragType;
|
|
14
16
|
/**
|
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DragConfiguration = void 0;
|
|
4
4
|
class DragConfiguration {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* FixedToPlane when the scene uses plane detection; otherwise FixedDistance,
|
|
7
|
+
* which keeps the object at its grab distance and follows the finger.
|
|
8
|
+
* (FixedToWorld raycast-snapped objects toward the camera on drag start, so
|
|
9
|
+
* they appeared to grow and were hard to place.) undefined if not draggable.
|
|
8
10
|
*/
|
|
9
11
|
static getDragType(asset, scene) {
|
|
10
12
|
if (!asset.is_draggable)
|
|
@@ -13,7 +15,7 @@ class DragConfiguration {
|
|
|
13
15
|
if (planeDetection === "AUTOMATIC" || planeDetection === "MANUAL") {
|
|
14
16
|
return "FixedToPlane";
|
|
15
17
|
}
|
|
16
|
-
return "
|
|
18
|
+
return "FixedDistance";
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Returns a drag plane that passes through the object's current position,
|