@reactvision/react-viro 2.57.3 → 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 (39) hide show
  1. package/android/react_viro/react_viro-release.aar +0 -0
  2. package/components/Studio/StudioARScene.tsx +153 -4
  3. package/components/Studio/StudioSceneErrorBoundary.tsx +48 -0
  4. package/components/Studio/StudioSceneNavigator.tsx +186 -41
  5. package/components/Studio/VRTStudioModule.ts +26 -0
  6. package/components/Studio/domain/dragConfiguration.ts +6 -4
  7. package/components/Studio/domain/materialConfig.ts +75 -42
  8. package/components/Studio/domain/physicsConfig.ts +75 -32
  9. package/components/Studio/domain/viroNodeFactory.tsx +53 -14
  10. package/components/Studio/index.ts +4 -0
  11. package/components/Studio/types.ts +3 -8
  12. package/components/Utilities/ViroVersion.ts +1 -1
  13. package/components/ViroXRSceneNavigator.tsx +2 -0
  14. package/dist/components/Studio/StudioARScene.d.ts +6 -0
  15. package/dist/components/Studio/StudioARScene.js +101 -6
  16. package/dist/components/Studio/StudioSceneErrorBoundary.d.ts +28 -0
  17. package/dist/components/Studio/StudioSceneErrorBoundary.js +31 -0
  18. package/dist/components/Studio/StudioSceneNavigator.d.ts +28 -3
  19. package/dist/components/Studio/StudioSceneNavigator.js +84 -20
  20. package/dist/components/Studio/VRTStudioModule.d.ts +17 -0
  21. package/dist/components/Studio/VRTStudioModule.js +14 -0
  22. package/dist/components/Studio/domain/dragConfiguration.d.ts +5 -3
  23. package/dist/components/Studio/domain/dragConfiguration.js +5 -3
  24. package/dist/components/Studio/domain/materialConfig.d.ts +0 -1
  25. package/dist/components/Studio/domain/materialConfig.js +12 -5
  26. package/dist/components/Studio/domain/physicsConfig.d.ts +0 -1
  27. package/dist/components/Studio/domain/physicsConfig.js +18 -6
  28. package/dist/components/Studio/domain/viroNodeFactory.d.ts +2 -2
  29. package/dist/components/Studio/domain/viroNodeFactory.js +31 -18
  30. package/dist/components/Studio/index.d.ts +1 -0
  31. package/dist/components/Studio/types.d.ts +1 -0
  32. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  33. package/dist/components/Utilities/ViroVersion.js +1 -1
  34. package/dist/components/ViroXRSceneNavigator.d.ts +2 -0
  35. package/dist/index.d.ts +1 -1
  36. package/index.ts +2 -0
  37. package/ios/dist/include/RVStudioWatermarkState.h +25 -0
  38. package/ios/dist/lib/libViroReact.a +0 -0
  39. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { StudioAsset, StudioSceneMeta } from "../types";
2
2
 
3
- export type DragType = "FixedToWorld" | "FixedToPlane" | undefined;
3
+ export type DragType = "FixedToPlane" | "FixedDistance" | undefined;
4
4
 
5
5
  export type DragPlane = {
6
6
  planePoint: [number, number, number];
@@ -10,8 +10,10 @@ export type DragPlane = {
10
10
 
11
11
  export class DragConfiguration {
12
12
  /**
13
- * Chooses FixedToPlane when the scene uses plane detection, FixedToWorld otherwise.
14
- * Returns undefined if the asset is not draggable.
13
+ * FixedToPlane when the scene uses plane detection; otherwise FixedDistance,
14
+ * which keeps the object at its grab distance and follows the finger.
15
+ * (FixedToWorld raycast-snapped objects toward the camera on drag start, so
16
+ * they appeared to grow and were hard to place.) undefined if not draggable.
15
17
  */
16
18
  static getDragType(asset: StudioAsset, scene: StudioSceneMeta | null): DragType {
17
19
  if (!asset.is_draggable) return undefined;
@@ -21,7 +23,7 @@ export class DragConfiguration {
21
23
  if (planeDetection === "AUTOMATIC" || planeDetection === "MANUAL") {
22
24
  return "FixedToPlane";
23
25
  }
24
- return "FixedToWorld";
26
+ return "FixedDistance";
25
27
  }
26
28
 
27
29
  /**
@@ -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
 
6
5
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -51,7 +50,9 @@ const TEXTURE_KEYS = [
51
50
  "specularTexture",
52
51
  ] as const;
53
52
 
54
- function textureToViro(uri: string | null | undefined): { uri: string } | undefined {
53
+ function textureToViro(
54
+ uri: string | null | undefined
55
+ ): { uri: string } | undefined {
55
56
  if (uri == null || uri === "") return undefined;
56
57
  return { uri };
57
58
  }
@@ -76,7 +77,9 @@ const TIME_WORD_RE = /\btime\b/i;
76
77
  const CAMERA_TEXTURE_RE = /\bcamera_texture\b/;
77
78
  const RF_VIEWPORT_RE = /\b_rf_vpw\b|\b_rf_vph\b/;
78
79
 
79
- export function materialConfigNeedsTimeUniform(config: MaterialConfig): boolean {
80
+ export function materialConfigNeedsTimeUniform(
81
+ config: MaterialConfig
82
+ ): boolean {
80
83
  if (config.materialUniforms?.some((u) => u.name === "time")) return true;
81
84
  return collectShaderModifierStrings(config).some((s) => TIME_WORD_RE.test(s));
82
85
  }
@@ -85,8 +88,12 @@ export function materialConfigNeedsTimeUniform(config: MaterialConfig): boolean
85
88
  * True if the shader uses _rf_vpw/_rf_vph viewport uniforms.
86
89
  * These must be pushed via ViroMaterials.updateShaderUniform on mount and orientation change.
87
90
  */
88
- export function materialConfigNeedsViewportUniforms(config: MaterialConfig): boolean {
89
- return collectShaderModifierStrings(config).some((s) => RF_VIEWPORT_RE.test(s));
91
+ export function materialConfigNeedsViewportUniforms(
92
+ config: MaterialConfig
93
+ ): boolean {
94
+ return collectShaderModifierStrings(config).some((s) =>
95
+ RF_VIEWPORT_RE.test(s)
96
+ );
90
97
  }
91
98
 
92
99
  /**
@@ -101,7 +108,8 @@ function injectMissingGlslDeclarations(uniforms: string, body: string): string {
101
108
  result;
102
109
  }
103
110
  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;
111
+ result =
112
+ "uniform highp float _rf_vpw;\nuniform highp float _rf_vph;\n" + result;
105
113
  }
106
114
  return result;
107
115
  }
@@ -112,13 +120,20 @@ function injectMissingGlslDeclarations(uniforms: string, body: string): string {
112
120
  * GLSL only, we add the runtime binding here.
113
121
  */
114
122
  function mergeMaterialUniformsForViro(
115
- config: MaterialConfig,
123
+ config: MaterialConfig
116
124
  ): Array<{ name: string; type: string; value: unknown }> | undefined {
117
125
  const list = config.materialUniforms
118
- ? config.materialUniforms.map((u) => ({ name: u.name, type: u.type, value: u.value }))
126
+ ? config.materialUniforms.map((u) => ({
127
+ name: u.name,
128
+ type: u.type,
129
+ value: u.value,
130
+ }))
119
131
  : [];
120
132
 
121
- if (materialConfigNeedsTimeUniform(config) && !list.some((u) => u.name === "time")) {
133
+ if (
134
+ materialConfigNeedsTimeUniform(config) &&
135
+ !list.some((u) => u.name === "time")
136
+ ) {
122
137
  list.push({ name: "time", type: "float", value: 0 });
123
138
  }
124
139
 
@@ -139,7 +154,7 @@ function mergeMaterialUniformsForViro(
139
154
  * layer binds the camera feed even when the DB JSON omits `requiresCameraTexture`.
140
155
  */
141
156
  function normalizeShaderModifiersForViro(
142
- mods: NonNullable<MaterialConfig["shaderModifiers"]>,
157
+ mods: NonNullable<MaterialConfig["shaderModifiers"]>
143
158
  ): Record<string, string | object> {
144
159
  const out: Record<string, string | object> = {};
145
160
  for (const [key, stage] of Object.entries(mods)) {
@@ -199,39 +214,51 @@ export function parseMaterialConfig(raw: unknown): MaterialConfig | null {
199
214
 
200
215
  try {
201
216
  const config: MaterialConfig = {
202
- lightingModel:
203
- (["Constant", "Lambert", "Blinn", "Phong", "PBR"].includes(r.lightingModel as string)
204
- ? r.lightingModel
205
- : "PBR") as MaterialConfig["lightingModel"],
217
+ lightingModel: (["Constant", "Lambert", "Blinn", "Phong", "PBR"].includes(
218
+ r.lightingModel as string
219
+ )
220
+ ? r.lightingModel
221
+ : "PBR") as MaterialConfig["lightingModel"],
206
222
  };
207
223
 
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;
224
+ if (typeof r.presetName === "string") config.presetName = r.presetName;
225
+ if (typeof r.diffuseColor === "string")
226
+ config.diffuseColor = r.diffuseColor;
227
+ if (typeof r.roughness === "number") config.roughness = r.roughness;
228
+ if (typeof r.metalness === "number") config.metalness = r.metalness;
229
+ if (typeof r.shininess === "number") config.shininess = r.shininess;
230
+ if (typeof r.alpha === "number") config.alpha = r.alpha;
231
+ if (typeof r.blendMode === "string") config.blendMode = r.blendMode;
215
232
  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;
233
+ config.bloomThreshold = r.bloomThreshold;
234
+ if (["Clamp", "Repeat", "Mirror"].includes(r.wrapS as string))
235
+ config.wrapS = r.wrapS as any;
236
+ if (["Clamp", "Repeat", "Mirror"].includes(r.wrapT as string))
237
+ config.wrapT = r.wrapT as any;
238
+ if (typeof r.transparencyMode === "string")
239
+ config.transparencyMode = r.transparencyMode;
240
+ if (typeof r.cullMode === "string") config.cullMode = r.cullMode;
221
241
 
222
242
  for (const key of TEXTURE_KEYS) {
223
243
  const v = r[key];
224
244
  if (typeof v === "string" || v === null) (config as any)[key] = v;
225
245
  }
226
246
 
227
- if (r.shaderModifiers && typeof r.shaderModifiers === "object" && !Array.isArray(r.shaderModifiers)) {
228
- config.shaderModifiers = r.shaderModifiers as Record<string, ShaderModifierStage | string>;
247
+ if (
248
+ r.shaderModifiers &&
249
+ typeof r.shaderModifiers === "object" &&
250
+ !Array.isArray(r.shaderModifiers)
251
+ ) {
252
+ config.shaderModifiers = r.shaderModifiers as Record<
253
+ string,
254
+ ShaderModifierStage | string
255
+ >;
229
256
  }
230
257
 
231
258
  if (Array.isArray(r.materialUniforms)) {
232
259
  config.materialUniforms = r.materialUniforms.filter(
233
260
  (u): u is { name: string; type: string; value: unknown } =>
234
- u && typeof u.name === "string" && typeof u.type === "string",
261
+ u && typeof u.name === "string" && typeof u.type === "string"
235
262
  );
236
263
  }
237
264
 
@@ -245,20 +272,24 @@ export function parseMaterialConfig(raw: unknown): MaterialConfig | null {
245
272
  /**
246
273
  * Maps a validated Studio material_config to Viro's `createMaterials` definition shape.
247
274
  */
248
- export function buildViroMaterialDefinition(config: MaterialConfig): ViroMaterialDefinition {
275
+ export function buildViroMaterialDefinition(
276
+ config: MaterialConfig
277
+ ): ViroMaterialDefinition {
249
278
  const out: ViroMaterialDefinition = { lightingModel: config.lightingModel };
250
279
 
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;
280
+ if (config.diffuseColor !== undefined) out.diffuseColor = config.diffuseColor;
281
+ if (config.roughness !== undefined) out.roughness = config.roughness;
282
+ if (config.metalness !== undefined) out.metalness = config.metalness;
283
+ if (config.shininess !== undefined) out.shininess = config.shininess;
284
+ if (config.alpha !== undefined) out.alpha = config.alpha;
285
+ if (config.blendMode !== undefined) out.blendMode = config.blendMode;
286
+ if (config.bloomThreshold != undefined)
287
+ out.bloomThreshold = config.bloomThreshold;
288
+ if (config.wrapS !== undefined) out.wrapS = config.wrapS;
289
+ if (config.wrapT !== undefined) out.wrapT = config.wrapT;
290
+ if (config.transparencyMode !== undefined)
291
+ out.transparencyMode = config.transparencyMode;
292
+ if (config.cullMode !== undefined) out.cullMode = config.cullMode;
262
293
 
263
294
  for (const key of TEXTURE_KEYS) {
264
295
  const mapped = textureToViro((config as any)[key]);
@@ -266,7 +297,9 @@ export function buildViroMaterialDefinition(config: MaterialConfig): ViroMateria
266
297
  }
267
298
 
268
299
  if (config.shaderModifiers !== undefined) {
269
- out.shaderModifiers = normalizeShaderModifiersForViro(config.shaderModifiers);
300
+ out.shaderModifiers = normalizeShaderModifiersForViro(
301
+ config.shaderModifiers
302
+ );
270
303
  }
271
304
 
272
305
  const mergedUniforms = mergeMaterialUniformsForViro(config);
@@ -1,6 +1,5 @@
1
1
  /**
2
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
3
  */
5
4
 
6
5
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -12,7 +11,15 @@ type ForceEntry = { value: Vec3; position?: Vec3 };
12
11
  type PhysicsShape =
13
12
  | { type: "Box"; params: [number, number, number] }
14
13
  | { type: "Sphere"; params: [number] }
15
- | { type: "Compound"; children: Array<{ type: "Box" | "Sphere"; params: number[]; position: Vec3; rotation?: Vec3 }> };
14
+ | {
15
+ type: "Compound";
16
+ children: Array<{
17
+ type: "Box" | "Sphere";
18
+ params: number[];
19
+ position: Vec3;
20
+ rotation?: Vec3;
21
+ }>;
22
+ };
16
23
 
17
24
  export type PhysicsBodyConfig = {
18
25
  enabled: boolean;
@@ -42,7 +49,9 @@ export type BuildViroPhysicsBodyOptions = {
42
49
  // ─── Helpers ──────────────────────────────────────────────────────────────────
43
50
 
44
51
  function isVec3(v: unknown): v is Vec3 {
45
- return Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number");
52
+ return (
53
+ Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number")
54
+ );
46
55
  }
47
56
 
48
57
  function parseShape(raw: unknown): PhysicsShape | undefined {
@@ -56,11 +65,24 @@ function parseShape(raw: unknown): PhysicsShape | undefined {
56
65
  return { type: "Sphere", params: [r.params[0] as number] };
57
66
  }
58
67
  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
- });
68
+ const children = (r.children as unknown[]).filter(
69
+ (
70
+ c
71
+ ): c is {
72
+ type: "Box" | "Sphere";
73
+ params: number[];
74
+ position: Vec3;
75
+ rotation?: Vec3;
76
+ } => {
77
+ if (!c || typeof c !== "object") return false;
78
+ const ch = c as Record<string, unknown>;
79
+ return (
80
+ (ch.type === "Box" || ch.type === "Sphere") &&
81
+ Array.isArray(ch.params) &&
82
+ isVec3(ch.position)
83
+ );
84
+ }
85
+ );
64
86
  if (children.length > 0) return { type: "Compound", children };
65
87
  }
66
88
  return undefined;
@@ -68,12 +90,17 @@ function parseShape(raw: unknown): PhysicsShape | undefined {
68
90
 
69
91
  function mapShapeToViro(shape: PhysicsShape): Record<string, unknown> {
70
92
  if (shape.type === "Box") return { type: "Box", params: [...shape.params] };
71
- if (shape.type === "Sphere") return { type: "Sphere", params: [...shape.params] };
93
+ if (shape.type === "Sphere")
94
+ return { type: "Sphere", params: [...shape.params] };
72
95
  return {
73
96
  type: "Compound",
74
97
  params: [],
75
98
  children: shape.children.map((c) => {
76
- const base: Record<string, unknown> = { type: c.type, params: [...c.params], position: [...c.position] };
99
+ const base: Record<string, unknown> = {
100
+ type: c.type,
101
+ params: [...c.params],
102
+ position: [...c.position],
103
+ };
77
104
  if (c.rotation) base.rotation = [...c.rotation];
78
105
  return base;
79
106
  }),
@@ -84,13 +111,15 @@ function normalizeTorque(torque: Vec3 | Vec3[]): Vec3 {
84
111
  if (Array.isArray(torque[0])) {
85
112
  return (torque as Vec3[]).reduce<Vec3>(
86
113
  (acc, t) => [acc[0] + t[0], acc[1] + t[1], acc[2] + t[2]],
87
- [0, 0, 0],
114
+ [0, 0, 0]
88
115
  );
89
116
  }
90
117
  return [...(torque as Vec3)] as Vec3;
91
118
  }
92
119
 
93
- function normalizeForce(force: ForceEntry | ForceEntry[]): Array<{ value: number[]; position?: number[] }> {
120
+ function normalizeForce(
121
+ force: ForceEntry | ForceEntry[]
122
+ ): Array<{ value: number[]; position?: number[] }> {
94
123
  const arr = Array.isArray(force) ? force : [force];
95
124
  return arr.map((f) => ({
96
125
  value: [...f.value],
@@ -103,7 +132,9 @@ function normalizeForce(force: ForceEntry | ForceEntry[]): Array<{ value: number
103
132
  /**
104
133
  * Parses `scene.physics_world_config` JSON. Returns null if missing or invalid.
105
134
  */
106
- export function parsePhysicsWorldConfig(raw: unknown): PhysicsWorldConfig | null {
135
+ export function parsePhysicsWorldConfig(
136
+ raw: unknown
137
+ ): PhysicsWorldConfig | null {
107
138
  if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
108
139
  const r = raw as Record<string, unknown>;
109
140
  try {
@@ -124,9 +155,11 @@ export function parsePhysicsBodyConfig(raw: unknown): PhysicsBodyConfig | null {
124
155
  if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
125
156
  const r = raw as Record<string, unknown>;
126
157
  try {
127
- const type = (["Dynamic", "Kinematic", "Static"].includes(r.type as string)
128
- ? r.type
129
- : undefined) as PhysicsBodyConfig["type"] | undefined;
158
+ const type = (
159
+ ["Dynamic", "Kinematic", "Static"].includes(r.type as string)
160
+ ? r.type
161
+ : undefined
162
+ ) as PhysicsBodyConfig["type"] | undefined;
130
163
  if (!type) return null;
131
164
 
132
165
  const mass = typeof r.mass === "number" ? r.mass : 0;
@@ -139,12 +172,12 @@ export function parsePhysicsBodyConfig(raw: unknown): PhysicsBodyConfig | null {
139
172
  const shape = parseShape(r.shape);
140
173
  if (shape) config.shape = shape;
141
174
  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[];
175
+ if (typeof r.friction === "number") config.friction = r.friction;
176
+ if (typeof r.useGravity === "boolean") config.useGravity = r.useGravity;
177
+ if (typeof r.viroTag === "string") config.viroTag = r.viroTag;
178
+ if (isVec3(r.velocity)) config.velocity = r.velocity;
179
+ if (r.torque != null) config.torque = r.torque as Vec3 | Vec3[];
180
+ if (r.force != null) config.force = r.force as ForceEntry | ForceEntry[];
148
181
 
149
182
  return config;
150
183
  } catch {
@@ -166,23 +199,33 @@ export function buildViroPhysicsWorld(config: PhysicsWorldConfig): {
166
199
  /** Maps validated Studio physics_config to Viro `physicsBody` prop. */
167
200
  export function buildViroPhysicsBody(
168
201
  config: PhysicsBodyConfig,
169
- options?: BuildViroPhysicsBodyOptions,
202
+ options?: BuildViroPhysicsBodyOptions
170
203
  ): Record<string, unknown> {
171
204
  const kinematicDrag =
172
- options?.kinematicDragOverride === true && config.type === "Dynamic" && config.enabled;
205
+ options?.kinematicDragOverride === true &&
206
+ config.type === "Dynamic" &&
207
+ config.enabled;
173
208
 
174
209
  const type = kinematicDrag ? "Kinematic" : config.type;
175
210
  const mass = kinematicDrag ? 0 : config.mass;
176
- const shape = mapShapeToViro(config.shape ?? { type: "Box", params: [1, 1, 1] });
211
+ const shape = mapShapeToViro(
212
+ config.shape ?? { type: "Box", params: [1, 1, 1] }
213
+ );
177
214
 
178
- const body: Record<string, unknown> = { type, mass, shape, enabled: config.enabled };
215
+ const body: Record<string, unknown> = {
216
+ type,
217
+ mass,
218
+ shape,
219
+ enabled: config.enabled,
220
+ };
179
221
 
180
222
  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);
223
+ if (config.friction !== undefined) body.friction = config.friction;
224
+ if (config.useGravity !== undefined)
225
+ body.useGravity = kinematicDrag ? false : config.useGravity;
226
+ if (config.velocity !== undefined) body.velocity = [...config.velocity];
227
+ if (config.torque !== undefined) body.torque = normalizeTorque(config.torque);
228
+ if (config.force !== undefined) body.force = normalizeForce(config.force);
186
229
 
187
230
  return body;
188
231
  }
@@ -193,7 +236,7 @@ export function buildViroPhysicsBody(
193
236
  */
194
237
  export function shouldUseKinematicPhysicsDrag(
195
238
  asset: { is_draggable: boolean },
196
- config: PhysicsBodyConfig | null,
239
+ config: PhysicsBodyConfig | null
197
240
  ): boolean {
198
241
  return (
199
242
  asset.is_draggable === true &&
@@ -20,7 +20,11 @@ import {
20
20
  } from "./apiRequestHelpers";
21
21
  import { parseMaterialConfig, studioMaterialName } from "./materialConfig";
22
22
  import { DragConfiguration } from "./dragConfiguration";
23
- import { buildViroPhysicsBody, parsePhysicsBodyConfig } from "./physicsConfig";
23
+ import {
24
+ buildViroPhysicsBody,
25
+ parsePhysicsBodyConfig,
26
+ shouldUseKinematicPhysicsDrag,
27
+ } from "./physicsConfig";
24
28
  import { StudioVariableStore } from "./variableStore";
25
29
  import { StudioVisibilityStore } from "./visibilityStore";
26
30
 
@@ -54,6 +58,7 @@ export function createNodeConfig(
54
58
  scene: StudioSceneMeta | null,
55
59
  onAnimationTrigger?: (targetAssetId: string, animKey: string) => void,
56
60
  animationStates?: Record<string, ViroAnimationProp>,
61
+ isDragActive?: (assetId: string) => boolean,
57
62
  onSceneChange?: (sceneId: string, sceneName: string) => void,
58
63
  runtimeCtx?: SequenceRuntimeContext
59
64
  ): NodeConfig {
@@ -107,8 +112,12 @@ export function createNodeConfig(
107
112
  }
108
113
 
109
114
  const parsedPhysics = parsePhysicsBodyConfig(asset.physics_config);
115
+ const dragActive = isDragActive?.(asset.id) ?? false;
110
116
  const physicsBody = parsedPhysics
111
- ? buildViroPhysicsBody(parsedPhysics)
117
+ ? buildViroPhysicsBody(parsedPhysics, {
118
+ kinematicDragOverride:
119
+ dragActive && shouldUseKinematicPhysicsDrag(asset, parsedPhysics),
120
+ })
112
121
  : undefined;
113
122
  const viroTag = parsedPhysics ? asset.id : undefined;
114
123
 
@@ -194,6 +203,7 @@ function create3DObject(
194
203
  asset: StudioAsset,
195
204
  config: NodeConfig,
196
205
  onAssetLoaded?: (id: string) => void,
206
+ notifyPhysicsDrag?: (assetId: string) => void,
197
207
  onCollision?: (
198
208
  viroTag: string,
199
209
  collidedPoint: [number, number, number],
@@ -241,7 +251,9 @@ function create3DObject(
241
251
  }
242
252
  // Viro derives native canDrag from `onDrag != undefined`; without this prop
243
253
  // the drag recognizer is never attached, even when dragType is set.
244
- {...(config.dragType ? { onDrag: () => {} } : {})}
254
+ {...(config.dragType
255
+ ? { onDrag: () => notifyPhysicsDrag?.(asset.id) }
256
+ : {})}
245
257
  {...(shaderOverrides ? { shaderOverrides } : {})}
246
258
  {...(config.physicsBody
247
259
  ? { physicsBody: config.physicsBody as any, viroTag: config.viroTag }
@@ -254,7 +266,8 @@ function create3DObject(
254
266
  function createImage(
255
267
  asset: StudioAsset,
256
268
  config: NodeConfig,
257
- onAssetLoaded?: (id: string) => void
269
+ onAssetLoaded?: (id: string) => void,
270
+ notifyPhysicsDrag?: (assetId: string) => void
258
271
  ): React.ReactElement | null {
259
272
  if (!asset.file_url) {
260
273
  console.warn(`[Studio] Image "${asset.name}" has no file_url`);
@@ -273,7 +286,9 @@ function createImage(
273
286
  onClick={config.onClick}
274
287
  onLoadEnd={() => onAssetLoaded?.(asset.id)}
275
288
  onError={(e) => console.error(`[Studio] Image "${asset.name}" error:`, e)}
276
- {...(config.dragType ? { onDrag: () => {} } : {})}
289
+ {...(config.dragType
290
+ ? { onDrag: () => notifyPhysicsDrag?.(asset.id) }
291
+ : {})}
277
292
  />
278
293
  );
279
294
  }
@@ -288,10 +303,11 @@ const VariableText: React.FC<{
288
303
  asset: StudioAsset;
289
304
  config: NodeConfig;
290
305
  store?: StudioVariableStore;
306
+ notifyPhysicsDrag?: (assetId: string) => void;
291
307
  // Injected by VisibleNode via cloneElement; TEXT is the only node type that
292
308
  // is a component wrapper, so it forwards visibility to its ViroText.
293
309
  visible?: boolean;
294
- }> = ({ asset, config, store, visible }) => {
310
+ }> = ({ asset, config, store, notifyPhysicsDrag, visible }) => {
295
311
  const template = asset.name ?? "";
296
312
  const compute = () =>
297
313
  store
@@ -323,7 +339,9 @@ const VariableText: React.FC<{
323
339
  color: "#FFFFFF",
324
340
  textAlign: "center",
325
341
  }}
326
- {...(config.dragType ? { onDrag: () => {} } : {})}
342
+ {...(config.dragType
343
+ ? { onDrag: () => notifyPhysicsDrag?.(asset.id) }
344
+ : {})}
327
345
  />
328
346
  );
329
347
  };
@@ -357,16 +375,24 @@ const VisibleNode: React.FC<{
357
375
  function createText(
358
376
  asset: StudioAsset,
359
377
  config: NodeConfig,
378
+ notifyPhysicsDrag?: (assetId: string) => void,
360
379
  store?: StudioVariableStore
361
380
  ): React.ReactElement {
362
381
  return (
363
- <VariableText key={asset.id} asset={asset} config={config} store={store} />
382
+ <VariableText
383
+ key={asset.id}
384
+ asset={asset}
385
+ config={config}
386
+ store={store}
387
+ notifyPhysicsDrag={notifyPhysicsDrag}
388
+ />
364
389
  );
365
390
  }
366
391
 
367
392
  function createVideo(
368
393
  asset: StudioAsset,
369
- config: NodeConfig
394
+ config: NodeConfig,
395
+ notifyPhysicsDrag?: (assetId: string) => void
370
396
  ): React.ReactElement | null {
371
397
  if (!asset.file_url) {
372
398
  console.warn(`[Studio] Video "${asset.name}" has no file_url`);
@@ -386,7 +412,9 @@ function createVideo(
386
412
  loop={true}
387
413
  muted={false}
388
414
  onError={(e) => console.error(`[Studio] Video "${asset.name}" error:`, e)}
389
- {...(config.dragType ? { onDrag: () => {} } : {})}
415
+ {...(config.dragType
416
+ ? { onDrag: () => notifyPhysicsDrag?.(asset.id) }
417
+ : {})}
390
418
  />
391
419
  );
392
420
  }
@@ -404,6 +432,8 @@ export function createNode(
404
432
  collidedPoint: [number, number, number],
405
433
  collidedNormal: [number, number, number]
406
434
  ) => void,
435
+ isDragActive?: (assetId: string) => boolean,
436
+ notifyPhysicsDrag?: (assetId: string) => void,
407
437
  onSceneChange?: (sceneId: string, sceneName: string) => void,
408
438
  runtimeCtx?: SequenceRuntimeContext
409
439
  ): React.ReactElement | null {
@@ -415,6 +445,7 @@ export function createNode(
415
445
  scene,
416
446
  onAnimationTrigger,
417
447
  animationStates,
448
+ isDragActive,
418
449
  onSceneChange,
419
450
  runtimeCtx
420
451
  );
@@ -422,16 +453,24 @@ export function createNode(
422
453
  let node: React.ReactElement | null;
423
454
  switch (type) {
424
455
  case "3D-MODEL":
425
- node = create3DObject(asset, config, onAssetLoaded, onCollision);
456
+ // NOTE: notifyPhysicsDrag and onCollision are distinct wirings — keep both;
457
+ // a drag-only merge here once silently killed collisions.
458
+ node = create3DObject(
459
+ asset,
460
+ config,
461
+ onAssetLoaded,
462
+ notifyPhysicsDrag,
463
+ onCollision
464
+ );
426
465
  break;
427
466
  case "IMAGE":
428
- node = createImage(asset, config, onAssetLoaded);
467
+ node = createImage(asset, config, onAssetLoaded, notifyPhysicsDrag);
429
468
  break;
430
469
  case "TEXT":
431
- node = createText(asset, config, runtimeCtx?.variableStore);
470
+ node = createText(asset, config, notifyPhysicsDrag, runtimeCtx?.variableStore);
432
471
  break;
433
472
  case "VIDEO":
434
- node = createVideo(asset, config);
473
+ node = createVideo(asset, config, notifyPhysicsDrag);
435
474
  break;
436
475
  default:
437
476
  console.warn(`[Studio] Unknown asset type "${type}" for "${asset.name}"`);
@@ -1,5 +1,9 @@
1
1
  export { StudioARScene } from "./StudioARScene";
2
2
  export { StudioSceneNavigator } from "./StudioSceneNavigator";
3
+ export type {
4
+ StudioSceneNavigatorHandle,
5
+ StudioSceneNavigatorProps,
6
+ } from "./StudioSceneNavigator";
3
7
  export type {
4
8
  StudioAnimation,
5
9
  StudioAsset,
@@ -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.3";
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;