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.
- package/dist/helpers/SoundManager.d.ts +35 -0
- package/dist/helpers/SoundManager.js +93 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -5
- package/dist/shared/GameCanvas.d.ts +6 -3
- package/dist/shared/GameCanvas.js +4 -4
- package/dist/tools/loading/GameWithLoader.d.ts +6 -0
- package/dist/tools/loading/GameWithLoader.js +8 -0
- package/dist/tools/loading/loading.d.ts +2 -0
- package/dist/tools/loading/loading.js +38 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +11 -0
- package/dist/tools/prefabeditor/EditorContext.js +9 -0
- package/dist/tools/prefabeditor/EditorTree.d.ts +1 -3
- package/dist/tools/prefabeditor/EditorTree.js +38 -3
- package/dist/tools/prefabeditor/EditorUI.d.ts +1 -5
- package/dist/tools/prefabeditor/EditorUI.js +15 -13
- package/dist/tools/prefabeditor/ExportHelper.d.ts +7 -0
- package/dist/tools/prefabeditor/ExportHelper.js +55 -0
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
- package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +4 -2
- package/dist/tools/prefabeditor/PrefabRoot.js +18 -41
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +3 -1
- package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
- package/dist/tools/prefabeditor/components/Input.js +9 -3
- package/dist/tools/prefabeditor/components/MaterialComponent.js +23 -4
- package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
- package/dist/tools/prefabeditor/components/TransformComponent.js +11 -3
- package/dist/tools/prefabeditor/utils.d.ts +7 -1
- package/dist/tools/prefabeditor/utils.js +41 -0
- package/package.json +1 -1
- package/src/helpers/SoundManager.ts +130 -0
- package/src/index.ts +13 -12
- package/src/shared/GameCanvas.tsx +9 -3
- package/src/tools/prefabeditor/EditorContext.tsx +20 -0
- package/src/tools/prefabeditor/EditorTree.tsx +83 -22
- package/src/tools/prefabeditor/EditorUI.tsx +14 -14
- package/src/tools/prefabeditor/PrefabEditor.tsx +79 -50
- package/src/tools/prefabeditor/PrefabRoot.tsx +26 -64
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +3 -1
- package/src/tools/prefabeditor/components/Input.tsx +11 -3
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +77 -5
- package/src/tools/prefabeditor/components/ModelComponent.tsx +3 -1
- package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
- package/src/tools/prefabeditor/utils.ts +43 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
declare class SoundManager {
|
|
2
|
+
private static _instance;
|
|
3
|
+
context: AudioContext;
|
|
4
|
+
private buffers;
|
|
5
|
+
private masterGain;
|
|
6
|
+
private sfxGain;
|
|
7
|
+
private musicGain;
|
|
8
|
+
private constructor();
|
|
9
|
+
/** Singleton accessor */
|
|
10
|
+
static get instance(): SoundManager;
|
|
11
|
+
/** Required once after user gesture (browser) */
|
|
12
|
+
resume(): void;
|
|
13
|
+
/** Preload a sound from URL */
|
|
14
|
+
load(path: string, url: string): Promise<void>;
|
|
15
|
+
/** Play from already-loaded buffer (fails silently if not loaded) */
|
|
16
|
+
playSync(path: string, { volume, playbackRate, detune, pitch, }?: {
|
|
17
|
+
volume?: number;
|
|
18
|
+
playbackRate?: number;
|
|
19
|
+
detune?: number;
|
|
20
|
+
pitch?: number;
|
|
21
|
+
}): void;
|
|
22
|
+
/** Load and play SFX - accepts file path directly */
|
|
23
|
+
play(path: string, options?: {
|
|
24
|
+
volume?: number;
|
|
25
|
+
playbackRate?: number;
|
|
26
|
+
detune?: number;
|
|
27
|
+
pitch?: number;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
/** Volume controls */
|
|
30
|
+
setMasterVolume(v: number): void;
|
|
31
|
+
setSfxVolume(v: number): void;
|
|
32
|
+
setMusicVolume(v: number): void;
|
|
33
|
+
}
|
|
34
|
+
export declare const sound: SoundManager;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
};
|
|
10
|
+
class SoundManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.buffers = new Map();
|
|
13
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
14
|
+
this.context = new AudioCtx();
|
|
15
|
+
this.masterGain = this.context.createGain();
|
|
16
|
+
this.sfxGain = this.context.createGain();
|
|
17
|
+
this.musicGain = this.context.createGain();
|
|
18
|
+
this.sfxGain.connect(this.masterGain);
|
|
19
|
+
this.musicGain.connect(this.masterGain);
|
|
20
|
+
this.masterGain.connect(this.context.destination);
|
|
21
|
+
this.masterGain.gain.value = 1;
|
|
22
|
+
this.sfxGain.gain.value = 1;
|
|
23
|
+
this.musicGain.gain.value = 1;
|
|
24
|
+
}
|
|
25
|
+
/** Singleton accessor */
|
|
26
|
+
static get instance() {
|
|
27
|
+
if (typeof window === 'undefined') {
|
|
28
|
+
// Return a dummy instance for SSR
|
|
29
|
+
return new Proxy({}, {
|
|
30
|
+
get: () => () => { }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (!SoundManager._instance) {
|
|
34
|
+
SoundManager._instance = new SoundManager();
|
|
35
|
+
}
|
|
36
|
+
return SoundManager._instance;
|
|
37
|
+
}
|
|
38
|
+
/** Required once after user gesture (browser) */
|
|
39
|
+
resume() {
|
|
40
|
+
if (this.context.state !== "running") {
|
|
41
|
+
this.context.resume();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Preload a sound from URL */
|
|
45
|
+
load(path, url) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
if (this.buffers.has(path))
|
|
48
|
+
return;
|
|
49
|
+
const res = yield fetch(url);
|
|
50
|
+
const arrayBuffer = yield res.arrayBuffer();
|
|
51
|
+
const buffer = yield this.context.decodeAudioData(arrayBuffer);
|
|
52
|
+
this.buffers.set(path, buffer);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Play from already-loaded buffer (fails silently if not loaded) */
|
|
56
|
+
playSync(path, { volume = 1, playbackRate = 1, detune = 0, pitch = 1, } = {}) {
|
|
57
|
+
this.resume();
|
|
58
|
+
const buffer = this.buffers.get(path);
|
|
59
|
+
if (!buffer)
|
|
60
|
+
return;
|
|
61
|
+
const src = this.context.createBufferSource();
|
|
62
|
+
const gain = this.context.createGain();
|
|
63
|
+
src.buffer = buffer;
|
|
64
|
+
src.playbackRate.value = playbackRate * pitch;
|
|
65
|
+
src.detune.value = detune;
|
|
66
|
+
gain.gain.value = volume;
|
|
67
|
+
src.connect(gain);
|
|
68
|
+
gain.connect(this.sfxGain);
|
|
69
|
+
src.start();
|
|
70
|
+
}
|
|
71
|
+
/** Load and play SFX - accepts file path directly */
|
|
72
|
+
play(path, options) {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
// Auto-load from path if not already loaded
|
|
75
|
+
if (!this.buffers.has(path)) {
|
|
76
|
+
yield this.load(path, path);
|
|
77
|
+
}
|
|
78
|
+
this.playSync(path, options);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Volume controls */
|
|
82
|
+
setMasterVolume(v) {
|
|
83
|
+
this.masterGain.gain.value = v;
|
|
84
|
+
}
|
|
85
|
+
setSfxVolume(v) {
|
|
86
|
+
this.sfxGain.gain.value = v;
|
|
87
|
+
}
|
|
88
|
+
setMusicVolume(v) {
|
|
89
|
+
this.musicGain.gain.value = v;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
SoundManager._instance = null;
|
|
93
|
+
export const sound = SoundManager.instance;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export { default as GameCanvas } from './shared/GameCanvas';
|
|
2
2
|
export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
3
|
+
export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
|
|
3
4
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
4
|
-
export {
|
|
5
|
-
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
5
|
+
export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
|
|
6
6
|
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
7
7
|
export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
|
|
8
|
+
export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
|
|
8
9
|
export * as editorStyles from './tools/prefabeditor/styles';
|
|
9
10
|
export * from './tools/prefabeditor/utils';
|
|
11
|
+
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
12
|
+
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
13
|
+
export { sound as soundManager } from './helpers/SoundManager';
|
|
10
14
|
export * from './helpers';
|
|
11
|
-
export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// Components
|
|
1
|
+
// Core Components
|
|
2
2
|
export { default as GameCanvas } from './shared/GameCanvas';
|
|
3
|
+
// Prefab Editor
|
|
3
4
|
export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
4
5
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
5
|
-
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
6
|
-
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
7
|
-
// Component Registry
|
|
8
6
|
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
9
|
-
// Editor Styles & Utils
|
|
10
7
|
export * as editorStyles from './tools/prefabeditor/styles';
|
|
11
8
|
export * from './tools/prefabeditor/utils';
|
|
9
|
+
// Asset Tools
|
|
10
|
+
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
11
|
+
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
12
|
+
export { sound as soundManager } from './helpers/SoundManager';
|
|
12
13
|
// Helpers
|
|
13
14
|
export * from './helpers';
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { CanvasProps } from "@react-three/fiber";
|
|
1
2
|
import { WebGPURendererParameters } from "three/src/renderers/webgpu/WebGPURenderer.Nodes.js";
|
|
2
|
-
|
|
3
|
+
interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
|
|
3
4
|
loader?: boolean;
|
|
4
5
|
children: React.ReactNode;
|
|
5
|
-
|
|
6
|
-
}
|
|
6
|
+
glConfig?: WebGPURendererParameters;
|
|
7
|
+
}
|
|
8
|
+
export default function GameCanvas({ loader, children, glConfig, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -32,17 +32,17 @@ extend({
|
|
|
32
32
|
SpriteNodeMaterial: SpriteNodeMaterial,
|
|
33
33
|
});
|
|
34
34
|
export default function GameCanvas(_a) {
|
|
35
|
-
var { loader = false, children } = _a, props = __rest(_a, ["loader", "children"]);
|
|
35
|
+
var { loader = false, children, glConfig } = _a, props = __rest(_a, ["loader", "children", "glConfig"]);
|
|
36
36
|
const [frameloop, setFrameloop] = useState("never");
|
|
37
|
-
return _jsx(_Fragment, { children: _jsxs(Canvas, { style: { touchAction: 'none' }, shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
|
|
37
|
+
return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: { touchAction: 'none' }, shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
|
|
38
38
|
const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
|
|
39
39
|
// @ts-expect-error futuristic
|
|
40
|
-
shadowMap: true, antialias: true },
|
|
40
|
+
shadowMap: true, antialias: true }, glConfig));
|
|
41
41
|
yield renderer.init().then(() => {
|
|
42
42
|
setFrameloop("always");
|
|
43
43
|
});
|
|
44
44
|
return renderer;
|
|
45
45
|
}), camera: {
|
|
46
46
|
position: [0, 1, 5],
|
|
47
|
-
}, children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] }) });
|
|
47
|
+
} }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
|
|
48
48
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, useState } from "react";
|
|
4
|
+
import LoadingSpinner from "../../sketches/loading/loading";
|
|
5
|
+
export default function GameWithLoader({ children }) {
|
|
6
|
+
const [isCanvasReady, setIsCanvasReady] = useState(false);
|
|
7
|
+
return (_jsxs(_Fragment, { children: [!isCanvasReady && _jsx(LoadingSpinner, {}), cloneElement(children, { onCanvasReady: () => setIsCanvasReady(true) })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
const lines = [
|
|
5
|
+
"$ initializing...",
|
|
6
|
+
"✓ loading scene",
|
|
7
|
+
"✓ ready"
|
|
8
|
+
];
|
|
9
|
+
const LoadingSpinner = () => {
|
|
10
|
+
const [index, setIndex] = useState(0);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (index < lines.length) {
|
|
13
|
+
const timer = setTimeout(() => setIndex(index + 1), 300);
|
|
14
|
+
return () => clearTimeout(timer);
|
|
15
|
+
}
|
|
16
|
+
}, [index]);
|
|
17
|
+
return (_jsxs("div", { className: "terminal-loading", children: [lines.slice(0, index).map((line, i) => (_jsx("div", { children: line }, i))), _jsx("div", { className: "cursor", children: "\u2588" }), _jsx("style", { jsx: true, children: `
|
|
18
|
+
.terminal-loading {
|
|
19
|
+
position: fixed;
|
|
20
|
+
inset: 0;
|
|
21
|
+
z-index: 50;
|
|
22
|
+
background: #0a0a0a;
|
|
23
|
+
color: #00ff00;
|
|
24
|
+
font-family: monospace;
|
|
25
|
+
padding: 2rem;
|
|
26
|
+
font-size: 14px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.cursor {
|
|
30
|
+
animation: blink 1s infinite;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes blink {
|
|
34
|
+
50% { opacity: 0; }
|
|
35
|
+
}
|
|
36
|
+
` })] }));
|
|
37
|
+
};
|
|
38
|
+
export default LoadingSpinner;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface EditorContextType {
|
|
2
|
+
transformMode: "translate" | "rotate" | "scale";
|
|
3
|
+
setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
|
|
4
|
+
snapResolution: number;
|
|
5
|
+
setSnapResolution: (resolution: number) => void;
|
|
6
|
+
onScreenshot?: () => void;
|
|
7
|
+
onExportGLB?: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const EditorContext: import("react").Context<EditorContextType | null>;
|
|
10
|
+
export declare function useEditorContext(): EditorContextType;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
export const EditorContext = createContext(null);
|
|
3
|
+
export function useEditorContext() {
|
|
4
|
+
const context = useContext(EditorContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error("useEditorContext must be used within EditorContext.Provider");
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Dispatch, SetStateAction } from 'react';
|
|
2
2
|
import { Prefab } from "./types";
|
|
3
|
-
export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId,
|
|
3
|
+
export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }: {
|
|
4
4
|
prefabData?: Prefab;
|
|
5
5
|
setPrefabData?: Dispatch<SetStateAction<Prefab>>;
|
|
6
6
|
selectedId: string | null;
|
|
7
7
|
setSelectedId: Dispatch<SetStateAction<string | null>>;
|
|
8
|
-
onSave?: () => void;
|
|
9
|
-
onLoad?: () => void;
|
|
10
8
|
onUndo?: () => void;
|
|
11
9
|
onRedo?: () => void;
|
|
12
10
|
canUndo?: boolean;
|
|
@@ -1,9 +1,19 @@
|
|
|
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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
11
|
import { useState } from 'react';
|
|
3
12
|
import { getComponent } from './components/ComponentRegistry';
|
|
4
13
|
import { base, tree, menu } from './styles';
|
|
5
|
-
import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
|
|
6
|
-
|
|
14
|
+
import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
|
|
15
|
+
import { useEditorContext } from './EditorContext';
|
|
16
|
+
export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }) {
|
|
7
17
|
const [contextMenu, setContextMenu] = useState(null);
|
|
8
18
|
const [draggedId, setDraggedId] = useState(null);
|
|
9
19
|
const [collapsedIds, setCollapsedIds] = useState(new Set());
|
|
@@ -111,5 +121,30 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
111
121
|
visibility: hasChildren ? 'visible' : 'hidden'
|
|
112
122
|
}, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id })] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
|
|
113
123
|
};
|
|
114
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (
|
|
124
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: () => setFileMenuOpen(false) }))] })] }))] }), !collapsed && _jsx("div", { style: tree.scroll, children: renderNode(prefabData.root) })] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
|
|
125
|
+
}
|
|
126
|
+
function FileMenu({ prefabData, setPrefabData, onClose }) {
|
|
127
|
+
const { onScreenshot, onExportGLB } = useEditorContext();
|
|
128
|
+
const handleLoad = () => __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
const loadedPrefab = yield loadJson();
|
|
130
|
+
if (!loadedPrefab)
|
|
131
|
+
return;
|
|
132
|
+
setPrefabData(loadedPrefab);
|
|
133
|
+
onClose();
|
|
134
|
+
});
|
|
135
|
+
const handleSave = () => {
|
|
136
|
+
saveJson(prefabData, "prefab");
|
|
137
|
+
onClose();
|
|
138
|
+
};
|
|
139
|
+
const handleLoadIntoScene = () => __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
const loadedPrefab = yield loadJson();
|
|
141
|
+
if (!loadedPrefab)
|
|
142
|
+
return;
|
|
143
|
+
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, prev.root.id, root => {
|
|
144
|
+
var _a;
|
|
145
|
+
return (Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), regenerateIds(loadedPrefab.root)] }));
|
|
146
|
+
}) })));
|
|
147
|
+
onClose();
|
|
148
|
+
});
|
|
149
|
+
return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: 28, right: 0 }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: handleLoad, children: "\uD83D\uDCE5 Load Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleSave, children: "\uD83D\uDCBE Save Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleLoadIntoScene, children: "\uD83D\uDCC2 Load into Scene" }), _jsx("button", { style: menu.item, onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "\uD83D\uDCF8 Screenshot" }), _jsx("button", { style: menu.item, onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "\uD83D\uDCE6 Export GLB" })] }));
|
|
115
150
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { Dispatch, SetStateAction } from 'react';
|
|
2
2
|
import { Prefab } from "./types";
|
|
3
|
-
declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId,
|
|
3
|
+
declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePath, onUndo, onRedo, canUndo, canRedo }: {
|
|
4
4
|
prefabData?: Prefab;
|
|
5
5
|
setPrefabData?: Dispatch<SetStateAction<Prefab>>;
|
|
6
6
|
selectedId: string | null;
|
|
7
7
|
setSelectedId: Dispatch<SetStateAction<string | null>>;
|
|
8
|
-
transformMode: "translate" | "rotate" | "scale";
|
|
9
|
-
setTransformMode: (m: "translate" | "rotate" | "scale") => void;
|
|
10
8
|
basePath?: string;
|
|
11
|
-
onSave?: () => void;
|
|
12
|
-
onLoad?: () => void;
|
|
13
9
|
onUndo?: () => void;
|
|
14
10
|
onRedo?: () => void;
|
|
15
11
|
canUndo?: boolean;
|
|
@@ -15,8 +15,10 @@ import EditorTree from './EditorTree';
|
|
|
15
15
|
import { getAllComponents } from './components/ComponentRegistry';
|
|
16
16
|
import { base, inspector } from './styles';
|
|
17
17
|
import { findNode, updateNode, deleteNode } from './utils';
|
|
18
|
-
|
|
18
|
+
import { useEditorContext } from './EditorContext';
|
|
19
|
+
function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePath, onUndo, onRedo, canUndo, canRedo }) {
|
|
19
20
|
const [collapsed, setCollapsed] = useState(false);
|
|
21
|
+
const { transformMode, setTransformMode } = useEditorContext();
|
|
20
22
|
const updateNodeHandler = (updater) => {
|
|
21
23
|
if (!prefabData || !setPrefabData || !selectedId)
|
|
22
24
|
return;
|
|
@@ -34,7 +36,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
34
36
|
.prefab-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
35
37
|
.prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
36
38
|
.prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
|
|
37
|
-
` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId,
|
|
39
|
+
` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
|
|
38
40
|
}
|
|
39
41
|
function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
|
|
40
42
|
var _a;
|
|
@@ -47,23 +49,23 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
47
49
|
if (!newAvailable.includes(addType))
|
|
48
50
|
setAddType(newAvailable[0] || "");
|
|
49
51
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
50
|
-
return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style:
|
|
52
|
+
return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("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 }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
51
53
|
if (!comp)
|
|
52
54
|
return null;
|
|
53
55
|
const def = ALL_COMPONENTS[comp.type];
|
|
54
56
|
if (!def)
|
|
55
57
|
return _jsxs("div", { style: { color: '#ff8888', fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
|
|
56
|
-
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), onClick: () => updateNode(n => {
|
|
58
|
+
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
|
|
57
59
|
const _a = n.components || {}, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
|
|
58
60
|
return Object.assign(Object.assign({}, n), { components: rest });
|
|
59
|
-
}), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath, transformMode: transformMode, setTransformMode: setTransformMode }))] }, key));
|
|
60
|
-
})] }), available.length > 0 && (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
}), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath, transformMode: transformMode, setTransformMode: setTransformMode }))] }, key));
|
|
62
|
+
})] }), available.length > 0 && (_jsx("div", { children: _jsxs("div", { style: base.row, children: [_jsx("select", { style: Object.assign(Object.assign({}, base.input), { flex: 1 }), value: addType, onChange: e => setAddType(e.target.value), children: available.map(k => _jsx("option", { value: k, children: k }, k)) }), _jsx("button", { style: base.btn, disabled: !addType, onClick: () => {
|
|
63
|
+
if (!addType)
|
|
64
|
+
return;
|
|
65
|
+
const def = ALL_COMPONENTS[addType];
|
|
66
|
+
if (def) {
|
|
67
|
+
updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [addType.toLowerCase()]: { type: def.name, properties: def.defaultProperties } }) })));
|
|
68
|
+
}
|
|
69
|
+
}, title: "Add Component", children: "+" })] }) }))] });
|
|
68
70
|
}
|
|
69
71
|
export default EditorUI;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SCREENSHOT_EVENT = "prefab-editor-screenshot";
|
|
2
|
+
export declare const EXPORT_GLB_EVENT = "prefab-editor-export-glb";
|
|
3
|
+
export declare function ExportHelper({ prefabName }: {
|
|
4
|
+
prefabName: string;
|
|
5
|
+
}): null;
|
|
6
|
+
export declare function triggerScreenshot(): void;
|
|
7
|
+
export declare function triggerExportGLB(): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useThree } from "@react-three/fiber";
|
|
3
|
+
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
|
4
|
+
// Custom events for triggering exports
|
|
5
|
+
export const SCREENSHOT_EVENT = "prefab-editor-screenshot";
|
|
6
|
+
export const EXPORT_GLB_EVENT = "prefab-editor-export-glb";
|
|
7
|
+
export function ExportHelper({ prefabName }) {
|
|
8
|
+
const { gl, scene } = useThree();
|
|
9
|
+
const sceneRef = useRef(scene);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
sceneRef.current = scene;
|
|
12
|
+
}, [scene]);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const handleScreenshot = () => {
|
|
15
|
+
const canvas = gl.domElement;
|
|
16
|
+
canvas.toBlob((blob) => {
|
|
17
|
+
if (!blob)
|
|
18
|
+
return;
|
|
19
|
+
const url = URL.createObjectURL(blob);
|
|
20
|
+
const a = document.createElement('a');
|
|
21
|
+
a.href = url;
|
|
22
|
+
a.download = `${prefabName || 'screenshot'}.png`;
|
|
23
|
+
a.click();
|
|
24
|
+
URL.revokeObjectURL(url);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const handleExportGLB = () => {
|
|
28
|
+
const exporter = new GLTFExporter();
|
|
29
|
+
exporter.parse(sceneRef.current, (result) => {
|
|
30
|
+
const blob = new Blob([result], { type: 'application/octet-stream' });
|
|
31
|
+
const url = URL.createObjectURL(blob);
|
|
32
|
+
const a = document.createElement('a');
|
|
33
|
+
a.href = url;
|
|
34
|
+
a.download = `${prefabName || 'scene'}.glb`;
|
|
35
|
+
a.click();
|
|
36
|
+
URL.revokeObjectURL(url);
|
|
37
|
+
}, (error) => {
|
|
38
|
+
console.error('Error exporting GLB:', error);
|
|
39
|
+
}, { binary: true });
|
|
40
|
+
};
|
|
41
|
+
window.addEventListener(SCREENSHOT_EVENT, handleScreenshot);
|
|
42
|
+
window.addEventListener(EXPORT_GLB_EVENT, handleExportGLB);
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener(SCREENSHOT_EVENT, handleScreenshot);
|
|
45
|
+
window.removeEventListener(EXPORT_GLB_EVENT, handleExportGLB);
|
|
46
|
+
};
|
|
47
|
+
}, [gl, prefabName]);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export function triggerScreenshot() {
|
|
51
|
+
window.dispatchEvent(new Event(SCREENSHOT_EVENT));
|
|
52
|
+
}
|
|
53
|
+
export function triggerExportGLB() {
|
|
54
|
+
window.dispatchEvent(new Event(EXPORT_GLB_EVENT));
|
|
55
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { Prefab } from "./types";
|
|
2
|
-
|
|
2
|
+
import { PrefabRootRef } from "./PrefabRoot";
|
|
3
|
+
export interface PrefabEditorRef {
|
|
4
|
+
screenshot: () => void;
|
|
5
|
+
exportGLB: () => void;
|
|
6
|
+
prefab: Prefab;
|
|
7
|
+
setPrefab: (prefab: Prefab) => void;
|
|
8
|
+
rootRef: React.RefObject<PrefabRootRef | null>;
|
|
9
|
+
}
|
|
10
|
+
declare const PrefabEditor: import("react").ForwardRefExoticComponent<{
|
|
3
11
|
basePath?: string;
|
|
4
12
|
initialPrefab?: Prefab;
|
|
5
13
|
onPrefabChange?: (prefab: Prefab) => void;
|
|
6
14
|
children?: React.ReactNode;
|
|
7
|
-
}
|
|
15
|
+
} & import("react").RefAttributes<PrefabEditorRef>>;
|
|
8
16
|
export default PrefabEditor;
|