react-three-game 0.0.15 → 0.0.16
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.
- package/README.md +49 -63
- package/dist/tools/dragdrop/DragDropLoader.js +1 -0
- package/dist/tools/prefabeditor/EditorTree.js +116 -2
- package/dist/tools/prefabeditor/EditorUI.js +160 -6
- package/dist/tools/prefabeditor/PrefabEditor.js +56 -2
- package/dist/tools/prefabeditor/types.d.ts +2 -2
- package/dist/tools/prefabeditor/types.js +1 -0
- package/package.json +1 -1
- package/src/tools/dragdrop/DragDropLoader.tsx +1 -0
- package/src/tools/prefabeditor/EditorTree.tsx +154 -14
- package/src/tools/prefabeditor/EditorUI.tsx +198 -51
- package/src/tools/prefabeditor/PrefabEditor.tsx +67 -5
- package/src/tools/prefabeditor/types.ts +3 -3
package/README.md
CHANGED
|
@@ -17,45 +17,26 @@ npm i react-three-game @react-three/fiber three
|
|
|
17
17
|
Scenes are JSON prefabs. Components are registered modules. Hierarchy is declarative.
|
|
18
18
|
|
|
19
19
|
```jsx
|
|
20
|
-
<PrefabRoot data={{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}} />
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Tailwind CSS Support
|
|
34
|
-
|
|
35
|
-
This library uses Tailwind CSS for styling its editor components. To ensure styles are correctly applied in your application, you need to configure Tailwind to scan the library's source files.
|
|
36
|
-
|
|
37
|
-
### Tailwind v4
|
|
38
|
-
|
|
39
|
-
Add the library path to your CSS entry point using the `@source` directive:
|
|
40
|
-
|
|
41
|
-
```css
|
|
42
|
-
@import "tailwindcss";
|
|
43
|
-
@source "../../node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}";
|
|
20
|
+
<PrefabRoot data={{
|
|
21
|
+
root: {
|
|
22
|
+
id: "cube",
|
|
23
|
+
components: {
|
|
24
|
+
transform: { type: "Transform", properties: { position: [0, 1, 0] } },
|
|
25
|
+
geometry: { type: "Geometry", properties: { geometryType: "box" } },
|
|
26
|
+
material: { type: "Material", properties: { color: "green" } },
|
|
27
|
+
physics: { type: "Physics", properties: { type: "dynamic" } }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}} />
|
|
44
31
|
```
|
|
45
32
|
|
|
46
|
-
|
|
33
|
+
## Styling
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
The prefab editor UI ships with **inline styles** (no Tailwind / CSS framework required). That means you can install and render it without any additional build-time CSS configuration.
|
|
49
36
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// ...
|
|
54
|
-
"./node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}",
|
|
55
|
-
],
|
|
56
|
-
// ...
|
|
57
|
-
}
|
|
58
|
-
```
|
|
37
|
+
If you want to fully restyle the editor, you can:
|
|
38
|
+
- Wrap `PrefabEditor` in your own layout and override positioning.
|
|
39
|
+
- Fork/compose the editor UI components (they’re plain React components).
|
|
59
40
|
|
|
60
41
|
## Quick Start
|
|
61
42
|
|
|
@@ -64,39 +45,44 @@ npm install react-three-game @react-three/fiber @react-three/rapier three
|
|
|
64
45
|
```
|
|
65
46
|
|
|
66
47
|
```jsx
|
|
67
|
-
import {
|
|
48
|
+
import { Physics } from '@react-three/rapier';
|
|
49
|
+
import { GameCanvas, PrefabRoot } from 'react-three-game';
|
|
68
50
|
|
|
69
51
|
export default function App() {
|
|
70
52
|
return (
|
|
71
53
|
<GameCanvas>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
54
|
+
<Physics>
|
|
55
|
+
<ambientLight intensity={0.8} />
|
|
56
|
+
<PrefabRoot
|
|
57
|
+
data={{
|
|
58
|
+
id: "scene",
|
|
59
|
+
name: "scene",
|
|
60
|
+
root: {
|
|
61
|
+
id: "root",
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
id: "ground",
|
|
65
|
+
components: {
|
|
66
|
+
transform: { type: "Transform", properties: { position: [0, 0, 0], rotation: [-1.57, 0, 0] } },
|
|
67
|
+
geometry: { type: "Geometry", properties: { geometryType: "plane", args: [50, 50] } },
|
|
68
|
+
material: { type: "Material", properties: { color: "green" } },
|
|
69
|
+
physics: { type: "Physics", properties: { type: "fixed" } }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "player",
|
|
74
|
+
components: {
|
|
75
|
+
transform: { type: "Transform", properties: { position: [0, 2, 0] } },
|
|
76
|
+
geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
|
|
77
|
+
material: { type: "Material", properties: { color: "#ff6b6b" } },
|
|
78
|
+
physics: { type: "Physics", properties: { type: "dynamic" } }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
95
82
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}} />
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</Physics>
|
|
100
86
|
</GameCanvas>
|
|
101
87
|
);
|
|
102
88
|
}
|
|
@@ -6,6 +6,94 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
6
6
|
const [draggedId, setDraggedId] = useState(null);
|
|
7
7
|
const [collapsedIds, setCollapsedIds] = useState(new Set());
|
|
8
8
|
const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);
|
|
9
|
+
const styles = {
|
|
10
|
+
panel: {
|
|
11
|
+
background: "rgba(0,0,0,0.55)",
|
|
12
|
+
color: "rgba(255,255,255,0.9)",
|
|
13
|
+
border: "1px solid rgba(255,255,255,0.12)",
|
|
14
|
+
borderRadius: 6,
|
|
15
|
+
overflow: "hidden",
|
|
16
|
+
maxHeight: "85vh",
|
|
17
|
+
display: "flex",
|
|
18
|
+
flexDirection: "column",
|
|
19
|
+
backdropFilter: "blur(6px)",
|
|
20
|
+
WebkitBackdropFilter: "blur(6px)",
|
|
21
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
22
|
+
fontSize: 11,
|
|
23
|
+
lineHeight: 1.2,
|
|
24
|
+
userSelect: "none",
|
|
25
|
+
WebkitUserSelect: "none",
|
|
26
|
+
},
|
|
27
|
+
panelHeader: {
|
|
28
|
+
padding: "4px 6px",
|
|
29
|
+
borderBottom: "1px solid rgba(255,255,255,0.10)",
|
|
30
|
+
display: "flex",
|
|
31
|
+
gap: 8,
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "space-between",
|
|
34
|
+
cursor: "pointer",
|
|
35
|
+
background: "rgba(255,255,255,0.05)",
|
|
36
|
+
textTransform: "uppercase",
|
|
37
|
+
letterSpacing: "0.08em",
|
|
38
|
+
fontSize: 10,
|
|
39
|
+
color: "rgba(255,255,255,0.7)",
|
|
40
|
+
},
|
|
41
|
+
scroll: {
|
|
42
|
+
overflowY: "auto",
|
|
43
|
+
},
|
|
44
|
+
row: {
|
|
45
|
+
display: "flex",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
padding: "2px 6px",
|
|
48
|
+
borderBottom: "1px solid rgba(255,255,255,0.07)",
|
|
49
|
+
cursor: "pointer",
|
|
50
|
+
whiteSpace: "nowrap",
|
|
51
|
+
},
|
|
52
|
+
rowSelected: {
|
|
53
|
+
background: "rgba(255,255,255,0.10)",
|
|
54
|
+
},
|
|
55
|
+
chevron: {
|
|
56
|
+
width: 12,
|
|
57
|
+
textAlign: "center",
|
|
58
|
+
opacity: 0.55,
|
|
59
|
+
fontSize: 10,
|
|
60
|
+
marginRight: 4,
|
|
61
|
+
cursor: "pointer",
|
|
62
|
+
},
|
|
63
|
+
idText: {
|
|
64
|
+
fontSize: 11,
|
|
65
|
+
overflow: "hidden",
|
|
66
|
+
textOverflow: "ellipsis",
|
|
67
|
+
},
|
|
68
|
+
contextMenu: {
|
|
69
|
+
position: "fixed",
|
|
70
|
+
zIndex: 50,
|
|
71
|
+
minWidth: 120,
|
|
72
|
+
background: "rgba(0,0,0,0.82)",
|
|
73
|
+
border: "1px solid rgba(255,255,255,0.16)",
|
|
74
|
+
borderRadius: 6,
|
|
75
|
+
overflow: "hidden",
|
|
76
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
|
|
77
|
+
backdropFilter: "blur(6px)",
|
|
78
|
+
WebkitBackdropFilter: "blur(6px)",
|
|
79
|
+
},
|
|
80
|
+
menuItem: {
|
|
81
|
+
width: "100%",
|
|
82
|
+
textAlign: "left",
|
|
83
|
+
padding: "6px 8px",
|
|
84
|
+
background: "transparent",
|
|
85
|
+
border: "none",
|
|
86
|
+
color: "rgba(255,255,255,0.9)",
|
|
87
|
+
font: "inherit",
|
|
88
|
+
cursor: "pointer",
|
|
89
|
+
},
|
|
90
|
+
menuItemDanger: {
|
|
91
|
+
color: "rgba(255,120,120,0.95)",
|
|
92
|
+
},
|
|
93
|
+
menuDivider: {
|
|
94
|
+
borderTop: "1px solid rgba(255,255,255,0.10)",
|
|
95
|
+
}
|
|
96
|
+
};
|
|
9
97
|
if (!prefabData || !setPrefabData)
|
|
10
98
|
return null;
|
|
11
99
|
const handleContextMenu = (e, nodeId) => {
|
|
@@ -132,9 +220,35 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
132
220
|
const isSelected = node.id === selectedId;
|
|
133
221
|
const isCollapsed = collapsedIds.has(node.id);
|
|
134
222
|
const hasChildren = node.children && node.children.length > 0;
|
|
135
|
-
return (_jsxs("div", {
|
|
223
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, styles.row), (isSelected ? styles.rowSelected : null)), { paddingLeft: `${depth * 10 + 6}px` }), onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), draggable: node.id !== prefabData.root.id, onDragStart: (e) => handleDragStart(e, node.id), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), onPointerEnter: (e) => {
|
|
224
|
+
if (!isSelected)
|
|
225
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.06)";
|
|
226
|
+
}, onPointerLeave: (e) => {
|
|
227
|
+
if (!isSelected)
|
|
228
|
+
e.currentTarget.style.background = "transparent";
|
|
229
|
+
}, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.chevron), { visibility: hasChildren ? 'visible' : 'hidden' }), onClick: (e) => hasChildren && toggleCollapse(e, node.id), onPointerEnter: (e) => {
|
|
230
|
+
e.currentTarget.style.opacity = "0.9";
|
|
231
|
+
}, onPointerLeave: (e) => {
|
|
232
|
+
e.currentTarget.style.opacity = "0.55";
|
|
233
|
+
}, children: isCollapsed ? '▶' : '▼' }), _jsx("span", { style: styles.idText, children: node.id })] }), !isCollapsed && node.children && (_jsx("div", { children: node.children.map(child => renderNode(child, depth + 1)) }))] }, node.id));
|
|
136
234
|
};
|
|
137
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", {
|
|
235
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, styles.panel), { width: isTreeCollapsed ? 'auto' : '14rem' }), onClick: closeContextMenu, children: [_jsxs("div", { style: styles.panelHeader, onClick: (e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }, onPointerEnter: (e) => {
|
|
236
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
237
|
+
}, onPointerLeave: (e) => {
|
|
238
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.05)";
|
|
239
|
+
}, children: [_jsx("span", { children: "Prefab Graph" }), _jsx("span", { style: { fontSize: 10, opacity: 0.8 }, children: isTreeCollapsed ? '▶' : '◀' })] }), !isTreeCollapsed && (_jsx("div", { style: Object.assign(Object.assign({}, styles.scroll), { padding: 2 }), children: renderNode(prefabData.root) }))] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, styles.contextMenu), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: closeContextMenu, children: [_jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuDivider), onClick: () => handleAddChild(contextMenu.nodeId), onPointerEnter: (e) => {
|
|
240
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
241
|
+
}, onPointerLeave: (e) => {
|
|
242
|
+
e.currentTarget.style.background = "transparent";
|
|
243
|
+
}, children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuDivider), onClick: () => handleDuplicate(contextMenu.nodeId), onPointerEnter: (e) => {
|
|
244
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
245
|
+
}, onPointerLeave: (e) => {
|
|
246
|
+
e.currentTarget.style.background = "transparent";
|
|
247
|
+
}, children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuItemDanger), onClick: () => handleDelete(contextMenu.nodeId), onPointerEnter: (e) => {
|
|
248
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
249
|
+
}, onPointerLeave: (e) => {
|
|
250
|
+
e.currentTarget.style.background = "transparent";
|
|
251
|
+
}, children: "Delete" })] }))] }))] }));
|
|
138
252
|
}
|
|
139
253
|
// --- Helpers ---
|
|
140
254
|
function findNode(root, id) {
|
|
@@ -4,6 +4,46 @@ import EditorTree from './EditorTree';
|
|
|
4
4
|
import { getAllComponents } from './components/ComponentRegistry';
|
|
5
5
|
function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }) {
|
|
6
6
|
const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
|
|
7
|
+
const ui = {
|
|
8
|
+
panel: {
|
|
9
|
+
position: 'absolute',
|
|
10
|
+
top: 8,
|
|
11
|
+
right: 8,
|
|
12
|
+
zIndex: 20,
|
|
13
|
+
width: 260,
|
|
14
|
+
background: 'rgba(0,0,0,0.55)',
|
|
15
|
+
color: 'rgba(255,255,255,0.9)',
|
|
16
|
+
border: '1px solid rgba(255,255,255,0.12)',
|
|
17
|
+
borderRadius: 6,
|
|
18
|
+
overflow: 'hidden',
|
|
19
|
+
backdropFilter: 'blur(6px)',
|
|
20
|
+
WebkitBackdropFilter: 'blur(6px)',
|
|
21
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
22
|
+
fontSize: 11,
|
|
23
|
+
lineHeight: 1.2,
|
|
24
|
+
},
|
|
25
|
+
header: {
|
|
26
|
+
padding: '4px 6px',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'space-between',
|
|
30
|
+
cursor: 'pointer',
|
|
31
|
+
background: 'rgba(255,255,255,0.05)',
|
|
32
|
+
borderBottom: '1px solid rgba(255,255,255,0.10)',
|
|
33
|
+
textTransform: 'uppercase',
|
|
34
|
+
letterSpacing: '0.08em',
|
|
35
|
+
fontSize: 10,
|
|
36
|
+
color: 'rgba(255,255,255,0.7)',
|
|
37
|
+
userSelect: 'none',
|
|
38
|
+
WebkitUserSelect: 'none',
|
|
39
|
+
},
|
|
40
|
+
left: {
|
|
41
|
+
position: 'absolute',
|
|
42
|
+
top: 8,
|
|
43
|
+
left: 8,
|
|
44
|
+
zIndex: 20,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
7
47
|
const updateNode = (updater) => {
|
|
8
48
|
if (!prefabData || !setPrefabData || !selectedId)
|
|
9
49
|
return;
|
|
@@ -24,12 +64,112 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
24
64
|
};
|
|
25
65
|
const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
|
|
26
66
|
// if (!selectedNode) return null;
|
|
27
|
-
return _jsxs(_Fragment, { children: [_jsxs("div", { style:
|
|
67
|
+
return _jsxs(_Fragment, { children: [_jsxs("div", { style: ui.panel, children: [_jsxs("div", { style: ui.header, onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), onPointerEnter: (e) => {
|
|
68
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
69
|
+
}, onPointerLeave: (e) => {
|
|
70
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.05)';
|
|
71
|
+
}, children: [_jsx("span", { children: "Inspector" }), _jsx("span", { style: { fontSize: 10, opacity: 0.8 }, children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: ui.left, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
|
|
28
72
|
}
|
|
29
73
|
function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
|
|
30
74
|
const ALL_COMPONENTS = getAllComponents();
|
|
31
75
|
const allComponentKeys = Object.keys(ALL_COMPONENTS);
|
|
32
76
|
const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
|
|
77
|
+
const s = {
|
|
78
|
+
root: {
|
|
79
|
+
display: 'flex',
|
|
80
|
+
flexDirection: 'column',
|
|
81
|
+
gap: 6,
|
|
82
|
+
padding: 6,
|
|
83
|
+
maxHeight: '80vh',
|
|
84
|
+
overflowY: 'auto',
|
|
85
|
+
},
|
|
86
|
+
section: {
|
|
87
|
+
paddingBottom: 6,
|
|
88
|
+
borderBottom: '1px solid rgba(255,255,255,0.10)',
|
|
89
|
+
},
|
|
90
|
+
label: {
|
|
91
|
+
display: 'block',
|
|
92
|
+
fontSize: 10,
|
|
93
|
+
opacity: 0.7,
|
|
94
|
+
textTransform: 'uppercase',
|
|
95
|
+
letterSpacing: '0.08em',
|
|
96
|
+
marginBottom: 4,
|
|
97
|
+
},
|
|
98
|
+
input: {
|
|
99
|
+
width: '100%',
|
|
100
|
+
background: 'rgba(255,255,255,0.06)',
|
|
101
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
102
|
+
borderRadius: 4,
|
|
103
|
+
padding: '4px 6px',
|
|
104
|
+
color: 'rgba(255,255,255,0.92)',
|
|
105
|
+
font: 'inherit',
|
|
106
|
+
outline: 'none',
|
|
107
|
+
},
|
|
108
|
+
row: {
|
|
109
|
+
display: 'flex',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
justifyContent: 'space-between',
|
|
112
|
+
gap: 8,
|
|
113
|
+
},
|
|
114
|
+
button: {
|
|
115
|
+
padding: '2px 6px',
|
|
116
|
+
background: 'transparent',
|
|
117
|
+
color: 'rgba(255,255,255,0.9)',
|
|
118
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
119
|
+
borderRadius: 4,
|
|
120
|
+
cursor: 'pointer',
|
|
121
|
+
font: 'inherit',
|
|
122
|
+
},
|
|
123
|
+
buttonActive: {
|
|
124
|
+
background: 'rgba(255,255,255,0.10)',
|
|
125
|
+
},
|
|
126
|
+
smallDanger: {
|
|
127
|
+
background: 'transparent',
|
|
128
|
+
border: 'none',
|
|
129
|
+
cursor: 'pointer',
|
|
130
|
+
color: 'rgba(255,120,120,0.95)',
|
|
131
|
+
font: 'inherit',
|
|
132
|
+
padding: '2px 4px',
|
|
133
|
+
},
|
|
134
|
+
componentHeader: {
|
|
135
|
+
display: 'flex',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
justifyContent: 'space-between',
|
|
138
|
+
padding: '4px 0',
|
|
139
|
+
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
|
140
|
+
marginBottom: 4,
|
|
141
|
+
},
|
|
142
|
+
componentTitle: {
|
|
143
|
+
fontSize: 10,
|
|
144
|
+
textTransform: 'uppercase',
|
|
145
|
+
letterSpacing: '0.08em',
|
|
146
|
+
opacity: 0.8,
|
|
147
|
+
},
|
|
148
|
+
select: {
|
|
149
|
+
flex: 1,
|
|
150
|
+
background: 'rgba(255,255,255,0.06)',
|
|
151
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
152
|
+
borderRadius: 4,
|
|
153
|
+
padding: '4px 6px',
|
|
154
|
+
color: 'rgba(255,255,255,0.92)',
|
|
155
|
+
font: 'inherit',
|
|
156
|
+
outline: 'none',
|
|
157
|
+
},
|
|
158
|
+
addButton: {
|
|
159
|
+
width: 28,
|
|
160
|
+
padding: '4px 0',
|
|
161
|
+
background: 'rgba(255,255,255,0.08)',
|
|
162
|
+
color: 'rgba(255,255,255,0.92)',
|
|
163
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
164
|
+
borderRadius: 4,
|
|
165
|
+
cursor: 'pointer',
|
|
166
|
+
font: 'inherit',
|
|
167
|
+
},
|
|
168
|
+
disabled: {
|
|
169
|
+
opacity: 0.35,
|
|
170
|
+
cursor: 'not-allowed',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
33
173
|
const componentKeys = Object.keys(node.components || {}).join(',');
|
|
34
174
|
useEffect(() => {
|
|
35
175
|
// Components stored on nodes use lowercase keys (e.g. 'geometry'),
|
|
@@ -39,19 +179,25 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
39
179
|
setAddComponentType(available[0] || "");
|
|
40
180
|
}
|
|
41
181
|
}, [componentKeys, addComponentType, node.components, allComponentKeys]);
|
|
42
|
-
return _jsxs("div", {
|
|
182
|
+
return _jsxs("div", { style: s.root, children: [_jsx("div", { style: s.section, children: _jsx("input", { style: s.input, value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) }) }), _jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, s.row), s.section), { paddingBottom: 6 }), children: [_jsx("label", { style: Object.assign(Object.assign({}, s.label), { marginBottom: 0 }), children: "Components" }), _jsx("button", { onClick: deleteNode, style: s.smallDanger, title: "Delete node", children: "\u2715" })] }), _jsxs("div", { style: s.section, children: [_jsx("label", { style: s.label, children: "Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign(Object.assign({}, s.button), { flex: 1 }), (transformMode === mode ? s.buttonActive : null)), onPointerEnter: (e) => {
|
|
183
|
+
if (transformMode !== mode)
|
|
184
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
185
|
+
}, onPointerLeave: (e) => {
|
|
186
|
+
if (transformMode !== mode)
|
|
187
|
+
e.currentTarget.style.background = 'transparent';
|
|
188
|
+
}, children: mode[0].toUpperCase() }, mode))) })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
43
189
|
if (!comp)
|
|
44
190
|
return null;
|
|
45
191
|
const componentDef = ALL_COMPONENTS[comp.type];
|
|
46
192
|
if (!componentDef)
|
|
47
|
-
return _jsxs("div", {
|
|
193
|
+
return _jsxs("div", { style: { padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }, children: ["Unknown component type: ", comp.type, _jsx("textarea", { defaultValue: JSON.stringify(comp) })] }, key);
|
|
48
194
|
const EditorComp = componentDef.Editor;
|
|
49
|
-
return (_jsxs("div", {
|
|
195
|
+
return (_jsxs("div", { style: { padding: '0 2px' }, children: [_jsxs("div", { style: s.componentHeader, children: [_jsx("span", { style: s.componentTitle, children: key }), _jsx("button", { onClick: () => updateNode(n => {
|
|
50
196
|
const components = Object.assign({}, n.components);
|
|
51
197
|
delete components[key];
|
|
52
198
|
return Object.assign(Object.assign({}, n), { components });
|
|
53
|
-
}),
|
|
54
|
-
}), _jsxs("div", {
|
|
199
|
+
}), style: s.smallDanger, title: "Remove component", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, 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 })) : null] }, key));
|
|
200
|
+
}), _jsxs("div", { style: Object.assign(Object.assign({}, s.section), { borderBottom: 'none', paddingBottom: 0 }), children: [_jsx("label", { style: s.label, children: "Add Component" }), _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx("select", { style: s.select, value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { style: Object.assign(Object.assign({}, s.addButton), (!addComponentType ? s.disabled : null)), disabled: !addComponentType, onClick: () => {
|
|
55
201
|
var _a;
|
|
56
202
|
if (!addComponentType)
|
|
57
203
|
return;
|
|
@@ -60,6 +206,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
60
206
|
const key = addComponentType.toLowerCase();
|
|
61
207
|
updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: { type: def.name, properties: def.defaultProperties } }) })));
|
|
62
208
|
}
|
|
209
|
+
}, onPointerEnter: (e) => {
|
|
210
|
+
if (!addComponentType)
|
|
211
|
+
return;
|
|
212
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.12)';
|
|
213
|
+
}, onPointerLeave: (e) => {
|
|
214
|
+
if (!addComponentType)
|
|
215
|
+
return;
|
|
216
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
63
217
|
}, children: "+" })] })] })] });
|
|
64
218
|
}
|
|
65
219
|
function findNode(root, id) {
|
|
@@ -50,11 +50,65 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
|
|
|
50
50
|
};
|
|
51
51
|
return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, ref: prefabRef,
|
|
52
52
|
// props for edit mode
|
|
53
|
-
editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: {
|
|
53
|
+
editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: {
|
|
54
|
+
position: "absolute",
|
|
55
|
+
top: 8,
|
|
56
|
+
left: "50%",
|
|
57
|
+
transform: "translateX(-50%)",
|
|
58
|
+
display: "flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
gap: 6,
|
|
61
|
+
padding: "2px 4px",
|
|
62
|
+
background: "rgba(0,0,0,0.55)",
|
|
63
|
+
border: "1px solid rgba(255,255,255,0.12)",
|
|
64
|
+
borderRadius: 4,
|
|
65
|
+
color: "rgba(255,255,255,0.9)",
|
|
66
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
67
|
+
fontSize: 11,
|
|
68
|
+
lineHeight: 1,
|
|
69
|
+
WebkitUserSelect: "none",
|
|
70
|
+
userSelect: "none",
|
|
71
|
+
}, children: [_jsx("button", { style: {
|
|
72
|
+
padding: "2px 6px",
|
|
73
|
+
font: "inherit",
|
|
74
|
+
background: "transparent",
|
|
75
|
+
color: "inherit",
|
|
76
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
77
|
+
borderRadius: 3,
|
|
78
|
+
cursor: "pointer",
|
|
79
|
+
}, onClick: () => setEditMode(!editMode), onPointerEnter: (e) => {
|
|
80
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
81
|
+
}, onPointerLeave: (e) => {
|
|
82
|
+
e.currentTarget.style.background = "transparent";
|
|
83
|
+
}, children: editMode ? "▶" : "⏸" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx("button", { style: {
|
|
84
|
+
padding: "2px 6px",
|
|
85
|
+
font: "inherit",
|
|
86
|
+
background: "transparent",
|
|
87
|
+
color: "inherit",
|
|
88
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
89
|
+
borderRadius: 3,
|
|
90
|
+
cursor: "pointer",
|
|
91
|
+
}, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
92
|
const prefab = yield loadJson();
|
|
55
93
|
if (prefab)
|
|
56
94
|
setLoadedPrefab(prefab);
|
|
57
|
-
}),
|
|
95
|
+
}), onPointerEnter: (e) => {
|
|
96
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
97
|
+
}, onPointerLeave: (e) => {
|
|
98
|
+
e.currentTarget.style.background = "transparent";
|
|
99
|
+
}, children: "\uD83D\uDCE5" }), _jsx("button", { style: {
|
|
100
|
+
padding: "2px 6px",
|
|
101
|
+
font: "inherit",
|
|
102
|
+
background: "transparent",
|
|
103
|
+
color: "inherit",
|
|
104
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
105
|
+
borderRadius: 3,
|
|
106
|
+
cursor: "pointer",
|
|
107
|
+
}, onClick: () => saveJson(loadedPrefab, "prefab"), onPointerEnter: (e) => {
|
|
108
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
109
|
+
}, onPointerLeave: (e) => {
|
|
110
|
+
e.currentTarget.style.background = "transparent";
|
|
111
|
+
}, children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
|
|
58
112
|
};
|
|
59
113
|
const saveJson = (data, filename) => {
|
|
60
114
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
package/package.json
CHANGED
|
@@ -15,6 +15,95 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
15
15
|
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
|
|
16
16
|
const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);
|
|
17
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
|
+
contextMenu: {
|
|
78
|
+
position: "fixed",
|
|
79
|
+
zIndex: 50,
|
|
80
|
+
minWidth: 120,
|
|
81
|
+
background: "rgba(0,0,0,0.82)",
|
|
82
|
+
border: "1px solid rgba(255,255,255,0.16)",
|
|
83
|
+
borderRadius: 6,
|
|
84
|
+
overflow: "hidden",
|
|
85
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
|
|
86
|
+
backdropFilter: "blur(6px)",
|
|
87
|
+
WebkitBackdropFilter: "blur(6px)",
|
|
88
|
+
},
|
|
89
|
+
menuItem: {
|
|
90
|
+
width: "100%",
|
|
91
|
+
textAlign: "left",
|
|
92
|
+
padding: "6px 8px",
|
|
93
|
+
background: "transparent",
|
|
94
|
+
border: "none",
|
|
95
|
+
color: "rgba(255,255,255,0.9)",
|
|
96
|
+
font: "inherit",
|
|
97
|
+
cursor: "pointer",
|
|
98
|
+
},
|
|
99
|
+
menuItemDanger: {
|
|
100
|
+
color: "rgba(255,120,120,0.95)",
|
|
101
|
+
},
|
|
102
|
+
menuDivider: {
|
|
103
|
+
borderTop: "1px solid rgba(255,255,255,0.10)",
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
18
107
|
if (!prefabData || !setPrefabData) return null;
|
|
19
108
|
|
|
20
109
|
const handleContextMenu = (e: MouseEvent, nodeId: string) => {
|
|
@@ -152,24 +241,42 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
152
241
|
const hasChildren = node.children && node.children.length > 0;
|
|
153
242
|
|
|
154
243
|
return (
|
|
155
|
-
<div key={node.id}
|
|
244
|
+
<div key={node.id}>
|
|
156
245
|
<div
|
|
157
|
-
|
|
158
|
-
|
|
246
|
+
style={{
|
|
247
|
+
...styles.row,
|
|
248
|
+
...(isSelected ? styles.rowSelected : null),
|
|
249
|
+
paddingLeft: `${depth * 10 + 6}px`,
|
|
250
|
+
}}
|
|
159
251
|
onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
|
|
160
252
|
onContextMenu={(e) => handleContextMenu(e, node.id)}
|
|
161
253
|
draggable={node.id !== prefabData.root.id}
|
|
162
254
|
onDragStart={(e) => handleDragStart(e, node.id)}
|
|
163
255
|
onDragOver={(e) => handleDragOver(e, node.id)}
|
|
164
256
|
onDrop={(e) => handleDrop(e, node.id)}
|
|
257
|
+
onPointerEnter={(e) => {
|
|
258
|
+
if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.06)";
|
|
259
|
+
}}
|
|
260
|
+
onPointerLeave={(e) => {
|
|
261
|
+
if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "transparent";
|
|
262
|
+
}}
|
|
165
263
|
>
|
|
166
264
|
<span
|
|
167
|
-
|
|
265
|
+
style={{
|
|
266
|
+
...styles.chevron,
|
|
267
|
+
visibility: hasChildren ? 'visible' : 'hidden',
|
|
268
|
+
}}
|
|
168
269
|
onClick={(e) => hasChildren && toggleCollapse(e, node.id)}
|
|
270
|
+
onPointerEnter={(e) => {
|
|
271
|
+
(e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
|
|
272
|
+
}}
|
|
273
|
+
onPointerLeave={(e) => {
|
|
274
|
+
(e.currentTarget as HTMLSpanElement).style.opacity = "0.55";
|
|
275
|
+
}}
|
|
169
276
|
>
|
|
170
277
|
{isCollapsed ? '▶' : '▼'}
|
|
171
278
|
</span>
|
|
172
|
-
<span
|
|
279
|
+
<span style={styles.idText}>
|
|
173
280
|
{node.id}
|
|
174
281
|
</span>
|
|
175
282
|
</div>
|
|
@@ -184,16 +291,28 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
184
291
|
|
|
185
292
|
return (
|
|
186
293
|
<>
|
|
187
|
-
<div
|
|
294
|
+
<div
|
|
295
|
+
style={{
|
|
296
|
+
...styles.panel,
|
|
297
|
+
width: isTreeCollapsed ? 'auto' : '14rem',
|
|
298
|
+
}}
|
|
299
|
+
onClick={closeContextMenu}
|
|
300
|
+
>
|
|
188
301
|
<div
|
|
189
|
-
|
|
302
|
+
style={styles.panelHeader}
|
|
190
303
|
onClick={(e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }}
|
|
304
|
+
onPointerEnter={(e) => {
|
|
305
|
+
(e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.08)";
|
|
306
|
+
}}
|
|
307
|
+
onPointerLeave={(e) => {
|
|
308
|
+
(e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.05)";
|
|
309
|
+
}}
|
|
191
310
|
>
|
|
192
311
|
<span>Prefab Graph</span>
|
|
193
|
-
<span
|
|
312
|
+
<span style={{ fontSize: 10, opacity: 0.8 }}>{isTreeCollapsed ? '▶' : '◀'}</span>
|
|
194
313
|
</div>
|
|
195
314
|
{!isTreeCollapsed && (
|
|
196
|
-
<div
|
|
315
|
+
<div style={{ ...styles.scroll, padding: 2 }}>
|
|
197
316
|
{renderNode(prefabData.root)}
|
|
198
317
|
</div>
|
|
199
318
|
)}
|
|
@@ -201,28 +320,49 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
201
320
|
|
|
202
321
|
{contextMenu && (
|
|
203
322
|
<div
|
|
204
|
-
|
|
205
|
-
|
|
323
|
+
style={{
|
|
324
|
+
...styles.contextMenu,
|
|
325
|
+
top: contextMenu.y,
|
|
326
|
+
left: contextMenu.x,
|
|
327
|
+
}}
|
|
206
328
|
onClick={(e) => e.stopPropagation()}
|
|
207
329
|
onPointerLeave={closeContextMenu}
|
|
208
330
|
>
|
|
209
331
|
<button
|
|
210
|
-
|
|
332
|
+
style={{ ...styles.menuItem, ...styles.menuDivider }}
|
|
211
333
|
onClick={() => handleAddChild(contextMenu.nodeId)}
|
|
334
|
+
onPointerEnter={(e) => {
|
|
335
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
336
|
+
}}
|
|
337
|
+
onPointerLeave={(e) => {
|
|
338
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
339
|
+
}}
|
|
212
340
|
>
|
|
213
341
|
Add Child
|
|
214
342
|
</button>
|
|
215
343
|
{contextMenu.nodeId !== prefabData.root.id && (
|
|
216
344
|
<>
|
|
217
345
|
<button
|
|
218
|
-
|
|
346
|
+
style={{ ...styles.menuItem, ...styles.menuDivider }}
|
|
219
347
|
onClick={() => handleDuplicate(contextMenu.nodeId)}
|
|
348
|
+
onPointerEnter={(e) => {
|
|
349
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
350
|
+
}}
|
|
351
|
+
onPointerLeave={(e) => {
|
|
352
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
353
|
+
}}
|
|
220
354
|
>
|
|
221
355
|
Duplicate
|
|
222
356
|
</button>
|
|
223
357
|
<button
|
|
224
|
-
|
|
358
|
+
style={{ ...styles.menuItem, ...styles.menuItemDanger }}
|
|
225
359
|
onClick={() => handleDelete(contextMenu.nodeId)}
|
|
360
|
+
onPointerEnter={(e) => {
|
|
361
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
362
|
+
}}
|
|
363
|
+
onPointerLeave={(e) => {
|
|
364
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
365
|
+
}}
|
|
226
366
|
>
|
|
227
367
|
Delete
|
|
228
368
|
</button>
|
|
@@ -15,6 +15,47 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
15
15
|
}) {
|
|
16
16
|
const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
|
|
17
17
|
|
|
18
|
+
const ui: Record<string, React.CSSProperties> = {
|
|
19
|
+
panel: {
|
|
20
|
+
position: 'absolute',
|
|
21
|
+
top: 8,
|
|
22
|
+
right: 8,
|
|
23
|
+
zIndex: 20,
|
|
24
|
+
width: 260,
|
|
25
|
+
background: 'rgba(0,0,0,0.55)',
|
|
26
|
+
color: 'rgba(255,255,255,0.9)',
|
|
27
|
+
border: '1px solid rgba(255,255,255,0.12)',
|
|
28
|
+
borderRadius: 6,
|
|
29
|
+
overflow: 'hidden',
|
|
30
|
+
backdropFilter: 'blur(6px)',
|
|
31
|
+
WebkitBackdropFilter: 'blur(6px)',
|
|
32
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
33
|
+
fontSize: 11,
|
|
34
|
+
lineHeight: 1.2,
|
|
35
|
+
},
|
|
36
|
+
header: {
|
|
37
|
+
padding: '4px 6px',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'space-between',
|
|
41
|
+
cursor: 'pointer',
|
|
42
|
+
background: 'rgba(255,255,255,0.05)',
|
|
43
|
+
borderBottom: '1px solid rgba(255,255,255,0.10)',
|
|
44
|
+
textTransform: 'uppercase',
|
|
45
|
+
letterSpacing: '0.08em',
|
|
46
|
+
fontSize: 10,
|
|
47
|
+
color: 'rgba(255,255,255,0.7)',
|
|
48
|
+
userSelect: 'none',
|
|
49
|
+
WebkitUserSelect: 'none',
|
|
50
|
+
},
|
|
51
|
+
left: {
|
|
52
|
+
position: 'absolute',
|
|
53
|
+
top: 8,
|
|
54
|
+
left: 8,
|
|
55
|
+
zIndex: 20,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
18
59
|
const updateNode = (updater: (n: GameObjectType) => GameObjectType) => {
|
|
19
60
|
if (!prefabData || !setPrefabData || !selectedId) return;
|
|
20
61
|
setPrefabData(prev => ({
|
|
@@ -40,13 +81,19 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
40
81
|
|
|
41
82
|
// if (!selectedNode) return null;
|
|
42
83
|
return <>
|
|
43
|
-
<div style={
|
|
84
|
+
<div style={ui.panel}>
|
|
44
85
|
<div
|
|
45
|
-
|
|
86
|
+
style={ui.header}
|
|
46
87
|
onClick={() => setIsInspectorCollapsed(!isInspectorCollapsed)}
|
|
88
|
+
onPointerEnter={(e) => {
|
|
89
|
+
(e.currentTarget as HTMLDivElement).style.background = 'rgba(255,255,255,0.08)';
|
|
90
|
+
}}
|
|
91
|
+
onPointerLeave={(e) => {
|
|
92
|
+
(e.currentTarget as HTMLDivElement).style.background = 'rgba(255,255,255,0.05)';
|
|
93
|
+
}}
|
|
47
94
|
>
|
|
48
95
|
<span>Inspector</span>
|
|
49
|
-
<span
|
|
96
|
+
<span style={{ fontSize: 10, opacity: 0.8 }}>{isInspectorCollapsed ? '◀' : '▶'}</span>
|
|
50
97
|
</div>
|
|
51
98
|
{!isInspectorCollapsed && selectedNode && (
|
|
52
99
|
<NodeInspector
|
|
@@ -59,7 +106,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
59
106
|
/>
|
|
60
107
|
)}
|
|
61
108
|
</div>
|
|
62
|
-
<div style={
|
|
109
|
+
<div style={ui.left}>
|
|
63
110
|
<EditorTree
|
|
64
111
|
prefabData={prefabData}
|
|
65
112
|
setPrefabData={setPrefabData}
|
|
@@ -82,6 +129,103 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
82
129
|
const allComponentKeys = Object.keys(ALL_COMPONENTS);
|
|
83
130
|
const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
|
|
84
131
|
|
|
132
|
+
const s: Record<string, React.CSSProperties> = {
|
|
133
|
+
root: {
|
|
134
|
+
display: 'flex',
|
|
135
|
+
flexDirection: 'column',
|
|
136
|
+
gap: 6,
|
|
137
|
+
padding: 6,
|
|
138
|
+
maxHeight: '80vh',
|
|
139
|
+
overflowY: 'auto',
|
|
140
|
+
},
|
|
141
|
+
section: {
|
|
142
|
+
paddingBottom: 6,
|
|
143
|
+
borderBottom: '1px solid rgba(255,255,255,0.10)',
|
|
144
|
+
},
|
|
145
|
+
label: {
|
|
146
|
+
display: 'block',
|
|
147
|
+
fontSize: 10,
|
|
148
|
+
opacity: 0.7,
|
|
149
|
+
textTransform: 'uppercase',
|
|
150
|
+
letterSpacing: '0.08em',
|
|
151
|
+
marginBottom: 4,
|
|
152
|
+
},
|
|
153
|
+
input: {
|
|
154
|
+
width: '100%',
|
|
155
|
+
background: 'rgba(255,255,255,0.06)',
|
|
156
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
157
|
+
borderRadius: 4,
|
|
158
|
+
padding: '4px 6px',
|
|
159
|
+
color: 'rgba(255,255,255,0.92)',
|
|
160
|
+
font: 'inherit',
|
|
161
|
+
outline: 'none',
|
|
162
|
+
},
|
|
163
|
+
row: {
|
|
164
|
+
display: 'flex',
|
|
165
|
+
alignItems: 'center',
|
|
166
|
+
justifyContent: 'space-between',
|
|
167
|
+
gap: 8,
|
|
168
|
+
},
|
|
169
|
+
button: {
|
|
170
|
+
padding: '2px 6px',
|
|
171
|
+
background: 'transparent',
|
|
172
|
+
color: 'rgba(255,255,255,0.9)',
|
|
173
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
174
|
+
borderRadius: 4,
|
|
175
|
+
cursor: 'pointer',
|
|
176
|
+
font: 'inherit',
|
|
177
|
+
},
|
|
178
|
+
buttonActive: {
|
|
179
|
+
background: 'rgba(255,255,255,0.10)',
|
|
180
|
+
},
|
|
181
|
+
smallDanger: {
|
|
182
|
+
background: 'transparent',
|
|
183
|
+
border: 'none',
|
|
184
|
+
cursor: 'pointer',
|
|
185
|
+
color: 'rgba(255,120,120,0.95)',
|
|
186
|
+
font: 'inherit',
|
|
187
|
+
padding: '2px 4px',
|
|
188
|
+
},
|
|
189
|
+
componentHeader: {
|
|
190
|
+
display: 'flex',
|
|
191
|
+
alignItems: 'center',
|
|
192
|
+
justifyContent: 'space-between',
|
|
193
|
+
padding: '4px 0',
|
|
194
|
+
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
|
195
|
+
marginBottom: 4,
|
|
196
|
+
},
|
|
197
|
+
componentTitle: {
|
|
198
|
+
fontSize: 10,
|
|
199
|
+
textTransform: 'uppercase',
|
|
200
|
+
letterSpacing: '0.08em',
|
|
201
|
+
opacity: 0.8,
|
|
202
|
+
},
|
|
203
|
+
select: {
|
|
204
|
+
flex: 1,
|
|
205
|
+
background: 'rgba(255,255,255,0.06)',
|
|
206
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
207
|
+
borderRadius: 4,
|
|
208
|
+
padding: '4px 6px',
|
|
209
|
+
color: 'rgba(255,255,255,0.92)',
|
|
210
|
+
font: 'inherit',
|
|
211
|
+
outline: 'none',
|
|
212
|
+
},
|
|
213
|
+
addButton: {
|
|
214
|
+
width: 28,
|
|
215
|
+
padding: '4px 0',
|
|
216
|
+
background: 'rgba(255,255,255,0.08)',
|
|
217
|
+
color: 'rgba(255,255,255,0.92)',
|
|
218
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
219
|
+
borderRadius: 4,
|
|
220
|
+
cursor: 'pointer',
|
|
221
|
+
font: 'inherit',
|
|
222
|
+
},
|
|
223
|
+
disabled: {
|
|
224
|
+
opacity: 0.35,
|
|
225
|
+
cursor: 'not-allowed',
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
85
229
|
const componentKeys = Object.keys(node.components || {}).join(',');
|
|
86
230
|
useEffect(() => {
|
|
87
231
|
// Components stored on nodes use lowercase keys (e.g. 'geometry'),
|
|
@@ -92,28 +236,44 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
92
236
|
}
|
|
93
237
|
}, [componentKeys, addComponentType, node.components, allComponentKeys]);
|
|
94
238
|
|
|
95
|
-
return <div
|
|
96
|
-
<div
|
|
239
|
+
return <div style={s.root}>
|
|
240
|
+
<div style={s.section}>
|
|
97
241
|
<input
|
|
98
|
-
|
|
242
|
+
style={s.input}
|
|
99
243
|
value={node.id}
|
|
100
244
|
onChange={e => updateNode(n => ({ ...n, id: e.target.value }))}
|
|
101
245
|
/>
|
|
102
246
|
</div>
|
|
103
247
|
|
|
104
|
-
<div
|
|
105
|
-
<label
|
|
106
|
-
<button
|
|
248
|
+
<div style={{ ...s.row, ...s.section, paddingBottom: 6 }}>
|
|
249
|
+
<label style={{ ...s.label, marginBottom: 0 }}>Components</label>
|
|
250
|
+
<button
|
|
251
|
+
onClick={deleteNode}
|
|
252
|
+
style={s.smallDanger}
|
|
253
|
+
title="Delete node"
|
|
254
|
+
>
|
|
255
|
+
✕
|
|
256
|
+
</button>
|
|
107
257
|
</div>
|
|
108
258
|
|
|
109
|
-
<div
|
|
110
|
-
<label
|
|
111
|
-
<div
|
|
259
|
+
<div style={s.section}>
|
|
260
|
+
<label style={s.label}>Mode</label>
|
|
261
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
112
262
|
{["translate", "rotate", "scale"].map(mode => (
|
|
113
263
|
<button
|
|
114
264
|
key={mode}
|
|
115
265
|
onClick={() => setTransformMode(mode as any)}
|
|
116
|
-
|
|
266
|
+
style={{
|
|
267
|
+
...s.button,
|
|
268
|
+
flex: 1,
|
|
269
|
+
...(transformMode === mode ? s.buttonActive : null),
|
|
270
|
+
}}
|
|
271
|
+
onPointerEnter={(e) => {
|
|
272
|
+
if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
|
|
273
|
+
}}
|
|
274
|
+
onPointerLeave={(e) => {
|
|
275
|
+
if (transformMode !== mode) (e.currentTarget as HTMLButtonElement).style.background = 'transparent';
|
|
276
|
+
}}
|
|
117
277
|
>
|
|
118
278
|
{mode[0].toUpperCase()}
|
|
119
279
|
</button>
|
|
@@ -121,53 +281,29 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
121
281
|
</div>
|
|
122
282
|
</div>
|
|
123
283
|
|
|
124
|
-
{/* Components */}
|
|
125
|
-
{/* {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
|
|
126
|
-
if (!comp) return null;
|
|
127
|
-
return (
|
|
128
|
-
<div key={key} className="border border-cyan-500/20 mx-1 my-0.5 bg-black/20">
|
|
129
|
-
<div className="flex justify-between items-center px-1 py-0.5 border-b border-cyan-500/20 bg-cyan-500/5">
|
|
130
|
-
<span className="font-mono text-[10px] text-cyan-300 uppercase">{key}</span>
|
|
131
|
-
<button
|
|
132
|
-
onClick={() => updateNode(n => {
|
|
133
|
-
const components = { ...n.components };
|
|
134
|
-
delete components[key as keyof typeof components];
|
|
135
|
-
return { ...n, components };
|
|
136
|
-
})}
|
|
137
|
-
className="text-[9px] text-red-400/60 hover:text-red-400"
|
|
138
|
-
>
|
|
139
|
-
✕
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
|
-
<div className="px-1 py-0.5">
|
|
143
|
-
<ComponentEditor component={comp} onChange={(newComp: any) => updateNode(n => ({
|
|
144
|
-
...n,
|
|
145
|
-
components: { ...n.components, [key]: newComp }
|
|
146
|
-
}))} />
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
})} */}
|
|
284
|
+
{/* Components (legacy renderer removed) */}
|
|
151
285
|
|
|
152
286
|
{node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
|
|
153
287
|
if (!comp) return null;
|
|
154
288
|
const componentDef = ALL_COMPONENTS[comp.type];
|
|
155
|
-
if (!componentDef) return <div key={key}
|
|
289
|
+
if (!componentDef) return <div key={key} style={{ padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }}>
|
|
290
|
+
Unknown component type: {comp.type}
|
|
156
291
|
<textarea defaultValue={JSON.stringify(comp)} />
|
|
157
292
|
</div>;
|
|
158
293
|
|
|
159
294
|
const EditorComp = componentDef.Editor;
|
|
160
295
|
return (
|
|
161
|
-
<div key={key}
|
|
162
|
-
<div
|
|
163
|
-
<span
|
|
296
|
+
<div key={key} style={{ padding: '0 2px' }}>
|
|
297
|
+
<div style={s.componentHeader}>
|
|
298
|
+
<span style={s.componentTitle}>{key}</span>
|
|
164
299
|
<button
|
|
165
300
|
onClick={() => updateNode(n => {
|
|
166
301
|
const components = { ...n.components };
|
|
167
302
|
delete components[key as keyof typeof components];
|
|
168
303
|
return { ...n, components };
|
|
169
304
|
})}
|
|
170
|
-
|
|
305
|
+
style={s.smallDanger}
|
|
306
|
+
title="Remove component"
|
|
171
307
|
>
|
|
172
308
|
✕
|
|
173
309
|
</button>
|
|
@@ -193,11 +329,11 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
193
329
|
})}
|
|
194
330
|
|
|
195
331
|
{/* Add Component */}
|
|
196
|
-
<div
|
|
197
|
-
<label
|
|
198
|
-
<div
|
|
332
|
+
<div style={{ ...s.section, borderBottom: 'none', paddingBottom: 0 }}>
|
|
333
|
+
<label style={s.label}>Add Component</label>
|
|
334
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
199
335
|
<select
|
|
200
|
-
|
|
336
|
+
style={s.select}
|
|
201
337
|
value={addComponentType}
|
|
202
338
|
onChange={e => setAddComponentType(e.target.value)}
|
|
203
339
|
>
|
|
@@ -206,7 +342,10 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
206
342
|
))}
|
|
207
343
|
</select>
|
|
208
344
|
<button
|
|
209
|
-
|
|
345
|
+
style={{
|
|
346
|
+
...s.addButton,
|
|
347
|
+
...(!addComponentType ? s.disabled : null),
|
|
348
|
+
}}
|
|
210
349
|
disabled={!addComponentType}
|
|
211
350
|
onClick={() => {
|
|
212
351
|
if (!addComponentType) return;
|
|
@@ -222,6 +361,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
222
361
|
}));
|
|
223
362
|
}
|
|
224
363
|
}}
|
|
364
|
+
onPointerEnter={(e) => {
|
|
365
|
+
if (!addComponentType) return;
|
|
366
|
+
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.12)';
|
|
367
|
+
}}
|
|
368
|
+
onPointerLeave={(e) => {
|
|
369
|
+
if (!addComponentType) return;
|
|
370
|
+
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
|
|
371
|
+
}}
|
|
225
372
|
>
|
|
226
373
|
+
|
|
227
374
|
</button>
|
|
@@ -67,26 +67,88 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { b
|
|
|
67
67
|
</Physics>
|
|
68
68
|
</GameCanvas>
|
|
69
69
|
|
|
70
|
-
<div
|
|
70
|
+
<div
|
|
71
|
+
style={{
|
|
72
|
+
position: "absolute",
|
|
73
|
+
top: 8,
|
|
74
|
+
left: "50%",
|
|
75
|
+
transform: "translateX(-50%)",
|
|
76
|
+
display: "flex",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
gap: 6,
|
|
79
|
+
padding: "2px 4px",
|
|
80
|
+
background: "rgba(0,0,0,0.55)",
|
|
81
|
+
border: "1px solid rgba(255,255,255,0.12)",
|
|
82
|
+
borderRadius: 4,
|
|
83
|
+
color: "rgba(255,255,255,0.9)",
|
|
84
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
85
|
+
fontSize: 11,
|
|
86
|
+
lineHeight: 1,
|
|
87
|
+
WebkitUserSelect: "none",
|
|
88
|
+
userSelect: "none",
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
71
91
|
<button
|
|
72
|
-
|
|
92
|
+
style={{
|
|
93
|
+
padding: "2px 6px",
|
|
94
|
+
font: "inherit",
|
|
95
|
+
background: "transparent",
|
|
96
|
+
color: "inherit",
|
|
97
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
98
|
+
borderRadius: 3,
|
|
99
|
+
cursor: "pointer",
|
|
100
|
+
}}
|
|
73
101
|
onClick={() => setEditMode(!editMode)}
|
|
102
|
+
onPointerEnter={(e) => {
|
|
103
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
104
|
+
}}
|
|
105
|
+
onPointerLeave={(e) => {
|
|
106
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
107
|
+
}}
|
|
74
108
|
>
|
|
75
109
|
{editMode ? "▶" : "⏸"}
|
|
76
110
|
</button>
|
|
77
|
-
<span
|
|
111
|
+
<span style={{ opacity: 0.35 }}>|</span>
|
|
78
112
|
<button
|
|
79
|
-
|
|
113
|
+
style={{
|
|
114
|
+
padding: "2px 6px",
|
|
115
|
+
font: "inherit",
|
|
116
|
+
background: "transparent",
|
|
117
|
+
color: "inherit",
|
|
118
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
119
|
+
borderRadius: 3,
|
|
120
|
+
cursor: "pointer",
|
|
121
|
+
}}
|
|
80
122
|
onClick={async () => {
|
|
81
123
|
const prefab = await loadJson();
|
|
82
124
|
if (prefab) setLoadedPrefab(prefab);
|
|
83
125
|
}}
|
|
126
|
+
onPointerEnter={(e) => {
|
|
127
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
128
|
+
}}
|
|
129
|
+
onPointerLeave={(e) => {
|
|
130
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
131
|
+
}}
|
|
84
132
|
>
|
|
85
133
|
📥
|
|
86
134
|
</button>
|
|
87
135
|
<button
|
|
88
|
-
|
|
136
|
+
style={{
|
|
137
|
+
padding: "2px 6px",
|
|
138
|
+
font: "inherit",
|
|
139
|
+
background: "transparent",
|
|
140
|
+
color: "inherit",
|
|
141
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
142
|
+
borderRadius: 3,
|
|
143
|
+
cursor: "pointer",
|
|
144
|
+
}}
|
|
89
145
|
onClick={() => saveJson(loadedPrefab, "prefab")}
|
|
146
|
+
onPointerEnter={(e) => {
|
|
147
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
148
|
+
}}
|
|
149
|
+
onPointerLeave={(e) => {
|
|
150
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
151
|
+
}}
|
|
90
152
|
>
|
|
91
153
|
💾
|
|
92
154
|
</button>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ThreeElements } from "@react-three/fiber"
|
|
1
|
+
// import { ThreeElements } from "@react-three/fiber"
|
|
2
2
|
|
|
3
3
|
export interface Prefab {
|
|
4
|
-
id
|
|
5
|
-
name
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
author?: string;
|
|
8
8
|
version?: string;
|