Files
esengine/packages/editor-core/src/Services/ProjectService.ts
YHH 63f006ab62 feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
2025-12-03 22:15:22 +08:00

459 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger, Scene } from '@esengine/ecs-framework';
import { MessageHub } from './MessageHub';
import type { IFileAPI } from '../Types/IFileAPI';
const logger = createLogger('ProjectService');
export type ProjectType = 'esengine' | 'unknown';
export interface ProjectInfo {
path: string;
type: ProjectType;
name: string;
configPath?: string;
}
/**
* UI 设计分辨率配置
* UI Design Resolution Configuration
*/
export interface UIDesignResolution {
/** 设计宽度 / Design width */
width: number;
/** 设计高度 / Design height */
height: number;
}
/**
* 插件配置
* Plugin Configuration
*/
export interface PluginSettings {
/** 启用的插件 ID 列表 / Enabled plugin IDs */
enabledPlugins: string[];
}
/**
* 模块配置
* Module Configuration
*/
export interface ModuleSettings {
/**
* 禁用的模块 ID 列表(黑名单方式)
* Disabled module IDs (blacklist approach)
* Modules NOT in this list are enabled.
* 不在此列表中的模块为启用状态。
*/
disabledModules: string[];
}
export interface ProjectConfig {
projectType?: ProjectType;
/** User scripts directory (default: 'scripts') | 用户脚本目录(默认:'scripts' */
scriptsPath?: string;
/** Build output directory | 构建输出目录 */
buildOutput?: string;
/** Scenes directory | 场景目录 */
scenesPath?: string;
/** Default scene file | 默认场景文件 */
defaultScene?: string;
/** UI design resolution | UI 设计分辨率 */
uiDesignResolution?: UIDesignResolution;
/** Plugin settings | 插件配置 */
plugins?: PluginSettings;
/** Module settings | 模块配置 */
modules?: ModuleSettings;
}
@Injectable()
export class ProjectService implements IService {
private currentProject: ProjectInfo | null = null;
private projectConfig: ProjectConfig | null = null;
private messageHub: MessageHub;
private fileAPI: IFileAPI;
constructor(messageHub: MessageHub, fileAPI: IFileAPI) {
this.messageHub = messageHub;
this.fileAPI = fileAPI;
}
public async createProject(projectPath: string): Promise<void> {
try {
const sep = projectPath.includes('\\') ? '\\' : '/';
const configPath = `${projectPath}${sep}ecs-editor.config.json`;
const configExists = await this.fileAPI.pathExists(configPath);
if (configExists) {
throw new Error('ECS project already exists in this directory');
}
const config: ProjectConfig = {
projectType: 'esengine',
scriptsPath: 'scripts',
buildOutput: '.esengine/compiled',
scenesPath: 'scenes',
defaultScene: 'main.ecs'
};
await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2));
const scenesPath = `${projectPath}${sep}${config.scenesPath}`;
await this.fileAPI.createDirectory(scenesPath);
const defaultScenePath = `${scenesPath}${sep}${config.defaultScene}`;
const emptyScene = new Scene();
const sceneData = emptyScene.serialize({
format: 'json',
pretty: true,
includeMetadata: true
}) as string;
await this.fileAPI.writeFileContent(defaultScenePath, sceneData);
await this.messageHub.publish('project:created', {
path: projectPath
});
logger.info('Project created', { path: projectPath });
} catch (error) {
logger.error('Failed to create project', error);
throw error;
}
}
public async openProject(projectPath: string): Promise<void> {
try {
const projectInfo = await this.validateProject(projectPath);
this.currentProject = projectInfo;
if (projectInfo.configPath) {
this.projectConfig = await this.loadConfig(projectInfo.configPath);
}
await this.messageHub.publish('project:opened', {
path: projectPath,
type: projectInfo.type,
name: projectInfo.name
});
logger.info('Project opened', { path: projectPath, type: projectInfo.type });
} catch (error) {
logger.error('Failed to open project', error);
throw error;
}
}
public async closeProject(): Promise<void> {
if (!this.currentProject) {
logger.warn('No project is currently open');
return;
}
const projectPath = this.currentProject.path;
this.currentProject = null;
this.projectConfig = null;
await this.messageHub.publish('project:closed', { path: projectPath });
logger.info('Project closed', { path: projectPath });
}
public getCurrentProject(): ProjectInfo | null {
return this.currentProject;
}
public getProjectConfig(): ProjectConfig | null {
return this.projectConfig;
}
public isProjectOpen(): boolean {
return this.currentProject !== null;
}
/**
* Get user scripts directory path.
* 获取用户脚本目录路径。
*
* @returns Scripts directory path | 脚本目录路径
*/
public getScriptsPath(): string | null {
if (!this.currentProject) {
return null;
}
const scriptsPath = this.projectConfig?.scriptsPath || 'scripts';
const sep = this.currentProject.path.includes('\\') ? '\\' : '/';
return `${this.currentProject.path}${sep}${scriptsPath}`;
}
/**
* Get editor scripts directory path (scripts/editor).
* 获取编辑器脚本目录路径scripts/editor
*
* @returns Editor scripts directory path | 编辑器脚本目录路径
*/
public getEditorScriptsPath(): string | null {
const scriptsPath = this.getScriptsPath();
if (!scriptsPath) {
return null;
}
const sep = scriptsPath.includes('\\') ? '\\' : '/';
return `${scriptsPath}${sep}editor`;
}
public getScenesPath(): string | null {
if (!this.currentProject) {
return null;
}
const scenesPath = this.projectConfig?.scenesPath || 'assets/scenes';
const sep = this.currentProject.path.includes('\\') ? '\\' : '/';
return `${this.currentProject.path}${sep}${scenesPath}`;
}
public getDefaultScenePath(): string | null {
if (!this.currentProject) {
return null;
}
const scenesPath = this.getScenesPath();
if (!scenesPath) {
return null;
}
const defaultScene = this.projectConfig?.defaultScene || 'main.scene';
const sep = this.currentProject.path.includes('\\') ? '\\' : '/';
return `${scenesPath}${sep}${defaultScene}`;
}
private async validateProject(projectPath: string): Promise<ProjectInfo> {
const projectName = projectPath.split(/[\\/]/).pop() || 'Unknown Project';
const projectInfo: ProjectInfo = {
path: projectPath,
type: 'unknown',
name: projectName
};
const sep = projectPath.includes('\\') ? '\\' : '/';
const configPath = `${projectPath}${sep}ecs-editor.config.json`;
try {
projectInfo.configPath = configPath;
projectInfo.type = 'esengine';
} catch (error) {
logger.warn('No ecs-editor.config.json found, using defaults');
}
return projectInfo;
}
private async loadConfig(configPath: string): Promise<ProjectConfig> {
try {
const content = await this.fileAPI.readFileContent(configPath);
logger.debug('Raw config content:', content);
const config = JSON.parse(content) as ProjectConfig;
logger.debug('Parsed config plugins:', config.plugins);
const result: ProjectConfig = {
projectType: config.projectType || 'esengine',
scriptsPath: config.scriptsPath || 'scripts',
buildOutput: config.buildOutput || '.esengine/compiled',
scenesPath: config.scenesPath || 'scenes',
defaultScene: config.defaultScene || 'main.ecs',
uiDesignResolution: config.uiDesignResolution,
plugins: config.plugins,
modules: config.modules
};
logger.debug('Loaded config result:', result);
return result;
} catch (error) {
logger.warn('Failed to load config, using defaults', error);
return {
projectType: 'esengine',
scriptsPath: 'scripts',
buildOutput: '.esengine/compiled',
scenesPath: 'scenes',
defaultScene: 'main.ecs'
};
}
}
/**
* 保存项目配置
*/
public async saveConfig(): Promise<void> {
if (!this.currentProject?.configPath || !this.projectConfig) {
logger.warn('No project or config to save');
return;
}
try {
const content = JSON.stringify(this.projectConfig, null, 2);
await this.fileAPI.writeFileContent(this.currentProject.configPath, content);
logger.info('Project config saved');
} catch (error) {
logger.error('Failed to save project config', error);
throw error;
}
}
/**
* 更新项目配置
*/
public async updateConfig(updates: Partial<ProjectConfig>): Promise<void> {
if (!this.projectConfig) {
logger.warn('No project config to update');
return;
}
this.projectConfig = {
...this.projectConfig,
...updates
};
await this.saveConfig();
await this.messageHub.publish('project:configUpdated', { config: this.projectConfig });
}
/**
* 获取 UI 设计分辨率
* Get UI design resolution
*
* @returns UI design resolution, defaults to 1920x1080 if not set
*/
public getUIDesignResolution(): UIDesignResolution {
return this.projectConfig?.uiDesignResolution || { width: 1920, height: 1080 };
}
/**
* 设置 UI 设计分辨率
* Set UI design resolution
*
* @param resolution - The new design resolution
*/
public async setUIDesignResolution(resolution: UIDesignResolution): Promise<void> {
await this.updateConfig({ uiDesignResolution: resolution });
}
/**
* 获取启用的插件列表
* Get enabled plugins list
*/
public getEnabledPlugins(): string[] {
return this.projectConfig?.plugins?.enabledPlugins || [];
}
/**
* 获取插件配置
* Get plugin settings
*/
public getPluginSettings(): PluginSettings | null {
logger.debug('getPluginSettings called, projectConfig:', this.projectConfig);
logger.debug('getPluginSettings plugins:', this.projectConfig?.plugins);
return this.projectConfig?.plugins || null;
}
/**
* 设置启用的插件列表
* Set enabled plugins list
*
* @param enabledPlugins - Array of enabled plugin IDs
*/
public async setEnabledPlugins(enabledPlugins: string[]): Promise<void> {
await this.updateConfig({
plugins: {
enabledPlugins
}
});
await this.messageHub.publish('project:pluginsChanged', { enabledPlugins });
logger.info('Plugin settings saved', { count: enabledPlugins.length });
}
/**
* 启用插件
* Enable a plugin
*/
public async enablePlugin(pluginId: string): Promise<void> {
const current = this.getEnabledPlugins();
if (!current.includes(pluginId)) {
await this.setEnabledPlugins([...current, pluginId]);
}
}
/**
* 禁用插件
* Disable a plugin
*/
public async disablePlugin(pluginId: string): Promise<void> {
const current = this.getEnabledPlugins();
await this.setEnabledPlugins(current.filter(id => id !== pluginId));
}
// ==================== Module Settings ====================
/**
* 获取禁用的模块列表(黑名单)
* Get disabled modules list (blacklist)
* @returns Array of disabled module IDs
*/
public getDisabledModules(): string[] {
return this.projectConfig?.modules?.disabledModules || [];
}
/**
* 获取模块配置
* Get module settings
*/
public getModuleSettings(): ModuleSettings | null {
return this.projectConfig?.modules || null;
}
/**
* 设置禁用的模块列表
* Set disabled modules list
*
* @param disabledModules - Array of disabled module IDs
*/
public async setDisabledModules(disabledModules: string[]): Promise<void> {
await this.updateConfig({
modules: {
disabledModules
}
});
await this.messageHub.publish('project:modulesChanged', { disabledModules });
logger.info('Module settings saved', { disabledCount: disabledModules.length });
}
/**
* 禁用模块
* Disable a module
*/
public async disableModule(moduleId: string): Promise<void> {
const current = this.getDisabledModules();
if (!current.includes(moduleId)) {
await this.setDisabledModules([...current, moduleId]);
}
}
/**
* 启用模块
* Enable a module
*/
public async enableModule(moduleId: string): Promise<void> {
const current = this.getDisabledModules();
await this.setDisabledModules(current.filter(id => id !== moduleId));
}
/**
* 检查模块是否启用
* Check if a module is enabled
*/
public isModuleEnabled(moduleId: string): boolean {
const disabled = this.getDisabledModules();
return !disabled.includes(moduleId);
}
public dispose(): void {
this.currentProject = null;
this.projectConfig = null;
logger.info('ProjectService disposed');
}
}