react-three-game 0.0.59 → 0.0.61
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/tools/dragdrop/DragDropLoader.d.ts +8 -8
- package/dist/tools/dragdrop/DragDropLoader.js +33 -15
- package/dist/tools/dragdrop/index.d.ts +3 -3
- package/dist/tools/dragdrop/index.js +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
- 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 -136
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -145
- 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 -262
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
- 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
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
-
import type { LoadedModel } from "./modelLoader";
|
|
3
|
-
export interface
|
|
2
|
+
import type { LoadedModel, LoadedTexture } from "./modelLoader";
|
|
3
|
+
export interface AssetLoadOptions {
|
|
4
4
|
onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
|
|
5
|
-
|
|
5
|
+
onTextureLoaded?: (texture: LoadedTexture, filename: string, file: File) => void | Promise<void>;
|
|
6
|
+
onUnhandledFile?: (file: File) => void | Promise<void>;
|
|
6
7
|
onFilesLoaded?: (files: File[]) => void | Promise<void>;
|
|
7
|
-
|
|
8
|
-
parseModels?: boolean;
|
|
8
|
+
onLoadError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
|
|
9
9
|
}
|
|
10
10
|
type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
|
|
11
|
-
export interface DragDropLoaderProps extends
|
|
11
|
+
export interface DragDropLoaderProps extends AssetLoadOptions, DivProps {
|
|
12
12
|
children?: ReactNode;
|
|
13
13
|
}
|
|
14
|
-
export interface FilePickerProps extends
|
|
14
|
+
export interface FilePickerProps extends AssetLoadOptions, DivProps {
|
|
15
15
|
accept?: string;
|
|
16
16
|
children?: ReactNode;
|
|
17
17
|
multiple?: boolean;
|
|
18
18
|
}
|
|
19
|
-
export declare function loadFiles(files: File[], { onModelLoaded,
|
|
19
|
+
export declare function loadFiles(files: File[], { onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }: AssetLoadOptions): Promise<void>;
|
|
20
20
|
export declare function DragDropLoader({ children, ...divProps }: DragDropLoaderProps): import("react/jsx-runtime").JSX.Element;
|
|
21
21
|
export declare function FilePicker({ accept, children, multiple, ...divProps }: FilePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
22
22
|
export {};
|
|
@@ -20,27 +20,45 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
20
20
|
};
|
|
21
21
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
22
|
import { useRef } from "react";
|
|
23
|
-
import { canParseModelFile, parseModelFromFile } from "./modelLoader";
|
|
23
|
+
import { canParseModelFile, canParseTextureFile, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
|
|
24
|
+
const DEFAULT_ACCEPT = ".glb,.gltf,.fbx,.png,.jpg,.jpeg,.webp,.gif,.bmp,.svg";
|
|
24
25
|
function getFiles(fileList) {
|
|
25
26
|
return fileList ? Array.from(fileList) : [];
|
|
26
27
|
}
|
|
27
28
|
export function loadFiles(files_1, _a) {
|
|
28
|
-
return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded,
|
|
29
|
+
return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }) {
|
|
29
30
|
yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const shouldParseModel = canParseModelFile(file);
|
|
32
|
+
const shouldParseTexture = canParseTextureFile(file);
|
|
33
|
+
if (shouldParseModel) {
|
|
34
|
+
const result = yield parseModelFromFile(file);
|
|
35
|
+
if (result.success && result.model) {
|
|
36
|
+
yield (onModelLoaded === null || onModelLoaded === void 0 ? void 0 : onModelLoaded(result.model, file.name, file));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (onLoadError) {
|
|
40
|
+
yield onLoadError(result.error, file.name, file);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.error("Model parse error:", result.error);
|
|
32
44
|
return;
|
|
33
45
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
if (shouldParseTexture) {
|
|
47
|
+
const result = yield parseTextureFromFile(file);
|
|
48
|
+
if (result.success && result.texture) {
|
|
49
|
+
yield (onTextureLoaded === null || onTextureLoaded === void 0 ? void 0 : onTextureLoaded(result.texture, file.name, file));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (onLoadError) {
|
|
53
|
+
yield onLoadError(result.error, file.name, file);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.error("Texture parse error:", result.error);
|
|
37
57
|
return;
|
|
38
58
|
}
|
|
39
|
-
if (
|
|
40
|
-
yield
|
|
41
|
-
return;
|
|
59
|
+
if (onUnhandledFile) {
|
|
60
|
+
yield onUnhandledFile(file);
|
|
42
61
|
}
|
|
43
|
-
console.error("Model parse error:", result.error);
|
|
44
62
|
})));
|
|
45
63
|
yield (onFilesLoaded === null || onFilesLoaded === void 0 ? void 0 : onFilesLoaded(files));
|
|
46
64
|
});
|
|
@@ -50,11 +68,11 @@ function reportFileLoadError(error) {
|
|
|
50
68
|
}
|
|
51
69
|
function createLoadHandlers(options) {
|
|
52
70
|
return {
|
|
53
|
-
onFileLoaded: options.onFileLoaded,
|
|
54
71
|
onFilesLoaded: options.onFilesLoaded,
|
|
55
|
-
onModelError: options.onModelError,
|
|
56
72
|
onModelLoaded: options.onModelLoaded,
|
|
57
|
-
|
|
73
|
+
onTextureLoaded: options.onTextureLoaded,
|
|
74
|
+
onUnhandledFile: options.onUnhandledFile,
|
|
75
|
+
onLoadError: options.onLoadError,
|
|
58
76
|
};
|
|
59
77
|
}
|
|
60
78
|
export function DragDropLoader(_a) {
|
|
@@ -73,7 +91,7 @@ export function DragDropLoader(_a) {
|
|
|
73
91
|
return (_jsx("div", Object.assign({}, divProps, { onDrop: handleDrop, onDragOver: handleDragOver, children: children })));
|
|
74
92
|
}
|
|
75
93
|
export function FilePicker(_a) {
|
|
76
|
-
var { accept =
|
|
94
|
+
var { accept = DEFAULT_ACCEPT, children, multiple = true } = _a, divProps = __rest(_a, ["accept", "children", "multiple"]);
|
|
77
95
|
const inputRef = useRef(null);
|
|
78
96
|
const { onClick } = divProps, wrapperProps = __rest(divProps, ["onClick"]);
|
|
79
97
|
const loadOptions = createLoadHandlers(divProps);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
|
|
2
|
-
export type {
|
|
3
|
-
export { loadModel, parseModelFromFile } from "./modelLoader";
|
|
4
|
-
export type { LoadedModel, ModelLoadResult, ProgressCallback } from "./modelLoader";
|
|
2
|
+
export type { AssetLoadOptions, DragDropLoaderProps, FilePickerProps } from "./DragDropLoader";
|
|
3
|
+
export { loadModel, loadTexture, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
|
|
4
|
+
export type { LoadedModel, LoadedTexture, ModelLoadResult, ProgressCallback, TextureLoadResult } from "./modelLoader";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
|
|
2
|
-
export { loadModel, parseModelFromFile } from "./modelLoader";
|
|
2
|
+
export { loadModel, loadTexture, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import type { Object3D } from "three";
|
|
1
|
+
import type { Object3D, Texture } from "three";
|
|
2
2
|
export type LoadedModel = Object3D;
|
|
3
|
+
export type LoadedTexture = Texture;
|
|
3
4
|
export type ModelLoadResult = {
|
|
4
5
|
success: boolean;
|
|
5
6
|
model?: LoadedModel;
|
|
6
7
|
error?: unknown;
|
|
7
8
|
};
|
|
9
|
+
export type TextureLoadResult = {
|
|
10
|
+
success: boolean;
|
|
11
|
+
texture?: LoadedTexture;
|
|
12
|
+
error?: unknown;
|
|
13
|
+
};
|
|
8
14
|
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
9
15
|
export declare function canParseModelFile(file: File | string): boolean;
|
|
16
|
+
export declare function canParseTextureFile(file: File | string): boolean;
|
|
10
17
|
export declare function parseModelFromFile(file: File): Promise<ModelLoadResult>;
|
|
18
|
+
export declare function parseTextureFromFile(file: File): Promise<TextureLoadResult>;
|
|
11
19
|
export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
|
|
20
|
+
export declare function loadTexture(filename: string): Promise<TextureLoadResult>;
|
|
@@ -7,12 +7,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { SRGBColorSpace, TextureLoader } from "three";
|
|
10
11
|
import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
|
|
11
12
|
const dracoLoader = new DRACOLoader();
|
|
12
13
|
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
13
14
|
const gltfLoader = new GLTFLoader();
|
|
14
15
|
gltfLoader.setDRACOLoader(dracoLoader);
|
|
15
16
|
const fbxLoader = new FBXLoader();
|
|
17
|
+
const textureLoader = new TextureLoader();
|
|
18
|
+
const TEXTURE_FILE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg"];
|
|
16
19
|
function normalizeModelPath(name) {
|
|
17
20
|
return name.split(/[?#]/, 1)[0].toLowerCase();
|
|
18
21
|
}
|
|
@@ -30,6 +33,11 @@ export function canParseModelFile(file) {
|
|
|
30
33
|
const filename = typeof file === "string" ? file : file.name;
|
|
31
34
|
return getModelFileKind(filename) !== null;
|
|
32
35
|
}
|
|
36
|
+
export function canParseTextureFile(file) {
|
|
37
|
+
const filename = typeof file === "string" ? file : file.name;
|
|
38
|
+
const normalizedName = normalizeModelPath(filename);
|
|
39
|
+
return TEXTURE_FILE_EXTENSIONS.some(extension => normalizedName.endsWith(extension));
|
|
40
|
+
}
|
|
33
41
|
function parseModelBuffer(arrayBuffer, sourceName) {
|
|
34
42
|
const modelFileKind = getModelFileKind(sourceName);
|
|
35
43
|
if (modelFileKind === "gltf") {
|
|
@@ -68,6 +76,19 @@ export function parseModelFromFile(file) {
|
|
|
68
76
|
reader.readAsArrayBuffer(file);
|
|
69
77
|
});
|
|
70
78
|
}
|
|
79
|
+
export function parseTextureFromFile(file) {
|
|
80
|
+
return new Promise(resolve => {
|
|
81
|
+
const url = URL.createObjectURL(file);
|
|
82
|
+
textureLoader.load(url, texture => {
|
|
83
|
+
texture.colorSpace = SRGBColorSpace;
|
|
84
|
+
resolve({ success: true, texture });
|
|
85
|
+
URL.revokeObjectURL(url);
|
|
86
|
+
}, undefined, error => {
|
|
87
|
+
resolve({ success: false, error });
|
|
88
|
+
URL.revokeObjectURL(url);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
71
92
|
export function loadModel(filename, onProgress) {
|
|
72
93
|
return __awaiter(this, void 0, void 0, function* () {
|
|
73
94
|
try {
|
|
@@ -102,3 +123,21 @@ export function loadModel(filename, onProgress) {
|
|
|
102
123
|
}
|
|
103
124
|
});
|
|
104
125
|
}
|
|
126
|
+
export function loadTexture(filename) {
|
|
127
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
128
|
+
try {
|
|
129
|
+
if (!canParseTextureFile(filename)) {
|
|
130
|
+
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
131
|
+
}
|
|
132
|
+
return yield new Promise(resolve => {
|
|
133
|
+
textureLoader.load(filename, texture => {
|
|
134
|
+
texture.colorSpace = SRGBColorSpace;
|
|
135
|
+
resolve({ success: true, texture });
|
|
136
|
+
}, undefined, error => resolve({ success: false, error }));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return { success: false, error };
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -7,7 +7,7 @@ import EditorUI from "./EditorUI";
|
|
|
7
7
|
import { base, toolbar } from "./styles";
|
|
8
8
|
import { EditorContext } from "./EditorContext";
|
|
9
9
|
import { exportGLB, createModelNode, createImageNode } from "./utils";
|
|
10
|
-
import {
|
|
10
|
+
import { loadFiles } from "../dragdrop";
|
|
11
11
|
const DEFAULT_PREFAB = {
|
|
12
12
|
id: "prefab-default",
|
|
13
13
|
name: "New Prefab",
|
|
@@ -120,8 +120,6 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
120
120
|
}, []);
|
|
121
121
|
// --- Drag & drop files to add nodes ---
|
|
122
122
|
useEffect(() => {
|
|
123
|
-
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
|
|
124
|
-
const MODEL_EXTS = ['glb', 'gltf', 'fbx'];
|
|
125
123
|
function handleDragOver(e) {
|
|
126
124
|
e.preventDefault();
|
|
127
125
|
e.stopPropagation();
|
|
@@ -131,39 +129,32 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
|
|
|
131
129
|
e.preventDefault();
|
|
132
130
|
e.stopPropagation();
|
|
133
131
|
const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
|
|
134
|
-
files
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const baseName = file.name.replace(/\.[^.]+$/, '');
|
|
140
|
-
if (MODEL_EXTS.includes(ext)) {
|
|
141
|
-
const modelPath = `models/${file.name}`;
|
|
132
|
+
void loadFiles(files, {
|
|
133
|
+
onModelLoaded: (model, filename) => {
|
|
134
|
+
var _a;
|
|
135
|
+
const modelPath = `models/${filename}`;
|
|
136
|
+
const baseName = filename.replace(/\.[^.]+$/, '');
|
|
142
137
|
const newNode = createModelNode(modelPath, baseName);
|
|
143
138
|
updatePrefab(prev => {
|
|
144
139
|
var _a;
|
|
145
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] }) }));
|
|
146
141
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.error('Drop parse error:', result.error);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
else if (IMAGE_EXTS.includes(ext)) {
|
|
158
|
-
const texturePath = `textures/${file.name}`;
|
|
142
|
+
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, model);
|
|
143
|
+
},
|
|
144
|
+
onTextureLoaded: (texture, filename) => {
|
|
145
|
+
var _a;
|
|
146
|
+
const texturePath = `textures/${filename}`;
|
|
147
|
+
const baseName = filename.replace(/\.[^.]+$/, '');
|
|
159
148
|
const newNode = createImageNode(texturePath, baseName);
|
|
160
149
|
updatePrefab(prev => {
|
|
161
150
|
var _a;
|
|
162
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] }) }));
|
|
163
152
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(texturePath, texture);
|
|
154
|
+
},
|
|
155
|
+
onLoadError: error => {
|
|
156
|
+
console.error('Drop asset error:', error);
|
|
157
|
+
},
|
|
167
158
|
});
|
|
168
159
|
}
|
|
169
160
|
window.addEventListener('dragover', handleDragOver);
|
|
@@ -5,7 +5,7 @@ export interface PrefabRootRef {
|
|
|
5
5
|
root: Group | null;
|
|
6
6
|
rigidBodyRefs: Map<string, any>;
|
|
7
7
|
injectModel: (filename: string, model: Object3D) => void;
|
|
8
|
-
injectTexture: (filename: string,
|
|
8
|
+
injectTexture: (filename: string, texture: Texture) => void;
|
|
9
9
|
focusNode: (nodeId: string) => void;
|
|
10
10
|
}
|
|
11
11
|
export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
|
|
@@ -40,15 +40,9 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
40
40
|
const injectModel = useCallback((filename, model) => {
|
|
41
41
|
setModels(m => (Object.assign(Object.assign({}, m), { [filename]: model })));
|
|
42
42
|
}, []);
|
|
43
|
-
const injectTexture = useCallback((filename,
|
|
43
|
+
const injectTexture = useCallback((filename, texture) => {
|
|
44
44
|
loading.current.add(filename);
|
|
45
|
-
|
|
46
|
-
const loader = new TextureLoader();
|
|
47
|
-
loader.load(url, tex => {
|
|
48
|
-
tex.colorSpace = SRGBColorSpace;
|
|
49
|
-
setTextures(t => (Object.assign(Object.assign({}, t), { [filename]: tex })));
|
|
50
|
-
URL.revokeObjectURL(url);
|
|
51
|
-
}, undefined, () => URL.revokeObjectURL(url));
|
|
45
|
+
setTextures(t => (Object.assign(Object.assign({}, t), { [filename]: texture })));
|
|
52
46
|
}, []);
|
|
53
47
|
useImperativeHandle(ref, () => ({
|
|
54
48
|
root: rootRef.current,
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-three-game",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.61",
|
|
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
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { RapierRigidBody } from '@react-three/rapier';
|
|
2
|
-
export type EntityEventType = 'sensor:enter' | 'sensor:exit' | 'collision:enter' | 'collision:exit';
|
|
3
|
-
export interface EntityEventPayload {
|
|
4
|
-
sourceEntityId: string;
|
|
5
|
-
targetEntityId: string | null;
|
|
6
|
-
targetRigidBody: RapierRigidBody | null | undefined;
|
|
7
|
-
}
|
|
8
|
-
type EventHandler = (payload: EntityEventPayload) => void;
|
|
9
|
-
/**
|
|
10
|
-
* Entity event system for physics interactions.
|
|
11
|
-
*
|
|
12
|
-
* Events:
|
|
13
|
-
* - sensor:enter - Fired when something enters a sensor collider
|
|
14
|
-
* - sensor:exit - Fired when something exits a sensor collider
|
|
15
|
-
* - collision:enter - Fired when a collision starts
|
|
16
|
-
* - collision:exit - Fired when a collision ends
|
|
17
|
-
*/
|
|
18
|
-
export declare const entityEvents: {
|
|
19
|
-
/**
|
|
20
|
-
* Emit an event to all subscribers
|
|
21
|
-
*/
|
|
22
|
-
emit(type: EntityEventType, payload: EntityEventPayload): void;
|
|
23
|
-
/**
|
|
24
|
-
* Subscribe to an event type
|
|
25
|
-
* @returns Unsubscribe function
|
|
26
|
-
*/
|
|
27
|
-
on(type: EntityEventType, handler: EventHandler): () => void;
|
|
28
|
-
/**
|
|
29
|
-
* Unsubscribe from an event type
|
|
30
|
-
*/
|
|
31
|
-
off(type: EntityEventType, handler: EventHandler): void;
|
|
32
|
-
/**
|
|
33
|
-
* Remove all subscribers (useful for cleanup)
|
|
34
|
-
*/
|
|
35
|
-
clear(): void;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* React hook to subscribe to entity events.
|
|
39
|
-
* Automatically cleans up on unmount.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* useEntityEvent('sensor:enter', (payload) => {
|
|
43
|
-
* if (payload.sourceEntityId === 'trigger-zone') {
|
|
44
|
-
* console.log('Player entered trigger zone!');
|
|
45
|
-
* }
|
|
46
|
-
* });
|
|
47
|
-
*/
|
|
48
|
-
export declare function useEntityEvent(type: EntityEventType, handler: EventHandler, deps?: any[]): void;
|
|
49
|
-
/**
|
|
50
|
-
* Helper to extract entity ID from Rapier collision data.
|
|
51
|
-
* Entity IDs are stored in RigidBody userData.
|
|
52
|
-
*/
|
|
53
|
-
export declare function getEntityIdFromRigidBody(rigidBody: RapierRigidBody | null | undefined): string | null;
|
|
54
|
-
export {};
|