react-three-game 0.0.18 → 0.0.20

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.
@@ -2,7 +2,8 @@ import { Dispatch, SetStateAction, useState, useEffect } from 'react';
2
2
  import { Prefab, GameObject as GameObjectType } from "./types";
3
3
  import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
-
5
+ import { base, inspector } from './styles';
6
+ import { findNode, updateNode, deleteNode } from './utils';
6
7
 
7
8
  function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }: {
8
9
  prefabData?: Prefab;
@@ -13,100 +14,42 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
13
14
  setTransformMode: (m: "translate" | "rotate" | "scale") => void;
14
15
  basePath?: string;
15
16
  }) {
16
- const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
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
- };
17
+ const [collapsed, setCollapsed] = useState(false);
58
18
 
59
- const updateNode = (updater: (n: GameObjectType) => GameObjectType) => {
19
+ const updateNodeHandler = (updater: (n: GameObjectType) => GameObjectType) => {
60
20
  if (!prefabData || !setPrefabData || !selectedId) return;
61
21
  setPrefabData(prev => ({
62
22
  ...prev,
63
- root: updatePrefabNode(prev.root, selectedId, updater)
23
+ root: updateNode(prev.root, selectedId, updater)
64
24
  }));
65
25
  };
66
26
 
67
- const deleteNode = () => {
68
- if (!prefabData || !setPrefabData || !selectedId) return;
69
- if (selectedId === prefabData.root.id) {
70
- alert("Cannot delete root node");
71
- return;
72
- }
73
- setPrefabData(prev => {
74
- const newRoot = deletePrefabNode(prev.root, selectedId);
75
- return { ...prev, root: newRoot! };
76
- });
27
+ const deleteNodeHandler = () => {
28
+ if (!prefabData || !setPrefabData || !selectedId || selectedId === prefabData.root.id) return;
29
+ setPrefabData(prev => ({ ...prev, root: deleteNode(prev.root, selectedId)! }));
77
30
  setSelectedId(null);
78
31
  };
79
32
 
80
33
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
81
34
 
82
- // if (!selectedNode) return null;
83
35
  return <>
84
- <div style={ui.panel}>
85
- <div
86
- style={ui.header}
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
- }}
94
- >
36
+ <div style={inspector.panel}>
37
+ <div style={base.header} onClick={() => setCollapsed(!collapsed)}>
95
38
  <span>Inspector</span>
96
- <span style={{ fontSize: 10, opacity: 0.8 }}>{isInspectorCollapsed ? '◀' : ''}</span>
39
+ <span>{collapsed ? '◀' : ''}</span>
97
40
  </div>
98
- {!isInspectorCollapsed && selectedNode && (
41
+ {!collapsed && selectedNode && (
99
42
  <NodeInspector
100
43
  node={selectedNode}
101
- updateNode={updateNode}
102
- deleteNode={deleteNode}
44
+ updateNode={updateNodeHandler}
45
+ deleteNode={deleteNodeHandler}
103
46
  transformMode={transformMode}
104
47
  setTransformMode={setTransformMode}
105
48
  basePath={basePath}
106
49
  />
107
50
  )}
108
51
  </div>
109
- <div style={ui.left}>
52
+ <div style={{ position: 'absolute', top: 8, left: 8, zIndex: 20 }}>
110
53
  <EditorTree
111
54
  prefabData={prefabData}
112
55
  setPrefabData={setPrefabData}
@@ -117,6 +60,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
117
60
  </>;
118
61
  }
119
62
 
63
+
120
64
  function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }: {
121
65
  node: GameObjectType;
122
66
  updateNode: (updater: (n: GameObjectType) => GameObjectType) => void;
@@ -126,270 +70,109 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
126
70
  basePath?: string;
127
71
  }) {
128
72
  const ALL_COMPONENTS = getAllComponents();
129
- const allComponentKeys = Object.keys(ALL_COMPONENTS);
130
- const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
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
- };
73
+ const allKeys = Object.keys(ALL_COMPONENTS);
74
+ const available = allKeys.filter(k => !node.components?.[k.toLowerCase()]);
75
+ const [addType, setAddType] = useState(available[0] || "");
228
76
 
229
- const componentKeys = Object.keys(node.components || {}).join(',');
230
77
  useEffect(() => {
231
- // Components stored on nodes use lowercase keys (e.g. 'geometry'),
232
- // while the registry keys are the component names (e.g. 'Geometry').
233
- const available = allComponentKeys.filter(k => !node.components?.[k.toLowerCase()]);
234
- if (!available.includes(addComponentType)) {
235
- setAddComponentType(available[0] || "");
236
- }
237
- }, [componentKeys, addComponentType, node.components, allComponentKeys]);
238
-
239
- return <div style={s.root}>
240
- <div style={s.section}>
78
+ const newAvailable = allKeys.filter(k => !node.components?.[k.toLowerCase()]);
79
+ if (!newAvailable.includes(addType)) setAddType(newAvailable[0] || "");
80
+ }, [Object.keys(node.components || {}).join(',')]);
81
+
82
+ return <div style={inspector.content}>
83
+ {/* Node ID */}
84
+ <div style={base.section}>
85
+ <div style={base.label}>Node ID</div>
241
86
  <input
242
- style={s.input}
87
+ style={base.input}
243
88
  value={node.id}
244
89
  onChange={e => updateNode(n => ({ ...n, id: e.target.value }))}
245
90
  />
246
91
  </div>
247
92
 
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>
257
- </div>
258
-
259
- {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
260
- if (!comp) return null;
261
- const componentDef = ALL_COMPONENTS[comp.type];
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}
264
- <textarea defaultValue={JSON.stringify(comp)} />
265
- </div>;
93
+ {/* Components */}
94
+ <div style={base.section}>
95
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
96
+ <div style={base.label}>Components</div>
97
+ <button style={{ ...base.btn, ...base.btnDanger }} onClick={deleteNode}>Delete Node</button>
98
+ </div>
266
99
 
267
- const EditorComp = componentDef.Editor;
268
- return (
269
- <div key={key} style={{ padding: '0 2px' }}>
270
- <div style={s.componentHeader}>
271
- <span style={s.componentTitle}>{key}</span>
272
- <button
273
- onClick={() => updateNode(n => {
274
- const components = { ...n.components };
275
- delete components[key as keyof typeof components];
276
- return { ...n, components };
277
- })}
278
- style={s.smallDanger}
279
- title="Remove component"
280
- >
281
-
282
- </button>
283
- </div>
284
- {EditorComp ? (
285
- <EditorComp
286
- component={comp}
287
- onUpdate={(newProps: any) => updateNode(n => ({
288
- ...n,
289
- components: {
290
- ...n.components,
291
- [key]: {
292
- ...comp,
293
- properties: { ...comp.properties, ...newProps }
100
+ {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
101
+ if (!comp) return null;
102
+ const def = ALL_COMPONENTS[comp.type];
103
+ if (!def) return <div key={key} style={{ color: '#ff8888', fontSize: 11 }}>
104
+ Unknown: {comp.type}
105
+ </div>;
106
+
107
+ return (
108
+ <div key={key} style={{ marginBottom: 8 }}>
109
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
110
+ <div style={{ fontSize: 11, fontWeight: 500 }}>{key}</div>
111
+ <button
112
+ style={{ ...base.btn, padding: '2px 6px' }}
113
+ onClick={() => updateNode(n => {
114
+ const { [key]: _, ...rest } = n.components || {};
115
+ return { ...n, components: rest };
116
+ })}
117
+ >
118
+
119
+ </button>
120
+ </div>
121
+ {def.Editor && (
122
+ <def.Editor
123
+ component={comp}
124
+ onUpdate={(newProps: any) => updateNode(n => ({
125
+ ...n,
126
+ components: {
127
+ ...n.components,
128
+ [key]: { ...comp, properties: { ...comp.properties, ...newProps } }
294
129
  }
295
- }
296
- }))}
297
- basePath={basePath}
298
- transformMode={transformMode}
299
- setTransformMode={setTransformMode}
300
- />
301
- ) : null}
302
- </div>
303
- );
304
- })}
130
+ }))}
131
+ basePath={basePath}
132
+ transformMode={transformMode}
133
+ setTransformMode={setTransformMode}
134
+ />
135
+ )}
136
+ </div>
137
+ );
138
+ })}
139
+ </div>
305
140
 
306
141
  {/* Add Component */}
307
- <div style={{ ...s.section, borderBottom: 'none', paddingBottom: 0 }}>
308
- <label style={s.label}>Add Component</label>
309
- <div style={{ display: 'flex', gap: 6 }}>
310
- <select
311
- style={s.select}
312
- value={addComponentType}
313
- onChange={e => setAddComponentType(e.target.value)}
314
- >
315
- {allComponentKeys.filter(k => !node.components?.[k.toLowerCase()]).map(k => (
316
- <option key={k} value={k}>{k}</option>
317
- ))}
318
- </select>
319
- <button
320
- style={{
321
- ...s.addButton,
322
- ...(!addComponentType ? s.disabled : null),
323
- }}
324
- disabled={!addComponentType}
325
- onClick={() => {
326
- if (!addComponentType) return;
327
- const def = ALL_COMPONENTS[addComponentType];
328
- if (def && !node.components?.[addComponentType.toLowerCase()]) {
329
- const key = addComponentType.toLowerCase();
330
- updateNode(n => ({
331
- ...n,
332
- components: {
333
- ...n.components,
334
- [key]: { type: def.name, properties: def.defaultProperties }
335
- }
336
- }));
337
- }
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
- }}
347
- >
348
- +
349
- </button>
142
+ {available.length > 0 && (
143
+ <div>
144
+ <div style={base.label}>Add Component</div>
145
+ <div style={base.row}>
146
+ <select
147
+ style={{ ...base.input, flex: 1 }}
148
+ value={addType}
149
+ onChange={e => setAddType(e.target.value)}
150
+ >
151
+ {available.map(k => <option key={k} value={k}>{k}</option>)}
152
+ </select>
153
+ <button
154
+ style={base.btn}
155
+ disabled={!addType}
156
+ onClick={() => {
157
+ if (!addType) return;
158
+ const def = ALL_COMPONENTS[addType];
159
+ if (def) {
160
+ updateNode(n => ({
161
+ ...n,
162
+ components: {
163
+ ...n.components,
164
+ [addType.toLowerCase()]: { type: def.name, properties: def.defaultProperties }
165
+ }
166
+ }));
167
+ }
168
+ }}
169
+ >
170
+ +
171
+ </button>
172
+ </div>
350
173
  </div>
351
- </div>
352
-
353
-
174
+ )}
354
175
  </div>
355
176
  }
356
177
 
357
- function findNode(root: GameObjectType, id: string): GameObjectType | null {
358
- if (root.id === id) return root;
359
- if (root.children) {
360
- for (const child of root.children) {
361
- const found = findNode(child, id);
362
- if (found) return found;
363
- }
364
- }
365
- return null;
366
- }
367
-
368
- function updatePrefabNode(root: GameObjectType, id: string, update: (node: GameObjectType) => GameObjectType): GameObjectType {
369
- if (root.id === id) {
370
- return update(root);
371
- }
372
- if (root.children) {
373
- return {
374
- ...root,
375
- children: root.children.map(child => updatePrefabNode(child, id, update))
376
- };
377
- }
378
- return root;
379
- }
380
-
381
- function deletePrefabNode(root: GameObjectType, id: string): GameObjectType | null {
382
- if (root.id === id) return null;
383
-
384
- if (root.children) {
385
- return {
386
- ...root,
387
- children: root.children
388
- .map(child => deletePrefabNode(child, id))
389
- .filter((child): child is GameObjectType => child !== null)
390
- };
391
- }
392
- return root;
393
- }
394
-
395
178
  export default EditorUI;