react-three-game 0.0.26 → 0.0.28

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.
@@ -10,13 +10,15 @@ export type InstanceData = {
10
10
  type: 'dynamic' | 'fixed';
11
11
  };
12
12
  };
13
- export declare function GameInstanceProvider({ children, models, onSelect, registerRef }: {
13
+ export declare function GameInstanceProvider({ children, models, onSelect, registerRef, selectedId, editMode }: {
14
14
  children: React.ReactNode;
15
15
  models: {
16
16
  [filename: string]: Object3D;
17
17
  };
18
18
  onSelect?: (id: string | null) => void;
19
19
  registerRef?: (id: string, obj: Object3D | null) => void;
20
+ selectedId?: string | null;
21
+ editMode?: boolean;
20
22
  }): import("react/jsx-runtime").JSX.Element;
21
23
  export declare const GameInstance: React.ForwardRefExoticComponent<{
22
24
  id: string;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
3
- import { Merged } from '@react-three/drei';
3
+ import { Merged, useHelper } from '@react-three/drei';
4
4
  import { InstancedRigidBodies } from "@react-three/rapier";
5
- import { Mesh, Matrix4, Vector3, Quaternion, Euler } from "three";
5
+ import { Mesh, Matrix4, Vector3, Quaternion, Euler, BoxHelper } from "three";
6
6
  // Helper functions for comparison
7
7
  function arrayEquals(a, b) {
8
8
  if (a === b)
@@ -25,7 +25,7 @@ function instanceEquals(a, b) {
25
25
  ((_a = a.physics) === null || _a === void 0 ? void 0 : _a.type) === ((_b = b.physics) === null || _b === void 0 ? void 0 : _b.type);
26
26
  }
27
27
  const GameInstanceContext = createContext(null);
28
- export function GameInstanceProvider({ children, models, onSelect, registerRef }) {
28
+ export function GameInstanceProvider({ children, models, onSelect, registerRef, selectedId, editMode }) {
29
29
  const [instances, setInstances] = useState([]);
30
30
  const addInstance = useCallback((instance) => {
31
31
  setInstances(prev => {
@@ -119,7 +119,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
119
119
  const partKey = `${modelKey}__${i}`;
120
120
  meshesForModel[partKey] = flatMeshes[partKey];
121
121
  }
122
- return (_jsx(Merged, { meshes: meshesForModel, castShadow: true, receiveShadow: true, children: (instancesMap) => (_jsx(NonPhysicsInstancedGroup, { modelKey: modelKey, group: group, partCount: partCount, instancesMap: instancesMap, onSelect: onSelect, registerRef: registerRef })) }, key));
122
+ return (_jsx(Merged, { meshes: meshesForModel, castShadow: true, receiveShadow: true, children: (instancesMap) => (_jsx(NonPhysicsInstancedGroup, { modelKey: modelKey, group: group, partCount: partCount, instancesMap: instancesMap, onSelect: onSelect, registerRef: registerRef, selectedId: selectedId, editMode: editMode })) }, key));
123
123
  })] }));
124
124
  }
125
125
  // Render physics-enabled instances using InstancedRigidBodies
@@ -160,16 +160,23 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
160
160
  return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
161
161
  }) }));
162
162
  }
163
- // Render non-physics instances using Merged's per-instance groups
164
- function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef }) {
163
+ // Render non-physics instances using Merged (instancing without rigid bodies)
164
+ function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef, selectedId, editMode }) {
165
165
  // Pre-compute which Instance components exist for this model
166
166
  const InstanceComponents = useMemo(() => Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean), [instancesMap, modelKey, partCount]);
167
- return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, InstanceComponents: InstanceComponents, onSelect: onSelect, registerRef: registerRef }, inst.id))) }));
167
+ return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, InstanceComponents: InstanceComponents, onSelect: onSelect, registerRef: registerRef, selectedId: selectedId, editMode: editMode }, inst.id))) }));
168
168
  }
169
169
  // Individual instance item with its own click state
170
- function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef }) {
170
+ function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef, selectedId, editMode }) {
171
171
  const clickValid = useRef(false);
172
- return (_jsx("group", { ref: (el) => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, el), position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
172
+ const groupRef = useRef(null);
173
+ const isSelected = selectedId === instance.id;
174
+ // Use BoxHelper when object is selected in edit mode
175
+ useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
176
+ useEffect(() => {
177
+ registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, groupRef.current);
178
+ }, [instance.id, registerRef]);
179
+ return (_jsx("group", { ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
173
180
  if (clickValid.current) {
174
181
  e.stopPropagation();
175
182
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.id);
@@ -44,7 +44,7 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
44
44
  const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
45
45
  onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(resolved);
46
46
  };
47
- return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { debug: true, paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, basePath: basePath }), children] }) }), _jsx(SaveDataPanel, { currentData: loadedPrefab, onDataChange: updatePrefab, editMode: editMode, onEditModeChange: setEditMode }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
47
+ return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, basePath: basePath }), children] }) }), _jsx(SaveDataPanel, { currentData: loadedPrefab, onDataChange: updatePrefab, editMode: editMode, onEditModeChange: setEditMode }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
48
48
  };
49
49
  const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }) => {
50
50
  const [history, setHistory] = useState([currentData]);
@@ -9,9 +9,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
12
- import { MapControls, TransformControls } from "@react-three/drei";
12
+ import { MapControls, TransformControls, useHelper } from "@react-three/drei";
13
13
  import { useState, useRef, useEffect, forwardRef, useCallback } from "react";
14
- import { Vector3, Euler, Quaternion, SRGBColorSpace, TextureLoader, Matrix4 } from "three";
14
+ import { Vector3, Euler, Quaternion, SRGBColorSpace, TextureLoader, Matrix4, BoxHelper } from "three";
15
15
  import { getComponent, registerComponent } from "./components/ComponentRegistry";
16
16
  import { loadModel } from "../dragdrop/modelLoader";
17
17
  import { GameInstance, GameInstanceProvider } from "./InstanceProvider";
@@ -105,10 +105,10 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
105
105
  });
106
106
  loadAssets();
107
107
  }, [data, loadedModels, loadedTextures]);
108
- return _jsxs("group", { ref: ref, children: [_jsx(GameInstanceProvider, { models: loadedModels, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: new Matrix4() }) }), editMode && _jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedId && selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange }))] })] });
108
+ return _jsxs("group", { ref: ref, children: [_jsx(GameInstanceProvider, { models: loadedModels, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, selectedId: selectedId, editMode: editMode, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: new Matrix4() }) }), editMode && _jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedId && selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange }))] })] });
109
109
  });
110
110
  function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = new Matrix4(), }) {
111
- var _a, _b, _c, _d, _e;
111
+ var _a, _b, _c, _d, _e, _f;
112
112
  // Early return if gameObject is null or undefined
113
113
  if (!gameObject)
114
114
  return null;
@@ -146,11 +146,21 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
146
146
  const core = renderCoreNode(gameObject, ctx, parentMatrix);
147
147
  // --- 5. Render children recursively (always relative transforms) ---
148
148
  const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
149
- // --- 6. Inner content group with full transform ---
150
- const innerGroup = (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [core, children] }));
149
+ // --- 6. Inner content group with full transform and selection helper ---
150
+ const groupRef = useRef(null);
151
+ const isSelected = selectedId === gameObject.id;
152
+ // Show BoxHelper when selected in edit mode
153
+ useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
154
+ useEffect(() => {
155
+ registerRef(gameObject.id, groupRef.current);
156
+ }, [gameObject.id, registerRef]);
157
+ const innerGroup = (_jsxs("group", { ref: groupRef, position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [core, children] }));
151
158
  // --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
152
159
  const physics = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.physics;
153
- if (physics && !editMode) {
160
+ // Determine if model is safe/ready for physics. No model => safe; model => only safe once loaded.
161
+ const modelReady = !((_f = gameObject.components) === null || _f === void 0 ? void 0 : _f.model) ||
162
+ !!loadedModels[gameObject.components.model.properties.filename];
163
+ if (physics && !editMode && modelReady) {
154
164
  const physicsDef = getComponent('Physics');
155
165
  if (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) {
156
166
  return (_jsx(physicsDef.View, { properties: physics.properties, children: innerGroup }));
@@ -183,7 +193,6 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
183
193
  const contextProps = {
184
194
  loadedModels: ctx.loadedModels,
185
195
  loadedTextures: ctx.loadedTextures,
186
- isSelected: ctx.selectedId === gameObject.id,
187
196
  editMode: ctx.editMode,
188
197
  parentMatrix,
189
198
  registerRef: ctx.registerRef,
@@ -25,7 +25,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
25
25
  import { useMemo } from 'react';
26
26
  import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace } from 'three';
27
27
  // View for Material component
28
- function MaterialComponentView({ properties, loadedTextures, isSelected }) {
28
+ function MaterialComponentView({ properties, loadedTextures }) {
29
29
  var _a;
30
30
  const textureName = properties === null || properties === void 0 ? void 0 : properties.texture;
31
31
  const repeat = properties === null || properties === void 0 ? void 0 : properties.repeat;
@@ -52,8 +52,7 @@ function MaterialComponentView({ properties, loadedTextures, isSelected }) {
52
52
  return _jsx("meshStandardMaterial", { color: "red", wireframe: true });
53
53
  }
54
54
  const { color, wireframe = false } = properties;
55
- const displayColor = isSelected ? "cyan" : color;
56
- return (_jsx("meshStandardMaterial", { color: displayColor, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture, side: DoubleSide }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
55
+ return (_jsx("meshStandardMaterial", { color: color, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture, side: DoubleSide }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
57
56
  }
58
57
  const MaterialComponent = {
59
58
  name: 'Material',
@@ -3,14 +3,16 @@ import { RigidBody } from "@react-three/rapier";
3
3
  const selectClass = "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50";
4
4
  const labelClass = "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5";
5
5
  function PhysicsComponentEditor({ component, onUpdate }) {
6
- const { type, collider = 'hull' } = component.properties;
6
+ const { type = 'dynamic', collider = 'hull' } = component.properties;
7
7
  return (_jsxs("div", { children: [_jsx("label", { className: labelClass, children: "Type" }), _jsxs("select", { className: selectClass, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] }), _jsx("label", { className: `${labelClass} mt-2`, children: "Collider" }), _jsxs("select", { className: selectClass, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] }));
8
8
  }
9
9
  function PhysicsComponentView({ properties, editMode, children }) {
10
10
  if (editMode)
11
11
  return _jsx(_Fragment, { children: children });
12
12
  const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
13
- return (_jsx(RigidBody, { type: properties.type, colliders: colliders, children: children }));
13
+ // Remount RigidBody when collider/type changes to avoid Rapier hook dependency warnings
14
+ const rbKey = `${properties.type || 'dynamic'}_${colliders}`;
15
+ return (_jsx(RigidBody, { type: properties.type, colliders: colliders, children: children }, rbKey));
14
16
  }
15
17
  const PhysicsComponent = {
16
18
  name: 'Physics',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
2
- import { Merged } from '@react-three/drei';
2
+ import { Merged, useHelper } from '@react-three/drei';
3
3
  import { InstancedRigidBodies } from "@react-three/rapier";
4
- import { Mesh, Matrix4, Object3D, Group, Vector3, Quaternion, Euler, InstancedMesh } from "three";
4
+ import { Mesh, Matrix4, Object3D, Group, Vector3, Quaternion, Euler, InstancedMesh, BoxHelper } from "three";
5
5
 
6
6
  // --- Types ---
7
7
  export type InstanceData = {
@@ -46,12 +46,16 @@ export function GameInstanceProvider({
46
46
  children,
47
47
  models,
48
48
  onSelect,
49
- registerRef
49
+ registerRef,
50
+ selectedId,
51
+ editMode
50
52
  }: {
51
53
  children: React.ReactNode,
52
54
  models: { [filename: string]: Object3D },
53
55
  onSelect?: (id: string | null) => void,
54
56
  registerRef?: (id: string, obj: Object3D | null) => void,
57
+ selectedId?: string | null,
58
+ editMode?: boolean
55
59
  }) {
56
60
  const [instances, setInstances] = useState<InstanceData[]>([]);
57
61
 
@@ -187,6 +191,8 @@ export function GameInstanceProvider({
187
191
  instancesMap={instancesMap}
188
192
  onSelect={onSelect}
189
193
  registerRef={registerRef}
194
+ selectedId={selectedId}
195
+ editMode={editMode}
190
196
  />
191
197
  )}
192
198
  </Merged>
@@ -269,14 +275,16 @@ function InstancedRigidGroup({
269
275
  );
270
276
  }
271
277
 
272
- // Render non-physics instances using Merged's per-instance groups
278
+ // Render non-physics instances using Merged (instancing without rigid bodies)
273
279
  function NonPhysicsInstancedGroup({
274
280
  modelKey,
275
281
  group,
276
282
  partCount,
277
283
  instancesMap,
278
284
  onSelect,
279
- registerRef
285
+ registerRef,
286
+ selectedId,
287
+ editMode
280
288
  }: {
281
289
  modelKey: string;
282
290
  group: { physicsType: string, instances: InstanceData[] };
@@ -284,6 +292,8 @@ function NonPhysicsInstancedGroup({
284
292
  instancesMap: Record<string, React.ComponentType<any>>;
285
293
  onSelect?: (id: string | null) => void;
286
294
  registerRef?: (id: string, obj: Object3D | null) => void;
295
+ selectedId?: string | null;
296
+ editMode?: boolean;
287
297
  }) {
288
298
  // Pre-compute which Instance components exist for this model
289
299
  const InstanceComponents = useMemo(() =>
@@ -300,6 +310,8 @@ function NonPhysicsInstancedGroup({
300
310
  InstanceComponents={InstanceComponents}
301
311
  onSelect={onSelect}
302
312
  registerRef={registerRef}
313
+ selectedId={selectedId}
314
+ editMode={editMode}
303
315
  />
304
316
  ))}
305
317
  </>
@@ -311,18 +323,31 @@ function InstanceGroupItem({
311
323
  instance,
312
324
  InstanceComponents,
313
325
  onSelect,
314
- registerRef
326
+ registerRef,
327
+ selectedId,
328
+ editMode
315
329
  }: {
316
330
  instance: InstanceData;
317
331
  InstanceComponents: React.ComponentType<any>[];
318
332
  onSelect?: (id: string | null) => void;
319
333
  registerRef?: (id: string, obj: Object3D | null) => void;
334
+ selectedId?: string | null;
335
+ editMode?: boolean;
320
336
  }) {
321
337
  const clickValid = useRef(false);
338
+ const groupRef = useRef<Group>(null!);
339
+ const isSelected = selectedId === instance.id;
340
+
341
+ // Use BoxHelper when object is selected in edit mode
342
+ useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
343
+
344
+ useEffect(() => {
345
+ registerRef?.(instance.id, groupRef.current);
346
+ }, [instance.id, registerRef]);
322
347
 
323
348
  return (
324
349
  <group
325
- ref={(el) => registerRef?.(instance.id, el)}
350
+ ref={groupRef}
326
351
  position={instance.position}
327
352
  rotation={instance.rotation}
328
353
  scale={instance.scale}
@@ -42,7 +42,7 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { b
42
42
 
43
43
  return <>
44
44
  <GameCanvas>
45
- <Physics debug paused={editMode}>
45
+ <Physics paused={editMode}>
46
46
  <ambientLight intensity={1.5} />
47
47
  <gridHelper args={[10, 10]} position={[0, -1, 0]} />
48
48
  <PrefabRoot
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { MapControls, TransformControls } from "@react-three/drei";
3
+ import { MapControls, TransformControls, useHelper } from "@react-three/drei";
4
4
  import { useState, useRef, useEffect, forwardRef, useCallback } from "react";
5
- import { Vector3, Euler, Quaternion, Group, Object3D, SRGBColorSpace, Texture, TextureLoader, Matrix4 } from "three";
5
+ import { Vector3, Euler, Quaternion, Group, Object3D, SRGBColorSpace, Texture, TextureLoader, Matrix4, BoxHelper } from "three";
6
6
  import { Prefab, GameObject as GameObjectType } from "./types";
7
7
  import { getComponent, registerComponent } from "./components/ComponentRegistry";
8
8
  import { ThreeEvent } from "@react-three/fiber";
@@ -128,7 +128,13 @@ export const PrefabRoot = forwardRef<Group, {
128
128
 
129
129
 
130
130
  return <group ref={ref}>
131
- <GameInstanceProvider models={loadedModels} onSelect={editMode ? onSelect : undefined} registerRef={registerRef}>
131
+ <GameInstanceProvider
132
+ models={loadedModels}
133
+ onSelect={editMode ? onSelect : undefined}
134
+ registerRef={registerRef}
135
+ selectedId={selectedId}
136
+ editMode={editMode}
137
+ >
132
138
  <GameObjectRenderer
133
139
  gameObject={data.root}
134
140
  selectedId={selectedId}
@@ -237,10 +243,20 @@ function GameObjectRenderer({
237
243
  />
238
244
  ));
239
245
 
240
- // --- 6. Inner content group with full transform ---
246
+ // --- 6. Inner content group with full transform and selection helper ---
247
+ const groupRef = useRef<Group>(null!);
248
+ const isSelected = selectedId === gameObject.id;
249
+
250
+ // Show BoxHelper when selected in edit mode
251
+ useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
252
+
253
+ useEffect(() => {
254
+ registerRef(gameObject.id, groupRef.current);
255
+ }, [gameObject.id, registerRef]);
256
+
241
257
  const innerGroup = (
242
258
  <group
243
- ref={(el) => registerRef(gameObject.id, el)}
259
+ ref={groupRef}
244
260
  position={transformProps.position}
245
261
  rotation={transformProps.rotation}
246
262
  scale={transformProps.scale}
@@ -255,7 +271,13 @@ function GameObjectRenderer({
255
271
 
256
272
  // --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
257
273
  const physics = gameObject.components?.physics;
258
- if (physics && !editMode) {
274
+
275
+ // Determine if model is safe/ready for physics. No model => safe; model => only safe once loaded.
276
+ const modelReady =
277
+ !gameObject.components?.model ||
278
+ !!loadedModels[gameObject.components.model.properties.filename];
279
+
280
+ if (physics && !editMode && modelReady) {
259
281
  const physicsDef = getComponent('Physics');
260
282
  if (physicsDef?.View) {
261
283
  return (
@@ -304,7 +326,6 @@ function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matr
304
326
  const contextProps = {
305
327
  loadedModels: ctx.loadedModels,
306
328
  loadedTextures: ctx.loadedTextures,
307
- isSelected: ctx.selectedId === gameObject.id,
308
329
  editMode: ctx.editMode,
309
330
  parentMatrix,
310
331
  registerRef: ctx.registerRef,
@@ -102,7 +102,7 @@ import { useMemo } from 'react';
102
102
  import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, Texture } from 'three';
103
103
 
104
104
  // View for Material component
105
- function MaterialComponentView({ properties, loadedTextures, isSelected }: { properties: any, loadedTextures?: Record<string, Texture>, isSelected?: boolean }) {
105
+ function MaterialComponentView({ properties, loadedTextures }: { properties: any, loadedTextures?: Record<string, Texture> }) {
106
106
  const textureName = properties?.texture;
107
107
  const repeat = properties?.repeat;
108
108
  const repeatCount = properties?.repeatCount;
@@ -128,12 +128,11 @@ function MaterialComponentView({ properties, loadedTextures, isSelected }: { pro
128
128
  }
129
129
 
130
130
  const { color, wireframe = false } = properties;
131
- const displayColor = isSelected ? "cyan" : color;
132
131
 
133
132
  return (
134
133
  <meshStandardMaterial
135
134
  key={finalTexture?.uuid ?? 'no-texture'}
136
- color={displayColor}
135
+ color={color}
137
136
  wireframe={wireframe}
138
137
  map={finalTexture}
139
138
  transparent={!!finalTexture}
@@ -1,11 +1,12 @@
1
1
  import { RigidBody } from "@react-three/rapier";
2
+ import type { ReactNode } from 'react';
2
3
  import { Component } from "./ComponentRegistry";
3
4
 
4
5
  const selectClass = "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50";
5
6
  const labelClass = "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5";
6
7
 
7
- function PhysicsComponentEditor({ component, onUpdate }: { component: any; onUpdate: (props: any) => void }) {
8
- const { type, collider = 'hull' } = component.properties;
8
+ function PhysicsComponentEditor({ component, onUpdate }: { component: { properties: { type?: 'dynamic' | 'fixed'; collider?: string;[k: string]: any } }; onUpdate: (props: Partial<Record<string, any>>) => void }) {
9
+ const { type = 'dynamic', collider = 'hull' } = component.properties;
9
10
  return (
10
11
  <div>
11
12
  <label className={labelClass}>Type</label>
@@ -26,9 +27,9 @@ function PhysicsComponentEditor({ component, onUpdate }: { component: any; onUpd
26
27
  }
27
28
 
28
29
  interface PhysicsViewProps {
29
- properties: { type: 'dynamic' | 'fixed'; collider?: string };
30
+ properties: { type?: 'dynamic' | 'fixed'; collider?: string };
30
31
  editMode?: boolean;
31
- children?: React.ReactNode;
32
+ children?: ReactNode;
32
33
  }
33
34
 
34
35
  function PhysicsComponentView({ properties, editMode, children }: PhysicsViewProps) {
@@ -36,8 +37,11 @@ function PhysicsComponentView({ properties, editMode, children }: PhysicsViewPro
36
37
 
37
38
  const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
38
39
 
40
+ // Remount RigidBody when collider/type changes to avoid Rapier hook dependency warnings
41
+ const rbKey = `${properties.type || 'dynamic'}_${colliders}`;
42
+
39
43
  return (
40
- <RigidBody type={properties.type} colliders={colliders as any}>
44
+ <RigidBody key={rbKey} type={properties.type} colliders={colliders as any}>
41
45
  {children}
42
46
  </RigidBody>
43
47
  );