react-three-game 0.0.37 → 0.0.39

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 (45) hide show
  1. package/dist/helpers/SoundManager.d.ts +35 -0
  2. package/dist/helpers/SoundManager.js +93 -0
  3. package/dist/index.d.ts +6 -3
  4. package/dist/index.js +6 -5
  5. package/dist/shared/GameCanvas.d.ts +6 -3
  6. package/dist/shared/GameCanvas.js +4 -4
  7. package/dist/tools/loading/GameWithLoader.d.ts +6 -0
  8. package/dist/tools/loading/GameWithLoader.js +8 -0
  9. package/dist/tools/loading/loading.d.ts +2 -0
  10. package/dist/tools/loading/loading.js +38 -0
  11. package/dist/tools/prefabeditor/EditorContext.d.ts +11 -0
  12. package/dist/tools/prefabeditor/EditorContext.js +9 -0
  13. package/dist/tools/prefabeditor/EditorTree.d.ts +1 -3
  14. package/dist/tools/prefabeditor/EditorTree.js +38 -3
  15. package/dist/tools/prefabeditor/EditorUI.d.ts +1 -5
  16. package/dist/tools/prefabeditor/EditorUI.js +15 -13
  17. package/dist/tools/prefabeditor/ExportHelper.d.ts +7 -0
  18. package/dist/tools/prefabeditor/ExportHelper.js +55 -0
  19. package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
  20. package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
  21. package/dist/tools/prefabeditor/PrefabRoot.d.ts +4 -2
  22. package/dist/tools/prefabeditor/PrefabRoot.js +18 -41
  23. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +3 -1
  24. package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
  25. package/dist/tools/prefabeditor/components/Input.js +9 -3
  26. package/dist/tools/prefabeditor/components/MaterialComponent.js +23 -4
  27. package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
  28. package/dist/tools/prefabeditor/components/TransformComponent.js +11 -3
  29. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  30. package/dist/tools/prefabeditor/utils.js +41 -0
  31. package/package.json +1 -1
  32. package/src/helpers/SoundManager.ts +130 -0
  33. package/src/index.ts +13 -12
  34. package/src/shared/GameCanvas.tsx +9 -3
  35. package/src/tools/prefabeditor/EditorContext.tsx +20 -0
  36. package/src/tools/prefabeditor/EditorTree.tsx +83 -22
  37. package/src/tools/prefabeditor/EditorUI.tsx +14 -14
  38. package/src/tools/prefabeditor/PrefabEditor.tsx +79 -50
  39. package/src/tools/prefabeditor/PrefabRoot.tsx +26 -64
  40. package/src/tools/prefabeditor/components/ComponentRegistry.ts +3 -1
  41. package/src/tools/prefabeditor/components/Input.tsx +11 -3
  42. package/src/tools/prefabeditor/components/MaterialComponent.tsx +77 -5
  43. package/src/tools/prefabeditor/components/ModelComponent.tsx +3 -1
  44. package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
  45. package/src/tools/prefabeditor/utils.ts +43 -1
@@ -0,0 +1,130 @@
1
+
2
+ class SoundManager {
3
+ private static _instance: SoundManager | null = null
4
+
5
+ public context: AudioContext
6
+ private buffers = new Map<string, AudioBuffer>()
7
+
8
+ private masterGain: GainNode
9
+ private sfxGain: GainNode
10
+ private musicGain: GainNode
11
+
12
+ private constructor() {
13
+ const AudioCtx =
14
+ window.AudioContext || (window as any).webkitAudioContext
15
+
16
+ this.context = new AudioCtx()
17
+
18
+ this.masterGain = this.context.createGain()
19
+ this.sfxGain = this.context.createGain()
20
+ this.musicGain = this.context.createGain()
21
+
22
+ this.sfxGain.connect(this.masterGain)
23
+ this.musicGain.connect(this.masterGain)
24
+ this.masterGain.connect(this.context.destination)
25
+
26
+ this.masterGain.gain.value = 1
27
+ this.sfxGain.gain.value = 1
28
+ this.musicGain.gain.value = 1
29
+ }
30
+
31
+ /** Singleton accessor */
32
+ static get instance(): SoundManager {
33
+ if (typeof window === 'undefined') {
34
+ // Return a dummy instance for SSR
35
+ return new Proxy({} as SoundManager, {
36
+ get: () => () => {}
37
+ })
38
+ }
39
+ if (!SoundManager._instance) {
40
+ SoundManager._instance = new SoundManager()
41
+ }
42
+ return SoundManager._instance
43
+ }
44
+
45
+ /** Required once after user gesture (browser) */
46
+ resume() {
47
+ if (this.context.state !== "running") {
48
+ this.context.resume()
49
+ }
50
+ }
51
+
52
+ /** Preload a sound from URL */
53
+ async load(path: string, url: string) {
54
+ if (this.buffers.has(path)) return
55
+
56
+ const res = await fetch(url)
57
+ const arrayBuffer = await res.arrayBuffer()
58
+ const buffer = await this.context.decodeAudioData(arrayBuffer)
59
+
60
+ this.buffers.set(path, buffer)
61
+ }
62
+
63
+ /** Play from already-loaded buffer (fails silently if not loaded) */
64
+ playSync(
65
+ path: string,
66
+ {
67
+ volume = 1,
68
+ playbackRate = 1,
69
+ detune = 0,
70
+ pitch = 1,
71
+ }: {
72
+ volume?: number
73
+ playbackRate?: number
74
+ detune?: number
75
+ pitch?: number
76
+ } = {}
77
+ ) {
78
+ this.resume()
79
+
80
+ const buffer = this.buffers.get(path)
81
+ if (!buffer) return
82
+
83
+ const src = this.context.createBufferSource()
84
+ const gain = this.context.createGain()
85
+
86
+ src.buffer = buffer
87
+ src.playbackRate.value = playbackRate * pitch
88
+ src.detune.value = detune
89
+
90
+ gain.gain.value = volume
91
+
92
+ src.connect(gain)
93
+ gain.connect(this.sfxGain)
94
+
95
+ src.start()
96
+ }
97
+
98
+ /** Load and play SFX - accepts file path directly */
99
+ async play(
100
+ path: string,
101
+ options?: {
102
+ volume?: number
103
+ playbackRate?: number
104
+ detune?: number
105
+ pitch?: number
106
+ }
107
+ ) {
108
+ // Auto-load from path if not already loaded
109
+ if (!this.buffers.has(path)) {
110
+ await this.load(path, path)
111
+ }
112
+
113
+ this.playSync(path, options)
114
+ }
115
+
116
+ /** Volume controls */
117
+ setMasterVolume(v: number) {
118
+ this.masterGain.gain.value = v
119
+ }
120
+
121
+ setSfxVolume(v: number) {
122
+ this.sfxGain.gain.value = v
123
+ }
124
+
125
+ setMusicVolume(v: number) {
126
+ this.musicGain.gain.value = v
127
+ }
128
+ }
129
+
130
+ export const sound = SoundManager.instance
package/src/index.ts CHANGED
@@ -1,7 +1,18 @@
1
- // Components
1
+ // Core Components
2
2
  export { default as GameCanvas } from './shared/GameCanvas';
3
+
4
+ // Prefab Editor
3
5
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
6
+ export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
4
7
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
8
+ export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
9
+ export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
10
+ export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
11
+ export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
12
+ export * as editorStyles from './tools/prefabeditor/styles';
13
+ export * from './tools/prefabeditor/utils';
14
+
15
+ // Asset Tools
5
16
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
6
17
  export {
7
18
  TextureListViewer,
@@ -9,17 +20,7 @@ export {
9
20
  SoundListViewer,
10
21
  SharedCanvas,
11
22
  } from './tools/assetviewer/page';
12
-
13
- // Component Registry
14
- export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
15
- export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
16
-
17
- // Editor Styles & Utils
18
- export * as editorStyles from './tools/prefabeditor/styles';
19
- export * from './tools/prefabeditor/utils';
23
+ export { sound as soundManager } from './helpers/SoundManager';
20
24
 
21
25
  // Helpers
22
26
  export * from './helpers';
23
-
24
- // Types
25
- export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Canvas, extend } from "@react-three/fiber";
3
+ import { Canvas, extend, CanvasProps } from "@react-three/fiber";
4
4
  import { WebGPURenderer, MeshBasicNodeMaterial, MeshStandardNodeMaterial, SpriteNodeMaterial, PCFShadowMap } from "three/webgpu";
5
5
  import { Suspense, useState } from "react";
6
6
  import { WebGPURendererParameters } from "three/src/renderers/webgpu/WebGPURenderer.Nodes.js";
@@ -15,8 +15,13 @@ extend({
15
15
  SpriteNodeMaterial: SpriteNodeMaterial,
16
16
  });
17
17
 
18
+ interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
19
+ loader?: boolean;
20
+ children: React.ReactNode;
21
+ glConfig?: WebGPURendererParameters;
22
+ }
18
23
 
19
- export default function GameCanvas({ loader = false, children, ...props }: { loader?: boolean, children: React.ReactNode, props?: WebGPURendererParameters }) {
24
+ export default function GameCanvas({ loader = false, children, glConfig, ...props }: GameCanvasProps) {
20
25
  const [frameloop, setFrameloop] = useState<"never" | "always">("never");
21
26
 
22
27
  return <>
@@ -30,7 +35,7 @@ export default function GameCanvas({ loader = false, children, ...props }: { loa
30
35
  // @ts-expect-error futuristic
31
36
  shadowMap: true,
32
37
  antialias: true,
33
- ...props,
38
+ ...glConfig,
34
39
  });
35
40
  await renderer.init().then(() => {
36
41
  setFrameloop("always");
@@ -40,6 +45,7 @@ export default function GameCanvas({ loader = false, children, ...props }: { loa
40
45
  camera={{
41
46
  position: [0, 1, 5],
42
47
  }}
48
+ {...props}
43
49
  >
44
50
  <Suspense>
45
51
  {children}
@@ -0,0 +1,20 @@
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
+ onScreenshot?: () => void;
9
+ onExportGLB?: () => void;
10
+ }
11
+
12
+ export const EditorContext = createContext<EditorContextType | null>(null);
13
+
14
+ export function useEditorContext() {
15
+ const context = useContext(EditorContext);
16
+ if (!context) {
17
+ throw new Error("useEditorContext must be used within EditorContext.Provider");
18
+ }
19
+ return context;
20
+ }
@@ -2,15 +2,14 @@ import { Dispatch, SetStateAction, useState, MouseEvent } from 'react';
2
2
  import { Prefab, GameObject } from "./types";
3
3
  import { getComponent } from './components/ComponentRegistry';
4
4
  import { base, tree, menu } from './styles';
5
- import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
5
+ import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
6
+ import { useEditorContext } from './EditorContext';
6
7
 
7
8
  export default function EditorTree({
8
9
  prefabData,
9
10
  setPrefabData,
10
11
  selectedId,
11
12
  setSelectedId,
12
- onSave,
13
- onLoad,
14
13
  onUndo,
15
14
  onRedo,
16
15
  canUndo,
@@ -20,8 +19,6 @@ export default function EditorTree({
20
19
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
21
20
  selectedId: string | null;
22
21
  setSelectedId: Dispatch<SetStateAction<string | null>>;
23
- onSave?: () => void;
24
- onLoad?: () => void;
25
22
  onUndo?: () => void;
26
23
  onRedo?: () => void;
27
24
  canUndo?: boolean;
@@ -212,23 +209,11 @@ export default function EditorTree({
212
209
 
213
210
  </button>
214
211
  {fileMenuOpen && (
215
- <div
216
- style={{ ...menu.container, top: 28, right: 0 }}
217
- onClick={(e) => e.stopPropagation()}
218
- >
219
- <button
220
- style={menu.item}
221
- onClick={() => { onLoad?.(); setFileMenuOpen(false); }}
222
- >
223
- 📥 Load
224
- </button>
225
- <button
226
- style={menu.item}
227
- onClick={() => { onSave?.(); setFileMenuOpen(false); }}
228
- >
229
- 💾 Save
230
- </button>
231
- </div>
212
+ <FileMenu
213
+ prefabData={prefabData}
214
+ setPrefabData={setPrefabData}
215
+ onClose={() => setFileMenuOpen(false)}
216
+ />
232
217
  )}
233
218
  </div>
234
219
  </div>
@@ -261,3 +246,79 @@ export default function EditorTree({
261
246
  </>
262
247
  );
263
248
  }
249
+
250
+ function FileMenu({
251
+ prefabData,
252
+ setPrefabData,
253
+ onClose
254
+ }: {
255
+ prefabData: Prefab;
256
+ setPrefabData: Dispatch<SetStateAction<Prefab>>;
257
+ onClose: () => void;
258
+ }) {
259
+ const { onScreenshot, onExportGLB } = useEditorContext();
260
+
261
+ const handleLoad = async () => {
262
+ const loadedPrefab = await loadJson();
263
+ if (!loadedPrefab) return;
264
+ setPrefabData(loadedPrefab);
265
+ onClose();
266
+ };
267
+
268
+ const handleSave = () => {
269
+ saveJson(prefabData, "prefab");
270
+ onClose();
271
+ };
272
+
273
+ const handleLoadIntoScene = async () => {
274
+ const loadedPrefab = await loadJson();
275
+ if (!loadedPrefab) return;
276
+
277
+ setPrefabData(prev => ({
278
+ ...prev,
279
+ root: updateNodeById(prev.root, prev.root.id, root => ({
280
+ ...root,
281
+ children: [...(root.children ?? []), regenerateIds(loadedPrefab.root)]
282
+ }))
283
+ }));
284
+ onClose();
285
+ };
286
+
287
+ return (
288
+ <div
289
+ style={{ ...menu.container, top: 28, right: 0 }}
290
+ onClick={(e) => e.stopPropagation()}
291
+ >
292
+ <button
293
+ style={menu.item}
294
+ onClick={handleLoad}
295
+ >
296
+ 📥 Load Prefab JSON
297
+ </button>
298
+ <button
299
+ style={menu.item}
300
+ onClick={handleSave}
301
+ >
302
+ 💾 Save Prefab JSON
303
+ </button>
304
+ <button
305
+ style={menu.item}
306
+ onClick={handleLoadIntoScene}
307
+ >
308
+ 📂 Load into Scene
309
+ </button>
310
+ <button
311
+ style={menu.item}
312
+ onClick={() => { onScreenshot?.(); onClose(); }}
313
+ >
314
+ 📸 Screenshot
315
+ </button>
316
+ <button
317
+ style={menu.item}
318
+ onClick={() => { onExportGLB?.(); onClose(); }}
319
+ >
320
+ 📦 Export GLB
321
+ </button>
322
+ </div>
323
+ );
324
+ }
@@ -4,17 +4,14 @@ import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
5
  import { base, inspector } from './styles';
6
6
  import { findNode, updateNode, deleteNode } from './utils';
7
+ import { useEditorContext } from './EditorContext';
7
8
 
8
9
  function EditorUI({
9
10
  prefabData,
10
11
  setPrefabData,
11
12
  selectedId,
12
13
  setSelectedId,
13
- transformMode,
14
- setTransformMode,
15
14
  basePath,
16
- onSave,
17
- onLoad,
18
15
  onUndo,
19
16
  onRedo,
20
17
  canUndo,
@@ -24,17 +21,14 @@ function EditorUI({
24
21
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
25
22
  selectedId: string | null;
26
23
  setSelectedId: Dispatch<SetStateAction<string | null>>;
27
- transformMode: "translate" | "rotate" | "scale";
28
- setTransformMode: (m: "translate" | "rotate" | "scale") => void;
29
24
  basePath?: string;
30
- onSave?: () => void;
31
- onLoad?: () => void;
32
25
  onUndo?: () => void;
33
26
  onRedo?: () => void;
34
27
  canUndo?: boolean;
35
28
  canRedo?: boolean;
36
29
  }) {
37
30
  const [collapsed, setCollapsed] = useState(false);
31
+ const { transformMode, setTransformMode } = useEditorContext();
38
32
 
39
33
  const updateNodeHandler = (updater: (n: GameObjectType) => GameObjectType) => {
40
34
  if (!prefabData || !setPrefabData || !selectedId) return;
@@ -81,8 +75,6 @@ function EditorUI({
81
75
  setPrefabData={setPrefabData}
82
76
  selectedId={selectedId}
83
77
  setSelectedId={setSelectedId}
84
- onSave={onSave}
85
- onLoad={onLoad}
86
78
  onUndo={onUndo}
87
79
  onRedo={onRedo}
88
80
  canUndo={canUndo}
@@ -119,12 +111,19 @@ function NodeInspector({
119
111
  }, [Object.keys(node.components || {}).join(',')]);
120
112
 
121
113
  return <div style={inspector.content} className="prefab-scroll">
122
- {/* Node ID */}
114
+ {/* Node Name */}
123
115
  <div style={base.section}>
124
- <div style={base.label}>Node ID</div>
116
+ <div style={{ display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }}>
117
+ <div style={{ fontSize: 10, color: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }}>
118
+ {node.id}
119
+ </div>
120
+ <button style={{ ...base.btn, ...base.btnDanger }} title="Delete Node" onClick={deleteNode}>❌</button>
121
+ </div>
122
+
125
123
  <input
126
124
  style={base.input}
127
125
  value={node.name ?? ""}
126
+ placeholder='Node name'
128
127
  onChange={e =>
129
128
  updateNode(n => ({ ...n, name: e.target.value }))
130
129
  }
@@ -135,7 +134,6 @@ function NodeInspector({
135
134
  <div style={base.section}>
136
135
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
137
136
  <div style={base.label}>Components</div>
138
- <button style={{ ...base.btn, ...base.btnDanger }} onClick={deleteNode}>Delete Node</button>
139
137
  </div>
140
138
 
141
139
  {node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
@@ -151,6 +149,7 @@ function NodeInspector({
151
149
  <div style={{ fontSize: 11, fontWeight: 500 }}>{key}</div>
152
150
  <button
153
151
  style={{ ...base.btn, padding: '2px 6px' }}
152
+ title="Remove Component"
154
153
  onClick={() => updateNode(n => {
155
154
  const { [key]: _, ...rest } = n.components || {};
156
155
  return { ...n, components: rest };
@@ -162,6 +161,7 @@ function NodeInspector({
162
161
  {def.Editor && (
163
162
  <def.Editor
164
163
  component={comp}
164
+ node={node}
165
165
  onUpdate={(newProps: any) => updateNode(n => ({
166
166
  ...n,
167
167
  components: {
@@ -182,7 +182,6 @@ function NodeInspector({
182
182
  {/* Add Component */}
183
183
  {available.length > 0 && (
184
184
  <div>
185
- <div style={base.label}>Add Component</div>
186
185
  <div style={base.row}>
187
186
  <select
188
187
  style={{ ...base.input, flex: 1 }}
@@ -207,6 +206,7 @@ function NodeInspector({
207
206
  }));
208
207
  }
209
208
  }}
209
+ title="Add Component"
210
210
  >
211
211
  +
212
212
  </button>
@@ -1,12 +1,23 @@
1
1
  "use client";
2
2
 
3
3
  import GameCanvas from "../../shared/GameCanvas";
4
- import { useState, useRef, useEffect } from "react";
4
+ import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
5
5
  import { Prefab } from "./types";
6
- import PrefabRoot from "./PrefabRoot";
6
+ import PrefabRoot, { PrefabRootRef } from "./PrefabRoot";
7
7
  import { Physics } from "@react-three/rapier";
8
8
  import EditorUI from "./EditorUI";
9
9
  import { base, toolbar } from "./styles";
10
+ import { EditorContext } from "./EditorContext";
11
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
12
+ import { Group } from "three";
13
+
14
+ export interface PrefabEditorRef {
15
+ screenshot: () => void;
16
+ exportGLB: () => void;
17
+ prefab: Prefab;
18
+ setPrefab: (prefab: Prefab) => void;
19
+ rootRef: React.RefObject<PrefabRootRef | null>;
20
+ }
10
21
 
11
22
  const DEFAULT_PREFAB: Prefab = {
12
23
  id: "prefab-default",
@@ -22,20 +33,23 @@ const DEFAULT_PREFAB: Prefab = {
22
33
  }
23
34
  };
24
35
 
25
- const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
36
+ const PrefabEditor = forwardRef<PrefabEditorRef, {
26
37
  basePath?: string;
27
38
  initialPrefab?: Prefab;
28
39
  onPrefabChange?: (prefab: Prefab) => void;
29
40
  children?: React.ReactNode;
30
- }) => {
41
+ }>(({ basePath, initialPrefab, onPrefabChange, children }, ref) => {
31
42
  const [editMode, setEditMode] = useState(true);
32
43
  const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? DEFAULT_PREFAB);
33
44
  const [selectedId, setSelectedId] = useState<string | null>(null);
34
45
  const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
46
+ const [snapResolution, setSnapResolution] = useState(0);
35
47
  const [history, setHistory] = useState<Prefab[]>([loadedPrefab]);
36
48
  const [historyIndex, setHistoryIndex] = useState(0);
37
49
  const throttleRef = useRef<NodeJS.Timeout | null>(null);
38
50
  const lastDataRef = useRef(JSON.stringify(loadedPrefab));
51
+ const prefabRootRef = useRef<PrefabRootRef>(null);
52
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
39
53
 
40
54
  useEffect(() => {
41
55
  if (initialPrefab) setLoadedPrefab(initialPrefab);
@@ -84,29 +98,76 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
84
98
  return () => { if (throttleRef.current) clearTimeout(throttleRef.current); };
85
99
  }, [loadedPrefab]);
86
100
 
87
- const handleLoad = async () => {
88
- const prefab = await loadJson();
89
- if (prefab) {
90
- setLoadedPrefab(prefab);
91
- onPrefabChange?.(prefab);
92
- setHistory([prefab]);
93
- setHistoryIndex(0);
94
- lastDataRef.current = JSON.stringify(prefab);
95
- }
101
+ const handleScreenshot = () => {
102
+ const canvas = canvasRef.current;
103
+ if (!canvas) return;
104
+
105
+ canvas.toBlob((blob) => {
106
+ if (!blob) return;
107
+ const url = URL.createObjectURL(blob);
108
+ const a = document.createElement('a');
109
+ a.href = url;
110
+ a.download = `${loadedPrefab.name || 'screenshot'}.png`;
111
+ a.click();
112
+ URL.revokeObjectURL(url);
113
+ });
114
+ };
115
+
116
+ const handleExportGLB = () => {
117
+ const sceneRoot = prefabRootRef.current?.root;
118
+ if (!sceneRoot) return;
119
+
120
+ const exporter = new GLTFExporter();
121
+ exporter.parse(
122
+ sceneRoot,
123
+ (result) => {
124
+ const blob = new Blob([result as ArrayBuffer], { type: 'application/octet-stream' });
125
+ const url = URL.createObjectURL(blob);
126
+ const a = document.createElement('a');
127
+ a.href = url;
128
+ a.download = `${loadedPrefab.name || 'scene'}.glb`;
129
+ a.click();
130
+ URL.revokeObjectURL(url);
131
+ },
132
+ (error) => {
133
+ console.error('Error exporting GLB:', error);
134
+ },
135
+ { binary: true }
136
+ );
96
137
  };
97
138
 
98
- return <>
139
+ useEffect(() => {
140
+ const canvas = document.querySelector('canvas');
141
+ if (canvas) canvasRef.current = canvas;
142
+ }, []);
143
+
144
+ useImperativeHandle(ref, () => ({
145
+ screenshot: handleScreenshot,
146
+ exportGLB: handleExportGLB,
147
+ prefab: loadedPrefab,
148
+ setPrefab: setLoadedPrefab,
149
+ rootRef: prefabRootRef
150
+ }), [loadedPrefab]);
151
+
152
+ return <EditorContext.Provider value={{
153
+ transformMode,
154
+ setTransformMode,
155
+ snapResolution,
156
+ setSnapResolution,
157
+ onScreenshot: handleScreenshot,
158
+ onExportGLB: handleExportGLB
159
+ }}>
99
160
  <GameCanvas>
100
161
  <Physics debug={editMode} paused={editMode}>
101
162
  <ambientLight intensity={1.5} />
102
163
  <gridHelper args={[10, 10]} position={[0, -1, 0]} />
103
164
  <PrefabRoot
165
+ ref={prefabRootRef}
104
166
  data={loadedPrefab}
105
167
  editMode={editMode}
106
168
  onPrefabChange={updatePrefab}
107
169
  selectedId={selectedId}
108
170
  onSelect={setSelectedId}
109
- transformMode={transformMode}
110
171
  basePath={basePath}
111
172
  />
112
173
  {children}
@@ -123,47 +184,15 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: {
123
184
  setPrefabData={updatePrefab}
124
185
  selectedId={selectedId}
125
186
  setSelectedId={setSelectedId}
126
- transformMode={transformMode}
127
- setTransformMode={setTransformMode}
128
187
  basePath={basePath}
129
- onSave={() => saveJson(loadedPrefab, "prefab")}
130
- onLoad={handleLoad}
131
188
  onUndo={undo}
132
189
  onRedo={redo}
133
190
  canUndo={historyIndex > 0}
134
191
  canRedo={historyIndex < history.length - 1}
135
192
  />}
136
- </>
137
- }
138
-
139
-
140
- const saveJson = (data: Prefab, filename: string) => {
141
- const a = document.createElement('a');
142
- a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
143
- a.download = `${filename || 'prefab'}.json`;
144
- a.click();
145
- };
146
-
147
- const loadJson = () => new Promise<Prefab | undefined>(resolve => {
148
- const input = document.createElement('input');
149
- input.type = 'file';
150
- input.accept = '.json,application/json';
151
- input.onchange = e => {
152
- const file = (e.target as HTMLInputElement).files?.[0];
153
- if (!file) return resolve(undefined);
154
- const reader = new FileReader();
155
- reader.onload = e => {
156
- try {
157
- const text = e.target?.result;
158
- if (typeof text === 'string') resolve(JSON.parse(text) as Prefab);
159
- } catch (err) {
160
- console.error('Error parsing prefab JSON:', err);
161
- resolve(undefined);
162
- }
163
- };
164
- reader.readAsText(file);
165
- };
166
- input.click();
193
+ </EditorContext.Provider>
167
194
  });
168
195
 
196
+ PrefabEditor.displayName = "PrefabEditor";
197
+
169
198
  export default PrefabEditor;