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.
- package/README.md +16 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +35 -14
- package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
- package/dist/tools/prefabeditor/Dropdown.js +82 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
- package/dist/tools/prefabeditor/EditorTree.js +149 -91
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/EditorUI.js +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
- package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
- package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
- package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
- package/dist/tools/prefabeditor/components/Input.js +73 -21
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
- package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +5 -2
- package/dist/tools/prefabeditor/styles.js +7 -3
- package/dist/tools/prefabeditor/utils.d.ts +4 -3
- package/dist/tools/prefabeditor/utils.js +53 -5
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/SKILL.md +4 -1
- package/src/index.ts +7 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +77 -45
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +242 -178
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/EditorUI.tsx +1 -1
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
- package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
- package/src/tools/prefabeditor/components/Input.tsx +220 -27
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
- package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +7 -3
- 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
|
-
|
|
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.
|
|
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`
|
|
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
|
-
}),
|
|
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
|
|
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(
|
|
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 }),
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
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:
|
|
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 },
|
|
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
|
}
|