react-three-game 0.0.18 → 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/PrefabEditor.js +33 -99
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +0 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +7 -23
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +31 -43
- 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 +1 -1
- 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/PrefabEditor.tsx +40 -151
- package/src/tools/prefabeditor/PrefabRoot.tsx +11 -32
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +38 -53
- package/src/tools/prefabeditor/styles.ts +195 -0
- package/src/tools/prefabeditor/types.ts +4 -12
- package/src/tools/prefabeditor/utils.ts +80 -0
|
@@ -2,34 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
import GameCanvas from "../../shared/GameCanvas";
|
|
4
4
|
import { useState, useRef, useEffect } from "react";
|
|
5
|
-
import {
|
|
6
|
-
import { Prefab, } from "./types";
|
|
5
|
+
import { Prefab } from "./types";
|
|
7
6
|
import PrefabRoot from "./PrefabRoot";
|
|
8
7
|
import { Physics } from "@react-three/rapier";
|
|
9
8
|
import EditorUI from "./EditorUI";
|
|
9
|
+
import { base, toolbar } from "./styles";
|
|
10
10
|
|
|
11
11
|
const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { basePath?: string, initialPrefab?: Prefab, onPrefabChange?: (prefab: Prefab) => void, children?: React.ReactNode }) => {
|
|
12
12
|
const [editMode, setEditMode] = useState(true);
|
|
13
13
|
const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"position": [0, 0, 0],
|
|
23
|
-
"rotation": [0, 0, 0],
|
|
24
|
-
"scale": [1, 1, 1]
|
|
25
|
-
}
|
|
14
|
+
id: "prefab-default",
|
|
15
|
+
name: "New Prefab",
|
|
16
|
+
root: {
|
|
17
|
+
id: "root",
|
|
18
|
+
components: {
|
|
19
|
+
transform: {
|
|
20
|
+
type: "Transform",
|
|
21
|
+
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
26
22
|
}
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
25
|
});
|
|
30
26
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
31
27
|
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
|
|
32
|
-
const prefabRef = useRef<Group>(null);
|
|
33
28
|
|
|
34
29
|
// Sync internal state with external initialPrefab prop
|
|
35
30
|
useEffect(() => {
|
|
@@ -52,15 +47,11 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { b
|
|
|
52
47
|
<gridHelper args={[10, 10]} position={[0, -1, 0]} />
|
|
53
48
|
<PrefabRoot
|
|
54
49
|
data={loadedPrefab}
|
|
55
|
-
ref={prefabRef}
|
|
56
|
-
|
|
57
|
-
// props for edit mode
|
|
58
50
|
editMode={editMode}
|
|
59
51
|
onPrefabChange={updatePrefab}
|
|
60
52
|
selectedId={selectedId}
|
|
61
53
|
onSelect={setSelectedId}
|
|
62
54
|
transformMode={transformMode}
|
|
63
|
-
setTransformMode={setTransformMode}
|
|
64
55
|
basePath={basePath}
|
|
65
56
|
/>
|
|
66
57
|
{children}
|
|
@@ -98,189 +89,87 @@ const SaveDataPanel = ({
|
|
|
98
89
|
}) => {
|
|
99
90
|
const [history, setHistory] = useState<Prefab[]>([currentData]);
|
|
100
91
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
101
|
-
const
|
|
102
|
-
const
|
|
92
|
+
const throttleRef = useRef<NodeJS.Timeout | null>(null);
|
|
93
|
+
const lastDataRef = useRef<string>(JSON.stringify(currentData));
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
const handleUndo = () => {
|
|
95
|
+
const undo = () => {
|
|
106
96
|
if (historyIndex > 0) {
|
|
107
97
|
const newIndex = historyIndex - 1;
|
|
108
98
|
setHistoryIndex(newIndex);
|
|
109
|
-
|
|
99
|
+
lastDataRef.current = JSON.stringify(history[newIndex]);
|
|
110
100
|
onDataChange(history[newIndex]);
|
|
111
101
|
}
|
|
112
102
|
};
|
|
113
103
|
|
|
114
|
-
const
|
|
104
|
+
const redo = () => {
|
|
115
105
|
if (historyIndex < history.length - 1) {
|
|
116
106
|
const newIndex = historyIndex + 1;
|
|
117
107
|
setHistoryIndex(newIndex);
|
|
118
|
-
|
|
108
|
+
lastDataRef.current = JSON.stringify(history[newIndex]);
|
|
119
109
|
onDataChange(history[newIndex]);
|
|
120
110
|
}
|
|
121
111
|
};
|
|
122
112
|
|
|
123
|
-
// Keyboard shortcuts for undo/redo
|
|
124
113
|
useEffect(() => {
|
|
125
114
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
126
|
-
// Undo: Ctrl+Z (Cmd+Z on Mac)
|
|
127
115
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
128
116
|
e.preventDefault();
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
|
|
132
|
-
else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
|
|
117
|
+
undo();
|
|
118
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
|
|
133
119
|
e.preventDefault();
|
|
134
|
-
|
|
120
|
+
redo();
|
|
135
121
|
}
|
|
136
122
|
};
|
|
137
|
-
|
|
138
123
|
window.addEventListener('keydown', handleKeyDown);
|
|
139
124
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
140
125
|
}, [historyIndex, history]);
|
|
141
126
|
|
|
142
|
-
// Throttled history update when currentData changes
|
|
143
127
|
useEffect(() => {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
// Skip if data hasn't actually changed
|
|
147
|
-
if (currentDataStr === lastSavedDataRef.current) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
128
|
+
const currentStr = JSON.stringify(currentData);
|
|
129
|
+
if (currentStr === lastDataRef.current) return;
|
|
150
130
|
|
|
151
|
-
|
|
152
|
-
if (throttleTimeoutRef.current) {
|
|
153
|
-
clearTimeout(throttleTimeoutRef.current);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Set new throttled update
|
|
157
|
-
throttleTimeoutRef.current = setTimeout(() => {
|
|
158
|
-
lastSavedDataRef.current = currentDataStr;
|
|
131
|
+
if (throttleRef.current) clearTimeout(throttleRef.current);
|
|
159
132
|
|
|
133
|
+
throttleRef.current = setTimeout(() => {
|
|
134
|
+
lastDataRef.current = currentStr;
|
|
160
135
|
setHistory(prev => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Add new state
|
|
164
|
-
newHistory.push(currentData);
|
|
165
|
-
// Limit history size to 50 states
|
|
166
|
-
if (newHistory.length > 50) {
|
|
167
|
-
newHistory.shift();
|
|
168
|
-
return newHistory;
|
|
169
|
-
}
|
|
170
|
-
return newHistory;
|
|
136
|
+
const newHistory = [...prev.slice(0, historyIndex + 1), currentData];
|
|
137
|
+
return newHistory.length > 50 ? newHistory.slice(1) : newHistory;
|
|
171
138
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const newHistory = history.slice(0, prev + 1);
|
|
175
|
-
newHistory.push(currentData);
|
|
176
|
-
return Math.min(newHistory.length - 1, 49);
|
|
177
|
-
});
|
|
178
|
-
}, 500); // 500ms throttle
|
|
139
|
+
setHistoryIndex(prev => Math.min(prev + 1, 49));
|
|
140
|
+
}, 500);
|
|
179
141
|
|
|
180
142
|
return () => {
|
|
181
|
-
if (
|
|
182
|
-
clearTimeout(throttleTimeoutRef.current);
|
|
183
|
-
}
|
|
143
|
+
if (throttleRef.current) clearTimeout(throttleRef.current);
|
|
184
144
|
};
|
|
185
|
-
}, [currentData
|
|
145
|
+
}, [currentData]);
|
|
186
146
|
|
|
187
147
|
const handleLoad = async () => {
|
|
188
148
|
const prefab = await loadJson();
|
|
189
149
|
if (prefab) {
|
|
190
150
|
onDataChange(prefab);
|
|
191
|
-
// Reset history when loading new file
|
|
192
151
|
setHistory([prefab]);
|
|
193
152
|
setHistoryIndex(0);
|
|
194
|
-
|
|
153
|
+
lastDataRef.current = JSON.stringify(prefab);
|
|
195
154
|
}
|
|
196
155
|
};
|
|
197
156
|
|
|
198
157
|
const canUndo = historyIndex > 0;
|
|
199
158
|
const canRedo = historyIndex < history.length - 1;
|
|
200
159
|
|
|
201
|
-
return <div style={
|
|
202
|
-
|
|
203
|
-
top: 8,
|
|
204
|
-
left: "50%",
|
|
205
|
-
transform: "translateX(-50%)",
|
|
206
|
-
display: "flex",
|
|
207
|
-
alignItems: "center",
|
|
208
|
-
gap: 6,
|
|
209
|
-
padding: "2px 4px",
|
|
210
|
-
background: "rgba(0,0,0,0.55)",
|
|
211
|
-
border: "1px solid rgba(255,255,255,0.12)",
|
|
212
|
-
borderRadius: 4,
|
|
213
|
-
color: "rgba(255,255,255,0.9)",
|
|
214
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
215
|
-
fontSize: 11,
|
|
216
|
-
lineHeight: 1,
|
|
217
|
-
WebkitUserSelect: "none",
|
|
218
|
-
userSelect: "none",
|
|
219
|
-
}}>
|
|
220
|
-
<PanelButton onClick={() => onEditModeChange(!editMode)}>
|
|
160
|
+
return <div style={toolbar.panel}>
|
|
161
|
+
<button style={base.btn} onClick={() => onEditModeChange(!editMode)}>
|
|
221
162
|
{editMode ? "▶" : "⏸"}
|
|
222
|
-
</
|
|
223
|
-
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<PanelButton onClick={handleRedo} disabled={!canRedo} title="Redo (Ctrl+Shift+Z)">
|
|
231
|
-
↷
|
|
232
|
-
</PanelButton>
|
|
233
|
-
|
|
234
|
-
<span style={{ opacity: 0.35 }}>|</span>
|
|
235
|
-
|
|
236
|
-
<PanelButton onClick={handleLoad} title="Load JSON">
|
|
237
|
-
📥
|
|
238
|
-
</PanelButton>
|
|
239
|
-
|
|
240
|
-
<PanelButton onClick={() => saveJson(currentData, "prefab")} title="Save JSON">
|
|
241
|
-
💾
|
|
242
|
-
</PanelButton>
|
|
163
|
+
</button>
|
|
164
|
+
<div style={toolbar.divider} />
|
|
165
|
+
<button style={{ ...base.btn, ...(canUndo ? {} : toolbar.disabled) }} onClick={undo} disabled={!canUndo}>↶</button>
|
|
166
|
+
<button style={{ ...base.btn, ...(canRedo ? {} : toolbar.disabled) }} onClick={redo} disabled={!canRedo}>↷</button>
|
|
167
|
+
<div style={toolbar.divider} />
|
|
168
|
+
<button style={base.btn} onClick={handleLoad}>📥</button>
|
|
169
|
+
<button style={base.btn} onClick={() => saveJson(currentData, "prefab")}>💾</button>
|
|
243
170
|
</div>;
|
|
244
171
|
};
|
|
245
172
|
|
|
246
|
-
const PanelButton = ({
|
|
247
|
-
onClick,
|
|
248
|
-
disabled,
|
|
249
|
-
title,
|
|
250
|
-
children
|
|
251
|
-
}: {
|
|
252
|
-
onClick: () => void;
|
|
253
|
-
disabled?: boolean;
|
|
254
|
-
title?: string;
|
|
255
|
-
children: React.ReactNode;
|
|
256
|
-
}) => {
|
|
257
|
-
return <button
|
|
258
|
-
style={{
|
|
259
|
-
padding: "2px 6px",
|
|
260
|
-
font: "inherit",
|
|
261
|
-
background: "transparent",
|
|
262
|
-
color: disabled ? "rgba(255,255,255,0.3)" : "inherit",
|
|
263
|
-
border: "1px solid rgba(255,255,255,0.18)",
|
|
264
|
-
borderRadius: 3,
|
|
265
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
266
|
-
opacity: disabled ? 0.5 : 1,
|
|
267
|
-
}}
|
|
268
|
-
onClick={onClick}
|
|
269
|
-
disabled={disabled}
|
|
270
|
-
title={title}
|
|
271
|
-
onPointerEnter={(e) => {
|
|
272
|
-
if (!disabled) {
|
|
273
|
-
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
274
|
-
}
|
|
275
|
-
}}
|
|
276
|
-
onPointerLeave={(e) => {
|
|
277
|
-
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
278
|
-
}}
|
|
279
|
-
>
|
|
280
|
-
{children}
|
|
281
|
-
</button>;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
173
|
const saveJson = (data: any, filename: string) => {
|
|
285
174
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
|
286
175
|
const downloadAnchorNode = document.createElement('a');
|
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { MapControls, TransformControls } from "@react-three/drei";
|
|
4
|
-
import { useState, useRef, useEffect, forwardRef,
|
|
5
|
-
import { Vector3, Euler, Quaternion,
|
|
4
|
+
import { useState, useRef, useEffect, forwardRef, useCallback } from "react";
|
|
5
|
+
import { Vector3, Euler, Quaternion, Group, Object3D, SRGBColorSpace, Texture, TextureLoader, Matrix4 } from "three";
|
|
6
6
|
import { Prefab, GameObject as GameObjectType } from "./types";
|
|
7
|
-
import { getComponent } from "./components/ComponentRegistry";
|
|
7
|
+
import { getComponent, registerComponent } from "./components/ComponentRegistry";
|
|
8
8
|
import { ThreeEvent } from "@react-three/fiber";
|
|
9
9
|
import { loadModel } from "../dragdrop/modelLoader";
|
|
10
10
|
import { GameInstance, GameInstanceProvider } from "./InstanceProvider";
|
|
11
|
-
|
|
12
|
-
// register all components
|
|
13
|
-
import { registerComponent } from './components/ComponentRegistry';
|
|
11
|
+
import { updateNode } from "./utils";
|
|
14
12
|
import components from './components/';
|
|
15
|
-
components.forEach(registerComponent);
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return update(root);
|
|
20
|
-
}
|
|
21
|
-
if (root.children) {
|
|
22
|
-
return {
|
|
23
|
-
...root,
|
|
24
|
-
children: root.children.map(child => updatePrefabNode(child, id, update))
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
return root;
|
|
28
|
-
}
|
|
14
|
+
// Register all components
|
|
15
|
+
components.forEach(registerComponent);
|
|
29
16
|
|
|
30
17
|
export const PrefabRoot = forwardRef<Group, {
|
|
31
18
|
editMode?: boolean;
|
|
@@ -34,29 +21,21 @@ export const PrefabRoot = forwardRef<Group, {
|
|
|
34
21
|
selectedId?: string | null;
|
|
35
22
|
onSelect?: (id: string | null) => void;
|
|
36
23
|
transformMode?: "translate" | "rotate" | "scale";
|
|
37
|
-
setTransformMode?: (mode: "translate" | "rotate" | "scale") => void;
|
|
38
24
|
basePath?: string;
|
|
39
|
-
}>(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode,
|
|
25
|
+
}>(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, basePath = "" }, ref) => {
|
|
40
26
|
const [loadedModels, setLoadedModels] = useState<Record<string, Object3D>>({});
|
|
41
27
|
const [loadedTextures, setLoadedTextures] = useState<Record<string, Texture>>({});
|
|
42
|
-
// const [prefabRoot, setPrefabRoot] = useState<Prefab>(data); // Removed local state
|
|
43
28
|
const loadingRefs = useRef<Set<string>>(new Set());
|
|
44
29
|
const objectRefs = useRef<Record<string, Object3D | null>>({});
|
|
45
30
|
const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
|
|
46
31
|
|
|
47
32
|
const registerRef = useCallback((id: string, obj: Object3D | null) => {
|
|
48
33
|
objectRefs.current[id] = obj;
|
|
49
|
-
if (id === selectedId)
|
|
50
|
-
setSelectedObject(obj);
|
|
51
|
-
}
|
|
34
|
+
if (id === selectedId) setSelectedObject(obj);
|
|
52
35
|
}, [selectedId]);
|
|
53
36
|
|
|
54
37
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
setSelectedObject(objectRefs.current[selectedId] || null);
|
|
57
|
-
} else {
|
|
58
|
-
setSelectedObject(null);
|
|
59
|
-
}
|
|
38
|
+
setSelectedObject(selectedId ? objectRefs.current[selectedId] || null : null);
|
|
60
39
|
}, [selectedId]);
|
|
61
40
|
|
|
62
41
|
const onTransformChange = () => {
|
|
@@ -82,10 +61,10 @@ export const PrefabRoot = forwardRef<Group, {
|
|
|
82
61
|
const le = new Euler().setFromQuaternion(lq);
|
|
83
62
|
|
|
84
63
|
// 4. Write back LOCAL transform into the prefab node
|
|
85
|
-
const newRoot =
|
|
64
|
+
const newRoot = updateNode(data.root, selectedId, (node) => ({
|
|
86
65
|
...node,
|
|
87
66
|
components: {
|
|
88
|
-
...node
|
|
67
|
+
...node.components,
|
|
89
68
|
transform: {
|
|
90
69
|
type: "Transform",
|
|
91
70
|
properties: {
|
|
@@ -193,37 +193,54 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
193
193
|
const { scene } = useThree();
|
|
194
194
|
const directionalLightRef = useRef<DirectionalLight>(null);
|
|
195
195
|
const targetRef = useRef<Object3D>(new Object3D());
|
|
196
|
-
const lastUpdate = useRef(0);
|
|
197
196
|
const cameraHelperRef = useRef<CameraHelper | null>(null);
|
|
198
|
-
const lastPositionRef = useRef<Vector3>(new Vector3());
|
|
199
197
|
|
|
200
|
-
// Add target to scene
|
|
198
|
+
// Add target to scene once
|
|
201
199
|
useEffect(() => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
200
|
+
const target = targetRef.current;
|
|
201
|
+
scene.add(target);
|
|
202
|
+
return () => {
|
|
203
|
+
scene.remove(target);
|
|
204
|
+
};
|
|
208
205
|
}, [scene]);
|
|
209
206
|
|
|
210
|
-
//
|
|
207
|
+
// Set up light target reference once
|
|
211
208
|
useEffect(() => {
|
|
212
|
-
if (directionalLightRef.current
|
|
213
|
-
const lightWorldPos = new Vector3();
|
|
214
|
-
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
215
|
-
targetRef.current.position.set(
|
|
216
|
-
lightWorldPos.x + targetOffset[0],
|
|
217
|
-
lightWorldPos.y + targetOffset[1],
|
|
218
|
-
lightWorldPos.z + targetOffset[2]
|
|
219
|
-
);
|
|
209
|
+
if (directionalLightRef.current) {
|
|
220
210
|
directionalLightRef.current.target = targetRef.current;
|
|
221
211
|
}
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
// Update target position and mark shadow for update when light moves or offset changes
|
|
215
|
+
useFrame(() => {
|
|
216
|
+
if (!directionalLightRef.current) return;
|
|
217
|
+
|
|
218
|
+
const lightWorldPos = new Vector3();
|
|
219
|
+
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
220
|
+
|
|
221
|
+
const newTargetPos = new Vector3(
|
|
222
|
+
lightWorldPos.x + targetOffset[0],
|
|
223
|
+
lightWorldPos.y + targetOffset[1],
|
|
224
|
+
lightWorldPos.z + targetOffset[2]
|
|
225
|
+
);
|
|
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
|
+
}
|
|
222
239
|
});
|
|
223
240
|
|
|
241
|
+
// Create/destroy camera helper for edit mode
|
|
224
242
|
useEffect(() => {
|
|
225
|
-
|
|
226
|
-
if (editMode && directionalLightRef.current && directionalLightRef.current.shadow.camera) {
|
|
243
|
+
if (editMode && directionalLightRef.current?.shadow.camera) {
|
|
227
244
|
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
228
245
|
cameraHelperRef.current = helper;
|
|
229
246
|
scene.add(helper);
|
|
@@ -232,44 +249,12 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
|
|
|
232
249
|
if (cameraHelperRef.current) {
|
|
233
250
|
scene.remove(cameraHelperRef.current);
|
|
234
251
|
cameraHelperRef.current.dispose();
|
|
252
|
+
cameraHelperRef.current = null;
|
|
235
253
|
}
|
|
236
254
|
};
|
|
237
255
|
}
|
|
238
256
|
}, [editMode, scene]);
|
|
239
257
|
|
|
240
|
-
useFrame(({ clock }) => {
|
|
241
|
-
if (!directionalLightRef.current || !directionalLightRef.current.shadow) return;
|
|
242
|
-
|
|
243
|
-
// Disable auto-update for shadows
|
|
244
|
-
if (directionalLightRef.current.shadow.autoUpdate) {
|
|
245
|
-
directionalLightRef.current.shadow.autoUpdate = false;
|
|
246
|
-
directionalLightRef.current.shadow.needsUpdate = true;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Check if position has changed
|
|
250
|
-
const currentPosition = new Vector3();
|
|
251
|
-
directionalLightRef.current.getWorldPosition(currentPosition);
|
|
252
|
-
|
|
253
|
-
const positionChanged = !currentPosition.equals(lastPositionRef.current);
|
|
254
|
-
|
|
255
|
-
if (positionChanged) {
|
|
256
|
-
lastPositionRef.current.copy(currentPosition);
|
|
257
|
-
directionalLightRef.current.shadow.needsUpdate = true;
|
|
258
|
-
lastUpdate.current = clock.elapsedTime; // Reset timer on position change
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Update shadow map infrequently (every 5 seconds) if position hasn't changed
|
|
262
|
-
if (!editMode && !positionChanged && clock.elapsedTime - lastUpdate.current > 5) {
|
|
263
|
-
lastUpdate.current = clock.elapsedTime;
|
|
264
|
-
directionalLightRef.current.shadow.needsUpdate = true;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Update camera helper in edit mode
|
|
268
|
-
if (editMode && cameraHelperRef.current) {
|
|
269
|
-
cameraHelperRef.current.update();
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
258
|
return (
|
|
274
259
|
<>
|
|
275
260
|
<directionalLight
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Shared editor styles - single source of truth for all prefab editor UI
|
|
2
|
+
|
|
3
|
+
export const colors = {
|
|
4
|
+
bg: 'rgba(0,0,0,0.6)',
|
|
5
|
+
bgLight: 'rgba(255,255,255,0.06)',
|
|
6
|
+
bgHover: 'rgba(255,255,255,0.1)',
|
|
7
|
+
border: 'rgba(255,255,255,0.15)',
|
|
8
|
+
borderLight: 'rgba(255,255,255,0.1)',
|
|
9
|
+
borderFaint: 'rgba(255,255,255,0.05)',
|
|
10
|
+
text: '#fff',
|
|
11
|
+
textMuted: 'rgba(255,255,255,0.7)',
|
|
12
|
+
danger: '#ffaaaa',
|
|
13
|
+
dangerBg: 'rgba(255,80,80,0.2)',
|
|
14
|
+
dangerBorder: 'rgba(255,80,80,0.4)',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const fonts = {
|
|
18
|
+
family: 'system-ui, sans-serif',
|
|
19
|
+
size: 11,
|
|
20
|
+
sizeSm: 10,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Base component styles
|
|
24
|
+
export const base = {
|
|
25
|
+
panel: {
|
|
26
|
+
background: colors.bg,
|
|
27
|
+
color: colors.text,
|
|
28
|
+
border: `1px solid ${colors.border}`,
|
|
29
|
+
borderRadius: 4,
|
|
30
|
+
overflow: 'hidden',
|
|
31
|
+
backdropFilter: 'blur(8px)',
|
|
32
|
+
fontFamily: fonts.family,
|
|
33
|
+
fontSize: fonts.size,
|
|
34
|
+
} as React.CSSProperties,
|
|
35
|
+
|
|
36
|
+
header: {
|
|
37
|
+
padding: '6px 8px',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'space-between',
|
|
41
|
+
cursor: 'pointer',
|
|
42
|
+
background: colors.bgLight,
|
|
43
|
+
borderBottom: `1px solid ${colors.borderLight}`,
|
|
44
|
+
fontSize: fonts.size,
|
|
45
|
+
fontWeight: 500,
|
|
46
|
+
textTransform: 'uppercase',
|
|
47
|
+
letterSpacing: 0.5,
|
|
48
|
+
} as React.CSSProperties,
|
|
49
|
+
|
|
50
|
+
input: {
|
|
51
|
+
width: '100%',
|
|
52
|
+
background: colors.bgHover,
|
|
53
|
+
border: `1px solid ${colors.border}`,
|
|
54
|
+
borderRadius: 3,
|
|
55
|
+
padding: '4px 6px',
|
|
56
|
+
color: colors.text,
|
|
57
|
+
fontSize: fonts.size,
|
|
58
|
+
outline: 'none',
|
|
59
|
+
} as React.CSSProperties,
|
|
60
|
+
|
|
61
|
+
btn: {
|
|
62
|
+
background: colors.bgHover,
|
|
63
|
+
border: `1px solid ${colors.border}`,
|
|
64
|
+
borderRadius: 3,
|
|
65
|
+
padding: '4px 8px',
|
|
66
|
+
color: colors.text,
|
|
67
|
+
fontSize: fonts.size,
|
|
68
|
+
cursor: 'pointer',
|
|
69
|
+
outline: 'none',
|
|
70
|
+
} as React.CSSProperties,
|
|
71
|
+
|
|
72
|
+
btnDanger: {
|
|
73
|
+
background: colors.dangerBg,
|
|
74
|
+
borderColor: colors.dangerBorder,
|
|
75
|
+
color: colors.danger,
|
|
76
|
+
} as React.CSSProperties,
|
|
77
|
+
|
|
78
|
+
label: {
|
|
79
|
+
fontSize: fonts.sizeSm,
|
|
80
|
+
opacity: 0.7,
|
|
81
|
+
marginBottom: 4,
|
|
82
|
+
textTransform: 'uppercase',
|
|
83
|
+
letterSpacing: 0.5,
|
|
84
|
+
} as React.CSSProperties,
|
|
85
|
+
|
|
86
|
+
row: {
|
|
87
|
+
display: 'flex',
|
|
88
|
+
gap: 6,
|
|
89
|
+
} as React.CSSProperties,
|
|
90
|
+
|
|
91
|
+
section: {
|
|
92
|
+
paddingBottom: 8,
|
|
93
|
+
borderBottom: `1px solid ${colors.borderLight}`,
|
|
94
|
+
} as React.CSSProperties,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Specific panel styles
|
|
98
|
+
export const inspector = {
|
|
99
|
+
panel: {
|
|
100
|
+
...base.panel,
|
|
101
|
+
position: 'absolute' as const,
|
|
102
|
+
top: 8,
|
|
103
|
+
right: 8,
|
|
104
|
+
zIndex: 20,
|
|
105
|
+
width: 260,
|
|
106
|
+
},
|
|
107
|
+
content: {
|
|
108
|
+
padding: 8,
|
|
109
|
+
maxHeight: '80vh',
|
|
110
|
+
overflowY: 'auto' as const,
|
|
111
|
+
display: 'flex',
|
|
112
|
+
flexDirection: 'column' as const,
|
|
113
|
+
gap: 8,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const tree = {
|
|
118
|
+
panel: {
|
|
119
|
+
...base.panel,
|
|
120
|
+
maxHeight: '85vh',
|
|
121
|
+
display: 'flex',
|
|
122
|
+
flexDirection: 'column' as const,
|
|
123
|
+
userSelect: 'none' as const,
|
|
124
|
+
},
|
|
125
|
+
scroll: {
|
|
126
|
+
overflowY: 'auto' as const,
|
|
127
|
+
padding: 4,
|
|
128
|
+
},
|
|
129
|
+
row: {
|
|
130
|
+
display: 'flex',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
padding: '3px 6px',
|
|
133
|
+
borderBottom: `1px solid ${colors.borderFaint}`,
|
|
134
|
+
cursor: 'pointer',
|
|
135
|
+
whiteSpace: 'nowrap' as const,
|
|
136
|
+
} as React.CSSProperties,
|
|
137
|
+
selected: {
|
|
138
|
+
background: 'rgba(255,255,255,0.12)',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const menu = {
|
|
143
|
+
container: {
|
|
144
|
+
position: 'fixed' as const,
|
|
145
|
+
zIndex: 50,
|
|
146
|
+
minWidth: 120,
|
|
147
|
+
background: 'rgba(0,0,0,0.85)',
|
|
148
|
+
border: `1px solid ${colors.border}`,
|
|
149
|
+
borderRadius: 4,
|
|
150
|
+
overflow: 'hidden',
|
|
151
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
|
|
152
|
+
backdropFilter: 'blur(8px)',
|
|
153
|
+
},
|
|
154
|
+
item: {
|
|
155
|
+
width: '100%',
|
|
156
|
+
textAlign: 'left' as const,
|
|
157
|
+
padding: '6px 8px',
|
|
158
|
+
background: 'transparent',
|
|
159
|
+
border: 'none',
|
|
160
|
+
color: colors.text,
|
|
161
|
+
fontSize: fonts.size,
|
|
162
|
+
cursor: 'pointer',
|
|
163
|
+
outline: 'none',
|
|
164
|
+
} as React.CSSProperties,
|
|
165
|
+
danger: {
|
|
166
|
+
color: colors.danger,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const toolbar = {
|
|
171
|
+
panel: {
|
|
172
|
+
position: 'absolute' as const,
|
|
173
|
+
top: 8,
|
|
174
|
+
left: '50%',
|
|
175
|
+
transform: 'translateX(-50%)',
|
|
176
|
+
display: 'flex',
|
|
177
|
+
gap: 6,
|
|
178
|
+
padding: '4px 6px',
|
|
179
|
+
background: colors.bg,
|
|
180
|
+
border: `1px solid ${colors.border}`,
|
|
181
|
+
borderRadius: 4,
|
|
182
|
+
color: colors.text,
|
|
183
|
+
fontFamily: fonts.family,
|
|
184
|
+
fontSize: fonts.size,
|
|
185
|
+
backdropFilter: 'blur(8px)',
|
|
186
|
+
},
|
|
187
|
+
divider: {
|
|
188
|
+
width: 1,
|
|
189
|
+
background: 'rgba(255,255,255,0.2)',
|
|
190
|
+
},
|
|
191
|
+
disabled: {
|
|
192
|
+
opacity: 0.4,
|
|
193
|
+
cursor: 'not-allowed',
|
|
194
|
+
},
|
|
195
|
+
};
|