feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API * feat(rapier2d): 新增Rapier2D WASM绑定包 * feat(physics-rapier2d): 添加跨平台WASM加载器 * feat(asset-system): 添加运行时资产目录和bundle格式 * feat(asset-system-editor): 新增编辑器资产管理包 * feat(editor-core): 添加构建系统和模块管理 * feat(editor-app): 重构浏览器预览使用import maps * feat(platform-web): 添加BrowserRuntime和资产读取 * feat(engine): 添加材质系统和着色器管理 * feat(material): 新增材质系统和着色器编辑器 * feat(tilemap): 增强tilemap编辑器和动画系统 * feat(modules): 添加module.json配置 * feat(core): 添加module.json和类型定义更新 * chore: 更新依赖和构建配置 * refactor(plugins): 更新插件模板使用ModuleManifest * chore: 添加第三方依赖库 * chore: 移除BehaviourTree-ai和ecs-astar子模块 * docs: 更新README和文档主题样式 * fix: 修复Rust文档测试和添加rapier2d WASM绑定 * fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题 * feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea) * fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖 * fix: 添加缺失的包依赖修复CI构建 * fix: 修复CodeQL检测到的代码问题 * fix: 修复构建错误和缺失依赖 * fix: 修复类型检查错误 * fix(material-system): 修复tsconfig配置支持TypeScript项目引用 * fix(editor-core): 修复Rollup构建配置添加tauri external * fix: 修复CodeQL检测到的代码问题 * fix: 修复CodeQL检测到的代码问题
This commit is contained in:
275
packages/asset-system/src/runtime/RuntimeCatalog.ts
Normal file
275
packages/asset-system/src/runtime/RuntimeCatalog.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Runtime Catalog for Asset Resolution
|
||||
* 资产解析的运行时目录
|
||||
*
|
||||
* Provides GUID-based asset lookup at runtime.
|
||||
* 提供运行时基于 GUID 的资产查找。
|
||||
*/
|
||||
|
||||
import { AssetGUID, AssetType } from '../types/AssetTypes';
|
||||
import {
|
||||
IRuntimeCatalog,
|
||||
IRuntimeAssetLocation,
|
||||
IRuntimeBundleInfo
|
||||
} from '../bundle/BundleFormat';
|
||||
|
||||
/**
|
||||
* Runtime Catalog Manager
|
||||
* 运行时目录管理器
|
||||
*
|
||||
* Loads and manages the asset catalog for runtime GUID resolution.
|
||||
*/
|
||||
export class RuntimeCatalog {
|
||||
private _catalog: IRuntimeCatalog | null = null;
|
||||
private _loadedBundles = new Map<string, ArrayBuffer>();
|
||||
private _loadingBundles = new Map<string, Promise<ArrayBuffer>>();
|
||||
private _baseUrl: string = './';
|
||||
|
||||
/**
|
||||
* Set base URL for loading catalog and bundles
|
||||
* 设置加载目录和包的基础 URL
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
this._baseUrl = url.endsWith('/') ? url : `${url}/`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load catalog from URL
|
||||
* 从 URL 加载目录
|
||||
*/
|
||||
async loadCatalog(catalogUrl?: string): Promise<void> {
|
||||
const url = catalogUrl || `${this._baseUrl}asset-catalog.json`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load catalog: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this._catalog = this._parseCatalog(data);
|
||||
|
||||
console.log(`[RuntimeCatalog] Loaded catalog with ${Object.keys(this._catalog.assets).length} assets`);
|
||||
} catch (error) {
|
||||
console.error('[RuntimeCatalog] Failed to load catalog:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with pre-loaded catalog data
|
||||
* 使用预加载的目录数据初始化
|
||||
*/
|
||||
initWithData(catalogData: IRuntimeCatalog): void {
|
||||
this._catalog = catalogData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if catalog is loaded
|
||||
* 检查目录是否已加载
|
||||
*/
|
||||
isLoaded(): boolean {
|
||||
return this._catalog !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset location by GUID
|
||||
* 根据 GUID 获取资产位置
|
||||
*/
|
||||
getAssetLocation(guid: AssetGUID): IRuntimeAssetLocation | null {
|
||||
if (!this._catalog) {
|
||||
console.warn('[RuntimeCatalog] Catalog not loaded');
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._catalog.assets[guid] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset exists in catalog
|
||||
* 检查资产是否存在于目录中
|
||||
*/
|
||||
hasAsset(guid: AssetGUID): boolean {
|
||||
return this._catalog?.assets[guid] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assets of a specific type
|
||||
* 获取特定类型的所有资产
|
||||
*/
|
||||
getAssetsByType(type: AssetType): AssetGUID[] {
|
||||
if (!this._catalog) return [];
|
||||
|
||||
return Object.entries(this._catalog.assets)
|
||||
.filter(([_, loc]) => loc.type === type)
|
||||
.map(([guid]) => guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bundle info
|
||||
* 获取包信息
|
||||
*/
|
||||
getBundleInfo(bundleName: string): IRuntimeBundleInfo | null {
|
||||
return this._catalog?.bundles[bundleName] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a bundle
|
||||
* 加载包
|
||||
*/
|
||||
async loadBundle(bundleName: string): Promise<ArrayBuffer> {
|
||||
// Return cached bundle
|
||||
const cached = this._loadedBundles.get(bundleName);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Return pending load
|
||||
const pending = this._loadingBundles.get(bundleName);
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
|
||||
// Start new load
|
||||
const bundleInfo = this.getBundleInfo(bundleName);
|
||||
if (!bundleInfo) {
|
||||
throw new Error(`Bundle not found in catalog: ${bundleName}`);
|
||||
}
|
||||
|
||||
const loadPromise = this._fetchBundle(bundleInfo);
|
||||
this._loadingBundles.set(bundleName, loadPromise);
|
||||
|
||||
try {
|
||||
const data = await loadPromise;
|
||||
this._loadedBundles.set(bundleName, data);
|
||||
return data;
|
||||
} finally {
|
||||
this._loadingBundles.delete(bundleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset data by GUID
|
||||
* 根据 GUID 加载资产数据
|
||||
*/
|
||||
async loadAssetData(guid: AssetGUID): Promise<ArrayBuffer> {
|
||||
const location = this.getAssetLocation(guid);
|
||||
if (!location) {
|
||||
throw new Error(`Asset not found in catalog: ${guid}`);
|
||||
}
|
||||
|
||||
// Load the bundle containing this asset
|
||||
const bundleData = await this.loadBundle(location.bundle);
|
||||
|
||||
// Extract asset data from bundle
|
||||
return bundleData.slice(location.offset, location.offset + location.size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload bundles marked for preloading
|
||||
* 预加载标记为预加载的包
|
||||
*/
|
||||
async preloadBundles(): Promise<void> {
|
||||
if (!this._catalog) return;
|
||||
|
||||
const preloadPromises: Promise<void>[] = [];
|
||||
|
||||
for (const [name, info] of Object.entries(this._catalog.bundles)) {
|
||||
if (info.preload) {
|
||||
preloadPromises.push(
|
||||
this.loadBundle(name).then(() => {
|
||||
console.log(`[RuntimeCatalog] Preloaded bundle: ${name}`);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(preloadPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload a bundle from memory
|
||||
* 从内存卸载包
|
||||
*/
|
||||
unloadBundle(bundleName: string): void {
|
||||
this._loadedBundles.delete(bundleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all loaded bundles
|
||||
* 清除所有已加载的包
|
||||
*/
|
||||
clearBundles(): void {
|
||||
this._loadedBundles.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get catalog statistics
|
||||
* 获取目录统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
totalAssets: number;
|
||||
totalBundles: number;
|
||||
loadedBundles: number;
|
||||
assetsByType: Record<string, number>;
|
||||
} {
|
||||
if (!this._catalog) {
|
||||
return {
|
||||
totalAssets: 0,
|
||||
totalBundles: 0,
|
||||
loadedBundles: 0,
|
||||
assetsByType: {}
|
||||
};
|
||||
}
|
||||
|
||||
const assetsByType: Record<string, number> = {};
|
||||
for (const loc of Object.values(this._catalog.assets)) {
|
||||
assetsByType[loc.type] = (assetsByType[loc.type] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalAssets: Object.keys(this._catalog.assets).length,
|
||||
totalBundles: Object.keys(this._catalog.bundles).length,
|
||||
loadedBundles: this._loadedBundles.size,
|
||||
assetsByType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse catalog JSON to typed structure
|
||||
* 将目录 JSON 解析为类型化结构
|
||||
*/
|
||||
private _parseCatalog(data: unknown): IRuntimeCatalog {
|
||||
const raw = data as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
version: (raw.version as string) || '1.0',
|
||||
createdAt: (raw.createdAt as number) || Date.now(),
|
||||
bundles: (raw.bundles as Record<string, IRuntimeBundleInfo>) || {},
|
||||
assets: (raw.assets as Record<AssetGUID, IRuntimeAssetLocation>) || {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch bundle data
|
||||
* 获取包数据
|
||||
*/
|
||||
private async _fetchBundle(info: IRuntimeBundleInfo): Promise<ArrayBuffer> {
|
||||
const url = info.url.startsWith('http')
|
||||
? info.url
|
||||
: `${this._baseUrl}${info.url}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load bundle: ${url} (${response.status})`);
|
||||
}
|
||||
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global runtime catalog instance
|
||||
* 全局运行时目录实例
|
||||
*/
|
||||
export const runtimeCatalog = new RuntimeCatalog();
|
||||
Reference in New Issue
Block a user