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