@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,93 @@
1
+ import { MutableRefObject } from "react";
2
+ import { StudioAnimation, StudioCollisionBinding } from "../types";
3
+ import { canonicalizeCollisionAssetIds, collisionPairKey } from "./collisionPairKey";
4
+ import { executeFunctionWithRelations } from "./sceneNavigationHandler";
5
+
6
+ const DEFAULT_COOLDOWN_MS = 750;
7
+
8
+ function pairCooldownKey(pairKey: string, functionId: string): string {
9
+ return `${pairKey}::${functionId}`;
10
+ }
11
+
12
+ /**
13
+ * Dispatches scene functions for collision bindings matching the canonical pair.
14
+ * Cooldown prevents per-frame spam while physics contacts overlap.
15
+ */
16
+ export function dispatchCollisionBindingActions(params: {
17
+ selfPlacementId: string;
18
+ otherTag: string;
19
+ bindingsByPairKey: Map<string, StudioCollisionBinding[]>;
20
+ sceneNavigator?: unknown;
21
+ animations: StudioAnimation[];
22
+ onSceneChange?: (sceneId: string, sceneName: string) => void;
23
+ onAnimationTrigger?: (targetAssetId: string, animationKey: string) => void;
24
+ cooldownMs?: number;
25
+ lastFiredRef: MutableRefObject<Map<string, number>>;
26
+ }): void {
27
+ const {
28
+ selfPlacementId,
29
+ otherTag,
30
+ bindingsByPairKey,
31
+ sceneNavigator,
32
+ animations,
33
+ onSceneChange,
34
+ onAnimationTrigger,
35
+ cooldownMs = DEFAULT_COOLDOWN_MS,
36
+ lastFiredRef,
37
+ } = params;
38
+
39
+ if (!otherTag) return;
40
+
41
+ const { asset_x_id, asset_y_id } = canonicalizeCollisionAssetIds(
42
+ selfPlacementId,
43
+ otherTag
44
+ );
45
+ const pKey = collisionPairKey(asset_x_id, asset_y_id);
46
+ const rows = bindingsByPairKey.get(pKey);
47
+ if (!rows?.length) return;
48
+
49
+ const now = Date.now();
50
+ const map = lastFiredRef.current;
51
+
52
+ for (const row of rows) {
53
+ const fn = row.scene_function;
54
+ if (!fn) continue;
55
+
56
+ const ck = pairCooldownKey(pKey, row.function_id);
57
+ const last = map.get(ck) ?? 0;
58
+ if (now - last < cooldownMs) continue;
59
+ map.set(ck, now);
60
+
61
+ executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimationTrigger, 0, onSceneChange);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Returns an onCollision handler for a given placement asset ID.
67
+ */
68
+ export function createPlacementCollisionHandler(
69
+ placementId: string,
70
+ bindingsByPairKey: Map<string, StudioCollisionBinding[]>,
71
+ sceneNavigator: unknown,
72
+ animations: StudioAnimation[],
73
+ lastFiredRef: MutableRefObject<Map<string, number>>,
74
+ onAnimationTrigger?: (targetAssetId: string, animationKey: string) => void,
75
+ onSceneChange?: (sceneId: string, sceneName: string) => void,
76
+ ): (
77
+ viroTag: string,
78
+ collidedPoint: [number, number, number],
79
+ collidedNormal: [number, number, number]
80
+ ) => void {
81
+ return (viroTag) => {
82
+ dispatchCollisionBindingActions({
83
+ selfPlacementId: placementId,
84
+ otherTag: viroTag,
85
+ bindingsByPairKey,
86
+ sceneNavigator,
87
+ animations,
88
+ onSceneChange,
89
+ onAnimationTrigger,
90
+ lastFiredRef,
91
+ });
92
+ };
93
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Canonical collision pair — matches DB constraint where asset_x_id < asset_y_id lexicographically.
3
+ */
4
+ export function canonicalizeCollisionAssetIds(
5
+ a: string,
6
+ b: string
7
+ ): { asset_x_id: string; asset_y_id: string } {
8
+ return a < b
9
+ ? { asset_x_id: a, asset_y_id: b }
10
+ : { asset_x_id: b, asset_y_id: a };
11
+ }
12
+
13
+ export function collisionPairKey(assetX: string, assetY: string): string {
14
+ return `${assetX}|${assetY}`;
15
+ }
@@ -0,0 +1,48 @@
1
+ import { StudioAsset, StudioSceneMeta } from "../types";
2
+
3
+ export type DragType = "FixedToWorld" | "FixedToPlane" | undefined;
4
+
5
+ export type DragPlane = {
6
+ planePoint: [number, number, number];
7
+ planeNormal: [number, number, number];
8
+ maxDistance: number;
9
+ };
10
+
11
+ export class DragConfiguration {
12
+ /**
13
+ * Chooses FixedToPlane when the scene uses plane detection, FixedToWorld otherwise.
14
+ * Returns undefined if the asset is not draggable.
15
+ */
16
+ static getDragType(asset: StudioAsset, scene: StudioSceneMeta | null): DragType {
17
+ if (!asset.is_draggable) return undefined;
18
+
19
+ const planeDetection = ((scene?.plane_detection as string) ?? "NONE").toUpperCase();
20
+
21
+ if (planeDetection === "AUTOMATIC" || planeDetection === "MANUAL") {
22
+ return "FixedToPlane";
23
+ }
24
+ return "FixedToWorld";
25
+ }
26
+
27
+ /**
28
+ * Returns a drag plane that passes through the object's current position,
29
+ * preventing objects from jumping on drag start. maxDistance caps how far
30
+ * objects can travel from the camera.
31
+ */
32
+ static getDragPlane(
33
+ planeAlignment: string,
34
+ objectPosition: [number, number, number],
35
+ ): DragPlane {
36
+ switch (planeAlignment.toLowerCase()) {
37
+ case "horizontal":
38
+ case "horizontalupward":
39
+ return { planePoint: objectPosition, planeNormal: [0, 1, 0], maxDistance: 1.5 };
40
+ case "horizontaldownward":
41
+ return { planePoint: objectPosition, planeNormal: [0, -1, 0], maxDistance: 1.5 };
42
+ case "vertical":
43
+ return { planePoint: objectPosition, planeNormal: [0, 0, 1], maxDistance: 1.5 };
44
+ default:
45
+ return { planePoint: objectPosition, planeNormal: [0, 1, 0], maxDistance: 1.5 };
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Studio material_config parsing and Viro material definition building.
3
+ * Ported from studio-go/domain/materialConfig.ts — no zod dependency.
4
+ */
5
+
6
+ // ─── Types ────────────────────────────────────────────────────────────────────
7
+
8
+ type ShaderModifierStage = {
9
+ uniforms?: string;
10
+ body?: string;
11
+ varyings?: unknown;
12
+ requiresSceneDepth?: boolean;
13
+ requiresCameraTexture?: boolean;
14
+ priority?: unknown;
15
+ };
16
+
17
+ export type MaterialConfig = {
18
+ presetName?: string;
19
+ lightingModel: "Constant" | "Lambert" | "Blinn" | "Phong" | "PBR";
20
+ diffuseColor?: string;
21
+ roughness?: number;
22
+ metalness?: number;
23
+ shininess?: number;
24
+ alpha?: number;
25
+ blendMode?: string;
26
+ bloomThreshold?: number | null;
27
+ wrapS?: "Clamp" | "Repeat" | "Mirror";
28
+ wrapT?: "Clamp" | "Repeat" | "Mirror";
29
+ diffuseTexture?: string | null;
30
+ normalTexture?: string | null;
31
+ roughnessTexture?: string | null;
32
+ metalnessTexture?: string | null;
33
+ ambientOcclusionTexture?: string | null;
34
+ specularTexture?: string | null;
35
+ shaderModifiers?: Record<string, ShaderModifierStage | string>;
36
+ materialUniforms?: Array<{ name: string; type: string; value: unknown }>;
37
+ transparencyMode?: string;
38
+ cullMode?: string;
39
+ };
40
+
41
+ export type ViroMaterialDefinition = Record<string, unknown>;
42
+
43
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
44
+
45
+ const TEXTURE_KEYS = [
46
+ "diffuseTexture",
47
+ "normalTexture",
48
+ "roughnessTexture",
49
+ "metalnessTexture",
50
+ "ambientOcclusionTexture",
51
+ "specularTexture",
52
+ ] as const;
53
+
54
+ function textureToViro(uri: string | null | undefined): { uri: string } | undefined {
55
+ if (uri == null || uri === "") return undefined;
56
+ return { uri };
57
+ }
58
+
59
+ function collectShaderModifierStrings(config: MaterialConfig): string[] {
60
+ const mods = config.shaderModifiers;
61
+ if (!mods) return [];
62
+ const strings: string[] = [];
63
+ for (const stage of Object.values(mods)) {
64
+ if (typeof stage === "string") {
65
+ strings.push(stage);
66
+ } else if (stage && typeof stage === "object") {
67
+ const s = stage as ShaderModifierStage;
68
+ if (typeof s.uniforms === "string") strings.push(s.uniforms);
69
+ if (typeof s.body === "string") strings.push(s.body);
70
+ }
71
+ }
72
+ return strings;
73
+ }
74
+
75
+ const TIME_WORD_RE = /\btime\b/i;
76
+ const CAMERA_TEXTURE_RE = /\bcamera_texture\b/;
77
+ const RF_VIEWPORT_RE = /\b_rf_vpw\b|\b_rf_vph\b/;
78
+
79
+ export function materialConfigNeedsTimeUniform(config: MaterialConfig): boolean {
80
+ if (config.materialUniforms?.some((u) => u.name === "time")) return true;
81
+ return collectShaderModifierStrings(config).some((s) => TIME_WORD_RE.test(s));
82
+ }
83
+
84
+ /**
85
+ * True if the shader uses _rf_vpw/_rf_vph viewport uniforms.
86
+ * These must be pushed via ViroMaterials.updateShaderUniform on mount and orientation change.
87
+ */
88
+ export function materialConfigNeedsViewportUniforms(config: MaterialConfig): boolean {
89
+ return collectShaderModifierStrings(config).some((s) => RF_VIEWPORT_RE.test(s));
90
+ }
91
+
92
+ /**
93
+ * Prepends GLSL uniform declarations that the body references but the uniforms block omits.
94
+ * Required when shaders are authored for remote delivery without explicit sampler declarations.
95
+ */
96
+ function injectMissingGlslDeclarations(uniforms: string, body: string): string {
97
+ let result = uniforms;
98
+ if (CAMERA_TEXTURE_RE.test(body) && !CAMERA_TEXTURE_RE.test(result)) {
99
+ result =
100
+ "uniform sampler2D camera_texture;\nuniform highp mat4 camera_image_transform;\n" +
101
+ result;
102
+ }
103
+ if (RF_VIEWPORT_RE.test(body) && !/\b_rf_vpw\b/.test(result)) {
104
+ result = "uniform highp float _rf_vpw;\nuniform highp float _rf_vph;\n" + result;
105
+ }
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * iOS only applies `ViroMaterials.updateShaderUniform` to uniforms registered
111
+ * via `materialUniforms`. If a shader references `time` or viewport uniforms in
112
+ * GLSL only, we add the runtime binding here.
113
+ */
114
+ function mergeMaterialUniformsForViro(
115
+ config: MaterialConfig,
116
+ ): Array<{ name: string; type: string; value: unknown }> | undefined {
117
+ const list = config.materialUniforms
118
+ ? config.materialUniforms.map((u) => ({ name: u.name, type: u.type, value: u.value }))
119
+ : [];
120
+
121
+ if (materialConfigNeedsTimeUniform(config) && !list.some((u) => u.name === "time")) {
122
+ list.push({ name: "time", type: "float", value: 0 });
123
+ }
124
+
125
+ if (materialConfigNeedsViewportUniforms(config)) {
126
+ if (!list.some((u) => u.name === "_rf_vpw"))
127
+ list.push({ name: "_rf_vpw", type: "float", value: 0 });
128
+ if (!list.some((u) => u.name === "_rf_vph"))
129
+ list.push({ name: "_rf_vph", type: "float", value: 0 });
130
+ }
131
+
132
+ return list.length > 0 ? list : undefined;
133
+ }
134
+
135
+ /**
136
+ * Studio stores modifiers as `{ uniforms, body }`; Viro works best with a single
137
+ * GLSL string per stage. Stages with advanced fields are kept as structured objects.
138
+ * Camera texture usage is detected from GLSL and auto-flagged so the Viro native
139
+ * layer binds the camera feed even when the DB JSON omits `requiresCameraTexture`.
140
+ */
141
+ function normalizeShaderModifiersForViro(
142
+ mods: NonNullable<MaterialConfig["shaderModifiers"]>,
143
+ ): Record<string, string | object> {
144
+ const out: Record<string, string | object> = {};
145
+ for (const [key, stage] of Object.entries(mods)) {
146
+ if (typeof stage === "string") {
147
+ out[key] = stage;
148
+ continue;
149
+ }
150
+ if (stage && typeof stage === "object") {
151
+ const s = stage as ShaderModifierStage;
152
+ const uniforms = typeof s.uniforms === "string" ? s.uniforms : "";
153
+ const body = typeof s.body === "string" ? s.body : "";
154
+
155
+ // Detect camera texture usage in GLSL even when the flag is absent from DB JSON.
156
+ const usesCameraTexture =
157
+ CAMERA_TEXTURE_RE.test(body) || CAMERA_TEXTURE_RE.test(uniforms);
158
+
159
+ const hasAdvanced =
160
+ s.varyings != null ||
161
+ s.requiresSceneDepth === true ||
162
+ s.requiresCameraTexture === true ||
163
+ usesCameraTexture ||
164
+ s.priority != null;
165
+
166
+ if (hasAdvanced) {
167
+ if (usesCameraTexture && s.requiresCameraTexture !== true) {
168
+ // Auto-fix: flag the stage and inject any missing GLSL declarations so the
169
+ // native Viro layer binds camera_texture and the shader compiles cleanly.
170
+ out[key] = {
171
+ ...s,
172
+ requiresCameraTexture: true,
173
+ uniforms: injectMissingGlslDeclarations(uniforms, body),
174
+ };
175
+ } else {
176
+ out[key] = stage as object;
177
+ }
178
+ continue;
179
+ }
180
+ const merged = [uniforms.trim(), body.trim()].filter(Boolean).join("\n");
181
+ out[key] = merged.length > 0 ? merged : (stage as object);
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+
187
+ // ─── Public API ───────────────────────────────────────────────────────────────
188
+
189
+ export function studioMaterialName(assetId: string): string {
190
+ return `studio_${assetId}`;
191
+ }
192
+
193
+ /**
194
+ * Parses `scene_assets.material_config` JSON. Returns null if missing or invalid.
195
+ */
196
+ export function parseMaterialConfig(raw: unknown): MaterialConfig | null {
197
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
198
+ const r = raw as Record<string, unknown>;
199
+
200
+ try {
201
+ const config: MaterialConfig = {
202
+ lightingModel:
203
+ (["Constant", "Lambert", "Blinn", "Phong", "PBR"].includes(r.lightingModel as string)
204
+ ? r.lightingModel
205
+ : "PBR") as MaterialConfig["lightingModel"],
206
+ };
207
+
208
+ if (typeof r.presetName === "string") config.presetName = r.presetName;
209
+ if (typeof r.diffuseColor === "string") config.diffuseColor = r.diffuseColor;
210
+ if (typeof r.roughness === "number") config.roughness = r.roughness;
211
+ if (typeof r.metalness === "number") config.metalness = r.metalness;
212
+ if (typeof r.shininess === "number") config.shininess = r.shininess;
213
+ if (typeof r.alpha === "number") config.alpha = r.alpha;
214
+ if (typeof r.blendMode === "string") config.blendMode = r.blendMode;
215
+ if (r.bloomThreshold != null && typeof r.bloomThreshold === "number")
216
+ config.bloomThreshold = r.bloomThreshold;
217
+ if (["Clamp","Repeat","Mirror"].includes(r.wrapS as string)) config.wrapS = r.wrapS as any;
218
+ if (["Clamp","Repeat","Mirror"].includes(r.wrapT as string)) config.wrapT = r.wrapT as any;
219
+ if (typeof r.transparencyMode === "string") config.transparencyMode = r.transparencyMode;
220
+ if (typeof r.cullMode === "string") config.cullMode = r.cullMode;
221
+
222
+ for (const key of TEXTURE_KEYS) {
223
+ const v = r[key];
224
+ if (typeof v === "string" || v === null) (config as any)[key] = v;
225
+ }
226
+
227
+ if (r.shaderModifiers && typeof r.shaderModifiers === "object" && !Array.isArray(r.shaderModifiers)) {
228
+ config.shaderModifiers = r.shaderModifiers as Record<string, ShaderModifierStage | string>;
229
+ }
230
+
231
+ if (Array.isArray(r.materialUniforms)) {
232
+ config.materialUniforms = r.materialUniforms.filter(
233
+ (u): u is { name: string; type: string; value: unknown } =>
234
+ u && typeof u.name === "string" && typeof u.type === "string",
235
+ );
236
+ }
237
+
238
+ return config;
239
+ } catch (e) {
240
+ console.warn("[material_config] Failed to parse material_config", e);
241
+ return null;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Maps a validated Studio material_config to Viro's `createMaterials` definition shape.
247
+ */
248
+ export function buildViroMaterialDefinition(config: MaterialConfig): ViroMaterialDefinition {
249
+ const out: ViroMaterialDefinition = { lightingModel: config.lightingModel };
250
+
251
+ if (config.diffuseColor !== undefined) out.diffuseColor = config.diffuseColor;
252
+ if (config.roughness !== undefined) out.roughness = config.roughness;
253
+ if (config.metalness !== undefined) out.metalness = config.metalness;
254
+ if (config.shininess !== undefined) out.shininess = config.shininess;
255
+ if (config.alpha !== undefined) out.alpha = config.alpha;
256
+ if (config.blendMode !== undefined) out.blendMode = config.blendMode;
257
+ if (config.bloomThreshold!= undefined) out.bloomThreshold = config.bloomThreshold;
258
+ if (config.wrapS !== undefined) out.wrapS = config.wrapS;
259
+ if (config.wrapT !== undefined) out.wrapT = config.wrapT;
260
+ if (config.transparencyMode !== undefined) out.transparencyMode = config.transparencyMode;
261
+ if (config.cullMode !== undefined) out.cullMode = config.cullMode;
262
+
263
+ for (const key of TEXTURE_KEYS) {
264
+ const mapped = textureToViro((config as any)[key]);
265
+ if (mapped !== undefined) out[key] = mapped;
266
+ }
267
+
268
+ if (config.shaderModifiers !== undefined) {
269
+ out.shaderModifiers = normalizeShaderModifiersForViro(config.shaderModifiers);
270
+ }
271
+
272
+ const mergedUniforms = mergeMaterialUniformsForViro(config);
273
+ if (mergedUniforms !== undefined) out.materialUniforms = mergedUniforms;
274
+
275
+ return out;
276
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Studio physics_config and physics_world_config parsing and Viro prop building.
3
+ * Ported from studio-go/domain/physicsConfig.ts — no zod dependency.
4
+ */
5
+
6
+ // ─── Types ────────────────────────────────────────────────────────────────────
7
+
8
+ type Vec3 = [number, number, number];
9
+
10
+ type ForceEntry = { value: Vec3; position?: Vec3 };
11
+
12
+ type PhysicsShape =
13
+ | { type: "Box"; params: [number, number, number] }
14
+ | { type: "Sphere"; params: [number] }
15
+ | { type: "Compound"; children: Array<{ type: "Box" | "Sphere"; params: number[]; position: Vec3; rotation?: Vec3 }> };
16
+
17
+ export type PhysicsBodyConfig = {
18
+ enabled: boolean;
19
+ type: "Dynamic" | "Kinematic" | "Static";
20
+ mass: number;
21
+ shape?: PhysicsShape;
22
+ restitution?: number;
23
+ friction?: number;
24
+ useGravity?: boolean;
25
+ viroTag?: string;
26
+ force?: ForceEntry | ForceEntry[];
27
+ torque?: Vec3 | Vec3[];
28
+ velocity?: Vec3;
29
+ };
30
+
31
+ export type PhysicsWorldConfig = {
32
+ enabled: boolean;
33
+ gravity: Vec3;
34
+ drawBounds: boolean;
35
+ };
36
+
37
+ export type BuildViroPhysicsBodyOptions = {
38
+ /** Forces Dynamic body to Kinematic with mass 0 while dragging. */
39
+ kinematicDragOverride?: boolean;
40
+ };
41
+
42
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
43
+
44
+ function isVec3(v: unknown): v is Vec3 {
45
+ return Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number");
46
+ }
47
+
48
+ function parseShape(raw: unknown): PhysicsShape | undefined {
49
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
50
+ const r = raw as Record<string, unknown>;
51
+
52
+ if (r.type === "Box" && Array.isArray(r.params) && r.params.length === 3) {
53
+ return { type: "Box", params: r.params as [number, number, number] };
54
+ }
55
+ if (r.type === "Sphere" && Array.isArray(r.params) && r.params.length >= 1) {
56
+ return { type: "Sphere", params: [r.params[0] as number] };
57
+ }
58
+ if (r.type === "Compound" && Array.isArray(r.children)) {
59
+ const children = (r.children as unknown[]).filter((c): c is { type: "Box" | "Sphere"; params: number[]; position: Vec3; rotation?: Vec3 } => {
60
+ if (!c || typeof c !== "object") return false;
61
+ const ch = c as Record<string, unknown>;
62
+ return (ch.type === "Box" || ch.type === "Sphere") && Array.isArray(ch.params) && isVec3(ch.position);
63
+ });
64
+ if (children.length > 0) return { type: "Compound", children };
65
+ }
66
+ return undefined;
67
+ }
68
+
69
+ function mapShapeToViro(shape: PhysicsShape): Record<string, unknown> {
70
+ if (shape.type === "Box") return { type: "Box", params: [...shape.params] };
71
+ if (shape.type === "Sphere") return { type: "Sphere", params: [...shape.params] };
72
+ return {
73
+ type: "Compound",
74
+ params: [],
75
+ children: shape.children.map((c) => {
76
+ const base: Record<string, unknown> = { type: c.type, params: [...c.params], position: [...c.position] };
77
+ if (c.rotation) base.rotation = [...c.rotation];
78
+ return base;
79
+ }),
80
+ };
81
+ }
82
+
83
+ function normalizeTorque(torque: Vec3 | Vec3[]): Vec3 {
84
+ if (Array.isArray(torque[0])) {
85
+ return (torque as Vec3[]).reduce<Vec3>(
86
+ (acc, t) => [acc[0] + t[0], acc[1] + t[1], acc[2] + t[2]],
87
+ [0, 0, 0],
88
+ );
89
+ }
90
+ return [...(torque as Vec3)] as Vec3;
91
+ }
92
+
93
+ function normalizeForce(force: ForceEntry | ForceEntry[]): Array<{ value: number[]; position?: number[] }> {
94
+ const arr = Array.isArray(force) ? force : [force];
95
+ return arr.map((f) => ({
96
+ value: [...f.value],
97
+ position: f.position != null ? [...f.position] : undefined,
98
+ }));
99
+ }
100
+
101
+ // ─── Public API ───────────────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * Parses `scene.physics_world_config` JSON. Returns null if missing or invalid.
105
+ */
106
+ export function parsePhysicsWorldConfig(raw: unknown): PhysicsWorldConfig | null {
107
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
108
+ const r = raw as Record<string, unknown>;
109
+ try {
110
+ return {
111
+ enabled: typeof r.enabled === "boolean" ? r.enabled : false,
112
+ gravity: isVec3(r.gravity) ? r.gravity : [0, -9.8, 0],
113
+ drawBounds: typeof r.drawBounds === "boolean" ? r.drawBounds : false,
114
+ };
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Parses `asset.physics_config` JSON. Returns null if missing or invalid.
122
+ */
123
+ export function parsePhysicsBodyConfig(raw: unknown): PhysicsBodyConfig | null {
124
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
125
+ const r = raw as Record<string, unknown>;
126
+ try {
127
+ const type = (["Dynamic", "Kinematic", "Static"].includes(r.type as string)
128
+ ? r.type
129
+ : undefined) as PhysicsBodyConfig["type"] | undefined;
130
+ if (!type) return null;
131
+
132
+ const mass = typeof r.mass === "number" ? r.mass : 0;
133
+ const config: PhysicsBodyConfig = {
134
+ enabled: typeof r.enabled === "boolean" ? r.enabled : true,
135
+ type,
136
+ mass,
137
+ };
138
+
139
+ const shape = parseShape(r.shape);
140
+ if (shape) config.shape = shape;
141
+ if (typeof r.restitution === "number") config.restitution = r.restitution;
142
+ if (typeof r.friction === "number") config.friction = r.friction;
143
+ if (typeof r.useGravity === "boolean") config.useGravity = r.useGravity;
144
+ if (typeof r.viroTag === "string") config.viroTag = r.viroTag;
145
+ if (isVec3(r.velocity)) config.velocity = r.velocity;
146
+ if (r.torque != null) config.torque = r.torque as Vec3 | Vec3[];
147
+ if (r.force != null) config.force = r.force as ForceEntry | ForceEntry[];
148
+
149
+ return config;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /** Viro `ViroARScene` physicsWorld prop. */
156
+ export function buildViroPhysicsWorld(config: PhysicsWorldConfig): {
157
+ gravity: Vec3;
158
+ drawBounds?: boolean;
159
+ } {
160
+ return {
161
+ gravity: [...config.gravity] as Vec3,
162
+ ...(config.drawBounds ? { drawBounds: true } : {}),
163
+ };
164
+ }
165
+
166
+ /** Maps validated Studio physics_config to Viro `physicsBody` prop. */
167
+ export function buildViroPhysicsBody(
168
+ config: PhysicsBodyConfig,
169
+ options?: BuildViroPhysicsBodyOptions,
170
+ ): Record<string, unknown> {
171
+ const kinematicDrag =
172
+ options?.kinematicDragOverride === true && config.type === "Dynamic" && config.enabled;
173
+
174
+ const type = kinematicDrag ? "Kinematic" : config.type;
175
+ const mass = kinematicDrag ? 0 : config.mass;
176
+ const shape = mapShapeToViro(config.shape ?? { type: "Box", params: [1, 1, 1] });
177
+
178
+ const body: Record<string, unknown> = { type, mass, shape, enabled: config.enabled };
179
+
180
+ if (config.restitution !== undefined) body.restitution = config.restitution;
181
+ if (config.friction !== undefined) body.friction = config.friction;
182
+ if (config.useGravity !== undefined) body.useGravity = kinematicDrag ? false : config.useGravity;
183
+ if (config.velocity !== undefined) body.velocity = [...config.velocity];
184
+ if (config.torque !== undefined) body.torque = normalizeTorque(config.torque);
185
+ if (config.force !== undefined) body.force = normalizeForce(config.force);
186
+
187
+ return body;
188
+ }
189
+
190
+ /**
191
+ * Draggable Dynamic bodies need kinematic override during drag so the simulation
192
+ * doesn't fight the gesture.
193
+ */
194
+ export function shouldUseKinematicPhysicsDrag(
195
+ asset: { is_draggable: boolean },
196
+ config: PhysicsBodyConfig | null,
197
+ ): boolean {
198
+ return (
199
+ asset.is_draggable === true &&
200
+ config != null &&
201
+ config.enabled === true &&
202
+ config.type === "Dynamic"
203
+ );
204
+ }