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.
Files changed (68) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/shared/ContactShadow.d.ts +8 -0
  4. package/dist/shared/ContactShadow.js +32 -0
  5. package/dist/shared/GameCanvas.js +1 -3
  6. package/dist/tools/assetviewer/page.js +36 -15
  7. package/dist/tools/dragdrop/DragDropLoader.js +17 -40
  8. package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
  9. package/dist/tools/dragdrop/modelLoader.js +39 -0
  10. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  11. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  12. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  13. package/dist/tools/prefabeditor/EditorTree.js +139 -70
  14. package/dist/tools/prefabeditor/EditorUI.js +5 -10
  15. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
  17. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
  18. package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
  19. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  20. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  21. package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
  22. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  23. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  24. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  25. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  26. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  27. package/dist/tools/prefabeditor/components/Input.js +100 -47
  28. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  32. package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -11
  33. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  34. package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
  35. package/dist/tools/prefabeditor/components/index.js +5 -1
  36. package/dist/tools/prefabeditor/styles.d.ts +17 -4
  37. package/dist/tools/prefabeditor/styles.js +69 -32
  38. package/dist/tools/prefabeditor/utils.d.ts +8 -3
  39. package/dist/tools/prefabeditor/utils.js +92 -6
  40. package/package.json +1 -1
  41. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
  42. package/src/index.ts +7 -0
  43. package/src/shared/ContactShadow.tsx +74 -0
  44. package/src/shared/GameCanvas.tsx +0 -3
  45. package/src/tools/assetviewer/page.tsx +78 -46
  46. package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
  47. package/src/tools/dragdrop/modelLoader.ts +36 -0
  48. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  49. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  50. package/src/tools/prefabeditor/EditorTree.tsx +237 -115
  51. package/src/tools/prefabeditor/EditorUI.tsx +6 -11
  52. package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
  53. package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
  54. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  55. package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
  56. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
  57. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  58. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  59. package/src/tools/prefabeditor/components/Input.tsx +247 -53
  60. package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
  61. package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
  62. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  63. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
  64. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  65. package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
  66. package/src/tools/prefabeditor/components/index.ts +5 -1
  67. package/src/tools/prefabeditor/styles.ts +71 -32
  68. 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
- }), 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: {
159
- position: 'absolute',
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 { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
5
- // Shared file handling logic
13
+ import { parseModelFromFile } from "./modelLoader";
6
14
  function handleFiles(files, onModelLoaded) {
7
- files.forEach((file) => {
8
- if (file.name.endsWith(".glb") || file.name.endsWith(".gltf")) {
9
- loadGLTFFile(file, onModelLoaded);
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
- reader.readAsArrayBuffer(file);
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
  }