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
package/README.md
CHANGED
|
@@ -17,44 +17,17 @@ 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}";
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Tailwind v3
|
|
47
|
-
|
|
48
|
-
Add the library path to your `tailwind.config.js`:
|
|
49
|
-
|
|
50
|
-
```js
|
|
51
|
-
module.exports = {
|
|
52
|
-
content: [
|
|
53
|
-
// ...
|
|
54
|
-
"./node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}",
|
|
55
|
-
],
|
|
56
|
-
// ...
|
|
57
|
-
}
|
|
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
|
+
}} />
|
|
58
31
|
```
|
|
59
32
|
|
|
60
33
|
## Quick Start
|
|
@@ -64,39 +37,44 @@ npm install react-three-game @react-three/fiber @react-three/rapier three
|
|
|
64
37
|
```
|
|
65
38
|
|
|
66
39
|
```jsx
|
|
67
|
-
import {
|
|
40
|
+
import { Physics } from '@react-three/rapier';
|
|
41
|
+
import { GameCanvas, PrefabRoot } from 'react-three-game';
|
|
68
42
|
|
|
69
43
|
export default function App() {
|
|
70
44
|
return (
|
|
71
45
|
<GameCanvas>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
46
|
+
<Physics>
|
|
47
|
+
<ambientLight intensity={0.8} />
|
|
48
|
+
<PrefabRoot
|
|
49
|
+
data={{
|
|
50
|
+
id: "scene",
|
|
51
|
+
name: "scene",
|
|
52
|
+
root: {
|
|
53
|
+
id: "root",
|
|
54
|
+
children: [
|
|
55
|
+
{
|
|
56
|
+
id: "ground",
|
|
57
|
+
components: {
|
|
58
|
+
transform: { type: "Transform", properties: { position: [0, 0, 0], rotation: [-1.57, 0, 0] } },
|
|
59
|
+
geometry: { type: "Geometry", properties: { geometryType: "plane", args: [50, 50] } },
|
|
60
|
+
material: { type: "Material", properties: { color: "green" } },
|
|
61
|
+
physics: { type: "Physics", properties: { type: "fixed" } }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "player",
|
|
66
|
+
components: {
|
|
67
|
+
transform: { type: "Transform", properties: { position: [0, 2, 0] } },
|
|
68
|
+
geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
|
|
69
|
+
material: { type: "Material", properties: { color: "#ff6b6b" } },
|
|
70
|
+
physics: { type: "Physics", properties: { type: "dynamic" } }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
95
74
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}} />
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
</Physics>
|
|
100
78
|
</GameCanvas>
|
|
101
79
|
);
|
|
102
80
|
}
|
|
@@ -122,28 +100,100 @@ interface GameObject {
|
|
|
122
100
|
|
|
123
101
|
## Custom Components
|
|
124
102
|
|
|
125
|
-
|
|
103
|
+
Extend the engine by registering your own components. Components have two parts:
|
|
104
|
+
- **Editor**: UI for inspector panel (edit mode)
|
|
105
|
+
- **View**: Three.js runtime renderer (play mode)
|
|
106
|
+
|
|
107
|
+
### Component Interface
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
126
110
|
import { Component } from 'react-three-game';
|
|
127
111
|
|
|
128
|
-
|
|
129
|
-
name:
|
|
112
|
+
interface Component {
|
|
113
|
+
name: string;
|
|
114
|
+
Editor: FC<{ component: any; onUpdate: (newComp: any) => void }>;
|
|
115
|
+
View?: FC<{ properties: any; children?: React.ReactNode }>;
|
|
116
|
+
defaultProperties: any;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Example: Rotator Component
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { Component, registerComponent } from 'react-three-game';
|
|
124
|
+
import { useFrame } from '@react-three/fiber';
|
|
125
|
+
import { useRef } from 'react';
|
|
126
|
+
|
|
127
|
+
const RotatorComponent: Component = {
|
|
128
|
+
name: 'Rotator',
|
|
129
|
+
|
|
130
130
|
Editor: ({ component, onUpdate }) => (
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
<div>
|
|
132
|
+
<label>Speed</label>
|
|
133
|
+
<input
|
|
134
|
+
type="number"
|
|
135
|
+
value={component.properties.speed ?? 1.0}
|
|
136
|
+
onChange={e => onUpdate({ ...component.properties, speed: parseFloat(e.target.value) })}
|
|
137
|
+
/>
|
|
138
|
+
<label>Axis</label>
|
|
139
|
+
<select
|
|
140
|
+
value={component.properties.axis ?? 'y'}
|
|
141
|
+
onChange={e => onUpdate({ ...component.properties, axis: e.target.value })}
|
|
142
|
+
>
|
|
143
|
+
<option value="x">X</option>
|
|
144
|
+
<option value="y">Y</option>
|
|
145
|
+
<option value="z">Z</option>
|
|
146
|
+
</select>
|
|
147
|
+
</div>
|
|
135
148
|
),
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
|
|
150
|
+
View: ({ properties, children }) => {
|
|
151
|
+
const ref = useRef();
|
|
152
|
+
const speed = properties.speed ?? 1.0;
|
|
153
|
+
const axis = properties.axis ?? 'y';
|
|
154
|
+
|
|
155
|
+
useFrame((state, delta) => {
|
|
156
|
+
if (ref.current) {
|
|
157
|
+
ref.current.rotation[axis] += delta * speed;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return <group ref={ref}>{children}</group>;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
defaultProperties: { speed: 1.0, axis: 'y' }
|
|
140
165
|
};
|
|
141
166
|
|
|
142
|
-
// Register
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
// Register before using PrefabEditor
|
|
168
|
+
registerComponent(RotatorComponent);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Usage in Prefab JSON
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"id": "spinning-cube",
|
|
176
|
+
"components": {
|
|
177
|
+
"transform": { "type": "Transform", "properties": { "position": [0, 1, 0] } },
|
|
178
|
+
"geometry": { "type": "Geometry", "properties": { "geometryType": "box" } },
|
|
179
|
+
"material": { "type": "Material", "properties": { "color": "#ff6b6b" } },
|
|
180
|
+
"rotator": { "type": "Rotator", "properties": { "speed": 2.0, "axis": "y" } }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
145
183
|
```
|
|
146
184
|
|
|
185
|
+
### Wrapper vs Leaf Components
|
|
186
|
+
|
|
187
|
+
**Wrapper components** (accept `children`) wrap the rendered content:
|
|
188
|
+
- Use for behaviors that need to manipulate the scene graph (animations, controllers)
|
|
189
|
+
- Example: Rotator wraps mesh to apply rotation
|
|
190
|
+
|
|
191
|
+
**Leaf components** (no `children`) render as siblings:
|
|
192
|
+
- Use for standalone effects (lights, particles, audio sources)
|
|
193
|
+
- Example: SpotLight renders a `<spotLight>` element
|
|
194
|
+
|
|
195
|
+
The engine automatically detects component type by checking if `View` accepts `children` prop.
|
|
196
|
+
|
|
147
197
|
## Built-in Components
|
|
148
198
|
|
|
149
199
|
| Component | Properties |
|
|
@@ -172,41 +222,13 @@ Transform gizmos (T/R/S keys), drag-to-reorder tree, import/export JSON, edit/pl
|
|
|
172
222
|
### Transform Hierarchy
|
|
173
223
|
- Local transforms stored in JSON (relative to parent)
|
|
174
224
|
- World transforms computed at runtime via matrix multiplication
|
|
175
|
-
- `computeParentWorldMatrix(root, targetId)` traverses tree for parent's world matrix
|
|
176
225
|
- TransformControls extract world matrix → compute parent inverse → derive new local transform
|
|
177
226
|
|
|
178
227
|
### GPU Instancing
|
|
179
|
-
Enable with `model.properties.instanced = true
|
|
180
|
-
```json
|
|
181
|
-
{
|
|
182
|
-
"components": {
|
|
183
|
-
"model": {
|
|
184
|
-
"type": "Model",
|
|
185
|
-
"properties": {
|
|
186
|
-
"filename": "tree.glb",
|
|
187
|
-
"instanced": true
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
Uses drei's `<Merged>` + `<InstancedRigidBodies>` for physics. World-space transforms, terminal nodes.
|
|
228
|
+
Enable with `model.properties.instanced = true` for optimized repeated geometry. Uses drei's `<Merged>` + `<InstancedRigidBodies>`.
|
|
194
229
|
|
|
195
230
|
### Model Loading
|
|
196
|
-
|
|
197
|
-
- Singleton loaders in `modelLoader.ts`
|
|
198
|
-
- Draco decoder from `https://www.gstatic.com/draco/v1/decoders/`
|
|
199
|
-
- Auto-loads when `model.properties.filename` detected
|
|
200
|
-
|
|
201
|
-
### WebGPU Renderer
|
|
202
|
-
```tsx
|
|
203
|
-
<Canvas gl={async ({ canvas }) => {
|
|
204
|
-
const renderer = new WebGPURenderer({ canvas, shadowMap: true });
|
|
205
|
-
await renderer.init(); // Required
|
|
206
|
-
return renderer;
|
|
207
|
-
}}>
|
|
208
|
-
```
|
|
209
|
-
Use `MeshStandardNodeMaterial` not `MeshStandardMaterial`.
|
|
231
|
+
Supports GLB/GLTF (with Draco compression) and FBX. Models auto-load when `model.properties.filename` is detected.
|
|
210
232
|
|
|
211
233
|
## Patterns
|
|
212
234
|
|
|
@@ -216,15 +238,6 @@ import levelData from './prefabs/arena.json';
|
|
|
216
238
|
<PrefabRoot data={levelData} />
|
|
217
239
|
```
|
|
218
240
|
|
|
219
|
-
### Mix with React Components
|
|
220
|
-
```jsx
|
|
221
|
-
<Physics>
|
|
222
|
-
<PrefabRoot data={environment} />
|
|
223
|
-
<Player />
|
|
224
|
-
<AIEnemies />
|
|
225
|
-
</Physics>
|
|
226
|
-
```
|
|
227
|
-
|
|
228
241
|
### Update Prefab Nodes
|
|
229
242
|
```typescript
|
|
230
243
|
function updatePrefabNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject {
|
|
@@ -236,58 +249,6 @@ function updatePrefabNode(root: GameObject, id: string, update: (node: GameObjec
|
|
|
236
249
|
}
|
|
237
250
|
```
|
|
238
251
|
|
|
239
|
-
## AI Agent Reference
|
|
240
|
-
|
|
241
|
-
### Prefab JSON Schema
|
|
242
|
-
```typescript
|
|
243
|
-
{
|
|
244
|
-
"id": "unique-id",
|
|
245
|
-
"root": {
|
|
246
|
-
"id": "root-id",
|
|
247
|
-
"components": {
|
|
248
|
-
"transform": {
|
|
249
|
-
"type": "Transform",
|
|
250
|
-
"properties": {
|
|
251
|
-
"position": [x, y, z], // world units
|
|
252
|
-
"rotation": [x, y, z], // radians
|
|
253
|
-
"scale": [x, y, z] // multipliers
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
"geometry": {
|
|
257
|
-
"type": "Geometry",
|
|
258
|
-
"properties": {
|
|
259
|
-
"geometryType": "box" | "sphere" | "plane" | "cylinder" | "cone" | "torus",
|
|
260
|
-
"args": [/* geometry-specific */]
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
"material": {
|
|
264
|
-
"type": "Material",
|
|
265
|
-
"properties": {
|
|
266
|
-
"color": "#rrggbb",
|
|
267
|
-
"texture": "/path/to/texture.jpg",
|
|
268
|
-
"metalness": 0.0-1.0,
|
|
269
|
-
"roughness": 0.0-1.0
|
|
270
|
-
}
|
|
271
|
-
},
|
|
272
|
-
"physics": {
|
|
273
|
-
"type": "Physics",
|
|
274
|
-
"properties": {
|
|
275
|
-
"type": "dynamic" | "fixed"
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
"model": {
|
|
279
|
-
"type": "Model",
|
|
280
|
-
"properties": {
|
|
281
|
-
"filename": "/models/asset.glb",
|
|
282
|
-
"instanced": true
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
"children": [/* recursive GameObjects */]
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
252
|
## Development
|
|
292
253
|
|
|
293
254
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
|
3
3
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
4
4
|
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
5
5
|
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
6
|
+
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
7
|
+
export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
|
|
6
8
|
export * from './helpers';
|
|
7
9
|
export type { Prefab, GameObject } from './tools/prefabeditor/types';
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,7 @@ export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
|
4
4
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
5
5
|
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
6
6
|
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
7
|
+
// Component Registry
|
|
8
|
+
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
7
9
|
// Helpers
|
|
8
10
|
export * from './helpers';
|
|
@@ -6,6 +6,105 @@ 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
|
+
dragHandle: {
|
|
69
|
+
width: 14,
|
|
70
|
+
height: 14,
|
|
71
|
+
display: "flex",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
justifyContent: "center",
|
|
74
|
+
marginRight: 4,
|
|
75
|
+
opacity: 0.4,
|
|
76
|
+
cursor: "grab",
|
|
77
|
+
fontSize: 10,
|
|
78
|
+
},
|
|
79
|
+
contextMenu: {
|
|
80
|
+
position: "fixed",
|
|
81
|
+
zIndex: 50,
|
|
82
|
+
minWidth: 120,
|
|
83
|
+
background: "rgba(0,0,0,0.82)",
|
|
84
|
+
border: "1px solid rgba(255,255,255,0.16)",
|
|
85
|
+
borderRadius: 6,
|
|
86
|
+
overflow: "hidden",
|
|
87
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
|
|
88
|
+
backdropFilter: "blur(6px)",
|
|
89
|
+
WebkitBackdropFilter: "blur(6px)",
|
|
90
|
+
},
|
|
91
|
+
menuItem: {
|
|
92
|
+
width: "100%",
|
|
93
|
+
textAlign: "left",
|
|
94
|
+
padding: "6px 8px",
|
|
95
|
+
background: "transparent",
|
|
96
|
+
border: "none",
|
|
97
|
+
color: "rgba(255,255,255,0.9)",
|
|
98
|
+
font: "inherit",
|
|
99
|
+
cursor: "pointer",
|
|
100
|
+
},
|
|
101
|
+
menuItemDanger: {
|
|
102
|
+
color: "rgba(255,120,120,0.95)",
|
|
103
|
+
},
|
|
104
|
+
menuDivider: {
|
|
105
|
+
borderTop: "1px solid rgba(255,255,255,0.10)",
|
|
106
|
+
}
|
|
107
|
+
};
|
|
9
108
|
if (!prefabData || !setPrefabData)
|
|
10
109
|
return null;
|
|
11
110
|
const handleContextMenu = (e, nodeId) => {
|
|
@@ -77,38 +176,36 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
77
176
|
};
|
|
78
177
|
// Drag and Drop
|
|
79
178
|
const handleDragStart = (e, id) => {
|
|
80
|
-
e.stopPropagation();
|
|
81
179
|
if (id === prefabData.root.id) {
|
|
82
|
-
e.preventDefault();
|
|
180
|
+
e.preventDefault();
|
|
83
181
|
return;
|
|
84
182
|
}
|
|
85
|
-
setDraggedId(id);
|
|
86
183
|
e.dataTransfer.effectAllowed = "move";
|
|
184
|
+
e.dataTransfer.setData("text/plain", id);
|
|
185
|
+
setDraggedId(id);
|
|
186
|
+
};
|
|
187
|
+
const handleDragEnd = () => {
|
|
188
|
+
setDraggedId(null);
|
|
87
189
|
};
|
|
88
190
|
const handleDragOver = (e, targetId) => {
|
|
89
|
-
e.preventDefault();
|
|
90
|
-
e.stopPropagation();
|
|
91
191
|
if (!draggedId || draggedId === targetId)
|
|
92
192
|
return;
|
|
93
|
-
// Check for cycles: target cannot be a descendant of dragged node
|
|
94
193
|
const draggedNode = findNode(prefabData.root, draggedId);
|
|
95
194
|
if (draggedNode && findNode(draggedNode, targetId))
|
|
96
195
|
return;
|
|
196
|
+
e.preventDefault();
|
|
97
197
|
e.dataTransfer.dropEffect = "move";
|
|
98
198
|
};
|
|
99
199
|
const handleDrop = (e, targetId) => {
|
|
100
|
-
e.preventDefault();
|
|
101
|
-
e.stopPropagation();
|
|
102
200
|
if (!draggedId || draggedId === targetId)
|
|
103
201
|
return;
|
|
202
|
+
e.preventDefault();
|
|
104
203
|
setPrefabData(prev => {
|
|
105
204
|
var _a;
|
|
106
205
|
const newRoot = JSON.parse(JSON.stringify(prev.root));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (draggedNodeRef && findNode(draggedNodeRef, targetId))
|
|
206
|
+
const draggedNode = findNode(newRoot, draggedId);
|
|
207
|
+
if (draggedNode && findNode(draggedNode, targetId))
|
|
110
208
|
return prev;
|
|
111
|
-
// Remove from old parent
|
|
112
209
|
const parent = findParent(newRoot, draggedId);
|
|
113
210
|
if (!parent)
|
|
114
211
|
return prev;
|
|
@@ -116,7 +213,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
116
213
|
if (!nodeToMove)
|
|
117
214
|
return prev;
|
|
118
215
|
parent.children = parent.children.filter(c => c.id !== draggedId);
|
|
119
|
-
// Add to new parent
|
|
120
216
|
const target = findNode(newRoot, targetId);
|
|
121
217
|
if (target) {
|
|
122
218
|
target.children = target.children || [];
|
|
@@ -132,9 +228,39 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
132
228
|
const isSelected = node.id === selectedId;
|
|
133
229
|
const isCollapsed = collapsedIds.has(node.id);
|
|
134
230
|
const hasChildren = node.children && node.children.length > 0;
|
|
135
|
-
return (_jsxs("div", {
|
|
231
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, styles.row), (isSelected ? styles.rowSelected : null)), { paddingLeft: `${depth * 10 + 6}px`, cursor: node.id !== prefabData.root.id ? "grab" : "pointer" }), draggable: node.id !== prefabData.root.id, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: handleDragEnd, onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), onPointerEnter: (e) => {
|
|
232
|
+
if (!isSelected)
|
|
233
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.06)";
|
|
234
|
+
}, onPointerLeave: (e) => {
|
|
235
|
+
if (!isSelected)
|
|
236
|
+
e.currentTarget.style.background = "transparent";
|
|
237
|
+
}, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.chevron), { visibility: hasChildren ? 'visible' : 'hidden' }), onClick: (e) => hasChildren && toggleCollapse(e, node.id), onPointerEnter: (e) => {
|
|
238
|
+
e.currentTarget.style.opacity = "0.9";
|
|
239
|
+
}, onPointerLeave: (e) => {
|
|
240
|
+
e.currentTarget.style.opacity = "0.55";
|
|
241
|
+
}, children: isCollapsed ? '▶' : '▼' }), node.id !== prefabData.root.id && (_jsx("span", { style: styles.dragHandle, onPointerEnter: (e) => {
|
|
242
|
+
e.currentTarget.style.opacity = "0.9";
|
|
243
|
+
}, onPointerLeave: (e) => {
|
|
244
|
+
e.currentTarget.style.opacity = "0.4";
|
|
245
|
+
}, children: "\u22EE\u22EE" })), _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
246
|
};
|
|
137
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", {
|
|
247
|
+
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) => {
|
|
248
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
249
|
+
}, onPointerLeave: (e) => {
|
|
250
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.05)";
|
|
251
|
+
}, 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) => {
|
|
252
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
253
|
+
}, onPointerLeave: (e) => {
|
|
254
|
+
e.currentTarget.style.background = "transparent";
|
|
255
|
+
}, 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) => {
|
|
256
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
257
|
+
}, onPointerLeave: (e) => {
|
|
258
|
+
e.currentTarget.style.background = "transparent";
|
|
259
|
+
}, children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, styles.menuItem), styles.menuItemDanger), onClick: () => handleDelete(contextMenu.nodeId), onPointerEnter: (e) => {
|
|
260
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
261
|
+
}, onPointerLeave: (e) => {
|
|
262
|
+
e.currentTarget.style.background = "transparent";
|
|
263
|
+
}, children: "Delete" })] }))] }))] }));
|
|
138
264
|
}
|
|
139
265
|
// --- Helpers ---
|
|
140
266
|
function findNode(root, id) {
|