react-three-game 0.0.16 → 0.0.18
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 +88 -113
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/tools/prefabeditor/EditorTree.js +27 -15
- package/dist/tools/prefabeditor/EditorUI.js +2 -8
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
- package/dist/tools/prefabeditor/PrefabEditor.js +128 -59
- package/dist/tools/prefabeditor/PrefabRoot.js +51 -33
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
- package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
- package/dist/tools/prefabeditor/components/RotatorComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/RotatorComponent.js +42 -0
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
- package/dist/tools/prefabeditor/components/TransformComponent.js +28 -3
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
- package/package.json +8 -8
- package/src/index.ts +4 -0
- package/src/tools/prefabeditor/EditorTree.tsx +39 -16
- package/src/tools/prefabeditor/EditorUI.tsx +2 -27
- package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
- package/src/tools/prefabeditor/PrefabEditor.tsx +202 -86
- package/src/tools/prefabeditor/PrefabRoot.tsx +62 -54
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +7 -1
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
- package/src/tools/prefabeditor/components/TransformComponent.tsx +69 -16
- package/src/tools/prefabeditor/components/index.ts +2 -0
package/README.md
CHANGED
|
@@ -30,14 +30,6 @@ Scenes are JSON prefabs. Components are registered modules. Hierarchy is declara
|
|
|
30
30
|
}} />
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
## Styling
|
|
34
|
-
|
|
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.
|
|
36
|
-
|
|
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).
|
|
40
|
-
|
|
41
33
|
## Quick Start
|
|
42
34
|
|
|
43
35
|
```bash
|
|
@@ -108,28 +100,100 @@ interface GameObject {
|
|
|
108
100
|
|
|
109
101
|
## Custom Components
|
|
110
102
|
|
|
111
|
-
|
|
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
|
|
112
110
|
import { Component } from 'react-three-game';
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
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
|
+
|
|
116
130
|
Editor: ({ component, onUpdate }) => (
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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>
|
|
124
148
|
),
|
|
125
|
-
|
|
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' }
|
|
126
165
|
};
|
|
127
166
|
|
|
128
|
-
// Register
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
183
|
```
|
|
132
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
|
+
|
|
133
197
|
## Built-in Components
|
|
134
198
|
|
|
135
199
|
| Component | Properties |
|
|
@@ -158,41 +222,13 @@ Transform gizmos (T/R/S keys), drag-to-reorder tree, import/export JSON, edit/pl
|
|
|
158
222
|
### Transform Hierarchy
|
|
159
223
|
- Local transforms stored in JSON (relative to parent)
|
|
160
224
|
- World transforms computed at runtime via matrix multiplication
|
|
161
|
-
- `computeParentWorldMatrix(root, targetId)` traverses tree for parent's world matrix
|
|
162
225
|
- TransformControls extract world matrix → compute parent inverse → derive new local transform
|
|
163
226
|
|
|
164
227
|
### GPU Instancing
|
|
165
|
-
Enable with `model.properties.instanced = true
|
|
166
|
-
```json
|
|
167
|
-
{
|
|
168
|
-
"components": {
|
|
169
|
-
"model": {
|
|
170
|
-
"type": "Model",
|
|
171
|
-
"properties": {
|
|
172
|
-
"filename": "tree.glb",
|
|
173
|
-
"instanced": true
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
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>`.
|
|
180
229
|
|
|
181
230
|
### Model Loading
|
|
182
|
-
|
|
183
|
-
- Singleton loaders in `modelLoader.ts`
|
|
184
|
-
- Draco decoder from `https://www.gstatic.com/draco/v1/decoders/`
|
|
185
|
-
- Auto-loads when `model.properties.filename` detected
|
|
186
|
-
|
|
187
|
-
### WebGPU Renderer
|
|
188
|
-
```tsx
|
|
189
|
-
<Canvas gl={async ({ canvas }) => {
|
|
190
|
-
const renderer = new WebGPURenderer({ canvas, shadowMap: true });
|
|
191
|
-
await renderer.init(); // Required
|
|
192
|
-
return renderer;
|
|
193
|
-
}}>
|
|
194
|
-
```
|
|
195
|
-
Use `MeshStandardNodeMaterial` not `MeshStandardMaterial`.
|
|
231
|
+
Supports GLB/GLTF (with Draco compression) and FBX. Models auto-load when `model.properties.filename` is detected.
|
|
196
232
|
|
|
197
233
|
## Patterns
|
|
198
234
|
|
|
@@ -202,15 +238,6 @@ import levelData from './prefabs/arena.json';
|
|
|
202
238
|
<PrefabRoot data={levelData} />
|
|
203
239
|
```
|
|
204
240
|
|
|
205
|
-
### Mix with React Components
|
|
206
|
-
```jsx
|
|
207
|
-
<Physics>
|
|
208
|
-
<PrefabRoot data={environment} />
|
|
209
|
-
<Player />
|
|
210
|
-
<AIEnemies />
|
|
211
|
-
</Physics>
|
|
212
|
-
```
|
|
213
|
-
|
|
214
241
|
### Update Prefab Nodes
|
|
215
242
|
```typescript
|
|
216
243
|
function updatePrefabNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject {
|
|
@@ -222,58 +249,6 @@ function updatePrefabNode(root: GameObject, id: string, update: (node: GameObjec
|
|
|
222
249
|
}
|
|
223
250
|
```
|
|
224
251
|
|
|
225
|
-
## AI Agent Reference
|
|
226
|
-
|
|
227
|
-
### Prefab JSON Schema
|
|
228
|
-
```typescript
|
|
229
|
-
{
|
|
230
|
-
"id": "unique-id",
|
|
231
|
-
"root": {
|
|
232
|
-
"id": "root-id",
|
|
233
|
-
"components": {
|
|
234
|
-
"transform": {
|
|
235
|
-
"type": "Transform",
|
|
236
|
-
"properties": {
|
|
237
|
-
"position": [x, y, z], // world units
|
|
238
|
-
"rotation": [x, y, z], // radians
|
|
239
|
-
"scale": [x, y, z] // multipliers
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
"geometry": {
|
|
243
|
-
"type": "Geometry",
|
|
244
|
-
"properties": {
|
|
245
|
-
"geometryType": "box" | "sphere" | "plane" | "cylinder" | "cone" | "torus",
|
|
246
|
-
"args": [/* geometry-specific */]
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
"material": {
|
|
250
|
-
"type": "Material",
|
|
251
|
-
"properties": {
|
|
252
|
-
"color": "#rrggbb",
|
|
253
|
-
"texture": "/path/to/texture.jpg",
|
|
254
|
-
"metalness": 0.0-1.0,
|
|
255
|
-
"roughness": 0.0-1.0
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
"physics": {
|
|
259
|
-
"type": "Physics",
|
|
260
|
-
"properties": {
|
|
261
|
-
"type": "dynamic" | "fixed"
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
"model": {
|
|
265
|
-
"type": "Model",
|
|
266
|
-
"properties": {
|
|
267
|
-
"filename": "/models/asset.glb",
|
|
268
|
-
"instanced": true
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
},
|
|
272
|
-
"children": [/* recursive GameObjects */]
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
252
|
## Development
|
|
278
253
|
|
|
279
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';
|
|
@@ -65,6 +65,17 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
65
65
|
overflow: "hidden",
|
|
66
66
|
textOverflow: "ellipsis",
|
|
67
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
|
+
},
|
|
68
79
|
contextMenu: {
|
|
69
80
|
position: "fixed",
|
|
70
81
|
zIndex: 50,
|
|
@@ -165,38 +176,36 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
165
176
|
};
|
|
166
177
|
// Drag and Drop
|
|
167
178
|
const handleDragStart = (e, id) => {
|
|
168
|
-
e.stopPropagation();
|
|
169
179
|
if (id === prefabData.root.id) {
|
|
170
|
-
e.preventDefault();
|
|
180
|
+
e.preventDefault();
|
|
171
181
|
return;
|
|
172
182
|
}
|
|
173
|
-
setDraggedId(id);
|
|
174
183
|
e.dataTransfer.effectAllowed = "move";
|
|
184
|
+
e.dataTransfer.setData("text/plain", id);
|
|
185
|
+
setDraggedId(id);
|
|
186
|
+
};
|
|
187
|
+
const handleDragEnd = () => {
|
|
188
|
+
setDraggedId(null);
|
|
175
189
|
};
|
|
176
190
|
const handleDragOver = (e, targetId) => {
|
|
177
|
-
e.preventDefault();
|
|
178
|
-
e.stopPropagation();
|
|
179
191
|
if (!draggedId || draggedId === targetId)
|
|
180
192
|
return;
|
|
181
|
-
// Check for cycles: target cannot be a descendant of dragged node
|
|
182
193
|
const draggedNode = findNode(prefabData.root, draggedId);
|
|
183
194
|
if (draggedNode && findNode(draggedNode, targetId))
|
|
184
195
|
return;
|
|
196
|
+
e.preventDefault();
|
|
185
197
|
e.dataTransfer.dropEffect = "move";
|
|
186
198
|
};
|
|
187
199
|
const handleDrop = (e, targetId) => {
|
|
188
|
-
e.preventDefault();
|
|
189
|
-
e.stopPropagation();
|
|
190
200
|
if (!draggedId || draggedId === targetId)
|
|
191
201
|
return;
|
|
202
|
+
e.preventDefault();
|
|
192
203
|
setPrefabData(prev => {
|
|
193
204
|
var _a;
|
|
194
205
|
const newRoot = JSON.parse(JSON.stringify(prev.root));
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (draggedNodeRef && findNode(draggedNodeRef, targetId))
|
|
206
|
+
const draggedNode = findNode(newRoot, draggedId);
|
|
207
|
+
if (draggedNode && findNode(draggedNode, targetId))
|
|
198
208
|
return prev;
|
|
199
|
-
// Remove from old parent
|
|
200
209
|
const parent = findParent(newRoot, draggedId);
|
|
201
210
|
if (!parent)
|
|
202
211
|
return prev;
|
|
@@ -204,7 +213,6 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
204
213
|
if (!nodeToMove)
|
|
205
214
|
return prev;
|
|
206
215
|
parent.children = parent.children.filter(c => c.id !== draggedId);
|
|
207
|
-
// Add to new parent
|
|
208
216
|
const target = findNode(newRoot, targetId);
|
|
209
217
|
if (target) {
|
|
210
218
|
target.children = target.children || [];
|
|
@@ -220,7 +228,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
220
228
|
const isSelected = node.id === selectedId;
|
|
221
229
|
const isCollapsed = collapsedIds.has(node.id);
|
|
222
230
|
const hasChildren = node.children && node.children.length > 0;
|
|
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
|
|
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) => {
|
|
224
232
|
if (!isSelected)
|
|
225
233
|
e.currentTarget.style.background = "rgba(255,255,255,0.06)";
|
|
226
234
|
}, onPointerLeave: (e) => {
|
|
@@ -230,7 +238,11 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
230
238
|
e.currentTarget.style.opacity = "0.9";
|
|
231
239
|
}, onPointerLeave: (e) => {
|
|
232
240
|
e.currentTarget.style.opacity = "0.55";
|
|
233
|
-
}, children: isCollapsed ? '▶' : '▼' }),
|
|
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));
|
|
234
246
|
};
|
|
235
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) => {
|
|
236
248
|
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
@@ -179,13 +179,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
179
179
|
setAddComponentType(available[0] || "");
|
|
180
180
|
}
|
|
181
181
|
}, [componentKeys, addComponentType, node.components, allComponentKeys]);
|
|
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" })] }),
|
|
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]) => {
|
|
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" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
189
183
|
if (!comp)
|
|
190
184
|
return null;
|
|
191
185
|
const componentDef = ALL_COMPONENTS[comp.type];
|
|
@@ -196,7 +190,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
196
190
|
const components = Object.assign({}, n.components);
|
|
197
191
|
delete components[key];
|
|
198
192
|
return Object.assign(Object.assign({}, n), { components });
|
|
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));
|
|
193
|
+
}), 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, transformMode: transformMode, setTransformMode: setTransformMode })) : null] }, key));
|
|
200
194
|
}), _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: () => {
|
|
201
195
|
var _a;
|
|
202
196
|
if (!addComponentType)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import
|
|
2
|
+
import { Object3D, Group } from "three";
|
|
3
3
|
export type InstanceData = {
|
|
4
4
|
id: string;
|
|
5
5
|
position: [number, number, number];
|
|
@@ -13,10 +13,10 @@ export type InstanceData = {
|
|
|
13
13
|
export declare function GameInstanceProvider({ children, models, onSelect, registerRef }: {
|
|
14
14
|
children: React.ReactNode;
|
|
15
15
|
models: {
|
|
16
|
-
[filename: string]:
|
|
16
|
+
[filename: string]: Object3D;
|
|
17
17
|
};
|
|
18
18
|
onSelect?: (id: string | null) => void;
|
|
19
|
-
registerRef?: (id: string, obj:
|
|
19
|
+
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
20
20
|
}): import("react/jsx-runtime").JSX.Element;
|
|
21
21
|
export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
22
22
|
id: string;
|
|
@@ -27,4 +27,4 @@ export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
|
27
27
|
physics?: {
|
|
28
28
|
type: "dynamic" | "fixed";
|
|
29
29
|
};
|
|
30
|
-
} & React.RefAttributes<
|
|
30
|
+
} & React.RefAttributes<Group<import("three").Object3DEventMap>>>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
|
|
3
3
|
import { Merged } from '@react-three/drei';
|
|
4
|
-
import * as THREE from 'three';
|
|
5
4
|
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
5
|
+
import { Mesh, Matrix4 } from "three";
|
|
6
|
+
// Helper functions for comparison
|
|
6
7
|
function arrayEquals(a, b) {
|
|
7
8
|
if (a === b)
|
|
8
9
|
return true;
|
|
@@ -30,6 +31,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
30
31
|
setInstances(prev => {
|
|
31
32
|
const idx = prev.findIndex(i => i.id === instance.id);
|
|
32
33
|
if (idx !== -1) {
|
|
34
|
+
// Update existing if changed
|
|
33
35
|
if (instanceEquals(prev[idx], instance)) {
|
|
34
36
|
return prev;
|
|
35
37
|
}
|
|
@@ -37,6 +39,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
37
39
|
copy[idx] = instance;
|
|
38
40
|
return copy;
|
|
39
41
|
}
|
|
42
|
+
// Add new
|
|
40
43
|
return [...prev, instance];
|
|
41
44
|
});
|
|
42
45
|
}, []);
|
|
@@ -47,14 +50,14 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
47
50
|
return prev.filter(i => i.id !== id);
|
|
48
51
|
});
|
|
49
52
|
}, []);
|
|
50
|
-
// Flatten all model meshes once
|
|
53
|
+
// Flatten all model meshes once (models → flat mesh parts)
|
|
51
54
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
52
55
|
const flatMeshes = {};
|
|
53
56
|
const modelParts = {};
|
|
54
57
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
55
58
|
const root = model;
|
|
56
59
|
root.updateWorldMatrix(false, true);
|
|
57
|
-
const rootInverse = new
|
|
60
|
+
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
58
61
|
let partIndex = 0;
|
|
59
62
|
root.traverse((obj) => {
|
|
60
63
|
if (obj.isMesh) {
|
|
@@ -62,7 +65,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
62
65
|
const relativeTransform = obj.matrixWorld.clone().premultiply(rootInverse);
|
|
63
66
|
geom.applyMatrix4(relativeTransform);
|
|
64
67
|
const partKey = `${modelKey}__${partIndex}`;
|
|
65
|
-
flatMeshes[partKey] = new
|
|
68
|
+
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
66
69
|
partIndex++;
|
|
67
70
|
}
|
|
68
71
|
});
|
|
@@ -70,7 +73,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
70
73
|
});
|
|
71
74
|
return { flatMeshes, modelParts };
|
|
72
75
|
}, [models]);
|
|
73
|
-
// Group instances by meshPath + physics type
|
|
76
|
+
// Group instances by meshPath + physics type for batch rendering
|
|
74
77
|
const grouped = useMemo(() => {
|
|
75
78
|
var _a;
|
|
76
79
|
const groups = {};
|
|
@@ -104,7 +107,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
104
107
|
const partCount = modelParts[modelKey] || 0;
|
|
105
108
|
if (partCount === 0)
|
|
106
109
|
return null;
|
|
107
|
-
//
|
|
110
|
+
// Create mesh subset for this specific model
|
|
108
111
|
const meshesForModel = {};
|
|
109
112
|
for (let i = 0; i < partCount; i++) {
|
|
110
113
|
const partKey = `${modelKey}__${i}`;
|
|
@@ -113,7 +116,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
|
|
|
113
116
|
return (_jsx(Merged, { meshes: meshesForModel, castShadow: true, receiveShadow: true, children: (instancesMap) => (_jsx(NonPhysicsInstancedGroup, { modelKey: modelKey, group: group, partCount: partCount, instancesMap: instancesMap, onSelect: onSelect, registerRef: registerRef })) }, key));
|
|
114
117
|
})] }));
|
|
115
118
|
}
|
|
116
|
-
//
|
|
119
|
+
// Render physics-enabled instances using InstancedRigidBodies
|
|
117
120
|
function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
118
121
|
const instances = useMemo(() => group.instances.map(inst => ({
|
|
119
122
|
key: inst.id,
|
|
@@ -126,12 +129,17 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
|
126
129
|
return (_jsx("instancedMesh", { args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
|
|
127
130
|
}) }));
|
|
128
131
|
}
|
|
129
|
-
//
|
|
132
|
+
// Render non-physics instances using Merged's per-instance groups
|
|
130
133
|
function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef }) {
|
|
131
134
|
const clickValid = useRef(false);
|
|
132
|
-
const handlePointerDown = (e) => {
|
|
133
|
-
|
|
134
|
-
clickValid.current =
|
|
135
|
+
const handlePointerDown = (e) => {
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
clickValid.current = true;
|
|
138
|
+
};
|
|
139
|
+
const handlePointerMove = () => {
|
|
140
|
+
if (clickValid.current)
|
|
141
|
+
clickValid.current = false;
|
|
142
|
+
};
|
|
135
143
|
const handlePointerUp = (e, id) => {
|
|
136
144
|
if (clickValid.current) {
|
|
137
145
|
e.stopPropagation();
|
|
@@ -146,7 +154,7 @@ function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, on
|
|
|
146
154
|
return _jsx(Instance, {}, i);
|
|
147
155
|
}) }, inst.id))) }));
|
|
148
156
|
}
|
|
149
|
-
//
|
|
157
|
+
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
150
158
|
export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
151
159
|
const ctx = useContext(GameInstanceContext);
|
|
152
160
|
const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
|
|
@@ -167,6 +175,6 @@ export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation
|
|
|
167
175
|
removeInstance(instance.id);
|
|
168
176
|
};
|
|
169
177
|
}, [addInstance, removeInstance, instance]);
|
|
170
|
-
// No visual
|
|
178
|
+
// No visual rendering - provider handles all instanced visuals
|
|
171
179
|
return null;
|
|
172
180
|
});
|