react-three-game 0.0.56 → 0.0.57

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 (59) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/shared/GameCanvas.js +1 -3
  4. package/dist/tools/assetviewer/page.js +35 -14
  5. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  6. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  7. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  8. package/dist/tools/prefabeditor/EditorTree.js +138 -56
  9. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  10. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  11. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  12. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  13. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  14. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  15. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  16. package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
  17. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  18. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  20. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  21. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  22. package/dist/tools/prefabeditor/components/Input.js +73 -21
  23. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
  24. package/dist/tools/prefabeditor/components/MaterialComponent.js +122 -14
  25. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  26. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  27. package/dist/tools/prefabeditor/components/SpotLightComponent.js +4 -12
  28. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  29. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  30. package/dist/tools/prefabeditor/components/index.js +5 -1
  31. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  32. package/dist/tools/prefabeditor/styles.js +7 -3
  33. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  34. package/dist/tools/prefabeditor/utils.js +53 -5
  35. package/package.json +1 -1
  36. package/src/index.ts +7 -0
  37. package/src/shared/GameCanvas.tsx +0 -3
  38. package/src/tools/assetviewer/page.tsx +77 -45
  39. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  40. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  41. package/src/tools/prefabeditor/EditorTree.tsx +234 -101
  42. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  43. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  44. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  45. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  46. package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
  47. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
  48. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  49. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  50. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  51. package/src/tools/prefabeditor/components/MaterialComponent.tsx +178 -16
  52. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  53. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  54. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +11 -17
  55. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  56. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  57. package/src/tools/prefabeditor/components/index.ts +5 -1
  58. package/src/tools/prefabeditor/styles.ts +7 -3
  59. package/src/tools/prefabeditor/utils.ts +55 -4
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ 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
6
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
- export { FieldRenderer, Input, Label, Vector3Input, ColorInput, StringInput, BooleanInput, SelectInput, } from './tools/prefabeditor/components/Input';
7
+ export { FieldRenderer, FieldGroup, Input, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
8
8
  export * from './tools/prefabeditor/utils';
9
9
  export type { ExportGLBOptions } from './tools/prefabeditor/utils';
10
10
  export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
9
9
  // Prefab Editor - Component Registry
10
10
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
11
11
  // Prefab Editor - Input Components
12
- export { FieldRenderer, Input, Label, Vector3Input, ColorInput, StringInput, BooleanInput, SelectInput, } from './tools/prefabeditor/components/Input';
12
+ export { FieldRenderer, FieldGroup, Input, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
13
13
  // Prefab Editor - Styles & Utils
14
14
  export * from './tools/prefabeditor/utils';
15
15
  // Game Events (physics + custom events)
@@ -41,7 +41,5 @@ export default function GameCanvas(_a) {
41
41
  setFrameloop("always");
42
42
  });
43
43
  return renderer;
44
- }), camera: {
45
- position: [0, 1, 5],
46
- } }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
44
+ }) }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
47
45
  }
@@ -1,14 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Canvas, useLoader } from "@react-three/fiber";
2
+ import { Canvas } from "@react-three/fiber";
3
3
  import { OrbitControls, View, PerspectiveCamera } from "@react-three/drei";
4
- import { Suspense, useEffect, useState, useRef } from "react";
4
+ import { Component as ReactComponent, Suspense, useEffect, useState, useRef } from "react";
5
5
  import { TextureLoader } from "three";
6
6
  import { loadModel } from "../dragdrop/modelLoader";
7
+ class ErrorBoundary extends ReactComponent {
8
+ constructor(props) {
9
+ super(props);
10
+ this.state = { hasError: false };
11
+ }
12
+ static getDerivedStateFromError() { return { hasError: true }; }
13
+ componentDidCatch() { var _a, _b; (_b = (_a = this.props).onError) === null || _b === void 0 ? void 0 : _b.call(_a); }
14
+ render() { return this.state.hasError ? null : this.props.children; }
15
+ }
7
16
  // view models and textures in manifest, onselect callback
8
17
  const styles = {
9
18
  errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
10
19
  flexFillRelative: { flex: 1, position: 'relative' },
11
- bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
20
+ bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', color: '#f9fafb', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
21
+ textLight: { color: '#f9fafb' },
12
22
  iconLarge: { fontSize: 20 }
13
23
  };
14
24
  function getItemsInPath(files, currentPath) {
@@ -39,6 +49,7 @@ function FolderTile({ name, onClick }) {
39
49
  maxWidth: 60,
40
50
  aspectRatio: '1 / 1',
41
51
  backgroundColor: '#1f2937', /* gray-800 */
52
+ color: '#f9fafb',
42
53
  cursor: 'pointer',
43
54
  display: 'flex',
44
55
  flexDirection: 'column',
@@ -67,14 +78,14 @@ function useInView() {
67
78
  function AssetListViewer({ files, selected, onSelect, renderCard }) {
68
79
  const [currentPath, setCurrentPath] = useState('');
69
80
  const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
70
- return (_jsxs("div", { children: [currentPath && (_jsx("button", { onClick: () => {
81
+ return (_jsxs("div", { style: styles.textLight, children: [currentPath && (_jsx("button", { onClick: () => {
71
82
  const pathParts = currentPath.split('/').filter(Boolean);
72
83
  pathParts.pop();
73
84
  setCurrentPath(pathParts.join('/'));
74
85
  }, style: { marginBottom: 4, padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }, children: "\u2190 Back" })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }, children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, onSelect) }, file)))] })] }));
75
86
  }
76
87
  export function TextureListViewer({ files, selected, onSelect, basePath = "" }) {
77
- return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(TextureCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
88
+ 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(TextureCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }) }), _jsx(SharedCanvas, {})] }));
78
89
  }
79
90
  function TextureCard({ file, onSelect, basePath = "" }) {
80
91
  const [error, setError] = useState(false);
@@ -84,17 +95,24 @@ function TextureCard({ file, onSelect, basePath = "" }) {
84
95
  if (error) {
85
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" }) }));
86
97
  }
87
- return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#1f2937', 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 }), _jsxs(Suspense, { fallback: null, children: [_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: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
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() })] }));
88
99
  }
89
100
  function TextureSphere({ url, onError }) {
90
- const texture = useLoader(TextureLoader, url, undefined, (error) => {
91
- console.error('Failed to load texture:', url, error);
92
- onError === null || onError === void 0 ? void 0 : onError();
93
- });
101
+ const [texture, setTexture] = useState(null);
102
+ useEffect(() => {
103
+ setTexture(null);
104
+ const loader = new TextureLoader();
105
+ loader.load(url, (tex) => setTexture(tex), undefined, (err) => {
106
+ console.warn('Failed to load texture:', url, err);
107
+ onError === null || onError === void 0 ? void 0 : onError();
108
+ });
109
+ }, [url]);
110
+ if (!texture)
111
+ return null;
94
112
  return (_jsxs("mesh", { position: [0, 0, 0], children: [_jsx("sphereGeometry", { args: [1, 32, 32] }), _jsx("meshStandardMaterial", { map: texture })] }));
95
113
  }
96
114
  export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
97
- return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(ModelCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
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, {})] }));
98
116
  }
99
117
  function ModelCard({ file, onSelect, basePath = "" }) {
100
118
  const [error, setError] = useState(false);
@@ -103,7 +121,7 @@ function ModelCard({ file, onSelect, basePath = "" }) {
103
121
  if (error) {
104
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" }) }));
105
123
  }
106
- return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', 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: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
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() })] }));
107
125
  }
108
126
  function ModelPreview({ url, onError }) {
109
127
  const [model, setModel] = useState(null);
@@ -135,7 +153,7 @@ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
135
153
  function SoundCard({ file, onSelect, basePath = "" }) {
136
154
  const fileName = file.split('/').pop() || '';
137
155
  const fullPath = basePath ? `/${basePath}${file}` : file;
138
- return (_jsxs("div", { onClick: () => onSelect(file), style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }, children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
156
+ 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 })] }));
139
157
  }
140
158
  // Single Asset Viewer Components - display only one selected asset
141
159
  export function SingleTextureViewer({ file, basePath = "" }) {
@@ -155,12 +173,15 @@ export function SingleSoundViewer({ file, basePath = "" }) {
155
173
  }
156
174
  // Shared Canvas Component - can be used independently in any viewer
157
175
  export function SharedCanvas() {
158
- return (_jsx(Canvas, { shadows: true, dpr: [1, 1.5], camera: { position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }, style: {
176
+ return (_jsx(Canvas, { shadows: true, dpr: [1, 1.5], gl: { alpha: true }, camera: { position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }, onCreated: ({ gl }) => {
177
+ gl.setClearAlpha(0);
178
+ }, style: {
159
179
  position: 'fixed',
160
180
  top: 0,
161
181
  left: 0,
162
182
  width: '100vw',
163
183
  height: '100vh',
164
184
  pointerEvents: 'none',
185
+ background: 'transparent',
165
186
  }, eventSource: typeof document !== 'undefined' ? document.getElementById('root') || undefined : undefined, eventPrefix: "client", children: _jsx(View.Port, {}) }));
166
187
  }
@@ -0,0 +1,15 @@
1
+ import { ReactNode } from 'react';
2
+ type Placement = 'bottom-start' | 'bottom-end' | 'left-start' | 'right-start';
3
+ export declare function Dropdown({ trigger, children, placement, offset, zIndex, }: {
4
+ trigger: (props: {
5
+ ref: React.RefObject<HTMLButtonElement | null>;
6
+ isOpen: boolean;
7
+ toggle: () => void;
8
+ close: () => void;
9
+ }) => ReactNode;
10
+ children: ReactNode | ((close: () => void) => ReactNode);
11
+ placement?: Placement;
12
+ offset?: number;
13
+ zIndex?: number;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ export function Dropdown({ trigger, children, placement = 'bottom-end', offset = 6, zIndex = 1000, }) {
5
+ var _a, _b;
6
+ const [isOpen, setIsOpen] = useState(false);
7
+ const [position, setPosition] = useState(null);
8
+ const triggerRef = useRef(null);
9
+ const panelRef = useRef(null);
10
+ const close = () => setIsOpen(false);
11
+ const toggle = () => setIsOpen(prev => !prev);
12
+ useLayoutEffect(() => {
13
+ if (!isOpen || !triggerRef.current || !panelRef.current || typeof window === 'undefined')
14
+ return;
15
+ const updatePosition = () => {
16
+ var _a, _b;
17
+ const triggerRect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
18
+ const panelRect = (_b = panelRef.current) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
19
+ if (!triggerRect || !panelRect)
20
+ return;
21
+ let left = triggerRect.left;
22
+ let top = triggerRect.bottom + offset;
23
+ if (placement === 'bottom-end') {
24
+ left = triggerRect.right - panelRect.width;
25
+ top = triggerRect.bottom + offset;
26
+ }
27
+ else if (placement === 'bottom-start') {
28
+ left = triggerRect.left;
29
+ top = triggerRect.bottom + offset;
30
+ }
31
+ else if (placement === 'left-start') {
32
+ left = triggerRect.left - panelRect.width - offset;
33
+ top = triggerRect.top;
34
+ }
35
+ else if (placement === 'right-start') {
36
+ left = triggerRect.right + offset;
37
+ top = triggerRect.top;
38
+ }
39
+ left = Math.max(8, Math.min(left, window.innerWidth - panelRect.width - 8));
40
+ top = Math.max(8, Math.min(top, window.innerHeight - panelRect.height - 8));
41
+ setPosition({ left, top });
42
+ };
43
+ updatePosition();
44
+ window.addEventListener('resize', updatePosition);
45
+ window.addEventListener('scroll', updatePosition, true);
46
+ return () => {
47
+ window.removeEventListener('resize', updatePosition);
48
+ window.removeEventListener('scroll', updatePosition, true);
49
+ };
50
+ }, [isOpen, placement, offset]);
51
+ useEffect(() => {
52
+ if (!isOpen)
53
+ return;
54
+ const handlePointerDown = (event) => {
55
+ var _a, _b;
56
+ const target = event.target;
57
+ if (!target)
58
+ return;
59
+ if ((_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.contains(target))
60
+ return;
61
+ if ((_b = panelRef.current) === null || _b === void 0 ? void 0 : _b.contains(target))
62
+ return;
63
+ close();
64
+ };
65
+ const handleKeyDown = (event) => {
66
+ if (event.key === 'Escape')
67
+ close();
68
+ };
69
+ document.addEventListener('pointerdown', handlePointerDown);
70
+ document.addEventListener('keydown', handleKeyDown);
71
+ return () => {
72
+ document.removeEventListener('pointerdown', handlePointerDown);
73
+ document.removeEventListener('keydown', handleKeyDown);
74
+ };
75
+ }, [isOpen]);
76
+ return (_jsxs(_Fragment, { children: [trigger({ ref: triggerRef, isOpen, toggle, close }), isOpen && typeof document !== 'undefined' && createPortal(_jsx("div", { ref: panelRef, onMouseLeave: close, style: {
77
+ position: 'fixed',
78
+ left: (_a = position === null || position === void 0 ? void 0 : position.left) !== null && _a !== void 0 ? _a : -9999,
79
+ top: (_b = position === null || position === void 0 ? void 0 : position.top) !== null && _b !== void 0 ? _b : -9999,
80
+ zIndex,
81
+ }, children: typeof children === 'function' ? children(close) : children }), document.body)] }));
82
+ }
@@ -3,6 +3,11 @@ interface EditorContextType {
3
3
  setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
4
4
  snapResolution: number;
5
5
  setSnapResolution: (resolution: number) => void;
6
+ positionSnap: number;
7
+ setPositionSnap: (resolution: number) => void;
8
+ rotationSnap: number;
9
+ setRotationSnap: (resolution: number) => void;
10
+ onFocusNode?: (nodeId: string) => void;
6
11
  onScreenshot?: () => void;
7
12
  onExportGLB?: () => void;
8
13
  }
@@ -13,20 +13,74 @@ import { getComponent } from './components/ComponentRegistry';
13
13
  import { base, colors, tree, menu } from './styles';
14
14
  import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
15
15
  import { useEditorContext } from './EditorContext';
16
+ import { Dropdown } from './Dropdown';
17
+ function moveNode(root, draggedId, targetId, position) {
18
+ const draggedNode = findNode(root, draggedId);
19
+ const oldParent = findParent(root, draggedId);
20
+ if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) {
21
+ return root;
22
+ }
23
+ if (position === 'before') {
24
+ const targetParent = findParent(root, targetId);
25
+ if (!(targetParent === null || targetParent === void 0 ? void 0 : targetParent.children))
26
+ return root;
27
+ if (targetParent.id === oldParent.id) {
28
+ const siblings = targetParent.children.filter(child => child.id !== draggedId);
29
+ const targetIndex = siblings.findIndex(child => child.id === targetId);
30
+ if (targetIndex === -1)
31
+ return root;
32
+ siblings.splice(targetIndex, 0, draggedNode);
33
+ return updateNodeById(root, targetParent.id, parent => (Object.assign(Object.assign({}, parent), { children: siblings })));
34
+ }
35
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => {
36
+ var _a;
37
+ return (Object.assign(Object.assign({}, parent), { children: ((_a = parent.children) !== null && _a !== void 0 ? _a : []).filter(child => child.id !== draggedId) }));
38
+ });
39
+ return updateNodeById(rootWithoutDragged, targetParent.id, parent => {
40
+ var _a;
41
+ const children = [...((_a = parent.children) !== null && _a !== void 0 ? _a : [])];
42
+ const targetIndex = children.findIndex(child => child.id === targetId);
43
+ if (targetIndex === -1)
44
+ return parent;
45
+ children.splice(targetIndex, 0, draggedNode);
46
+ return Object.assign(Object.assign({}, parent), { children });
47
+ });
48
+ }
49
+ const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => {
50
+ var _a;
51
+ return (Object.assign(Object.assign({}, parent), { children: ((_a = parent.children) !== null && _a !== void 0 ? _a : []).filter(child => child.id !== draggedId) }));
52
+ });
53
+ return updateNodeById(rootWithoutDragged, targetId, target => {
54
+ var _a;
55
+ return (Object.assign(Object.assign({}, target), { children: [...((_a = target.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
56
+ });
57
+ }
58
+ function duplicateNodeBelow(root, nodeId) {
59
+ const node = findNode(root, nodeId);
60
+ const parent = findParent(root, nodeId);
61
+ if (!node || !parent)
62
+ return null;
63
+ const duplicate = cloneNode(node);
64
+ const nextRoot = updateNodeById(root, parent.id, currentParent => (Object.assign(Object.assign({}, currentParent), { children: (() => {
65
+ var _a;
66
+ const children = [...((_a = currentParent.children) !== null && _a !== void 0 ? _a : [])];
67
+ const index = children.findIndex(child => child.id === nodeId);
68
+ if (index === -1)
69
+ return [...children, duplicate];
70
+ children.splice(index + 1, 0, duplicate);
71
+ return children;
72
+ })() })));
73
+ return { root: nextRoot, duplicatedId: duplicate.id };
74
+ }
16
75
  export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }) {
17
- const [contextMenu, setContextMenu] = useState(null);
76
+ const { onFocusNode } = useEditorContext();
18
77
  const [draggedId, setDraggedId] = useState(null);
78
+ const [dropTarget, setDropTarget] = useState(null);
19
79
  const [collapsedIds, setCollapsedIds] = useState(new Set());
20
80
  const [collapsed, setCollapsed] = useState(false);
21
- const [fileMenuOpen, setFileMenuOpen] = useState(false);
22
81
  const [searchQuery, setSearchQuery] = useState('');
23
82
  if (!prefabData || !setPrefabData)
24
83
  return null;
25
- const handleContextMenu = (e, nodeId) => {
26
- e.preventDefault();
27
- e.stopPropagation();
28
- setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
29
- };
30
84
  const toggleCollapse = (e, id) => {
31
85
  e.stopPropagation();
32
86
  setCollapsedIds(prev => {
@@ -36,35 +90,33 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
36
90
  });
37
91
  };
38
92
  const handleAddChild = (parentId) => {
93
+ var _a;
94
+ const newNode = {
95
+ id: crypto.randomUUID(),
96
+ name: "New Node",
97
+ components: {
98
+ transform: {
99
+ type: "Transform",
100
+ properties: Object.assign({}, (_a = getComponent('Transform')) === null || _a === void 0 ? void 0 : _a.defaultProperties)
101
+ }
102
+ }
103
+ };
39
104
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parentId, parent => {
40
- var _a, _b;
41
- return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), {
42
- id: crypto.randomUUID(),
43
- name: "New Node",
44
- components: {
45
- transform: {
46
- type: "Transform",
47
- properties: Object.assign({}, (_b = getComponent('Transform')) === null || _b === void 0 ? void 0 : _b.defaultProperties)
48
- }
49
- }
50
- }] }));
105
+ var _a;
106
+ return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), newNode] }));
51
107
  }) })));
52
- setContextMenu(null);
108
+ setSelectedId(newNode.id);
53
109
  };
54
110
  const handleDuplicate = (nodeId) => {
55
111
  if (nodeId === prefabData.root.id)
56
112
  return;
57
113
  setPrefabData(prev => {
58
- const node = findNode(prev.root, nodeId);
59
- const parent = findParent(prev.root, nodeId);
60
- if (!node || !parent)
114
+ const result = duplicateNodeBelow(prev.root, nodeId);
115
+ if (!result)
61
116
  return prev;
62
- return Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parent.id, p => {
63
- var _a;
64
- return (Object.assign(Object.assign({}, p), { children: [...((_a = p.children) !== null && _a !== void 0 ? _a : []), cloneNode(node)] }));
65
- }) });
117
+ setSelectedId(result.duplicatedId);
118
+ return Object.assign(Object.assign({}, prev), { root: result.root });
66
119
  });
67
- setContextMenu(null);
68
120
  };
69
121
  const handleDelete = (nodeId) => {
70
122
  if (nodeId === prefabData.root.id)
@@ -72,11 +124,9 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
72
124
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: deleteNode(prev.root, nodeId) })));
73
125
  if (selectedId === nodeId)
74
126
  setSelectedId(null);
75
- setContextMenu(null);
76
127
  };
77
128
  const handleToggleDisabled = (nodeId) => {
78
129
  setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, nodeId, node => (Object.assign(Object.assign({}, node), { disabled: !node.disabled }))) })));
79
- setContextMenu(null);
80
130
  };
81
131
  const handleDragStart = (e, id) => {
82
132
  if (id === prefabData.root.id)
@@ -84,31 +134,38 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
84
134
  e.dataTransfer.effectAllowed = "move";
85
135
  setDraggedId(id);
86
136
  };
87
- const handleDragOver = (e, targetId) => {
137
+ const getDropPosition = (e, isRoot) => {
138
+ if (isRoot)
139
+ return 'inside';
140
+ const rect = e.currentTarget.getBoundingClientRect();
141
+ return e.clientY <= rect.top + rect.height * 0.35 ? 'before' : 'inside';
142
+ };
143
+ const handleDragOver = (e, targetId, isRoot) => {
88
144
  if (!draggedId || draggedId === targetId)
89
145
  return;
90
146
  const draggedNode = findNode(prefabData.root, draggedId);
91
147
  if (draggedNode && findNode(draggedNode, targetId))
92
148
  return;
93
149
  e.preventDefault();
150
+ setDropTarget({ id: targetId, position: getDropPosition(e, isRoot) });
94
151
  };
95
- const handleDrop = (e, targetId) => {
152
+ const handleDragLeave = (e, targetId) => {
153
+ const relatedTarget = e.relatedTarget;
154
+ if (relatedTarget instanceof Node && e.currentTarget.contains(relatedTarget))
155
+ return;
156
+ setDropTarget(current => (current === null || current === void 0 ? void 0 : current.id) === targetId ? null : current);
157
+ };
158
+ const handleDrop = (e, targetId, isRoot) => {
96
159
  if (!draggedId || draggedId === targetId)
97
160
  return;
98
161
  e.preventDefault();
162
+ const dropPosition = getDropPosition(e, isRoot);
99
163
  setPrefabData(prev => {
100
- const draggedNode = findNode(prev.root, draggedId);
101
- const oldParent = findParent(prev.root, draggedId);
102
- if (!draggedNode || !oldParent || findNode(draggedNode, targetId))
103
- return prev;
104
- let root = updateNodeById(prev.root, oldParent.id, p => (Object.assign(Object.assign({}, p), { children: p.children.filter(c => c.id !== draggedId) })));
105
- root = updateNodeById(root, targetId, t => {
106
- var _a;
107
- return (Object.assign(Object.assign({}, t), { children: [...((_a = t.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
108
- });
109
- return Object.assign(Object.assign({}, prev), { root });
164
+ const root = moveNode(prev.root, draggedId, targetId, dropPosition);
165
+ return root === prev.root ? prev : Object.assign(Object.assign({}, prev), { root });
110
166
  });
111
167
  setDraggedId(null);
168
+ setDropTarget(null);
112
169
  };
113
170
  const matchesSearch = (node, query) => {
114
171
  var _a, _b, _c;
@@ -130,26 +187,51 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
130
187
  const isCollapsed = collapsedIds.has(node.id);
131
188
  const hasChildren = node.children && node.children.length > 0;
132
189
  const isRoot = node.id === prefabData.root.id;
133
- return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px`, opacity: node.disabled ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }), draggable: !isRoot, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: () => setDraggedId(null), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, children: [_jsx("span", { style: {
190
+ const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === node.id;
191
+ const showDropBefore = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'before';
192
+ const showDropInside = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'inside';
193
+ return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px`, opacity: node.disabled ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderTop: showDropBefore ? `2px solid ${colors.accent}` : undefined, boxShadow: showDropInside ? `inset 0 0 0 1px ${colors.accentBorder}` : undefined }), draggable: !isRoot, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: () => { setDraggedId(null); setDropTarget(null); }, onDragOver: (e) => handleDragOver(e, node.id, isRoot), onDragLeave: (e) => handleDragLeave(e, node.id), onDrop: (e) => handleDrop(e, node.id, isRoot), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, children: [_jsx("span", { style: {
134
194
  width: 12,
135
195
  opacity: 0.6,
136
196
  marginRight: 4,
137
197
  cursor: 'pointer',
138
198
  visibility: hasChildren ? 'visible' : 'hidden'
139
- }, 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 && (_jsx("button", { style: {
140
- background: 'none',
141
- border: 'none',
142
- cursor: 'pointer',
143
- padding: '0 4px',
144
- fontSize: 14,
145
- opacity: node.disabled ? 0.5 : 0.7,
146
- color: 'inherit',
147
- }, onClick: (e) => {
148
- e.stopPropagation();
149
- handleToggleDisabled(node.id);
150
- }, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '' }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
199
+ }, 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("button", { ref: ref, style: {
200
+ background: 'none',
201
+ border: 'none',
202
+ cursor: 'pointer',
203
+ padding: '0 4px',
204
+ fontSize: 14,
205
+ opacity: 0.7,
206
+ color: 'inherit',
207
+ }, onClick: (e) => {
208
+ e.stopPropagation();
209
+ toggle();
210
+ }, title: "Node Actions", children: "\u22EF" })), children: (close) => (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { handleAddChild(node.id); close(); }, children: "Add Child" }), _jsx("button", { style: menu.item, onClick: () => { setSelectedId(node.id); onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(node.id); close(); }, children: "Focus Camera" }), _jsx("button", { style: menu.item, onClick: () => { handleDuplicate(node.id); close(); }, children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => { handleDelete(node.id); close(); }, children: "Delete" })] })) }), _jsx("button", { style: {
211
+ background: 'none',
212
+ border: 'none',
213
+ cursor: 'pointer',
214
+ padding: '0 4px',
215
+ fontSize: 14,
216
+ opacity: node.disabled ? 0.5 : 0.7,
217
+ color: 'inherit',
218
+ }, onClick: (e) => {
219
+ e.stopPropagation();
220
+ handleToggleDisabled(node.id);
221
+ }, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' })] })), isRoot && (_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx("button", { ref: ref, style: {
222
+ background: 'none',
223
+ border: 'none',
224
+ cursor: 'pointer',
225
+ padding: '0 4px',
226
+ fontSize: 14,
227
+ opacity: 0.7,
228
+ color: 'inherit',
229
+ }, onClick: (e) => {
230
+ e.stopPropagation();
231
+ toggle();
232
+ }, title: "Scene Actions", children: "\u22EF" })), children: (close) => (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { handleAddChild(node.id); close(); }, children: "Add Child" }), _jsx("button", { style: menu.item, onClick: () => { setSelectedId(node.id); onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(node.id); close(); }, children: "Focus Camera" })] })) }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
151
233
  };
152
- return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: () => setFileMenuOpen(false) }))] })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: Object.assign(Object.assign({}, base.input), { padding: '4px 8px' }) }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
234
+ return (_jsx(_Fragment, { children: _jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx("button", { ref: ref, style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); toggle(); }, title: "File", children: "\u22EE" })), children: (close) => (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: close })) })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: Object.assign(Object.assign({}, base.input), { padding: '4px 8px' }) }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }) }));
153
235
  }
154
236
  function FileMenu({ prefabData, setPrefabData, onClose }) {
155
237
  const { onScreenshot, onExportGLB } = useEditorContext();
@@ -174,5 +256,5 @@ function FileMenu({ prefabData, setPrefabData, onClose }) {
174
256
  }) })));
175
257
  onClose();
176
258
  });
177
- return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'absolute', top: 28, right: 0 }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: handleLoad, children: "\uD83D\uDCE5 Load Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleSave, children: "\uD83D\uDCBE Save Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleLoadIntoScene, children: "\uD83D\uDCC2 Load into Scene" }), _jsx("button", { style: menu.item, onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "\uD83D\uDCF8 Screenshot" }), _jsx("button", { style: menu.item, onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "\uD83D\uDCE6 Export GLB" })] }));
259
+ return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: handleLoad, children: "\uD83D\uDCE5 Load Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleSave, children: "\uD83D\uDCBE Save Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleLoadIntoScene, children: "\uD83D\uDCC2 Load into Scene" }), _jsx("button", { style: menu.item, onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "\uD83D\uDCF8 Screenshot" }), _jsx("button", { style: menu.item, onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "\uD83D\uDCE6 Export GLB" })] }));
178
260
  }
@@ -42,7 +42,7 @@ function NodeInspector({ node, updateNode, deleteNode, basePath }) {
42
42
  if (!newAvailable.includes(addType))
43
43
  setAddType(newAvailable[0] || "");
44
44
  }, [Object.keys(node.components || {}).join(',')]);
45
- return _jsxs("div", { style: Object.assign(Object.assign({}, inspector.content), { paddingRight: 2 }), 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]) => {
45
+ 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
46
  if (!comp)
47
47
  return null;
48
48
  const def = ALL_COMPONENTS[comp.type];
@@ -12,6 +12,7 @@ declare const PrefabEditor: import("react").ForwardRefExoticComponent<{
12
12
  initialPrefab?: Prefab;
13
13
  physics?: boolean;
14
14
  onPrefabChange?: (prefab: Prefab) => void;
15
+ uiPlugins?: React.ReactNode[] | React.ReactNode;
15
16
  children?: React.ReactNode;
16
17
  } & import("react").RefAttributes<PrefabEditorRef>>;
17
18
  export default PrefabEditor;