react-three-game 0.0.32 → 0.0.34

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 (27) hide show
  1. package/dist/tools/prefabeditor/EditorTree.d.ts +7 -1
  2. package/dist/tools/prefabeditor/EditorTree.js +18 -29
  3. package/dist/tools/prefabeditor/EditorUI.d.ts +7 -1
  4. package/dist/tools/prefabeditor/EditorUI.js +4 -3
  5. package/dist/tools/prefabeditor/PrefabEditor.js +65 -86
  6. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +13 -1
  7. package/dist/tools/prefabeditor/components/GeometryComponent.js +17 -9
  8. package/dist/tools/prefabeditor/components/Input.d.ts +19 -0
  9. package/dist/tools/prefabeditor/components/Input.js +123 -0
  10. package/dist/tools/prefabeditor/components/MaterialComponent.js +15 -4
  11. package/dist/tools/prefabeditor/components/ModelComponent.js +2 -1
  12. package/dist/tools/prefabeditor/components/PhysicsComponent.js +12 -3
  13. package/dist/tools/prefabeditor/components/SpotLightComponent.js +12 -1
  14. package/dist/tools/prefabeditor/components/TransformComponent.d.ts +0 -5
  15. package/dist/tools/prefabeditor/components/TransformComponent.js +21 -89
  16. package/package.json +1 -1
  17. package/src/tools/prefabeditor/EditorTree.tsx +86 -39
  18. package/src/tools/prefabeditor/EditorUI.tsx +38 -5
  19. package/src/tools/prefabeditor/PrefabEditor.tsx +108 -148
  20. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +32 -125
  21. package/src/tools/prefabeditor/components/GeometryComponent.tsx +27 -24
  22. package/src/tools/prefabeditor/components/Input.tsx +200 -0
  23. package/src/tools/prefabeditor/components/MaterialComponent.tsx +31 -22
  24. package/src/tools/prefabeditor/components/ModelComponent.tsx +5 -4
  25. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +31 -17
  26. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +26 -43
  27. package/src/tools/prefabeditor/components/TransformComponent.tsx +34 -165
@@ -1,8 +1,14 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { Prefab } from "./types";
3
- export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }: {
3
+ export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }: {
4
4
  prefabData?: Prefab;
5
5
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
6
  selectedId: string | null;
7
7
  setSelectedId: Dispatch<SetStateAction<string | null>>;
8
+ onSave?: () => void;
9
+ onLoad?: () => void;
10
+ onUndo?: () => void;
11
+ onRedo?: () => void;
12
+ canUndo?: boolean;
13
+ canRedo?: boolean;
8
14
  }): import("react/jsx-runtime").JSX.Element | null;
@@ -3,11 +3,12 @@ import { useState } from 'react';
3
3
  import { getComponent } from './components/ComponentRegistry';
4
4
  import { base, tree, menu } from './styles';
5
5
  import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
6
- export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }) {
6
+ export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }) {
7
7
  const [contextMenu, setContextMenu] = useState(null);
8
8
  const [draggedId, setDraggedId] = useState(null);
9
9
  const [collapsedIds, setCollapsedIds] = useState(new Set());
10
10
  const [collapsed, setCollapsed] = useState(false);
11
+ const [fileMenuOpen, setFileMenuOpen] = useState(false);
11
12
  if (!prefabData || !setPrefabData)
12
13
  return null;
13
14
  const handleContextMenu = (e, nodeId) => {
@@ -23,22 +24,19 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
23
24
  return next;
24
25
  });
25
26
  };
26
- // Actions
27
27
  const handleAddChild = (parentId) => {
28
- var _a;
29
- const newNode = {
30
- id: crypto.randomUUID(),
31
- name: "New Node",
32
- components: {
33
- transform: {
34
- type: "Transform",
35
- properties: Object.assign({}, (_a = getComponent('Transform')) === null || _a === void 0 ? void 0 : _a.defaultProperties)
36
- }
37
- }
38
- };
39
28
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parentId, parent => {
40
- var _a;
41
- return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), newNode] }));
29
+ var _a, _b;
30
+ return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), {
31
+ id: crypto.randomUUID(),
32
+ name: "New Node",
33
+ components: {
34
+ transform: {
35
+ type: "Transform",
36
+ properties: Object.assign({}, (_b = getComponent('Transform')) === null || _b === void 0 ? void 0 : _b.defaultProperties)
37
+ }
38
+ }
39
+ }] }));
42
40
  }) })));
43
41
  setContextMenu(null);
44
42
  };
@@ -50,10 +48,9 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
50
48
  const parent = findParent(prev.root, nodeId);
51
49
  if (!node || !parent)
52
50
  return prev;
53
- const clone = cloneNode(node);
54
51
  return Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parent.id, p => {
55
52
  var _a;
56
- return (Object.assign(Object.assign({}, p), { children: [...((_a = p.children) !== null && _a !== void 0 ? _a : []), clone] }));
53
+ return (Object.assign(Object.assign({}, p), { children: [...((_a = p.children) !== null && _a !== void 0 ? _a : []), cloneNode(node)] }));
57
54
  }) });
58
55
  });
59
56
  setContextMenu(null);
@@ -66,12 +63,9 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
66
63
  setSelectedId(null);
67
64
  setContextMenu(null);
68
65
  };
69
- // Drag and Drop
70
66
  const handleDragStart = (e, id) => {
71
- if (id === prefabData.root.id) {
72
- e.preventDefault();
73
- return;
74
- }
67
+ if (id === prefabData.root.id)
68
+ return e.preventDefault();
75
69
  e.dataTransfer.effectAllowed = "move";
76
70
  setDraggedId(id);
77
71
  };
@@ -90,14 +84,9 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
90
84
  setPrefabData(prev => {
91
85
  const draggedNode = findNode(prev.root, draggedId);
92
86
  const oldParent = findParent(prev.root, draggedId);
93
- if (!draggedNode || !oldParent)
94
- return prev;
95
- // Prevent dropping into own subtree
96
- if (findNode(draggedNode, targetId))
87
+ if (!draggedNode || !oldParent || findNode(draggedNode, targetId))
97
88
  return prev;
98
- // 1. Remove from old parent
99
89
  let root = updateNodeById(prev.root, oldParent.id, p => (Object.assign(Object.assign({}, p), { children: p.children.filter(c => c.id !== draggedId) })));
100
- // 2. Add to new parent
101
90
  root = updateNodeById(root, targetId, t => {
102
91
  var _a;
103
92
  return (Object.assign(Object.assign({}, t), { children: [...((_a = t.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
@@ -122,5 +111,5 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
122
111
  visibility: hasChildren ? 'visible' : 'hidden'
123
112
  }, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id })] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
124
113
  };
125
- return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => setContextMenu(null), children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Scene" }), _jsx("span", { children: collapsed ? '' : '' })] }), !collapsed && _jsx("div", { 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" })] }))] }))] }));
114
+ 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 && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: 28, right: 0 }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { onLoad === null || onLoad === void 0 ? void 0 : onLoad(); setFileMenuOpen(false); }, children: "\uD83D\uDCE5 Load" }), _jsx("button", { style: menu.item, onClick: () => { onSave === null || onSave === void 0 ? void 0 : onSave(); setFileMenuOpen(false); }, children: "\uD83D\uDCBE Save" })] }))] })] }))] }), !collapsed && _jsx("div", { 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" })] }))] }))] }));
126
115
  }
@@ -1,6 +1,6 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { Prefab } from "./types";
3
- declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }: {
3
+ declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }: {
4
4
  prefabData?: Prefab;
5
5
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
6
  selectedId: string | null;
@@ -8,5 +8,11 @@ declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId
8
8
  transformMode: "translate" | "rotate" | "scale";
9
9
  setTransformMode: (m: "translate" | "rotate" | "scale") => void;
10
10
  basePath?: string;
11
+ onSave?: () => void;
12
+ onLoad?: () => void;
13
+ onUndo?: () => void;
14
+ onRedo?: () => void;
15
+ canUndo?: boolean;
16
+ canRedo?: boolean;
11
17
  }): import("react/jsx-runtime").JSX.Element;
12
18
  export default EditorUI;
@@ -15,7 +15,7 @@ import EditorTree from './EditorTree';
15
15
  import { getAllComponents } from './components/ComponentRegistry';
16
16
  import { base, inspector } from './styles';
17
17
  import { findNode, updateNode, deleteNode } from './utils';
18
- function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }) {
18
+ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }) {
19
19
  const [collapsed, setCollapsed] = useState(false);
20
20
  const updateNodeHandler = (updater) => {
21
21
  if (!prefabData || !setPrefabData || !selectedId)
@@ -29,11 +29,12 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
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: `.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
32
+ return _jsxs(_Fragment, { children: [_jsx("style", { children: `
33
+ .prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
33
34
  .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
34
35
  .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
35
36
  .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
36
- ` }), _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, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
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, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onSave: onSave, onLoad: onLoad, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
37
38
  }
38
39
  function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
39
40
  var _a;
@@ -15,65 +15,54 @@ import PrefabRoot from "./PrefabRoot";
15
15
  import { Physics } from "@react-three/rapier";
16
16
  import EditorUI from "./EditorUI";
17
17
  import { base, toolbar } from "./styles";
18
- const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) => {
19
- const [editMode, setEditMode] = useState(true);
20
- const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : {
21
- id: "prefab-default",
22
- name: "New Prefab",
23
- root: {
24
- id: "root",
25
- components: {
26
- transform: {
27
- type: "Transform",
28
- properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
29
- }
18
+ const DEFAULT_PREFAB = {
19
+ id: "prefab-default",
20
+ name: "New Prefab",
21
+ root: {
22
+ id: "root",
23
+ components: {
24
+ transform: {
25
+ type: "Transform",
26
+ properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
30
27
  }
31
28
  }
32
- });
29
+ }
30
+ };
31
+ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) => {
32
+ const [editMode, setEditMode] = useState(true);
33
+ const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
33
34
  const [selectedId, setSelectedId] = useState(null);
34
35
  const [transformMode, setTransformMode] = useState("translate");
35
- // Sync internal state with external initialPrefab prop
36
+ const [history, setHistory] = useState([loadedPrefab]);
37
+ const [historyIndex, setHistoryIndex] = useState(0);
38
+ const throttleRef = useRef(null);
39
+ const lastDataRef = useRef(JSON.stringify(loadedPrefab));
36
40
  useEffect(() => {
37
- if (initialPrefab) {
41
+ if (initialPrefab)
38
42
  setLoadedPrefab(initialPrefab);
39
- }
40
43
  }, [initialPrefab]);
41
- // Wrapper to update prefab and notify parent
42
44
  const updatePrefab = (newPrefab) => {
43
45
  setLoadedPrefab(newPrefab);
44
46
  const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
45
47
  onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(resolved);
46
48
  };
47
- 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, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, 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 })] });
48
- };
49
- const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }) => {
50
- const [history, setHistory] = useState([currentData]);
51
- const [historyIndex, setHistoryIndex] = useState(0);
52
- const throttleRef = useRef(null);
53
- const lastDataRef = useRef(JSON.stringify(currentData));
54
- const undo = () => {
55
- if (historyIndex > 0) {
56
- const newIndex = historyIndex - 1;
57
- setHistoryIndex(newIndex);
58
- lastDataRef.current = JSON.stringify(history[newIndex]);
59
- onDataChange(history[newIndex]);
60
- }
61
- };
62
- const redo = () => {
63
- if (historyIndex < history.length - 1) {
64
- const newIndex = historyIndex + 1;
65
- setHistoryIndex(newIndex);
66
- lastDataRef.current = JSON.stringify(history[newIndex]);
67
- onDataChange(history[newIndex]);
68
- }
49
+ const applyHistory = (index) => {
50
+ setHistoryIndex(index);
51
+ lastDataRef.current = JSON.stringify(history[index]);
52
+ setLoadedPrefab(history[index]);
53
+ onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(history[index]);
69
54
  };
55
+ const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
56
+ const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
70
57
  useEffect(() => {
71
58
  const handleKeyDown = (e) => {
72
- if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
59
+ if (!(e.ctrlKey || e.metaKey))
60
+ return;
61
+ if (e.key === 'z' && !e.shiftKey) {
73
62
  e.preventDefault();
74
63
  undo();
75
64
  }
76
- else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
65
+ else if ((e.shiftKey && e.key === 'z') || e.key === 'y') {
77
66
  e.preventDefault();
78
67
  redo();
79
68
  }
@@ -82,7 +71,7 @@ const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }
82
71
  return () => window.removeEventListener('keydown', handleKeyDown);
83
72
  }, [historyIndex, history]);
84
73
  useEffect(() => {
85
- const currentStr = JSON.stringify(currentData);
74
+ const currentStr = JSON.stringify(loadedPrefab);
86
75
  if (currentStr === lastDataRef.current)
87
76
  return;
88
77
  if (throttleRef.current)
@@ -90,66 +79,56 @@ const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }
90
79
  throttleRef.current = setTimeout(() => {
91
80
  lastDataRef.current = currentStr;
92
81
  setHistory(prev => {
93
- const newHistory = [...prev.slice(0, historyIndex + 1), currentData];
82
+ const newHistory = [...prev.slice(0, historyIndex + 1), loadedPrefab];
94
83
  return newHistory.length > 50 ? newHistory.slice(1) : newHistory;
95
84
  });
96
85
  setHistoryIndex(prev => Math.min(prev + 1, 49));
97
86
  }, 500);
98
- return () => {
99
- if (throttleRef.current)
100
- clearTimeout(throttleRef.current);
101
- };
102
- }, [currentData]);
87
+ return () => { if (throttleRef.current)
88
+ clearTimeout(throttleRef.current); };
89
+ }, [loadedPrefab]);
103
90
  const handleLoad = () => __awaiter(void 0, void 0, void 0, function* () {
104
91
  const prefab = yield loadJson();
105
92
  if (prefab) {
106
- onDataChange(prefab);
93
+ setLoadedPrefab(prefab);
94
+ onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(prefab);
107
95
  setHistory([prefab]);
108
96
  setHistoryIndex(0);
109
97
  lastDataRef.current = JSON.stringify(prefab);
110
98
  }
111
99
  });
112
- const canUndo = historyIndex > 0;
113
- const canRedo = historyIndex < history.length - 1;
114
- return _jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => onEditModeChange(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("div", { style: toolbar.divider }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), (canUndo ? {} : toolbar.disabled)), onClick: undo, disabled: !canUndo, children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), (canRedo ? {} : toolbar.disabled)), onClick: redo, disabled: !canRedo, children: "\u21B7" }), _jsx("div", { style: toolbar.divider }), _jsx("button", { style: base.btn, onClick: handleLoad, children: "\uD83D\uDCE5" }), _jsx("button", { style: base.btn, onClick: () => saveJson(currentData, "prefab"), children: "\uD83D\uDCBE" })] });
100
+ 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, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, basePath: basePath }), children] }) }), _jsx("div", { style: toolbar.panel, children: _jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }) }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath, onSave: () => saveJson(loadedPrefab, "prefab"), onLoad: handleLoad, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
115
101
  };
116
102
  const saveJson = (data, filename) => {
117
- const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
118
- const downloadAnchorNode = document.createElement('a');
119
- downloadAnchorNode.setAttribute("href", dataStr);
120
- downloadAnchorNode.setAttribute("download", (filename || 'prefab') + ".json");
121
- document.body.appendChild(downloadAnchorNode);
122
- downloadAnchorNode.click();
123
- downloadAnchorNode.remove();
103
+ const a = document.createElement('a');
104
+ a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
105
+ a.download = `${filename || 'prefab'}.json`;
106
+ a.click();
124
107
  };
125
- const loadJson = () => __awaiter(void 0, void 0, void 0, function* () {
126
- return new Promise((resolve) => {
127
- const input = document.createElement('input');
128
- input.type = 'file';
129
- input.accept = '.json,application/json';
130
- input.onchange = e => {
108
+ const loadJson = () => new Promise(resolve => {
109
+ const input = document.createElement('input');
110
+ input.type = 'file';
111
+ input.accept = '.json,application/json';
112
+ input.onchange = e => {
113
+ var _a;
114
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
115
+ if (!file)
116
+ return resolve(undefined);
117
+ const reader = new FileReader();
118
+ reader.onload = e => {
131
119
  var _a;
132
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
133
- if (!file)
134
- return resolve(undefined);
135
- const reader = new FileReader();
136
- reader.onload = e => {
137
- var _a;
138
- try {
139
- const text = (_a = e.target) === null || _a === void 0 ? void 0 : _a.result;
140
- if (typeof text === 'string') {
141
- const json = JSON.parse(text);
142
- resolve(json);
143
- }
144
- }
145
- catch (err) {
146
- console.error('Error parsing prefab JSON:', err);
147
- resolve(undefined);
148
- }
149
- };
150
- reader.readAsText(file);
120
+ try {
121
+ const text = (_a = e.target) === null || _a === void 0 ? void 0 : _a.result;
122
+ if (typeof text === 'string')
123
+ resolve(JSON.parse(text));
124
+ }
125
+ catch (err) {
126
+ console.error('Error parsing prefab JSON:', err);
127
+ resolve(undefined);
128
+ }
151
129
  };
152
- input.click();
153
- });
130
+ reader.readAsText(file);
131
+ };
132
+ input.click();
154
133
  });
155
134
  export default PrefabEditor;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useRef, useEffect } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import { Vector3 } from "three";
5
+ import { Input, Label } from "./Input";
5
6
  function DirectionalLightComponentEditor({ component, onUpdate }) {
6
7
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
7
8
  const props = {
@@ -17,7 +18,18 @@ function DirectionalLightComponentEditor({ component, onUpdate }) {
17
18
  shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
18
19
  targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
19
20
  };
20
- return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: '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", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Shadow Camera" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Near" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Far" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Top" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Bottom" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Left" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Right" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Target Offset" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "X" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, 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", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Y" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, 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", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Z" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, 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
+ const textInputStyle = {
22
+ flex: 1,
23
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
24
+ border: '1px solid rgba(34, 211, 238, 0.3)',
25
+ padding: '2px 4px',
26
+ fontSize: '10px',
27
+ color: 'rgba(165, 243, 252, 1)',
28
+ fontFamily: 'monospace',
29
+ outline: 'none',
30
+ };
31
+ const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 };
32
+ return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: '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", style: textInputStyle, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { color: e.target.value })) })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Intensity" }), _jsx(Input, { step: "0.1", value: props.intensity, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { intensity: value })) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { castShadow: e.target.checked })) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Shadow Map Size" }), _jsx(Input, { step: "256", value: props.shadowMapSize, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowMapSize: value })) })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Shadow Camera" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Near" }), _jsx(Input, { step: "0.1", value: props.shadowCameraNear, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraNear: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Far" }), _jsx(Input, { step: "1", value: props.shadowCameraFar, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraFar: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Top" }), _jsx(Input, { step: "1", value: props.shadowCameraTop, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraTop: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Bottom" }), _jsx(Input, { step: "1", value: props.shadowCameraBottom, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraBottom: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Left" }), _jsx(Input, { step: "1", value: props.shadowCameraLeft, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraLeft: value })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Right" }), _jsx(Input, { step: "1", value: props.shadowCameraRight, onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { shadowCameraRight: value })) })] })] })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Target Offset" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "X" }), _jsx(Input, { step: "0.5", value: props.targetOffset[0], onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { targetOffset: [value, props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Y" }), _jsx(Input, { step: "0.5", value: props.targetOffset[1], onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { targetOffset: [props.targetOffset[0], value, props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Z" }), _jsx(Input, { step: "0.5", value: props.targetOffset[2], onChange: value => onUpdate(Object.assign(Object.assign({}, component.properties), { targetOffset: [props.targetOffset[0], props.targetOffset[1], value] })) })] })] })] })] });
21
33
  }
22
34
  function DirectionalLightView({ properties, editMode }) {
23
35
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Input, Label } from "./Input";
2
3
  const GEOMETRY_ARGS = {
3
4
  box: {
4
5
  labels: ["Width", "Height", "Depth"],
@@ -16,17 +17,24 @@ const GEOMETRY_ARGS = {
16
17
  function GeometryComponentEditor({ component, onUpdate, }) {
17
18
  const { geometryType, args = [] } = component.properties;
18
19
  const schema = GEOMETRY_ARGS[geometryType];
19
- return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "label", children: "Type" }), _jsxs("select", { className: "select", value: geometryType, onChange: e => {
20
- const type = e.target.value;
21
- onUpdate({
22
- geometryType: type,
23
- args: GEOMETRY_ARGS[type].defaults,
24
- });
25
- }, children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] }), schema.labels.map((label, i) => {
20
+ const selectStyle = {
21
+ width: '100%',
22
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
23
+ border: '1px solid rgba(34, 211, 238, 0.3)',
24
+ padding: '2px 4px',
25
+ fontSize: '10px',
26
+ color: 'rgba(165, 243, 252, 1)',
27
+ fontFamily: 'monospace',
28
+ outline: 'none',
29
+ };
30
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Type" }), _jsxs("select", { style: selectStyle, value: geometryType, onChange: e => {
31
+ const type = e.target.value;
32
+ onUpdate({ geometryType: type, args: GEOMETRY_ARGS[type].defaults });
33
+ }, children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] })] }), schema.labels.map((label, i) => {
26
34
  var _a;
27
- return (_jsxs("div", { children: [_jsx("label", { className: "label", children: label }), _jsx("input", { type: "number", className: "input", value: (_a = args[i]) !== null && _a !== void 0 ? _a : schema.defaults[i], step: "0.1", onChange: e => {
35
+ return (_jsxs("div", { children: [_jsx(Label, { children: label }), _jsx(Input, { value: (_a = args[i]) !== null && _a !== void 0 ? _a : schema.defaults[i], step: "0.1", onChange: value => {
28
36
  const next = [...args];
29
- next[i] = parseFloat(e.target.value);
37
+ next[i] = value;
30
38
  onUpdate({ args: next });
31
39
  } })] }, label));
32
40
  })] }));
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ interface InputProps {
3
+ value: number;
4
+ onChange: (value: number) => void;
5
+ step?: string | number;
6
+ min?: number;
7
+ max?: number;
8
+ style?: React.CSSProperties;
9
+ }
10
+ export declare function Input({ value, onChange, step, min, max, style }: InputProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function Label({ children }: {
12
+ children: React.ReactNode;
13
+ }): import("react/jsx-runtime").JSX.Element;
14
+ export declare function Vector3Input({ label, value, onChange }: {
15
+ label: string;
16
+ value: [number, number, number];
17
+ onChange: (v: [number, number, number]) => void;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ export {};
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ // Shared styles
4
+ const styles = {
5
+ input: {
6
+ width: '100%',
7
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
8
+ border: '1px solid rgba(34, 211, 238, 0.3)',
9
+ padding: '2px 4px',
10
+ fontSize: '10px',
11
+ color: 'rgba(165, 243, 252, 1)',
12
+ fontFamily: 'monospace',
13
+ outline: 'none',
14
+ },
15
+ label: {
16
+ display: 'block',
17
+ fontSize: '9px',
18
+ color: 'rgba(34, 211, 238, 0.6)',
19
+ textTransform: 'uppercase',
20
+ letterSpacing: '0.05em',
21
+ marginBottom: 2,
22
+ },
23
+ };
24
+ export function Input({ value, onChange, step, min, max, style }) {
25
+ return (_jsx("input", { type: "number", value: value, onChange: (e) => onChange(parseFloat(e.target.value)), step: step, min: min, max: max, style: Object.assign(Object.assign({}, styles.input), style) }));
26
+ }
27
+ export function Label({ children }) {
28
+ return _jsx("label", { style: styles.label, children: children });
29
+ }
30
+ export function Vector3Input({ label, value, onChange }) {
31
+ const [draft, setDraft] = useState(() => value.map(v => v.toString()));
32
+ // Sync external changes (gizmo, undo, etc.)
33
+ useEffect(() => {
34
+ setDraft(value.map(v => v.toString()));
35
+ }, [value[0], value[1], value[2]]);
36
+ const dragState = useRef(null);
37
+ const commit = (index) => {
38
+ const num = parseFloat(draft[index]);
39
+ if (Number.isFinite(num)) {
40
+ const next = [...value];
41
+ next[index] = num;
42
+ onChange(next);
43
+ }
44
+ };
45
+ const startScrub = (e, index) => {
46
+ e.preventDefault();
47
+ dragState.current = {
48
+ index,
49
+ startX: e.clientX,
50
+ startValue: value[index]
51
+ };
52
+ e.target.setPointerCapture(e.pointerId);
53
+ document.body.style.cursor = "ew-resize";
54
+ };
55
+ const onScrubMove = (e) => {
56
+ if (!dragState.current)
57
+ return;
58
+ const { index, startX, startValue } = dragState.current;
59
+ const dx = e.clientX - startX;
60
+ let speed = 0.02;
61
+ if (e.shiftKey)
62
+ speed *= 0.1; // fine
63
+ if (e.altKey)
64
+ speed *= 5; // coarse
65
+ const nextValue = startValue + dx * speed;
66
+ const next = [...value];
67
+ next[index] = nextValue;
68
+ setDraft(d => {
69
+ const copy = [...d];
70
+ copy[index] = nextValue.toFixed(3);
71
+ return copy;
72
+ });
73
+ onChange(next);
74
+ };
75
+ const endScrub = (e) => {
76
+ if (!dragState.current)
77
+ return;
78
+ dragState.current = null;
79
+ document.body.style.cursor = "";
80
+ e.target.releasePointerCapture(e.pointerId);
81
+ };
82
+ const axes = [
83
+ { key: "x", color: 'rgba(248, 113, 113, 1)', index: 0 },
84
+ { key: "y", color: 'rgba(134, 239, 172, 1)', index: 1 },
85
+ { key: "z", color: 'rgba(96, 165, 250, 1)', index: 2 }
86
+ ];
87
+ 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: {
88
+ flex: 1,
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ gap: 4,
92
+ backgroundColor: 'rgba(0, 0, 0, 0.3)',
93
+ border: '1px solid rgba(34, 211, 238, 0.2)',
94
+ borderRadius: 4,
95
+ padding: '4px 6px',
96
+ minHeight: 32,
97
+ }, children: [_jsx("span", { style: {
98
+ fontSize: '12px',
99
+ fontWeight: 'bold',
100
+ color,
101
+ width: 12,
102
+ cursor: 'ew-resize',
103
+ userSelect: 'none',
104
+ }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: key.toUpperCase() }), _jsx("input", { style: {
105
+ flex: 1,
106
+ backgroundColor: 'transparent',
107
+ border: 'none',
108
+ fontSize: '12px',
109
+ color: 'rgba(165, 243, 252, 1)',
110
+ fontFamily: 'monospace',
111
+ outline: 'none',
112
+ width: '100%',
113
+ minWidth: 0,
114
+ }, type: "text", value: draft[index], onChange: e => {
115
+ const next = [...draft];
116
+ next[index] = e.target.value;
117
+ setDraft(next);
118
+ }, onBlur: () => commit(index), onKeyDown: e => {
119
+ if (e.key === "Enter") {
120
+ e.target.blur();
121
+ }
122
+ } })] }, key))) })] }));
123
+ }