Feature/editor optimization (#251)

* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
YHH
2025-12-01 22:28:51 +08:00
committed by GitHub
parent 189714c727
commit b42a7b4e43
468 changed files with 18301 additions and 9075 deletions

View File

@@ -0,0 +1,824 @@
/**
* Unified Game Runtime
* 统一游戏运行时
*
* 这是编辑器预览和独立运行的统一入口点
* This is the unified entry point for editor preview and standalone runtime
*/
import { Core, Scene, SceneSerializer, HierarchySystem } from '@esengine/ecs-framework';
import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen';
import { TransformComponent, TransformSystem } from '@esengine/engine-core';
import { AssetManager, EngineIntegration } from '@esengine/asset-system';
import {
runtimePluginManager,
type SystemContext,
type IRuntimeModule
} from './PluginManager';
import {
loadEnabledPlugins,
type PluginPackageInfo,
type ProjectPluginConfig
} from './PluginLoader';
import {
BUILTIN_PLUGIN_PACKAGES,
mergeProjectConfig,
type ProjectConfig
} from './ProjectConfig';
import type { IPlatformAdapter, PlatformAdapterConfig } from './IPlatformAdapter';
/**
* 运行时配置
* Runtime configuration
*/
export interface GameRuntimeConfig {
/** 平台适配器 */
platform: IPlatformAdapter;
/** 项目配置 */
projectConfig?: Partial<ProjectConfig>;
/** Canvas ID */
canvasId: string;
/** 初始宽度 */
width?: number;
/** 初始高度 */
height?: number;
/** 是否自动启动渲染循环 */
autoStartRenderLoop?: boolean;
/** UI 画布尺寸 */
uiCanvasSize?: { width: number; height: number };
/**
* 跳过内部插件加载
* 编辑器模式下,插件由 editor-core 的 PluginManager 管理
* Skip internal plugin loading - editor mode uses editor-core's PluginManager
*/
skipPluginLoading?: boolean;
}
/**
* 运行时状态
* Runtime state
*/
export interface RuntimeState {
initialized: boolean;
running: boolean;
paused: boolean;
}
/**
* 统一游戏运行时
* Unified Game Runtime
*
* 提供编辑器预览和独立运行的统一实现
* Provides unified implementation for editor preview and standalone runtime
*/
export class GameRuntime {
private _platform: IPlatformAdapter;
private _bridge: EngineBridge | null = null;
private _scene: Scene | null = null;
private _renderSystem: EngineRenderSystem | null = null;
private _cameraSystem: CameraSystem | null = null;
private _assetManager: AssetManager | null = null;
private _engineIntegration: EngineIntegration | null = null;
private _projectConfig: ProjectConfig;
private _config: GameRuntimeConfig;
private _state: RuntimeState = {
initialized: false,
running: false,
paused: false
};
private _animationFrameId: number | null = null;
private _lastTime = 0;
// 系统上下文,供插件使用
private _systemContext: SystemContext | null = null;
// 场景快照(用于编辑器预览后恢复)
private _sceneSnapshot: string | null = null;
// Gizmo 注册表注入函数
private _gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any;
private _hasGizmoProvider?: (component: any) => boolean;
constructor(config: GameRuntimeConfig) {
this._config = config;
this._platform = config.platform;
this._projectConfig = mergeProjectConfig(config.projectConfig || {});
}
/**
* 获取运行时状态
*/
get state(): RuntimeState {
return { ...this._state };
}
/**
* 获取场景
*/
get scene(): Scene | null {
return this._scene;
}
/**
* 获取引擎桥接
*/
get bridge(): EngineBridge | null {
return this._bridge;
}
/**
* 获取渲染系统
*/
get renderSystem(): EngineRenderSystem | null {
return this._renderSystem;
}
/**
* 获取资产管理器
*/
get assetManager(): AssetManager | null {
return this._assetManager;
}
/**
* 获取引擎集成
*/
get engineIntegration(): EngineIntegration | null {
return this._engineIntegration;
}
/**
* 获取系统上下文
*/
get systemContext(): SystemContext | null {
return this._systemContext;
}
/**
* 更新系统上下文(用于编辑器模式下同步外部创建的系统引用)
* Update system context (for syncing externally created system references in editor mode)
*/
updateSystemContext(updates: Partial<SystemContext>): void {
if (this._systemContext) {
Object.assign(this._systemContext, updates);
}
}
/**
* 获取平台适配器
*/
get platform(): IPlatformAdapter {
return this._platform;
}
/**
* 初始化运行时
* Initialize runtime
*/
async initialize(): Promise<void> {
if (this._state.initialized) {
return;
}
try {
// 1. 初始化平台
await this._platform.initialize({
canvasId: this._config.canvasId,
width: this._config.width,
height: this._config.height,
isEditor: this._platform.isEditorMode()
});
// 2. 获取 WASM 模块并创建引擎桥接
const wasmModule = await this._platform.getWasmModule();
this._bridge = new EngineBridge({
canvasId: this._config.canvasId,
width: this._config.width,
height: this._config.height
});
await this._bridge.initializeWithModule(wasmModule);
// 3. 设置路径解析器
this._bridge.setPathResolver((path: string) => {
return this._platform.pathResolver.resolve(path);
});
// 4. 初始化 ECS Core
if (!Core.Instance) {
Core.create({ debug: false });
}
// 5. 创建或获取场景
if (Core.scene) {
this._scene = Core.scene as Scene;
} else {
this._scene = new Scene({ name: 'GameScene' });
Core.setScene(this._scene);
}
// 6. 添加基础系统
this._scene.addSystem(new HierarchySystem());
this._scene.addSystem(new TransformSystem());
this._cameraSystem = new CameraSystem(this._bridge);
this._scene.addSystem(this._cameraSystem);
this._renderSystem = new EngineRenderSystem(this._bridge, TransformComponent);
// 7. 设置 UI 画布尺寸
if (this._config.uiCanvasSize) {
this._renderSystem.setUICanvasSize(
this._config.uiCanvasSize.width,
this._config.uiCanvasSize.height
);
} else {
this._renderSystem.setUICanvasSize(1920, 1080);
}
// 8. 创建资产系统
this._assetManager = new AssetManager();
this._engineIntegration = new EngineIntegration(this._assetManager, this._bridge);
// 9. 加载并初始化插件(编辑器模式下跳过,由 editor-core 的 PluginManager 处理)
if (!this._config.skipPluginLoading) {
await this._initializePlugins();
}
// 10. 创建系统上下文
this._systemContext = {
isEditor: this._platform.isEditorMode(),
engineBridge: this._bridge,
renderSystem: this._renderSystem,
assetManager: this._assetManager
};
// 11. 让插件创建系统(编辑器模式下跳过,由 EngineService.initializeModuleSystems 处理)
if (!this._config.skipPluginLoading) {
runtimePluginManager.createSystemsForScene(this._scene, this._systemContext);
}
// 11. 设置 UI 渲染数据提供者(如果有)
if (this._systemContext.uiRenderProvider) {
this._renderSystem.setUIRenderDataProvider(this._systemContext.uiRenderProvider);
}
// 12. 添加渲染系统(在所有其他系统之后)
this._scene.addSystem(this._renderSystem);
// 13. 启动默认 world
const defaultWorld = Core.worldManager.getWorld('__default__');
if (defaultWorld && !defaultWorld.isActive) {
defaultWorld.start();
}
// 14. 编辑器模式下的特殊处理
if (this._platform.isEditorMode()) {
// 禁用游戏逻辑系统
this._disableGameLogicSystems();
}
this._state.initialized = true;
// 15. 自动启动渲染循环
if (this._config.autoStartRenderLoop !== false) {
this._startRenderLoop();
}
} catch (error) {
console.error('[GameRuntime] Initialization failed:', error);
throw error;
}
}
/**
* 加载并初始化插件
*/
private async _initializePlugins(): Promise<void> {
// 检查是否已有插件注册(静态导入场景)
// Check if plugins are already registered (static import scenario)
const hasPlugins = runtimePluginManager.getPlugins().length > 0;
if (!hasPlugins) {
// 没有预注册的插件,尝试动态加载
// No pre-registered plugins, try dynamic loading
await loadEnabledPlugins(
{ plugins: this._projectConfig.plugins },
BUILTIN_PLUGIN_PACKAGES
);
}
// 初始化插件(注册组件和服务)
await runtimePluginManager.initializeRuntime(Core.services);
}
/**
* 禁用游戏逻辑系统(编辑器模式)
*/
private _disableGameLogicSystems(): void {
const ctx = this._systemContext;
if (!ctx) return;
// 这些系统由插件创建,通过 context 传递引用
if (ctx.animatorSystem) {
ctx.animatorSystem.enabled = false;
}
if (ctx.behaviorTreeSystem) {
ctx.behaviorTreeSystem.enabled = false;
}
if (ctx.physicsSystem) {
ctx.physicsSystem.enabled = false;
}
}
/**
* 启用游戏逻辑系统(预览/运行模式)
*/
private _enableGameLogicSystems(): void {
const ctx = this._systemContext;
if (!ctx) return;
if (ctx.animatorSystem) {
ctx.animatorSystem.enabled = true;
}
if (ctx.behaviorTreeSystem) {
ctx.behaviorTreeSystem.enabled = true;
ctx.behaviorTreeSystem.startAllAutoStartTrees?.();
}
if (ctx.physicsSystem) {
ctx.physicsSystem.enabled = true;
}
}
/**
* 启动渲染循环
*/
private _startRenderLoop(): void {
if (this._animationFrameId !== null) {
return;
}
this._lastTime = performance.now();
this._renderLoop();
}
/**
* 渲染循环
*/
private _renderLoop = (): void => {
const currentTime = performance.now();
const deltaTime = (currentTime - this._lastTime) / 1000;
this._lastTime = currentTime;
// 更新 ECS
Core.update(deltaTime);
this._animationFrameId = requestAnimationFrame(this._renderLoop);
};
/**
* 停止渲染循环
*/
private _stopRenderLoop(): void {
if (this._animationFrameId !== null) {
cancelAnimationFrame(this._animationFrameId);
this._animationFrameId = null;
}
}
/**
* 开始运行(启用游戏逻辑)
* Start running (enable game logic)
*/
start(): void {
if (!this._state.initialized || this._state.running) {
return;
}
this._state.running = true;
this._state.paused = false;
// 启用预览模式
if (this._renderSystem) {
this._renderSystem.setPreviewMode(true);
}
// 启用游戏逻辑系统
this._enableGameLogicSystems();
// 绑定 UI 输入
const ctx = this._systemContext;
if (ctx?.uiInputSystem && this._config.canvasId) {
const canvas = document.getElementById(this._config.canvasId) as HTMLCanvasElement;
if (canvas) {
ctx.uiInputSystem.bindToCanvas(canvas);
}
}
// 确保渲染循环在运行
this._startRenderLoop();
}
/**
* 暂停运行
* Pause running
*/
pause(): void {
if (!this._state.running || this._state.paused) {
return;
}
this._state.paused = true;
}
/**
* 恢复运行
* Resume running
*/
resume(): void {
if (!this._state.running || !this._state.paused) {
return;
}
this._state.paused = false;
}
/**
* 停止运行(禁用游戏逻辑)
* Stop running (disable game logic)
*/
stop(): void {
if (!this._state.running) {
return;
}
this._state.running = false;
this._state.paused = false;
// 禁用预览模式
if (this._renderSystem) {
this._renderSystem.setPreviewMode(false);
}
// 解绑 UI 输入
const ctx = this._systemContext;
if (ctx?.uiInputSystem) {
ctx.uiInputSystem.unbind?.();
}
// 禁用游戏逻辑系统
this._disableGameLogicSystems();
// 重置物理系统
if (ctx?.physicsSystem) {
ctx.physicsSystem.reset?.();
}
}
/**
* 单步执行
* Step forward one frame
*/
step(): void {
if (!this._state.initialized) {
return;
}
// 启用系统执行一帧
this._enableGameLogicSystems();
Core.update(1 / 60);
this._disableGameLogicSystems();
}
/**
* 加载场景数据
* Load scene data
*/
async loadScene(sceneData: string | object): Promise<void> {
if (!this._scene) {
throw new Error('Scene not initialized');
}
const jsonStr = typeof sceneData === 'string'
? sceneData
: JSON.stringify(sceneData);
SceneSerializer.deserialize(this._scene, jsonStr, {
strategy: 'replace',
preserveIds: true
});
}
/**
* 从 URL 加载场景
* Load scene from URL
*/
async loadSceneFromUrl(url: string): Promise<void> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load scene from ${url}: ${response.status}`);
}
const sceneJson = await response.text();
await this.loadScene(sceneJson);
}
/**
* 调整视口大小
* Resize viewport
*/
resize(width: number, height: number): void {
if (this._bridge) {
this._bridge.resize(width, height);
}
this._platform.resize(width, height);
}
/**
* 设置相机
* Set camera
*/
setCamera(config: { x: number; y: number; zoom: number; rotation?: number }): void {
if (this._bridge) {
this._bridge.setCamera({
x: config.x,
y: config.y,
zoom: config.zoom,
rotation: config.rotation ?? 0
});
}
}
/**
* 获取相机状态
* Get camera state
*/
getCamera(): { x: number; y: number; zoom: number; rotation: number } {
if (this._bridge) {
return this._bridge.getCamera();
}
return { x: 0, y: 0, zoom: 1, rotation: 0 };
}
/**
* 设置网格显示
* Set grid visibility
*/
setShowGrid(show: boolean): void {
if (this._bridge) {
this._bridge.setShowGrid(show);
}
}
/**
* 设置 Gizmo 显示
* Set gizmo visibility
*/
setShowGizmos(show: boolean): void {
if (this._renderSystem) {
this._renderSystem.setShowGizmos(show);
}
}
/**
* 设置清除颜色
* Set clear color
*/
setClearColor(r: number, g: number, b: number, a: number = 1.0): void {
if (this._bridge) {
this._bridge.setClearColor(r, g, b, a);
}
}
/**
* 获取统计信息
* Get stats
*/
getStats(): { fps: number; drawCalls: number; spriteCount: number } {
if (!this._renderSystem) {
return { fps: 0, drawCalls: 0, spriteCount: 0 };
}
const engineStats = this._renderSystem.getStats();
return {
fps: engineStats?.fps ?? 0,
drawCalls: engineStats?.drawCalls ?? 0,
spriteCount: this._renderSystem.spriteCount
};
}
// ===== 编辑器特有功能 =====
// ===== Editor-specific features =====
/**
* 设置 Gizmo 注册表(编辑器模式)
* Set gizmo registry (editor mode)
*/
setGizmoRegistry(
gizmoDataProvider: (component: any, entity: any, isSelected: boolean) => any,
hasGizmoProvider: (component: any) => boolean
): void {
this._gizmoDataProvider = gizmoDataProvider;
this._hasGizmoProvider = hasGizmoProvider;
if (this._renderSystem) {
this._renderSystem.setGizmoRegistry(gizmoDataProvider, hasGizmoProvider);
}
}
/**
* 设置选中的实体 ID编辑器模式
* Set selected entity IDs (editor mode)
*/
setSelectedEntityIds(ids: number[]): void {
if (this._renderSystem) {
this._renderSystem.setSelectedEntityIds(ids);
}
}
/**
* 设置变换工具模式(编辑器模式)
* Set transform tool mode (editor mode)
*/
setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void {
if (this._renderSystem) {
this._renderSystem.setTransformMode(mode);
}
}
/**
* 获取变换工具模式
* Get transform tool mode
*/
getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' {
return this._renderSystem?.getTransformMode() ?? 'select';
}
/**
* 设置 UI 画布尺寸
* Set UI canvas size
*/
setUICanvasSize(width: number, height: number): void {
if (this._renderSystem) {
this._renderSystem.setUICanvasSize(width, height);
}
}
/**
* 获取 UI 画布尺寸
* Get UI canvas size
*/
getUICanvasSize(): { width: number; height: number } {
return this._renderSystem?.getUICanvasSize() ?? { width: 0, height: 0 };
}
/**
* 设置 UI 画布边界显示
* Set UI canvas boundary visibility
*/
setShowUICanvasBoundary(show: boolean): void {
if (this._renderSystem) {
this._renderSystem.setShowUICanvasBoundary(show);
}
}
/**
* 获取 UI 画布边界显示状态
* Get UI canvas boundary visibility
*/
getShowUICanvasBoundary(): boolean {
return this._renderSystem?.getShowUICanvasBoundary() ?? true;
}
// ===== 场景快照 API =====
// ===== Scene Snapshot API =====
/**
* 保存场景快照
* Save scene snapshot
*/
saveSceneSnapshot(): boolean {
if (!this._scene) {
console.warn('[GameRuntime] Cannot save snapshot: no scene');
return false;
}
try {
this._sceneSnapshot = SceneSerializer.serialize(this._scene, {
format: 'json',
pretty: false,
includeMetadata: false
}) as string;
return true;
} catch (error) {
console.error('[GameRuntime] Failed to save snapshot:', error);
return false;
}
}
/**
* 恢复场景快照
* Restore scene snapshot
*/
async restoreSceneSnapshot(): Promise<boolean> {
if (!this._scene || !this._sceneSnapshot) {
console.warn('[GameRuntime] Cannot restore: no scene or snapshot');
return false;
}
try {
// 清除缓存
const ctx = this._systemContext;
if (ctx?.tilemapSystem) {
ctx.tilemapSystem.clearCache?.();
}
// 反序列化场景
SceneSerializer.deserialize(this._scene, this._sceneSnapshot, {
strategy: 'replace',
preserveIds: true
});
this._sceneSnapshot = null;
return true;
} catch (error) {
console.error('[GameRuntime] Failed to restore snapshot:', error);
return false;
}
}
/**
* 检查是否有快照
* Check if snapshot exists
*/
hasSnapshot(): boolean {
return this._sceneSnapshot !== null;
}
// ===== 多视口 API =====
// ===== Multi-viewport API =====
/**
* 注册视口
* Register viewport
*/
registerViewport(id: string, canvasId: string): void {
if (this._bridge) {
this._bridge.registerViewport(id, canvasId);
}
}
/**
* 注销视口
* Unregister viewport
*/
unregisterViewport(id: string): void {
if (this._bridge) {
this._bridge.unregisterViewport(id);
}
}
/**
* 设置活动视口
* Set active viewport
*/
setActiveViewport(id: string): boolean {
if (this._bridge) {
return this._bridge.setActiveViewport(id);
}
return false;
}
/**
* 释放资源
* Dispose resources
*/
dispose(): void {
this.stop();
this._stopRenderLoop();
if (this._assetManager) {
this._assetManager.dispose();
this._assetManager = null;
}
this._engineIntegration = null;
this._scene = null;
if (this._bridge) {
this._bridge.dispose();
this._bridge = null;
}
this._renderSystem = null;
this._cameraSystem = null;
this._systemContext = null;
this._platform.dispose();
this._state.initialized = false;
}
}
/**
* 创建游戏运行时实例
* Create game runtime instance
*/
export function createGameRuntime(config: GameRuntimeConfig): GameRuntime {
return new GameRuntime(config);
}

View File

@@ -0,0 +1,152 @@
/**
* Platform Adapter Interface
* 平台适配器接口
*
* 定义不同平台(编辑器、浏览器、原生)需要实现的适配器接口
* Defines the adapter interface that different platforms need to implement
*/
/**
* 资源路径解析器
* Asset path resolver
*/
export interface IPathResolver {
/**
* 解析资源路径为可加载的 URL
* Resolve asset path to a loadable URL
*/
resolve(path: string): string;
}
/**
* 平台能力标识
* Platform capability flags
*/
export interface PlatformCapabilities {
/** 是否支持文件系统访问 / Supports file system access */
fileSystem: boolean;
/** 是否支持热重载 / Supports hot reload */
hotReload: boolean;
/** 是否支持 Gizmo 显示 / Supports gizmo display */
gizmos: boolean;
/** 是否支持网格显示 / Supports grid display */
grid: boolean;
/** 是否支持场景编辑 / Supports scene editing */
sceneEditing: boolean;
}
/**
* 平台适配器配置
* Platform adapter configuration
*/
export interface PlatformAdapterConfig {
/** Canvas 元素 ID */
canvasId: string;
/** 初始宽度 */
width?: number;
/** 初始高度 */
height?: number;
/** 是否为编辑器模式 */
isEditor?: boolean;
}
/**
* 平台适配器接口
* Platform adapter interface
*
* 不同平台通过实现此接口来提供平台特定的功能
* Different platforms implement this interface to provide platform-specific functionality
*/
export interface IPlatformAdapter {
/**
* 平台名称
* Platform name
*/
readonly name: string;
/**
* 平台能力
* Platform capabilities
*/
readonly capabilities: PlatformCapabilities;
/**
* 路径解析器
* Path resolver
*/
readonly pathResolver: IPathResolver;
/**
* 初始化平台
* Initialize platform
*/
initialize(config: PlatformAdapterConfig): Promise<void>;
/**
* 获取 WASM 模块
* Get WASM module
*
* 不同平台可能以不同方式加载 WASM
* Different platforms may load WASM in different ways
*/
getWasmModule(): Promise<any>;
/**
* 获取 Canvas 元素
* Get canvas element
*/
getCanvas(): HTMLCanvasElement | null;
/**
* 调整视口大小
* Resize viewport
*/
resize(width: number, height: number): void;
/**
* 获取当前视口尺寸
* Get current viewport size
*/
getViewportSize(): { width: number; height: number };
/**
* 是否为编辑器模式
* Whether in editor mode
*/
isEditorMode(): boolean;
/**
* 设置是否显示网格(仅编辑器模式有效)
* Set whether to show grid (only effective in editor mode)
*/
setShowGrid?(show: boolean): void;
/**
* 设置是否显示 Gizmos仅编辑器模式有效
* Set whether to show gizmos (only effective in editor mode)
*/
setShowGizmos?(show: boolean): void;
/**
* 释放资源
* Dispose resources
*/
dispose(): void;
}
/**
* 默认路径解析器(直接返回路径)
* Default path resolver (returns path as-is)
*/
export class DefaultPathResolver implements IPathResolver {
resolve(path: string): string {
// 如果已经是 URL直接返回
if (path.startsWith('http://') ||
path.startsWith('https://') ||
path.startsWith('data:') ||
path.startsWith('blob:')) {
return path;
}
return path;
}
}

View File

@@ -0,0 +1,118 @@
import { runtimePluginManager, type IPlugin } from './PluginManager';
export interface PluginPackageInfo {
plugin: boolean;
pluginExport: string;
category?: string;
isEnginePlugin?: boolean;
}
export interface PluginConfig {
enabled: boolean;
options?: Record<string, any>;
}
export interface ProjectPluginConfig {
plugins: Record<string, PluginConfig>;
}
interface LoadedPluginInfo {
id: string;
plugin: IPlugin;
packageInfo: PluginPackageInfo;
}
const loadedPlugins = new Map<string, LoadedPluginInfo>();
/**
* 从模块动态加载插件
* @param packageId 包 ID如 '@esengine/sprite'
* @param packageInfo 包的 esengine 配置
*/
export async function loadPlugin(
packageId: string,
packageInfo: PluginPackageInfo
): Promise<IPlugin | null> {
if (loadedPlugins.has(packageId)) {
return loadedPlugins.get(packageId)!.plugin;
}
try {
const module = await import(/* @vite-ignore */ packageId);
const exportName = packageInfo.pluginExport || 'default';
const plugin = module[exportName] as IPlugin;
if (!plugin || !plugin.descriptor) {
console.warn(`[PluginLoader] Invalid plugin export from ${packageId}`);
return null;
}
loadedPlugins.set(packageId, {
id: packageId,
plugin,
packageInfo
});
return plugin;
} catch (error) {
console.error(`[PluginLoader] Failed to load plugin ${packageId}:`, error);
return null;
}
}
/**
* 根据项目配置加载所有启用的插件
*/
export async function loadEnabledPlugins(
config: ProjectPluginConfig,
packageInfoMap: Record<string, PluginPackageInfo>
): Promise<void> {
const sortedPlugins: Array<{ id: string; info: PluginPackageInfo }> = [];
for (const [id, pluginConfig] of Object.entries(config.plugins)) {
if (!pluginConfig.enabled) continue;
const packageInfo = packageInfoMap[id];
if (!packageInfo) {
console.warn(`[PluginLoader] No package info for ${id}, skipping`);
continue;
}
sortedPlugins.push({ id, info: packageInfo });
}
// 引擎核心插件优先加载
sortedPlugins.sort((a, b) => {
if (a.info.isEnginePlugin && !b.info.isEnginePlugin) return -1;
if (!a.info.isEnginePlugin && b.info.isEnginePlugin) return 1;
return 0;
});
for (const { id, info } of sortedPlugins) {
const plugin = await loadPlugin(id, info);
if (plugin) {
runtimePluginManager.register(plugin);
}
}
}
/**
* 注册预加载的插件(用于已静态导入的插件)
*/
export function registerStaticPlugin(plugin: IPlugin): void {
runtimePluginManager.register(plugin);
}
/**
* 获取已加载的插件列表
*/
export function getLoadedPlugins(): IPlugin[] {
return Array.from(loadedPlugins.values()).map(info => info.plugin);
}
/**
* 重置插件加载器状态
*/
export function resetPluginLoader(): void {
loadedPlugins.clear();
}

View File

@@ -0,0 +1,166 @@
/**
* Runtime Plugin Manager
* 运行时插件管理器
*/
import { ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework';
import type { IScene } from '@esengine/ecs-framework';
export interface SystemContext {
isEditor: boolean;
[key: string]: any;
}
export interface PluginDescriptor {
id: string;
name: string;
version: string;
description?: string;
category?: string;
enabledByDefault?: boolean;
isEnginePlugin?: boolean;
}
export interface IRuntimeModule {
registerComponents?(registry: typeof ComponentRegistry): void;
registerServices?(services: ServiceContainer): void;
createSystems?(scene: IScene, context: SystemContext): void;
onSystemsCreated?(scene: IScene, context: SystemContext): void;
onInitialize?(): Promise<void>;
onDestroy?(): void;
}
export interface IPlugin {
readonly descriptor: PluginDescriptor;
readonly runtimeModule?: IRuntimeModule;
}
export class RuntimePluginManager {
private _plugins = new Map<string, IPlugin>();
private _enabledPlugins = new Set<string>();
private _bInitialized = false;
register(plugin: IPlugin): void {
const id = plugin.descriptor.id;
if (this._plugins.has(id)) {
return;
}
this._plugins.set(id, plugin);
if (plugin.descriptor.enabledByDefault !== false) {
this._enabledPlugins.add(id);
}
}
enable(pluginId: string): void {
this._enabledPlugins.add(pluginId);
}
disable(pluginId: string): void {
this._enabledPlugins.delete(pluginId);
}
isEnabled(pluginId: string): boolean {
return this._enabledPlugins.has(pluginId);
}
loadConfig(config: { enabledPlugins: string[] }): void {
this._enabledPlugins.clear();
for (const id of config.enabledPlugins) {
this._enabledPlugins.add(id);
}
// 始终启用引擎核心插件
for (const [id, plugin] of this._plugins) {
if (plugin.descriptor.isEnginePlugin) {
this._enabledPlugins.add(id);
}
}
}
async initializeRuntime(services: ServiceContainer): Promise<void> {
if (this._bInitialized) {
return;
}
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
if (mod?.registerComponents) {
try {
mod.registerComponents(ComponentRegistry);
} catch (e) {
console.error(`[PluginManager] Failed to register components for ${id}:`, e);
}
}
}
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
if (mod?.registerServices) {
try {
mod.registerServices(services);
} catch (e) {
console.error(`[PluginManager] Failed to register services for ${id}:`, e);
}
}
}
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
if (mod?.onInitialize) {
try {
await mod.onInitialize();
} catch (e) {
console.error(`[PluginManager] Failed to initialize ${id}:`, e);
}
}
}
this._bInitialized = true;
}
createSystemsForScene(scene: IScene, context: SystemContext): void {
// Phase 1: 创建系统
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
if (mod?.createSystems) {
try {
mod.createSystems(scene, context);
} catch (e) {
console.error(`[PluginManager] Failed to create systems for ${id}:`, e);
}
}
}
// Phase 2: 连接跨插件依赖
for (const [id, plugin] of this._plugins) {
if (!this._enabledPlugins.has(id)) continue;
const mod = plugin.runtimeModule;
if (mod?.onSystemsCreated) {
try {
mod.onSystemsCreated(scene, context);
} catch (e) {
console.error(`[PluginManager] Failed to wire dependencies for ${id}:`, e);
}
}
}
}
getPlugins(): IPlugin[] {
return Array.from(this._plugins.values());
}
getPlugin(id: string): IPlugin | undefined {
return this._plugins.get(id);
}
reset(): void {
this._plugins.clear();
this._enabledPlugins.clear();
this._bInitialized = false;
}
}
export const runtimePluginManager = new RuntimePluginManager();

View File

@@ -0,0 +1,128 @@
import type { PluginPackageInfo, PluginConfig } from './PluginLoader';
export interface ProjectConfig {
name: string;
version: string;
plugins: Record<string, PluginConfig>;
}
/**
* 内置引擎插件的包信息
* 这些信息在构建时从各包的 package.json 中提取
*/
export const BUILTIN_PLUGIN_PACKAGES: Record<string, PluginPackageInfo> = {
'@esengine/engine-core': {
plugin: true,
pluginExport: 'EnginePlugin',
category: 'core',
isEnginePlugin: true
},
'@esengine/camera': {
plugin: true,
pluginExport: 'CameraPlugin',
category: 'core',
isEnginePlugin: true
},
'@esengine/sprite': {
plugin: true,
pluginExport: 'SpritePlugin',
category: 'rendering',
isEnginePlugin: true
},
'@esengine/audio': {
plugin: true,
pluginExport: 'AudioPlugin',
category: 'audio',
isEnginePlugin: true
},
'@esengine/ui': {
plugin: true,
pluginExport: 'UIPlugin',
category: 'ui'
},
'@esengine/tilemap': {
plugin: true,
pluginExport: 'TilemapPlugin',
category: 'tilemap'
},
'@esengine/behavior-tree': {
plugin: true,
pluginExport: 'BehaviorTreePlugin',
category: 'ai'
},
'@esengine/physics-rapier2d': {
plugin: true,
pluginExport: 'PhysicsPlugin',
category: 'physics'
}
};
/**
* 创建默认项目配置
*/
export function createDefaultProjectConfig(): ProjectConfig {
return {
name: 'New Project',
version: '1.0.0',
plugins: {
'@esengine/engine-core': { enabled: true },
'@esengine/camera': { enabled: true },
'@esengine/sprite': { enabled: true },
'@esengine/audio': { enabled: true },
'@esengine/ui': { enabled: true },
'@esengine/tilemap': { enabled: false },
'@esengine/behavior-tree': { enabled: false },
'@esengine/physics-rapier2d': { enabled: false }
}
};
}
/**
* 合并用户配置与默认配置
*/
export function mergeProjectConfig(
userConfig: Partial<ProjectConfig>
): ProjectConfig {
const defaultConfig = createDefaultProjectConfig();
return {
name: userConfig.name || defaultConfig.name,
version: userConfig.version || defaultConfig.version,
plugins: {
...defaultConfig.plugins,
...userConfig.plugins
}
};
}
/**
* 从编辑器的 enabledPlugins 列表创建项目配置
* Create project config from editor's enabledPlugins list
*
* @param enabledPlugins - 启用的插件 ID 列表 / List of enabled plugin IDs
*/
export function createProjectConfigFromEnabledList(
enabledPlugins: string[]
): ProjectConfig {
const defaultConfig = createDefaultProjectConfig();
// 先禁用所有非核心插件
// First disable all non-core plugins
const plugins: Record<string, PluginConfig> = {};
for (const id of Object.keys(defaultConfig.plugins)) {
const packageInfo = BUILTIN_PLUGIN_PACKAGES[id];
// 核心插件始终启用
// Core plugins are always enabled
if (packageInfo?.isEnginePlugin) {
plugins[id] = { enabled: true };
} else {
plugins[id] = { enabled: enabledPlugins.includes(id) };
}
}
return {
...defaultConfig,
plugins
};
}

View File

@@ -0,0 +1,66 @@
/**
* Runtime Bootstrap
* 运行时启动器 - 提供通用的初始化流程
*/
import { Core } from '@esengine/ecs-framework';
import type { IScene } from '@esengine/ecs-framework';
import {
runtimePluginManager,
type IPlugin,
type IRuntimeModule,
type PluginDescriptor,
type SystemContext
} from './PluginManager';
export interface RuntimeConfig {
enabledPlugins?: string[];
isEditor?: boolean;
}
/**
* 创建插件(简化工厂)
*/
export function createPlugin(
descriptor: PluginDescriptor,
runtimeModule: IRuntimeModule
): IPlugin {
return { descriptor, runtimeModule };
}
/**
* 注册插件到运行时
*/
export function registerPlugin(plugin: IPlugin): void {
runtimePluginManager.register(plugin);
}
/**
* 初始化运行时
* @param config 运行时配置
*/
export async function initializeRuntime(config?: RuntimeConfig): Promise<void> {
if (config?.enabledPlugins) {
runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins });
} else {
for (const plugin of runtimePluginManager.getPlugins()) {
runtimePluginManager.enable(plugin.descriptor.id);
}
}
await runtimePluginManager.initializeRuntime(Core.services);
}
/**
* 为场景创建系统
*/
export function createSystemsForScene(scene: IScene, context: SystemContext): void {
runtimePluginManager.createSystemsForScene(scene, context);
}
/**
* 重置运行时(用于热重载等场景)
*/
export function resetRuntime(): void {
runtimePluginManager.reset();
}

View File

@@ -0,0 +1,143 @@
/**
* Browser Platform Adapter
* 浏览器平台适配器
*
* 用于独立浏览器运行时的平台适配器
* Platform adapter for standalone browser runtime
*/
import type {
IPlatformAdapter,
IPathResolver,
PlatformCapabilities,
PlatformAdapterConfig
} from '../IPlatformAdapter';
/**
* 浏览器路径解析器
* Browser path resolver
*/
export class BrowserPathResolver implements IPathResolver {
private _baseUrl: string;
constructor(baseUrl: string = '') {
this._baseUrl = baseUrl;
}
resolve(path: string): string {
// 如果已经是完整 URL直接返回
if (path.startsWith('http://') ||
path.startsWith('https://') ||
path.startsWith('data:') ||
path.startsWith('blob:') ||
path.startsWith('/asset?')) {
return path;
}
// 相对路径,添加资产请求前缀
return `/asset?path=${encodeURIComponent(path)}`;
}
/**
* 更新基础 URL
*/
setBaseUrl(baseUrl: string): void {
this._baseUrl = baseUrl;
}
}
/**
* 浏览器平台适配器配置
*/
export interface BrowserPlatformConfig {
/** WASM 模块(预加载的)*/
wasmModule?: any;
/** WASM 模块加载器(异步加载)*/
wasmModuleLoader?: () => Promise<any>;
/** 资产基础 URL */
assetBaseUrl?: string;
}
/**
* 浏览器平台适配器
* Browser platform adapter
*/
export class BrowserPlatformAdapter implements IPlatformAdapter {
readonly name = 'browser';
readonly capabilities: PlatformCapabilities = {
fileSystem: false,
hotReload: false,
gizmos: false,
grid: false,
sceneEditing: false
};
private _pathResolver: BrowserPathResolver;
private _canvas: HTMLCanvasElement | null = null;
private _config: BrowserPlatformConfig;
private _viewportSize = { width: 0, height: 0 };
constructor(config: BrowserPlatformConfig = {}) {
this._config = config;
this._pathResolver = new BrowserPathResolver(config.assetBaseUrl || '');
}
get pathResolver(): IPathResolver {
return this._pathResolver;
}
async initialize(config: PlatformAdapterConfig): Promise<void> {
// 获取 Canvas
this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
if (!this._canvas) {
throw new Error(`Canvas not found: ${config.canvasId}`);
}
// 设置尺寸
const width = config.width || window.innerWidth;
const height = config.height || window.innerHeight;
this._canvas.width = width;
this._canvas.height = height;
this._viewportSize = { width, height };
}
async getWasmModule(): Promise<any> {
// 如果已提供模块,直接返回
if (this._config.wasmModule) {
return this._config.wasmModule;
}
// 如果提供了加载器,使用加载器
if (this._config.wasmModuleLoader) {
return this._config.wasmModuleLoader();
}
// 默认:尝试动态导入
throw new Error('No WASM module or loader provided');
}
getCanvas(): HTMLCanvasElement | null {
return this._canvas;
}
resize(width: number, height: number): void {
if (this._canvas) {
this._canvas.width = width;
this._canvas.height = height;
}
this._viewportSize = { width, height };
}
getViewportSize(): { width: number; height: number } {
return { ...this._viewportSize };
}
isEditorMode(): boolean {
return false;
}
dispose(): void {
this._canvas = null;
}
}

View File

@@ -0,0 +1,185 @@
/**
* Editor Platform Adapter
* 编辑器平台适配器
*
* 用于 Tauri 编辑器内嵌预览的平台适配器
* Platform adapter for Tauri editor embedded preview
*/
import type {
IPlatformAdapter,
IPathResolver,
PlatformCapabilities,
PlatformAdapterConfig
} from '../IPlatformAdapter';
/**
* 编辑器路径解析器
* Editor path resolver
*
* 使用 Tauri 的 convertFileSrc 转换本地文件路径
* Uses Tauri's convertFileSrc to convert local file paths
*/
export class EditorPathResolver implements IPathResolver {
private _pathTransformer: (path: string) => string;
constructor(pathTransformer: (path: string) => string) {
this._pathTransformer = pathTransformer;
}
resolve(path: string): string {
// 如果已经是 URL直接返回
if (path.startsWith('http://') ||
path.startsWith('https://') ||
path.startsWith('data:') ||
path.startsWith('blob:') ||
path.startsWith('asset://')) {
return path;
}
// 使用 Tauri 路径转换器
return this._pathTransformer(path);
}
/**
* 更新路径转换器
*/
setPathTransformer(transformer: (path: string) => string): void {
this._pathTransformer = transformer;
}
}
/**
* 编辑器平台适配器配置
*/
export interface EditorPlatformConfig {
/** WASM 模块(预加载的)*/
wasmModule: any;
/** 路径转换函数(使用 Tauri 的 convertFileSrc*/
pathTransformer: (path: string) => string;
/** Gizmo 数据提供者 */
gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any;
/** Gizmo 检查函数 */
hasGizmoProvider?: (component: any) => boolean;
}
/**
* 编辑器平台适配器
* Editor platform adapter
*/
export class EditorPlatformAdapter implements IPlatformAdapter {
readonly name = 'editor';
readonly capabilities: PlatformCapabilities = {
fileSystem: true,
hotReload: true,
gizmos: true,
grid: true,
sceneEditing: true
};
private _pathResolver: EditorPathResolver;
private _canvas: HTMLCanvasElement | null = null;
private _config: EditorPlatformConfig;
private _viewportSize = { width: 0, height: 0 };
private _showGrid = true;
private _showGizmos = true;
constructor(config: EditorPlatformConfig) {
this._config = config;
this._pathResolver = new EditorPathResolver(config.pathTransformer);
}
get pathResolver(): IPathResolver {
return this._pathResolver;
}
/**
* 获取 Gizmo 数据提供者
*/
get gizmoDataProvider() {
return this._config.gizmoDataProvider;
}
/**
* 获取 Gizmo 检查函数
*/
get hasGizmoProvider() {
return this._config.hasGizmoProvider;
}
async initialize(config: PlatformAdapterConfig): Promise<void> {
// 获取 Canvas
this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
if (!this._canvas) {
throw new Error(`Canvas not found: ${config.canvasId}`);
}
// 处理 DPR 缩放
const dpr = window.devicePixelRatio || 1;
const container = this._canvas.parentElement;
if (container) {
const rect = container.getBoundingClientRect();
const width = config.width || Math.floor(rect.width * dpr);
const height = config.height || Math.floor(rect.height * dpr);
this._canvas.width = width;
this._canvas.height = height;
this._canvas.style.width = `${rect.width}px`;
this._canvas.style.height = `${rect.height}px`;
this._viewportSize = { width, height };
} else {
const width = config.width || window.innerWidth;
const height = config.height || window.innerHeight;
this._canvas.width = width;
this._canvas.height = height;
this._viewportSize = { width, height };
}
}
async getWasmModule(): Promise<any> {
return this._config.wasmModule;
}
getCanvas(): HTMLCanvasElement | null {
return this._canvas;
}
resize(width: number, height: number): void {
if (this._canvas) {
this._canvas.width = width;
this._canvas.height = height;
}
this._viewportSize = { width, height };
}
getViewportSize(): { width: number; height: number } {
return { ...this._viewportSize };
}
isEditorMode(): boolean {
return true;
}
setShowGrid(show: boolean): void {
this._showGrid = show;
}
getShowGrid(): boolean {
return this._showGrid;
}
setShowGizmos(show: boolean): void {
this._showGizmos = show;
}
getShowGizmos(): boolean {
return this._showGizmos;
}
dispose(): void {
this._canvas = null;
}
}

View File

@@ -0,0 +1,7 @@
/**
* Platform Adapters
* 平台适配器
*/
export { BrowserPlatformAdapter, BrowserPathResolver, type BrowserPlatformConfig } from './BrowserPlatformAdapter';
export { EditorPlatformAdapter, EditorPathResolver, type EditorPlatformConfig } from './EditorPlatformAdapter';

View File

@@ -0,0 +1,72 @@
export {
RuntimePluginManager,
runtimePluginManager,
type SystemContext,
type PluginDescriptor,
type IRuntimeModule,
type IPlugin
} from './PluginManager';
export {
createPlugin,
registerPlugin,
initializeRuntime,
createSystemsForScene,
resetRuntime,
type RuntimeConfig
} from './RuntimeBootstrap';
export {
loadPlugin,
loadEnabledPlugins,
registerStaticPlugin,
getLoadedPlugins,
resetPluginLoader,
type PluginPackageInfo,
type PluginConfig,
type ProjectPluginConfig
} from './PluginLoader';
export {
BUILTIN_PLUGIN_PACKAGES,
createDefaultProjectConfig,
mergeProjectConfig,
createProjectConfigFromEnabledList,
type ProjectConfig
} from './ProjectConfig';
// Platform Adapter
export {
DefaultPathResolver,
type IPlatformAdapter,
type IPathResolver,
type PlatformCapabilities,
type PlatformAdapterConfig
} from './IPlatformAdapter';
// Game Runtime
export {
GameRuntime,
createGameRuntime,
type GameRuntimeConfig,
type RuntimeState
} from './GameRuntime';
// Platform Adapters
export {
BrowserPlatformAdapter,
BrowserPathResolver,
type BrowserPlatformConfig,
EditorPlatformAdapter,
EditorPathResolver,
type EditorPlatformConfig
} from './adapters';
// Browser File System Service
export {
BrowserFileSystemService,
createBrowserFileSystem,
type AssetCatalog,
type AssetCatalogEntry,
type BrowserFileSystemOptions
} from './services/BrowserFileSystemService';

View File

@@ -0,0 +1,306 @@
/**
* Browser File System Service
* 浏览器文件系统服务
*
* 在浏览器运行时环境中,通过 HTTP fetch 加载资产文件。
* 使用资产目录(asset-catalog.json)来解析 GUID 到实际 URL。
*
* In browser runtime environment, loads asset files via HTTP fetch.
* Uses asset catalog to resolve GUIDs to actual URLs.
*/
/**
* Asset catalog entry
*/
export interface AssetCatalogEntry {
guid: string;
path: string;
type: string;
size: number;
hash: string;
}
/**
* Asset catalog loaded from JSON
*/
export interface AssetCatalog {
version: string;
createdAt: number;
entries: Record<string, AssetCatalogEntry>;
}
/**
* Browser file system service options
*/
export interface BrowserFileSystemOptions {
/** Base URL for assets (e.g., '/assets' or 'https://cdn.example.com/assets') */
baseUrl?: string;
/** Asset catalog URL */
catalogUrl?: string;
/** Enable caching */
enableCache?: boolean;
}
/**
* Browser File System Service
*
* Provides file system-like API for browser environments
* by fetching files over HTTP.
*/
export class BrowserFileSystemService {
private _baseUrl: string;
private _catalogUrl: string;
private _catalog: AssetCatalog | null = null;
private _cache = new Map<string, string>();
private _enableCache: boolean;
private _initialized = false;
constructor(options: BrowserFileSystemOptions = {}) {
this._baseUrl = options.baseUrl ?? '/assets';
this._catalogUrl = options.catalogUrl ?? '/asset-catalog.json';
this._enableCache = options.enableCache ?? true;
}
/**
* Initialize service and load catalog
*/
async initialize(): Promise<void> {
if (this._initialized) return;
try {
await this._loadCatalog();
this._initialized = true;
console.log('[BrowserFileSystem] Initialized with',
Object.keys(this._catalog?.entries ?? {}).length, 'assets');
} catch (error) {
console.warn('[BrowserFileSystem] Failed to load catalog:', error);
// Continue without catalog - will use path-based loading
this._initialized = true;
}
}
/**
* Load asset catalog
*/
private async _loadCatalog(): Promise<void> {
const response = await fetch(this._catalogUrl);
if (!response.ok) {
throw new Error(`Failed to fetch catalog: ${response.status}`);
}
this._catalog = await response.json();
}
/**
* Read file content as string
* @param path - Can be GUID, relative path, or absolute path
*/
async readFile(path: string): Promise<string> {
const url = this._resolveUrl(path);
// Check cache
if (this._enableCache && this._cache.has(url)) {
return this._cache.get(url)!;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch file: ${url} (${response.status})`);
}
const content = await response.text();
// Cache result
if (this._enableCache) {
this._cache.set(url, content);
}
return content;
}
/**
* Read file as binary (ArrayBuffer)
*/
async readBinary(path: string): Promise<ArrayBuffer> {
const url = this._resolveUrl(path);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch binary: ${url} (${response.status})`);
}
return response.arrayBuffer();
}
/**
* Read file as Blob
*/
async readBlob(path: string): Promise<Blob> {
const url = this._resolveUrl(path);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch blob: ${url} (${response.status})`);
}
return response.blob();
}
/**
* Check if file exists (via HEAD request)
*/
async exists(path: string): Promise<boolean> {
const url = this._resolveUrl(path);
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch {
return false;
}
}
/**
* Resolve path to URL
* Handles GUID, relative path, and absolute path
*/
private _resolveUrl(path: string): string {
// Check if it's a GUID and we have a catalog
if (this._catalog && this._isGuid(path)) {
const entry = this._catalog.entries[path];
if (entry) {
return this._pathToUrl(entry.path);
}
}
// Check if it's an absolute Windows path (e.g., F:\...)
if (/^[A-Za-z]:[\\/]/.test(path)) {
// Try to extract relative path from absolute path
const relativePath = this._extractRelativePath(path);
if (relativePath) {
return this._pathToUrl(relativePath);
}
// Fallback: use just the filename
const filename = path.split(/[\\/]/).pop();
return `${this._baseUrl}/${filename}`;
}
// Check if it's already a URL
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/')) {
return path;
}
// Treat as relative path
return this._pathToUrl(path);
}
/**
* Convert relative path to URL
*/
private _pathToUrl(relativePath: string): string {
// Normalize path separators
const normalized = relativePath.replace(/\\/g, '/');
// Remove leading 'assets/' if baseUrl already includes it
let cleanPath = normalized;
if (cleanPath.startsWith('assets/') && this._baseUrl.endsWith('/assets')) {
cleanPath = cleanPath.substring(7);
}
// Ensure no double slashes
const base = this._baseUrl.endsWith('/') ? this._baseUrl.slice(0, -1) : this._baseUrl;
const path = cleanPath.startsWith('/') ? cleanPath.slice(1) : cleanPath;
return `${base}/${path}`;
}
/**
* Extract relative path from absolute path
*/
private _extractRelativePath(absolutePath: string): string | null {
const normalized = absolutePath.replace(/\\/g, '/');
// Look for 'assets/' in the path
const assetsIndex = normalized.toLowerCase().indexOf('/assets/');
if (assetsIndex >= 0) {
return normalized.substring(assetsIndex + 1); // Include 'assets/'
}
return null;
}
/**
* Check if string looks like a GUID
*/
private _isGuid(str: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
}
/**
* Get asset metadata from catalog
*/
getAssetMetadata(guidOrPath: string): AssetCatalogEntry | null {
if (!this._catalog) return null;
// Try as GUID
if (this._catalog.entries[guidOrPath]) {
return this._catalog.entries[guidOrPath];
}
// Try as path
for (const entry of Object.values(this._catalog.entries)) {
if (entry.path === guidOrPath) {
return entry;
}
}
return null;
}
/**
* Get all assets of a specific type
*/
getAssetsByType(type: string): AssetCatalogEntry[] {
if (!this._catalog) return [];
return Object.values(this._catalog.entries)
.filter(entry => entry.type === type);
}
/**
* Clear cache
*/
clearCache(): void {
this._cache.clear();
}
/**
* Get catalog
*/
get catalog(): AssetCatalog | null {
return this._catalog;
}
/**
* Check if initialized
*/
get isInitialized(): boolean {
return this._initialized;
}
/**
* Dispose service and clear resources
* Required by IService interface
*/
dispose(): void {
this._cache.clear();
this._catalog = null;
this._initialized = false;
}
}
/**
* Create and register browser file system service
*/
export function createBrowserFileSystem(options?: BrowserFileSystemOptions): BrowserFileSystemService {
return new BrowserFileSystemService(options);
}