react-three-game 0.0.20 → 0.0.22
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.js +28 -27
- package/dist/tools/prefabeditor/PrefabRoot.js +13 -17
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +10 -46
- package/dist/tools/prefabeditor/components/MaterialComponent.js +1 -1
- package/dist/tools/prefabeditor/components/ModelComponent.js +13 -9
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +11 -9
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
- package/package.json +1 -1
- package/src/tools/prefabeditor/InstanceProvider.tsx +64 -42
- package/src/tools/prefabeditor/PrefabRoot.tsx +19 -23
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +20 -61
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +10 -8
- package/src/tools/prefabeditor/components/ModelComponent.tsx +13 -9
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +34 -22
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +2 -0
|
@@ -51,19 +51,19 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
51
51
|
});
|
|
52
52
|
}, []);
|
|
53
53
|
// Flatten all model meshes once (models → flat mesh parts)
|
|
54
|
+
// Note: Geometry is cloned with baked transforms for instancing
|
|
54
55
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
55
56
|
const flatMeshes = {};
|
|
56
57
|
const modelParts = {};
|
|
57
58
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
59
|
+
model.updateWorldMatrix(false, true);
|
|
60
|
+
const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
|
|
61
61
|
let partIndex = 0;
|
|
62
|
-
|
|
62
|
+
model.traverse((obj) => {
|
|
63
63
|
if (obj.isMesh) {
|
|
64
|
+
// Clone geometry and bake relative transform
|
|
64
65
|
const geom = obj.geometry.clone();
|
|
65
|
-
|
|
66
|
-
geom.applyMatrix4(relativeTransform);
|
|
66
|
+
geom.applyMatrix4(obj.matrixWorld.clone().premultiply(rootInverse));
|
|
67
67
|
const partKey = `${modelKey}__${partIndex}`;
|
|
68
68
|
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
69
69
|
partIndex++;
|
|
@@ -73,6 +73,12 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
73
73
|
});
|
|
74
74
|
return { flatMeshes, modelParts };
|
|
75
75
|
}, [models]);
|
|
76
|
+
// Cleanup geometries when models change
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
return () => {
|
|
79
|
+
Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
|
|
80
|
+
};
|
|
81
|
+
}, [flatMeshes]);
|
|
76
82
|
// Group instances by meshPath + physics type for batch rendering
|
|
77
83
|
const grouped = useMemo(() => {
|
|
78
84
|
var _a;
|
|
@@ -124,35 +130,30 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
|
124
130
|
rotation: inst.rotation,
|
|
125
131
|
scale: inst.scale,
|
|
126
132
|
})), [group.instances]);
|
|
127
|
-
|
|
133
|
+
const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
|
|
134
|
+
return (_jsx(InstancedRigidBodies, { instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
|
|
128
135
|
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
136
|
+
if (!mesh)
|
|
137
|
+
return null;
|
|
129
138
|
return (_jsx("instancedMesh", { args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
|
|
130
139
|
}) }));
|
|
131
140
|
}
|
|
132
141
|
// Render non-physics instances using Merged's per-instance groups
|
|
133
142
|
function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef }) {
|
|
143
|
+
// Pre-compute which Instance components exist for this model
|
|
144
|
+
const InstanceComponents = useMemo(() => Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean), [instancesMap, modelKey, partCount]);
|
|
145
|
+
return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, InstanceComponents: InstanceComponents, onSelect: onSelect, registerRef: registerRef }, inst.id))) }));
|
|
146
|
+
}
|
|
147
|
+
// Individual instance item with its own click state
|
|
148
|
+
function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef }) {
|
|
134
149
|
const clickValid = useRef(false);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (clickValid.current)
|
|
150
|
+
return (_jsx("group", { ref: (el) => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, el), position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
|
|
151
|
+
if (clickValid.current) {
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.id);
|
|
154
|
+
}
|
|
141
155
|
clickValid.current = false;
|
|
142
|
-
|
|
143
|
-
const handlePointerUp = (e, id) => {
|
|
144
|
-
if (clickValid.current) {
|
|
145
|
-
e.stopPropagation();
|
|
146
|
-
onSelect === null || onSelect === void 0 ? void 0 : onSelect(id);
|
|
147
|
-
}
|
|
148
|
-
clickValid.current = false;
|
|
149
|
-
};
|
|
150
|
-
return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx("group", { ref: (el) => { registerRef === null || registerRef === void 0 ? void 0 : registerRef(inst.id, el); }, position: inst.position, rotation: inst.rotation, scale: inst.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: (e) => handlePointerUp(e, inst.id), children: Array.from({ length: partCount }).map((_, i) => {
|
|
151
|
-
const Instance = instancesMap[`${modelKey}__${i}`];
|
|
152
|
-
if (!Instance)
|
|
153
|
-
return null;
|
|
154
|
-
return _jsx(Instance, {}, i);
|
|
155
|
-
}) }, inst.id))) }));
|
|
156
|
+
}, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
|
|
156
157
|
}
|
|
157
158
|
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
158
159
|
export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
@@ -108,7 +108,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
108
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 }))] })] });
|
|
109
109
|
});
|
|
110
110
|
function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = new Matrix4(), }) {
|
|
111
|
-
var _a, _b, _c, _d;
|
|
111
|
+
var _a, _b, _c, _d, _e;
|
|
112
112
|
// Early return if gameObject is null or undefined
|
|
113
113
|
if (!gameObject)
|
|
114
114
|
return null;
|
|
@@ -144,12 +144,19 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
|
|
|
144
144
|
}
|
|
145
145
|
// --- 4. Render core content using component system ---
|
|
146
146
|
const core = renderCoreNode(gameObject, ctx, parentMatrix);
|
|
147
|
-
// --- 5.
|
|
148
|
-
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
149
|
-
// --- 6. Render children recursively (always relative transforms) ---
|
|
147
|
+
// --- 5. Render children recursively (always relative transforms) ---
|
|
150
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)));
|
|
151
|
-
// ---
|
|
152
|
-
|
|
149
|
+
// --- 6. Inner content group with full transform ---
|
|
150
|
+
const innerGroup = (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [core, children] }));
|
|
151
|
+
// --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
|
|
152
|
+
const physics = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.physics;
|
|
153
|
+
if (physics && !editMode) {
|
|
154
|
+
const physicsDef = getComponent('Physics');
|
|
155
|
+
if (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) {
|
|
156
|
+
return (_jsx(physicsDef.View, { properties: physics.properties, children: innerGroup }));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return innerGroup;
|
|
153
160
|
}
|
|
154
161
|
// Helper: render an instanced GameInstance (terminal node)
|
|
155
162
|
function renderInstancedNode(gameObject, worldMatrix, ctx) {
|
|
@@ -223,17 +230,6 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
223
230
|
return _jsx(View, Object.assign({ properties: properties }, contextProps, { children: content }), key);
|
|
224
231
|
}, coreContent);
|
|
225
232
|
}
|
|
226
|
-
// Helper: wrap core content with physics component when necessary
|
|
227
|
-
function wrapPhysicsIfNeeded(gameObject, content, ctx) {
|
|
228
|
-
var _a;
|
|
229
|
-
const physics = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics;
|
|
230
|
-
if (!physics)
|
|
231
|
-
return content;
|
|
232
|
-
const physicsDef = getComponent('Physics');
|
|
233
|
-
if (!physicsDef || !physicsDef.View)
|
|
234
|
-
return content;
|
|
235
|
-
return (_jsx(physicsDef.View, { properties: Object.assign(Object.assign({}, physics.properties), { id: gameObject.id }), editMode: ctx.editMode, children: content }));
|
|
236
|
-
}
|
|
237
233
|
export default PrefabRoot;
|
|
238
234
|
function getNodeTransformProps(node) {
|
|
239
235
|
var _a, _b, _c, _d, _e;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useEffect } from "react";
|
|
3
|
-
import { useFrame
|
|
4
|
-
import {
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { Vector3 } from "three";
|
|
5
5
|
function DirectionalLightComponentEditor({ component, onUpdate }) {
|
|
6
6
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
7
7
|
const props = {
|
|
@@ -32,60 +32,24 @@ function DirectionalLightView({ properties, editMode }) {
|
|
|
32
32
|
const shadowCameraLeft = (_j = properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30;
|
|
33
33
|
const shadowCameraRight = (_k = properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30;
|
|
34
34
|
const targetOffset = (_l = properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0];
|
|
35
|
-
const { scene } = useThree();
|
|
36
35
|
const directionalLightRef = useRef(null);
|
|
37
|
-
const targetRef = useRef(
|
|
38
|
-
|
|
39
|
-
// Add target to scene once
|
|
36
|
+
const targetRef = useRef(null);
|
|
37
|
+
// Set up light target reference when both refs are ready
|
|
40
38
|
useEffect(() => {
|
|
41
|
-
|
|
42
|
-
scene.add(target);
|
|
43
|
-
return () => {
|
|
44
|
-
scene.remove(target);
|
|
45
|
-
};
|
|
46
|
-
}, [scene]);
|
|
47
|
-
// Set up light target reference once
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (directionalLightRef.current) {
|
|
39
|
+
if (directionalLightRef.current && targetRef.current) {
|
|
50
40
|
directionalLightRef.current.target = targetRef.current;
|
|
51
41
|
}
|
|
52
42
|
}, []);
|
|
53
|
-
// Update target position
|
|
43
|
+
// Update target world position based on light position + offset
|
|
54
44
|
useFrame(() => {
|
|
55
|
-
if (!directionalLightRef.current)
|
|
45
|
+
if (!directionalLightRef.current || !targetRef.current)
|
|
56
46
|
return;
|
|
57
47
|
const lightWorldPos = new Vector3();
|
|
58
48
|
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!targetRef.current.position.equals(newTargetPos)) {
|
|
62
|
-
targetRef.current.position.copy(newTargetPos);
|
|
63
|
-
if (directionalLightRef.current.shadow) {
|
|
64
|
-
directionalLightRef.current.shadow.needsUpdate = true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Update camera helper in edit mode
|
|
68
|
-
if (editMode && cameraHelperRef.current) {
|
|
69
|
-
cameraHelperRef.current.update();
|
|
70
|
-
}
|
|
49
|
+
// Target is positioned relative to light's world position
|
|
50
|
+
targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
|
|
71
51
|
});
|
|
72
|
-
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
var _a;
|
|
75
|
-
if (editMode && ((_a = directionalLightRef.current) === null || _a === void 0 ? void 0 : _a.shadow.camera)) {
|
|
76
|
-
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
77
|
-
cameraHelperRef.current = helper;
|
|
78
|
-
scene.add(helper);
|
|
79
|
-
return () => {
|
|
80
|
-
if (cameraHelperRef.current) {
|
|
81
|
-
scene.remove(cameraHelperRef.current);
|
|
82
|
-
cameraHelperRef.current.dispose();
|
|
83
|
-
cameraHelperRef.current = null;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}, [editMode, scene]);
|
|
88
|
-
return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize": [shadowMapSize, shadowMapSize], "shadow-bias": -0.001, "shadow-normalBias": 0.02, children: _jsx("orthographicCamera", { attach: "shadow-camera", near: shadowCameraNear, far: shadowCameraFar, top: shadowCameraTop, bottom: shadowCameraBottom, left: shadowCameraLeft, right: shadowCameraRight }) }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
|
|
52
|
+
return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": -0.001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
|
|
89
53
|
const points = [
|
|
90
54
|
new Vector3(0, 0, 0),
|
|
91
55
|
new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
|
|
@@ -53,7 +53,7 @@ function MaterialComponentView({ properties, loadedTextures, isSelected }) {
|
|
|
53
53
|
}
|
|
54
54
|
const { color, wireframe = false } = properties;
|
|
55
55
|
const displayColor = isSelected ? "cyan" : color;
|
|
56
|
-
return _jsx("meshStandardMaterial", { color: displayColor, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture, side: DoubleSide }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture');
|
|
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'));
|
|
57
57
|
}
|
|
58
58
|
const MaterialComponent = {
|
|
59
59
|
name: 'Material',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { ModelListViewer } from '../../assetviewer/page';
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
4
4
|
function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
5
5
|
const [modelFiles, setModelFiles] = useState([]);
|
|
6
6
|
useEffect(() => {
|
|
@@ -22,19 +22,23 @@ function ModelComponentView({ properties, loadedModels, children }) {
|
|
|
22
22
|
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
23
23
|
if (!properties.filename || properties.instanced)
|
|
24
24
|
return _jsx(_Fragment, { children: children });
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const sourceModel = loadedModels === null || loadedModels === void 0 ? void 0 : loadedModels[properties.filename];
|
|
26
|
+
// Clone model once and set up shadows - memoized to avoid cloning on every render
|
|
27
|
+
const clonedModel = useMemo(() => {
|
|
28
|
+
if (!sourceModel)
|
|
29
|
+
return null;
|
|
30
|
+
const clone = sourceModel.clone();
|
|
31
|
+
clone.traverse((obj) => {
|
|
29
32
|
if (obj.isMesh) {
|
|
30
33
|
obj.castShadow = true;
|
|
31
34
|
obj.receiveShadow = true;
|
|
32
35
|
}
|
|
33
36
|
});
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
return clone;
|
|
38
|
+
}, [sourceModel]);
|
|
39
|
+
if (!clonedModel)
|
|
40
|
+
return _jsx(_Fragment, { children: children });
|
|
41
|
+
return _jsx("primitive", { object: clonedModel, children: children });
|
|
38
42
|
}
|
|
39
43
|
const ModelComponent = {
|
|
40
44
|
name: 'Model',
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { RigidBody } from "@react-three/rapier";
|
|
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
|
+
const labelClass = "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5";
|
|
2
5
|
function PhysicsComponentEditor({ component, onUpdate }) {
|
|
3
|
-
|
|
6
|
+
const { type, collider = 'hull' } = component.properties;
|
|
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)" })] })] }));
|
|
4
8
|
}
|
|
5
|
-
|
|
6
|
-
function PhysicsComponentView({ properties, children, editMode }) {
|
|
9
|
+
function PhysicsComponentView({ properties, editMode, children }) {
|
|
7
10
|
if (editMode)
|
|
8
|
-
return children;
|
|
9
|
-
|
|
11
|
+
return _jsx(_Fragment, { children: children });
|
|
12
|
+
const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
|
|
13
|
+
return (_jsx(RigidBody, { type: properties.type, colliders: colliders, children: children }));
|
|
10
14
|
}
|
|
11
15
|
const PhysicsComponent = {
|
|
12
16
|
name: 'Physics',
|
|
13
17
|
Editor: PhysicsComponentEditor,
|
|
14
18
|
View: PhysicsComponentView,
|
|
15
|
-
defaultProperties: {
|
|
16
|
-
type: 'dynamic'
|
|
17
|
-
}
|
|
19
|
+
defaultProperties: { type: 'dynamic', collider: 'hull' }
|
|
18
20
|
};
|
|
19
21
|
export default PhysicsComponent;
|
|
@@ -27,7 +27,7 @@ function SpotLightView({ properties, editMode }) {
|
|
|
27
27
|
spotLightRef.current.target = targetRef.current;
|
|
28
28
|
}
|
|
29
29
|
}, []);
|
|
30
|
-
return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
|
|
31
31
|
}
|
|
32
32
|
const SpotLightComponent = {
|
|
33
33
|
name: 'SpotLight',
|
package/package.json
CHANGED
|
@@ -81,22 +81,21 @@ export function GameInstanceProvider({
|
|
|
81
81
|
}, []);
|
|
82
82
|
|
|
83
83
|
// Flatten all model meshes once (models → flat mesh parts)
|
|
84
|
+
// Note: Geometry is cloned with baked transforms for instancing
|
|
84
85
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
85
86
|
const flatMeshes: Record<string, Mesh> = {};
|
|
86
87
|
const modelParts: Record<string, number> = {};
|
|
87
88
|
|
|
88
89
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
90
|
+
model.updateWorldMatrix(false, true);
|
|
91
|
+
const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
|
|
92
92
|
|
|
93
93
|
let partIndex = 0;
|
|
94
|
-
|
|
95
|
-
root.traverse((obj: any) => {
|
|
94
|
+
model.traverse((obj: any) => {
|
|
96
95
|
if (obj.isMesh) {
|
|
96
|
+
// Clone geometry and bake relative transform
|
|
97
97
|
const geom = obj.geometry.clone();
|
|
98
|
-
|
|
99
|
-
geom.applyMatrix4(relativeTransform);
|
|
98
|
+
geom.applyMatrix4(obj.matrixWorld.clone().premultiply(rootInverse));
|
|
100
99
|
|
|
101
100
|
const partKey = `${modelKey}__${partIndex}`;
|
|
102
101
|
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
@@ -109,6 +108,13 @@ export function GameInstanceProvider({
|
|
|
109
108
|
return { flatMeshes, modelParts };
|
|
110
109
|
}, [models]);
|
|
111
110
|
|
|
111
|
+
// Cleanup geometries when models change
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
return () => {
|
|
114
|
+
Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
|
|
115
|
+
};
|
|
116
|
+
}, [flatMeshes]);
|
|
117
|
+
|
|
112
118
|
// Group instances by meshPath + physics type for batch rendering
|
|
113
119
|
const grouped = useMemo(() => {
|
|
114
120
|
const groups: Record<string, { physicsType: string, instances: InstanceData[] }> = {};
|
|
@@ -213,21 +219,24 @@ function InstancedRigidGroup({
|
|
|
213
219
|
[group.instances]
|
|
214
220
|
);
|
|
215
221
|
|
|
222
|
+
const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
|
|
223
|
+
|
|
216
224
|
return (
|
|
217
225
|
<InstancedRigidBodies
|
|
218
226
|
instances={instances}
|
|
219
|
-
colliders={
|
|
227
|
+
colliders={colliders}
|
|
220
228
|
type={group.physicsType as 'dynamic' | 'fixed'}
|
|
221
229
|
>
|
|
222
230
|
{Array.from({ length: partCount }).map((_, i) => {
|
|
223
231
|
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
232
|
+
if (!mesh) return null;
|
|
224
233
|
return (
|
|
225
234
|
<instancedMesh
|
|
226
235
|
key={i}
|
|
227
236
|
args={[mesh.geometry, mesh.material, group.instances.length]}
|
|
228
237
|
castShadow
|
|
229
238
|
receiveShadow
|
|
230
|
-
frustumCulled={false}
|
|
239
|
+
frustumCulled={false} // Required: culling first instance hides all
|
|
231
240
|
/>
|
|
232
241
|
);
|
|
233
242
|
})}
|
|
@@ -251,49 +260,62 @@ function NonPhysicsInstancedGroup({
|
|
|
251
260
|
onSelect?: (id: string | null) => void;
|
|
252
261
|
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
253
262
|
}) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const handlePointerMove = () => {
|
|
262
|
-
if (clickValid.current) clickValid.current = false;
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const handlePointerUp = (e: any, id: string) => {
|
|
266
|
-
if (clickValid.current) {
|
|
267
|
-
e.stopPropagation();
|
|
268
|
-
onSelect?.(id);
|
|
269
|
-
}
|
|
270
|
-
clickValid.current = false;
|
|
271
|
-
};
|
|
263
|
+
// Pre-compute which Instance components exist for this model
|
|
264
|
+
const InstanceComponents = useMemo(() =>
|
|
265
|
+
Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean),
|
|
266
|
+
[instancesMap, modelKey, partCount]
|
|
267
|
+
);
|
|
272
268
|
|
|
273
269
|
return (
|
|
274
270
|
<>
|
|
275
271
|
{group.instances.map(inst => (
|
|
276
|
-
<
|
|
272
|
+
<InstanceGroupItem
|
|
277
273
|
key={inst.id}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
onPointerMove={handlePointerMove}
|
|
284
|
-
onPointerUp={(e) => handlePointerUp(e, inst.id)}
|
|
285
|
-
>
|
|
286
|
-
{Array.from({ length: partCount }).map((_, i) => {
|
|
287
|
-
const Instance = instancesMap[`${modelKey}__${i}`];
|
|
288
|
-
if (!Instance) return null;
|
|
289
|
-
return <Instance key={i} />;
|
|
290
|
-
})}
|
|
291
|
-
</group>
|
|
274
|
+
instance={inst}
|
|
275
|
+
InstanceComponents={InstanceComponents}
|
|
276
|
+
onSelect={onSelect}
|
|
277
|
+
registerRef={registerRef}
|
|
278
|
+
/>
|
|
292
279
|
))}
|
|
293
280
|
</>
|
|
294
281
|
);
|
|
295
282
|
}
|
|
296
283
|
|
|
284
|
+
// Individual instance item with its own click state
|
|
285
|
+
function InstanceGroupItem({
|
|
286
|
+
instance,
|
|
287
|
+
InstanceComponents,
|
|
288
|
+
onSelect,
|
|
289
|
+
registerRef
|
|
290
|
+
}: {
|
|
291
|
+
instance: InstanceData;
|
|
292
|
+
InstanceComponents: React.ComponentType<any>[];
|
|
293
|
+
onSelect?: (id: string | null) => void;
|
|
294
|
+
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
295
|
+
}) {
|
|
296
|
+
const clickValid = useRef(false);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<group
|
|
300
|
+
ref={(el) => registerRef?.(instance.id, el)}
|
|
301
|
+
position={instance.position}
|
|
302
|
+
rotation={instance.rotation}
|
|
303
|
+
scale={instance.scale}
|
|
304
|
+
onPointerDown={(e) => { e.stopPropagation(); clickValid.current = true; }}
|
|
305
|
+
onPointerMove={() => { clickValid.current = false; }}
|
|
306
|
+
onPointerUp={(e) => {
|
|
307
|
+
if (clickValid.current) {
|
|
308
|
+
e.stopPropagation();
|
|
309
|
+
onSelect?.(instance.id);
|
|
310
|
+
}
|
|
311
|
+
clickValid.current = false;
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
{InstanceComponents.map((Instance, i) => <Instance key={i} />)}
|
|
315
|
+
</group>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
297
319
|
|
|
298
320
|
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
299
321
|
export const GameInstance = React.forwardRef<Group, {
|
|
@@ -222,10 +222,7 @@ function GameObjectRenderer({
|
|
|
222
222
|
// --- 4. Render core content using component system ---
|
|
223
223
|
const core = renderCoreNode(gameObject, ctx, parentMatrix);
|
|
224
224
|
|
|
225
|
-
// --- 5.
|
|
226
|
-
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
227
|
-
|
|
228
|
-
// --- 6. Render children recursively (always relative transforms) ---
|
|
225
|
+
// --- 5. Render children recursively (always relative transforms) ---
|
|
229
226
|
const children = (gameObject.children ?? []).map((child) => (
|
|
230
227
|
<GameObjectRenderer
|
|
231
228
|
key={child.id}
|
|
@@ -240,8 +237,8 @@ function GameObjectRenderer({
|
|
|
240
237
|
/>
|
|
241
238
|
));
|
|
242
239
|
|
|
243
|
-
// ---
|
|
244
|
-
|
|
240
|
+
// --- 6. Inner content group with full transform ---
|
|
241
|
+
const innerGroup = (
|
|
245
242
|
<group
|
|
246
243
|
ref={(el) => registerRef(gameObject.id, el)}
|
|
247
244
|
position={transformProps.position}
|
|
@@ -251,10 +248,25 @@ function GameObjectRenderer({
|
|
|
251
248
|
onPointerMove={handlePointerMove}
|
|
252
249
|
onPointerUp={handlePointerUp}
|
|
253
250
|
>
|
|
254
|
-
{
|
|
251
|
+
{core}
|
|
255
252
|
{children}
|
|
256
253
|
</group>
|
|
257
254
|
);
|
|
255
|
+
|
|
256
|
+
// --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
|
|
257
|
+
const physics = gameObject.components?.physics;
|
|
258
|
+
if (physics && !editMode) {
|
|
259
|
+
const physicsDef = getComponent('Physics');
|
|
260
|
+
if (physicsDef?.View) {
|
|
261
|
+
return (
|
|
262
|
+
<physicsDef.View properties={physics.properties}>
|
|
263
|
+
{innerGroup}
|
|
264
|
+
</physicsDef.View>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return innerGroup;
|
|
258
270
|
}
|
|
259
271
|
|
|
260
272
|
// Helper: render an instanced GameInstance (terminal node)
|
|
@@ -364,22 +376,6 @@ function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matr
|
|
|
364
376
|
}, coreContent);
|
|
365
377
|
}
|
|
366
378
|
|
|
367
|
-
// Helper: wrap core content with physics component when necessary
|
|
368
|
-
function wrapPhysicsIfNeeded(gameObject: GameObjectType, content: React.ReactNode, ctx: any) {
|
|
369
|
-
const physics = gameObject.components?.physics;
|
|
370
|
-
if (!physics) return content;
|
|
371
|
-
const physicsDef = getComponent('Physics');
|
|
372
|
-
if (!physicsDef || !physicsDef.View) return content;
|
|
373
|
-
return (
|
|
374
|
-
<physicsDef.View
|
|
375
|
-
properties={{ ...physics.properties, id: gameObject.id }}
|
|
376
|
-
editMode={ctx.editMode}
|
|
377
|
-
>
|
|
378
|
-
{content}
|
|
379
|
-
</physicsDef.View>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
379
|
|
|
384
380
|
|
|
385
381
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
2
|
import { useRef, useEffect } from "react";
|
|
3
|
-
import { useFrame
|
|
4
|
-
import {
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { DirectionalLight, Object3D, Vector3 } from "three";
|
|
5
5
|
|
|
6
6
|
function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
7
7
|
const props = {
|
|
@@ -190,71 +190,31 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
190
190
|
const shadowCameraRight = properties.shadowCameraRight ?? 30;
|
|
191
191
|
const targetOffset = properties.targetOffset ?? [0, -5, 0];
|
|
192
192
|
|
|
193
|
-
const { scene } = useThree();
|
|
194
193
|
const directionalLightRef = useRef<DirectionalLight>(null);
|
|
195
|
-
const targetRef = useRef<Object3D>(
|
|
196
|
-
const cameraHelperRef = useRef<CameraHelper | null>(null);
|
|
194
|
+
const targetRef = useRef<Object3D>(null);
|
|
197
195
|
|
|
198
|
-
//
|
|
196
|
+
// Set up light target reference when both refs are ready
|
|
199
197
|
useEffect(() => {
|
|
200
|
-
|
|
201
|
-
scene.add(target);
|
|
202
|
-
return () => {
|
|
203
|
-
scene.remove(target);
|
|
204
|
-
};
|
|
205
|
-
}, [scene]);
|
|
206
|
-
|
|
207
|
-
// Set up light target reference once
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (directionalLightRef.current) {
|
|
198
|
+
if (directionalLightRef.current && targetRef.current) {
|
|
210
199
|
directionalLightRef.current.target = targetRef.current;
|
|
211
200
|
}
|
|
212
201
|
}, []);
|
|
213
202
|
|
|
214
|
-
// Update target position
|
|
203
|
+
// Update target world position based on light position + offset
|
|
215
204
|
useFrame(() => {
|
|
216
|
-
if (!directionalLightRef.current) return;
|
|
205
|
+
if (!directionalLightRef.current || !targetRef.current) return;
|
|
217
206
|
|
|
218
207
|
const lightWorldPos = new Vector3();
|
|
219
208
|
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
220
209
|
|
|
221
|
-
|
|
210
|
+
// Target is positioned relative to light's world position
|
|
211
|
+
targetRef.current.position.set(
|
|
222
212
|
lightWorldPos.x + targetOffset[0],
|
|
223
213
|
lightWorldPos.y + targetOffset[1],
|
|
224
214
|
lightWorldPos.z + targetOffset[2]
|
|
225
215
|
);
|
|
226
|
-
|
|
227
|
-
// Only update if position actually changed
|
|
228
|
-
if (!targetRef.current.position.equals(newTargetPos)) {
|
|
229
|
-
targetRef.current.position.copy(newTargetPos);
|
|
230
|
-
if (directionalLightRef.current.shadow) {
|
|
231
|
-
directionalLightRef.current.shadow.needsUpdate = true;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Update camera helper in edit mode
|
|
236
|
-
if (editMode && cameraHelperRef.current) {
|
|
237
|
-
cameraHelperRef.current.update();
|
|
238
|
-
}
|
|
239
216
|
});
|
|
240
217
|
|
|
241
|
-
// Create/destroy camera helper for edit mode
|
|
242
|
-
useEffect(() => {
|
|
243
|
-
if (editMode && directionalLightRef.current?.shadow.camera) {
|
|
244
|
-
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
245
|
-
cameraHelperRef.current = helper;
|
|
246
|
-
scene.add(helper);
|
|
247
|
-
|
|
248
|
-
return () => {
|
|
249
|
-
if (cameraHelperRef.current) {
|
|
250
|
-
scene.remove(cameraHelperRef.current);
|
|
251
|
-
cameraHelperRef.current.dispose();
|
|
252
|
-
cameraHelperRef.current = null;
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
}, [editMode, scene]);
|
|
257
|
-
|
|
258
218
|
return (
|
|
259
219
|
<>
|
|
260
220
|
<directionalLight
|
|
@@ -262,20 +222,19 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
262
222
|
color={color}
|
|
263
223
|
intensity={intensity}
|
|
264
224
|
castShadow={castShadow}
|
|
265
|
-
shadow-mapSize={
|
|
225
|
+
shadow-mapSize-width={shadowMapSize}
|
|
226
|
+
shadow-mapSize-height={shadowMapSize}
|
|
227
|
+
shadow-camera-near={shadowCameraNear}
|
|
228
|
+
shadow-camera-far={shadowCameraFar}
|
|
229
|
+
shadow-camera-top={shadowCameraTop}
|
|
230
|
+
shadow-camera-bottom={shadowCameraBottom}
|
|
231
|
+
shadow-camera-left={shadowCameraLeft}
|
|
232
|
+
shadow-camera-right={shadowCameraRight}
|
|
266
233
|
shadow-bias={-0.001}
|
|
267
234
|
shadow-normalBias={0.02}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
near={shadowCameraNear}
|
|
272
|
-
far={shadowCameraFar}
|
|
273
|
-
top={shadowCameraTop}
|
|
274
|
-
bottom={shadowCameraBottom}
|
|
275
|
-
left={shadowCameraLeft}
|
|
276
|
-
right={shadowCameraRight}
|
|
277
|
-
/>
|
|
278
|
-
</directionalLight>
|
|
235
|
+
/>
|
|
236
|
+
{/* Target object - rendered declaratively in scene graph */}
|
|
237
|
+
<object3D ref={targetRef} />
|
|
279
238
|
{editMode && (
|
|
280
239
|
<>
|
|
281
240
|
{/* Light source indicator */}
|
|
@@ -130,14 +130,16 @@ function MaterialComponentView({ properties, loadedTextures, isSelected }: { pro
|
|
|
130
130
|
const { color, wireframe = false } = properties;
|
|
131
131
|
const displayColor = isSelected ? "cyan" : color;
|
|
132
132
|
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
return (
|
|
134
|
+
<meshStandardMaterial
|
|
135
|
+
key={finalTexture?.uuid ?? 'no-texture'}
|
|
136
|
+
color={displayColor}
|
|
137
|
+
wireframe={wireframe}
|
|
138
|
+
map={finalTexture}
|
|
139
|
+
transparent={!!finalTexture}
|
|
140
|
+
side={DoubleSide}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
const MaterialComponent: Component = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModelListViewer } from '../../assetviewer/page';
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
3
3
|
import { Component } from './ComponentRegistry';
|
|
4
4
|
|
|
5
5
|
function ModelComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
@@ -49,20 +49,24 @@ function ModelComponentView({ properties, loadedModels, children }: { properties
|
|
|
49
49
|
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
50
50
|
if (!properties.filename || properties.instanced) return <>{children}</>;
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const sourceModel = loadedModels?.[properties.filename];
|
|
53
|
+
|
|
54
|
+
// Clone model once and set up shadows - memoized to avoid cloning on every render
|
|
55
|
+
const clonedModel = useMemo(() => {
|
|
56
|
+
if (!sourceModel) return null;
|
|
57
|
+
const clone = sourceModel.clone();
|
|
58
|
+
clone.traverse((obj: any) => {
|
|
56
59
|
if (obj.isMesh) {
|
|
57
60
|
obj.castShadow = true;
|
|
58
61
|
obj.receiveShadow = true;
|
|
59
62
|
}
|
|
60
63
|
});
|
|
61
|
-
return
|
|
62
|
-
}
|
|
64
|
+
return clone;
|
|
65
|
+
}, [sourceModel]);
|
|
66
|
+
|
|
67
|
+
if (!clonedModel) return <>{children}</>;
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
return <>{children}</>;
|
|
69
|
+
return <primitive object={clonedModel}>{children}</primitive>;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const ModelComponent: Component = {
|
|
@@ -1,29 +1,43 @@
|
|
|
1
|
+
import { RigidBody } from "@react-three/rapier";
|
|
1
2
|
import { Component } from "./ComponentRegistry";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
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
|
+
const labelClass = "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5";
|
|
6
|
+
|
|
7
|
+
function PhysicsComponentEditor({ component, onUpdate }: { component: any; onUpdate: (props: any) => void }) {
|
|
8
|
+
const { type, collider = 'hull' } = component.properties;
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<label className={labelClass}>Type</label>
|
|
12
|
+
<select className={selectClass} value={type} onChange={e => onUpdate({ type: e.target.value })}>
|
|
13
|
+
<option value="dynamic">Dynamic</option>
|
|
14
|
+
<option value="fixed">Fixed</option>
|
|
15
|
+
</select>
|
|
16
|
+
|
|
17
|
+
<label className={`${labelClass} mt-2`}>Collider</label>
|
|
18
|
+
<select className={selectClass} value={collider} onChange={e => onUpdate({ collider: e.target.value })}>
|
|
19
|
+
<option value="hull">Hull (convex)</option>
|
|
20
|
+
<option value="trimesh">Trimesh (exact)</option>
|
|
21
|
+
<option value="cuboid">Cuboid (box)</option>
|
|
22
|
+
<option value="ball">Ball (sphere)</option>
|
|
23
|
+
</select>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PhysicsViewProps {
|
|
29
|
+
properties: { type: 'dynamic' | 'fixed'; collider?: string };
|
|
30
|
+
editMode?: boolean;
|
|
31
|
+
children?: React.ReactNode;
|
|
15
32
|
}
|
|
16
33
|
|
|
34
|
+
function PhysicsComponentView({ properties, editMode, children }: PhysicsViewProps) {
|
|
35
|
+
if (editMode) return <>{children}</>;
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
|
|
19
38
|
|
|
20
|
-
function PhysicsComponentView({ properties, children, editMode }: any) {
|
|
21
|
-
if (editMode) return children;
|
|
22
39
|
return (
|
|
23
|
-
<RigidBody
|
|
24
|
-
type={properties.type}
|
|
25
|
-
colliders="cuboid"
|
|
26
|
-
>
|
|
40
|
+
<RigidBody type={properties.type} colliders={colliders as any}>
|
|
27
41
|
{children}
|
|
28
42
|
</RigidBody>
|
|
29
43
|
);
|
|
@@ -33,9 +47,7 @@ const PhysicsComponent: Component = {
|
|
|
33
47
|
name: 'Physics',
|
|
34
48
|
Editor: PhysicsComponentEditor,
|
|
35
49
|
View: PhysicsComponentView,
|
|
36
|
-
defaultProperties: {
|
|
37
|
-
type: 'dynamic'
|
|
38
|
-
}
|
|
50
|
+
defaultProperties: { type: 'dynamic', collider: 'hull' }
|
|
39
51
|
};
|
|
40
52
|
|
|
41
53
|
export default PhysicsComponent;
|
|
@@ -113,6 +113,8 @@ function SpotLightView({ properties, editMode }: { properties: any; editMode?: b
|
|
|
113
113
|
penumbra={penumbra}
|
|
114
114
|
distance={distance}
|
|
115
115
|
castShadow={castShadow}
|
|
116
|
+
shadow-mapSize-width={1024}
|
|
117
|
+
shadow-mapSize-height={1024}
|
|
116
118
|
shadow-bias={-0.0001}
|
|
117
119
|
shadow-normalBias={0.02}
|
|
118
120
|
/>
|