react-three-game 0.0.64 → 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.
Files changed (33) hide show
  1. package/LICENSE +2 -660
  2. package/README.md +164 -91
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +2 -1
  5. package/dist/tools/assetviewer/page.js +6 -6
  6. package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
  7. package/dist/tools/prefabeditor/EditorTree.js +17 -3
  8. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
  9. package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
  10. package/dist/tools/prefabeditor/EditorUI.js +3 -7
  11. package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
  12. package/dist/tools/prefabeditor/GameEvents.js +2 -1
  13. package/dist/tools/prefabeditor/InstanceProvider.d.ts +14 -0
  14. package/dist/tools/prefabeditor/InstanceProvider.js +198 -34
  15. package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
  16. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
  17. package/dist/tools/prefabeditor/PrefabRoot.js +88 -120
  18. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
  19. package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
  20. package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
  21. package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
  22. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
  23. package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
  24. package/dist/tools/prefabeditor/components/Input.js +71 -38
  25. package/dist/tools/prefabeditor/components/MaterialComponent.js +3 -3
  26. package/dist/tools/prefabeditor/components/ModelComponent.js +95 -16
  27. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
  28. package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
  29. package/dist/tools/prefabeditor/components/index.js +2 -0
  30. package/dist/tools/prefabeditor/types.d.ts +1 -0
  31. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  32. package/dist/tools/prefabeditor/utils.js +40 -2
  33. 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, Input, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
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, Input, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
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: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
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: '#1f2937', 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() })] }));
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);
@@ -114,14 +114,14 @@ function TextureSphere({ url, onError }) {
114
114
  export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
115
115
  return (_jsxs("div", { style: { position: 'relative', width: '100%', height: '100%' }, children: [_jsx("div", { style: { width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 4 }, children: _jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(ModelCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }) }), _jsx(SharedCanvas, {})] }));
116
116
  }
117
- function ModelCard({ file, onSelect, basePath = "" }) {
117
+ function ModelCard({ file, onSelect, basePath = "", size = 60, }) {
118
118
  const [error, setError] = useState(false);
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: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
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: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', 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() })] }));
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);
@@ -164,7 +164,7 @@ export function SingleTextureViewer({ file, basePath = "" }) {
164
164
  export function SingleModelViewer({ file, basePath = "" }) {
165
165
  if (!file)
166
166
  return null;
167
- return (_jsxs(_Fragment, { children: [_jsx(ModelCard, { file: file, basePath: basePath, onSelect: () => { } }), _jsx(SharedCanvas, {})] }));
167
+ return (_jsxs(_Fragment, { children: [_jsx(ModelCard, { file: file, basePath: basePath, onSelect: () => { }, size: 112 }), _jsx(SharedCanvas, {})] }));
168
168
  }
169
169
  export function SingleSoundViewer({ file, basePath = "" }) {
170
170
  if (!file)
@@ -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
- setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, nodeId, node => (Object.assign(Object.assign({}, node), { disabled: !node.disabled }))) })));
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) => (_jsx(TreeNodeMenu, { isRoot: isRoot, nodeId: nodeId, onAddChild: handleAddChild, onFocus: handleFocus, onDuplicate: isRoot ? undefined : handleDuplicate, onDelete: isRoot ? undefined : handleDelete, onClose: 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 || !panelRef.current || typeof window === 'undefined')
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, useEffect } from 'react';
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 [addType, setAddType] = useState(available[0] || "");
40
- useEffect(() => {
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 physics events:
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 physics events:
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
@@ -1,8 +1,19 @@
1
1
  import React from "react";
2
2
  import { Object3D, Group } from "three";
3
3
  import { PhysicsProps } from "./components/PhysicsComponent";
4
+ export type RepeatAxisConfig = {
5
+ axis: 'x' | 'y' | 'z';
6
+ count: number;
7
+ offset: number;
8
+ };
9
+ export declare const DEFAULT_REPEAT_AXES: RepeatAxisConfig[];
10
+ export declare function normalizeRepeatAxes(value: unknown): RepeatAxisConfig[];
11
+ export declare function getRepeatAxesFromModelProperties(properties: Record<string, any>): RepeatAxisConfig[];
4
12
  export type InstanceData = {
5
13
  id: string;
14
+ sourceId: string;
15
+ clickable?: boolean;
16
+ locked?: boolean;
6
17
  position: [number, number, number];
7
18
  rotation: [number, number, number];
8
19
  scale: [number, number, number];
@@ -22,7 +33,10 @@ export declare function GameInstanceProvider({ children, models, onSelect, regis
22
33
  export declare function useInstanceCheck(id: string): boolean;
23
34
  export declare const GameInstance: React.ForwardRefExoticComponent<{
24
35
  id: string;
36
+ sourceId?: string;
37
+ clickable?: boolean;
25
38
  modelUrl: string;
39
+ locked?: boolean;
26
40
  position: [number, number, number];
27
41
  rotation: [number, number, number];
28
42
  scale: [number, number, number];