react-three-game 0.0.17 → 0.0.19
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/.github/copilot-instructions.md +54 -183
- package/README.md +69 -214
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -0
- package/dist/tools/prefabeditor/EditorTree.d.ts +2 -4
- package/dist/tools/prefabeditor/EditorTree.js +20 -194
- package/dist/tools/prefabeditor/EditorUI.js +43 -224
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
- package/dist/tools/prefabeditor/PrefabEditor.js +33 -99
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +0 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +33 -50
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +102 -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/dist/tools/prefabeditor/styles.d.ts +1809 -0
- package/dist/tools/prefabeditor/styles.js +168 -0
- package/dist/tools/prefabeditor/types.d.ts +3 -14
- package/dist/tools/prefabeditor/types.js +0 -1
- package/dist/tools/prefabeditor/utils.d.ts +19 -0
- package/dist/tools/prefabeditor/utils.js +72 -0
- package/package.json +3 -3
- package/src/index.ts +5 -1
- package/src/tools/prefabeditor/EditorTree.tsx +38 -270
- package/src/tools/prefabeditor/EditorUI.tsx +105 -322
- package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
- package/src/tools/prefabeditor/PrefabEditor.tsx +40 -151
- package/src/tools/prefabeditor/PrefabRoot.tsx +41 -73
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +317 -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
- package/src/tools/prefabeditor/styles.ts +195 -0
- package/src/tools/prefabeditor/types.ts +4 -12
- package/src/tools/prefabeditor/utils.ts +80 -0
|
@@ -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
|
});
|
|
@@ -14,28 +14,24 @@ import { useState, useRef, useEffect } from "react";
|
|
|
14
14
|
import PrefabRoot from "./PrefabRoot";
|
|
15
15
|
import { Physics } from "@react-three/rapier";
|
|
16
16
|
import EditorUI from "./EditorUI";
|
|
17
|
+
import { base, toolbar } from "./styles";
|
|
17
18
|
const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) => {
|
|
18
19
|
const [editMode, setEditMode] = useState(true);
|
|
19
20
|
const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"position": [0, 0, 0],
|
|
29
|
-
"rotation": [0, 0, 0],
|
|
30
|
-
"scale": [1, 1, 1]
|
|
31
|
-
}
|
|
21
|
+
id: "prefab-default",
|
|
22
|
+
name: "New Prefab",
|
|
23
|
+
root: {
|
|
24
|
+
id: "root",
|
|
25
|
+
components: {
|
|
26
|
+
transform: {
|
|
27
|
+
type: "Transform",
|
|
28
|
+
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
32
29
|
}
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
32
|
});
|
|
36
33
|
const [selectedId, setSelectedId] = useState(null);
|
|
37
34
|
const [transformMode, setTransformMode] = useState("translate");
|
|
38
|
-
const prefabRef = useRef(null);
|
|
39
35
|
// Sync internal state with external initialPrefab prop
|
|
40
36
|
useEffect(() => {
|
|
41
37
|
if (initialPrefab) {
|
|
@@ -48,136 +44,74 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
|
|
|
48
44
|
const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
|
|
49
45
|
onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(resolved);
|
|
50
46
|
};
|
|
51
|
-
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,
|
|
52
|
-
// props for edit mode
|
|
53
|
-
editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, 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 })] });
|
|
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 })] });
|
|
54
48
|
};
|
|
55
49
|
const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }) => {
|
|
56
50
|
const [history, setHistory] = useState([currentData]);
|
|
57
51
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const handleUndo = () => {
|
|
52
|
+
const throttleRef = useRef(null);
|
|
53
|
+
const lastDataRef = useRef(JSON.stringify(currentData));
|
|
54
|
+
const undo = () => {
|
|
62
55
|
if (historyIndex > 0) {
|
|
63
56
|
const newIndex = historyIndex - 1;
|
|
64
57
|
setHistoryIndex(newIndex);
|
|
65
|
-
|
|
58
|
+
lastDataRef.current = JSON.stringify(history[newIndex]);
|
|
66
59
|
onDataChange(history[newIndex]);
|
|
67
60
|
}
|
|
68
61
|
};
|
|
69
|
-
const
|
|
62
|
+
const redo = () => {
|
|
70
63
|
if (historyIndex < history.length - 1) {
|
|
71
64
|
const newIndex = historyIndex + 1;
|
|
72
65
|
setHistoryIndex(newIndex);
|
|
73
|
-
|
|
66
|
+
lastDataRef.current = JSON.stringify(history[newIndex]);
|
|
74
67
|
onDataChange(history[newIndex]);
|
|
75
68
|
}
|
|
76
69
|
};
|
|
77
|
-
// Keyboard shortcuts for undo/redo
|
|
78
70
|
useEffect(() => {
|
|
79
71
|
const handleKeyDown = (e) => {
|
|
80
|
-
// Undo: Ctrl+Z (Cmd+Z on Mac)
|
|
81
72
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
82
73
|
e.preventDefault();
|
|
83
|
-
|
|
74
|
+
undo();
|
|
84
75
|
}
|
|
85
|
-
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
|
|
86
76
|
else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
|
|
87
77
|
e.preventDefault();
|
|
88
|
-
|
|
78
|
+
redo();
|
|
89
79
|
}
|
|
90
80
|
};
|
|
91
81
|
window.addEventListener('keydown', handleKeyDown);
|
|
92
82
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
93
83
|
}, [historyIndex, history]);
|
|
94
|
-
// Throttled history update when currentData changes
|
|
95
84
|
useEffect(() => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
if (currentDataStr === lastSavedDataRef.current) {
|
|
85
|
+
const currentStr = JSON.stringify(currentData);
|
|
86
|
+
if (currentStr === lastDataRef.current)
|
|
99
87
|
return;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
// Set new throttled update
|
|
106
|
-
throttleTimeoutRef.current = setTimeout(() => {
|
|
107
|
-
lastSavedDataRef.current = currentDataStr;
|
|
88
|
+
if (throttleRef.current)
|
|
89
|
+
clearTimeout(throttleRef.current);
|
|
90
|
+
throttleRef.current = setTimeout(() => {
|
|
91
|
+
lastDataRef.current = currentStr;
|
|
108
92
|
setHistory(prev => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// Add new state
|
|
112
|
-
newHistory.push(currentData);
|
|
113
|
-
// Limit history size to 50 states
|
|
114
|
-
if (newHistory.length > 50) {
|
|
115
|
-
newHistory.shift();
|
|
116
|
-
return newHistory;
|
|
117
|
-
}
|
|
118
|
-
return newHistory;
|
|
93
|
+
const newHistory = [...prev.slice(0, historyIndex + 1), currentData];
|
|
94
|
+
return newHistory.length > 50 ? newHistory.slice(1) : newHistory;
|
|
119
95
|
});
|
|
120
|
-
setHistoryIndex(prev =>
|
|
121
|
-
|
|
122
|
-
newHistory.push(currentData);
|
|
123
|
-
return Math.min(newHistory.length - 1, 49);
|
|
124
|
-
});
|
|
125
|
-
}, 500); // 500ms throttle
|
|
96
|
+
setHistoryIndex(prev => Math.min(prev + 1, 49));
|
|
97
|
+
}, 500);
|
|
126
98
|
return () => {
|
|
127
|
-
if (
|
|
128
|
-
clearTimeout(
|
|
129
|
-
}
|
|
99
|
+
if (throttleRef.current)
|
|
100
|
+
clearTimeout(throttleRef.current);
|
|
130
101
|
};
|
|
131
|
-
}, [currentData
|
|
102
|
+
}, [currentData]);
|
|
132
103
|
const handleLoad = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
104
|
const prefab = yield loadJson();
|
|
134
105
|
if (prefab) {
|
|
135
106
|
onDataChange(prefab);
|
|
136
|
-
// Reset history when loading new file
|
|
137
107
|
setHistory([prefab]);
|
|
138
108
|
setHistoryIndex(0);
|
|
139
|
-
|
|
109
|
+
lastDataRef.current = JSON.stringify(prefab);
|
|
140
110
|
}
|
|
141
111
|
});
|
|
142
112
|
const canUndo = historyIndex > 0;
|
|
143
113
|
const canRedo = historyIndex < history.length - 1;
|
|
144
|
-
return _jsxs("div", { style: {
|
|
145
|
-
position: "absolute",
|
|
146
|
-
top: 8,
|
|
147
|
-
left: "50%",
|
|
148
|
-
transform: "translateX(-50%)",
|
|
149
|
-
display: "flex",
|
|
150
|
-
alignItems: "center",
|
|
151
|
-
gap: 6,
|
|
152
|
-
padding: "2px 4px",
|
|
153
|
-
background: "rgba(0,0,0,0.55)",
|
|
154
|
-
border: "1px solid rgba(255,255,255,0.12)",
|
|
155
|
-
borderRadius: 4,
|
|
156
|
-
color: "rgba(255,255,255,0.9)",
|
|
157
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
158
|
-
fontSize: 11,
|
|
159
|
-
lineHeight: 1,
|
|
160
|
-
WebkitUserSelect: "none",
|
|
161
|
-
userSelect: "none",
|
|
162
|
-
}, children: [_jsx(PanelButton, { onClick: () => onEditModeChange(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx(PanelButton, { onClick: handleUndo, disabled: !canUndo, title: "Undo (Ctrl+Z)", children: "\u21B6" }), _jsx(PanelButton, { onClick: handleRedo, disabled: !canRedo, title: "Redo (Ctrl+Shift+Z)", children: "\u21B7" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx(PanelButton, { onClick: handleLoad, title: "Load JSON", children: "\uD83D\uDCE5" }), _jsx(PanelButton, { onClick: () => saveJson(currentData, "prefab"), title: "Save JSON", children: "\uD83D\uDCBE" })] });
|
|
163
|
-
};
|
|
164
|
-
const PanelButton = ({ onClick, disabled, title, children }) => {
|
|
165
|
-
return _jsx("button", { style: {
|
|
166
|
-
padding: "2px 6px",
|
|
167
|
-
font: "inherit",
|
|
168
|
-
background: "transparent",
|
|
169
|
-
color: disabled ? "rgba(255,255,255,0.3)" : "inherit",
|
|
170
|
-
border: "1px solid rgba(255,255,255,0.18)",
|
|
171
|
-
borderRadius: 3,
|
|
172
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
173
|
-
opacity: disabled ? 0.5 : 1,
|
|
174
|
-
}, onClick: onClick, disabled: disabled, title: title, onPointerEnter: (e) => {
|
|
175
|
-
if (!disabled) {
|
|
176
|
-
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
177
|
-
}
|
|
178
|
-
}, onPointerLeave: (e) => {
|
|
179
|
-
e.currentTarget.style.background = "transparent";
|
|
180
|
-
}, children: children });
|
|
114
|
+
return _jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => onEditModeChange(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("div", { style: toolbar.divider }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), (canUndo ? {} : toolbar.disabled)), onClick: undo, disabled: !canUndo, children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), (canRedo ? {} : toolbar.disabled)), onClick: redo, disabled: !canRedo, children: "\u21B7" }), _jsx("div", { style: toolbar.divider }), _jsx("button", { style: base.btn, onClick: handleLoad, children: "\uD83D\uDCE5" }), _jsx("button", { style: base.btn, onClick: () => saveJson(currentData, "prefab"), children: "\uD83D\uDCBE" })] });
|
|
181
115
|
};
|
|
182
116
|
const saveJson = (data, filename) => {
|
|
183
117
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
|
@@ -7,7 +7,6 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
|
|
|
7
7
|
selectedId?: string | null;
|
|
8
8
|
onSelect?: (id: string | null) => void;
|
|
9
9
|
transformMode?: "translate" | "rotate" | "scale";
|
|
10
|
-
setTransformMode?: (mode: "translate" | "rotate" | "scale") => void;
|
|
11
10
|
basePath?: string;
|
|
12
11
|
} & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
|
|
13
12
|
export default PrefabRoot;
|
|
@@ -12,42 +12,26 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
12
12
|
import { MapControls, TransformControls } from "@react-three/drei";
|
|
13
13
|
import { useState, useRef, useEffect, forwardRef, useCallback } from "react";
|
|
14
14
|
import { Vector3, Euler, Quaternion, SRGBColorSpace, TextureLoader, Matrix4 } from "three";
|
|
15
|
-
import { getComponent } from "./components/ComponentRegistry";
|
|
15
|
+
import { getComponent, registerComponent } from "./components/ComponentRegistry";
|
|
16
16
|
import { loadModel } from "../dragdrop/modelLoader";
|
|
17
17
|
import { GameInstance, GameInstanceProvider } from "./InstanceProvider";
|
|
18
|
-
|
|
19
|
-
import { registerComponent } from './components/ComponentRegistry';
|
|
18
|
+
import { updateNode } from "./utils";
|
|
20
19
|
import components from './components/';
|
|
20
|
+
// Register all components
|
|
21
21
|
components.forEach(registerComponent);
|
|
22
|
-
|
|
23
|
-
if (root.id === id) {
|
|
24
|
-
return update(root);
|
|
25
|
-
}
|
|
26
|
-
if (root.children) {
|
|
27
|
-
return Object.assign(Object.assign({}, root), { children: root.children.map(child => updatePrefabNode(child, id, update)) });
|
|
28
|
-
}
|
|
29
|
-
return root;
|
|
30
|
-
}
|
|
31
|
-
export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, setTransformMode, basePath = "" }, ref) => {
|
|
22
|
+
export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, basePath = "" }, ref) => {
|
|
32
23
|
const [loadedModels, setLoadedModels] = useState({});
|
|
33
24
|
const [loadedTextures, setLoadedTextures] = useState({});
|
|
34
|
-
// const [prefabRoot, setPrefabRoot] = useState<Prefab>(data); // Removed local state
|
|
35
25
|
const loadingRefs = useRef(new Set());
|
|
36
26
|
const objectRefs = useRef({});
|
|
37
27
|
const [selectedObject, setSelectedObject] = useState(null);
|
|
38
28
|
const registerRef = useCallback((id, obj) => {
|
|
39
29
|
objectRefs.current[id] = obj;
|
|
40
|
-
if (id === selectedId)
|
|
30
|
+
if (id === selectedId)
|
|
41
31
|
setSelectedObject(obj);
|
|
42
|
-
}
|
|
43
32
|
}, [selectedId]);
|
|
44
33
|
useEffect(() => {
|
|
45
|
-
|
|
46
|
-
setSelectedObject(objectRefs.current[selectedId] || null);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
setSelectedObject(null);
|
|
50
|
-
}
|
|
34
|
+
setSelectedObject(selectedId ? objectRefs.current[selectedId] || null : null);
|
|
51
35
|
}, [selectedId]);
|
|
52
36
|
const onTransformChange = () => {
|
|
53
37
|
if (!selectedId || !onPrefabChange)
|
|
@@ -68,7 +52,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
68
52
|
localMatrix.decompose(lp, lq, ls);
|
|
69
53
|
const le = new Euler().setFromQuaternion(lq);
|
|
70
54
|
// 4. Write back LOCAL transform into the prefab node
|
|
71
|
-
const newRoot =
|
|
55
|
+
const newRoot = updateNode(data.root, selectedId, (node) => (Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { transform: {
|
|
72
56
|
type: "Transform",
|
|
73
57
|
properties: {
|
|
74
58
|
position: [lp.x, lp.y, lp.z],
|
|
@@ -128,13 +112,15 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
|
|
|
128
112
|
// Early return if gameObject is null or undefined
|
|
129
113
|
if (!gameObject)
|
|
130
114
|
return null;
|
|
131
|
-
|
|
115
|
+
if (gameObject.disabled === true || gameObject.hidden === true)
|
|
116
|
+
return null;
|
|
117
|
+
// Build context object for passing to helper functions
|
|
132
118
|
const ctx = { gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode };
|
|
133
|
-
// --- 1.
|
|
119
|
+
// --- 1. Compute transforms (local + world) ---
|
|
134
120
|
const transformProps = getNodeTransformProps(gameObject);
|
|
135
121
|
const localMatrix = new Matrix4().compose(new Vector3(...transformProps.position), new Quaternion().setFromEuler(new Euler(...transformProps.rotation)), new Vector3(...transformProps.scale));
|
|
136
122
|
const worldMatrix = parentMatrix.clone().multiply(localMatrix);
|
|
137
|
-
//
|
|
123
|
+
// --- 2. Handle selection interaction (edit mode only) ---
|
|
138
124
|
const clickValid = useRef(false);
|
|
139
125
|
const handlePointerDown = (e) => {
|
|
140
126
|
e.stopPropagation();
|
|
@@ -151,20 +137,18 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
|
|
|
151
137
|
}
|
|
152
138
|
clickValid.current = false;
|
|
153
139
|
};
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
// --- 2. If instanced, short-circuit to a tiny clean branch ---
|
|
140
|
+
// --- 3. If instanced model, short-circuit to GameInstance (terminal node) ---
|
|
157
141
|
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
142
|
if (isInstanced) {
|
|
159
143
|
return renderInstancedNode(gameObject, worldMatrix, ctx);
|
|
160
144
|
}
|
|
161
|
-
// ---
|
|
145
|
+
// --- 4. Render core content using component system ---
|
|
162
146
|
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 ---
|
|
147
|
+
// --- 5. Wrap with physics if needed (except in edit mode) ---
|
|
166
148
|
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
167
|
-
// --- 6.
|
|
149
|
+
// --- 6. Render children recursively (always relative transforms) ---
|
|
150
|
+
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
|
+
// --- 7. Final group wrapper with local transform ---
|
|
168
152
|
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
153
|
}
|
|
170
154
|
// Helper: render an instanced GameInstance (terminal node)
|
|
@@ -179,16 +163,16 @@ function renderInstancedNode(gameObject, worldMatrix, ctx) {
|
|
|
179
163
|
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
164
|
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
165
|
}
|
|
182
|
-
// Helper: render main
|
|
166
|
+
// Helper: render main content for a non-instanced node using the component system
|
|
183
167
|
function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
184
168
|
var _a, _b, _c;
|
|
185
169
|
const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
|
|
186
170
|
const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
|
|
187
|
-
const
|
|
171
|
+
const model = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
|
|
188
172
|
const geometryDef = geometry ? getComponent('Geometry') : undefined;
|
|
189
173
|
const materialDef = material ? getComponent('Material') : undefined;
|
|
190
|
-
const
|
|
191
|
-
//
|
|
174
|
+
const modelDef = model ? getComponent('Model') : undefined;
|
|
175
|
+
// Context props for all component Views
|
|
192
176
|
const contextProps = {
|
|
193
177
|
loadedModels: ctx.loadedModels,
|
|
194
178
|
loadedTextures: ctx.loadedTextures,
|
|
@@ -197,20 +181,19 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
197
181
|
parentMatrix,
|
|
198
182
|
registerRef: ctx.registerRef,
|
|
199
183
|
};
|
|
200
|
-
//
|
|
184
|
+
// Collect wrapper and leaf components (excluding transform/physics which are handled separately)
|
|
201
185
|
const wrapperComponents = [];
|
|
202
186
|
const leafComponents = [];
|
|
203
187
|
if (gameObject.components) {
|
|
204
188
|
Object.entries(gameObject.components)
|
|
205
|
-
.filter(([key]) =>
|
|
189
|
+
.filter(([key]) => !['geometry', 'material', 'model', 'transform', 'physics'].includes(key))
|
|
206
190
|
.forEach(([key, comp]) => {
|
|
207
191
|
if (!comp || !comp.type)
|
|
208
192
|
return;
|
|
209
193
|
const def = getComponent(comp.type);
|
|
210
194
|
if (!def || !def.View)
|
|
211
195
|
return;
|
|
212
|
-
//
|
|
213
|
-
// Components that wrap content should accept children prop
|
|
196
|
+
// Components that accept children are wrappers, others are leaves
|
|
214
197
|
const viewString = def.View.toString();
|
|
215
198
|
if (viewString.includes('children')) {
|
|
216
199
|
wrapperComponents.push({ key, View: def.View, properties: comp.properties });
|
|
@@ -220,19 +203,19 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
220
203
|
}
|
|
221
204
|
});
|
|
222
205
|
}
|
|
223
|
-
// Build
|
|
206
|
+
// Build core content based on what components exist
|
|
224
207
|
let coreContent;
|
|
225
|
-
//
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
coreContent = (_jsxs(
|
|
208
|
+
// Priority: Model > Geometry + Material > Empty
|
|
209
|
+
if (model && modelDef && modelDef.View) {
|
|
210
|
+
// Model component wraps its children (including material override)
|
|
211
|
+
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
212
|
}
|
|
230
213
|
else if (geometry && geometryDef && geometryDef.View) {
|
|
231
|
-
//
|
|
232
|
-
coreContent = (_jsxs("mesh", { children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)
|
|
214
|
+
// Geometry + Material = mesh
|
|
215
|
+
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
216
|
}
|
|
234
217
|
else {
|
|
235
|
-
// No
|
|
218
|
+
// No visual component - just render leaves
|
|
236
219
|
coreContent = _jsx(_Fragment, { children: leafComponents });
|
|
237
220
|
}
|
|
238
221
|
// Wrap core content with wrapper components (in order)
|
|
@@ -0,0 +1,102 @@
|
|
|
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 cameraHelperRef = useRef(null);
|
|
39
|
+
// Add target to scene once
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const target = targetRef.current;
|
|
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) {
|
|
50
|
+
directionalLightRef.current.target = targetRef.current;
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
// Update target position and mark shadow for update when light moves or offset changes
|
|
54
|
+
useFrame(() => {
|
|
55
|
+
if (!directionalLightRef.current)
|
|
56
|
+
return;
|
|
57
|
+
const lightWorldPos = new Vector3();
|
|
58
|
+
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
59
|
+
const newTargetPos = new Vector3(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
|
|
60
|
+
// Only update if position actually changed
|
|
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
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Create/destroy camera helper for edit mode
|
|
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) => {
|
|
89
|
+
const points = [
|
|
90
|
+
new Vector3(0, 0, 0),
|
|
91
|
+
new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
|
|
92
|
+
];
|
|
93
|
+
geo.setFromPoints(points);
|
|
94
|
+
} }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] }))] }));
|
|
95
|
+
}
|
|
96
|
+
const DirectionalLightComponent = {
|
|
97
|
+
name: 'DirectionalLight',
|
|
98
|
+
Editor: DirectionalLightComponentEditor,
|
|
99
|
+
View: DirectionalLightView,
|
|
100
|
+
defaultProperties: {}
|
|
101
|
+
};
|
|
102
|
+
export default DirectionalLightComponent;
|