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.
- package/README.md +88 -113
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/tools/prefabeditor/EditorTree.js +27 -15
- package/dist/tools/prefabeditor/EditorUI.js +2 -8
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
- package/dist/tools/prefabeditor/PrefabEditor.js +128 -59
- package/dist/tools/prefabeditor/PrefabRoot.js +51 -33
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
- package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
- package/dist/tools/prefabeditor/components/RotatorComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/RotatorComponent.js +42 -0
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
- package/dist/tools/prefabeditor/components/TransformComponent.js +28 -3
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
- package/package.json +8 -8
- package/src/index.ts +4 -0
- package/src/tools/prefabeditor/EditorTree.tsx +39 -16
- package/src/tools/prefabeditor/EditorUI.tsx +2 -27
- package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
- package/src/tools/prefabeditor/PrefabEditor.tsx +202 -86
- package/src/tools/prefabeditor/PrefabRoot.tsx +62 -54
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +7 -1
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
- package/src/tools/prefabeditor/components/TransformComponent.tsx +69 -16
- 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] }) }),
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
131
|
+
if (gameObject.disabled === true || gameObject.hidden === true)
|
|
132
|
+
return null;
|
|
133
|
+
// Build context object for passing to helper functions
|
|
132
134
|
const ctx = { gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode };
|
|
133
|
-
// --- 1.
|
|
135
|
+
// --- 1. Compute transforms (local + world) ---
|
|
134
136
|
const transformProps = getNodeTransformProps(gameObject);
|
|
135
137
|
const localMatrix = new Matrix4().compose(new Vector3(...transformProps.position), new Quaternion().setFromEuler(new Euler(...transformProps.rotation)), new Vector3(...transformProps.scale));
|
|
136
138
|
const worldMatrix = parentMatrix.clone().multiply(localMatrix);
|
|
137
|
-
//
|
|
139
|
+
// --- 2. Handle selection interaction (edit mode only) ---
|
|
138
140
|
const clickValid = useRef(false);
|
|
139
141
|
const handlePointerDown = (e) => {
|
|
140
142
|
e.stopPropagation();
|
|
@@ -151,20 +153,18 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
|
|
|
151
153
|
}
|
|
152
154
|
clickValid.current = false;
|
|
153
155
|
};
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
// --- 2. If instanced, short-circuit to a tiny clean branch ---
|
|
156
|
+
// --- 3. If instanced model, short-circuit to GameInstance (terminal node) ---
|
|
157
157
|
const isInstanced = !!((_c = (_b = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced);
|
|
158
158
|
if (isInstanced) {
|
|
159
159
|
return renderInstancedNode(gameObject, worldMatrix, ctx);
|
|
160
160
|
}
|
|
161
|
-
// ---
|
|
161
|
+
// --- 4. Render core content using component system ---
|
|
162
162
|
const core = renderCoreNode(gameObject, ctx, parentMatrix);
|
|
163
|
-
// --- 5.
|
|
164
|
-
const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
|
|
165
|
-
// --- 4. Wrap with physics if needed ---
|
|
163
|
+
// --- 5. Wrap with physics if needed (except in edit mode) ---
|
|
166
164
|
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
167
|
-
// --- 6.
|
|
165
|
+
// --- 6. Render children recursively (always relative transforms) ---
|
|
166
|
+
const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
|
|
167
|
+
// --- 7. Final group wrapper with local transform ---
|
|
168
168
|
return (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [physicsWrapped, children] }));
|
|
169
169
|
}
|
|
170
170
|
// Helper: render an instanced GameInstance (terminal node)
|
|
@@ -179,16 +179,16 @@ function renderInstancedNode(gameObject, worldMatrix, ctx) {
|
|
|
179
179
|
const modelUrl = (_d = (_c = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.model) === null || _c === void 0 ? void 0 : _c.properties) === null || _d === void 0 ? void 0 : _d.filename;
|
|
180
180
|
return (_jsx(GameInstance, { id: gameObject.id, modelUrl: modelUrl, position: [wp.x, wp.y, wp.z], rotation: [we.x, we.y, we.z], scale: [ws.x, ws.y, ws.z], physics: ctx.editMode ? undefined : physics === null || physics === void 0 ? void 0 : physics.properties }));
|
|
181
181
|
}
|
|
182
|
-
// Helper: render main
|
|
182
|
+
// Helper: render main content for a non-instanced node using the component system
|
|
183
183
|
function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
184
184
|
var _a, _b, _c;
|
|
185
185
|
const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
|
|
186
186
|
const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
|
|
187
|
-
const
|
|
187
|
+
const model = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
|
|
188
188
|
const geometryDef = geometry ? getComponent('Geometry') : undefined;
|
|
189
189
|
const materialDef = material ? getComponent('Material') : undefined;
|
|
190
|
-
const
|
|
191
|
-
//
|
|
190
|
+
const modelDef = model ? getComponent('Model') : undefined;
|
|
191
|
+
// Context props for all component Views
|
|
192
192
|
const contextProps = {
|
|
193
193
|
loadedModels: ctx.loadedModels,
|
|
194
194
|
loadedTextures: ctx.loadedTextures,
|
|
@@ -197,29 +197,47 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
197
197
|
parentMatrix,
|
|
198
198
|
registerRef: ctx.registerRef,
|
|
199
199
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
208
|
+
return;
|
|
206
209
|
const def = getComponent(comp.type);
|
|
207
210
|
if (!def || !def.View)
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
233
|
+
else {
|
|
234
|
+
// No visual component - just render leaves
|
|
235
|
+
coreContent = _jsx(_Fragment, { children: leafComponents });
|
|
220
236
|
}
|
|
221
|
-
//
|
|
222
|
-
return
|
|
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,114 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import { CameraHelper, Object3D, Vector3 } from "three";
|
|
5
|
+
function DirectionalLightComponentEditor({ component, onUpdate }) {
|
|
6
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
7
|
+
const props = {
|
|
8
|
+
color: (_a = component.properties.color) !== null && _a !== void 0 ? _a : '#ffffff',
|
|
9
|
+
intensity: (_b = component.properties.intensity) !== null && _b !== void 0 ? _b : 1.0,
|
|
10
|
+
castShadow: (_c = component.properties.castShadow) !== null && _c !== void 0 ? _c : true,
|
|
11
|
+
shadowMapSize: (_d = component.properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024,
|
|
12
|
+
shadowCameraNear: (_e = component.properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1,
|
|
13
|
+
shadowCameraFar: (_f = component.properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100,
|
|
14
|
+
shadowCameraTop: (_g = component.properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30,
|
|
15
|
+
shadowCameraBottom: (_h = component.properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30,
|
|
16
|
+
shadowCameraLeft: (_j = component.properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30,
|
|
17
|
+
shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
|
|
18
|
+
targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
|
|
19
|
+
};
|
|
20
|
+
return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Cast Shadow" }), _jsx("input", { type: "checkbox", className: "h-4 w-4 bg-black/40 border border-cyan-500/30 cursor-pointer", checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Shadow Camera" }), _jsxs("div", { className: "grid grid-cols-2 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Near" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Far" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Top" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Bottom" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Left" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Right" }), _jsx("input", { type: "number", step: "1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { className: "border-t border-cyan-500/20 pt-2 mt-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: "Target Offset" }), _jsxs("div", { className: "grid grid-cols-3 gap-1", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "X" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[0], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Y" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[1], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[8px] text-cyan-400/50 mb-0.5", children: "Z" }), _jsx("input", { type: "number", step: "0.5", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.targetOffset[2], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)] })) })] })] })] })] });
|
|
21
|
+
}
|
|
22
|
+
function DirectionalLightView({ properties, editMode }) {
|
|
23
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
24
|
+
const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
|
|
25
|
+
const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
|
|
26
|
+
const castShadow = (_c = properties.castShadow) !== null && _c !== void 0 ? _c : true;
|
|
27
|
+
const shadowMapSize = (_d = properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024;
|
|
28
|
+
const shadowCameraNear = (_e = properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1;
|
|
29
|
+
const shadowCameraFar = (_f = properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100;
|
|
30
|
+
const shadowCameraTop = (_g = properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30;
|
|
31
|
+
const shadowCameraBottom = (_h = properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30;
|
|
32
|
+
const shadowCameraLeft = (_j = properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30;
|
|
33
|
+
const shadowCameraRight = (_k = properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30;
|
|
34
|
+
const targetOffset = (_l = properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0];
|
|
35
|
+
const { scene } = useThree();
|
|
36
|
+
const directionalLightRef = useRef(null);
|
|
37
|
+
const targetRef = useRef(new Object3D());
|
|
38
|
+
const lastUpdate = useRef(0);
|
|
39
|
+
const cameraHelperRef = useRef(null);
|
|
40
|
+
const lastPositionRef = useRef(new Vector3());
|
|
41
|
+
// Add target to scene
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (targetRef.current) {
|
|
44
|
+
scene.add(targetRef.current);
|
|
45
|
+
return () => {
|
|
46
|
+
scene.remove(targetRef.current);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}, [scene]);
|
|
50
|
+
// Update target position when light position or offset changes
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (directionalLightRef.current && targetRef.current) {
|
|
53
|
+
const lightWorldPos = new Vector3();
|
|
54
|
+
directionalLightRef.current.getWorldPosition(lightWorldPos);
|
|
55
|
+
targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
|
|
56
|
+
directionalLightRef.current.target = targetRef.current;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
// Create camera helper for edit mode and add to scene
|
|
61
|
+
if (editMode && directionalLightRef.current && directionalLightRef.current.shadow.camera) {
|
|
62
|
+
const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
|
|
63
|
+
cameraHelperRef.current = helper;
|
|
64
|
+
scene.add(helper);
|
|
65
|
+
return () => {
|
|
66
|
+
if (cameraHelperRef.current) {
|
|
67
|
+
scene.remove(cameraHelperRef.current);
|
|
68
|
+
cameraHelperRef.current.dispose();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}, [editMode, scene]);
|
|
73
|
+
useFrame(({ clock }) => {
|
|
74
|
+
if (!directionalLightRef.current || !directionalLightRef.current.shadow)
|
|
75
|
+
return;
|
|
76
|
+
// Disable auto-update for shadows
|
|
77
|
+
if (directionalLightRef.current.shadow.autoUpdate) {
|
|
78
|
+
directionalLightRef.current.shadow.autoUpdate = false;
|
|
79
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
80
|
+
}
|
|
81
|
+
// Check if position has changed
|
|
82
|
+
const currentPosition = new Vector3();
|
|
83
|
+
directionalLightRef.current.getWorldPosition(currentPosition);
|
|
84
|
+
const positionChanged = !currentPosition.equals(lastPositionRef.current);
|
|
85
|
+
if (positionChanged) {
|
|
86
|
+
lastPositionRef.current.copy(currentPosition);
|
|
87
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
88
|
+
lastUpdate.current = clock.elapsedTime; // Reset timer on position change
|
|
89
|
+
}
|
|
90
|
+
// Update shadow map infrequently (every 5 seconds) if position hasn't changed
|
|
91
|
+
if (!editMode && !positionChanged && clock.elapsedTime - lastUpdate.current > 5) {
|
|
92
|
+
lastUpdate.current = clock.elapsedTime;
|
|
93
|
+
directionalLightRef.current.shadow.needsUpdate = true;
|
|
94
|
+
}
|
|
95
|
+
// Update camera helper in edit mode
|
|
96
|
+
if (editMode && cameraHelperRef.current) {
|
|
97
|
+
cameraHelperRef.current.update();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize": [shadowMapSize, shadowMapSize], "shadow-bias": -0.001, "shadow-normalBias": 0.02, children: _jsx("orthographicCamera", { attach: "shadow-camera", near: shadowCameraNear, far: shadowCameraFar, top: shadowCameraTop, bottom: shadowCameraBottom, left: shadowCameraLeft, right: shadowCameraRight }) }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
|
|
101
|
+
const points = [
|
|
102
|
+
new Vector3(0, 0, 0),
|
|
103
|
+
new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
|
|
104
|
+
];
|
|
105
|
+
geo.setFromPoints(points);
|
|
106
|
+
} }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] }))] }));
|
|
107
|
+
}
|
|
108
|
+
const DirectionalLightComponent = {
|
|
109
|
+
name: 'DirectionalLight',
|
|
110
|
+
Editor: DirectionalLightComponentEditor,
|
|
111
|
+
View: DirectionalLightView,
|
|
112
|
+
defaultProperties: {}
|
|
113
|
+
};
|
|
114
|
+
export default DirectionalLightComponent;
|
|
@@ -21,12 +21,20 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
21
21
|
function ModelComponentView({ properties, loadedModels, children }) {
|
|
22
22
|
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
23
23
|
if (!properties.filename || properties.instanced)
|
|
24
|
-
return children
|
|
24
|
+
return _jsx(_Fragment, { children: children });
|
|
25
25
|
if (loadedModels && loadedModels[properties.filename]) {
|
|
26
|
-
|
|
26
|
+
const clonedModel = loadedModels[properties.filename].clone();
|
|
27
|
+
// Enable shadows on all meshes in the model
|
|
28
|
+
clonedModel.traverse((obj) => {
|
|
29
|
+
if (obj.isMesh) {
|
|
30
|
+
obj.castShadow = true;
|
|
31
|
+
obj.receiveShadow = true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return _jsx("primitive", { object: clonedModel, children: children });
|
|
27
35
|
}
|
|
28
|
-
//
|
|
29
|
-
return children
|
|
36
|
+
// Model not loaded yet - render children only
|
|
37
|
+
return _jsx(_Fragment, { children: children });
|
|
30
38
|
}
|
|
31
39
|
const ModelComponent = {
|
|
32
40
|
name: 'Model',
|
|
@@ -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
|
-
|
|
16
|
-
function SpotLightView({ properties }) {
|
|
15
|
+
function SpotLightView({ properties, editMode }) {
|
|
17
16
|
var _a, _b, _c, _d, _e, _f;
|
|
18
|
-
// Provide defaults in case properties are missing
|
|
19
17
|
const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
|
|
20
18
|
const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
|
|
21
19
|
const angle = (_c = properties.angle) !== null && _c !== void 0 ? _c : Math.PI / 6;
|
|
22
20
|
const penumbra = (_d = properties.penumbra) !== null && _d !== void 0 ? _d : 0.5;
|
|
23
21
|
const distance = (_e = properties.distance) !== null && _e !== void 0 ? _e : 100;
|
|
24
22
|
const castShadow = (_f = properties.castShadow) !== null && _f !== void 0 ? _f : true;
|
|
25
|
-
|
|
23
|
+
const spotLightRef = useRef(null);
|
|
24
|
+
const targetRef = useRef(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (spotLightRef.current && targetRef.current) {
|
|
27
|
+
spotLightRef.current.target = targetRef.current;
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
|
|
26
31
|
}
|
|
27
32
|
const SpotLightComponent = {
|
|
28
33
|
name: 'SpotLight',
|