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

@@ -165,6 +165,33 @@ export class CommandManager {
const batchCommand = new BatchCommand(commands);
this.execute(batchCommand);
}
/**
* 将命令推入撤销栈但不执行
* Push command to undo stack without executing
*
* 用于已经执行过的操作(如拖动变换),只需要记录到历史
* Used for operations that have already been performed (like drag transforms),
* only need to record to history
*/
pushWithoutExecute(command: ICommand): void {
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
return;
}
}
this.undoStack.push(command);
this.redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
}
}
/**

View File

@@ -1,12 +1,18 @@
import { Entity, Component } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import { Entity, Component, getComponentDependencies, getComponentTypeName } from '@esengine/ecs-framework';
import { MessageHub, ComponentRegistry } from '@esengine/editor-core';
import { Core } from '@esengine/ecs-framework';
import { BaseCommand } from '../BaseCommand';
/**
* 添加组件命令
*
* 自动添加缺失的依赖组件(通过 @ECSComponent requires 选项声明)
* Automatically adds missing dependency components (declared via @ECSComponent requires option)
*/
export class AddComponentCommand extends BaseCommand {
private component: Component | null = null;
/** 自动添加的依赖组件(用于撤销时一并移除) | Auto-added dependencies (for undo removal) */
private autoAddedDependencies: Component[] = [];
constructor(
private messageHub: MessageHub,
@@ -18,9 +24,12 @@ export class AddComponentCommand extends BaseCommand {
}
execute(): void {
// 先添加缺失的依赖组件 | Add missing dependencies first
this.addMissingDependencies();
this.component = new this.ComponentClass();
// 应用初始数据
// 应用初始数据 | Apply initial data
if (this.initialData) {
for (const [key, value] of Object.entries(this.initialData)) {
(this.component as any)[key] = value;
@@ -35,20 +44,90 @@ export class AddComponentCommand extends BaseCommand {
});
}
/**
* 添加缺失的依赖组件
* Add missing dependency components
*/
private addMissingDependencies(): void {
const dependencies = getComponentDependencies(this.ComponentClass);
if (!dependencies || dependencies.length === 0) {
return;
}
const componentRegistry = Core.services.tryResolve(ComponentRegistry) as ComponentRegistry | null;
if (!componentRegistry) {
return;
}
for (const depName of dependencies) {
// 检查实体是否已有该依赖组件 | Check if entity already has this dependency
const depInfo = componentRegistry.getComponent(depName);
if (!depInfo?.type) {
console.warn(`Dependency component not found in registry: ${depName}`);
continue;
}
const DepClass = depInfo.type;
// 使用名称检查而非类引用,因为打包可能导致同一个类有多个副本
// Use name-based check instead of class reference, as bundling may create multiple copies of the same class
const foundByName = this.entity.components.find(c => c.constructor.name === DepClass.name);
if (foundByName) {
// 组件已存在(通过名称匹配),跳过添加
// Component already exists (matched by name), skip adding
continue;
}
// 自动添加依赖组件 | Auto-add dependency component
const depComponent = new DepClass();
this.entity.addComponent(depComponent);
this.autoAddedDependencies.push(depComponent);
this.messageHub.publish('component:added', {
entity: this.entity,
component: depComponent,
isAutoDependency: true
});
}
}
undo(): void {
if (!this.component) return;
// 先移除主组件 | Remove main component first
this.entity.removeComponent(this.component);
this.messageHub.publish('component:removed', {
entity: this.entity,
componentType: this.ComponentClass.name
componentType: getComponentTypeName(this.ComponentClass)
});
// 移除自动添加的依赖组件(逆序) | Remove auto-added dependencies (reverse order)
for (let i = this.autoAddedDependencies.length - 1; i >= 0; i--) {
const dep = this.autoAddedDependencies[i];
if (dep) {
this.entity.removeComponent(dep);
this.messageHub.publish('component:removed', {
entity: this.entity,
componentType: dep.constructor.name,
isAutoDependency: true
});
}
}
this.component = null;
this.autoAddedDependencies = [];
}
getDescription(): string {
return `添加组件: ${this.ComponentClass.name}`;
const mainName = getComponentTypeName(this.ComponentClass);
if (this.autoAddedDependencies.length > 0) {
const depNames = this.autoAddedDependencies.map(d => d.constructor.name).join(', ');
return `添加组件: ${mainName} (+ 依赖: ${depNames})`;
}
return `添加组件: ${mainName}`;
}
}

View File

@@ -1,3 +1,4 @@
export type { ICommand } from './ICommand';
export { BaseCommand } from './BaseCommand';
export { CommandManager } from './CommandManager';
export { TransformCommand, type TransformState, type TransformOperationType } from './transform/TransformCommand';

View File

@@ -0,0 +1,65 @@
/**
* 应用预制体命令
* Apply prefab command
*
* 将预制体实例的修改应用到源预制体文件。
* Applies modifications from a prefab instance to the source prefab file.
*/
import { Entity, PrefabInstanceComponent } from '@esengine/ecs-framework';
import type { MessageHub, PrefabService } from '@esengine/editor-core';
import { BaseCommand } from '../BaseCommand';
/**
* 应用预制体命令
* Apply prefab command
*/
export class ApplyPrefabCommand extends BaseCommand {
private previousModifiedProperties: string[] = [];
private previousOriginalValues: Record<string, unknown> = {};
private success: boolean = false;
constructor(
private prefabService: PrefabService,
private messageHub: MessageHub,
private entity: Entity
) {
super();
}
async execute(): Promise<void> {
// 保存当前状态用于撤销 | Save current state for undo
const comp = this.entity.getComponent(PrefabInstanceComponent);
if (comp) {
this.previousModifiedProperties = [...comp.modifiedProperties];
this.previousOriginalValues = { ...comp.originalValues };
}
// 执行应用操作 | Execute apply operation
this.success = await this.prefabService.applyToPrefab(this.entity);
if (!this.success) {
throw new Error('Failed to apply changes to prefab');
}
}
undo(): void {
// 恢复修改状态 | Restore modification state
const comp = this.entity.getComponent(PrefabInstanceComponent);
if (comp) {
comp.modifiedProperties = this.previousModifiedProperties;
comp.originalValues = this.previousOriginalValues;
}
// 发布事件通知 UI 更新 | Publish event to notify UI update
this.messageHub.publish('component:property:changed', {
entityId: this.entity.id
});
}
getDescription(): string {
const comp = this.entity.getComponent(PrefabInstanceComponent);
const prefabName = comp?.sourcePrefabPath?.split(/[/\\]/).pop()?.replace('.prefab', '') || 'Prefab';
return `应用修改到预制体: ${prefabName}`;
}
}

View File

@@ -0,0 +1,128 @@
/**
* 断开预制体链接命令
* Break prefab link command
*
* 断开实体与源预制体的关联,使其成为普通实体。
* Breaks the link between an entity and its source prefab, making it a regular entity.
*/
import { Entity, PrefabInstanceComponent, Core } from '@esengine/ecs-framework';
import type { MessageHub, PrefabService } from '@esengine/editor-core';
import { BaseCommand } from '../BaseCommand';
/**
* 保存的预制体实例组件状态
* Saved prefab instance component state
*/
interface PrefabInstanceState {
entityId: number;
sourcePrefabGuid: string;
sourcePrefabPath: string;
isRoot: boolean;
rootInstanceEntityId: number | null;
modifiedProperties: string[];
originalValues: Record<string, unknown>;
instantiatedAt: number;
}
/**
* 断开预制体链接命令
* Break prefab link command
*/
export class BreakPrefabLinkCommand extends BaseCommand {
private removedStates: PrefabInstanceState[] = [];
constructor(
private prefabService: PrefabService,
private messageHub: MessageHub,
private entity: Entity
) {
super();
}
execute(): void {
// 保存所有将被移除的组件状态 | Save all component states that will be removed
this.removedStates = [];
const comp = this.entity.getComponent(PrefabInstanceComponent);
if (!comp) {
throw new Error('Entity is not a prefab instance');
}
// 保存根实体的状态 | Save root entity state
this.saveComponentState(this.entity);
// 如果是根节点,也保存所有子实体的状态
// If it's root, also save all children's state
if (comp.isRoot) {
const scene = Core.scene;
if (scene) {
scene.entities.forEach((e) => {
if (e.id === this.entity.id) return;
const childComp = e.getComponent(PrefabInstanceComponent);
if (childComp && childComp.rootInstanceEntityId === this.entity.id) {
this.saveComponentState(e);
}
});
}
}
// 执行断开链接操作 | Execute break link operation
this.prefabService.breakPrefabLink(this.entity);
}
undo(): void {
// 恢复所有被移除的组件 | Restore all removed components
const scene = Core.scene;
if (!scene) return;
for (const state of this.removedStates) {
const entity = scene.findEntityById(state.entityId);
if (!entity) continue;
// 创建并恢复组件 | Create and restore component
const comp = new PrefabInstanceComponent(
state.sourcePrefabGuid,
state.sourcePrefabPath,
state.isRoot
);
comp.rootInstanceEntityId = state.rootInstanceEntityId;
comp.modifiedProperties = state.modifiedProperties;
comp.originalValues = state.originalValues;
comp.instantiatedAt = state.instantiatedAt;
entity.addComponent(comp);
}
// 发布事件通知 UI 更新 | Publish event to notify UI update
this.messageHub.publish('prefab:link:restored', {
entityId: this.entity.id
});
}
getDescription(): string {
const state = this.removedStates.find(s => s.entityId === this.entity.id);
const prefabName = state?.sourcePrefabPath?.split(/[/\\]/).pop()?.replace('.prefab', '') || 'Prefab';
return `断开预制体链接: ${prefabName}`;
}
/**
* 保存实体的预制体实例组件状态
* Save entity's prefab instance component state
*/
private saveComponentState(entity: Entity): void {
const comp = entity.getComponent(PrefabInstanceComponent);
if (!comp) return;
this.removedStates.push({
entityId: entity.id,
sourcePrefabGuid: comp.sourcePrefabGuid,
sourcePrefabPath: comp.sourcePrefabPath,
isRoot: comp.isRoot,
rootInstanceEntityId: comp.rootInstanceEntityId,
modifiedProperties: [...comp.modifiedProperties],
originalValues: { ...comp.originalValues },
instantiatedAt: comp.instantiatedAt
});
}
}

View File

@@ -0,0 +1,150 @@
/**
* 创建预制体命令
* Create prefab command
*
* 从选中的实体创建预制体资产并保存到文件系统。
* Creates a prefab asset from the selected entity and saves it to the file system.
*/
import { Core, Entity, HierarchySystem, PrefabSerializer } from '@esengine/ecs-framework';
import type { PrefabData } from '@esengine/ecs-framework';
import type { MessageHub, IFileAPI, ProjectService, AssetRegistryService } from '@esengine/editor-core';
import { BaseCommand } from '../BaseCommand';
/**
* 创建预制体命令选项
* Create prefab command options
*/
export interface CreatePrefabOptions {
/** 预制体名称 | Prefab name */
name: string;
/** 保存路径(不包含文件名) | Save path (without filename) */
savePath?: string;
/** 预制体描述 | Prefab description */
description?: string;
/** 预制体标签 | Prefab tags */
tags?: string[];
/** 是否包含子实体 | Whether to include child entities */
includeChildren?: boolean;
}
/**
* 创建预制体命令
* Create prefab command
*/
export class CreatePrefabCommand extends BaseCommand {
private savedFilePath: string | null = null;
private savedGuid: string | null = null;
constructor(
private messageHub: MessageHub,
private fileAPI: IFileAPI,
private projectService: ProjectService | undefined,
private assetRegistry: AssetRegistryService | null,
private sourceEntity: Entity,
private options: CreatePrefabOptions
) {
super();
}
async execute(): Promise<void> {
const scene = Core.scene;
if (!scene) {
throw new Error('场景未初始化 | Scene not initialized');
}
// 获取层级系统 | Get hierarchy system
const hierarchySystem = scene.getSystem(HierarchySystem);
// 创建预制体数据 | Create prefab data
const prefabData = PrefabSerializer.createPrefab(
this.sourceEntity,
{
name: this.options.name,
description: this.options.description,
tags: this.options.tags,
includeChildren: this.options.includeChildren ?? true
},
hierarchySystem ?? undefined
);
// 序列化为 JSON | Serialize to JSON
const prefabJson = PrefabSerializer.serialize(prefabData, true);
// 确定保存路径 | Determine save path
let savePath = this.options.savePath;
if (!savePath && this.projectService?.isProjectOpen()) {
// 默认保存到项目的 prefabs 目录 | Default save to project's prefabs directory
const currentProject = this.projectService.getCurrentProject();
if (currentProject) {
const projectRoot = currentProject.path;
const sep = projectRoot.includes('\\') ? '\\' : '/';
savePath = `${projectRoot}${sep}assets${sep}prefabs`;
// 确保目录存在 | Ensure directory exists
await this.fileAPI.createDirectory(savePath);
}
}
// 构建完整文件路径 | Build complete file path
let fullPath: string | null = null;
if (savePath) {
const sep = savePath.includes('\\') ? '\\' : '/';
fullPath = `${savePath}${sep}${this.options.name}.prefab`;
} else {
// 打开保存对话框 | Open save dialog
fullPath = await this.fileAPI.saveSceneDialog(`${this.options.name}.prefab`);
}
if (!fullPath) {
throw new Error('保存被取消 | Save cancelled');
}
// 确保扩展名正确 | Ensure correct extension
if (!fullPath.endsWith('.prefab')) {
fullPath += '.prefab';
}
// 保存文件 | Save file
await this.fileAPI.writeFileContent(fullPath, prefabJson);
this.savedFilePath = fullPath;
// 注册资产以生成 .meta 文件 | Register asset to generate .meta file
if (this.assetRegistry) {
const guid = await this.assetRegistry.registerAsset(fullPath);
this.savedGuid = guid;
console.log(`[CreatePrefabCommand] Registered prefab asset with GUID: ${guid}`);
}
// 发布事件 | Publish event
await this.messageHub.publish('prefab:created', {
path: fullPath,
guid: this.savedGuid,
name: this.options.name,
sourceEntityId: this.sourceEntity.id,
sourceEntityName: this.sourceEntity.name
});
}
undo(): void {
// 预制体创建是一个文件系统操作,撤销意味着删除文件
// Prefab creation is a file system operation, undo means deleting the file
// 但为了安全,我们不自动删除文件,只是清除引用
// But for safety, we don't auto-delete the file, just clear the reference
this.savedFilePath = null;
// TODO: 如果需要完整撤销,可以实现文件删除
// TODO: If full undo is needed, implement file deletion
}
getDescription(): string {
return `创建预制体: ${this.options.name}`;
}
/**
* 获取保存的文件路径
* Get saved file path
*/
getSavedFilePath(): string | null {
return this.savedFilePath;
}
}

View File

@@ -0,0 +1,143 @@
/**
* 实例化预制体命令
* Instantiate prefab command
*
* 从预制体资产创建实体实例。
* Creates an entity instance from a prefab asset.
*/
import { Core, Entity, HierarchySystem, PrefabSerializer, ComponentRegistry } from '@esengine/ecs-framework';
import type { EntityStoreService, MessageHub } from '@esengine/editor-core';
import type { PrefabData, ComponentType } from '@esengine/ecs-framework';
import { BaseCommand } from '../BaseCommand';
/**
* 实例化预制体命令选项
* Instantiate prefab command options
*/
export interface InstantiatePrefabOptions {
/** 父实体 | Parent entity */
parent?: Entity;
/** 实例名称(可选,默认使用预制体名称) | Instance name (optional, defaults to prefab name) */
name?: string;
/** 位置覆盖 | Position override */
position?: { x: number; y: number };
/** 是否追踪为预制体实例 | Whether to track as prefab instance */
trackInstance?: boolean;
}
/**
* 实例化预制体命令
* Instantiate prefab command
*/
export class InstantiatePrefabCommand extends BaseCommand {
private createdEntity: Entity | null = null;
private createdEntityIds: number[] = [];
constructor(
private entityStore: EntityStoreService,
private messageHub: MessageHub,
private prefabData: PrefabData,
private options: InstantiatePrefabOptions = {}
) {
super();
}
execute(): void {
const scene = Core.scene;
if (!scene) {
throw new Error('场景未初始化 | Scene not initialized');
}
// 获取组件注册表 | Get component registry
// ComponentRegistry.getAllComponentNames() returns Map<string, Function>
// We need to cast it to Map<string, ComponentType>
const componentRegistry = ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>;
// 实例化预制体 | Instantiate prefab
this.createdEntity = PrefabSerializer.instantiate(
this.prefabData,
scene,
componentRegistry,
{
parentId: this.options.parent?.id,
name: this.options.name,
position: this.options.position,
trackInstance: this.options.trackInstance ?? true
}
);
// 收集所有创建的实体 ID用于撤销 | Collect all created entity IDs (for undo)
this.collectEntityIds(this.createdEntity);
// 更新 EntityStore | Update EntityStore
this.entityStore.syncFromScene();
// 选中创建的实体 | Select created entity
this.entityStore.selectEntity(this.createdEntity);
// 发布事件 | Publish event
this.messageHub.publish('entity:added', { entity: this.createdEntity });
this.messageHub.publish('prefab:instantiated', {
entity: this.createdEntity,
prefabName: this.prefabData.metadata.name,
prefabGuid: this.prefabData.metadata.guid
});
}
undo(): void {
if (!this.createdEntity) return;
const scene = Core.scene;
if (!scene) return;
// 移除所有创建的实体 | Remove all created entities
for (const entityId of this.createdEntityIds) {
const entity = scene.findEntityById(entityId);
if (entity) {
scene.entities.remove(entity);
}
}
// 更新 EntityStore | Update EntityStore
this.entityStore.syncFromScene();
// 发布事件 | Publish event
this.messageHub.publish('entity:removed', { entityId: this.createdEntity.id });
this.createdEntity = null;
this.createdEntityIds = [];
}
getDescription(): string {
const name = this.options.name || this.prefabData.metadata.name;
return `实例化预制体: ${name}`;
}
/**
* 获取创建的根实体
* Get created root entity
*/
getCreatedEntity(): Entity | null {
return this.createdEntity;
}
/**
* 递归收集实体 ID
* Recursively collect entity IDs
*/
private collectEntityIds(entity: Entity): void {
this.createdEntityIds.push(entity.id);
const scene = Core.scene;
if (!scene) return;
const hierarchySystem = scene.getSystem(HierarchySystem);
if (hierarchySystem) {
const children = hierarchySystem.getChildren(entity);
for (const child of children) {
this.collectEntityIds(child);
}
}
}
}

View File

@@ -0,0 +1,155 @@
/**
* 还原预制体实例命令
* Revert prefab instance command
*
* 将预制体实例还原为源预制体的状态。
* Reverts a prefab instance to the state of the source prefab.
*/
import { Entity, PrefabInstanceComponent } from '@esengine/ecs-framework';
import type { MessageHub, PrefabService } from '@esengine/editor-core';
import { BaseCommand } from '../BaseCommand';
/**
* 组件快照
* Component snapshot
*/
interface ComponentSnapshot {
typeName: string;
data: Record<string, unknown>;
}
/**
* 还原预制体实例命令
* Revert prefab instance command
*/
export class RevertPrefabCommand extends BaseCommand {
private previousSnapshots: ComponentSnapshot[] = [];
private previousModifiedProperties: string[] = [];
private previousOriginalValues: Record<string, unknown> = {};
private success: boolean = false;
constructor(
private prefabService: PrefabService,
private messageHub: MessageHub,
private entity: Entity
) {
super();
}
async execute(): Promise<void> {
// 保存当前状态用于撤销 | Save current state for undo
const comp = this.entity.getComponent(PrefabInstanceComponent);
if (comp) {
this.previousModifiedProperties = [...comp.modifiedProperties];
this.previousOriginalValues = { ...comp.originalValues };
// 保存所有修改的属性当前值 | Save current values of all modified properties
this.previousSnapshots = [];
for (const key of comp.modifiedProperties) {
const [componentType, ...pathParts] = key.split('.');
const propertyPath = pathParts.join('.');
for (const compInstance of this.entity.components) {
const typeName = (compInstance.constructor as any).__componentTypeName || compInstance.constructor.name;
if (typeName === componentType) {
const value = this.getNestedValue(compInstance, propertyPath);
this.previousSnapshots.push({
typeName: key,
data: { value: this.deepClone(value) }
});
break;
}
}
}
}
// 执行还原操作 | Execute revert operation
this.success = await this.prefabService.revertInstance(this.entity);
if (!this.success) {
throw new Error('Failed to revert prefab instance');
}
}
undo(): void {
// 恢复修改的属性值 | Restore modified property values
for (const snapshot of this.previousSnapshots) {
const [componentType, ...pathParts] = snapshot.typeName.split('.');
const propertyPath = pathParts.join('.');
for (const compInstance of this.entity.components) {
const typeName = (compInstance.constructor as any).__componentTypeName || compInstance.constructor.name;
if (typeName === componentType) {
this.setNestedValue(compInstance, propertyPath, snapshot.data.value);
break;
}
}
}
// 恢复修改状态 | Restore modification state
const comp = this.entity.getComponent(PrefabInstanceComponent);
if (comp) {
comp.modifiedProperties = this.previousModifiedProperties;
comp.originalValues = this.previousOriginalValues;
}
// 发布事件通知 UI 更新 | Publish event to notify UI update
this.messageHub.publish('component:property:changed', {
entityId: this.entity.id
});
}
getDescription(): string {
const comp = this.entity.getComponent(PrefabInstanceComponent);
const prefabName = comp?.sourcePrefabPath?.split(/[/\\]/).pop()?.replace('.prefab', '') || 'Prefab';
return `还原预制体实例: ${prefabName}`;
}
/**
* 获取嵌套属性值
* Get nested property value
*/
private getNestedValue(obj: any, path: string): unknown {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current === null || current === undefined) return undefined;
current = current[part];
}
return current;
}
/**
* 设置嵌套属性值
* Set nested property value
*/
private setNestedValue(obj: any, path: string, value: unknown): void {
const parts = path.split('.');
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i]!;
if (current[key] === null || current[key] === undefined) {
current[key] = {};
}
current = current[key];
}
current[parts[parts.length - 1]!] = value;
}
/**
* 深拷贝值
* Deep clone value
*/
private deepClone(value: unknown): unknown {
if (value === null || value === undefined) return value;
if (typeof value === 'object') {
try {
return JSON.parse(JSON.stringify(value));
} catch {
return value;
}
}
return value;
}
}

View File

@@ -0,0 +1,14 @@
/**
* 预制体命令导出
* Prefab commands export
*/
export { CreatePrefabCommand } from './CreatePrefabCommand';
export type { CreatePrefabOptions } from './CreatePrefabCommand';
export { InstantiatePrefabCommand } from './InstantiatePrefabCommand';
export type { InstantiatePrefabOptions } from './InstantiatePrefabCommand';
export { ApplyPrefabCommand } from './ApplyPrefabCommand';
export { RevertPrefabCommand } from './RevertPrefabCommand';
export { BreakPrefabLinkCommand } from './BreakPrefabLinkCommand';

View File

@@ -0,0 +1,193 @@
import { Entity, Component } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import { TransformComponent } from '@esengine/engine-core';
import { UITransformComponent } from '@esengine/ui';
import { BaseCommand } from '../BaseCommand';
import { ICommand } from '../ICommand';
/**
* Transform 状态快照
* Transform state snapshot
*/
export interface TransformState {
// TransformComponent
positionX?: number;
positionY?: number;
positionZ?: number;
rotationX?: number;
rotationY?: number;
rotationZ?: number;
scaleX?: number;
scaleY?: number;
scaleZ?: number;
// UITransformComponent
x?: number;
y?: number;
width?: number;
height?: number;
rotation?: number;
uiScaleX?: number;
uiScaleY?: number;
}
/**
* 变换操作类型
* Transform operation type
*/
export type TransformOperationType = 'move' | 'rotate' | 'scale';
/**
* 变换命令
* Transform command for undo/redo support
*/
export class TransformCommand extends BaseCommand {
private readonly componentType: 'transform' | 'uiTransform';
private readonly timestamp: number;
constructor(
private readonly messageHub: MessageHub,
private readonly entity: Entity,
private readonly component: Component,
private readonly operationType: TransformOperationType,
private readonly oldState: TransformState,
private newState: TransformState
) {
super();
this.componentType = component instanceof TransformComponent ? 'transform' : 'uiTransform';
this.timestamp = Date.now();
}
execute(): void {
this.applyState(this.newState);
this.notifyChange();
}
undo(): void {
this.applyState(this.oldState);
this.notifyChange();
}
getDescription(): string {
const opNames: Record<TransformOperationType, string> = {
move: '移动',
rotate: '旋转',
scale: '缩放'
};
return `${opNames[this.operationType]} ${this.entity.name || 'Entity'}`;
}
/**
* 检查是否可以与另一个命令合并
* 只有相同实体、相同操作类型、且在短时间内的命令可以合并
*/
canMergeWith(other: ICommand): boolean {
if (!(other instanceof TransformCommand)) return false;
// 相同实体、相同组件、相同操作类型
if (this.entity !== other.entity) return false;
if (this.component !== other.component) return false;
if (this.operationType !== other.operationType) return false;
// 时间间隔小于 500ms 才能合并(连续拖动)
const timeDiff = other.timestamp - this.timestamp;
return timeDiff < 500;
}
mergeWith(other: ICommand): ICommand {
if (!(other instanceof TransformCommand)) {
throw new Error('无法合并不同类型的命令');
}
// 保留原始 oldState使用新命令的 newState
return new TransformCommand(
this.messageHub,
this.entity,
this.component,
this.operationType,
this.oldState,
other.newState
);
}
/**
* 应用变换状态
* Apply transform state
*/
private applyState(state: TransformState): void {
if (this.componentType === 'transform') {
const transform = this.component as TransformComponent;
if (state.positionX !== undefined) transform.position.x = state.positionX;
if (state.positionY !== undefined) transform.position.y = state.positionY;
if (state.positionZ !== undefined) transform.position.z = state.positionZ;
if (state.rotationX !== undefined) transform.rotation.x = state.rotationX;
if (state.rotationY !== undefined) transform.rotation.y = state.rotationY;
if (state.rotationZ !== undefined) transform.rotation.z = state.rotationZ;
if (state.scaleX !== undefined) transform.scale.x = state.scaleX;
if (state.scaleY !== undefined) transform.scale.y = state.scaleY;
if (state.scaleZ !== undefined) transform.scale.z = state.scaleZ;
} else {
const uiTransform = this.component as UITransformComponent;
if (state.x !== undefined) uiTransform.x = state.x;
if (state.y !== undefined) uiTransform.y = state.y;
if (state.rotation !== undefined) uiTransform.rotation = state.rotation;
if (state.uiScaleX !== undefined) uiTransform.scaleX = state.uiScaleX;
if (state.uiScaleY !== undefined) uiTransform.scaleY = state.uiScaleY;
}
}
/**
* 通知属性变更
* Notify property change
*/
private notifyChange(): void {
const propertyName = this.operationType === 'move'
? (this.componentType === 'transform' ? 'position' : 'x')
: this.operationType === 'rotate'
? 'rotation'
: (this.componentType === 'transform' ? 'scale' : 'scaleX');
this.messageHub.publish('component:property:changed', {
entity: this.entity,
component: this.component,
propertyName,
value: this.componentType === 'transform'
? (this.component as TransformComponent)[propertyName as keyof TransformComponent]
: (this.component as UITransformComponent)[propertyName as keyof UITransformComponent]
});
// 通知 Inspector 刷新 | Notify Inspector to refresh
this.messageHub.publish('entity:select', { entityId: this.entity.id });
}
/**
* 从 TransformComponent 捕获状态
* Capture state from TransformComponent
*/
static captureTransformState(transform: TransformComponent): TransformState {
return {
positionX: transform.position.x,
positionY: transform.position.y,
positionZ: transform.position.z,
rotationX: transform.rotation.x,
rotationY: transform.rotation.y,
rotationZ: transform.rotation.z,
scaleX: transform.scale.x,
scaleY: transform.scale.y,
scaleZ: transform.scale.z
};
}
/**
* 从 UITransformComponent 捕获状态
* Capture state from UITransformComponent
*/
static captureUITransformState(uiTransform: UITransformComponent): TransformState {
return {
x: uiTransform.x,
y: uiTransform.y,
rotation: uiTransform.rotation,
uiScaleX: uiTransform.scaleX,
uiScaleY: uiTransform.scaleY
};
}
}