react-three-game 0.0.16 → 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.
Files changed (33) hide show
  1. package/README.md +88 -113
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/tools/prefabeditor/EditorTree.js +27 -15
  5. package/dist/tools/prefabeditor/EditorUI.js +2 -8
  6. package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
  7. package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
  8. package/dist/tools/prefabeditor/PrefabEditor.js +128 -59
  9. package/dist/tools/prefabeditor/PrefabRoot.js +51 -33
  10. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
  11. package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
  12. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
  13. package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
  14. package/dist/tools/prefabeditor/components/RotatorComponent.d.ts +3 -0
  15. package/dist/tools/prefabeditor/components/RotatorComponent.js +42 -0
  16. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
  17. package/dist/tools/prefabeditor/components/TransformComponent.js +28 -3
  18. package/dist/tools/prefabeditor/components/index.js +2 -0
  19. package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
  20. package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
  21. package/package.json +8 -8
  22. package/src/index.ts +4 -0
  23. package/src/tools/prefabeditor/EditorTree.tsx +39 -16
  24. package/src/tools/prefabeditor/EditorUI.tsx +2 -27
  25. package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
  26. package/src/tools/prefabeditor/PrefabEditor.tsx +202 -86
  27. package/src/tools/prefabeditor/PrefabRoot.tsx +62 -54
  28. package/src/tools/prefabeditor/components/ComponentRegistry.ts +7 -1
  29. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
  30. package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
  31. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
  32. package/src/tools/prefabeditor/components/TransformComponent.tsx +69 -16
  33. package/src/tools/prefabeditor/components/index.ts +2 -0
@@ -0,0 +1,332 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ import { useRef, useEffect } from "react";
3
+ import { useFrame, useThree } from "@react-three/fiber";
4
+ import { CameraHelper, DirectionalLight, Object3D, Vector3 } from "three";
5
+
6
+ function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
7
+ const props = {
8
+ color: component.properties.color ?? '#ffffff',
9
+ intensity: component.properties.intensity ?? 1.0,
10
+ castShadow: component.properties.castShadow ?? true,
11
+ shadowMapSize: component.properties.shadowMapSize ?? 1024,
12
+ shadowCameraNear: component.properties.shadowCameraNear ?? 0.1,
13
+ shadowCameraFar: component.properties.shadowCameraFar ?? 100,
14
+ shadowCameraTop: component.properties.shadowCameraTop ?? 30,
15
+ shadowCameraBottom: component.properties.shadowCameraBottom ?? -30,
16
+ shadowCameraLeft: component.properties.shadowCameraLeft ?? -30,
17
+ shadowCameraRight: component.properties.shadowCameraRight ?? 30,
18
+ targetOffset: component.properties.targetOffset ?? [0, -5, 0]
19
+ };
20
+
21
+ return <div className="flex flex-col gap-2">
22
+ <div>
23
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Color</label>
24
+ <div className="flex gap-0.5">
25
+ <input
26
+ type="color"
27
+ className="h-5 w-5 bg-transparent border-none cursor-pointer"
28
+ value={props.color}
29
+ onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
30
+ />
31
+ <input
32
+ type="text"
33
+ 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"
34
+ value={props.color}
35
+ onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
36
+ />
37
+ </div>
38
+ </div>
39
+ <div>
40
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Intensity</label>
41
+ <input
42
+ type="number"
43
+ step="0.1"
44
+ 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"
45
+ value={props.intensity}
46
+ onChange={e => onUpdate({ ...component.properties, 'intensity': parseFloat(e.target.value) })}
47
+ />
48
+ </div>
49
+ <div>
50
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Cast Shadow</label>
51
+ <input
52
+ type="checkbox"
53
+ className="h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer"
54
+ checked={props.castShadow}
55
+ onChange={e => onUpdate({ ...component.properties, 'castShadow': e.target.checked })}
56
+ />
57
+ </div>
58
+ <div>
59
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Shadow Map Size</label>
60
+ <input
61
+ type="number"
62
+ step="256"
63
+ 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"
64
+ value={props.shadowMapSize}
65
+ onChange={e => onUpdate({ ...component.properties, 'shadowMapSize': parseFloat(e.target.value) })}
66
+ />
67
+ </div>
68
+ <div className="border-t border-cyan-500/20 pt-2 mt-2">
69
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">Shadow Camera</label>
70
+ <div className="grid grid-cols-2 gap-1">
71
+ <div>
72
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Near</label>
73
+ <input
74
+ type="number"
75
+ step="0.1"
76
+ 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"
77
+ value={props.shadowCameraNear}
78
+ onChange={e => onUpdate({ ...component.properties, 'shadowCameraNear': parseFloat(e.target.value) })}
79
+ />
80
+ </div>
81
+ <div>
82
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Far</label>
83
+ <input
84
+ type="number"
85
+ step="1"
86
+ 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"
87
+ value={props.shadowCameraFar}
88
+ onChange={e => onUpdate({ ...component.properties, 'shadowCameraFar': parseFloat(e.target.value) })}
89
+ />
90
+ </div>
91
+ <div>
92
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Top</label>
93
+ <input
94
+ type="number"
95
+ step="1"
96
+ 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"
97
+ value={props.shadowCameraTop}
98
+ onChange={e => onUpdate({ ...component.properties, 'shadowCameraTop': parseFloat(e.target.value) })}
99
+ />
100
+ </div>
101
+ <div>
102
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Bottom</label>
103
+ <input
104
+ type="number"
105
+ step="1"
106
+ 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"
107
+ value={props.shadowCameraBottom}
108
+ onChange={e => onUpdate({ ...component.properties, 'shadowCameraBottom': parseFloat(e.target.value) })}
109
+ />
110
+ </div>
111
+ <div>
112
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Left</label>
113
+ <input
114
+ type="number"
115
+ step="1"
116
+ 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"
117
+ value={props.shadowCameraLeft}
118
+ onChange={e => onUpdate({ ...component.properties, 'shadowCameraLeft': parseFloat(e.target.value) })}
119
+ />
120
+ </div>
121
+ <div>
122
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Right</label>
123
+ <input
124
+ type="number"
125
+ step="1"
126
+ 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"
127
+ value={props.shadowCameraRight}
128
+ onChange={e => onUpdate({ ...component.properties, 'shadowCameraRight': parseFloat(e.target.value) })}
129
+ />
130
+ </div>
131
+ </div>
132
+ </div>
133
+ <div className="border-t border-cyan-500/20 pt-2 mt-2">
134
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">Target Offset</label>
135
+ <div className="grid grid-cols-3 gap-1">
136
+ <div>
137
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">X</label>
138
+ <input
139
+ type="number"
140
+ step="0.5"
141
+ 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"
142
+ value={props.targetOffset[0]}
143
+ onChange={e => onUpdate({
144
+ ...component.properties,
145
+ 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]]
146
+ })}
147
+ />
148
+ </div>
149
+ <div>
150
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Y</label>
151
+ <input
152
+ type="number"
153
+ step="0.5"
154
+ 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"
155
+ value={props.targetOffset[1]}
156
+ onChange={e => onUpdate({
157
+ ...component.properties,
158
+ 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]]
159
+ })}
160
+ />
161
+ </div>
162
+ <div>
163
+ <label className="block text-[8px] text-cyan-400/50 mb-0.5">Z</label>
164
+ <input
165
+ type="number"
166
+ step="0.5"
167
+ 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"
168
+ value={props.targetOffset[2]}
169
+ onChange={e => onUpdate({
170
+ ...component.properties,
171
+ 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)]
172
+ })}
173
+ />
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>;
178
+ }
179
+
180
+ function DirectionalLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
181
+ const color = properties.color ?? '#ffffff';
182
+ const intensity = properties.intensity ?? 1.0;
183
+ const castShadow = properties.castShadow ?? true;
184
+ const shadowMapSize = properties.shadowMapSize ?? 1024;
185
+ const shadowCameraNear = properties.shadowCameraNear ?? 0.1;
186
+ const shadowCameraFar = properties.shadowCameraFar ?? 100;
187
+ const shadowCameraTop = properties.shadowCameraTop ?? 30;
188
+ const shadowCameraBottom = properties.shadowCameraBottom ?? -30;
189
+ const shadowCameraLeft = properties.shadowCameraLeft ?? -30;
190
+ const shadowCameraRight = properties.shadowCameraRight ?? 30;
191
+ const targetOffset = properties.targetOffset ?? [0, -5, 0];
192
+
193
+ const { scene } = useThree();
194
+ const directionalLightRef = useRef<DirectionalLight>(null);
195
+ const targetRef = useRef<Object3D>(new Object3D());
196
+ const lastUpdate = useRef(0);
197
+ const cameraHelperRef = useRef<CameraHelper | null>(null);
198
+ const lastPositionRef = useRef<Vector3>(new Vector3());
199
+
200
+ // Add target to scene
201
+ useEffect(() => {
202
+ if (targetRef.current) {
203
+ scene.add(targetRef.current);
204
+ return () => {
205
+ scene.remove(targetRef.current);
206
+ };
207
+ }
208
+ }, [scene]);
209
+
210
+ // Update target position when light position or offset changes
211
+ useEffect(() => {
212
+ if (directionalLightRef.current && targetRef.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
+ );
220
+ directionalLightRef.current.target = targetRef.current;
221
+ }
222
+ });
223
+
224
+ useEffect(() => {
225
+ // Create camera helper for edit mode and add to scene
226
+ if (editMode && directionalLightRef.current && directionalLightRef.current.shadow.camera) {
227
+ const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
228
+ cameraHelperRef.current = helper;
229
+ scene.add(helper);
230
+
231
+ return () => {
232
+ if (cameraHelperRef.current) {
233
+ scene.remove(cameraHelperRef.current);
234
+ cameraHelperRef.current.dispose();
235
+ }
236
+ };
237
+ }
238
+ }, [editMode, scene]);
239
+
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
+ return (
274
+ <>
275
+ <directionalLight
276
+ ref={directionalLightRef}
277
+ color={color}
278
+ intensity={intensity}
279
+ castShadow={castShadow}
280
+ shadow-mapSize={[shadowMapSize, shadowMapSize]}
281
+ shadow-bias={-0.001}
282
+ shadow-normalBias={0.02}
283
+ >
284
+ <orthographicCamera
285
+ attach="shadow-camera"
286
+ near={shadowCameraNear}
287
+ far={shadowCameraFar}
288
+ top={shadowCameraTop}
289
+ bottom={shadowCameraBottom}
290
+ left={shadowCameraLeft}
291
+ right={shadowCameraRight}
292
+ />
293
+ </directionalLight>
294
+ {editMode && (
295
+ <>
296
+ {/* Light source indicator */}
297
+ <mesh>
298
+ <sphereGeometry args={[0.3, 8, 6]} />
299
+ <meshBasicMaterial color={color} wireframe />
300
+ </mesh>
301
+ {/* Target indicator */}
302
+ <mesh position={targetOffset as [number, number, number]}>
303
+ <sphereGeometry args={[0.2, 8, 6]} />
304
+ <meshBasicMaterial color={color} wireframe opacity={0.5} transparent />
305
+ </mesh>
306
+ {/* Direction line */}
307
+ <line>
308
+ <bufferGeometry
309
+ onUpdate={(geo) => {
310
+ const points = [
311
+ new Vector3(0, 0, 0),
312
+ new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
313
+ ];
314
+ geo.setFromPoints(points);
315
+ }}
316
+ />
317
+ <lineBasicMaterial color={color} opacity={0.6} transparent />
318
+ </line>
319
+ </>
320
+ )}
321
+ </>
322
+ );
323
+ }
324
+
325
+ const DirectionalLightComponent: Component = {
326
+ name: 'DirectionalLight',
327
+ Editor: DirectionalLightComponentEditor,
328
+ View: DirectionalLightView,
329
+ defaultProperties: {}
330
+ };
331
+
332
+ export default DirectionalLightComponent;
@@ -47,12 +47,22 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }: { componen
47
47
  // View for Model component
48
48
  function ModelComponentView({ properties, loadedModels, children }: { properties: any, loadedModels?: Record<string, any>, children?: React.ReactNode }) {
49
49
  // Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
50
- if (!properties.filename || properties.instanced) return children || null;
50
+ if (!properties.filename || properties.instanced) return <>{children}</>;
51
+
51
52
  if (loadedModels && loadedModels[properties.filename]) {
52
- return <>{<primitive object={loadedModels[properties.filename].clone()} />}{children}</>;
53
+ const clonedModel = loadedModels[properties.filename].clone();
54
+ // Enable shadows on all meshes in the model
55
+ clonedModel.traverse((obj: any) => {
56
+ if (obj.isMesh) {
57
+ obj.castShadow = true;
58
+ obj.receiveShadow = true;
59
+ }
60
+ });
61
+ return <primitive object={clonedModel}>{children}</primitive>;
53
62
  }
54
- // Optionally, render a placeholder if model is not loaded
55
- return children || null;
63
+
64
+ // Model not loaded yet - render children only
65
+ return <>{children}</>;
56
66
  }
57
67
 
58
68
  const ModelComponent: Component = {
@@ -1,9 +1,7 @@
1
-
2
1
  import { Component } from "./ComponentRegistry";
3
- import { useRef } from "react";
2
+ import { useRef, useEffect } from "react";
4
3
 
5
4
  function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
6
- // Provide default values to prevent NaN
7
5
  const props = {
8
6
  color: component.properties.color ?? '#ffffff',
9
7
  intensity: component.properties.intensity ?? 1.0,
@@ -88,10 +86,7 @@ function SpotLightComponentEditor({ component, onUpdate }: { component: any; onU
88
86
  </div>;
89
87
  }
90
88
 
91
-
92
- // The view component for SpotLight
93
- function SpotLightView({ properties }: { properties: any }) {
94
- // Provide defaults in case properties are missing
89
+ function SpotLightView({ properties, editMode }: { properties: any; editMode?: boolean }) {
95
90
  const color = properties.color ?? '#ffffff';
96
91
  const intensity = properties.intensity ?? 1.0;
97
92
  const angle = properties.angle ?? Math.PI / 6;
@@ -99,16 +94,41 @@ function SpotLightView({ properties }: { properties: any }) {
99
94
  const distance = properties.distance ?? 100;
100
95
  const castShadow = properties.castShadow ?? true;
101
96
 
97
+ const spotLightRef = useRef<any>(null);
98
+ const targetRef = useRef<any>(null);
99
+
100
+ useEffect(() => {
101
+ if (spotLightRef.current && targetRef.current) {
102
+ spotLightRef.current.target = targetRef.current;
103
+ }
104
+ }, []);
105
+
102
106
  return (
103
107
  <>
104
108
  <spotLight
109
+ ref={spotLightRef}
105
110
  color={color}
106
111
  intensity={intensity}
107
112
  angle={angle}
108
113
  penumbra={penumbra}
109
114
  distance={distance}
110
115
  castShadow={castShadow}
116
+ shadow-bias={-0.0001}
117
+ shadow-normalBias={0.02}
111
118
  />
119
+ <object3D ref={targetRef} position={[0, -5, 0]} />
120
+ {editMode && (
121
+ <>
122
+ <mesh>
123
+ <sphereGeometry args={[0.2, 8, 6]} />
124
+ <meshBasicMaterial color={color} wireframe />
125
+ </mesh>
126
+ <mesh position={[0, -5, 0]}>
127
+ <sphereGeometry args={[0.15, 8, 6]} />
128
+ <meshBasicMaterial color={color} wireframe opacity={0.5} transparent />
129
+ </mesh>
130
+ </>
131
+ )}
112
132
  </>
113
133
  );
114
134
  }
@@ -1,8 +1,54 @@
1
1
 
2
2
  import { Component } from "./ComponentRegistry";
3
3
 
4
- function TransformComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
4
+ function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }: {
5
+ component: any;
6
+ onUpdate: (newComp: any) => void;
7
+ transformMode?: "translate" | "rotate" | "scale";
8
+ setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
9
+ }) {
10
+ const s = {
11
+ button: {
12
+ padding: '2px 6px',
13
+ background: 'transparent',
14
+ color: 'rgba(255,255,255,0.9)',
15
+ border: '1px solid rgba(255,255,255,0.14)',
16
+ borderRadius: 4,
17
+ cursor: 'pointer',
18
+ font: 'inherit',
19
+ },
20
+ buttonActive: {
21
+ background: 'rgba(255,255,255,0.10)',
22
+ },
23
+ };
24
+
5
25
  return <div className="flex flex-col">
26
+ {transformMode && setTransformMode && (
27
+ <div className="mb-2">
28
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">Transform Mode</label>
29
+ <div style={{ display: 'flex', gap: 6 }}>
30
+ {["translate", "rotate", "scale"].map(mode => (
31
+ <button
32
+ key={mode}
33
+ onClick={() => setTransformMode(mode as any)}
34
+ style={{
35
+ ...s.button,
36
+ flex: 1,
37
+ ...(transformMode === mode ? s.buttonActive : {}),
38
+ }}
39
+ onPointerEnter={(e) => {
40
+ if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
41
+ }}
42
+ onPointerLeave={(e) => {
43
+ if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'transparent';
44
+ }}
45
+ >
46
+ {mode}
47
+ </button>
48
+ ))}
49
+ </div>
50
+ </div>
51
+ )}
6
52
  <Vector3Input label="Position" value={component.properties.position} onChange={v => onUpdate({ position: v })} />
7
53
  <Vector3Input label="Rotation" value={component.properties.rotation} onChange={v => onUpdate({ rotation: v })} />
8
54
  <Vector3Input label="Scale" value={component.properties.scale} onChange={v => onUpdate({ scale: v })} />
@@ -29,21 +75,28 @@ export function Vector3Input({ label, value, onChange }: { label: string, value:
29
75
  onChange(newValue);
30
76
  };
31
77
 
32
- return <div className="mb-1">
33
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">{label}</label>
34
- <div className="flex gap-0.5">
35
- <div className="relative flex-1">
36
- <span className="absolute left-0.5 top-0 text-[8px] text-red-400/80 font-mono">X</span>
37
- <input className="w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50" type="number" step="0.1" value={value[0]} onChange={e => handleChange(0, e.target.value)} />
38
- </div>
39
- <div className="relative flex-1">
40
- <span className="absolute left-0.5 top-0 text-[8px] text-green-400/80 font-mono">Y</span>
41
- <input className="w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50" type="number" step="0.1" value={value[1]} onChange={e => handleChange(1, e.target.value)} />
42
- </div>
43
- <div className="relative flex-1">
44
- <span className="absolute left-0.5 top-0 text-[8px] text-blue-400/80 font-mono">Z</span>
45
- <input className="w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50" type="number" step="0.1" value={value[2]} onChange={e => handleChange(2, e.target.value)} />
46
- </div>
78
+ const axes = [
79
+ { key: 'x', color: 'red', index: 0 },
80
+ { key: 'y', color: 'green', index: 1 },
81
+ { key: 'z', color: 'blue', index: 2 }
82
+ ] as const;
83
+
84
+ return <div className="mb-2">
85
+ <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1">{label}</label>
86
+ <div className="flex gap-1">
87
+ {axes.map(({ key, color, index }) => (
88
+ <div key={key} className="flex-1 flex items-center gap-1 bg-black/30 border border-cyan-500/20 rounded px-1.5 py-1 min-h-[32px]">
89
+ <span className={`text-xs font-bold text-${color}-400 w-3`}>{key.toUpperCase()}</span>
90
+ <input
91
+ className="flex-1 bg-transparent text-xs text-cyan-200 font-mono outline-none w-full min-w-0"
92
+ type="number"
93
+ step="0.1"
94
+ value={value[index].toFixed(2)}
95
+ onChange={e => handleChange(index, e.target.value)}
96
+ onFocus={e => e.target.select()}
97
+ />
98
+ </div>
99
+ ))}
47
100
  </div>
48
101
  </div>
49
102
  }
@@ -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
 
8
9
  export default [
@@ -11,6 +12,7 @@ export default [
11
12
  MaterialComponent,
12
13
  PhysicsComponent,
13
14
  SpotLightComponent,
15
+ DirectionalLightComponent,
14
16
  ModelComponent
15
17
  ];
16
18