@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.
Files changed (51) hide show
  1. package/README.md +8 -0
  2. package/android/react_viro/react_viro-release.aar +0 -0
  3. package/android/viro_renderer/viro_renderer-release.aar +0 -0
  4. package/components/AR/ViroARSceneNavigator.tsx +11 -3
  5. package/components/Studio/StudioARScene.tsx +153 -4
  6. package/components/Studio/StudioSceneErrorBoundary.tsx +48 -0
  7. package/components/Studio/StudioSceneNavigator.tsx +186 -41
  8. package/components/Studio/VRTStudioModule.ts +26 -0
  9. package/components/Studio/domain/dragConfiguration.ts +6 -4
  10. package/components/Studio/domain/materialConfig.ts +75 -42
  11. package/components/Studio/domain/physicsConfig.ts +75 -32
  12. package/components/Studio/domain/viroNodeFactory.tsx +53 -14
  13. package/components/Studio/index.ts +4 -0
  14. package/components/Studio/types.ts +3 -8
  15. package/components/Utilities/ViroVersion.ts +1 -1
  16. package/components/ViroXRSceneNavigator.tsx +2 -0
  17. package/dist/components/AR/ViroARSceneNavigator.d.ts +11 -3
  18. package/dist/components/Studio/StudioARScene.d.ts +6 -0
  19. package/dist/components/Studio/StudioARScene.js +101 -6
  20. package/dist/components/Studio/StudioSceneErrorBoundary.d.ts +28 -0
  21. package/dist/components/Studio/StudioSceneErrorBoundary.js +31 -0
  22. package/dist/components/Studio/StudioSceneNavigator.d.ts +28 -3
  23. package/dist/components/Studio/StudioSceneNavigator.js +84 -20
  24. package/dist/components/Studio/VRTStudioModule.d.ts +17 -0
  25. package/dist/components/Studio/VRTStudioModule.js +14 -0
  26. package/dist/components/Studio/domain/dragConfiguration.d.ts +5 -3
  27. package/dist/components/Studio/domain/dragConfiguration.js +5 -3
  28. package/dist/components/Studio/domain/materialConfig.d.ts +0 -1
  29. package/dist/components/Studio/domain/materialConfig.js +12 -5
  30. package/dist/components/Studio/domain/physicsConfig.d.ts +0 -1
  31. package/dist/components/Studio/domain/physicsConfig.js +18 -6
  32. package/dist/components/Studio/domain/viroNodeFactory.d.ts +2 -2
  33. package/dist/components/Studio/domain/viroNodeFactory.js +31 -18
  34. package/dist/components/Studio/index.d.ts +1 -0
  35. package/dist/components/Studio/types.d.ts +1 -0
  36. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  37. package/dist/components/Utilities/ViroVersion.js +1 -1
  38. package/dist/components/ViroXRSceneNavigator.d.ts +2 -0
  39. package/dist/index.d.ts +1 -1
  40. package/index.ts +2 -0
  41. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
  42. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
  43. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +8 -0
  44. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSessioniOS.h +14 -3
  45. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROFrontCameraProvider.h +34 -0
  46. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROViewAR.h +3 -1
  47. package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
  48. package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
  49. package/ios/dist/include/RVStudioWatermarkState.h +25 -0
  50. package/ios/dist/lib/libViroReact.a +0 -0
  51. 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.2";
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
- * On iOS uses ARFaceTrackingConfiguration (requires TrueDepth camera, iPhone X+).
153
- * On Android uses ARCore Augmented Faces mode (front camera).
154
- * World tracking, plane detection, and LiDAR are unavailable in this mode.
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}>{children}</ViroARScene_1.ViroARScene>;
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
- interface StudioSceneNavigatorProps {
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 function StudioSceneNavigator({ sceneId, worldAlignment, autofocus, style, onSceneReady, onError, onSceneChange, onExitViro, }: StudioSceneNavigatorProps): React.JSX.Element;
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 = 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() { return <ViroARScene_1.ViroARScene />; }
50
- function LoadingVRScene() { return <ViroScene_1.ViroScene />; }
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, left: 0, right: 0, bottom: 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
- // On Quest: pre-register animations and materials before VRActivity launches.
139
- // This mirrors the module-level registration pattern used by XRSceneContent —
140
- // native registrations complete before any Viro components mount, eliminating
141
- // the race between registerAnimations/createMaterials native calls and the
142
- // Fabric commit that creates those components.
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: onSceneReadyRef.current,
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
- // On Quest: setting vrSceneEntry triggers ViroXRSceneNavigator to mount
158
- // with StudioARScene as vrInitialScene VRActivity gets content immediately.
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 () => { cancelled = true; };
232
+ return () => {
233
+ cancelled = true;
234
+ };
179
235
  }, [sceneId, loadScene]);
180
- // On Quest: show a spinner until scene data is ready, then mount
181
- // ViroXRSceneNavigator (which launches VRActivity with content immediately).
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 (<ViroXRSceneNavigator_1.ViroXRSceneNavigator ref={navigatorRef} arInitialScene={{ scene: LoadingARScene }} vrInitialScene={vrSceneEntry ?? { scene: LoadingVRScene }} worldAlignment={worldAlignment} autofocus={autofocus} onExitViro={onExitViro} style={style ?? react_native_1.StyleSheet.absoluteFill}/>);
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 = "FixedToWorld" | "FixedToPlane" | undefined;
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
- * Chooses FixedToPlane when the scene uses plane detection, FixedToWorld otherwise.
11
- * Returns undefined if the asset is not draggable.
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
- * Chooses FixedToPlane when the scene uses plane detection, FixedToWorld otherwise.
7
- * Returns undefined if the asset is not draggable.
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 "FixedToWorld";
18
+ return "FixedDistance";
17
19
  }
18
20
  /**
19
21
  * Returns a drag plane that passes through the object's current position,
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Studio material_config parsing and Viro material definition building.
3
- * Ported from studio-go/domain/materialConfig.ts — no zod dependency.
4
3
  */
5
4
  type ShaderModifierStage = {
6
5
  uniforms?: string;