react-three-game 0.0.56 → 0.0.58

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 (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. package/src/tools/prefabeditor/utils.ts +55 -4
package/README.md CHANGED
@@ -60,14 +60,23 @@ import { GameCanvas, PrefabRoot } from 'react-three-game';
60
60
  ## GameObject Schema
61
61
 
62
62
  ```typescript
63
+ interface Prefab {
64
+ id?: string;
65
+ name?: string;
66
+ root: GameObject;
67
+ }
68
+
63
69
  interface GameObject {
64
70
  id: string;
71
+ name?: string;
65
72
  disabled?: boolean;
66
73
  components?: Record<string, { type: string; properties: any }>;
67
74
  children?: GameObject[];
68
75
  }
69
76
  ```
70
77
 
78
+ `disabled` is the canonical visibility toggle in the current schema. Transforms are local to the parent node.
79
+
71
80
  ## Built-in Components
72
81
 
73
82
  | Component | Key Properties |
@@ -110,7 +119,7 @@ const Rotator: Component = {
110
119
  registerComponent(Rotator); // before rendering PrefabEditor
111
120
  ```
112
121
 
113
- **Wrapper** components accept `children` (animations, controllers). **Leaf** components don't (lights, particles).
122
+ Components may render visible content, wrap child content, or contribute runtime behavior. Keep those semantics explicit in the component `View` rather than relying on hidden tree rules.
114
123
 
115
124
  ### Schema-Driven Field Types
116
125
 
@@ -152,12 +161,16 @@ import { PrefabEditor } from 'react-three-game';
152
161
  </PrefabEditor>
153
162
  ```
154
163
 
155
- Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Import/export JSON. Physics only runs in play mode.
164
+ Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
165
+
166
+ Editor menu structure:
167
+ - `Menu > File`: new scene, load/save prefab JSON, load prefab into scene
168
+ - `Menu > Export`: `GLB`, `PNG`
156
169
 
157
170
  ## Internals
158
171
 
159
172
  - **Transforms**: Local in JSON, world computed via matrix multiplication
160
- - **Instancing**: `model.properties.instanced = true` `<Merged>` + `<InstancedRigidBodies>`
173
+ - **Instancing**: `model.properties.instanced = true` switches the node to the batched instance path (`<Merged>` / `<InstancedRigidBodies>`) instead of the standard model render path
161
174
  - **Models**: GLB/GLTF (Draco) and FBX auto-load from `filename`
162
175
 
163
176
  ## Tree Utilities
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
  }