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
@@ -50,65 +50,134 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
50
50
  };
51
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, ref: prefabRef,
52
52
  // props for edit mode
53
- editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: {
54
- position: "absolute",
55
- top: 8,
56
- left: "50%",
57
- transform: "translateX(-50%)",
58
- display: "flex",
59
- alignItems: "center",
60
- gap: 6,
61
- padding: "2px 4px",
62
- background: "rgba(0,0,0,0.55)",
63
- border: "1px solid rgba(255,255,255,0.12)",
64
- borderRadius: 4,
65
- color: "rgba(255,255,255,0.9)",
66
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
67
- fontSize: 11,
68
- lineHeight: 1,
69
- WebkitUserSelect: "none",
70
- userSelect: "none",
71
- }, children: [_jsx("button", { style: {
72
- padding: "2px 6px",
73
- font: "inherit",
74
- background: "transparent",
75
- color: "inherit",
76
- border: "1px solid rgba(255,255,255,0.18)",
77
- borderRadius: 3,
78
- cursor: "pointer",
79
- }, onClick: () => setEditMode(!editMode), onPointerEnter: (e) => {
80
- e.currentTarget.style.background = "rgba(255,255,255,0.08)";
81
- }, onPointerLeave: (e) => {
82
- e.currentTarget.style.background = "transparent";
83
- }, children: editMode ? "▶" : "⏸" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx("button", { style: {
84
- padding: "2px 6px",
85
- font: "inherit",
86
- background: "transparent",
87
- color: "inherit",
88
- border: "1px solid rgba(255,255,255,0.18)",
89
- borderRadius: 3,
90
- cursor: "pointer",
91
- }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
92
- const prefab = yield loadJson();
93
- if (prefab)
94
- setLoadedPrefab(prefab);
95
- }), onPointerEnter: (e) => {
96
- e.currentTarget.style.background = "rgba(255,255,255,0.08)";
97
- }, onPointerLeave: (e) => {
98
- e.currentTarget.style.background = "transparent";
99
- }, children: "\uD83D\uDCE5" }), _jsx("button", { style: {
100
- padding: "2px 6px",
101
- font: "inherit",
102
- background: "transparent",
103
- color: "inherit",
104
- border: "1px solid rgba(255,255,255,0.18)",
105
- borderRadius: 3,
106
- cursor: "pointer",
107
- }, onClick: () => saveJson(loadedPrefab, "prefab"), onPointerEnter: (e) => {
108
- e.currentTarget.style.background = "rgba(255,255,255,0.08)";
109
- }, onPointerLeave: (e) => {
110
- e.currentTarget.style.background = "transparent";
111
- }, children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
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 })] });
54
+ };
55
+ const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }) => {
56
+ const [history, setHistory] = useState([currentData]);
57
+ const [historyIndex, setHistoryIndex] = useState(0);
58
+ const throttleTimeoutRef = useRef(null);
59
+ const lastSavedDataRef = useRef(JSON.stringify(currentData));
60
+ // Define undo/redo handlers
61
+ const handleUndo = () => {
62
+ if (historyIndex > 0) {
63
+ const newIndex = historyIndex - 1;
64
+ setHistoryIndex(newIndex);
65
+ lastSavedDataRef.current = JSON.stringify(history[newIndex]);
66
+ onDataChange(history[newIndex]);
67
+ }
68
+ };
69
+ const handleRedo = () => {
70
+ if (historyIndex < history.length - 1) {
71
+ const newIndex = historyIndex + 1;
72
+ setHistoryIndex(newIndex);
73
+ lastSavedDataRef.current = JSON.stringify(history[newIndex]);
74
+ onDataChange(history[newIndex]);
75
+ }
76
+ };
77
+ // Keyboard shortcuts for undo/redo
78
+ useEffect(() => {
79
+ const handleKeyDown = (e) => {
80
+ // Undo: Ctrl+Z (Cmd+Z on Mac)
81
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
82
+ e.preventDefault();
83
+ handleUndo();
84
+ }
85
+ // Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
86
+ else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
87
+ e.preventDefault();
88
+ handleRedo();
89
+ }
90
+ };
91
+ window.addEventListener('keydown', handleKeyDown);
92
+ return () => window.removeEventListener('keydown', handleKeyDown);
93
+ }, [historyIndex, history]);
94
+ // Throttled history update when currentData changes
95
+ useEffect(() => {
96
+ const currentDataStr = JSON.stringify(currentData);
97
+ // Skip if data hasn't actually changed
98
+ if (currentDataStr === lastSavedDataRef.current) {
99
+ return;
100
+ }
101
+ // Clear existing throttle timeout
102
+ if (throttleTimeoutRef.current) {
103
+ clearTimeout(throttleTimeoutRef.current);
104
+ }
105
+ // Set new throttled update
106
+ throttleTimeoutRef.current = setTimeout(() => {
107
+ lastSavedDataRef.current = currentDataStr;
108
+ setHistory(prev => {
109
+ // Slice history at current index (discard future states)
110
+ const newHistory = prev.slice(0, historyIndex + 1);
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;
119
+ });
120
+ setHistoryIndex(prev => {
121
+ const newHistory = history.slice(0, prev + 1);
122
+ newHistory.push(currentData);
123
+ return Math.min(newHistory.length - 1, 49);
124
+ });
125
+ }, 500); // 500ms throttle
126
+ return () => {
127
+ if (throttleTimeoutRef.current) {
128
+ clearTimeout(throttleTimeoutRef.current);
129
+ }
130
+ };
131
+ }, [currentData, historyIndex, history]);
132
+ const handleLoad = () => __awaiter(void 0, void 0, void 0, function* () {
133
+ const prefab = yield loadJson();
134
+ if (prefab) {
135
+ onDataChange(prefab);
136
+ // Reset history when loading new file
137
+ setHistory([prefab]);
138
+ setHistoryIndex(0);
139
+ lastSavedDataRef.current = JSON.stringify(prefab);
140
+ }
141
+ });
142
+ const canUndo = historyIndex > 0;
143
+ 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 });
112
181
  };
113
182
  const saveJson = (data, filename) => {
114
183
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
@@ -128,13 +128,15 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
128
128
  // Early return if gameObject is null or undefined
129
129
  if (!gameObject)
130
130
  return null;
131
- // Build a small context object to avoid long param lists
131
+ if (gameObject.disabled === true || gameObject.hidden === true)
132
+ return null;
133
+ // Build context object for passing to helper functions
132
134
  const ctx = { gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode };
133
- // --- 1. Transform (local + world) ---
135
+ // --- 1. Compute transforms (local + world) ---
134
136
  const transformProps = getNodeTransformProps(gameObject);
135
137
  const localMatrix = new Matrix4().compose(new Vector3(...transformProps.position), new Quaternion().setFromEuler(new Euler(...transformProps.rotation)), new Vector3(...transformProps.scale));
136
138
  const worldMatrix = parentMatrix.clone().multiply(localMatrix);
137
- // preserve click/drag detection from previous implementation
139
+ // --- 2. Handle selection interaction (edit mode only) ---
138
140
  const clickValid = useRef(false);
139
141
  const handlePointerDown = (e) => {
140
142
  e.stopPropagation();
@@ -151,20 +153,18 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
151
153
  }
152
154
  clickValid.current = false;
153
155
  };
154
- if (gameObject.disabled === true || gameObject.hidden === true)
155
- return null;
156
- // --- 2. If instanced, short-circuit to a tiny clean branch ---
156
+ // --- 3. If instanced model, short-circuit to GameInstance (terminal node) ---
157
157
  const isInstanced = !!((_c = (_b = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced);
158
158
  if (isInstanced) {
159
159
  return renderInstancedNode(gameObject, worldMatrix, ctx);
160
160
  }
161
- // --- 3. Core content decided by component registry ---
161
+ // --- 4. Render core content using component system ---
162
162
  const core = renderCoreNode(gameObject, ctx, parentMatrix);
163
- // --- 5. Render children (always relative transforms) ---
164
- const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
165
- // --- 4. Wrap with physics if needed ---
163
+ // --- 5. Wrap with physics if needed (except in edit mode) ---
166
164
  const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
167
- // --- 6. Final group wrapper ---
165
+ // --- 6. Render children recursively (always relative transforms) ---
166
+ const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
167
+ // --- 7. Final group wrapper with local transform ---
168
168
  return (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [physicsWrapped, children] }));
169
169
  }
170
170
  // Helper: render an instanced GameInstance (terminal node)
@@ -179,16 +179,16 @@ function renderInstancedNode(gameObject, worldMatrix, ctx) {
179
179
  const modelUrl = (_d = (_c = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.model) === null || _c === void 0 ? void 0 : _c.properties) === null || _d === void 0 ? void 0 : _d.filename;
180
180
  return (_jsx(GameInstance, { id: gameObject.id, modelUrl: modelUrl, position: [wp.x, wp.y, wp.z], rotation: [we.x, we.y, we.z], scale: [ws.x, ws.y, ws.z], physics: ctx.editMode ? undefined : physics === null || physics === void 0 ? void 0 : physics.properties }));
181
181
  }
182
- // Helper: render main model/geometry content for a non-instanced node
182
+ // Helper: render main content for a non-instanced node using the component system
183
183
  function renderCoreNode(gameObject, ctx, parentMatrix) {
184
184
  var _a, _b, _c;
185
185
  const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
186
186
  const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
187
- const modelComp = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
187
+ const model = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
188
188
  const geometryDef = geometry ? getComponent('Geometry') : undefined;
189
189
  const materialDef = material ? getComponent('Material') : undefined;
190
- const isModelAvailable = !!(modelComp && modelComp.properties && modelComp.properties.filename && ctx.loadedModels[modelComp.properties.filename]);
191
- // Generic component views (exclude geometry/material/model)
190
+ const modelDef = model ? getComponent('Model') : undefined;
191
+ // Context props for all component Views
192
192
  const contextProps = {
193
193
  loadedModels: ctx.loadedModels,
194
194
  loadedTextures: ctx.loadedTextures,
@@ -197,29 +197,47 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
197
197
  parentMatrix,
198
198
  registerRef: ctx.registerRef,
199
199
  };
200
- const allComponentViews = gameObject.components
201
- ? Object.entries(gameObject.components)
202
- .filter(([key]) => key !== 'geometry' && key !== 'material' && key !== 'model' && key !== 'transform' && key !== 'physics')
203
- .map(([key, comp]) => {
200
+ // Collect wrapper and leaf components (excluding transform/physics which are handled separately)
201
+ const wrapperComponents = [];
202
+ const leafComponents = [];
203
+ if (gameObject.components) {
204
+ Object.entries(gameObject.components)
205
+ .filter(([key]) => !['geometry', 'material', 'model', 'transform', 'physics'].includes(key))
206
+ .forEach(([key, comp]) => {
204
207
  if (!comp || !comp.type)
205
- return null;
208
+ return;
206
209
  const def = getComponent(comp.type);
207
210
  if (!def || !def.View)
208
- return null;
209
- return _jsx(def.View, Object.assign({ properties: comp.properties }, contextProps), key);
210
- })
211
- : null;
212
- // If we have a model (non-instanced) render it as a primitive with material override
213
- if (isModelAvailable) {
214
- const modelObj = ctx.loadedModels[modelComp.properties.filename].clone();
215
- return (_jsxs("primitive", { object: modelObj, children: [material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), allComponentViews] }));
211
+ return;
212
+ // Components that accept children are wrappers, others are leaves
213
+ const viewString = def.View.toString();
214
+ if (viewString.includes('children')) {
215
+ wrapperComponents.push({ key, View: def.View, properties: comp.properties });
216
+ }
217
+ else {
218
+ leafComponents.push(_jsx(def.View, Object.assign({ properties: comp.properties }, contextProps), key));
219
+ }
220
+ });
221
+ }
222
+ // Build core content based on what components exist
223
+ let coreContent;
224
+ // Priority: Model > Geometry + Material > Empty
225
+ if (model && modelDef && modelDef.View) {
226
+ // Model component wraps its children (including material override)
227
+ coreContent = (_jsxs(modelDef.View, Object.assign({ properties: model.properties }, contextProps, { children: [material && materialDef && materialDef.View && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material")), leafComponents] })));
228
+ }
229
+ else if (geometry && geometryDef && geometryDef.View) {
230
+ // Geometry + Material = mesh
231
+ coreContent = (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && materialDef && materialDef.View && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material")), leafComponents] }));
216
232
  }
217
- // Otherwise, if geometry present, render a mesh
218
- if (geometry && geometryDef && geometryDef.View) {
219
- return (_jsxs("mesh", { children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps), "geometry"), material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), allComponentViews] }));
233
+ else {
234
+ // No visual component - just render leaves
235
+ coreContent = _jsx(_Fragment, { children: leafComponents });
220
236
  }
221
- // Default: render other component views (no geometry/model)
222
- return _jsx(_Fragment, { children: allComponentViews });
237
+ // Wrap core content with wrapper components (in order)
238
+ return wrapperComponents.reduce((content, { key, View, properties }) => {
239
+ return _jsx(View, Object.assign({ properties: properties }, contextProps, { children: content }), key);
240
+ }, coreContent);
223
241
  }
224
242
  // Helper: wrap core content with physics component when necessary
225
243
  function wrapPhysicsIfNeeded(gameObject, content, ctx) {
@@ -5,6 +5,8 @@ export interface Component {
5
5
  component: any;
6
6
  onUpdate: (newComp: any) => void;
7
7
  basePath?: string;
8
+ transformMode?: "translate" | "rotate" | "scale";
9
+ setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
8
10
  }>;
9
11
  defaultProperties: any;
10
12
  View?: FC<any>;
@@ -0,0 +1,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const DirectionalLightComponent: Component;
3
+ export default DirectionalLightComponent;
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useEffect } from "react";
3
+ import { useFrame, useThree } from "@react-three/fiber";
4
+ import { CameraHelper, Object3D, Vector3 } from "three";
5
+ function DirectionalLightComponentEditor({ component, onUpdate }) {
6
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
7
+ const props = {
8
+ color: (_a = component.properties.color) !== null && _a !== void 0 ? _a : '#ffffff',
9
+ intensity: (_b = component.properties.intensity) !== null && _b !== void 0 ? _b : 1.0,
10
+ castShadow: (_c = component.properties.castShadow) !== null && _c !== void 0 ? _c : true,
11
+ shadowMapSize: (_d = component.properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024,
12
+ shadowCameraNear: (_e = component.properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1,
13
+ shadowCameraFar: (_f = component.properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100,
14
+ shadowCameraTop: (_g = component.properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30,
15
+ shadowCameraBottom: (_h = component.properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30,
16
+ shadowCameraLeft: (_j = component.properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30,
17
+ shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
18
+ targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
19
+ };
20
+ return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Cast Shadow" }), _jsx("input", { type: "checkbox", className: "h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer", checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Shadow Camera" }), _jsxs("div", { className: "grid grid-cols-2 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Near" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Far" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Top" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Bottom" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Left" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Right" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Target Offset" }), _jsxs("div", { className: "grid grid-cols-3 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "X" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[0], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Y" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[1], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Z" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[2], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)] })) })] })] })] })] });
21
+ }
22
+ function DirectionalLightView({ properties, editMode }) {
23
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
24
+ const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
25
+ const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
26
+ const castShadow = (_c = properties.castShadow) !== null && _c !== void 0 ? _c : true;
27
+ const shadowMapSize = (_d = properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024;
28
+ const shadowCameraNear = (_e = properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1;
29
+ const shadowCameraFar = (_f = properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100;
30
+ const shadowCameraTop = (_g = properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30;
31
+ const shadowCameraBottom = (_h = properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30;
32
+ const shadowCameraLeft = (_j = properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30;
33
+ const shadowCameraRight = (_k = properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30;
34
+ const targetOffset = (_l = properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0];
35
+ const { scene } = useThree();
36
+ const directionalLightRef = useRef(null);
37
+ const targetRef = useRef(new Object3D());
38
+ const lastUpdate = useRef(0);
39
+ const cameraHelperRef = useRef(null);
40
+ const lastPositionRef = useRef(new Vector3());
41
+ // Add target to scene
42
+ useEffect(() => {
43
+ if (targetRef.current) {
44
+ scene.add(targetRef.current);
45
+ return () => {
46
+ scene.remove(targetRef.current);
47
+ };
48
+ }
49
+ }, [scene]);
50
+ // Update target position when light position or offset changes
51
+ useEffect(() => {
52
+ if (directionalLightRef.current && targetRef.current) {
53
+ const lightWorldPos = new Vector3();
54
+ directionalLightRef.current.getWorldPosition(lightWorldPos);
55
+ targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
56
+ directionalLightRef.current.target = targetRef.current;
57
+ }
58
+ });
59
+ useEffect(() => {
60
+ // Create camera helper for edit mode and add to scene
61
+ if (editMode && directionalLightRef.current && directionalLightRef.current.shadow.camera) {
62
+ const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
63
+ cameraHelperRef.current = helper;
64
+ scene.add(helper);
65
+ return () => {
66
+ if (cameraHelperRef.current) {
67
+ scene.remove(cameraHelperRef.current);
68
+ cameraHelperRef.current.dispose();
69
+ }
70
+ };
71
+ }
72
+ }, [editMode, scene]);
73
+ useFrame(({ clock }) => {
74
+ if (!directionalLightRef.current || !directionalLightRef.current.shadow)
75
+ return;
76
+ // Disable auto-update for shadows
77
+ if (directionalLightRef.current.shadow.autoUpdate) {
78
+ directionalLightRef.current.shadow.autoUpdate = false;
79
+ directionalLightRef.current.shadow.needsUpdate = true;
80
+ }
81
+ // Check if position has changed
82
+ const currentPosition = new Vector3();
83
+ directionalLightRef.current.getWorldPosition(currentPosition);
84
+ const positionChanged = !currentPosition.equals(lastPositionRef.current);
85
+ if (positionChanged) {
86
+ lastPositionRef.current.copy(currentPosition);
87
+ directionalLightRef.current.shadow.needsUpdate = true;
88
+ lastUpdate.current = clock.elapsedTime; // Reset timer on position change
89
+ }
90
+ // Update shadow map infrequently (every 5 seconds) if position hasn't changed
91
+ if (!editMode && !positionChanged && clock.elapsedTime - lastUpdate.current > 5) {
92
+ lastUpdate.current = clock.elapsedTime;
93
+ directionalLightRef.current.shadow.needsUpdate = true;
94
+ }
95
+ // Update camera helper in edit mode
96
+ if (editMode && cameraHelperRef.current) {
97
+ cameraHelperRef.current.update();
98
+ }
99
+ });
100
+ return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize": [shadowMapSize, shadowMapSize], "shadow-bias": -0.001, "shadow-normalBias": 0.02, children: _jsx("orthographicCamera", { attach: "shadow-camera", near: shadowCameraNear, far: shadowCameraFar, top: shadowCameraTop, bottom: shadowCameraBottom, left: shadowCameraLeft, right: shadowCameraRight }) }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
101
+ const points = [
102
+ new Vector3(0, 0, 0),
103
+ new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
104
+ ];
105
+ geo.setFromPoints(points);
106
+ } }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] }))] }));
107
+ }
108
+ const DirectionalLightComponent = {
109
+ name: 'DirectionalLight',
110
+ Editor: DirectionalLightComponentEditor,
111
+ View: DirectionalLightView,
112
+ defaultProperties: {}
113
+ };
114
+ export default DirectionalLightComponent;
@@ -21,12 +21,20 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
21
21
  function ModelComponentView({ properties, loadedModels, children }) {
22
22
  // Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
23
23
  if (!properties.filename || properties.instanced)
24
- return children || null;
24
+ return _jsx(_Fragment, { children: children });
25
25
  if (loadedModels && loadedModels[properties.filename]) {
26
- return _jsxs(_Fragment, { children: [_jsx("primitive", { object: loadedModels[properties.filename].clone() }), children] });
26
+ const clonedModel = loadedModels[properties.filename].clone();
27
+ // Enable shadows on all meshes in the model
28
+ clonedModel.traverse((obj) => {
29
+ if (obj.isMesh) {
30
+ obj.castShadow = true;
31
+ obj.receiveShadow = true;
32
+ }
33
+ });
34
+ return _jsx("primitive", { object: clonedModel, children: children });
27
35
  }
28
- // Optionally, render a placeholder if model is not loaded
29
- return children || null;
36
+ // Model not loaded yet - render children only
37
+ return _jsx(_Fragment, { children: children });
30
38
  }
31
39
  const ModelComponent = {
32
40
  name: 'Model',
@@ -0,0 +1,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const RotatorComponent: Component;
3
+ export default RotatorComponent;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useRef } from "react";
4
+ function RotatorComponentEditor({ component, onUpdate }) {
5
+ var _a, _b;
6
+ const props = {
7
+ speed: (_a = component.properties.speed) !== null && _a !== void 0 ? _a : 1.0,
8
+ axis: (_b = component.properties.axis) !== null && _b !== void 0 ? _b : 'y'
9
+ };
10
+ 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: "Rotation Speed" }), _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.speed, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { speed: parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Rotation Axis" }), _jsxs("select", { 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.axis, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { axis: e.target.value })), children: [_jsx("option", { value: "x", children: "X" }), _jsx("option", { value: "y", children: "Y" }), _jsx("option", { value: "z", children: "Z" })] })] })] });
11
+ }
12
+ // The view component for Rotator
13
+ function RotatorView({ properties, children }) {
14
+ var _a, _b;
15
+ const groupRef = useRef(null);
16
+ const speed = (_a = properties.speed) !== null && _a !== void 0 ? _a : 1.0;
17
+ const axis = (_b = properties.axis) !== null && _b !== void 0 ? _b : 'y';
18
+ useFrame((state, delta) => {
19
+ if (groupRef.current) {
20
+ if (axis === 'x') {
21
+ groupRef.current.rotation.x += delta * speed;
22
+ }
23
+ else if (axis === 'y') {
24
+ groupRef.current.rotation.y += delta * speed;
25
+ }
26
+ else if (axis === 'z') {
27
+ groupRef.current.rotation.z += delta * speed;
28
+ }
29
+ }
30
+ });
31
+ return (_jsx("group", { ref: groupRef, children: children }));
32
+ }
33
+ const RotatorComponent = {
34
+ name: 'Rotator',
35
+ Editor: RotatorComponentEditor,
36
+ View: RotatorView,
37
+ defaultProperties: {
38
+ speed: 1.0,
39
+ axis: 'y'
40
+ }
41
+ };
42
+ export default RotatorComponent;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useEffect } from "react";
2
3
  function SpotLightComponentEditor({ component, onUpdate }) {
3
4
  var _a, _b, _c, _d, _e, _f;
4
- // Provide default values to prevent NaN
5
5
  const props = {
6
6
  color: (_a = component.properties.color) !== null && _a !== void 0 ? _a : '#ffffff',
7
7
  intensity: (_b = component.properties.intensity) !== null && _b !== void 0 ? _b : 1.0,
@@ -12,17 +12,22 @@ function SpotLightComponentEditor({ component, onUpdate }) {
12
12
  };
13
13
  return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Angle" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: Math.PI, className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.angle, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'angle': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Penumbra" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.penumbra, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'penumbra': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Distance" }), _jsx("input", { type: "number", step: "1", min: "0", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.distance, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'distance': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Cast Shadow" }), _jsx("input", { type: "checkbox", className: "h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer", checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] })] });
14
14
  }
15
- // The view component for SpotLight
16
- function SpotLightView({ properties }) {
15
+ function SpotLightView({ properties, editMode }) {
17
16
  var _a, _b, _c, _d, _e, _f;
18
- // Provide defaults in case properties are missing
19
17
  const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
20
18
  const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
21
19
  const angle = (_c = properties.angle) !== null && _c !== void 0 ? _c : Math.PI / 6;
22
20
  const penumbra = (_d = properties.penumbra) !== null && _d !== void 0 ? _d : 0.5;
23
21
  const distance = (_e = properties.distance) !== null && _e !== void 0 ? _e : 100;
24
22
  const castShadow = (_f = properties.castShadow) !== null && _f !== void 0 ? _f : true;
25
- return (_jsx(_Fragment, { children: _jsx("spotLight", { color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow }) }));
23
+ const spotLightRef = useRef(null);
24
+ const targetRef = useRef(null);
25
+ useEffect(() => {
26
+ if (spotLightRef.current && targetRef.current) {
27
+ spotLightRef.current.target = targetRef.current;
28
+ }
29
+ }, []);
30
+ return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
26
31
  }
27
32
  const SpotLightComponent = {
28
33
  name: 'SpotLight',