feat(editor-core): 添加构建系统和模块管理
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/asset-system": "workspace:*",
|
||||
"@esengine/asset-system-editor": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
|
||||
@@ -10,15 +10,14 @@ import type { ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
// 从 PluginDescriptor 重新导出(来源于 engine-core)
|
||||
export type {
|
||||
PluginCategory,
|
||||
LoadingPhase,
|
||||
ModuleType,
|
||||
ModuleDescriptor,
|
||||
PluginDependency,
|
||||
PluginDescriptor,
|
||||
SystemContext,
|
||||
IRuntimeModule,
|
||||
IPlugin
|
||||
IPlugin,
|
||||
ModuleManifest,
|
||||
ModuleCategory,
|
||||
ModulePlatform,
|
||||
ModuleExports
|
||||
} from './PluginDescriptor';
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
/**
|
||||
* 插件描述符类型
|
||||
* Plugin descriptor types
|
||||
* 插件/模块类型定义
|
||||
* Plugin/Module type definitions
|
||||
*
|
||||
* 从 @esengine/engine-core 重新导出基础类型,并添加编辑器专用类型。
|
||||
* Re-export base types from @esengine/engine-core, and add editor-specific types.
|
||||
* 从 @esengine/engine-core 重新导出基础类型。
|
||||
* Re-export base types from @esengine/engine-core.
|
||||
*/
|
||||
|
||||
// 从 engine-core 重新导出所有插件相关类型
|
||||
// 从 engine-core 重新导出所有类型
|
||||
export type {
|
||||
PluginCategory,
|
||||
LoadingPhase,
|
||||
ModuleType,
|
||||
ModuleDescriptor,
|
||||
PluginDependency,
|
||||
PluginDescriptor,
|
||||
SystemContext,
|
||||
IRuntimeModule,
|
||||
IPlugin
|
||||
IPlugin,
|
||||
ModuleManifest,
|
||||
ModuleCategory,
|
||||
ModulePlatform,
|
||||
ModuleExports
|
||||
} from '@esengine/engine-core';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
import { createLogger, ComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IScene, ServiceContainer, IService } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
PluginDescriptor,
|
||||
PluginState,
|
||||
PluginCategory,
|
||||
LoadingPhase,
|
||||
IPlugin
|
||||
ModuleManifest,
|
||||
IPlugin,
|
||||
ModuleCategory,
|
||||
PluginState
|
||||
} from './PluginDescriptor';
|
||||
import type {
|
||||
SystemContext,
|
||||
@@ -21,6 +20,7 @@ import { ComponentActionRegistry } from '../Services/ComponentActionRegistry';
|
||||
import { FileActionRegistry } from '../Services/FileActionRegistry';
|
||||
import { UIRegistry } from '../Services/UIRegistry';
|
||||
import { MessageHub } from '../Services/MessageHub';
|
||||
import { moduleRegistry } from '../Services/Module/ModuleRegistry';
|
||||
|
||||
const logger = createLogger('PluginManager');
|
||||
|
||||
@@ -31,24 +31,29 @@ const logger = createLogger('PluginManager');
|
||||
export const IPluginManager = Symbol.for('IPluginManager');
|
||||
|
||||
/**
|
||||
* 标准化后的插件描述符(所有字段都有值)
|
||||
* Normalized plugin descriptor (all fields have values)
|
||||
* 标准化后的模块清单(所有字段都有值)
|
||||
* Normalized module manifest (all fields have values)
|
||||
*/
|
||||
export interface NormalizedPluginDescriptor {
|
||||
export interface NormalizedManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
version: string;
|
||||
description: string;
|
||||
category: PluginCategory;
|
||||
category: ModuleCategory;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
enabledByDefault: boolean;
|
||||
defaultEnabled: boolean;
|
||||
canContainContent: boolean;
|
||||
isEnginePlugin: boolean;
|
||||
isEngineModule: boolean;
|
||||
isCore: boolean;
|
||||
modules: Array<{ name: string; type: 'runtime' | 'editor'; loadingPhase: LoadingPhase }>;
|
||||
dependencies: Array<{ id: string; version?: string; optional?: boolean }>;
|
||||
dependencies: string[];
|
||||
exports: { components?: string[]; systems?: string[]; loaders?: string[]; other?: string[] };
|
||||
platforms: ('web' | 'desktop' | 'mobile')[];
|
||||
editorPackage?: string;
|
||||
jsSize?: number;
|
||||
wasmSize?: number;
|
||||
requiresWasm?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +61,7 @@ export interface NormalizedPluginDescriptor {
|
||||
* Normalized plugin (internal use)
|
||||
*/
|
||||
export interface NormalizedPlugin {
|
||||
descriptor: NormalizedPluginDescriptor;
|
||||
manifest: NormalizedManifest;
|
||||
runtimeModule?: IPlugin['runtimeModule'];
|
||||
editorModule?: IEditorModuleLoader;
|
||||
}
|
||||
@@ -112,18 +117,6 @@ export interface PluginConfig {
|
||||
enabledPlugins: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载阶段顺序
|
||||
* Loading phase order
|
||||
*/
|
||||
const LOADING_PHASE_ORDER: LoadingPhase[] = [
|
||||
'earliest',
|
||||
'preDefault',
|
||||
'default',
|
||||
'postDefault',
|
||||
'postEngine'
|
||||
];
|
||||
|
||||
/**
|
||||
* 统一插件管理器
|
||||
* Unified Plugin Manager
|
||||
@@ -168,31 +161,49 @@ export class PluginManager implements IService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化插件描述符,填充默认值
|
||||
* Normalize plugin descriptor, fill in defaults
|
||||
* 解析依赖 ID 为完整的插件 ID
|
||||
* Resolve dependency ID to full plugin ID
|
||||
*
|
||||
* 支持两种格式:
|
||||
* - 短 ID: "core" -> "@esengine/core"
|
||||
* - 完整 ID: "@esengine/core" -> "@esengine/core"
|
||||
*/
|
||||
private resolveDependencyId(depId: string): string {
|
||||
// 如果已经是完整 ID,直接返回
|
||||
if (depId.startsWith('@')) {
|
||||
return depId;
|
||||
}
|
||||
// 短 ID 转换为完整 ID
|
||||
return `@esengine/${depId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化模块清单,填充默认值
|
||||
* Normalize module manifest, fill in defaults
|
||||
*/
|
||||
private normalizePlugin(input: IPlugin): NormalizedPlugin {
|
||||
const d = input.descriptor;
|
||||
const m = input.manifest;
|
||||
return {
|
||||
descriptor: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
version: d.version,
|
||||
description: d.description ?? '',
|
||||
category: d.category ?? 'tools',
|
||||
tags: d.tags ?? [],
|
||||
icon: d.icon,
|
||||
enabledByDefault: d.enabledByDefault ?? false,
|
||||
canContainContent: d.canContainContent ?? false,
|
||||
isEnginePlugin: d.isEnginePlugin ?? true,
|
||||
isCore: d.isCore ?? false,
|
||||
modules: (d.modules ?? [{ name: 'Runtime', type: 'runtime' as const, loadingPhase: 'default' as const }]).map((m: { name: string; type: 'runtime' | 'editor'; loadingPhase?: LoadingPhase }) => ({
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
loadingPhase: m.loadingPhase ?? 'default' as LoadingPhase
|
||||
})),
|
||||
dependencies: d.dependencies ?? [],
|
||||
platforms: d.platforms ?? ['web', 'desktop']
|
||||
manifest: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
displayName: m.displayName,
|
||||
version: m.version,
|
||||
description: m.description ?? '',
|
||||
category: m.category ?? 'Other',
|
||||
tags: m.tags ?? [],
|
||||
icon: m.icon,
|
||||
defaultEnabled: m.defaultEnabled ?? false,
|
||||
canContainContent: m.canContainContent ?? false,
|
||||
isEngineModule: m.isEngineModule ?? true,
|
||||
isCore: m.isCore ?? false,
|
||||
dependencies: m.dependencies ?? [],
|
||||
exports: m.exports ?? {},
|
||||
platforms: m.platforms ?? ['web', 'desktop'],
|
||||
editorPackage: m.editorPackage,
|
||||
jsSize: m.jsSize,
|
||||
wasmSize: m.wasmSize,
|
||||
requiresWasm: m.requiresWasm
|
||||
},
|
||||
runtimeModule: input.runtimeModule,
|
||||
editorModule: input.editorModule as IEditorModuleLoader | undefined
|
||||
@@ -212,15 +223,15 @@ export class PluginManager implements IService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plugin.descriptor) {
|
||||
logger.error('Cannot register plugin: descriptor is null or undefined', plugin);
|
||||
if (!plugin.manifest) {
|
||||
logger.error('Cannot register plugin: manifest is null or undefined', plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = plugin.descriptor;
|
||||
const { id } = plugin.manifest;
|
||||
|
||||
if (!id) {
|
||||
logger.error('Cannot register plugin: descriptor.id is null or undefined', plugin.descriptor);
|
||||
logger.error('Cannot register plugin: manifest.id is null or undefined', plugin.manifest);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,7 +241,7 @@ export class PluginManager implements IService {
|
||||
}
|
||||
|
||||
const normalized = this.normalizePlugin(plugin);
|
||||
const enabled = normalized.descriptor.isCore || normalized.descriptor.enabledByDefault;
|
||||
const enabled = normalized.manifest.isCore || normalized.manifest.defaultEnabled;
|
||||
|
||||
this.plugins.set(id, {
|
||||
plugin: normalized,
|
||||
@@ -239,7 +250,7 @@ export class PluginManager implements IService {
|
||||
loadedAt: Date.now()
|
||||
});
|
||||
|
||||
logger.info(`Plugin registered: ${id} (${normalized.descriptor.name})`);
|
||||
logger.info(`Plugin registered: ${id} (${normalized.manifest.displayName})`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,7 +264,7 @@ export class PluginManager implements IService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plugin.plugin.descriptor.isCore) {
|
||||
if (plugin.plugin.manifest.isCore) {
|
||||
logger.warn(`Core plugin ${pluginId} cannot be disabled/enabled`);
|
||||
return false;
|
||||
}
|
||||
@@ -263,13 +274,33 @@ export class PluginManager implements IService {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查依赖
|
||||
const deps = plugin.plugin.descriptor.dependencies;
|
||||
for (const dep of deps) {
|
||||
if (dep.optional) continue;
|
||||
const depPlugin = this.plugins.get(dep.id);
|
||||
if (!depPlugin || !depPlugin.enabled) {
|
||||
logger.error(`Cannot enable ${pluginId}: dependency ${dep.id} is not enabled`);
|
||||
// 检查依赖(支持短 ID 和完整 ID)
|
||||
// Check dependencies (supports both short ID and full ID)
|
||||
const deps = plugin.plugin.manifest.dependencies;
|
||||
for (const depId of deps) {
|
||||
const resolvedDepId = this.resolveDependencyId(depId);
|
||||
const depPlugin = this.plugins.get(resolvedDepId);
|
||||
|
||||
// 如果依赖不在 plugins 中,检查是否是核心模块
|
||||
// If dependency is not in plugins, check if it's a core module
|
||||
if (!depPlugin) {
|
||||
// 核心模块(如 engine-core, core, math)不作为插件注册
|
||||
// Core modules (like engine-core, core, math) are not registered as plugins
|
||||
// 它们总是可用的,所以跳过检查
|
||||
// They are always available, so skip the check
|
||||
const shortId = depId.startsWith('@esengine/') ? depId.replace('@esengine/', '') : depId;
|
||||
// 动态查询 moduleRegistry 判断是否是核心模块
|
||||
// Dynamically query moduleRegistry to check if it's a core module
|
||||
const moduleEntry = moduleRegistry.getModule(shortId);
|
||||
if (moduleEntry?.isCore) {
|
||||
continue;
|
||||
}
|
||||
logger.error(`Cannot enable ${pluginId}: dependency ${depId} (resolved: ${resolvedDepId}) is not registered`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!depPlugin.enabled) {
|
||||
logger.error(`Cannot enable ${pluginId}: dependency ${depId} (resolved: ${resolvedDepId}) is not enabled`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -312,7 +343,7 @@ export class PluginManager implements IService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plugin.plugin.descriptor.isCore) {
|
||||
if (plugin.plugin.manifest.isCore) {
|
||||
logger.warn(`Core plugin ${pluginId} cannot be disabled`);
|
||||
return false;
|
||||
}
|
||||
@@ -322,12 +353,13 @@ export class PluginManager implements IService {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否有其他插件依赖此插件
|
||||
// 检查是否有其他插件依赖此插件(支持短 ID 和完整 ID)
|
||||
for (const [id, p] of this.plugins) {
|
||||
if (!p.enabled || id === pluginId) continue;
|
||||
const deps = p.plugin.descriptor.dependencies;
|
||||
const hasDep = deps.some(d => d.id === pluginId && !d.optional);
|
||||
if (hasDep) {
|
||||
const deps = p.plugin.manifest.dependencies;
|
||||
// 将每个依赖解析为完整 ID 后检查
|
||||
const resolvedDeps = deps.map(d => this.resolveDependencyId(d));
|
||||
if (resolvedDeps.includes(pluginId)) {
|
||||
logger.error(`Cannot disable ${pluginId}: plugin ${id} depends on it`);
|
||||
return false;
|
||||
}
|
||||
@@ -684,9 +716,9 @@ export class PluginManager implements IService {
|
||||
* 按类别获取插件
|
||||
* Get plugins by category
|
||||
*/
|
||||
getPluginsByCategory(category: PluginCategory): RegisteredPlugin[] {
|
||||
getPluginsByCategory(category: ModuleCategory): RegisteredPlugin[] {
|
||||
return this.getAllPlugins().filter(
|
||||
p => p.plugin.descriptor.category === category
|
||||
p => p.plugin.manifest.category === category
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1015,7 +1047,7 @@ export class PluginManager implements IService {
|
||||
exportConfig(): PluginConfig {
|
||||
const enabledPlugins: string[] = [];
|
||||
for (const [id, plugin] of this.plugins) {
|
||||
if (plugin.enabled && !plugin.plugin.descriptor.isCore) {
|
||||
if (plugin.enabled && !plugin.plugin.manifest.isCore) {
|
||||
enabledPlugins.push(id);
|
||||
}
|
||||
}
|
||||
@@ -1040,7 +1072,7 @@ export class PluginManager implements IService {
|
||||
const toDisable: string[] = [];
|
||||
|
||||
for (const [id, plugin] of this.plugins) {
|
||||
if (plugin.plugin.descriptor.isCore) {
|
||||
if (plugin.plugin.manifest.isCore) {
|
||||
continue; // 核心插件始终启用
|
||||
}
|
||||
|
||||
@@ -1068,35 +1100,13 @@ export class PluginManager implements IService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按加载阶段排序
|
||||
* Sort by loading phase
|
||||
* 按依赖排序(拓扑排序)
|
||||
* Sort by dependencies (topological sort)
|
||||
*/
|
||||
private sortByLoadingPhase(moduleType: 'runtime' | 'editor'): string[] {
|
||||
private sortByLoadingPhase(_moduleType: 'runtime' | 'editor'): string[] {
|
||||
const pluginIds = Array.from(this.plugins.keys());
|
||||
|
||||
// 先按依赖拓扑排序
|
||||
const sorted = this.topologicalSort(pluginIds);
|
||||
|
||||
// 再按加载阶段排序(稳定排序)
|
||||
sorted.sort((a, b) => {
|
||||
const pluginA = this.plugins.get(a);
|
||||
const pluginB = this.plugins.get(b);
|
||||
|
||||
const moduleA = moduleType === 'runtime'
|
||||
? pluginA?.plugin.descriptor.modules.find(m => m.type === 'runtime')
|
||||
: pluginA?.plugin.descriptor.modules.find(m => m.type === 'editor');
|
||||
|
||||
const moduleB = moduleType === 'runtime'
|
||||
? pluginB?.plugin.descriptor.modules.find(m => m.type === 'runtime')
|
||||
: pluginB?.plugin.descriptor.modules.find(m => m.type === 'editor');
|
||||
|
||||
const phaseA = moduleA?.loadingPhase || 'default';
|
||||
const phaseB = moduleB?.loadingPhase || 'default';
|
||||
|
||||
return LOADING_PHASE_ORDER.indexOf(phaseA) - LOADING_PHASE_ORDER.indexOf(phaseB);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
// 按依赖拓扑排序
|
||||
return this.topologicalSort(pluginIds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1113,10 +1123,12 @@ export class PluginManager implements IService {
|
||||
|
||||
const plugin = this.plugins.get(id);
|
||||
if (plugin) {
|
||||
const deps = plugin.plugin.descriptor.dependencies || [];
|
||||
for (const dep of deps) {
|
||||
if (pluginIds.includes(dep.id)) {
|
||||
visit(dep.id);
|
||||
const deps = plugin.plugin.manifest.dependencies || [];
|
||||
for (const depId of deps) {
|
||||
// 解析短 ID 为完整 ID
|
||||
const resolvedDepId = this.resolveDependencyId(depId);
|
||||
if (pluginIds.includes(resolvedDepId)) {
|
||||
visit(resolvedDepId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
198
packages/editor-core/src/Rendering/IViewportOverlay.ts
Normal file
198
packages/editor-core/src/Rendering/IViewportOverlay.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Viewport Overlay Interface
|
||||
* 视口覆盖层接口
|
||||
*
|
||||
* Defines the interface for rendering overlays on viewports (grid, selection, gizmos, etc.)
|
||||
* 定义在视口上渲染覆盖层的接口(网格、选区、辅助线等)
|
||||
*/
|
||||
|
||||
import type { ViewportCameraConfig } from '../Services/IViewportService';
|
||||
|
||||
/**
|
||||
* Context passed to overlay renderers
|
||||
* 传递给覆盖层渲染器的上下文
|
||||
*/
|
||||
export interface OverlayRenderContext {
|
||||
/** Current camera state | 当前相机状态 */
|
||||
camera: ViewportCameraConfig;
|
||||
/** Viewport dimensions | 视口尺寸 */
|
||||
viewport: { width: number; height: number };
|
||||
/** Device pixel ratio | 设备像素比 */
|
||||
dpr: number;
|
||||
/** Selected entity IDs (if applicable) | 选中的实体 ID(如果适用) */
|
||||
selectedEntityIds?: number[];
|
||||
/** Delta time since last frame | 距上一帧的时间差 */
|
||||
deltaTime: number;
|
||||
/** Add a line gizmo | 添加线条辅助线 */
|
||||
addLine(x1: number, y1: number, x2: number, y2: number, color: number, thickness?: number): void;
|
||||
/** Add a rectangle gizmo (outline) | 添加矩形辅助线(轮廓) */
|
||||
addRect(x: number, y: number, width: number, height: number, color: number, thickness?: number): void;
|
||||
/** Add a filled rectangle gizmo | 添加填充矩形辅助线 */
|
||||
addFilledRect(x: number, y: number, width: number, height: number, color: number): void;
|
||||
/** Add a circle gizmo (outline) | 添加圆形辅助线(轮廓) */
|
||||
addCircle(x: number, y: number, radius: number, color: number, thickness?: number): void;
|
||||
/** Add a filled circle gizmo | 添加填充圆形辅助线 */
|
||||
addFilledCircle(x: number, y: number, radius: number, color: number): void;
|
||||
/** Add text | 添加文本 */
|
||||
addText?(text: string, x: number, y: number, color: number, fontSize?: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for viewport overlays (grid, selection, etc.)
|
||||
* 视口覆盖层接口(网格、选区等)
|
||||
*/
|
||||
export interface IViewportOverlay {
|
||||
/** Unique overlay identifier | 唯一覆盖层标识符 */
|
||||
readonly id: string;
|
||||
/** Priority (higher = rendered later/on top) | 优先级(越高越晚渲染/在上层) */
|
||||
priority: number;
|
||||
/** Whether overlay is visible | 覆盖层是否可见 */
|
||||
visible: boolean;
|
||||
|
||||
/**
|
||||
* Render the overlay
|
||||
* 渲染覆盖层
|
||||
* @param context - Render context with camera, viewport info, and gizmo APIs
|
||||
*/
|
||||
render(context: OverlayRenderContext): void;
|
||||
|
||||
/**
|
||||
* Update the overlay (optional, called each frame before render)
|
||||
* 更新覆盖层(可选,每帧在渲染前调用)
|
||||
* @param deltaTime - Time since last update
|
||||
*/
|
||||
update?(deltaTime: number): void;
|
||||
|
||||
/**
|
||||
* Dispose the overlay resources
|
||||
* 释放覆盖层资源
|
||||
*/
|
||||
dispose?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for viewport overlays
|
||||
* 视口覆盖层基类
|
||||
*/
|
||||
export abstract class ViewportOverlayBase implements IViewportOverlay {
|
||||
abstract readonly id: string;
|
||||
priority = 0;
|
||||
visible = true;
|
||||
|
||||
abstract render(context: OverlayRenderContext): void;
|
||||
|
||||
update?(deltaTime: number): void;
|
||||
|
||||
dispose?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid overlay for viewports
|
||||
* 视口网格覆盖层
|
||||
*/
|
||||
export class GridOverlay extends ViewportOverlayBase {
|
||||
override readonly id = 'grid';
|
||||
override priority = 0;
|
||||
|
||||
/** Grid cell size in world units | 网格单元格大小(世界单位) */
|
||||
cellSize = 32;
|
||||
/** Grid line color (ARGB packed) | 网格线颜色(ARGB 打包) */
|
||||
lineColor = 0x40FFFFFF;
|
||||
/** Major grid line interval | 主网格线间隔 */
|
||||
majorLineInterval = 10;
|
||||
/** Major grid line color (ARGB packed) | 主网格线颜色(ARGB 打包) */
|
||||
majorLineColor = 0x60FFFFFF;
|
||||
/** Show axis lines | 显示轴线 */
|
||||
showAxisLines = true;
|
||||
/** X axis color | X 轴颜色 */
|
||||
xAxisColor = 0xFFFF5555;
|
||||
/** Y axis color | Y 轴颜色 */
|
||||
yAxisColor = 0xFF55FF55;
|
||||
|
||||
render(context: OverlayRenderContext): void {
|
||||
const { camera, viewport } = context;
|
||||
const halfWidth = (viewport.width / 2) / camera.zoom;
|
||||
const halfHeight = (viewport.height / 2) / camera.zoom;
|
||||
|
||||
// Calculate visible grid range
|
||||
const left = camera.x - halfWidth;
|
||||
const right = camera.x + halfWidth;
|
||||
const bottom = camera.y - halfHeight;
|
||||
const top = camera.y + halfHeight;
|
||||
|
||||
// Round to grid lines
|
||||
const startX = Math.floor(left / this.cellSize) * this.cellSize;
|
||||
const endX = Math.ceil(right / this.cellSize) * this.cellSize;
|
||||
const startY = Math.floor(bottom / this.cellSize) * this.cellSize;
|
||||
const endY = Math.ceil(top / this.cellSize) * this.cellSize;
|
||||
|
||||
// Draw vertical lines
|
||||
for (let x = startX; x <= endX; x += this.cellSize) {
|
||||
const isMajor = x % (this.cellSize * this.majorLineInterval) === 0;
|
||||
const color = isMajor ? this.majorLineColor : this.lineColor;
|
||||
context.addLine(x, startY, x, endY, color, isMajor ? 1.5 : 1);
|
||||
}
|
||||
|
||||
// Draw horizontal lines
|
||||
for (let y = startY; y <= endY; y += this.cellSize) {
|
||||
const isMajor = y % (this.cellSize * this.majorLineInterval) === 0;
|
||||
const color = isMajor ? this.majorLineColor : this.lineColor;
|
||||
context.addLine(startX, y, endX, y, color, isMajor ? 1.5 : 1);
|
||||
}
|
||||
|
||||
// Draw axis lines
|
||||
if (this.showAxisLines) {
|
||||
// X axis (red)
|
||||
if (bottom <= 0 && top >= 0) {
|
||||
context.addLine(startX, 0, endX, 0, this.xAxisColor, 2);
|
||||
}
|
||||
// Y axis (green)
|
||||
if (left <= 0 && right >= 0) {
|
||||
context.addLine(0, startY, 0, endY, this.yAxisColor, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection highlight overlay
|
||||
* 选区高亮覆盖层
|
||||
*/
|
||||
export class SelectionOverlay extends ViewportOverlayBase {
|
||||
override readonly id = 'selection';
|
||||
override priority = 100;
|
||||
|
||||
/** Selection highlight color (ARGB packed) | 选区高亮颜色(ARGB 打包) */
|
||||
highlightColor = 0x404488FF;
|
||||
/** Selection border color (ARGB packed) | 选区边框颜色(ARGB 打包) */
|
||||
borderColor = 0xFF4488FF;
|
||||
/** Border thickness | 边框厚度 */
|
||||
borderThickness = 2;
|
||||
|
||||
private _selections: Array<{ x: number; y: number; width: number; height: number }> = [];
|
||||
|
||||
/**
|
||||
* Set selection rectangles
|
||||
* 设置选区矩形
|
||||
*/
|
||||
setSelections(selections: Array<{ x: number; y: number; width: number; height: number }>): void {
|
||||
this._selections = selections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all selections
|
||||
* 清除所有选区
|
||||
*/
|
||||
clearSelections(): void {
|
||||
this._selections = [];
|
||||
}
|
||||
|
||||
render(context: OverlayRenderContext): void {
|
||||
for (const sel of this._selections) {
|
||||
// Draw filled rectangle
|
||||
context.addFilledRect(sel.x, sel.y, sel.width, sel.height, this.highlightColor);
|
||||
// Draw border
|
||||
context.addRect(sel.x, sel.y, sel.width, sel.height, this.borderColor, this.borderThickness);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
packages/editor-core/src/Rendering/index.ts
Normal file
6
packages/editor-core/src/Rendering/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Rendering module exports
|
||||
* 渲染模块导出
|
||||
*/
|
||||
|
||||
export * from './IViewportOverlay';
|
||||
@@ -4,22 +4,25 @@
|
||||
*
|
||||
* 负责扫描项目资产目录,为每个资产生成唯一GUID,
|
||||
* 并维护 GUID ↔ 路径 的映射关系。
|
||||
* 使用 .meta 文件持久化存储每个资产的 GUID。
|
||||
*
|
||||
* Responsible for scanning project asset directories,
|
||||
* generating unique GUIDs for each asset, and maintaining
|
||||
* GUID ↔ path mappings.
|
||||
* Uses .meta files to persistently store each asset's GUID.
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { Core, createLogger } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from './MessageHub';
|
||||
import {
|
||||
AssetMetaManager,
|
||||
IAssetMeta,
|
||||
IMetaFileSystem,
|
||||
inferAssetType
|
||||
} from '@esengine/asset-system-editor';
|
||||
|
||||
// Simple logger for AssetRegistry
|
||||
const logger = {
|
||||
info: (msg: string, ...args: unknown[]) => console.log(`[AssetRegistry] ${msg}`, ...args),
|
||||
warn: (msg: string, ...args: unknown[]) => console.warn(`[AssetRegistry] ${msg}`, ...args),
|
||||
error: (msg: string, ...args: unknown[]) => console.error(`[AssetRegistry] ${msg}`, ...args),
|
||||
debug: (msg: string, ...args: unknown[]) => console.debug(`[AssetRegistry] ${msg}`, ...args),
|
||||
};
|
||||
// Logger for AssetRegistry using core's logger
|
||||
const logger = createLogger('AssetRegistry');
|
||||
|
||||
/**
|
||||
* Asset GUID type (simplified, no dependency on asset-system)
|
||||
@@ -213,6 +216,9 @@ export class AssetRegistryService {
|
||||
private _messageHub: MessageHub | null = null;
|
||||
private _initialized = false;
|
||||
|
||||
/** Asset meta manager for .meta file management */
|
||||
private _metaManager: AssetMetaManager;
|
||||
|
||||
/** Manifest file name */
|
||||
static readonly MANIFEST_FILE = 'asset-manifest.json';
|
||||
/** Current manifest version */
|
||||
@@ -220,6 +226,15 @@ export class AssetRegistryService {
|
||||
|
||||
constructor() {
|
||||
this._database = new SimpleAssetDatabase();
|
||||
this._metaManager = new AssetMetaManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AssetMetaManager instance
|
||||
* 获取 AssetMetaManager 实例
|
||||
*/
|
||||
get metaManager(): AssetMetaManager {
|
||||
return this._metaManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,11 +285,32 @@ export class AssetRegistryService {
|
||||
|
||||
this._projectPath = projectPath;
|
||||
this._database.clear();
|
||||
this._metaManager.clear();
|
||||
|
||||
// Try to load existing manifest
|
||||
// Setup MetaManager with file system adapter
|
||||
const metaFs: IMetaFileSystem = {
|
||||
exists: (path: string) => this._fileSystem!.exists(path),
|
||||
readText: (path: string) => this._fileSystem!.readFile(path),
|
||||
writeText: (path: string, content: string) => this._fileSystem!.writeFile(path, content),
|
||||
delete: async (path: string) => {
|
||||
// Try to delete, ignore if not exists
|
||||
try {
|
||||
// Note: IFileSystem may not have delete, handle gracefully
|
||||
const fs = this._fileSystem as IFileSystem & { delete?: (p: string) => Promise<void> };
|
||||
if (fs.delete) {
|
||||
await fs.delete(path);
|
||||
}
|
||||
} catch {
|
||||
// Ignore delete errors
|
||||
}
|
||||
}
|
||||
};
|
||||
this._metaManager.setFileSystem(metaFs);
|
||||
|
||||
// Try to load existing manifest (for backward compatibility)
|
||||
await this._loadManifest();
|
||||
|
||||
// Scan assets directory
|
||||
// Scan assets directory (now uses .meta files)
|
||||
await this._scanAssetsDirectory();
|
||||
|
||||
// Save updated manifest
|
||||
@@ -422,15 +458,18 @@ export class AssetRegistryService {
|
||||
private async _registerAssetFile(absolutePath: string, relativePath: string): Promise<void> {
|
||||
if (!this._fileSystem || !this._manifest) return;
|
||||
|
||||
// Skip .meta files
|
||||
if (relativePath.endsWith('.meta')) return;
|
||||
|
||||
// Get file extension
|
||||
const lastDot = relativePath.lastIndexOf('.');
|
||||
if (lastDot === -1) return; // Skip files without extension
|
||||
|
||||
const extension = relativePath.substring(lastDot).toLowerCase();
|
||||
const assetType = EXTENSION_TYPE_MAP[extension];
|
||||
const assetType = EXTENSION_TYPE_MAP[extension] || inferAssetType(relativePath);
|
||||
|
||||
// Skip unknown file types
|
||||
if (!assetType) return;
|
||||
if (!assetType || assetType === 'binary') return;
|
||||
|
||||
// Get file info
|
||||
let stat: { size: number; mtime: number };
|
||||
@@ -440,15 +479,19 @@ export class AssetRegistryService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already in manifest
|
||||
let guid: AssetGUID;
|
||||
const existingEntry = this._manifest.assets[relativePath];
|
||||
// Use MetaManager to get or create meta (with .meta file)
|
||||
let meta: IAssetMeta;
|
||||
try {
|
||||
meta = await this._metaManager.getOrCreateMeta(absolutePath);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to get meta for ${relativePath}:`, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingEntry) {
|
||||
guid = existingEntry.guid;
|
||||
} else {
|
||||
// Generate new GUID
|
||||
guid = this._generateGUID();
|
||||
const guid = meta.guid;
|
||||
|
||||
// Update manifest for backward compatibility
|
||||
if (!this._manifest.assets[relativePath]) {
|
||||
this._manifest.assets[relativePath] = {
|
||||
guid,
|
||||
relativePath,
|
||||
|
||||
309
packages/editor-core/src/Services/Build/BuildService.ts
Normal file
309
packages/editor-core/src/Services/Build/BuildService.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Build Service.
|
||||
* 构建服务。
|
||||
*
|
||||
* Manages build pipelines and executes build tasks.
|
||||
* 管理构建管线和执行构建任务。
|
||||
*/
|
||||
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IBuildPipeline,
|
||||
IBuildPipelineRegistry,
|
||||
BuildPlatform,
|
||||
BuildConfig,
|
||||
BuildResult,
|
||||
BuildProgress
|
||||
} from './IBuildPipeline';
|
||||
import { BuildStatus } from './IBuildPipeline';
|
||||
|
||||
/**
|
||||
* Build task.
|
||||
* 构建任务。
|
||||
*/
|
||||
export interface BuildTask {
|
||||
/** Task ID | 任务 ID */
|
||||
id: string;
|
||||
/** Target platform | 目标平台 */
|
||||
platform: BuildPlatform;
|
||||
/** Build configuration | 构建配置 */
|
||||
config: BuildConfig;
|
||||
/** Current progress | 当前进度 */
|
||||
progress: BuildProgress;
|
||||
/** Start time | 开始时间 */
|
||||
startTime: Date;
|
||||
/** End time | 结束时间 */
|
||||
endTime?: Date;
|
||||
/** Abort controller | 中止控制器 */
|
||||
abortController: AbortController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Service.
|
||||
* 构建服务。
|
||||
*
|
||||
* Provides build pipeline registration and build task management.
|
||||
* 提供构建管线注册和构建任务管理。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const buildService = services.resolve(BuildService);
|
||||
*
|
||||
* // Register build pipeline | 注册构建管线
|
||||
* buildService.registerPipeline(new WebBuildPipeline());
|
||||
* buildService.registerPipeline(new WeChatBuildPipeline());
|
||||
*
|
||||
* // Execute build | 执行构建
|
||||
* const result = await buildService.build({
|
||||
* platform: BuildPlatform.Web,
|
||||
* outputPath: './dist',
|
||||
* isRelease: true,
|
||||
* sourceMap: false
|
||||
* }, (progress) => {
|
||||
* console.log(`${progress.message} (${progress.progress}%)`);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class BuildService implements IService, IBuildPipelineRegistry {
|
||||
private _pipelines = new Map<BuildPlatform, IBuildPipeline>();
|
||||
private _currentTask: BuildTask | null = null;
|
||||
private _taskHistory: BuildTask[] = [];
|
||||
private _maxHistorySize = 10;
|
||||
|
||||
/**
|
||||
* Dispose service resources.
|
||||
* 释放服务资源。
|
||||
*/
|
||||
dispose(): void {
|
||||
this.cancelBuild();
|
||||
this._pipelines.clear();
|
||||
this._taskHistory = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register build pipeline.
|
||||
* 注册构建管线。
|
||||
*
|
||||
* @param pipeline - Build pipeline instance | 构建管线实例
|
||||
*/
|
||||
register(pipeline: IBuildPipeline): void {
|
||||
if (this._pipelines.has(pipeline.platform)) {
|
||||
console.warn(`[BuildService] Overwriting existing pipeline: ${pipeline.platform} | 覆盖已存在的构建管线: ${pipeline.platform}`);
|
||||
}
|
||||
this._pipelines.set(pipeline.platform, pipeline);
|
||||
console.log(`[BuildService] Registered pipeline: ${pipeline.displayName} | 注册构建管线: ${pipeline.displayName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get build pipeline.
|
||||
* 获取构建管线。
|
||||
*
|
||||
* @param platform - Target platform | 目标平台
|
||||
* @returns Build pipeline, or undefined if not registered | 构建管线,如果未注册则返回 undefined
|
||||
*/
|
||||
get(platform: BuildPlatform): IBuildPipeline | undefined {
|
||||
return this._pipelines.get(platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered build pipelines.
|
||||
* 获取所有已注册的构建管线。
|
||||
*
|
||||
* @returns Build pipeline list | 构建管线列表
|
||||
*/
|
||||
getAll(): IBuildPipeline[] {
|
||||
return Array.from(this._pipelines.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if platform is registered.
|
||||
* 检查平台是否已注册。
|
||||
*
|
||||
* @param platform - Target platform | 目标平台
|
||||
* @returns Whether registered | 是否已注册
|
||||
*/
|
||||
has(platform: BuildPlatform): boolean {
|
||||
return this._pipelines.has(platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available build platforms.
|
||||
* 获取可用的构建平台。
|
||||
*
|
||||
* Checks availability of each registered platform.
|
||||
* 检查每个已注册平台的可用性。
|
||||
*
|
||||
* @returns Available platforms and their status | 可用平台及其状态
|
||||
*/
|
||||
async getAvailablePlatforms(): Promise<Array<{
|
||||
platform: BuildPlatform;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
}>> {
|
||||
const results = [];
|
||||
|
||||
for (const pipeline of this._pipelines.values()) {
|
||||
const availability = await pipeline.checkAvailability();
|
||||
results.push({
|
||||
platform: pipeline.platform,
|
||||
displayName: pipeline.displayName,
|
||||
description: pipeline.description,
|
||||
available: availability.available,
|
||||
reason: availability.reason
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute build.
|
||||
* 执行构建。
|
||||
*
|
||||
* @param config - Build configuration | 构建配置
|
||||
* @param onProgress - Progress callback | 进度回调
|
||||
* @returns Build result | 构建结果
|
||||
*/
|
||||
async build(
|
||||
config: BuildConfig,
|
||||
onProgress?: (progress: BuildProgress) => void
|
||||
): Promise<BuildResult> {
|
||||
// Check if there's an ongoing build | 检查是否有正在进行的构建
|
||||
if (this._currentTask) {
|
||||
throw new Error('A build task is already in progress | 已有构建任务正在进行中');
|
||||
}
|
||||
|
||||
// Get build pipeline | 获取构建管线
|
||||
const pipeline = this._pipelines.get(config.platform);
|
||||
if (!pipeline) {
|
||||
throw new Error(`Pipeline not found for platform ${config.platform} | 未找到平台 ${config.platform} 的构建管线`);
|
||||
}
|
||||
|
||||
// Validate configuration | 验证配置
|
||||
const errors = pipeline.validateConfig(config);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid build configuration | 构建配置无效:\n${errors.join('\n')}`);
|
||||
}
|
||||
|
||||
// Create build task | 创建构建任务
|
||||
const abortController = new AbortController();
|
||||
const task: BuildTask = {
|
||||
id: this._generateTaskId(),
|
||||
platform: config.platform,
|
||||
config,
|
||||
progress: {
|
||||
status: 'preparing' as BuildStatus,
|
||||
message: 'Preparing build... | 准备构建...',
|
||||
progress: 0,
|
||||
currentStep: 0,
|
||||
totalSteps: 0,
|
||||
warnings: []
|
||||
},
|
||||
startTime: new Date(),
|
||||
abortController
|
||||
};
|
||||
|
||||
this._currentTask = task;
|
||||
|
||||
try {
|
||||
// Execute build | 执行构建
|
||||
const result = await pipeline.build(
|
||||
config,
|
||||
(progress) => {
|
||||
task.progress = progress;
|
||||
onProgress?.(progress);
|
||||
},
|
||||
abortController.signal
|
||||
);
|
||||
|
||||
// Update task status | 更新任务状态
|
||||
task.endTime = new Date();
|
||||
task.progress.status = result.success ? BuildStatus.Completed : BuildStatus.Failed;
|
||||
|
||||
// Add to history | 添加到历史
|
||||
this._addToHistory(task);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Handle error | 处理错误
|
||||
task.endTime = new Date();
|
||||
task.progress.status = BuildStatus.Failed;
|
||||
task.progress.error = error instanceof Error ? error.message : String(error);
|
||||
|
||||
this._addToHistory(task);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
platform: config.platform,
|
||||
outputPath: config.outputPath,
|
||||
duration: task.endTime.getTime() - task.startTime.getTime(),
|
||||
outputFiles: [],
|
||||
warnings: task.progress.warnings,
|
||||
error: task.progress.error
|
||||
};
|
||||
} finally {
|
||||
this._currentTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel current build.
|
||||
* 取消当前构建。
|
||||
*/
|
||||
cancelBuild(): void {
|
||||
if (this._currentTask) {
|
||||
this._currentTask.abortController.abort();
|
||||
this._currentTask.progress.status = BuildStatus.Cancelled;
|
||||
console.log('[BuildService] Build cancelled | 构建已取消');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current build task.
|
||||
* 获取当前构建任务。
|
||||
*
|
||||
* @returns Current task, or null if none | 当前任务,如果没有则返回 null
|
||||
*/
|
||||
getCurrentTask(): BuildTask | null {
|
||||
return this._currentTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get build history.
|
||||
* 获取构建历史。
|
||||
*
|
||||
* @returns History task list (newest first) | 历史任务列表(最新的在前)
|
||||
*/
|
||||
getHistory(): BuildTask[] {
|
||||
return [...this._taskHistory];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear build history.
|
||||
* 清除构建历史。
|
||||
*/
|
||||
clearHistory(): void {
|
||||
this._taskHistory = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task ID.
|
||||
* 生成任务 ID。
|
||||
*/
|
||||
private _generateTaskId(): string {
|
||||
return `build-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add task to history.
|
||||
* 添加任务到历史。
|
||||
*/
|
||||
private _addToHistory(task: BuildTask): void {
|
||||
this._taskHistory.unshift(task);
|
||||
if (this._taskHistory.length > this._maxHistorySize) {
|
||||
this._taskHistory.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
319
packages/editor-core/src/Services/Build/IBuildPipeline.ts
Normal file
319
packages/editor-core/src/Services/Build/IBuildPipeline.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Build Pipeline Interface.
|
||||
* 构建管线接口。
|
||||
*
|
||||
* Defines the common process and configuration for platform builds.
|
||||
* 定义平台构建的通用流程和配置。
|
||||
*/
|
||||
|
||||
/**
|
||||
* Build target platform.
|
||||
* 构建目标平台。
|
||||
*/
|
||||
export enum BuildPlatform {
|
||||
/** Web/H5 browser | Web/H5 浏览器 */
|
||||
Web = 'web',
|
||||
/** WeChat MiniGame | 微信小游戏 */
|
||||
WeChatMiniGame = 'wechat-minigame',
|
||||
/** ByteDance MiniGame | 字节跳动小游戏 */
|
||||
ByteDanceMiniGame = 'bytedance-minigame',
|
||||
/** Alipay MiniGame | 支付宝小游戏 */
|
||||
AlipayMiniGame = 'alipay-minigame',
|
||||
/** Desktop application (Tauri) | 桌面应用 (Tauri) */
|
||||
Desktop = 'desktop',
|
||||
/** Android | Android */
|
||||
Android = 'android',
|
||||
/** iOS | iOS */
|
||||
iOS = 'ios'
|
||||
}
|
||||
|
||||
/**
|
||||
* Build status.
|
||||
* 构建状态。
|
||||
*/
|
||||
export enum BuildStatus {
|
||||
/** Idle | 空闲 */
|
||||
Idle = 'idle',
|
||||
/** Preparing | 准备中 */
|
||||
Preparing = 'preparing',
|
||||
/** Compiling | 编译中 */
|
||||
Compiling = 'compiling',
|
||||
/** Packaging assets | 打包资源 */
|
||||
Packaging = 'packaging',
|
||||
/** Copying files | 复制文件 */
|
||||
Copying = 'copying',
|
||||
/** Post-processing | 后处理 */
|
||||
PostProcessing = 'post-processing',
|
||||
/** Completed | 完成 */
|
||||
Completed = 'completed',
|
||||
/** Failed | 失败 */
|
||||
Failed = 'failed',
|
||||
/** Cancelled | 已取消 */
|
||||
Cancelled = 'cancelled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Build progress information.
|
||||
* 构建进度信息。
|
||||
*/
|
||||
export interface BuildProgress {
|
||||
/** Current status | 当前状态 */
|
||||
status: BuildStatus;
|
||||
/** Current step description | 当前步骤描述 */
|
||||
message: string;
|
||||
/** Overall progress (0-100) | 总体进度 (0-100) */
|
||||
progress: number;
|
||||
/** Current step index | 当前步骤索引 */
|
||||
currentStep: number;
|
||||
/** Total step count | 总步骤数 */
|
||||
totalSteps: number;
|
||||
/** Warning list | 警告列表 */
|
||||
warnings: string[];
|
||||
/** Error message (if failed) | 错误信息(如果失败) */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build configuration base class.
|
||||
* 构建配置基类。
|
||||
*/
|
||||
export interface BuildConfig {
|
||||
/** Target platform | 目标平台 */
|
||||
platform: BuildPlatform;
|
||||
/** Output directory | 输出目录 */
|
||||
outputPath: string;
|
||||
/** Whether release build (compression, optimization) | 是否为发布构建(压缩、优化) */
|
||||
isRelease: boolean;
|
||||
/** Whether to generate source map | 是否生成 source map */
|
||||
sourceMap: boolean;
|
||||
/** Scene list to include (empty means all) | 要包含的场景列表(空表示全部) */
|
||||
scenes?: string[];
|
||||
/** Plugin list to include (empty means all enabled) | 要包含的插件列表(空表示全部启用的) */
|
||||
plugins?: string[];
|
||||
/**
|
||||
* Enabled module IDs (whitelist approach).
|
||||
* 启用的模块 ID 列表(白名单方式)。
|
||||
* If set, only these modules will be included.
|
||||
* 如果设置,只会包含这些模块。
|
||||
*/
|
||||
enabledModules?: string[];
|
||||
/**
|
||||
* Disabled module IDs (blacklist approach).
|
||||
* 禁用的模块 ID 列表(黑名单方式)。
|
||||
* If set, all modules EXCEPT these will be included.
|
||||
* 如果设置,会包含除了这些之外的所有模块。
|
||||
* Takes precedence over enabledModules.
|
||||
* 优先于 enabledModules。
|
||||
*/
|
||||
disabledModules?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Web platform build configuration.
|
||||
* Web 平台构建配置。
|
||||
*/
|
||||
export interface WebBuildConfig extends BuildConfig {
|
||||
platform: BuildPlatform.Web;
|
||||
/** Output format | 输出格式 */
|
||||
format: 'iife' | 'esm';
|
||||
/**
|
||||
* Whether to bundle all modules into a single JS file.
|
||||
* 是否将所有模块打包成单个 JS 文件。
|
||||
* - true: Bundle into one runtime.browser.js (smaller total size, single request)
|
||||
* - false: Keep modules separate (better caching, parallel loading)
|
||||
* Default: true
|
||||
*/
|
||||
bundleModules: boolean;
|
||||
/** Whether to generate HTML file | 是否生成 HTML 文件 */
|
||||
generateHtml: boolean;
|
||||
/** HTML template path | HTML 模板路径 */
|
||||
htmlTemplate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WeChat MiniGame build configuration.
|
||||
* 微信小游戏构建配置。
|
||||
*/
|
||||
export interface WeChatBuildConfig extends BuildConfig {
|
||||
platform: BuildPlatform.WeChatMiniGame;
|
||||
/** AppID | AppID */
|
||||
appId: string;
|
||||
/** Whether to use subpackages | 是否分包 */
|
||||
useSubpackages: boolean;
|
||||
/** Main package size limit (KB) | 主包大小限制 (KB) */
|
||||
mainPackageLimit: number;
|
||||
/** Whether to enable plugins | 是否启用插件 */
|
||||
usePlugins: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build result.
|
||||
* 构建结果。
|
||||
*/
|
||||
export interface BuildResult {
|
||||
/** Whether successful | 是否成功 */
|
||||
success: boolean;
|
||||
/** Target platform | 目标平台 */
|
||||
platform: BuildPlatform;
|
||||
/** Output directory | 输出目录 */
|
||||
outputPath: string;
|
||||
/** Build duration (milliseconds) | 构建耗时(毫秒) */
|
||||
duration: number;
|
||||
/** Output file list | 输出文件列表 */
|
||||
outputFiles: string[];
|
||||
/** Warning list | 警告列表 */
|
||||
warnings: string[];
|
||||
/** Error message (if failed) | 错误信息(如果失败) */
|
||||
error?: string;
|
||||
/** Build statistics | 构建统计 */
|
||||
stats?: {
|
||||
/** Total file size (bytes) | 总文件大小 (bytes) */
|
||||
totalSize: number;
|
||||
/** JS file size | JS 文件大小 */
|
||||
jsSize: number;
|
||||
/** WASM file size | WASM 文件大小 */
|
||||
wasmSize: number;
|
||||
/** Asset file size | 资源文件大小 */
|
||||
assetsSize: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build step.
|
||||
* 构建步骤。
|
||||
*/
|
||||
export interface BuildStep {
|
||||
/** Step ID | 步骤 ID */
|
||||
id: string;
|
||||
/** Step name | 步骤名称 */
|
||||
name: string;
|
||||
/** Execute function | 执行函数 */
|
||||
execute: (context: BuildContext) => Promise<void>;
|
||||
/** Whether skippable | 是否可跳过 */
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build context.
|
||||
* 构建上下文。
|
||||
*
|
||||
* Shared state during the build process.
|
||||
* 在构建过程中共享的状态。
|
||||
*/
|
||||
export interface BuildContext {
|
||||
/** Build configuration | 构建配置 */
|
||||
config: BuildConfig;
|
||||
/** Project root directory | 项目根目录 */
|
||||
projectRoot: string;
|
||||
/** Temporary directory | 临时目录 */
|
||||
tempDir: string;
|
||||
/** Output directory | 输出目录 */
|
||||
outputDir: string;
|
||||
/** Progress report callback | 进度报告回调 */
|
||||
reportProgress: (message: string, progress?: number) => void;
|
||||
/** Add warning | 添加警告 */
|
||||
addWarning: (warning: string) => void;
|
||||
/** Abort signal | 中止信号 */
|
||||
abortSignal: AbortSignal;
|
||||
/** Shared data (passed between steps) | 共享数据(步骤间传递) */
|
||||
data: Map<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build pipeline interface.
|
||||
* 构建管线接口。
|
||||
*
|
||||
* Each platform implements its own build pipeline.
|
||||
* 每个平台实现自己的构建管线。
|
||||
*/
|
||||
export interface IBuildPipeline {
|
||||
/** Platform identifier | 平台标识 */
|
||||
readonly platform: BuildPlatform;
|
||||
|
||||
/** Platform display name | 平台显示名称 */
|
||||
readonly displayName: string;
|
||||
|
||||
/** Platform icon | 平台图标 */
|
||||
readonly icon?: string;
|
||||
|
||||
/** Platform description | 平台描述 */
|
||||
readonly description?: string;
|
||||
|
||||
/**
|
||||
* Get default configuration.
|
||||
* 获取默认配置。
|
||||
*/
|
||||
getDefaultConfig(): BuildConfig;
|
||||
|
||||
/**
|
||||
* Validate configuration.
|
||||
* 验证配置是否有效。
|
||||
*
|
||||
* @param config - Build configuration | 构建配置
|
||||
* @returns Validation error list (empty means valid) | 验证错误列表(空表示有效)
|
||||
*/
|
||||
validateConfig(config: BuildConfig): string[];
|
||||
|
||||
/**
|
||||
* Get build steps.
|
||||
* 获取构建步骤。
|
||||
*
|
||||
* @param config - Build configuration | 构建配置
|
||||
* @returns Build step list | 构建步骤列表
|
||||
*/
|
||||
getSteps(config: BuildConfig): BuildStep[];
|
||||
|
||||
/**
|
||||
* Execute build.
|
||||
* 执行构建。
|
||||
*
|
||||
* @param config - Build configuration | 构建配置
|
||||
* @param onProgress - Progress callback | 进度回调
|
||||
* @param abortSignal - Abort signal | 中止信号
|
||||
* @returns Build result | 构建结果
|
||||
*/
|
||||
build(
|
||||
config: BuildConfig,
|
||||
onProgress?: (progress: BuildProgress) => void,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<BuildResult>;
|
||||
|
||||
/**
|
||||
* Check platform availability.
|
||||
* 检查平台是否可用。
|
||||
*
|
||||
* For example, check if necessary tools are installed.
|
||||
* 例如检查必要的工具是否安装。
|
||||
*/
|
||||
checkAvailability(): Promise<{ available: boolean; reason?: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build pipeline registry interface.
|
||||
* 构建管线注册表接口。
|
||||
*/
|
||||
export interface IBuildPipelineRegistry {
|
||||
/**
|
||||
* Register build pipeline.
|
||||
* 注册构建管线。
|
||||
*/
|
||||
register(pipeline: IBuildPipeline): void;
|
||||
|
||||
/**
|
||||
* Get build pipeline.
|
||||
* 获取构建管线。
|
||||
*/
|
||||
get(platform: BuildPlatform): IBuildPipeline | undefined;
|
||||
|
||||
/**
|
||||
* Get all registered pipelines.
|
||||
* 获取所有已注册的管线。
|
||||
*/
|
||||
getAll(): IBuildPipeline[];
|
||||
|
||||
/**
|
||||
* Check if platform is registered.
|
||||
* 检查平台是否已注册。
|
||||
*/
|
||||
has(platform: BuildPlatform): boolean;
|
||||
}
|
||||
26
packages/editor-core/src/Services/Build/index.ts
Normal file
26
packages/editor-core/src/Services/Build/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Build System.
|
||||
* 构建系统。
|
||||
*
|
||||
* Provides cross-platform project build capabilities.
|
||||
* 提供跨平台的项目构建能力。
|
||||
*/
|
||||
|
||||
export {
|
||||
BuildPlatform,
|
||||
BuildStatus,
|
||||
type BuildProgress,
|
||||
type BuildConfig,
|
||||
type WebBuildConfig,
|
||||
type WeChatBuildConfig,
|
||||
type BuildResult,
|
||||
type BuildStep,
|
||||
type BuildContext,
|
||||
type IBuildPipeline,
|
||||
type IBuildPipelineRegistry
|
||||
} from './IBuildPipeline';
|
||||
|
||||
export { BuildService, type BuildTask } from './BuildService';
|
||||
|
||||
// Build pipelines | 构建管线
|
||||
export { WebBuildPipeline, WeChatBuildPipeline, type IBuildFileSystem } from './pipelines';
|
||||
@@ -0,0 +1,730 @@
|
||||
/**
|
||||
* WeChat MiniGame Build Pipeline.
|
||||
* 微信小游戏构建管线。
|
||||
*
|
||||
* Packages the project as a format that can run on WeChat MiniGame platform.
|
||||
* 将项目打包为可在微信小游戏平台运行的格式。
|
||||
*/
|
||||
|
||||
import type {
|
||||
IBuildPipeline,
|
||||
BuildConfig,
|
||||
BuildResult,
|
||||
BuildProgress,
|
||||
BuildStep,
|
||||
BuildContext,
|
||||
WeChatBuildConfig
|
||||
} from '../IBuildPipeline';
|
||||
import { BuildPlatform, BuildStatus } from '../IBuildPipeline';
|
||||
import type { IBuildFileSystem } from './WebBuildPipeline';
|
||||
|
||||
/**
|
||||
* WASM file configuration to be copied.
|
||||
* 需要复制的 WASM 文件配置。
|
||||
*/
|
||||
interface WasmFileConfig {
|
||||
/** Source file path (relative to node_modules) | 源文件路径(相对于 node_modules) */
|
||||
source: string;
|
||||
/** Target file path (relative to output directory) | 目标文件路径(相对于输出目录) */
|
||||
target: string;
|
||||
/** Description | 描述 */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WeChat MiniGame Build Pipeline.
|
||||
* 微信小游戏构建管线。
|
||||
*
|
||||
* Build steps:
|
||||
* 构建步骤:
|
||||
* 1. Prepare output directory | 准备输出目录
|
||||
* 2. Compile TypeScript | 编译 TypeScript
|
||||
* 3. Bundle runtime (using WeChat adapter) | 打包运行时(使用微信适配器)
|
||||
* 4. Copy WASM files | 复制 WASM 文件
|
||||
* 5. Copy asset files | 复制资源文件
|
||||
* 6. Generate game.json | 生成 game.json
|
||||
* 7. Generate game.js | 生成 game.js
|
||||
* 8. Post-process | 后处理
|
||||
*/
|
||||
export class WeChatBuildPipeline implements IBuildPipeline {
|
||||
readonly platform = BuildPlatform.WeChatMiniGame;
|
||||
readonly displayName = 'WeChat MiniGame | 微信小游戏';
|
||||
readonly description = 'Build as a format that can run on WeChat MiniGame platform | 构建为可在微信小游戏平台运行的格式';
|
||||
readonly icon = 'message-circle';
|
||||
|
||||
private _fileSystem: IBuildFileSystem | null = null;
|
||||
|
||||
/**
|
||||
* WASM file list to be copied.
|
||||
* 需要复制的 WASM 文件列表。
|
||||
*/
|
||||
private readonly _wasmFiles: WasmFileConfig[] = [
|
||||
{
|
||||
source: '@dimforge/rapier2d/rapier_wasm2d_bg.wasm',
|
||||
target: 'wasm/rapier2d_bg.wasm',
|
||||
description: 'Rapier2D Physics Engine | Rapier2D 物理引擎'
|
||||
}
|
||||
// More WASM files can be added here | 可以在这里添加更多 WASM 文件
|
||||
];
|
||||
|
||||
/**
|
||||
* Set build file system service.
|
||||
* 设置构建文件系统服务。
|
||||
*
|
||||
* @param fileSystem - Build file system service | 构建文件系统服务
|
||||
*/
|
||||
setFileSystem(fileSystem: IBuildFileSystem): void {
|
||||
this._fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default configuration.
|
||||
* 获取默认配置。
|
||||
*/
|
||||
getDefaultConfig(): WeChatBuildConfig {
|
||||
return {
|
||||
platform: BuildPlatform.WeChatMiniGame,
|
||||
outputPath: './dist/wechat',
|
||||
isRelease: true,
|
||||
sourceMap: false,
|
||||
appId: '',
|
||||
useSubpackages: false,
|
||||
mainPackageLimit: 4096, // 4MB
|
||||
usePlugins: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration.
|
||||
* 验证配置。
|
||||
*
|
||||
* @param config - Build configuration | 构建配置
|
||||
* @returns Validation error list | 验证错误列表
|
||||
*/
|
||||
validateConfig(config: BuildConfig): string[] {
|
||||
const errors: string[] = [];
|
||||
const wxConfig = config as WeChatBuildConfig;
|
||||
|
||||
if (!wxConfig.outputPath) {
|
||||
errors.push('Output path cannot be empty | 输出路径不能为空');
|
||||
}
|
||||
|
||||
if (!wxConfig.appId) {
|
||||
errors.push('AppID cannot be empty | AppID 不能为空');
|
||||
} else if (!/^wx[a-f0-9]{16}$/.test(wxConfig.appId)) {
|
||||
errors.push('AppID format is incorrect (should be 18 characters starting with wx) | AppID 格式不正确(应为 wx 开头的18位字符)');
|
||||
}
|
||||
|
||||
if (wxConfig.mainPackageLimit < 1024) {
|
||||
errors.push('Main package size limit cannot be less than 1MB | 主包大小限制不能小于 1MB');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get build steps.
|
||||
* 获取构建步骤。
|
||||
*
|
||||
* @param config - Build configuration | 构建配置
|
||||
* @returns Build step list | 构建步骤列表
|
||||
*/
|
||||
getSteps(config: BuildConfig): BuildStep[] {
|
||||
const wxConfig = config as WeChatBuildConfig;
|
||||
|
||||
const steps: BuildStep[] = [
|
||||
{
|
||||
id: 'prepare',
|
||||
name: 'Prepare output directory | 准备输出目录',
|
||||
execute: this._prepareOutputDir.bind(this)
|
||||
},
|
||||
{
|
||||
id: 'compile',
|
||||
name: 'Compile TypeScript | 编译 TypeScript',
|
||||
execute: this._compileTypeScript.bind(this)
|
||||
},
|
||||
{
|
||||
id: 'bundle-runtime',
|
||||
name: 'Bundle runtime | 打包运行时',
|
||||
execute: this._bundleRuntime.bind(this)
|
||||
},
|
||||
{
|
||||
id: 'copy-wasm',
|
||||
name: 'Copy WASM files | 复制 WASM 文件',
|
||||
execute: this._copyWasmFiles.bind(this)
|
||||
},
|
||||
{
|
||||
id: 'copy-assets',
|
||||
name: 'Copy asset files | 复制资源文件',
|
||||
execute: this._copyAssets.bind(this)
|
||||
},
|
||||
{
|
||||
id: 'generate-game-json',
|
||||
name: 'Generate game.json | 生成 game.json',
|
||||
execute: this._generateGameJson.bind(this)
|
||||
},
|
||||
{
|
||||
id: 'generate-game-js',
|
||||
name: 'Generate game.js | 生成 game.js',
|
||||
execute: this._generateGameJs.bind(this)
|
||||
},
|
||||
{
|
||||
id: 'generate-project-config',
|
||||
name: 'Generate project.config.json | 生成 project.config.json',
|
||||
execute: this._generateProjectConfig.bind(this)
|
||||
}
|
||||
];
|
||||
|
||||
if (wxConfig.useSubpackages) {
|
||||
steps.push({
|
||||
id: 'split-subpackages',
|
||||
name: 'Process subpackages | 分包处理',
|
||||
execute: this._splitSubpackages.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (wxConfig.isRelease) {
|
||||
steps.push({
|
||||
id: 'optimize',
|
||||
name: 'Optimize and compress | 优化压缩',
|
||||
execute: this._optimize.bind(this),
|
||||
optional: true
|
||||
});
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute build.
|
||||
* 执行构建。
|
||||
*
|
||||
* @param config - Build configuration | 构建配置
|
||||
* @param onProgress - Progress callback | 进度回调
|
||||
* @param abortSignal - Abort signal | 中止信号
|
||||
* @returns Build result | 构建结果
|
||||
*/
|
||||
async build(
|
||||
config: BuildConfig,
|
||||
onProgress?: (progress: BuildProgress) => void,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<BuildResult> {
|
||||
const startTime = Date.now();
|
||||
const warnings: string[] = [];
|
||||
const outputFiles: string[] = [];
|
||||
|
||||
const steps = this.getSteps(config);
|
||||
const totalSteps = steps.length;
|
||||
|
||||
// Infer project root from output path | 从输出路径推断项目根目录
|
||||
// outputPath is typically: /path/to/project/build/wechat-minigame
|
||||
// So we go up 2 levels to get project root | 所以我们向上2级获取项目根目录
|
||||
const outputPathParts = config.outputPath.replace(/\\/g, '/').split('/');
|
||||
const buildIndex = outputPathParts.lastIndexOf('build');
|
||||
const projectRoot = buildIndex > 0
|
||||
? outputPathParts.slice(0, buildIndex).join('/')
|
||||
: '.';
|
||||
|
||||
// Create build context | 创建构建上下文
|
||||
const context: BuildContext = {
|
||||
config,
|
||||
projectRoot,
|
||||
tempDir: `${projectRoot}/temp/build-wechat`,
|
||||
outputDir: config.outputPath,
|
||||
reportProgress: (message, progress) => {
|
||||
// Handled below | 在下面处理
|
||||
},
|
||||
addWarning: (warning) => {
|
||||
warnings.push(warning);
|
||||
},
|
||||
abortSignal: abortSignal || new AbortController().signal,
|
||||
data: new Map()
|
||||
};
|
||||
|
||||
// Store file system and WASM config for subsequent steps | 存储文件系统和 WASM 配置供后续步骤使用
|
||||
context.data.set('fileSystem', this._fileSystem);
|
||||
context.data.set('wasmFiles', this._wasmFiles);
|
||||
|
||||
try {
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
return {
|
||||
success: false,
|
||||
platform: config.platform,
|
||||
outputPath: config.outputPath,
|
||||
duration: Date.now() - startTime,
|
||||
outputFiles,
|
||||
warnings,
|
||||
error: 'Build cancelled | 构建已取消'
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.({
|
||||
status: this._getStatusForStep(step.id),
|
||||
message: step.name,
|
||||
progress: Math.round((i / totalSteps) * 100),
|
||||
currentStep: i + 1,
|
||||
totalSteps,
|
||||
warnings
|
||||
});
|
||||
|
||||
await step.execute(context);
|
||||
}
|
||||
|
||||
// Get output stats | 获取输出统计
|
||||
let stats: BuildResult['stats'] | undefined;
|
||||
if (this._fileSystem) {
|
||||
try {
|
||||
const totalSize = await this._fileSystem.getDirectorySize(config.outputPath);
|
||||
stats = {
|
||||
totalSize,
|
||||
jsSize: 0,
|
||||
wasmSize: 0,
|
||||
assetsSize: 0
|
||||
};
|
||||
} catch {
|
||||
// Ignore stats error | 忽略统计错误
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.({
|
||||
status: BuildStatus.Completed,
|
||||
message: 'Build completed | 构建完成',
|
||||
progress: 100,
|
||||
currentStep: totalSteps,
|
||||
totalSteps,
|
||||
warnings
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
platform: config.platform,
|
||||
outputPath: config.outputPath,
|
||||
duration: Date.now() - startTime,
|
||||
outputFiles,
|
||||
warnings,
|
||||
stats
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
onProgress?.({
|
||||
status: BuildStatus.Failed,
|
||||
message: 'Build failed | 构建失败',
|
||||
progress: 0,
|
||||
currentStep: 0,
|
||||
totalSteps,
|
||||
warnings,
|
||||
error: errorMessage
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
platform: config.platform,
|
||||
outputPath: config.outputPath,
|
||||
duration: Date.now() - startTime,
|
||||
outputFiles,
|
||||
warnings,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check availability.
|
||||
* 检查可用性。
|
||||
*/
|
||||
async checkAvailability(): Promise<{ available: boolean; reason?: string }> {
|
||||
// TODO: Check if WeChat DevTools is installed | 检查微信开发者工具是否安装
|
||||
return { available: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get build status for step.
|
||||
* 获取步骤的构建状态。
|
||||
*/
|
||||
private _getStatusForStep(stepId: string): BuildStatus {
|
||||
switch (stepId) {
|
||||
case 'prepare':
|
||||
return BuildStatus.Preparing;
|
||||
case 'compile':
|
||||
case 'bundle-runtime':
|
||||
return BuildStatus.Compiling;
|
||||
case 'copy-wasm':
|
||||
case 'copy-assets':
|
||||
return BuildStatus.Copying;
|
||||
case 'generate-game-json':
|
||||
case 'generate-game-js':
|
||||
case 'generate-project-config':
|
||||
case 'split-subpackages':
|
||||
case 'optimize':
|
||||
return BuildStatus.PostProcessing;
|
||||
default:
|
||||
return BuildStatus.Compiling;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Build Step Implementations | 构建步骤实现 ====================
|
||||
|
||||
/**
|
||||
* Prepare output directory.
|
||||
* 准备输出目录。
|
||||
*/
|
||||
private async _prepareOutputDir(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
|
||||
if (fs) {
|
||||
await fs.prepareBuildDirectory(context.outputDir);
|
||||
console.log('[WeChatBuild] Prepared output directory | 准备输出目录:', context.outputDir);
|
||||
} else {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile TypeScript.
|
||||
* 编译 TypeScript。
|
||||
*/
|
||||
private async _compileTypeScript(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
const wxConfig = context.config as WeChatBuildConfig;
|
||||
|
||||
if (!fs) {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find user script entry point | 查找用户脚本入口点
|
||||
const scriptsDir = `${context.projectRoot}/scripts`;
|
||||
const hasScripts = await fs.pathExists(scriptsDir);
|
||||
|
||||
if (!hasScripts) {
|
||||
console.log('[WeChatBuild] No scripts directory found | 未找到脚本目录');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find entry file | 查找入口文件
|
||||
const possibleEntries = ['index.ts', 'main.ts', 'game.ts', 'index.js', 'main.js'];
|
||||
let entryFile: string | null = null;
|
||||
|
||||
for (const entry of possibleEntries) {
|
||||
const entryPath = `${scriptsDir}/${entry}`;
|
||||
if (await fs.pathExists(entryPath)) {
|
||||
entryFile = entryPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entryFile) {
|
||||
console.log('[WeChatBuild] No entry file found | 未找到入口文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// Bundle user scripts for WeChat | 为微信打包用户脚本
|
||||
const result = await fs.bundleScripts({
|
||||
entryPoints: [entryFile],
|
||||
outputDir: `${context.outputDir}/libs`,
|
||||
format: 'iife', // WeChat uses iife format | 微信使用 iife 格式
|
||||
bundleName: 'user-code',
|
||||
minify: wxConfig.isRelease,
|
||||
sourceMap: wxConfig.sourceMap,
|
||||
external: ['@esengine/ecs-framework', '@esengine/core'],
|
||||
projectRoot: context.projectRoot,
|
||||
define: {
|
||||
'process.env.NODE_ENV': wxConfig.isRelease ? '"production"' : '"development"',
|
||||
'wx': 'wx' // WeChat global | 微信全局对象
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`User code compilation failed | 用户代码编译失败: ${result.error}`);
|
||||
}
|
||||
|
||||
result.warnings.forEach(w => context.addWarning(w));
|
||||
console.log('[WeChatBuild] Compiled TypeScript | 编译 TypeScript:', result.outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle runtime.
|
||||
* 打包运行时。
|
||||
*/
|
||||
private async _bundleRuntime(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
|
||||
if (!fs) {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy pre-built runtime files with WeChat adapter | 复制带微信适配器的预构建运行时文件
|
||||
const runtimeSrc = `${context.projectRoot}/node_modules/@esengine/platform-wechat/dist`;
|
||||
const runtimeDst = `${context.outputDir}/libs`;
|
||||
|
||||
const hasWxRuntime = await fs.pathExists(runtimeSrc);
|
||||
if (hasWxRuntime) {
|
||||
const count = await fs.copyDirectory(runtimeSrc, runtimeDst, ['*.js']);
|
||||
console.log(`[WeChatBuild] Copied WeChat runtime | 复制微信运行时: ${count} files`);
|
||||
} else {
|
||||
// Fallback to standard runtime | 回退到标准运行时
|
||||
const stdRuntimeSrc = `${context.projectRoot}/node_modules/@esengine/ecs-framework/dist`;
|
||||
const hasStdRuntime = await fs.pathExists(stdRuntimeSrc);
|
||||
if (hasStdRuntime) {
|
||||
const count = await fs.copyDirectory(stdRuntimeSrc, runtimeDst, ['*.js']);
|
||||
console.log(`[WeChatBuild] Copied standard runtime | 复制标准运行时: ${count} files`);
|
||||
context.addWarning('Using standard runtime, some WeChat-specific features may not work | 使用标准运行时,部分微信特有功能可能不可用');
|
||||
} else {
|
||||
context.addWarning('Runtime not found | 未找到运行时');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy WASM files.
|
||||
* 复制 WASM 文件。
|
||||
*
|
||||
* This is a critical step for WeChat MiniGame build.
|
||||
* 这是微信小游戏构建的关键步骤。
|
||||
*/
|
||||
private async _copyWasmFiles(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
const wasmFiles = context.data.get('wasmFiles') as WasmFileConfig[];
|
||||
|
||||
if (!fs) {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[WeChatBuild] Copying WASM files | 复制 WASM 文件:');
|
||||
|
||||
for (const file of wasmFiles) {
|
||||
const sourcePath = `${context.projectRoot}/node_modules/${file.source}`;
|
||||
const targetPath = `${context.outputDir}/${file.target}`;
|
||||
|
||||
const exists = await fs.pathExists(sourcePath);
|
||||
if (exists) {
|
||||
await fs.copyFile(sourcePath, targetPath);
|
||||
console.log(` - ${file.description}: ${file.source} -> ${file.target}`);
|
||||
} else {
|
||||
context.addWarning(`WASM file not found | 未找到 WASM 文件: ${file.source}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy engine WASM | 复制引擎 WASM
|
||||
const engineWasmSrc = `${context.projectRoot}/node_modules/@esengine/es-engine/pkg`;
|
||||
const hasEngineWasm = await fs.pathExists(engineWasmSrc);
|
||||
if (hasEngineWasm) {
|
||||
const count = await fs.copyDirectory(engineWasmSrc, `${context.outputDir}/wasm`, ['*.wasm']);
|
||||
console.log(`[WeChatBuild] Copied engine WASM | 复制引擎 WASM: ${count} files`);
|
||||
}
|
||||
|
||||
context.addWarning(
|
||||
'iOS WeChat requires WXWebAssembly for loading WASM | iOS 微信需要使用 WXWebAssembly 加载 WASM'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy asset files.
|
||||
* 复制资源文件。
|
||||
*/
|
||||
private async _copyAssets(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
|
||||
if (!fs) {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy scenes | 复制场景
|
||||
const scenesDir = `${context.projectRoot}/scenes`;
|
||||
if (await fs.pathExists(scenesDir)) {
|
||||
const count = await fs.copyDirectory(scenesDir, `${context.outputDir}/scenes`);
|
||||
console.log(`[WeChatBuild] Copied scenes | 复制场景: ${count} files`);
|
||||
}
|
||||
|
||||
// Copy assets | 复制资源
|
||||
const assetsDir = `${context.projectRoot}/assets`;
|
||||
if (await fs.pathExists(assetsDir)) {
|
||||
const count = await fs.copyDirectory(assetsDir, `${context.outputDir}/assets`);
|
||||
console.log(`[WeChatBuild] Copied assets | 复制资源: ${count} files`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate game.json.
|
||||
* 生成 game.json。
|
||||
*/
|
||||
private async _generateGameJson(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
const wxConfig = context.config as WeChatBuildConfig;
|
||||
|
||||
if (!fs) {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
const gameJson: Record<string, unknown> = {
|
||||
deviceOrientation: 'portrait',
|
||||
showStatusBar: false,
|
||||
networkTimeout: {
|
||||
request: 60000,
|
||||
connectSocket: 60000,
|
||||
uploadFile: 60000,
|
||||
downloadFile: 60000
|
||||
},
|
||||
// Declare WebAssembly usage | 声明使用 WebAssembly
|
||||
enableWebAssembly: true
|
||||
};
|
||||
|
||||
if (wxConfig.useSubpackages) {
|
||||
gameJson.subpackages = [];
|
||||
}
|
||||
|
||||
if (wxConfig.usePlugins) {
|
||||
gameJson.plugins = {};
|
||||
}
|
||||
|
||||
const jsonContent = JSON.stringify(gameJson, null, 2);
|
||||
await fs.writeJsonFile(`${context.outputDir}/game.json`, jsonContent);
|
||||
console.log('[WeChatBuild] Generated game.json | 生成 game.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate game.js.
|
||||
* 生成 game.js。
|
||||
*/
|
||||
private async _generateGameJs(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
|
||||
if (!fs) {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
const gameJs = `/**
|
||||
* WeChat MiniGame entry point.
|
||||
* 微信小游戏入口。
|
||||
*
|
||||
* Auto-generated, do not modify manually.
|
||||
* 自动生成,请勿手动修改。
|
||||
*/
|
||||
|
||||
// WeChat adapter | 微信适配器
|
||||
require('./libs/weapp-adapter.js');
|
||||
|
||||
// Load runtime | 加载运行时
|
||||
require('./libs/esengine-runtime.js');
|
||||
|
||||
// Load user code | 加载用户代码
|
||||
require('./libs/user-code.js');
|
||||
|
||||
// Initialize game | 初始化游戏
|
||||
(async function() {
|
||||
try {
|
||||
// Load WASM (use WXWebAssembly on iOS) | 加载 WASM(iOS 上使用 WXWebAssembly)
|
||||
const isIOS = wx.getSystemInfoSync().platform === 'ios';
|
||||
if (isIOS) {
|
||||
// iOS uses WXWebAssembly | iOS 使用 WXWebAssembly
|
||||
await ECS.initWasm('./wasm/es_engine_bg.wasm', { useWXWebAssembly: true });
|
||||
} else {
|
||||
await ECS.initWasm('./wasm/es_engine_bg.wasm');
|
||||
}
|
||||
|
||||
// Create runtime | 创建运行时
|
||||
const canvas = wx.createCanvas();
|
||||
const runtime = ECS.createRuntime({
|
||||
canvas: canvas,
|
||||
platform: 'wechat'
|
||||
});
|
||||
|
||||
// Load scene and start | 加载场景并启动
|
||||
await runtime.loadScene('./scenes/main.ecs');
|
||||
runtime.start();
|
||||
|
||||
console.log('[Game] Started successfully | 游戏启动成功');
|
||||
} catch (error) {
|
||||
console.error('[Game] Failed to start | 启动失败:', error);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
await fs.writeFile(`${context.outputDir}/game.js`, gameJs);
|
||||
console.log('[WeChatBuild] Generated game.js | 生成 game.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate project.config.json.
|
||||
* 生成 project.config.json。
|
||||
*/
|
||||
private async _generateProjectConfig(context: BuildContext): Promise<void> {
|
||||
const fs = context.data.get('fileSystem') as IBuildFileSystem | undefined;
|
||||
const wxConfig = context.config as WeChatBuildConfig;
|
||||
|
||||
if (!fs) {
|
||||
console.warn('[WeChatBuild] No file system service, skipping | 无文件系统服务,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectConfig = {
|
||||
description: 'ESEngine Game',
|
||||
packOptions: {
|
||||
ignore: [],
|
||||
include: []
|
||||
},
|
||||
setting: {
|
||||
urlCheck: false,
|
||||
es6: true,
|
||||
enhance: true,
|
||||
postcss: false,
|
||||
preloadBackgroundData: false,
|
||||
minified: wxConfig.isRelease,
|
||||
newFeature: true,
|
||||
autoAudits: false,
|
||||
coverView: true,
|
||||
showShadowRootInWxmlPanel: true,
|
||||
scopeDataCheck: false,
|
||||
checkInvalidKey: true,
|
||||
checkSiteMap: true,
|
||||
uploadWithSourceMap: !wxConfig.isRelease,
|
||||
compileHotReLoad: false,
|
||||
babelSetting: {
|
||||
ignore: [],
|
||||
disablePlugins: [],
|
||||
outputPath: ''
|
||||
}
|
||||
},
|
||||
compileType: 'game',
|
||||
libVersion: '2.25.0',
|
||||
appid: wxConfig.appId,
|
||||
projectname: 'ESEngine Game',
|
||||
condition: {}
|
||||
};
|
||||
|
||||
const jsonContent = JSON.stringify(projectConfig, null, 2);
|
||||
await fs.writeJsonFile(`${context.outputDir}/project.config.json`, jsonContent);
|
||||
console.log('[WeChatBuild] Generated project.config.json | 生成 project.config.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process subpackages.
|
||||
* 分包处理。
|
||||
*/
|
||||
private async _splitSubpackages(context: BuildContext): Promise<void> {
|
||||
const wxConfig = context.config as WeChatBuildConfig;
|
||||
console.log('[WeChatBuild] Processing subpackages, limit | 分包处理,限制:', wxConfig.mainPackageLimit, 'KB');
|
||||
|
||||
// TODO: Implement automatic subpackage splitting based on file sizes
|
||||
// 实现基于文件大小的自动分包
|
||||
context.addWarning('Subpackage splitting is not fully implemented yet | 分包功能尚未完全实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize and compress.
|
||||
* 优化压缩。
|
||||
*/
|
||||
private async _optimize(context: BuildContext): Promise<void> {
|
||||
console.log('[WeChatBuild] Optimization complete | 优化完成');
|
||||
// Minification is done during bundling | 压缩在打包时已完成
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Build Pipelines.
|
||||
* 构建管线。
|
||||
*/
|
||||
|
||||
export { WebBuildPipeline, type IBuildFileSystem } from './WebBuildPipeline';
|
||||
export { WeChatBuildPipeline } from './WeChatBuildPipeline';
|
||||
@@ -20,6 +20,12 @@ export interface ComponentInspectorContext {
|
||||
onAction?: (actionId: string, propertyName: string, component: Component) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspector render mode.
|
||||
* 检查器渲染模式。
|
||||
*/
|
||||
export type InspectorRenderMode = 'replace' | 'append';
|
||||
|
||||
/**
|
||||
* 组件检查器接口
|
||||
* Interface for custom component inspectors
|
||||
@@ -33,6 +39,12 @@ export interface IComponentInspector<T extends Component = Component> {
|
||||
readonly priority?: number;
|
||||
/** 目标组件类型名称列表 */
|
||||
readonly targetComponents: string[];
|
||||
/**
|
||||
* 渲染模式
|
||||
* - 'replace': 替换默认的 PropertyInspector(默认)
|
||||
* - 'append': 追加到默认的 PropertyInspector 后面
|
||||
*/
|
||||
readonly renderMode?: InspectorRenderMode;
|
||||
|
||||
/**
|
||||
* 判断是否可以处理该组件
|
||||
@@ -73,10 +85,12 @@ export class ComponentInspectorRegistry implements IService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可以处理指定组件的检查器
|
||||
* 查找可以处理指定组件的检查器(仅 replace 模式)
|
||||
* Find inspector that can handle the component (replace mode only)
|
||||
*/
|
||||
findInspector(component: Component): IComponentInspector | undefined {
|
||||
const inspectors = Array.from(this.inspectors.values())
|
||||
.filter(i => i.renderMode !== 'append')
|
||||
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
for (const inspector of inspectors) {
|
||||
@@ -93,14 +107,45 @@ export class ComponentInspectorRegistry implements IService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有自定义检查器
|
||||
* 查找所有追加模式的检查器
|
||||
* Find all append-mode inspectors for the component
|
||||
*/
|
||||
findAppendInspectors(component: Component): IComponentInspector[] {
|
||||
const result: IComponentInspector[] = [];
|
||||
const inspectors = Array.from(this.inspectors.values())
|
||||
.filter(i => i.renderMode === 'append')
|
||||
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
for (const inspector of inspectors) {
|
||||
try {
|
||||
if (inspector.canHandle(component)) {
|
||||
result.push(inspector);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error in canHandle for inspector ${inspector.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有自定义检查器(replace 模式)
|
||||
*/
|
||||
hasInspector(component: Component): boolean {
|
||||
return this.findInspector(component) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染组件
|
||||
* 检查是否有追加检查器
|
||||
*/
|
||||
hasAppendInspectors(component: Component): boolean {
|
||||
return this.findAppendInspectors(component).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染组件(replace 模式)
|
||||
* Render component with replace-mode inspector
|
||||
*/
|
||||
render(context: ComponentInspectorContext): React.ReactElement | null {
|
||||
const inspector = this.findInspector(context.component);
|
||||
@@ -120,6 +165,38 @@ export class ComponentInspectorRegistry implements IService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染追加检查器
|
||||
* Render append-mode inspectors
|
||||
*/
|
||||
renderAppendInspectors(context: ComponentInspectorContext): React.ReactElement[] {
|
||||
const inspectors = this.findAppendInspectors(context.component);
|
||||
const elements: React.ReactElement[] = [];
|
||||
|
||||
for (const inspector of inspectors) {
|
||||
try {
|
||||
elements.push(
|
||||
React.createElement(
|
||||
React.Fragment,
|
||||
{ key: inspector.id },
|
||||
inspector.render(context)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Error rendering append inspector ${inspector.id}:`, error);
|
||||
elements.push(
|
||||
React.createElement(
|
||||
'span',
|
||||
{ key: inspector.id, style: { color: '#f87171', fontStyle: 'italic' } },
|
||||
`[${inspector.name} Error]`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册的检查器
|
||||
*/
|
||||
|
||||
401
packages/editor-core/src/Services/EditorViewportService.ts
Normal file
401
packages/editor-core/src/Services/EditorViewportService.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Editor Viewport Service
|
||||
* 编辑器视口服务
|
||||
*
|
||||
* Manages editor viewports with preview scene support and overlay rendering.
|
||||
* 管理带有预览场景支持和覆盖层渲染的编辑器视口。
|
||||
*/
|
||||
|
||||
import type { IViewportService, ViewportCameraConfig } from './IViewportService';
|
||||
import type { IPreviewScene } from './PreviewSceneService';
|
||||
import { PreviewSceneService } from './PreviewSceneService';
|
||||
import type { IViewportOverlay, OverlayRenderContext } from '../Rendering/IViewportOverlay';
|
||||
|
||||
/**
|
||||
* Configuration for an editor viewport
|
||||
* 编辑器视口配置
|
||||
*/
|
||||
export interface EditorViewportConfig {
|
||||
/** Unique viewport identifier | 唯一视口标识符 */
|
||||
id: string;
|
||||
/** Canvas element ID | 画布元素 ID */
|
||||
canvasId: string;
|
||||
/** Preview scene ID (null = main scene) | 预览场景 ID(null = 主场景) */
|
||||
previewSceneId?: string;
|
||||
/** Whether to show grid | 是否显示网格 */
|
||||
showGrid?: boolean;
|
||||
/** Whether to show gizmos | 是否显示辅助线 */
|
||||
showGizmos?: boolean;
|
||||
/** Initial camera configuration | 初始相机配置 */
|
||||
camera?: ViewportCameraConfig;
|
||||
/** Clear color | 清除颜色 */
|
||||
clearColor?: { r: number; g: number; b: number; a: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewport state
|
||||
* 视口状态
|
||||
*/
|
||||
interface ViewportState {
|
||||
config: EditorViewportConfig;
|
||||
camera: ViewportCameraConfig;
|
||||
overlays: Map<string, IViewportOverlay>;
|
||||
lastUpdateTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gizmo renderer interface (provided by engine)
|
||||
* 辅助线渲染器接口(由引擎提供)
|
||||
*/
|
||||
export interface IGizmoRenderer {
|
||||
addLine(x1: number, y1: number, x2: number, y2: number, color: number, thickness?: number): void;
|
||||
addRect(x: number, y: number, width: number, height: number, color: number, thickness?: number): void;
|
||||
addFilledRect(x: number, y: number, width: number, height: number, color: number): void;
|
||||
addCircle(x: number, y: number, radius: number, color: number, thickness?: number): void;
|
||||
addFilledCircle(x: number, y: number, radius: number, color: number): void;
|
||||
addText?(text: string, x: number, y: number, color: number, fontSize?: number): void;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor Viewport Service Interface
|
||||
* 编辑器视口服务接口
|
||||
*/
|
||||
export interface IEditorViewportService {
|
||||
/**
|
||||
* Set the viewport service (from EngineService)
|
||||
* 设置视口服务(来自 EngineService)
|
||||
*/
|
||||
setViewportService(service: IViewportService): void;
|
||||
|
||||
/**
|
||||
* Set the gizmo renderer (from EngineService)
|
||||
* 设置辅助线渲染器(来自 EngineService)
|
||||
*/
|
||||
setGizmoRenderer(renderer: IGizmoRenderer): void;
|
||||
|
||||
/**
|
||||
* Register a new viewport
|
||||
* 注册新视口
|
||||
*/
|
||||
registerViewport(config: EditorViewportConfig): void;
|
||||
|
||||
/**
|
||||
* Unregister a viewport
|
||||
* 注销视口
|
||||
*/
|
||||
unregisterViewport(id: string): void;
|
||||
|
||||
/**
|
||||
* Get viewport configuration
|
||||
* 获取视口配置
|
||||
*/
|
||||
getViewportConfig(id: string): EditorViewportConfig | null;
|
||||
|
||||
/**
|
||||
* Set viewport camera
|
||||
* 设置视口相机
|
||||
*/
|
||||
setViewportCamera(id: string, camera: ViewportCameraConfig): void;
|
||||
|
||||
/**
|
||||
* Get viewport camera
|
||||
* 获取视口相机
|
||||
*/
|
||||
getViewportCamera(id: string): ViewportCameraConfig | null;
|
||||
|
||||
/**
|
||||
* Add overlay to a viewport
|
||||
* 向视口添加覆盖层
|
||||
*/
|
||||
addOverlay(viewportId: string, overlay: IViewportOverlay): void;
|
||||
|
||||
/**
|
||||
* Remove overlay from a viewport
|
||||
* 从视口移除覆盖层
|
||||
*/
|
||||
removeOverlay(viewportId: string, overlayId: string): void;
|
||||
|
||||
/**
|
||||
* Get overlay by ID
|
||||
* 通过 ID 获取覆盖层
|
||||
*/
|
||||
getOverlay<T extends IViewportOverlay>(viewportId: string, overlayId: string): T | null;
|
||||
|
||||
/**
|
||||
* Get all overlays for a viewport
|
||||
* 获取视口的所有覆盖层
|
||||
*/
|
||||
getOverlays(viewportId: string): IViewportOverlay[];
|
||||
|
||||
/**
|
||||
* Render a specific viewport
|
||||
* 渲染特定视口
|
||||
*/
|
||||
renderViewport(id: string): void;
|
||||
|
||||
/**
|
||||
* Update viewport (process preview scene systems and overlays)
|
||||
* 更新视口(处理预览场景系统和覆盖层)
|
||||
*/
|
||||
updateViewport(id: string, deltaTime: number): void;
|
||||
|
||||
/**
|
||||
* Resize a viewport
|
||||
* 调整视口大小
|
||||
*/
|
||||
resizeViewport(id: string, width: number, height: number): void;
|
||||
|
||||
/**
|
||||
* Check if a viewport exists
|
||||
* 检查视口是否存在
|
||||
*/
|
||||
hasViewport(id: string): boolean;
|
||||
|
||||
/**
|
||||
* Get all viewport IDs
|
||||
* 获取所有视口 ID
|
||||
*/
|
||||
getViewportIds(): string[];
|
||||
|
||||
/**
|
||||
* Dispose the service
|
||||
* 释放服务
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor Viewport Service Implementation
|
||||
* 编辑器视口服务实现
|
||||
*/
|
||||
export class EditorViewportService implements IEditorViewportService {
|
||||
private static _instance: EditorViewportService | null = null;
|
||||
|
||||
private _viewportService: IViewportService | null = null;
|
||||
private _gizmoRenderer: IGizmoRenderer | null = null;
|
||||
private _viewports: Map<string, ViewportState> = new Map();
|
||||
private _previewSceneService = PreviewSceneService.getInstance();
|
||||
private _viewportDimensions: Map<string, { width: number; height: number }> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(): EditorViewportService {
|
||||
if (!EditorViewportService._instance) {
|
||||
EditorViewportService._instance = new EditorViewportService();
|
||||
}
|
||||
return EditorViewportService._instance;
|
||||
}
|
||||
|
||||
setViewportService(service: IViewportService): void {
|
||||
this._viewportService = service;
|
||||
}
|
||||
|
||||
setGizmoRenderer(renderer: IGizmoRenderer): void {
|
||||
this._gizmoRenderer = renderer;
|
||||
}
|
||||
|
||||
registerViewport(config: EditorViewportConfig): void {
|
||||
if (this._viewports.has(config.id)) {
|
||||
console.warn(`[EditorViewportService] Viewport "${config.id}" already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register with viewport service
|
||||
if (this._viewportService) {
|
||||
this._viewportService.registerViewport(config.id, config.canvasId);
|
||||
this._viewportService.setViewportConfig(config.id, config.showGrid ?? false, config.showGizmos ?? false);
|
||||
|
||||
if (config.camera) {
|
||||
this._viewportService.setViewportCamera(config.id, config.camera);
|
||||
}
|
||||
}
|
||||
|
||||
// Create viewport state
|
||||
const state: ViewportState = {
|
||||
config,
|
||||
camera: config.camera ?? { x: 0, y: 0, zoom: 1 },
|
||||
overlays: new Map(),
|
||||
lastUpdateTime: performance.now()
|
||||
};
|
||||
|
||||
this._viewports.set(config.id, state);
|
||||
}
|
||||
|
||||
unregisterViewport(id: string): void {
|
||||
const state = this._viewports.get(id);
|
||||
if (!state) return;
|
||||
|
||||
// Dispose overlays
|
||||
for (const overlay of state.overlays.values()) {
|
||||
overlay.dispose?.();
|
||||
}
|
||||
|
||||
// Unregister from viewport service
|
||||
if (this._viewportService) {
|
||||
this._viewportService.unregisterViewport(id);
|
||||
}
|
||||
|
||||
this._viewports.delete(id);
|
||||
this._viewportDimensions.delete(id);
|
||||
}
|
||||
|
||||
getViewportConfig(id: string): EditorViewportConfig | null {
|
||||
return this._viewports.get(id)?.config ?? null;
|
||||
}
|
||||
|
||||
setViewportCamera(id: string, camera: ViewportCameraConfig): void {
|
||||
const state = this._viewports.get(id);
|
||||
if (!state) return;
|
||||
|
||||
state.camera = camera;
|
||||
|
||||
if (this._viewportService) {
|
||||
this._viewportService.setViewportCamera(id, camera);
|
||||
}
|
||||
}
|
||||
|
||||
getViewportCamera(id: string): ViewportCameraConfig | null {
|
||||
return this._viewports.get(id)?.camera ?? null;
|
||||
}
|
||||
|
||||
addOverlay(viewportId: string, overlay: IViewportOverlay): void {
|
||||
const state = this._viewports.get(viewportId);
|
||||
if (!state) {
|
||||
console.warn(`[EditorViewportService] Viewport "${viewportId}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.overlays.has(overlay.id)) {
|
||||
console.warn(`[EditorViewportService] Overlay "${overlay.id}" already exists in viewport "${viewportId}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
state.overlays.set(overlay.id, overlay);
|
||||
}
|
||||
|
||||
removeOverlay(viewportId: string, overlayId: string): void {
|
||||
const state = this._viewports.get(viewportId);
|
||||
if (!state) return;
|
||||
|
||||
const overlay = state.overlays.get(overlayId);
|
||||
if (overlay) {
|
||||
overlay.dispose?.();
|
||||
state.overlays.delete(overlayId);
|
||||
}
|
||||
}
|
||||
|
||||
getOverlay<T extends IViewportOverlay>(viewportId: string, overlayId: string): T | null {
|
||||
const state = this._viewports.get(viewportId);
|
||||
if (!state) return null;
|
||||
|
||||
return (state.overlays.get(overlayId) as T) ?? null;
|
||||
}
|
||||
|
||||
getOverlays(viewportId: string): IViewportOverlay[] {
|
||||
const state = this._viewports.get(viewportId);
|
||||
if (!state) return [];
|
||||
|
||||
// Return overlays sorted by priority
|
||||
return Array.from(state.overlays.values()).sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
|
||||
updateViewport(id: string, deltaTime: number): void {
|
||||
const state = this._viewports.get(id);
|
||||
if (!state) return;
|
||||
|
||||
// Update preview scene if configured
|
||||
if (state.config.previewSceneId) {
|
||||
const previewScene = this._previewSceneService.getScene(state.config.previewSceneId);
|
||||
if (previewScene) {
|
||||
previewScene.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Update overlays
|
||||
for (const overlay of state.overlays.values()) {
|
||||
if (overlay.visible && overlay.update) {
|
||||
overlay.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
state.lastUpdateTime = performance.now();
|
||||
}
|
||||
|
||||
renderViewport(id: string): void {
|
||||
const state = this._viewports.get(id);
|
||||
if (!state || !this._viewportService) return;
|
||||
|
||||
const dimensions = this._viewportDimensions.get(id) ?? { width: 800, height: 600 };
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
// Render overlays if gizmo renderer is available
|
||||
if (this._gizmoRenderer) {
|
||||
const context: OverlayRenderContext = {
|
||||
camera: state.camera,
|
||||
viewport: dimensions,
|
||||
dpr,
|
||||
deltaTime: (performance.now() - state.lastUpdateTime) / 1000,
|
||||
addLine: (x1, y1, x2, y2, color, thickness) =>
|
||||
this._gizmoRenderer!.addLine(x1, y1, x2, y2, color, thickness),
|
||||
addRect: (x, y, w, h, color, thickness) =>
|
||||
this._gizmoRenderer!.addRect(x, y, w, h, color, thickness),
|
||||
addFilledRect: (x, y, w, h, color) =>
|
||||
this._gizmoRenderer!.addFilledRect(x, y, w, h, color),
|
||||
addCircle: (x, y, r, color, thickness) =>
|
||||
this._gizmoRenderer!.addCircle(x, y, r, color, thickness),
|
||||
addFilledCircle: (x, y, r, color) =>
|
||||
this._gizmoRenderer!.addFilledCircle(x, y, r, color),
|
||||
addText: this._gizmoRenderer.addText
|
||||
? (text, x, y, color, fontSize) =>
|
||||
this._gizmoRenderer!.addText!(text, x, y, color, fontSize)
|
||||
: undefined
|
||||
};
|
||||
|
||||
// Render visible overlays in priority order
|
||||
const overlays = this.getOverlays(id);
|
||||
for (const overlay of overlays) {
|
||||
if (overlay.visible) {
|
||||
overlay.render(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render through viewport service
|
||||
this._viewportService.renderToViewport(id);
|
||||
}
|
||||
|
||||
resizeViewport(id: string, width: number, height: number): void {
|
||||
this._viewportDimensions.set(id, { width, height });
|
||||
|
||||
if (this._viewportService) {
|
||||
this._viewportService.resizeViewport(id, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
hasViewport(id: string): boolean {
|
||||
return this._viewports.has(id);
|
||||
}
|
||||
|
||||
getViewportIds(): string[] {
|
||||
return Array.from(this._viewports.keys());
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const id of this._viewports.keys()) {
|
||||
this.unregisterViewport(id);
|
||||
}
|
||||
this._viewportService = null;
|
||||
this._gizmoRenderer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service identifier for dependency injection
|
||||
* 依赖注入的服务标识符
|
||||
*/
|
||||
export const IEditorViewportServiceIdentifier = Symbol.for('IEditorViewportService');
|
||||
109
packages/editor-core/src/Services/IViewportService.ts
Normal file
109
packages/editor-core/src/Services/IViewportService.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Viewport Service Interface
|
||||
* 视口服务接口
|
||||
*
|
||||
* Provides a unified interface for rendering viewports using the engine.
|
||||
* Used by editor panels that need to display game content (e.g., Tilemap Editor).
|
||||
*
|
||||
* 提供使用引擎渲染视口的统一接口。
|
||||
* 供需要显示游戏内容的编辑器面板使用(如 Tilemap 编辑器)。
|
||||
*/
|
||||
|
||||
/**
|
||||
* Camera configuration for viewport
|
||||
* 视口相机配置
|
||||
*/
|
||||
export interface ViewportCameraConfig {
|
||||
x: number;
|
||||
y: number;
|
||||
zoom: number;
|
||||
rotation?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewport service interface
|
||||
* 视口服务接口
|
||||
*/
|
||||
export interface IViewportService {
|
||||
/**
|
||||
* Check if the renderer is initialized
|
||||
* 检查渲染器是否已初始化
|
||||
*/
|
||||
isInitialized(): boolean;
|
||||
|
||||
/**
|
||||
* Register a viewport with a canvas element
|
||||
* 注册一个视口和画布元素
|
||||
* @param viewportId - Unique identifier for the viewport | 视口的唯一标识符
|
||||
* @param canvasId - ID of the canvas element | 画布元素的 ID
|
||||
*/
|
||||
registerViewport(viewportId: string, canvasId: string): void;
|
||||
|
||||
/**
|
||||
* Unregister a viewport
|
||||
* 注销一个视口
|
||||
* @param viewportId - Viewport ID to unregister | 要注销的视口 ID
|
||||
*/
|
||||
unregisterViewport(viewportId: string): void;
|
||||
|
||||
/**
|
||||
* Set camera for a specific viewport
|
||||
* 设置特定视口的相机
|
||||
* @param viewportId - Viewport ID | 视口 ID
|
||||
* @param config - Camera configuration | 相机配置
|
||||
*/
|
||||
setViewportCamera(viewportId: string, config: ViewportCameraConfig): void;
|
||||
|
||||
/**
|
||||
* Get camera for a specific viewport
|
||||
* 获取特定视口的相机
|
||||
* @param viewportId - Viewport ID | 视口 ID
|
||||
* @returns Camera configuration or null | 相机配置或 null
|
||||
*/
|
||||
getViewportCamera(viewportId: string): ViewportCameraConfig | null;
|
||||
|
||||
/**
|
||||
* Set viewport configuration (grid, gizmos visibility)
|
||||
* 设置视口配置(网格、辅助线可见性)
|
||||
* @param viewportId - Viewport ID | 视口 ID
|
||||
* @param showGrid - Show grid | 显示网格
|
||||
* @param showGizmos - Show gizmos | 显示辅助线
|
||||
*/
|
||||
setViewportConfig(viewportId: string, showGrid: boolean, showGizmos: boolean): void;
|
||||
|
||||
/**
|
||||
* Resize a specific viewport
|
||||
* 调整特定视口的大小
|
||||
* @param viewportId - Viewport ID | 视口 ID
|
||||
* @param width - New width | 新宽度
|
||||
* @param height - New height | 新高度
|
||||
*/
|
||||
resizeViewport(viewportId: string, width: number, height: number): void;
|
||||
|
||||
/**
|
||||
* Render to a specific viewport
|
||||
* 渲染到特定视口
|
||||
* @param viewportId - Viewport ID | 视口 ID
|
||||
*/
|
||||
renderToViewport(viewportId: string): void;
|
||||
|
||||
/**
|
||||
* Load a texture and return its ID
|
||||
* 加载纹理并返回其 ID
|
||||
* @param path - Texture path | 纹理路径
|
||||
* @returns Promise resolving to texture ID | 解析为纹理 ID 的 Promise
|
||||
*/
|
||||
loadTexture(path: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* Dispose resources (required by IService)
|
||||
* 释放资源(IService 要求)
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service identifier for dependency injection
|
||||
* 依赖注入的服务标识符
|
||||
*/
|
||||
export const IViewportService_ID = Symbol.for('IViewportService');
|
||||
624
packages/editor-core/src/Services/Module/ModuleRegistry.ts
Normal file
624
packages/editor-core/src/Services/Module/ModuleRegistry.ts
Normal file
@@ -0,0 +1,624 @@
|
||||
/**
|
||||
* Module Registry Service.
|
||||
* 模块注册表服务。
|
||||
*
|
||||
* Manages engine modules, their dependencies, and project configurations.
|
||||
* 管理引擎模块、其依赖关系和项目配置。
|
||||
*/
|
||||
|
||||
import type {
|
||||
ModuleManifest,
|
||||
ModuleRegistryEntry,
|
||||
ModuleDisableValidation,
|
||||
ProjectModuleConfig,
|
||||
SceneModuleUsage,
|
||||
ScriptModuleUsage
|
||||
} from './ModuleTypes';
|
||||
|
||||
/**
|
||||
* File system interface for module operations.
|
||||
* 模块操作的文件系统接口。
|
||||
*/
|
||||
export interface IModuleFileSystem {
|
||||
/** Read JSON file | 读取 JSON 文件 */
|
||||
readJson<T>(path: string): Promise<T>;
|
||||
/** Write JSON file | 写入 JSON 文件 */
|
||||
writeJson(path: string, data: unknown): Promise<void>;
|
||||
/** Check if path exists | 检查路径是否存在 */
|
||||
pathExists(path: string): Promise<boolean>;
|
||||
/** List files by extension | 按扩展名列出文件 */
|
||||
listFiles(dir: string, extensions: string[], recursive?: boolean): Promise<string[]>;
|
||||
/** Read file as text | 读取文件为文本 */
|
||||
readText(path: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module Registry Service.
|
||||
* 模块注册表服务。
|
||||
*/
|
||||
export class ModuleRegistry {
|
||||
private _modules: Map<string, ModuleRegistryEntry> = new Map();
|
||||
private _projectConfig: ProjectModuleConfig = { enabled: [] };
|
||||
private _fileSystem: IModuleFileSystem | null = null;
|
||||
private _engineModulesPath: string = '';
|
||||
private _projectPath: string = '';
|
||||
|
||||
/**
|
||||
* Initialize the registry.
|
||||
* 初始化注册表。
|
||||
*
|
||||
* @param fileSystem - File system service | 文件系统服务
|
||||
* @param engineModulesPath - Path to engine modules | 引擎模块路径
|
||||
*/
|
||||
async initialize(
|
||||
fileSystem: IModuleFileSystem,
|
||||
engineModulesPath: string
|
||||
): Promise<void> {
|
||||
this._fileSystem = fileSystem;
|
||||
this._engineModulesPath = engineModulesPath;
|
||||
|
||||
// Load all module manifests | 加载所有模块清单
|
||||
await this._loadModuleManifests();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current project.
|
||||
* 设置当前项目。
|
||||
*
|
||||
* @param projectPath - Project path | 项目路径
|
||||
*/
|
||||
async setProject(projectPath: string): Promise<void> {
|
||||
this._projectPath = projectPath;
|
||||
await this._loadProjectConfig();
|
||||
this._updateModuleStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered modules.
|
||||
* 获取所有注册的模块。
|
||||
*/
|
||||
getAllModules(): ModuleRegistryEntry[] {
|
||||
return Array.from(this._modules.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module by ID.
|
||||
* 通过 ID 获取模块。
|
||||
*/
|
||||
getModule(id: string): ModuleRegistryEntry | undefined {
|
||||
return this._modules.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules for current project.
|
||||
* 获取当前项目启用的模块。
|
||||
*/
|
||||
getEnabledModules(): ModuleRegistryEntry[] {
|
||||
return this.getAllModules().filter(m => m.isEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get modules by category.
|
||||
* 按分类获取模块。
|
||||
*/
|
||||
getModulesByCategory(): Map<string, ModuleRegistryEntry[]> {
|
||||
const categories = new Map<string, ModuleRegistryEntry[]>();
|
||||
|
||||
for (const module of this._modules.values()) {
|
||||
const category = module.category;
|
||||
if (!categories.has(category)) {
|
||||
categories.set(category, []);
|
||||
}
|
||||
categories.get(category)!.push(module);
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a module can be disabled.
|
||||
* 验证模块是否可以禁用。
|
||||
*
|
||||
* @param moduleId - Module ID | 模块 ID
|
||||
*/
|
||||
async validateDisable(moduleId: string): Promise<ModuleDisableValidation> {
|
||||
const module = this._modules.get(moduleId);
|
||||
|
||||
if (!module) {
|
||||
return {
|
||||
canDisable: false,
|
||||
reason: 'core',
|
||||
message: `Module "${moduleId}" not found | 未找到模块 "${moduleId}"`
|
||||
};
|
||||
}
|
||||
|
||||
// Core modules cannot be disabled | 核心模块不能禁用
|
||||
if (module.isCore) {
|
||||
return {
|
||||
canDisable: false,
|
||||
reason: 'core',
|
||||
message: `"${module.displayName}" is a core module and cannot be disabled | "${module.displayName}" 是核心模块,不能禁用`
|
||||
};
|
||||
}
|
||||
|
||||
// Check if other enabled modules depend on this | 检查其他启用的模块是否依赖此模块
|
||||
const dependents = this._getEnabledDependents(moduleId);
|
||||
if (dependents.length > 0) {
|
||||
return {
|
||||
canDisable: false,
|
||||
reason: 'dependency',
|
||||
message: `The following enabled modules depend on "${module.displayName}" | 以下启用的模块依赖 "${module.displayName}"`,
|
||||
dependentModules: dependents
|
||||
};
|
||||
}
|
||||
|
||||
// Check scene usage | 检查场景使用
|
||||
const sceneUsages = await this._checkSceneUsage(moduleId);
|
||||
if (sceneUsages.length > 0) {
|
||||
return {
|
||||
canDisable: false,
|
||||
reason: 'scene-usage',
|
||||
message: `"${module.displayName}" components are used in scenes | "${module.displayName}" 组件在场景中使用`,
|
||||
sceneUsages
|
||||
};
|
||||
}
|
||||
|
||||
// Check script usage | 检查脚本使用
|
||||
const scriptUsages = await this._checkScriptUsage(moduleId);
|
||||
if (scriptUsages.length > 0) {
|
||||
return {
|
||||
canDisable: false,
|
||||
reason: 'script-usage',
|
||||
message: `"${module.displayName}" is imported in scripts | "${module.displayName}" 在脚本中被导入`,
|
||||
scriptUsages
|
||||
};
|
||||
}
|
||||
|
||||
return { canDisable: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a module.
|
||||
* 启用模块。
|
||||
*
|
||||
* @param moduleId - Module ID | 模块 ID
|
||||
*/
|
||||
async enableModule(moduleId: string): Promise<boolean> {
|
||||
const module = this._modules.get(moduleId);
|
||||
if (!module) return false;
|
||||
|
||||
// Enable dependencies first | 先启用依赖
|
||||
for (const depId of module.dependencies) {
|
||||
if (!this._projectConfig.enabled.includes(depId)) {
|
||||
await this.enableModule(depId);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable this module | 启用此模块
|
||||
if (!this._projectConfig.enabled.includes(moduleId)) {
|
||||
this._projectConfig.enabled.push(moduleId);
|
||||
await this._saveProjectConfig();
|
||||
this._updateModuleStates();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a module.
|
||||
* 禁用模块。
|
||||
*
|
||||
* @param moduleId - Module ID | 模块 ID
|
||||
* @param force - Force disable even if validation fails | 即使验证失败也强制禁用
|
||||
*/
|
||||
async disableModule(moduleId: string, force: boolean = false): Promise<boolean> {
|
||||
if (!force) {
|
||||
const validation = await this.validateDisable(moduleId);
|
||||
if (!validation.canDisable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const index = this._projectConfig.enabled.indexOf(moduleId);
|
||||
if (index !== -1) {
|
||||
this._projectConfig.enabled.splice(index, 1);
|
||||
await this._saveProjectConfig();
|
||||
this._updateModuleStates();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total build size for enabled modules (JS + WASM).
|
||||
* 获取启用模块的总构建大小(JS + WASM)。
|
||||
*/
|
||||
getTotalBuildSize(): { jsSize: number; wasmSize: number; total: number } {
|
||||
let jsSize = 0;
|
||||
let wasmSize = 0;
|
||||
for (const module of this.getEnabledModules()) {
|
||||
jsSize += module.jsSize || 0;
|
||||
wasmSize += module.wasmSize || 0;
|
||||
}
|
||||
return { jsSize, wasmSize, total: jsSize + wasmSize };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate build entry file content.
|
||||
* 生成构建入口文件内容。
|
||||
*
|
||||
* Creates a dynamic entry that only imports enabled modules.
|
||||
* 创建仅导入启用模块的动态入口。
|
||||
*/
|
||||
generateBuildEntry(): string {
|
||||
const enabledModules = this.getEnabledModules();
|
||||
const lines: string[] = [
|
||||
'// Auto-generated build entry',
|
||||
'// 自动生成的构建入口',
|
||||
''
|
||||
];
|
||||
|
||||
// Export core modules | 导出核心模块
|
||||
const coreModules = enabledModules.filter(m => m.isCore);
|
||||
for (const module of coreModules) {
|
||||
lines.push(`export * from '${module.name}';`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
|
||||
// Export optional modules | 导出可选模块
|
||||
const optionalModules = enabledModules.filter(m => !m.isCore);
|
||||
for (const module of optionalModules) {
|
||||
lines.push(`export * from '${module.name}';`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('// Module registration');
|
||||
lines.push('// 模块注册');
|
||||
lines.push(`import { registerModule } from '@esengine/core';`);
|
||||
lines.push('');
|
||||
|
||||
// Import module classes | 导入模块类
|
||||
for (const module of optionalModules) {
|
||||
const moduleName = this._toModuleClassName(module.id);
|
||||
lines.push(`import { ${moduleName} } from '${module.name}';`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
|
||||
// Register modules | 注册模块
|
||||
for (const module of optionalModules) {
|
||||
const moduleName = this._toModuleClassName(module.id);
|
||||
lines.push(`registerModule(${moduleName});`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ==================== Private Methods | 私有方法 ====================
|
||||
|
||||
/**
|
||||
* Load module manifests from engine modules directory.
|
||||
* 从引擎模块目录加载模块清单。
|
||||
*
|
||||
* Reads index.json which contains all module data including build-time calculated sizes.
|
||||
* 读取 index.json,其中包含所有模块数据,包括构建时计算的大小。
|
||||
*/
|
||||
private async _loadModuleManifests(): Promise<void> {
|
||||
if (!this._fileSystem) return;
|
||||
|
||||
// Read module index from engine/ directory
|
||||
// 从 engine/ 目录读取模块索引
|
||||
const indexPath = `${this._engineModulesPath}/index.json`;
|
||||
|
||||
try {
|
||||
if (await this._fileSystem.pathExists(indexPath)) {
|
||||
// Load from index.json generated by copy-modules script
|
||||
// 从 copy-modules 脚本生成的 index.json 加载
|
||||
const index = await this._fileSystem.readJson<{
|
||||
version: string;
|
||||
generatedAt: string;
|
||||
modules: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
hasRuntime: boolean;
|
||||
editorPackage?: string;
|
||||
isCore: boolean;
|
||||
category: string;
|
||||
jsSize?: number;
|
||||
requiresWasm?: boolean;
|
||||
wasmSize?: number;
|
||||
}>;
|
||||
}>(indexPath);
|
||||
|
||||
console.log(`[ModuleRegistry] Loaded ${index.modules.length} modules from index.json`);
|
||||
|
||||
// Use data directly from index.json (includes jsSize, wasmSize)
|
||||
// 直接使用 index.json 中的数据(包含 jsSize、wasmSize)
|
||||
for (const m of index.modules) {
|
||||
this._modules.set(m.id, {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
displayName: m.displayName,
|
||||
description: '',
|
||||
version: '1.0.0',
|
||||
category: m.category as any,
|
||||
isCore: m.isCore,
|
||||
defaultEnabled: m.isCore,
|
||||
dependencies: [],
|
||||
exports: {},
|
||||
editorPackage: m.editorPackage,
|
||||
jsSize: m.jsSize,
|
||||
wasmSize: m.wasmSize,
|
||||
requiresWasm: m.requiresWasm,
|
||||
// Registry entry fields
|
||||
path: `${this._engineModulesPath}/${m.id}`,
|
||||
isEnabled: false,
|
||||
dependents: [],
|
||||
dependenciesSatisfied: true
|
||||
});
|
||||
}
|
||||
|
||||
// Load full manifests for additional fields (description, dependencies, exports)
|
||||
// 加载完整清单以获取额外字段(描述、依赖、导出)
|
||||
for (const m of index.modules) {
|
||||
const manifestPath = `${this._engineModulesPath}/${m.id}/module.json`;
|
||||
try {
|
||||
if (await this._fileSystem.pathExists(manifestPath)) {
|
||||
const manifest = await this._fileSystem.readJson<ModuleManifest>(manifestPath);
|
||||
const existing = this._modules.get(m.id);
|
||||
if (existing) {
|
||||
// Merge manifest data but keep jsSize/wasmSize from index
|
||||
// 合并清单数据但保留 index 中的 jsSize/wasmSize
|
||||
existing.description = manifest.description || '';
|
||||
existing.version = manifest.version || '1.0.0';
|
||||
existing.dependencies = manifest.dependencies || [];
|
||||
existing.exports = manifest.exports || {};
|
||||
existing.tags = manifest.tags;
|
||||
existing.icon = manifest.icon;
|
||||
existing.platforms = manifest.platforms;
|
||||
existing.canContainContent = manifest.canContainContent;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors loading individual manifests
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`[ModuleRegistry] index.json not found at ${indexPath}, run 'pnpm copy-modules' first`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ModuleRegistry] Failed to load index.json:', error);
|
||||
}
|
||||
|
||||
// Compute dependents | 计算依赖者
|
||||
this._computeDependents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute which modules depend on each module.
|
||||
* 计算哪些模块依赖每个模块。
|
||||
*/
|
||||
private _computeDependents(): void {
|
||||
for (const module of this._modules.values()) {
|
||||
module.dependents = [];
|
||||
}
|
||||
|
||||
for (const module of this._modules.values()) {
|
||||
for (const depId of module.dependencies) {
|
||||
const dep = this._modules.get(depId);
|
||||
if (dep) {
|
||||
dep.dependents.push(module.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load project module configuration.
|
||||
* 加载项目模块配置。
|
||||
*/
|
||||
private async _loadProjectConfig(): Promise<void> {
|
||||
if (!this._fileSystem || !this._projectPath) return;
|
||||
|
||||
const configPath = `${this._projectPath}/esengine.project.json`;
|
||||
|
||||
try {
|
||||
if (await this._fileSystem.pathExists(configPath)) {
|
||||
const config = await this._fileSystem.readJson<{ modules?: ProjectModuleConfig }>(configPath);
|
||||
this._projectConfig = config.modules || { enabled: this._getDefaultEnabledModules() };
|
||||
} else {
|
||||
// Create default config | 创建默认配置
|
||||
this._projectConfig = { enabled: this._getDefaultEnabledModules() };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ModuleRegistry] Failed to load project config:', error);
|
||||
this._projectConfig = { enabled: this._getDefaultEnabledModules() };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save project module configuration.
|
||||
* 保存项目模块配置。
|
||||
*/
|
||||
private async _saveProjectConfig(): Promise<void> {
|
||||
if (!this._fileSystem || !this._projectPath) return;
|
||||
|
||||
const configPath = `${this._projectPath}/esengine.project.json`;
|
||||
|
||||
try {
|
||||
let config: Record<string, unknown> = {};
|
||||
|
||||
if (await this._fileSystem.pathExists(configPath)) {
|
||||
config = await this._fileSystem.readJson<Record<string, unknown>>(configPath);
|
||||
}
|
||||
|
||||
config.modules = this._projectConfig;
|
||||
await this._fileSystem.writeJson(configPath, config);
|
||||
} catch (error) {
|
||||
console.error('[ModuleRegistry] Failed to save project config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default enabled modules.
|
||||
* 获取默认启用的模块。
|
||||
*/
|
||||
private _getDefaultEnabledModules(): string[] {
|
||||
const defaults: string[] = [];
|
||||
|
||||
for (const module of this._modules.values()) {
|
||||
if (module.isCore || module.defaultEnabled) {
|
||||
defaults.push(module.id);
|
||||
}
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update module enabled states based on project config.
|
||||
* 根据项目配置更新模块启用状态。
|
||||
*/
|
||||
private _updateModuleStates(): void {
|
||||
for (const module of this._modules.values()) {
|
||||
module.isEnabled = module.isCore || this._projectConfig.enabled.includes(module.id);
|
||||
module.dependenciesSatisfied = module.dependencies.every(
|
||||
depId => this._projectConfig.enabled.includes(depId) || this._modules.get(depId)?.isCore
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules that depend on the given module.
|
||||
* 获取依赖给定模块的已启用模块。
|
||||
*/
|
||||
private _getEnabledDependents(moduleId: string): string[] {
|
||||
const module = this._modules.get(moduleId);
|
||||
if (!module) return [];
|
||||
|
||||
return module.dependents.filter(depId => {
|
||||
const dep = this._modules.get(depId);
|
||||
return dep?.isEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if module is used in any scene.
|
||||
* 检查模块是否在任何场景中使用。
|
||||
*/
|
||||
private async _checkSceneUsage(moduleId: string): Promise<SceneModuleUsage[]> {
|
||||
if (!this._fileSystem || !this._projectPath) return [];
|
||||
|
||||
const module = this._modules.get(moduleId);
|
||||
if (!module || !module.exports.components?.length) return [];
|
||||
|
||||
const usages: SceneModuleUsage[] = [];
|
||||
const sceneDir = `${this._projectPath}/assets`;
|
||||
|
||||
try {
|
||||
const sceneFiles = await this._fileSystem.listFiles(sceneDir, ['.ecs'], true);
|
||||
|
||||
for (const scenePath of sceneFiles) {
|
||||
const sceneContent = await this._fileSystem.readText(scenePath);
|
||||
const componentUsages: SceneModuleUsage['components'] = [];
|
||||
|
||||
for (const componentName of module.exports.components) {
|
||||
// Count occurrences of component type in scene
|
||||
// 计算场景中组件类型的出现次数
|
||||
const regex = new RegExp(`"type"\\s*:\\s*"${componentName}"`, 'g');
|
||||
const matches = sceneContent.match(regex);
|
||||
if (matches && matches.length > 0) {
|
||||
componentUsages.push({
|
||||
type: componentName,
|
||||
count: matches.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (componentUsages.length > 0) {
|
||||
usages.push({
|
||||
scenePath,
|
||||
components: componentUsages
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[ModuleRegistry] Failed to check scene usage:', error);
|
||||
}
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if module is imported in any user script.
|
||||
* 检查模块是否在任何用户脚本中被导入。
|
||||
*/
|
||||
private async _checkScriptUsage(moduleId: string): Promise<ScriptModuleUsage[]> {
|
||||
if (!this._fileSystem || !this._projectPath) return [];
|
||||
|
||||
const module = this._modules.get(moduleId);
|
||||
if (!module) return [];
|
||||
|
||||
const usages: ScriptModuleUsage[] = [];
|
||||
const scriptsDir = `${this._projectPath}/scripts`;
|
||||
|
||||
try {
|
||||
if (!await this._fileSystem.pathExists(scriptsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scriptFiles = await this._fileSystem.listFiles(scriptsDir, ['.ts', '.tsx', '.js'], true);
|
||||
|
||||
for (const scriptPath of scriptFiles) {
|
||||
const content = await this._fileSystem.readText(scriptPath);
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Check for import from module package | 检查模块包的导入
|
||||
if (line.includes(module.name) && line.includes('import')) {
|
||||
usages.push({
|
||||
scriptPath,
|
||||
line: i + 1,
|
||||
importStatement: line.trim()
|
||||
});
|
||||
}
|
||||
|
||||
// Check for component imports | 检查组件导入
|
||||
if (module.exports.components) {
|
||||
for (const component of module.exports.components) {
|
||||
if (line.includes(component) && line.includes('import')) {
|
||||
usages.push({
|
||||
scriptPath,
|
||||
line: i + 1,
|
||||
importStatement: line.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[ModuleRegistry] Failed to check script usage:', error);
|
||||
}
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert module ID to class name.
|
||||
* 将模块 ID 转换为类名。
|
||||
*/
|
||||
private _toModuleClassName(id: string): string {
|
||||
return id
|
||||
.split('-')
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('') + 'Module';
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance | 导出单例实例
|
||||
export const moduleRegistry = new ModuleRegistry();
|
||||
115
packages/editor-core/src/Services/Module/ModuleTypes.ts
Normal file
115
packages/editor-core/src/Services/Module/ModuleTypes.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Module System Types.
|
||||
* 模块系统类型定义。
|
||||
*
|
||||
* Re-exports core types from engine-core and defines editor-specific types.
|
||||
* 从 engine-core 重新导出核心类型,并定义编辑器专用类型。
|
||||
*/
|
||||
|
||||
import type {
|
||||
ModuleCategory,
|
||||
ModulePlatform,
|
||||
ModuleManifest,
|
||||
ModuleExports
|
||||
} from '@esengine/engine-core';
|
||||
|
||||
// Re-export core module types
|
||||
export type { ModuleCategory, ModulePlatform, ModuleManifest, ModuleExports };
|
||||
|
||||
/**
|
||||
* Module state in a project.
|
||||
* 项目中的模块状态。
|
||||
*/
|
||||
export interface ModuleState {
|
||||
/** Module ID | 模块 ID */
|
||||
id: string;
|
||||
|
||||
/** Whether enabled in this project | 在此项目中是否启用 */
|
||||
enabled: boolean;
|
||||
|
||||
/** Version being used | 使用的版本 */
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of validating a module disable operation.
|
||||
* 验证禁用模块操作的结果。
|
||||
*/
|
||||
export interface ModuleDisableValidation {
|
||||
/** Whether the module can be disabled | 是否可以禁用 */
|
||||
canDisable: boolean;
|
||||
|
||||
/** Reason why it cannot be disabled | 不能禁用的原因 */
|
||||
reason?: 'core' | 'dependency' | 'scene-usage' | 'script-usage';
|
||||
|
||||
/** Detailed message | 详细消息 */
|
||||
message?: string;
|
||||
|
||||
/** Scene files that use this module | 使用此模块的场景文件 */
|
||||
sceneUsages?: SceneModuleUsage[];
|
||||
|
||||
/** Script files that import this module | 导入此模块的脚本文件 */
|
||||
scriptUsages?: ScriptModuleUsage[];
|
||||
|
||||
/** Other modules that depend on this | 依赖此模块的其他模块 */
|
||||
dependentModules?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene usage of a module.
|
||||
* 场景对模块的使用。
|
||||
*/
|
||||
export interface SceneModuleUsage {
|
||||
/** Scene file path | 场景文件路径 */
|
||||
scenePath: string;
|
||||
|
||||
/** Components used from the module | 使用的模块组件 */
|
||||
components: {
|
||||
/** Component type name | 组件类型名 */
|
||||
type: string;
|
||||
/** Number of instances | 实例数量 */
|
||||
count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Script usage of a module.
|
||||
* 脚本对模块的使用。
|
||||
*/
|
||||
export interface ScriptModuleUsage {
|
||||
/** Script file path | 脚本文件路径 */
|
||||
scriptPath: string;
|
||||
|
||||
/** Line number of import | 导入的行号 */
|
||||
line: number;
|
||||
|
||||
/** Import statement | 导入语句 */
|
||||
importStatement: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project module configuration.
|
||||
* 项目模块配置。
|
||||
*/
|
||||
export interface ProjectModuleConfig {
|
||||
/** Enabled module IDs | 启用的模块 ID */
|
||||
enabled: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Module registry entry with computed properties.
|
||||
* 带计算属性的模块注册表条目。
|
||||
*/
|
||||
export interface ModuleRegistryEntry extends ModuleManifest {
|
||||
/** Full path to module directory | 模块目录完整路径 */
|
||||
path: string;
|
||||
|
||||
/** Whether module is currently enabled in project | 模块当前是否在项目中启用 */
|
||||
isEnabled: boolean;
|
||||
|
||||
/** Modules that depend on this module | 依赖此模块的模块 */
|
||||
dependents: string[];
|
||||
|
||||
/** Whether all dependencies are satisfied | 是否满足所有依赖 */
|
||||
dependenciesSatisfied: boolean;
|
||||
}
|
||||
7
packages/editor-core/src/Services/Module/index.ts
Normal file
7
packages/editor-core/src/Services/Module/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Module System exports.
|
||||
* 模块系统导出。
|
||||
*/
|
||||
|
||||
export * from './ModuleTypes';
|
||||
export * from './ModuleRegistry';
|
||||
272
packages/editor-core/src/Services/PreviewSceneService.ts
Normal file
272
packages/editor-core/src/Services/PreviewSceneService.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Preview Scene Service
|
||||
* 预览场景服务
|
||||
*
|
||||
* Manages isolated preview scenes for editor tools (tilemap editor, material preview, etc.)
|
||||
* 管理编辑器工具的隔离预览场景(瓦片地图编辑器、材质预览等)
|
||||
*/
|
||||
|
||||
import { Scene, EntitySystem, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Configuration for creating a preview scene
|
||||
* 创建预览场景的配置
|
||||
*/
|
||||
export interface PreviewSceneConfig {
|
||||
/** Unique identifier for the preview scene | 预览场景的唯一标识符 */
|
||||
id: string;
|
||||
/** Scene name | 场景名称 */
|
||||
name?: string;
|
||||
/** Systems to add to the scene | 要添加到场景的系统 */
|
||||
systems?: EntitySystem[];
|
||||
/** Initial clear color | 初始清除颜色 */
|
||||
clearColor?: { r: number; g: number; b: number; a: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an isolated preview scene for editor tools
|
||||
* 表示编辑器工具的隔离预览场景
|
||||
*/
|
||||
export interface IPreviewScene {
|
||||
/** Scene instance | 场景实例 */
|
||||
readonly scene: Scene;
|
||||
/** Unique identifier | 唯一标识符 */
|
||||
readonly id: string;
|
||||
/** Scene name | 场景名称 */
|
||||
readonly name: string;
|
||||
/** Clear color | 清除颜色 */
|
||||
clearColor: { r: number; g: number; b: number; a: number };
|
||||
|
||||
/**
|
||||
* Create a temporary entity (auto-cleaned on dispose)
|
||||
* 创建临时实体(dispose 时自动清理)
|
||||
*/
|
||||
createEntity(name: string): Entity;
|
||||
|
||||
/**
|
||||
* Remove a temporary entity
|
||||
* 移除临时实体
|
||||
*/
|
||||
removeEntity(entity: Entity): void;
|
||||
|
||||
/**
|
||||
* Get all entities in the scene
|
||||
* 获取场景中的所有实体
|
||||
*/
|
||||
getEntities(): readonly Entity[];
|
||||
|
||||
/**
|
||||
* Clear all temporary entities
|
||||
* 清除所有临时实体
|
||||
*/
|
||||
clearEntities(): void;
|
||||
|
||||
/**
|
||||
* Add a system to the scene
|
||||
* 向场景添加系统
|
||||
*/
|
||||
addSystem(system: EntitySystem): void;
|
||||
|
||||
/**
|
||||
* Remove a system from the scene
|
||||
* 从场景移除系统
|
||||
*/
|
||||
removeSystem(system: EntitySystem): void;
|
||||
|
||||
/**
|
||||
* Update the scene (process systems)
|
||||
* 更新场景(处理系统)
|
||||
*/
|
||||
update(deltaTime: number): void;
|
||||
|
||||
/**
|
||||
* Dispose the preview scene
|
||||
* 释放预览场景
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview scene implementation
|
||||
* 预览场景实现
|
||||
*/
|
||||
class PreviewScene implements IPreviewScene {
|
||||
readonly scene: Scene;
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
clearColor: { r: number; g: number; b: number; a: number };
|
||||
|
||||
private _entities: Set<Entity> = new Set();
|
||||
private _disposed = false;
|
||||
|
||||
constructor(config: PreviewSceneConfig) {
|
||||
this.id = config.id;
|
||||
this.name = config.name ?? `PreviewScene_${config.id}`;
|
||||
this.clearColor = config.clearColor ?? { r: 0.1, g: 0.1, b: 0.12, a: 1.0 };
|
||||
|
||||
// Create isolated scene
|
||||
this.scene = new Scene({ name: this.name });
|
||||
|
||||
// Add configured systems
|
||||
if (config.systems) {
|
||||
for (const system of config.systems) {
|
||||
this.scene.addSystem(system);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createEntity(name: string): Entity {
|
||||
if (this._disposed) {
|
||||
throw new Error(`PreviewScene ${this.id} is disposed`);
|
||||
}
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this._entities.add(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
removeEntity(entity: Entity): void {
|
||||
if (this._disposed) return;
|
||||
|
||||
if (this._entities.has(entity)) {
|
||||
this._entities.delete(entity);
|
||||
this.scene.destroyEntities([entity]);
|
||||
}
|
||||
}
|
||||
|
||||
getEntities(): readonly Entity[] {
|
||||
return Array.from(this._entities);
|
||||
}
|
||||
|
||||
clearEntities(): void {
|
||||
if (this._disposed) return;
|
||||
|
||||
const entities = Array.from(this._entities);
|
||||
if (entities.length > 0) {
|
||||
this.scene.destroyEntities(entities);
|
||||
}
|
||||
this._entities.clear();
|
||||
}
|
||||
|
||||
addSystem(system: EntitySystem): void {
|
||||
if (this._disposed) return;
|
||||
this.scene.addSystem(system);
|
||||
}
|
||||
|
||||
removeSystem(system: EntitySystem): void {
|
||||
if (this._disposed) return;
|
||||
this.scene.removeSystem(system);
|
||||
}
|
||||
|
||||
update(_deltaTime: number): void {
|
||||
if (this._disposed) return;
|
||||
this.scene.update();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._disposed) return;
|
||||
this._disposed = true;
|
||||
|
||||
// Clear all entities
|
||||
this.clearEntities();
|
||||
|
||||
// Scene cleanup is handled by GC
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Scene Service - manages all preview scenes
|
||||
* 预览场景服务 - 管理所有预览场景
|
||||
*/
|
||||
export class PreviewSceneService {
|
||||
private static _instance: PreviewSceneService | null = null;
|
||||
private _scenes: Map<string, PreviewScene> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(): PreviewSceneService {
|
||||
if (!PreviewSceneService._instance) {
|
||||
PreviewSceneService._instance = new PreviewSceneService();
|
||||
}
|
||||
return PreviewSceneService._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new preview scene
|
||||
* 创建新的预览场景
|
||||
*/
|
||||
createScene(config: PreviewSceneConfig): IPreviewScene {
|
||||
if (this._scenes.has(config.id)) {
|
||||
throw new Error(`Preview scene with id "${config.id}" already exists`);
|
||||
}
|
||||
|
||||
const scene = new PreviewScene(config);
|
||||
this._scenes.set(config.id, scene);
|
||||
return scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a preview scene by ID
|
||||
* 通过 ID 获取预览场景
|
||||
*/
|
||||
getScene(id: string): IPreviewScene | null {
|
||||
return this._scenes.get(id) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a preview scene exists
|
||||
* 检查预览场景是否存在
|
||||
*/
|
||||
hasScene(id: string): boolean {
|
||||
return this._scenes.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose a preview scene
|
||||
* 释放预览场景
|
||||
*/
|
||||
disposeScene(id: string): void {
|
||||
const scene = this._scenes.get(id);
|
||||
if (scene) {
|
||||
scene.dispose();
|
||||
this._scenes.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all preview scene IDs
|
||||
* 获取所有预览场景 ID
|
||||
*/
|
||||
getSceneIds(): string[] {
|
||||
return Array.from(this._scenes.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all preview scenes
|
||||
* 释放所有预览场景
|
||||
*/
|
||||
disposeAll(): void {
|
||||
for (const scene of this._scenes.values()) {
|
||||
scene.dispose();
|
||||
}
|
||||
this._scenes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the service
|
||||
* 释放服务
|
||||
*/
|
||||
dispose(): void {
|
||||
this.disposeAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service identifier for dependency injection
|
||||
* 依赖注入的服务标识符
|
||||
*/
|
||||
export const IPreviewSceneService = Symbol.for('IPreviewSceneService');
|
||||
@@ -35,17 +35,36 @@ export interface PluginSettings {
|
||||
enabledPlugins: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块配置
|
||||
* Module Configuration
|
||||
*/
|
||||
export interface ModuleSettings {
|
||||
/**
|
||||
* 禁用的模块 ID 列表(黑名单方式)
|
||||
* Disabled module IDs (blacklist approach)
|
||||
* Modules NOT in this list are enabled.
|
||||
* 不在此列表中的模块为启用状态。
|
||||
*/
|
||||
disabledModules: string[];
|
||||
}
|
||||
|
||||
export interface ProjectConfig {
|
||||
projectType?: ProjectType;
|
||||
componentsPath?: string;
|
||||
componentPattern?: string;
|
||||
/** User scripts directory (default: 'scripts') | 用户脚本目录(默认:'scripts') */
|
||||
scriptsPath?: string;
|
||||
/** Build output directory | 构建输出目录 */
|
||||
buildOutput?: string;
|
||||
/** Scenes directory | 场景目录 */
|
||||
scenesPath?: string;
|
||||
/** Default scene file | 默认场景文件 */
|
||||
defaultScene?: string;
|
||||
/** UI 设计分辨率 / UI design resolution */
|
||||
/** UI design resolution | UI 设计分辨率 */
|
||||
uiDesignResolution?: UIDesignResolution;
|
||||
/** 插件配置 / Plugin settings */
|
||||
/** Plugin settings | 插件配置 */
|
||||
plugins?: PluginSettings;
|
||||
/** Module settings | 模块配置 */
|
||||
modules?: ModuleSettings;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -72,9 +91,8 @@ export class ProjectService implements IService {
|
||||
|
||||
const config: ProjectConfig = {
|
||||
projectType: 'esengine',
|
||||
componentsPath: 'components',
|
||||
componentPattern: '**/*.ts',
|
||||
buildOutput: 'temp/editor-components',
|
||||
scriptsPath: 'scripts',
|
||||
buildOutput: '.esengine/compiled',
|
||||
scenesPath: 'scenes',
|
||||
defaultScene: 'main.ecs'
|
||||
};
|
||||
@@ -153,15 +171,34 @@ export class ProjectService implements IService {
|
||||
return this.currentProject !== null;
|
||||
}
|
||||
|
||||
public getComponentsPath(): string | null {
|
||||
/**
|
||||
* Get user scripts directory path.
|
||||
* 获取用户脚本目录路径。
|
||||
*
|
||||
* @returns Scripts directory path | 脚本目录路径
|
||||
*/
|
||||
public getScriptsPath(): string | null {
|
||||
if (!this.currentProject) {
|
||||
return null;
|
||||
}
|
||||
if (!this.projectConfig?.componentsPath) {
|
||||
return this.currentProject.path;
|
||||
}
|
||||
const scriptsPath = this.projectConfig?.scriptsPath || 'scripts';
|
||||
const sep = this.currentProject.path.includes('\\') ? '\\' : '/';
|
||||
return `${this.currentProject.path}${sep}${this.projectConfig.componentsPath}`;
|
||||
return `${this.currentProject.path}${sep}${scriptsPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get editor scripts directory path (scripts/editor).
|
||||
* 获取编辑器脚本目录路径(scripts/editor)。
|
||||
*
|
||||
* @returns Editor scripts directory path | 编辑器脚本目录路径
|
||||
*/
|
||||
public getEditorScriptsPath(): string | null {
|
||||
const scriptsPath = this.getScriptsPath();
|
||||
if (!scriptsPath) {
|
||||
return null;
|
||||
}
|
||||
const sep = scriptsPath.includes('\\') ? '\\' : '/';
|
||||
return `${scriptsPath}${sep}editor`;
|
||||
}
|
||||
|
||||
public getScenesPath(): string | null {
|
||||
@@ -214,15 +251,15 @@ export class ProjectService implements IService {
|
||||
logger.debug('Raw config content:', content);
|
||||
const config = JSON.parse(content) as ProjectConfig;
|
||||
logger.debug('Parsed config plugins:', config.plugins);
|
||||
const result = {
|
||||
const result: ProjectConfig = {
|
||||
projectType: config.projectType || 'esengine',
|
||||
componentsPath: config.componentsPath || '',
|
||||
componentPattern: config.componentPattern || '**/*.ts',
|
||||
buildOutput: config.buildOutput || 'temp/editor-components',
|
||||
scriptsPath: config.scriptsPath || 'scripts',
|
||||
buildOutput: config.buildOutput || '.esengine/compiled',
|
||||
scenesPath: config.scenesPath || 'scenes',
|
||||
defaultScene: config.defaultScene || 'main.ecs',
|
||||
uiDesignResolution: config.uiDesignResolution,
|
||||
plugins: config.plugins
|
||||
plugins: config.plugins,
|
||||
modules: config.modules
|
||||
};
|
||||
logger.debug('Loaded config result:', result);
|
||||
return result;
|
||||
@@ -230,9 +267,8 @@ export class ProjectService implements IService {
|
||||
logger.warn('Failed to load config, using defaults', error);
|
||||
return {
|
||||
projectType: 'esengine',
|
||||
componentsPath: '',
|
||||
componentPattern: '**/*.ts',
|
||||
buildOutput: 'temp/editor-components',
|
||||
scriptsPath: 'scripts',
|
||||
buildOutput: '.esengine/compiled',
|
||||
scenesPath: 'scenes',
|
||||
defaultScene: 'main.ecs'
|
||||
};
|
||||
@@ -350,6 +386,70 @@ export class ProjectService implements IService {
|
||||
await this.setEnabledPlugins(current.filter(id => id !== pluginId));
|
||||
}
|
||||
|
||||
// ==================== Module Settings ====================
|
||||
|
||||
/**
|
||||
* 获取禁用的模块列表(黑名单)
|
||||
* Get disabled modules list (blacklist)
|
||||
* @returns Array of disabled module IDs
|
||||
*/
|
||||
public getDisabledModules(): string[] {
|
||||
return this.projectConfig?.modules?.disabledModules || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块配置
|
||||
* Get module settings
|
||||
*/
|
||||
public getModuleSettings(): ModuleSettings | null {
|
||||
return this.projectConfig?.modules || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置禁用的模块列表
|
||||
* Set disabled modules list
|
||||
*
|
||||
* @param disabledModules - Array of disabled module IDs
|
||||
*/
|
||||
public async setDisabledModules(disabledModules: string[]): Promise<void> {
|
||||
await this.updateConfig({
|
||||
modules: {
|
||||
disabledModules
|
||||
}
|
||||
});
|
||||
await this.messageHub.publish('project:modulesChanged', { disabledModules });
|
||||
logger.info('Module settings saved', { disabledCount: disabledModules.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用模块
|
||||
* Disable a module
|
||||
*/
|
||||
public async disableModule(moduleId: string): Promise<void> {
|
||||
const current = this.getDisabledModules();
|
||||
if (!current.includes(moduleId)) {
|
||||
await this.setDisabledModules([...current, moduleId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用模块
|
||||
* Enable a module
|
||||
*/
|
||||
public async enableModule(moduleId: string): Promise<void> {
|
||||
const current = this.getDisabledModules();
|
||||
await this.setDisabledModules(current.filter(id => id !== moduleId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模块是否启用
|
||||
* Check if a module is enabled
|
||||
*/
|
||||
public isModuleEnabled(moduleId: string): boolean {
|
||||
const disabled = this.getDisabledModules();
|
||||
return !disabled.includes(moduleId);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.currentProject = null;
|
||||
this.projectConfig = null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
export type SettingType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'range' | 'pluginList' | 'collisionMatrix';
|
||||
export type SettingType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'range' | 'pluginList' | 'collisionMatrix' | 'moduleList';
|
||||
|
||||
export interface SettingOption {
|
||||
label: string;
|
||||
|
||||
273
packages/editor-core/src/Services/UserCode/IUserCodeService.ts
Normal file
273
packages/editor-core/src/Services/UserCode/IUserCodeService.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* User Code Service Interface.
|
||||
* 用户代码服务接口。
|
||||
*
|
||||
* Provides compilation and loading for user-written game logic code.
|
||||
* 提供用户编写的游戏逻辑代码的编译和加载功能。
|
||||
*
|
||||
* Directory convention:
|
||||
* 目录约定:
|
||||
* - scripts/ -> Runtime code (components, systems, etc.)
|
||||
* - scripts/editor/ -> Editor-only code (inspectors, gizmos, panels)
|
||||
*/
|
||||
|
||||
/**
|
||||
* User code target environment.
|
||||
* 用户代码目标环境。
|
||||
*/
|
||||
export enum UserCodeTarget {
|
||||
/** Runtime code - runs in game | 运行时代码 - 在游戏中运行 */
|
||||
Runtime = 'runtime',
|
||||
/** Editor code - runs only in editor | 编辑器代码 - 仅在编辑器中运行 */
|
||||
Editor = 'editor'
|
||||
}
|
||||
|
||||
/**
|
||||
* User script file information.
|
||||
* 用户脚本文件信息。
|
||||
*/
|
||||
export interface UserScriptInfo {
|
||||
/** Absolute file path | 文件绝对路径 */
|
||||
path: string;
|
||||
/** Relative path from scripts directory | 相对于 scripts 目录的路径 */
|
||||
relativePath: string;
|
||||
/** Target environment | 目标环境 */
|
||||
target: UserCodeTarget;
|
||||
/** Exported names (classes, functions) | 导出的名称(类、函数) */
|
||||
exports: string[];
|
||||
/** Last modified timestamp | 最后修改时间戳 */
|
||||
lastModified: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* User code compilation options.
|
||||
* 用户代码编译选项。
|
||||
*/
|
||||
export interface UserCodeCompileOptions {
|
||||
/** Project root directory | 项目根目录 */
|
||||
projectPath: string;
|
||||
/** Target environment | 目标环境 */
|
||||
target: UserCodeTarget;
|
||||
/** Output directory | 输出目录 */
|
||||
outputDir?: string;
|
||||
/** Whether to generate source maps | 是否生成 source map */
|
||||
sourceMap?: boolean;
|
||||
/** Whether to minify output | 是否压缩输出 */
|
||||
minify?: boolean;
|
||||
/** Output format | 输出格式 */
|
||||
format?: 'esm' | 'iife';
|
||||
}
|
||||
|
||||
/**
|
||||
* User code compilation result.
|
||||
* 用户代码编译结果。
|
||||
*/
|
||||
export interface UserCodeCompileResult {
|
||||
/** Whether compilation succeeded | 是否编译成功 */
|
||||
success: boolean;
|
||||
/** Output file path | 输出文件路径 */
|
||||
outputPath?: string;
|
||||
/** Compilation errors | 编译错误 */
|
||||
errors: CompileError[];
|
||||
/** Compilation warnings | 编译警告 */
|
||||
warnings: CompileError[];
|
||||
/** Compilation duration in ms | 编译耗时(毫秒) */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compilation error/warning.
|
||||
* 编译错误/警告。
|
||||
*/
|
||||
export interface CompileError {
|
||||
/** Error message | 错误信息 */
|
||||
message: string;
|
||||
/** Source file path | 源文件路径 */
|
||||
file?: string;
|
||||
/** Line number | 行号 */
|
||||
line?: number;
|
||||
/** Column number | 列号 */
|
||||
column?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loaded user code module.
|
||||
* 加载后的用户代码模块。
|
||||
*/
|
||||
export interface UserCodeModule {
|
||||
/** Module ID | 模块 ID */
|
||||
id: string;
|
||||
/** Target environment | 目标环境 */
|
||||
target: UserCodeTarget;
|
||||
/** All exported members | 所有导出的成员 */
|
||||
exports: Record<string, any>;
|
||||
/** Module version (hash of source) | 模块版本(源码哈希) */
|
||||
version: string;
|
||||
/** Load timestamp | 加载时间戳 */
|
||||
loadedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot reload event.
|
||||
* 热更新事件。
|
||||
*/
|
||||
export interface HotReloadEvent {
|
||||
/** Target environment | 目标环境 */
|
||||
target: UserCodeTarget;
|
||||
/** Changed source files | 变更的源文件 */
|
||||
changedFiles: string[];
|
||||
/** Previous module (if any) | 之前的模块(如果有) */
|
||||
previousModule?: UserCodeModule;
|
||||
/** New module | 新模块 */
|
||||
newModule: UserCodeModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* User Code Service interface.
|
||||
* 用户代码服务接口。
|
||||
*
|
||||
* Handles scanning, compilation, loading, and hot-reload of user scripts.
|
||||
* 处理用户脚本的扫描、编译、加载和热更新。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const userCodeService = services.resolve(UserCodeService);
|
||||
*
|
||||
* // Scan for user scripts | 扫描用户脚本
|
||||
* const scripts = await userCodeService.scan(projectPath);
|
||||
*
|
||||
* // Compile runtime code | 编译运行时代码
|
||||
* const result = await userCodeService.compile({
|
||||
* projectPath,
|
||||
* target: UserCodeTarget.Runtime
|
||||
* });
|
||||
*
|
||||
* // Load compiled module | 加载编译后的模块
|
||||
* if (result.success && result.outputPath) {
|
||||
* const module = await userCodeService.load(result.outputPath, UserCodeTarget.Runtime);
|
||||
* userCodeService.registerComponents(module);
|
||||
* }
|
||||
*
|
||||
* // Start hot reload | 启动热更新
|
||||
* await userCodeService.watch(projectPath, (event) => {
|
||||
* console.log('Code reloaded:', event.changedFiles);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export interface IUserCodeService {
|
||||
/**
|
||||
* Scan project for user scripts.
|
||||
* 扫描项目中的用户脚本。
|
||||
*
|
||||
* Looks for:
|
||||
* 查找:
|
||||
* - scripts/*.ts -> Runtime code | 运行时代码
|
||||
* - scripts/editor/*.tsx -> Editor code | 编辑器代码
|
||||
*
|
||||
* @param projectPath - Project root path | 项目根路径
|
||||
* @returns Discovered script files | 发现的脚本文件
|
||||
*/
|
||||
scan(projectPath: string): Promise<UserScriptInfo[]>;
|
||||
|
||||
/**
|
||||
* Compile user scripts.
|
||||
* 编译用户脚本。
|
||||
*
|
||||
* @param options - Compilation options | 编译选项
|
||||
* @returns Compilation result | 编译结果
|
||||
*/
|
||||
compile(options: UserCodeCompileOptions): Promise<UserCodeCompileResult>;
|
||||
|
||||
/**
|
||||
* Load compiled user code module.
|
||||
* 加载编译后的用户代码模块。
|
||||
*
|
||||
* @param modulePath - Path to compiled JS file | 编译后的 JS 文件路径
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Loaded module | 加载的模块
|
||||
*/
|
||||
load(modulePath: string, target: UserCodeTarget): Promise<UserCodeModule>;
|
||||
|
||||
/**
|
||||
* Unload user code module.
|
||||
* 卸载用户代码模块。
|
||||
*
|
||||
* @param target - Target environment to unload | 要卸载的目标环境
|
||||
*/
|
||||
unload(target: UserCodeTarget): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get currently loaded module.
|
||||
* 获取当前加载的模块。
|
||||
*
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Loaded module or undefined | 加载的模块或 undefined
|
||||
*/
|
||||
getModule(target: UserCodeTarget): UserCodeModule | undefined;
|
||||
|
||||
/**
|
||||
* Register runtime components/systems from user module.
|
||||
* 从用户模块注册运行时组件/系统。
|
||||
*
|
||||
* Automatically detects and registers:
|
||||
* 自动检测并注册:
|
||||
* - Classes extending Component
|
||||
* - Classes extending System
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
*/
|
||||
registerComponents(module: UserCodeModule): void;
|
||||
|
||||
/**
|
||||
* Register editor extensions from user module.
|
||||
* 从用户模块注册编辑器扩展。
|
||||
*
|
||||
* Automatically detects and registers:
|
||||
* 自动检测并注册:
|
||||
* - Component inspectors
|
||||
* - Gizmo providers
|
||||
* - Editor panels
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
*/
|
||||
registerEditorExtensions(module: UserCodeModule): void;
|
||||
|
||||
/**
|
||||
* Start watching for file changes (hot reload).
|
||||
* 开始监视文件变更(热更新)。
|
||||
*
|
||||
* @param projectPath - Project root path | 项目根路径
|
||||
* @param onReload - Callback when code is reloaded | 代码重新加载时的回调
|
||||
*/
|
||||
watch(projectPath: string, onReload: (event: HotReloadEvent) => void): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stop watching for file changes.
|
||||
* 停止监视文件变更。
|
||||
*/
|
||||
stopWatch(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if watching is active.
|
||||
* 检查是否正在监视。
|
||||
*/
|
||||
isWatching(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default scripts directory name.
|
||||
* 默认脚本目录名称。
|
||||
*/
|
||||
export const SCRIPTS_DIR = 'scripts';
|
||||
|
||||
/**
|
||||
* Editor scripts subdirectory name.
|
||||
* 编辑器脚本子目录名称。
|
||||
*/
|
||||
export const EDITOR_SCRIPTS_DIR = 'editor';
|
||||
|
||||
/**
|
||||
* Default output directory for compiled user code.
|
||||
* 编译后用户代码的默认输出目录。
|
||||
*/
|
||||
export const USER_CODE_OUTPUT_DIR = '.esengine/compiled';
|
||||
799
packages/editor-core/src/Services/UserCode/UserCodeService.ts
Normal file
799
packages/editor-core/src/Services/UserCode/UserCodeService.ts
Normal file
@@ -0,0 +1,799 @@
|
||||
/**
|
||||
* User Code Service Implementation.
|
||||
* 用户代码服务实现。
|
||||
*
|
||||
* Provides compilation, loading, and hot-reload for user-written scripts.
|
||||
* 提供用户脚本的编译、加载和热更新功能。
|
||||
*/
|
||||
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable, createLogger } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IUserCodeService,
|
||||
UserScriptInfo,
|
||||
UserCodeCompileOptions,
|
||||
UserCodeCompileResult,
|
||||
CompileError,
|
||||
UserCodeModule,
|
||||
HotReloadEvent
|
||||
} from './IUserCodeService';
|
||||
import {
|
||||
UserCodeTarget,
|
||||
SCRIPTS_DIR,
|
||||
EDITOR_SCRIPTS_DIR,
|
||||
USER_CODE_OUTPUT_DIR
|
||||
} from './IUserCodeService';
|
||||
import type { IFileSystem, FileEntry } from '../IFileSystem';
|
||||
|
||||
const logger = createLogger('UserCodeService');
|
||||
|
||||
/**
|
||||
* User Code Service.
|
||||
* 用户代码服务。
|
||||
*
|
||||
* Handles scanning, compilation, loading, and hot-reload of user scripts.
|
||||
* 处理用户脚本的扫描、编译、加载和热更新。
|
||||
*/
|
||||
@Injectable()
|
||||
export class UserCodeService implements IService, IUserCodeService {
|
||||
private _fileSystem: IFileSystem;
|
||||
private _runtimeModule: UserCodeModule | undefined;
|
||||
private _editorModule: UserCodeModule | undefined;
|
||||
private _watching = false;
|
||||
private _watchCallbacks: Array<(event: HotReloadEvent) => void> = [];
|
||||
private _currentProjectPath: string | undefined;
|
||||
private _eventUnlisten: (() => void) | undefined;
|
||||
|
||||
constructor(fileSystem: IFileSystem) {
|
||||
this._fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan project for user scripts.
|
||||
* 扫描项目中的用户脚本。
|
||||
*
|
||||
* @param projectPath - Project root path | 项目根路径
|
||||
* @returns Discovered script files | 发现的脚本文件
|
||||
*/
|
||||
async scan(projectPath: string): Promise<UserScriptInfo[]> {
|
||||
const scripts: UserScriptInfo[] = [];
|
||||
const sep = projectPath.includes('\\') ? '\\' : '/';
|
||||
const scriptsDir = `${projectPath}${sep}${SCRIPTS_DIR}`;
|
||||
|
||||
try {
|
||||
// Check if scripts directory exists | 检查脚本目录是否存在
|
||||
const exists = await this._fileSystem.exists(scriptsDir);
|
||||
if (!exists) {
|
||||
logger.debug('Scripts directory not found | 脚本目录不存在:', scriptsDir);
|
||||
return scripts;
|
||||
}
|
||||
|
||||
// Scan all TypeScript files | 扫描所有 TypeScript 文件
|
||||
const files = await this._scanDirectory(scriptsDir, '');
|
||||
|
||||
for (const file of files) {
|
||||
const isEditorScript = file.relativePath.startsWith(`${EDITOR_SCRIPTS_DIR}${sep}`) ||
|
||||
file.relativePath.startsWith(`${EDITOR_SCRIPTS_DIR}/`);
|
||||
|
||||
const scriptInfo: UserScriptInfo = {
|
||||
path: file.absolutePath,
|
||||
relativePath: file.relativePath,
|
||||
target: isEditorScript ? UserCodeTarget.Editor : UserCodeTarget.Runtime,
|
||||
exports: await this._extractExports(file.absolutePath),
|
||||
lastModified: file.lastModified
|
||||
};
|
||||
|
||||
scripts.push(scriptInfo);
|
||||
}
|
||||
|
||||
logger.info(`Scanned ${scripts.length} scripts | 扫描到 ${scripts.length} 个脚本`, {
|
||||
runtime: scripts.filter(s => s.target === UserCodeTarget.Runtime).length,
|
||||
editor: scripts.filter(s => s.target === UserCodeTarget.Editor).length
|
||||
});
|
||||
|
||||
return scripts;
|
||||
} catch (error) {
|
||||
logger.error('Failed to scan scripts | 扫描脚本失败:', error);
|
||||
return scripts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile user scripts.
|
||||
* 编译用户脚本。
|
||||
*
|
||||
* @param options - Compilation options | 编译选项
|
||||
* @returns Compilation result | 编译结果
|
||||
*/
|
||||
async compile(options: UserCodeCompileOptions): Promise<UserCodeCompileResult> {
|
||||
const startTime = Date.now();
|
||||
const errors: CompileError[] = [];
|
||||
const warnings: CompileError[] = [];
|
||||
|
||||
const sep = options.projectPath.includes('\\') ? '\\' : '/';
|
||||
const scriptsDir = `${options.projectPath}${sep}${SCRIPTS_DIR}`;
|
||||
const outputDir = options.outputDir || `${options.projectPath}${sep}${USER_CODE_OUTPUT_DIR}`;
|
||||
|
||||
try {
|
||||
// Scan scripts first | 先扫描脚本
|
||||
const allScripts = await this.scan(options.projectPath);
|
||||
const targetScripts = allScripts.filter(s => s.target === options.target);
|
||||
|
||||
if (targetScripts.length === 0) {
|
||||
logger.info(`No ${options.target} scripts to compile | 没有需要编译的 ${options.target} 脚本`);
|
||||
return {
|
||||
success: true,
|
||||
outputPath: undefined,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure output directory exists | 确保输出目录存在
|
||||
await this._ensureDirectory(outputDir);
|
||||
|
||||
// Determine output file name | 确定输出文件名
|
||||
const outputFileName = options.target === UserCodeTarget.Runtime
|
||||
? 'user-runtime.js'
|
||||
: 'user-editor.js';
|
||||
const outputPath = `${outputDir}${sep}${outputFileName}`;
|
||||
|
||||
// Build entry point content | 构建入口点内容
|
||||
const entryContent = this._buildEntryPoint(targetScripts, scriptsDir, options.target);
|
||||
|
||||
// Create temporary entry file | 创建临时入口文件
|
||||
const entryPath = `${outputDir}${sep}_entry_${options.target}.ts`;
|
||||
await this._fileSystem.writeFile(entryPath, entryContent);
|
||||
|
||||
// Compile using esbuild (via Tauri command or direct) | 使用 esbuild 编译
|
||||
const compileResult = await this._runEsbuild({
|
||||
entryPath,
|
||||
outputPath,
|
||||
format: options.format || 'esm',
|
||||
sourceMap: options.sourceMap ?? true,
|
||||
minify: options.minify ?? false,
|
||||
external: this._getExternalDependencies(options.target),
|
||||
projectRoot: options.projectPath
|
||||
});
|
||||
|
||||
if (!compileResult.success) {
|
||||
errors.push(...compileResult.errors);
|
||||
}
|
||||
|
||||
// Clean up temporary entry file | 清理临时入口文件
|
||||
try {
|
||||
await this._fileSystem.deleteFile(entryPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors | 忽略清理错误
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info(`Compilation ${compileResult.success ? 'succeeded' : 'failed'} | 编译${compileResult.success ? '成功' : '失败'}`, {
|
||||
target: options.target,
|
||||
duration: `${duration}ms`,
|
||||
files: targetScripts.length
|
||||
});
|
||||
|
||||
return {
|
||||
success: compileResult.success,
|
||||
outputPath: compileResult.success ? outputPath : undefined,
|
||||
errors,
|
||||
warnings,
|
||||
duration
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
errors.push({ message: errorMessage });
|
||||
|
||||
logger.error('Compilation failed | 编译失败:', error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errors,
|
||||
warnings,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load compiled user code module.
|
||||
* 加载编译后的用户代码模块。
|
||||
*
|
||||
* @param modulePath - Path to compiled JS file | 编译后的 JS 文件路径
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Loaded module | 加载的模块
|
||||
*/
|
||||
async load(modulePath: string, target: UserCodeTarget): Promise<UserCodeModule> {
|
||||
try {
|
||||
// Add cache-busting query parameter for hot reload | 添加缓存破坏参数用于热更新
|
||||
const cacheBuster = `?t=${Date.now()}`;
|
||||
const moduleUrl = `file://${modulePath}${cacheBuster}`;
|
||||
|
||||
// Dynamic import the module | 动态导入模块
|
||||
const moduleExports = await import(/* @vite-ignore */ moduleUrl);
|
||||
|
||||
const module: UserCodeModule = {
|
||||
id: `user-${target}-${Date.now()}`,
|
||||
target,
|
||||
exports: { ...moduleExports },
|
||||
version: String(Date.now()),
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
|
||||
// Store reference | 存储引用
|
||||
if (target === UserCodeTarget.Runtime) {
|
||||
this._runtimeModule = module;
|
||||
} else {
|
||||
this._editorModule = module;
|
||||
}
|
||||
|
||||
logger.info(`Module loaded | 模块已加载`, {
|
||||
target,
|
||||
exports: Object.keys(module.exports).length
|
||||
});
|
||||
|
||||
return module;
|
||||
} catch (error) {
|
||||
logger.error('Failed to load module | 加载模块失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload user code module.
|
||||
* 卸载用户代码模块。
|
||||
*
|
||||
* @param target - Target environment to unload | 要卸载的目标环境
|
||||
*/
|
||||
async unload(target: UserCodeTarget): Promise<void> {
|
||||
if (target === UserCodeTarget.Runtime) {
|
||||
this._runtimeModule = undefined;
|
||||
} else {
|
||||
this._editorModule = undefined;
|
||||
}
|
||||
logger.info(`Module unloaded | 模块已卸载`, { target });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently loaded module.
|
||||
* 获取当前加载的模块。
|
||||
*
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Loaded module or undefined | 加载的模块或 undefined
|
||||
*/
|
||||
getModule(target: UserCodeTarget): UserCodeModule | undefined {
|
||||
return target === UserCodeTarget.Runtime ? this._runtimeModule : this._editorModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register runtime components/systems from user module.
|
||||
* 从用户模块注册运行时组件/系统。
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
*/
|
||||
registerComponents(module: UserCodeModule): void {
|
||||
if (module.target !== UserCodeTarget.Runtime) {
|
||||
logger.warn('Cannot register components from editor module | 无法从编辑器模块注册组件');
|
||||
return;
|
||||
}
|
||||
|
||||
let componentCount = 0;
|
||||
let systemCount = 0;
|
||||
|
||||
for (const [name, exported] of Object.entries(module.exports)) {
|
||||
if (typeof exported !== 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a Component subclass | 检查是否是 Component 子类
|
||||
if (this._isComponentClass(exported)) {
|
||||
// Register with ComponentRegistry | 注册到 ComponentRegistry
|
||||
// Note: Actual registration depends on runtime context
|
||||
// 注意:实际注册取决于运行时上下文
|
||||
logger.debug(`Found component: ${name} | 发现组件: ${name}`);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Check if it's a System subclass | 检查是否是 System 子类
|
||||
if (this._isSystemClass(exported)) {
|
||||
logger.debug(`Found system: ${name} | 发现系统: ${name}`);
|
||||
systemCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Registered user code | 注册用户代码`, {
|
||||
components: componentCount,
|
||||
systems: systemCount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register editor extensions from user module.
|
||||
* 从用户模块注册编辑器扩展。
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
*/
|
||||
registerEditorExtensions(module: UserCodeModule): void {
|
||||
if (module.target !== UserCodeTarget.Editor) {
|
||||
logger.warn('Cannot register editor extensions from runtime module | 无法从运行时模块注册编辑器扩展');
|
||||
return;
|
||||
}
|
||||
|
||||
let inspectorCount = 0;
|
||||
let gizmoCount = 0;
|
||||
let panelCount = 0;
|
||||
|
||||
for (const [name, exported] of Object.entries(module.exports)) {
|
||||
if (typeof exported !== 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for inspector | 检查检查器
|
||||
if (this._isInspectorClass(exported)) {
|
||||
logger.debug(`Found inspector: ${name} | 发现检查器: ${name}`);
|
||||
inspectorCount++;
|
||||
}
|
||||
|
||||
// Check for gizmo | 检查 Gizmo
|
||||
if (this._isGizmoClass(exported)) {
|
||||
logger.debug(`Found gizmo: ${name} | 发现 Gizmo: ${name}`);
|
||||
gizmoCount++;
|
||||
}
|
||||
|
||||
// Check for panel | 检查面板
|
||||
if (this._isPanelComponent(exported)) {
|
||||
logger.debug(`Found panel: ${name} | 发现面板: ${name}`);
|
||||
panelCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Registered editor extensions | 注册编辑器扩展`, {
|
||||
inspectors: inspectorCount,
|
||||
gizmos: gizmoCount,
|
||||
panels: panelCount
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start watching for file changes (hot reload).
|
||||
* 开始监视文件变更(热更新)。
|
||||
*
|
||||
* @param projectPath - Project root path | 项目根路径
|
||||
* @param onReload - Callback when code is reloaded | 代码重新加载时的回调
|
||||
*/
|
||||
async watch(projectPath: string, onReload: (event: HotReloadEvent) => void): Promise<void> {
|
||||
if (this._watching) {
|
||||
this._watchCallbacks.push(onReload);
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentProjectPath = projectPath;
|
||||
|
||||
try {
|
||||
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
|
||||
// Start backend file watcher | 启动后端文件监视器
|
||||
await invoke('watch_scripts', {
|
||||
projectPath,
|
||||
scriptsDir: SCRIPTS_DIR
|
||||
});
|
||||
|
||||
// Listen for file change events | 监听文件变更事件
|
||||
this._eventUnlisten = await listen<{
|
||||
changeType: string;
|
||||
paths: string[];
|
||||
}>('user-code:file-changed', async (event) => {
|
||||
const { changeType, paths } = event.payload;
|
||||
|
||||
logger.info('File change detected | 检测到文件变更', { changeType, paths });
|
||||
|
||||
// Determine which targets are affected | 确定受影响的目标
|
||||
const isEditorChange = paths.some(p =>
|
||||
p.includes(`${EDITOR_SCRIPTS_DIR}/`) || p.includes(`${EDITOR_SCRIPTS_DIR}\\`)
|
||||
);
|
||||
const target = isEditorChange ? UserCodeTarget.Editor : UserCodeTarget.Runtime;
|
||||
|
||||
// Get previous module | 获取之前的模块
|
||||
const previousModule = this.getModule(target);
|
||||
|
||||
// Recompile the affected target | 重新编译受影响的目标
|
||||
const compileResult = await this.compile({
|
||||
projectPath,
|
||||
target
|
||||
});
|
||||
|
||||
if (compileResult.success && compileResult.outputPath) {
|
||||
// Reload the module | 重新加载模块
|
||||
const newModule = await this.load(compileResult.outputPath, target);
|
||||
|
||||
// Create hot reload event | 创建热更新事件
|
||||
const hotReloadEvent: HotReloadEvent = {
|
||||
target,
|
||||
changedFiles: paths,
|
||||
previousModule,
|
||||
newModule
|
||||
};
|
||||
|
||||
this._notifyHotReload(hotReloadEvent);
|
||||
} else {
|
||||
logger.error('Hot reload compilation failed | 热更新编译失败', compileResult.errors);
|
||||
}
|
||||
});
|
||||
|
||||
this._watching = true;
|
||||
this._watchCallbacks.push(onReload);
|
||||
|
||||
logger.info('Started watching for changes | 开始监视文件变更', {
|
||||
path: `${projectPath}/${SCRIPTS_DIR}`
|
||||
});
|
||||
} else {
|
||||
// Not in Tauri - just register callback for manual triggers
|
||||
// 不在 Tauri 环境 - 只注册回调用于手动触发
|
||||
logger.warn('File watching not available outside Tauri | 文件监视在 Tauri 外不可用');
|
||||
this._watching = true;
|
||||
this._watchCallbacks.push(onReload);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to start watching | 启动监视失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watching for file changes.
|
||||
* 停止监视文件变更。
|
||||
*/
|
||||
async stopWatch(): Promise<void> {
|
||||
if (!this._watching) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Unsubscribe from Tauri events | 取消订阅 Tauri 事件
|
||||
if (this._eventUnlisten) {
|
||||
this._eventUnlisten();
|
||||
this._eventUnlisten = undefined;
|
||||
}
|
||||
|
||||
// Stop backend file watcher | 停止后端文件监视器
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
await invoke('stop_watch_scripts', {
|
||||
projectPath: this._currentProjectPath
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Error stopping file watcher | 停止文件监视器时出错:', error);
|
||||
}
|
||||
|
||||
this._watching = false;
|
||||
this._watchCallbacks = [];
|
||||
this._currentProjectPath = undefined;
|
||||
|
||||
logger.info('Stopped watching for changes | 停止监视文件变更');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if watching is active.
|
||||
* 检查是否正在监视。
|
||||
*/
|
||||
isWatching(): boolean {
|
||||
return this._watching;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose service resources.
|
||||
* 释放服务资源。
|
||||
*/
|
||||
dispose(): void {
|
||||
this.stopWatch();
|
||||
this._runtimeModule = undefined;
|
||||
this._editorModule = undefined;
|
||||
}
|
||||
|
||||
// ==================== Private Methods | 私有方法 ====================
|
||||
|
||||
/**
|
||||
* Scan directory recursively for TypeScript files.
|
||||
* 递归扫描目录中的 TypeScript 文件。
|
||||
*/
|
||||
private async _scanDirectory(
|
||||
baseDir: string,
|
||||
relativePath: string
|
||||
): Promise<Array<{ absolutePath: string; relativePath: string; lastModified: number }>> {
|
||||
const results: Array<{ absolutePath: string; relativePath: string; lastModified: number }> = [];
|
||||
const sep = baseDir.includes('\\') ? '\\' : '/';
|
||||
const currentDir = relativePath ? `${baseDir}${sep}${relativePath}` : baseDir;
|
||||
|
||||
try {
|
||||
const entries: FileEntry[] = await this._fileSystem.listDirectory(currentDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryRelativePath = relativePath ? `${relativePath}${sep}${entry.name}` : entry.name;
|
||||
const entryAbsolutePath = `${baseDir}${sep}${entryRelativePath}`;
|
||||
|
||||
if (entry.isDirectory) {
|
||||
// Recursively scan subdirectories | 递归扫描子目录
|
||||
const subResults = await this._scanDirectory(baseDir, entryRelativePath);
|
||||
results.push(...subResults);
|
||||
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
|
||||
results.push({
|
||||
absolutePath: entryAbsolutePath,
|
||||
relativePath: entryRelativePath,
|
||||
lastModified: entry.modified?.getTime() || Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to scan directory | 扫描目录失败: ${currentDir}`, error);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract exported names from a TypeScript file.
|
||||
* 从 TypeScript 文件中提取导出的名称。
|
||||
*/
|
||||
private async _extractExports(filePath: string): Promise<string[]> {
|
||||
try {
|
||||
const content = await this._fileSystem.readFile(filePath);
|
||||
const exports: string[] = [];
|
||||
|
||||
// Simple regex-based extraction | 简单的正则表达式提取
|
||||
// Match: export class ClassName, export function funcName, export const varName
|
||||
const exportClassRegex = /export\s+class\s+(\w+)/g;
|
||||
const exportFunctionRegex = /export\s+function\s+(\w+)/g;
|
||||
const exportConstRegex = /export\s+const\s+(\w+)/g;
|
||||
const exportDefaultRegex = /export\s+default\s+(?:class|function)?\s*(\w+)?/g;
|
||||
|
||||
let match;
|
||||
while ((match = exportClassRegex.exec(content)) !== null) {
|
||||
exports.push(match[1]);
|
||||
}
|
||||
while ((match = exportFunctionRegex.exec(content)) !== null) {
|
||||
exports.push(match[1]);
|
||||
}
|
||||
while ((match = exportConstRegex.exec(content)) !== null) {
|
||||
exports.push(match[1]);
|
||||
}
|
||||
while ((match = exportDefaultRegex.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
exports.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return exports;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build entry point content that re-exports all user scripts.
|
||||
* 构建重新导出所有用户脚本的入口点内容。
|
||||
*/
|
||||
private _buildEntryPoint(
|
||||
scripts: UserScriptInfo[],
|
||||
scriptsDir: string,
|
||||
target: UserCodeTarget
|
||||
): string {
|
||||
const lines: string[] = [
|
||||
'// Auto-generated entry point for user scripts',
|
||||
'// 自动生成的用户脚本入口点',
|
||||
''
|
||||
];
|
||||
|
||||
for (const script of scripts) {
|
||||
// Convert absolute path to relative import | 将绝对路径转换为相对导入
|
||||
const relativePath = script.relativePath.replace(/\\/g, '/').replace(/\.tsx?$/, '');
|
||||
|
||||
if (script.exports.length > 0) {
|
||||
lines.push(`export { ${script.exports.join(', ')} } from './${SCRIPTS_DIR}/${relativePath}';`);
|
||||
} else {
|
||||
// Re-export everything if we couldn't detect specific exports
|
||||
// 如果无法检测到具体导出,则重新导出所有内容
|
||||
lines.push(`export * from './${SCRIPTS_DIR}/${relativePath}';`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external dependencies that should not be bundled.
|
||||
* 获取不应打包的外部依赖。
|
||||
*/
|
||||
private _getExternalDependencies(target: UserCodeTarget): string[] {
|
||||
const common = [
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/engine-core',
|
||||
'@esengine/core',
|
||||
'@esengine/math'
|
||||
];
|
||||
|
||||
if (target === UserCodeTarget.Editor) {
|
||||
return [
|
||||
...common,
|
||||
'@esengine/editor-core',
|
||||
'react',
|
||||
'react-dom'
|
||||
];
|
||||
}
|
||||
|
||||
return common;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run esbuild to compile TypeScript.
|
||||
* 运行 esbuild 编译 TypeScript。
|
||||
*
|
||||
* Uses Tauri command to invoke esbuild CLI.
|
||||
* 使用 Tauri 命令调用 esbuild CLI。
|
||||
*/
|
||||
private async _runEsbuild(options: {
|
||||
entryPath: string;
|
||||
outputPath: string;
|
||||
format: 'esm' | 'iife';
|
||||
sourceMap: boolean;
|
||||
minify: boolean;
|
||||
external: string[];
|
||||
projectRoot: string;
|
||||
}): Promise<{ success: boolean; errors: CompileError[] }> {
|
||||
try {
|
||||
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
// Use Tauri command | 使用 Tauri 命令
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
|
||||
const result = await invoke<{
|
||||
success: boolean;
|
||||
errors: Array<{
|
||||
message: string;
|
||||
file?: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
}>;
|
||||
outputPath?: string;
|
||||
}>('compile_typescript', {
|
||||
options: {
|
||||
entryPath: options.entryPath,
|
||||
outputPath: options.outputPath,
|
||||
format: options.format,
|
||||
sourceMap: options.sourceMap,
|
||||
minify: options.minify,
|
||||
external: options.external,
|
||||
projectRoot: options.projectRoot
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
errors: result.errors.map(e => ({
|
||||
message: e.message,
|
||||
file: e.file,
|
||||
line: e.line,
|
||||
column: e.column
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
// Not in Tauri environment, return mock success for development
|
||||
// 不在 Tauri 环境,返回模拟成功用于开发
|
||||
logger.warn('Not in Tauri environment, skipping compilation | 不在 Tauri 环境,跳过编译');
|
||||
return {
|
||||
success: true,
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error('esbuild compilation failed | esbuild 编译失败:', error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errors: [{ message: errorMessage }]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists, create if not.
|
||||
* 确保目录存在,如果不存在则创建。
|
||||
*/
|
||||
private async _ensureDirectory(dirPath: string): Promise<void> {
|
||||
try {
|
||||
const exists = await this._fileSystem.exists(dirPath);
|
||||
if (!exists) {
|
||||
await this._fileSystem.createDirectory(dirPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to ensure directory | 确保目录失败:', dirPath, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class extends Component.
|
||||
* 检查类是否继承自 Component。
|
||||
*/
|
||||
private _isComponentClass(cls: any): boolean {
|
||||
// Check prototype chain for Component | 检查原型链中是否有 Component
|
||||
let proto = cls.prototype;
|
||||
while (proto) {
|
||||
if (proto.constructor?.name === 'Component') {
|
||||
return true;
|
||||
}
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class extends System.
|
||||
* 检查类是否继承自 System。
|
||||
*/
|
||||
private _isSystemClass(cls: any): boolean {
|
||||
let proto = cls.prototype;
|
||||
while (proto) {
|
||||
const name = proto.constructor?.name;
|
||||
if (name === 'System' || name === 'EntityProcessingSystem') {
|
||||
return true;
|
||||
}
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class implements IComponentInspector.
|
||||
* 检查类是否实现了 IComponentInspector。
|
||||
*/
|
||||
private _isInspectorClass(cls: any): boolean {
|
||||
// Check for inspector interface markers | 检查检查器接口标记
|
||||
const instance = cls.prototype;
|
||||
return instance &&
|
||||
typeof instance.canHandle === 'function' &&
|
||||
typeof instance.render === 'function' &&
|
||||
'targetComponents' in instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class implements IGizmoProvider.
|
||||
* 检查类是否实现了 IGizmoProvider。
|
||||
*/
|
||||
private _isGizmoClass(cls: any): boolean {
|
||||
const instance = cls.prototype;
|
||||
return instance &&
|
||||
typeof instance.draw === 'function' &&
|
||||
'targetComponent' in instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a React panel component.
|
||||
* 检查值是否是 React 面板组件。
|
||||
*/
|
||||
private _isPanelComponent(cls: any): boolean {
|
||||
// Check for panel descriptor | 检查面板描述符
|
||||
return cls.panelDescriptor !== undefined ||
|
||||
cls.displayName?.includes('Panel') ||
|
||||
cls.name?.includes('Panel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify callbacks of hot reload event.
|
||||
* 通知回调热更新事件。
|
||||
*/
|
||||
private _notifyHotReload(event: HotReloadEvent): void {
|
||||
for (const callback of this._watchCallbacks) {
|
||||
try {
|
||||
callback(event);
|
||||
} catch (error) {
|
||||
logger.error('Hot reload callback error | 热更新回调错误:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
packages/editor-core/src/Services/UserCode/index.ts
Normal file
115
packages/editor-core/src/Services/UserCode/index.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* User Code System.
|
||||
* 用户代码系统。
|
||||
*
|
||||
* Provides compilation, loading, and hot-reload for user-written scripts.
|
||||
* 提供用户脚本的编译、加载和热更新功能。
|
||||
*
|
||||
* # Directory Convention | 目录约定
|
||||
*
|
||||
* ```
|
||||
* my-game/
|
||||
* ├── scripts/ # User scripts | 用户脚本
|
||||
* │ ├── Player.ts # Runtime code | 运行时代码
|
||||
* │ ├── Enemy.ts # Runtime code | 运行时代码
|
||||
* │ ├── systems/ # Can organize freely | 可自由组织
|
||||
* │ │ └── MovementSystem.ts
|
||||
* │ └── editor/ # Editor-only code | 仅编辑器代码
|
||||
* │ ├── PlayerInspector.tsx
|
||||
* │ └── EnemyGizmo.tsx
|
||||
* ├── scenes/
|
||||
* ├── assets/
|
||||
* └── esengine.config.json
|
||||
* ```
|
||||
*
|
||||
* # Rules | 规则
|
||||
*
|
||||
* 1. All `.ts` files in `scripts/` (except `scripts/editor/`) are Runtime code
|
||||
* `scripts/` 下所有 `.ts` 文件(除了 `scripts/editor/`)是运行时代码
|
||||
*
|
||||
* 2. All files in `scripts/editor/` are Editor-only code
|
||||
* `scripts/editor/` 下所有文件是编辑器专用代码
|
||||
*
|
||||
* 3. Editor code can import Runtime code, but not vice versa
|
||||
* 编辑器代码可以导入运行时代码,但反过来不行
|
||||
*
|
||||
* 4. Editor code is tree-shaken from production builds
|
||||
* 编辑器代码会从生产构建中移除
|
||||
*
|
||||
* # Workflow | 工作流程
|
||||
*
|
||||
* ```
|
||||
* [User writes .ts files]
|
||||
* ↓
|
||||
* [UserCodeService.scan()] - Discovers all scripts
|
||||
* ↓
|
||||
* [UserCodeService.compile()] - Compiles to JS using esbuild
|
||||
* ↓
|
||||
* [UserCodeService.load()] - Loads compiled module
|
||||
* ↓
|
||||
* [registerComponents()] - Registers with ECS runtime
|
||||
* [registerEditorExtensions()] - Registers inspectors/gizmos
|
||||
* ↓
|
||||
* [UserCodeService.watch()] - Hot reload on file changes
|
||||
* ```
|
||||
*
|
||||
* # Example User Component | 用户组件示例
|
||||
*
|
||||
* ```typescript
|
||||
* // scripts/Player.ts
|
||||
* import { Component, Serialize, Property } from '@esengine/ecs-framework';
|
||||
*
|
||||
* export class PlayerComponent extends Component {
|
||||
* @Serialize()
|
||||
* @Property({ label: 'Speed' })
|
||||
* speed: number = 5;
|
||||
*
|
||||
* @Serialize()
|
||||
* @Property({ label: 'Health' })
|
||||
* health: number = 100;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* # Example User Inspector | 用户检查器示例
|
||||
*
|
||||
* ```typescript
|
||||
* // scripts/editor/PlayerInspector.tsx
|
||||
* import React from 'react';
|
||||
* import { IComponentInspector } from '@esengine/editor-core';
|
||||
* import { PlayerComponent } from '../Player';
|
||||
*
|
||||
* export class PlayerInspector implements IComponentInspector<PlayerComponent> {
|
||||
* id = 'player-inspector';
|
||||
* name = 'Player Inspector';
|
||||
* targetComponents = ['PlayerComponent'];
|
||||
* renderMode = 'append' as const;
|
||||
*
|
||||
* canHandle(component: any): component is PlayerComponent {
|
||||
* return component instanceof PlayerComponent;
|
||||
* }
|
||||
*
|
||||
* render(context) {
|
||||
* return <div>Custom player UI here</div>;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
export type {
|
||||
IUserCodeService,
|
||||
UserScriptInfo,
|
||||
UserCodeCompileOptions,
|
||||
UserCodeCompileResult,
|
||||
CompileError,
|
||||
UserCodeModule,
|
||||
HotReloadEvent
|
||||
} from './IUserCodeService';
|
||||
|
||||
export {
|
||||
UserCodeTarget,
|
||||
SCRIPTS_DIR,
|
||||
EDITOR_SCRIPTS_DIR,
|
||||
USER_CODE_OUTPUT_DIR
|
||||
} from './IUserCodeService';
|
||||
|
||||
export { UserCodeService } from './UserCodeService';
|
||||
@@ -40,8 +40,21 @@ export * from './Services/FieldEditorRegistry';
|
||||
export * from './Services/ComponentInspectorRegistry';
|
||||
export * from './Services/ComponentActionRegistry';
|
||||
export * from './Services/AssetRegistryService';
|
||||
export * from './Services/IViewportService';
|
||||
export * from './Services/PreviewSceneService';
|
||||
export * from './Services/EditorViewportService';
|
||||
|
||||
// Build System | 构建系统
|
||||
export * from './Services/Build';
|
||||
|
||||
// User Code System | 用户代码系统
|
||||
export * from './Services/UserCode';
|
||||
|
||||
// Module System | 模块系统
|
||||
export * from './Services/Module';
|
||||
|
||||
export * from './Gizmos';
|
||||
export * from './Rendering';
|
||||
|
||||
export * from './Module/IEventBus';
|
||||
export * from './Module/ICommandRegistry';
|
||||
|
||||
Reference in New Issue
Block a user