react-three-game 0.0.65 → 0.0.67
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 +5 -3
- package/dist/index.js +3 -2
- package/dist/shared/GameCanvas.js +1 -1
- package/dist/tools/assetviewer/page.d.ts +13 -2
- package/dist/tools/assetviewer/page.js +61 -7
- package/dist/tools/dragdrop/index.d.ts +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +2 -0
- 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 +9 -6
- package/dist/tools/prefabeditor/PrefabRoot.js +52 -126
- 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 +4 -69
- package/dist/tools/prefabeditor/components/ModelComponent.js +5 -80
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +9 -7
- 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,8 +15,8 @@ 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';
|
|
20
|
-
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
22
|
+
export { TextureListViewer, ModelListViewer, SoundListViewer, TexturePicker, ModelPicker, SingleTextureViewer, SingleModelViewer, SingleSoundViewer, SharedCanvas, } from './tools/assetviewer/page';
|
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)
|
|
@@ -18,4 +19,4 @@ export { gameEvents, useGameEvent, getEntityIdFromRigidBody } from './tools/pref
|
|
|
18
19
|
export { entityEvents, useEntityEvent } from './tools/prefabeditor/GameEvents';
|
|
19
20
|
// Asset Tools
|
|
20
21
|
export * from './tools/dragdrop';
|
|
21
|
-
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
22
|
+
export { TextureListViewer, ModelListViewer, SoundListViewer, TexturePicker, ModelPicker, SingleTextureViewer, SingleModelViewer, SingleSoundViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
@@ -33,7 +33,7 @@ extend({
|
|
|
33
33
|
export default function GameCanvas(_a) {
|
|
34
34
|
var { loader = false, children, glConfig, canvasRef, onCreated, style } = _a, props = __rest(_a, ["loader", "children", "glConfig", "canvasRef", "onCreated", "style"]);
|
|
35
35
|
const [frameloop, setFrameloop] = useState("never");
|
|
36
|
-
return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: Object.assign({ touchAction: 'none', userSelect: 'none' }, style), shadows: { type: PCFShadowMap
|
|
36
|
+
return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: Object.assign({ touchAction: 'none', userSelect: 'none' }, style), shadows: { type: PCFShadowMap }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
|
|
37
37
|
const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
|
|
38
38
|
// @ts-expect-error futuristic
|
|
39
39
|
shadowMap: true, antialias: true }, glConfig));
|
|
@@ -19,14 +19,25 @@ interface SoundListViewerProps {
|
|
|
19
19
|
basePath?: string;
|
|
20
20
|
}
|
|
21
21
|
export declare function SoundListViewer({ files, selected, onSelect, basePath }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function TexturePicker({ value, onChange, basePath }: {
|
|
23
|
+
value: string | undefined;
|
|
24
|
+
onChange: (value: string | undefined) => void;
|
|
25
|
+
basePath?: string;
|
|
26
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
export declare function ModelPicker({ value, onChange, basePath, pickerKey }: {
|
|
28
|
+
value: string | undefined;
|
|
29
|
+
onChange: (value: string | undefined) => void;
|
|
30
|
+
basePath?: string;
|
|
31
|
+
pickerKey?: string;
|
|
32
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
22
33
|
export declare function SingleTextureViewer({ file, basePath }: {
|
|
23
34
|
file?: string;
|
|
24
35
|
basePath?: string;
|
|
25
|
-
}): import("react/jsx-runtime").JSX.Element
|
|
36
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
26
37
|
export declare function SingleModelViewer({ file, basePath }: {
|
|
27
38
|
file?: string;
|
|
28
39
|
basePath?: string;
|
|
29
|
-
}): import("react/jsx-runtime").JSX.Element
|
|
40
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
30
41
|
export declare function SingleSoundViewer({ file, basePath }: {
|
|
31
42
|
file?: string;
|
|
32
43
|
basePath?: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Canvas } from "@react-three/fiber";
|
|
3
3
|
import { OrbitControls, View, PerspectiveCamera } from "@react-three/drei";
|
|
4
|
-
import { Component as ReactComponent, Suspense, useEffect, useState, useRef } from "react";
|
|
4
|
+
import { Component as ReactComponent, Suspense, useEffect, useLayoutEffect, useState, useRef } from "react";
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
5
6
|
import { TextureLoader } from "three";
|
|
6
7
|
import { loadModel } from "../dragdrop";
|
|
7
8
|
class ErrorBoundary extends ReactComponent {
|
|
@@ -93,9 +94,9 @@ function TextureCard({ file, onSelect, basePath = "" }) {
|
|
|
93
94
|
const { ref, isInView } = useInView();
|
|
94
95
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
95
96
|
if (error) {
|
|
96
|
-
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#
|
|
97
|
+
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
98
|
}
|
|
98
|
-
return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#
|
|
99
|
+
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
100
|
}
|
|
100
101
|
function TextureSphere({ url, onError }) {
|
|
101
102
|
const [texture, setTexture] = useState(null);
|
|
@@ -119,9 +120,9 @@ function ModelCard({ file, onSelect, basePath = "", size = 60, }) {
|
|
|
119
120
|
const { ref, isInView } = useInView();
|
|
120
121
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
121
122
|
if (error) {
|
|
122
|
-
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#
|
|
123
|
+
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
124
|
}
|
|
124
|
-
return (_jsxs("div", { ref: ref, style: { width: size, aspectRatio: '1 / 1', backgroundColor: '#
|
|
125
|
+
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
126
|
}
|
|
126
127
|
function ModelPreview({ url, onError }) {
|
|
127
128
|
const [model, setModel] = useState(null);
|
|
@@ -155,15 +156,68 @@ function SoundCard({ file, onSelect, basePath = "" }) {
|
|
|
155
156
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
156
157
|
return (_jsxs("div", { onClick: () => onSelect(file), style: { aspectRatio: '1 / 1', backgroundColor: '#374151', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }, children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { color: '#f9fafb', fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
|
|
157
158
|
}
|
|
159
|
+
const PICKER_POPUP_WIDTH = 260;
|
|
160
|
+
const PICKER_POPUP_HEIGHT = 360;
|
|
161
|
+
function AssetPicker({ value, onChange, basePath, manifestFolder, preview, renderList, rootStyle, controlsStyle, changeButtonStyle, clearButtonStyle, popupStyle, }) {
|
|
162
|
+
const [files, setFiles] = useState([]);
|
|
163
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
164
|
+
const [resolvedPopupStyle, setResolvedPopupStyle] = useState(null);
|
|
165
|
+
const triggerRef = useRef(null);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
fetch(`${basePath}/${manifestFolder}/manifest.json`)
|
|
168
|
+
.then(r => r.json())
|
|
169
|
+
.then(data => setFiles(Array.isArray(data) ? data : data.files || []))
|
|
170
|
+
.catch(console.error);
|
|
171
|
+
}, [basePath, manifestFolder]);
|
|
172
|
+
useLayoutEffect(() => {
|
|
173
|
+
if (!showPicker || !triggerRef.current || typeof window === 'undefined')
|
|
174
|
+
return;
|
|
175
|
+
const updatePosition = () => {
|
|
176
|
+
var _a;
|
|
177
|
+
const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
|
|
178
|
+
if (!rect)
|
|
179
|
+
return;
|
|
180
|
+
const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
|
|
181
|
+
const fallbackLeft = rect.right + 8;
|
|
182
|
+
const fitsLeft = preferredLeft >= 8;
|
|
183
|
+
const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
|
|
184
|
+
const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
|
|
185
|
+
setResolvedPopupStyle(Object.assign({ position: 'fixed', left,
|
|
186
|
+
top, padding: 12, width: PICKER_POPUP_WIDTH, height: PICKER_POPUP_HEIGHT, overflow: 'hidden', zIndex: 1000, boxShadow: '0 4px 16px rgba(0,0,0,0.6)', background: '#111827', border: '1px solid rgba(255,255,255,0.12)', borderRadius: 6 }, popupStyle));
|
|
187
|
+
};
|
|
188
|
+
updatePosition();
|
|
189
|
+
window.addEventListener('resize', updatePosition);
|
|
190
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
191
|
+
return () => {
|
|
192
|
+
window.removeEventListener('resize', updatePosition);
|
|
193
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
194
|
+
};
|
|
195
|
+
}, [popupStyle, showPicker]);
|
|
196
|
+
return (_jsxs("div", { style: rootStyle, children: [preview, _jsxs("div", { style: controlsStyle, children: [_jsx("button", { ref: triggerRef, onClick: () => setShowPicker(!showPicker), style: changeButtonStyle, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => onChange(undefined), style: clearButtonStyle, children: "Clear" })] }), showPicker && resolvedPopupStyle && typeof document !== 'undefined' && createPortal(_jsx("div", { style: resolvedPopupStyle, onMouseLeave: () => setShowPicker(false), children: renderList({
|
|
197
|
+
files,
|
|
198
|
+
value,
|
|
199
|
+
onSelect: (file) => {
|
|
200
|
+
onChange(file);
|
|
201
|
+
setShowPicker(false);
|
|
202
|
+
},
|
|
203
|
+
basePath,
|
|
204
|
+
}) }), document.body)] }));
|
|
205
|
+
}
|
|
206
|
+
export function TexturePicker({ value, onChange, basePath = "" }) {
|
|
207
|
+
return (_jsx(AssetPicker, { value: value, onChange: onChange, basePath: basePath, manifestFolder: "textures", rootStyle: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, changeButtonStyle: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.12)', borderRadius: 3, marginTop: 4 }, clearButtonStyle: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(255,255,255,0.12)', borderRadius: 3, marginTop: 4, marginLeft: 4 }, preview: _jsx(SingleTextureViewer, { file: value, basePath: basePath }), renderList: ({ files, value: selectedValue, onSelect, basePath: currentBasePath }) => (_jsx(TextureListViewer, { files: files, selected: selectedValue || undefined, onSelect: onSelect, basePath: currentBasePath })) }));
|
|
208
|
+
}
|
|
209
|
+
export function ModelPicker({ value, onChange, basePath = "", pickerKey }) {
|
|
210
|
+
return (_jsx(AssetPicker, { value: value, onChange: onChange, basePath: basePath, manifestFolder: "models", rootStyle: { maxHeight: 160, overflow: 'visible', position: 'relative', display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'center' }, controlsStyle: { display: 'flex', flexDirection: 'column', gap: 6, flex: '0 0 84px', minWidth: 84, justifyContent: 'flex-end' }, changeButtonStyle: { width: '100%', padding: '6px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)' }, clearButtonStyle: { width: '100%', padding: '6px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)' }, popupStyle: { background: 'rgba(0,0,0,0.9)', border: '1px solid rgba(34, 211, 238, 0.3)' }, preview: _jsx("div", { style: { flex: '0 0 auto' }, children: _jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }) }), renderList: ({ files, value: selectedValue, onSelect, basePath: currentBasePath }) => (_jsx(ModelListViewer, { files: files, selected: selectedValue ? `/${selectedValue}` : undefined, onSelect: (file) => onSelect(file.startsWith('/') ? file.slice(1) : file), basePath: currentBasePath }, pickerKey)) }));
|
|
211
|
+
}
|
|
158
212
|
// Single Asset Viewer Components - display only one selected asset
|
|
159
213
|
export function SingleTextureViewer({ file, basePath = "" }) {
|
|
160
214
|
if (!file)
|
|
161
|
-
return
|
|
215
|
+
return _jsx("div", { style: { width: 60, aspectRatio: '1 / 1', backgroundColor: '#1f2937', border: '1px dashed rgba(255,255,255,0.12)' } });
|
|
162
216
|
return (_jsxs(_Fragment, { children: [_jsx(TextureCard, { file: file, basePath: basePath, onSelect: () => { } }), _jsx(SharedCanvas, {})] }));
|
|
163
217
|
}
|
|
164
218
|
export function SingleModelViewer({ file, basePath = "" }) {
|
|
165
219
|
if (!file)
|
|
166
|
-
return
|
|
220
|
+
return _jsx("div", { style: { width: 112, aspectRatio: '1 / 1', backgroundColor: '#1f2937', border: '1px dashed rgba(255,255,255,0.12)' } });
|
|
167
221
|
return (_jsxs(_Fragment, { children: [_jsx(ModelCard, { file: file, basePath: basePath, onSelect: () => { }, size: 112 }), _jsx(SharedCanvas, {})] }));
|
|
168
222
|
}
|
|
169
223
|
export function SingleSoundViewer({ file, basePath = "" }) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
|
|
2
2
|
export type { AssetLoadOptions, DragDropLoaderProps, FilePickerProps } from "./DragDropLoader";
|
|
3
3
|
export { loadModel, loadTexture, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
|
|
4
|
-
export type { LoadedModel, LoadedTexture, ModelLoadResult, ProgressCallback, TextureLoadResult } from "./modelLoader";
|
|
4
|
+
export type { LoadedModel, LoadedTexture, LoadedModels, LoadedTextures, ModelLoadResult, ProgressCallback, TextureLoadResult } from "./modelLoader";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Object3D, Texture } from "three";
|
|
2
2
|
export type LoadedModel = Object3D;
|
|
3
3
|
export type LoadedTexture = Texture;
|
|
4
|
+
export type LoadedModels = Record<string, LoadedModel>;
|
|
5
|
+
export type LoadedTextures = Record<string, LoadedTexture>;
|
|
4
6
|
export type ModelLoadResult = {
|
|
5
7
|
success: boolean;
|
|
6
8
|
model?: LoadedModel;
|
|
@@ -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;
|