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.
Files changed (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. package/src/tools/prefabeditor/utils.ts +55 -4
@@ -1,15 +1,26 @@
1
- import { Canvas, useLoader } from "@react-three/fiber";
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
- <AssetListViewer
145
- files={files}
146
- selected={selected}
147
- onSelect={onSelect}
148
- renderCard={(file, onSelectHandler) => (
149
- <TextureCard file={file} basePath={basePath} onSelect={onSelectHandler} />
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
- <Suspense fallback={null}>
188
- <ambientLight intensity={0.8} />
189
- <pointLight position={[5, 5, 5]} intensity={0.5} />
190
- <TextureSphere url={fullPath} onError={() => setError(true)} />
191
- <OrbitControls
192
- enableZoom={false}
193
- enablePan={false}
194
- autoRotate={isHovered}
195
- autoRotateSpeed={2}
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={{ backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }}>
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 = useLoader(TextureLoader, url, undefined, (error) => {
210
- console.error('Failed to load texture:', url, error);
211
- onError?.();
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
- <AssetListViewer
232
- files={files}
233
- selected={selected}
234
- onSelect={onSelect}
235
- renderCard={(file, onSelectHandler) => (
236
- <ModelCard file={file} basePath={basePath} onSelect={onSelectHandler} />
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={{ backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }}>
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
  }