From fb7a1b128296e6b58e45e2d7819d5cc9bb6dd0ba Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 15 Oct 2025 17:15:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AF=E5=8A=A8=E6=80=81=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 13 +- packages/editor-app/package.json | 2 + packages/editor-app/src-tauri/Cargo.lock | 7 + packages/editor-app/src-tauri/Cargo.toml | 2 +- packages/editor-app/src-tauri/src/main.rs | 69 ++- packages/editor-app/src-tauri/tauri.conf.json | 8 +- packages/editor-app/src/App.tsx | 97 ++-- packages/editor-app/src/api/tauri.ts | 7 + .../src/components/AddComponent.tsx | 15 +- .../src/components/EntityInspector.tsx | 44 +- .../src/components/PropertyInspector.tsx | 511 +++++++++++++++--- .../src/components/SceneHierarchy.tsx | 12 +- .../example-components/RigidBodyComponent.ts | 8 - .../src/example-components/SpriteComponent.ts | 8 - .../example-components/TransformComponent.ts | 9 - packages/editor-app/src/hooks/useLocale.ts | 14 +- packages/editor-app/src/main.tsx | 1 + packages/editor-app/src/styles/App.css | 195 ++++--- .../editor-app/src/styles/DockContainer.css | 10 +- .../editor-app/src/styles/EntityInspector.css | 250 ++++++--- .../src/styles/PropertyInspector.css | 377 +++++++++++-- .../editor-app/src/styles/ResizablePanel.css | 20 +- .../editor-app/src/styles/SceneHierarchy.css | 107 +++- packages/editor-app/src/styles/TabPanel.css | 73 ++- .../editor-app/src/styles/design-tokens.css | 122 +++++ packages/editor-app/src/styles/global.css | 148 +++++ packages/editor-app/vite.config.ts | 334 +++++++++++- .../src/Services/ComponentLoaderService.ts | 56 +- .../src/Services/ComponentRegistry.ts | 9 +- .../src/Services/PropertyMetadata.ts | 2 +- 30 files changed, 2069 insertions(+), 461 deletions(-) delete mode 100644 packages/editor-app/src/example-components/RigidBodyComponent.ts delete mode 100644 packages/editor-app/src/example-components/SpriteComponent.ts delete mode 100644 packages/editor-app/src/example-components/TransformComponent.ts create mode 100644 packages/editor-app/src/styles/design-tokens.css create mode 100644 packages/editor-app/src/styles/global.css diff --git a/package-lock.json b/package-lock.json index 6126fe62..d89dff8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10461,7 +10461,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -11114,6 +11114,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.545.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", + "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -16006,6 +16015,8 @@ "@esengine/editor-core": "file:../editor-core", "@tauri-apps/api": "^2.2.0", "@tauri-apps/plugin-shell": "^2.0.0", + "json5": "^2.2.3", + "lucide-react": "^0.545.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/packages/editor-app/package.json b/packages/editor-app/package.json index b01ce478..5c74e90b 100644 --- a/packages/editor-app/package.json +++ b/packages/editor-app/package.json @@ -17,6 +17,8 @@ "@esengine/editor-core": "file:../editor-core", "@tauri-apps/api": "^2.2.0", "@tauri-apps/plugin-shell": "^2.0.0", + "json5": "^2.2.3", + "lucide-react": "^0.545.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/packages/editor-app/src-tauri/Cargo.lock b/packages/editor-app/src-tauri/Cargo.lock index dba1d8eb..203640a1 100644 --- a/packages/editor-app/src-tauri/Cargo.lock +++ b/packages/editor-app/src-tauri/Cargo.lock @@ -1385,6 +1385,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -3592,6 +3598,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", diff --git a/packages/editor-app/src-tauri/Cargo.toml b/packages/editor-app/src-tauri/Cargo.toml index 9fe0c8da..c9211592 100644 --- a/packages/editor-app/src-tauri/Cargo.toml +++ b/packages/editor-app/src-tauri/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2.0", features = [] } [dependencies] -tauri = { version = "2.0", features = [] } +tauri = { version = "2.0", features = ["protocol-asset"] } tauri-plugin-shell = "2.0" tauri-plugin-dialog = "2.0" serde = { version = "1", features = ["derive"] } diff --git a/packages/editor-app/src-tauri/src/main.rs b/packages/editor-app/src-tauri/src/main.rs index 474db795..970acff1 100644 --- a/packages/editor-app/src-tauri/src/main.rs +++ b/packages/editor-app/src-tauri/src/main.rs @@ -3,6 +3,8 @@ use tauri::Manager; use tauri::AppHandle; +use std::sync::{Arc, Mutex}; +use std::collections::HashMap; // IPC Commands #[tauri::command] @@ -143,17 +145,77 @@ fn list_directory(path: String) -> Result, String> { Ok(entries) } +#[tauri::command] +fn set_project_base_path( + path: String, + state: tauri::State>>> +) -> Result<(), String> { + let mut paths = state.lock().map_err(|e| format!("Failed to lock state: {}", e))?; + paths.insert("current".to_string(), path); + Ok(()) +} + fn main() { + let project_paths: Arc>> = Arc::new(Mutex::new(HashMap::new())); + let project_paths_clone = Arc::clone(&project_paths); + tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) - .setup(|app| { - // 应用启动时的初始化逻辑 + .register_uri_scheme_protocol("project", move |_app, request| { + let project_paths = Arc::clone(&project_paths_clone); + + let uri = request.uri(); + let path = uri.path(); + + let file_path = { + let paths = project_paths.lock().unwrap(); + if let Some(base_path) = paths.get("current") { + format!("{}{}", base_path, path) + } else { + return tauri::http::Response::builder() + .status(404) + .body(Vec::new()) + .unwrap(); + } + }; + + match std::fs::read(&file_path) { + Ok(content) => { + let mime_type = if file_path.ends_with(".ts") || file_path.ends_with(".tsx") { + "application/javascript" + } else if file_path.ends_with(".js") { + "application/javascript" + } else if file_path.ends_with(".json") { + "application/json" + } else { + "text/plain" + }; + + tauri::http::Response::builder() + .status(200) + .header("Content-Type", mime_type) + .header("Access-Control-Allow-Origin", "*") + .body(content) + .unwrap() + } + Err(e) => { + eprintln!("Failed to read file {}: {}", file_path, e); + tauri::http::Response::builder() + .status(404) + .body(Vec::new()) + .unwrap() + } + } + }) + .setup(move |app| { #[cfg(debug_assertions)] { let window = app.get_webview_window("main").unwrap(); window.open_devtools(); } + + app.manage(project_paths); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -164,7 +226,8 @@ fn main() { open_project_dialog, scan_directory, read_file_content, - list_directory + list_directory, + set_project_base_path ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/packages/editor-app/src-tauri/tauri.conf.json b/packages/editor-app/src-tauri/tauri.conf.json index 786d5e07..f28f434a 100644 --- a/packages/editor-app/src-tauri/tauri.conf.json +++ b/packages/editor-app/src-tauri/tauri.conf.json @@ -29,7 +29,13 @@ } ], "security": { - "csp": null + "csp": null, + "assetProtocol": { + "enable": true, + "scope": { + "allow": ["**"] + } + } } }, "plugins": { diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index a834c2a4..44b17052 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { Core, Scene } from '@esengine/ecs-framework'; -import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, PropertyMetadataService, ProjectService, ComponentDiscoveryService, ComponentLoaderService } from '@esengine/editor-core'; +import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, ComponentLoaderService, PropertyMetadataService } from '@esengine/editor-core'; import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin'; import { StartupPage } from './components/StartupPage'; import { SceneHierarchy } from './components/SceneHierarchy'; @@ -8,11 +8,9 @@ import { EntityInspector } from './components/EntityInspector'; import { AssetBrowser } from './components/AssetBrowser'; import { DockContainer, DockablePanel } from './components/DockContainer'; import { TauriAPI } from './api/tauri'; -import { TransformComponent } from './example-components/TransformComponent'; -import { SpriteComponent } from './example-components/SpriteComponent'; -import { RigidBodyComponent } from './example-components/RigidBodyComponent'; import { useLocale } from './hooks/useLocale'; import { en, zh } from './locales'; +import { Plus, Trash2, Loader2, Globe } from 'lucide-react'; import './styles/App.css'; const coreInstance = Core.create({ debug: true }); @@ -22,36 +20,6 @@ localeService.registerTranslations('en', en); localeService.registerTranslations('zh', zh); Core.services.registerInstance(LocaleService, localeService); -const propertyMetadata = new PropertyMetadataService(); -Core.services.registerInstance(PropertyMetadataService, propertyMetadata); - -propertyMetadata.register(TransformComponent, { - properties: { - x: { type: 'number', label: 'X Position' }, - y: { type: 'number', label: 'Y Position' }, - rotation: { type: 'number', label: 'Rotation', min: 0, max: 360 }, - scaleX: { type: 'number', label: 'Scale X', min: 0, step: 0.1 }, - scaleY: { type: 'number', label: 'Scale Y', min: 0, step: 0.1 } - } -}); - -propertyMetadata.register(SpriteComponent, { - properties: { - texturePath: { type: 'string', label: 'Texture Path' }, - color: { type: 'color', label: 'Tint Color' }, - visible: { type: 'boolean', label: 'Visible' } - } -}); - -propertyMetadata.register(RigidBodyComponent, { - properties: { - mass: { type: 'number', label: 'Mass', min: 0, step: 0.1 }, - friction: { type: 'number', label: 'Friction', min: 0, max: 1, step: 0.01 }, - restitution: { type: 'number', label: 'Restitution', min: 0, max: 1, step: 0.01 }, - isDynamic: { type: 'boolean', label: 'Dynamic' } - } -}); - function App() { const [initialized, setInitialized] = useState(false); const [projectLoaded, setProjectLoaded] = useState(false); @@ -66,6 +34,8 @@ function App() { useEffect(() => { const initializeEditor = async () => { try { + (window as any).__ECS_FRAMEWORK__ = await import('@esengine/ecs-framework'); + const editorScene = new Scene(); Core.setScene(editorScene); @@ -77,27 +47,7 @@ function App() { const projectService = new ProjectService(messageHub); const componentDiscovery = new ComponentDiscoveryService(messageHub); const componentLoader = new ComponentLoaderService(messageHub, componentRegistry); - - componentRegistry.register({ - name: 'Transform', - type: TransformComponent, - category: 'Transform', - description: 'Position, rotation and scale' - }); - - componentRegistry.register({ - name: 'Sprite', - type: SpriteComponent, - category: 'Rendering', - description: 'Sprite renderer' - }); - - componentRegistry.register({ - name: 'RigidBody', - type: RigidBodyComponent, - category: 'Physics', - description: 'Physics body' - }); + const propertyMetadata = new PropertyMetadataService(); Core.services.registerInstance(UIRegistry, uiRegistry); Core.services.registerInstance(MessageHub, messageHub); @@ -107,6 +57,7 @@ function App() { Core.services.registerInstance(ProjectService, projectService); Core.services.registerInstance(ComponentDiscoveryService, componentDiscovery); Core.services.registerInstance(ComponentLoaderService, componentLoader); + Core.services.registerInstance(PropertyMetadataService, propertyMetadata); const pluginMgr = new EditorPluginManager(); pluginMgr.initialize(coreInstance, Core.services); @@ -145,6 +96,13 @@ function App() { } await projectService.openProject(projectPath); + + await fetch('/@user-project-set-path', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: projectPath }) + }); + setStatus('Scanning components...'); const componentsPath = projectService.getComponentsPath(); @@ -158,9 +116,14 @@ function App() { setStatus(`Loading ${componentInfos.length} components...`); - await loaderService.loadComponents(componentInfos); + const modulePathTransform = (filePath: string) => { + const relativePath = filePath.replace(projectPath, '').replace(/\\/g, '/'); + return `/@user-project${relativePath}`; + }; - setStatus(t('header.status.projectOpened') + ` (${componentInfos.length} components loaded)`); + await loaderService.loadComponents(componentInfos, modulePathTransform); + + setStatus(t('header.status.projectOpened') + ` (${componentInfos.length} components registered)`); } else { setStatus(t('header.status.projectOpened')); } @@ -202,7 +165,7 @@ function App() { useEffect(() => { if (projectLoaded && entityStore && messageHub) { - const defaultPanels: DockablePanel[] = [ + setPanels([ { id: 'scene-hierarchy', title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', @@ -252,14 +215,22 @@ function App() { ), closable: false } - ]; - setPanels(defaultPanels); + ]); } }, [projectLoaded, entityStore, messageHub, locale, currentProjectPath, t]); + const handlePanelMove = (panelId: string, newPosition: any) => { + setPanels(prevPanels => + prevPanels.map(panel => + panel.id === panelId ? { ...panel, position: newPosition } : panel + ) + ); + }; + if (!initialized) { return (
+

Loading Editor...

); @@ -282,20 +253,22 @@ function App() {

{t('app.title')}

{status}
- +
diff --git a/packages/editor-app/src/api/tauri.ts b/packages/editor-app/src/api/tauri.ts index 13e5b440..5f834f7c 100644 --- a/packages/editor-app/src/api/tauri.ts +++ b/packages/editor-app/src/api/tauri.ts @@ -56,6 +56,13 @@ export class TauriAPI { static async listDirectory(path: string): Promise { return await invoke('list_directory', { path }); } + + /** + * 设置项目基础路径,用于 Custom Protocol + */ + static async setProjectBasePath(path: string): Promise { + return await invoke('set_project_base_path', { path }); + } } export interface DirectoryEntry { diff --git a/packages/editor-app/src/components/AddComponent.tsx b/packages/editor-app/src/components/AddComponent.tsx index 8e0ab29e..086cbe13 100644 --- a/packages/editor-app/src/components/AddComponent.tsx +++ b/packages/editor-app/src/components/AddComponent.tsx @@ -16,13 +16,26 @@ export function AddComponent({ entity, componentRegistry, onAdd, onCancel }: Add const [filter, setFilter] = useState(''); useEffect(() => { + if (!componentRegistry) { + console.error('ComponentRegistry is null'); + return; + } + const allComponents = componentRegistry.getAllComponents(); + console.log('All registered components:', allComponents); + + allComponents.forEach(comp => { + console.log(`Component ${comp.name}: has type = ${!!comp.type}`); + }); + const existingComponentNames = entity.components.map(c => c.constructor.name); const availableComponents = allComponents.filter( - comp => !existingComponentNames.includes(comp.name) + comp => comp.type && !existingComponentNames.includes(comp.name) ); + console.log('Available components to add:', availableComponents); + console.log('Components filtered out:', allComponents.filter(comp => !comp.type).map(c => c.name)); setComponents(availableComponents); }, [entity, componentRegistry]); diff --git a/packages/editor-app/src/components/EntityInspector.tsx b/packages/editor-app/src/components/EntityInspector.tsx index 359ab432..b0bf94d4 100644 --- a/packages/editor-app/src/components/EntityInspector.tsx +++ b/packages/editor-app/src/components/EntityInspector.tsx @@ -3,6 +3,7 @@ import { Entity, Core } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core'; import { AddComponent } from './AddComponent'; import { PropertyInspector } from './PropertyInspector'; +import { FileSearch, Plus, ChevronDown, ChevronRight, X, Settings } from 'lucide-react'; import '../styles/EntityInspector.css'; interface EntityInspectorProps { @@ -37,11 +38,16 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit return; } + console.log('Attempting to create component:', componentName); const component = componentRegistry.createInstance(componentName); + console.log('Created component:', component); + if (component) { selectedEntity.addComponent(component); messageHub.publish('component:added', { entity: selectedEntity, component }); setShowAddComponent(false); + } else { + console.error('Failed to create component instance for:', componentName); } }; @@ -80,10 +86,15 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit return (
+

Inspector

-
No entity selected
+
+ +
No entity selected
+
Select an entity from the hierarchy
+
); @@ -94,11 +105,15 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit return (
+

Inspector

-
+
-
Entity Info
+
+ + Entity Info +
ID: @@ -117,44 +132,47 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
+ Components ({components.length})
{components.length === 0 ? ( -
No components
+
No components
) : (
    {components.map((component, index) => { const isExpanded = expandedComponents.has(index); return ( -
  • -
    +
  • +
    toggleComponentExpanded(index)}> - 🔧 + {component.constructor.name}
    {isExpanded && ( -
    +
    handlePropertyChange(component, propertyName, value)} diff --git a/packages/editor-app/src/components/PropertyInspector.tsx b/packages/editor-app/src/components/PropertyInspector.tsx index 6f44f48d..eef7ed04 100644 --- a/packages/editor-app/src/components/PropertyInspector.tsx +++ b/packages/editor-app/src/components/PropertyInspector.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Component, Core } from '@esengine/ecs-framework'; import { PropertyMetadataService, PropertyMetadata } from '@esengine/editor-core'; +import { ChevronRight, ChevronDown } from 'lucide-react'; import '../styles/PropertyInspector.css'; interface PropertyInspectorProps { @@ -48,97 +49,83 @@ export function PropertyInspector({ component, onChange }: PropertyInspectorProp switch (metadata.type) { case 'number': return ( -
    - - handleChange(propertyName, parseFloat(e.target.value) || 0)} - /> -
    + handleChange(propertyName, newValue)} + /> ); case 'string': return ( -
    - - handleChange(propertyName, e.target.value)} - /> -
    + handleChange(propertyName, newValue)} + /> ); case 'boolean': return ( -
    - - handleChange(propertyName, e.target.checked)} - /> -
    + handleChange(propertyName, newValue)} + /> ); case 'color': return ( -
    - - handleChange(propertyName, e.target.value)} - /> -
    + handleChange(propertyName, newValue)} + /> ); case 'vector2': + return ( + handleChange(propertyName, newValue)} + /> + ); + case 'vector3': return ( -
    - -
    - handleChange(propertyName, { ...value, x: parseFloat(e.target.value) || 0 })} - /> - handleChange(propertyName, { ...value, y: parseFloat(e.target.value) || 0 })} - /> - {metadata.type === 'vector3' && ( - handleChange(propertyName, { ...value, z: parseFloat(e.target.value) || 0 })} - /> - )} -
    -
    + handleChange(propertyName, newValue)} + /> + ); + + case 'enum': + return ( + handleChange(propertyName, newValue)} + /> ); default: @@ -154,3 +141,377 @@ export function PropertyInspector({ component, onChange }: PropertyInspectorProp
    ); } + +interface NumberFieldProps { + label: string; + value: number; + min?: number; + max?: number; + step?: number; + readOnly?: boolean; + onChange: (value: number) => void; +} + +function NumberField({ label, value, min, max, step = 0.1, readOnly, onChange }: NumberFieldProps) { + const [isDragging, setIsDragging] = useState(false); + const [dragStartX, setDragStartX] = useState(0); + const [dragStartValue, setDragStartValue] = useState(0); + const inputRef = useRef(null); + + const handleMouseDown = (e: React.MouseEvent) => { + if (readOnly) return; + setIsDragging(true); + setDragStartX(e.clientX); + setDragStartValue(value); + e.preventDefault(); + }; + + useEffect(() => { + if (!isDragging) return; + + const handleMouseMove = (e: MouseEvent) => { + const delta = e.clientX - dragStartX; + const sensitivity = e.shiftKey ? 0.1 : 1; + let newValue = dragStartValue + delta * step * sensitivity; + + if (min !== undefined) newValue = Math.max(min, newValue); + if (max !== undefined) newValue = Math.min(max, newValue); + + onChange(parseFloat(newValue.toFixed(3))); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, dragStartX, dragStartValue, step, min, max, onChange]); + + return ( +
    + + onChange(parseFloat(e.target.value) || 0)} + onFocus={(e) => e.target.select()} + /> +
    + ); +} + +interface StringFieldProps { + label: string; + value: string; + readOnly?: boolean; + onChange: (value: string) => void; +} + +function StringField({ label, value, readOnly, onChange }: StringFieldProps) { + return ( +
    + + onChange(e.target.value)} + onFocus={(e) => e.target.select()} + /> +
    + ); +} + +interface BooleanFieldProps { + label: string; + value: boolean; + readOnly?: boolean; + onChange: (value: boolean) => void; +} + +function BooleanField({ label, value, readOnly, onChange }: BooleanFieldProps) { + return ( +
    + + +
    + ); +} + +interface ColorFieldProps { + label: string; + value: string; + readOnly?: boolean; + onChange: (value: string) => void; +} + +function ColorField({ label, value, readOnly, onChange }: ColorFieldProps) { + return ( +
    + +
    +
    + onChange(e.target.value)} + /> + onChange(e.target.value)} + onFocus={(e) => e.target.select()} + /> +
    +
    + ); +} + +interface Vector2FieldProps { + label: string; + value: { x: number; y: number }; + readOnly?: boolean; + onChange: (value: { x: number; y: number }) => void; +} + +function Vector2Field({ label, value, readOnly, onChange }: Vector2FieldProps) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
    +
    + + +
    + {isExpanded ? ( +
    +
    + X + onChange({ ...value, x: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + Y + onChange({ ...value, y: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + ) : ( +
    +
    + X + onChange({ ...value, x: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + Y + onChange({ ...value, y: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + )} +
    + ); +} + +interface Vector3FieldProps { + label: string; + value: { x: number; y: number; z: number }; + readOnly?: boolean; + onChange: (value: { x: number; y: number; z: number }) => void; +} + +function Vector3Field({ label, value, readOnly, onChange }: Vector3FieldProps) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
    +
    + + +
    + {isExpanded ? ( +
    +
    + X + onChange({ ...value, x: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + Y + onChange({ ...value, y: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + Z + onChange({ ...value, z: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + ) : ( +
    +
    + X + onChange({ ...value, x: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + Y + onChange({ ...value, y: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + Z + onChange({ ...value, z: parseFloat(e.target.value) || 0 })} + onFocus={(e) => e.target.select()} + /> +
    +
    + )} +
    + ); +} + +interface EnumFieldProps { + label: string; + value: any; + options: Array<{ label: string; value: any }>; + readOnly?: boolean; + onChange: (value: any) => void; +} + +function EnumField({ label, value, options, readOnly, onChange }: EnumFieldProps) { + return ( +
    + + +
    + ); +} diff --git a/packages/editor-app/src/components/SceneHierarchy.tsx b/packages/editor-app/src/components/SceneHierarchy.tsx index 0d3ceee7..cda2457b 100644 --- a/packages/editor-app/src/components/SceneHierarchy.tsx +++ b/packages/editor-app/src/components/SceneHierarchy.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { Entity } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import { useLocale } from '../hooks/useLocale'; +import { Box, Layers } from 'lucide-react'; import '../styles/SceneHierarchy.css'; interface SceneHierarchyProps { @@ -45,11 +46,16 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) return (
    +

    {t('hierarchy.title')}

    -
    +
    {entities.length === 0 ? ( -
    {t('hierarchy.empty')}
    +
    + +
    {t('hierarchy.empty')}
    +
    Create an entity to get started
    +
    ) : (
      {entities.map(entity => ( @@ -58,7 +64,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) className={`entity-item ${selectedId === entity.id ? 'selected' : ''}`} onClick={() => handleEntityClick(entity)} > - 📦 + Entity {entity.id} ))} diff --git a/packages/editor-app/src/example-components/RigidBodyComponent.ts b/packages/editor-app/src/example-components/RigidBodyComponent.ts deleted file mode 100644 index eb06622e..00000000 --- a/packages/editor-app/src/example-components/RigidBodyComponent.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; - -export class RigidBodyComponent extends Component { - public mass: number = 1; - public velocityX: number = 0; - public velocityY: number = 0; - public gravity: boolean = true; -} diff --git a/packages/editor-app/src/example-components/SpriteComponent.ts b/packages/editor-app/src/example-components/SpriteComponent.ts deleted file mode 100644 index b7acd478..00000000 --- a/packages/editor-app/src/example-components/SpriteComponent.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; - -export class SpriteComponent extends Component { - public texture: string = ''; - public width: number = 100; - public height: number = 100; - public alpha: number = 1; -} diff --git a/packages/editor-app/src/example-components/TransformComponent.ts b/packages/editor-app/src/example-components/TransformComponent.ts deleted file mode 100644 index 835651b0..00000000 --- a/packages/editor-app/src/example-components/TransformComponent.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; - -export class TransformComponent extends Component { - public x: number = 0; - public y: number = 0; - public rotation: number = 0; - public scaleX: number = 1; - public scaleY: number = 1; -} diff --git a/packages/editor-app/src/hooks/useLocale.ts b/packages/editor-app/src/hooks/useLocale.ts index 5f3d6c3c..46fbf1ff 100644 --- a/packages/editor-app/src/hooks/useLocale.ts +++ b/packages/editor-app/src/hooks/useLocale.ts @@ -1,10 +1,10 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { Core } from '@esengine/ecs-framework'; import { LocaleService, type Locale } from '@esengine/editor-core'; export function useLocale() { - const localeService = Core.services.resolve(LocaleService); - const [locale, setLocale] = useState(localeService.getCurrentLocale()); + const localeService = useMemo(() => Core.services.resolve(LocaleService), []); + const [locale, setLocale] = useState(() => localeService.getCurrentLocale()); useEffect(() => { const unsubscribe = localeService.onChange((newLocale) => { @@ -14,13 +14,13 @@ export function useLocale() { return unsubscribe; }, [localeService]); - const t = (key: string, fallback?: string) => { + const t = useCallback((key: string, fallback?: string) => { return localeService.t(key, fallback); - }; + }, [localeService]); - const changeLocale = (newLocale: Locale) => { + const changeLocale = useCallback((newLocale: Locale) => { localeService.setLocale(newLocale); - }; + }, [localeService]); return { locale, diff --git a/packages/editor-app/src/main.tsx b/packages/editor-app/src/main.tsx index 42c1f750..42f8509d 100644 --- a/packages/editor-app/src/main.tsx +++ b/packages/editor-app/src/main.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import './styles/global.css'; import './styles/index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/packages/editor-app/src/styles/App.css b/packages/editor-app/src/styles/App.css index 5860fc78..ad4415e2 100644 --- a/packages/editor-app/src/styles/App.css +++ b/packages/editor-app/src/styles/App.css @@ -1,58 +1,140 @@ +.editor-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: var(--color-bg-base); + color: var(--color-text-secondary); +} + +.editor-loading h2 { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-medium); + margin-top: var(--spacing-md); +} + .editor-container { display: flex; flex-direction: column; width: 100%; height: 100%; - background-color: #1e1e1e; - color: #cccccc; + background-color: var(--color-bg-base); + color: var(--color-text-primary); } .editor-header { display: flex; - justify-content: space-between; align-items: center; - padding: 8px 16px; - background-color: #2d2d2d; - border-bottom: 1px solid #3e3e3e; - gap: 16px; + gap: var(--spacing-lg); + height: var(--layout-header-height); + padding: 0 var(--spacing-lg); + background-color: var(--color-bg-elevated); + border-bottom: 1px solid var(--color-border-default); + flex-shrink: 0; } .editor-header h1 { - font-size: 16px; - font-weight: 600; + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); margin: 0; + letter-spacing: -0.01em; } .header-toolbar { display: flex; - gap: 8px; + align-items: center; + gap: var(--spacing-sm); flex: 1; } .toolbar-btn { - padding: 6px 12px; - background-color: #0e639c; - color: #ffffff; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-xs); + height: var(--size-button-sm); + padding: 0 var(--spacing-md); + background-color: var(--color-primary); + color: var(--color-text-inverse); border: none; - border-radius: 3px; - font-size: 13px; + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); cursor: pointer; - transition: background-color 0.2s; + transition: all var(--transition-fast); + user-select: none; } .toolbar-btn:hover:not(:disabled) { - background-color: #1177bb; + background-color: var(--color-primary-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.toolbar-btn:active:not(:disabled) { + background-color: var(--color-primary-active); + transform: translateY(0); + box-shadow: var(--shadow-inner); } .toolbar-btn:disabled { - background-color: #3c3c3c; - color: #858585; + background-color: var(--color-bg-input); + color: var(--color-text-disabled); cursor: not-allowed; + opacity: 0.6; +} + +.toolbar-btn:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.toolbar-btn svg { + width: var(--size-icon-sm); + height: var(--size-icon-sm); +} + +.locale-btn { + width: var(--size-button-sm); + padding: 0; + background-color: var(--color-bg-overlay); + color: var(--color-text-primary); + font-weight: var(--font-weight-semibold); +} + +.locale-btn:hover:not(:disabled) { + background-color: var(--color-bg-hover); + color: var(--color-primary); } .editor-header .status { - font-size: 12px; - color: #4ec9b0; + display: flex; + align-items: center; + gap: var(--spacing-xs); + font-size: var(--font-size-sm); + color: var(--color-success); + white-space: nowrap; +} + +.editor-header .status::before { + content: ''; + width: 6px; + height: 6px; + background-color: var(--color-success); + border-radius: var(--radius-full); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } .editor-content { @@ -61,65 +143,40 @@ overflow: hidden; } -.sidebar-left, -.sidebar-right { - height: 100%; - background-color: #252526; - overflow: hidden; -} - -.sidebar-left h3, -.sidebar-right h3 { - font-size: 14px; - margin-bottom: 12px; - color: #ffffff; -} - -.loading { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - color: #858585; - font-size: 13px; -} - -.main-content { - height: 100%; - overflow: hidden; -} - .viewport { - height: 100%; - background-color: #1e1e1e; - padding: 12px; display: flex; flex-direction: column; + height: 100%; + background-color: var(--color-bg-base); + padding: var(--spacing-lg); } .viewport h3 { - font-size: 14px; - margin-bottom: 12px; - color: #ffffff; + font-size: var(--font-size-md); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); + margin: 0 0 var(--spacing-md) 0; } -.bottom-panel { - height: 100%; - background-color: #252526; - overflow-y: auto; -} - -.bottom-panel h4 { - font-size: 12px; - margin-bottom: 8px; - color: #ffffff; +.viewport p { + color: var(--color-text-secondary); + font-size: var(--font-size-base); } .editor-footer { display: flex; + align-items: center; justify-content: space-between; - padding: 4px 16px; - background-color: #007acc; - color: #ffffff; - font-size: 12px; + height: var(--layout-footer-height); + padding: 0 var(--spacing-lg); + background-color: var(--color-primary); + color: var(--color-text-inverse); + font-size: var(--font-size-xs); + flex-shrink: 0; +} + +.editor-footer span { + display: flex; + align-items: center; + gap: var(--spacing-sm); } diff --git a/packages/editor-app/src/styles/DockContainer.css b/packages/editor-app/src/styles/DockContainer.css index 021d631a..9fce6764 100644 --- a/packages/editor-app/src/styles/DockContainer.css +++ b/packages/editor-app/src/styles/DockContainer.css @@ -9,7 +9,7 @@ .dock-top, .dock-bottom, .dock-center { - background: #1e1e1e; + background: var(--color-bg-base); position: relative; width: 100%; height: 100%; @@ -17,17 +17,17 @@ } .dock-left { - border-right: 1px solid #3e3e3e; + border-right: 1px solid var(--color-border-default); } .dock-right { - border-left: 1px solid #3e3e3e; + border-left: 1px solid var(--color-border-default); } .dock-top { - border-bottom: 1px solid #3e3e3e; + border-bottom: 1px solid var(--color-border-default); } .dock-bottom { - border-top: 1px solid #3e3e3e; + border-top: 1px solid var(--color-border-default); } diff --git a/packages/editor-app/src/styles/EntityInspector.css b/packages/editor-app/src/styles/EntityInspector.css index 1179c313..41ca0eb4 100644 --- a/packages/editor-app/src/styles/EntityInspector.css +++ b/packages/editor-app/src/styles/EntityInspector.css @@ -2,171 +2,277 @@ display: flex; flex-direction: column; height: 100%; - background-color: #1e1e1e; - color: #cccccc; + background-color: var(--color-bg-base); + color: var(--color-text-primary); } .inspector-header { - padding: 10px; - border-bottom: 1px solid #3c3c3c; - background-color: #252526; + display: flex; + align-items: center; + gap: var(--spacing-sm); + height: var(--layout-panel-header); + padding: 0 var(--spacing-md); + border-bottom: 1px solid var(--color-border-default); + background-color: var(--color-bg-elevated); + flex-shrink: 0; +} + +.inspector-header-icon { + color: var(--color-text-secondary); + flex-shrink: 0; } .inspector-header h3 { margin: 0; - font-size: 14px; - font-weight: 600; - color: #cccccc; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); + text-transform: uppercase; + letter-spacing: 0.05em; } .inspector-content { flex: 1; overflow-y: auto; - padding: 12px; + overflow-x: hidden; + padding: var(--spacing-md); + min-height: 0; } .inspector-section { - margin-bottom: 16px; + margin-bottom: var(--spacing-lg); +} + +.inspector-section:last-child { + margin-bottom: 0; } .section-header { display: flex; - justify-content: space-between; align-items: center; - font-size: 12px; - font-weight: 600; - color: #858585; + gap: var(--spacing-sm); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 8px; - padding: 4px 0; - border-bottom: 1px solid #3c3c3c; + letter-spacing: 0.05em; + margin-bottom: var(--spacing-sm); + padding-bottom: var(--spacing-xs); + border-bottom: 1px solid var(--color-border-subtle); +} + +.section-icon { + color: var(--color-text-tertiary); + flex-shrink: 0; +} + +.section-header span { + flex: 1; } .add-component-btn { - background-color: #007acc; - color: #fff; - border: none; - border-radius: 3px; - width: 20px; - height: 20px; - font-size: 16px; - line-height: 16px; - cursor: pointer; - display: flex; + display: inline-flex; align-items: center; justify-content: center; - transition: background-color 0.2s; + width: 20px; + height: 20px; + background-color: var(--color-primary); + color: var(--color-text-inverse); + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition-fast); } .add-component-btn:hover { - background-color: #0098ff; + background-color: var(--color-primary-hover); + transform: scale(1.1); +} + +.add-component-btn:active { + transform: scale(0.95); } .section-content { - padding: 8px 0; + padding: var(--spacing-sm) 0; } .info-row { display: flex; justify-content: space-between; - padding: 6px 0; - font-size: 13px; + align-items: center; + padding: var(--spacing-xs) var(--spacing-sm); + font-size: var(--font-size-base); + border-radius: var(--radius-sm); + transition: background-color var(--transition-fast); +} + +.info-row:hover { + background-color: var(--color-bg-hover); } .info-label { - color: #858585; - font-weight: 500; + color: var(--color-text-secondary); + font-weight: var(--font-weight-medium); + font-size: var(--font-size-sm); } .info-value { - color: #cccccc; + color: var(--color-text-primary); + font-size: var(--font-size-sm); + font-family: var(--font-family-mono); } .component-list { list-style: none; margin: 0; padding: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + min-height: min-content; } .component-item { - margin-bottom: 8px; - background-color: #252526; - border: 1px solid #3c3c3c; - border-radius: 3px; - font-size: 13px; - overflow: hidden; + background: linear-gradient(135deg, var(--color-bg-elevated) 0%, var(--color-bg-base) 100%); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + overflow: visible; + transition: all var(--transition-base); + display: flex; + flex-direction: column; + min-height: min-content; } .component-item:hover { - border-color: #505050; + border-color: var(--color-border-strong); + box-shadow: var(--shadow-sm); +} + +.component-item.expanded { + background: var(--color-bg-overlay); + box-shadow: var(--shadow-md); + overflow: visible; } .component-header { display: flex; align-items: center; - padding: 8px; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); cursor: pointer; - transition: background-color 0.2s; + transition: background-color var(--transition-fast); + user-select: none; } .component-header:hover { - background-color: #2a2d2e; + background-color: var(--color-bg-hover); } .component-expand-btn { - background: none; - border: none; - color: #858585; - font-size: 10px; - cursor: pointer; - padding: 4px; - margin-right: 4px; - transition: color 0.2s; - display: flex; + display: inline-flex; align-items: center; justify-content: center; + background: none; + border: none; + color: var(--color-text-secondary); + padding: 0; + cursor: pointer; + transition: all var(--transition-fast); + flex-shrink: 0; } .component-expand-btn:hover { - color: #cccccc; + color: var(--color-primary); + transform: scale(1.1); } .component-icon { - margin-right: 8px; - font-size: 14px; + color: var(--color-text-secondary); + flex-shrink: 0; + transition: color var(--transition-fast); +} + +.component-item:hover .component-icon, +.component-item.expanded .component-icon { + color: var(--color-primary); } .component-name { flex: 1; - color: #cccccc; + color: var(--color-text-primary); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); } .remove-component-btn { + display: inline-flex; + align-items: center; + justify-content: center; background: none; border: none; - color: #858585; - font-size: 18px; - line-height: 14px; + color: var(--color-text-tertiary); + padding: var(--spacing-xs); cursor: pointer; - padding: 2px 6px; - transition: color 0.2s; - margin-left: 8px; + border-radius: var(--radius-sm); + transition: all var(--transition-fast); + flex-shrink: 0; + opacity: 0; +} + +.component-header:hover .remove-component-btn { + opacity: 1; } .remove-component-btn:hover { - color: #ff5555; + background-color: var(--color-error); + color: var(--color-text-inverse); + transform: scale(1.1); +} + +.remove-component-btn:active { + transform: scale(0.95); } .component-properties { - border-top: 1px solid #3c3c3c; - background-color: #1e1e1e; - padding: 4px; + border-top: 1px solid var(--color-border-default); + background-color: var(--color-bg-base); + overflow: visible; } .empty-state { - padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-4xl) var(--spacing-lg); text-align: center; - color: #858585; - font-size: 13px; + color: var(--color-text-secondary); + height: 100%; +} + +.empty-icon { + color: var(--color-text-tertiary); + margin-bottom: var(--spacing-md); + opacity: 0.5; +} + +.empty-title { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-xs); +} + +.empty-hint { + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); +} + +.empty-state-small { + padding: var(--spacing-lg); + text-align: center; + color: var(--color-text-tertiary); + font-size: var(--font-size-sm); + font-style: italic; } diff --git a/packages/editor-app/src/styles/PropertyInspector.css b/packages/editor-app/src/styles/PropertyInspector.css index 892ea7d9..9d9dd913 100644 --- a/packages/editor-app/src/styles/PropertyInspector.css +++ b/packages/editor-app/src/styles/PropertyInspector.css @@ -1,80 +1,385 @@ .property-inspector { padding: 8px; + display: flex; + flex-direction: column; + gap: 1px; + overflow: visible; } .property-field { display: flex; - flex-direction: column; - margin-bottom: 12px; -} - -.property-field-checkbox { flex-direction: row; align-items: center; + min-height: 24px; + padding: 4px 8px; + background: var(--color-bg-elevated); + border-bottom: 1px solid var(--color-border-subtle); gap: 8px; + transition: background-color var(--transition-fast); +} + +.property-field:hover { + background: rgba(255, 255, 255, 0.03); +} + +.property-field:last-child { + border-bottom: none; } .property-label { - font-size: 12px; + flex: 0 0 40%; + font-size: 11px; font-weight: 500; - color: #e0e0e0; - margin-bottom: 4px; + color: var(--color-text-secondary); + margin: 0; + user-select: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: 0.02em; } -.property-field-checkbox .property-label { - margin-bottom: 0; - flex: 1; +.property-label-draggable { + cursor: ew-resize; + position: relative; +} + +.property-label-draggable::before { + content: ''; + position: absolute; + left: -4px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 3px; + background: var(--color-text-tertiary); + border-radius: 50%; + opacity: 0.5; +} + +.property-label-draggable:hover::before { + opacity: 1; + background: var(--color-primary); } .property-input { - background: #2a2a2a; - border: 1px solid #444; - border-radius: 4px; - padding: 6px 8px; - color: #e0e0e0; - font-size: 13px; - font-family: inherit; + flex: 1; + background: var(--color-bg-inset); + border: 1px solid var(--color-border-default); + border-radius: 3px; + padding: 3px 6px; + color: var(--color-text-primary); + font-size: 11px; + font-family: var(--font-family-mono); + min-width: 0; + transition: all var(--transition-fast); +} + +.property-input:hover { + border-color: var(--color-border-hover); + background: var(--color-bg-base); } .property-input:focus { outline: none; - border-color: #4a9eff; - background: #333; + border-color: var(--color-primary); + background: var(--color-bg-base); + box-shadow: 0 0 0 1px var(--color-primary); } .property-input:disabled { - opacity: 0.5; + opacity: 0.4; cursor: not-allowed; + background: var(--color-bg-inset); } -.property-checkbox { - width: 18px; +.property-input-number, +.property-input-text { + text-align: right; +} + +.property-input-select { + cursor: pointer; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a0a0a0' d='M6 8L2 4h8z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 6px center; + padding-right: 24px; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; +} + +.property-input-select:hover { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ffffff' d='M6 8L2 4h8z'/%3E%3C/svg%3E"); +} + +.property-input-select:focus { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23007acc' d='M6 8L2 4h8z'/%3E%3C/svg%3E"); +} + +.property-input-select option { + background-color: var(--color-bg-elevated); + color: var(--color-text-primary); + padding: 4px 8px; +} + +.property-input-number::-webkit-inner-spin-button { + opacity: 0; + width: 0; +} + +.property-input-number:hover::-webkit-inner-spin-button { + opacity: 0.5; + width: auto; +} + +.property-field-boolean { + justify-content: space-between; +} + +.property-toggle { + position: relative; + width: 36px; height: 18px; + border-radius: 9px; + border: none; cursor: pointer; + transition: all var(--transition-base); + flex-shrink: 0; + padding: 0; } -.property-checkbox:disabled { - opacity: 0.5; +.property-toggle-off { + background: var(--color-bg-inset); + border: 1px solid var(--color-border-default); +} + +.property-toggle-off:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.05); + border-color: var(--color-border-hover); +} + +.property-toggle-on { + background: var(--color-primary); + border: 1px solid var(--color-primary); +} + +.property-toggle-on:hover:not(:disabled) { + background: var(--color-primary-hover); + border-color: var(--color-primary-hover); +} + +.property-toggle:disabled { + opacity: 0.4; cursor: not-allowed; } -.property-color { - height: 36px; - padding: 2px; - cursor: pointer; +.property-toggle-thumb { + position: absolute; + top: 2px; + width: 12px; + height: 12px; + border-radius: 50%; + background: white; + transition: transform var(--transition-base); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } -.property-vector { - display: flex; - gap: 6px; +.property-toggle-off .property-toggle-thumb { + left: 2px; + transform: translateX(0); } -.property-vector-input { +.property-toggle-on .property-toggle-thumb { + left: 2px; + transform: translateX(18px); +} + +.property-color-wrapper { flex: 1; + display: flex; + align-items: center; + gap: 6px; min-width: 0; } -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - opacity: 1; +.property-color-preview { + width: 20px; + height: 20px; + border-radius: 3px; + border: 1px solid var(--color-border-default); + flex-shrink: 0; + background-image: + linear-gradient(45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%), + linear-gradient(-45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.1) 75%), + linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.1) 75%); + background-size: 8px 8px; + background-position: 0 0, 0 4px, 4px -4px, -4px 0px; + cursor: pointer; + transition: transform var(--transition-fast); +} + +.property-color-preview:hover { + transform: scale(1.1); +} + +.property-input-color { + width: 0; + height: 0; + opacity: 0; + position: absolute; + pointer-events: none; +} + +.property-input-color-text { + flex: 1; + text-transform: uppercase; + letter-spacing: 0.05em; + font-family: var(--font-family-mono); + text-align: center; +} + +.property-label-row { + flex: 0 0 40%; + display: flex; + align-items: center; + gap: 4px; + min-width: 0; +} + +.property-expand-btn { + width: 16px; + height: 16px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + color: var(--color-text-tertiary); + cursor: pointer; + border-radius: 2px; + flex-shrink: 0; + transition: all var(--transition-fast); +} + +.property-expand-btn:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--color-text-secondary); +} + +.property-vector-compact { + flex: 1; + display: flex; + gap: 4px; + min-width: 0; +} + +.property-vector-axis-compact { + flex: 1; + display: flex; + align-items: center; + gap: 3px; + min-width: 0; +} + +.property-input-number-compact { + flex: 1; + min-width: 32px; + text-align: center; + padding: 2px 4px; + font-size: 10px; +} + +.property-vector-expanded { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px 0; +} + +.property-vector-axis { + display: flex; + align-items: center; + gap: 6px; +} + +.property-vector-axis-label { + width: 14px; + height: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: 700; + border-radius: 2px; + flex-shrink: 0; + letter-spacing: 0; + text-transform: uppercase; +} + +.property-vector-axis-x { + background: rgba(239, 68, 68, 0.2); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.property-vector-axis-y { + background: rgba(34, 197, 94, 0.2); + color: #4ade80; + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.property-vector-axis-z { + background: rgba(59, 130, 246, 0.2); + color: #60a5fa; + border: 1px solid rgba(59, 130, 246, 0.3); +} + +.property-field:focus-within { + background: rgba(255, 255, 255, 0.04); +} + +@keyframes property-field-highlight { + 0% { + background: rgba(0, 122, 204, 0.2); + } + 100% { + background: transparent; + } +} + +.property-field.property-changed { + animation: property-field-highlight 0.5s ease-out; +} + +input[type="number"].property-input { + -moz-appearance: textfield; +} + +input[type="number"].property-input::-webkit-outer-spin-button, +input[type="number"].property-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.property-input::placeholder { + color: var(--color-text-tertiary); + opacity: 0.5; +} + +.property-input:hover::placeholder { + opacity: 0.7; +} + +@media (prefers-reduced-motion: reduce) { + .property-input, + .property-toggle, + .property-toggle-thumb, + .property-expand-btn, + .property-color-preview { + transition: none; + } } diff --git a/packages/editor-app/src/styles/ResizablePanel.css b/packages/editor-app/src/styles/ResizablePanel.css index 70a03d25..4b74bb44 100644 --- a/packages/editor-app/src/styles/ResizablePanel.css +++ b/packages/editor-app/src/styles/ResizablePanel.css @@ -19,31 +19,35 @@ } .resizer { - background: #252526; + background: var(--color-bg-elevated); position: relative; - z-index: 10; - transition: background-color 0.2s ease; + z-index: var(--z-index-base); + transition: background-color var(--transition-fast); + flex-shrink: 0; } .resizer:hover { - background: #094771; + background: var(--color-primary-subtle); +} + +.resizer:active { + background: var(--color-primary); } .resizer-horizontal { width: 4px; cursor: col-resize; - flex-shrink: 0; } .resizer-vertical { height: 4px; cursor: row-resize; - flex-shrink: 0; } .resizer-handle { position: absolute; background: transparent; + pointer-events: none; } .resizer-horizontal .resizer-handle { @@ -62,10 +66,6 @@ height: 12px; } -.resizer:active { - background: #0e6caa; -} - body.resizing { cursor: col-resize !important; user-select: none !important; diff --git a/packages/editor-app/src/styles/SceneHierarchy.css b/packages/editor-app/src/styles/SceneHierarchy.css index ccfcf2df..ac9353aa 100644 --- a/packages/editor-app/src/styles/SceneHierarchy.css +++ b/packages/editor-app/src/styles/SceneHierarchy.css @@ -2,34 +2,68 @@ display: flex; flex-direction: column; height: 100%; - background-color: #1e1e1e; - color: #cccccc; + background-color: var(--color-bg-base); + color: var(--color-text-primary); } .hierarchy-header { - padding: 10px; - border-bottom: 1px solid #3c3c3c; - background-color: #252526; + display: flex; + align-items: center; + gap: var(--spacing-sm); + height: var(--layout-panel-header); + padding: 0 var(--spacing-md); + border-bottom: 1px solid var(--color-border-default); + background-color: var(--color-bg-elevated); + flex-shrink: 0; +} + +.hierarchy-header-icon { + color: var(--color-text-secondary); + flex-shrink: 0; } .hierarchy-header h3 { margin: 0; - font-size: 14px; - font-weight: 600; - color: #cccccc; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); + text-transform: uppercase; + letter-spacing: 0.05em; } .hierarchy-content { flex: 1; overflow-y: auto; - padding: 8px 0; + padding: var(--spacing-xs) 0; } .empty-state { - padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--spacing-4xl) var(--spacing-lg); text-align: center; - color: #858585; - font-size: 13px; + color: var(--color-text-secondary); + height: 100%; +} + +.empty-icon { + color: var(--color-text-tertiary); + margin-bottom: var(--spacing-md); + opacity: 0.5; +} + +.empty-title { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-xs); +} + +.empty-hint { + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); } .entity-list { @@ -41,30 +75,61 @@ .entity-item { display: flex; align-items: center; - padding: 6px 12px; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); cursor: pointer; user-select: none; - transition: background-color 0.1s; + transition: all var(--transition-fast); + position: relative; +} + +.entity-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 2px; + background-color: transparent; + transition: background-color var(--transition-fast); } .entity-item:hover { - background-color: #2a2d2e; + background-color: var(--color-bg-hover); } .entity-item.selected { - background-color: #094771; + background-color: var(--color-selected); +} + +.entity-item.selected::before { + background-color: var(--color-primary); } .entity-item.selected:hover { - background-color: #0e639c; + background-color: var(--color-selected-hover); } .entity-icon { - margin-right: 8px; - font-size: 14px; + color: var(--color-text-secondary); + flex-shrink: 0; + transition: color var(--transition-fast); +} + +.entity-item:hover .entity-icon, +.entity-item.selected .entity-icon { + color: var(--color-primary); } .entity-name { - font-size: 13px; - color: #cccccc; + font-size: var(--font-size-base); + color: var(--color-text-primary); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.entity-item.selected .entity-name { + font-weight: var(--font-weight-medium); } diff --git a/packages/editor-app/src/styles/TabPanel.css b/packages/editor-app/src/styles/TabPanel.css index aa77f165..eef1f8e5 100644 --- a/packages/editor-app/src/styles/TabPanel.css +++ b/packages/editor-app/src/styles/TabPanel.css @@ -2,15 +2,16 @@ display: flex; flex-direction: column; height: 100%; - background: #1e1e1e; + background: var(--color-bg-base); } .tab-header { - background: #252526; - border-bottom: 1px solid #3e3e3e; - min-height: 35px; + background: var(--color-bg-elevated); + border-bottom: 1px solid var(--color-border-default); + min-height: 38px; display: flex; align-items: flex-end; + flex-shrink: 0; } .tab-list { @@ -22,37 +23,42 @@ .tab-item { display: flex; align-items: center; - gap: 6px; - padding: 8px 12px; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); background: transparent; - color: #969696; - border-right: 1px solid #3e3e3e; + color: var(--color-text-secondary); + border-right: 1px solid var(--color-border-default); cursor: pointer; - transition: all 0.2s ease; - font-size: 13px; + transition: all var(--transition-fast); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); user-select: none; position: relative; } +.tab-item::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background-color: transparent; + transition: background-color var(--transition-fast); +} + .tab-item:hover { - background: #2a2d2e; - color: #cccccc; + background: var(--color-bg-hover); + color: var(--color-text-primary); } .tab-item.active { - background: #1e1e1e; - color: #ffffff; - border-bottom: 2px solid #007acc; + background: var(--color-bg-base); + color: var(--color-text-primary); } .tab-item.active::after { - content: ''; - position: absolute; - bottom: -1px; - left: 0; - right: 0; - height: 1px; - background: #1e1e1e; + background-color: var(--color-primary); } .tab-title { @@ -60,25 +66,32 @@ } .tab-close { - width: 16px; - height: 16px; - display: flex; + display: inline-flex; align-items: center; justify-content: center; + width: 16px; + height: 16px; background: transparent; border: none; - color: #858585; + color: var(--color-text-tertiary); cursor: pointer; - border-radius: 3px; + border-radius: var(--radius-sm); font-size: 18px; line-height: 1; padding: 0; - transition: all 0.2s ease; + transition: all var(--transition-fast); + opacity: 0; +} + +.tab-item:hover .tab-close, +.tab-item.active .tab-close { + opacity: 1; } .tab-close:hover { - background: #4e4e4e; - color: #ffffff; + background: var(--color-bg-hover); + color: var(--color-error); + transform: scale(1.2); } .tab-content { diff --git a/packages/editor-app/src/styles/design-tokens.css b/packages/editor-app/src/styles/design-tokens.css new file mode 100644 index 00000000..7b0dbdf4 --- /dev/null +++ b/packages/editor-app/src/styles/design-tokens.css @@ -0,0 +1,122 @@ +:root { + /* 颜色系统 - 背景 */ + --color-bg-base: #1e1e1e; + --color-bg-elevated: #252526; + --color-bg-overlay: #2d2d2d; + --color-bg-input: #3c3c3c; + --color-bg-hover: #2a2d2e; + --color-bg-active: #37373d; + + /* 颜色系统 - 文本 */ + --color-text-primary: #cccccc; + --color-text-secondary: #9d9d9d; + --color-text-tertiary: #6a6a6a; + --color-text-disabled: #4d4d4d; + --color-text-inverse: #ffffff; + + /* 颜色系统 - 边框 */ + --color-border-default: #3e3e42; + --color-border-subtle: #2b2b2b; + --color-border-strong: #505050; + + /* 颜色系统 - 主题色 */ + --color-primary: #007acc; + --color-primary-hover: #1177bb; + --color-primary-active: #0e639c; + --color-primary-subtle: rgba(0, 122, 204, 0.1); + + /* 颜色系统 - 功能色 */ + --color-success: #4ec9b0; + --color-warning: #ce9178; + --color-error: #f48771; + --color-info: #4fc1ff; + + /* 颜色系统 - 特殊 */ + --color-selected: #094771; + --color-selected-hover: #0e639c; + --color-focus: #007acc; + --color-shadow: rgba(0, 0, 0, 0.5); + + /* 字体系统 */ + --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; + --font-family-mono: 'Consolas', 'Monaco', 'Courier New', monospace; + + --font-size-xs: 11px; + --font-size-sm: 12px; + --font-size-base: 13px; + --font-size-md: 14px; + --font-size-lg: 16px; + --font-size-xl: 18px; + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + --line-height-tight: 1.2; + --line-height-base: 1.5; + --line-height-relaxed: 1.75; + + /* 间距系统 (4px 基准) */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-2xl: 24px; + --spacing-3xl: 32px; + --spacing-4xl: 40px; + + /* 圆角 */ + --radius-none: 0; + --radius-sm: 3px; + --radius-md: 4px; + --radius-lg: 6px; + --radius-xl: 8px; + --radius-full: 9999px; + + /* 阴影 */ + --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.4); + + /* 过渡 */ + --transition-fast: 0.1s cubic-bezier(0.4, 0, 0.2, 1); + --transition-base: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-bounce: 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); + + /* Z-index 层级 */ + --z-index-base: 1; + --z-index-dropdown: 10; + --z-index-sticky: 20; + --z-index-overlay: 30; + --z-index-modal: 40; + --z-index-popover: 50; + --z-index-tooltip: 60; + + /* 尺寸 */ + --size-icon-sm: 14px; + --size-icon-md: 16px; + --size-icon-lg: 20px; + --size-icon-xl: 24px; + + --size-input-sm: 28px; + --size-input-md: 32px; + --size-input-lg: 36px; + + --size-button-sm: 28px; + --size-button-md: 32px; + --size-button-lg: 36px; + + /* 布局 */ + --layout-sidebar-min: 180px; + --layout-sidebar-default: 250px; + --layout-sidebar-max: 400px; + --layout-header-height: 40px; + --layout-footer-height: 24px; + --layout-panel-header: 36px; +} diff --git a/packages/editor-app/src/styles/global.css b/packages/editor-app/src/styles/global.css new file mode 100644 index 00000000..a23f49e7 --- /dev/null +++ b/packages/editor-app/src/styles/global.css @@ -0,0 +1,148 @@ +@import './design-tokens.css'; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#root { + width: 100%; + height: 100%; + overflow: hidden; +} + +body { + font-family: var(--font-family-base); + font-size: var(--font-size-base); + line-height: var(--line-height-base); + color: var(--color-text-primary); + background-color: var(--color-bg-base); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +button { + font-family: inherit; + font-size: inherit; + cursor: pointer; +} + +input, +textarea, +select { + font-family: inherit; + font-size: inherit; +} + +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +::selection { + background-color: var(--color-primary); + color: var(--color-text-inverse); +} + +.scrollable { + overflow: auto; +} + +.scrollable::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +.scrollable::-webkit-scrollbar-track { + background: transparent; +} + +.scrollable::-webkit-scrollbar-thumb { + background: rgba(121, 121, 121, 0.4); + border-radius: 8px; + border: 3px solid transparent; + background-clip: padding-box; +} + +.scrollable::-webkit-scrollbar-thumb:hover { + background: rgba(100, 100, 100, 0.7); + background-clip: padding-box; +} + +.scrollable::-webkit-scrollbar-corner { + background: transparent; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-fadeIn { + animation: fadeIn var(--transition-base); +} + +.animate-slideDown { + animation: slideDown var(--transition-base); +} + +.animate-slideUp { + animation: slideUp var(--transition-base); +} + +.animate-scaleIn { + animation: scaleIn var(--transition-base); +} + +.animate-spin { + animation: spin 1s linear infinite; +} diff --git a/packages/editor-app/vite.config.ts b/packages/editor-app/vite.config.ts index 7f36420f..8910f565 100644 --- a/packages/editor-app/vite.config.ts +++ b/packages/editor-app/vite.config.ts @@ -1,10 +1,339 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import fs from 'fs'; +import path from 'path'; +import { transformSync } from 'esbuild'; +import JSON5 from 'json5'; const host = process.env.TAURI_DEV_HOST; +const userProjectPathMap = new Map(); +const userProjectDependencies = new Map>(); +const cocosEnginePaths = new Map(); +const allowedPaths = new Set(); + +const editorPackageMapping = new Map(); + +function loadEditorPackages() { + const packagesDir = path.resolve(__dirname, '..'); + if (!fs.existsSync(packagesDir)) { + return; + } + + const packageDirs = fs.readdirSync(packagesDir).filter(dir => { + const stat = fs.statSync(path.join(packagesDir, dir)); + return stat.isDirectory(); + }); + + for (const dir of packageDirs) { + const packageJsonPath = path.join(packagesDir, dir, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + if (packageJson.name && packageJson.name.startsWith('@esengine/')) { + const mainFile = packageJson.module || packageJson.main; + if (mainFile) { + const entryPath = path.join(packagesDir, dir, mainFile); + if (fs.existsSync(entryPath)) { + editorPackageMapping.set(packageJson.name, entryPath); + console.log(`[Vite] Mapped ${packageJson.name} -> ${entryPath}`); + } + } + } + } catch (e) { + console.error(`[Vite] Failed to read package.json for ${dir}:`, e); + } + } + } + + console.log(`[Vite] Loaded ${editorPackageMapping.size} editor packages`); +} + +loadEditorPackages(); + +function loadCocosEnginePath(projectPath: string) { + try { + const tsconfigPath = path.join(projectPath, 'tsconfig.json'); + if (!fs.existsSync(tsconfigPath)) { + console.log('[Vite] No tsconfig.json found in user project'); + return; + } + + const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf-8'); + const tsconfig = JSON5.parse(tsconfigContent); + + if (tsconfig.extends) { + const extendedPath = path.join(projectPath, tsconfig.extends); + if (fs.existsSync(extendedPath)) { + const extendedContent = fs.readFileSync(extendedPath, 'utf-8'); + const extendedConfig = JSON5.parse(extendedContent); + + if (extendedConfig.compilerOptions?.paths?.['db://internal/*']) { + const cocosInternalPaths = extendedConfig.compilerOptions.paths['db://internal/*']; + if (cocosInternalPaths && cocosInternalPaths.length > 0) { + let cocosEnginePath = cocosInternalPaths[0]; + cocosEnginePath = cocosEnginePath.replace(/[\/\\]\*$/, ''); + cocosEnginePath = cocosEnginePath.replace(/[\/\\]editor[\/\\]assets$/, ''); + + const exportsBasePath = path.join(cocosEnginePath, 'exports', 'base.ts'); + if (fs.existsSync(exportsBasePath)) { + cocosEnginePaths.set(projectPath, exportsBasePath); + allowedPaths.add(cocosEnginePath); + console.log(`[Vite] Found Cocos Creator engine at: ${exportsBasePath}`); + console.log(`[Vite] Added Cocos engine path to allowed paths: ${cocosEnginePath}`); + } else { + console.log(`[Vite] Cocos engine base.ts not found at: ${exportsBasePath}`); + } + } + } + } + } + } catch (error) { + console.error('[Vite] Failed to load Cocos engine path:', error); + } +} + +function loadUserProjectDependencies(projectPath: string) { + try { + const packageJsonPath = path.join(projectPath, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + console.log('[Vite] No package.json found in user project'); + return; + } + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + const deps = new Set(); + + if (packageJson.dependencies) { + Object.keys(packageJson.dependencies).forEach(dep => deps.add(dep)); + } + if (packageJson.devDependencies) { + Object.keys(packageJson.devDependencies).forEach(dep => deps.add(dep)); + } + + userProjectDependencies.set(projectPath, deps); + console.log(`[Vite] Loaded ${deps.size} dependencies from user project`); + + loadCocosEnginePath(projectPath); + } catch (error) { + console.error('[Vite] Failed to load user project dependencies:', error); + } +} + +const userProjectPlugin = () => ({ + name: 'user-project-middleware', + configureServer(server: any) { + allowedPaths.add(path.resolve(__dirname, '..')); + + server.middlewares.use(async (req: any, res: any, next: any) => { + if (req.url?.startsWith('/@user-project/')) { + console.log('[Vite] Received request:', req.url); + const urlWithoutQuery = req.url.split('?')[0]; + const relativePath = decodeURIComponent(urlWithoutQuery.substring('/@user-project'.length)); + console.log('[Vite] Resolved relative path:', relativePath); + + let projectPath: string | null = null; + + for (const [, path] of userProjectPathMap) { + projectPath = path; + break; + } + + if (!projectPath) { + res.statusCode = 503; + res.end('Project path not set. Please open a project first.'); + return; + } + + const filePath = path.join(projectPath, relativePath); + console.log('[Vite] Looking for file:', filePath); + console.log('[Vite] File exists:', fs.existsSync(filePath)); + + if (!fs.existsSync(filePath)) { + console.error('[Vite] File not found:', filePath); + res.statusCode = 404; + res.end(`File not found: ${filePath}`); + return; + } + + if (fs.statSync(filePath).isDirectory()) { + res.statusCode = 400; + res.end(`Path is a directory: ${filePath}`); + return; + } + + try { + console.log('[Vite] Reading and transforming file:', filePath); + let content = fs.readFileSync(filePath, 'utf-8'); + + editorPackageMapping.forEach((srcPath, packageName) => { + const escapedPackageName = packageName.replace(/\//g, '\\/'); + const regex = new RegExp(`from\\s+['"]${escapedPackageName}['"]`, 'g'); + content = content.replace( + regex, + `from "/@fs/${srcPath.replace(/\\/g, '/')}"` + ); + }); + + const deps = userProjectDependencies.get(projectPath); + if (deps) { + deps.forEach(dep => { + if (editorPackageMapping.has(dep)) { + return; + } + + const depPath = path.join(projectPath, 'node_modules', dep); + if (fs.existsSync(depPath)) { + const packageJsonPath = path.join(depPath, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + const mainFile = pkg.module || pkg.main || 'index.js'; + const entryPath = path.join(depPath, mainFile); + + if (fs.existsSync(entryPath)) { + const escapedDep = dep.replace(/\//g, '\\/').replace(/@/g, '\\@'); + const regex = new RegExp(`from\\s+['"]${escapedDep}['"]`, 'g'); + content = content.replace( + regex, + `from "/@fs/${entryPath.replace(/\\/g, '/')}"` + ); + } + } catch (e) { + // 忽略解析错误 + } + } + } + }); + } + + content = content.replace( + /from\s+['"]cc['"]/g, + 'from "/@cocos-shim"' + ); + + const fileDir = path.dirname(filePath); + const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g; + content = content.replace(relativeImportRegex, (match, importPath) => { + if (importPath.match(/\.(ts|js|tsx|jsx)$/)) { + return match; + } + + const possibleExtensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js']; + for (const ext of possibleExtensions) { + const resolvedPath = path.join(fileDir, importPath + ext); + if (fs.existsSync(resolvedPath)) { + const normalizedImport = (importPath + ext).replace(/\\/g, '/'); + return match.replace(importPath, normalizedImport); + } + } + + return match; + }); + + const result = transformSync(content, { + loader: 'ts', + format: 'esm', + target: 'es2020', + sourcemap: 'inline', + sourcefile: filePath, + }); + + console.log('[Vite] Successfully transformed file:', filePath); + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Cache-Control', 'no-cache'); + res.end(result.code); + } catch (err: any) { + console.error('[Vite] Failed to transform TypeScript:', err); + res.statusCode = 500; + res.end(`Failed to compile: ${err.message}`); + } + return; + } + + if (req.url === '/@ecs-framework-shim') { + let projectPath: string | null = null; + for (const [, p] of userProjectPathMap) { + projectPath = p; + break; + } + + if (projectPath) { + const userFrameworkPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework', 'bin', 'index.js'); + if (fs.existsSync(userFrameworkPath)) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end('export * from "/@fs/' + userFrameworkPath.replace(/\\/g, '/') + '";'); + return; + } + } + + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end('export * from "/@fs/' + path.resolve(__dirname, '../core/src/index.ts').replace(/\\/g, '/') + '";'); + return; + } + + if (req.url === '/@cocos-shim') { + console.log('[Vite] Using Cocos Creator fallback shim (editor environment)'); + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end(` + export default {}; + export class Node {} + export class Component {} + export class Vec2 { + constructor(x = 0, y = 0) { this.x = x; this.y = y; } + static distance(a, b) { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); } + length() { return Math.sqrt(this.x ** 2 + this.y ** 2); } + normalize() { const len = this.length(); if (len > 0) { this.x /= len; this.y /= len; } return this; } + set(x, y) { this.x = x; this.y = y; return this; } + } + export class Vec3 { + constructor(x = 0, y = 0, z = 0) { this.x = x; this.y = y; this.z = z; } + static distance(a, b) { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2); } + length() { return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2); } + normalize() { const len = this.length(); if (len > 0) { this.x /= len; this.y /= len; this.z /= len; } return this; } + set(x, y, z) { this.x = x; this.y = y; this.z = z; return this; } + } + export class Color { constructor(r = 255, g = 255, b = 255, a = 255) { this.r = r; this.g = g; this.b = b; this.a = a; } } + export class Quat { constructor(x = 0, y = 0, z = 0, w = 1) { this.x = x; this.y = y; this.z = z; this.w = w; } } + export function tween() { return { to() { return this; }, call() { return this; }, start() {} }; } + export function v3(x, y, z) { return new Vec3(x, y, z); } + `); + return; + } + + if (req.url === '/@user-project-set-path') { + let body = ''; + req.on('data', (chunk: any) => { + body += chunk.toString(); + }); + req.on('end', () => { + try { + const { path: projectPath } = JSON.parse(body); + userProjectPathMap.set('current', projectPath); + allowedPaths.add(projectPath); + loadUserProjectDependencies(projectPath); + console.log('[Vite] User project path set to:', projectPath); + res.statusCode = 200; + res.end('OK'); + } catch (err) { + res.statusCode = 400; + res.end('Invalid request'); + } + }); + return; + } + + next(); + }); + } +}); + export default defineConfig({ - plugins: [react()], + plugins: [react(), userProjectPlugin()], clearScreen: false, server: { host: host || false, @@ -17,6 +346,9 @@ export default defineConfig({ port: 5183, } : undefined, + fs: { + strict: false, + }, }, envPrefix: ['VITE_', 'TAURI_'], build: { diff --git a/packages/editor-core/src/Services/ComponentLoaderService.ts b/packages/editor-core/src/Services/ComponentLoaderService.ts index 1e2f8d42..4a8efc27 100644 --- a/packages/editor-core/src/Services/ComponentLoaderService.ts +++ b/packages/editor-core/src/Services/ComponentLoaderService.ts @@ -57,40 +57,58 @@ export class ComponentLoaderService implements IService { modulePathTransform?: (filePath: string) => string ): Promise { try { - const modulePath = modulePathTransform - ? modulePathTransform(componentInfo.path) - : this.convertToModulePath(componentInfo.path); - - logger.debug(`Loading component from: ${modulePath}`); - - const module = await import(/* @vite-ignore */ modulePath); - if (!componentInfo.className) { logger.warn(`No class name found for component: ${componentInfo.fileName}`); return null; } - const componentClass = module[componentInfo.className]; + let componentClass: typeof Component | undefined; - if (!componentClass || !(componentClass.prototype instanceof Component)) { - logger.error(`Invalid component class: ${componentInfo.className}`); - return null; + if (modulePathTransform) { + const modulePath = modulePathTransform(componentInfo.path); + logger.info(`Attempting to load component from: ${modulePath}`); + logger.info(`Looking for export: ${componentInfo.className}`); + + try { + const module = await import(/* @vite-ignore */ modulePath); + logger.info(`Module loaded, exports:`, Object.keys(module)); + + componentClass = module[componentInfo.className] || module.default; + + if (!componentClass) { + logger.warn(`Component class ${componentInfo.className} not found in module exports`); + logger.warn(`Available exports: ${Object.keys(module).join(', ')}`); + } else { + logger.info(`Successfully loaded component class: ${componentInfo.className}`); + } + } catch (error) { + logger.error(`Failed to import component module: ${modulePath}`, error); + } } this.componentRegistry.register({ name: componentInfo.className, - type: componentClass + type: componentClass as any, + category: componentInfo.className.includes('Transform') ? 'Transform' : + componentInfo.className.includes('Render') || componentInfo.className.includes('Sprite') ? 'Rendering' : + componentInfo.className.includes('Physics') || componentInfo.className.includes('RigidBody') ? 'Physics' : + 'Custom', + description: `Component from ${componentInfo.fileName}`, + metadata: { + path: componentInfo.path, + fileName: componentInfo.fileName + } }); const loadedInfo: LoadedComponentInfo = { fileInfo: componentInfo, - componentClass, + componentClass: (componentClass || Component) as any, loadedAt: Date.now() }; this.loadedComponents.set(componentInfo.path, loadedInfo); - logger.info(`Component loaded and registered: ${componentInfo.className}`); + logger.info(`Component ${componentClass ? 'loaded' : 'metadata registered'}: ${componentInfo.className}`); return loadedInfo; } catch (error) { @@ -125,13 +143,7 @@ export class ComponentLoaderService implements IService { } private convertToModulePath(filePath: string): string { - const normalizedPath = filePath.replace(/\\/g, '/'); - - if (normalizedPath.startsWith('http://') || normalizedPath.startsWith('https://')) { - return normalizedPath; - } - - return `file:///${normalizedPath}`; + return filePath; } public dispose(): void { diff --git a/packages/editor-core/src/Services/ComponentRegistry.ts b/packages/editor-core/src/Services/ComponentRegistry.ts index a239d8f3..0e5101e9 100644 --- a/packages/editor-core/src/Services/ComponentRegistry.ts +++ b/packages/editor-core/src/Services/ComponentRegistry.ts @@ -2,9 +2,14 @@ import { Injectable, IService, Component } from '@esengine/ecs-framework'; export interface ComponentTypeInfo { name: string; - type: new (...args: any[]) => Component; + type?: new (...args: any[]) => Component; category?: string; description?: string; + metadata?: { + path?: string; + fileName?: string; + [key: string]: any; + }; } /** @@ -40,7 +45,7 @@ export class ComponentRegistry implements IService { public createInstance(name: string, ...args: any[]): Component | null { const info = this.components.get(name); - if (!info) return null; + if (!info || !info.type) return null; return new info.type(...args); } diff --git a/packages/editor-core/src/Services/PropertyMetadata.ts b/packages/editor-core/src/Services/PropertyMetadata.ts index c8e2b44e..ee7f8598 100644 --- a/packages/editor-core/src/Services/PropertyMetadata.ts +++ b/packages/editor-core/src/Services/PropertyMetadata.ts @@ -4,7 +4,7 @@ import { createLogger } from '@esengine/ecs-framework'; const logger = createLogger('PropertyMetadata'); -export type PropertyType = 'number' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3'; +export type PropertyType = 'number' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'enum'; export interface PropertyMetadata { type: PropertyType;