feat: 预制体系统与架构改进 (#303)

* feat(prefab): 实现预制体系统和编辑器 UX 改进

## 预制体系统
- 新增 PrefabSerializer: 预制体序列化/反序列化
- 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改
- 新增 PrefabService: 预制体核心服务
- 新增 PrefabLoader: 预制体资产加载器
- 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink

## 预制体编辑模式
- 支持双击 .prefab 文件进入编辑模式
- 预制体编辑模式工具栏 (保存/退出)
- 预制体实例指示器和操作菜单

## 编辑器 UX 改进
- SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航
- 支持双击实体名称内联编辑
- 删除实体时显示子节点数量警告
- 右键菜单添加重命名/复制选项及快捷键提示
- 布局持久化和重置功能

## Bug 修复
- 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题
- 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见
- 修复 Inspector 资源字段高度不正确问题

* feat(editor): 改进编辑器 UX 交互体验

- ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计
- SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮
- PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理
- EntityInspector: 组件折叠状态持久化、属性搜索清除按钮
- Viewport: 变换操作实时数值显示
- 国际化: 添加相关文本 (en/zh)

* fix(build): 修复 Web 构建资产加载和编辑器 UX 改进

构建系统修复:
- 修复 asset-catalog.json 字段名不匹配 (entries vs assets)
- 修复 BrowserFileSystemService 支持两种目录格式
- 修复 bundle 策略检测逻辑 (空对象判断)
- 修复 module.json 中 assetExtensions 声明和类型推断

行为树修复:
- 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath
- 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree)

编辑器 UX 改进:
- 构建完成对话框添加"打开文件夹"按钮
- 构建完成对话框样式优化 (圆形图标背景、按钮布局)
- SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列)
- SceneHierarchy 隐藏滚动条

错误追踪:
- 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log)
- 添加 append_to_log Tauri 命令

* feat(render): 修复 UI 渲染和点击特效系统

## UI 渲染修复
- 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数
- 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap
- Web 运行时添加 assetPathResolver 支持 GUID 解析
- UIInteractableComponent.blockEvents 默认值改为 false

## 点击特效系统
- 新增 ClickFxComponent 和 ClickFxSystem
- 支持在点击位置播放粒子效果
- 支持多种触发模式和粒子轮换

## Camera 系统重构
- CameraSystem 从 ecs-engine-bindgen 移至 camera 包
- 新增 CameraManager 统一管理相机

## 编辑器改进
- 改进属性面板 UI 交互
- 粒子编辑器面板优化
- Transform 命令系统

* feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层

- 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay)
- 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性
- 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染
- 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer
- 更新粒子编辑器面板支持新的排序属性
- 优化 UI 渲染系统使用新的排序层级

* feat(ci): 集成 SignPath 代码签名服务

- 添加 SignPath 自动签名工作流(Windows)
- 配置 release-editor.yml 支持代码签名
- 将构建改为草稿模式,等待签名完成后发布
- 添加证书文件到 .gitignore 防止泄露

* fix(asset): 修复 Web 构建资产路径解析和全局单例移除

## 资产路径修复
- 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接
- BrowserPathResolver 支持两种模式:
  - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser)
  - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建)
- BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理

## 架构改进 - 移除全局单例
- 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入
- 移除 globalPathResolver 导出,改用 PathResolutionService
- 移除 globalPathResolutionService 导出
- ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖
- EngineService 使用 new AssetManager() 替代全局实例

## 新增服务
- PathResolutionService: 统一路径解析接口
- RuntimeModeService: 运行时模式查询服务
- SerializationContext: EntityRef 序列化上下文

## 其他改进
- 完善 ServiceToken 注释说明本地定义的意图
- 导出 BrowserPathResolveMode 类型

* fix(build): 添加 world-streaming composite 设置修复类型检查

* fix(build): 移除 world-streaming 引用避免 composite 冲突

* fix(build): 将 const enum 改为 enum 兼容 isolatedModules

* fix(build): 添加缺失的 IAssetManager 导入
This commit is contained in:
YHH
2025-12-13 19:44:08 +08:00
committed by GitHub
parent a716d8006c
commit beaa1d09de
258 changed files with 17725 additions and 3030 deletions

View File

@@ -1,5 +1,15 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable, Core, createLogger, SceneSerializer, Scene } from '@esengine/ecs-framework';
import type { IService, Entity, PrefabData } from '@esengine/ecs-framework';
import {
Injectable,
Core,
createLogger,
SceneSerializer,
Scene,
PrefabSerializer,
HierarchySystem,
ComponentRegistry
} from '@esengine/ecs-framework';
import type { ComponentType } from '@esengine/ecs-framework';
import type { SceneResourceManager } from '@esengine/asset-system';
import type { MessageHub } from './MessageHub';
import type { IFileAPI } from '../Types/IFileAPI';
@@ -16,6 +26,29 @@ export interface SceneState {
isSaved: boolean;
}
/**
* 预制体编辑模式状态
* Prefab edit mode state
*/
export interface PrefabEditModeState {
/** 是否处于预制体编辑模式 | Whether in prefab edit mode */
isActive: boolean;
/** 预制体文件路径 | Prefab file path */
prefabPath: string;
/** 预制体名称 | Prefab name */
prefabName: string;
/** 预制体 GUID | Prefab GUID */
prefabGuid?: string;
/** 原始预制体数据(用于比较修改) | Original prefab data (for modification comparison) */
originalPrefabData: PrefabData;
/** 原场景路径 | Original scene path */
originalScenePath: string | null;
/** 原场景名称 | Original scene name */
originalSceneName: string;
/** 原场景是否已修改 | Whether original scene was modified */
originalSceneModified: boolean;
}
@Injectable()
export class SceneManagerService implements IService {
private sceneState: SceneState = {
@@ -25,6 +58,12 @@ export class SceneManagerService implements IService {
isSaved: false
};
/** 预制体编辑模式状态 | Prefab edit mode state */
private prefabEditModeState: PrefabEditModeState | null = null;
/** 预制体编辑时场景中的根实体 | Root entity in scene during prefab editing */
private prefabRootEntity: Entity | null = null;
private unsubscribeHandlers: Array<() => void> = [];
private sceneResourceManager: SceneResourceManager | null = null;
@@ -196,17 +235,15 @@ export class SceneManagerService implements IService {
public async saveSceneAs(filePath?: string): Promise<void> {
let path: string | null | undefined = filePath;
if (!path) {
let defaultName = this.sceneState.sceneName || 'Untitled';
const defaultName = this.sceneState.sceneName || 'Untitled';
let scenesDir: string | undefined;
// 获取场景目录,限制保存位置 | Get scenes directory to restrict save location
if (this.projectService?.isProjectOpen()) {
const scenesPath = this.projectService.getScenesPath();
if (scenesPath) {
const sep = scenesPath.includes('\\') ? '\\' : '/';
defaultName = `${scenesPath}${sep}${defaultName}`;
}
scenesDir = this.projectService.getScenesPath() ?? undefined;
}
path = await this.fileAPI.saveSceneDialog(defaultName);
path = await this.fileAPI.saveSceneDialog(defaultName, scenesDir);
if (!path) {
return;
}
@@ -250,17 +287,15 @@ export class SceneManagerService implements IService {
public async exportScene(filePath?: string): Promise<void> {
let path: string | null | undefined = filePath;
if (!path) {
let defaultName = (this.sceneState.sceneName || 'Untitled') + '.ecs.bin';
const defaultName = (this.sceneState.sceneName || 'Untitled') + '.ecs.bin';
let scenesDir: string | undefined;
// 获取场景目录,限制保存位置 | Get scenes directory to restrict save location
if (this.projectService?.isProjectOpen()) {
const scenesPath = this.projectService.getScenesPath();
if (scenesPath) {
const sep = scenesPath.includes('\\') ? '\\' : '/';
defaultName = `${scenesPath}${sep}${defaultName}`;
}
scenesDir = this.projectService.getScenesPath() ?? undefined;
}
path = await this.fileAPI.saveSceneDialog(defaultName);
path = await this.fileAPI.saveSceneDialog(defaultName, scenesDir);
if (!path) {
return;
}
@@ -309,20 +344,352 @@ export class SceneManagerService implements IService {
return true;
}
// ===== 预制体编辑模式 API | Prefab Edit Mode API =====
/**
* 进入预制体编辑模式
* Enter prefab edit mode
*
* @param prefabPath - 预制体文件路径 | Prefab file path
*/
public async enterPrefabEditMode(prefabPath: string): Promise<void> {
// 如果已在预制体编辑模式,先退出
// If already in prefab edit mode, exit first
if (this.prefabEditModeState?.isActive) {
await this.exitPrefabEditMode(false);
}
const scene = Core.scene as Scene | null;
if (!scene) {
throw new Error('No active scene');
}
try {
// 1. 读取预制体文件 | Read prefab file
const prefabJson = await this.fileAPI.readFileContent(prefabPath);
const prefabData = PrefabSerializer.deserialize(prefabJson);
// 2. 验证预制体数据 | Validate prefab data
const validation = PrefabSerializer.validate(prefabData);
if (!validation.valid) {
throw new Error(`Invalid prefab: ${validation.errors?.join(', ')}`);
}
// 3. 保存当前场景状态 | Save current scene state
const savedScenePath = this.sceneState.currentScenePath;
const savedSceneName = this.sceneState.sceneName;
const savedSceneModified = this.sceneState.isModified;
// 4. 请求保存场景快照(通过 MessageHub由 EngineService 处理)
// Request to save scene snapshot (via MessageHub, handled by EngineService)
const snapshotSaved = await this.messageHub.request<void, boolean>(
'engine:saveSceneSnapshot',
undefined,
5000
).catch(() => false);
if (!snapshotSaved) {
logger.warn('Failed to save scene snapshot, proceeding without snapshot');
}
// 5. 清空场景 | Clear scene
scene.entities.removeAllEntities();
// 5.1 清理查询系统和系统缓存 | Clear query system and system caches
scene.querySystem.setEntities([]);
scene.clearSystemEntityCaches();
// 5.2 重置所有系统的实体跟踪状态 | Reset entity tracking for all systems
for (const system of scene.systems) {
system.resetEntityTracking();
}
// 6. 获取组件注册表 | Get component registry
// ComponentRegistry.getAllComponentNames() 返回 Map<string, Function>
// 需要转换为 Map<string, ComponentType>
const nameToType = ComponentRegistry.getAllComponentNames();
const componentRegistry = new Map<string, ComponentType>();
nameToType.forEach((type, name) => {
componentRegistry.set(name, type as ComponentType);
});
// 7. 实例化预制体到场景 | Instantiate prefab to scene
logger.info(`Instantiating prefab with ${componentRegistry.size} registered component types`);
logger.debug('Available component types:', Array.from(componentRegistry.keys()));
this.prefabRootEntity = PrefabSerializer.instantiate(
prefabData,
scene,
componentRegistry,
{
trackInstance: false, // 编辑模式不追踪实例 | Don't track instance in edit mode
preserveIds: false
}
);
logger.info(`Prefab instantiated, root entity: ${this.prefabRootEntity?.name} (id: ${this.prefabRootEntity?.id})`);
logger.info(`Scene entity count: ${scene.entities.count}`);
// 7.1 强制重建查询系统 | Force rebuild query system
// 使用 setEntities 完全重置,确保所有索引正确重建
// Using setEntities to fully reset, ensuring all indexes are correctly rebuilt
const allEntities = Array.from(scene.entities.buffer);
scene.querySystem.setEntities(allEntities);
// 7.2 重置所有系统的实体跟踪状态,强制它们重新扫描
// Reset all system entity tracking, forcing them to rescan
for (const system of scene.systems) {
system.resetEntityTracking();
}
// 7.3 清理系统缓存 | Clear system caches
scene.clearSystemEntityCaches();
// 8. 加载场景资源(纹理、音频等)| Load scene resources (textures, audio, etc.)
if (this.sceneResourceManager) {
await this.sceneResourceManager.loadSceneResources(scene);
logger.info('Scene resources loaded for prefab');
} else {
logger.warn('SceneResourceManager not available, skipping resource loading');
}
// 9. 设置预制体编辑模式状态 | Set prefab edit mode state
const prefabName = prefabData.metadata.name || prefabPath.split(/[/\\]/).pop()?.replace('.prefab', '') || 'Prefab';
this.prefabEditModeState = {
isActive: true,
prefabPath,
prefabName,
prefabGuid: prefabData.metadata.guid,
originalPrefabData: prefabData,
originalScenePath: savedScenePath,
originalSceneName: savedSceneName,
originalSceneModified: savedSceneModified
};
// 10. 更新场景状态 | Update scene state
this.sceneState = {
currentScenePath: null,
sceneName: `Prefab: ${prefabName}`,
isModified: false,
isSaved: true
};
// 11. 同步到 EntityStore | Sync to EntityStore
this.entityStore?.syncFromScene();
// 12. 发布事件 | Publish events
await this.messageHub.publish('prefab:editMode:enter', {
prefabPath,
prefabName,
prefabGuid: prefabData.metadata.guid
});
await this.messageHub.publish('prefab:editMode:changed', {
isActive: true,
prefabPath,
prefabName
});
logger.info(`Entered prefab edit mode: ${prefabPath}`);
} catch (error) {
logger.error('Failed to enter prefab edit mode:', error);
throw error;
}
}
/**
* 退出预制体编辑模式
* Exit prefab edit mode
*
* @param save - 是否保存修改 | Whether to save changes
*/
public async exitPrefabEditMode(save: boolean = false): Promise<void> {
if (!this.prefabEditModeState?.isActive) {
logger.warn('Not in prefab edit mode');
return;
}
const scene = Core.scene as Scene | null;
if (!scene) {
throw new Error('No active scene');
}
try {
// 1. 如果需要保存,先保存预制体 | If save requested, save prefab first
if (save && this.sceneState.isModified) {
await this.savePrefab();
}
// 2. 清空当前场景 | Clear current scene
scene.entities.removeAllEntities();
this.prefabRootEntity = null;
// 3. 请求恢复场景快照(通过 MessageHub由 EngineService 处理)
// Request to restore scene snapshot (via MessageHub, handled by EngineService)
const snapshotRestored = await this.messageHub.request<void, boolean>(
'engine:restoreSceneSnapshot',
undefined,
5000
).catch(() => false);
// 4. 恢复场景状态 | Restore scene state
const originalState = this.prefabEditModeState;
this.sceneState = {
currentScenePath: originalState.originalScenePath,
sceneName: originalState.originalSceneName,
isModified: originalState.originalSceneModified,
isSaved: !originalState.originalSceneModified
};
// 5. 清除预制体编辑模式状态 | Clear prefab edit mode state
this.prefabEditModeState = null;
// 6. 同步到 EntityStore | Sync to EntityStore
this.entityStore?.syncFromScene();
// 7. 发布事件 | Publish events
await this.messageHub.publish('prefab:editMode:exit', { saved: save });
await this.messageHub.publish('prefab:editMode:changed', {
isActive: false
});
if (snapshotRestored) {
await this.messageHub.publish('scene:restored', {});
}
logger.info(`Exited prefab edit mode, saved: ${save}`);
} catch (error) {
logger.error('Failed to exit prefab edit mode:', error);
throw error;
}
}
/**
* 保存预制体
* Save prefab
*/
public async savePrefab(): Promise<void> {
if (!this.prefabEditModeState?.isActive) {
throw new Error('Not in prefab edit mode');
}
const scene = Core.scene as Scene | null;
if (!scene) {
throw new Error('No active scene');
}
if (!this.prefabRootEntity) {
throw new Error('No prefab root entity');
}
try {
const hierarchySystem = scene.getSystem(HierarchySystem) ?? undefined;
// 1. 从根实体创建预制体数据 | Create prefab data from root entity
const newPrefabData = PrefabSerializer.createPrefab(
this.prefabRootEntity,
{
name: this.prefabEditModeState.prefabName,
description: this.prefabEditModeState.originalPrefabData.metadata.description,
tags: this.prefabEditModeState.originalPrefabData.metadata.tags,
includeChildren: true
},
hierarchySystem
);
// 2. 保持原有 GUID | Preserve original GUID
if (this.prefabEditModeState.prefabGuid) {
newPrefabData.metadata.guid = this.prefabEditModeState.prefabGuid;
}
// 3. 保持原有创建时间,更新修改时间 | Preserve creation time, update modification time
newPrefabData.metadata.createdAt = this.prefabEditModeState.originalPrefabData.metadata.createdAt;
newPrefabData.metadata.modifiedAt = Date.now();
// 4. 序列化并保存 | Serialize and save
const prefabJson = PrefabSerializer.serialize(newPrefabData, true);
await this.fileAPI.saveProject(this.prefabEditModeState.prefabPath, prefabJson);
// 5. 更新原始数据(用于后续修改检测)| Update original data (for subsequent modification detection)
this.prefabEditModeState.originalPrefabData = newPrefabData;
// 6. 标记为已保存 | Mark as saved
this.sceneState.isModified = false;
this.sceneState.isSaved = true;
// 7. 发布事件 | Publish event
await this.messageHub.publish('prefab:saved', {
prefabPath: this.prefabEditModeState.prefabPath,
prefabName: this.prefabEditModeState.prefabName
});
logger.info(`Prefab saved: ${this.prefabEditModeState.prefabPath}`);
} catch (error) {
logger.error('Failed to save prefab:', error);
throw error;
}
}
/**
* 检查是否处于预制体编辑模式
* Check if in prefab edit mode
*/
public isPrefabEditMode(): boolean {
return this.prefabEditModeState?.isActive ?? false;
}
/**
* 获取预制体编辑模式状态
* Get prefab edit mode state
*/
public getPrefabEditModeState(): PrefabEditModeState | null {
return this.prefabEditModeState ? { ...this.prefabEditModeState } : null;
}
/**
* 检查预制体是否已修改
* Check if prefab has been modified
*/
public isPrefabModified(): boolean {
return (this.prefabEditModeState?.isActive ?? false) && this.sceneState.isModified;
}
private setupAutoModificationTracking(): void {
// 实体级别事件 | Entity-level events
const unsubscribeEntityAdded = this.messageHub.subscribe('entity:added', () => {
this.markAsModified();
});
const unsubscribeEntityRemoved = this.messageHub.subscribe('entity:removed', () => {
this.markAsModified();
});
const unsubscribeEntityReordered = this.messageHub.subscribe('entity:reordered', () => {
this.markAsModified();
});
this.unsubscribeHandlers.push(unsubscribeEntityAdded, unsubscribeEntityRemoved, unsubscribeEntityReordered);
// 组件级别事件 | Component-level events
const unsubscribeComponentAdded = this.messageHub.subscribe('component:added', () => {
this.markAsModified();
});
const unsubscribeComponentRemoved = this.messageHub.subscribe('component:removed', () => {
this.markAsModified();
});
const unsubscribeComponentPropertyChanged = this.messageHub.subscribe('component:property:changed', () => {
this.markAsModified();
});
// 通用场景修改事件 | Generic scene modification event
const unsubscribeSceneModified = this.messageHub.subscribe('scene:modified', () => {
this.markAsModified();
});
this.unsubscribeHandlers.push(
unsubscribeEntityAdded,
unsubscribeEntityRemoved,
unsubscribeEntityReordered,
unsubscribeComponentAdded,
unsubscribeComponentRemoved,
unsubscribeComponentPropertyChanged,
unsubscribeSceneModified
);
logger.debug('Auto modification tracking setup complete');
}