react-three-game 0.0.36 → 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());
@@ -106,7 +106,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
106
106
  });
107
107
  }, [data, models, textures]);
108
108
  /* ---------------- Render ---------------- */
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, 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 }))] }))] }));
110
110
  });
111
111
  /* -------------------------------------------------- */
112
112
  /* Renderer Switch */
@@ -116,9 +116,26 @@ export function GameObjectRenderer(props) {
116
116
  const node = props.gameObject;
117
117
  if (!node || node.hidden || node.disabled)
118
118
  return null;
119
- 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)
120
- ? _jsx(InstancedNode, Object.assign({}, props))
121
- : _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);
122
139
  }
123
140
  /* -------------------------------------------------- */
124
141
  /* InstancedNode (terminal) */
@@ -126,24 +143,50 @@ export function GameObjectRenderer(props) {
126
143
  function isPhysicsProps(v) {
127
144
  return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic";
128
145
  }
129
- function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode }) {
130
- 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;
131
148
  const world = parentMatrix.clone().multiply(compose(gameObject));
132
- 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);
133
152
  const physicsProps = isPhysicsProps((_b = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics) === null || _b === void 0 ? void 0 : _b.properties)
134
153
  ? (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.physics) === null || _d === void 0 ? void 0 : _d.properties
135
154
  : undefined;
136
- 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 }));
137
177
  }
138
178
  /* -------------------------------------------------- */
139
179
  /* StandardNode */
140
180
  /* -------------------------------------------------- */
141
- function StandardNode({ gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
142
- 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;
143
183
  const groupRef = useRef(null);
184
+ const helperRef = useRef(null);
144
185
  const clickValid = useRef(false);
145
186
  const isSelected = selectedId === gameObject.id;
146
- 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)
147
190
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
148
191
  useEffect(() => {
149
192
  registerRef(gameObject.id, groupRef.current);
@@ -158,20 +201,29 @@ function StandardNode({ gameObject, selectedId, onSelect, registerRef, loadedMod
158
201
  if (clickValid.current) {
159
202
  e.stopPropagation();
160
203
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(gameObject.id);
204
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
161
205
  }
162
206
  clickValid.current = false;
163
207
  };
164
- 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)))] })));
165
- const physics = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.physics;
166
- 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) ||
167
210
  loadedModels[gameObject.components.model.properties.filename];
168
- if (physics && !editMode && ready) {
169
- const def = getComponent("Physics");
170
- return (def === null || def === void 0 ? void 0 : def.View)
171
- ? _jsx(def.View, { properties: physics.properties, children: inner })
172
- : 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));
173
225
  }
174
- 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 }));
175
227
  }
176
228
  function walk(node, fn) {
177
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.36",
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>>({});
@@ -148,6 +149,7 @@ export const PrefabRoot = forwardRef<Group, {
148
149
  gameObject={data.root}
149
150
  selectedId={selectedId}
150
151
  onSelect={editMode ? onSelect : undefined}
152
+ onClick={onClick}
151
153
  registerRef={registerRef}
152
154
  loadedModels={models}
153
155
  loadedTextures={textures}
@@ -180,9 +182,29 @@ export const PrefabRoot = forwardRef<Group, {
180
182
  export function GameObjectRenderer(props: RendererProps) {
181
183
  const node = props.gameObject;
182
184
  if (!node || node.hidden || node.disabled) return null;
183
- return node.components?.model?.properties?.instanced
184
- ? <InstancedNode {...props} />
185
- : <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} />;
186
208
  }
187
209
 
188
210
  /* -------------------------------------------------- */
@@ -192,23 +214,79 @@ function isPhysicsProps(v: any): v is PhysicsProps {
192
214
  return v?.type === "fixed" || v?.type === "dynamic";
193
215
  }
194
216
 
195
- function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode }: RendererProps) {
217
+ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, registerRef, selectedId: _selectedId, onSelect, onClick }: RendererProps) {
196
218
  const world = parentMatrix.clone().multiply(compose(gameObject));
197
- 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
+
198
224
  const physicsProps = isPhysicsProps(
199
225
  gameObject.components?.physics?.properties
200
226
  )
201
227
  ? gameObject.components?.physics?.properties
202
228
  : undefined;
203
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
+
204
282
  return (
205
283
  <GameInstance
206
284
  id={gameObject.id}
207
285
  modelUrl={gameObject.components?.model?.properties?.filename}
208
- position={position}
209
- rotation={rotation}
210
- scale={scale}
211
- physics={editMode ? undefined : physicsProps}
286
+ position={worldPosition}
287
+ rotation={worldRotation}
288
+ scale={worldScale}
289
+ physics={physicsProps}
212
290
  />
213
291
  );
214
292
  }
@@ -221,6 +299,7 @@ function StandardNode({
221
299
  gameObject,
222
300
  selectedId,
223
301
  onSelect,
302
+ onClick,
224
303
  registerRef,
225
304
  loadedModels,
226
305
  loadedTextures,
@@ -229,12 +308,16 @@ function StandardNode({
229
308
  }: RendererProps) {
230
309
 
231
310
  const groupRef = useRef<Object3D | null>(null);
311
+ const helperRef = useRef<Object3D | null>(null);
232
312
  const clickValid = useRef(false);
233
313
  const isSelected = selectedId === gameObject.id;
234
- const helperRef = groupRef as React.RefObject<Object3D>;
235
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)
236
319
  useHelper(
237
- editMode && isSelected ? helperRef : null,
320
+ editMode && isSelected ? helperRef as React.RefObject<Object3D> : null,
238
321
  BoxHelper,
239
322
  "cyan"
240
323
  );
@@ -255,17 +338,27 @@ function StandardNode({
255
338
  if (clickValid.current) {
256
339
  e.stopPropagation();
257
340
  onSelect?.(gameObject.id);
341
+ onClick?.(e, gameObject);
258
342
  }
259
343
  clickValid.current = false;
260
344
  };
261
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
+
262
357
  const inner = (
263
358
  <group
264
- ref={groupRef}
265
- {...getNodeTransformProps(gameObject)}
266
- onPointerDown={onDown}
267
- onPointerMove={() => (clickValid.current = false)}
268
- onPointerUp={onUp}
359
+ onPointerDown={editMode ? onDown : undefined}
360
+ onPointerMove={editMode ? () => (clickValid.current = false) : undefined}
361
+ onPointerUp={editMode ? onUp : undefined}
269
362
  >
270
363
  {renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix)}
271
364
  {gameObject.children?.map(child => (
@@ -275,6 +368,7 @@ function StandardNode({
275
368
  gameObject={child}
276
369
  selectedId={selectedId}
277
370
  onSelect={onSelect}
371
+ onClick={onClick}
278
372
  registerRef={registerRef}
279
373
  loadedModels={loadedModels}
280
374
  loadedTextures={loadedTextures}
@@ -285,18 +379,73 @@ function StandardNode({
285
379
  </group>
286
380
  );
287
381
 
288
- const physics = gameObject.components?.physics;
289
- const ready = !gameObject.components?.model ||
290
- 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
+ }
291
421
 
292
- if (physics && !editMode && ready) {
293
- const def = getComponent("Physics");
294
- return def?.View
295
- ? <def.View properties={physics.properties}>{inner}</def.View>
296
- : 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
+ );
297
434
  }
298
435
 
299
- 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
+ );
300
449
  }
301
450
 
302
451
  /* -------------------------------------------------- */
@@ -307,6 +456,7 @@ interface RendererProps {
307
456
  gameObject: GameObjectType; // ← no longer optional
308
457
  selectedId?: string | null;
309
458
  onSelect?: (id: string) => void;
459
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
310
460
  registerRef: (id: string, obj: Object3D | null) => void;
311
461
  loadedModels: Record<string, Object3D>;
312
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
  );