react-three-game 0.0.54 → 0.0.56
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/shared/ContactShadow.d.ts +8 -0
- package/dist/shared/ContactShadow.js +32 -0
- package/dist/tools/assetviewer/page.js +1 -1
- package/dist/tools/dragdrop/DragDropLoader.js +17 -40
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/EditorTree.js +3 -16
- package/dist/tools/prefabeditor/EditorUI.js +5 -10
- package/dist/tools/prefabeditor/PrefabEditor.js +57 -1
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +17 -2
- package/dist/tools/prefabeditor/components/Input.js +27 -26
- package/dist/tools/prefabeditor/components/MaterialComponent.js +9 -2
- package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +1 -1
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +3 -0
- package/dist/tools/prefabeditor/components/TransformComponent.js +13 -11
- package/dist/tools/prefabeditor/styles.d.ts +12 -2
- package/dist/tools/prefabeditor/styles.js +63 -30
- package/dist/tools/prefabeditor/utils.d.ts +4 -0
- package/dist/tools/prefabeditor/utils.js +39 -1
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
- package/src/shared/ContactShadow.tsx +74 -0
- package/src/tools/assetviewer/page.tsx +1 -1
- package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
- package/src/tools/dragdrop/modelLoader.ts +36 -0
- package/src/tools/prefabeditor/EditorTree.tsx +4 -15
- package/src/tools/prefabeditor/EditorUI.tsx +5 -10
- package/src/tools/prefabeditor/PrefabEditor.tsx +60 -1
- package/src/tools/prefabeditor/PrefabRoot.tsx +21 -2
- package/src/tools/prefabeditor/components/Input.tsx +27 -26
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +14 -5
- package/src/tools/prefabeditor/components/ModelComponent.tsx +2 -2
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +1 -1
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +4 -0
- package/src/tools/prefabeditor/components/TransformComponent.tsx +17 -11
- package/src/tools/prefabeditor/styles.ts +65 -30
- package/src/tools/prefabeditor/utils.ts +41 -1
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { FieldRenderer, Label } from "./Input";
|
|
3
3
|
import { useEditorContext } from "../EditorContext";
|
|
4
|
+
import { colors } from "../styles";
|
|
4
5
|
const buttonStyle = {
|
|
5
|
-
padding: '
|
|
6
|
-
background:
|
|
7
|
-
color:
|
|
8
|
-
border:
|
|
9
|
-
borderRadius:
|
|
6
|
+
padding: '4px 8px',
|
|
7
|
+
background: colors.bgSurface,
|
|
8
|
+
color: colors.text,
|
|
9
|
+
border: `1px solid ${colors.border}`,
|
|
10
|
+
borderRadius: 3,
|
|
10
11
|
cursor: 'pointer',
|
|
11
12
|
font: 'inherit',
|
|
13
|
+
fontSize: 11,
|
|
12
14
|
flex: 1,
|
|
13
15
|
};
|
|
14
16
|
function TransformModeSelector({ transformMode, setTransformMode, snapResolution, setSnapResolution }) {
|
|
15
17
|
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs(Label, { children: ["Transform Mode ", snapResolution > 0 && `(Snap: ${snapResolution})`] }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => {
|
|
16
18
|
const isActive = transformMode === mode;
|
|
17
|
-
return (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign({}, buttonStyle), { background: isActive ?
|
|
19
|
+
return (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign({}, buttonStyle), { background: isActive ? colors.accentBg : colors.bgSurface, borderColor: isActive ? colors.accentBorder : colors.border, color: isActive ? colors.accent : colors.text }), onPointerEnter: (e) => {
|
|
18
20
|
if (!isActive)
|
|
19
|
-
e.currentTarget.style.background =
|
|
21
|
+
e.currentTarget.style.background = colors.bgHover;
|
|
20
22
|
}, onPointerLeave: (e) => {
|
|
21
23
|
if (!isActive)
|
|
22
|
-
e.currentTarget.style.background =
|
|
24
|
+
e.currentTarget.style.background = colors.bgSurface;
|
|
23
25
|
}, children: mode }, mode));
|
|
24
|
-
}) }), _jsx("div", { style: { marginTop: 6 }, children: _jsxs("button", { onClick: () => setSnapResolution(snapResolution > 0 ? 0 : 0.1), style: Object.assign(Object.assign({}, buttonStyle), { background: snapResolution > 0 ?
|
|
26
|
+
}) }), _jsx("div", { style: { marginTop: 6 }, children: _jsxs("button", { onClick: () => setSnapResolution(snapResolution > 0 ? 0 : 0.1), style: Object.assign(Object.assign({}, buttonStyle), { background: snapResolution > 0 ? colors.accentBg : colors.bgSurface, borderColor: snapResolution > 0 ? colors.accentBorder : colors.border, color: snapResolution > 0 ? colors.accent : colors.text, width: '100%' }), onPointerEnter: (e) => {
|
|
25
27
|
if (snapResolution === 0)
|
|
26
|
-
e.currentTarget.style.background =
|
|
28
|
+
e.currentTarget.style.background = colors.bgHover;
|
|
27
29
|
}, onPointerLeave: (e) => {
|
|
28
30
|
if (snapResolution === 0)
|
|
29
|
-
e.currentTarget.style.background =
|
|
31
|
+
e.currentTarget.style.background = colors.bgSurface;
|
|
30
32
|
}, children: ["Snap: ", snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'] }) })] }));
|
|
31
33
|
}
|
|
32
34
|
function TransformComponentEditor({ component, onUpdate }) {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
export declare const colors: {
|
|
2
2
|
bg: string;
|
|
3
|
+
bgSurface: string;
|
|
3
4
|
bgLight: string;
|
|
4
5
|
bgHover: string;
|
|
6
|
+
bgInput: string;
|
|
5
7
|
border: string;
|
|
6
8
|
borderLight: string;
|
|
7
9
|
borderFaint: string;
|
|
8
10
|
text: string;
|
|
9
11
|
textMuted: string;
|
|
12
|
+
textDim: string;
|
|
13
|
+
accent: string;
|
|
14
|
+
accentBg: string;
|
|
15
|
+
accentBorder: string;
|
|
10
16
|
danger: string;
|
|
11
17
|
dangerBg: string;
|
|
12
18
|
dangerBorder: string;
|
|
@@ -1759,6 +1765,7 @@ export declare const tree: {
|
|
|
1759
1765
|
row: React.CSSProperties;
|
|
1760
1766
|
selected: {
|
|
1761
1767
|
background: string;
|
|
1768
|
+
borderBottomColor: string;
|
|
1762
1769
|
};
|
|
1763
1770
|
};
|
|
1764
1771
|
export declare const menu: {
|
|
@@ -1771,7 +1778,6 @@ export declare const menu: {
|
|
|
1771
1778
|
borderRadius: number;
|
|
1772
1779
|
overflow: string;
|
|
1773
1780
|
boxShadow: string;
|
|
1774
|
-
backdropFilter: string;
|
|
1775
1781
|
};
|
|
1776
1782
|
item: React.CSSProperties;
|
|
1777
1783
|
danger: {
|
|
@@ -1793,7 +1799,7 @@ export declare const toolbar: {
|
|
|
1793
1799
|
color: string;
|
|
1794
1800
|
fontFamily: string;
|
|
1795
1801
|
fontSize: number;
|
|
1796
|
-
|
|
1802
|
+
boxShadow: string;
|
|
1797
1803
|
};
|
|
1798
1804
|
divider: {
|
|
1799
1805
|
width: number;
|
|
@@ -1804,3 +1810,7 @@ export declare const toolbar: {
|
|
|
1804
1810
|
cursor: string;
|
|
1805
1811
|
};
|
|
1806
1812
|
};
|
|
1813
|
+
export declare const scrollbarCSS: string;
|
|
1814
|
+
export declare const componentCard: {
|
|
1815
|
+
container: React.CSSProperties;
|
|
1816
|
+
};
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
// Shared editor styles - single source of truth for all prefab editor UI
|
|
2
2
|
export const colors = {
|
|
3
|
-
bg: '
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
bg: '#1e1e1e',
|
|
4
|
+
bgSurface: '#252526',
|
|
5
|
+
bgLight: '#2d2d2d',
|
|
6
|
+
bgHover: '#2a2d2e',
|
|
7
|
+
bgInput: '#1a1a1a',
|
|
8
|
+
border: '#3c3c3c',
|
|
9
|
+
borderLight: '#333333',
|
|
10
|
+
borderFaint: '#2a2a2a',
|
|
11
|
+
text: '#cccccc',
|
|
12
|
+
textMuted: '#999999',
|
|
13
|
+
textDim: '#666666',
|
|
14
|
+
accent: '#4c9eff',
|
|
15
|
+
accentBg: 'rgba(76, 158, 255, 0.12)',
|
|
16
|
+
accentBorder: 'rgba(76, 158, 255, 0.4)',
|
|
17
|
+
danger: '#f44747',
|
|
18
|
+
dangerBg: 'rgba(244, 71, 71, 0.12)',
|
|
19
|
+
dangerBorder: 'rgba(244, 71, 71, 0.35)',
|
|
14
20
|
};
|
|
15
21
|
export const fonts = {
|
|
16
|
-
family: 'system-ui, sans-serif',
|
|
22
|
+
family: 'system-ui, -apple-system, sans-serif',
|
|
17
23
|
size: 11,
|
|
18
24
|
sizeSm: 10,
|
|
19
25
|
};
|
|
@@ -24,12 +30,12 @@ export const base = {
|
|
|
24
30
|
color: colors.text,
|
|
25
31
|
border: `1px solid ${colors.border}`,
|
|
26
32
|
borderRadius: 4,
|
|
27
|
-
backdropFilter: 'blur(8px)',
|
|
28
33
|
fontFamily: fonts.family,
|
|
29
34
|
fontSize: fonts.size,
|
|
35
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
30
36
|
},
|
|
31
37
|
header: {
|
|
32
|
-
padding: '
|
|
38
|
+
padding: '7px 10px',
|
|
33
39
|
display: 'flex',
|
|
34
40
|
alignItems: 'center',
|
|
35
41
|
justifyContent: 'space-between',
|
|
@@ -37,22 +43,23 @@ export const base = {
|
|
|
37
43
|
background: colors.bgLight,
|
|
38
44
|
borderBottom: `1px solid ${colors.borderLight}`,
|
|
39
45
|
fontSize: fonts.size,
|
|
40
|
-
fontWeight:
|
|
46
|
+
fontWeight: 600,
|
|
41
47
|
textTransform: 'uppercase',
|
|
42
|
-
letterSpacing: 0.
|
|
48
|
+
letterSpacing: 0.8,
|
|
49
|
+
color: colors.text,
|
|
43
50
|
},
|
|
44
51
|
input: {
|
|
45
52
|
width: '100%',
|
|
46
|
-
background: colors.
|
|
53
|
+
background: colors.bgInput,
|
|
47
54
|
border: `1px solid ${colors.border}`,
|
|
48
55
|
borderRadius: 3,
|
|
49
|
-
padding: '
|
|
56
|
+
padding: '5px 8px',
|
|
50
57
|
color: colors.text,
|
|
51
58
|
fontSize: fonts.size,
|
|
52
59
|
outline: 'none',
|
|
53
60
|
},
|
|
54
61
|
btn: {
|
|
55
|
-
background: colors.
|
|
62
|
+
background: colors.bgLight,
|
|
56
63
|
border: `1px solid ${colors.border}`,
|
|
57
64
|
borderRadius: 3,
|
|
58
65
|
padding: '4px 8px',
|
|
@@ -68,10 +75,11 @@ export const base = {
|
|
|
68
75
|
},
|
|
69
76
|
label: {
|
|
70
77
|
fontSize: fonts.sizeSm,
|
|
71
|
-
|
|
78
|
+
color: colors.textMuted,
|
|
72
79
|
marginBottom: 4,
|
|
73
80
|
textTransform: 'uppercase',
|
|
74
81
|
letterSpacing: 0.5,
|
|
82
|
+
fontWeight: 500,
|
|
75
83
|
},
|
|
76
84
|
row: {
|
|
77
85
|
display: 'flex',
|
|
@@ -100,36 +108,39 @@ export const tree = {
|
|
|
100
108
|
overflowY: 'auto',
|
|
101
109
|
padding: 4,
|
|
102
110
|
scrollbarWidth: 'thin',
|
|
103
|
-
scrollbarColor:
|
|
111
|
+
scrollbarColor: `${colors.bgLight} transparent`,
|
|
104
112
|
},
|
|
105
113
|
row: {
|
|
106
114
|
display: 'flex',
|
|
107
115
|
alignItems: 'center',
|
|
108
116
|
padding: '3px 6px',
|
|
109
|
-
|
|
117
|
+
borderBottomWidth: 1,
|
|
118
|
+
borderBottomStyle: 'solid',
|
|
119
|
+
borderBottomColor: colors.borderFaint,
|
|
110
120
|
cursor: 'pointer',
|
|
111
121
|
whiteSpace: 'nowrap',
|
|
122
|
+
borderRadius: 2,
|
|
112
123
|
},
|
|
113
124
|
selected: {
|
|
114
|
-
background:
|
|
125
|
+
background: colors.accentBg,
|
|
126
|
+
borderBottomColor: colors.accentBorder,
|
|
115
127
|
},
|
|
116
128
|
};
|
|
117
129
|
export const menu = {
|
|
118
130
|
container: {
|
|
119
131
|
position: 'fixed',
|
|
120
132
|
zIndex: 50,
|
|
121
|
-
minWidth:
|
|
122
|
-
background:
|
|
133
|
+
minWidth: 140,
|
|
134
|
+
background: colors.bgSurface,
|
|
123
135
|
border: `1px solid ${colors.border}`,
|
|
124
136
|
borderRadius: 4,
|
|
125
137
|
overflow: 'hidden',
|
|
126
|
-
boxShadow: '0
|
|
127
|
-
backdropFilter: 'blur(8px)',
|
|
138
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
|
128
139
|
},
|
|
129
140
|
item: {
|
|
130
141
|
width: '100%',
|
|
131
142
|
textAlign: 'left',
|
|
132
|
-
padding: '
|
|
143
|
+
padding: '7px 12px',
|
|
133
144
|
background: 'transparent',
|
|
134
145
|
border: 'none',
|
|
135
146
|
color: colors.text,
|
|
@@ -156,14 +167,36 @@ export const toolbar = {
|
|
|
156
167
|
color: colors.text,
|
|
157
168
|
fontFamily: fonts.family,
|
|
158
169
|
fontSize: fonts.size,
|
|
159
|
-
|
|
170
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
160
171
|
},
|
|
161
172
|
divider: {
|
|
162
173
|
width: 1,
|
|
163
|
-
background:
|
|
174
|
+
background: colors.borderLight,
|
|
164
175
|
},
|
|
165
176
|
disabled: {
|
|
166
177
|
opacity: 0.4,
|
|
167
178
|
cursor: 'not-allowed',
|
|
168
179
|
},
|
|
169
180
|
};
|
|
181
|
+
// Shared scrollbar CSS (inject via <style> tag since CSS can't be bundled)
|
|
182
|
+
export const scrollbarCSS = `
|
|
183
|
+
.prefab-scroll::-webkit-scrollbar,
|
|
184
|
+
.tree-scroll::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
185
|
+
.prefab-scroll::-webkit-scrollbar-track,
|
|
186
|
+
.tree-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
187
|
+
.prefab-scroll::-webkit-scrollbar-thumb,
|
|
188
|
+
.tree-scroll::-webkit-scrollbar-thumb { background: ${colors.border}; border-radius: 3px; }
|
|
189
|
+
.prefab-scroll::-webkit-scrollbar-thumb:hover,
|
|
190
|
+
.tree-scroll::-webkit-scrollbar-thumb:hover { background: #555; }
|
|
191
|
+
.prefab-scroll { scrollbar-width: thin; scrollbar-color: ${colors.border} transparent; }
|
|
192
|
+
`;
|
|
193
|
+
// Reusable component card style for inspector sections
|
|
194
|
+
export const componentCard = {
|
|
195
|
+
container: {
|
|
196
|
+
marginBottom: 8,
|
|
197
|
+
backgroundColor: colors.bgSurface,
|
|
198
|
+
padding: 8,
|
|
199
|
+
borderRadius: 4,
|
|
200
|
+
border: `1px solid ${colors.border}`,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
@@ -44,3 +44,7 @@ export declare function regenerateIds(node: GameObject): GameObject;
|
|
|
44
44
|
/** Get component data from a node */
|
|
45
45
|
export declare function getComponent<T = any>(node: GameObject, type: string): T | undefined;
|
|
46
46
|
export declare function updateNodeById(root: GameObject, id: string, updater: (node: GameObject) => GameObject): GameObject;
|
|
47
|
+
/** Create a GameObject node for a 3D model file */
|
|
48
|
+
export declare function createModelNode(filename: string, name?: string): GameObject;
|
|
49
|
+
/** Create a GameObject node for an image as a textured plane */
|
|
50
|
+
export declare function createImageNode(texturePath: string, name?: string): GameObject;
|
|
@@ -149,7 +149,7 @@ export function deleteNode(root, id) {
|
|
|
149
149
|
/** Deep clone a node with new IDs */
|
|
150
150
|
export function cloneNode(node) {
|
|
151
151
|
var _a, _b;
|
|
152
|
-
return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(), name: `${(_a = node.name) !== null && _a !== void 0 ? _a :
|
|
152
|
+
return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(), name: `${(_a = node.name) !== null && _a !== void 0 ? _a : node.id} Copy`, children: (_b = node.children) === null || _b === void 0 ? void 0 : _b.map(cloneNode) });
|
|
153
153
|
}
|
|
154
154
|
/** Recursively update all IDs in a node tree */
|
|
155
155
|
export function regenerateIds(node) {
|
|
@@ -180,3 +180,41 @@ export function updateNodeById(root, id, updater) {
|
|
|
180
180
|
return root;
|
|
181
181
|
return Object.assign(Object.assign({}, root), { children: newChildren });
|
|
182
182
|
}
|
|
183
|
+
/** Create a GameObject node for a 3D model file */
|
|
184
|
+
export function createModelNode(filename, name) {
|
|
185
|
+
return {
|
|
186
|
+
id: crypto.randomUUID(),
|
|
187
|
+
name: name !== null && name !== void 0 ? name : filename.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
|
|
188
|
+
components: {
|
|
189
|
+
transform: {
|
|
190
|
+
type: 'Transform',
|
|
191
|
+
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
192
|
+
},
|
|
193
|
+
model: {
|
|
194
|
+
type: 'Model',
|
|
195
|
+
properties: { filename, instanced: false }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Create a GameObject node for an image as a textured plane */
|
|
201
|
+
export function createImageNode(texturePath, name) {
|
|
202
|
+
return {
|
|
203
|
+
id: crypto.randomUUID(),
|
|
204
|
+
name: name !== null && name !== void 0 ? name : texturePath.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
|
|
205
|
+
components: {
|
|
206
|
+
transform: {
|
|
207
|
+
type: 'Transform',
|
|
208
|
+
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
209
|
+
},
|
|
210
|
+
geometry: {
|
|
211
|
+
type: 'Geometry',
|
|
212
|
+
properties: { geometryType: 'plane', args: [1, 1] }
|
|
213
|
+
},
|
|
214
|
+
material: {
|
|
215
|
+
type: 'Material',
|
|
216
|
+
properties: { color: '#ffffff', texture: texturePath }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import * as THREE from "three/webgpu";
|
|
5
|
+
import {
|
|
6
|
+
float,
|
|
7
|
+
uv,
|
|
8
|
+
vec3,
|
|
9
|
+
smoothstep,
|
|
10
|
+
uniform,
|
|
11
|
+
length,
|
|
12
|
+
} from "three/tsl";
|
|
13
|
+
|
|
14
|
+
interface ContactShadowProps {
|
|
15
|
+
opacity?: number;
|
|
16
|
+
blur?: number;
|
|
17
|
+
scale?: number;
|
|
18
|
+
yOffset?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ContactShadow = ({
|
|
22
|
+
opacity = 0.4,
|
|
23
|
+
blur = 2.5,
|
|
24
|
+
scale = 1.2,
|
|
25
|
+
yOffset = 0.05,
|
|
26
|
+
}: ContactShadowProps) => {
|
|
27
|
+
const material = useMemo(() => {
|
|
28
|
+
const mat = new THREE.MeshBasicNodeMaterial();
|
|
29
|
+
mat.transparent = true;
|
|
30
|
+
mat.depthWrite = false;
|
|
31
|
+
mat.depthTest = true;
|
|
32
|
+
mat.side = THREE.DoubleSide;
|
|
33
|
+
mat.polygonOffset = true;
|
|
34
|
+
mat.polygonOffsetFactor = -1;
|
|
35
|
+
mat.polygonOffsetUnits = -1;
|
|
36
|
+
|
|
37
|
+
const uOpacity = uniform(opacity);
|
|
38
|
+
const uBlur = uniform(blur);
|
|
39
|
+
|
|
40
|
+
// UVs centered around origin
|
|
41
|
+
const centeredUV = uv().sub(0.5).mul(2.0);
|
|
42
|
+
|
|
43
|
+
// IMPORTANT: use functional length(), not .length()
|
|
44
|
+
const dist = length(centeredUV);
|
|
45
|
+
|
|
46
|
+
const innerRadius = float(0.0);
|
|
47
|
+
const outerRadius = float(1.0);
|
|
48
|
+
const blurAmount = uBlur.div(10.0);
|
|
49
|
+
|
|
50
|
+
const alpha = smoothstep(
|
|
51
|
+
outerRadius,
|
|
52
|
+
innerRadius.add(blurAmount),
|
|
53
|
+
dist
|
|
54
|
+
).mul(uOpacity);
|
|
55
|
+
|
|
56
|
+
mat.colorNode = vec3(0.0, 0.0, 0.0);
|
|
57
|
+
mat.opacityNode = alpha;
|
|
58
|
+
|
|
59
|
+
return mat;
|
|
60
|
+
}, [opacity, blur]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<mesh
|
|
64
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
65
|
+
position={[0, yOffset, 0]}
|
|
66
|
+
material={material}
|
|
67
|
+
renderOrder={1}
|
|
68
|
+
>
|
|
69
|
+
<planeGeometry args={[scale, scale]} />
|
|
70
|
+
</mesh>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default ContactShadow;
|
|
@@ -1,54 +1,22 @@
|
|
|
1
1
|
// DragDropLoader.tsx
|
|
2
2
|
import { useEffect, ChangeEvent } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { parseModelFromFile } from "./modelLoader";
|
|
4
4
|
|
|
5
5
|
interface DragDropLoaderProps {
|
|
6
6
|
onModelLoaded: (model: any, filename: string) => void;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
// Shared file handling logic
|
|
10
9
|
function handleFiles(files: File[], onModelLoaded: (model: any, filename: string) => void) {
|
|
11
|
-
files.forEach((file) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
files.forEach(async (file) => {
|
|
11
|
+
const result = await parseModelFromFile(file);
|
|
12
|
+
if (result.success && result.model) {
|
|
13
|
+
onModelLoaded(result.model, file.name);
|
|
14
|
+
} else {
|
|
15
|
+
console.error("Model parse error:", result.error);
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function loadGLTFFile(file: File, onModelLoaded: (model: any, filename: string) => void) {
|
|
21
|
-
const reader = new FileReader();
|
|
22
|
-
reader.onload = (event) => {
|
|
23
|
-
const arrayBuffer = event.target?.result;
|
|
24
|
-
if (arrayBuffer) {
|
|
25
|
-
const loader = new GLTFLoader();
|
|
26
|
-
const dracoLoader = new DRACOLoader();
|
|
27
|
-
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
28
|
-
loader.setDRACOLoader(dracoLoader);
|
|
29
|
-
loader.parse(arrayBuffer as ArrayBuffer, "", (gltf) => {
|
|
30
|
-
onModelLoaded(gltf.scene, file.name);
|
|
31
|
-
}, (error) => {
|
|
32
|
-
console.error("GLTFLoader parse error", error);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
reader.readAsArrayBuffer(file);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function loadFBXFile(file: File, onModelLoaded: (model: any, filename: string) => void) {
|
|
40
|
-
const reader = new FileReader();
|
|
41
|
-
reader.onload = (event) => {
|
|
42
|
-
const arrayBuffer = event.target?.result;
|
|
43
|
-
if (arrayBuffer) {
|
|
44
|
-
const loader = new FBXLoader();
|
|
45
|
-
const model = loader.parse(arrayBuffer as ArrayBuffer, "");
|
|
46
|
-
onModelLoaded(model, file.name);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
reader.readAsArrayBuffer(file);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
20
|
export function DragDropLoader({ onModelLoaded }: DragDropLoaderProps) {
|
|
53
21
|
useEffect(() => {
|
|
54
22
|
function handleDrop(e: DragEvent) {
|
|
@@ -17,6 +17,42 @@ gltfLoader.setDRACOLoader(dracoLoader);
|
|
|
17
17
|
|
|
18
18
|
const fbxLoader = new FBXLoader();
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parse a model from a File object (e.g. from drag-drop or file picker).
|
|
22
|
+
* Returns the parsed Three.js Object3D scene.
|
|
23
|
+
*/
|
|
24
|
+
export function parseModelFromFile(file: File): Promise<ModelLoadResult> {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const reader = new FileReader();
|
|
27
|
+
reader.onload = (event) => {
|
|
28
|
+
const arrayBuffer = event.target?.result as ArrayBuffer;
|
|
29
|
+
if (!arrayBuffer) {
|
|
30
|
+
resolve({ success: false, error: new Error('Failed to read file') });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const name = file.name.toLowerCase();
|
|
34
|
+
if (name.endsWith('.glb') || name.endsWith('.gltf')) {
|
|
35
|
+
gltfLoader.parse(arrayBuffer, '', (gltf) => {
|
|
36
|
+
resolve({ success: true, model: gltf.scene });
|
|
37
|
+
}, (error) => {
|
|
38
|
+
resolve({ success: false, error });
|
|
39
|
+
});
|
|
40
|
+
} else if (name.endsWith('.fbx')) {
|
|
41
|
+
try {
|
|
42
|
+
const model = fbxLoader.parse(arrayBuffer, '');
|
|
43
|
+
resolve({ success: true, model });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
resolve({ success: false, error });
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
resolve({ success: false, error: new Error(`Unsupported file format: ${file.name}`) });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
reader.onerror = () => resolve({ success: false, error: reader.error });
|
|
52
|
+
reader.readAsArrayBuffer(file);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
20
56
|
export async function loadModel(
|
|
21
57
|
filename: string,
|
|
22
58
|
onProgress?: ProgressCallback
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Dispatch, SetStateAction, useState, MouseEvent } from 'react';
|
|
2
2
|
import { Prefab, GameObject } from "./types";
|
|
3
3
|
import { getComponent } from './components/ComponentRegistry';
|
|
4
|
-
import { base, tree, menu } from './styles';
|
|
4
|
+
import { base, colors, tree, menu } from './styles';
|
|
5
5
|
import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
|
|
6
6
|
import { useEditorContext } from './EditorContext';
|
|
7
7
|
|
|
@@ -223,11 +223,6 @@ export default function EditorTree({
|
|
|
223
223
|
|
|
224
224
|
return (
|
|
225
225
|
<>
|
|
226
|
-
<style>{`
|
|
227
|
-
.tree-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
228
|
-
.tree-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
229
|
-
.tree-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
230
|
-
`}</style>
|
|
231
226
|
<div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => { setContextMenu(null); setFileMenuOpen(false); }}>
|
|
232
227
|
<div style={base.header}>
|
|
233
228
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} onClick={() => setCollapsed(!collapsed)}>
|
|
@@ -273,7 +268,7 @@ export default function EditorTree({
|
|
|
273
268
|
</div>
|
|
274
269
|
{!collapsed && (
|
|
275
270
|
<>
|
|
276
|
-
<div style={{ padding: '4px 4px', borderBottom:
|
|
271
|
+
<div style={{ padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }}>
|
|
277
272
|
<input
|
|
278
273
|
type="text"
|
|
279
274
|
placeholder="Search nodes..."
|
|
@@ -281,14 +276,8 @@ export default function EditorTree({
|
|
|
281
276
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
282
277
|
onClick={(e) => e.stopPropagation()}
|
|
283
278
|
style={{
|
|
284
|
-
|
|
279
|
+
...base.input,
|
|
285
280
|
padding: '4px 8px',
|
|
286
|
-
background: 'rgba(255,255,255,0.05)',
|
|
287
|
-
border: '1px solid rgba(255,255,255,0.1)',
|
|
288
|
-
borderRadius: 3,
|
|
289
|
-
color: 'inherit',
|
|
290
|
-
fontSize: 11,
|
|
291
|
-
outline: 'none',
|
|
292
281
|
}}
|
|
293
282
|
/>
|
|
294
283
|
</div>
|
|
@@ -361,7 +350,7 @@ function FileMenu({
|
|
|
361
350
|
|
|
362
351
|
return (
|
|
363
352
|
<div
|
|
364
|
-
style={{ ...menu.container, top: 28, right: 0 }}
|
|
353
|
+
style={{ ...menu.container, position: 'absolute', top: 28, right: 0 }}
|
|
365
354
|
onClick={(e) => e.stopPropagation()}
|
|
366
355
|
>
|
|
367
356
|
<button
|
|
@@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useState, useEffect } from 'react';
|
|
|
2
2
|
import { Prefab, GameObject as GameObjectType } from "./types";
|
|
3
3
|
import EditorTree from './EditorTree';
|
|
4
4
|
import { getAllComponents } from './components/ComponentRegistry';
|
|
5
|
-
import { base, inspector } from './styles';
|
|
5
|
+
import { base, colors, inspector, scrollbarCSS, componentCard } from './styles';
|
|
6
6
|
import { findNode, updateNode, deleteNode } from './utils';
|
|
7
7
|
|
|
8
8
|
function EditorUI({
|
|
@@ -45,12 +45,7 @@ function EditorUI({
|
|
|
45
45
|
const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
|
|
46
46
|
|
|
47
47
|
return <>
|
|
48
|
-
<style>{
|
|
49
|
-
.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
50
|
-
.prefab-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
51
|
-
.prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
52
|
-
.prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
|
|
53
|
-
`}</style>
|
|
48
|
+
<style>{scrollbarCSS}</style>
|
|
54
49
|
<div style={inspector.panel}>
|
|
55
50
|
<div style={base.header} onClick={() => setCollapsed(!collapsed)}>
|
|
56
51
|
<span>Inspector</span>
|
|
@@ -106,7 +101,7 @@ function NodeInspector({
|
|
|
106
101
|
{/* Node Name */}
|
|
107
102
|
<div style={base.section}>
|
|
108
103
|
<div style={{ display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }}>
|
|
109
|
-
<div style={{ fontSize: 10, color:
|
|
104
|
+
<div style={{ fontSize: 10, color: colors.textDim, wordBreak: 'break-all', border: `1px solid ${colors.border}`, padding: '2px 6px', borderRadius: 3, flex: 1, fontFamily: 'monospace' }}>
|
|
110
105
|
{node.id}
|
|
111
106
|
</div>
|
|
112
107
|
<button style={{ ...base.btn, ...base.btnDanger }} title="Delete Node" onClick={deleteNode}>❌</button>
|
|
@@ -131,12 +126,12 @@ function NodeInspector({
|
|
|
131
126
|
{node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
|
|
132
127
|
if (!comp) return null;
|
|
133
128
|
const def = ALL_COMPONENTS[comp.type];
|
|
134
|
-
if (!def) return <div key={key} style={{ color:
|
|
129
|
+
if (!def) return <div key={key} style={{ color: colors.danger, fontSize: 11 }}>
|
|
135
130
|
Unknown: {comp.type}
|
|
136
131
|
</div>;
|
|
137
132
|
|
|
138
133
|
return (
|
|
139
|
-
<div key={key} style={
|
|
134
|
+
<div key={key} style={componentCard.container}>
|
|
140
135
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
|
141
136
|
<div style={{ fontSize: 11, fontWeight: 500 }}>{key}</div>
|
|
142
137
|
<button
|