diff --git a/packages/editor-app/scripts/bundle-runtime.mjs b/packages/editor-app/scripts/bundle-runtime.mjs index 102cd625..604f8481 100644 --- a/packages/editor-app/scripts/bundle-runtime.mjs +++ b/packages/editor-app/scripts/bundle-runtime.mjs @@ -37,6 +37,25 @@ const filesToBundle = [ } ]; +// Type definition files for IDE intellisense +// 用于 IDE 智能感知的类型定义文件 +const typesDir = path.join(bundleDir, 'types'); +if (!fs.existsSync(typesDir)) { + fs.mkdirSync(typesDir, { recursive: true }); + console.log(`Created types directory: ${typesDir}`); +} + +const typeFilesToBundle = [ + { + src: path.join(rootPath, 'packages/core/dist/index.d.ts'), + dst: path.join(typesDir, 'ecs-framework.d.ts') + }, + { + src: path.join(rootPath, 'packages/engine-core/dist/index.d.ts'), + dst: path.join(typesDir, 'engine-core.d.ts') + } +]; + // Copy files let success = true; for (const { src, dst } of filesToBundle) { @@ -59,6 +78,24 @@ for (const { src, dst } of filesToBundle) { } } +// Copy type definition files (optional - don't fail if not found) +// 复制类型定义文件(可选 - 找不到不报错) +for (const { src, dst } of typeFilesToBundle) { + try { + if (!fs.existsSync(src)) { + console.warn(`Type definition not found: ${src}`); + console.log(' Build packages first: pnpm --filter @esengine/core build'); + continue; + } + + fs.copyFileSync(src, dst); + const stats = fs.statSync(dst); + console.log(`✓ Bundled type definition ${path.basename(dst)} (${(stats.size / 1024).toFixed(2)} KB)`); + } catch (error) { + console.warn(`Failed to bundle type definition ${path.basename(src)}: ${error.message}`); + } +} + // Update tauri.conf.json to include runtime directory if (success) { const tauriConfigPath = path.join(editorPath, 'src-tauri', 'tauri.conf.json'); diff --git a/packages/editor-app/src-tauri/src/commands/system.rs b/packages/editor-app/src-tauri/src/commands/system.rs index 309964b2..318f762b 100644 --- a/packages/editor-app/src-tauri/src/commands/system.rs +++ b/packages/editor-app/src-tauri/src/commands/system.rs @@ -75,10 +75,35 @@ pub fn open_file_with_default_app(file_path: String) -> Result<(), String> { /// Show file in system file explorer #[tauri::command] pub fn show_in_folder(file_path: String) -> Result<(), String> { + println!("[show_in_folder] Received path: {}", file_path); + #[cfg(target_os = "windows")] { + use std::path::Path; + + // Normalize path separators for Windows + // 规范化路径分隔符 + let normalized_path = file_path.replace('/', "\\"); + println!("[show_in_folder] Normalized path: {}", normalized_path); + + // Verify the path exists before trying to show it + // 验证路径存在 + let path = Path::new(&normalized_path); + let exists = path.exists(); + println!("[show_in_folder] Path exists: {}", exists); + + if !exists { + return Err(format!("Path does not exist: {}", normalized_path)); + } + + // Windows explorer requires /select, to be concatenated with the path + // without spaces. Use a single argument to avoid shell parsing issues. + // Windows 资源管理器要求 /select, 与路径连接在一起,中间没有空格 + let select_arg = format!("/select,{}", normalized_path); + println!("[show_in_folder] Explorer arg: {}", select_arg); + Command::new("explorer") - .args(["/select,", &file_path]) + .arg(&select_arg) .spawn() .map_err(|e| format!("Failed to show in folder: {}", e))?; } @@ -117,6 +142,55 @@ pub fn get_temp_dir() -> Result { .ok_or_else(|| "Failed to get temp directory".to_string()) } +/// Open project folder with specified editor +/// 使用指定编辑器打开项目文件夹 +/// +/// @param project_path - Project folder path | 项目文件夹路径 +/// @param editor_command - Editor command (e.g., "code", "cursor") | 编辑器命令 +/// @param file_path - Optional file to open (will be opened in the editor) | 可选的要打开的文件 +#[tauri::command] +pub fn open_with_editor( + project_path: String, + editor_command: String, + file_path: Option, +) -> Result<(), String> { + use std::path::Path; + + // Normalize paths + let normalized_project = project_path.replace('/', "\\"); + let normalized_file = file_path.map(|f| f.replace('/', "\\")); + + // Verify project path exists + let project = Path::new(&normalized_project); + if !project.exists() { + return Err(format!("Project path does not exist: {}", normalized_project)); + } + + println!( + "[open_with_editor] editor: {}, project: {}, file: {:?}", + editor_command, normalized_project, normalized_file + ); + + let mut cmd = Command::new(&editor_command); + + // Add project folder as first argument + cmd.arg(&normalized_project); + + // If a specific file is provided, add it as a second argument + // Most editors support: editor + if let Some(ref file) = normalized_file { + let file_path = Path::new(file); + if file_path.exists() { + cmd.arg(file); + } + } + + cmd.spawn() + .map_err(|e| format!("Failed to open with editor '{}': {}", editor_command, e))?; + + Ok(()) +} + /// Get application resource directory #[tauri::command] pub fn get_app_resource_dir(app: AppHandle) -> Result { @@ -138,6 +212,97 @@ pub fn get_current_dir() -> Result { .map_err(|e| format!("Failed to get current directory: {}", e)) } +/// Copy type definitions to project for IDE intellisense +/// 复制类型定义文件到项目以支持 IDE 智能感知 +#[tauri::command] +pub fn copy_type_definitions(app: AppHandle, project_path: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let project = Path::new(&project_path); + if !project.exists() { + return Err(format!("Project path does not exist: {}", project_path)); + } + + // Create types directory in project + // 在项目中创建 types 目录 + let types_dir = project.join("types"); + if !types_dir.exists() { + fs::create_dir_all(&types_dir) + .map_err(|e| format!("Failed to create types directory: {}", e))?; + } + + // Get resource directory (where bundled files are) + // 获取资源目录(打包文件所在位置) + let resource_dir = app.path() + .resource_dir() + .map_err(|e| format!("Failed to get resource directory: {}", e))?; + + // Type definition files to copy + // 要复制的类型定义文件 + // Format: (resource_path, workspace_path, dest_name) + // 格式:(资源路径,工作区路径,目标文件名) + // Note: resource_path is relative to Tauri resource dir (runtime/ is mapped to .) + // 注意:resource_path 相对于 Tauri 资源目录(runtime/ 映射到 .) + let type_files = [ + ("types/ecs-framework.d.ts", "packages/core/dist/index.d.ts", "ecs-framework.d.ts"), + ("types/engine-core.d.ts", "packages/engine-core/dist/index.d.ts", "engine-core.d.ts"), + ]; + + // Try to find workspace root (for development mode) + // 尝试查找工作区根目录(用于开发模式) + let workspace_root = std::env::current_dir() + .ok() + .and_then(|cwd| { + // Look for pnpm-workspace.yaml or package.json in parent directories + // 在父目录中查找 pnpm-workspace.yaml 或 package.json + let mut dir = cwd.as_path(); + loop { + if dir.join("pnpm-workspace.yaml").exists() { + return Some(dir.to_path_buf()); + } + match dir.parent() { + Some(parent) => dir = parent, + None => return None, + } + } + }); + + let mut copied_count = 0; + for (resource_relative, workspace_relative, dest_name) in type_files { + let dest_path = types_dir.join(dest_name); + + // Try resource directory first (production mode) + // 首先尝试资源目录(生产模式) + let src_path = resource_dir.join(resource_relative); + if src_path.exists() { + fs::copy(&src_path, &dest_path) + .map_err(|e| format!("Failed to copy {}: {}", resource_relative, e))?; + println!("[copy_type_definitions] Copied {} to {}", src_path.display(), dest_path.display()); + copied_count += 1; + continue; + } + + // Try workspace directory (development mode) + // 尝试工作区目录(开发模式) + if let Some(ref ws_root) = workspace_root { + let ws_src_path = ws_root.join(workspace_relative); + if ws_src_path.exists() { + fs::copy(&ws_src_path, &dest_path) + .map_err(|e| format!("Failed to copy {}: {}", workspace_relative, e))?; + println!("[copy_type_definitions] Copied {} to {} (dev mode)", ws_src_path.display(), dest_path.display()); + copied_count += 1; + continue; + } + } + + println!("[copy_type_definitions] {} not found, skipping", dest_name); + } + + println!("[copy_type_definitions] Copied {} type definition files to {}", copied_count, types_dir.display()); + Ok(()) +} + /// Start a local HTTP server for runtime preview #[tauri::command] pub fn start_local_server(root_path: String, port: u16) -> Result { diff --git a/packages/editor-app/src-tauri/src/main.rs b/packages/editor-app/src-tauri/src/main.rs index db68af26..cac4365e 100644 --- a/packages/editor-app/src-tauri/src/main.rs +++ b/packages/editor-app/src-tauri/src/main.rs @@ -81,6 +81,8 @@ fn main() { commands::open_file_with_default_app, commands::show_in_folder, commands::get_temp_dir, + commands::open_with_editor, + commands::copy_type_definitions, commands::get_app_resource_dir, commands::get_current_dir, commands::start_local_server, diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 29bf6c0a..6537ece9 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -381,6 +381,14 @@ function App() { // 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件) await TauriAPI.setProjectBasePath(projectPath); + // 复制类型定义到项目,用于 IDE 智能感知 + // Copy type definitions to project for IDE intellisense + try { + await TauriAPI.copyTypeDefinitions(projectPath); + } catch (e) { + console.warn('[App] Failed to copy type definitions:', e); + } + const settings = SettingsService.getInstance(); settings.addRecentProject(projectPath); @@ -465,7 +473,9 @@ function App() { }; const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => { - const fullProjectPath = `${projectPath}\\${projectName}`; + // 使用与 projectPath 相同的路径分隔符 | Use same separator as projectPath + const sep = projectPath.includes('/') ? '/' : '\\'; + const fullProjectPath = `${projectPath}${sep}${projectName}`; try { setIsLoading(true); @@ -631,6 +641,13 @@ function App() { await pluginLoader.unloadProjectPlugins(pluginManager); } + // 清理场景(会清理所有实体和系统) + // Clear scene (clears all entities and systems) + const scene = Core.scene; + if (scene) { + scene.end(); + } + // 清理模块系统 const engineService = EngineService.getInstance(); engineService.clearModuleSystems(); @@ -817,6 +834,28 @@ function App() { onOpenProject={handleOpenProject} onCreateProject={handleCreateProject} onOpenRecentProject={handleOpenRecentProject} + onRemoveRecentProject={(projectPath) => { + settings.removeRecentProject(projectPath); + // 强制重新渲染 | Force re-render + setStatus(t('header.status.ready')); + }} + onDeleteProject={async (projectPath) => { + try { + await TauriAPI.deleteFolder(projectPath); + // 删除成功后从列表中移除并触发重新渲染 + // Remove from list and trigger re-render after successful deletion + settings.removeRecentProject(projectPath); + setStatus(t('header.status.ready')); + } catch (error) { + console.error('Failed to delete project:', error); + setErrorDialog({ + title: locale === 'zh' ? '删除项目失败' : 'Failed to Delete Project', + message: locale === 'zh' + ? `无法删除项目:\n${error instanceof Error ? error.message : String(error)}` + : `Failed to delete project:\n${error instanceof Error ? error.message : String(error)}` + }); + } + }} onLocaleChange={handleLocaleChange} recentProjects={recentProjects} locale={locale} diff --git a/packages/editor-app/src/api/tauri.ts b/packages/editor-app/src/api/tauri.ts index 4ba68925..58dd03ff 100644 --- a/packages/editor-app/src/api/tauri.ts +++ b/packages/editor-app/src/api/tauri.ts @@ -168,6 +168,26 @@ export class TauriAPI { await invoke('show_in_folder', { filePath: path }); } + /** + * 使用指定编辑器打开项目 + * Open project with specified editor + * + * @param projectPath 项目文件夹路径 | Project folder path + * @param editorCommand 编辑器命令(如 "code", "cursor")| Editor command + * @param filePath 可选的要打开的文件路径 | Optional file path to open + */ + static async openWithEditor( + projectPath: string, + editorCommand: string, + filePath?: string + ): Promise { + await invoke('open_with_editor', { + projectPath, + editorCommand, + filePath: filePath || null + }); + } + /** * 打开行为树文件选择对话框 * @returns 用户选择的文件路径,取消则返回 null @@ -311,6 +331,16 @@ export class TauriAPI { return await invoke('generate_qrcode', { text }); } + /** + * 复制类型定义文件到项目 + * Copy type definition files to project for IDE intellisense + * + * @param projectPath 项目路径 | Project path + */ + static async copyTypeDefinitions(projectPath: string): Promise { + return await invoke('copy_type_definitions', { projectPath }); + } + /** * 将本地文件路径转换为 Tauri 可访问的 asset URL * @param filePath 本地文件路径 diff --git a/packages/editor-app/src/app/managers/PluginInstaller.ts b/packages/editor-app/src/app/managers/PluginInstaller.ts index ef4fa3ea..c5361a9d 100644 --- a/packages/editor-app/src/app/managers/PluginInstaller.ts +++ b/packages/editor-app/src/app/managers/PluginInstaller.ts @@ -14,6 +14,7 @@ import { SceneInspectorPlugin } from '../../plugins/builtin/SceneInspectorPlugin import { ProfilerPlugin } from '../../plugins/builtin/ProfilerPlugin'; import { EditorAppearancePlugin } from '../../plugins/builtin/EditorAppearancePlugin'; import { ProjectSettingsPlugin } from '../../plugins/builtin/ProjectSettingsPlugin'; +import { AssetMetaPlugin } from '../../plugins/builtin/AssetMetaPlugin'; // Note: PluginConfigPlugin removed - module management is now unified in ProjectSettingsPlugin // 统一模块插件(从编辑器包导入完整插件,包含 runtime + editor) @@ -38,6 +39,7 @@ export class PluginInstaller { { name: 'ProfilerPlugin', plugin: ProfilerPlugin }, { name: 'EditorAppearancePlugin', plugin: EditorAppearancePlugin }, { name: 'ProjectSettingsPlugin', plugin: ProjectSettingsPlugin }, + { name: 'AssetMetaPlugin', plugin: AssetMetaPlugin }, ]; for (const { name, plugin } of builtinPlugins) { diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 0fb3d6ce..c788ba01 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -40,6 +40,7 @@ import { import { Core } from '@esengine/ecs-framework'; import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; +import { SettingsService } from '../services/SettingsService'; import { ContextMenu, ContextMenuItem } from './ContextMenu'; import { PromptDialog } from './PromptDialog'; import '../styles/ContentBrowser.css'; @@ -210,8 +211,124 @@ export function ContentBrowser({ 'Shader': { en: 'Shader', zh: '着色器' }, 'Tilemap': { en: 'Tilemap', zh: '瓦片地图' }, 'Tileset': { en: 'Tileset', zh: '瓦片集' }, + 'Component': { en: 'Component', zh: '组件' }, + 'System': { en: 'System', zh: '系统' }, + 'TypeScript': { en: 'TypeScript', zh: 'TypeScript' }, }; + // 注册内置的 TypeScript 文件创建模板 + // Register built-in TypeScript file creation templates + useEffect(() => { + if (!fileActionRegistry) return; + + const builtinTemplates: FileCreationTemplate[] = [ + { + id: 'ts-component', + label: 'Component', + extension: '.ts', + icon: 'FileCode', + category: 'Script', + getContent: (fileName: string) => { + const className = fileName.replace(/\.ts$/, ''); + return `import { Component, ECSComponent, Property, Serialize, Serializable } from '@esengine/ecs-framework'; + +/** + * ${className} + */ +@ECSComponent('${className}') +@Serializable({ version: 1, typeId: '${className}' }) +export class ${className} extends Component { + // 在这里添加组件属性 + // Add component properties here + + @Serialize() + @Property({ type: 'number', label: 'Example Property' }) + public exampleProperty: number = 0; + + onInitialize(): void { + // 组件初始化时调用 + // Called when component is initialized + } + + onDestroy(): void { + // 组件销毁时调用 + // Called when component is destroyed + } +} +`; + } + }, + { + id: 'ts-system', + label: 'System', + extension: '.ts', + icon: 'FileCode', + category: 'Script', + getContent: (fileName: string) => { + const className = fileName.replace(/\.ts$/, ''); + return `import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework'; + +/** + * ${className} + */ +export class ${className} extends EntitySystem { + // 定义系统处理的组件类型 + // Define component types this system processes + protected getMatcher(): Matcher { + // 返回匹配器,指定需要哪些组件 + // Return matcher specifying required components + // return Matcher.all(SomeComponent); + return Matcher.empty(); + } + + protected updateEntity(entity: Entity, deltaTime: number): void { + // 处理每个实体 + // Process each entity + } + + // 可选:系统初始化 + // Optional: System initialization + // onInitialize(): void { + // super.onInitialize(); + // } +} +`; + } + }, + { + id: 'ts-script', + label: 'TypeScript', + extension: '.ts', + icon: 'FileCode', + category: 'Script', + getContent: (fileName: string) => { + const name = fileName.replace(/\.ts$/, ''); + return `/** + * ${name} + */ + +export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void { + // 在这里编写代码 + // Write your code here +} +`; + } + } + ]; + + // 注册模板 + for (const template of builtinTemplates) { + fileActionRegistry.registerCreationTemplate(template); + } + + // 清理函数 + return () => { + for (const template of builtinTemplates) { + fileActionRegistry.unregisterCreationTemplate(template); + } + }; + }, [fileActionRegistry]); + const getTemplateLabel = (label: string): string => { const mapping = templateLabels[label]; if (mapping) { @@ -439,6 +556,24 @@ export function ContentBrowser({ return; } + // 脚本文件使用配置的编辑器打开 + // Open script files with configured editor + if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') { + const settings = SettingsService.getInstance(); + const editorCommand = settings.getScriptEditorCommand(); + + if (editorCommand && projectPath) { + try { + await TauriAPI.openWithEditor(projectPath, editorCommand, asset.path); + return; + } catch (error) { + console.error('Failed to open with editor:', error); + // 如果失败,回退到系统默认应用 + // Fall back to system default app if failed + } + } + } + if (fileActionRegistry) { const handled = await fileActionRegistry.handleDoubleClick(asset.path); if (handled) return; @@ -450,7 +585,7 @@ export function ContentBrowser({ console.error('Failed to open file:', error); } } - }, [loadAssets, onOpenScene, fileActionRegistry]); + }, [loadAssets, onOpenScene, fileActionRegistry, projectPath]); // Handle context menu const handleContextMenu = useCallback((e: React.MouseEvent, asset?: AssetItem) => { @@ -799,9 +934,10 @@ export function ContentBrowser({ icon: , onClick: async () => { try { + console.log('[ContentBrowser] showInFolder path:', asset.path); await TauriAPI.showInFolder(asset.path); } catch (error) { - console.error('Failed to show in folder:', error); + console.error('Failed to show in folder:', error, 'Path:', asset.path); } } }); @@ -1126,10 +1262,16 @@ export function ContentBrowser({ )} {/* Create File Dialog */} - {createFileDialog && ( + {createFileDialog && (() => { + // 规范化扩展名(确保有点号前缀) + // Normalize extension (ensure dot prefix) + const ext = createFileDialog.template.extension.startsWith('.') + ? createFileDialog.template.extension + : `.${createFileDialog.template.extension}`; + return ( setCreateFileDialog(null)} /> - )} + ); + })()} ); } diff --git a/packages/editor-app/src/components/FileTree.tsx b/packages/editor-app/src/components/FileTree.tsx index 74c589fd..be3a2c44 100644 --- a/packages/editor-app/src/components/FileTree.tsx +++ b/packages/editor-app/src/components/FileTree.tsx @@ -833,9 +833,10 @@ export const FileTree = forwardRef(({ rootPath, o icon: , onClick: async () => { try { + console.log('[FileTree] showInFolder path:', node.path); await TauriAPI.showInFolder(node.path); } catch (error) { - console.error('Failed to show in folder:', error); + console.error('Failed to show in folder:', error, 'Path:', node.path); } } }); diff --git a/packages/editor-app/src/components/StartupPage.tsx b/packages/editor-app/src/components/StartupPage.tsx index f39e1541..95700d96 100644 --- a/packages/editor-app/src/components/StartupPage.tsx +++ b/packages/editor-app/src/components/StartupPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { getVersion } from '@tauri-apps/api/app'; -import { Globe, ChevronDown, Download, X, Loader2 } from 'lucide-react'; +import { Globe, ChevronDown, Download, X, Loader2, Trash2 } from 'lucide-react'; import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater'; import { StartupLogo } from './StartupLogo'; import '../styles/StartupPage.css'; @@ -11,6 +11,8 @@ interface StartupPageProps { onOpenProject: () => void; onCreateProject: () => void; onOpenRecentProject?: (projectPath: string) => void; + onRemoveRecentProject?: (projectPath: string) => void; + onDeleteProject?: (projectPath: string) => Promise; onLocaleChange?: (locale: Locale) => void; recentProjects?: string[]; locale: string; @@ -21,11 +23,13 @@ const LANGUAGES = [ { code: 'zh', name: '中文' } ]; -export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { +export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { const [showLogo, setShowLogo] = useState(true); const [hoveredProject, setHoveredProject] = useState(null); const [appVersion, setAppVersion] = useState(''); const [showLangMenu, setShowLangMenu] = useState(false); + const [contextMenu, setContextMenu] = useState<{ x: number; y: number; project: string } | null>(null); + const [deleteConfirm, setDeleteConfirm] = useState(null); const [updateInfo, setUpdateInfo] = useState(null); const [showUpdateBanner, setShowUpdateBanner] = useState(false); const [isInstalling, setIsInstalling] = useState(false); @@ -66,7 +70,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec updateAvailable: 'New version available', updateNow: 'Update Now', installing: 'Installing...', - later: 'Later' + later: 'Later', + removeFromList: 'Remove from List', + deleteProject: 'Delete Project', + deleteConfirmTitle: 'Delete Project', + deleteConfirmMessage: 'Are you sure you want to permanently delete this project? This action cannot be undone.', + cancel: 'Cancel', + delete: 'Delete' }, zh: { title: 'ESEngine 编辑器', @@ -78,7 +88,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec updateAvailable: '发现新版本', updateNow: '立即更新', installing: '正在安装...', - later: '稍后' + later: '稍后', + removeFromList: '从列表中移除', + deleteProject: '删除项目', + deleteConfirmTitle: '删除项目', + deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。', + cancel: '取消', + delete: '删除' } }; @@ -136,6 +152,10 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec onMouseEnter={() => setHoveredProject(project)} onMouseLeave={() => setHoveredProject(null)} onClick={() => onOpenRecentProject?.(project)} + onContextMenu={(e) => { + e.preventDefault(); + setContextMenu({ x: e.clientX, y: e.clientY, project }); + }} style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }} > @@ -145,6 +165,18 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
{project.split(/[\\/]/).pop()}
{project}
+ {onRemoveRecentProject && ( + + )} ))} @@ -217,6 +249,78 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec )} + + {/* 右键菜单 | Context Menu */} + {contextMenu && ( +
setContextMenu(null)} + > +
e.stopPropagation()} + > + + {onDeleteProject && ( + + )} +
+
+ )} + + {/* 删除确认对话框 | Delete Confirmation Dialog */} + {deleteConfirm && ( +
+
+
+ +

{t.deleteConfirmTitle}

+
+
+

{t.deleteConfirmMessage}

+

{deleteConfirm}

+
+
+ + +
+
+
+ )} ); } diff --git a/packages/editor-app/src/hooks/useEngine.ts b/packages/editor-app/src/hooks/useEngine.ts index 5b67bd68..13bc3f2a 100644 --- a/packages/editor-app/src/hooks/useEngine.ts +++ b/packages/editor-app/src/hooks/useEngine.ts @@ -14,6 +14,15 @@ import { EditorEngineSync } from '../services/EditorEngineSync'; let engineInitialized = false; let engineInitializing = false; +/** + * 重置引擎初始化状态(在项目关闭时调用) + * Reset engine initialization state (called when project is closed) + */ +export function resetEngineState(): void { + engineInitialized = false; + engineInitializing = false; +} + export interface EngineState { initialized: boolean; running: boolean; diff --git a/packages/editor-app/src/plugins/builtin/AssetMetaPlugin.ts b/packages/editor-app/src/plugins/builtin/AssetMetaPlugin.ts new file mode 100644 index 00000000..c026fa06 --- /dev/null +++ b/packages/editor-app/src/plugins/builtin/AssetMetaPlugin.ts @@ -0,0 +1,67 @@ +/** + * Asset Meta Plugin + * 资产元数据插件 + * + * Handles .meta file generation for project assets. + * 处理项目资产的 .meta 文件生成。 + */ + +import type { ServiceContainer } from '@esengine/ecs-framework'; +import { createLogger } from '@esengine/ecs-framework'; +import type { IPlugin, IEditorModuleLoader, ModuleManifest } from '@esengine/editor-core'; +import { AssetRegistryService } from '@esengine/editor-core'; + +const logger = createLogger('AssetMetaPlugin'); + +/** + * Asset Meta Editor Module + * 资产元数据编辑器模块 + */ +class AssetMetaEditorModule implements IEditorModuleLoader { + private _assetRegistry: AssetRegistryService | null = null; + + async install(_services: ServiceContainer): Promise { + // 创建 AssetRegistryService 并初始化 + // Create AssetRegistryService and initialize + this._assetRegistry = new AssetRegistryService(); + + // 初始化服务(订阅 project:opened 事件) + // Initialize service (subscribes to project:opened event) + await this._assetRegistry.initialize(); + + logger.info('AssetRegistryService initialized'); + } + + async uninstall(): Promise { + if (this._assetRegistry) { + this._assetRegistry.unloadProject(); + this._assetRegistry = null; + } + logger.info('Uninstalled'); + } + + async onEditorReady(): Promise { + logger.info('Editor is ready'); + } +} + +const manifest: ModuleManifest = { + id: '@esengine/asset-meta', + name: '@esengine/asset-meta', + displayName: 'Asset Meta', + version: '1.0.0', + description: 'Generates .meta files for project assets | 为项目资产生成 .meta 文件', + category: 'Other', + icon: 'FileJson', + isCore: true, + defaultEnabled: true, + isEngineModule: false, // 不是引擎模块,不会被打包到 runtime + canContainContent: false, + dependencies: [], + exports: {} +}; + +export const AssetMetaPlugin: IPlugin = { + manifest, + editorModule: new AssetMetaEditorModule() +}; diff --git a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx index c949f8e7..a1f720d6 100644 --- a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx @@ -56,6 +56,32 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader { step: 1 } ] + }, + { + id: 'scriptEditor', + title: '脚本编辑器', + description: '配置用于打开脚本文件的外部编辑器', + settings: [ + { + key: 'editor.scriptEditor', + label: '脚本编辑器', + type: 'select', + defaultValue: 'system', + description: '双击脚本文件时使用的编辑器', + options: SettingsService.SCRIPT_EDITORS.map(editor => ({ + value: editor.id, + label: editor.name + })) + }, + { + key: 'editor.customScriptEditorCommand', + label: '自定义编辑器命令', + type: 'string', + defaultValue: '', + description: '当选择"自定义"时,填写编辑器的命令行命令(如 notepad++)', + placeholder: '例如:notepad++' + } + ] } ] }); diff --git a/packages/editor-app/src/plugins/builtin/index.ts b/packages/editor-app/src/plugins/builtin/index.ts index a880462c..31634f41 100644 --- a/packages/editor-app/src/plugins/builtin/index.ts +++ b/packages/editor-app/src/plugins/builtin/index.ts @@ -8,6 +8,7 @@ export { SceneInspectorPlugin } from './SceneInspectorPlugin'; export { ProfilerPlugin } from './ProfilerPlugin'; export { EditorAppearancePlugin } from './EditorAppearancePlugin'; export { ProjectSettingsPlugin } from './ProjectSettingsPlugin'; +export { AssetMetaPlugin } from './AssetMetaPlugin'; // Note: PluginConfigPlugin removed - module management is now unified in ProjectSettingsPlugin // TODO: Re-enable when blueprint-editor package is fixed // export { BlueprintPlugin } from '@esengine/blueprint-editor'; diff --git a/packages/editor-app/src/services/EngineService.ts b/packages/editor-app/src/services/EngineService.ts index c89273d5..ec173cd8 100644 --- a/packages/editor-app/src/services/EngineService.ts +++ b/packages/editor-app/src/services/EngineService.ts @@ -28,6 +28,7 @@ import { type GameRuntimeConfig } from '@esengine/runtime-core'; import { getMaterialManager } from '@esengine/material-system'; +import { resetEngineState } from '../hooks/useEngine'; import { convertFileSrc } from '@tauri-apps/api/core'; import { IdGenerator } from '../utils/idGenerator'; import { TauriAssetReader } from './TauriAssetReader'; @@ -245,7 +246,14 @@ export class EngineService { ctx.uiInputSystem.unbind?.(); } + // 清理 viewport | Clear viewport + this.unregisterViewport('editor-viewport'); + + // 重置 useEngine 的模块级状态 | Reset useEngine module-level state + resetEngineState(); + this._modulesInitialized = false; + this._initialized = false; } /** diff --git a/packages/editor-app/src/services/SettingsService.ts b/packages/editor-app/src/services/SettingsService.ts index 5b9105c0..b403a753 100644 --- a/packages/editor-app/src/services/SettingsService.ts +++ b/packages/editor-app/src/services/SettingsService.ts @@ -70,9 +70,11 @@ export class SettingsService { } public addRecentProject(projectPath: string): void { + // 规范化路径,防止双重转义 | Normalize path to prevent double escaping + const normalizedPath = projectPath.replace(/\\\\/g, '\\'); const recentProjects = this.getRecentProjects(); - const filtered = recentProjects.filter((p) => p !== projectPath); - const updated = [projectPath, ...filtered].slice(0, 10); + const filtered = recentProjects.filter((p) => p !== normalizedPath); + const updated = [normalizedPath, ...filtered].slice(0, 10); this.set('recentProjects', updated); } @@ -85,4 +87,64 @@ export class SettingsService { public clearRecentProjects(): void { this.set('recentProjects', []); } + + // ==================== Script Editor Settings ==================== + + /** + * 支持的脚本编辑器类型 + * Supported script editor types + */ + public static readonly SCRIPT_EDITORS = [ + { id: 'system', name: 'System Default', nameZh: '系统默认', command: '' }, + { id: 'vscode', name: 'Visual Studio Code', nameZh: 'Visual Studio Code', command: 'code' }, + { id: 'cursor', name: 'Cursor', nameZh: 'Cursor', command: 'cursor' }, + { id: 'webstorm', name: 'WebStorm', nameZh: 'WebStorm', command: 'webstorm' }, + { id: 'sublime', name: 'Sublime Text', nameZh: 'Sublime Text', command: 'subl' }, + { id: 'custom', name: 'Custom', nameZh: '自定义', command: '' } + ]; + + /** + * 获取脚本编辑器设置 + * Get script editor setting + */ + public getScriptEditor(): string { + return this.get('editor.scriptEditor', 'system'); + } + + /** + * 设置脚本编辑器 + * Set script editor + */ + public setScriptEditor(editorId: string): void { + this.set('editor.scriptEditor', editorId); + } + + /** + * 获取自定义脚本编辑器命令 + * Get custom script editor command + */ + public getCustomScriptEditorCommand(): string { + return this.get('editor.customScriptEditorCommand', ''); + } + + /** + * 设置自定义脚本编辑器命令 + * Set custom script editor command + */ + public setCustomScriptEditorCommand(command: string): void { + this.set('editor.customScriptEditorCommand', command); + } + + /** + * 获取当前脚本编辑器的命令 + * Get current script editor command + */ + public getScriptEditorCommand(): string { + const editorId = this.getScriptEditor(); + if (editorId === 'custom') { + return this.getCustomScriptEditorCommand(); + } + const editor = SettingsService.SCRIPT_EDITORS.find(e => e.id === editorId); + return editor?.command || ''; + } } diff --git a/packages/editor-app/src/services/TauriFileSystemService.ts b/packages/editor-app/src/services/TauriFileSystemService.ts index b886ba9b..91cb6cb3 100644 --- a/packages/editor-app/src/services/TauriFileSystemService.ts +++ b/packages/editor-app/src/services/TauriFileSystemService.ts @@ -22,10 +22,19 @@ export class TauriFileSystemService implements IFileSystem { async exists(path: string): Promise { try { - await invoke('read_file_content', { path }); + // 首先尝试作为目录列出内容 + // First try to list as directory + await invoke('list_directory', { path }); return true; } catch { - return false; + // 如果不是目录,尝试读取文件 + // If not a directory, try reading as file + try { + await invoke('read_file_content', { path }); + return true; + } catch { + return false; + } } } @@ -34,11 +43,19 @@ export class TauriFileSystemService implements IFileSystem { } async listDirectory(path: string): Promise { - const entries = await invoke>('list_directory', { path }); + const entries = await invoke>('list_directory', { path }); return entries.map((entry) => ({ name: entry.name, isDirectory: entry.is_dir, - path: entry.path + path: entry.path, + size: entry.size, + modified: entry.modified ? new Date(entry.modified * 1000) : undefined })); } diff --git a/packages/editor-app/src/styles/StartupPage.css b/packages/editor-app/src/styles/StartupPage.css index 2d10a33a..fd50aaac 100644 --- a/packages/editor-app/src/styles/StartupPage.css +++ b/packages/editor-app/src/styles/StartupPage.css @@ -174,6 +174,32 @@ white-space: nowrap; } +.recent-remove-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background: transparent; + border: none; + border-radius: 4px; + color: #6e6e6e; + cursor: pointer; + opacity: 0; + transition: all 0.15s; + flex-shrink: 0; +} + +.recent-item:hover .recent-remove-btn { + opacity: 1; +} + +.recent-remove-btn:hover { + background: rgba(255, 80, 80, 0.15); + color: #f87171; +} + .startup-footer { display: flex; align-items: center; @@ -346,3 +372,157 @@ opacity: 0.5; cursor: not-allowed; } + +/* 右键菜单样式 | Context Menu Styles */ +.startup-context-menu-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; +} + +.startup-context-menu { + position: fixed; + min-width: 180px; + background: #252529; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + padding: 4px 0; + z-index: 1001; +} + +.startup-context-menu-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 14px; + background: transparent; + border: none; + color: #cccccc; + font-size: 13px; + text-align: left; + cursor: pointer; + transition: all 0.1s; +} + +.startup-context-menu-item:hover { + background: #3b82f6; + color: #ffffff; +} + +.startup-context-menu-item.danger { + color: #f87171; +} + +.startup-context-menu-item.danger:hover { + background: #dc2626; + color: #ffffff; +} + +/* 对话框样式 | Dialog Styles */ +.startup-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1002; +} + +.startup-dialog { + width: 400px; + background: #2d2d30; + border: 1px solid #3e3e42; + border-radius: 8px; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +.startup-dialog-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: #252526; + border-bottom: 1px solid #3e3e42; +} + +.startup-dialog-header h3 { + margin: 0; + font-size: 15px; + font-weight: 500; + color: #ffffff; +} + +.dialog-icon-danger { + color: #f87171; +} + +.startup-dialog-body { + padding: 20px; +} + +.startup-dialog-body p { + margin: 0 0 12px 0; + font-size: 13px; + color: #cccccc; + line-height: 1.5; +} + +.startup-dialog-body p:last-child { + margin-bottom: 0; +} + +.startup-dialog-path { + padding: 10px 12px; + background: #1e1e1e; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + color: #858585; + word-break: break-all; +} + +.startup-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 16px 20px; + background: #252526; + border-top: 1px solid #3e3e42; +} + +.startup-dialog-btn { + padding: 8px 16px; + border: 1px solid #3e3e42; + border-radius: 4px; + background: #2d2d30; + color: #cccccc; + font-size: 13px; + cursor: pointer; + transition: all 0.15s; +} + +.startup-dialog-btn:hover { + background: #37373d; + border-color: #555; +} + +.startup-dialog-btn.danger { + background: #dc2626; + border-color: #dc2626; + color: #ffffff; +} + +.startup-dialog-btn.danger:hover { + background: #b91c1c; + border-color: #b91c1c; +} diff --git a/packages/editor-core/src/Plugin/PluginManager.ts b/packages/editor-core/src/Plugin/PluginManager.ts index 5211c243..7d316ae7 100644 --- a/packages/editor-core/src/Plugin/PluginManager.ts +++ b/packages/editor-core/src/Plugin/PluginManager.ts @@ -1159,6 +1159,11 @@ export class PluginManager implements IService { } } } + + // 重置初始化状态,允许下次重新初始化运行时 + // Reset initialized flag to allow re-initialization + this.initialized = false; + logger.debug('Scene systems cleared, runtime can be re-initialized'); } /** diff --git a/packages/editor-core/src/Services/AssetRegistryService.ts b/packages/editor-core/src/Services/AssetRegistryService.ts index 7d30f756..7e36804b 100644 --- a/packages/editor-core/src/Services/AssetRegistryService.ts +++ b/packages/editor-core/src/Services/AssetRegistryService.ts @@ -20,6 +20,8 @@ import { IMetaFileSystem, inferAssetType } from '@esengine/asset-system-editor'; +import type { IFileSystem, FileEntry } from './IFileSystem'; +import { IFileSystemService } from './IFileSystem'; // Logger for AssetRegistry using core's logger const logger = createLogger('AssetRegistry'); @@ -114,6 +116,9 @@ const EXTENSION_TYPE_MAP: Record = { // Data '.json': 'json', '.txt': 'text', + // Scripts + '.ts': 'script', + '.js': 'script', // Custom types '.btree': 'btree', '.ecs': 'scene', @@ -122,17 +127,8 @@ const EXTENSION_TYPE_MAP: Record = { '.tsx': 'tileset', }; -/** - * File system interface for asset scanning - */ -interface IFileSystem { - readDir(path: string): Promise; - readFile(path: string): Promise; - writeFile(path: string, content: string): Promise; - exists(path: string): Promise; - stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean }>; - isDirectory(path: string): Promise; -} +// 使用从 IFileSystem.ts 导入的标准接口 +// Using standard interface imported from IFileSystem.ts /** * Simple in-memory asset database @@ -243,9 +239,9 @@ export class AssetRegistryService { async initialize(): Promise { if (this._initialized) return; - // Get file system service - const IFileSystemServiceKey = Symbol.for('IFileSystemService'); - this._fileSystem = Core.services.tryResolve(IFileSystemServiceKey) as IFileSystem | null; + // Get file system service using the exported Symbol + // 使用导出的 Symbol 获取文件系统服务 + this._fileSystem = Core.services.tryResolve(IFileSystemService) as IFileSystem | null; // Get message hub this._messageHub = Core.services.tryResolve(MessageHub) as MessageHub | null; @@ -254,10 +250,11 @@ export class AssetRegistryService { if (this._messageHub) { this._messageHub.subscribe('project:opened', this._onProjectOpened.bind(this)); this._messageHub.subscribe('project:closed', this._onProjectClosed.bind(this)); + } else { + logger.warn('MessageHub not available, cannot subscribe to project events'); } this._initialized = true; - logger.info('AssetRegistryService initialized'); } /** @@ -288,18 +285,16 @@ export class AssetRegistryService { this._metaManager.clear(); // Setup MetaManager with file system adapter + // 设置 MetaManager 的文件系统适配器 const metaFs: IMetaFileSystem = { exists: (path: string) => this._fileSystem!.exists(path), readText: (path: string) => this._fileSystem!.readFile(path), writeText: (path: string, content: string) => this._fileSystem!.writeFile(path, content), delete: async (path: string) => { - // Try to delete, ignore if not exists + // Try to delete using deleteFile + // 尝试使用 deleteFile 删除 try { - // Note: IFileSystem may not have delete, handle gracefully - const fs = this._fileSystem as IFileSystem & { delete?: (p: string) => Promise }; - if (fs.delete) { - await fs.delete(path); - } + await this._fileSystem!.deleteFile(path); } catch { // Ignore delete errors } @@ -398,53 +393,61 @@ export class AssetRegistryService { } /** - * Scan assets directory and register all assets + * Scan all project directories for assets + * 扫描项目中所有目录的资产 */ private async _scanAssetsDirectory(): Promise { if (!this._fileSystem || !this._projectPath) return; const sep = this._projectPath.includes('\\') ? '\\' : '/'; - const assetsPath = `${this._projectPath}${sep}assets`; - try { - const exists = await this._fileSystem.exists(assetsPath); - if (!exists) { - logger.info('No assets directory found'); - return; + // 扫描多个目录:assets, scripts, scenes + // Scan multiple directories: assets, scripts, scenes + const directoriesToScan = [ + { path: `${this._projectPath}${sep}assets`, name: 'assets' }, + { path: `${this._projectPath}${sep}scripts`, name: 'scripts' }, + { path: `${this._projectPath}${sep}scenes`, name: 'scenes' } + ]; + + for (const dir of directoriesToScan) { + try { + const exists = await this._fileSystem.exists(dir.path); + if (!exists) continue; + + await this._scanDirectory(dir.path, dir.name); + } catch (error) { + logger.error(`Failed to scan ${dir.name} directory:`, error); } - - await this._scanDirectory(assetsPath, 'assets'); - } catch (error) { - logger.error('Failed to scan assets directory:', error); } } /** * Recursively scan a directory + * 递归扫描目录 */ private async _scanDirectory(absolutePath: string, relativePath: string): Promise { if (!this._fileSystem) return; try { - const entries = await this._fileSystem.readDir(absolutePath); + // 使用标准 IFileSystem.listDirectory + // Use standard IFileSystem.listDirectory + const entries: FileEntry[] = await this._fileSystem.listDirectory(absolutePath); const sep = absolutePath.includes('\\') ? '\\' : '/'; for (const entry of entries) { - const entryAbsPath = `${absolutePath}${sep}${entry}`; - const entryRelPath = `${relativePath}/${entry}`; + const entryAbsPath = entry.path || `${absolutePath}${sep}${entry.name}`; + const entryRelPath = `${relativePath}/${entry.name}`; try { - const isDir = await this._fileSystem.isDirectory(entryAbsPath); - - if (isDir) { + if (entry.isDirectory) { // Recursively scan subdirectory await this._scanDirectory(entryAbsPath, entryRelPath); } else { - // Register file as asset - await this._registerAssetFile(entryAbsPath, entryRelPath); + // Register file as asset with size from entry + await this._registerAssetFile(entryAbsPath, entryRelPath, entry.size, entry.modified); } } catch (error) { - logger.warn(`Failed to process entry ${entry}:`, error); + logger.warn(`Failed to process entry ${entry.name}:`, error); } } } catch (error) { @@ -454,8 +457,19 @@ export class AssetRegistryService { /** * Register a single asset file + * 注册单个资产文件 + * + * @param absolutePath - 绝对路径 | Absolute path + * @param relativePath - 相对路径 | Relative path + * @param size - 文件大小(可选)| File size (optional) + * @param modified - 修改时间(可选)| Modified time (optional) */ - private async _registerAssetFile(absolutePath: string, relativePath: string): Promise { + private async _registerAssetFile( + absolutePath: string, + relativePath: string, + size?: number, + modified?: Date + ): Promise { if (!this._fileSystem || !this._manifest) return; // Skip .meta files @@ -471,18 +485,16 @@ export class AssetRegistryService { // Skip unknown file types if (!assetType || assetType === 'binary') return; - // Get file info - let stat: { size: number; mtime: number }; - try { - stat = await this._fileSystem.stat(absolutePath); - } catch { - return; - } + // Use provided size/modified or default values + const fileSize = size ?? 0; + const fileMtime = modified ? modified.getTime() : Date.now(); // Use MetaManager to get or create meta (with .meta file) let meta: IAssetMeta; try { + logger.debug(`Creating/loading meta for: ${relativePath}`); meta = await this._metaManager.getOrCreateMeta(absolutePath); + logger.debug(`Meta created/loaded for ${relativePath}: guid=${meta.guid}`); } catch (e) { logger.warn(`Failed to get meta for ${relativePath}:`, e); return; @@ -510,9 +522,9 @@ export class AssetRegistryService { path: relativePath, type: assetType, name, - size: stat.size, + size: fileSize, hash: '', // Could compute hash if needed - lastModified: stat.mtime + lastModified: fileMtime }; // Register in database diff --git a/packages/editor-core/src/Services/ProjectService.ts b/packages/editor-core/src/Services/ProjectService.ts index c264a22e..d251c7e6 100644 --- a/packages/editor-core/src/Services/ProjectService.ts +++ b/packages/editor-core/src/Services/ProjectService.ts @@ -94,11 +94,15 @@ export class ProjectService implements IService { scriptsPath: 'scripts', buildOutput: '.esengine/compiled', scenesPath: 'scenes', - defaultScene: 'main.ecs' + defaultScene: 'main.ecs', + plugins: { enabledPlugins: [] }, + modules: { disabledModules: [] } }; await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2)); + // Create scenes folder and default scene + // 创建场景文件夹和默认场景 const scenesPath = `${projectPath}${sep}${config.scenesPath}`; await this.fileAPI.createDirectory(scenesPath); @@ -111,6 +115,55 @@ export class ProjectService implements IService { }) as string; await this.fileAPI.writeFileContent(defaultScenePath, sceneData); + // Create scripts folder for user scripts + // 创建用户脚本文件夹 + const scriptsPath = `${projectPath}${sep}${config.scriptsPath}`; + await this.fileAPI.createDirectory(scriptsPath); + + // Create scripts/editor folder for editor extension scripts + // 创建编辑器扩展脚本文件夹 + const editorScriptsPath = `${scriptsPath}${sep}editor`; + await this.fileAPI.createDirectory(editorScriptsPath); + + // Create assets folder for project assets (textures, audio, etc.) + // 创建资源文件夹(纹理、音频等) + const assetsPath = `${projectPath}${sep}assets`; + await this.fileAPI.createDirectory(assetsPath); + + // Create types folder for type definitions + // 创建类型定义文件夹 + const typesPath = `${projectPath}${sep}types`; + await this.fileAPI.createDirectory(typesPath); + + // Create tsconfig.json for TypeScript support + // 创建 tsconfig.json 用于 TypeScript 支持 + const tsConfig = { + compilerOptions: { + target: 'ES2020', + module: 'ESNext', + moduleResolution: 'bundler', + lib: ['ES2020', 'DOM'], + strict: true, + esModuleInterop: true, + skipLibCheck: true, + forceConsistentCasingInFileNames: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + noEmit: true, + // Reference local type definitions + // 引用本地类型定义文件 + typeRoots: ['./types'], + paths: { + '@esengine/ecs-framework': ['./types/ecs-framework.d.ts'], + '@esengine/engine-core': ['./types/engine-core.d.ts'] + } + }, + include: ['scripts/**/*.ts'], + exclude: ['.esengine'] + }; + const tsConfigPath = `${projectPath}${sep}tsconfig.json`; + await this.fileAPI.writeFileContent(tsConfigPath, JSON.stringify(tsConfig, null, 2)); + await this.messageHub.publish('project:created', { path: projectPath }); @@ -258,8 +311,10 @@ export class ProjectService implements IService { scenesPath: config.scenesPath || 'scenes', defaultScene: config.defaultScene || 'main.ecs', uiDesignResolution: config.uiDesignResolution, - plugins: config.plugins, - modules: config.modules + // Provide default empty plugins config for legacy projects + // 为旧项目提供默认的空插件配置 + plugins: config.plugins || { enabledPlugins: [] }, + modules: config.modules || { disabledModules: [] } }; logger.debug('Loaded config result:', result); return result;