refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { ICommand } from './ICommand';
|
||||
|
||||
/**
|
||||
* 命令基类
|
||||
* 提供默认实现,具体命令继承此类
|
||||
*/
|
||||
export abstract class BaseCommand implements ICommand {
|
||||
abstract execute(): void;
|
||||
abstract undo(): void;
|
||||
abstract getDescription(): string;
|
||||
|
||||
/**
|
||||
* 默认不支持合并
|
||||
*/
|
||||
canMergeWith(_other: ICommand): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认抛出错误
|
||||
*/
|
||||
mergeWith(_other: ICommand): ICommand {
|
||||
throw new Error(`${this.constructor.name} 不支持合并操作`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { ICommand } from './ICommand';
|
||||
|
||||
/**
|
||||
* 命令历史记录配置
|
||||
*/
|
||||
export interface CommandManagerConfig {
|
||||
/**
|
||||
* 最大历史记录数量
|
||||
*/
|
||||
maxHistorySize?: number;
|
||||
|
||||
/**
|
||||
* 是否自动合并相似命令
|
||||
*/
|
||||
autoMerge?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令管理器
|
||||
* 管理命令的执行、撤销、重做以及历史记录
|
||||
*/
|
||||
export class CommandManager {
|
||||
private undoStack: ICommand[] = [];
|
||||
private redoStack: ICommand[] = [];
|
||||
private readonly config: Required<CommandManagerConfig>;
|
||||
private isExecuting = false;
|
||||
|
||||
constructor(config: CommandManagerConfig = {}) {
|
||||
this.config = {
|
||||
maxHistorySize: config.maxHistorySize ?? 100,
|
||||
autoMerge: config.autoMerge ?? true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
execute(command: ICommand): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中执行新命令');
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.execute();
|
||||
|
||||
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();
|
||||
}
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销上一个命令
|
||||
*/
|
||||
undo(): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中撤销');
|
||||
}
|
||||
|
||||
const command = this.undoStack.pop();
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.undo();
|
||||
this.redoStack.push(command);
|
||||
} catch (error) {
|
||||
this.undoStack.push(command);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做上一个被撤销的命令
|
||||
*/
|
||||
redo(): void {
|
||||
if (this.isExecuting) {
|
||||
throw new Error('不能在命令执行过程中重做');
|
||||
}
|
||||
|
||||
const command = this.redoStack.pop();
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
|
||||
try {
|
||||
command.execute();
|
||||
this.undoStack.push(command);
|
||||
} catch (error) {
|
||||
this.redoStack.push(command);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以撤销
|
||||
*/
|
||||
canUndo(): boolean {
|
||||
return this.undoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以重做
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取撤销栈的描述列表
|
||||
*/
|
||||
getUndoHistory(): string[] {
|
||||
return this.undoStack.map((cmd) => cmd.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重做栈的描述列表
|
||||
*/
|
||||
getRedoHistory(): string[] {
|
||||
return this.redoStack.map((cmd) => cmd.getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有历史记录
|
||||
*/
|
||||
clear(): void {
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行命令(作为单一操作,可以一次撤销)
|
||||
*/
|
||||
executeBatch(commands: ICommand[]): void {
|
||||
if (commands.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量命令
|
||||
* 将多个命令组合为一个命令
|
||||
*/
|
||||
class BatchCommand implements ICommand {
|
||||
constructor(private readonly commands: ICommand[]) {}
|
||||
|
||||
execute(): void {
|
||||
for (const command of this.commands) {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
for (let i = this.commands.length - 1; i >= 0; i--) {
|
||||
const command = this.commands[i];
|
||||
if (command) {
|
||||
command.undo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `批量操作 (${this.commands.length} 个命令)`;
|
||||
}
|
||||
|
||||
canMergeWith(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
mergeWith(): ICommand {
|
||||
throw new Error('批量命令不支持合并');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 命令接口
|
||||
* 实现命令模式,支持撤销/重做功能
|
||||
*/
|
||||
export interface ICommand {
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
execute(): void;
|
||||
|
||||
/**
|
||||
* 撤销命令
|
||||
*/
|
||||
undo(): void;
|
||||
|
||||
/**
|
||||
* 获取命令描述(用于显示历史记录)
|
||||
*/
|
||||
getDescription(): string;
|
||||
|
||||
/**
|
||||
* 检查命令是否可以合并
|
||||
* 用于优化撤销/重做历史,例如连续的移动操作可以合并为一个
|
||||
*/
|
||||
canMergeWith(other: ICommand): boolean;
|
||||
|
||||
/**
|
||||
* 与另一个命令合并
|
||||
*/
|
||||
mergeWith(other: ICommand): ICommand;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Entity, Component, getComponentDependencies, getComponentTypeName } from '@esengine/ecs-framework';
|
||||
import { MessageHub, EditorComponentRegistry } 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,
|
||||
private entity: Entity,
|
||||
private ComponentClass: new () => Component,
|
||||
private initialData?: Record<string, unknown>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
this.entity.addComponent(this.component);
|
||||
|
||||
this.messageHub.publish('component:added', {
|
||||
entity: this.entity,
|
||||
component: this.component
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加缺失的依赖组件
|
||||
* Add missing dependency components
|
||||
*/
|
||||
private addMissingDependencies(): void {
|
||||
const dependencies = getComponentDependencies(this.ComponentClass);
|
||||
|
||||
if (!dependencies || dependencies.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentRegistry = Core.services.tryResolve(EditorComponentRegistry) as EditorComponentRegistry | 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: 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 {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* 移除组件命令
|
||||
*/
|
||||
export class RemoveComponentCommand extends BaseCommand {
|
||||
private componentData: Record<string, unknown> = {};
|
||||
private ComponentClass: new () => Component;
|
||||
|
||||
constructor(
|
||||
private messageHub: MessageHub,
|
||||
private entity: Entity,
|
||||
private component: Component
|
||||
) {
|
||||
super();
|
||||
this.ComponentClass = component.constructor as new () => Component;
|
||||
|
||||
// 保存组件数据用于撤销
|
||||
for (const key of Object.keys(component)) {
|
||||
if (key !== 'entity' && key !== 'id') {
|
||||
this.componentData[key] = (component as any)[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.entity.removeComponent(this.component);
|
||||
|
||||
this.messageHub.publish('component:removed', {
|
||||
entity: this.entity,
|
||||
componentType: this.ComponentClass.name
|
||||
});
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const newComponent = new this.ComponentClass();
|
||||
|
||||
// 恢复数据
|
||||
for (const [key, value] of Object.entries(this.componentData)) {
|
||||
(newComponent as any)[key] = value;
|
||||
}
|
||||
|
||||
this.entity.addComponent(newComponent);
|
||||
this.component = newComponent;
|
||||
|
||||
this.messageHub.publish('component:added', {
|
||||
entity: this.entity,
|
||||
component: newComponent
|
||||
});
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `移除组件: ${this.ComponentClass.name}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ICommand } from '../ICommand';
|
||||
|
||||
/**
|
||||
* 更新组件属性命令
|
||||
*/
|
||||
export class UpdateComponentCommand extends BaseCommand {
|
||||
private oldValue: unknown;
|
||||
|
||||
constructor(
|
||||
private messageHub: MessageHub,
|
||||
private entity: Entity,
|
||||
private component: Component,
|
||||
private propertyName: string,
|
||||
private newValue: unknown
|
||||
) {
|
||||
super();
|
||||
this.oldValue = (component as any)[propertyName];
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
(this.component as any)[this.propertyName] = this.newValue;
|
||||
|
||||
this.messageHub.publish('component:property:changed', {
|
||||
entity: this.entity,
|
||||
component: this.component,
|
||||
propertyName: this.propertyName,
|
||||
value: this.newValue
|
||||
});
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
(this.component as any)[this.propertyName] = this.oldValue;
|
||||
|
||||
this.messageHub.publish('component:property:changed', {
|
||||
entity: this.entity,
|
||||
component: this.component,
|
||||
propertyName: this.propertyName,
|
||||
value: this.oldValue
|
||||
});
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `更新 ${this.component.constructor.name}.${this.propertyName}`;
|
||||
}
|
||||
|
||||
canMergeWith(other: ICommand): boolean {
|
||||
if (!(other instanceof UpdateComponentCommand)) return false;
|
||||
|
||||
return (
|
||||
this.entity === other.entity &&
|
||||
this.component === other.component &&
|
||||
this.propertyName === other.propertyName
|
||||
);
|
||||
}
|
||||
|
||||
mergeWith(other: ICommand): ICommand {
|
||||
if (!(other instanceof UpdateComponentCommand)) {
|
||||
throw new Error('无法合并不同类型的命令');
|
||||
}
|
||||
|
||||
// 保留原始值,使用新命令的新值
|
||||
const merged = new UpdateComponentCommand(
|
||||
this.messageHub,
|
||||
this.entity,
|
||||
this.component,
|
||||
this.propertyName,
|
||||
other.newValue
|
||||
);
|
||||
merged.oldValue = this.oldValue;
|
||||
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { AddComponentCommand } from './AddComponentCommand';
|
||||
export { RemoveComponentCommand } from './RemoveComponentCommand';
|
||||
export { UpdateComponentCommand } from './UpdateComponentCommand';
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* 创建带动画组件的Sprite实体命令
|
||||
*/
|
||||
export class CreateAnimatedSpriteEntityCommand extends BaseCommand {
|
||||
private entity: Entity | null = null;
|
||||
private entityId: number | null = null;
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entityName: string,
|
||||
private parentEntity?: Entity
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('场景未初始化');
|
||||
}
|
||||
|
||||
this.entity = scene.createEntity(this.entityName);
|
||||
this.entityId = this.entity.id;
|
||||
|
||||
// 添加 Transform、Sprite、Animator 和 Hierarchy 组件
|
||||
this.entity.addComponent(new TransformComponent());
|
||||
this.entity.addComponent(new SpriteComponent());
|
||||
this.entity.addComponent(new SpriteAnimatorComponent());
|
||||
this.entity.addComponent(new HierarchyComponent());
|
||||
|
||||
if (this.parentEntity) {
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
hierarchySystem?.setParent(this.entity, this.parentEntity);
|
||||
}
|
||||
|
||||
this.entityStore.addEntity(this.entity, this.parentEntity);
|
||||
this.entityStore.selectEntity(this.entity);
|
||||
|
||||
this.messageHub.publish('entity:added', { entity: this.entity });
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.entity) return;
|
||||
|
||||
this.entityStore.removeEntity(this.entity);
|
||||
this.entity.destroy();
|
||||
|
||||
this.messageHub.publish('entity:removed', { entityId: this.entityId });
|
||||
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建动画Sprite实体: ${this.entityName}`;
|
||||
}
|
||||
|
||||
getCreatedEntity(): Entity | null {
|
||||
return this.entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { CameraComponent } from '@esengine/camera';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* 创建带Camera组件的实体命令
|
||||
*/
|
||||
export class CreateCameraEntityCommand extends BaseCommand {
|
||||
private entity: Entity | null = null;
|
||||
private entityId: number | null = null;
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entityName: string,
|
||||
private parentEntity?: Entity
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('场景未初始化');
|
||||
}
|
||||
|
||||
this.entity = scene.createEntity(this.entityName);
|
||||
this.entityId = this.entity.id;
|
||||
|
||||
// 添加 Transform、Camera 和 Hierarchy 组件
|
||||
this.entity.addComponent(new TransformComponent());
|
||||
this.entity.addComponent(new CameraComponent());
|
||||
this.entity.addComponent(new HierarchyComponent());
|
||||
|
||||
if (this.parentEntity) {
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
hierarchySystem?.setParent(this.entity, this.parentEntity);
|
||||
}
|
||||
|
||||
this.entityStore.addEntity(this.entity, this.parentEntity);
|
||||
this.entityStore.selectEntity(this.entity);
|
||||
|
||||
this.messageHub.publish('entity:added', { entity: this.entity });
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.entity) return;
|
||||
|
||||
this.entityStore.removeEntity(this.entity);
|
||||
this.entity.destroy();
|
||||
|
||||
this.messageHub.publish('entity:removed', { entityId: this.entityId });
|
||||
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建Camera实体: ${this.entityName}`;
|
||||
}
|
||||
|
||||
getCreatedEntity(): Entity | null {
|
||||
return this.entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* 创建实体命令
|
||||
*/
|
||||
export class CreateEntityCommand extends BaseCommand {
|
||||
private entity: Entity | null = null;
|
||||
private entityId: number | null = null;
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entityName: string,
|
||||
private parentEntity?: Entity
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('场景未初始化');
|
||||
}
|
||||
|
||||
this.entity = scene.createEntity(this.entityName);
|
||||
this.entityId = this.entity.id;
|
||||
|
||||
// 自动添加 Transform 组件
|
||||
this.entity.addComponent(new TransformComponent());
|
||||
|
||||
// 添加 HierarchyComponent 支持层级结构
|
||||
this.entity.addComponent(new HierarchyComponent());
|
||||
|
||||
if (this.parentEntity) {
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
hierarchySystem?.setParent(this.entity, this.parentEntity);
|
||||
}
|
||||
|
||||
this.entityStore.addEntity(this.entity, this.parentEntity);
|
||||
this.entityStore.selectEntity(this.entity);
|
||||
|
||||
this.messageHub.publish('entity:added', { entity: this.entity });
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.entity) return;
|
||||
|
||||
this.entityStore.removeEntity(this.entity);
|
||||
this.entity.destroy();
|
||||
|
||||
this.messageHub.publish('entity:removed', { entityId: this.entityId });
|
||||
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建实体: ${this.entityName}`;
|
||||
}
|
||||
|
||||
getCreatedEntity(): Entity | null {
|
||||
return this.entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { SpriteComponent } from '@esengine/sprite';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* 创建带Sprite组件的实体命令
|
||||
*/
|
||||
export class CreateSpriteEntityCommand extends BaseCommand {
|
||||
private entity: Entity | null = null;
|
||||
private entityId: number | null = null;
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entityName: string,
|
||||
private parentEntity?: Entity
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('场景未初始化');
|
||||
}
|
||||
|
||||
this.entity = scene.createEntity(this.entityName);
|
||||
this.entityId = this.entity.id;
|
||||
|
||||
// 添加 Transform、Sprite 和 Hierarchy 组件
|
||||
this.entity.addComponent(new TransformComponent());
|
||||
this.entity.addComponent(new SpriteComponent());
|
||||
this.entity.addComponent(new HierarchyComponent());
|
||||
|
||||
if (this.parentEntity) {
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
hierarchySystem?.setParent(this.entity, this.parentEntity);
|
||||
}
|
||||
|
||||
this.entityStore.addEntity(this.entity, this.parentEntity);
|
||||
this.entityStore.selectEntity(this.entity);
|
||||
|
||||
this.messageHub.publish('entity:added', { entity: this.entity });
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.entity) return;
|
||||
|
||||
this.entityStore.removeEntity(this.entity);
|
||||
this.entity.destroy();
|
||||
|
||||
this.messageHub.publish('entity:removed', { entityId: this.entityId });
|
||||
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建Sprite实体: ${this.entityName}`;
|
||||
}
|
||||
|
||||
getCreatedEntity(): Entity | null {
|
||||
return this.entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { TilemapComponent } from '@esengine/tilemap';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* Tilemap创建选项
|
||||
*/
|
||||
export interface TilemapCreationOptions {
|
||||
/** 地图宽度(瓦片数),默认10 */
|
||||
width?: number;
|
||||
/** 地图高度(瓦片数),默认10 */
|
||||
height?: number;
|
||||
/** 瓦片宽度(像素),默认32 */
|
||||
tileWidth?: number;
|
||||
/** 瓦片高度(像素),默认32 */
|
||||
tileHeight?: number;
|
||||
/** 渲染层级,默认0 */
|
||||
sortingOrder?: number;
|
||||
/** 初始Tileset源路径 */
|
||||
tilesetSource?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带Tilemap组件的实体命令
|
||||
*/
|
||||
export class CreateTilemapEntityCommand extends BaseCommand {
|
||||
private entity: Entity | null = null;
|
||||
private entityId: number | null = null;
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entityName: string,
|
||||
private parentEntity?: Entity,
|
||||
private options: TilemapCreationOptions = {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('场景未初始化');
|
||||
}
|
||||
|
||||
this.entity = scene.createEntity(this.entityName);
|
||||
this.entityId = this.entity.id;
|
||||
|
||||
// 添加 Transform 和 Hierarchy 组件
|
||||
this.entity.addComponent(new TransformComponent());
|
||||
this.entity.addComponent(new HierarchyComponent());
|
||||
|
||||
// 创建并配置Tilemap组件
|
||||
const tilemapComponent = new TilemapComponent();
|
||||
|
||||
// 应用配置选项
|
||||
const {
|
||||
width = 10,
|
||||
height = 10,
|
||||
tileWidth = 32,
|
||||
tileHeight = 32,
|
||||
sortingOrder = 0,
|
||||
tilesetSource
|
||||
} = this.options;
|
||||
|
||||
tilemapComponent.tileWidth = tileWidth;
|
||||
tilemapComponent.tileHeight = tileHeight;
|
||||
tilemapComponent.sortingOrder = sortingOrder;
|
||||
|
||||
// 初始化空白地图
|
||||
tilemapComponent.initializeEmpty(width, height);
|
||||
|
||||
// 添加初始 Tileset
|
||||
if (tilesetSource) {
|
||||
tilemapComponent.addTileset(tilesetSource);
|
||||
}
|
||||
|
||||
this.entity.addComponent(tilemapComponent);
|
||||
|
||||
if (this.parentEntity) {
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
hierarchySystem?.setParent(this.entity, this.parentEntity);
|
||||
}
|
||||
|
||||
this.entityStore.addEntity(this.entity, this.parentEntity);
|
||||
this.entityStore.selectEntity(this.entity);
|
||||
|
||||
this.messageHub.publish('entity:added', { entity: this.entity });
|
||||
this.messageHub.publish('tilemap:created', {
|
||||
entity: this.entity,
|
||||
component: tilemapComponent
|
||||
});
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.entity) return;
|
||||
|
||||
this.entityStore.removeEntity(this.entity);
|
||||
this.entity.destroy();
|
||||
|
||||
this.messageHub.publish('entity:removed', { entityId: this.entityId });
|
||||
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `创建Tilemap实体: ${this.entityName}`;
|
||||
}
|
||||
|
||||
getCreatedEntity(): Entity | null {
|
||||
return this.entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Core, Entity, Component, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* 删除实体命令
|
||||
*/
|
||||
export class DeleteEntityCommand extends BaseCommand {
|
||||
private entityId: number;
|
||||
private entityName: string;
|
||||
private parentEntityId: number | null;
|
||||
private components: Component[] = [];
|
||||
private childEntityIds: number[] = [];
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entity: Entity
|
||||
) {
|
||||
super();
|
||||
this.entityId = entity.id;
|
||||
this.entityName = entity.name;
|
||||
|
||||
// 通过 HierarchyComponent 获取父实体 ID
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
this.parentEntityId = hierarchy?.parentId ?? null;
|
||||
|
||||
// 保存组件状态用于撤销
|
||||
this.components = [...entity.components];
|
||||
|
||||
// 保存子实体 ID
|
||||
this.childEntityIds = hierarchy?.childIds ? [...hierarchy.childIds] : [];
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
// 先移除子实体
|
||||
for (const childId of this.childEntityIds) {
|
||||
const child = scene.findEntityById(childId);
|
||||
if (child) {
|
||||
this.entityStore.removeEntity(child);
|
||||
}
|
||||
}
|
||||
|
||||
this.entityStore.removeEntity(this.entity);
|
||||
this.entity.destroy();
|
||||
|
||||
this.messageHub.publish('entity:removed', { entityId: this.entityId });
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('场景未初始化');
|
||||
}
|
||||
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
|
||||
// 重新创建实体
|
||||
const newEntity = scene.createEntity(this.entityName);
|
||||
|
||||
// 设置父实体
|
||||
if (this.parentEntityId !== null && hierarchySystem) {
|
||||
const parentEntity = scene.findEntityById(this.parentEntityId);
|
||||
if (parentEntity) {
|
||||
hierarchySystem.setParent(newEntity, parentEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复组件
|
||||
for (const component of this.components) {
|
||||
// 创建组件副本
|
||||
const ComponentClass = component.constructor as new () => Component;
|
||||
const newComponent = new ComponentClass();
|
||||
|
||||
// 复制属性
|
||||
for (const key of Object.keys(component)) {
|
||||
if (key !== 'entity' && key !== 'id') {
|
||||
(newComponent as any)[key] = (component as any)[key];
|
||||
}
|
||||
}
|
||||
|
||||
newEntity.addComponent(newComponent);
|
||||
}
|
||||
|
||||
// 恢复子实体
|
||||
for (const childId of this.childEntityIds) {
|
||||
const child = scene.findEntityById(childId);
|
||||
if (child && hierarchySystem) {
|
||||
hierarchySystem.setParent(child, newEntity);
|
||||
this.entityStore.addEntity(child, newEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取父实体
|
||||
const parentEntity = this.parentEntityId !== null
|
||||
? scene.findEntityById(this.parentEntityId) ?? undefined
|
||||
: undefined;
|
||||
|
||||
this.entityStore.addEntity(newEntity, parentEntity);
|
||||
this.entityStore.selectEntity(newEntity);
|
||||
|
||||
// 更新引用
|
||||
this.entity = newEntity;
|
||||
|
||||
this.messageHub.publish('entity:added', { entity: newEntity });
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `删除实体: ${this.entityName}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
/**
|
||||
* 拖放位置类型
|
||||
*/
|
||||
export enum DropPosition {
|
||||
/** 在目标之前 */
|
||||
BEFORE = 'before',
|
||||
/** 在目标内部(作为子级) */
|
||||
INSIDE = 'inside',
|
||||
/** 在目标之后 */
|
||||
AFTER = 'after'
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新设置实体父级命令
|
||||
*
|
||||
* 支持拖拽重排功能,可以将实体移动到:
|
||||
* - 另一个实体之前 (BEFORE)
|
||||
* - 另一个实体内部作为子级 (INSIDE)
|
||||
* - 另一个实体之后 (AFTER)
|
||||
*/
|
||||
export class ReparentEntityCommand extends BaseCommand {
|
||||
private oldParentId: number | null;
|
||||
private oldSiblingIndex: number;
|
||||
|
||||
constructor(
|
||||
private entityStore: EntityStoreService,
|
||||
private messageHub: MessageHub,
|
||||
private entity: Entity,
|
||||
private targetEntity: Entity,
|
||||
private dropPosition: DropPosition
|
||||
) {
|
||||
super();
|
||||
|
||||
// 保存原始状态用于撤销
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
this.oldParentId = hierarchy?.parentId ?? null;
|
||||
|
||||
// 获取在兄弟列表中的原始索引
|
||||
this.oldSiblingIndex = this.getSiblingIndex(entity);
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
console.warn('[ReparentEntityCommand] No scene available');
|
||||
return;
|
||||
}
|
||||
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
if (!hierarchySystem) {
|
||||
console.warn('[ReparentEntityCommand] No HierarchySystem found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保目标实体有 HierarchyComponent
|
||||
if (!this.targetEntity.getComponent(HierarchyComponent)) {
|
||||
this.targetEntity.addComponent(new HierarchyComponent());
|
||||
}
|
||||
|
||||
console.log(`[ReparentEntityCommand] Moving ${this.entity.name} to ${this.targetEntity.name} (${this.dropPosition})`);
|
||||
|
||||
switch (this.dropPosition) {
|
||||
case DropPosition.INSIDE:
|
||||
// 移动到目标实体内部作为最后一个子级
|
||||
hierarchySystem.setParent(this.entity, this.targetEntity);
|
||||
break;
|
||||
|
||||
case DropPosition.BEFORE:
|
||||
case DropPosition.AFTER:
|
||||
// 移动到目标实体的同级
|
||||
this.moveToSibling(hierarchySystem);
|
||||
break;
|
||||
}
|
||||
|
||||
this.entityStore.syncFromScene();
|
||||
this.messageHub.publish('entity:reparented', {
|
||||
entityId: this.entity.id,
|
||||
targetId: this.targetEntity.id,
|
||||
position: this.dropPosition
|
||||
});
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
if (!hierarchySystem) return;
|
||||
|
||||
// 恢复到原始父级
|
||||
const oldParent = this.oldParentId !== null
|
||||
? scene.findEntityById(this.oldParentId)
|
||||
: null;
|
||||
|
||||
if (oldParent) {
|
||||
// 恢复到原始父级的指定位置
|
||||
hierarchySystem.insertChildAt(oldParent, this.entity, this.oldSiblingIndex);
|
||||
} else {
|
||||
// 恢复到根级
|
||||
hierarchySystem.setParent(this.entity, null);
|
||||
}
|
||||
|
||||
this.entityStore.syncFromScene();
|
||||
this.messageHub.publish('entity:reparented', {
|
||||
entityId: this.entity.id,
|
||||
targetId: null,
|
||||
position: 'undo'
|
||||
});
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const positionText = this.dropPosition === DropPosition.INSIDE
|
||||
? '移入'
|
||||
: this.dropPosition === DropPosition.BEFORE ? '移动到前面' : '移动到后面';
|
||||
return `${positionText}: ${this.entity.name} -> ${this.targetEntity.name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动到目标的同级位置
|
||||
*/
|
||||
private moveToSibling(hierarchySystem: HierarchySystem): void {
|
||||
const targetHierarchy = this.targetEntity.getComponent(HierarchyComponent);
|
||||
const targetParentId = targetHierarchy?.parentId ?? null;
|
||||
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
// 获取目标的父实体
|
||||
const targetParent = targetParentId !== null
|
||||
? scene.findEntityById(targetParentId)
|
||||
: null;
|
||||
|
||||
// 获取目标在兄弟列表中的索引
|
||||
let targetIndex = this.getSiblingIndex(this.targetEntity);
|
||||
|
||||
// 根据放置位置调整索引
|
||||
if (this.dropPosition === DropPosition.AFTER) {
|
||||
targetIndex++;
|
||||
}
|
||||
|
||||
// 如果移动到同一父级下,需要考虑原位置对索引的影响
|
||||
const entityHierarchy = this.entity.getComponent(HierarchyComponent);
|
||||
const entityParentId = entityHierarchy?.parentId ?? null;
|
||||
|
||||
const bSameParent = entityParentId === targetParentId;
|
||||
if (bSameParent) {
|
||||
const currentIndex = this.getSiblingIndex(this.entity);
|
||||
if (currentIndex < targetIndex) {
|
||||
targetIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[ReparentEntityCommand] moveToSibling: targetParent=${targetParent?.name ?? 'ROOT'}, targetIndex=${targetIndex}`);
|
||||
|
||||
if (targetParent) {
|
||||
// 有父级,插入到父级的指定位置
|
||||
hierarchySystem.insertChildAt(targetParent, this.entity, targetIndex);
|
||||
} else {
|
||||
// 目标在根级
|
||||
// 先确保实体移动到根级
|
||||
if (entityParentId !== null) {
|
||||
hierarchySystem.setParent(this.entity, null);
|
||||
}
|
||||
// 然后调整根级顺序
|
||||
this.entityStore.reorderEntity(this.entity.id, targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体在兄弟列表中的索引
|
||||
*/
|
||||
private getSiblingIndex(entity: Entity): number {
|
||||
const scene = Core.scene;
|
||||
if (!scene) return 0;
|
||||
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
const parentId = hierarchy?.parentId;
|
||||
|
||||
if (parentId === null || parentId === undefined) {
|
||||
// 根级实体,从 EntityStoreService 获取
|
||||
return this.entityStore.getRootEntityIds().indexOf(entity.id);
|
||||
}
|
||||
|
||||
const parent = scene.findEntityById(parentId);
|
||||
if (!parent) return 0;
|
||||
|
||||
const parentHierarchy = parent.getComponent(HierarchyComponent);
|
||||
return parentHierarchy?.childIds.indexOf(entity.id) ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { CreateEntityCommand } from './CreateEntityCommand';
|
||||
export { CreateSpriteEntityCommand } from './CreateSpriteEntityCommand';
|
||||
export { CreateAnimatedSpriteEntityCommand } from './CreateAnimatedSpriteEntityCommand';
|
||||
export { CreateCameraEntityCommand } from './CreateCameraEntityCommand';
|
||||
export { CreateTilemapEntityCommand } from './CreateTilemapEntityCommand';
|
||||
export { DeleteEntityCommand } from './DeleteEntityCommand';
|
||||
export { ReparentEntityCommand, DropPosition } from './ReparentEntityCommand';
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type { ICommand } from './ICommand';
|
||||
export { BaseCommand } from './BaseCommand';
|
||||
export { CommandManager } from './CommandManager';
|
||||
export { TransformCommand, type TransformState, type TransformOperationType } from './transform/TransformCommand';
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 实例化预制体命令
|
||||
* Instantiate prefab command
|
||||
*
|
||||
* 从预制体资产创建实体实例。
|
||||
* Creates an entity instance from a prefab asset.
|
||||
*/
|
||||
|
||||
import { Core, Entity, HierarchySystem, PrefabSerializer, GlobalComponentRegistry } 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
|
||||
// GlobalComponentRegistry.getAllComponentNames() returns Map<string, Function>
|
||||
// We need to cast it to Map<string, ComponentType>
|
||||
const componentRegistry = GlobalComponentRegistry.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { ICommand } from '../ICommand';
|
||||
|
||||
/**
|
||||
* Transform 状态快照
|
||||
* Transform state snapshot
|
||||
*/
|
||||
export interface TransformState {
|
||||
positionX?: number;
|
||||
positionY?: number;
|
||||
positionZ?: number;
|
||||
rotationX?: number;
|
||||
rotationY?: number;
|
||||
rotationZ?: number;
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
scaleZ?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换操作类型
|
||||
* Transform operation type
|
||||
*/
|
||||
export type TransformOperationType = 'move' | 'rotate' | 'scale';
|
||||
|
||||
/**
|
||||
* 变换命令
|
||||
* Transform command for undo/redo support
|
||||
*/
|
||||
export class TransformCommand extends BaseCommand {
|
||||
private readonly timestamp: number;
|
||||
|
||||
constructor(
|
||||
private readonly messageHub: MessageHub,
|
||||
private readonly entity: Entity,
|
||||
private readonly component: TransformComponent,
|
||||
private readonly operationType: TransformOperationType,
|
||||
private readonly oldState: TransformState,
|
||||
private newState: TransformState
|
||||
) {
|
||||
super();
|
||||
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 {
|
||||
const transform = this.component;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知属性变更
|
||||
* Notify property change
|
||||
*/
|
||||
private notifyChange(): void {
|
||||
const propertyName = this.operationType === 'move'
|
||||
? 'position'
|
||||
: this.operationType === 'rotate'
|
||||
? 'rotation'
|
||||
: 'scale';
|
||||
|
||||
this.messageHub.publish('component:property:changed', {
|
||||
entity: this.entity,
|
||||
component: this.component,
|
||||
propertyName,
|
||||
value: this.component[propertyName as keyof TransformComponent]
|
||||
});
|
||||
|
||||
// 通知 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
|
||||
};
|
||||
}
|
||||
}
|
||||
1
packages/editor/editor-app/src/application/index.ts
Normal file
1
packages/editor/editor-app/src/application/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './commands';
|
||||
Reference in New Issue
Block a user