react-three-game 0.0.55 → 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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/ContactShadow.d.ts +8 -0
- package/dist/shared/ContactShadow.js +32 -0
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +36 -15
- package/dist/tools/dragdrop/DragDropLoader.js +17 -40
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- 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 +139 -70
- package/dist/tools/prefabeditor/EditorUI.js +5 -10
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
- 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 +25 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
- 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 +100 -47
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
- 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 +6 -11
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +17 -4
- package/dist/tools/prefabeditor/styles.js +69 -32
- package/dist/tools/prefabeditor/utils.d.ts +8 -3
- package/dist/tools/prefabeditor/utils.js +92 -6
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
- package/src/index.ts +7 -0
- package/src/shared/ContactShadow.tsx +74 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +78 -46
- package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
- package/src/tools/dragdrop/modelLoader.ts +36 -0
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +237 -115
- package/src/tools/prefabeditor/EditorUI.tsx +6 -11
- package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
- package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
- 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 +247 -53
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
- package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +71 -32
- package/src/tools/prefabeditor/utils.ts +96 -5
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)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface ContactShadowProps {
|
|
2
|
+
opacity?: number;
|
|
3
|
+
blur?: number;
|
|
4
|
+
scale?: number;
|
|
5
|
+
yOffset?: number;
|
|
6
|
+
}
|
|
7
|
+
declare const ContactShadow: ({ opacity, blur, scale, yOffset, }: ContactShadowProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default ContactShadow;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import * as THREE from "three/webgpu";
|
|
5
|
+
import { float, uv, vec3, smoothstep, uniform, length, } from "three/tsl";
|
|
6
|
+
const ContactShadow = ({ opacity = 0.4, blur = 2.5, scale = 1.2, yOffset = 0.05, }) => {
|
|
7
|
+
const material = useMemo(() => {
|
|
8
|
+
const mat = new THREE.MeshBasicNodeMaterial();
|
|
9
|
+
mat.transparent = true;
|
|
10
|
+
mat.depthWrite = false;
|
|
11
|
+
mat.depthTest = true;
|
|
12
|
+
mat.side = THREE.DoubleSide;
|
|
13
|
+
mat.polygonOffset = true;
|
|
14
|
+
mat.polygonOffsetFactor = -1;
|
|
15
|
+
mat.polygonOffsetUnits = -1;
|
|
16
|
+
const uOpacity = uniform(opacity);
|
|
17
|
+
const uBlur = uniform(blur);
|
|
18
|
+
// UVs centered around origin
|
|
19
|
+
const centeredUV = uv().sub(0.5).mul(2.0);
|
|
20
|
+
// IMPORTANT: use functional length(), not .length()
|
|
21
|
+
const dist = length(centeredUV);
|
|
22
|
+
const innerRadius = float(0.0);
|
|
23
|
+
const outerRadius = float(1.0);
|
|
24
|
+
const blurAmount = uBlur.div(10.0);
|
|
25
|
+
const alpha = smoothstep(outerRadius, innerRadius.add(blurAmount), dist).mul(uOpacity);
|
|
26
|
+
mat.colorNode = vec3(0.0, 0.0, 0.0);
|
|
27
|
+
mat.opacityNode = alpha;
|
|
28
|
+
return mat;
|
|
29
|
+
}, [opacity, blur]);
|
|
30
|
+
return (_jsx("mesh", { rotation: [-Math.PI / 2, 0, 0], position: [0, yOffset, 0], material: material, renderOrder: 1, children: _jsx("planeGeometry", { args: [scale, scale] }) }));
|
|
31
|
+
};
|
|
32
|
+
export default ContactShadow;
|
|
@@ -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 },
|
|
159
|
-
|
|
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: {
|
|
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
|
}
|
|
@@ -1,49 +1,26 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
1
10
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
11
|
// DragDropLoader.tsx
|
|
3
12
|
import { useEffect } from "react";
|
|
4
|
-
import {
|
|
5
|
-
// Shared file handling logic
|
|
13
|
+
import { parseModelFromFile } from "./modelLoader";
|
|
6
14
|
function handleFiles(files, onModelLoaded) {
|
|
7
|
-
files.forEach((file) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
else if (file.name.endsWith(".fbx")) {
|
|
12
|
-
loadFBXFile(file, onModelLoaded);
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
function loadGLTFFile(file, onModelLoaded) {
|
|
17
|
-
const reader = new FileReader();
|
|
18
|
-
reader.onload = (event) => {
|
|
19
|
-
var _a;
|
|
20
|
-
const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
21
|
-
if (arrayBuffer) {
|
|
22
|
-
const loader = new GLTFLoader();
|
|
23
|
-
const dracoLoader = new DRACOLoader();
|
|
24
|
-
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
25
|
-
loader.setDRACOLoader(dracoLoader);
|
|
26
|
-
loader.parse(arrayBuffer, "", (gltf) => {
|
|
27
|
-
onModelLoaded(gltf.scene, file.name);
|
|
28
|
-
}, (error) => {
|
|
29
|
-
console.error("GLTFLoader parse error", error);
|
|
30
|
-
});
|
|
15
|
+
files.forEach((file) => __awaiter(this, void 0, void 0, function* () {
|
|
16
|
+
const result = yield parseModelFromFile(file);
|
|
17
|
+
if (result.success && result.model) {
|
|
18
|
+
onModelLoaded(result.model, file.name);
|
|
31
19
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
function loadFBXFile(file, onModelLoaded) {
|
|
36
|
-
const reader = new FileReader();
|
|
37
|
-
reader.onload = (event) => {
|
|
38
|
-
var _a;
|
|
39
|
-
const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
40
|
-
if (arrayBuffer) {
|
|
41
|
-
const loader = new FBXLoader();
|
|
42
|
-
const model = loader.parse(arrayBuffer, "");
|
|
43
|
-
onModelLoaded(model, file.name);
|
|
20
|
+
else {
|
|
21
|
+
console.error("Model parse error:", result.error);
|
|
44
22
|
}
|
|
45
|
-
};
|
|
46
|
-
reader.readAsArrayBuffer(file);
|
|
23
|
+
}));
|
|
47
24
|
}
|
|
48
25
|
export function DragDropLoader({ onModelLoaded }) {
|
|
49
26
|
useEffect(() => {
|
|
@@ -4,4 +4,9 @@ export type ModelLoadResult = {
|
|
|
4
4
|
error?: any;
|
|
5
5
|
};
|
|
6
6
|
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Parse a model from a File object (e.g. from drag-drop or file picker).
|
|
9
|
+
* Returns the parsed Three.js Object3D scene.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseModelFromFile(file: File): Promise<ModelLoadResult>;
|
|
7
12
|
export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
|
|
@@ -14,6 +14,45 @@ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
|
14
14
|
const gltfLoader = new GLTFLoader();
|
|
15
15
|
gltfLoader.setDRACOLoader(dracoLoader);
|
|
16
16
|
const fbxLoader = new FBXLoader();
|
|
17
|
+
/**
|
|
18
|
+
* Parse a model from a File object (e.g. from drag-drop or file picker).
|
|
19
|
+
* Returns the parsed Three.js Object3D scene.
|
|
20
|
+
*/
|
|
21
|
+
export function parseModelFromFile(file) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const reader = new FileReader();
|
|
24
|
+
reader.onload = (event) => {
|
|
25
|
+
var _a;
|
|
26
|
+
const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
27
|
+
if (!arrayBuffer) {
|
|
28
|
+
resolve({ success: false, error: new Error('Failed to read file') });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const name = file.name.toLowerCase();
|
|
32
|
+
if (name.endsWith('.glb') || name.endsWith('.gltf')) {
|
|
33
|
+
gltfLoader.parse(arrayBuffer, '', (gltf) => {
|
|
34
|
+
resolve({ success: true, model: gltf.scene });
|
|
35
|
+
}, (error) => {
|
|
36
|
+
resolve({ success: false, error });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (name.endsWith('.fbx')) {
|
|
40
|
+
try {
|
|
41
|
+
const model = fbxLoader.parse(arrayBuffer, '');
|
|
42
|
+
resolve({ success: true, model });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
resolve({ success: false, error });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
resolve({ success: false, error: new Error(`Unsupported file format: ${file.name}`) });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
reader.onerror = () => resolve({ success: false, error: reader.error });
|
|
53
|
+
reader.readAsArrayBuffer(file);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
17
56
|
export function loadModel(filename, onProgress) {
|
|
18
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19
58
|
try {
|
|
@@ -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
|
}
|