react-three-game 0.0.35 → 0.0.37

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.
@@ -19,6 +19,7 @@ export declare function GameInstanceProvider({ children, models, onSelect, regis
19
19
  selectedId?: string | null;
20
20
  editMode?: boolean;
21
21
  }): import("react/jsx-runtime").JSX.Element;
22
+ export declare function useInstanceCheck(id: string): boolean;
22
23
  export declare const GameInstance: React.ForwardRefExoticComponent<{
23
24
  id: string;
24
25
  modelUrl: string;
@@ -50,6 +50,9 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
50
50
  return prev.filter(i => i.id !== id);
51
51
  });
52
52
  }, []);
53
+ const hasInstance = useCallback((id) => {
54
+ return instances.some(i => i.id === id);
55
+ }, [instances]);
53
56
  // Flatten all model meshes once (models → flat mesh parts)
54
57
  // Note: Geometry is cloned with baked transforms for instancing
55
58
  const { flatMeshes, modelParts } = useMemo(() => {
@@ -97,7 +100,8 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
97
100
  removeInstance,
98
101
  instances,
99
102
  meshes: flatMeshes,
100
- modelParts
103
+ modelParts,
104
+ hasInstance
101
105
  }, children: [children, Object.entries(grouped).map(([key, group]) => {
102
106
  if (group.physicsType === 'none')
103
107
  return null;
@@ -105,7 +109,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
105
109
  const partCount = modelParts[modelKey] || 0;
106
110
  if (partCount === 0)
107
111
  return null;
108
- return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes }, key));
112
+ return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes, onSelect: onSelect, editMode: editMode }, key));
109
113
  }), Object.entries(grouped).map(([key, group]) => {
110
114
  if (group.physicsType !== 'none')
111
115
  return null;
@@ -123,8 +127,9 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
123
127
  })] }));
124
128
  }
125
129
  // Render physics-enabled instances using InstancedRigidBodies
126
- function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
130
+ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect, editMode }) {
127
131
  const meshRefs = useRef([]);
132
+ const rigidBodiesRef = useRef(null);
128
133
  const instances = useMemo(() => group.instances.map(inst => ({
129
134
  key: inst.id,
130
135
  position: inst.position,
@@ -151,14 +156,47 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
151
156
  });
152
157
  mesh.instanceMatrix.needsUpdate = true;
153
158
  });
159
+ // Update rigid body positions when instances change
160
+ if (rigidBodiesRef.current) {
161
+ try {
162
+ group.instances.forEach((inst, i) => {
163
+ var _a;
164
+ const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a.at(i);
165
+ if (body && body.setTranslation && body.setRotation) {
166
+ pos.set(...inst.position);
167
+ euler.set(...inst.rotation);
168
+ quat.setFromEuler(euler);
169
+ body.setTranslation(pos, false);
170
+ body.setRotation(quat, false);
171
+ }
172
+ });
173
+ }
174
+ catch (error) {
175
+ // Ignore errors when switching between instanced/non-instanced states
176
+ console.warn('Failed to update rigidbody positions:', error);
177
+ }
178
+ }
154
179
  }, [group.instances]);
155
180
  const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
156
- return (_jsx(InstancedRigidBodies, { instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
181
+ // Handle click on instanced mesh in edit mode
182
+ const handleClick = (e) => {
183
+ if (!editMode || !onSelect)
184
+ return;
185
+ e.stopPropagation();
186
+ // Get the instance index from the intersection
187
+ const instanceId = e.instanceId;
188
+ if (instanceId !== undefined && group.instances[instanceId]) {
189
+ onSelect(group.instances[instanceId].id);
190
+ }
191
+ };
192
+ // Add key to force remount when instance count changes significantly (helps with cleanup)
193
+ const rigidBodyKey = `rb_${modelKey}_${group.physicsType}_${group.instances.length}`;
194
+ return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
157
195
  const mesh = flatMeshes[`${modelKey}__${i}`];
158
196
  if (!mesh)
159
197
  return null;
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
- }) }));
198
+ return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false, onClick: editMode ? handleClick : undefined }, i));
199
+ }) }, rigidBodyKey));
162
200
  }
163
201
  // Render non-physics instances using Merged (instancing without rigid bodies)
164
202
  function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef, selectedId, editMode }) {
@@ -184,6 +222,12 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef
184
222
  clickValid.current = false;
185
223
  }, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
186
224
  }
225
+ // Hook to check if an instance exists
226
+ export function useInstanceCheck(id) {
227
+ var _a;
228
+ const ctx = useContext(GameInstanceContext);
229
+ return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
230
+ }
187
231
  // GameInstance component: registers an instance for batch rendering (renders nothing itself)
188
232
  export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
189
233
  const ctx = useContext(GameInstanceContext);
@@ -196,7 +240,7 @@ export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation
196
240
  rotation,
197
241
  scale,
198
242
  physics,
199
- }), [id, modelUrl, position, rotation, scale, physics]);
243
+ }), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), JSON.stringify(physics)]);
200
244
  useEffect(() => {
201
245
  if (!addInstance || !removeInstance)
202
246
  return;
@@ -97,7 +97,7 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
97
97
  lastDataRef.current = JSON.stringify(prefab);
98
98
  }
99
99
  });
100
- 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("div", { style: toolbar.panel, children: _jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }) }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath, onSave: () => saveJson(loadedPrefab, "prefab"), onLoad: handleLoad, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
100
+ return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { debug: editMode, 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("div", { style: toolbar.panel, children: _jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }) }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath, onSave: () => saveJson(loadedPrefab, "prefab"), onLoad: handleLoad, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
101
101
  };
102
102
  const saveJson = (data, filename) => {
103
103
  const a = document.createElement('a');
@@ -1,4 +1,5 @@
1
1
  import { Group, Matrix4, Object3D, Texture } from "three";
2
+ import { ThreeEvent } from "@react-three/fiber";
2
3
  import { Prefab, GameObject as GameObjectType } from "./types";
3
4
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
4
5
  editMode?: boolean;
@@ -6,6 +7,7 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
6
7
  onPrefabChange?: (data: Prefab) => void;
7
8
  selectedId?: string | null;
8
9
  onSelect?: (id: string | null) => void;
10
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
9
11
  transformMode?: "translate" | "rotate" | "scale";
10
12
  basePath?: string;
11
13
  } & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
@@ -14,6 +16,7 @@ interface RendererProps {
14
16
  gameObject: GameObjectType;
15
17
  selectedId?: string | null;
16
18
  onSelect?: (id: string) => void;
19
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
17
20
  registerRef: (id: string, obj: Object3D | null) => void;
18
21
  loadedModels: Record<string, Object3D>;
19
22
  loadedTextures: Record<string, Texture>;
@@ -15,7 +15,7 @@ import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, V
15
15
  import { getComponent, registerComponent } from "./components/ComponentRegistry";
16
16
  import components from "./components";
17
17
  import { loadModel } from "../dragdrop/modelLoader";
18
- import { GameInstance, GameInstanceProvider } from "./InstanceProvider";
18
+ import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
19
19
  import { updateNode } from "./utils";
20
20
  /* -------------------------------------------------- */
21
21
  /* Setup */
@@ -25,7 +25,7 @@ const IDENTITY = new Matrix4();
25
25
  /* -------------------------------------------------- */
26
26
  /* PrefabRoot */
27
27
  /* -------------------------------------------------- */
28
- export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, basePath = "" }, ref) => {
28
+ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, transformMode, basePath = "" }, ref) => {
29
29
  const [models, setModels] = useState({});
30
30
  const [textures, setTextures] = useState({});
31
31
  const loading = useRef(new Set());
@@ -36,6 +36,19 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
36
36
  if (id === selectedId)
37
37
  setSelectedObject(obj);
38
38
  }, [selectedId]);
39
+ // Suppress TransformControls scene graph warnings during transitions
40
+ useEffect(() => {
41
+ const originalError = console.error;
42
+ console.error = (...args) => {
43
+ if (typeof args[0] === 'string' && args[0].includes('TransformControls') && args[0].includes('scene graph')) {
44
+ return; // Suppress this specific error
45
+ }
46
+ originalError.apply(console, args);
47
+ };
48
+ return () => {
49
+ console.error = originalError;
50
+ };
51
+ }, []);
39
52
  useEffect(() => {
40
53
  var _a;
41
54
  setSelectedObject(selectedId ? (_a = objectRefs.current[selectedId]) !== null && _a !== void 0 ? _a : null : null);
@@ -93,7 +106,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
93
106
  });
94
107
  }, [data, models, textures]);
95
108
  /* ---------------- Render ---------------- */
96
- return (_jsxs("group", { ref: ref, children: [_jsx(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange }))] }))] }));
109
+ return (_jsxs("group", { ref: ref, children: [_jsx(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange }))] }))] }));
97
110
  });
98
111
  /* -------------------------------------------------- */
99
112
  /* Renderer Switch */
@@ -103,9 +116,26 @@ export function GameObjectRenderer(props) {
103
116
  const node = props.gameObject;
104
117
  if (!node || node.hidden || node.disabled)
105
118
  return null;
106
- return ((_c = (_b = (_a = node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced)
107
- ? _jsx(InstancedNode, Object.assign({}, props))
108
- : _jsx(StandardNode, Object.assign({}, props));
119
+ const isInstanced = (_c = (_b = (_a = node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced;
120
+ const prevInstancedRef = useRef(undefined);
121
+ const [isTransitioning, setIsTransitioning] = useState(false);
122
+ useEffect(() => {
123
+ // Detect instanced mode change
124
+ if (prevInstancedRef.current !== undefined && prevInstancedRef.current !== isInstanced) {
125
+ setIsTransitioning(true);
126
+ // Wait for cleanup, then allow new mode to render
127
+ const timer = setTimeout(() => setIsTransitioning(false), 100);
128
+ return () => clearTimeout(timer);
129
+ }
130
+ prevInstancedRef.current = isInstanced;
131
+ }, [isInstanced]);
132
+ // Don't render during transition to avoid physics conflicts
133
+ if (isTransitioning)
134
+ return null;
135
+ const key = `${node.id}_${isInstanced ? 'instanced' : 'standard'}`;
136
+ return isInstanced
137
+ ? _jsx(InstancedNode, Object.assign({}, props), key)
138
+ : _jsx(StandardNode, Object.assign({}, props), key);
109
139
  }
110
140
  /* -------------------------------------------------- */
111
141
  /* InstancedNode (terminal) */
@@ -113,24 +143,50 @@ export function GameObjectRenderer(props) {
113
143
  function isPhysicsProps(v) {
114
144
  return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic";
115
145
  }
116
- function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode }) {
117
- var _a, _b, _c, _d, _e, _f, _g;
146
+ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, registerRef, selectedId: _selectedId, onSelect, onClick }) {
147
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
118
148
  const world = parentMatrix.clone().multiply(compose(gameObject));
119
- const { position, rotation, scale } = decompose(world);
149
+ const { position: worldPosition, rotation: worldRotation, scale: worldScale } = decompose(world);
150
+ // Get local transform for proxy group (used by transform controls)
151
+ const localTransform = getNodeTransformProps(gameObject);
120
152
  const physicsProps = isPhysicsProps((_b = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics) === null || _b === void 0 ? void 0 : _b.properties)
121
153
  ? (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.physics) === null || _d === void 0 ? void 0 : _d.properties
122
154
  : undefined;
123
- return (_jsx(GameInstance, { id: gameObject.id, modelUrl: (_g = (_f = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.model) === null || _f === void 0 ? void 0 : _f.properties) === null || _g === void 0 ? void 0 : _g.filename, position: position, rotation: rotation, scale: scale, physics: editMode ? undefined : physicsProps }));
155
+ const groupRef = useRef(null);
156
+ const clickValid = useRef(false);
157
+ useEffect(() => {
158
+ if (editMode) {
159
+ registerRef(gameObject.id, groupRef.current);
160
+ return () => registerRef(gameObject.id, null);
161
+ }
162
+ }, [gameObject.id, registerRef, editMode]);
163
+ const modelUrl = (_g = (_f = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.model) === null || _f === void 0 ? void 0 : _f.properties) === null || _g === void 0 ? void 0 : _g.filename;
164
+ // In edit mode, create a proxy group at the same position for transform controls
165
+ // The GameInstance still needs the actual position so it renders correctly
166
+ if (editMode) {
167
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
168
+ if (clickValid.current) {
169
+ e.stopPropagation();
170
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(gameObject.id);
171
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
172
+ }
173
+ clickValid.current = false;
174
+ }, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx(GameInstance, { id: gameObject.id, modelUrl: modelUrl, position: worldPosition, rotation: worldRotation, scale: worldScale, physics: physicsProps })] }));
175
+ }
176
+ return (_jsx(GameInstance, { id: gameObject.id, modelUrl: (_k = (_j = (_h = gameObject.components) === null || _h === void 0 ? void 0 : _h.model) === null || _j === void 0 ? void 0 : _j.properties) === null || _k === void 0 ? void 0 : _k.filename, position: worldPosition, rotation: worldRotation, scale: worldScale, physics: physicsProps }));
124
177
  }
125
178
  /* -------------------------------------------------- */
126
179
  /* StandardNode */
127
180
  /* -------------------------------------------------- */
128
- function StandardNode({ gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
129
- var _a, _b, _c;
181
+ function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
182
+ var _a, _b, _c, _d, _e, _f;
130
183
  const groupRef = useRef(null);
184
+ const helperRef = useRef(null);
131
185
  const clickValid = useRef(false);
132
186
  const isSelected = selectedId === gameObject.id;
133
- const helperRef = groupRef;
187
+ // Check if this object still exists as an instance (to prevent physics overlap)
188
+ const stillInstanced = useInstanceCheck(gameObject.id);
189
+ // Use helperRef for BoxHelper (shows actual content bounds at correct position)
134
190
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
135
191
  useEffect(() => {
136
192
  registerRef(gameObject.id, groupRef.current);
@@ -145,20 +201,29 @@ function StandardNode({ gameObject, selectedId, onSelect, registerRef, loadedMod
145
201
  if (clickValid.current) {
146
202
  e.stopPropagation();
147
203
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(gameObject.id);
204
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
148
205
  }
149
206
  clickValid.current = false;
150
207
  };
151
- const inner = (_jsxs("group", Object.assign({ ref: groupRef }, getNodeTransformProps(gameObject), { onPointerDown: onDown, onPointerMove: () => (clickValid.current = false), onPointerUp: onUp, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_a = gameObject.children) === null || _a === void 0 ? void 0 : _a.map(child => (_jsx(GameObjectRenderer, { child, gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] })));
152
- const physics = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.physics;
153
- const ready = !((_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) ||
208
+ const physics = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics;
209
+ const ready = !((_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.model) ||
154
210
  loadedModels[gameObject.components.model.properties.filename];
155
- if (physics && !editMode && ready) {
156
- const def = getComponent("Physics");
157
- return (def === null || def === void 0 ? void 0 : def.View)
158
- ? _jsx(def.View, { properties: physics.properties, children: inner })
159
- : inner;
211
+ const hasPhysics = physics && ready && !stillInstanced;
212
+ const transform = getNodeTransformProps(gameObject);
213
+ // Prepare physics wrapper if needed
214
+ const physicsDef = hasPhysics ? getComponent("Physics") : null;
215
+ const isInstanced = (_e = (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.properties) === null || _e === void 0 ? void 0 : _e.instanced;
216
+ const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
217
+ const inner = (_jsxs("group", { onPointerDown: editMode ? onDown : undefined, onPointerMove: editMode ? () => (clickValid.current = false) : undefined, onPointerUp: editMode ? onUp : undefined, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_f = gameObject.children) === null || _f === void 0 ? void 0 : _f.map(child => (_jsx(GameObjectRenderer, { child, gameObject: child, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] }));
218
+ // In edit mode, use proxy group pattern
219
+ if (editMode) {
220
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey)) : null] }));
221
+ }
222
+ // In play mode, apply transform directly to content
223
+ if (hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View)) {
224
+ return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey));
160
225
  }
161
- return inner;
226
+ return (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, onPointerDown: onDown, onPointerMove: () => (clickValid.current = false), onPointerUp: onUp, children: inner }));
162
227
  }
163
228
  function walk(node, fn) {
164
229
  var _a;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { RigidBody } from "@react-three/rapier";
3
3
  import { Label } from "./Input";
4
4
  function PhysicsComponentEditor({ component, onUpdate }) {
@@ -15,13 +15,14 @@ function PhysicsComponentEditor({ component, onUpdate }) {
15
15
  };
16
16
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Type" }), _jsxs("select", { style: selectStyle, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Collider" }), _jsxs("select", { style: selectStyle, 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)" })] })] })] }));
17
17
  }
18
- function PhysicsComponentView({ properties, editMode, children }) {
19
- if (editMode)
20
- return _jsx(_Fragment, { children: children });
18
+ function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }) {
21
19
  const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
22
- // Remount RigidBody when collider/type changes to avoid Rapier hook dependency warnings
23
- const rbKey = `${properties.type || 'dynamic'}_${colliders}`;
24
- return (_jsx(RigidBody, { type: properties.type, colliders: colliders, children: children }, rbKey));
20
+ // In edit mode, include position/rotation in key to force remount when transform changes
21
+ // This ensures the RigidBody debug visualization updates even when physics is paused
22
+ const rbKey = editMode
23
+ ? `${properties.type || 'dynamic'}_${colliders}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
24
+ : `${properties.type || 'dynamic'}_${colliders}`;
25
+ return (_jsx(RigidBody, { type: properties.type, colliders: colliders, position: position, rotation: rotation, scale: scale, children: children }, rbKey));
25
26
  }
26
27
  const PhysicsComponent = {
27
28
  name: 'Physics',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -40,6 +40,7 @@ type GameInstanceContextType = {
40
40
  instances: InstanceData[];
41
41
  meshes: Record<string, Mesh>;
42
42
  modelParts?: Record<string, number>;
43
+ hasInstance: (id: string) => boolean;
43
44
  };
44
45
  const GameInstanceContext = createContext<GameInstanceContextType | null>(null);
45
46
 
@@ -84,6 +85,10 @@ export function GameInstanceProvider({
84
85
  });
85
86
  }, []);
86
87
 
88
+ const hasInstance = useCallback((id: string) => {
89
+ return instances.some(i => i.id === id);
90
+ }, [instances]);
91
+
87
92
  // Flatten all model meshes once (models → flat mesh parts)
88
93
  // Note: Geometry is cloned with baked transforms for instancing
89
94
  const { flatMeshes, modelParts } = useMemo(() => {
@@ -138,7 +143,8 @@ export function GameInstanceProvider({
138
143
  removeInstance,
139
144
  instances,
140
145
  meshes: flatMeshes,
141
- modelParts
146
+ modelParts,
147
+ hasInstance
142
148
  }}
143
149
  >
144
150
  {/* Render normal prefab hierarchy (non-instanced objects) */}
@@ -158,6 +164,8 @@ export function GameInstanceProvider({
158
164
  modelKey={modelKey}
159
165
  partCount={partCount}
160
166
  flatMeshes={flatMeshes}
167
+ onSelect={onSelect}
168
+ editMode={editMode}
161
169
  />
162
170
  );
163
171
  })}
@@ -208,14 +216,19 @@ function InstancedRigidGroup({
208
216
  group,
209
217
  modelKey,
210
218
  partCount,
211
- flatMeshes
219
+ flatMeshes,
220
+ onSelect,
221
+ editMode
212
222
  }: {
213
223
  group: { physicsType: string, instances: InstanceData[] },
214
224
  modelKey: string,
215
225
  partCount: number,
216
- flatMeshes: Record<string, Mesh>
226
+ flatMeshes: Record<string, Mesh>,
227
+ onSelect?: (id: string | null) => void,
228
+ editMode?: boolean
217
229
  }) {
218
230
  const meshRefs = useRef<(InstancedMesh | null)[]>([]);
231
+ const rigidBodiesRef = useRef<any>(null);
219
232
 
220
233
  const instances = useMemo(
221
234
  () => group.instances.map(inst => ({
@@ -248,12 +261,48 @@ function InstancedRigidGroup({
248
261
  });
249
262
  mesh.instanceMatrix.needsUpdate = true;
250
263
  });
264
+
265
+ // Update rigid body positions when instances change
266
+ if (rigidBodiesRef.current) {
267
+ try {
268
+ group.instances.forEach((inst, i) => {
269
+ const body = rigidBodiesRef.current?.at(i);
270
+ if (body && body.setTranslation && body.setRotation) {
271
+ pos.set(...inst.position);
272
+ euler.set(...inst.rotation);
273
+ quat.setFromEuler(euler);
274
+ body.setTranslation(pos, false);
275
+ body.setRotation(quat, false);
276
+ }
277
+ });
278
+ } catch (error) {
279
+ // Ignore errors when switching between instanced/non-instanced states
280
+ console.warn('Failed to update rigidbody positions:', error);
281
+ }
282
+ }
251
283
  }, [group.instances]);
252
284
 
253
285
  const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
254
286
 
287
+ // Handle click on instanced mesh in edit mode
288
+ const handleClick = (e: any) => {
289
+ if (!editMode || !onSelect) return;
290
+ e.stopPropagation();
291
+
292
+ // Get the instance index from the intersection
293
+ const instanceId = e.instanceId;
294
+ if (instanceId !== undefined && group.instances[instanceId]) {
295
+ onSelect(group.instances[instanceId].id);
296
+ }
297
+ };
298
+
299
+ // Add key to force remount when instance count changes significantly (helps with cleanup)
300
+ const rigidBodyKey = `rb_${modelKey}_${group.physicsType}_${group.instances.length}`;
301
+
255
302
  return (
256
303
  <InstancedRigidBodies
304
+ key={rigidBodyKey}
305
+ ref={rigidBodiesRef}
257
306
  instances={instances}
258
307
  colliders={colliders}
259
308
  type={group.physicsType as 'dynamic' | 'fixed'}
@@ -269,6 +318,7 @@ function InstancedRigidGroup({
269
318
  castShadow
270
319
  receiveShadow
271
320
  frustumCulled={false}
321
+ onClick={editMode ? handleClick : undefined}
272
322
  />
273
323
  );
274
324
  })}
@@ -368,6 +418,12 @@ function InstanceGroupItem({
368
418
  }
369
419
 
370
420
 
421
+ // Hook to check if an instance exists
422
+ export function useInstanceCheck(id: string): boolean {
423
+ const ctx = useContext(GameInstanceContext);
424
+ return ctx?.hasInstance(id) ?? false;
425
+ }
426
+
371
427
  // GameInstance component: registers an instance for batch rendering (renders nothing itself)
372
428
  export const GameInstance = React.forwardRef<Group, {
373
429
  id: string;
@@ -395,7 +451,7 @@ export const GameInstance = React.forwardRef<Group, {
395
451
  rotation,
396
452
  scale,
397
453
  physics,
398
- }), [id, modelUrl, position, rotation, scale, physics]);
454
+ }), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), JSON.stringify(physics)]);
399
455
 
400
456
  useEffect(() => {
401
457
  if (!addInstance || !removeInstance) return;
@@ -97,7 +97,7 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
97
97
 
98
98
  return <>
99
99
  <GameCanvas>
100
- <Physics paused={editMode}>
100
+ <Physics debug={editMode} paused={editMode}>
101
101
  <ambientLight intensity={1.5} />
102
102
  <gridHelper args={[10, 10]} position={[0, -1, 0]} />
103
103
  <PrefabRoot
@@ -9,7 +9,7 @@ import { Prefab, GameObject as GameObjectType } from "./types";
9
9
  import { getComponent, registerComponent } from "./components/ComponentRegistry";
10
10
  import components from "./components";
11
11
  import { loadModel } from "../dragdrop/modelLoader";
12
- import { GameInstance, GameInstanceProvider } from "./InstanceProvider";
12
+ import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
13
13
  import { updateNode } from "./utils";
14
14
  import { PhysicsProps } from "./components/PhysicsComponent";
15
15
 
@@ -31,9 +31,10 @@ export const PrefabRoot = forwardRef<Group, {
31
31
  onPrefabChange?: (data: Prefab) => void;
32
32
  selectedId?: string | null;
33
33
  onSelect?: (id: string | null) => void;
34
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
34
35
  transformMode?: "translate" | "rotate" | "scale";
35
36
  basePath?: string;
36
- }>(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, basePath = "" }, ref) => {
37
+ }>(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, transformMode, basePath = "" }, ref) => {
37
38
 
38
39
  const [models, setModels] = useState<Record<string, Object3D>>({});
39
40
  const [textures, setTextures] = useState<Record<string, Texture>>({});
@@ -46,6 +47,20 @@ export const PrefabRoot = forwardRef<Group, {
46
47
  if (id === selectedId) setSelectedObject(obj);
47
48
  }, [selectedId]);
48
49
 
50
+ // Suppress TransformControls scene graph warnings during transitions
51
+ useEffect(() => {
52
+ const originalError = console.error;
53
+ console.error = (...args: any[]) => {
54
+ if (typeof args[0] === 'string' && args[0].includes('TransformControls') && args[0].includes('scene graph')) {
55
+ return; // Suppress this specific error
56
+ }
57
+ originalError.apply(console, args);
58
+ };
59
+ return () => {
60
+ console.error = originalError;
61
+ };
62
+ }, []);
63
+
49
64
  useEffect(() => {
50
65
  setSelectedObject(selectedId ? objectRefs.current[selectedId] ?? null : null);
51
66
  }, [selectedId]);
@@ -134,6 +149,7 @@ export const PrefabRoot = forwardRef<Group, {
134
149
  gameObject={data.root}
135
150
  selectedId={selectedId}
136
151
  onSelect={editMode ? onSelect : undefined}
152
+ onClick={onClick}
137
153
  registerRef={registerRef}
138
154
  loadedModels={models}
139
155
  loadedTextures={textures}
@@ -166,9 +182,29 @@ export const PrefabRoot = forwardRef<Group, {
166
182
  export function GameObjectRenderer(props: RendererProps) {
167
183
  const node = props.gameObject;
168
184
  if (!node || node.hidden || node.disabled) return null;
169
- return node.components?.model?.properties?.instanced
170
- ? <InstancedNode {...props} />
171
- : <StandardNode {...props} />;
185
+
186
+ const isInstanced = node.components?.model?.properties?.instanced;
187
+ const prevInstancedRef = useRef<boolean | undefined>(undefined);
188
+ const [isTransitioning, setIsTransitioning] = useState(false);
189
+
190
+ useEffect(() => {
191
+ // Detect instanced mode change
192
+ if (prevInstancedRef.current !== undefined && prevInstancedRef.current !== isInstanced) {
193
+ setIsTransitioning(true);
194
+ // Wait for cleanup, then allow new mode to render
195
+ const timer = setTimeout(() => setIsTransitioning(false), 100);
196
+ return () => clearTimeout(timer);
197
+ }
198
+ prevInstancedRef.current = isInstanced;
199
+ }, [isInstanced]);
200
+
201
+ // Don't render during transition to avoid physics conflicts
202
+ if (isTransitioning) return null;
203
+
204
+ const key = `${node.id}_${isInstanced ? 'instanced' : 'standard'}`;
205
+ return isInstanced
206
+ ? <InstancedNode key={key} {...props} />
207
+ : <StandardNode key={key} {...props} />;
172
208
  }
173
209
 
174
210
  /* -------------------------------------------------- */
@@ -178,23 +214,79 @@ function isPhysicsProps(v: any): v is PhysicsProps {
178
214
  return v?.type === "fixed" || v?.type === "dynamic";
179
215
  }
180
216
 
181
- function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode }: RendererProps) {
217
+ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, registerRef, selectedId: _selectedId, onSelect, onClick }: RendererProps) {
182
218
  const world = parentMatrix.clone().multiply(compose(gameObject));
183
- const { position, rotation, scale } = decompose(world);
219
+ const { position: worldPosition, rotation: worldRotation, scale: worldScale } = decompose(world);
220
+
221
+ // Get local transform for proxy group (used by transform controls)
222
+ const localTransform = getNodeTransformProps(gameObject);
223
+
184
224
  const physicsProps = isPhysicsProps(
185
225
  gameObject.components?.physics?.properties
186
226
  )
187
227
  ? gameObject.components?.physics?.properties
188
228
  : undefined;
189
229
 
230
+ const groupRef = useRef<Group>(null);
231
+ const clickValid = useRef(false);
232
+
233
+ useEffect(() => {
234
+ if (editMode) {
235
+ registerRef(gameObject.id, groupRef.current);
236
+ return () => registerRef(gameObject.id, null);
237
+ }
238
+ }, [gameObject.id, registerRef, editMode]);
239
+
240
+ const modelUrl = gameObject.components?.model?.properties?.filename;
241
+
242
+ // In edit mode, create a proxy group at the same position for transform controls
243
+ // The GameInstance still needs the actual position so it renders correctly
244
+ if (editMode) {
245
+ return (
246
+ <>
247
+ {/* Proxy group for transform controls - uses LOCAL transform */}
248
+ <group
249
+ ref={groupRef}
250
+ position={localTransform.position}
251
+ rotation={localTransform.rotation}
252
+ scale={localTransform.scale}
253
+ onPointerDown={(e) => { e.stopPropagation(); clickValid.current = true; }}
254
+ onPointerMove={() => { clickValid.current = false; }}
255
+ onPointerUp={(e) => {
256
+ if (clickValid.current) {
257
+ e.stopPropagation();
258
+ onSelect?.(gameObject.id);
259
+ onClick?.(e, gameObject);
260
+ }
261
+ clickValid.current = false;
262
+ }}
263
+ >
264
+ {/* Tiny invisible mesh for raycasting/selection */}
265
+ <mesh visible={false}>
266
+ <boxGeometry args={[0.01, 0.01, 0.01]} />
267
+ </mesh>
268
+ </group>
269
+ {/* Actual instance rendered by provider - uses WORLD transform */}
270
+ <GameInstance
271
+ id={gameObject.id}
272
+ modelUrl={modelUrl}
273
+ position={worldPosition}
274
+ rotation={worldRotation}
275
+ scale={worldScale}
276
+ physics={physicsProps}
277
+ />
278
+ </>
279
+ );
280
+ }
281
+
190
282
  return (
191
283
  <GameInstance
192
284
  id={gameObject.id}
193
285
  modelUrl={gameObject.components?.model?.properties?.filename}
194
- position={position}
195
- rotation={rotation}
196
- scale={scale}
197
- physics={editMode ? undefined : physicsProps}
286
+ position={worldPosition}
287
+ rotation={worldRotation}
288
+ scale={worldScale}
289
+ physics={physicsProps}
198
290
  />
199
291
  );
200
292
  }
@@ -207,6 +299,7 @@ function StandardNode({
207
299
  gameObject,
208
300
  selectedId,
209
301
  onSelect,
302
+ onClick,
210
303
  registerRef,
211
304
  loadedModels,
212
305
  loadedTextures,
@@ -215,12 +308,16 @@ function StandardNode({
215
308
  }: RendererProps) {
216
309
 
217
310
  const groupRef = useRef<Object3D | null>(null);
311
+ const helperRef = useRef<Object3D | null>(null);
218
312
  const clickValid = useRef(false);
219
313
  const isSelected = selectedId === gameObject.id;
220
- const helperRef = groupRef as React.RefObject<Object3D>;
221
314
 
315
+ // Check if this object still exists as an instance (to prevent physics overlap)
316
+ const stillInstanced = useInstanceCheck(gameObject.id);
317
+
318
+ // Use helperRef for BoxHelper (shows actual content bounds at correct position)
222
319
  useHelper(
223
- editMode && isSelected ? helperRef : null,
320
+ editMode && isSelected ? helperRef as React.RefObject<Object3D> : null,
224
321
  BoxHelper,
225
322
  "cyan"
226
323
  );
@@ -241,17 +338,27 @@ function StandardNode({
241
338
  if (clickValid.current) {
242
339
  e.stopPropagation();
243
340
  onSelect?.(gameObject.id);
341
+ onClick?.(e, gameObject);
244
342
  }
245
343
  clickValid.current = false;
246
344
  };
247
345
 
346
+ const physics = gameObject.components?.physics;
347
+ const ready = !gameObject.components?.model ||
348
+ loadedModels[gameObject.components.model.properties.filename];
349
+ const hasPhysics = physics && ready && !stillInstanced;
350
+ const transform = getNodeTransformProps(gameObject);
351
+
352
+ // Prepare physics wrapper if needed
353
+ const physicsDef = hasPhysics ? getComponent("Physics") : null;
354
+ const isInstanced = gameObject.components?.model?.properties?.instanced;
355
+ const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
356
+
248
357
  const inner = (
249
358
  <group
250
- ref={groupRef}
251
- {...getNodeTransformProps(gameObject)}
252
- onPointerDown={onDown}
253
- onPointerMove={() => (clickValid.current = false)}
254
- onPointerUp={onUp}
359
+ onPointerDown={editMode ? onDown : undefined}
360
+ onPointerMove={editMode ? () => (clickValid.current = false) : undefined}
361
+ onPointerUp={editMode ? onUp : undefined}
255
362
  >
256
363
  {renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix)}
257
364
  {gameObject.children?.map(child => (
@@ -261,6 +368,7 @@ function StandardNode({
261
368
  gameObject={child}
262
369
  selectedId={selectedId}
263
370
  onSelect={onSelect}
371
+ onClick={onClick}
264
372
  registerRef={registerRef}
265
373
  loadedModels={loadedModels}
266
374
  loadedTextures={loadedTextures}
@@ -271,18 +379,73 @@ function StandardNode({
271
379
  </group>
272
380
  );
273
381
 
274
- const physics = gameObject.components?.physics;
275
- const ready = !gameObject.components?.model ||
276
- loadedModels[gameObject.components.model.properties.filename];
382
+ // In edit mode, use proxy group pattern
383
+ if (editMode) {
384
+ return (
385
+ <>
386
+ {/* Proxy group for transform controls - uses LOCAL transform */}
387
+ <group
388
+ ref={groupRef}
389
+ position={transform.position}
390
+ rotation={transform.rotation}
391
+ scale={transform.scale}
392
+ >
393
+ {/* Tiny invisible mesh for raycasting/selection */}
394
+ <mesh visible={false}>
395
+ <boxGeometry args={[0.01, 0.01, 0.01]} />
396
+ </mesh>
397
+ </group>
398
+ {/* Helper group for BoxHelper - same transform as proxy, contains actual geometry */}
399
+ <group
400
+ ref={helperRef}
401
+ position={transform.position}
402
+ rotation={transform.rotation}
403
+ scale={transform.scale}
404
+ >
405
+ {inner}
406
+ </group>
407
+ {/* Actual content with physics wrapper if needed */}
408
+ {hasPhysics && physicsDef?.View ? (
409
+ <physicsDef.View
410
+ key={physicsKey}
411
+ properties={physics.properties}
412
+ position={transform.position}
413
+ rotation={transform.rotation}
414
+ scale={transform.scale}
415
+ editMode={editMode}
416
+ >{inner}</physicsDef.View>
417
+ ) : null}
418
+ </>
419
+ );
420
+ }
277
421
 
278
- if (physics && !editMode && ready) {
279
- const def = getComponent("Physics");
280
- return def?.View
281
- ? <def.View properties={physics.properties}>{inner}</def.View>
282
- : inner;
422
+ // In play mode, apply transform directly to content
423
+ if (hasPhysics && physicsDef?.View) {
424
+ return (
425
+ <physicsDef.View
426
+ key={physicsKey}
427
+ properties={physics.properties}
428
+ position={transform.position}
429
+ rotation={transform.rotation}
430
+ scale={transform.scale}
431
+ editMode={editMode}
432
+ >{inner}</physicsDef.View>
433
+ );
283
434
  }
284
435
 
285
- return inner;
436
+ return (
437
+ <group
438
+ ref={groupRef}
439
+ position={transform.position}
440
+ rotation={transform.rotation}
441
+ scale={transform.scale}
442
+ onPointerDown={onDown}
443
+ onPointerMove={() => (clickValid.current = false)}
444
+ onPointerUp={onUp}
445
+ >
446
+ {inner}
447
+ </group>
448
+ );
286
449
  }
287
450
 
288
451
  /* -------------------------------------------------- */
@@ -293,6 +456,7 @@ interface RendererProps {
293
456
  gameObject: GameObjectType; // ← no longer optional
294
457
  selectedId?: string | null;
295
458
  onSelect?: (id: string) => void;
459
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
296
460
  registerRef: (id: string, obj: Object3D | null) => void;
297
461
  loadedModels: Record<string, Object3D>;
298
462
  loadedTextures: Record<string, Texture>;
@@ -1,7 +1,9 @@
1
- import { RigidBody } from "@react-three/rapier";
1
+ import { RigidBody, RapierRigidBody } from "@react-three/rapier";
2
2
  import type { ReactNode } from 'react';
3
+ import { useEffect, useRef } from 'react';
3
4
  import { Component } from "./ComponentRegistry";
4
5
  import { Label } from "./Input";
6
+ import { Quaternion, Euler } from 'three';
5
7
 
6
8
  export interface PhysicsProps {
7
9
  type: "fixed" | "dynamic";
@@ -52,18 +54,29 @@ interface PhysicsViewProps {
52
54
  properties: { type?: 'dynamic' | 'fixed'; collider?: string };
53
55
  editMode?: boolean;
54
56
  children?: ReactNode;
57
+ position?: [number, number, number];
58
+ rotation?: [number, number, number];
59
+ scale?: [number, number, number];
55
60
  }
56
61
 
57
- function PhysicsComponentView({ properties, editMode, children }: PhysicsViewProps) {
58
- if (editMode) return <>{children}</>;
59
-
62
+ function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }: PhysicsViewProps) {
60
63
  const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
61
64
 
62
- // Remount RigidBody when collider/type changes to avoid Rapier hook dependency warnings
63
- const rbKey = `${properties.type || 'dynamic'}_${colliders}`;
65
+ // In edit mode, include position/rotation in key to force remount when transform changes
66
+ // This ensures the RigidBody debug visualization updates even when physics is paused
67
+ const rbKey = editMode
68
+ ? `${properties.type || 'dynamic'}_${colliders}_${position?.join(',')}_${rotation?.join(',')}`
69
+ : `${properties.type || 'dynamic'}_${colliders}`;
64
70
 
65
71
  return (
66
- <RigidBody key={rbKey} type={properties.type} colliders={colliders as any}>
72
+ <RigidBody
73
+ key={rbKey}
74
+ type={properties.type}
75
+ colliders={colliders as any}
76
+ position={position}
77
+ rotation={rotation}
78
+ scale={scale}
79
+ >
67
80
  {children}
68
81
  </RigidBody>
69
82
  );