Skip to content

Commit 99f59ce

Browse files
committed
fix
1 parent 00762eb commit 99f59ce

12 files changed

Lines changed: 586 additions & 56 deletions

File tree

.cursorrules

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
- Promote Single Responsibility Principle, DRY, KISS principles.
55
- Prefix interfaces with I
66
- Use cursor running command tools when needed.
7-
7+
- Dont dump a lot of code into a single, favor small components in separate files to promote SRP
88

99
## CRITICAL RULES
10+
1011
- Stop asking to do changes. DO IT, I trust you. I'll review later.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"zustand": "^5.0.0"
4040
},
4141
"devDependencies": {
42+
"@tailwindcss/vite": "^4.1.4",
4243
"@types/adm-zip": "^0.5.7",
4344
"@types/react": "^19.1.2",
4445
"@types/react-dom": "^19.1.2",

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';
22

3-
import Editor from '@/components/Editor';
3+
import Editor from '@/editor/Editor';
44
import { MainScene } from '@/game/scenes/MainScene';
55

66
/**

src/components/Editor.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/editor/Editor.tsx

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import React, { useCallback, useEffect, useRef, useState } from 'react';
2+
3+
import HierarchyPanel from './components/HierarchyPanel';
4+
import InspectorPanel from './components/InspectorPanel';
5+
import ViewportPanel from './components/ViewportPanel';
6+
import { useLocalStorage } from './components/useLocalStorage';
7+
8+
export interface ITransform {
9+
position: [number, number, number];
10+
rotation: [number, number, number];
11+
scale: [number, number, number];
12+
}
13+
14+
export interface ISceneObject {
15+
id: string;
16+
name: string;
17+
components: {
18+
Transform: ITransform;
19+
Mesh: string;
20+
Material: string;
21+
};
22+
}
23+
24+
const Editor: React.FC = () => {
25+
// Use localStorage for persisting objects
26+
const [objects, setObjects] = useLocalStorage<ISceneObject[]>('editor-scene', []);
27+
const [selectedId, setSelectedId] = useState<string>('');
28+
const [statusMessage, setStatusMessage] = useState<string>('Ready');
29+
const fileInputRef = useRef<HTMLInputElement>(null);
30+
31+
const selectedObject = objects.find((obj) => obj.id === selectedId);
32+
33+
// Select first object if nothing is selected and objects exist
34+
useEffect(() => {
35+
if (!selectedId && objects.length > 0) {
36+
setSelectedId(objects[0].id);
37+
}
38+
}, [selectedId, objects]);
39+
40+
// Add new object for testing
41+
const handleAddObject = () => {
42+
const newId = Date.now().toString();
43+
const newObj: ISceneObject = {
44+
id: newId,
45+
name: `Object${objects.length + 1}`,
46+
components: {
47+
Transform: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] },
48+
Mesh: 'Cube',
49+
Material: 'Default',
50+
},
51+
};
52+
setObjects((prev) => [...prev, newObj]);
53+
setSelectedId(newId);
54+
setStatusMessage(`Added new object: ${newObj.name}`);
55+
};
56+
57+
// Use useCallback to prevent new function references on every render
58+
const handleTransformChange = useCallback(
59+
(transform: {
60+
position: [number, number, number];
61+
rotation: [number, number, number];
62+
scale: [number, number, number];
63+
}) => {
64+
if (!selectedId) return;
65+
66+
setObjects((prev) =>
67+
prev.map((obj) =>
68+
obj.id === selectedId
69+
? {
70+
...obj,
71+
components: {
72+
...obj.components,
73+
Transform: transform,
74+
},
75+
}
76+
: obj,
77+
),
78+
);
79+
},
80+
[selectedId, setObjects],
81+
);
82+
83+
// Save scene as JSON
84+
const handleSave = () => {
85+
const dataStr = JSON.stringify(objects, null, 2);
86+
const blob = new Blob([dataStr], { type: 'application/json' });
87+
const url = URL.createObjectURL(blob);
88+
const a = document.createElement('a');
89+
a.href = url;
90+
a.download = 'scene.json';
91+
a.click();
92+
URL.revokeObjectURL(url);
93+
setStatusMessage('Scene saved to file');
94+
};
95+
96+
// Load scene from JSON
97+
const handleLoad = (e: React.ChangeEvent<HTMLInputElement>) => {
98+
const file = e.target.files?.[0];
99+
if (!file) return;
100+
const reader = new FileReader();
101+
reader.onload = (event) => {
102+
try {
103+
const loaded = JSON.parse(event.target?.result as string);
104+
if (Array.isArray(loaded)) {
105+
setObjects(loaded);
106+
setSelectedId(loaded[0]?.id || '');
107+
setStatusMessage(`Loaded scene with ${loaded.length} objects`);
108+
}
109+
} catch (err) {
110+
alert('Failed to load scene: Invalid JSON');
111+
setStatusMessage('Error: Failed to load scene');
112+
}
113+
};
114+
reader.readAsText(file);
115+
};
116+
117+
// Clear scene
118+
const handleClear = () => {
119+
if (window.confirm('Are you sure you want to clear the scene?')) {
120+
setObjects([]);
121+
setSelectedId('');
122+
setStatusMessage('Scene cleared');
123+
}
124+
};
125+
126+
// Keyboard shortcuts
127+
useEffect(() => {
128+
const handleKeyDown = (e: KeyboardEvent) => {
129+
// Only handle shortcuts when not typing in input fields
130+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
131+
return;
132+
}
133+
134+
// Ctrl+N: New Object
135+
if (e.ctrlKey && e.key === 'n') {
136+
e.preventDefault();
137+
handleAddObject();
138+
}
139+
140+
// Ctrl+S: Save Scene
141+
if (e.ctrlKey && e.key === 's') {
142+
e.preventDefault();
143+
handleSave();
144+
}
145+
146+
// Delete: Remove selected object
147+
if (e.key === 'Delete' && selectedId) {
148+
e.preventDefault();
149+
setObjects((prev) => prev.filter((obj) => obj.id !== selectedId));
150+
setSelectedId('');
151+
setStatusMessage('Object deleted');
152+
}
153+
};
154+
155+
window.addEventListener('keydown', handleKeyDown);
156+
return () => window.removeEventListener('keydown', handleKeyDown);
157+
}, [selectedId, setObjects, handleAddObject, handleSave]);
158+
159+
return (
160+
<div className="w-full h-screen flex flex-col bg-[#232323] text-white">
161+
<header className="p-2 bg-[#1a1a1a] border-b border-[#222] flex items-center shadow-sm justify-between">
162+
<div className="flex items-center">
163+
<h1 className="text-lg font-bold tracking-wide mr-4">Game Editor</h1>
164+
<div className="text-xs bg-blue-900/30 px-2 py-1 rounded text-blue-200">
165+
{objects.length} object{objects.length !== 1 ? 's' : ''}
166+
</div>
167+
</div>
168+
<div className="flex gap-2">
169+
<button
170+
className="px-3 py-1 rounded bg-green-700 hover:bg-green-800 text-xs font-semibold"
171+
onClick={handleAddObject}
172+
title="Add Object (Ctrl+N)"
173+
>
174+
+ Add Object
175+
</button>
176+
<button
177+
className="px-3 py-1 rounded bg-blue-700 hover:bg-blue-800 text-xs font-semibold"
178+
onClick={handleSave}
179+
title="Save Scene (Ctrl+S)"
180+
>
181+
Save Scene
182+
</button>
183+
<button
184+
className="px-3 py-1 rounded bg-gray-700 hover:bg-gray-800 text-xs font-semibold"
185+
onClick={() => fileInputRef.current?.click()}
186+
title="Load Scene"
187+
>
188+
Load Scene
189+
</button>
190+
<button
191+
className="px-3 py-1 rounded bg-red-700 hover:bg-red-800 text-xs font-semibold"
192+
onClick={handleClear}
193+
title="Clear Scene"
194+
>
195+
Clear
196+
</button>
197+
<input
198+
ref={fileInputRef}
199+
type="file"
200+
accept="application/json"
201+
className="hidden"
202+
onChange={handleLoad}
203+
/>
204+
</div>
205+
</header>
206+
<main className="flex-1 flex overflow-hidden">
207+
<HierarchyPanel objects={objects} selectedId={selectedId} setSelectedId={setSelectedId} />
208+
{selectedObject ? (
209+
<>
210+
<ViewportPanel selectedObject={selectedObject} />
211+
<InspectorPanel
212+
selectedObject={selectedObject}
213+
onTransformChange={handleTransformChange}
214+
/>
215+
</>
216+
) : (
217+
<div className="flex-1 flex items-center justify-center text-gray-400 text-lg">
218+
<div className="max-w-md text-center px-4">
219+
<div className="mb-2">No object selected or scene is empty.</div>
220+
<button
221+
className="px-3 py-1 rounded bg-green-700 hover:bg-green-800 text-sm mt-2"
222+
onClick={handleAddObject}
223+
>
224+
Add Object
225+
</button>
226+
</div>
227+
</div>
228+
)}
229+
</main>
230+
<footer className="h-6 bg-[#1a1a1a] border-t border-[#222] flex items-center text-xs px-3 justify-between text-gray-400">
231+
<div>{statusMessage}</div>
232+
<div className="flex gap-3">
233+
<div title="Add Object" className="opacity-70">
234+
Ctrl+N
235+
</div>
236+
<div title="Save Scene" className="opacity-70">
237+
Ctrl+S
238+
</div>
239+
<div title="Delete Object" className="opacity-70">
240+
Delete
241+
</div>
242+
</div>
243+
</footer>
244+
</div>
245+
);
246+
};
247+
248+
export default Editor;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
3+
import type { ISceneObject } from '../Editor';
4+
5+
export interface IHierarchyPanelProps {
6+
objects: ISceneObject[];
7+
selectedId: string;
8+
setSelectedId: (id: string) => void;
9+
}
10+
11+
const HierarchyPanel: React.FC<IHierarchyPanelProps> = ({ objects, selectedId, setSelectedId }) => (
12+
<aside className="w-60 bg-[#23272e] border-r border-[#181a1b] p-2 flex-shrink-0 flex flex-col">
13+
<div className="font-semibold mb-2 text-xs uppercase tracking-wider text-gray-400">
14+
Hierarchy
15+
</div>
16+
<ul className="space-y-1 text-sm">
17+
{objects.map((obj) => (
18+
<li
19+
key={obj.id}
20+
className={`px-2 py-1 rounded cursor-pointer select-none ${obj.id === selectedId ? 'bg-blue-700 text-white' : 'hover:bg-gray-700'}`}
21+
onClick={() => setSelectedId(obj.id)}
22+
>
23+
{obj.name}
24+
</li>
25+
))}
26+
</ul>
27+
</aside>
28+
);
29+
30+
export default HierarchyPanel;

0 commit comments

Comments
 (0)