react-three-game 0.0.17 → 0.0.18
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 +4 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
- package/dist/tools/prefabeditor/PrefabRoot.js +26 -27
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
- package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
- package/package.json +3 -3
- package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
- package/src/tools/prefabeditor/PrefabRoot.tsx +30 -41
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
- package/src/tools/prefabeditor/components/index.ts +2 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import
|
|
2
|
+
import { Object3D, Group } from "three";
|
|
3
3
|
export type InstanceData = {
|
|
4
4
|
id: string;
|
|
5
5
|
position: [number, number, number];
|
|
@@ -13,10 +13,10 @@ export type InstanceData = {
|
|
|
13
13
|
export declare function GameInstanceProvider({ children, models, onSelect, registerRef }: {
|
|
14
14
|
children: React.ReactNode;
|
|
15
15
|
models: {
|
|
16
|
-
[filename: string]:
|
|
16
|
+
[filename: string]: Object3D;
|
|
17
17
|
};
|
|
18
18
|
onSelect?: (id: string | null) => void;
|
|
19
|
-
registerRef?: (id: string, obj:
|
|
19
|
+
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
20
20
|
}): import("react/jsx-runtime").JSX.Element;
|
|
21
21
|
export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
22
22
|
id: string;
|
|
@@ -27,4 +27,4 @@ export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
|
27
27
|
physics?: {
|
|
28
28
|
type: "dynamic" | "fixed";
|
|
29
29
|
};
|
|
30
|
-
} & React.RefAttributes<
|
|
30
|
+
} & React.RefAttributes<Group<import("three").Object3DEventMap>>>;
|
|
@@ -1,8 +1,9 @@
|
|
|
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
3
|
import { Merged } from '@react-three/drei';
|
|
4
|
-
import * as THREE from 'three';
|
|
5
4
|
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
5
|
+
import { Mesh, Matrix4 } from "three";
|
|
6
|
+
// Helper functions for comparison
|
|
6
7
|
function arrayEquals(a, b) {
|
|
7
8
|
if (a === b)
|
|
8
9
|
return true;
|
|
@@ -30,6 +31,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
30
31
|
setInstances(prev => {
|
|
31
32
|
const idx = prev.findIndex(i => i.id === instance.id);
|
|
32
33
|
if (idx !== -1) {
|
|
34
|
+
// Update existing if changed
|
|
33
35
|
if (instanceEquals(prev[idx], instance)) {
|
|
34
36
|
return prev;
|
|
35
37
|
}
|
|
@@ -37,6 +39,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
37
39
|
copy[idx] = instance;
|
|
38
40
|
return copy;
|
|
39
41
|
}
|
|
42
|
+
// Add new
|
|
40
43
|
return [...prev, instance];
|
|
41
44
|
});
|
|
42
45
|
}, []);
|
|
@@ -47,14 +50,14 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
47
50
|
return prev.filter(i => i.id !== id);
|
|
48
51
|
});
|
|
49
52
|
}, []);
|
|
50
|
-
// Flatten all model meshes once
|
|
53
|
+
// Flatten all model meshes once (models → flat mesh parts)
|
|
51
54
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
52
55
|
const flatMeshes = {};
|
|
53
56
|
const modelParts = {};
|
|
54
57
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
55
58
|
const root = model;
|
|
56
59
|
root.updateWorldMatrix(false, true);
|
|
57
|
-
const rootInverse = new
|
|
60
|
+
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
58
61
|
let partIndex = 0;
|
|
59
62
|
root.traverse((obj) => {
|
|
60
63
|
if (obj.isMesh) {
|
|
@@ -62,7 +65,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
62
65
|
const relativeTransform = obj.matrixWorld.clone().premultiply(rootInverse);
|
|
63
66
|
geom.applyMatrix4(relativeTransform);
|
|
64
67
|
const partKey = `${modelKey}__${partIndex}`;
|
|
65
|
-
flatMeshes[partKey] = new
|
|
68
|
+
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
66
69
|
partIndex++;
|
|
67
70
|
}
|
|
68
71
|
});
|
|
@@ -70,7 +73,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
70
73
|
});
|
|
71
74
|
return { flatMeshes, modelParts };
|
|
72
75
|
}, [models]);
|
|
73
|
-
// Group instances by meshPath + physics type
|
|
76
|
+
// Group instances by meshPath + physics type for batch rendering
|
|
74
77
|
const grouped = useMemo(() => {
|
|
75
78
|
var _a;
|
|
76
79
|
const groups = {};
|
|
@@ -104,7 +107,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
104
107
|
const partCount = modelParts[modelKey] || 0;
|
|
105
108
|
if (partCount === 0)
|
|
106
109
|
return null;
|
|
107
|
-
//
|
|
110
|
+
// Create mesh subset for this specific model
|
|
108
111
|
const meshesForModel = {};
|
|
109
112
|
for (let i = 0; i < partCount; i++) {
|
|
110
113
|
const partKey = `${modelKey}__${i}`;
|
|
@@ -113,7 +116,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
113
116
|
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));
|
|
114
117
|
})] }));
|
|
115
118
|
}
|
|
116
|
-
//
|
|
119
|
+
// Render physics-enabled instances using InstancedRigidBodies
|
|
117
120
|
function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
118
121
|
const instances = useMemo(() => group.instances.map(inst => ({
|
|
119
122
|
key: inst.id,
|
|
@@ -126,12 +129,17 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
|
126
129
|
return (_jsx("instancedMesh", { args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
|
|
127
130
|
}) }));
|
|
128
131
|
}
|
|
129
|
-
//
|
|
132
|
+
// Render non-physics instances using Merged's per-instance groups
|
|
130
133
|
function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef }) {
|
|
131
134
|
const clickValid = useRef(false);
|
|
132
|
-
const handlePointerDown = (e) => {
|
|
133
|
-
|
|
134
|
-
clickValid.current =
|
|
135
|
+
const handlePointerDown = (e) => {
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
clickValid.current = true;
|
|
138
|
+
};
|
|
139
|
+
const handlePointerMove = () => {
|
|
140
|
+
if (clickValid.current)
|
|
141
|
+
clickValid.current = false;
|
|
142
|
+
};
|
|
135
143
|
const handlePointerUp = (e, id) => {
|
|
136
144
|
if (clickValid.current) {
|
|
137
145
|
e.stopPropagation();
|
|
@@ -146,7 +154,7 @@ function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, on
|
|
|
146
154
|
return _jsx(Instance, {}, i);
|
|
147
155
|
}) }, inst.id))) }));
|
|
148
156
|
}
|
|
149
|
-
//
|
|
157
|
+
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
150
158
|
export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
151
159
|
const ctx = useContext(GameInstanceContext);
|
|
152
160
|
const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
|
|
@@ -167,6 +175,6 @@ export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation
|
|
|
167
175
|
removeInstance(instance.id);
|
|
168
176
|
};
|
|
169
177
|
}, [addInstance, removeInstance, instance]);
|
|
170
|
-
// No visual
|
|
178
|
+
// No visual rendering - provider handles all instanced visuals
|
|
171
179
|
return null;
|
|
172
180
|
});
|
|
@@ -128,13 +128,15 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
|
|
|
128
128
|
// Early return if gameObject is null or undefined
|
|
129
129
|
if (!gameObject)
|
|
130
130
|
return null;
|
|
131
|
-
|
|
131
|
+
if (gameObject.disabled === true || gameObject.hidden === true)
|
|
132
|
+
return null;
|
|
133
|
+
// Build context object for passing to helper functions
|
|
132
134
|
const ctx = { gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode };
|
|
133
|
-
// --- 1.
|
|
135
|
+
// --- 1. Compute transforms (local + world) ---
|
|
134
136
|
const transformProps = getNodeTransformProps(gameObject);
|
|
135
137
|
const localMatrix = new Matrix4().compose(new Vector3(...transformProps.position), new Quaternion().setFromEuler(new Euler(...transformProps.rotation)), new Vector3(...transformProps.scale));
|
|
136
138
|
const worldMatrix = parentMatrix.clone().multiply(localMatrix);
|
|
137
|
-
//
|
|
139
|
+
// --- 2. Handle selection interaction (edit mode only) ---
|
|
138
140
|
const clickValid = useRef(false);
|
|
139
141
|
const handlePointerDown = (e) => {
|
|
140
142
|
e.stopPropagation();
|
|
@@ -151,20 +153,18 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
|
|
|
151
153
|
}
|
|
152
154
|
clickValid.current = false;
|
|
153
155
|
};
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
// --- 2. If instanced, short-circuit to a tiny clean branch ---
|
|
156
|
+
// --- 3. If instanced model, short-circuit to GameInstance (terminal node) ---
|
|
157
157
|
const isInstanced = !!((_c = (_b = (_a = gameObject.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);
|
|
158
158
|
if (isInstanced) {
|
|
159
159
|
return renderInstancedNode(gameObject, worldMatrix, ctx);
|
|
160
160
|
}
|
|
161
|
-
// ---
|
|
161
|
+
// --- 4. Render core content using component system ---
|
|
162
162
|
const core = renderCoreNode(gameObject, ctx, parentMatrix);
|
|
163
|
-
// --- 5.
|
|
164
|
-
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)));
|
|
165
|
-
// --- 4. Wrap with physics if needed ---
|
|
163
|
+
// --- 5. Wrap with physics if needed (except in edit mode) ---
|
|
166
164
|
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
167
|
-
// --- 6.
|
|
165
|
+
// --- 6. Render children recursively (always relative transforms) ---
|
|
166
|
+
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)));
|
|
167
|
+
// --- 7. Final group wrapper with local transform ---
|
|
168
168
|
return (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [physicsWrapped, children] }));
|
|
169
169
|
}
|
|
170
170
|
// Helper: render an instanced GameInstance (terminal node)
|
|
@@ -179,16 +179,16 @@ function renderInstancedNode(gameObject, worldMatrix, ctx) {
|
|
|
179
179
|
const modelUrl = (_d = (_c = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.model) === null || _c === void 0 ? void 0 : _c.properties) === null || _d === void 0 ? void 0 : _d.filename;
|
|
180
180
|
return (_jsx(GameInstance, { id: gameObject.id, modelUrl: modelUrl, position: [wp.x, wp.y, wp.z], rotation: [we.x, we.y, we.z], scale: [ws.x, ws.y, ws.z], physics: ctx.editMode ? undefined : physics === null || physics === void 0 ? void 0 : physics.properties }));
|
|
181
181
|
}
|
|
182
|
-
// Helper: render main
|
|
182
|
+
// Helper: render main content for a non-instanced node using the component system
|
|
183
183
|
function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
184
184
|
var _a, _b, _c;
|
|
185
185
|
const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
|
|
186
186
|
const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
|
|
187
|
-
const
|
|
187
|
+
const model = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
|
|
188
188
|
const geometryDef = geometry ? getComponent('Geometry') : undefined;
|
|
189
189
|
const materialDef = material ? getComponent('Material') : undefined;
|
|
190
|
-
const
|
|
191
|
-
//
|
|
190
|
+
const modelDef = model ? getComponent('Model') : undefined;
|
|
191
|
+
// Context props for all component Views
|
|
192
192
|
const contextProps = {
|
|
193
193
|
loadedModels: ctx.loadedModels,
|
|
194
194
|
loadedTextures: ctx.loadedTextures,
|
|
@@ -197,20 +197,19 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
197
197
|
parentMatrix,
|
|
198
198
|
registerRef: ctx.registerRef,
|
|
199
199
|
};
|
|
200
|
-
//
|
|
200
|
+
// Collect wrapper and leaf components (excluding transform/physics which are handled separately)
|
|
201
201
|
const wrapperComponents = [];
|
|
202
202
|
const leafComponents = [];
|
|
203
203
|
if (gameObject.components) {
|
|
204
204
|
Object.entries(gameObject.components)
|
|
205
|
-
.filter(([key]) =>
|
|
205
|
+
.filter(([key]) => !['geometry', 'material', 'model', 'transform', 'physics'].includes(key))
|
|
206
206
|
.forEach(([key, comp]) => {
|
|
207
207
|
if (!comp || !comp.type)
|
|
208
208
|
return;
|
|
209
209
|
const def = getComponent(comp.type);
|
|
210
210
|
if (!def || !def.View)
|
|
211
211
|
return;
|
|
212
|
-
//
|
|
213
|
-
// Components that wrap content should accept children prop
|
|
212
|
+
// Components that accept children are wrappers, others are leaves
|
|
214
213
|
const viewString = def.View.toString();
|
|
215
214
|
if (viewString.includes('children')) {
|
|
216
215
|
wrapperComponents.push({ key, View: def.View, properties: comp.properties });
|
|
@@ -220,19 +219,19 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
220
219
|
}
|
|
221
220
|
});
|
|
222
221
|
}
|
|
223
|
-
// Build
|
|
222
|
+
// Build core content based on what components exist
|
|
224
223
|
let coreContent;
|
|
225
|
-
//
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
coreContent = (_jsxs(
|
|
224
|
+
// Priority: Model > Geometry + Material > Empty
|
|
225
|
+
if (model && modelDef && modelDef.View) {
|
|
226
|
+
// Model component wraps its children (including material override)
|
|
227
|
+
coreContent = (_jsxs(modelDef.View, Object.assign({ properties: model.properties }, contextProps, { children: [material && materialDef && materialDef.View && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material")), leafComponents] })));
|
|
229
228
|
}
|
|
230
229
|
else if (geometry && geometryDef && geometryDef.View) {
|
|
231
|
-
//
|
|
232
|
-
coreContent = (_jsxs("mesh", { children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)
|
|
230
|
+
// Geometry + Material = mesh
|
|
231
|
+
coreContent = (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && materialDef && materialDef.View && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material")), leafComponents] }));
|
|
233
232
|
}
|
|
234
233
|
else {
|
|
235
|
-
// No
|
|
234
|
+
// No visual component - just render leaves
|
|
236
235
|
coreContent = _jsx(_Fragment, { children: leafComponents });
|
|
237
236
|
}
|
|
238
237
|
// Wrap core content with wrapper components (in order)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import { CameraHelper, Object3D, Vector3 } from "three";
|
|
5
|
+
function DirectionalLightComponentEditor({ component, onUpdate }) {
|
|
6
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
7
|
+
const props = {
|
|
8
|
+
color: (_a = component.properties.color) !== null && _a !== void 0 ? _a : '#ffffff',
|
|
9
|
+
intensity: (_b = component.properties.intensity) !== null && _b !== void 0 ? _b : 1.0,
|
|
10
|
+
castShadow: (_c = component.properties.castShadow) !== null && _c !== void 0 ? _c : true,
|
|
11
|
+
shadowMapSize: (_d = component.properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024,
|
|
12
|
+
shadowCameraNear: (_e = component.properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1,
|
|
13
|
+
shadowCameraFar: (_f = component.properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100,
|
|
14
|
+
shadowCameraTop: (_g = component.properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30,
|
|
15
|
+
shadowCameraBottom: (_h = component.properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30,
|
|
16
|
+
shadowCameraLeft: (_j = component.properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30,
|
|
17
|
+
shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
|
|
18
|
+
targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
|
|
19
|
+
};
|
|
20
|
+
return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", className: "flex-1 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", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", className: "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", value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Cast Shadow" }), _jsx("input", { type: "checkbox", className: "h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer", checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", className: "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", value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Shadow Camera" }), _jsxs("div", { className: "grid grid-cols-2 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Near" }), _jsx("input", { type: "number", step: "0.1", className: "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", value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Far" }), _jsx("input", { type: "number", step: "1", className: "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", value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Top" }), _jsx("input", { type: "number", step: "1", className: "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", value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Bottom" }), _jsx("input", { type: "number", step: "1", className: "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", value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Left" }), _jsx("input", { type: "number", step: "1", className: "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", value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Right" }), _jsx("input", { type: "number", step: "1", className: "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", value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Target Offset" }), _jsxs("div", { className: "grid grid-cols-3 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "X" }), _jsx("input", { type: "number", step: "0.5", className: "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", value: props.targetOffset[0], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Y" }), _jsx("input", { type: "number", step: "0.5", className: "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", value: props.targetOffset[1], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Z" }), _jsx("input", { type: "number", step: "0.5", className: "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", value: props.targetOffset[2], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)] })) })] })] })] })] });
|
|
21
|
+
}
|
|
22
|
+
function DirectionalLightView({ properties, editMode }) {
|
|
23
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
24
|
+
const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
|
|
25
|
+
const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
|
|
26
|
+
const castShadow = (_c = properties.castShadow) !== null && _c !== void 0 ? _c : true;
|
|
27
|
+
const shadowMapSize = (_d = properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024;
|
|
28
|
+
const shadowCameraNear = (_e = properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1;
|
|
29
|
+
const shadowCameraFar = (_f = properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100;
|
|
30
|
+
const shadowCameraTop = (_g = properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30;
|
|
31
|
+
const shadowCameraBottom = (_h = properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30;
|
|
32
|
+
const shadowCameraLeft = (_j = properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30;
|
|
33
|
+
const shadowCameraRight = (_k = properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30;
|
|
34
|
+
const targetOffset = (_l = properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0];
|
|
35
|
+
const { scene } = useThree();
|
|
36
|
+
const directionalLightRef = useRef(null);
|
|
37
|
+
const targetRef = useRef(new Object3D());
|
|
38
|
+
const lastUpdate = useRef(0);
|
|
39
|
+
const cameraHelperRef = useRef(null);
|
|
40
|
+
const lastPositionRef = useRef(new Vector3());
|
|
41
|
+
// Add target to scene
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (targetRef.current) {
|
|
44
|
+
scene.add(targetRef.current);
|
|
45
|
+
return () => {
|
|
46
|
+
scene.remove(targetRef.current);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}, [scene]);
|
|
50
|
+
// Update target position when light position or offset changes
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (directionalLightRef.current && targetRef.current) {
|
|
53
|
+
const lightWorldPos = new Vector3();
|
|
54
|
+
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
55
|
+
targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
|
|
56
|
+
directionalLightRef.current.target = targetRef.current;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
// Create camera helper for edit mode and add to scene
|
|
61
|
+
if (editMode && directionalLightRef.current && directionalLightRef.current.shadow.camera) {
|
|
62
|
+
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
63
|
+
cameraHelperRef.current = helper;
|
|
64
|
+
scene.add(helper);
|
|
65
|
+
return () => {
|
|
66
|
+
if (cameraHelperRef.current) {
|
|
67
|
+
scene.remove(cameraHelperRef.current);
|
|
68
|
+
cameraHelperRef.current.dispose();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}, [editMode, scene]);
|
|
73
|
+
useFrame(({ clock }) => {
|
|
74
|
+
if (!directionalLightRef.current || !directionalLightRef.current.shadow)
|
|
75
|
+
return;
|
|
76
|
+
// Disable auto-update for shadows
|
|
77
|
+
if (directionalLightRef.current.shadow.autoUpdate) {
|
|
78
|
+
directionalLightRef.current.shadow.autoUpdate = false;
|
|
79
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
80
|
+
}
|
|
81
|
+
// Check if position has changed
|
|
82
|
+
const currentPosition = new Vector3();
|
|
83
|
+
directionalLightRef.current.getWorldPosition(currentPosition);
|
|
84
|
+
const positionChanged = !currentPosition.equals(lastPositionRef.current);
|
|
85
|
+
if (positionChanged) {
|
|
86
|
+
lastPositionRef.current.copy(currentPosition);
|
|
87
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
88
|
+
lastUpdate.current = clock.elapsedTime; // Reset timer on position change
|
|
89
|
+
}
|
|
90
|
+
// Update shadow map infrequently (every 5 seconds) if position hasn't changed
|
|
91
|
+
if (!editMode && !positionChanged && clock.elapsedTime - lastUpdate.current > 5) {
|
|
92
|
+
lastUpdate.current = clock.elapsedTime;
|
|
93
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
94
|
+
}
|
|
95
|
+
// Update camera helper in edit mode
|
|
96
|
+
if (editMode && cameraHelperRef.current) {
|
|
97
|
+
cameraHelperRef.current.update();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
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) => {
|
|
101
|
+
const points = [
|
|
102
|
+
new Vector3(0, 0, 0),
|
|
103
|
+
new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
|
|
104
|
+
];
|
|
105
|
+
geo.setFromPoints(points);
|
|
106
|
+
} }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] }))] }));
|
|
107
|
+
}
|
|
108
|
+
const DirectionalLightComponent = {
|
|
109
|
+
name: 'DirectionalLight',
|
|
110
|
+
Editor: DirectionalLightComponentEditor,
|
|
111
|
+
View: DirectionalLightView,
|
|
112
|
+
defaultProperties: {}
|
|
113
|
+
};
|
|
114
|
+
export default DirectionalLightComponent;
|
|
@@ -21,12 +21,20 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
21
21
|
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
|
-
return children
|
|
24
|
+
return _jsx(_Fragment, { children: children });
|
|
25
25
|
if (loadedModels && loadedModels[properties.filename]) {
|
|
26
|
-
|
|
26
|
+
const clonedModel = loadedModels[properties.filename].clone();
|
|
27
|
+
// Enable shadows on all meshes in the model
|
|
28
|
+
clonedModel.traverse((obj) => {
|
|
29
|
+
if (obj.isMesh) {
|
|
30
|
+
obj.castShadow = true;
|
|
31
|
+
obj.receiveShadow = true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return _jsx("primitive", { object: clonedModel, children: children });
|
|
27
35
|
}
|
|
28
|
-
//
|
|
29
|
-
return children
|
|
36
|
+
// Model not loaded yet - render children only
|
|
37
|
+
return _jsx(_Fragment, { children: children });
|
|
30
38
|
}
|
|
31
39
|
const ModelComponent = {
|
|
32
40
|
name: 'Model',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
2
3
|
function SpotLightComponentEditor({ component, onUpdate }) {
|
|
3
4
|
var _a, _b, _c, _d, _e, _f;
|
|
4
|
-
// Provide default values to prevent NaN
|
|
5
5
|
const props = {
|
|
6
6
|
color: (_a = component.properties.color) !== null && _a !== void 0 ? _a : '#ffffff',
|
|
7
7
|
intensity: (_b = component.properties.intensity) !== null && _b !== void 0 ? _b : 1.0,
|
|
@@ -12,17 +12,22 @@ function SpotLightComponentEditor({ component, onUpdate }) {
|
|
|
12
12
|
};
|
|
13
13
|
return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", className: "flex-1 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", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", className: "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", value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Angle" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: Math.PI, className: "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", value: props.angle, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'angle': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Penumbra" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: "1", className: "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", value: props.penumbra, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'penumbra': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Distance" }), _jsx("input", { type: "number", step: "1", min: "0", className: "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", value: props.distance, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'distance': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Cast Shadow" }), _jsx("input", { type: "checkbox", className: "h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer", checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] })] });
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
function SpotLightView({ properties }) {
|
|
15
|
+
function SpotLightView({ properties, editMode }) {
|
|
17
16
|
var _a, _b, _c, _d, _e, _f;
|
|
18
|
-
// Provide defaults in case properties are missing
|
|
19
17
|
const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
|
|
20
18
|
const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
|
|
21
19
|
const angle = (_c = properties.angle) !== null && _c !== void 0 ? _c : Math.PI / 6;
|
|
22
20
|
const penumbra = (_d = properties.penumbra) !== null && _d !== void 0 ? _d : 0.5;
|
|
23
21
|
const distance = (_e = properties.distance) !== null && _e !== void 0 ? _e : 100;
|
|
24
22
|
const castShadow = (_f = properties.castShadow) !== null && _f !== void 0 ? _f : true;
|
|
25
|
-
|
|
23
|
+
const spotLightRef = useRef(null);
|
|
24
|
+
const targetRef = useRef(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (spotLightRef.current && targetRef.current) {
|
|
27
|
+
spotLightRef.current.target = targetRef.current;
|
|
28
|
+
}
|
|
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 })] })] }))] }));
|
|
26
31
|
}
|
|
27
32
|
const SpotLightComponent = {
|
|
28
33
|
name: 'SpotLight',
|
|
@@ -3,6 +3,7 @@ import TransformComponent from './TransformComponent';
|
|
|
3
3
|
import MaterialComponent from './MaterialComponent';
|
|
4
4
|
import PhysicsComponent from './PhysicsComponent';
|
|
5
5
|
import SpotLightComponent from './SpotLightComponent';
|
|
6
|
+
import DirectionalLightComponent from './DirectionalLightComponent';
|
|
6
7
|
import ModelComponent from './ModelComponent';
|
|
7
8
|
export default [
|
|
8
9
|
GeometryComponent,
|
|
@@ -10,5 +11,6 @@ export default [
|
|
|
10
11
|
MaterialComponent,
|
|
11
12
|
PhysicsComponent,
|
|
12
13
|
SpotLightComponent,
|
|
14
|
+
DirectionalLightComponent,
|
|
13
15
|
ModelComponent
|
|
14
16
|
];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Object3D } from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to load a model (GLB/GLTF/FBX) using drei's optimized loaders
|
|
4
|
+
* Returns the loaded model with proper caching and suspense support
|
|
5
|
+
*/
|
|
6
|
+
export declare function useModel(filename: string | undefined): Object3D | null;
|
|
7
|
+
/**
|
|
8
|
+
* Preload a model to avoid suspense boundaries during runtime
|
|
9
|
+
*/
|
|
10
|
+
export declare function preloadModel(filename: string): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useGLTF, useFBX } from '@react-three/drei';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to load a model (GLB/GLTF/FBX) using drei's optimized loaders
|
|
5
|
+
* Returns the loaded model with proper caching and suspense support
|
|
6
|
+
*/
|
|
7
|
+
export function useModel(filename) {
|
|
8
|
+
const isFBX = filename === null || filename === void 0 ? void 0 : filename.toLowerCase().endsWith('.fbx');
|
|
9
|
+
const isGLTF = (filename === null || filename === void 0 ? void 0 : filename.toLowerCase().endsWith('.glb')) || (filename === null || filename === void 0 ? void 0 : filename.toLowerCase().endsWith('.gltf'));
|
|
10
|
+
// Normalize path (ensure leading slash)
|
|
11
|
+
const normalizedPath = useMemo(() => {
|
|
12
|
+
if (!filename)
|
|
13
|
+
return '';
|
|
14
|
+
return filename.startsWith('/') ? filename : `/${filename}`;
|
|
15
|
+
}, [filename]);
|
|
16
|
+
// Load models using drei hooks (these handle caching automatically)
|
|
17
|
+
const gltf = useGLTF(isGLTF && normalizedPath ? normalizedPath : '', true);
|
|
18
|
+
const fbx = useFBX(isFBX && normalizedPath ? normalizedPath : '');
|
|
19
|
+
// Return the appropriate model
|
|
20
|
+
if (!filename)
|
|
21
|
+
return null;
|
|
22
|
+
if (isGLTF)
|
|
23
|
+
return gltf.scene;
|
|
24
|
+
if (isFBX)
|
|
25
|
+
return fbx;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Preload a model to avoid suspense boundaries during runtime
|
|
30
|
+
*/
|
|
31
|
+
export function preloadModel(filename) {
|
|
32
|
+
const normalizedPath = filename.startsWith('/') ? filename : `/${filename}`;
|
|
33
|
+
const isFBX = filename.toLowerCase().endsWith('.fbx');
|
|
34
|
+
if (isFBX) {
|
|
35
|
+
useFBX.preload(normalizedPath);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
useGLTF.preload(normalizedPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-three-game",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Batteries included React Three Fiber game engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@react-three/rapier": ">=2.0.0",
|
|
23
23
|
"react": ">=18.0.0",
|
|
24
24
|
"react-dom": ">=18.0.0",
|
|
25
|
-
"three": ">=0.
|
|
25
|
+
"three": ">=0.182.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@react-three/drei": "^10.7.7",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"concurrently": "^9.2.1",
|
|
35
35
|
"react": "^19.2.0",
|
|
36
36
|
"react-dom": "^19.2.0",
|
|
37
|
-
"three": "^0.
|
|
37
|
+
"three": "^0.182.0",
|
|
38
38
|
"typescript": "^5.9.3"
|
|
39
39
|
}
|
|
40
40
|
}
|