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.
Files changed (39) hide show
  1. package/dist/shared/ContactShadow.d.ts +8 -0
  2. package/dist/shared/ContactShadow.js +32 -0
  3. package/dist/tools/assetviewer/page.js +1 -1
  4. package/dist/tools/dragdrop/DragDropLoader.js +17 -40
  5. package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
  6. package/dist/tools/dragdrop/modelLoader.js +39 -0
  7. package/dist/tools/prefabeditor/EditorTree.js +3 -16
  8. package/dist/tools/prefabeditor/EditorUI.js +5 -10
  9. package/dist/tools/prefabeditor/PrefabEditor.js +57 -1
  10. package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
  11. package/dist/tools/prefabeditor/PrefabRoot.js +17 -2
  12. package/dist/tools/prefabeditor/components/Input.js +27 -26
  13. package/dist/tools/prefabeditor/components/MaterialComponent.js +9 -2
  14. package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
  15. package/dist/tools/prefabeditor/components/PhysicsComponent.js +1 -1
  16. package/dist/tools/prefabeditor/components/SpotLightComponent.js +3 -0
  17. package/dist/tools/prefabeditor/components/TransformComponent.js +13 -11
  18. package/dist/tools/prefabeditor/styles.d.ts +12 -2
  19. package/dist/tools/prefabeditor/styles.js +63 -30
  20. package/dist/tools/prefabeditor/utils.d.ts +4 -0
  21. package/dist/tools/prefabeditor/utils.js +39 -1
  22. package/package.json +1 -1
  23. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
  24. package/src/shared/ContactShadow.tsx +74 -0
  25. package/src/tools/assetviewer/page.tsx +1 -1
  26. package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
  27. package/src/tools/dragdrop/modelLoader.ts +36 -0
  28. package/src/tools/prefabeditor/EditorTree.tsx +4 -15
  29. package/src/tools/prefabeditor/EditorUI.tsx +5 -10
  30. package/src/tools/prefabeditor/PrefabEditor.tsx +60 -1
  31. package/src/tools/prefabeditor/PrefabRoot.tsx +21 -2
  32. package/src/tools/prefabeditor/components/Input.tsx +27 -26
  33. package/src/tools/prefabeditor/components/MaterialComponent.tsx +14 -5
  34. package/src/tools/prefabeditor/components/ModelComponent.tsx +2 -2
  35. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +1 -1
  36. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +4 -0
  37. package/src/tools/prefabeditor/components/TransformComponent.tsx +17 -11
  38. package/src/tools/prefabeditor/styles.ts +65 -30
  39. 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: '2px 6px',
6
- background: 'transparent',
7
- color: 'rgba(255,255,255,0.9)',
8
- border: '1px solid rgba(255,255,255,0.14)',
9
- borderRadius: 4,
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 ? 'rgba(255,255,255,0.10)' : 'transparent' }), onPointerEnter: (e) => {
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 = 'rgba(255,255,255,0.08)';
21
+ e.currentTarget.style.background = colors.bgHover;
20
22
  }, onPointerLeave: (e) => {
21
23
  if (!isActive)
22
- e.currentTarget.style.background = 'transparent';
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 ? 'rgba(255,255,255,0.10)' : 'transparent', width: '100%' }), onPointerEnter: (e) => {
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 = 'rgba(255,255,255,0.08)';
28
+ e.currentTarget.style.background = colors.bgHover;
27
29
  }, onPointerLeave: (e) => {
28
30
  if (snapResolution === 0)
29
- e.currentTarget.style.background = 'transparent';
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
- backdropFilter: string;
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: '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)',
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: '6px 8px',
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: 500,
46
+ fontWeight: 600,
41
47
  textTransform: 'uppercase',
42
- letterSpacing: 0.5,
48
+ letterSpacing: 0.8,
49
+ color: colors.text,
43
50
  },
44
51
  input: {
45
52
  width: '100%',
46
- background: colors.bgHover,
53
+ background: colors.bgInput,
47
54
  border: `1px solid ${colors.border}`,
48
55
  borderRadius: 3,
49
- padding: '4px 6px',
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.bgHover,
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
- opacity: 0.7,
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: 'rgba(255,255,255,0.06) transparent',
111
+ scrollbarColor: `${colors.bgLight} transparent`,
104
112
  },
105
113
  row: {
106
114
  display: 'flex',
107
115
  alignItems: 'center',
108
116
  padding: '3px 6px',
109
- borderBottom: `1px solid ${colors.borderFaint}`,
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: 'rgba(255,255,255,0.12)',
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: 120,
122
- background: 'rgba(0,0,0,0.85)',
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 8px 24px rgba(0,0,0,0.5)',
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: '6px 8px',
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
- backdropFilter: 'blur(8px)',
170
+ boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
160
171
  },
161
172
  divider: {
162
173
  width: 1,
163
- background: 'rgba(255,255,255,0.2)',
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 : "Node"} Copy`, children: (_b = node.children) === null || _b === void 0 ? void 0 : _b.map(cloneNode) });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -0,0 +1,6 @@
1
+ For large scenes, bake the shadows.
2
+
3
+ // trigger a shadow update when needed
4
+ envMesh.castShadow = true;
5
+ directionalLight.current.shadow.autoUpdate = false;
6
+ directionalLight.current.shadow.needsUpdate = true;
@@ -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;
@@ -377,7 +377,7 @@ export function SharedCanvas() {
377
377
  dpr={[1, 1.5]}
378
378
  camera={{ position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }}
379
379
  style={{
380
- position: 'absolute',
380
+ position: 'fixed',
381
381
  top: 0,
382
382
  left: 0,
383
383
  width: '100vw',
@@ -1,54 +1,22 @@
1
1
  // DragDropLoader.tsx
2
2
  import { useEffect, ChangeEvent } from "react";
3
- import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
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
- if (file.name.endsWith(".glb") || file.name.endsWith(".gltf")) {
13
- loadGLTFFile(file, onModelLoaded);
14
- } else if (file.name.endsWith(".fbx")) {
15
- loadFBXFile(file, onModelLoaded);
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: '1px solid rgba(255,255,255,0.1)' }}>
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
- width: '100%',
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: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }}>
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: '#ff8888', fontSize: 11 }}>
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={{ marginBottom: 8, backgroundColor: 'rgba(255, 255, 255, 0.1)', padding: 8, borderRadius: 4 }}>
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