react-three-game 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.github/copilot-instructions.md +54 -183
  2. package/README.md +69 -214
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +3 -0
  5. package/dist/tools/prefabeditor/EditorTree.d.ts +2 -4
  6. package/dist/tools/prefabeditor/EditorTree.js +20 -194
  7. package/dist/tools/prefabeditor/EditorUI.js +43 -224
  8. package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
  9. package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
  10. package/dist/tools/prefabeditor/PrefabEditor.js +33 -99
  11. package/dist/tools/prefabeditor/PrefabRoot.d.ts +0 -1
  12. package/dist/tools/prefabeditor/PrefabRoot.js +33 -50
  13. package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
  14. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +102 -0
  15. package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
  16. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
  17. package/dist/tools/prefabeditor/components/index.js +2 -0
  18. package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
  19. package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
  20. package/dist/tools/prefabeditor/styles.d.ts +1809 -0
  21. package/dist/tools/prefabeditor/styles.js +168 -0
  22. package/dist/tools/prefabeditor/types.d.ts +3 -14
  23. package/dist/tools/prefabeditor/types.js +0 -1
  24. package/dist/tools/prefabeditor/utils.d.ts +19 -0
  25. package/dist/tools/prefabeditor/utils.js +72 -0
  26. package/package.json +3 -3
  27. package/src/index.ts +5 -1
  28. package/src/tools/prefabeditor/EditorTree.tsx +38 -270
  29. package/src/tools/prefabeditor/EditorUI.tsx +105 -322
  30. package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
  31. package/src/tools/prefabeditor/PrefabEditor.tsx +40 -151
  32. package/src/tools/prefabeditor/PrefabRoot.tsx +41 -73
  33. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +317 -0
  34. package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
  35. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
  36. package/src/tools/prefabeditor/components/index.ts +2 -0
  37. package/src/tools/prefabeditor/styles.ts +195 -0
  38. package/src/tools/prefabeditor/types.ts +4 -12
  39. package/src/tools/prefabeditor/utils.ts +80 -0
@@ -1,119 +1,19 @@
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, tree, menu } from './styles';
5
+ import { findNode, findParent, deleteNode, cloneNode } from './utils';
4
6
 
5
- interface EditorTreeProps {
7
+ export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }: {
6
8
  prefabData?: Prefab;
7
9
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
8
10
  selectedId: string | null;
9
11
  setSelectedId: Dispatch<SetStateAction<string | null>>;
10
- }
11
-
12
- export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }: EditorTreeProps) {
12
+ }) {
13
13
  const [contextMenu, setContextMenu] = useState<{ x: number, y: number, nodeId: string } | null>(null);
14
14
  const [draggedId, setDraggedId] = useState<string | null>(null);
15
15
  const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
16
- const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);
17
-
18
- const styles: Record<string, React.CSSProperties> = {
19
- panel: {
20
- background: "rgba(0,0,0,0.55)",
21
- color: "rgba(255,255,255,0.9)",
22
- border: "1px solid rgba(255,255,255,0.12)",
23
- borderRadius: 6,
24
- overflow: "hidden",
25
- maxHeight: "85vh",
26
- display: "flex",
27
- flexDirection: "column",
28
- backdropFilter: "blur(6px)",
29
- WebkitBackdropFilter: "blur(6px)",
30
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
31
- fontSize: 11,
32
- lineHeight: 1.2,
33
- userSelect: "none",
34
- WebkitUserSelect: "none",
35
- },
36
- panelHeader: {
37
- padding: "4px 6px",
38
- borderBottom: "1px solid rgba(255,255,255,0.10)",
39
- display: "flex",
40
- gap: 8,
41
- alignItems: "center",
42
- justifyContent: "space-between",
43
- cursor: "pointer",
44
- background: "rgba(255,255,255,0.05)",
45
- textTransform: "uppercase",
46
- letterSpacing: "0.08em",
47
- fontSize: 10,
48
- color: "rgba(255,255,255,0.7)",
49
- },
50
- scroll: {
51
- overflowY: "auto",
52
- },
53
- row: {
54
- display: "flex",
55
- alignItems: "center",
56
- padding: "2px 6px",
57
- borderBottom: "1px solid rgba(255,255,255,0.07)",
58
- cursor: "pointer",
59
- whiteSpace: "nowrap",
60
- },
61
- rowSelected: {
62
- background: "rgba(255,255,255,0.10)",
63
- },
64
- chevron: {
65
- width: 12,
66
- textAlign: "center",
67
- opacity: 0.55,
68
- fontSize: 10,
69
- marginRight: 4,
70
- cursor: "pointer",
71
- },
72
- idText: {
73
- fontSize: 11,
74
- overflow: "hidden",
75
- textOverflow: "ellipsis",
76
- },
77
- dragHandle: {
78
- width: 14,
79
- height: 14,
80
- display: "flex",
81
- alignItems: "center",
82
- justifyContent: "center",
83
- marginRight: 4,
84
- opacity: 0.4,
85
- cursor: "grab",
86
- fontSize: 10,
87
- },
88
- contextMenu: {
89
- position: "fixed",
90
- zIndex: 50,
91
- minWidth: 120,
92
- background: "rgba(0,0,0,0.82)",
93
- border: "1px solid rgba(255,255,255,0.16)",
94
- borderRadius: 6,
95
- overflow: "hidden",
96
- boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
97
- backdropFilter: "blur(6px)",
98
- WebkitBackdropFilter: "blur(6px)",
99
- },
100
- menuItem: {
101
- width: "100%",
102
- textAlign: "left",
103
- padding: "6px 8px",
104
- background: "transparent",
105
- border: "none",
106
- color: "rgba(255,255,255,0.9)",
107
- font: "inherit",
108
- cursor: "pointer",
109
- },
110
- menuItemDanger: {
111
- color: "rgba(255,120,120,0.95)",
112
- },
113
- menuDivider: {
114
- borderTop: "1px solid rgba(255,255,255,0.10)",
115
- }
116
- };
16
+ const [collapsed, setCollapsed] = useState(false);
117
17
 
118
18
  if (!prefabData || !setPrefabData) return null;
119
19
 
@@ -123,14 +23,11 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
123
23
  setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
124
24
  };
125
25
 
126
- const closeContextMenu = () => setContextMenu(null);
127
-
128
26
  const toggleCollapse = (e: MouseEvent, id: string) => {
129
27
  e.stopPropagation();
130
28
  setCollapsedIds(prev => {
131
29
  const next = new Set(prev);
132
- if (next.has(id)) next.delete(id);
133
- else next.add(id);
30
+ next.has(id) ? next.delete(id) : next.add(id);
134
31
  return next;
135
32
  });
136
33
  };
@@ -148,7 +45,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
148
45
  };
149
46
 
150
47
  setPrefabData(prev => {
151
- const newRoot = JSON.parse(JSON.stringify(prev.root)); // Deep clone for safety
48
+ const newRoot = JSON.parse(JSON.stringify(prev.root));
152
49
  const parent = findNode(newRoot, parentId);
153
50
  if (parent) {
154
51
  parent.children = parent.children || [];
@@ -156,11 +53,11 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
156
53
  }
157
54
  return { ...prev, root: newRoot };
158
55
  });
159
- closeContextMenu();
56
+ setContextMenu(null);
160
57
  };
161
58
 
162
59
  const handleDuplicate = (nodeId: string) => {
163
- if (nodeId === prefabData.root.id) return; // Cannot duplicate root
60
+ if (nodeId === prefabData.root.id) return;
164
61
 
165
62
  setPrefabData(prev => {
166
63
  const newRoot = JSON.parse(JSON.stringify(prev.root));
@@ -174,18 +71,15 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
174
71
  }
175
72
  return { ...prev, root: newRoot };
176
73
  });
177
- closeContextMenu();
74
+ setContextMenu(null);
178
75
  };
179
76
 
180
77
  const handleDelete = (nodeId: string) => {
181
- if (nodeId === prefabData.root.id) return; // Cannot delete root
78
+ if (nodeId === prefabData.root.id) return;
182
79
 
183
- setPrefabData(prev => {
184
- const newRoot = deleteNodeFromTree(JSON.parse(JSON.stringify(prev.root)), nodeId);
185
- return { ...prev, root: newRoot! };
186
- });
80
+ setPrefabData(prev => ({ ...prev, root: deleteNode(prev.root, nodeId)! }));
187
81
  if (selectedId === nodeId) setSelectedId(null);
188
- closeContextMenu();
82
+ setContextMenu(null);
189
83
  };
190
84
 
191
85
  // Drag and Drop
@@ -195,26 +89,18 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
195
89
  return;
196
90
  }
197
91
  e.dataTransfer.effectAllowed = "move";
198
- e.dataTransfer.setData("text/plain", id);
199
92
  setDraggedId(id);
200
93
  };
201
94
 
202
- const handleDragEnd = () => {
203
- setDraggedId(null);
204
- };
205
-
206
95
  const handleDragOver = (e: React.DragEvent, targetId: string) => {
207
96
  if (!draggedId || draggedId === targetId) return;
208
97
  const draggedNode = findNode(prefabData.root, draggedId);
209
98
  if (draggedNode && findNode(draggedNode, targetId)) return;
210
-
211
99
  e.preventDefault();
212
- e.dataTransfer.dropEffect = "move";
213
100
  };
214
101
 
215
102
  const handleDrop = (e: React.DragEvent, targetId: string) => {
216
103
  if (!draggedId || draggedId === targetId) return;
217
-
218
104
  e.preventDefault();
219
105
 
220
106
  setPrefabData(prev => {
@@ -241,152 +127,75 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
241
127
  setDraggedId(null);
242
128
  };
243
129
 
244
- const renderNode = (node: GameObject, depth: number = 0) => {
130
+ const renderNode = (node: GameObject, depth = 0): React.ReactNode => {
245
131
  if (!node) return null;
246
132
 
247
133
  const isSelected = node.id === selectedId;
248
134
  const isCollapsed = collapsedIds.has(node.id);
249
135
  const hasChildren = node.children && node.children.length > 0;
136
+ const isRoot = node.id === prefabData.root.id;
250
137
 
251
138
  return (
252
139
  <div key={node.id}>
253
140
  <div
254
141
  style={{
255
- ...styles.row,
256
- ...(isSelected ? styles.rowSelected : null),
257
- paddingLeft: `${depth * 10 + 6}px`,
258
- cursor: node.id !== prefabData.root.id ? "grab" : "pointer",
142
+ ...tree.row,
143
+ ...(isSelected ? tree.selected : {}),
144
+ paddingLeft: `${depth * 12 + 6}px`,
259
145
  }}
260
- draggable={node.id !== prefabData.root.id}
146
+ draggable={!isRoot}
261
147
  onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
262
148
  onContextMenu={(e) => handleContextMenu(e, node.id)}
263
149
  onDragStart={(e) => handleDragStart(e, node.id)}
264
- onDragEnd={handleDragEnd}
150
+ onDragEnd={() => setDraggedId(null)}
265
151
  onDragOver={(e) => handleDragOver(e, node.id)}
266
152
  onDrop={(e) => handleDrop(e, node.id)}
267
- onPointerEnter={(e) => {
268
- if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.06)";
269
- }}
270
- onPointerLeave={(e) => {
271
- if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "transparent";
272
- }}
273
153
  >
274
154
  <span
275
155
  style={{
276
- ...styles.chevron,
277
- visibility: hasChildren ? 'visible' : 'hidden',
156
+ width: 12,
157
+ opacity: 0.6,
158
+ marginRight: 4,
159
+ cursor: 'pointer',
160
+ visibility: hasChildren ? 'visible' : 'hidden'
278
161
  }}
279
162
  onClick={(e) => hasChildren && toggleCollapse(e, node.id)}
280
- onPointerEnter={(e) => {
281
- (e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
282
- }}
283
- onPointerLeave={(e) => {
284
- (e.currentTarget as HTMLSpanElement).style.opacity = "0.55";
285
- }}
286
163
  >
287
164
  {isCollapsed ? '▶' : '▼'}
288
165
  </span>
289
- {node.id !== prefabData.root.id && (
290
- <span
291
- style={styles.dragHandle}
292
- onPointerEnter={(e) => {
293
- (e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
294
- }}
295
- onPointerLeave={(e) => {
296
- (e.currentTarget as HTMLSpanElement).style.opacity = "0.4";
297
- }}
298
- >
299
- ⋮⋮
300
- </span>
301
- )}
302
- <span style={styles.idText}>
303
- {node.id}
304
- </span>
166
+ {!isRoot && <span style={{ marginRight: 4, opacity: 0.4 }}>⋮⋮</span>}
167
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{node.id}</span>
305
168
  </div>
306
- {!isCollapsed && node.children && (
307
- <div>
308
- {node.children.map(child => renderNode(child, depth + 1))}
309
- </div>
310
- )}
169
+ {!isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))}
311
170
  </div>
312
171
  );
313
172
  };
314
173
 
315
174
  return (
316
175
  <>
317
- <div
318
- style={{
319
- ...styles.panel,
320
- width: isTreeCollapsed ? 'auto' : '14rem',
321
- }}
322
- onClick={closeContextMenu}
323
- >
324
- <div
325
- style={styles.panelHeader}
326
- onClick={(e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }}
327
- onPointerEnter={(e) => {
328
- (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.08)";
329
- }}
330
- onPointerLeave={(e) => {
331
- (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.05)";
332
- }}
333
- >
334
- <span>Prefab Graph</span>
335
- <span style={{ fontSize: 10, opacity: 0.8 }}>{isTreeCollapsed ? '▶' : '◀'}</span>
176
+ <div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => setContextMenu(null)}>
177
+ <div style={base.header} onClick={() => setCollapsed(!collapsed)}>
178
+ <span>Scene</span>
179
+ <span>{collapsed ? '' : ''}</span>
336
180
  </div>
337
- {!isTreeCollapsed && (
338
- <div style={{ ...styles.scroll, padding: 2 }}>
339
- {renderNode(prefabData.root)}
340
- </div>
341
- )}
181
+ {!collapsed && <div style={tree.scroll}>{renderNode(prefabData.root)}</div>}
342
182
  </div>
343
183
 
344
184
  {contextMenu && (
345
185
  <div
346
- style={{
347
- ...styles.contextMenu,
348
- top: contextMenu.y,
349
- left: contextMenu.x,
350
- }}
186
+ style={{ ...menu.container, top: contextMenu.y, left: contextMenu.x }}
351
187
  onClick={(e) => e.stopPropagation()}
352
- onPointerLeave={closeContextMenu}
188
+ onPointerLeave={() => setContextMenu(null)}
353
189
  >
354
- <button
355
- style={{ ...styles.menuItem, ...styles.menuDivider }}
356
- onClick={() => handleAddChild(contextMenu.nodeId)}
357
- onPointerEnter={(e) => {
358
- (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
359
- }}
360
- onPointerLeave={(e) => {
361
- (e.currentTarget as HTMLButtonElement).style.background = "transparent";
362
- }}
363
- >
190
+ <button style={menu.item} onClick={() => handleAddChild(contextMenu.nodeId)}>
364
191
  Add Child
365
192
  </button>
366
193
  {contextMenu.nodeId !== prefabData.root.id && (
367
194
  <>
368
- <button
369
- style={{ ...styles.menuItem, ...styles.menuDivider }}
370
- onClick={() => handleDuplicate(contextMenu.nodeId)}
371
- onPointerEnter={(e) => {
372
- (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
373
- }}
374
- onPointerLeave={(e) => {
375
- (e.currentTarget as HTMLButtonElement).style.background = "transparent";
376
- }}
377
- >
195
+ <button style={menu.item} onClick={() => handleDuplicate(contextMenu.nodeId)}>
378
196
  Duplicate
379
197
  </button>
380
- <button
381
- style={{ ...styles.menuItem, ...styles.menuItemDanger }}
382
- onClick={() => handleDelete(contextMenu.nodeId)}
383
- onPointerEnter={(e) => {
384
- (e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
385
- }}
386
- onPointerLeave={(e) => {
387
- (e.currentTarget as HTMLButtonElement).style.background = "transparent";
388
- }}
389
- >
198
+ <button style={{ ...menu.item, ...menu.danger }} onClick={() => handleDelete(contextMenu.nodeId)}>
390
199
  Delete
391
200
  </button>
392
201
  </>
@@ -396,44 +205,3 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
396
205
  </>
397
206
  );
398
207
  }
399
-
400
- // --- Helpers ---
401
-
402
- function findNode(root: GameObject, id: string): GameObject | null {
403
- if (root.id === id) return root;
404
- if (root.children) {
405
- for (const child of root.children) {
406
- const found = findNode(child, id);
407
- if (found) return found;
408
- }
409
- }
410
- return null;
411
- }
412
-
413
- function findParent(root: GameObject, id: string): GameObject | null {
414
- if (!root.children) return null;
415
- for (const child of root.children) {
416
- if (child.id === id) return root;
417
- const found = findParent(child, id);
418
- if (found) return found;
419
- }
420
- return null;
421
- }
422
-
423
- function deleteNodeFromTree(root: GameObject, id: string): GameObject | null {
424
- if (root.id === id) return null;
425
- if (root.children) {
426
- root.children = root.children
427
- .map(child => deleteNodeFromTree(child, id))
428
- .filter((child): child is GameObject => child !== null);
429
- }
430
- return root;
431
- }
432
-
433
- function cloneNode(node: GameObject): GameObject {
434
- const newNode = { ...node, id: crypto.randomUUID() };
435
- if (newNode.children) {
436
- newNode.children = newNode.children.map(child => cloneNode(child));
437
- }
438
- return newNode;
439
- }