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.
Files changed (38) hide show
  1. package/LICENSE +2 -660
  2. package/README.md +164 -91
  3. package/dist/index.d.ts +5 -3
  4. package/dist/index.js +3 -2
  5. package/dist/shared/GameCanvas.js +1 -1
  6. package/dist/tools/assetviewer/page.d.ts +13 -2
  7. package/dist/tools/assetviewer/page.js +61 -7
  8. package/dist/tools/dragdrop/index.d.ts +1 -1
  9. package/dist/tools/dragdrop/modelLoader.d.ts +2 -0
  10. package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
  11. package/dist/tools/prefabeditor/EditorTree.js +17 -3
  12. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
  13. package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
  14. package/dist/tools/prefabeditor/EditorUI.js +3 -7
  15. package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
  16. package/dist/tools/prefabeditor/GameEvents.js +2 -1
  17. package/dist/tools/prefabeditor/InstanceProvider.d.ts +4 -0
  18. package/dist/tools/prefabeditor/InstanceProvider.js +44 -12
  19. package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
  20. package/dist/tools/prefabeditor/PrefabRoot.d.ts +9 -6
  21. package/dist/tools/prefabeditor/PrefabRoot.js +52 -126
  22. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
  23. package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
  24. package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
  25. package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
  26. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
  27. package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
  28. package/dist/tools/prefabeditor/components/Input.js +71 -38
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +4 -69
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +5 -80
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
  32. package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
  33. package/dist/tools/prefabeditor/components/SpotLightComponent.js +9 -7
  34. package/dist/tools/prefabeditor/components/index.js +2 -0
  35. package/dist/tools/prefabeditor/types.d.ts +1 -0
  36. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  37. package/dist/tools/prefabeditor/utils.js +34 -1
  38. 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,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, 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)
@@ -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, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
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 | null;
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 | null;
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: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
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: '#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() })] }));
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: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
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: '#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() })] }));
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 null;
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 null;
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
- 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;