Feature/tilemap editor (#237)

* feat: 添加 Tilemap 编辑器插件和组件生命周期支持

* feat(editor-core): 添加声明式插件注册 API

* feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor

* feat: 添加 editor-runtime SDK 和插件系统改进

* fix(ci): 修复SceneResourceManager里变量未使用问题
This commit is contained in:
YHH
2025-11-25 22:23:19 +08:00
committed by GitHub
parent 551ca7805d
commit 3fb6f919f8
166 changed files with 54691 additions and 8674 deletions

View File

@@ -31,3 +31,7 @@ export class CompilerRegistry implements IService {
this.clear();
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const ICompilerRegistry = Symbol.for('ICompilerRegistry');

View File

@@ -0,0 +1,95 @@
/**
* Component Action Registry Service
*
* Manages component-specific actions for the inspector panel
*/
import { injectable } from 'tsyringe';
import type { IService, Component, Entity } from '@esengine/ecs-framework';
import type { ReactNode } from 'react';
export interface ComponentAction {
id: string;
componentName: string;
label: string;
icon?: ReactNode;
order?: number;
execute: (component: Component, entity: Entity) => void | Promise<void>;
}
@injectable()
export class ComponentActionRegistry implements IService {
private actions: Map<string, ComponentAction[]> = new Map();
/**
* Register a component action
*/
register(action: ComponentAction): void {
const componentName = action.componentName;
if (!this.actions.has(componentName)) {
this.actions.set(componentName, []);
}
const actions = this.actions.get(componentName)!;
const existingIndex = actions.findIndex(a => a.id === action.id);
if (existingIndex >= 0) {
console.warn(`[ComponentActionRegistry] Action '${action.id}' already exists for '${componentName}', overwriting`);
actions[existingIndex] = action;
} else {
actions.push(action);
}
}
/**
* Register multiple actions
*/
registerMany(actions: ComponentAction[]): void {
for (const action of actions) {
this.register(action);
}
}
/**
* Unregister an action by ID
*/
unregister(componentName: string, actionId: string): void {
const actions = this.actions.get(componentName);
if (actions) {
const index = actions.findIndex(a => a.id === actionId);
if (index >= 0) {
actions.splice(index, 1);
}
}
}
/**
* Get all actions for a component type sorted by order
*/
getActionsForComponent(componentName: string): ComponentAction[] {
const actions = this.actions.get(componentName) || [];
return [...actions].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Check if a component has any actions
*/
hasActions(componentName: string): boolean {
const actions = this.actions.get(componentName);
return actions !== undefined && actions.length > 0;
}
/**
* Clear all actions
*/
clear(): void {
this.actions.clear();
}
/**
* Dispose resources
*/
dispose(): void {
this.actions.clear();
}
}

View File

@@ -0,0 +1,76 @@
/**
* Entity Creation Registry Service
*
* Manages entity creation templates for the scene hierarchy context menu
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import type { EntityCreationTemplate } from '../Types/UITypes';
@injectable()
export class EntityCreationRegistry implements IService {
private templates: Map<string, EntityCreationTemplate> = new Map();
/**
* Register an entity creation template
*/
register(template: EntityCreationTemplate): void {
if (this.templates.has(template.id)) {
console.warn(`[EntityCreationRegistry] Template '${template.id}' already exists, overwriting`);
}
this.templates.set(template.id, template);
}
/**
* Register multiple templates
*/
registerMany(templates: EntityCreationTemplate[]): void {
for (const template of templates) {
this.register(template);
}
}
/**
* Unregister a template by ID
*/
unregister(id: string): void {
this.templates.delete(id);
}
/**
* Get all registered templates sorted by order
*/
getAll(): EntityCreationTemplate[] {
return Array.from(this.templates.values())
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Get a template by ID
*/
get(id: string): EntityCreationTemplate | undefined {
return this.templates.get(id);
}
/**
* Check if a template exists
*/
has(id: string): boolean {
return this.templates.has(id);
}
/**
* Clear all templates
*/
clear(): void {
this.templates.clear();
}
/**
* Dispose resources
*/
dispose(): void {
this.templates.clear();
}
}

View File

@@ -14,8 +14,13 @@ export interface SaveDialogOptions extends DialogOptions {
}
export interface IDialog {
dispose(): void;
openDialog(options: OpenDialogOptions): Promise<string | string[] | null>;
saveDialog(options: SaveDialogOptions): Promise<string | null>;
showMessage(title: string, message: string, type?: 'info' | 'warning' | 'error'): Promise<void>;
showConfirm(title: string, message: string): Promise<boolean>;
}
// Service identifier for DI registration
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IDialogService = Symbol.for('IDialogService');

View File

@@ -1,4 +1,5 @@
export interface IFileSystem {
dispose(): void;
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>;
writeBinary(path: string, data: Uint8Array): Promise<void>;
@@ -8,6 +9,12 @@ export interface IFileSystem {
deleteFile(path: string): Promise<void>;
deleteDirectory(path: string): Promise<void>;
scanFiles(basePath: string, pattern: string): Promise<string[]>;
/**
* Convert a local file path to an asset URL that can be used in browser contexts (img src, audio src, etc.)
* @param filePath The local file path
* @returns The converted asset URL
*/
convertToAssetUrl(filePath: string): string;
}
export interface FileEntry {
@@ -17,3 +24,7 @@ export interface FileEntry {
size?: number;
modified?: Date;
}
// Service identifier for DI registration
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IFileSystemService = Symbol.for('IFileSystemService');

View File

@@ -75,3 +75,7 @@ export class InspectorRegistry implements IService {
this.clear();
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IInspectorRegistry = Symbol.for('IInspectorRegistry');

View File

@@ -215,3 +215,7 @@ export class MessageHub implements IService {
logger.info('MessageHub disposed');
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IMessageHub = Symbol.for('IMessageHub');

View File

@@ -1,5 +1,6 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable, Core, createLogger, SceneSerializer, Scene } from '@esengine/ecs-framework';
import type { SceneResourceManager } from '@esengine/asset-system';
import type { MessageHub } from './MessageHub';
import type { IFileAPI } from '../Types/IFileAPI';
import type { ProjectService } from './ProjectService';
@@ -24,6 +25,7 @@ export class SceneManagerService implements IService {
};
private unsubscribeHandlers: Array<() => void> = [];
private sceneResourceManager: SceneResourceManager | null = null;
constructor(
private messageHub: MessageHub,
@@ -35,6 +37,14 @@ export class SceneManagerService implements IService {
logger.info('SceneManagerService initialized');
}
/**
* 设置场景资源管理器
* Set scene resource manager
*/
public setSceneResourceManager(manager: SceneResourceManager | null): void {
this.sceneResourceManager = manager;
}
public async newScene(): Promise<void> {
if (!await this.canClose()) {
return;
@@ -91,6 +101,13 @@ export class SceneManagerService implements IService {
strategy: 'replace'
});
// 加载场景资源 / Load scene resources
if (this.sceneResourceManager) {
await this.sceneResourceManager.loadSceneResources(scene);
} else {
logger.warn('[SceneManagerService] SceneResourceManager not available, skipping resource loading');
}
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
const sceneName = fileName.replace('.ecs', '');