react-three-game 0.0.15 → 0.0.17
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 +132 -171
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/tools/dragdrop/DragDropLoader.js +1 -0
- package/dist/tools/prefabeditor/EditorTree.js +141 -15
- package/dist/tools/prefabeditor/EditorUI.js +154 -6
- package/dist/tools/prefabeditor/PrefabEditor.js +128 -5
- package/dist/tools/prefabeditor/PrefabRoot.js +34 -15
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
- package/dist/tools/prefabeditor/components/RotatorComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/RotatorComponent.js +42 -0
- package/dist/tools/prefabeditor/components/TransformComponent.js +28 -3
- package/dist/tools/prefabeditor/types.d.ts +2 -2
- package/dist/tools/prefabeditor/types.js +1 -0
- package/package.json +7 -7
- package/src/index.ts +4 -0
- package/src/tools/dragdrop/DragDropLoader.tsx +1 -0
- package/src/tools/prefabeditor/EditorTree.tsx +193 -30
- package/src/tools/prefabeditor/EditorUI.tsx +185 -63
- package/src/tools/prefabeditor/PrefabEditor.tsx +202 -24
- package/src/tools/prefabeditor/PrefabRoot.tsx +38 -19
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +7 -1
- package/src/tools/prefabeditor/components/TransformComponent.tsx +69 -16
- package/src/tools/prefabeditor/types.ts +3 -3
|
@@ -15,6 +15,106 @@ 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
|
+
dragHandle: {
|
|
78
|
+
width: 14,
|
|
79
|
+
height: 14,
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
marginRight: 4,
|
|
84
|
+
opacity: 0.4,
|
|
85
|
+
cursor: "grab",
|
|
86
|
+
fontSize: 10,
|
|
87
|
+
},
|
|
88
|
+
contextMenu: {
|
|
89
|
+
position: "fixed",
|
|
90
|
+
zIndex: 50,
|
|
91
|
+
minWidth: 120,
|
|
92
|
+
background: "rgba(0,0,0,0.82)",
|
|
93
|
+
border: "1px solid rgba(255,255,255,0.16)",
|
|
94
|
+
borderRadius: 6,
|
|
95
|
+
overflow: "hidden",
|
|
96
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
|
|
97
|
+
backdropFilter: "blur(6px)",
|
|
98
|
+
WebkitBackdropFilter: "blur(6px)",
|
|
99
|
+
},
|
|
100
|
+
menuItem: {
|
|
101
|
+
width: "100%",
|
|
102
|
+
textAlign: "left",
|
|
103
|
+
padding: "6px 8px",
|
|
104
|
+
background: "transparent",
|
|
105
|
+
border: "none",
|
|
106
|
+
color: "rgba(255,255,255,0.9)",
|
|
107
|
+
font: "inherit",
|
|
108
|
+
cursor: "pointer",
|
|
109
|
+
},
|
|
110
|
+
menuItemDanger: {
|
|
111
|
+
color: "rgba(255,120,120,0.95)",
|
|
112
|
+
},
|
|
113
|
+
menuDivider: {
|
|
114
|
+
borderTop: "1px solid rgba(255,255,255,0.10)",
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
18
118
|
if (!prefabData || !setPrefabData) return null;
|
|
19
119
|
|
|
20
120
|
const handleContextMenu = (e: MouseEvent, nodeId: string) => {
|
|
@@ -90,40 +190,38 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
90
190
|
|
|
91
191
|
// Drag and Drop
|
|
92
192
|
const handleDragStart = (e: React.DragEvent, id: string) => {
|
|
93
|
-
e.stopPropagation();
|
|
94
193
|
if (id === prefabData.root.id) {
|
|
95
|
-
e.preventDefault();
|
|
194
|
+
e.preventDefault();
|
|
96
195
|
return;
|
|
97
196
|
}
|
|
98
|
-
setDraggedId(id);
|
|
99
197
|
e.dataTransfer.effectAllowed = "move";
|
|
198
|
+
e.dataTransfer.setData("text/plain", id);
|
|
199
|
+
setDraggedId(id);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleDragEnd = () => {
|
|
203
|
+
setDraggedId(null);
|
|
100
204
|
};
|
|
101
205
|
|
|
102
206
|
const handleDragOver = (e: React.DragEvent, targetId: string) => {
|
|
103
|
-
e.preventDefault();
|
|
104
|
-
e.stopPropagation();
|
|
105
207
|
if (!draggedId || draggedId === targetId) return;
|
|
106
|
-
|
|
107
|
-
// Check for cycles: target cannot be a descendant of dragged node
|
|
108
208
|
const draggedNode = findNode(prefabData.root, draggedId);
|
|
109
209
|
if (draggedNode && findNode(draggedNode, targetId)) return;
|
|
110
210
|
|
|
211
|
+
e.preventDefault();
|
|
111
212
|
e.dataTransfer.dropEffect = "move";
|
|
112
213
|
};
|
|
113
214
|
|
|
114
215
|
const handleDrop = (e: React.DragEvent, targetId: string) => {
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
e.stopPropagation();
|
|
117
216
|
if (!draggedId || draggedId === targetId) return;
|
|
118
217
|
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
|
|
119
220
|
setPrefabData(prev => {
|
|
120
221
|
const newRoot = JSON.parse(JSON.stringify(prev.root));
|
|
222
|
+
const draggedNode = findNode(newRoot, draggedId);
|
|
223
|
+
if (draggedNode && findNode(draggedNode, targetId)) return prev;
|
|
121
224
|
|
|
122
|
-
// Check cycle again on the fresh tree
|
|
123
|
-
const draggedNodeRef = findNode(newRoot, draggedId);
|
|
124
|
-
if (draggedNodeRef && findNode(draggedNodeRef, targetId)) return prev;
|
|
125
|
-
|
|
126
|
-
// Remove from old parent
|
|
127
225
|
const parent = findParent(newRoot, draggedId);
|
|
128
226
|
if (!parent) return prev;
|
|
129
227
|
|
|
@@ -132,7 +230,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
132
230
|
|
|
133
231
|
parent.children = parent.children!.filter(c => c.id !== draggedId);
|
|
134
232
|
|
|
135
|
-
// Add to new parent
|
|
136
233
|
const target = findNode(newRoot, targetId);
|
|
137
234
|
if (target) {
|
|
138
235
|
target.children = target.children || [];
|
|
@@ -152,24 +249,57 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
152
249
|
const hasChildren = node.children && node.children.length > 0;
|
|
153
250
|
|
|
154
251
|
return (
|
|
155
|
-
<div key={node.id}
|
|
252
|
+
<div key={node.id}>
|
|
156
253
|
<div
|
|
157
|
-
|
|
158
|
-
|
|
254
|
+
style={{
|
|
255
|
+
...styles.row,
|
|
256
|
+
...(isSelected ? styles.rowSelected : null),
|
|
257
|
+
paddingLeft: `${depth * 10 + 6}px`,
|
|
258
|
+
cursor: node.id !== prefabData.root.id ? "grab" : "pointer",
|
|
259
|
+
}}
|
|
260
|
+
draggable={node.id !== prefabData.root.id}
|
|
159
261
|
onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
|
|
160
262
|
onContextMenu={(e) => handleContextMenu(e, node.id)}
|
|
161
|
-
draggable={node.id !== prefabData.root.id}
|
|
162
263
|
onDragStart={(e) => handleDragStart(e, node.id)}
|
|
264
|
+
onDragEnd={handleDragEnd}
|
|
163
265
|
onDragOver={(e) => handleDragOver(e, node.id)}
|
|
164
266
|
onDrop={(e) => handleDrop(e, node.id)}
|
|
267
|
+
onPointerEnter={(e) => {
|
|
268
|
+
if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.06)";
|
|
269
|
+
}}
|
|
270
|
+
onPointerLeave={(e) => {
|
|
271
|
+
if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "transparent";
|
|
272
|
+
}}
|
|
165
273
|
>
|
|
166
274
|
<span
|
|
167
|
-
|
|
275
|
+
style={{
|
|
276
|
+
...styles.chevron,
|
|
277
|
+
visibility: hasChildren ? 'visible' : 'hidden',
|
|
278
|
+
}}
|
|
168
279
|
onClick={(e) => hasChildren && toggleCollapse(e, node.id)}
|
|
280
|
+
onPointerEnter={(e) => {
|
|
281
|
+
(e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
|
|
282
|
+
}}
|
|
283
|
+
onPointerLeave={(e) => {
|
|
284
|
+
(e.currentTarget as HTMLSpanElement).style.opacity = "0.55";
|
|
285
|
+
}}
|
|
169
286
|
>
|
|
170
287
|
{isCollapsed ? '▶' : '▼'}
|
|
171
288
|
</span>
|
|
172
|
-
|
|
289
|
+
{node.id !== prefabData.root.id && (
|
|
290
|
+
<span
|
|
291
|
+
style={styles.dragHandle}
|
|
292
|
+
onPointerEnter={(e) => {
|
|
293
|
+
(e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
|
|
294
|
+
}}
|
|
295
|
+
onPointerLeave={(e) => {
|
|
296
|
+
(e.currentTarget as HTMLSpanElement).style.opacity = "0.4";
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
⋮⋮
|
|
300
|
+
</span>
|
|
301
|
+
)}
|
|
302
|
+
<span style={styles.idText}>
|
|
173
303
|
{node.id}
|
|
174
304
|
</span>
|
|
175
305
|
</div>
|
|
@@ -184,16 +314,28 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
184
314
|
|
|
185
315
|
return (
|
|
186
316
|
<>
|
|
187
|
-
<div
|
|
317
|
+
<div
|
|
318
|
+
style={{
|
|
319
|
+
...styles.panel,
|
|
320
|
+
width: isTreeCollapsed ? 'auto' : '14rem',
|
|
321
|
+
}}
|
|
322
|
+
onClick={closeContextMenu}
|
|
323
|
+
>
|
|
188
324
|
<div
|
|
189
|
-
|
|
325
|
+
style={styles.panelHeader}
|
|
190
326
|
onClick={(e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }}
|
|
327
|
+
onPointerEnter={(e) => {
|
|
328
|
+
(e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.08)";
|
|
329
|
+
}}
|
|
330
|
+
onPointerLeave={(e) => {
|
|
331
|
+
(e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.05)";
|
|
332
|
+
}}
|
|
191
333
|
>
|
|
192
334
|
<span>Prefab Graph</span>
|
|
193
|
-
<span
|
|
335
|
+
<span style={{ fontSize: 10, opacity: 0.8 }}>{isTreeCollapsed ? '▶' : '◀'}</span>
|
|
194
336
|
</div>
|
|
195
337
|
{!isTreeCollapsed && (
|
|
196
|
-
<div
|
|
338
|
+
<div style={{ ...styles.scroll, padding: 2 }}>
|
|
197
339
|
{renderNode(prefabData.root)}
|
|
198
340
|
</div>
|
|
199
341
|
)}
|
|
@@ -201,28 +343,49 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
201
343
|
|
|
202
344
|
{contextMenu && (
|
|
203
345
|
<div
|
|
204
|
-
|
|
205
|
-
|
|
346
|
+
style={{
|
|
347
|
+
...styles.contextMenu,
|
|
348
|
+
top: contextMenu.y,
|
|
349
|
+
left: contextMenu.x,
|
|
350
|
+
}}
|
|
206
351
|
onClick={(e) => e.stopPropagation()}
|
|
207
352
|
onPointerLeave={closeContextMenu}
|
|
208
353
|
>
|
|
209
354
|
<button
|
|
210
|
-
|
|
355
|
+
style={{ ...styles.menuItem, ...styles.menuDivider }}
|
|
211
356
|
onClick={() => handleAddChild(contextMenu.nodeId)}
|
|
357
|
+
onPointerEnter={(e) => {
|
|
358
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
359
|
+
}}
|
|
360
|
+
onPointerLeave={(e) => {
|
|
361
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
362
|
+
}}
|
|
212
363
|
>
|
|
213
364
|
Add Child
|
|
214
365
|
</button>
|
|
215
366
|
{contextMenu.nodeId !== prefabData.root.id && (
|
|
216
367
|
<>
|
|
217
368
|
<button
|
|
218
|
-
|
|
369
|
+
style={{ ...styles.menuItem, ...styles.menuDivider }}
|
|
219
370
|
onClick={() => handleDuplicate(contextMenu.nodeId)}
|
|
371
|
+
onPointerEnter={(e) => {
|
|
372
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
373
|
+
}}
|
|
374
|
+
onPointerLeave={(e) => {
|
|
375
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
376
|
+
}}
|
|
220
377
|
>
|
|
221
378
|
Duplicate
|
|
222
379
|
</button>
|
|
223
380
|
<button
|
|
224
|
-
|
|
381
|
+
style={{ ...styles.menuItem, ...styles.menuItemDanger }}
|
|
225
382
|
onClick={() => handleDelete(contextMenu.nodeId)}
|
|
383
|
+
onPointerEnter={(e) => {
|
|
384
|
+
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
385
|
+
}}
|
|
386
|
+
onPointerLeave={(e) => {
|
|
387
|
+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
388
|
+
}}
|
|
226
389
|
>
|
|
227
390
|
Delete
|
|
228
391
|
</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,82 +236,47 @@ 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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<button
|
|
114
|
-
key={mode}
|
|
115
|
-
onClick={() => setTransformMode(mode as any)}
|
|
116
|
-
className={`flex-1 px-1 py-0.5 text-[10px] font-mono border ${transformMode === mode ? 'bg-cyan-500/30 border-cyan-400/50 text-cyan-200' : 'bg-black/30 border-cyan-500/20 text-cyan-400/60 hover:border-cyan-400/30'}`}
|
|
117
|
-
>
|
|
118
|
-
{mode[0].toUpperCase()}
|
|
119
|
-
</button>
|
|
120
|
-
))}
|
|
121
|
-
</div>
|
|
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>
|
|
122
257
|
</div>
|
|
123
258
|
|
|
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
|
-
})} */}
|
|
151
|
-
|
|
152
259
|
{node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
|
|
153
260
|
if (!comp) return null;
|
|
154
261
|
const componentDef = ALL_COMPONENTS[comp.type];
|
|
155
|
-
if (!componentDef) return <div key={key}
|
|
262
|
+
if (!componentDef) return <div key={key} style={{ padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }}>
|
|
263
|
+
Unknown component type: {comp.type}
|
|
156
264
|
<textarea defaultValue={JSON.stringify(comp)} />
|
|
157
265
|
</div>;
|
|
158
266
|
|
|
159
267
|
const EditorComp = componentDef.Editor;
|
|
160
268
|
return (
|
|
161
|
-
<div key={key}
|
|
162
|
-
<div
|
|
163
|
-
<span
|
|
269
|
+
<div key={key} style={{ padding: '0 2px' }}>
|
|
270
|
+
<div style={s.componentHeader}>
|
|
271
|
+
<span style={s.componentTitle}>{key}</span>
|
|
164
272
|
<button
|
|
165
273
|
onClick={() => updateNode(n => {
|
|
166
274
|
const components = { ...n.components };
|
|
167
275
|
delete components[key as keyof typeof components];
|
|
168
276
|
return { ...n, components };
|
|
169
277
|
})}
|
|
170
|
-
|
|
278
|
+
style={s.smallDanger}
|
|
279
|
+
title="Remove component"
|
|
171
280
|
>
|
|
172
281
|
✕
|
|
173
282
|
</button>
|
|
@@ -186,6 +295,8 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
186
295
|
}
|
|
187
296
|
}))}
|
|
188
297
|
basePath={basePath}
|
|
298
|
+
transformMode={transformMode}
|
|
299
|
+
setTransformMode={setTransformMode}
|
|
189
300
|
/>
|
|
190
301
|
) : null}
|
|
191
302
|
</div>
|
|
@@ -193,11 +304,11 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
193
304
|
})}
|
|
194
305
|
|
|
195
306
|
{/* Add Component */}
|
|
196
|
-
<div
|
|
197
|
-
<label
|
|
198
|
-
<div
|
|
307
|
+
<div style={{ ...s.section, borderBottom: 'none', paddingBottom: 0 }}>
|
|
308
|
+
<label style={s.label}>Add Component</label>
|
|
309
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
199
310
|
<select
|
|
200
|
-
|
|
311
|
+
style={s.select}
|
|
201
312
|
value={addComponentType}
|
|
202
313
|
onChange={e => setAddComponentType(e.target.value)}
|
|
203
314
|
>
|
|
@@ -206,7 +317,10 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
206
317
|
))}
|
|
207
318
|
</select>
|
|
208
319
|
<button
|
|
209
|
-
|
|
320
|
+
style={{
|
|
321
|
+
...s.addButton,
|
|
322
|
+
...(!addComponentType ? s.disabled : null),
|
|
323
|
+
}}
|
|
210
324
|
disabled={!addComponentType}
|
|
211
325
|
onClick={() => {
|
|
212
326
|
if (!addComponentType) return;
|
|
@@ -222,6 +336,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
222
336
|
}));
|
|
223
337
|
}
|
|
224
338
|
}}
|
|
339
|
+
onPointerEnter={(e) => {
|
|
340
|
+
if (!addComponentType) return;
|
|
341
|
+
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.12)';
|
|
342
|
+
}}
|
|
343
|
+
onPointerLeave={(e) => {
|
|
344
|
+
if (!addComponentType) return;
|
|
345
|
+
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.08)';
|
|
346
|
+
}}
|
|
225
347
|
>
|
|
226
348
|
+
|
|
227
349
|
</button>
|