react-three-game 0.0.59 → 0.0.61
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/tools/dragdrop/DragDropLoader.d.ts +8 -8
- package/dist/tools/dragdrop/DragDropLoader.js +33 -15
- package/dist/tools/dragdrop/index.d.ts +3 -3
- package/dist/tools/dragdrop/index.js +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
- package/package.json +9 -3
- package/.gitattributes +0 -2
- package/.github/copilot-instructions.md +0 -83
- package/.github/workflows/nextjs.yml +0 -99
- package/.gitmodules +0 -3
- package/assets/architecture.png +0 -0
- package/assets/editor.gif +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/react-three-game-logo.png +0 -0
- package/dist/tools/dragdrop/page.d.ts +0 -1
- package/dist/tools/dragdrop/page.js +0 -11
- package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
- package/dist/tools/prefabeditor/EntityEvents.js +0 -85
- package/dist/tools/prefabeditor/page.d.ts +0 -1
- package/dist/tools/prefabeditor/page.js +0 -5
- package/react-three-game-skill/.gitattributes +0 -2
- package/react-three-game-skill/README.md +0 -7
- package/react-three-game-skill/react-three-game/SKILL.md +0 -514
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
- package/src/helpers/SoundManager.ts +0 -130
- package/src/helpers/index.ts +0 -91
- package/src/index.ts +0 -59
- package/src/shared/ContactShadow.tsx +0 -74
- package/src/shared/GameCanvas.tsx +0 -52
- package/src/tools/assetviewer/page.tsx +0 -425
- package/src/tools/dragdrop/DragDropLoader.tsx +0 -136
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -145
- package/src/tools/dragdrop/page.tsx +0 -45
- package/src/tools/prefabeditor/Dropdown.tsx +0 -112
- package/src/tools/prefabeditor/EditorContext.tsx +0 -25
- package/src/tools/prefabeditor/EditorTree.tsx +0 -452
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
- package/src/tools/prefabeditor/EditorUI.tsx +0 -204
- package/src/tools/prefabeditor/EventSystem.tsx +0 -36
- package/src/tools/prefabeditor/GameEvents.ts +0 -191
- package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
- package/src/tools/prefabeditor/PrefabEditor.tsx +0 -262
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
- package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
- package/src/tools/prefabeditor/components/Input.tsx +0 -820
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
- package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
- package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
- package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
- package/src/tools/prefabeditor/components/index.ts +0 -26
- package/src/tools/prefabeditor/page.tsx +0 -10
- package/src/tools/prefabeditor/styles.ts +0 -235
- package/src/tools/prefabeditor/types.ts +0 -20
- package/src/tools/prefabeditor/utils.ts +0 -312
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { ChangeEvent, useRef } from "react";
|
|
2
|
-
import type { DragEvent, HTMLAttributes, MouseEvent, ReactNode } from "react";
|
|
3
|
-
import type { LoadedModel } from "./modelLoader";
|
|
4
|
-
import { canParseModelFile, parseModelFromFile } from "./modelLoader";
|
|
5
|
-
|
|
6
|
-
export interface FileLoadOptions {
|
|
7
|
-
onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
|
|
8
|
-
onFileLoaded?: (file: File) => void | Promise<void>;
|
|
9
|
-
onFilesLoaded?: (files: File[]) => void | Promise<void>;
|
|
10
|
-
onModelError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
|
|
11
|
-
parseModels?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
|
|
15
|
-
|
|
16
|
-
export interface DragDropLoaderProps extends FileLoadOptions, DivProps {
|
|
17
|
-
children?: ReactNode;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface FilePickerProps extends FileLoadOptions, DivProps {
|
|
21
|
-
accept?: string;
|
|
22
|
-
children?: ReactNode;
|
|
23
|
-
multiple?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getFiles(fileList?: FileList | null) {
|
|
27
|
-
return fileList ? Array.from(fileList) : [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function loadFiles(
|
|
31
|
-
files: File[],
|
|
32
|
-
{ onModelLoaded, onFileLoaded, onFilesLoaded, onModelError, parseModels = true }: FileLoadOptions,
|
|
33
|
-
) {
|
|
34
|
-
await Promise.all(
|
|
35
|
-
files.map(async (file) => {
|
|
36
|
-
await onFileLoaded?.(file);
|
|
37
|
-
|
|
38
|
-
if (!parseModels || !canParseModelFile(file) || (!onModelLoaded && !onModelError)) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const result = await parseModelFromFile(file);
|
|
43
|
-
|
|
44
|
-
if (result.success && result.model) {
|
|
45
|
-
await onModelLoaded?.(result.model, file.name, file);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (onModelError) {
|
|
50
|
-
await onModelError(result.error, file.name, file);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
console.error("Model parse error:", result.error);
|
|
55
|
-
}),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
await onFilesLoaded?.(files);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function reportFileLoadError(error: unknown) {
|
|
62
|
-
console.error("File load error:", error);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function createLoadHandlers(options: FileLoadOptions) {
|
|
66
|
-
return {
|
|
67
|
-
onFileLoaded: options.onFileLoaded,
|
|
68
|
-
onFilesLoaded: options.onFilesLoaded,
|
|
69
|
-
onModelError: options.onModelError,
|
|
70
|
-
onModelLoaded: options.onModelLoaded,
|
|
71
|
-
parseModels: options.parseModels,
|
|
72
|
-
} satisfies FileLoadOptions;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function DragDropLoader({
|
|
76
|
-
children,
|
|
77
|
-
...divProps
|
|
78
|
-
}: DragDropLoaderProps) {
|
|
79
|
-
const loadOptions = createLoadHandlers(divProps);
|
|
80
|
-
|
|
81
|
-
function handleDrop(event: DragEvent<HTMLDivElement>) {
|
|
82
|
-
event.preventDefault();
|
|
83
|
-
event.stopPropagation();
|
|
84
|
-
|
|
85
|
-
void loadFiles(getFiles(event.dataTransfer?.files), loadOptions).catch(reportFileLoadError);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function handleDragOver(event: DragEvent<HTMLDivElement>) {
|
|
89
|
-
event.preventDefault();
|
|
90
|
-
event.stopPropagation();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<div {...divProps} onDrop={handleDrop} onDragOver={handleDragOver}>
|
|
95
|
-
{children}
|
|
96
|
-
</div>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function FilePicker({
|
|
101
|
-
accept = ".glb,.gltf,.fbx",
|
|
102
|
-
children,
|
|
103
|
-
multiple = true,
|
|
104
|
-
...divProps
|
|
105
|
-
}: FilePickerProps) {
|
|
106
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
107
|
-
const { onClick, ...wrapperProps } = divProps;
|
|
108
|
-
const loadOptions = createLoadHandlers(divProps);
|
|
109
|
-
|
|
110
|
-
function onChange(event: ChangeEvent<HTMLInputElement>) {
|
|
111
|
-
void loadFiles(getFiles(event.target.files), loadOptions).catch(reportFileLoadError);
|
|
112
|
-
event.target.value = "";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function handleClick(event: MouseEvent<HTMLDivElement>) {
|
|
116
|
-
onClick?.(event);
|
|
117
|
-
|
|
118
|
-
if (!event.defaultPrevented) {
|
|
119
|
-
inputRef.current?.click();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<div {...wrapperProps} onClick={handleClick}>
|
|
125
|
-
<input
|
|
126
|
-
ref={inputRef}
|
|
127
|
-
type="file"
|
|
128
|
-
accept={accept}
|
|
129
|
-
multiple={multiple}
|
|
130
|
-
onChange={onChange}
|
|
131
|
-
hidden
|
|
132
|
-
/>
|
|
133
|
-
{children ?? "Select Files"}
|
|
134
|
-
</div>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
|
|
2
|
-
export type { DragDropLoaderProps, FileLoadOptions, FilePickerProps } from "./DragDropLoader";
|
|
3
|
-
export { loadModel, parseModelFromFile } from "./modelLoader";
|
|
4
|
-
export type { LoadedModel, ModelLoadResult, ProgressCallback } from "./modelLoader";
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import type { Object3D } from "three";
|
|
2
|
-
import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
|
|
3
|
-
|
|
4
|
-
export type LoadedModel = Object3D;
|
|
5
|
-
|
|
6
|
-
export type ModelLoadResult = {
|
|
7
|
-
success: boolean;
|
|
8
|
-
model?: LoadedModel;
|
|
9
|
-
error?: unknown;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
13
|
-
|
|
14
|
-
const dracoLoader = new DRACOLoader();
|
|
15
|
-
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
16
|
-
|
|
17
|
-
const gltfLoader = new GLTFLoader();
|
|
18
|
-
gltfLoader.setDRACOLoader(dracoLoader);
|
|
19
|
-
|
|
20
|
-
const fbxLoader = new FBXLoader();
|
|
21
|
-
|
|
22
|
-
type ModelFileKind = "gltf" | "fbx";
|
|
23
|
-
|
|
24
|
-
function normalizeModelPath(name: string) {
|
|
25
|
-
return name.split(/[?#]/, 1)[0].toLowerCase();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getModelFileKind(name: string): ModelFileKind | null {
|
|
29
|
-
const normalizedName = normalizeModelPath(name);
|
|
30
|
-
|
|
31
|
-
if (normalizedName.endsWith(".glb") || normalizedName.endsWith(".gltf")) {
|
|
32
|
-
return "gltf";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (normalizedName.endsWith(".fbx")) {
|
|
36
|
-
return "fbx";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function canParseModelFile(file: File | string) {
|
|
43
|
-
const filename = typeof file === "string" ? file : file.name;
|
|
44
|
-
return getModelFileKind(filename) !== null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseModelBuffer(arrayBuffer: ArrayBuffer, sourceName: string): Promise<ModelLoadResult> {
|
|
48
|
-
const modelFileKind = getModelFileKind(sourceName);
|
|
49
|
-
|
|
50
|
-
if (modelFileKind === "gltf") {
|
|
51
|
-
return new Promise(resolve => {
|
|
52
|
-
gltfLoader.parse(
|
|
53
|
-
arrayBuffer,
|
|
54
|
-
"",
|
|
55
|
-
gltf => {
|
|
56
|
-
resolve({ success: true, model: gltf.scene });
|
|
57
|
-
},
|
|
58
|
-
error => {
|
|
59
|
-
resolve({ success: false, error });
|
|
60
|
-
},
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (modelFileKind === "fbx") {
|
|
66
|
-
try {
|
|
67
|
-
const model = fbxLoader.parse(arrayBuffer, "");
|
|
68
|
-
return Promise.resolve({ success: true, model });
|
|
69
|
-
} catch (error) {
|
|
70
|
-
return Promise.resolve({ success: false, error });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return Promise.resolve({ success: false, error: new Error(`Unsupported file format: ${sourceName}`) });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function parseModelFromFile(file: File): Promise<ModelLoadResult> {
|
|
78
|
-
return new Promise(resolve => {
|
|
79
|
-
const reader = new FileReader();
|
|
80
|
-
|
|
81
|
-
reader.onload = event => {
|
|
82
|
-
const arrayBuffer = event.target?.result as ArrayBuffer;
|
|
83
|
-
|
|
84
|
-
if (!arrayBuffer) {
|
|
85
|
-
resolve({ success: false, error: new Error("Failed to read file") });
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
void parseModelBuffer(arrayBuffer, file.name).then(resolve);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
reader.onerror = () => resolve({ success: false, error: reader.error });
|
|
93
|
-
reader.readAsArrayBuffer(file);
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function loadModel(
|
|
98
|
-
filename: string,
|
|
99
|
-
onProgress?: ProgressCallback,
|
|
100
|
-
): Promise<ModelLoadResult> {
|
|
101
|
-
try {
|
|
102
|
-
const fullPath = filename;
|
|
103
|
-
const modelFileKind = getModelFileKind(filename);
|
|
104
|
-
|
|
105
|
-
if (modelFileKind === "gltf") {
|
|
106
|
-
return new Promise(resolve => {
|
|
107
|
-
gltfLoader.load(
|
|
108
|
-
fullPath,
|
|
109
|
-
gltf => resolve({ success: true, model: gltf.scene }),
|
|
110
|
-
progressEvent => {
|
|
111
|
-
if (!onProgress) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const total = progressEvent.total || progressEvent.loaded;
|
|
116
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
117
|
-
},
|
|
118
|
-
error => resolve({ success: false, error }),
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (modelFileKind === "fbx") {
|
|
124
|
-
return new Promise(resolve => {
|
|
125
|
-
fbxLoader.load(
|
|
126
|
-
fullPath,
|
|
127
|
-
model => resolve({ success: true, model }),
|
|
128
|
-
progressEvent => {
|
|
129
|
-
if (!onProgress) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const total = progressEvent.total || progressEvent.loaded;
|
|
134
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
135
|
-
},
|
|
136
|
-
error => resolve({ success: false, error }),
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return { success: false, error };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Physics, RigidBody } from "@react-three/rapier";
|
|
4
|
-
import { OrbitControls } from "@react-three/drei";
|
|
5
|
-
import { useState } from "react";
|
|
6
|
-
import { DragDropLoader } from "./index";
|
|
7
|
-
import GameCanvas from "../../shared/GameCanvas";
|
|
8
|
-
|
|
9
|
-
export default function Home() {
|
|
10
|
-
const [models, setModels] = useState<any[]>([]);
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<DragDropLoader
|
|
14
|
-
onModelLoaded={model => setModels(prev => [...prev, model])}
|
|
15
|
-
className="w-full items-center justify-items-center min-h-screen"
|
|
16
|
-
style={{ height: "100vh" }}
|
|
17
|
-
>
|
|
18
|
-
<div className="w-full items-center justify-items-center min-h-screen" style={{ height: "100vh" }}>
|
|
19
|
-
<GameCanvas>
|
|
20
|
-
<Physics>
|
|
21
|
-
<RigidBody>
|
|
22
|
-
<mesh castShadow>
|
|
23
|
-
<boxGeometry args={[1, 1, 1]} />
|
|
24
|
-
<meshStandardMaterial color="orange" />
|
|
25
|
-
</mesh>
|
|
26
|
-
</RigidBody>
|
|
27
|
-
<RigidBody type="fixed">
|
|
28
|
-
<mesh position={[0, -2, 0]} scale={[10, 0.1, 10]} receiveShadow>
|
|
29
|
-
<boxGeometry />
|
|
30
|
-
<meshStandardMaterial color="gray" />
|
|
31
|
-
</mesh>
|
|
32
|
-
</RigidBody>
|
|
33
|
-
{/* Render loaded models */}
|
|
34
|
-
{models.map((model, idx) => (
|
|
35
|
-
<primitive object={model} key={idx} position={[0, 0, 0]} />
|
|
36
|
-
))}
|
|
37
|
-
<ambientLight intensity={0.5} />
|
|
38
|
-
<pointLight position={[10, 10, 10]} castShadow intensity={1000} />
|
|
39
|
-
<OrbitControls />
|
|
40
|
-
</Physics>
|
|
41
|
-
</GameCanvas>
|
|
42
|
-
</div>
|
|
43
|
-
</DragDropLoader>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
|
|
4
|
-
type Placement = 'bottom-start' | 'bottom-end' | 'left-start' | 'right-start';
|
|
5
|
-
|
|
6
|
-
export function Dropdown({
|
|
7
|
-
trigger,
|
|
8
|
-
children,
|
|
9
|
-
placement = 'bottom-end',
|
|
10
|
-
offset = 6,
|
|
11
|
-
zIndex = 1000,
|
|
12
|
-
}: {
|
|
13
|
-
trigger: (props: { ref: React.RefObject<HTMLButtonElement | null>; isOpen: boolean; toggle: () => void; close: () => void; }) => ReactNode;
|
|
14
|
-
children: ReactNode | ((close: () => void) => ReactNode);
|
|
15
|
-
placement?: Placement;
|
|
16
|
-
offset?: number;
|
|
17
|
-
zIndex?: number;
|
|
18
|
-
}) {
|
|
19
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
20
|
-
const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
|
|
21
|
-
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
22
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
23
|
-
|
|
24
|
-
const close = () => setIsOpen(false);
|
|
25
|
-
const toggle = () => setIsOpen(prev => !prev);
|
|
26
|
-
|
|
27
|
-
useLayoutEffect(() => {
|
|
28
|
-
if (!isOpen || !triggerRef.current || !panelRef.current || typeof window === 'undefined') return;
|
|
29
|
-
|
|
30
|
-
const updatePosition = () => {
|
|
31
|
-
const triggerRect = triggerRef.current?.getBoundingClientRect();
|
|
32
|
-
const panelRect = panelRef.current?.getBoundingClientRect();
|
|
33
|
-
if (!triggerRect || !panelRect) return;
|
|
34
|
-
|
|
35
|
-
let left = triggerRect.left;
|
|
36
|
-
let top = triggerRect.bottom + offset;
|
|
37
|
-
|
|
38
|
-
if (placement === 'bottom-end') {
|
|
39
|
-
left = triggerRect.right - panelRect.width;
|
|
40
|
-
top = triggerRect.bottom + offset;
|
|
41
|
-
} else if (placement === 'bottom-start') {
|
|
42
|
-
left = triggerRect.left;
|
|
43
|
-
top = triggerRect.bottom + offset;
|
|
44
|
-
} else if (placement === 'left-start') {
|
|
45
|
-
left = triggerRect.left - panelRect.width - offset;
|
|
46
|
-
top = triggerRect.top;
|
|
47
|
-
} else if (placement === 'right-start') {
|
|
48
|
-
left = triggerRect.right + offset;
|
|
49
|
-
top = triggerRect.top;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
left = Math.max(8, Math.min(left, window.innerWidth - panelRect.width - 8));
|
|
53
|
-
top = Math.max(8, Math.min(top, window.innerHeight - panelRect.height - 8));
|
|
54
|
-
|
|
55
|
-
setPosition({ left, top });
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
updatePosition();
|
|
59
|
-
window.addEventListener('resize', updatePosition);
|
|
60
|
-
window.addEventListener('scroll', updatePosition, true);
|
|
61
|
-
|
|
62
|
-
return () => {
|
|
63
|
-
window.removeEventListener('resize', updatePosition);
|
|
64
|
-
window.removeEventListener('scroll', updatePosition, true);
|
|
65
|
-
};
|
|
66
|
-
}, [isOpen, placement, offset]);
|
|
67
|
-
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (!isOpen) return;
|
|
70
|
-
|
|
71
|
-
const handlePointerDown = (event: PointerEvent) => {
|
|
72
|
-
const target = event.target as Node | null;
|
|
73
|
-
if (!target) return;
|
|
74
|
-
if (triggerRef.current?.contains(target)) return;
|
|
75
|
-
if (panelRef.current?.contains(target)) return;
|
|
76
|
-
close();
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
80
|
-
if (event.key === 'Escape') close();
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
document.addEventListener('pointerdown', handlePointerDown);
|
|
84
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
85
|
-
|
|
86
|
-
return () => {
|
|
87
|
-
document.removeEventListener('pointerdown', handlePointerDown);
|
|
88
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
89
|
-
};
|
|
90
|
-
}, [isOpen]);
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<>
|
|
94
|
-
{trigger({ ref: triggerRef, isOpen, toggle, close })}
|
|
95
|
-
{isOpen && typeof document !== 'undefined' && createPortal(
|
|
96
|
-
<div
|
|
97
|
-
ref={panelRef}
|
|
98
|
-
onMouseLeave={close}
|
|
99
|
-
style={{
|
|
100
|
-
position: 'fixed',
|
|
101
|
-
left: position?.left ?? -9999,
|
|
102
|
-
top: position?.top ?? -9999,
|
|
103
|
-
zIndex,
|
|
104
|
-
}}
|
|
105
|
-
>
|
|
106
|
-
{typeof children === 'function' ? children(close) : children}
|
|
107
|
-
</div>,
|
|
108
|
-
document.body
|
|
109
|
-
)}
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext } from "react";
|
|
2
|
-
|
|
3
|
-
interface EditorContextType {
|
|
4
|
-
transformMode: "translate" | "rotate" | "scale";
|
|
5
|
-
setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
|
|
6
|
-
snapResolution: number;
|
|
7
|
-
setSnapResolution: (resolution: number) => void;
|
|
8
|
-
positionSnap: number;
|
|
9
|
-
setPositionSnap: (resolution: number) => void;
|
|
10
|
-
rotationSnap: number;
|
|
11
|
-
setRotationSnap: (resolution: number) => void;
|
|
12
|
-
onFocusNode?: (nodeId: string) => void;
|
|
13
|
-
onScreenshot?: () => void;
|
|
14
|
-
onExportGLB?: () => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const EditorContext = createContext<EditorContextType | null>(null);
|
|
18
|
-
|
|
19
|
-
export function useEditorContext() {
|
|
20
|
-
const context = useContext(EditorContext);
|
|
21
|
-
if (!context) {
|
|
22
|
-
throw new Error("useEditorContext must be used within EditorContext.Provider");
|
|
23
|
-
}
|
|
24
|
-
return context;
|
|
25
|
-
}
|