react-three-game 0.0.60 → 0.0.62
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 +56 -0
- package/dist/index.d.ts +1 -1
- package/dist/shared/GameCanvas.d.ts +2 -1
- package/dist/shared/GameCanvas.js +7 -2
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +18 -4
- package/dist/tools/prefabeditor/PrefabEditor.js +90 -36
- package/dist/tools/prefabeditor/utils.d.ts +2 -0
- package/dist/tools/prefabeditor/utils.js +15 -0
- package/package.json +9 -3
- package/.gitattributes +0 -2
- package/.github/copilot-instructions.md +0 -83
- package/.github/workflows/nextjs.yml +0 -99
- package/.gitmodules +0 -3
- package/assets/architecture.png +0 -0
- package/assets/editor.gif +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/react-three-game-logo.png +0 -0
- package/dist/tools/dragdrop/page.d.ts +0 -1
- package/dist/tools/dragdrop/page.js +0 -11
- package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
- package/dist/tools/prefabeditor/EntityEvents.js +0 -85
- package/dist/tools/prefabeditor/page.d.ts +0 -1
- package/dist/tools/prefabeditor/page.js +0 -5
- package/react-three-game-skill/.gitattributes +0 -2
- package/react-three-game-skill/README.md +0 -7
- package/react-three-game-skill/react-three-game/SKILL.md +0 -514
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
- package/src/helpers/SoundManager.ts +0 -130
- package/src/helpers/index.ts +0 -91
- package/src/index.ts +0 -59
- package/src/shared/ContactShadow.tsx +0 -74
- package/src/shared/GameCanvas.tsx +0 -52
- package/src/tools/assetviewer/page.tsx +0 -425
- package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -204
- package/src/tools/dragdrop/page.tsx +0 -45
- package/src/tools/prefabeditor/Dropdown.tsx +0 -112
- package/src/tools/prefabeditor/EditorContext.tsx +0 -25
- package/src/tools/prefabeditor/EditorTree.tsx +0 -452
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
- package/src/tools/prefabeditor/EditorUI.tsx +0 -204
- package/src/tools/prefabeditor/EventSystem.tsx +0 -36
- package/src/tools/prefabeditor/GameEvents.ts +0 -191
- package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
- package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
- package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
- package/src/tools/prefabeditor/components/Input.tsx +0 -820
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
- package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
- package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
- package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
- package/src/tools/prefabeditor/components/index.ts +0 -26
- package/src/tools/prefabeditor/page.tsx +0 -10
- package/src/tools/prefabeditor/styles.ts +0 -235
- package/src/tools/prefabeditor/types.ts +0 -20
- package/src/tools/prefabeditor/utils.ts +0 -312
package/README.md
CHANGED
|
@@ -161,17 +161,73 @@ The `FieldRenderer` component auto-generates editor UI from a field schema:
|
|
|
161
161
|
## Prefab Editor
|
|
162
162
|
|
|
163
163
|
```jsx
|
|
164
|
+
import { useRef } from 'react';
|
|
164
165
|
import { PrefabEditor } from 'react-three-game';
|
|
165
166
|
|
|
166
167
|
// Standalone editor
|
|
167
168
|
<PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
|
|
168
169
|
|
|
170
|
+
// Canvas-only editing mode (keeps canvas selection/gizmos, hides hierarchy + inspector + toolbar)
|
|
171
|
+
<PrefabEditor initialPrefab={sceneData} showUI={false} />
|
|
172
|
+
|
|
169
173
|
// With custom R3F components
|
|
170
174
|
<PrefabEditor initialPrefab={sceneData}>
|
|
171
175
|
<CustomComponent />
|
|
172
176
|
</PrefabEditor>
|
|
173
177
|
```
|
|
174
178
|
|
|
179
|
+
### Embedded / Headless Editor
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { useRef } from 'react';
|
|
183
|
+
import type { Object3D } from 'three';
|
|
184
|
+
import { PrefabEditor, type PrefabEditorRef } from 'react-three-game';
|
|
185
|
+
|
|
186
|
+
export function EmbeddedEditor({ prefab, onPrefabChange }: {
|
|
187
|
+
prefab: any;
|
|
188
|
+
onPrefabChange: (nextPrefab: any) => void;
|
|
189
|
+
}) {
|
|
190
|
+
const editorRef = useRef<PrefabEditorRef>(null);
|
|
191
|
+
|
|
192
|
+
function loadScene(nextPrefab: any) {
|
|
193
|
+
editorRef.current?.replacePrefab(nextPrefab);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function importRuntimeModel(model: Object3D) {
|
|
197
|
+
editorRef.current?.addModel('models/runtime/chair.glb', model, {
|
|
198
|
+
name: 'Chair',
|
|
199
|
+
parentId: 'root',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div style={{ position: 'relative', height: 600 }}>
|
|
205
|
+
<div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
|
|
206
|
+
<button onClick={() => loadScene(prefab)}>Reload Scene</button>
|
|
207
|
+
<button onClick={() => editorRef.current?.exportGLBData()}>Export GLB Data</button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<PrefabEditor
|
|
211
|
+
ref={editorRef}
|
|
212
|
+
initialPrefab={prefab}
|
|
213
|
+
onPrefabChange={onPrefabChange}
|
|
214
|
+
showUI={false}
|
|
215
|
+
physics={false}
|
|
216
|
+
enableWindowDrop={false}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`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`:
|
|
224
|
+
|
|
225
|
+
- `replacePrefab(prefab)` replaces the current scene through the editor state pipeline and resets editor history/selection.
|
|
226
|
+
- `addModel(path, model, options?)` creates a model node and injects the runtime asset in one step.
|
|
227
|
+
- `addTexture(path, texture, options?)` creates a textured plane node and injects the runtime texture in one step.
|
|
228
|
+
- `exportGLBData()` returns the GLB `ArrayBuffer` without triggering a download.
|
|
229
|
+
- `setPrefab(prefab)` remains as a backward-compatible alias for `replacePrefab(prefab)`.
|
|
230
|
+
|
|
175
231
|
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
|
|
176
232
|
|
|
177
233
|
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, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
9
10
|
export {};
|
|
@@ -31,7 +31,7 @@ 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 } = _a, props = __rest(_a, ["loader", "children", "glConfig", "canvasRef", "onCreated"]);
|
|
35
35
|
const [frameloop, setFrameloop] = useState("never");
|
|
36
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 }) {
|
|
37
37
|
const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
|
|
@@ -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,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Object3D, Texture } from "three";
|
|
2
|
+
import { GameObject, Prefab } from "./types";
|
|
2
3
|
import { PrefabRootRef } from "./PrefabRoot";
|
|
4
|
+
import type { ExportGLBOptions } from "./utils";
|
|
5
|
+
export interface PrefabEditorAssetOptions {
|
|
6
|
+
name?: string;
|
|
7
|
+
parentId?: string;
|
|
8
|
+
select?: boolean;
|
|
9
|
+
}
|
|
3
10
|
export interface PrefabEditorRef {
|
|
4
11
|
screenshot: () => void;
|
|
5
|
-
exportGLB: () =>
|
|
12
|
+
exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | object | undefined>;
|
|
13
|
+
exportGLBData: () => Promise<ArrayBuffer | undefined>;
|
|
6
14
|
prefab: Prefab;
|
|
7
15
|
setPrefab: (prefab: Prefab) => void;
|
|
16
|
+
replacePrefab: (prefab: Prefab) => void;
|
|
17
|
+
addModel: (path: string, model: Object3D, options?: PrefabEditorAssetOptions) => GameObject;
|
|
18
|
+
addTexture: (path: string, texture: Texture, options?: PrefabEditorAssetOptions) => GameObject;
|
|
8
19
|
rootRef: React.RefObject<PrefabRootRef | null>;
|
|
9
20
|
}
|
|
10
|
-
|
|
21
|
+
export interface PrefabEditorProps {
|
|
11
22
|
basePath?: string;
|
|
12
23
|
initialPrefab?: Prefab;
|
|
13
24
|
physics?: boolean;
|
|
14
25
|
onPrefabChange?: (prefab: Prefab) => void;
|
|
26
|
+
showUI?: boolean;
|
|
27
|
+
enableWindowDrop?: boolean;
|
|
15
28
|
uiPlugins?: React.ReactNode[] | React.ReactNode;
|
|
16
29
|
children?: React.ReactNode;
|
|
17
|
-
}
|
|
30
|
+
}
|
|
31
|
+
declare const PrefabEditor: import("react").ForwardRefExoticComponent<PrefabEditorProps & import("react").RefAttributes<PrefabEditorRef>>;
|
|
18
32
|
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, 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,71 @@ 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
|
+
useEffect(() => {
|
|
50
|
+
onPrefabChangeRef.current = onPrefabChange;
|
|
51
|
+
}, [onPrefabChange]);
|
|
52
|
+
const replacePrefab = (prefab, options) => {
|
|
53
|
+
if (throttleRef.current)
|
|
54
|
+
clearTimeout(throttleRef.current);
|
|
55
|
+
lastDataRef.current = JSON.stringify(prefab);
|
|
56
|
+
pendingPrefabChangeRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? null : prefab;
|
|
57
|
+
setSelectedId(null);
|
|
58
|
+
setHistory([prefab]);
|
|
59
|
+
setHistoryIndex(0);
|
|
60
|
+
setLoadedPrefab(prefab);
|
|
61
|
+
};
|
|
38
62
|
useEffect(() => {
|
|
39
63
|
if (initialPrefab)
|
|
40
|
-
|
|
64
|
+
replacePrefab(initialPrefab, { notifyChange: false });
|
|
41
65
|
}, [initialPrefab]);
|
|
42
66
|
const updatePrefab = (newPrefab) => {
|
|
43
|
-
setLoadedPrefab(
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
setLoadedPrefab(prev => {
|
|
68
|
+
const resolved = typeof newPrefab === 'function' ? newPrefab(prev) : newPrefab;
|
|
69
|
+
if (Object.is(resolved, prev)) {
|
|
70
|
+
pendingPrefabChangeRef.current = null;
|
|
71
|
+
return prev;
|
|
72
|
+
}
|
|
73
|
+
pendingPrefabChangeRef.current = resolved;
|
|
74
|
+
return resolved;
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
var _a;
|
|
79
|
+
if (pendingPrefabChangeRef.current !== loadedPrefab)
|
|
80
|
+
return;
|
|
81
|
+
(_a = onPrefabChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onPrefabChangeRef, loadedPrefab);
|
|
82
|
+
pendingPrefabChangeRef.current = null;
|
|
83
|
+
}, [loadedPrefab]);
|
|
84
|
+
const insertPrefabNode = (node, options) => {
|
|
85
|
+
updatePrefab(prev => {
|
|
86
|
+
return Object.assign(Object.assign({}, prev), { root: insertNode(prev.root, node, options === null || options === void 0 ? void 0 : options.parentId) });
|
|
87
|
+
});
|
|
88
|
+
if ((options === null || options === void 0 ? void 0 : options.select) !== false) {
|
|
89
|
+
setSelectedId(node.id);
|
|
90
|
+
}
|
|
91
|
+
return node;
|
|
92
|
+
};
|
|
93
|
+
const addModel = (path, model, options) => {
|
|
94
|
+
var _a;
|
|
95
|
+
const node = createModelNode(path, options === null || options === void 0 ? void 0 : options.name);
|
|
96
|
+
insertPrefabNode(node, options);
|
|
97
|
+
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(path, model);
|
|
98
|
+
return node;
|
|
99
|
+
};
|
|
100
|
+
const addTexture = (path, texture, options) => {
|
|
101
|
+
var _a;
|
|
102
|
+
const node = createImageNode(path, options === null || options === void 0 ? void 0 : options.name);
|
|
103
|
+
insertPrefabNode(node, options);
|
|
104
|
+
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(path, texture);
|
|
105
|
+
return node;
|
|
46
106
|
};
|
|
47
107
|
const applyHistory = (index) => {
|
|
48
108
|
setHistoryIndex(index);
|
|
49
109
|
lastDataRef.current = JSON.stringify(history[index]);
|
|
110
|
+
pendingPrefabChangeRef.current = history[index];
|
|
50
111
|
setLoadedPrefab(history[index]);
|
|
51
|
-
onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(history[index]);
|
|
52
112
|
};
|
|
53
113
|
const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
|
|
54
114
|
const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
|
|
@@ -100,26 +160,28 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
100
160
|
URL.revokeObjectURL(url);
|
|
101
161
|
});
|
|
102
162
|
};
|
|
103
|
-
const handleExportGLB = () => {
|
|
163
|
+
const handleExportGLB = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
104
164
|
var _a;
|
|
105
165
|
const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
|
|
106
166
|
if (!sceneRoot)
|
|
107
167
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
168
|
+
return exportSceneGLB(sceneRoot, Object.assign({ filename: `${loadedPrefab.name || 'scene'}.glb` }, options));
|
|
169
|
+
});
|
|
170
|
+
const handleExportGLBData = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
var _a;
|
|
172
|
+
const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
|
|
173
|
+
if (!sceneRoot)
|
|
174
|
+
return;
|
|
175
|
+
return exportGLBData(sceneRoot);
|
|
176
|
+
});
|
|
112
177
|
const handleFocusNode = (nodeId) => {
|
|
113
178
|
var _a;
|
|
114
179
|
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.focusNode(nodeId);
|
|
115
180
|
};
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
const canvas = document.querySelector('canvas');
|
|
118
|
-
if (canvas)
|
|
119
|
-
canvasRef.current = canvas;
|
|
120
|
-
}, []);
|
|
121
181
|
// --- Drag & drop files to add nodes ---
|
|
122
182
|
useEffect(() => {
|
|
183
|
+
if (!enableWindowDrop)
|
|
184
|
+
return;
|
|
123
185
|
function handleDragOver(e) {
|
|
124
186
|
e.preventDefault();
|
|
125
187
|
e.stopPropagation();
|
|
@@ -131,26 +193,14 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
131
193
|
const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
|
|
132
194
|
void loadFiles(files, {
|
|
133
195
|
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] }) }));
|
|
196
|
+
addModel(`models/${filename}`, model, {
|
|
197
|
+
name: filename.replace(/\.[^.]+$/, '')
|
|
141
198
|
});
|
|
142
|
-
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, model);
|
|
143
199
|
},
|
|
144
200
|
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] }) }));
|
|
201
|
+
addTexture(`textures/${filename}`, texture, {
|
|
202
|
+
name: filename.replace(/\.[^.]+$/, '')
|
|
152
203
|
});
|
|
153
|
-
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(texturePath, texture);
|
|
154
204
|
},
|
|
155
205
|
onLoadError: error => {
|
|
156
206
|
console.error('Drop asset error:', error);
|
|
@@ -163,12 +213,16 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
163
213
|
window.removeEventListener('dragover', handleDragOver);
|
|
164
214
|
window.removeEventListener('drop', handleDrop);
|
|
165
215
|
};
|
|
166
|
-
}, [
|
|
216
|
+
}, [enableWindowDrop]);
|
|
167
217
|
useImperativeHandle(ref, () => ({
|
|
168
218
|
screenshot: handleScreenshot,
|
|
169
219
|
exportGLB: handleExportGLB,
|
|
220
|
+
exportGLBData: handleExportGLBData,
|
|
170
221
|
prefab: loadedPrefab,
|
|
171
|
-
setPrefab:
|
|
222
|
+
setPrefab: replacePrefab,
|
|
223
|
+
replacePrefab,
|
|
224
|
+
addModel,
|
|
225
|
+
addTexture,
|
|
172
226
|
rootRef: prefabRootRef
|
|
173
227
|
}), [loadedPrefab]);
|
|
174
228
|
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] }));
|
|
@@ -184,7 +238,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
184
238
|
onFocusNode: handleFocusNode,
|
|
185
239
|
onScreenshot: handleScreenshot,
|
|
186
240
|
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 })] });
|
|
241
|
+
}, children: [_jsx(GameCanvas, { camera: { position: [0, 5, 15] }, canvasRef: canvasRef, 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
242
|
});
|
|
189
243
|
PrefabEditor.displayName = "PrefabEditor";
|
|
190
244
|
export default PrefabEditor;
|
|
@@ -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)
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-three-game",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.62",
|
|
4
4
|
"description": "high performance 3D game engine for React",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
8
13
|
"scripts": {
|
|
9
|
-
"
|
|
14
|
+
"clean": "rm -rf dist",
|
|
15
|
+
"watch": "npm run clean && tsc --watch",
|
|
10
16
|
"dev": "concurrently \"npm run watch\" \"cd docs && npm run dev\"",
|
|
11
|
-
"build": "tsc",
|
|
17
|
+
"build": "npm run clean && tsc",
|
|
12
18
|
"release": "npm run build && npm publish --access public"
|
|
13
19
|
},
|
|
14
20
|
"keywords": [],
|
package/.gitattributes
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# react-three-game - AI Coding Agent Instructions
|
|
2
|
-
|
|
3
|
-
## Project Overview
|
|
4
|
-
AI-native 3D game engine where **scenes are JSON prefabs**. Unity-like GameObject+Component architecture built on React Three Fiber + WebGPU.
|
|
5
|
-
|
|
6
|
-
## Monorepo Structure
|
|
7
|
-
- **`/src`** → Library source, builds to `/dist`, published as `react-three-game`
|
|
8
|
-
- **`/docs`** → Next.js 16 site, imports library via `"react-three-game": "file:.."`
|
|
9
|
-
- **`npm run dev`** → Runs `tsc --watch` + Next.js concurrently (hot reload works)
|
|
10
|
-
|
|
11
|
-
## Prefab JSON Schema
|
|
12
|
-
```typescript
|
|
13
|
-
// See docs/app/samples/*.json for examples
|
|
14
|
-
interface Prefab { id?: string; name?: string; root: GameObject; }
|
|
15
|
-
interface GameObject {
|
|
16
|
-
id: string; // Use crypto.randomUUID() for new nodes
|
|
17
|
-
disabled?: boolean;
|
|
18
|
-
components?: Record<string, { type: string; properties: any }>;
|
|
19
|
-
children?: GameObject[];
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
**Transforms are LOCAL** (parent-relative). Rotations in radians. Colors as CSS strings.
|
|
23
|
-
|
|
24
|
-
## Component System (`src/tools/prefabeditor/components/`)
|
|
25
|
-
Every component has `Editor` (inspector UI) + optional `View` (Three.js render):
|
|
26
|
-
```typescript
|
|
27
|
-
const MyComponent: Component = {
|
|
28
|
-
name: 'MyComponent', // TitleCase for registry, lowercase key in JSON
|
|
29
|
-
Editor: ({ component, onUpdate }) => <input onChange={e => onUpdate({ value: e.target.value })} />,
|
|
30
|
-
View: ({ properties, children }) => <group>{children}</group>, // Wrapper components accept children
|
|
31
|
-
defaultProperties: { value: 0 }
|
|
32
|
-
};
|
|
33
|
-
```
|
|
34
|
-
**To add a component:** Create file → export from `components/index.ts` → auto-registered in `PrefabRoot.tsx`.
|
|
35
|
-
|
|
36
|
-
## Key Files
|
|
37
|
-
| File | Purpose |
|
|
38
|
-
|------|---------|
|
|
39
|
-
| `src/index.ts` | All public exports - add new features here |
|
|
40
|
-
| `src/tools/prefabeditor/PrefabRoot.tsx` | Pure renderer - renders prefab as Three.js objects for R3F integration |
|
|
41
|
-
| `src/tools/prefabeditor/PrefabEditor.tsx` | Managed scene with editor UI and play/pause controls for physics |
|
|
42
|
-
| `src/tools/prefabeditor/utils.ts` | Tree helpers: `findNode`, `updateNode`, `deleteNode`, `cloneNode` |
|
|
43
|
-
| `src/shared/GameCanvas.tsx` | WebGPU renderer setup (use `MeshStandardNodeMaterial`) |
|
|
44
|
-
|
|
45
|
-
## Usage Modes
|
|
46
|
-
|
|
47
|
-
**PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Render it inside a regular `@react-three/fiber` `Canvas`. `GameCanvas` provides the WebGPU canvas setup. Add a `Physics` wrapper to enable physics. Use this to integrate prefabs into larger R3F scenes.
|
|
48
|
-
|
|
49
|
-
**PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children.
|
|
50
|
-
|
|
51
|
-
## Critical Patterns
|
|
52
|
-
|
|
53
|
-
### Tree Manipulation (Immutable)
|
|
54
|
-
```typescript
|
|
55
|
-
import { updateNode, findNode, deleteNode } from 'react-three-game';
|
|
56
|
-
const newRoot = updateNode(root, nodeId, node => ({ ...node, components: { ...node.components, physics: {...} } }));
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### WebGPU Materials
|
|
60
|
-
Use node materials only: `MeshStandardNodeMaterial`, `MeshBasicNodeMaterial` (not `MeshStandardMaterial`).
|
|
61
|
-
|
|
62
|
-
### Physics Wrapping
|
|
63
|
-
`PhysicsComponent.View` wraps children in `<RigidBody>` only when `editMode=false`. Edit mode pauses physics.
|
|
64
|
-
|
|
65
|
-
### Model Instancing
|
|
66
|
-
Set `model.properties.instanced = true` → uses `InstanceProvider.tsx` for batched rendering with physics.
|
|
67
|
-
|
|
68
|
-
## Built-in Components
|
|
69
|
-
`Transform`, `Geometry` (box/sphere/plane/cylinder), `Material` (color/texture), `Physics` (dynamic/fixed), `Model` (GLB/FBX), `SpotLight`, `DirectionalLight`, `AmbientLight`, `Text`
|
|
70
|
-
|
|
71
|
-
## Custom Components (User-space)
|
|
72
|
-
See `docs/app/demo/editor/RotatorComponent.tsx` for runtime behavior example using `useFrame`. Register with `registerComponent()` before rendering `<PrefabEditor>`.
|
|
73
|
-
|
|
74
|
-
## Development Workflow
|
|
75
|
-
1. **Edit library**: Modify `/src`, auto-rebuilds via `tsc --watch`
|
|
76
|
-
2. **Test in docs**: Changes reflect at `http://localhost:3000`
|
|
77
|
-
3. **Add sample prefabs**: `docs/app/samples/*.json`
|
|
78
|
-
4. **Release**: `npm run release` (builds + publishes)
|
|
79
|
-
|
|
80
|
-
## Conventions
|
|
81
|
-
- Component keys: lowercase in JSON (`"transform"`), TitleCase in registry (`"Transform"`)
|
|
82
|
-
- Asset paths: Relative to `/public` (e.g., `models/cars/taxi/model.glb`)
|
|
83
|
-
- All Three.js renders must be inside `<GameCanvas>` (WebGPU init required)
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# Sample workflow for building and deploying a Next.js site to GitHub Pages
|
|
2
|
-
#
|
|
3
|
-
# To get started with Next.js see: https://nextjs.org/docs/getting-started
|
|
4
|
-
#
|
|
5
|
-
name: Deploy Next.js site to Pages
|
|
6
|
-
|
|
7
|
-
on:
|
|
8
|
-
# Runs on pushes targeting the default branch
|
|
9
|
-
push:
|
|
10
|
-
branches: ["main"]
|
|
11
|
-
|
|
12
|
-
# Allows you to run this workflow manually from the Actions tab
|
|
13
|
-
workflow_dispatch:
|
|
14
|
-
|
|
15
|
-
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
16
|
-
permissions:
|
|
17
|
-
contents: read
|
|
18
|
-
pages: write
|
|
19
|
-
id-token: write
|
|
20
|
-
|
|
21
|
-
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
|
22
|
-
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
|
23
|
-
concurrency:
|
|
24
|
-
group: "pages"
|
|
25
|
-
cancel-in-progress: false
|
|
26
|
-
|
|
27
|
-
jobs:
|
|
28
|
-
# Build job
|
|
29
|
-
build:
|
|
30
|
-
runs-on: ubuntu-latest
|
|
31
|
-
steps:
|
|
32
|
-
- name: Checkout
|
|
33
|
-
uses: actions/checkout@v4
|
|
34
|
-
- name: Detect package manager
|
|
35
|
-
id: detect-package-manager
|
|
36
|
-
run: |
|
|
37
|
-
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
|
|
38
|
-
echo "manager=yarn" >> $GITHUB_OUTPUT
|
|
39
|
-
echo "command=install" >> $GITHUB_OUTPUT
|
|
40
|
-
echo "runner=yarn" >> $GITHUB_OUTPUT
|
|
41
|
-
exit 0
|
|
42
|
-
elif [ -f "${{ github.workspace }}/package.json" ]; then
|
|
43
|
-
echo "manager=npm" >> $GITHUB_OUTPUT
|
|
44
|
-
echo "command=install" >> $GITHUB_OUTPUT
|
|
45
|
-
echo "runner=npx --no-install" >> $GITHUB_OUTPUT
|
|
46
|
-
exit 0
|
|
47
|
-
else
|
|
48
|
-
echo "Unable to determine package manager"
|
|
49
|
-
exit 1
|
|
50
|
-
fi
|
|
51
|
-
- name: Setup Node
|
|
52
|
-
uses: actions/setup-node@v4
|
|
53
|
-
with:
|
|
54
|
-
node-version: "20"
|
|
55
|
-
cache: ${{ steps.detect-package-manager.outputs.manager }}
|
|
56
|
-
- name: Setup Pages
|
|
57
|
-
uses: actions/configure-pages@v5
|
|
58
|
-
with:
|
|
59
|
-
# Automatically inject basePath in your Next.js configuration file and disable
|
|
60
|
-
# server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
|
|
61
|
-
#
|
|
62
|
-
# You may remove this line if you want to manage the configuration yourself.
|
|
63
|
-
static_site_generator: next
|
|
64
|
-
- name: Restore cache
|
|
65
|
-
uses: actions/cache@v4
|
|
66
|
-
with:
|
|
67
|
-
path: |
|
|
68
|
-
docs/.next/cache
|
|
69
|
-
# Generate a new cache whenever packages or source files change.
|
|
70
|
-
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
|
71
|
-
# If source files changed but packages didn't, rebuild from a prior cache.
|
|
72
|
-
restore-keys: |
|
|
73
|
-
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
|
|
74
|
-
- name: Install root dependencies
|
|
75
|
-
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
|
|
76
|
-
- name: Build library
|
|
77
|
-
run: npm run build
|
|
78
|
-
- name: Install docs dependencies
|
|
79
|
-
working-directory: ./docs
|
|
80
|
-
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
|
|
81
|
-
- name: Build Next.js docs
|
|
82
|
-
working-directory: ./docs
|
|
83
|
-
run: ${{ steps.detect-package-manager.outputs.runner }} next build
|
|
84
|
-
- name: Upload artifact
|
|
85
|
-
uses: actions/upload-pages-artifact@v3
|
|
86
|
-
with:
|
|
87
|
-
path: ./docs/out
|
|
88
|
-
|
|
89
|
-
# Deployment job
|
|
90
|
-
deploy:
|
|
91
|
-
environment:
|
|
92
|
-
name: github-pages
|
|
93
|
-
url: ${{ steps.deployment.outputs.page_url }}
|
|
94
|
-
runs-on: ubuntu-latest
|
|
95
|
-
needs: build
|
|
96
|
-
steps:
|
|
97
|
-
- name: Deploy to GitHub Pages
|
|
98
|
-
id: deployment
|
|
99
|
-
uses: actions/deploy-pages@v4
|
package/.gitmodules
DELETED
package/assets/architecture.png
DELETED
|
Binary file
|
package/assets/editor.gif
DELETED
|
Binary file
|
package/assets/favicon.ico
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default function Home(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Physics, RigidBody } from "@react-three/rapier";
|
|
4
|
-
import { OrbitControls } from "@react-three/drei";
|
|
5
|
-
import { useState } from "react";
|
|
6
|
-
import { DragDropLoader } from "./index";
|
|
7
|
-
import GameCanvas from "../../shared/GameCanvas";
|
|
8
|
-
export default function Home() {
|
|
9
|
-
const [models, setModels] = useState([]);
|
|
10
|
-
return (_jsx(DragDropLoader, { onModelLoaded: model => setModels(prev => [...prev, model]), className: "w-full items-center justify-items-center min-h-screen", style: { height: "100vh" }, children: _jsx("div", { className: "w-full items-center justify-items-center min-h-screen", style: { height: "100vh" }, children: _jsx(GameCanvas, { children: _jsxs(Physics, { children: [_jsx(RigidBody, { children: _jsxs("mesh", { castShadow: true, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: "orange" })] }) }), _jsx(RigidBody, { type: "fixed", children: _jsxs("mesh", { position: [0, -2, 0], scale: [10, 0.1, 10], receiveShadow: true, children: [_jsx("boxGeometry", {}), _jsx("meshStandardMaterial", { color: "gray" })] }) }), models.map((model, idx) => (_jsx("primitive", { object: model, position: [0, 0, 0] }, idx))), _jsx("ambientLight", { intensity: 0.5 }), _jsx("pointLight", { position: [10, 10, 10], castShadow: true, intensity: 1000 }), _jsx(OrbitControls, {})] }) }) }) }));
|
|
11
|
-
}
|