react-three-game 0.0.33 → 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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -4,16 +4,34 @@ import { getComponent } from './components/ComponentRegistry';
4
4
  import { base, tree, menu } from './styles';
5
5
  import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
6
6
 
7
- export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }: {
7
+ export default function EditorTree({
8
+ prefabData,
9
+ setPrefabData,
10
+ selectedId,
11
+ setSelectedId,
12
+ onSave,
13
+ onLoad,
14
+ onUndo,
15
+ onRedo,
16
+ canUndo,
17
+ canRedo
18
+ }: {
8
19
  prefabData?: Prefab;
9
20
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
10
21
  selectedId: string | null;
11
22
  setSelectedId: Dispatch<SetStateAction<string | null>>;
23
+ onSave?: () => void;
24
+ onLoad?: () => void;
25
+ onUndo?: () => void;
26
+ onRedo?: () => void;
27
+ canUndo?: boolean;
28
+ canRedo?: boolean;
12
29
  }) {
13
30
  const [contextMenu, setContextMenu] = useState<{ x: number, y: number, nodeId: string } | null>(null);
14
31
  const [draggedId, setDraggedId] = useState<string | null>(null);
15
32
  const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
16
33
  const [collapsed, setCollapsed] = useState(false);
34
+ const [fileMenuOpen, setFileMenuOpen] = useState(false);
17
35
 
18
36
  if (!prefabData || !setPrefabData) return null;
19
37
 
@@ -32,24 +50,21 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
32
50
  });
33
51
  };
34
52
 
35
- // Actions
36
53
  const handleAddChild = (parentId: string) => {
37
- const newNode: GameObject = {
38
- id: crypto.randomUUID(),
39
- name: "New Node",
40
- components: {
41
- transform: {
42
- type: "Transform",
43
- properties: { ...getComponent('Transform')?.defaultProperties }
44
- }
45
- }
46
- };
47
-
48
54
  setPrefabData(prev => ({
49
55
  ...prev,
50
56
  root: updateNodeById(prev.root, parentId, parent => ({
51
57
  ...parent,
52
- children: [...(parent.children ?? []), newNode]
58
+ children: [...(parent.children ?? []), {
59
+ id: crypto.randomUUID(),
60
+ name: "New Node",
61
+ components: {
62
+ transform: {
63
+ type: "Transform",
64
+ properties: { ...getComponent('Transform')?.defaultProperties }
65
+ }
66
+ }
67
+ }]
53
68
  }))
54
69
  }));
55
70
  setContextMenu(null);
@@ -57,41 +72,30 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
57
72
 
58
73
  const handleDuplicate = (nodeId: string) => {
59
74
  if (nodeId === prefabData.root.id) return;
60
-
61
75
  setPrefabData(prev => {
62
76
  const node = findNode(prev.root, nodeId);
63
77
  const parent = findParent(prev.root, nodeId);
64
78
  if (!node || !parent) return prev;
65
-
66
- const clone = cloneNode(node);
67
-
68
79
  return {
69
80
  ...prev,
70
81
  root: updateNodeById(prev.root, parent.id, p => ({
71
82
  ...p,
72
- children: [...(p.children ?? []), clone]
83
+ children: [...(p.children ?? []), cloneNode(node)]
73
84
  }))
74
85
  };
75
86
  });
76
-
77
87
  setContextMenu(null);
78
88
  };
79
89
 
80
-
81
90
  const handleDelete = (nodeId: string) => {
82
91
  if (nodeId === prefabData.root.id) return;
83
-
84
92
  setPrefabData(prev => ({ ...prev, root: deleteNode(prev.root, nodeId)! }));
85
93
  if (selectedId === nodeId) setSelectedId(null);
86
94
  setContextMenu(null);
87
95
  };
88
96
 
89
- // Drag and Drop
90
97
  const handleDragStart = (e: React.DragEvent, id: string) => {
91
- if (id === prefabData.root.id) {
92
- e.preventDefault();
93
- return;
94
- }
98
+ if (id === prefabData.root.id) return e.preventDefault();
95
99
  e.dataTransfer.effectAllowed = "move";
96
100
  setDraggedId(id);
97
101
  };
@@ -106,22 +110,16 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
106
110
  const handleDrop = (e: React.DragEvent, targetId: string) => {
107
111
  if (!draggedId || draggedId === targetId) return;
108
112
  e.preventDefault();
109
-
110
113
  setPrefabData(prev => {
111
114
  const draggedNode = findNode(prev.root, draggedId);
112
115
  const oldParent = findParent(prev.root, draggedId);
113
- if (!draggedNode || !oldParent) return prev;
114
-
115
- // Prevent dropping into own subtree
116
- if (findNode(draggedNode, targetId)) return prev;
116
+ if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) return prev;
117
117
 
118
- // 1. Remove from old parent
119
118
  let root = updateNodeById(prev.root, oldParent.id, p => ({
120
119
  ...p,
121
120
  children: p.children!.filter(c => c.id !== draggedId)
122
121
  }));
123
122
 
124
- // 2. Add to new parent
125
123
  root = updateNodeById(root, targetId, t => ({
126
124
  ...t,
127
125
  children: [...(t.children ?? []), draggedNode]
@@ -129,7 +127,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
129
127
 
130
128
  return { ...prev, root };
131
129
  });
132
-
133
130
  setDraggedId(null);
134
131
  };
135
132
 
@@ -182,10 +179,60 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
182
179
 
183
180
  return (
184
181
  <>
185
- <div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => setContextMenu(null)}>
186
- <div style={base.header} onClick={() => setCollapsed(!collapsed)}>
187
- <span>Scene</span>
188
- <span>{collapsed ? '▶' : ''}</span>
182
+ <div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => { setContextMenu(null); setFileMenuOpen(false); }}>
183
+ <div style={base.header}>
184
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} onClick={() => setCollapsed(!collapsed)}>
185
+ <span>{collapsed ? '▶' : ''}</span>
186
+ <span>Scene</span>
187
+ </div>
188
+ {!collapsed && (
189
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
190
+ <button
191
+ style={{ ...base.btn, padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }}
192
+ onClick={(e) => { e.stopPropagation(); onUndo?.(); }}
193
+ disabled={!canUndo}
194
+ title="Undo"
195
+ >
196
+
197
+ </button>
198
+ <button
199
+ style={{ ...base.btn, padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }}
200
+ onClick={(e) => { e.stopPropagation(); onRedo?.(); }}
201
+ disabled={!canRedo}
202
+ title="Redo"
203
+ >
204
+
205
+ </button>
206
+ <div style={{ position: 'relative' }}>
207
+ <button
208
+ style={{ ...base.btn, padding: '2px 6px', fontSize: 10 }}
209
+ onClick={(e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }}
210
+ title="File"
211
+ >
212
+
213
+ </button>
214
+ {fileMenuOpen && (
215
+ <div
216
+ style={{ ...menu.container, top: 28, right: 0 }}
217
+ onClick={(e) => e.stopPropagation()}
218
+ >
219
+ <button
220
+ style={menu.item}
221
+ onClick={() => { onLoad?.(); setFileMenuOpen(false); }}
222
+ >
223
+ 📥 Load
224
+ </button>
225
+ <button
226
+ style={menu.item}
227
+ onClick={() => { onSave?.(); setFileMenuOpen(false); }}
228
+ >
229
+ 💾 Save
230
+ </button>
231
+ </div>
232
+ )}
233
+ </div>
234
+ </div>
235
+ )}
189
236
  </div>
190
237
  {!collapsed && <div style={tree.scroll}>{renderNode(prefabData.root)}</div>}
191
238
  </div>
@@ -5,7 +5,21 @@ import { getAllComponents } from './components/ComponentRegistry';
5
5
  import { base, inspector } from './styles';
6
6
  import { findNode, updateNode, deleteNode } from './utils';
7
7
 
8
- function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }: {
8
+ function EditorUI({
9
+ prefabData,
10
+ setPrefabData,
11
+ selectedId,
12
+ setSelectedId,
13
+ transformMode,
14
+ setTransformMode,
15
+ basePath,
16
+ onSave,
17
+ onLoad,
18
+ onUndo,
19
+ onRedo,
20
+ canUndo,
21
+ canRedo
22
+ }: {
9
23
  prefabData?: Prefab;
10
24
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
11
25
  selectedId: string | null;
@@ -13,6 +27,12 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
13
27
  transformMode: "translate" | "rotate" | "scale";
14
28
  setTransformMode: (m: "translate" | "rotate" | "scale") => void;
15
29
  basePath?: string;
30
+ onSave?: () => void;
31
+ onLoad?: () => void;
32
+ onUndo?: () => void;
33
+ onRedo?: () => void;
34
+ canUndo?: boolean;
35
+ canRedo?: boolean;
16
36
  }) {
17
37
  const [collapsed, setCollapsed] = useState(false);
18
38
 
@@ -33,11 +53,12 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
33
53
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
34
54
 
35
55
  return <>
36
- <style>{`.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
56
+ <style>{`
57
+ .prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
37
58
  .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
38
59
  .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
39
60
  .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
40
- `}</style>
61
+ `}</style>
41
62
  <div style={inspector.panel}>
42
63
  <div style={base.header} onClick={() => setCollapsed(!collapsed)}>
43
64
  <span>Inspector</span>
@@ -51,7 +72,6 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
51
72
  transformMode={transformMode}
52
73
  setTransformMode={setTransformMode}
53
74
  basePath={basePath}
54
- // add class to make scrollbar gutter transparent via CSS above
55
75
  />
56
76
  )}
57
77
  </div>
@@ -61,13 +81,26 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
61
81
  setPrefabData={setPrefabData}
62
82
  selectedId={selectedId}
63
83
  setSelectedId={setSelectedId}
84
+ onSave={onSave}
85
+ onLoad={onLoad}
86
+ onUndo={onUndo}
87
+ onRedo={onRedo}
88
+ canUndo={canUndo}
89
+ canRedo={canRedo}
64
90
  />
65
91
  </div>
66
92
  </>;
67
93
  }
68
94
 
69
95
 
70
- function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }: {
96
+ function NodeInspector({
97
+ node,
98
+ updateNode,
99
+ deleteNode,
100
+ transformMode,
101
+ setTransformMode,
102
+ basePath
103
+ }: {
71
104
  node: GameObjectType;
72
105
  updateNode: (updater: (n: GameObjectType) => GameObjectType) => void;
73
106
  deleteNode: () => void;
@@ -8,202 +8,162 @@ import { Physics } from "@react-three/rapier";
8
8
  import EditorUI from "./EditorUI";
9
9
  import { base, toolbar } from "./styles";
10
10
 
11
- const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { basePath?: string, initialPrefab?: Prefab, onPrefabChange?: (prefab: Prefab) => void, children?: React.ReactNode }) => {
12
- const [editMode, setEditMode] = useState(true);
13
- const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? {
14
- id: "prefab-default",
15
- name: "New Prefab",
16
- root: {
17
- id: "root",
18
- components: {
19
- transform: {
20
- type: "Transform",
21
- properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
22
- }
11
+ const DEFAULT_PREFAB: Prefab = {
12
+ id: "prefab-default",
13
+ name: "New Prefab",
14
+ root: {
15
+ id: "root",
16
+ components: {
17
+ transform: {
18
+ type: "Transform",
19
+ properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
23
20
  }
24
21
  }
25
- });
22
+ }
23
+ };
24
+
25
+ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
26
+ basePath?: string;
27
+ initialPrefab?: Prefab;
28
+ onPrefabChange?: (prefab: Prefab) => void;
29
+ children?: React.ReactNode;
30
+ }) => {
31
+ const [editMode, setEditMode] = useState(true);
32
+ const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? DEFAULT_PREFAB);
26
33
  const [selectedId, setSelectedId] = useState<string | null>(null);
27
34
  const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
35
+ const [history, setHistory] = useState<Prefab[]>([loadedPrefab]);
36
+ const [historyIndex, setHistoryIndex] = useState(0);
37
+ const throttleRef = useRef<NodeJS.Timeout | null>(null);
38
+ const lastDataRef = useRef(JSON.stringify(loadedPrefab));
28
39
 
29
- // Sync internal state with external initialPrefab prop
30
40
  useEffect(() => {
31
- if (initialPrefab) {
32
- setLoadedPrefab(initialPrefab);
33
- }
41
+ if (initialPrefab) setLoadedPrefab(initialPrefab);
34
42
  }, [initialPrefab]);
35
43
 
36
- // Wrapper to update prefab and notify parent
37
44
  const updatePrefab = (newPrefab: Prefab | ((prev: Prefab) => Prefab)) => {
38
45
  setLoadedPrefab(newPrefab);
39
46
  const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
40
47
  onPrefabChange?.(resolved);
41
48
  };
42
49
 
43
- return <>
44
- <GameCanvas>
45
- <Physics paused={editMode}>
46
- <ambientLight intensity={1.5} />
47
- <gridHelper args={[10, 10]} position={[0, -1, 0]} />
48
- <PrefabRoot
49
- data={loadedPrefab}
50
- editMode={editMode}
51
- onPrefabChange={updatePrefab}
52
- selectedId={selectedId}
53
- onSelect={setSelectedId}
54
- transformMode={transformMode}
55
- basePath={basePath}
56
- />
57
- {children}
58
- </Physics>
59
- </GameCanvas>
60
-
61
- <SaveDataPanel
62
- currentData={loadedPrefab}
63
- onDataChange={updatePrefab}
64
- editMode={editMode}
65
- onEditModeChange={setEditMode}
66
- />
67
- {editMode && <EditorUI
68
- prefabData={loadedPrefab}
69
- setPrefabData={updatePrefab}
70
- selectedId={selectedId}
71
- setSelectedId={setSelectedId}
72
- transformMode={transformMode}
73
- setTransformMode={setTransformMode}
74
- basePath={basePath}
75
- />}
76
- </>
77
- }
78
-
79
- const SaveDataPanel = ({
80
- currentData,
81
- onDataChange,
82
- editMode,
83
- onEditModeChange
84
- }: {
85
- currentData: Prefab;
86
- onDataChange: (data: Prefab) => void;
87
- editMode: boolean;
88
- onEditModeChange: (mode: boolean) => void;
89
- }) => {
90
- const [history, setHistory] = useState<Prefab[]>([currentData]);
91
- const [historyIndex, setHistoryIndex] = useState(0);
92
- const throttleRef = useRef<NodeJS.Timeout | null>(null);
93
- const lastDataRef = useRef<string>(JSON.stringify(currentData));
94
-
95
- const undo = () => {
96
- if (historyIndex > 0) {
97
- const newIndex = historyIndex - 1;
98
- setHistoryIndex(newIndex);
99
- lastDataRef.current = JSON.stringify(history[newIndex]);
100
- onDataChange(history[newIndex]);
101
- }
50
+ const applyHistory = (index: number) => {
51
+ setHistoryIndex(index);
52
+ lastDataRef.current = JSON.stringify(history[index]);
53
+ setLoadedPrefab(history[index]);
54
+ onPrefabChange?.(history[index]);
102
55
  };
103
56
 
104
- const redo = () => {
105
- if (historyIndex < history.length - 1) {
106
- const newIndex = historyIndex + 1;
107
- setHistoryIndex(newIndex);
108
- lastDataRef.current = JSON.stringify(history[newIndex]);
109
- onDataChange(history[newIndex]);
110
- }
111
- };
57
+ const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
58
+ const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
112
59
 
113
60
  useEffect(() => {
114
61
  const handleKeyDown = (e: KeyboardEvent) => {
115
- if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
116
- e.preventDefault();
117
- undo();
118
- } else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
119
- e.preventDefault();
120
- redo();
121
- }
62
+ if (!(e.ctrlKey || e.metaKey)) return;
63
+ if (e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
64
+ else if ((e.shiftKey && e.key === 'z') || e.key === 'y') { e.preventDefault(); redo(); }
122
65
  };
123
66
  window.addEventListener('keydown', handleKeyDown);
124
67
  return () => window.removeEventListener('keydown', handleKeyDown);
125
68
  }, [historyIndex, history]);
126
69
 
127
70
  useEffect(() => {
128
- const currentStr = JSON.stringify(currentData);
71
+ const currentStr = JSON.stringify(loadedPrefab);
129
72
  if (currentStr === lastDataRef.current) return;
130
-
131
73
  if (throttleRef.current) clearTimeout(throttleRef.current);
132
74
 
133
75
  throttleRef.current = setTimeout(() => {
134
76
  lastDataRef.current = currentStr;
135
77
  setHistory(prev => {
136
- const newHistory = [...prev.slice(0, historyIndex + 1), currentData];
78
+ const newHistory = [...prev.slice(0, historyIndex + 1), loadedPrefab];
137
79
  return newHistory.length > 50 ? newHistory.slice(1) : newHistory;
138
80
  });
139
81
  setHistoryIndex(prev => Math.min(prev + 1, 49));
140
82
  }, 500);
141
83
 
142
- return () => {
143
- if (throttleRef.current) clearTimeout(throttleRef.current);
144
- };
145
- }, [currentData]);
84
+ return () => { if (throttleRef.current) clearTimeout(throttleRef.current); };
85
+ }, [loadedPrefab]);
146
86
 
147
87
  const handleLoad = async () => {
148
88
  const prefab = await loadJson();
149
89
  if (prefab) {
150
- onDataChange(prefab);
90
+ setLoadedPrefab(prefab);
91
+ onPrefabChange?.(prefab);
151
92
  setHistory([prefab]);
152
93
  setHistoryIndex(0);
153
94
  lastDataRef.current = JSON.stringify(prefab);
154
95
  }
155
96
  };
156
97
 
157
- const canUndo = historyIndex > 0;
158
- const canRedo = historyIndex < history.length - 1;
159
-
160
- return <div style={toolbar.panel}>
161
- <button style={base.btn} onClick={() => onEditModeChange(!editMode)}>
162
- {editMode ? "▶" : "⏸"}
163
- </button>
164
- <div style={toolbar.divider} />
165
- <button style={{ ...base.btn, ...(canUndo ? {} : toolbar.disabled) }} onClick={undo} disabled={!canUndo}>↶</button>
166
- <button style={{ ...base.btn, ...(canRedo ? {} : toolbar.disabled) }} onClick={redo} disabled={!canRedo}>↷</button>
167
- <div style={toolbar.divider} />
168
- <button style={base.btn} onClick={handleLoad}>📥</button>
169
- <button style={base.btn} onClick={() => saveJson(currentData, "prefab")}>💾</button>
170
- </div>;
171
- };
98
+ return <>
99
+ <GameCanvas>
100
+ <Physics paused={editMode}>
101
+ <ambientLight intensity={1.5} />
102
+ <gridHelper args={[10, 10]} position={[0, -1, 0]} />
103
+ <PrefabRoot
104
+ data={loadedPrefab}
105
+ editMode={editMode}
106
+ onPrefabChange={updatePrefab}
107
+ selectedId={selectedId}
108
+ onSelect={setSelectedId}
109
+ transformMode={transformMode}
110
+ basePath={basePath}
111
+ />
112
+ {children}
113
+ </Physics>
114
+ </GameCanvas>
115
+
116
+ <div style={toolbar.panel}>
117
+ <button style={base.btn} onClick={() => setEditMode(!editMode)}>
118
+ {editMode ? "▶" : "⏸"}
119
+ </button>
120
+ </div>
121
+ {editMode && <EditorUI
122
+ prefabData={loadedPrefab}
123
+ setPrefabData={updatePrefab}
124
+ selectedId={selectedId}
125
+ setSelectedId={setSelectedId}
126
+ transformMode={transformMode}
127
+ setTransformMode={setTransformMode}
128
+ basePath={basePath}
129
+ onSave={() => saveJson(loadedPrefab, "prefab")}
130
+ onLoad={handleLoad}
131
+ onUndo={undo}
132
+ onRedo={redo}
133
+ canUndo={historyIndex > 0}
134
+ canRedo={historyIndex < history.length - 1}
135
+ />}
136
+ </>
137
+ }
172
138
 
173
- const saveJson = (data: any, filename: string) => {
174
- const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
175
- const downloadAnchorNode = document.createElement('a');
176
- downloadAnchorNode.setAttribute("href", dataStr);
177
- downloadAnchorNode.setAttribute("download", (filename || 'prefab') + ".json");
178
- document.body.appendChild(downloadAnchorNode);
179
- downloadAnchorNode.click();
180
- downloadAnchorNode.remove();
139
+
140
+ const saveJson = (data: Prefab, filename: string) => {
141
+ const a = document.createElement('a');
142
+ a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
143
+ a.download = `${filename || 'prefab'}.json`;
144
+ a.click();
181
145
  };
182
146
 
183
- const loadJson = async () => {
184
- return new Promise<Prefab | undefined>((resolve) => {
185
- const input = document.createElement('input');
186
- input.type = 'file';
187
- input.accept = '.json,application/json';
188
- input.onchange = e => {
189
- const file = (e.target as HTMLInputElement).files?.[0];
190
- if (!file) return resolve(undefined);
191
- const reader = new FileReader();
192
- reader.onload = e => {
193
- try {
194
- const text = e.target?.result;
195
- if (typeof text === 'string') {
196
- const json = JSON.parse(text);
197
- resolve(json as Prefab);
198
- }
199
- } catch (err) {
200
- console.error('Error parsing prefab JSON:', err);
201
- resolve(undefined);
202
- }
203
- };
204
- reader.readAsText(file);
147
+ const loadJson = () => new Promise<Prefab | undefined>(resolve => {
148
+ const input = document.createElement('input');
149
+ input.type = 'file';
150
+ input.accept = '.json,application/json';
151
+ input.onchange = e => {
152
+ const file = (e.target as HTMLInputElement).files?.[0];
153
+ if (!file) return resolve(undefined);
154
+ const reader = new FileReader();
155
+ reader.onload = e => {
156
+ try {
157
+ const text = e.target?.result;
158
+ if (typeof text === 'string') resolve(JSON.parse(text) as Prefab);
159
+ } catch (err) {
160
+ console.error('Error parsing prefab JSON:', err);
161
+ resolve(undefined);
162
+ }
205
163
  };
206
- input.click();
207
- });
208
- };
164
+ reader.readAsText(file);
165
+ };
166
+ input.click();
167
+ });
168
+
209
169
  export default PrefabEditor;