@reactvision/react-viro 2.53.1 → 2.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +85 -46
  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/ViroARCamera.tsx +5 -0
  5. package/components/AR/ViroARImageMarker.tsx +5 -0
  6. package/components/AR/ViroARObjectMarker.tsx +5 -0
  7. package/components/AR/ViroARPlane.tsx +5 -0
  8. package/components/AR/ViroARPlaneSelector.tsx +5 -0
  9. package/components/AR/ViroARScene.tsx +5 -0
  10. package/components/AR/ViroARSceneNavigator.tsx +84 -0
  11. package/components/AR/ViroCommonProps.ts +11 -0
  12. package/components/Material/ViroMaterials.ts +51 -0
  13. package/components/Studio/StudioARScene.tsx +368 -0
  14. package/components/Studio/StudioSceneNavigator.tsx +191 -0
  15. package/components/Studio/VRTStudioModule.ts +40 -0
  16. package/components/Studio/domain/animationRegistry.ts +86 -0
  17. package/components/Studio/domain/collisionBindingsRuntime.ts +93 -0
  18. package/components/Studio/domain/collisionPairKey.ts +15 -0
  19. package/components/Studio/domain/dragConfiguration.ts +48 -0
  20. package/components/Studio/domain/materialConfig.ts +276 -0
  21. package/components/Studio/domain/physicsConfig.ts +204 -0
  22. package/components/Studio/domain/sceneNavigationHandler.ts +150 -0
  23. package/components/Studio/domain/studioMaterials.ts +33 -0
  24. package/components/Studio/domain/triggerImageRegistry.ts +64 -0
  25. package/components/Studio/domain/useStudioShaderTimeUniforms.ts +51 -0
  26. package/components/Studio/domain/useStudioShaderViewportUniforms.ts +52 -0
  27. package/components/Studio/domain/viroNodeFactory.tsx +323 -0
  28. package/components/Studio/index.ts +18 -0
  29. package/components/Studio/types.ts +164 -0
  30. package/components/Types/ViroEvents.ts +53 -0
  31. package/components/Utilities/VRModuleOpenXR.ts +50 -0
  32. package/components/Utilities/VRQuestNavigatorBridge.ts +168 -0
  33. package/components/Utilities/ViroPlatform.ts +52 -0
  34. package/components/Utilities/ViroUtils.tsx +48 -0
  35. package/components/Utilities/ViroVersion.ts +1 -1
  36. package/components/Utilities/useAnySourceHover.ts +55 -0
  37. package/components/Utilities/useAnySourcePressed.ts +70 -0
  38. package/components/Viro360Image.tsx +7 -0
  39. package/components/ViroQuestEntryPoint.tsx +79 -0
  40. package/components/ViroVRSceneNavigator.tsx +44 -19
  41. package/components/ViroXRSceneNavigator.tsx +217 -0
  42. package/components/VisionOS/ViroVisionOSModule.ts +93 -0
  43. package/dist/components/AR/ViroARCamera.d.ts +1 -1
  44. package/dist/components/AR/ViroARCamera.js +5 -0
  45. package/dist/components/AR/ViroARImageMarker.d.ts +1 -1
  46. package/dist/components/AR/ViroARImageMarker.js +5 -0
  47. package/dist/components/AR/ViroARObjectMarker.d.ts +1 -1
  48. package/dist/components/AR/ViroARObjectMarker.js +5 -0
  49. package/dist/components/AR/ViroARPlane.d.ts +1 -1
  50. package/dist/components/AR/ViroARPlane.js +5 -0
  51. package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
  52. package/dist/components/AR/ViroARPlaneSelector.js +5 -0
  53. package/dist/components/AR/ViroARScene.d.ts +1 -1
  54. package/dist/components/AR/ViroARScene.js +5 -0
  55. package/dist/components/AR/ViroARSceneNavigator.d.ts +36 -0
  56. package/dist/components/AR/ViroARSceneNavigator.js +41 -0
  57. package/dist/components/AR/ViroCommonProps.d.ts +11 -0
  58. package/dist/components/Material/ViroMaterials.d.ts +12 -0
  59. package/dist/components/Material/ViroMaterials.js +25 -0
  60. package/dist/components/ReactVisionClient.d.ts +25 -0
  61. package/dist/components/ReactVisionClient.js +11 -0
  62. package/dist/components/Studio/StudioARScene.d.ts +15 -0
  63. package/dist/components/Studio/StudioARScene.js +299 -0
  64. package/dist/components/Studio/StudioSceneNavigator.d.ts +31 -0
  65. package/dist/components/Studio/StudioSceneNavigator.js +174 -0
  66. package/dist/components/Studio/VRTStudioModule.d.ts +15 -0
  67. package/dist/components/Studio/VRTStudioModule.js +31 -0
  68. package/dist/components/Studio/domain/animationRegistry.d.ts +11 -0
  69. package/dist/components/Studio/domain/animationRegistry.js +67 -0
  70. package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +21 -0
  71. package/dist/components/Studio/domain/collisionBindingsRuntime.js +54 -0
  72. package/dist/components/Studio/domain/collisionPairKey.d.ts +8 -0
  73. package/dist/components/Studio/domain/collisionPairKey.js +15 -0
  74. package/dist/components/Studio/domain/dragConfiguration.d.ts +20 -0
  75. package/dist/components/Studio/domain/dragConfiguration.js +37 -0
  76. package/dist/components/Studio/domain/materialConfig.d.ts +56 -0
  77. package/dist/components/Studio/domain/materialConfig.js +239 -0
  78. package/dist/components/Studio/domain/physicsConfig.d.ts +69 -0
  79. package/dist/components/Studio/domain/physicsConfig.js +165 -0
  80. package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +12 -0
  81. package/dist/components/Studio/domain/sceneNavigationHandler.js +112 -0
  82. package/dist/components/Studio/domain/studioMaterials.d.ts +6 -0
  83. package/dist/components/Studio/domain/studioMaterials.js +30 -0
  84. package/dist/components/Studio/domain/triggerImageRegistry.d.ts +13 -0
  85. package/dist/components/Studio/domain/triggerImageRegistry.js +47 -0
  86. package/dist/components/Studio/domain/useStudioShaderTimeUniforms.d.ts +6 -0
  87. package/dist/components/Studio/domain/useStudioShaderTimeUniforms.js +48 -0
  88. package/dist/components/Studio/domain/useStudioShaderViewportUniforms.d.ts +6 -0
  89. package/dist/components/Studio/domain/useStudioShaderViewportUniforms.js +48 -0
  90. package/dist/components/Studio/domain/viroNodeFactory.d.ts +28 -0
  91. package/dist/components/Studio/domain/viroNodeFactory.js +193 -0
  92. package/dist/components/Studio/index.d.ts +3 -0
  93. package/dist/components/Studio/index.js +7 -0
  94. package/dist/components/Studio/types.d.ts +149 -0
  95. package/dist/components/Studio/types.js +4 -0
  96. package/dist/components/Types/ViroEvents.d.ts +49 -1
  97. package/dist/components/Types/ViroEvents.js +1 -0
  98. package/dist/components/Utilities/VRModuleOpenXR.d.ts +32 -0
  99. package/dist/components/Utilities/VRModuleOpenXR.js +44 -0
  100. package/dist/components/Utilities/VRQuestNavigatorBridge.d.ts +85 -0
  101. package/dist/components/Utilities/VRQuestNavigatorBridge.js +124 -0
  102. package/dist/components/Utilities/ViroPlatform.d.ts +10 -0
  103. package/dist/components/Utilities/ViroPlatform.js +43 -0
  104. package/dist/components/Utilities/ViroUtils.d.ts +19 -0
  105. package/dist/components/Utilities/ViroUtils.js +34 -0
  106. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  107. package/dist/components/Utilities/ViroVersion.js +1 -1
  108. package/dist/components/Utilities/useAnySourceHover.d.ts +36 -0
  109. package/dist/components/Utilities/useAnySourceHover.js +48 -0
  110. package/dist/components/Utilities/useAnySourcePressed.d.ts +37 -0
  111. package/dist/components/Utilities/useAnySourcePressed.js +61 -0
  112. package/dist/components/Viro360Image.d.ts +7 -0
  113. package/dist/components/ViroQuestEntryPoint.d.ts +13 -0
  114. package/dist/components/ViroQuestEntryPoint.js +104 -0
  115. package/dist/components/ViroVRSceneNavigator.d.ts +24 -10
  116. package/dist/components/ViroVRSceneNavigator.js +21 -18
  117. package/dist/components/ViroXRSceneNavigator.d.ts +54 -0
  118. package/dist/components/ViroXRSceneNavigator.js +173 -0
  119. package/dist/components/VisionOS/ViroVisionOSModule.d.ts +65 -0
  120. package/dist/components/VisionOS/ViroVisionOSModule.js +91 -0
  121. package/dist/index.d.ts +16 -3
  122. package/dist/index.js +34 -2
  123. package/dist/plugins/withViro.d.ts +28 -1
  124. package/dist/plugins/withViroAndroid.js +312 -7
  125. package/dist/plugins/withViroIos.js +17 -8
  126. package/dist/plugins/withViroVisionOS.d.ts +24 -0
  127. package/dist/plugins/withViroVisionOS.js +265 -0
  128. package/index.ts +66 -0
  129. package/ios/ViroReact.podspec +15 -5
  130. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
  131. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
  132. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSession.h +30 -1
  133. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSessioniOS.h +16 -0
  134. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROGLTFLoader.h +34 -0
  135. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROInputControllerBase.h +74 -0
  136. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROInputType.h +11 -3
  137. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMaterial.h +29 -0
  138. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMorpher.h +4 -0
  139. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROPlatformUtil.h +13 -0
  140. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROPortal.h +17 -0
  141. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VRORenderContext.h +41 -0
  142. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VRORenderer.h +23 -0
  143. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROSemantics.h +14 -0
  144. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROViewAR.h +11 -0
  145. package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
  146. package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
  147. package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
  148. package/ios/dist/ViroRenderer/ViroKit.podspec +5 -0
  149. package/ios/dist/include/VRT360Image.h +1 -0
  150. package/ios/dist/include/VRTARSceneNavigator.h +7 -0
  151. package/ios/dist/include/VRTStudioModule.h +6 -0
  152. package/ios/dist/lib/libViroReact.a +0 -0
  153. package/package.json +8 -8
@@ -0,0 +1,150 @@
1
+ import { Alert } from "react-native";
2
+ import { isQuest } from "../../Utilities/ViroPlatform";
3
+ import { StudioAnimation, StudioSceneFunction, StudioSceneResponse } from "../types";
4
+ import { VRTStudioModule } from "../VRTStudioModule";
5
+
6
+ type SceneNavigator = any; // ViroARSceneNavigator navigator object passed to AR scenes
7
+
8
+ const ANIMATION_CHAIN_MAX_DEPTH = 10;
9
+
10
+ /**
11
+ * Resolves a scene function by ID from a flat list.
12
+ */
13
+ function resolveById(
14
+ id: string,
15
+ fns: StudioSceneFunction[]
16
+ ): StudioSceneFunction | undefined {
17
+ return fns.find((f) => f.id === id);
18
+ }
19
+
20
+ /**
21
+ * Looks up target_asset_id for an ANIMATION-type scene function.
22
+ * The inline scene_animation only has the animation UUID — we resolve it
23
+ * from the top-level animations array.
24
+ */
25
+ function resolveAnimationTargetAssetId(
26
+ animationId: string,
27
+ animations: StudioAnimation[]
28
+ ): string | undefined {
29
+ return animations.find((a) => a.id === animationId)?.target_asset_id;
30
+ }
31
+
32
+ /**
33
+ * Single dispatcher for all scene function types.
34
+ * Used by onClick, onCollision, and on_load_function triggers.
35
+ */
36
+ export function executeFunctionWithRelations(
37
+ fn: StudioSceneFunction,
38
+ sceneNavigator: SceneNavigator | undefined,
39
+ animations: StudioAnimation[],
40
+ onAnimationTrigger?: (targetAssetId: string, animationKey: string) => void,
41
+ depth = 0,
42
+ onSceneChange?: (sceneId: string, sceneName: string) => void,
43
+ ): void {
44
+ if (depth > ANIMATION_CHAIN_MAX_DEPTH) {
45
+ console.warn(
46
+ `[Studio] Max animation chain depth (${ANIMATION_CHAIN_MAX_DEPTH}) exceeded for function ${fn.id}.`
47
+ );
48
+ return;
49
+ }
50
+
51
+ if (fn.function_type === "NAVIGATION") {
52
+ const nav = fn.scene_navigation;
53
+ if (!nav?.navigate_to || !sceneNavigator) return;
54
+ void navigateToScene(sceneNavigator, nav.navigate_to, animations, onSceneChange);
55
+ } else if (fn.function_type === "ALERT") {
56
+ const alrt = fn.scene_alert;
57
+ if (!alrt) return;
58
+ if (isQuest) {
59
+ // Alert.alert shows a 2D panel dialog — invisible in the VR compositor.
60
+ // Log it so it's not silently swallowed; in-scene VR alert UI is a TODO.
61
+ console.warn(
62
+ `[Studio] Alert (Quest — not shown in VR): "${alrt.alert_title}" — ${alrt.alert_message}`
63
+ );
64
+ return;
65
+ }
66
+ Alert.alert(alrt.alert_title ?? "Alert", alrt.alert_message ?? "", [
67
+ { text: "OK", style: "default" },
68
+ ]);
69
+ } else if (fn.function_type === "ANIMATION") {
70
+ const anim = fn.scene_animation;
71
+ if (!anim || !onAnimationTrigger) return;
72
+
73
+ const animLookupId = fn.animation ?? anim.id;
74
+ const targetAssetId = resolveAnimationTargetAssetId(animLookupId, animations);
75
+ if (!targetAssetId) {
76
+ console.warn(
77
+ `[Studio] ANIMATION function ${fn.id}: could not resolve target_asset_id for animation ${anim.id}`
78
+ );
79
+ return;
80
+ }
81
+ onAnimationTrigger(targetAssetId, anim.animation_key);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Executes the scene's on_load_function if set.
87
+ */
88
+ export function executeOnLoadFunction(
89
+ functionId: string,
90
+ functions: StudioSceneFunction[],
91
+ sceneNavigator: SceneNavigator | undefined,
92
+ animations: StudioAnimation[],
93
+ onAnimationTrigger?: (targetAssetId: string, animationKey: string) => void,
94
+ onSceneChange?: (sceneId: string, sceneName: string) => void,
95
+ ): void {
96
+ const fn = resolveById(functionId, functions);
97
+ if (!fn) {
98
+ console.warn(`[Studio] on_load_function ${functionId} not found.`);
99
+ return;
100
+ }
101
+ executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimationTrigger, 0, onSceneChange);
102
+ }
103
+
104
+ /**
105
+ * Navigates to a new AR scene by fetching its data via rvGetScene and
106
+ * pushing it onto the ViroARSceneNavigator stack.
107
+ *
108
+ * The sceneNavigator object exposes rvGetScene as a method — no separate
109
+ * API client needed here.
110
+ */
111
+ async function navigateToScene(
112
+ sceneNavigator: SceneNavigator,
113
+ targetSceneId: string,
114
+ currentAnimations: StudioAnimation[],
115
+ onSceneChange?: (sceneId: string, sceneName: string) => void,
116
+ ): Promise<void> {
117
+ if (!sceneNavigator) {
118
+ console.error("[Studio] SceneNavigator not available for navigation");
119
+ Alert.alert("Navigation Error", "Unable to navigate to scene");
120
+ return;
121
+ }
122
+
123
+ console.log(`[Studio] Navigating to scene: ${targetSceneId}`);
124
+
125
+ try {
126
+ const result = await VRTStudioModule.rvGetScene(targetSceneId);
127
+ if (!result?.success) {
128
+ throw new Error(result?.error ?? "rvGetScene failed");
129
+ }
130
+
131
+ const sceneData: StudioSceneResponse = JSON.parse(result.data!);
132
+
133
+ // Lazy import to avoid circular dependency
134
+ const { StudioARScene } = require("../StudioARScene");
135
+
136
+ sceneNavigator.push({
137
+ scene: StudioARScene,
138
+ passProps: {
139
+ sceneData,
140
+ onSceneChange,
141
+ },
142
+ });
143
+
144
+ onSceneChange?.(targetSceneId, sceneData.scene.name ?? targetSceneId);
145
+ console.log(`[Studio] Navigated to scene: ${sceneData.scene.name}`);
146
+ } catch (error) {
147
+ console.error("[Studio] Error navigating to scene:", error);
148
+ Alert.alert("Navigation Error", "Failed to load scene");
149
+ }
150
+ }
@@ -0,0 +1,33 @@
1
+ import { ViroMaterials } from "../../Material/ViroMaterials";
2
+ import { StudioAsset } from "../types";
3
+ import {
4
+ buildViroMaterialDefinition,
5
+ parseMaterialConfig,
6
+ studioMaterialName,
7
+ } from "./materialConfig";
8
+
9
+ /**
10
+ * Registers Viro materials for scene assets that have a valid `material_config`.
11
+ * Call synchronously after fetching assets and before rendering AR nodes.
12
+ */
13
+ export function registerStudioMaterialsForAssets(assets: StudioAsset[]): void {
14
+ const materials: Record<string, ReturnType<typeof buildViroMaterialDefinition>> = {};
15
+
16
+ for (const asset of assets) {
17
+ if (asset.asset_type_name !== "3D-MODEL") continue;
18
+ if (asset.material_config == null) continue;
19
+
20
+ const config = parseMaterialConfig(asset.material_config);
21
+ if (!config) continue;
22
+
23
+ materials[studioMaterialName(asset.id)] = buildViroMaterialDefinition(config);
24
+ }
25
+
26
+ if (Object.keys(materials).length === 0) return;
27
+
28
+ try {
29
+ ViroMaterials.createMaterials(materials as any);
30
+ } catch (err) {
31
+ console.error("[studioMaterials] ViroMaterials.createMaterials failed", err);
32
+ }
33
+ }
@@ -0,0 +1,64 @@
1
+ import { ViroARTrackingTargets } from "../../AR/ViroARTrackingTargets";
2
+ import { StudioAsset } from "../types";
3
+
4
+ const DEFAULT_PHYSICAL_WIDTH = 0.2; // meters
5
+ const DEFAULT_ORIENTATION = "Up" as const;
6
+
7
+ /**
8
+ * Registers trigger image targets with ViroReact for image recognition.
9
+ * One target per asset with trigger_image_url.
10
+ * Must be called before rendering ViroARImageMarker components.
11
+ *
12
+ * @returns Map from trigger_image_url → target name for lookup in ViroARImageMarker
13
+ */
14
+ export function registerTriggerImageTargets(
15
+ assets: StudioAsset[]
16
+ ): Map<string, string> {
17
+ const assetsWithTrigger = assets.filter(
18
+ (a): a is StudioAsset & { trigger_image_url: string } =>
19
+ !!a.trigger_image_url
20
+ );
21
+
22
+ if (assetsWithTrigger.length === 0) {
23
+ return new Map();
24
+ }
25
+
26
+ const urlToTargetName = new Map<string, string>();
27
+ const targets: Record<
28
+ string,
29
+ {
30
+ source: { uri: string };
31
+ orientation: string;
32
+ physicalWidth: number;
33
+ type: string;
34
+ }
35
+ > = {};
36
+
37
+ assetsWithTrigger.forEach((asset, index) => {
38
+ const targetName = `studio-trigger-${index}`;
39
+ urlToTargetName.set(asset.trigger_image_url, targetName);
40
+ targets[targetName] = {
41
+ source: { uri: asset.trigger_image_url },
42
+ orientation: asset.trigger_image_orientation ?? DEFAULT_ORIENTATION,
43
+ physicalWidth:
44
+ asset.trigger_image_physical_width_m ?? DEFAULT_PHYSICAL_WIDTH,
45
+ type: "Image",
46
+ };
47
+ });
48
+
49
+ ViroARTrackingTargets.createTargets(targets);
50
+ return urlToTargetName;
51
+ }
52
+
53
+ /**
54
+ * Cleans up trigger image targets when the scene unmounts.
55
+ */
56
+ export function cleanupTriggerImageTargets(targetNames: string[]): void {
57
+ targetNames.forEach((name) => {
58
+ try {
59
+ ViroARTrackingTargets.deleteTarget(name);
60
+ } catch (error) {
61
+ console.warn(`[Studio] Failed to delete trigger target "${name}":`, error);
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,51 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { ViroMaterials } from "../../Material/ViroMaterials";
3
+ import { StudioAsset } from "../types";
4
+ import {
5
+ materialConfigNeedsTimeUniform,
6
+ parseMaterialConfig,
7
+ studioMaterialName,
8
+ } from "./materialConfig";
9
+
10
+ /** ~60fps — matches Viro starter kit / shader docs. */
11
+ const TIME_TICK_MS = 16;
12
+
13
+ function computeTimeUniformPayload(assets: StudioAsset[]): { key: string; names: string[] } {
14
+ const names: string[] = [];
15
+ for (const asset of assets) {
16
+ if (asset.asset_type_name !== "3D-MODEL") continue;
17
+ if (asset.material_config == null) continue;
18
+ const config = parseMaterialConfig(asset.material_config);
19
+ if (!config || !materialConfigNeedsTimeUniform(config)) continue;
20
+ names.push(studioMaterialName(asset.id));
21
+ }
22
+ names.sort();
23
+ return { key: names.join("|"), names };
24
+ }
25
+
26
+ /**
27
+ * Drives the `time` shader uniform for materials that use animated presets.
28
+ * Uses `setInterval(16)` and `Date.now() % 1000000` like working Viro starter kits.
29
+ */
30
+ export function useStudioShaderTimeUniforms(assets: StudioAsset[]): void {
31
+ const payload = useMemo(() => computeTimeUniformPayload(assets), [assets]);
32
+
33
+ const payloadRef = useRef(payload);
34
+ payloadRef.current = payload;
35
+
36
+ const applyTimeUniforms = useCallback(() => {
37
+ const { names } = payloadRef.current;
38
+ if (names.length === 0) return;
39
+ const time = Date.now() % 1_000_000;
40
+ for (const name of names) {
41
+ ViroMaterials.updateShaderUniform(name, "time", "float", time);
42
+ }
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ if (payload.names.length === 0) return;
47
+ const id = setInterval(applyTimeUniforms, TIME_TICK_MS);
48
+ applyTimeUniforms();
49
+ return () => clearInterval(id);
50
+ }, [payload.key, applyTimeUniforms]);
51
+ }
@@ -0,0 +1,52 @@
1
+ import { useCallback, useEffect, useMemo } from "react";
2
+ import { Dimensions, PixelRatio } from "react-native";
3
+ import { ViroMaterials } from "../../Material/ViroMaterials";
4
+ import { StudioAsset } from "../types";
5
+ import {
6
+ materialConfigNeedsViewportUniforms,
7
+ parseMaterialConfig,
8
+ studioMaterialName,
9
+ } from "./materialConfig";
10
+
11
+ function computeViewportPayload(assets: StudioAsset[]): {
12
+ key: string;
13
+ names: string[];
14
+ } {
15
+ const names: string[] = [];
16
+ for (const asset of assets) {
17
+ if (asset.asset_type_name !== "3D-MODEL") continue;
18
+ if (asset.material_config == null) continue;
19
+ const config = parseMaterialConfig(asset.material_config);
20
+ if (!config || !materialConfigNeedsViewportUniforms(config)) continue;
21
+ names.push(studioMaterialName(asset.id));
22
+ }
23
+ names.sort();
24
+ return { key: names.join("|"), names };
25
+ }
26
+
27
+ /**
28
+ * Pushes _rf_vpw / _rf_vph (physical pixel dimensions) to materials that sample the camera
29
+ * feed via gl_FragCoord. Called on mount and whenever the screen dimensions change.
30
+ */
31
+ export function useStudioShaderViewportUniforms(assets: StudioAsset[]): void {
32
+ const payload = useMemo(() => computeViewportPayload(assets), [assets]);
33
+
34
+ const pushViewport = useCallback(() => {
35
+ if (payload.names.length === 0) return;
36
+ const { width, height } = Dimensions.get("screen");
37
+ const pr = PixelRatio.get();
38
+ const pw = width * pr;
39
+ const ph = height * pr;
40
+ for (const name of payload.names) {
41
+ ViroMaterials.updateShaderUniform(name, "_rf_vpw", "float", pw);
42
+ ViroMaterials.updateShaderUniform(name, "_rf_vph", "float", ph);
43
+ }
44
+ }, [payload]);
45
+
46
+ useEffect(() => {
47
+ if (payload.names.length === 0) return;
48
+ pushViewport();
49
+ const sub = Dimensions.addEventListener("change", pushViewport);
50
+ return () => sub.remove();
51
+ }, [payload.key, pushViewport]);
52
+ }
@@ -0,0 +1,323 @@
1
+ import * as React from "react";
2
+ import { Platform } from "react-native";
3
+ import { Viro3DObject } from "../../Viro3DObject";
4
+ import { ViroImage } from "../../ViroImage";
5
+ import { ViroText } from "../../ViroText";
6
+ import { ViroVideo } from "../../ViroVideo";
7
+ import { StudioAnimation, StudioAsset, StudioSceneMeta, ViroAnimationProp } from "../types";
8
+ import { executeFunctionWithRelations } from "./sceneNavigationHandler";
9
+ import { parseMaterialConfig, studioMaterialName } from "./materialConfig";
10
+ import { DragConfiguration } from "./dragConfiguration";
11
+ import { buildViroPhysicsBody, parsePhysicsBodyConfig } from "./physicsConfig";
12
+
13
+ type SceneNavigator = any;
14
+
15
+ export type NodeConfig = {
16
+ position: [number, number, number];
17
+ rotation: [number, number, number];
18
+ scale: [number, number, number];
19
+ dragType?: "FixedDistance" | "FixedDistanceOrigin" | "FixedToWorld" | "FixedToPlane";
20
+ dragPlane?: { planePoint: [number, number, number]; planeNormal: [number, number, number]; maxDistance: number };
21
+ physicsBody?: Record<string, unknown>;
22
+ viroTag?: string;
23
+ onClick?: () => void;
24
+ animation?: ViroAnimationProp;
25
+ };
26
+
27
+ /**
28
+ * Derives the transform config for an asset.
29
+ * Clamps Z to -2 for non-trigger assets to guarantee visibility.
30
+ */
31
+ export function createNodeConfig(
32
+ asset: StudioAsset,
33
+ sceneNavigator: SceneNavigator | undefined,
34
+ animations: StudioAnimation[],
35
+ scene: StudioSceneMeta | null,
36
+ onAnimationTrigger?: (targetAssetId: string, animKey: string) => void,
37
+ animationStates?: Record<string, ViroAnimationProp>,
38
+ onSceneChange?: (sceneId: string, sceneName: string) => void
39
+ ): NodeConfig {
40
+ const hasTriggerImage = !!asset.trigger_image_url;
41
+
42
+ let posZ = asset.position_z ?? -2;
43
+ if (!hasTriggerImage && posZ > -0.5) {
44
+ console.warn(
45
+ `[Studio/NodeFactory] Asset "${asset.name}" Z=${posZ} too close, clamping to -2`
46
+ );
47
+ posZ = -2;
48
+ }
49
+
50
+ const position: [number, number, number] = [
51
+ asset.position_x ?? 0,
52
+ asset.position_y ?? 0,
53
+ posZ,
54
+ ];
55
+
56
+ // Apply trigger image orientation offset to rotation Z
57
+ let rotationZ = asset.rotation_z ?? 0;
58
+ if (hasTriggerImage && asset.trigger_image_orientation) {
59
+ const offsets: Record<string, number> = {
60
+ Left: -90,
61
+ Right: 90,
62
+ Down: 180,
63
+ Up: 0,
64
+ };
65
+ rotationZ += offsets[asset.trigger_image_orientation] ?? 0;
66
+ }
67
+
68
+ const rotation: [number, number, number] = [
69
+ asset.rotation_x ?? 0,
70
+ asset.rotation_y ?? 0,
71
+ rotationZ,
72
+ ];
73
+
74
+ let scaleValue = asset.scale ?? 1;
75
+ if (scaleValue < 0.01) scaleValue = 0.1;
76
+ if (scaleValue > 10) scaleValue = 2;
77
+ const scale: [number, number, number] = [scaleValue, scaleValue, scaleValue];
78
+
79
+ const dragType = DragConfiguration.getDragType(asset, scene);
80
+
81
+ let dragPlane: NodeConfig["dragPlane"];
82
+ if (dragType === "FixedToPlane") {
83
+ dragPlane = DragConfiguration.getDragPlane(
84
+ scene?.plane_direction ?? "Horizontal",
85
+ position,
86
+ );
87
+ }
88
+
89
+ const parsedPhysics = parsePhysicsBodyConfig(asset.physics_config);
90
+ const physicsBody = parsedPhysics ? buildViroPhysicsBody(parsedPhysics) : undefined;
91
+ const viroTag = parsedPhysics ? asset.id : undefined;
92
+
93
+ const onClick = createOnClickHandler(
94
+ asset,
95
+ sceneNavigator,
96
+ animations,
97
+ onAnimationTrigger,
98
+ onSceneChange
99
+ );
100
+
101
+ const animation = animationStates?.[asset.id];
102
+
103
+ return { position, rotation, scale, dragType, dragPlane, physicsBody, viroTag, onClick, animation };
104
+ }
105
+
106
+ function createOnClickHandler(
107
+ asset: StudioAsset,
108
+ sceneNavigator: SceneNavigator | undefined,
109
+ animations: StudioAnimation[],
110
+ onAnimationTrigger?: (targetAssetId: string, animKey: string) => void,
111
+ onSceneChange?: (sceneId: string, sceneName: string) => void
112
+ ): (() => void) | undefined {
113
+ const fn = asset.scene_function;
114
+ if (!fn) return undefined;
115
+
116
+ if (fn.function_type === "NAVIGATION" && !fn.scene_navigation?.navigate_to) {
117
+ console.warn(`[Studio] Asset "${asset.name}" has NAVIGATION but no target scene`);
118
+ return undefined;
119
+ }
120
+ if (fn.function_type === "ALERT" && !fn.scene_alert) {
121
+ console.warn(`[Studio] Asset "${asset.name}" has ALERT but no alert data`);
122
+ return undefined;
123
+ }
124
+ if (fn.function_type === "ANIMATION" && !fn.scene_animation) {
125
+ console.warn(`[Studio] Asset "${asset.name}" has ANIMATION but no animation data`);
126
+ return undefined;
127
+ }
128
+
129
+ return () =>
130
+ executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimationTrigger, 0, onSceneChange);
131
+ }
132
+
133
+ /** Resolves asset type from asset_type_name. */
134
+ function resolveType(
135
+ asset: StudioAsset
136
+ ): "3D-MODEL" | "TEXT" | "IMAGE" | "VIDEO" | null {
137
+ return asset.asset_type_name ?? null;
138
+ }
139
+
140
+ /** Infers 3D model format from file extension. */
141
+ function inferModelType(url: string): "GLB" | "GLTF" | "OBJ" | "VRX" {
142
+ const ext = url.toLowerCase().split(".").pop();
143
+ if (ext === "gltf") return "GLTF";
144
+ if (ext === "obj") return "OBJ";
145
+ if (ext === "vrx") return "VRX";
146
+ return "GLB";
147
+ }
148
+
149
+ function create3DObject(
150
+ asset: StudioAsset,
151
+ config: NodeConfig,
152
+ onAssetLoaded?: (id: string) => void,
153
+ onCollision?: (viroTag: string, collidedPoint: [number, number, number], collidedNormal: [number, number, number]) => void
154
+ ): React.ReactElement | null {
155
+ if (!asset.file_url) {
156
+ console.warn(`[Studio] 3D model "${asset.name}" has no file_url`);
157
+ return null;
158
+ }
159
+
160
+ const modelType = inferModelType(asset.file_url);
161
+
162
+ // Android: slightly reduce scale for stability
163
+ const scale =
164
+ Platform.OS === "android"
165
+ ? ([
166
+ config.scale[0] * 0.8,
167
+ config.scale[1] * 0.8,
168
+ config.scale[2] * 0.8,
169
+ ] as [number, number, number])
170
+ : config.scale;
171
+
172
+ const hasMaterialConfig = parseMaterialConfig(asset.material_config) !== null;
173
+ const shaderOverrides = hasMaterialConfig ? [studioMaterialName(asset.id)] : undefined;
174
+
175
+ return (
176
+ <Viro3DObject
177
+ key={asset.id}
178
+ source={{ uri: asset.file_url }}
179
+ position={config.position}
180
+ rotation={config.rotation}
181
+ scale={scale}
182
+ type={modelType}
183
+ dragType={config.dragType}
184
+ dragPlane={config.dragPlane}
185
+ animation={config.animation as any}
186
+ onClick={config.onClick}
187
+ renderingOrder={Platform.OS === "android" ? 1 : 0}
188
+ onLoadEnd={() => onAssetLoaded?.(asset.id)}
189
+ onError={(e) =>
190
+ console.error(`[Studio] 3D model "${asset.name}" error:`, e)
191
+ }
192
+ // Viro derives native canDrag from `onDrag != undefined`; without this prop
193
+ // the drag recognizer is never attached, even when dragType is set.
194
+ {...(config.dragType ? { onDrag: () => {} } : {})}
195
+ {...(shaderOverrides ? { shaderOverrides } : {})}
196
+ {...(config.physicsBody ? { physicsBody: config.physicsBody as any, viroTag: config.viroTag } : {})}
197
+ {...(onCollision ? { onCollision: onCollision as any } : {})}
198
+ />
199
+ );
200
+ }
201
+
202
+ function createImage(
203
+ asset: StudioAsset,
204
+ config: NodeConfig,
205
+ onAssetLoaded?: (id: string) => void
206
+ ): React.ReactElement | null {
207
+ if (!asset.file_url) {
208
+ console.warn(`[Studio] Image "${asset.name}" has no file_url`);
209
+ return null;
210
+ }
211
+
212
+ return (
213
+ <ViroImage
214
+ key={asset.id}
215
+ source={{ uri: asset.file_url }}
216
+ position={config.position}
217
+ rotation={config.rotation}
218
+ scale={config.scale}
219
+ dragType={config.dragType}
220
+ animation={config.animation as any}
221
+ onClick={config.onClick}
222
+ onLoadEnd={() => onAssetLoaded?.(asset.id)}
223
+ onError={(e) =>
224
+ console.error(`[Studio] Image "${asset.name}" error:`, e)
225
+ }
226
+ {...(config.dragType ? { onDrag: () => {} } : {})}
227
+ />
228
+ );
229
+ }
230
+
231
+ function createText(
232
+ asset: StudioAsset,
233
+ config: NodeConfig
234
+ ): React.ReactElement {
235
+ return (
236
+ <ViroText
237
+ key={asset.id}
238
+ text={asset.name ?? ""}
239
+ position={config.position}
240
+ rotation={config.rotation}
241
+ scale={config.scale}
242
+ dragType={config.dragType}
243
+ animation={config.animation as any}
244
+ onClick={config.onClick}
245
+ style={{
246
+ fontFamily: "Arial",
247
+ fontSize: 20,
248
+ color: "#FFFFFF",
249
+ textAlign: "center",
250
+ }}
251
+ {...(config.dragType ? { onDrag: () => {} } : {})}
252
+ />
253
+ );
254
+ }
255
+
256
+ function createVideo(
257
+ asset: StudioAsset,
258
+ config: NodeConfig
259
+ ): React.ReactElement | null {
260
+ if (!asset.file_url) {
261
+ console.warn(`[Studio] Video "${asset.name}" has no file_url`);
262
+ return null;
263
+ }
264
+
265
+ return (
266
+ <ViroVideo
267
+ key={asset.id}
268
+ source={{ uri: asset.file_url }}
269
+ position={config.position}
270
+ rotation={config.rotation}
271
+ scale={config.scale}
272
+ dragType={config.dragType}
273
+ animation={config.animation as any}
274
+ onClick={config.onClick}
275
+ loop={true}
276
+ muted={false}
277
+ onError={(e) =>
278
+ console.error(`[Studio] Video "${asset.name}" error:`, e)
279
+ }
280
+ {...(config.dragType ? { onDrag: () => {} } : {})}
281
+ />
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Creates the appropriate Viro component for a StudioAsset.
287
+ */
288
+ export function createNode(
289
+ asset: StudioAsset,
290
+ sceneNavigator: SceneNavigator | undefined,
291
+ animations: StudioAnimation[],
292
+ scene: StudioSceneMeta | null,
293
+ onAnimationTrigger?: (targetAssetId: string, animKey: string) => void,
294
+ animationStates?: Record<string, ViroAnimationProp>,
295
+ onAssetLoaded?: (id: string) => void,
296
+ onCollision?: (viroTag: string, collidedPoint: [number, number, number], collidedNormal: [number, number, number]) => void,
297
+ onSceneChange?: (sceneId: string, sceneName: string) => void
298
+ ): React.ReactElement | null {
299
+ const type = resolveType(asset);
300
+ const config = createNodeConfig(
301
+ asset,
302
+ sceneNavigator,
303
+ animations,
304
+ scene,
305
+ onAnimationTrigger,
306
+ animationStates,
307
+ onSceneChange
308
+ );
309
+
310
+ switch (type) {
311
+ case "3D-MODEL":
312
+ return create3DObject(asset, config, onAssetLoaded, onCollision);
313
+ case "IMAGE":
314
+ return createImage(asset, config, onAssetLoaded);
315
+ case "TEXT":
316
+ return createText(asset, config);
317
+ case "VIDEO":
318
+ return createVideo(asset, config);
319
+ default:
320
+ console.warn(`[Studio] Unknown asset type "${type}" for "${asset.name}"`);
321
+ return null;
322
+ }
323
+ }
@@ -0,0 +1,18 @@
1
+ export { StudioARScene } from "./StudioARScene";
2
+ export { StudioSceneNavigator } from "./StudioSceneNavigator";
3
+ export type {
4
+ StudioAnimation,
5
+ StudioAsset,
6
+ StudioCollisionBinding,
7
+ StudioProjectApiResponse,
8
+ StudioProjectAsset,
9
+ StudioProjectMeta,
10
+ StudioProjectOpeningScene,
11
+ StudioProjectOverview,
12
+ StudioProjectSceneSummary,
13
+ StudioSceneCreatedBy,
14
+ StudioSceneFunction,
15
+ StudioSceneMeta,
16
+ StudioSceneResponse,
17
+ ViroAnimationProp,
18
+ } from "./types";