react-three-game 0.0.18 → 0.0.20
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/.github/copilot-instructions.md +54 -183
- package/README.md +69 -214
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -0
- package/dist/tools/assetviewer/page.js +24 -13
- package/dist/tools/prefabeditor/EditorTree.d.ts +2 -4
- package/dist/tools/prefabeditor/EditorTree.js +20 -194
- package/dist/tools/prefabeditor/EditorUI.js +43 -224
- package/dist/tools/prefabeditor/PrefabEditor.js +33 -99
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +0 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +7 -23
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +31 -43
- package/dist/tools/prefabeditor/styles.d.ts +1809 -0
- package/dist/tools/prefabeditor/styles.js +168 -0
- package/dist/tools/prefabeditor/types.d.ts +3 -14
- package/dist/tools/prefabeditor/types.js +0 -1
- package/dist/tools/prefabeditor/utils.d.ts +19 -0
- package/dist/tools/prefabeditor/utils.js +72 -0
- package/package.json +8 -3
- package/src/index.ts +5 -1
- package/src/tools/assetviewer/page.tsx +22 -12
- package/src/tools/prefabeditor/EditorTree.tsx +38 -270
- package/src/tools/prefabeditor/EditorUI.tsx +105 -322
- package/src/tools/prefabeditor/PrefabEditor.tsx +40 -151
- package/src/tools/prefabeditor/PrefabRoot.tsx +11 -32
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +38 -53
- package/src/tools/prefabeditor/styles.ts +195 -0
- package/src/tools/prefabeditor/types.ts +4 -12
- package/src/tools/prefabeditor/utils.ts +80 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Shared editor styles - single source of truth for all prefab editor UI
|
|
2
|
+
export const colors = {
|
|
3
|
+
bg: 'rgba(0,0,0,0.6)',
|
|
4
|
+
bgLight: 'rgba(255,255,255,0.06)',
|
|
5
|
+
bgHover: 'rgba(255,255,255,0.1)',
|
|
6
|
+
border: 'rgba(255,255,255,0.15)',
|
|
7
|
+
borderLight: 'rgba(255,255,255,0.1)',
|
|
8
|
+
borderFaint: 'rgba(255,255,255,0.05)',
|
|
9
|
+
text: '#fff',
|
|
10
|
+
textMuted: 'rgba(255,255,255,0.7)',
|
|
11
|
+
danger: '#ffaaaa',
|
|
12
|
+
dangerBg: 'rgba(255,80,80,0.2)',
|
|
13
|
+
dangerBorder: 'rgba(255,80,80,0.4)',
|
|
14
|
+
};
|
|
15
|
+
export const fonts = {
|
|
16
|
+
family: 'system-ui, sans-serif',
|
|
17
|
+
size: 11,
|
|
18
|
+
sizeSm: 10,
|
|
19
|
+
};
|
|
20
|
+
// Base component styles
|
|
21
|
+
export const base = {
|
|
22
|
+
panel: {
|
|
23
|
+
background: colors.bg,
|
|
24
|
+
color: colors.text,
|
|
25
|
+
border: `1px solid ${colors.border}`,
|
|
26
|
+
borderRadius: 4,
|
|
27
|
+
overflow: 'hidden',
|
|
28
|
+
backdropFilter: 'blur(8px)',
|
|
29
|
+
fontFamily: fonts.family,
|
|
30
|
+
fontSize: fonts.size,
|
|
31
|
+
},
|
|
32
|
+
header: {
|
|
33
|
+
padding: '6px 8px',
|
|
34
|
+
display: 'flex',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'space-between',
|
|
37
|
+
cursor: 'pointer',
|
|
38
|
+
background: colors.bgLight,
|
|
39
|
+
borderBottom: `1px solid ${colors.borderLight}`,
|
|
40
|
+
fontSize: fonts.size,
|
|
41
|
+
fontWeight: 500,
|
|
42
|
+
textTransform: 'uppercase',
|
|
43
|
+
letterSpacing: 0.5,
|
|
44
|
+
},
|
|
45
|
+
input: {
|
|
46
|
+
width: '100%',
|
|
47
|
+
background: colors.bgHover,
|
|
48
|
+
border: `1px solid ${colors.border}`,
|
|
49
|
+
borderRadius: 3,
|
|
50
|
+
padding: '4px 6px',
|
|
51
|
+
color: colors.text,
|
|
52
|
+
fontSize: fonts.size,
|
|
53
|
+
outline: 'none',
|
|
54
|
+
},
|
|
55
|
+
btn: {
|
|
56
|
+
background: colors.bgHover,
|
|
57
|
+
border: `1px solid ${colors.border}`,
|
|
58
|
+
borderRadius: 3,
|
|
59
|
+
padding: '4px 8px',
|
|
60
|
+
color: colors.text,
|
|
61
|
+
fontSize: fonts.size,
|
|
62
|
+
cursor: 'pointer',
|
|
63
|
+
outline: 'none',
|
|
64
|
+
},
|
|
65
|
+
btnDanger: {
|
|
66
|
+
background: colors.dangerBg,
|
|
67
|
+
borderColor: colors.dangerBorder,
|
|
68
|
+
color: colors.danger,
|
|
69
|
+
},
|
|
70
|
+
label: {
|
|
71
|
+
fontSize: fonts.sizeSm,
|
|
72
|
+
opacity: 0.7,
|
|
73
|
+
marginBottom: 4,
|
|
74
|
+
textTransform: 'uppercase',
|
|
75
|
+
letterSpacing: 0.5,
|
|
76
|
+
},
|
|
77
|
+
row: {
|
|
78
|
+
display: 'flex',
|
|
79
|
+
gap: 6,
|
|
80
|
+
},
|
|
81
|
+
section: {
|
|
82
|
+
paddingBottom: 8,
|
|
83
|
+
borderBottom: `1px solid ${colors.borderLight}`,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
// Specific panel styles
|
|
87
|
+
export const inspector = {
|
|
88
|
+
panel: Object.assign(Object.assign({}, base.panel), { position: 'absolute', top: 8, right: 8, zIndex: 20, width: 260 }),
|
|
89
|
+
content: {
|
|
90
|
+
padding: 8,
|
|
91
|
+
maxHeight: '80vh',
|
|
92
|
+
overflowY: 'auto',
|
|
93
|
+
display: 'flex',
|
|
94
|
+
flexDirection: 'column',
|
|
95
|
+
gap: 8,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
export const tree = {
|
|
99
|
+
panel: Object.assign(Object.assign({}, base.panel), { maxHeight: '85vh', display: 'flex', flexDirection: 'column', userSelect: 'none' }),
|
|
100
|
+
scroll: {
|
|
101
|
+
overflowY: 'auto',
|
|
102
|
+
padding: 4,
|
|
103
|
+
},
|
|
104
|
+
row: {
|
|
105
|
+
display: 'flex',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
padding: '3px 6px',
|
|
108
|
+
borderBottom: `1px solid ${colors.borderFaint}`,
|
|
109
|
+
cursor: 'pointer',
|
|
110
|
+
whiteSpace: 'nowrap',
|
|
111
|
+
},
|
|
112
|
+
selected: {
|
|
113
|
+
background: 'rgba(255,255,255,0.12)',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
export const menu = {
|
|
117
|
+
container: {
|
|
118
|
+
position: 'fixed',
|
|
119
|
+
zIndex: 50,
|
|
120
|
+
minWidth: 120,
|
|
121
|
+
background: 'rgba(0,0,0,0.85)',
|
|
122
|
+
border: `1px solid ${colors.border}`,
|
|
123
|
+
borderRadius: 4,
|
|
124
|
+
overflow: 'hidden',
|
|
125
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
|
|
126
|
+
backdropFilter: 'blur(8px)',
|
|
127
|
+
},
|
|
128
|
+
item: {
|
|
129
|
+
width: '100%',
|
|
130
|
+
textAlign: 'left',
|
|
131
|
+
padding: '6px 8px',
|
|
132
|
+
background: 'transparent',
|
|
133
|
+
border: 'none',
|
|
134
|
+
color: colors.text,
|
|
135
|
+
fontSize: fonts.size,
|
|
136
|
+
cursor: 'pointer',
|
|
137
|
+
outline: 'none',
|
|
138
|
+
},
|
|
139
|
+
danger: {
|
|
140
|
+
color: colors.danger,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
export const toolbar = {
|
|
144
|
+
panel: {
|
|
145
|
+
position: 'absolute',
|
|
146
|
+
top: 8,
|
|
147
|
+
left: '50%',
|
|
148
|
+
transform: 'translateX(-50%)',
|
|
149
|
+
display: 'flex',
|
|
150
|
+
gap: 6,
|
|
151
|
+
padding: '4px 6px',
|
|
152
|
+
background: colors.bg,
|
|
153
|
+
border: `1px solid ${colors.border}`,
|
|
154
|
+
borderRadius: 4,
|
|
155
|
+
color: colors.text,
|
|
156
|
+
fontFamily: fonts.family,
|
|
157
|
+
fontSize: fonts.size,
|
|
158
|
+
backdropFilter: 'blur(8px)',
|
|
159
|
+
},
|
|
160
|
+
divider: {
|
|
161
|
+
width: 1,
|
|
162
|
+
background: 'rgba(255,255,255,0.2)',
|
|
163
|
+
},
|
|
164
|
+
disabled: {
|
|
165
|
+
opacity: 0.4,
|
|
166
|
+
cursor: 'not-allowed',
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
export interface Prefab {
|
|
2
2
|
id?: string;
|
|
3
3
|
name?: string;
|
|
4
|
-
description?: string;
|
|
5
|
-
author?: string;
|
|
6
|
-
version?: string;
|
|
7
|
-
assets?: string[] | {
|
|
8
|
-
[assetName: string]: string;
|
|
9
|
-
};
|
|
10
|
-
onStart?: (target: any) => void;
|
|
11
4
|
root: GameObject;
|
|
12
5
|
}
|
|
13
6
|
export interface GameObject {
|
|
14
7
|
id: string;
|
|
15
8
|
disabled?: boolean;
|
|
16
9
|
hidden?: boolean;
|
|
17
|
-
ref?: any;
|
|
18
10
|
children?: GameObject[];
|
|
19
11
|
components?: {
|
|
20
|
-
[
|
|
12
|
+
[key: string]: ComponentData | undefined;
|
|
21
13
|
};
|
|
22
14
|
}
|
|
23
|
-
interface
|
|
15
|
+
export interface ComponentData {
|
|
24
16
|
type: string;
|
|
25
|
-
properties:
|
|
26
|
-
[key: string]: any;
|
|
27
|
-
};
|
|
17
|
+
properties: Record<string, any>;
|
|
28
18
|
}
|
|
29
|
-
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GameObject } from "./types";
|
|
2
|
+
/** Find a node by ID in the tree */
|
|
3
|
+
export declare function findNode(root: GameObject, id: string): GameObject | null;
|
|
4
|
+
/** Find the parent of a node by ID */
|
|
5
|
+
export declare function findParent(root: GameObject, id: string): GameObject | null;
|
|
6
|
+
/** Find all nodes matching a predicate */
|
|
7
|
+
export declare function findAll(root: GameObject, predicate: (node: GameObject) => boolean): GameObject[];
|
|
8
|
+
/** Find all nodes that have a specific component type */
|
|
9
|
+
export declare function findByComponent(root: GameObject, componentType: string): GameObject[];
|
|
10
|
+
/** Get a flattened list of all nodes */
|
|
11
|
+
export declare function flatten(root: GameObject): GameObject[];
|
|
12
|
+
/** Immutably update a node by ID */
|
|
13
|
+
export declare function updateNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject;
|
|
14
|
+
/** Immutably delete a node by ID */
|
|
15
|
+
export declare function deleteNode(root: GameObject, id: string): GameObject | null;
|
|
16
|
+
/** Deep clone a node with new IDs */
|
|
17
|
+
export declare function cloneNode(node: GameObject): GameObject;
|
|
18
|
+
/** Get component data from a node */
|
|
19
|
+
export declare function getComponent<T = any>(node: GameObject, type: string): T | undefined;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Find a node by ID in the tree */
|
|
2
|
+
export function findNode(root, id) {
|
|
3
|
+
var _a;
|
|
4
|
+
if (root.id === id)
|
|
5
|
+
return root;
|
|
6
|
+
for (const child of (_a = root.children) !== null && _a !== void 0 ? _a : []) {
|
|
7
|
+
const found = findNode(child, id);
|
|
8
|
+
if (found)
|
|
9
|
+
return found;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
/** Find the parent of a node by ID */
|
|
14
|
+
export function findParent(root, id) {
|
|
15
|
+
var _a;
|
|
16
|
+
for (const child of (_a = root.children) !== null && _a !== void 0 ? _a : []) {
|
|
17
|
+
if (child.id === id)
|
|
18
|
+
return root;
|
|
19
|
+
const found = findParent(child, id);
|
|
20
|
+
if (found)
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/** Find all nodes matching a predicate */
|
|
26
|
+
export function findAll(root, predicate) {
|
|
27
|
+
var _a;
|
|
28
|
+
const results = [];
|
|
29
|
+
if (predicate(root))
|
|
30
|
+
results.push(root);
|
|
31
|
+
for (const child of (_a = root.children) !== null && _a !== void 0 ? _a : []) {
|
|
32
|
+
results.push(...findAll(child, predicate));
|
|
33
|
+
}
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
/** Find all nodes that have a specific component type */
|
|
37
|
+
export function findByComponent(root, componentType) {
|
|
38
|
+
return findAll(root, node => { var _a; return Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).some(c => (c === null || c === void 0 ? void 0 : c.type) === componentType); });
|
|
39
|
+
}
|
|
40
|
+
/** Get a flattened list of all nodes */
|
|
41
|
+
export function flatten(root) {
|
|
42
|
+
return findAll(root, () => true);
|
|
43
|
+
}
|
|
44
|
+
/** Immutably update a node by ID */
|
|
45
|
+
export function updateNode(root, id, update) {
|
|
46
|
+
if (root.id === id)
|
|
47
|
+
return update(root);
|
|
48
|
+
if (!root.children)
|
|
49
|
+
return root;
|
|
50
|
+
return Object.assign(Object.assign({}, root), { children: root.children.map(child => updateNode(child, id, update)) });
|
|
51
|
+
}
|
|
52
|
+
/** Immutably delete a node by ID */
|
|
53
|
+
export function deleteNode(root, id) {
|
|
54
|
+
if (root.id === id)
|
|
55
|
+
return null;
|
|
56
|
+
if (!root.children)
|
|
57
|
+
return root;
|
|
58
|
+
return Object.assign(Object.assign({}, root), { children: root.children
|
|
59
|
+
.map(child => deleteNode(child, id))
|
|
60
|
+
.filter((child) => child !== null) });
|
|
61
|
+
}
|
|
62
|
+
/** Deep clone a node with new IDs */
|
|
63
|
+
export function cloneNode(node) {
|
|
64
|
+
var _a;
|
|
65
|
+
return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(), children: (_a = node.children) === null || _a === void 0 ? void 0 : _a.map(cloneNode) });
|
|
66
|
+
}
|
|
67
|
+
/** Get component data from a node */
|
|
68
|
+
export function getComponent(node, type) {
|
|
69
|
+
var _a;
|
|
70
|
+
const comp = Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).find(c => (c === null || c === void 0 ? void 0 : c.type) === type);
|
|
71
|
+
return comp === null || comp === void 0 ? void 0 : comp.properties;
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-three-game",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Batteries included React Three Fiber game engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
"author": "prnth",
|
|
16
16
|
"license": "VPL",
|
|
17
17
|
"type": "module",
|
|
18
|
-
"workspaces": [
|
|
18
|
+
"workspaces": [
|
|
19
|
+
"docs"
|
|
20
|
+
],
|
|
19
21
|
"peerDependencies": {
|
|
20
|
-
"@react-three/fiber": ">=9.0.0",
|
|
21
22
|
"@react-three/drei": ">=10.0.0",
|
|
23
|
+
"@react-three/fiber": ">=9.0.0",
|
|
22
24
|
"@react-three/rapier": ">=2.0.0",
|
|
23
25
|
"react": ">=18.0.0",
|
|
24
26
|
"react-dom": ">=18.0.0",
|
|
@@ -36,5 +38,8 @@
|
|
|
36
38
|
"react-dom": "^19.2.0",
|
|
37
39
|
"three": "^0.182.0",
|
|
38
40
|
"typescript": "^5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"react-error-boundary": "^6.0.0"
|
|
39
44
|
}
|
|
40
45
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,8 +14,12 @@ export {
|
|
|
14
14
|
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
15
15
|
export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
|
|
16
16
|
|
|
17
|
+
// Editor Styles & Utils
|
|
18
|
+
export * as editorStyles from './tools/prefabeditor/styles';
|
|
19
|
+
export * from './tools/prefabeditor/utils';
|
|
20
|
+
|
|
17
21
|
// Helpers
|
|
18
22
|
export * from './helpers';
|
|
19
23
|
|
|
20
24
|
// Types
|
|
21
|
-
export type { Prefab, GameObject } from './tools/prefabeditor/types';
|
|
25
|
+
export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Canvas, useLoader } from "@react-three/fiber";
|
|
4
|
-
import { OrbitControls,
|
|
4
|
+
import { OrbitControls, Stage, View, PerspectiveCamera } from "@react-three/drei";
|
|
5
5
|
import { Suspense, useEffect, useState, useRef } from "react";
|
|
6
6
|
import { TextureLoader } from "three";
|
|
7
|
+
import { loadModel } from "../dragdrop/modelLoader";
|
|
7
8
|
|
|
8
9
|
// view models and textures in manifest, onselect callback
|
|
9
10
|
|
|
@@ -290,19 +291,28 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
function ModelPreview({ url, onError }: { url: string; onError?: () => void }) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
294
|
+
const [model, setModel] = useState<any>(null);
|
|
295
|
+
const onErrorRef = useRef(onError);
|
|
296
|
+
onErrorRef.current = onError;
|
|
297
297
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
let cancelled = false;
|
|
300
|
+
setModel(null);
|
|
301
|
+
|
|
302
|
+
loadModel(url).then((result) => {
|
|
303
|
+
if (cancelled) return;
|
|
304
|
+
if (result.success && result.model) {
|
|
305
|
+
setModel(result.model);
|
|
306
|
+
} else {
|
|
307
|
+
onErrorRef.current?.();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return () => { cancelled = true; };
|
|
312
|
+
}, [url]);
|
|
302
313
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return <primitive object={fbx} scale={0.01} />;
|
|
314
|
+
if (!model) return null;
|
|
315
|
+
return <primitive object={model} />;
|
|
306
316
|
}
|
|
307
317
|
|
|
308
318
|
interface SoundListViewerProps {
|