react-three-game 0.0.56 → 0.0.58

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 (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. package/src/tools/prefabeditor/utils.ts +55 -4
@@ -1,32 +1,79 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
2
  import { useState } from 'react';
12
3
  import { getComponent } from './components/ComponentRegistry';
13
- import { base, colors, tree, menu } from './styles';
14
- import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
4
+ import { base, colors, tree } from './styles';
5
+ import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
15
6
  import { useEditorContext } from './EditorContext';
7
+ import { Dropdown } from './Dropdown';
8
+ import { FileMenu, MenuTriggerButton, TreeContextMenu, TreeNodeMenu } from './EditorTreeMenus';
9
+ function moveNode(root, draggedId, targetId, position) {
10
+ const draggedNode = findNode(root, draggedId);
11
+ const oldParent = findParent(root, draggedId);
12
+ if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) {
13
+ return root;
14
+ }
15
+ if (position === 'before') {
16
+ const targetParent = findParent(root, targetId);
17
+ if (!(targetParent === null || targetParent === void 0 ? void 0 : targetParent.children))
18
+ return root;
19
+ if (targetParent.id === oldParent.id) {
20
+ const siblings = targetParent.children.filter(child => child.id !== draggedId);
21
+ const targetIndex = siblings.findIndex(child => child.id === targetId);
22
+ if (targetIndex === -1)
23
+ return root;
24
+ siblings.splice(targetIndex, 0, draggedNode);
25
+ return updateNodeById(root, targetParent.id, parent => (Object.assign(Object.assign({}, parent), { children: siblings })));
26
+ }
27
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => {
28
+ var _a;
29
+ return (Object.assign(Object.assign({}, parent), { children: ((_a = parent.children) !== null && _a !== void 0 ? _a : []).filter(child => child.id !== draggedId) }));
30
+ });
31
+ return updateNodeById(rootWithoutDragged, targetParent.id, parent => {
32
+ var _a;
33
+ const children = [...((_a = parent.children) !== null && _a !== void 0 ? _a : [])];
34
+ const targetIndex = children.findIndex(child => child.id === targetId);
35
+ if (targetIndex === -1)
36
+ return parent;
37
+ children.splice(targetIndex, 0, draggedNode);
38
+ return Object.assign(Object.assign({}, parent), { children });
39
+ });
40
+ }
41
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => {
42
+ var _a;
43
+ return (Object.assign(Object.assign({}, parent), { children: ((_a = parent.children) !== null && _a !== void 0 ? _a : []).filter(child => child.id !== draggedId) }));
44
+ });
45
+ return updateNodeById(rootWithoutDragged, targetId, target => {
46
+ var _a;
47
+ return (Object.assign(Object.assign({}, target), { children: [...((_a = target.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
48
+ });
49
+ }
50
+ function duplicateNodeBelow(root, nodeId) {
51
+ const node = findNode(root, nodeId);
52
+ const parent = findParent(root, nodeId);
53
+ if (!node || !parent)
54
+ return null;
55
+ const duplicate = cloneNode(node);
56
+ const nextRoot = updateNodeById(root, parent.id, currentParent => (Object.assign(Object.assign({}, currentParent), { children: (() => {
57
+ var _a;
58
+ const children = [...((_a = currentParent.children) !== null && _a !== void 0 ? _a : [])];
59
+ const index = children.findIndex(child => child.id === nodeId);
60
+ if (index === -1)
61
+ return [...children, duplicate];
62
+ children.splice(index + 1, 0, duplicate);
63
+ return children;
64
+ })() })));
65
+ return { root: nextRoot, duplicatedId: duplicate.id };
66
+ }
16
67
  export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }) {
17
- const [contextMenu, setContextMenu] = useState(null);
68
+ const { onFocusNode } = useEditorContext();
18
69
  const [draggedId, setDraggedId] = useState(null);
70
+ const [dropTarget, setDropTarget] = useState(null);
19
71
  const [collapsedIds, setCollapsedIds] = useState(new Set());
20
72
  const [collapsed, setCollapsed] = useState(false);
21
- const [fileMenuOpen, setFileMenuOpen] = useState(false);
22
73
  const [searchQuery, setSearchQuery] = useState('');
74
+ const [contextMenu, setContextMenu] = useState(null);
23
75
  if (!prefabData || !setPrefabData)
24
76
  return null;
25
- const handleContextMenu = (e, nodeId) => {
26
- e.preventDefault();
27
- e.stopPropagation();
28
- setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
29
- };
30
77
  const toggleCollapse = (e, id) => {
31
78
  e.stopPropagation();
32
79
  setCollapsedIds(prev => {
@@ -36,35 +83,33 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
36
83
  });
37
84
  };
38
85
  const handleAddChild = (parentId) => {
86
+ var _a;
87
+ const newNode = {
88
+ id: crypto.randomUUID(),
89
+ name: "New Node",
90
+ components: {
91
+ transform: {
92
+ type: "Transform",
93
+ properties: Object.assign({}, (_a = getComponent('Transform')) === null || _a === void 0 ? void 0 : _a.defaultProperties)
94
+ }
95
+ }
96
+ };
39
97
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parentId, parent => {
40
- var _a, _b;
41
- return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), {
42
- id: crypto.randomUUID(),
43
- name: "New Node",
44
- components: {
45
- transform: {
46
- type: "Transform",
47
- properties: Object.assign({}, (_b = getComponent('Transform')) === null || _b === void 0 ? void 0 : _b.defaultProperties)
48
- }
49
- }
50
- }] }));
98
+ var _a;
99
+ return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), newNode] }));
51
100
  }) })));
52
- setContextMenu(null);
101
+ setSelectedId(newNode.id);
53
102
  };
54
103
  const handleDuplicate = (nodeId) => {
55
104
  if (nodeId === prefabData.root.id)
56
105
  return;
57
106
  setPrefabData(prev => {
58
- const node = findNode(prev.root, nodeId);
59
- const parent = findParent(prev.root, nodeId);
60
- if (!node || !parent)
107
+ const result = duplicateNodeBelow(prev.root, nodeId);
108
+ if (!result)
61
109
  return prev;
62
- return Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parent.id, p => {
63
- var _a;
64
- return (Object.assign(Object.assign({}, p), { children: [...((_a = p.children) !== null && _a !== void 0 ? _a : []), cloneNode(node)] }));
65
- }) });
110
+ setSelectedId(result.duplicatedId);
111
+ return Object.assign(Object.assign({}, prev), { root: result.root });
66
112
  });
67
- setContextMenu(null);
68
113
  };
69
114
  const handleDelete = (nodeId) => {
70
115
  if (nodeId === prefabData.root.id)
@@ -72,43 +117,58 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
72
117
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: deleteNode(prev.root, nodeId) })));
73
118
  if (selectedId === nodeId)
74
119
  setSelectedId(null);
75
- setContextMenu(null);
76
120
  };
77
121
  const handleToggleDisabled = (nodeId) => {
78
122
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, nodeId, node => (Object.assign(Object.assign({}, node), { disabled: !node.disabled }))) })));
79
- setContextMenu(null);
80
123
  };
124
+ const closeContextMenu = () => setContextMenu(null);
125
+ const openContextMenu = (nodeId, x, y) => {
126
+ setSelectedId(nodeId);
127
+ setContextMenu({ nodeId, x, y });
128
+ };
129
+ const handleFocus = (nodeId) => {
130
+ setSelectedId(nodeId);
131
+ onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(nodeId);
132
+ };
133
+ const renderTreeNodeMenu = (nodeId, isRoot, onClose) => (_jsx(TreeNodeMenu, { isRoot: isRoot, nodeId: nodeId, onAddChild: handleAddChild, onFocus: handleFocus, onDuplicate: isRoot ? undefined : handleDuplicate, onDelete: isRoot ? undefined : handleDelete, onClose: onClose }));
81
134
  const handleDragStart = (e, id) => {
82
135
  if (id === prefabData.root.id)
83
136
  return e.preventDefault();
84
137
  e.dataTransfer.effectAllowed = "move";
85
138
  setDraggedId(id);
86
139
  };
87
- const handleDragOver = (e, targetId) => {
140
+ const getDropPosition = (e, isRoot) => {
141
+ if (isRoot)
142
+ return 'inside';
143
+ const rect = e.currentTarget.getBoundingClientRect();
144
+ return e.clientY <= rect.top + rect.height * 0.35 ? 'before' : 'inside';
145
+ };
146
+ const handleDragOver = (e, targetId, isRoot) => {
88
147
  if (!draggedId || draggedId === targetId)
89
148
  return;
90
149
  const draggedNode = findNode(prefabData.root, draggedId);
91
150
  if (draggedNode && findNode(draggedNode, targetId))
92
151
  return;
93
152
  e.preventDefault();
153
+ setDropTarget({ id: targetId, position: getDropPosition(e, isRoot) });
154
+ };
155
+ const handleDragLeave = (e, targetId) => {
156
+ const relatedTarget = e.relatedTarget;
157
+ if (relatedTarget instanceof Node && e.currentTarget.contains(relatedTarget))
158
+ return;
159
+ setDropTarget(current => (current === null || current === void 0 ? void 0 : current.id) === targetId ? null : current);
94
160
  };
95
- const handleDrop = (e, targetId) => {
161
+ const handleDrop = (e, targetId, isRoot) => {
96
162
  if (!draggedId || draggedId === targetId)
97
163
  return;
98
164
  e.preventDefault();
165
+ const dropPosition = getDropPosition(e, isRoot);
99
166
  setPrefabData(prev => {
100
- const draggedNode = findNode(prev.root, draggedId);
101
- const oldParent = findParent(prev.root, draggedId);
102
- if (!draggedNode || !oldParent || findNode(draggedNode, targetId))
103
- return prev;
104
- let root = updateNodeById(prev.root, oldParent.id, p => (Object.assign(Object.assign({}, p), { children: p.children.filter(c => c.id !== draggedId) })));
105
- root = updateNodeById(root, targetId, t => {
106
- var _a;
107
- return (Object.assign(Object.assign({}, t), { children: [...((_a = t.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
108
- });
109
- return Object.assign(Object.assign({}, prev), { root });
167
+ const root = moveNode(prev.root, draggedId, targetId, dropPosition);
168
+ return root === prev.root ? prev : Object.assign(Object.assign({}, prev), { root });
110
169
  });
111
170
  setDraggedId(null);
171
+ setDropTarget(null);
112
172
  };
113
173
  const matchesSearch = (node, query) => {
114
174
  var _a, _b, _c;
@@ -130,49 +190,47 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
130
190
  const isCollapsed = collapsedIds.has(node.id);
131
191
  const hasChildren = node.children && node.children.length > 0;
132
192
  const isRoot = node.id === prefabData.root.id;
133
- return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px`, opacity: node.disabled ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }), draggable: !isRoot, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: () => setDraggedId(null), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, children: [_jsx("span", { style: {
193
+ const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === node.id;
194
+ const showDropBefore = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'before';
195
+ const showDropInside = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'inside';
196
+ return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px`, opacity: node.disabled ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderTop: showDropBefore ? `2px solid ${colors.accent}` : undefined, boxShadow: showDropInside ? `inset 0 0 0 1px ${colors.accentBorder}` : undefined }), draggable: !isRoot, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => {
197
+ e.preventDefault();
198
+ e.stopPropagation();
199
+ openContextMenu(node.id, e.clientX, e.clientY);
200
+ }, onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: () => { setDraggedId(null); setDropTarget(null); }, onDragOver: (e) => handleDragOver(e, node.id, isRoot), onDragLeave: (e) => handleDragLeave(e, node.id), onDrop: (e) => handleDrop(e, node.id, isRoot), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, children: [_jsx("span", { style: {
134
201
  width: 12,
135
202
  opacity: 0.6,
136
203
  marginRight: 4,
137
204
  cursor: 'pointer',
138
205
  visibility: hasChildren ? 'visible' : 'hidden'
139
- }, 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 })] }), !isRoot && (_jsx("button", { style: {
140
- background: 'none',
141
- border: 'none',
142
- cursor: 'pointer',
143
- padding: '0 4px',
144
- fontSize: 14,
145
- opacity: node.disabled ? 0.5 : 0.7,
146
- color: 'inherit',
147
- }, onClick: (e) => {
148
- e.stopPropagation();
149
- handleToggleDisabled(node.id);
150
- }, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
151
- };
152
- return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: () => setFileMenuOpen(false) }))] })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: Object.assign(Object.assign({}, base.input), { padding: '4px 8px' }) }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
153
- }
154
- function FileMenu({ prefabData, setPrefabData, onClose }) {
155
- const { onScreenshot, onExportGLB } = useEditorContext();
156
- const handleLoad = () => __awaiter(this, void 0, void 0, function* () {
157
- const loadedPrefab = yield loadJson();
158
- if (!loadedPrefab)
159
- return;
160
- setPrefabData(loadedPrefab);
161
- onClose();
162
- });
163
- const handleSave = () => {
164
- saveJson(prefabData, "prefab");
165
- onClose();
206
+ }, 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 })] }), !isRoot && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Node Actions", style: {
207
+ background: 'none',
208
+ border: 'none',
209
+ cursor: 'pointer',
210
+ padding: '0 4px',
211
+ fontSize: 14,
212
+ opacity: 0.7,
213
+ color: 'inherit',
214
+ }, children: "\u22EF" })), children: (close) => renderTreeNodeMenu(node.id, false, close) }), _jsx("button", { style: {
215
+ background: 'none',
216
+ border: 'none',
217
+ cursor: 'pointer',
218
+ padding: '0 4px',
219
+ fontSize: 14,
220
+ opacity: node.disabled ? 0.5 : 0.7,
221
+ color: 'inherit',
222
+ }, onClick: (e) => {
223
+ e.stopPropagation();
224
+ handleToggleDisabled(node.id);
225
+ }, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' })] })), isRoot && (_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Scene Actions", style: {
226
+ background: 'none',
227
+ border: 'none',
228
+ cursor: 'pointer',
229
+ padding: '0 4px',
230
+ fontSize: 14,
231
+ opacity: 0.7,
232
+ color: 'inherit',
233
+ }, children: "\u22EF" })), children: (close) => renderTreeNodeMenu(node.id, true, close) }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
166
234
  };
167
- const handleLoadIntoScene = () => __awaiter(this, void 0, void 0, function* () {
168
- const loadedPrefab = yield loadJson();
169
- if (!loadedPrefab)
170
- return;
171
- setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, prev.root.id, root => {
172
- var _a;
173
- return (Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), regenerateIds(loadedPrefab.root)] }));
174
- }) })));
175
- onClose();
176
- });
177
- return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'absolute', top: 28, right: 0 }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: handleLoad, children: "\uD83D\uDCE5 Load Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleSave, children: "\uD83D\uDCBE Save Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleLoadIntoScene, children: "\uD83D\uDCC2 Load into Scene" }), _jsx("button", { style: menu.item, onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "\uD83D\uDCF8 Screenshot" }), _jsx("button", { style: menu.item, onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "\uD83D\uDCE6 Export GLB" })] }));
235
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), 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" }), _jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Menu", style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), children: "\u22EE" })), children: (close) => (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: close })) })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: Object.assign(Object.assign({}, base.input), { padding: '4px 8px' }) }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }), _jsx(TreeContextMenu, { contextMenu: contextMenu, onClose: closeContextMenu, children: (nodeId, close) => renderTreeNodeMenu(nodeId, nodeId === prefabData.root.id, close) })] }));
178
236
  }
@@ -0,0 +1,33 @@
1
+ import { Dispatch, SetStateAction } from 'react';
2
+ import { Prefab } from './types';
3
+ export type TreeContextMenuState = {
4
+ nodeId: string;
5
+ x: number;
6
+ y: number;
7
+ } | null;
8
+ export declare function MenuTriggerButton({ buttonRef, onToggle, title, style, children, }: {
9
+ buttonRef: React.RefObject<HTMLButtonElement | null>;
10
+ onToggle: () => void;
11
+ title: string;
12
+ style: React.CSSProperties;
13
+ children: React.ReactNode;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ export declare function TreeNodeMenu({ isRoot, nodeId, onAddChild, onFocus, onDuplicate, onDelete, onClose, }: {
16
+ isRoot: boolean;
17
+ nodeId: string;
18
+ onAddChild: (parentId: string) => void;
19
+ onFocus: (nodeId: string) => void;
20
+ onDuplicate?: (nodeId: string) => void;
21
+ onDelete?: (nodeId: string) => void;
22
+ onClose: () => void;
23
+ }): import("react/jsx-runtime").JSX.Element;
24
+ export declare function TreeContextMenu({ contextMenu, onClose, children, }: {
25
+ contextMenu: TreeContextMenuState;
26
+ onClose: () => void;
27
+ children: (nodeId: string, onClose: () => void) => React.ReactNode;
28
+ }): import("react").ReactPortal | null;
29
+ export declare function FileMenu({ prefabData, setPrefabData, onClose }: {
30
+ prefabData: Prefab;
31
+ setPrefabData: Dispatch<SetStateAction<Prefab>>;
32
+ onClose: () => void;
33
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,136 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ import { useEffect, useRef, useState } from 'react';
12
+ import { createPortal } from 'react-dom';
13
+ import { menu } from './styles';
14
+ import { useEditorContext } from './EditorContext';
15
+ import { getComponent } from './components/ComponentRegistry';
16
+ import { loadJson, saveJson, regenerateIds, updateNodeById } from './utils';
17
+ function createEmptyPrefab() {
18
+ var _a;
19
+ return {
20
+ id: crypto.randomUUID(),
21
+ name: 'New Scene',
22
+ root: {
23
+ id: crypto.randomUUID(),
24
+ name: 'Scene',
25
+ components: {
26
+ transform: {
27
+ type: 'Transform',
28
+ properties: Object.assign({}, (_a = getComponent('Transform')) === null || _a === void 0 ? void 0 : _a.defaultProperties)
29
+ }
30
+ },
31
+ children: []
32
+ }
33
+ };
34
+ }
35
+ function MenuPanel({ children, style, }) {
36
+ return (_jsx("div", { style: Object.assign(Object.assign(Object.assign({}, menu.container), { position: 'static' }), style), onClick: (e) => e.stopPropagation(), children: children }));
37
+ }
38
+ function MenuItemButton({ children, onClick, danger = false, style, }) {
39
+ return (_jsx("button", { style: danger ? Object.assign(Object.assign(Object.assign({}, menu.item), menu.danger), style) : Object.assign(Object.assign({}, menu.item), style), onClick: onClick, children: children }));
40
+ }
41
+ function MenuSubmenu({ label, children, }) {
42
+ const [isOpen, setIsOpen] = useState(false);
43
+ return (_jsxs("div", { style: { position: 'relative' }, onMouseEnter: () => setIsOpen(true), onMouseLeave: () => setIsOpen(false), children: [_jsxs(MenuItemButton, { onClick: () => setIsOpen(open => !open), style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }, children: [_jsx("span", { children: label }), _jsx("span", { "aria-hidden": "true", children: "\u203A" })] }), isOpen && (_jsx("div", { style: {
44
+ position: 'absolute',
45
+ top: 0,
46
+ left: '100%',
47
+ zIndex: 1,
48
+ }, children: _jsx(MenuPanel, { children: children }) }))] }));
49
+ }
50
+ export function MenuTriggerButton({ buttonRef, onToggle, title, style, children, }) {
51
+ return (_jsx("button", { ref: buttonRef, style: style, onClick: (e) => {
52
+ e.stopPropagation();
53
+ onToggle();
54
+ }, title: title, children: children }));
55
+ }
56
+ export function TreeNodeMenu({ isRoot, nodeId, onAddChild, onFocus, onDuplicate, onDelete, onClose, }) {
57
+ return (_jsxs(MenuPanel, { children: [_jsx(MenuItemButton, { onClick: () => { onAddChild(nodeId); onClose(); }, children: "Add Child" }), _jsx(MenuItemButton, { onClick: () => { onFocus(nodeId); onClose(); }, children: "Focus Camera" }), !isRoot && onDuplicate && (_jsx(MenuItemButton, { onClick: () => { onDuplicate(nodeId); onClose(); }, children: "Duplicate" })), !isRoot && onDelete && (_jsx(MenuItemButton, { danger: true, onClick: () => { onDelete(nodeId); onClose(); }, children: "Delete" }))] }));
58
+ }
59
+ export function TreeContextMenu({ contextMenu, onClose, children, }) {
60
+ var _a, _b;
61
+ const panelRef = useRef(null);
62
+ const [position, setPosition] = useState(null);
63
+ useEffect(() => {
64
+ if (!contextMenu)
65
+ return;
66
+ const handlePointerDown = (event) => {
67
+ var _a;
68
+ const target = event.target;
69
+ if (!target)
70
+ return;
71
+ if ((_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.contains(target))
72
+ return;
73
+ onClose();
74
+ };
75
+ const handleKeyDown = (event) => {
76
+ if (event.key === 'Escape')
77
+ onClose();
78
+ };
79
+ document.addEventListener('pointerdown', handlePointerDown);
80
+ document.addEventListener('keydown', handleKeyDown);
81
+ return () => {
82
+ document.removeEventListener('pointerdown', handlePointerDown);
83
+ document.removeEventListener('keydown', handleKeyDown);
84
+ };
85
+ }, [contextMenu, onClose]);
86
+ useEffect(() => {
87
+ if (!contextMenu || !panelRef.current || typeof window === 'undefined')
88
+ return;
89
+ const panelRect = panelRef.current.getBoundingClientRect();
90
+ const left = Math.max(8, Math.min(contextMenu.x, window.innerWidth - panelRect.width - 8));
91
+ const top = Math.max(8, Math.min(contextMenu.y, window.innerHeight - panelRect.height - 8));
92
+ setPosition({ left, top });
93
+ }, [contextMenu]);
94
+ useEffect(() => {
95
+ if (!contextMenu) {
96
+ setPosition(null);
97
+ }
98
+ }, [contextMenu]);
99
+ if (!contextMenu || typeof document === 'undefined')
100
+ return null;
101
+ return createPortal(_jsx("div", { ref: panelRef, style: {
102
+ position: 'fixed',
103
+ left: (_a = position === null || position === void 0 ? void 0 : position.left) !== null && _a !== void 0 ? _a : contextMenu.x,
104
+ top: (_b = position === null || position === void 0 ? void 0 : position.top) !== null && _b !== void 0 ? _b : contextMenu.y,
105
+ zIndex: 1000,
106
+ }, onMouseLeave: onClose, onContextMenu: (e) => e.preventDefault(), children: children(contextMenu.nodeId, onClose) }), document.body);
107
+ }
108
+ export function FileMenu({ prefabData, setPrefabData, onClose }) {
109
+ const { onScreenshot, onExportGLB } = useEditorContext();
110
+ const handleNewScene = () => {
111
+ setPrefabData(createEmptyPrefab());
112
+ onClose();
113
+ };
114
+ const handleNewSceneFromPrefab = () => __awaiter(this, void 0, void 0, function* () {
115
+ const loadedPrefab = yield loadJson();
116
+ if (!loadedPrefab)
117
+ return;
118
+ setPrefabData(loadedPrefab);
119
+ onClose();
120
+ });
121
+ const handleSave = () => {
122
+ saveJson(prefabData, 'prefab');
123
+ onClose();
124
+ };
125
+ const handleLoadIntoScene = () => __awaiter(this, void 0, void 0, function* () {
126
+ const loadedPrefab = yield loadJson();
127
+ if (!loadedPrefab)
128
+ return;
129
+ setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, prev.root.id, root => {
130
+ var _a;
131
+ return (Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), regenerateIds(loadedPrefab.root)] }));
132
+ }) })));
133
+ onClose();
134
+ });
135
+ return (_jsxs(MenuPanel, { style: { overflow: 'visible' }, children: [_jsxs(MenuSubmenu, { label: "File", children: [_jsx(MenuItemButton, { onClick: handleNewScene, children: "New Scene" }), _jsx(MenuItemButton, { onClick: handleNewSceneFromPrefab, children: "New Scene from Prefab" }), _jsx(MenuItemButton, { onClick: handleLoadIntoScene, children: "Load Prefab into Scene" }), _jsx(MenuItemButton, { onClick: handleSave, children: "Save Prefab" })] }), _jsxs(MenuSubmenu, { label: "Export", children: [_jsx(MenuItemButton, { onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "GLB" }), _jsx(MenuItemButton, { onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "PNG" })] })] }));
136
+ }
@@ -42,7 +42,7 @@ function NodeInspector({ node, updateNode, deleteNode, basePath }) {
42
42
  if (!newAvailable.includes(addType))
43
43
  setAddType(newAvailable[0] || "");
44
44
  }, [Object.keys(node.components || {}).join(',')]);
45
- return _jsxs("div", { style: Object.assign(Object.assign({}, inspector.content), { paddingRight: 2 }), className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: colors.textDim, wordBreak: 'break-all', border: `1px solid ${colors.border}`, padding: '2px 6px', borderRadius: 3, flex: 1, fontFamily: 'monospace' }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
45
+ return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: colors.textDim, wordBreak: 'break-all', border: `1px solid ${colors.border}`, padding: '2px 6px', borderRadius: 3, flex: 1, fontFamily: 'monospace' }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
46
46
  if (!comp)
47
47
  return null;
48
48
  const def = ALL_COMPONENTS[comp.type];
@@ -12,6 +12,7 @@ declare const PrefabEditor: import("react").ForwardRefExoticComponent<{
12
12
  initialPrefab?: Prefab;
13
13
  physics?: boolean;
14
14
  onPrefabChange?: (prefab: Prefab) => void;
15
+ uiPlugins?: React.ReactNode[] | React.ReactNode;
15
16
  children?: React.ReactNode;
16
17
  } & import("react").RefAttributes<PrefabEditorRef>>;
17
18
  export default PrefabEditor;
@@ -21,12 +21,14 @@ const DEFAULT_PREFAB = {
21
21
  }
22
22
  }
23
23
  };
24
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, children }, ref) => {
24
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, uiPlugins, children }, ref) => {
25
25
  const [editMode, setEditMode] = useState(true);
26
26
  const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
27
27
  const [selectedId, setSelectedId] = useState(null);
28
28
  const [transformMode, setTransformMode] = useState("translate");
29
29
  const [snapResolution, setSnapResolution] = useState(0);
30
+ const [positionSnap, setPositionSnap] = useState(0.5);
31
+ const [rotationSnap, setRotationSnap] = useState(Math.PI / 4);
30
32
  const [history, setHistory] = useState([loadedPrefab]);
31
33
  const [historyIndex, setHistoryIndex] = useState(0);
32
34
  const throttleRef = useRef(null);
@@ -107,6 +109,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
107
109
  filename: `${loadedPrefab.name || 'scene'}.glb`
108
110
  });
109
111
  };
112
+ const handleFocusNode = (nodeId) => {
113
+ var _a;
114
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.focusNode(nodeId);
115
+ };
110
116
  useEffect(() => {
111
117
  const canvas = document.querySelector('canvas');
112
118
  if (canvas)
@@ -180,9 +186,14 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
180
186
  setTransformMode,
181
187
  snapResolution,
182
188
  setSnapResolution,
189
+ positionSnap,
190
+ setPositionSnap,
191
+ rotationSnap,
192
+ setRotationSnap,
193
+ onFocusNode: handleFocusNode,
183
194
  onScreenshot: handleScreenshot,
184
195
  onExportGLB: handleExportGLB
185
- }, children: [_jsx(GameCanvas, { children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content }), _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, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
196
+ }, children: [_jsx(GameCanvas, { camera: { position: [0, 5, 15] }, children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content }), _jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), uiPlugins] }), _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
186
197
  });
187
198
  PrefabEditor.displayName = "PrefabEditor";
188
199
  export default PrefabEditor;
@@ -6,6 +6,7 @@ export interface PrefabRootRef {
6
6
  rigidBodyRefs: Map<string, any>;
7
7
  injectModel: (filename: string, model: Object3D) => void;
8
8
  injectTexture: (filename: string, file: File) => void;
9
+ focusNode: (nodeId: string) => void;
9
10
  }
10
11
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
11
12
  editMode?: boolean;