react-three-game 0.0.55 → 0.0.57

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 (68) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/shared/ContactShadow.d.ts +8 -0
  4. package/dist/shared/ContactShadow.js +32 -0
  5. package/dist/shared/GameCanvas.js +1 -3
  6. package/dist/tools/assetviewer/page.js +36 -15
  7. package/dist/tools/dragdrop/DragDropLoader.js +17 -40
  8. package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
  9. package/dist/tools/dragdrop/modelLoader.js +39 -0
  10. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  11. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  12. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  13. package/dist/tools/prefabeditor/EditorTree.js +139 -70
  14. package/dist/tools/prefabeditor/EditorUI.js +5 -10
  15. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
  17. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
  18. package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
  19. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  20. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  21. package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
  22. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  23. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  24. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  25. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  26. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  27. package/dist/tools/prefabeditor/components/Input.js +100 -47
  28. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  32. package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -11
  33. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  34. package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
  35. package/dist/tools/prefabeditor/components/index.js +5 -1
  36. package/dist/tools/prefabeditor/styles.d.ts +17 -4
  37. package/dist/tools/prefabeditor/styles.js +69 -32
  38. package/dist/tools/prefabeditor/utils.d.ts +8 -3
  39. package/dist/tools/prefabeditor/utils.js +92 -6
  40. package/package.json +1 -1
  41. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
  42. package/src/index.ts +7 -0
  43. package/src/shared/ContactShadow.tsx +74 -0
  44. package/src/shared/GameCanvas.tsx +0 -3
  45. package/src/tools/assetviewer/page.tsx +78 -46
  46. package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
  47. package/src/tools/dragdrop/modelLoader.ts +36 -0
  48. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  49. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  50. package/src/tools/prefabeditor/EditorTree.tsx +237 -115
  51. package/src/tools/prefabeditor/EditorUI.tsx +6 -11
  52. package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
  53. package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
  54. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  55. package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
  56. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
  57. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  58. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  59. package/src/tools/prefabeditor/components/Input.tsx +247 -53
  60. package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
  61. package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
  62. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  63. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
  64. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  65. package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
  66. package/src/tools/prefabeditor/components/index.ts +5 -1
  67. package/src/tools/prefabeditor/styles.ts +71 -32
  68. package/src/tools/prefabeditor/utils.ts +96 -5
@@ -10,23 +10,77 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
11
  import { useState } from 'react';
12
12
  import { getComponent } from './components/ComponentRegistry';
13
- import { base, tree, menu } from './styles';
13
+ import { base, colors, tree, menu } from './styles';
14
14
  import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
15
15
  import { useEditorContext } from './EditorContext';
16
+ import { Dropdown } from './Dropdown';
17
+ function moveNode(root, draggedId, targetId, position) {
18
+ const draggedNode = findNode(root, draggedId);
19
+ const oldParent = findParent(root, draggedId);
20
+ if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) {
21
+ return root;
22
+ }
23
+ if (position === 'before') {
24
+ const targetParent = findParent(root, targetId);
25
+ if (!(targetParent === null || targetParent === void 0 ? void 0 : targetParent.children))
26
+ return root;
27
+ if (targetParent.id === oldParent.id) {
28
+ const siblings = targetParent.children.filter(child => child.id !== draggedId);
29
+ const targetIndex = siblings.findIndex(child => child.id === targetId);
30
+ if (targetIndex === -1)
31
+ return root;
32
+ siblings.splice(targetIndex, 0, draggedNode);
33
+ return updateNodeById(root, targetParent.id, parent => (Object.assign(Object.assign({}, parent), { children: siblings })));
34
+ }
35
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => {
36
+ var _a;
37
+ return (Object.assign(Object.assign({}, parent), { children: ((_a = parent.children) !== null && _a !== void 0 ? _a : []).filter(child => child.id !== draggedId) }));
38
+ });
39
+ return updateNodeById(rootWithoutDragged, targetParent.id, parent => {
40
+ var _a;
41
+ const children = [...((_a = parent.children) !== null && _a !== void 0 ? _a : [])];
42
+ const targetIndex = children.findIndex(child => child.id === targetId);
43
+ if (targetIndex === -1)
44
+ return parent;
45
+ children.splice(targetIndex, 0, draggedNode);
46
+ return Object.assign(Object.assign({}, parent), { children });
47
+ });
48
+ }
49
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => {
50
+ var _a;
51
+ return (Object.assign(Object.assign({}, parent), { children: ((_a = parent.children) !== null && _a !== void 0 ? _a : []).filter(child => child.id !== draggedId) }));
52
+ });
53
+ return updateNodeById(rootWithoutDragged, targetId, target => {
54
+ var _a;
55
+ return (Object.assign(Object.assign({}, target), { children: [...((_a = target.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
56
+ });
57
+ }
58
+ function duplicateNodeBelow(root, nodeId) {
59
+ const node = findNode(root, nodeId);
60
+ const parent = findParent(root, nodeId);
61
+ if (!node || !parent)
62
+ return null;
63
+ const duplicate = cloneNode(node);
64
+ const nextRoot = updateNodeById(root, parent.id, currentParent => (Object.assign(Object.assign({}, currentParent), { children: (() => {
65
+ var _a;
66
+ const children = [...((_a = currentParent.children) !== null && _a !== void 0 ? _a : [])];
67
+ const index = children.findIndex(child => child.id === nodeId);
68
+ if (index === -1)
69
+ return [...children, duplicate];
70
+ children.splice(index + 1, 0, duplicate);
71
+ return children;
72
+ })() })));
73
+ return { root: nextRoot, duplicatedId: duplicate.id };
74
+ }
16
75
  export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }) {
17
- const [contextMenu, setContextMenu] = useState(null);
76
+ const { onFocusNode } = useEditorContext();
18
77
  const [draggedId, setDraggedId] = useState(null);
78
+ const [dropTarget, setDropTarget] = useState(null);
19
79
  const [collapsedIds, setCollapsedIds] = useState(new Set());
20
80
  const [collapsed, setCollapsed] = useState(false);
21
- const [fileMenuOpen, setFileMenuOpen] = useState(false);
22
81
  const [searchQuery, setSearchQuery] = useState('');
23
82
  if (!prefabData || !setPrefabData)
24
83
  return null;
25
- const handleContextMenu = (e, nodeId) => {
26
- e.preventDefault();
27
- e.stopPropagation();
28
- setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
29
- };
30
84
  const toggleCollapse = (e, id) => {
31
85
  e.stopPropagation();
32
86
  setCollapsedIds(prev => {
@@ -36,35 +90,33 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
36
90
  });
37
91
  };
38
92
  const handleAddChild = (parentId) => {
93
+ var _a;
94
+ const newNode = {
95
+ id: crypto.randomUUID(),
96
+ name: "New Node",
97
+ components: {
98
+ transform: {
99
+ type: "Transform",
100
+ properties: Object.assign({}, (_a = getComponent('Transform')) === null || _a === void 0 ? void 0 : _a.defaultProperties)
101
+ }
102
+ }
103
+ };
39
104
  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
- }] }));
105
+ var _a;
106
+ return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), newNode] }));
51
107
  }) })));
52
- setContextMenu(null);
108
+ setSelectedId(newNode.id);
53
109
  };
54
110
  const handleDuplicate = (nodeId) => {
55
111
  if (nodeId === prefabData.root.id)
56
112
  return;
57
113
  setPrefabData(prev => {
58
- const node = findNode(prev.root, nodeId);
59
- const parent = findParent(prev.root, nodeId);
60
- if (!node || !parent)
114
+ const result = duplicateNodeBelow(prev.root, nodeId);
115
+ if (!result)
61
116
  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
- }) });
117
+ setSelectedId(result.duplicatedId);
118
+ return Object.assign(Object.assign({}, prev), { root: result.root });
66
119
  });
67
- setContextMenu(null);
68
120
  };
69
121
  const handleDelete = (nodeId) => {
70
122
  if (nodeId === prefabData.root.id)
@@ -72,11 +124,9 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
72
124
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: deleteNode(prev.root, nodeId) })));
73
125
  if (selectedId === nodeId)
74
126
  setSelectedId(null);
75
- setContextMenu(null);
76
127
  };
77
128
  const handleToggleDisabled = (nodeId) => {
78
129
  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
130
  };
81
131
  const handleDragStart = (e, id) => {
82
132
  if (id === prefabData.root.id)
@@ -84,31 +134,38 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
84
134
  e.dataTransfer.effectAllowed = "move";
85
135
  setDraggedId(id);
86
136
  };
87
- const handleDragOver = (e, targetId) => {
137
+ const getDropPosition = (e, isRoot) => {
138
+ if (isRoot)
139
+ return 'inside';
140
+ const rect = e.currentTarget.getBoundingClientRect();
141
+ return e.clientY <= rect.top + rect.height * 0.35 ? 'before' : 'inside';
142
+ };
143
+ const handleDragOver = (e, targetId, isRoot) => {
88
144
  if (!draggedId || draggedId === targetId)
89
145
  return;
90
146
  const draggedNode = findNode(prefabData.root, draggedId);
91
147
  if (draggedNode && findNode(draggedNode, targetId))
92
148
  return;
93
149
  e.preventDefault();
150
+ setDropTarget({ id: targetId, position: getDropPosition(e, isRoot) });
151
+ };
152
+ const handleDragLeave = (e, targetId) => {
153
+ const relatedTarget = e.relatedTarget;
154
+ if (relatedTarget instanceof Node && e.currentTarget.contains(relatedTarget))
155
+ return;
156
+ setDropTarget(current => (current === null || current === void 0 ? void 0 : current.id) === targetId ? null : current);
94
157
  };
95
- const handleDrop = (e, targetId) => {
158
+ const handleDrop = (e, targetId, isRoot) => {
96
159
  if (!draggedId || draggedId === targetId)
97
160
  return;
98
161
  e.preventDefault();
162
+ const dropPosition = getDropPosition(e, isRoot);
99
163
  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 });
164
+ const root = moveNode(prev.root, draggedId, targetId, dropPosition);
165
+ return root === prev.root ? prev : Object.assign(Object.assign({}, prev), { root });
110
166
  });
111
167
  setDraggedId(null);
168
+ setDropTarget(null);
112
169
  };
113
170
  const matchesSearch = (node, query) => {
114
171
  var _a, _b, _c;
@@ -130,39 +187,51 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
130
187
  const isCollapsed = collapsedIds.has(node.id);
131
188
  const hasChildren = node.children && node.children.length > 0;
132
189
  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: {
190
+ const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === node.id;
191
+ const showDropBefore = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'before';
192
+ const showDropInside = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'inside';
193
+ 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); }, 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
194
  width: 12,
135
195
  opacity: 0.6,
136
196
  marginRight: 4,
137
197
  cursor: 'pointer',
138
198
  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: [_jsx("style", { children: `
153
- .tree-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
154
- .tree-scroll::-webkit-scrollbar-track { background: transparent; }
155
- .tree-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
156
- ` }), _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 rgba(255,255,255,0.1)' }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: {
157
- width: '100%',
158
- padding: '4px 8px',
159
- background: 'rgba(255,255,255,0.05)',
160
- border: '1px solid rgba(255,255,255,0.1)',
161
- borderRadius: 3,
199
+ }, 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("button", { ref: ref, style: {
200
+ background: 'none',
201
+ border: 'none',
202
+ cursor: 'pointer',
203
+ padding: '0 4px',
204
+ fontSize: 14,
205
+ opacity: 0.7,
206
+ color: 'inherit',
207
+ }, onClick: (e) => {
208
+ e.stopPropagation();
209
+ toggle();
210
+ }, title: "Node Actions", children: "\u22EF" })), children: (close) => (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { handleAddChild(node.id); close(); }, children: "Add Child" }), _jsx("button", { style: menu.item, onClick: () => { setSelectedId(node.id); onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(node.id); close(); }, children: "Focus Camera" }), _jsx("button", { style: menu.item, onClick: () => { handleDuplicate(node.id); close(); }, children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => { handleDelete(node.id); close(); }, children: "Delete" })] })) }), _jsx("button", { style: {
211
+ background: 'none',
212
+ border: 'none',
213
+ cursor: 'pointer',
214
+ padding: '0 4px',
215
+ fontSize: 14,
216
+ opacity: node.disabled ? 0.5 : 0.7,
162
217
  color: 'inherit',
163
- fontSize: 11,
164
- outline: 'none',
165
- } }) }), _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" })] }))] }))] }));
218
+ }, onClick: (e) => {
219
+ e.stopPropagation();
220
+ handleToggleDisabled(node.id);
221
+ }, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' })] })), isRoot && (_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx("button", { ref: ref, style: {
222
+ background: 'none',
223
+ border: 'none',
224
+ cursor: 'pointer',
225
+ padding: '0 4px',
226
+ fontSize: 14,
227
+ opacity: 0.7,
228
+ color: 'inherit',
229
+ }, onClick: (e) => {
230
+ e.stopPropagation();
231
+ toggle();
232
+ }, title: "Scene Actions", children: "\u22EF" })), children: (close) => (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { handleAddChild(node.id); close(); }, children: "Add Child" }), _jsx("button", { style: menu.item, onClick: () => { setSelectedId(node.id); onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(node.id); close(); }, children: "Focus Camera" })] })) }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
233
+ };
234
+ return (_jsx(_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("button", { ref: ref, style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); toggle(); }, title: "File", 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) })] }))] }) }));
166
235
  }
167
236
  function FileMenu({ prefabData, setPrefabData, onClose }) {
168
237
  const { onScreenshot, onExportGLB } = useEditorContext();
@@ -187,5 +256,5 @@ function FileMenu({ prefabData, setPrefabData, onClose }) {
187
256
  }) })));
188
257
  onClose();
189
258
  });
190
- return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { 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" })] }));
259
+ return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), 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" })] }));
191
260
  }
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  import { useState, useEffect } from 'react';
14
14
  import EditorTree from './EditorTree';
15
15
  import { getAllComponents } from './components/ComponentRegistry';
16
- import { base, inspector } from './styles';
16
+ import { base, colors, inspector, scrollbarCSS, componentCard } from './styles';
17
17
  import { findNode, updateNode, deleteNode } from './utils';
18
18
  function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePath, onUndo, onRedo, canUndo, canRedo }) {
19
19
  const [collapsed, setCollapsed] = useState(false);
@@ -29,12 +29,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePa
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: `
33
- .prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
34
- .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
35
- .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
36
- .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
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, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
32
+ return _jsxs(_Fragment, { children: [_jsx("style", { children: scrollbarCSS }), _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, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
38
33
  }
39
34
  function NodeInspector({ node, updateNode, deleteNode, basePath }) {
40
35
  var _a;
@@ -47,13 +42,13 @@ function NodeInspector({ node, updateNode, deleteNode, basePath }) {
47
42
  if (!newAvailable.includes(addType))
48
43
  setAddType(newAvailable[0] || "");
49
44
  }, [Object.keys(node.components || {}).join(',')]);
50
- 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: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }, 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]) => {
51
46
  if (!comp)
52
47
  return null;
53
48
  const def = ALL_COMPONENTS[comp.type];
54
49
  if (!def)
55
- return _jsxs("div", { style: { color: '#ff8888', fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
56
- return (_jsxs("div", { style: { marginBottom: 8, backgroundColor: 'rgba(255, 255, 255, 0.1)', padding: 8, borderRadius: 4 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
50
+ return _jsxs("div", { style: { color: colors.danger, fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
51
+ return (_jsxs("div", { style: componentCard.container, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
57
52
  const _a = n.components || {}, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
58
53
  return Object.assign(Object.assign({}, n), { components: rest });
59
54
  }), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath }))] }, key));
@@ -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;
@@ -6,7 +6,8 @@ import { Physics } from "@react-three/rapier";
6
6
  import EditorUI from "./EditorUI";
7
7
  import { base, toolbar } from "./styles";
8
8
  import { EditorContext } from "./EditorContext";
9
- import { exportGLB } from "./utils";
9
+ import { exportGLB, createModelNode, createImageNode } from "./utils";
10
+ import { parseModelFromFile } from "../dragdrop/modelLoader";
10
11
  const DEFAULT_PREFAB = {
11
12
  id: "prefab-default",
12
13
  name: "New Prefab",
@@ -20,12 +21,14 @@ const DEFAULT_PREFAB = {
20
21
  }
21
22
  }
22
23
  };
23
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, children }, ref) => {
24
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, uiPlugins, children }, ref) => {
24
25
  const [editMode, setEditMode] = useState(true);
25
26
  const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
26
27
  const [selectedId, setSelectedId] = useState(null);
27
28
  const [transformMode, setTransformMode] = useState("translate");
28
29
  const [snapResolution, setSnapResolution] = useState(0);
30
+ const [positionSnap, setPositionSnap] = useState(0.5);
31
+ const [rotationSnap, setRotationSnap] = useState(Math.PI / 4);
29
32
  const [history, setHistory] = useState([loadedPrefab]);
30
33
  const [historyIndex, setHistoryIndex] = useState(0);
31
34
  const throttleRef = useRef(null);
@@ -106,11 +109,70 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
106
109
  filename: `${loadedPrefab.name || 'scene'}.glb`
107
110
  });
108
111
  };
112
+ const handleFocusNode = (nodeId) => {
113
+ var _a;
114
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.focusNode(nodeId);
115
+ };
109
116
  useEffect(() => {
110
117
  const canvas = document.querySelector('canvas');
111
118
  if (canvas)
112
119
  canvasRef.current = canvas;
113
120
  }, []);
121
+ // --- Drag & drop files to add nodes ---
122
+ useEffect(() => {
123
+ const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
124
+ const MODEL_EXTS = ['glb', 'gltf', 'fbx'];
125
+ function handleDragOver(e) {
126
+ e.preventDefault();
127
+ e.stopPropagation();
128
+ }
129
+ function handleDrop(e) {
130
+ var _a;
131
+ e.preventDefault();
132
+ e.stopPropagation();
133
+ const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
134
+ files.forEach(file => {
135
+ var _a, _b;
136
+ const ext = (_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
137
+ if (!ext)
138
+ return;
139
+ const baseName = file.name.replace(/\.[^.]+$/, '');
140
+ if (MODEL_EXTS.includes(ext)) {
141
+ const modelPath = `models/${file.name}`;
142
+ const newNode = createModelNode(modelPath, baseName);
143
+ updatePrefab(prev => {
144
+ var _a;
145
+ return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
146
+ });
147
+ parseModelFromFile(file).then(result => {
148
+ var _a;
149
+ if (result.success && result.model) {
150
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, result.model);
151
+ }
152
+ else {
153
+ console.error('Drop parse error:', result.error);
154
+ }
155
+ });
156
+ }
157
+ else if (IMAGE_EXTS.includes(ext)) {
158
+ const texturePath = `textures/${file.name}`;
159
+ const newNode = createImageNode(texturePath, baseName);
160
+ updatePrefab(prev => {
161
+ var _a;
162
+ return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
163
+ });
164
+ // Inject a blob URL texture so it renders immediately
165
+ (_b = prefabRootRef.current) === null || _b === void 0 ? void 0 : _b.injectTexture(texturePath, file);
166
+ }
167
+ });
168
+ }
169
+ window.addEventListener('dragover', handleDragOver);
170
+ window.addEventListener('drop', handleDrop);
171
+ return () => {
172
+ window.removeEventListener('dragover', handleDragOver);
173
+ window.removeEventListener('drop', handleDrop);
174
+ };
175
+ }, [loadedPrefab]);
114
176
  useImperativeHandle(ref, () => ({
115
177
  screenshot: handleScreenshot,
116
178
  exportGLB: handleExportGLB,
@@ -124,9 +186,14 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
124
186
  setTransformMode,
125
187
  snapResolution,
126
188
  setSnapResolution,
189
+ positionSnap,
190
+ setPositionSnap,
191
+ rotationSnap,
192
+ setRotationSnap,
193
+ onFocusNode: handleFocusNode,
127
194
  onScreenshot: handleScreenshot,
128
195
  onExportGLB: handleExportGLB
129
- }, 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 })] });
130
197
  });
131
198
  PrefabEditor.displayName = "PrefabEditor";
132
199
  export default PrefabEditor;
@@ -4,6 +4,9 @@ import { Prefab, GameObject as GameObjectType } from "./types";
4
4
  export interface PrefabRootRef {
5
5
  root: Group | null;
6
6
  rigidBodyRefs: Map<string, any>;
7
+ injectModel: (filename: string, model: Object3D) => void;
8
+ injectTexture: (filename: string, file: File) => void;
9
+ focusNode: (nodeId: string) => void;
7
10
  }
8
11
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
9
12
  editMode?: boolean;