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.
Files changed (33) hide show
  1. package/README.md +88 -113
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/tools/prefabeditor/EditorTree.js +27 -15
  5. package/dist/tools/prefabeditor/EditorUI.js +2 -8
  6. package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -4
  7. package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
  8. package/dist/tools/prefabeditor/PrefabEditor.js +128 -59
  9. package/dist/tools/prefabeditor/PrefabRoot.js +51 -33
  10. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
  11. package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
  12. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
  13. package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
  14. package/dist/tools/prefabeditor/components/RotatorComponent.d.ts +3 -0
  15. package/dist/tools/prefabeditor/components/RotatorComponent.js +42 -0
  16. package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
  17. package/dist/tools/prefabeditor/components/TransformComponent.js +28 -3
  18. package/dist/tools/prefabeditor/components/index.js +2 -0
  19. package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
  20. package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
  21. package/package.json +8 -8
  22. package/src/index.ts +4 -0
  23. package/src/tools/prefabeditor/EditorTree.tsx +39 -16
  24. package/src/tools/prefabeditor/EditorUI.tsx +2 -27
  25. package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
  26. package/src/tools/prefabeditor/PrefabEditor.tsx +202 -86
  27. package/src/tools/prefabeditor/PrefabRoot.tsx +62 -54
  28. package/src/tools/prefabeditor/components/ComponentRegistry.ts +7 -1
  29. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
  30. package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
  31. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
  32. package/src/tools/prefabeditor/components/TransformComponent.tsx +69 -16
  33. 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
- ```tsx
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
- const LaserComponent: Component = {
115
- name: 'Laser',
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
- <input
118
- value={component.properties.damage}
119
- onChange={e => onUpdate({ damage: +e.target.value })}
120
- />
121
- ),
122
- View: ({ properties }) => (
123
- <pointLight color="red" intensity={properties.damage} />
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
- defaultProperties: { damage: 10 }
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
- import { registerComponent } from 'react-three-game';
130
- registerComponent(LaserComponent);
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
- - Supports GLB/GLTF (Draco compression) and FBX
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(); // Cannot drag root
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
- // Check cycle again on the fresh tree
196
- const draggedNodeRef = findNode(newRoot, draggedId);
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` }), onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), draggable: node.id !== prefabData.root.id, onDragStart: (e) => handleDragStart(e, node.id), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), onPointerEnter: (e) => {
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 ? '▶' : '▼' }), _jsx("span", { style: styles.idText, children: node.id })] }), !isCollapsed && node.children && (_jsx("div", { children: node.children.map(child => renderNode(child, depth + 1)) }))] }, node.id));
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" })] }), _jsxs("div", { style: s.section, children: [_jsx("label", { style: s.label, children: "Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign(Object.assign({}, s.button), { flex: 1 }), (transformMode === mode ? s.buttonActive : null)), onPointerEnter: (e) => {
183
- if (transformMode !== mode)
184
- e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
185
- }, onPointerLeave: (e) => {
186
- if (transformMode !== mode)
187
- e.currentTarget.style.background = 'transparent';
188
- }, children: mode[0].toUpperCase() }, mode))) })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
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 * as THREE from 'three';
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]: THREE.Object3D;
16
+ [filename: string]: Object3D;
17
17
  };
18
18
  onSelect?: (id: string | null) => void;
19
- registerRef?: (id: string, obj: THREE.Object3D | null) => void;
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<THREE.Group<THREE.Object3DEventMap>>>;
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 THREE.Matrix4().copy(root.matrixWorld).invert();
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 THREE.Mesh(geom, obj.material);
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
- // Restrict meshes to just this model's parts for this Merged
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
- // Physics instancing stays the same
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
- // Non-physics instanced visuals: per-instance group using Merged's Instance components
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) => { e.stopPropagation(); clickValid.current = true; };
133
- const handlePointerMove = () => { if (clickValid.current)
134
- clickValid.current = false; };
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
- // --- GameInstance: just registers an instance, renders nothing ---
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 here provider will render visuals for all instances
178
+ // No visual rendering - provider handles all instanced visuals
171
179
  return null;
172
180
  });