react-three-game 0.0.61 → 0.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -1
- package/dist/index.d.ts +1 -1
- package/dist/shared/GameCanvas.d.ts +2 -1
- package/dist/shared/GameCanvas.js +8 -3
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +20 -4
- package/dist/tools/prefabeditor/PrefabEditor.js +93 -37
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -2
- package/dist/tools/prefabeditor/PrefabRoot.js +9 -16
- package/dist/tools/prefabeditor/utils.d.ts +2 -0
- package/dist/tools/prefabeditor/utils.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
JSON-first 3D game engine. React Three Fiber + WebGPU + Rapier Physics.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm i react-three-game @react-three/fiber @react-three/rapier three
|
|
6
|
+
npm i react-three-game @react-three/drei @react-three/fiber @react-three/rapier three
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|

|
|
@@ -102,8 +102,10 @@ interface GameObject {
|
|
|
102
102
|
## Custom Components
|
|
103
103
|
|
|
104
104
|
```tsx
|
|
105
|
+
import { useRef } from 'react';
|
|
105
106
|
import { Component, registerComponent, FieldRenderer, FieldDefinition } from 'react-three-game';
|
|
106
107
|
import { useFrame } from '@react-three/fiber';
|
|
108
|
+
import type { Group } from 'three';
|
|
107
109
|
|
|
108
110
|
const rotatorFields: FieldDefinition[] = [
|
|
109
111
|
{ name: 'speed', type: 'number', label: 'Speed', step: 0.1 },
|
|
@@ -161,17 +163,75 @@ The `FieldRenderer` component auto-generates editor UI from a field schema:
|
|
|
161
163
|
## Prefab Editor
|
|
162
164
|
|
|
163
165
|
```jsx
|
|
166
|
+
import { useRef } from 'react';
|
|
164
167
|
import { PrefabEditor } from 'react-three-game';
|
|
165
168
|
|
|
166
169
|
// Standalone editor
|
|
167
170
|
<PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
|
|
168
171
|
|
|
172
|
+
// Canvas-only editing mode (keeps canvas selection/gizmos, hides hierarchy + inspector + toolbar)
|
|
173
|
+
<PrefabEditor initialPrefab={sceneData} showUI={false} />
|
|
174
|
+
|
|
169
175
|
// With custom R3F components
|
|
170
176
|
<PrefabEditor initialPrefab={sceneData}>
|
|
171
177
|
<CustomComponent />
|
|
172
178
|
</PrefabEditor>
|
|
173
179
|
```
|
|
174
180
|
|
|
181
|
+
### Embedded / Headless Editor
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { useRef } from 'react';
|
|
185
|
+
import type { Object3D } from 'three';
|
|
186
|
+
import { PrefabEditor, type PrefabEditorRef } from 'react-three-game';
|
|
187
|
+
|
|
188
|
+
export function EmbeddedEditor({ prefab, onPrefabChange }: {
|
|
189
|
+
prefab: any;
|
|
190
|
+
onPrefabChange: (nextPrefab: any) => void;
|
|
191
|
+
}) {
|
|
192
|
+
const editorRef = useRef<PrefabEditorRef>(null);
|
|
193
|
+
|
|
194
|
+
function loadScene(nextPrefab: any) {
|
|
195
|
+
editorRef.current?.replacePrefab(nextPrefab);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function importRuntimeModel(model: Object3D) {
|
|
199
|
+
editorRef.current?.addModel('models/runtime/chair.glb', model, {
|
|
200
|
+
name: 'Chair',
|
|
201
|
+
parentId: 'root',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div style={{ position: 'relative', height: 600 }}>
|
|
207
|
+
<div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
|
|
208
|
+
<button onClick={() => loadScene(prefab)}>Reload Scene</button>
|
|
209
|
+
<button onClick={() => editorRef.current?.exportGLBData()}>Export GLB Data</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<PrefabEditor
|
|
213
|
+
ref={editorRef}
|
|
214
|
+
initialPrefab={prefab}
|
|
215
|
+
onPrefabChange={onPrefabChange}
|
|
216
|
+
showUI={false}
|
|
217
|
+
physics={false}
|
|
218
|
+
enableWindowDrop={false}
|
|
219
|
+
canvasProps={{ style: { height: '100%', width: '100%' } }}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`showUI={false}` hides the built-in editor chrome but keeps canvas selection, transform controls, and scene interaction. For embedded tools, use the editor ref instead of reaching through `rootRef`:
|
|
227
|
+
|
|
228
|
+
- `replacePrefab(prefab)` replaces the current scene through the editor state pipeline and resets editor history/selection.
|
|
229
|
+
- `addModel(path, model, options?)` creates a model node and injects the runtime asset in one step.
|
|
230
|
+
- `addTexture(path, texture, options?)` creates a textured plane node and injects the runtime texture in one step.
|
|
231
|
+
- `exportGLBData()` returns the GLB `ArrayBuffer` without triggering a download.
|
|
232
|
+
- `canvasProps` forwards canvas-level sizing, camera, event, and style props to `GameCanvas`.
|
|
233
|
+
- `setPrefab(prefab)` remains as a backward-compatible alias for `replacePrefab(prefab)`.
|
|
234
|
+
|
|
175
235
|
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
|
|
176
236
|
|
|
177
237
|
Editor menu structure:
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { registerComponent } from './tools/prefabeditor/components/ComponentRegi
|
|
|
7
7
|
export { FieldRenderer, FieldGroup, Input, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
|
|
8
8
|
export * from './tools/prefabeditor/utils';
|
|
9
9
|
export type { ExportGLBOptions } from './tools/prefabeditor/utils';
|
|
10
|
-
export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
|
|
10
|
+
export type { PrefabEditorAssetOptions, PrefabEditorProps, PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
|
|
11
11
|
export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
|
|
12
12
|
export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
|
|
13
13
|
export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
|
|
@@ -4,6 +4,7 @@ interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
|
|
|
4
4
|
loader?: boolean;
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
glConfig?: WebGPURendererParameters;
|
|
7
|
+
canvasRef?: React.RefObject<HTMLCanvasElement | null>;
|
|
7
8
|
}
|
|
8
|
-
export default function GameCanvas({ loader, children, glConfig, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export default function GameCanvas({ loader, children, glConfig, canvasRef, onCreated, style, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
9
10
|
export {};
|
|
@@ -31,9 +31,9 @@ extend({
|
|
|
31
31
|
SpriteNodeMaterial: SpriteNodeMaterial,
|
|
32
32
|
});
|
|
33
33
|
export default function GameCanvas(_a) {
|
|
34
|
-
var { loader = false, children, glConfig } = _a, props = __rest(_a, ["loader", "children", "glConfig"]);
|
|
34
|
+
var { loader = false, children, glConfig, canvasRef, onCreated, style } = _a, props = __rest(_a, ["loader", "children", "glConfig", "canvasRef", "onCreated", "style"]);
|
|
35
35
|
const [frameloop, setFrameloop] = useState("never");
|
|
36
|
-
return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: { touchAction: 'none', userSelect: 'none' }, shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
|
|
36
|
+
return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: Object.assign({ touchAction: 'none', userSelect: 'none' }, style), shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
|
|
37
37
|
const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
|
|
38
38
|
// @ts-expect-error futuristic
|
|
39
39
|
shadowMap: true, antialias: true }, glConfig));
|
|
@@ -41,5 +41,10 @@ export default function GameCanvas(_a) {
|
|
|
41
41
|
setFrameloop("always");
|
|
42
42
|
});
|
|
43
43
|
return renderer;
|
|
44
|
-
})
|
|
44
|
+
}), onCreated: (state) => {
|
|
45
|
+
if (canvasRef) {
|
|
46
|
+
canvasRef.current = state.gl.domElement;
|
|
47
|
+
}
|
|
48
|
+
onCreated === null || onCreated === void 0 ? void 0 : onCreated(state);
|
|
49
|
+
} }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
|
|
45
50
|
}
|
|
@@ -1,18 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import GameCanvas from "../../shared/GameCanvas";
|
|
2
|
+
import { Object3D, Texture } from "three";
|
|
3
|
+
import { GameObject, Prefab } from "./types";
|
|
2
4
|
import { PrefabRootRef } from "./PrefabRoot";
|
|
5
|
+
import type { ExportGLBOptions } from "./utils";
|
|
6
|
+
export interface PrefabEditorAssetOptions {
|
|
7
|
+
name?: string;
|
|
8
|
+
parentId?: string;
|
|
9
|
+
select?: boolean;
|
|
10
|
+
}
|
|
3
11
|
export interface PrefabEditorRef {
|
|
4
12
|
screenshot: () => void;
|
|
5
|
-
exportGLB: () =>
|
|
13
|
+
exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | object | undefined>;
|
|
14
|
+
exportGLBData: () => Promise<ArrayBuffer | undefined>;
|
|
6
15
|
prefab: Prefab;
|
|
7
16
|
setPrefab: (prefab: Prefab) => void;
|
|
17
|
+
replacePrefab: (prefab: Prefab) => void;
|
|
18
|
+
addModel: (path: string, model: Object3D, options?: PrefabEditorAssetOptions) => GameObject;
|
|
19
|
+
addTexture: (path: string, texture: Texture, options?: PrefabEditorAssetOptions) => GameObject;
|
|
8
20
|
rootRef: React.RefObject<PrefabRootRef | null>;
|
|
9
21
|
}
|
|
10
|
-
|
|
22
|
+
export interface PrefabEditorProps {
|
|
11
23
|
basePath?: string;
|
|
12
24
|
initialPrefab?: Prefab;
|
|
13
25
|
physics?: boolean;
|
|
14
26
|
onPrefabChange?: (prefab: Prefab) => void;
|
|
27
|
+
showUI?: boolean;
|
|
28
|
+
enableWindowDrop?: boolean;
|
|
29
|
+
canvasProps?: Omit<React.ComponentProps<typeof GameCanvas>, 'children' | 'canvasRef'>;
|
|
15
30
|
uiPlugins?: React.ReactNode[] | React.ReactNode;
|
|
16
31
|
children?: React.ReactNode;
|
|
17
|
-
}
|
|
32
|
+
}
|
|
33
|
+
declare const PrefabEditor: import("react").ForwardRefExoticComponent<PrefabEditorProps & import("react").RefAttributes<PrefabEditorRef>>;
|
|
18
34
|
export default PrefabEditor;
|
|
@@ -1,3 +1,12 @@
|
|
|
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, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
11
|
import GameCanvas from "../../shared/GameCanvas";
|
|
3
12
|
import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
|
|
@@ -6,7 +15,7 @@ import { Physics } from "@react-three/rapier";
|
|
|
6
15
|
import EditorUI from "./EditorUI";
|
|
7
16
|
import { base, toolbar } from "./styles";
|
|
8
17
|
import { EditorContext } from "./EditorContext";
|
|
9
|
-
import {
|
|
18
|
+
import { createImageNode, createModelNode, exportGLB as exportSceneGLB, exportGLBData, insertNode } from "./utils";
|
|
10
19
|
import { loadFiles } from "../dragdrop";
|
|
11
20
|
const DEFAULT_PREFAB = {
|
|
12
21
|
id: "prefab-default",
|
|
@@ -21,7 +30,7 @@ const DEFAULT_PREFAB = {
|
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
};
|
|
24
|
-
const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, uiPlugins, children }, ref) => {
|
|
33
|
+
const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
|
|
25
34
|
const [editMode, setEditMode] = useState(true);
|
|
26
35
|
const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
|
|
27
36
|
const [selectedId, setSelectedId] = useState(null);
|
|
@@ -35,20 +44,73 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
35
44
|
const lastDataRef = useRef(JSON.stringify(loadedPrefab));
|
|
36
45
|
const prefabRootRef = useRef(null);
|
|
37
46
|
const canvasRef = useRef(null);
|
|
47
|
+
const onPrefabChangeRef = useRef(onPrefabChange);
|
|
48
|
+
const pendingPrefabChangeRef = useRef(null);
|
|
49
|
+
const [injectedModels, setInjectedModels] = useState({});
|
|
50
|
+
const [injectedTextures, setInjectedTextures] = useState({});
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
onPrefabChangeRef.current = onPrefabChange;
|
|
53
|
+
}, [onPrefabChange]);
|
|
54
|
+
const replacePrefab = (prefab, options) => {
|
|
55
|
+
if (throttleRef.current)
|
|
56
|
+
clearTimeout(throttleRef.current);
|
|
57
|
+
lastDataRef.current = JSON.stringify(prefab);
|
|
58
|
+
pendingPrefabChangeRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? null : prefab;
|
|
59
|
+
setSelectedId(null);
|
|
60
|
+
setInjectedModels({});
|
|
61
|
+
setInjectedTextures({});
|
|
62
|
+
setHistory([prefab]);
|
|
63
|
+
setHistoryIndex(0);
|
|
64
|
+
setLoadedPrefab(prefab);
|
|
65
|
+
};
|
|
38
66
|
useEffect(() => {
|
|
39
67
|
if (initialPrefab)
|
|
40
|
-
|
|
68
|
+
replacePrefab(initialPrefab, { notifyChange: false });
|
|
41
69
|
}, [initialPrefab]);
|
|
42
70
|
const updatePrefab = (newPrefab) => {
|
|
43
|
-
setLoadedPrefab(
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
setLoadedPrefab(prev => {
|
|
72
|
+
const resolved = typeof newPrefab === 'function' ? newPrefab(prev) : newPrefab;
|
|
73
|
+
if (Object.is(resolved, prev)) {
|
|
74
|
+
pendingPrefabChangeRef.current = null;
|
|
75
|
+
return prev;
|
|
76
|
+
}
|
|
77
|
+
pendingPrefabChangeRef.current = resolved;
|
|
78
|
+
return resolved;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
var _a;
|
|
83
|
+
if (pendingPrefabChangeRef.current !== loadedPrefab)
|
|
84
|
+
return;
|
|
85
|
+
(_a = onPrefabChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onPrefabChangeRef, loadedPrefab);
|
|
86
|
+
pendingPrefabChangeRef.current = null;
|
|
87
|
+
}, [loadedPrefab]);
|
|
88
|
+
const insertPrefabNode = (node, options) => {
|
|
89
|
+
updatePrefab(prev => {
|
|
90
|
+
return Object.assign(Object.assign({}, prev), { root: insertNode(prev.root, node, options === null || options === void 0 ? void 0 : options.parentId) });
|
|
91
|
+
});
|
|
92
|
+
if ((options === null || options === void 0 ? void 0 : options.select) !== false) {
|
|
93
|
+
setSelectedId(node.id);
|
|
94
|
+
}
|
|
95
|
+
return node;
|
|
96
|
+
};
|
|
97
|
+
const addModel = (path, model, options) => {
|
|
98
|
+
const node = createModelNode(path, options === null || options === void 0 ? void 0 : options.name);
|
|
99
|
+
insertPrefabNode(node, options);
|
|
100
|
+
setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model })));
|
|
101
|
+
return node;
|
|
102
|
+
};
|
|
103
|
+
const addTexture = (path, texture, options) => {
|
|
104
|
+
const node = createImageNode(path, options === null || options === void 0 ? void 0 : options.name);
|
|
105
|
+
insertPrefabNode(node, options);
|
|
106
|
+
setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture })));
|
|
107
|
+
return node;
|
|
46
108
|
};
|
|
47
109
|
const applyHistory = (index) => {
|
|
48
110
|
setHistoryIndex(index);
|
|
49
111
|
lastDataRef.current = JSON.stringify(history[index]);
|
|
112
|
+
pendingPrefabChangeRef.current = history[index];
|
|
50
113
|
setLoadedPrefab(history[index]);
|
|
51
|
-
onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(history[index]);
|
|
52
114
|
};
|
|
53
115
|
const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
|
|
54
116
|
const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
|
|
@@ -100,26 +162,28 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
100
162
|
URL.revokeObjectURL(url);
|
|
101
163
|
});
|
|
102
164
|
};
|
|
103
|
-
const handleExportGLB = () => {
|
|
165
|
+
const handleExportGLB = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
104
166
|
var _a;
|
|
105
167
|
const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
|
|
106
168
|
if (!sceneRoot)
|
|
107
169
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
return exportSceneGLB(sceneRoot, Object.assign({ filename: `${loadedPrefab.name || 'scene'}.glb` }, options));
|
|
171
|
+
});
|
|
172
|
+
const handleExportGLBData = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
173
|
+
var _a;
|
|
174
|
+
const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
|
|
175
|
+
if (!sceneRoot)
|
|
176
|
+
return;
|
|
177
|
+
return exportGLBData(sceneRoot);
|
|
178
|
+
});
|
|
112
179
|
const handleFocusNode = (nodeId) => {
|
|
113
180
|
var _a;
|
|
114
181
|
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.focusNode(nodeId);
|
|
115
182
|
};
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
const canvas = document.querySelector('canvas');
|
|
118
|
-
if (canvas)
|
|
119
|
-
canvasRef.current = canvas;
|
|
120
|
-
}, []);
|
|
121
183
|
// --- Drag & drop files to add nodes ---
|
|
122
184
|
useEffect(() => {
|
|
185
|
+
if (!enableWindowDrop)
|
|
186
|
+
return;
|
|
123
187
|
function handleDragOver(e) {
|
|
124
188
|
e.preventDefault();
|
|
125
189
|
e.stopPropagation();
|
|
@@ -131,26 +195,14 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
131
195
|
const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
|
|
132
196
|
void loadFiles(files, {
|
|
133
197
|
onModelLoaded: (model, filename) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const baseName = filename.replace(/\.[^.]+$/, '');
|
|
137
|
-
const newNode = createModelNode(modelPath, baseName);
|
|
138
|
-
updatePrefab(prev => {
|
|
139
|
-
var _a;
|
|
140
|
-
return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
|
|
198
|
+
addModel(`models/${filename}`, model, {
|
|
199
|
+
name: filename.replace(/\.[^.]+$/, '')
|
|
141
200
|
});
|
|
142
|
-
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, model);
|
|
143
201
|
},
|
|
144
202
|
onTextureLoaded: (texture, filename) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const baseName = filename.replace(/\.[^.]+$/, '');
|
|
148
|
-
const newNode = createImageNode(texturePath, baseName);
|
|
149
|
-
updatePrefab(prev => {
|
|
150
|
-
var _a;
|
|
151
|
-
return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
|
|
203
|
+
addTexture(`textures/${filename}`, texture, {
|
|
204
|
+
name: filename.replace(/\.[^.]+$/, '')
|
|
152
205
|
});
|
|
153
|
-
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(texturePath, texture);
|
|
154
206
|
},
|
|
155
207
|
onLoadError: error => {
|
|
156
208
|
console.error('Drop asset error:', error);
|
|
@@ -163,15 +215,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
163
215
|
window.removeEventListener('dragover', handleDragOver);
|
|
164
216
|
window.removeEventListener('drop', handleDrop);
|
|
165
217
|
};
|
|
166
|
-
}, [
|
|
218
|
+
}, [enableWindowDrop]);
|
|
167
219
|
useImperativeHandle(ref, () => ({
|
|
168
220
|
screenshot: handleScreenshot,
|
|
169
221
|
exportGLB: handleExportGLB,
|
|
222
|
+
exportGLBData: handleExportGLBData,
|
|
170
223
|
prefab: loadedPrefab,
|
|
171
|
-
setPrefab:
|
|
224
|
+
setPrefab: replacePrefab,
|
|
225
|
+
replacePrefab,
|
|
226
|
+
addModel,
|
|
227
|
+
addTexture,
|
|
172
228
|
rootRef: prefabRootRef
|
|
173
229
|
}), [loadedPrefab]);
|
|
174
|
-
const content = (_jsxs(_Fragment, { 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] }));
|
|
230
|
+
const content = (_jsxs(_Fragment, { 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, injectedModels: injectedModels, injectedTextures: injectedTextures }), children] }));
|
|
175
231
|
return _jsxs(EditorContext.Provider, { value: {
|
|
176
232
|
transformMode,
|
|
177
233
|
setTransformMode,
|
|
@@ -184,7 +240,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
184
240
|
onFocusNode: handleFocusNode,
|
|
185
241
|
onScreenshot: handleScreenshot,
|
|
186
242
|
onExportGLB: handleExportGLB
|
|
187
|
-
}, children: [_jsx(GameCanvas, { camera: { position: [0, 5, 15] }, children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content }), _jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), uiPlugins] }), _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
|
|
243
|
+
}, children: [_jsx(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), uiPlugins] }), _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] }))] });
|
|
188
244
|
});
|
|
189
245
|
PrefabEditor.displayName = "PrefabEditor";
|
|
190
246
|
export default PrefabEditor;
|
|
@@ -4,8 +4,6 @@ import { Prefab, GameObject as GameObjectType } from "./types";
|
|
|
4
4
|
export interface PrefabRootRef {
|
|
5
5
|
root: Group | null;
|
|
6
6
|
rigidBodyRefs: Map<string, any>;
|
|
7
|
-
injectModel: (filename: string, model: Object3D) => void;
|
|
8
|
-
injectTexture: (filename: string, texture: Texture) => void;
|
|
9
7
|
focusNode: (nodeId: string) => void;
|
|
10
8
|
}
|
|
11
9
|
export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
|
|
@@ -16,6 +14,8 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
|
|
|
16
14
|
onSelect?: (id: string | null) => void;
|
|
17
15
|
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
18
16
|
basePath?: string;
|
|
17
|
+
injectedModels?: Record<string, Object3D>;
|
|
18
|
+
injectedTextures?: Record<string, Texture>;
|
|
19
19
|
} & import("react").RefAttributes<PrefabRootRef>>;
|
|
20
20
|
export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
|
|
21
21
|
interface RendererProps {
|
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import { MapControls, TransformControls, useHelper } from "@react-three/drei";
|
|
12
|
-
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
12
|
+
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
13
13
|
import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, Vector3, } from "three";
|
|
14
14
|
import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
|
|
15
15
|
import components from "./components";
|
|
@@ -19,7 +19,7 @@ import { focusCameraOnObject, updateNode } from "./utils";
|
|
|
19
19
|
import { EditorContext } from "./EditorContext";
|
|
20
20
|
components.forEach(registerComponent);
|
|
21
21
|
const IDENTITY = new Matrix4();
|
|
22
|
-
export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, basePath = "" }, ref) => {
|
|
22
|
+
export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, basePath = "", injectedModels = {}, injectedTextures = {} }, ref) => {
|
|
23
23
|
var _a, _b, _c, _d;
|
|
24
24
|
// optional editor context
|
|
25
25
|
const editorContext = useContext(EditorContext);
|
|
@@ -37,18 +37,11 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
37
37
|
const [selectedObject, setSelectedObject] = useState(null);
|
|
38
38
|
const rootRef = useRef(null);
|
|
39
39
|
const controlsRef = useRef(null);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
}, []);
|
|
43
|
-
const injectTexture = useCallback((filename, texture) => {
|
|
44
|
-
loading.current.add(filename);
|
|
45
|
-
setTextures(t => (Object.assign(Object.assign({}, t), { [filename]: texture })));
|
|
46
|
-
}, []);
|
|
40
|
+
const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
|
|
41
|
+
const availableTextures = useMemo(() => (Object.assign(Object.assign({}, textures), injectedTextures)), [textures, injectedTextures]);
|
|
47
42
|
useImperativeHandle(ref, () => ({
|
|
48
43
|
root: rootRef.current,
|
|
49
44
|
rigidBodyRefs: rigidBodyRefs.current,
|
|
50
|
-
injectModel,
|
|
51
|
-
injectTexture,
|
|
52
45
|
focusNode: (nodeId) => {
|
|
53
46
|
const object = objectRefs.current[nodeId];
|
|
54
47
|
const controls = controlsRef.current;
|
|
@@ -57,7 +50,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
57
50
|
return;
|
|
58
51
|
focusCameraOnObject(object, camera, controls.target, () => { var _a; return (_a = controls.update) === null || _a === void 0 ? void 0 : _a.call(controls); });
|
|
59
52
|
}
|
|
60
|
-
}), [
|
|
53
|
+
}), []);
|
|
61
54
|
const registerRef = useCallback((id, obj) => {
|
|
62
55
|
objectRefs.current[id] = obj;
|
|
63
56
|
if (id === selectedId)
|
|
@@ -107,7 +100,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
107
100
|
texturesToLoad.add(node.components.material.properties.normalMapTexture);
|
|
108
101
|
});
|
|
109
102
|
modelsToLoad.forEach((file) => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
-
if (
|
|
103
|
+
if (availableModels[file] || loading.current.has(file))
|
|
111
104
|
return;
|
|
112
105
|
loading.current.add(file);
|
|
113
106
|
const path = file.startsWith("/")
|
|
@@ -121,7 +114,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
121
114
|
}));
|
|
122
115
|
const loader = new TextureLoader();
|
|
123
116
|
texturesToLoad.forEach(file => {
|
|
124
|
-
if (
|
|
117
|
+
if (availableTextures[file] || loading.current.has(file) || failedTextures.current.has(file))
|
|
125
118
|
return;
|
|
126
119
|
loading.current.add(file);
|
|
127
120
|
// Handle full URLs (http/https) or regular paths
|
|
@@ -139,8 +132,8 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
139
132
|
failedTextures.current.add(file);
|
|
140
133
|
});
|
|
141
134
|
});
|
|
142
|
-
}, [data,
|
|
143
|
-
return (_jsxs("group", { ref: rootRef, children: [_jsx(GameInstanceProvider, { models:
|
|
135
|
+
}, [data, availableModels, availableTextures, basePath]);
|
|
136
|
+
return (_jsxs("group", { ref: rootRef, children: [_jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: availableModels, loadedTextures: availableTextures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: snapResolution > 0 ? snapResolution : undefined }, `transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`))] }))] }));
|
|
144
137
|
});
|
|
145
138
|
export function GameObjectRenderer(props) {
|
|
146
139
|
var _a, _b, _c;
|
|
@@ -36,6 +36,8 @@ export declare function findByComponent(root: GameObject, componentType: string)
|
|
|
36
36
|
export declare function flatten(root: GameObject): GameObject[];
|
|
37
37
|
/** Immutably update a node by ID */
|
|
38
38
|
export declare function updateNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject;
|
|
39
|
+
/** Immutably insert a node under a parent ID, defaulting to the root when the parent is missing */
|
|
40
|
+
export declare function insertNode(root: GameObject, node: GameObject, parentId?: string): GameObject;
|
|
39
41
|
/** Immutably delete a node by ID */
|
|
40
42
|
export declare function deleteNode(root: GameObject, id: string): GameObject | null;
|
|
41
43
|
/** Deep clone a node with new IDs */
|
|
@@ -184,6 +184,21 @@ export function updateNode(root, id, update) {
|
|
|
184
184
|
return root;
|
|
185
185
|
return Object.assign(Object.assign({}, root), { children: root.children.map(child => updateNode(child, id, update)) });
|
|
186
186
|
}
|
|
187
|
+
/** Immutably insert a node under a parent ID, defaulting to the root when the parent is missing */
|
|
188
|
+
export function insertNode(root, node, parentId) {
|
|
189
|
+
var _a, _b;
|
|
190
|
+
if (!parentId || parentId === root.id) {
|
|
191
|
+
return Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), node] });
|
|
192
|
+
}
|
|
193
|
+
const nextRoot = updateNode(root, parentId, parent => {
|
|
194
|
+
var _a;
|
|
195
|
+
return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), node] }));
|
|
196
|
+
});
|
|
197
|
+
if (nextRoot === root) {
|
|
198
|
+
return Object.assign(Object.assign({}, root), { children: [...((_b = root.children) !== null && _b !== void 0 ? _b : []), node] });
|
|
199
|
+
}
|
|
200
|
+
return nextRoot;
|
|
201
|
+
}
|
|
187
202
|
/** Immutably delete a node by ID */
|
|
188
203
|
export function deleteNode(root, id) {
|
|
189
204
|
if (root.id === id)
|