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.
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +51 -7
- package/dist/tools/prefabeditor/PrefabEditor.js +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +87 -22
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +8 -7
- package/package.json +1 -1
- package/src/tools/prefabeditor/InstanceProvider.tsx +60 -4
- package/src/tools/prefabeditor/PrefabEditor.tsx +1 -1
- package/src/tools/prefabeditor/PrefabRoot.tsx +191 -27
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +20 -7
|
@@ -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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
const
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
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
|
@@ -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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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={
|
|
195
|
-
rotation={
|
|
196
|
-
scale={
|
|
197
|
-
physics={
|
|
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
|
-
|
|
251
|
-
{
|
|
252
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
63
|
-
|
|
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
|
|
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
|
);
|