react-three-game 0.0.54 → 0.0.56

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 (39) hide show
  1. package/dist/shared/ContactShadow.d.ts +8 -0
  2. package/dist/shared/ContactShadow.js +32 -0
  3. package/dist/tools/assetviewer/page.js +1 -1
  4. package/dist/tools/dragdrop/DragDropLoader.js +17 -40
  5. package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
  6. package/dist/tools/dragdrop/modelLoader.js +39 -0
  7. package/dist/tools/prefabeditor/EditorTree.js +3 -16
  8. package/dist/tools/prefabeditor/EditorUI.js +5 -10
  9. package/dist/tools/prefabeditor/PrefabEditor.js +57 -1
  10. package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
  11. package/dist/tools/prefabeditor/PrefabRoot.js +17 -2
  12. package/dist/tools/prefabeditor/components/Input.js +27 -26
  13. package/dist/tools/prefabeditor/components/MaterialComponent.js +9 -2
  14. package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
  15. package/dist/tools/prefabeditor/components/PhysicsComponent.js +1 -1
  16. package/dist/tools/prefabeditor/components/SpotLightComponent.js +3 -0
  17. package/dist/tools/prefabeditor/components/TransformComponent.js +13 -11
  18. package/dist/tools/prefabeditor/styles.d.ts +12 -2
  19. package/dist/tools/prefabeditor/styles.js +63 -30
  20. package/dist/tools/prefabeditor/utils.d.ts +4 -0
  21. package/dist/tools/prefabeditor/utils.js +39 -1
  22. package/package.json +1 -1
  23. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
  24. package/src/shared/ContactShadow.tsx +74 -0
  25. package/src/tools/assetviewer/page.tsx +1 -1
  26. package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
  27. package/src/tools/dragdrop/modelLoader.ts +36 -0
  28. package/src/tools/prefabeditor/EditorTree.tsx +4 -15
  29. package/src/tools/prefabeditor/EditorUI.tsx +5 -10
  30. package/src/tools/prefabeditor/PrefabEditor.tsx +60 -1
  31. package/src/tools/prefabeditor/PrefabRoot.tsx +21 -2
  32. package/src/tools/prefabeditor/components/Input.tsx +27 -26
  33. package/src/tools/prefabeditor/components/MaterialComponent.tsx +14 -5
  34. package/src/tools/prefabeditor/components/ModelComponent.tsx +2 -2
  35. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +1 -1
  36. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +4 -0
  37. package/src/tools/prefabeditor/components/TransformComponent.tsx +17 -11
  38. package/src/tools/prefabeditor/styles.ts +65 -30
  39. package/src/tools/prefabeditor/utils.ts +41 -1
@@ -0,0 +1,8 @@
1
+ interface ContactShadowProps {
2
+ opacity?: number;
3
+ blur?: number;
4
+ scale?: number;
5
+ yOffset?: number;
6
+ }
7
+ declare const ContactShadow: ({ opacity, blur, scale, yOffset, }: ContactShadowProps) => import("react/jsx-runtime").JSX.Element;
8
+ export default ContactShadow;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import * as THREE from "three/webgpu";
5
+ import { float, uv, vec3, smoothstep, uniform, length, } from "three/tsl";
6
+ const ContactShadow = ({ opacity = 0.4, blur = 2.5, scale = 1.2, yOffset = 0.05, }) => {
7
+ const material = useMemo(() => {
8
+ const mat = new THREE.MeshBasicNodeMaterial();
9
+ mat.transparent = true;
10
+ mat.depthWrite = false;
11
+ mat.depthTest = true;
12
+ mat.side = THREE.DoubleSide;
13
+ mat.polygonOffset = true;
14
+ mat.polygonOffsetFactor = -1;
15
+ mat.polygonOffsetUnits = -1;
16
+ const uOpacity = uniform(opacity);
17
+ const uBlur = uniform(blur);
18
+ // UVs centered around origin
19
+ const centeredUV = uv().sub(0.5).mul(2.0);
20
+ // IMPORTANT: use functional length(), not .length()
21
+ const dist = length(centeredUV);
22
+ const innerRadius = float(0.0);
23
+ const outerRadius = float(1.0);
24
+ const blurAmount = uBlur.div(10.0);
25
+ const alpha = smoothstep(outerRadius, innerRadius.add(blurAmount), dist).mul(uOpacity);
26
+ mat.colorNode = vec3(0.0, 0.0, 0.0);
27
+ mat.opacityNode = alpha;
28
+ return mat;
29
+ }, [opacity, blur]);
30
+ return (_jsx("mesh", { rotation: [-Math.PI / 2, 0, 0], position: [0, yOffset, 0], material: material, renderOrder: 1, children: _jsx("planeGeometry", { args: [scale, scale] }) }));
31
+ };
32
+ export default ContactShadow;
@@ -156,7 +156,7 @@ export function SingleSoundViewer({ file, basePath = "" }) {
156
156
  // Shared Canvas Component - can be used independently in any viewer
157
157
  export function SharedCanvas() {
158
158
  return (_jsx(Canvas, { shadows: true, dpr: [1, 1.5], camera: { position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }, style: {
159
- position: 'absolute',
159
+ position: 'fixed',
160
160
  top: 0,
161
161
  left: 0,
162
162
  width: '100vw',
@@ -1,49 +1,26 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
11
  // DragDropLoader.tsx
3
12
  import { useEffect } from "react";
4
- import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
5
- // Shared file handling logic
13
+ import { parseModelFromFile } from "./modelLoader";
6
14
  function handleFiles(files, onModelLoaded) {
7
- files.forEach((file) => {
8
- if (file.name.endsWith(".glb") || file.name.endsWith(".gltf")) {
9
- loadGLTFFile(file, onModelLoaded);
10
- }
11
- else if (file.name.endsWith(".fbx")) {
12
- loadFBXFile(file, onModelLoaded);
13
- }
14
- });
15
- }
16
- function loadGLTFFile(file, onModelLoaded) {
17
- const reader = new FileReader();
18
- reader.onload = (event) => {
19
- var _a;
20
- const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
21
- if (arrayBuffer) {
22
- const loader = new GLTFLoader();
23
- const dracoLoader = new DRACOLoader();
24
- dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
25
- loader.setDRACOLoader(dracoLoader);
26
- loader.parse(arrayBuffer, "", (gltf) => {
27
- onModelLoaded(gltf.scene, file.name);
28
- }, (error) => {
29
- console.error("GLTFLoader parse error", error);
30
- });
15
+ files.forEach((file) => __awaiter(this, void 0, void 0, function* () {
16
+ const result = yield parseModelFromFile(file);
17
+ if (result.success && result.model) {
18
+ onModelLoaded(result.model, file.name);
31
19
  }
32
- };
33
- reader.readAsArrayBuffer(file);
34
- }
35
- function loadFBXFile(file, onModelLoaded) {
36
- const reader = new FileReader();
37
- reader.onload = (event) => {
38
- var _a;
39
- const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
40
- if (arrayBuffer) {
41
- const loader = new FBXLoader();
42
- const model = loader.parse(arrayBuffer, "");
43
- onModelLoaded(model, file.name);
20
+ else {
21
+ console.error("Model parse error:", result.error);
44
22
  }
45
- };
46
- reader.readAsArrayBuffer(file);
23
+ }));
47
24
  }
48
25
  export function DragDropLoader({ onModelLoaded }) {
49
26
  useEffect(() => {
@@ -4,4 +4,9 @@ export type ModelLoadResult = {
4
4
  error?: any;
5
5
  };
6
6
  export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
7
+ /**
8
+ * Parse a model from a File object (e.g. from drag-drop or file picker).
9
+ * Returns the parsed Three.js Object3D scene.
10
+ */
11
+ export declare function parseModelFromFile(file: File): Promise<ModelLoadResult>;
7
12
  export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
@@ -14,6 +14,45 @@ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
14
14
  const gltfLoader = new GLTFLoader();
15
15
  gltfLoader.setDRACOLoader(dracoLoader);
16
16
  const fbxLoader = new FBXLoader();
17
+ /**
18
+ * Parse a model from a File object (e.g. from drag-drop or file picker).
19
+ * Returns the parsed Three.js Object3D scene.
20
+ */
21
+ export function parseModelFromFile(file) {
22
+ return new Promise((resolve) => {
23
+ const reader = new FileReader();
24
+ reader.onload = (event) => {
25
+ var _a;
26
+ const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
27
+ if (!arrayBuffer) {
28
+ resolve({ success: false, error: new Error('Failed to read file') });
29
+ return;
30
+ }
31
+ const name = file.name.toLowerCase();
32
+ if (name.endsWith('.glb') || name.endsWith('.gltf')) {
33
+ gltfLoader.parse(arrayBuffer, '', (gltf) => {
34
+ resolve({ success: true, model: gltf.scene });
35
+ }, (error) => {
36
+ resolve({ success: false, error });
37
+ });
38
+ }
39
+ else if (name.endsWith('.fbx')) {
40
+ try {
41
+ const model = fbxLoader.parse(arrayBuffer, '');
42
+ resolve({ success: true, model });
43
+ }
44
+ catch (error) {
45
+ resolve({ success: false, error });
46
+ }
47
+ }
48
+ else {
49
+ resolve({ success: false, error: new Error(`Unsupported file format: ${file.name}`) });
50
+ }
51
+ };
52
+ reader.onerror = () => resolve({ success: false, error: reader.error });
53
+ reader.readAsArrayBuffer(file);
54
+ });
55
+ }
17
56
  export function loadModel(filename, onProgress) {
18
57
  return __awaiter(this, void 0, void 0, function* () {
19
58
  try {
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
11
  import { useState } from 'react';
12
12
  import { getComponent } from './components/ComponentRegistry';
13
- import { base, tree, menu } from './styles';
13
+ import { base, colors, tree, menu } from './styles';
14
14
  import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
15
15
  import { useEditorContext } from './EditorContext';
16
16
  export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }) {
@@ -149,20 +149,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
149
149
  handleToggleDisabled(node.id);
150
150
  }, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
151
151
  };
152
- return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
153
- .tree-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
154
- .tree-scroll::-webkit-scrollbar-track { background: transparent; }
155
- .tree-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
156
- ` }), _jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: () => setFileMenuOpen(false) }))] })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 4px', borderBottom: '1px solid rgba(255,255,255,0.1)' }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: {
157
- width: '100%',
158
- padding: '4px 8px',
159
- background: 'rgba(255,255,255,0.05)',
160
- border: '1px solid rgba(255,255,255,0.1)',
161
- borderRadius: 3,
162
- color: 'inherit',
163
- fontSize: 11,
164
- outline: 'none',
165
- } }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
152
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: () => setFileMenuOpen(false) }))] })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: Object.assign(Object.assign({}, base.input), { padding: '4px 8px' }) }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
166
153
  }
167
154
  function FileMenu({ prefabData, setPrefabData, onClose }) {
168
155
  const { onScreenshot, onExportGLB } = useEditorContext();
@@ -187,5 +174,5 @@ function FileMenu({ prefabData, setPrefabData, onClose }) {
187
174
  }) })));
188
175
  onClose();
189
176
  });
190
- return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: 28, right: 0 }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: handleLoad, children: "\uD83D\uDCE5 Load Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleSave, children: "\uD83D\uDCBE Save Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleLoadIntoScene, children: "\uD83D\uDCC2 Load into Scene" }), _jsx("button", { style: menu.item, onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "\uD83D\uDCF8 Screenshot" }), _jsx("button", { style: menu.item, onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "\uD83D\uDCE6 Export GLB" })] }));
177
+ return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'absolute', top: 28, right: 0 }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: handleLoad, children: "\uD83D\uDCE5 Load Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleSave, children: "\uD83D\uDCBE Save Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleLoadIntoScene, children: "\uD83D\uDCC2 Load into Scene" }), _jsx("button", { style: menu.item, onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "\uD83D\uDCF8 Screenshot" }), _jsx("button", { style: menu.item, onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "\uD83D\uDCE6 Export GLB" })] }));
191
178
  }
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  import { useState, useEffect } from 'react';
14
14
  import EditorTree from './EditorTree';
15
15
  import { getAllComponents } from './components/ComponentRegistry';
16
- import { base, inspector } from './styles';
16
+ import { base, colors, inspector, scrollbarCSS, componentCard } from './styles';
17
17
  import { findNode, updateNode, deleteNode } from './utils';
18
18
  function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePath, onUndo, onRedo, canUndo, canRedo }) {
19
19
  const [collapsed, setCollapsed] = useState(false);
@@ -29,12 +29,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePa
29
29
  setSelectedId(null);
30
30
  };
31
31
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
32
- return _jsxs(_Fragment, { children: [_jsx("style", { children: `
33
- .prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
34
- .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
35
- .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
36
- .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
37
- ` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
32
+ return _jsxs(_Fragment, { children: [_jsx("style", { children: scrollbarCSS }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
38
33
  }
39
34
  function NodeInspector({ node, updateNode, deleteNode, basePath }) {
40
35
  var _a;
@@ -47,13 +42,13 @@ function NodeInspector({ node, updateNode, deleteNode, basePath }) {
47
42
  if (!newAvailable.includes(addType))
48
43
  setAddType(newAvailable[0] || "");
49
44
  }, [Object.keys(node.components || {}).join(',')]);
50
- return _jsxs("div", { style: Object.assign(Object.assign({}, inspector.content), { paddingRight: 2 }), className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
45
+ return _jsxs("div", { style: Object.assign(Object.assign({}, inspector.content), { paddingRight: 2 }), className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: colors.textDim, wordBreak: 'break-all', border: `1px solid ${colors.border}`, padding: '2px 6px', borderRadius: 3, flex: 1, fontFamily: 'monospace' }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
51
46
  if (!comp)
52
47
  return null;
53
48
  const def = ALL_COMPONENTS[comp.type];
54
49
  if (!def)
55
- return _jsxs("div", { style: { color: '#ff8888', fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
56
- return (_jsxs("div", { style: { marginBottom: 8, backgroundColor: 'rgba(255, 255, 255, 0.1)', padding: 8, borderRadius: 4 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
50
+ return _jsxs("div", { style: { color: colors.danger, fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
51
+ return (_jsxs("div", { style: componentCard.container, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
57
52
  const _a = n.components || {}, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
58
53
  return Object.assign(Object.assign({}, n), { components: rest });
59
54
  }), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath }))] }, key));
@@ -6,7 +6,8 @@ import { Physics } from "@react-three/rapier";
6
6
  import EditorUI from "./EditorUI";
7
7
  import { base, toolbar } from "./styles";
8
8
  import { EditorContext } from "./EditorContext";
9
- import { exportGLB } from "./utils";
9
+ import { exportGLB, createModelNode, createImageNode } from "./utils";
10
+ import { parseModelFromFile } from "../dragdrop/modelLoader";
10
11
  const DEFAULT_PREFAB = {
11
12
  id: "prefab-default",
12
13
  name: "New Prefab",
@@ -111,6 +112,61 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
111
112
  if (canvas)
112
113
  canvasRef.current = canvas;
113
114
  }, []);
115
+ // --- Drag & drop files to add nodes ---
116
+ useEffect(() => {
117
+ const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
118
+ const MODEL_EXTS = ['glb', 'gltf', 'fbx'];
119
+ function handleDragOver(e) {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ }
123
+ function handleDrop(e) {
124
+ var _a;
125
+ e.preventDefault();
126
+ e.stopPropagation();
127
+ const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
128
+ files.forEach(file => {
129
+ var _a, _b;
130
+ const ext = (_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
131
+ if (!ext)
132
+ return;
133
+ const baseName = file.name.replace(/\.[^.]+$/, '');
134
+ if (MODEL_EXTS.includes(ext)) {
135
+ const modelPath = `models/${file.name}`;
136
+ const newNode = createModelNode(modelPath, baseName);
137
+ updatePrefab(prev => {
138
+ var _a;
139
+ return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
140
+ });
141
+ parseModelFromFile(file).then(result => {
142
+ var _a;
143
+ if (result.success && result.model) {
144
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, result.model);
145
+ }
146
+ else {
147
+ console.error('Drop parse error:', result.error);
148
+ }
149
+ });
150
+ }
151
+ else if (IMAGE_EXTS.includes(ext)) {
152
+ const texturePath = `textures/${file.name}`;
153
+ const newNode = createImageNode(texturePath, baseName);
154
+ updatePrefab(prev => {
155
+ var _a;
156
+ return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
157
+ });
158
+ // Inject a blob URL texture so it renders immediately
159
+ (_b = prefabRootRef.current) === null || _b === void 0 ? void 0 : _b.injectTexture(texturePath, file);
160
+ }
161
+ });
162
+ }
163
+ window.addEventListener('dragover', handleDragOver);
164
+ window.addEventListener('drop', handleDrop);
165
+ return () => {
166
+ window.removeEventListener('dragover', handleDragOver);
167
+ window.removeEventListener('drop', handleDrop);
168
+ };
169
+ }, [loadedPrefab]);
114
170
  useImperativeHandle(ref, () => ({
115
171
  screenshot: handleScreenshot,
116
172
  exportGLB: handleExportGLB,
@@ -4,6 +4,8 @@ import { Prefab, GameObject as GameObjectType } from "./types";
4
4
  export interface PrefabRootRef {
5
5
  root: Group | null;
6
6
  rigidBodyRefs: Map<string, any>;
7
+ injectModel: (filename: string, model: Object3D) => void;
8
+ injectTexture: (filename: string, file: File) => void;
7
9
  }
8
10
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
9
11
  editMode?: boolean;
@@ -33,10 +33,25 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
33
33
  const rigidBodyRefs = useRef(new Map());
34
34
  const [selectedObject, setSelectedObject] = useState(null);
35
35
  const rootRef = useRef(null);
36
+ const injectModel = useCallback((filename, model) => {
37
+ setModels(m => (Object.assign(Object.assign({}, m), { [filename]: model })));
38
+ }, []);
39
+ const injectTexture = useCallback((filename, file) => {
40
+ loading.current.add(filename);
41
+ const url = URL.createObjectURL(file);
42
+ const loader = new TextureLoader();
43
+ loader.load(url, tex => {
44
+ tex.colorSpace = SRGBColorSpace;
45
+ setTextures(t => (Object.assign(Object.assign({}, t), { [filename]: tex })));
46
+ URL.revokeObjectURL(url);
47
+ }, undefined, () => URL.revokeObjectURL(url));
48
+ }, []);
36
49
  useImperativeHandle(ref, () => ({
37
50
  root: rootRef.current,
38
- rigidBodyRefs: rigidBodyRefs.current
39
- }), []);
51
+ rigidBodyRefs: rigidBodyRefs.current,
52
+ injectModel,
53
+ injectTexture
54
+ }), [injectModel, injectTexture]);
40
55
  const registerRef = useCallback((id, obj) => {
41
56
  objectRefs.current[id] = obj;
42
57
  if (id === selectedId)
@@ -1,28 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { colors } from '../styles';
3
4
  // ============================================================================
4
- // Shared Styles
5
+ // Shared Styles (derived from shared color tokens)
5
6
  // ============================================================================
6
- // Shared styles
7
7
  const styles = {
8
8
  input: {
9
9
  width: '80px',
10
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
11
- border: '1px solid rgba(34, 211, 238, 0.3)',
12
- padding: '2px 4px',
13
- fontSize: '10px',
14
- color: 'rgba(165, 243, 252, 1)',
10
+ backgroundColor: colors.bgInput,
11
+ border: `1px solid ${colors.border}`,
12
+ padding: '3px 6px',
13
+ fontSize: '11px',
14
+ color: colors.text,
15
15
  fontFamily: 'monospace',
16
16
  outline: 'none',
17
17
  textAlign: 'right',
18
+ borderRadius: 3,
18
19
  },
19
20
  label: {
20
21
  display: 'block',
21
- fontSize: '9px',
22
- color: 'rgba(34, 211, 238, 0.9)',
22
+ fontSize: '10px',
23
+ color: colors.textMuted,
23
24
  textTransform: 'uppercase',
24
25
  letterSpacing: '0.05em',
25
26
  marginBottom: 2,
27
+ fontWeight: 500,
26
28
  },
27
29
  };
28
30
  export function Input({ value, onChange, step, min, max, style, label }) {
@@ -161,23 +163,23 @@ export function Vector3Input({ label, value, onChange, snap }) {
161
163
  e.target.releasePointerCapture(e.pointerId);
162
164
  };
163
165
  const axes = [
164
- { key: "x", color: 'rgba(248, 113, 113, 1)', index: 0 },
165
- { key: "y", color: 'rgba(134, 239, 172, 1)', index: 1 },
166
- { key: "z", color: 'rgba(96, 165, 250, 1)', index: 2 }
166
+ { key: "x", color: '#e06c75', index: 0 },
167
+ { key: "y", color: '#98c379', index: 1 },
168
+ { key: "z", color: '#61afef', index: 2 }
167
169
  ];
168
170
  return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 4 }), children: label }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axes.map(({ key, color, index }) => (_jsxs("div", { style: {
169
171
  flex: 1,
170
172
  display: 'flex',
171
173
  alignItems: 'center',
172
174
  gap: 4,
173
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
174
- border: '1px solid rgba(34, 211, 238, 0.2)',
175
- borderRadius: 4,
175
+ backgroundColor: colors.bgInput,
176
+ border: `1px solid ${colors.border}`,
177
+ borderRadius: 3,
176
178
  padding: '4px 6px',
177
- minHeight: 32,
179
+ minHeight: 28,
178
180
  }, children: [_jsx("span", { style: {
179
- fontSize: '12px',
180
- fontWeight: 'bold',
181
+ fontSize: 11,
182
+ fontWeight: 600,
181
183
  color,
182
184
  width: 12,
183
185
  cursor: 'ew-resize',
@@ -186,8 +188,8 @@ export function Vector3Input({ label, value, onChange, snap }) {
186
188
  flex: 1,
187
189
  backgroundColor: 'transparent',
188
190
  border: 'none',
189
- fontSize: '12px',
190
- color: 'rgba(165, 243, 252, 1)',
191
+ fontSize: 11,
192
+ color: colors.text,
191
193
  fontFamily: 'monospace',
192
194
  outline: 'none',
193
195
  width: '100%',
@@ -209,11 +211,11 @@ export function ColorInput({ label, value, onChange }) {
209
211
  return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsxs("div", { style: { display: 'flex', gap: 4, justifyContent: 'space-between' }, children: [_jsx("input", { type: "color", style: {
210
212
  height: 32,
211
213
  width: 48,
212
- backgroundColor: 'transparent',
213
- border: '1px solid rgba(34, 211, 238, 0.3)',
214
- borderRadius: 4,
214
+ backgroundColor: colors.bgInput,
215
+ border: `1px solid ${colors.border}`,
216
+ borderRadius: 3,
215
217
  cursor: 'pointer',
216
- padding: 0,
218
+ padding: 2,
217
219
  flexShrink: 0,
218
220
  }, value: value, onChange: e => onChange(e.target.value) }), _jsx("input", { type: "text", style: Object.assign({}, styles.input), value: value, onChange: e => onChange(e.target.value) })] })] }));
219
221
  }
@@ -224,8 +226,7 @@ export function BooleanInput({ label, value, onChange }) {
224
226
  return (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between' }, children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "checkbox", style: {
225
227
  height: 16,
226
228
  width: 16,
227
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
228
- border: '1px solid rgba(34, 211, 238, 0.3)',
229
+ accentColor: colors.accent,
229
230
  cursor: 'pointer',
230
231
  }, checked: value, onChange: e => onChange(e.target.checked) })] }));
231
232
  }
@@ -13,6 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
14
14
  import { useEffect, useState } from 'react';
15
15
  import { FieldRenderer, Input } from './Input';
16
+ import { colors } from '../styles';
16
17
  import { useMemo } from 'react';
17
18
  import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter } from 'three';
18
19
  function TexturePicker({ value, onChange, basePath }) {
@@ -24,9 +25,15 @@ function TexturePicker({ value, onChange, basePath }) {
24
25
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
25
26
  .catch(console.error);
26
27
  }, [basePath]);
27
- return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleTextureViewer, { file: value || undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
28
+ // Only show 3D preview for server-hosted textures (starting with / or http)
29
+ const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
30
+ return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [canPreview
31
+ ? _jsx(SingleTextureViewer, { file: value, basePath: basePath })
32
+ : value
33
+ ? _jsx("span", { style: { fontSize: 10, opacity: 0.6, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: value })
34
+ : null, _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
28
35
  onChange(undefined);
29
- }, style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(TextureListViewer, { files: textureFiles, selected: value || undefined, onSelect: (file) => {
36
+ }, style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', right: 60, top: 60, transform: 'translate(-100%,0%)', background: colors.bg, padding: 16, border: `1px solid ${colors.border}`, borderRadius: 4, maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', width: 220, zIndex: 1000, boxShadow: '0 4px 16px rgba(0,0,0,0.6)' }, children: _jsx(TextureListViewer, { files: textureFiles, selected: value || undefined, onSelect: (file) => {
30
37
  onChange(file);
31
38
  setShowPicker(false);
32
39
  }, basePath: basePath }) }))] }));
@@ -16,9 +16,9 @@ function ModelPicker({ value, onChange, basePath, nodeId }) {
16
16
  onChange(filename);
17
17
  setShowPicker(false);
18
18
  };
19
- return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
19
+ return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
20
20
  onChange(undefined);
21
- }, style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(ModelListViewer, { files: modelFiles, selected: value ? `/${value}` : undefined, onSelect: handleModelSelect, basePath: basePath }, nodeId) }))] }));
21
+ }, style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', right: 60, top: 60, transform: 'translate(-100%,0%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', width: 220, zIndex: 1000 }, children: _jsx(ModelListViewer, { files: modelFiles, selected: value ? `/${value}` : undefined, onSelect: handleModelSelect, basePath: basePath }, nodeId) }))] }));
22
22
  }
23
23
  function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
24
24
  const fields = [
@@ -93,7 +93,7 @@ const physicsFields = [
93
93
  },
94
94
  ];
95
95
  function PhysicsComponentEditor({ component, onUpdate }) {
96
- return (_jsx(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: (props) => onUpdate(Object.assign(Object.assign({}, component), { properties: Object.assign(Object.assign({}, component.properties), props) })) }));
96
+ return (_jsx(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: onUpdate }));
97
97
  }
98
98
  function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }) {
99
99
  const { type, colliders, sensor, activeCollisionTypes } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes"]);
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useEffect } from "react";
3
3
  import { FieldRenderer } from "./Input";
4
+ import { useHelper } from "@react-three/drei";
5
+ import { SpotLightHelper } from "three";
4
6
  const spotLightFields = [
5
7
  { name: 'color', type: 'color', label: 'Color' },
6
8
  { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
@@ -22,6 +24,7 @@ function SpotLightView({ properties, editMode }) {
22
24
  const castShadow = (_f = properties.castShadow) !== null && _f !== void 0 ? _f : true;
23
25
  const spotLightRef = useRef(null);
24
26
  const targetRef = useRef(null);
27
+ useHelper(editMode ? spotLightRef : null, SpotLightHelper, color);
25
28
  useEffect(() => {
26
29
  if (spotLightRef.current && targetRef.current) {
27
30
  spotLightRef.current.target = targetRef.current;