react-three-game 0.0.65 → 0.0.66
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/LICENSE +2 -660
- package/README.md +164 -91
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/tools/assetviewer/page.js +4 -4
- package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
- package/dist/tools/prefabeditor/EditorTree.js +17 -3
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
- package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
- package/dist/tools/prefabeditor/EditorUI.js +3 -7
- package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
- package/dist/tools/prefabeditor/GameEvents.js +2 -1
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +44 -12
- package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +36 -119
- package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
- package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
- package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
- package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
- package/dist/tools/prefabeditor/components/Input.js +71 -38
- package/dist/tools/prefabeditor/components/MaterialComponent.js +3 -3
- package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/types.d.ts +1 -0
- package/dist/tools/prefabeditor/utils.d.ts +7 -1
- package/dist/tools/prefabeditor/utils.js +34 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,127 @@ export default function Home() {
|
|
|
68
68
|
|
|
69
69
|
`GameCanvas` provides the library's WebGPU canvas setup.
|
|
70
70
|
|
|
71
|
+
## Prefab Editor
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
import { useRef } from 'react';
|
|
75
|
+
import { PrefabEditor } from 'react-three-game';
|
|
76
|
+
|
|
77
|
+
// Standalone editor
|
|
78
|
+
<PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
|
|
79
|
+
|
|
80
|
+
// Canvas-only editing mode (keeps canvas selection/gizmos, hides hierarchy + inspector + toolbar)
|
|
81
|
+
<PrefabEditor initialPrefab={sceneData} showUI={false} />
|
|
82
|
+
|
|
83
|
+
// With custom R3F components
|
|
84
|
+
<PrefabEditor initialPrefab={sceneData}>
|
|
85
|
+
<CustomComponent />
|
|
86
|
+
</PrefabEditor>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Embedded / Headless Editor
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useRef } from 'react';
|
|
93
|
+
import type { Object3D } from 'three';
|
|
94
|
+
import { PrefabEditor, type PrefabEditorRef } from 'react-three-game';
|
|
95
|
+
|
|
96
|
+
export function EmbeddedEditor({ prefab, onPrefabChange }: {
|
|
97
|
+
prefab: any;
|
|
98
|
+
onPrefabChange: (nextPrefab: any) => void;
|
|
99
|
+
}) {
|
|
100
|
+
const editorRef = useRef<PrefabEditorRef>(null);
|
|
101
|
+
|
|
102
|
+
function loadScene(nextPrefab: any) {
|
|
103
|
+
editorRef.current?.replacePrefab(nextPrefab);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function importRuntimeModel(model: Object3D) {
|
|
107
|
+
editorRef.current?.addModel('models/runtime/chair.glb', model, {
|
|
108
|
+
name: 'Chair',
|
|
109
|
+
parentId: 'root',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div style={{ position: 'relative', height: 600 }}>
|
|
115
|
+
<div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
|
|
116
|
+
<button onClick={() => loadScene(prefab)}>Reload Scene</button>
|
|
117
|
+
<button onClick={() => editorRef.current?.exportGLBData()}>Export GLB Data</button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<PrefabEditor
|
|
121
|
+
ref={editorRef}
|
|
122
|
+
initialPrefab={prefab}
|
|
123
|
+
onPrefabChange={onPrefabChange}
|
|
124
|
+
showUI={false}
|
|
125
|
+
physics={false}
|
|
126
|
+
enableWindowDrop={false}
|
|
127
|
+
canvasProps={{ style: { height: '100%', width: '100%' } }}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`showUI={false}` hides the built-in editor chrome but keeps canvas selection, transform controls, and scene interaction. For embedded tools, use the editor ref instead of reaching through `rootRef`:
|
|
135
|
+
|
|
136
|
+
- `replacePrefab(prefab)` replaces the current scene through the editor state pipeline and resets editor history/selection.
|
|
137
|
+
- `setPrefab(prefab)` updates the current prefab in place and preserves selection when the selected node ID still exists.
|
|
138
|
+
- `addModel(path, model, options?)` creates a model node and injects the runtime asset in one step.
|
|
139
|
+
- `addTexture(path, texture, options?)` creates a textured plane node and injects the runtime texture in one step.
|
|
140
|
+
- `exportGLBData()` returns the GLB `ArrayBuffer` without triggering a download.
|
|
141
|
+
- `canvasProps` forwards canvas-level sizing, camera, event, and style props to `GameCanvas`.
|
|
142
|
+
|
|
143
|
+
### Editor State Bridge
|
|
144
|
+
|
|
145
|
+
Compose small helper components inside `PrefabEditor` when custom UI needs to control the editor.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { useEffect } from 'react';
|
|
149
|
+
import {
|
|
150
|
+
PrefabEditor,
|
|
151
|
+
useEditorContext,
|
|
152
|
+
type EditorContextType,
|
|
153
|
+
} from 'react-three-game';
|
|
154
|
+
|
|
155
|
+
function EditorStateBridge({ onReady }: { onReady: (editorState: EditorContextType) => void }) {
|
|
156
|
+
const editorState = useEditorContext();
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
onReady(editorState);
|
|
160
|
+
}, [editorState, onReady]);
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function CustomEditor() {
|
|
166
|
+
return (
|
|
167
|
+
<PrefabEditor initialPrefab={sceneData} showUI={false}>
|
|
168
|
+
<EditorStateBridge onReady={({ setTransformMode }) => setTransformMode('translate')} />
|
|
169
|
+
</PrefabEditor>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `useEditorContext()` is available to children rendered inside `PrefabEditor`.
|
|
175
|
+
- Use it for editor state: transform mode, edit/play checks, focus actions, and export buttons.
|
|
176
|
+
- Keep bridge components small and focused on one editor concern.
|
|
177
|
+
|
|
178
|
+
### Edit Mode vs Play Mode
|
|
179
|
+
|
|
180
|
+
- edit mode: selection, inspector, transform gizmos, editor shortcuts
|
|
181
|
+
- play mode: physics and runtime behavior
|
|
182
|
+
- skip editor-only code when `editMode` is `false`
|
|
183
|
+
- use `showUI={false}` for custom shells
|
|
184
|
+
- use `enableWindowDrop={false}` when the host app owns drag/drop
|
|
185
|
+
|
|
186
|
+
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
|
|
187
|
+
|
|
188
|
+
Editor menu structure:
|
|
189
|
+
- `Menu > File`: new scene, load/save prefab JSON, load prefab into scene
|
|
190
|
+
- `Menu > Export`: `GLB`, `PNG`
|
|
191
|
+
|
|
71
192
|
## GameObject Schema
|
|
72
193
|
|
|
73
194
|
```typescript
|
|
@@ -95,10 +216,52 @@ interface GameObject {
|
|
|
95
216
|
| Transform | `position`, `rotation`, `scale` — all `[x,y,z]` arrays, rotation in radians |
|
|
96
217
|
| Geometry | `geometryType`: box/sphere/plane/cylinder, `args`: dimension array |
|
|
97
218
|
| Material | `color`, `texture?`, `metalness?`, `roughness?` |
|
|
98
|
-
| Physics | `type`: dynamic/fixed/kinematicPosition/kinematicVelocity, `mass?`, `restitution?` (bounciness), `friction?`, plus any Rapier props |
|
|
219
|
+
| Physics | `type`: dynamic/fixed/kinematicPosition/kinematicVelocity, `mass?`, `restitution?` (bounciness), `friction?`, `linearVelocity?`, `angularVelocity?`, plus any Rapier props |
|
|
99
220
|
| Model | `filename` (GLB/FBX path), `instanced?` for GPU batching |
|
|
100
221
|
| SpotLight | `color`, `intensity`, `angle`, `penumbra` |
|
|
101
222
|
|
|
223
|
+
## Tree Utilities
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { findNode, updateNode, updateNodeById, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
|
|
227
|
+
|
|
228
|
+
const node = findNode(root, nodeId);
|
|
229
|
+
const updated = updateNode(root, nodeId, n => ({ ...n, disabled: true })); // or updateNodeById
|
|
230
|
+
const afterDelete = deleteNode(root, nodeId);
|
|
231
|
+
const cloned = cloneNode(node);
|
|
232
|
+
const glbData = await exportGLBData(sceneRoot); // export scene to GLB ArrayBuffer
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Scene State Updates
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
function VisibilityToggle({
|
|
239
|
+
editorRef,
|
|
240
|
+
visible,
|
|
241
|
+
}: {
|
|
242
|
+
editorRef: React.RefObject<PrefabEditorRef | null>;
|
|
243
|
+
visible: boolean;
|
|
244
|
+
}) {
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
const editor = editorRef.current;
|
|
247
|
+
if (!editor) return;
|
|
248
|
+
|
|
249
|
+
const prefab = editor.prefab;
|
|
250
|
+
const root = updateNodeById(prefab.root, 'helper-grid', node => ({
|
|
251
|
+
...node,
|
|
252
|
+
disabled: !visible,
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
editor.setPrefab({ ...prefab, root });
|
|
256
|
+
}, [editorRef, visible]);
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- Use `updateNode(...)` or `updateNodeById(...)` for scene state changes inside the prefab tree.
|
|
263
|
+
- For sensor and collision event patterns, see advanced physics examples.
|
|
264
|
+
|
|
102
265
|
## Custom Components
|
|
103
266
|
|
|
104
267
|
```tsx
|
|
@@ -160,102 +323,12 @@ The `FieldRenderer` component auto-generates editor UI from a field schema:
|
|
|
160
323
|
}
|
|
161
324
|
```
|
|
162
325
|
|
|
163
|
-
## Prefab Editor
|
|
164
|
-
|
|
165
|
-
```jsx
|
|
166
|
-
import { useRef } from 'react';
|
|
167
|
-
import { PrefabEditor } from 'react-three-game';
|
|
168
|
-
|
|
169
|
-
// Standalone editor
|
|
170
|
-
<PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
|
|
171
|
-
|
|
172
|
-
// Canvas-only editing mode (keeps canvas selection/gizmos, hides hierarchy + inspector + toolbar)
|
|
173
|
-
<PrefabEditor initialPrefab={sceneData} showUI={false} />
|
|
174
|
-
|
|
175
|
-
// With custom R3F components
|
|
176
|
-
<PrefabEditor initialPrefab={sceneData}>
|
|
177
|
-
<CustomComponent />
|
|
178
|
-
</PrefabEditor>
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Embedded / Headless Editor
|
|
182
|
-
|
|
183
|
-
```tsx
|
|
184
|
-
import { useRef } from 'react';
|
|
185
|
-
import type { Object3D } from 'three';
|
|
186
|
-
import { PrefabEditor, type PrefabEditorRef } from 'react-three-game';
|
|
187
|
-
|
|
188
|
-
export function EmbeddedEditor({ prefab, onPrefabChange }: {
|
|
189
|
-
prefab: any;
|
|
190
|
-
onPrefabChange: (nextPrefab: any) => void;
|
|
191
|
-
}) {
|
|
192
|
-
const editorRef = useRef<PrefabEditorRef>(null);
|
|
193
|
-
|
|
194
|
-
function loadScene(nextPrefab: any) {
|
|
195
|
-
editorRef.current?.replacePrefab(nextPrefab);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function importRuntimeModel(model: Object3D) {
|
|
199
|
-
editorRef.current?.addModel('models/runtime/chair.glb', model, {
|
|
200
|
-
name: 'Chair',
|
|
201
|
-
parentId: 'root',
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<div style={{ position: 'relative', height: 600 }}>
|
|
207
|
-
<div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
|
|
208
|
-
<button onClick={() => loadScene(prefab)}>Reload Scene</button>
|
|
209
|
-
<button onClick={() => editorRef.current?.exportGLBData()}>Export GLB Data</button>
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<PrefabEditor
|
|
213
|
-
ref={editorRef}
|
|
214
|
-
initialPrefab={prefab}
|
|
215
|
-
onPrefabChange={onPrefabChange}
|
|
216
|
-
showUI={false}
|
|
217
|
-
physics={false}
|
|
218
|
-
enableWindowDrop={false}
|
|
219
|
-
canvasProps={{ style: { height: '100%', width: '100%' } }}
|
|
220
|
-
/>
|
|
221
|
-
</div>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
`showUI={false}` hides the built-in editor chrome but keeps canvas selection, transform controls, and scene interaction. For embedded tools, use the editor ref instead of reaching through `rootRef`:
|
|
227
|
-
|
|
228
|
-
- `replacePrefab(prefab)` replaces the current scene through the editor state pipeline and resets editor history/selection.
|
|
229
|
-
- `addModel(path, model, options?)` creates a model node and injects the runtime asset in one step.
|
|
230
|
-
- `addTexture(path, texture, options?)` creates a textured plane node and injects the runtime texture in one step.
|
|
231
|
-
- `exportGLBData()` returns the GLB `ArrayBuffer` without triggering a download.
|
|
232
|
-
- `canvasProps` forwards canvas-level sizing, camera, event, and style props to `GameCanvas`.
|
|
233
|
-
- `setPrefab(prefab)` remains as a backward-compatible alias for `replacePrefab(prefab)`.
|
|
234
|
-
|
|
235
|
-
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
|
|
236
|
-
|
|
237
|
-
Editor menu structure:
|
|
238
|
-
- `Menu > File`: new scene, load/save prefab JSON, load prefab into scene
|
|
239
|
-
- `Menu > Export`: `GLB`, `PNG`
|
|
240
|
-
|
|
241
326
|
## Internals
|
|
242
327
|
|
|
243
328
|
- **Transforms**: Local in JSON, world computed via matrix multiplication
|
|
244
329
|
- **Instancing**: `model.properties.instanced = true` switches the node to the batched instance path (`<Merged>` / `<InstancedRigidBodies>`) instead of the standard model render path
|
|
245
330
|
- **Models**: GLB/GLTF (Draco) and FBX auto-load from `filename`
|
|
246
331
|
|
|
247
|
-
## Tree Utilities
|
|
248
|
-
|
|
249
|
-
```typescript
|
|
250
|
-
import { findNode, updateNode, updateNodeById, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
|
|
251
|
-
|
|
252
|
-
const node = findNode(root, nodeId);
|
|
253
|
-
const updated = updateNode(root, nodeId, n => ({ ...n, disabled: true })); // or updateNodeById
|
|
254
|
-
const afterDelete = deleteNode(root, nodeId);
|
|
255
|
-
const cloned = cloneNode(node);
|
|
256
|
-
const glbData = await exportGLBData(sceneRoot); // export scene to GLB ArrayBuffer
|
|
257
|
-
```
|
|
258
|
-
|
|
259
332
|
## Development
|
|
260
333
|
|
|
261
334
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ export * from './helpers';
|
|
|
3
3
|
export { sound as soundManager } from './helpers/SoundManager';
|
|
4
4
|
export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
5
5
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
6
|
+
export { useEditorContext } from './tools/prefabeditor/EditorContext';
|
|
7
|
+
export type { EditorContextType } from './tools/prefabeditor/EditorContext';
|
|
6
8
|
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
7
|
-
export { FieldRenderer, FieldGroup,
|
|
9
|
+
export { FieldRenderer, FieldGroup, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
|
|
8
10
|
export * from './tools/prefabeditor/utils';
|
|
9
11
|
export type { ExportGLBOptions } from './tools/prefabeditor/utils';
|
|
10
12
|
export type { PrefabEditorAssetOptions, PrefabEditorProps, PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
|
|
@@ -13,7 +15,7 @@ export type { Component } from './tools/prefabeditor/components/ComponentRegistr
|
|
|
13
15
|
export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
|
|
14
16
|
export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
|
|
15
17
|
export { gameEvents, useGameEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
|
|
16
|
-
export type { GameEventType, GameEventMap, GameEventPayload, PhysicsEventType, PhysicsEventPayload } from './tools/prefabeditor/GameEvents';
|
|
18
|
+
export type { GameEventType, GameEventMap, GameEventPayload, PhysicsEventType, InteractionEventType, PhysicsEventPayload, ClickEventPayload } from './tools/prefabeditor/GameEvents';
|
|
17
19
|
export { entityEvents, useEntityEvent } from './tools/prefabeditor/GameEvents';
|
|
18
20
|
export type { EntityEventType, EntityEventPayload } from './tools/prefabeditor/GameEvents';
|
|
19
21
|
export * from './tools/dragdrop';
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,11 @@ export { sound as soundManager } from './helpers/SoundManager';
|
|
|
6
6
|
// Prefab Editor - Components
|
|
7
7
|
export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
8
8
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
9
|
+
export { useEditorContext } from './tools/prefabeditor/EditorContext';
|
|
9
10
|
// Prefab Editor - Component Registry
|
|
10
11
|
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
11
12
|
// Prefab Editor - Input Components
|
|
12
|
-
export { FieldRenderer, FieldGroup,
|
|
13
|
+
export { FieldRenderer, FieldGroup, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
|
|
13
14
|
// Prefab Editor - Styles & Utils
|
|
14
15
|
export * from './tools/prefabeditor/utils';
|
|
15
16
|
// Game Events (physics + custom events)
|
|
@@ -93,9 +93,9 @@ function TextureCard({ file, onSelect, basePath = "" }) {
|
|
|
93
93
|
const { ref, isInView } = useInView();
|
|
94
94
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
95
95
|
if (error) {
|
|
96
|
-
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#
|
|
96
|
+
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#c30000', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
|
|
97
97
|
}
|
|
98
|
-
return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#
|
|
98
|
+
return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#aeaeae', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { style: { flex: 1, position: 'relative' }, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })) : null }), _jsx("div", { style: styles.bottomLabel, children: file.split('/').pop() })] }));
|
|
99
99
|
}
|
|
100
100
|
function TextureSphere({ url, onError }) {
|
|
101
101
|
const [texture, setTexture] = useState(null);
|
|
@@ -119,9 +119,9 @@ function ModelCard({ file, onSelect, basePath = "", size = 60, }) {
|
|
|
119
119
|
const { ref, isInView } = useInView();
|
|
120
120
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
121
121
|
if (error) {
|
|
122
|
-
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#
|
|
122
|
+
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#c30000', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
|
|
123
123
|
}
|
|
124
|
-
return (_jsxs("div", { ref: ref, style: { width: size, aspectRatio: '1 / 1', backgroundColor: '#
|
|
124
|
+
return (_jsxs("div", { ref: ref, style: { width: size, aspectRatio: '1 / 1', backgroundColor: '#aeaeae', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 1 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: styles.bottomLabel, children: file.split('/').pop() })] }));
|
|
125
125
|
}
|
|
126
126
|
function ModelPreview({ url, onError }) {
|
|
127
127
|
const [model, setModel] = useState(null);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
interface EditorContextType {
|
|
1
|
+
export interface EditorContextType {
|
|
2
|
+
editMode: boolean;
|
|
2
3
|
transformMode: "translate" | "rotate" | "scale";
|
|
3
4
|
setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
|
|
4
5
|
snapResolution: number;
|
|
@@ -13,4 +14,3 @@ interface EditorContextType {
|
|
|
13
14
|
}
|
|
14
15
|
export declare const EditorContext: import("react").Context<EditorContextType | null>;
|
|
15
16
|
export declare function useEditorContext(): EditorContextType;
|
|
16
|
-
export {};
|
|
@@ -118,8 +118,19 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
118
118
|
if (selectedId === nodeId)
|
|
119
119
|
setSelectedId(null);
|
|
120
120
|
};
|
|
121
|
+
const toggleNodeFlag = (nodeId, key) => {
|
|
122
|
+
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, nodeId, node => (Object.assign(Object.assign({}, node), { [key]: !node[key] }))) })));
|
|
123
|
+
};
|
|
121
124
|
const handleToggleDisabled = (nodeId) => {
|
|
122
|
-
|
|
125
|
+
toggleNodeFlag(nodeId, 'disabled');
|
|
126
|
+
};
|
|
127
|
+
const handleToggleLocked = (nodeId) => {
|
|
128
|
+
var _a;
|
|
129
|
+
const willLock = !((_a = findNode(prefabData.root, nodeId)) === null || _a === void 0 ? void 0 : _a.locked);
|
|
130
|
+
toggleNodeFlag(nodeId, 'locked');
|
|
131
|
+
if (willLock && selectedId === nodeId) {
|
|
132
|
+
setSelectedId(null);
|
|
133
|
+
}
|
|
123
134
|
};
|
|
124
135
|
const closeContextMenu = () => setContextMenu(null);
|
|
125
136
|
const openContextMenu = (nodeId, x, y) => {
|
|
@@ -130,7 +141,10 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
130
141
|
setSelectedId(nodeId);
|
|
131
142
|
onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(nodeId);
|
|
132
143
|
};
|
|
133
|
-
const renderTreeNodeMenu = (nodeId, isRoot, onClose) =>
|
|
144
|
+
const renderTreeNodeMenu = (nodeId, isRoot, onClose) => {
|
|
145
|
+
var _a;
|
|
146
|
+
return (_jsx(TreeNodeMenu, { isRoot: isRoot, nodeId: nodeId, locked: (_a = findNode(prefabData.root, nodeId)) === null || _a === void 0 ? void 0 : _a.locked, onAddChild: handleAddChild, onFocus: handleFocus, onToggleLock: isRoot ? undefined : handleToggleLocked, onDuplicate: isRoot ? undefined : handleDuplicate, onDelete: isRoot ? undefined : handleDelete, onClose: onClose }));
|
|
147
|
+
};
|
|
134
148
|
const handleDragStart = (e, id) => {
|
|
135
149
|
if (id === prefabData.root.id)
|
|
136
150
|
return e.preventDefault();
|
|
@@ -203,7 +217,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
203
217
|
marginRight: 4,
|
|
204
218
|
cursor: 'pointer',
|
|
205
219
|
visibility: hasChildren ? 'visible' : 'hidden'
|
|
206
|
-
}, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id })] }), !isRoot && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Node Actions", style: {
|
|
220
|
+
}, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id }), node.locked && _jsx("span", { style: { marginLeft: 6, opacity: 0.6 }, children: "\uD83D\uDD12" })] }), !isRoot && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Node Actions", style: {
|
|
207
221
|
background: 'none',
|
|
208
222
|
border: 'none',
|
|
209
223
|
cursor: 'pointer',
|
|
@@ -12,11 +12,13 @@ export declare function MenuTriggerButton({ buttonRef, onToggle, title, style, c
|
|
|
12
12
|
style: React.CSSProperties;
|
|
13
13
|
children: React.ReactNode;
|
|
14
14
|
}): import("react/jsx-runtime").JSX.Element;
|
|
15
|
-
export declare function TreeNodeMenu({ isRoot, nodeId, onAddChild, onFocus, onDuplicate, onDelete, onClose, }: {
|
|
15
|
+
export declare function TreeNodeMenu({ isRoot, nodeId, locked, onAddChild, onFocus, onToggleLock, onDuplicate, onDelete, onClose, }: {
|
|
16
16
|
isRoot: boolean;
|
|
17
17
|
nodeId: string;
|
|
18
|
+
locked?: boolean;
|
|
18
19
|
onAddChild: (parentId: string) => void;
|
|
19
20
|
onFocus: (nodeId: string) => void;
|
|
21
|
+
onToggleLock?: (nodeId: string) => void;
|
|
20
22
|
onDuplicate?: (nodeId: string) => void;
|
|
21
23
|
onDelete?: (nodeId: string) => void;
|
|
22
24
|
onClose: () => void;
|
|
@@ -53,8 +53,8 @@ export function MenuTriggerButton({ buttonRef, onToggle, title, style, children,
|
|
|
53
53
|
onToggle();
|
|
54
54
|
}, title: title, children: children }));
|
|
55
55
|
}
|
|
56
|
-
export function TreeNodeMenu({ isRoot, nodeId, onAddChild, onFocus, onDuplicate, onDelete, onClose, }) {
|
|
57
|
-
return (_jsxs(MenuPanel, { children: [_jsx(MenuItemButton, { onClick: () => { onAddChild(nodeId); onClose(); }, children: "Add Child" }), _jsx(MenuItemButton, { onClick: () => { onFocus(nodeId); onClose(); }, children: "Focus Camera" }), !isRoot && onDuplicate && (_jsx(MenuItemButton, { onClick: () => { onDuplicate(nodeId); onClose(); }, children: "Duplicate" })), !isRoot && onDelete && (_jsx(MenuItemButton, { danger: true, onClick: () => { onDelete(nodeId); onClose(); }, children: "Delete" }))] }));
|
|
56
|
+
export function TreeNodeMenu({ isRoot, nodeId, locked, onAddChild, onFocus, onToggleLock, onDuplicate, onDelete, onClose, }) {
|
|
57
|
+
return (_jsxs(MenuPanel, { children: [_jsx(MenuItemButton, { onClick: () => { onAddChild(nodeId); onClose(); }, children: "Add Child" }), _jsx(MenuItemButton, { onClick: () => { onFocus(nodeId); onClose(); }, children: "Focus Camera" }), !isRoot && onToggleLock && (_jsx(MenuItemButton, { onClick: () => { onToggleLock(nodeId); onClose(); }, children: locked ? 'Unlock' : 'Lock' })), !isRoot && onDuplicate && (_jsx(MenuItemButton, { onClick: () => { onDuplicate(nodeId); onClose(); }, children: "Duplicate" })), !isRoot && onDelete && (_jsx(MenuItemButton, { danger: true, onClick: () => { onDelete(nodeId); onClose(); }, children: "Delete" }))] }));
|
|
58
58
|
}
|
|
59
59
|
export function TreeContextMenu({ contextMenu, onClose, children, }) {
|
|
60
60
|
var _a, _b;
|
|
@@ -84,18 +84,17 @@ export function TreeContextMenu({ contextMenu, onClose, children, }) {
|
|
|
84
84
|
};
|
|
85
85
|
}, [contextMenu, onClose]);
|
|
86
86
|
useEffect(() => {
|
|
87
|
-
if (!contextMenu
|
|
87
|
+
if (!contextMenu) {
|
|
88
|
+
setPosition(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!panelRef.current || typeof window === 'undefined')
|
|
88
92
|
return;
|
|
89
93
|
const panelRect = panelRef.current.getBoundingClientRect();
|
|
90
94
|
const left = Math.max(8, Math.min(contextMenu.x, window.innerWidth - panelRect.width - 8));
|
|
91
95
|
const top = Math.max(8, Math.min(contextMenu.y, window.innerHeight - panelRect.height - 8));
|
|
92
96
|
setPosition({ left, top });
|
|
93
97
|
}, [contextMenu]);
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (!contextMenu) {
|
|
96
|
-
setPosition(null);
|
|
97
|
-
}
|
|
98
|
-
}, [contextMenu]);
|
|
99
98
|
if (!contextMenu || typeof document === 'undefined')
|
|
100
99
|
return null;
|
|
101
100
|
return createPortal(_jsx("div", { ref: panelRef, style: {
|
|
@@ -10,7 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
13
|
-
import { useState
|
|
13
|
+
import { useState } from 'react';
|
|
14
14
|
import EditorTree from './EditorTree';
|
|
15
15
|
import { getAllComponents } from './components/ComponentRegistry';
|
|
16
16
|
import { base, colors, inspector, scrollbarCSS, componentCard } from './styles';
|
|
@@ -36,12 +36,8 @@ function NodeInspector({ node, updateNode, deleteNode, basePath }) {
|
|
|
36
36
|
const ALL_COMPONENTS = getAllComponents();
|
|
37
37
|
const allKeys = Object.keys(ALL_COMPONENTS);
|
|
38
38
|
const available = allKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); });
|
|
39
|
-
const [
|
|
40
|
-
|
|
41
|
-
const newAvailable = allKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); });
|
|
42
|
-
if (!newAvailable.includes(addType))
|
|
43
|
-
setAddType(newAvailable[0] || "");
|
|
44
|
-
}, [Object.keys(node.components || {}).join(',')]);
|
|
39
|
+
const [preferredAddType, setAddType] = useState(available[0] || "");
|
|
40
|
+
const addType = available.includes(preferredAddType) ? preferredAddType : (available[0] || "");
|
|
45
41
|
return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: colors.textDim, wordBreak: 'break-all', border: `1px solid ${colors.border}`, padding: '2px 6px', borderRadius: 3, flex: 1, fontFamily: 'monospace' }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
46
42
|
if (!comp)
|
|
47
43
|
return null;
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { RapierRigidBody } from '@react-three/rapier';
|
|
2
2
|
/** Physics event types (built-in) */
|
|
3
3
|
export type PhysicsEventType = 'sensor:enter' | 'sensor:exit' | 'collision:enter' | 'collision:exit';
|
|
4
|
+
export type InteractionEventType = 'click';
|
|
4
5
|
/** Payload for physics events */
|
|
5
6
|
export interface PhysicsEventPayload {
|
|
6
7
|
sourceEntityId: string;
|
|
7
8
|
targetEntityId: string | null;
|
|
8
9
|
targetRigidBody: RapierRigidBody | null | undefined;
|
|
9
10
|
}
|
|
11
|
+
export interface ClickEventPayload {
|
|
12
|
+
sourceEntityId: string;
|
|
13
|
+
instanceEntityId?: string;
|
|
14
|
+
point: [number, number, number];
|
|
15
|
+
button: number;
|
|
16
|
+
altKey: boolean;
|
|
17
|
+
ctrlKey: boolean;
|
|
18
|
+
metaKey: boolean;
|
|
19
|
+
shiftKey: boolean;
|
|
20
|
+
}
|
|
10
21
|
/**
|
|
11
22
|
* Register your custom event types here by extending this interface:
|
|
12
23
|
*
|
|
@@ -22,6 +33,7 @@ export interface GameEventMap {
|
|
|
22
33
|
'sensor:exit': PhysicsEventPayload;
|
|
23
34
|
'collision:enter': PhysicsEventPayload;
|
|
24
35
|
'collision:exit': PhysicsEventPayload;
|
|
36
|
+
'click': ClickEventPayload;
|
|
25
37
|
}
|
|
26
38
|
/** All registered event types */
|
|
27
39
|
export type GameEventType = keyof GameEventMap | (string & {});
|
|
@@ -31,11 +43,12 @@ type EventHandler<T = unknown> = (payload: T) => void;
|
|
|
31
43
|
/**
|
|
32
44
|
* Game event system for all game interactions.
|
|
33
45
|
*
|
|
34
|
-
* Built-in
|
|
46
|
+
* Built-in events:
|
|
35
47
|
* - sensor:enter - Something entered a sensor collider
|
|
36
48
|
* - sensor:exit - Something exited a sensor collider
|
|
37
49
|
* - collision:enter - A collision started
|
|
38
50
|
* - collision:exit - A collision ended
|
|
51
|
+
* - click - A prefab entity with a Click component was clicked in play mode
|
|
39
52
|
*
|
|
40
53
|
* Custom events:
|
|
41
54
|
* - Emit any event type with any payload
|
|
@@ -4,11 +4,12 @@ const subscribers = new Map();
|
|
|
4
4
|
/**
|
|
5
5
|
* Game event system for all game interactions.
|
|
6
6
|
*
|
|
7
|
-
* Built-in
|
|
7
|
+
* Built-in events:
|
|
8
8
|
* - sensor:enter - Something entered a sensor collider
|
|
9
9
|
* - sensor:exit - Something exited a sensor collider
|
|
10
10
|
* - collision:enter - A collision started
|
|
11
11
|
* - collision:exit - A collision ended
|
|
12
|
+
* - click - A prefab entity with a Click component was clicked in play mode
|
|
12
13
|
*
|
|
13
14
|
* Custom events:
|
|
14
15
|
* - Emit any event type with any payload
|
|
@@ -12,6 +12,8 @@ export declare function getRepeatAxesFromModelProperties(properties: Record<stri
|
|
|
12
12
|
export type InstanceData = {
|
|
13
13
|
id: string;
|
|
14
14
|
sourceId: string;
|
|
15
|
+
clickable?: boolean;
|
|
16
|
+
locked?: boolean;
|
|
15
17
|
position: [number, number, number];
|
|
16
18
|
rotation: [number, number, number];
|
|
17
19
|
scale: [number, number, number];
|
|
@@ -32,7 +34,9 @@ export declare function useInstanceCheck(id: string): boolean;
|
|
|
32
34
|
export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
33
35
|
id: string;
|
|
34
36
|
sourceId?: string;
|
|
37
|
+
clickable?: boolean;
|
|
35
38
|
modelUrl: string;
|
|
39
|
+
locked?: boolean;
|
|
36
40
|
position: [number, number, number];
|
|
37
41
|
rotation: [number, number, number];
|
|
38
42
|
scale: [number, number, number];
|