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,9 +1,80 @@
1
1
  import { Dispatch, SetStateAction, useState, MouseEvent } from 'react';
2
2
  import { Prefab, GameObject } from "./types";
3
3
  import { getComponent } from './components/ComponentRegistry';
4
- import { base, colors, tree, menu } from './styles';
5
- 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';
6
6
  import { useEditorContext } from './EditorContext';
7
+ import { Dropdown } from './Dropdown';
8
+ import { FileMenu, MenuTriggerButton, TreeContextMenu, TreeContextMenuState, TreeNodeMenu } from './EditorTreeMenus';
9
+
10
+ type DropPosition = 'before' | 'inside';
11
+
12
+ function moveNode(root: GameObject, draggedId: string, targetId: string, position: DropPosition): GameObject {
13
+ const draggedNode = findNode(root, draggedId);
14
+ const oldParent = findParent(root, draggedId);
15
+
16
+ if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) {
17
+ return root;
18
+ }
19
+
20
+ if (position === 'before') {
21
+ const targetParent = findParent(root, targetId);
22
+ if (!targetParent?.children) return root;
23
+
24
+ if (targetParent.id === oldParent.id) {
25
+ const siblings = targetParent.children.filter(child => child.id !== draggedId);
26
+ const targetIndex = siblings.findIndex(child => child.id === targetId);
27
+ if (targetIndex === -1) return root;
28
+
29
+ siblings.splice(targetIndex, 0, draggedNode);
30
+ return updateNodeById(root, targetParent.id, parent => ({ ...parent, children: siblings }));
31
+ }
32
+
33
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => ({
34
+ ...parent,
35
+ children: (parent.children ?? []).filter(child => child.id !== draggedId)
36
+ }));
37
+
38
+ return updateNodeById(rootWithoutDragged, targetParent.id, parent => {
39
+ const children = [...(parent.children ?? [])];
40
+ const targetIndex = children.findIndex(child => child.id === targetId);
41
+ if (targetIndex === -1) return parent;
42
+
43
+ children.splice(targetIndex, 0, draggedNode);
44
+ return { ...parent, children };
45
+ });
46
+ }
47
+
48
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => ({
49
+ ...parent,
50
+ children: (parent.children ?? []).filter(child => child.id !== draggedId)
51
+ }));
52
+
53
+ return updateNodeById(rootWithoutDragged, targetId, target => ({
54
+ ...target,
55
+ children: [...(target.children ?? []), draggedNode]
56
+ }));
57
+ }
58
+
59
+ function duplicateNodeBelow(root: GameObject, nodeId: string): { root: GameObject; duplicatedId: string } | null {
60
+ const node = findNode(root, nodeId);
61
+ const parent = findParent(root, nodeId);
62
+ if (!node || !parent) return null;
63
+
64
+ const duplicate = cloneNode(node);
65
+ const nextRoot = updateNodeById(root, parent.id, currentParent => ({
66
+ ...currentParent,
67
+ children: (() => {
68
+ const children = [...(currentParent.children ?? [])];
69
+ const index = children.findIndex(child => child.id === nodeId);
70
+ if (index === -1) return [...children, duplicate];
71
+ children.splice(index + 1, 0, duplicate);
72
+ return children;
73
+ })()
74
+ }));
75
+
76
+ return { root: nextRoot, duplicatedId: duplicate.id };
77
+ }
7
78
 
8
79
  export default function EditorTree({
9
80
  prefabData,
@@ -24,21 +95,16 @@ export default function EditorTree({
24
95
  canUndo?: boolean;
25
96
  canRedo?: boolean;
26
97
  }) {
27
- const [contextMenu, setContextMenu] = useState<{ x: number, y: number, nodeId: string } | null>(null);
98
+ const { onFocusNode } = useEditorContext();
28
99
  const [draggedId, setDraggedId] = useState<string | null>(null);
100
+ const [dropTarget, setDropTarget] = useState<{ id: string; position: DropPosition } | null>(null);
29
101
  const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
30
102
  const [collapsed, setCollapsed] = useState(false);
31
- const [fileMenuOpen, setFileMenuOpen] = useState(false);
32
103
  const [searchQuery, setSearchQuery] = useState('');
104
+ const [contextMenu, setContextMenu] = useState<TreeContextMenuState>(null);
33
105
 
34
106
  if (!prefabData || !setPrefabData) return null;
35
107
 
36
- const handleContextMenu = (e: MouseEvent, nodeId: string) => {
37
- e.preventDefault();
38
- e.stopPropagation();
39
- setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
40
- };
41
-
42
108
  const toggleCollapse = (e: MouseEvent, id: string) => {
43
109
  e.stopPropagation();
44
110
  setCollapsedIds(prev => {
@@ -49,47 +115,46 @@ export default function EditorTree({
49
115
  };
50
116
 
51
117
  const handleAddChild = (parentId: string) => {
118
+ const newNode = {
119
+ id: crypto.randomUUID(),
120
+ name: "New Node",
121
+ components: {
122
+ transform: {
123
+ type: "Transform",
124
+ properties: { ...getComponent('Transform')?.defaultProperties }
125
+ }
126
+ }
127
+ };
128
+
52
129
  setPrefabData(prev => ({
53
130
  ...prev,
54
131
  root: updateNodeById(prev.root, parentId, parent => ({
55
132
  ...parent,
56
- children: [...(parent.children ?? []), {
57
- id: crypto.randomUUID(),
58
- name: "New Node",
59
- components: {
60
- transform: {
61
- type: "Transform",
62
- properties: { ...getComponent('Transform')?.defaultProperties }
63
- }
64
- }
65
- }]
133
+ children: [...(parent.children ?? []), newNode]
66
134
  }))
67
135
  }));
68
- setContextMenu(null);
136
+ setSelectedId(newNode.id);
69
137
  };
70
138
 
71
139
  const handleDuplicate = (nodeId: string) => {
72
140
  if (nodeId === prefabData.root.id) return;
73
141
  setPrefabData(prev => {
74
- const node = findNode(prev.root, nodeId);
75
- const parent = findParent(prev.root, nodeId);
76
- if (!node || !parent) return prev;
142
+ const result = duplicateNodeBelow(prev.root, nodeId);
143
+ if (!result) return prev;
144
+
145
+ setSelectedId(result.duplicatedId);
146
+
77
147
  return {
78
148
  ...prev,
79
- root: updateNodeById(prev.root, parent.id, p => ({
80
- ...p,
81
- children: [...(p.children ?? []), cloneNode(node)]
82
- }))
149
+ root: result.root
83
150
  };
84
151
  });
85
- setContextMenu(null);
86
152
  };
87
153
 
88
154
  const handleDelete = (nodeId: string) => {
89
155
  if (nodeId === prefabData.root.id) return;
90
156
  setPrefabData(prev => ({ ...prev, root: deleteNode(prev.root, nodeId)! }));
91
157
  if (selectedId === nodeId) setSelectedId(null);
92
- setContextMenu(null);
93
158
  };
94
159
 
95
160
  const handleToggleDisabled = (nodeId: string) => {
@@ -100,43 +165,68 @@ export default function EditorTree({
100
165
  disabled: !node.disabled
101
166
  }))
102
167
  }));
103
- setContextMenu(null);
104
168
  };
105
169
 
170
+ const closeContextMenu = () => setContextMenu(null);
171
+
172
+ const openContextMenu = (nodeId: string, x: number, y: number) => {
173
+ setSelectedId(nodeId);
174
+ setContextMenu({ nodeId, x, y });
175
+ };
176
+
177
+ const handleFocus = (nodeId: string) => {
178
+ setSelectedId(nodeId);
179
+ onFocusNode?.(nodeId);
180
+ };
181
+
182
+ const renderTreeNodeMenu = (nodeId: string, isRoot: boolean, onClose: () => void) => (
183
+ <TreeNodeMenu
184
+ isRoot={isRoot}
185
+ nodeId={nodeId}
186
+ onAddChild={handleAddChild}
187
+ onFocus={handleFocus}
188
+ onDuplicate={isRoot ? undefined : handleDuplicate}
189
+ onDelete={isRoot ? undefined : handleDelete}
190
+ onClose={onClose}
191
+ />
192
+ );
193
+
106
194
  const handleDragStart = (e: React.DragEvent, id: string) => {
107
195
  if (id === prefabData.root.id) return e.preventDefault();
108
196
  e.dataTransfer.effectAllowed = "move";
109
197
  setDraggedId(id);
110
198
  };
111
199
 
112
- const handleDragOver = (e: React.DragEvent, targetId: string) => {
200
+ const getDropPosition = (e: React.DragEvent<HTMLDivElement>, isRoot: boolean): DropPosition => {
201
+ if (isRoot) return 'inside';
202
+ const rect = e.currentTarget.getBoundingClientRect();
203
+ return e.clientY <= rect.top + rect.height * 0.35 ? 'before' : 'inside';
204
+ };
205
+
206
+ const handleDragOver = (e: React.DragEvent<HTMLDivElement>, targetId: string, isRoot: boolean) => {
113
207
  if (!draggedId || draggedId === targetId) return;
114
208
  const draggedNode = findNode(prefabData.root, draggedId);
115
209
  if (draggedNode && findNode(draggedNode, targetId)) return;
116
210
  e.preventDefault();
211
+ setDropTarget({ id: targetId, position: getDropPosition(e, isRoot) });
117
212
  };
118
213
 
119
- const handleDrop = (e: React.DragEvent, targetId: string) => {
214
+ const handleDragLeave = (e: React.DragEvent<HTMLDivElement>, targetId: string) => {
215
+ const relatedTarget = e.relatedTarget;
216
+ if (relatedTarget instanceof Node && e.currentTarget.contains(relatedTarget)) return;
217
+ setDropTarget(current => current?.id === targetId ? null : current);
218
+ };
219
+
220
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>, targetId: string, isRoot: boolean) => {
120
221
  if (!draggedId || draggedId === targetId) return;
121
222
  e.preventDefault();
223
+ const dropPosition = getDropPosition(e, isRoot);
122
224
  setPrefabData(prev => {
123
- const draggedNode = findNode(prev.root, draggedId);
124
- const oldParent = findParent(prev.root, draggedId);
125
- if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) return prev;
126
-
127
- let root = updateNodeById(prev.root, oldParent.id, p => ({
128
- ...p,
129
- children: p.children!.filter(c => c.id !== draggedId)
130
- }));
131
-
132
- root = updateNodeById(root, targetId, t => ({
133
- ...t,
134
- children: [...(t.children ?? []), draggedNode]
135
- }));
136
-
137
- return { ...prev, root };
225
+ const root = moveNode(prev.root, draggedId, targetId, dropPosition);
226
+ return root === prev.root ? prev : { ...prev, root };
138
227
  });
139
228
  setDraggedId(null);
229
+ setDropTarget(null);
140
230
  };
141
231
 
142
232
 
@@ -156,6 +246,9 @@ export default function EditorTree({
156
246
  const isCollapsed = collapsedIds.has(node.id);
157
247
  const hasChildren = node.children && node.children.length > 0;
158
248
  const isRoot = node.id === prefabData.root.id;
249
+ const isDropTarget = dropTarget?.id === node.id;
250
+ const showDropBefore = isDropTarget && dropTarget?.position === 'before';
251
+ const showDropInside = isDropTarget && dropTarget?.position === 'inside';
159
252
 
160
253
  return (
161
254
  <div key={node.id}>
@@ -168,14 +261,21 @@ export default function EditorTree({
168
261
  display: 'flex',
169
262
  alignItems: 'center',
170
263
  justifyContent: 'space-between',
264
+ borderTop: showDropBefore ? `2px solid ${colors.accent}` : undefined,
265
+ boxShadow: showDropInside ? `inset 0 0 0 1px ${colors.accentBorder}` : undefined,
171
266
  }}
172
267
  draggable={!isRoot}
173
268
  onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
174
- onContextMenu={(e) => handleContextMenu(e, node.id)}
269
+ onContextMenu={(e) => {
270
+ e.preventDefault();
271
+ e.stopPropagation();
272
+ openContextMenu(node.id, e.clientX, e.clientY);
273
+ }}
175
274
  onDragStart={(e) => handleDragStart(e, node.id)}
176
- onDragEnd={() => setDraggedId(null)}
177
- onDragOver={(e) => handleDragOver(e, node.id)}
178
- onDrop={(e) => handleDrop(e, node.id)}
275
+ onDragEnd={() => { setDraggedId(null); setDropTarget(null); }}
276
+ onDragOver={(e) => handleDragOver(e, node.id, isRoot)}
277
+ onDragLeave={(e) => handleDragLeave(e, node.id)}
278
+ onDrop={(e) => handleDrop(e, node.id, isRoot)}
179
279
  >
180
280
  <div style={{ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }}>
181
281
  <span
@@ -196,24 +296,74 @@ export default function EditorTree({
196
296
  </span>
197
297
  </div>
198
298
  {!isRoot && (
199
- <button
200
- style={{
201
- background: 'none',
202
- border: 'none',
203
- cursor: 'pointer',
204
- padding: '0 4px',
205
- fontSize: 14,
206
- opacity: node.disabled ? 0.5 : 0.7,
207
- color: 'inherit',
208
- }}
209
- onClick={(e) => {
210
- e.stopPropagation();
211
- handleToggleDisabled(node.id);
212
- }}
213
- title={node.disabled ? 'Enable' : 'Disable'}
299
+ <div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
300
+ <Dropdown
301
+ placement="bottom-end"
302
+ trigger={({ ref, toggle }) => (
303
+ <MenuTriggerButton
304
+ buttonRef={ref}
305
+ onToggle={toggle}
306
+ title="Node Actions"
307
+ style={{
308
+ background: 'none',
309
+ border: 'none',
310
+ cursor: 'pointer',
311
+ padding: '0 4px',
312
+ fontSize: 14,
313
+ opacity: 0.7,
314
+ color: 'inherit',
315
+ }}
316
+ >
317
+
318
+ </MenuTriggerButton>
319
+ )}
320
+ >
321
+ {(close) => renderTreeNodeMenu(node.id, false, close)}
322
+ </Dropdown>
323
+ <button
324
+ style={{
325
+ background: 'none',
326
+ border: 'none',
327
+ cursor: 'pointer',
328
+ padding: '0 4px',
329
+ fontSize: 14,
330
+ opacity: node.disabled ? 0.5 : 0.7,
331
+ color: 'inherit',
332
+ }}
333
+ onClick={(e) => {
334
+ e.stopPropagation();
335
+ handleToggleDisabled(node.id);
336
+ }}
337
+ title={node.disabled ? 'Enable' : 'Disable'}
338
+ >
339
+ {node.disabled ? '◎' : '◉'}
340
+ </button>
341
+ </div>
342
+ )}
343
+ {isRoot && (
344
+ <Dropdown
345
+ placement="bottom-end"
346
+ trigger={({ ref, toggle }) => (
347
+ <MenuTriggerButton
348
+ buttonRef={ref}
349
+ onToggle={toggle}
350
+ title="Scene Actions"
351
+ style={{
352
+ background: 'none',
353
+ border: 'none',
354
+ cursor: 'pointer',
355
+ padding: '0 4px',
356
+ fontSize: 14,
357
+ opacity: 0.7,
358
+ color: 'inherit',
359
+ }}
360
+ >
361
+
362
+ </MenuTriggerButton>
363
+ )}
214
364
  >
215
- {node.disabled ? '◎' : '◉'}
216
- </button>
365
+ {(close) => renderTreeNodeMenu(node.id, true, close)}
366
+ </Dropdown>
217
367
  )}
218
368
  </div>
219
369
  {!isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))}
@@ -223,7 +373,7 @@ export default function EditorTree({
223
373
 
224
374
  return (
225
375
  <>
226
- <div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => { setContextMenu(null); setFileMenuOpen(false); }}>
376
+ <div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }}>
227
377
  <div style={base.header}>
228
378
  <div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} onClick={() => setCollapsed(!collapsed)}>
229
379
  <span>{collapsed ? '▶' : '▼'}</span>
@@ -247,22 +397,27 @@ export default function EditorTree({
247
397
  >
248
398
 
249
399
  </button>
250
- <div style={{ position: 'relative' }}>
251
- <button
252
- style={{ ...base.btn, padding: '2px 6px', fontSize: 10 }}
253
- onClick={(e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }}
254
- title="File"
255
- >
256
-
257
- </button>
258
- {fileMenuOpen && (
400
+ <Dropdown
401
+ placement="bottom-end"
402
+ trigger={({ ref, toggle }) => (
403
+ <MenuTriggerButton
404
+ buttonRef={ref}
405
+ onToggle={toggle}
406
+ title="Menu"
407
+ style={{ ...base.btn, padding: '2px 6px', fontSize: 10 }}
408
+ >
409
+
410
+ </MenuTriggerButton>
411
+ )}
412
+ >
413
+ {(close) => (
259
414
  <FileMenu
260
415
  prefabData={prefabData}
261
416
  setPrefabData={setPrefabData}
262
- onClose={() => setFileMenuOpen(false)}
417
+ onClose={close}
263
418
  />
264
419
  )}
265
- </div>
420
+ </Dropdown>
266
421
  </div>
267
422
  )}
268
423
  </div>
@@ -285,104 +440,13 @@ export default function EditorTree({
285
440
  </>
286
441
  )}
287
442
  </div>
443
+ <TreeContextMenu
444
+ contextMenu={contextMenu}
445
+ onClose={closeContextMenu}
446
+ >
447
+ {(nodeId, close) => renderTreeNodeMenu(nodeId, nodeId === prefabData.root.id, close)}
448
+ </TreeContextMenu>
288
449
 
289
- {contextMenu && (
290
- <div
291
- style={{ ...menu.container, top: contextMenu.y, left: contextMenu.x }}
292
- onClick={(e) => e.stopPropagation()}
293
- onPointerLeave={() => setContextMenu(null)}
294
- >
295
- <button style={menu.item} onClick={() => handleAddChild(contextMenu.nodeId)}>
296
- Add Child
297
- </button>
298
- {contextMenu.nodeId !== prefabData.root.id && (
299
- <>
300
- <button style={menu.item} onClick={() => handleDuplicate(contextMenu.nodeId)}>
301
- Duplicate
302
- </button>
303
- <button style={{ ...menu.item, ...menu.danger }} onClick={() => handleDelete(contextMenu.nodeId)}>
304
- Delete
305
- </button>
306
- </>
307
- )}
308
- </div>
309
- )}
310
450
  </>
311
451
  );
312
452
  }
313
-
314
- function FileMenu({
315
- prefabData,
316
- setPrefabData,
317
- onClose
318
- }: {
319
- prefabData: Prefab;
320
- setPrefabData: Dispatch<SetStateAction<Prefab>>;
321
- onClose: () => void;
322
- }) {
323
- const { onScreenshot, onExportGLB } = useEditorContext();
324
-
325
- const handleLoad = async () => {
326
- const loadedPrefab = await loadJson();
327
- if (!loadedPrefab) return;
328
- setPrefabData(loadedPrefab);
329
- onClose();
330
- };
331
-
332
- const handleSave = () => {
333
- saveJson(prefabData, "prefab");
334
- onClose();
335
- };
336
-
337
- const handleLoadIntoScene = async () => {
338
- const loadedPrefab = await loadJson();
339
- if (!loadedPrefab) return;
340
-
341
- setPrefabData(prev => ({
342
- ...prev,
343
- root: updateNodeById(prev.root, prev.root.id, root => ({
344
- ...root,
345
- children: [...(root.children ?? []), regenerateIds(loadedPrefab.root)]
346
- }))
347
- }));
348
- onClose();
349
- };
350
-
351
- return (
352
- <div
353
- style={{ ...menu.container, position: 'absolute', top: 28, right: 0 }}
354
- onClick={(e) => e.stopPropagation()}
355
- >
356
- <button
357
- style={menu.item}
358
- onClick={handleLoad}
359
- >
360
- 📥 Load Prefab JSON
361
- </button>
362
- <button
363
- style={menu.item}
364
- onClick={handleSave}
365
- >
366
- 💾 Save Prefab JSON
367
- </button>
368
- <button
369
- style={menu.item}
370
- onClick={handleLoadIntoScene}
371
- >
372
- 📂 Load into Scene
373
- </button>
374
- <button
375
- style={menu.item}
376
- onClick={() => { onScreenshot?.(); onClose(); }}
377
- >
378
- 📸 Screenshot
379
- </button>
380
- <button
381
- style={menu.item}
382
- onClick={() => { onExportGLB?.(); onClose(); }}
383
- >
384
- 📦 Export GLB
385
- </button>
386
- </div>
387
- );
388
- }