react-three-game 0.0.36 → 0.0.38
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/index.d.ts +5 -3
- package/dist/index.js +5 -5
- 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 +4 -2
- package/dist/tools/prefabeditor/ExportHelper.d.ts +7 -0
- package/dist/tools/prefabeditor/ExportHelper.js +55 -0
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +51 -7
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
- package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +7 -2
- package/dist/tools/prefabeditor/PrefabRoot.js +78 -49
- package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
- package/dist/tools/prefabeditor/components/Input.js +9 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +8 -7
- 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/index.ts +12 -12
- package/src/tools/prefabeditor/EditorContext.tsx +20 -0
- package/src/tools/prefabeditor/EditorTree.tsx +83 -22
- package/src/tools/prefabeditor/EditorUI.tsx +2 -10
- package/src/tools/prefabeditor/InstanceProvider.tsx +60 -4
- package/src/tools/prefabeditor/PrefabEditor.tsx +80 -51
- package/src/tools/prefabeditor/PrefabRoot.tsx +181 -69
- package/src/tools/prefabeditor/components/Input.tsx +11 -3
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +20 -7
- package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
- package/src/tools/prefabeditor/utils.ts +43 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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';
|
|
10
13
|
export * from './helpers';
|
|
11
|
-
export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
12
|
// Helpers
|
|
13
13
|
export * from './helpers';
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
|
@@ -19,6 +19,7 @@ export declare function GameInstanceProvider({ children, models, onSelect, regis
|
|
|
19
19
|
selectedId?: string | null;
|
|
20
20
|
editMode?: boolean;
|
|
21
21
|
}): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function useInstanceCheck(id: string): boolean;
|
|
22
23
|
export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
23
24
|
id: string;
|
|
24
25
|
modelUrl: string;
|
|
@@ -50,6 +50,9 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
50
50
|
return prev.filter(i => i.id !== id);
|
|
51
51
|
});
|
|
52
52
|
}, []);
|
|
53
|
+
const hasInstance = useCallback((id) => {
|
|
54
|
+
return instances.some(i => i.id === id);
|
|
55
|
+
}, [instances]);
|
|
53
56
|
// Flatten all model meshes once (models → flat mesh parts)
|
|
54
57
|
// Note: Geometry is cloned with baked transforms for instancing
|
|
55
58
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
@@ -97,7 +100,8 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
97
100
|
removeInstance,
|
|
98
101
|
instances,
|
|
99
102
|
meshes: flatMeshes,
|
|
100
|
-
modelParts
|
|
103
|
+
modelParts,
|
|
104
|
+
hasInstance
|
|
101
105
|
}, children: [children, Object.entries(grouped).map(([key, group]) => {
|
|
102
106
|
if (group.physicsType === 'none')
|
|
103
107
|
return null;
|
|
@@ -105,7 +109,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
105
109
|
const partCount = modelParts[modelKey] || 0;
|
|
106
110
|
if (partCount === 0)
|
|
107
111
|
return null;
|
|
108
|
-
return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes }, key));
|
|
112
|
+
return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes, onSelect: onSelect, editMode: editMode }, key));
|
|
109
113
|
}), Object.entries(grouped).map(([key, group]) => {
|
|
110
114
|
if (group.physicsType !== 'none')
|
|
111
115
|
return null;
|
|
@@ -123,8 +127,9 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
|
|
|
123
127
|
})] }));
|
|
124
128
|
}
|
|
125
129
|
// Render physics-enabled instances using InstancedRigidBodies
|
|
126
|
-
function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
130
|
+
function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect, editMode }) {
|
|
127
131
|
const meshRefs = useRef([]);
|
|
132
|
+
const rigidBodiesRef = useRef(null);
|
|
128
133
|
const instances = useMemo(() => group.instances.map(inst => ({
|
|
129
134
|
key: inst.id,
|
|
130
135
|
position: inst.position,
|
|
@@ -151,14 +156,47 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
|
151
156
|
});
|
|
152
157
|
mesh.instanceMatrix.needsUpdate = true;
|
|
153
158
|
});
|
|
159
|
+
// Update rigid body positions when instances change
|
|
160
|
+
if (rigidBodiesRef.current) {
|
|
161
|
+
try {
|
|
162
|
+
group.instances.forEach((inst, i) => {
|
|
163
|
+
var _a;
|
|
164
|
+
const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a.at(i);
|
|
165
|
+
if (body && body.setTranslation && body.setRotation) {
|
|
166
|
+
pos.set(...inst.position);
|
|
167
|
+
euler.set(...inst.rotation);
|
|
168
|
+
quat.setFromEuler(euler);
|
|
169
|
+
body.setTranslation(pos, false);
|
|
170
|
+
body.setRotation(quat, false);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// Ignore errors when switching between instanced/non-instanced states
|
|
176
|
+
console.warn('Failed to update rigidbody positions:', error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
154
179
|
}, [group.instances]);
|
|
155
180
|
const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
|
|
156
|
-
|
|
181
|
+
// Handle click on instanced mesh in edit mode
|
|
182
|
+
const handleClick = (e) => {
|
|
183
|
+
if (!editMode || !onSelect)
|
|
184
|
+
return;
|
|
185
|
+
e.stopPropagation();
|
|
186
|
+
// Get the instance index from the intersection
|
|
187
|
+
const instanceId = e.instanceId;
|
|
188
|
+
if (instanceId !== undefined && group.instances[instanceId]) {
|
|
189
|
+
onSelect(group.instances[instanceId].id);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
// Add key to force remount when instance count changes significantly (helps with cleanup)
|
|
193
|
+
const rigidBodyKey = `rb_${modelKey}_${group.physicsType}_${group.instances.length}`;
|
|
194
|
+
return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
|
|
157
195
|
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
158
196
|
if (!mesh)
|
|
159
197
|
return null;
|
|
160
|
-
return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
|
|
161
|
-
}) }));
|
|
198
|
+
return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false, onClick: editMode ? handleClick : undefined }, i));
|
|
199
|
+
}) }, rigidBodyKey));
|
|
162
200
|
}
|
|
163
201
|
// Render non-physics instances using Merged (instancing without rigid bodies)
|
|
164
202
|
function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef, selectedId, editMode }) {
|
|
@@ -184,6 +222,12 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef
|
|
|
184
222
|
clickValid.current = false;
|
|
185
223
|
}, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
|
|
186
224
|
}
|
|
225
|
+
// Hook to check if an instance exists
|
|
226
|
+
export function useInstanceCheck(id) {
|
|
227
|
+
var _a;
|
|
228
|
+
const ctx = useContext(GameInstanceContext);
|
|
229
|
+
return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
|
|
230
|
+
}
|
|
187
231
|
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
188
232
|
export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
189
233
|
const ctx = useContext(GameInstanceContext);
|
|
@@ -196,7 +240,7 @@ export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation
|
|
|
196
240
|
rotation,
|
|
197
241
|
scale,
|
|
198
242
|
physics,
|
|
199
|
-
}), [id, modelUrl, position, rotation, scale, physics]);
|
|
243
|
+
}), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), JSON.stringify(physics)]);
|
|
200
244
|
useEffect(() => {
|
|
201
245
|
if (!addInstance || !removeInstance)
|
|
202
246
|
return;
|
|
@@ -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;
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
3
|
import GameCanvas from "../../shared/GameCanvas";
|
|
13
|
-
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
|
|
14
5
|
import PrefabRoot from "./PrefabRoot";
|
|
15
6
|
import { Physics } from "@react-three/rapier";
|
|
16
7
|
import EditorUI from "./EditorUI";
|
|
17
8
|
import { base, toolbar } from "./styles";
|
|
9
|
+
import { EditorContext } from "./EditorContext";
|
|
10
|
+
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
|
18
11
|
const DEFAULT_PREFAB = {
|
|
19
12
|
id: "prefab-default",
|
|
20
13
|
name: "New Prefab",
|
|
@@ -28,15 +21,18 @@ const DEFAULT_PREFAB = {
|
|
|
28
21
|
}
|
|
29
22
|
}
|
|
30
23
|
};
|
|
31
|
-
const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) => {
|
|
24
|
+
const PrefabEditor = forwardRef(({ basePath, initialPrefab, onPrefabChange, children }, ref) => {
|
|
32
25
|
const [editMode, setEditMode] = useState(true);
|
|
33
26
|
const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
|
|
34
27
|
const [selectedId, setSelectedId] = useState(null);
|
|
35
28
|
const [transformMode, setTransformMode] = useState("translate");
|
|
29
|
+
const [snapResolution, setSnapResolution] = useState(0);
|
|
36
30
|
const [history, setHistory] = useState([loadedPrefab]);
|
|
37
31
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
38
32
|
const throttleRef = useRef(null);
|
|
39
33
|
const lastDataRef = useRef(JSON.stringify(loadedPrefab));
|
|
34
|
+
const prefabRootRef = useRef(null);
|
|
35
|
+
const canvasRef = useRef(null);
|
|
40
36
|
useEffect(() => {
|
|
41
37
|
if (initialPrefab)
|
|
42
38
|
setLoadedPrefab(initialPrefab);
|
|
@@ -87,48 +83,59 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
|
|
|
87
83
|
return () => { if (throttleRef.current)
|
|
88
84
|
clearTimeout(throttleRef.current); };
|
|
89
85
|
}, [loadedPrefab]);
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
a.click();
|
|
107
|
-
};
|
|
108
|
-
const loadJson = () => new Promise(resolve => {
|
|
109
|
-
const input = document.createElement('input');
|
|
110
|
-
input.type = 'file';
|
|
111
|
-
input.accept = '.json,application/json';
|
|
112
|
-
input.onchange = e => {
|
|
86
|
+
const handleScreenshot = () => {
|
|
87
|
+
const canvas = canvasRef.current;
|
|
88
|
+
if (!canvas)
|
|
89
|
+
return;
|
|
90
|
+
canvas.toBlob((blob) => {
|
|
91
|
+
if (!blob)
|
|
92
|
+
return;
|
|
93
|
+
const url = URL.createObjectURL(blob);
|
|
94
|
+
const a = document.createElement('a');
|
|
95
|
+
a.href = url;
|
|
96
|
+
a.download = `${loadedPrefab.name || 'screenshot'}.png`;
|
|
97
|
+
a.click();
|
|
98
|
+
URL.revokeObjectURL(url);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const handleExportGLB = () => {
|
|
113
102
|
var _a;
|
|
114
|
-
const
|
|
115
|
-
if (!
|
|
116
|
-
return
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
130
|
-
reader.readAsText(file);
|
|
103
|
+
const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
|
|
104
|
+
if (!sceneRoot)
|
|
105
|
+
return;
|
|
106
|
+
const exporter = new GLTFExporter();
|
|
107
|
+
exporter.parse(sceneRoot, (result) => {
|
|
108
|
+
const blob = new Blob([result], { type: 'application/octet-stream' });
|
|
109
|
+
const url = URL.createObjectURL(blob);
|
|
110
|
+
const a = document.createElement('a');
|
|
111
|
+
a.href = url;
|
|
112
|
+
a.download = `${loadedPrefab.name || 'scene'}.glb`;
|
|
113
|
+
a.click();
|
|
114
|
+
URL.revokeObjectURL(url);
|
|
115
|
+
}, (error) => {
|
|
116
|
+
console.error('Error exporting GLB:', error);
|
|
117
|
+
}, { binary: true });
|
|
131
118
|
};
|
|
132
|
-
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const canvas = document.querySelector('canvas');
|
|
121
|
+
if (canvas)
|
|
122
|
+
canvasRef.current = canvas;
|
|
123
|
+
}, []);
|
|
124
|
+
useImperativeHandle(ref, () => ({
|
|
125
|
+
screenshot: handleScreenshot,
|
|
126
|
+
exportGLB: handleExportGLB,
|
|
127
|
+
prefab: loadedPrefab,
|
|
128
|
+
setPrefab: setLoadedPrefab,
|
|
129
|
+
rootRef: prefabRootRef
|
|
130
|
+
}), [loadedPrefab]);
|
|
131
|
+
return _jsxs(EditorContext.Provider, { value: {
|
|
132
|
+
transformMode,
|
|
133
|
+
setTransformMode,
|
|
134
|
+
snapResolution,
|
|
135
|
+
setSnapResolution,
|
|
136
|
+
onScreenshot: handleScreenshot,
|
|
137
|
+
onExportGLB: handleExportGLB
|
|
138
|
+
}, children: [_jsx(GameCanvas, { children: _jsxs(Physics, { debug: editMode, paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, basePath: basePath }), children] }) }), _jsx("div", { style: toolbar.panel, children: _jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }) }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
|
|
133
139
|
});
|
|
140
|
+
PrefabEditor.displayName = "PrefabEditor";
|
|
134
141
|
export default PrefabEditor;
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { Group, Matrix4, Object3D, Texture } from "three";
|
|
2
|
+
import { ThreeEvent } from "@react-three/fiber";
|
|
2
3
|
import { Prefab, GameObject as GameObjectType } from "./types";
|
|
4
|
+
export interface PrefabRootRef {
|
|
5
|
+
root: Group | null;
|
|
6
|
+
}
|
|
3
7
|
export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
|
|
4
8
|
editMode?: boolean;
|
|
5
9
|
data: Prefab;
|
|
6
10
|
onPrefabChange?: (data: Prefab) => void;
|
|
7
11
|
selectedId?: string | null;
|
|
8
12
|
onSelect?: (id: string | null) => void;
|
|
9
|
-
|
|
13
|
+
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
10
14
|
basePath?: string;
|
|
11
|
-
} & import("react").RefAttributes<
|
|
15
|
+
} & import("react").RefAttributes<PrefabRootRef>>;
|
|
12
16
|
export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
17
|
interface RendererProps {
|
|
14
18
|
gameObject: GameObjectType;
|
|
15
19
|
selectedId?: string | null;
|
|
16
20
|
onSelect?: (id: string) => void;
|
|
21
|
+
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
17
22
|
registerRef: (id: string, obj: Object3D | null) => void;
|
|
18
23
|
loadedModels: Record<string, Object3D>;
|
|
19
24
|
loadedTextures: Record<string, Texture>;
|