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
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { Canvas
|
|
1
|
+
import { Canvas } from "@react-three/fiber";
|
|
2
2
|
import { OrbitControls, Stage, View, PerspectiveCamera } from "@react-three/drei";
|
|
3
|
-
import { Suspense, useEffect, useState, useRef } from "react";
|
|
3
|
+
import { Component as ReactComponent, Suspense, useEffect, useState, useRef } from "react";
|
|
4
4
|
import { TextureLoader } from "three";
|
|
5
5
|
import { loadModel } from "../dragdrop/modelLoader";
|
|
6
6
|
|
|
7
|
+
class ErrorBoundary extends ReactComponent<{ onError?: () => void; children: React.ReactNode }, { hasError: boolean }> {
|
|
8
|
+
constructor(props: any) {
|
|
9
|
+
super(props);
|
|
10
|
+
this.state = { hasError: false };
|
|
11
|
+
}
|
|
12
|
+
static getDerivedStateFromError() { return { hasError: true }; }
|
|
13
|
+
componentDidCatch() { this.props.onError?.(); }
|
|
14
|
+
render() { return this.state.hasError ? null : this.props.children; }
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
// view models and textures in manifest, onselect callback
|
|
8
18
|
|
|
9
19
|
const styles: Record<string, any> = {
|
|
10
20
|
errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
|
|
11
21
|
flexFillRelative: { flex: 1, position: 'relative' },
|
|
12
|
-
bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
|
|
22
|
+
bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', color: '#f9fafb', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
|
|
23
|
+
textLight: { color: '#f9fafb' },
|
|
13
24
|
iconLarge: { fontSize: 20 }
|
|
14
25
|
};
|
|
15
26
|
|
|
@@ -49,6 +60,7 @@ function FolderTile({ name, onClick }: { name: string; onClick: () => void }) {
|
|
|
49
60
|
maxWidth: 60,
|
|
50
61
|
aspectRatio: '1 / 1',
|
|
51
62
|
backgroundColor: '#1f2937', /* gray-800 */
|
|
63
|
+
color: '#f9fafb',
|
|
52
64
|
cursor: 'pointer',
|
|
53
65
|
display: 'flex',
|
|
54
66
|
flexDirection: 'column',
|
|
@@ -100,7 +112,7 @@ function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListVie
|
|
|
100
112
|
const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
|
|
101
113
|
|
|
102
114
|
return (
|
|
103
|
-
<div>
|
|
115
|
+
<div style={styles.textLight}>
|
|
104
116
|
{currentPath && (
|
|
105
117
|
<button
|
|
106
118
|
onClick={() => {
|
|
@@ -140,17 +152,19 @@ interface TextureListViewerProps {
|
|
|
140
152
|
|
|
141
153
|
export function TextureListViewer({ files, selected, onSelect, basePath = "" }: TextureListViewerProps) {
|
|
142
154
|
return (
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
|
156
|
+
<div style={{ width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 4 }}>
|
|
157
|
+
<AssetListViewer
|
|
158
|
+
files={files}
|
|
159
|
+
selected={selected}
|
|
160
|
+
onSelect={onSelect}
|
|
161
|
+
renderCard={(file, onSelectHandler) => (
|
|
162
|
+
<TextureCard file={file} basePath={basePath} onSelect={onSelectHandler} />
|
|
163
|
+
)}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
152
166
|
<SharedCanvas />
|
|
153
|
-
|
|
167
|
+
</div>
|
|
154
168
|
);
|
|
155
169
|
}
|
|
156
170
|
|
|
@@ -175,7 +189,7 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
|
|
|
175
189
|
return (
|
|
176
190
|
<div
|
|
177
191
|
ref={ref}
|
|
178
|
-
style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
192
|
+
style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#1f2937', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
179
193
|
onClick={() => onSelect(file)}
|
|
180
194
|
onMouseEnter={() => setIsHovered(true)}
|
|
181
195
|
onMouseLeave={() => setIsHovered(false)}
|
|
@@ -184,21 +198,19 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
|
|
|
184
198
|
{isInView ? (
|
|
185
199
|
<View style={{ width: '100%', height: '100%' }}>
|
|
186
200
|
<PerspectiveCamera makeDefault position={[0, 0, 2.5]} fov={50} />
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
/>
|
|
197
|
-
</Suspense>
|
|
201
|
+
<ambientLight intensity={0.8} />
|
|
202
|
+
<pointLight position={[5, 5, 5]} intensity={0.5} />
|
|
203
|
+
<TextureSphere url={fullPath} onError={() => setError(true)} />
|
|
204
|
+
<OrbitControls
|
|
205
|
+
enableZoom={false}
|
|
206
|
+
enablePan={false}
|
|
207
|
+
autoRotate={isHovered}
|
|
208
|
+
autoRotateSpeed={2}
|
|
209
|
+
/>
|
|
198
210
|
</View>
|
|
199
211
|
) : null}
|
|
200
212
|
</div>
|
|
201
|
-
<div style={
|
|
213
|
+
<div style={styles.bottomLabel}>
|
|
202
214
|
{file.split('/').pop()}
|
|
203
215
|
</div>
|
|
204
216
|
</div>
|
|
@@ -206,10 +218,23 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
|
|
|
206
218
|
}
|
|
207
219
|
|
|
208
220
|
function TextureSphere({ url, onError }: { url: string; onError?: () => void }) {
|
|
209
|
-
const texture
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
const [texture, setTexture] = useState<any>(null);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
setTexture(null);
|
|
225
|
+
const loader = new TextureLoader();
|
|
226
|
+
loader.load(
|
|
227
|
+
url,
|
|
228
|
+
(tex) => setTexture(tex),
|
|
229
|
+
undefined,
|
|
230
|
+
(err) => {
|
|
231
|
+
console.warn('Failed to load texture:', url, err);
|
|
232
|
+
onError?.();
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
}, [url]);
|
|
236
|
+
|
|
237
|
+
if (!texture) return null;
|
|
213
238
|
return (
|
|
214
239
|
<mesh position={[0, 0, 0]}>
|
|
215
240
|
<sphereGeometry args={[1, 32, 32]} />
|
|
@@ -227,17 +252,19 @@ interface ModelListViewerProps {
|
|
|
227
252
|
|
|
228
253
|
export function ModelListViewer({ files, selected, onSelect, basePath = "" }: ModelListViewerProps) {
|
|
229
254
|
return (
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
255
|
+
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
|
256
|
+
<div style={{ width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 4 }}>
|
|
257
|
+
<AssetListViewer
|
|
258
|
+
files={files}
|
|
259
|
+
selected={selected}
|
|
260
|
+
onSelect={onSelect}
|
|
261
|
+
renderCard={(file, onSelectHandler) => (
|
|
262
|
+
<ModelCard file={file} basePath={basePath} onSelect={onSelectHandler} />
|
|
263
|
+
)}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
239
266
|
<SharedCanvas />
|
|
240
|
-
|
|
267
|
+
</div>
|
|
241
268
|
);
|
|
242
269
|
}
|
|
243
270
|
|
|
@@ -261,7 +288,7 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
261
288
|
return (
|
|
262
289
|
<div
|
|
263
290
|
ref={ref}
|
|
264
|
-
style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
291
|
+
style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
265
292
|
onClick={() => onSelect(file)}
|
|
266
293
|
>
|
|
267
294
|
<div style={styles.flexFillRelative}>
|
|
@@ -277,7 +304,7 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
277
304
|
</View>
|
|
278
305
|
) : null}
|
|
279
306
|
</div>
|
|
280
|
-
<div style={
|
|
307
|
+
<div style={styles.bottomLabel}>
|
|
281
308
|
{file.split('/').pop()}
|
|
282
309
|
</div>
|
|
283
310
|
</div>
|
|
@@ -335,10 +362,10 @@ function SoundCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
335
362
|
return (
|
|
336
363
|
<div
|
|
337
364
|
onClick={() => onSelect(file)}
|
|
338
|
-
style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
|
|
365
|
+
style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
|
|
339
366
|
>
|
|
340
367
|
<div style={styles.iconLarge}>🔊</div>
|
|
341
|
-
<div style={{ fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }}>{fileName}</div>
|
|
368
|
+
<div style={{ color: '#f9fafb', fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }}>{fileName}</div>
|
|
342
369
|
</div>
|
|
343
370
|
);
|
|
344
371
|
}
|
|
@@ -375,7 +402,11 @@ export function SharedCanvas() {
|
|
|
375
402
|
<Canvas
|
|
376
403
|
shadows
|
|
377
404
|
dpr={[1, 1.5]}
|
|
405
|
+
gl={{ alpha: true }}
|
|
378
406
|
camera={{ position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }}
|
|
407
|
+
onCreated={({ gl }) => {
|
|
408
|
+
gl.setClearAlpha(0);
|
|
409
|
+
}}
|
|
379
410
|
style={{
|
|
380
411
|
position: 'fixed',
|
|
381
412
|
top: 0,
|
|
@@ -383,6 +414,7 @@ export function SharedCanvas() {
|
|
|
383
414
|
width: '100vw',
|
|
384
415
|
height: '100vh',
|
|
385
416
|
pointerEvents: 'none',
|
|
417
|
+
background: 'transparent',
|
|
386
418
|
}}
|
|
387
419
|
eventSource={typeof document !== 'undefined' ? document.getElementById('root') || undefined : undefined}
|
|
388
420
|
eventPrefix="client"
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
}
|
|
@@ -5,6 +5,11 @@ interface EditorContextType {
|
|
|
5
5
|
setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
|
|
6
6
|
snapResolution: number;
|
|
7
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;
|
|
8
13
|
onScreenshot?: () => void;
|
|
9
14
|
onExportGLB?: () => void;
|
|
10
15
|
}
|