Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统 * feat(engine): 完善2D渲染管线和编辑器视口功能 * feat(editor): 实现Viewport变换工具系统 * feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示 * feat(editor): 实现Run on Device移动预览功能 * feat(editor): 添加组件属性控制和依赖关系系统 * feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器 * feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(ci): 迁移项目到pnpm并修复CI构建问题 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 移除 network 相关包 * chore: 移除 network 相关包
This commit is contained in:
130
packages/asset-system/src/core/AssetCache.ts
Normal file
130
packages/asset-system/src/core/AssetCache.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Asset cache implementation
|
||||
* 资产缓存实现
|
||||
*/
|
||||
|
||||
import { AssetGUID } from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* Cache entry
|
||||
* 缓存条目
|
||||
*/
|
||||
interface CacheEntry {
|
||||
guid: AssetGUID;
|
||||
asset: unknown;
|
||||
lastAccessTime: number;
|
||||
accessCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset cache implementation
|
||||
* 资产缓存实现
|
||||
*/
|
||||
export class AssetCache {
|
||||
private readonly _cache = new Map<AssetGUID, CacheEntry>();
|
||||
|
||||
constructor() {
|
||||
// 无配置,无限制缓存 / No config, unlimited cache
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached asset
|
||||
* 获取缓存的资产
|
||||
*/
|
||||
get<T = unknown>(guid: AssetGUID): T | null {
|
||||
const entry = this._cache.get(guid);
|
||||
if (!entry) return null;
|
||||
|
||||
// 更新访问信息 / Update access info
|
||||
entry.lastAccessTime = Date.now();
|
||||
entry.accessCount++;
|
||||
|
||||
return entry.asset as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached asset
|
||||
* 设置缓存的资产
|
||||
*/
|
||||
set<T = unknown>(guid: AssetGUID, asset: T): void {
|
||||
const now = Date.now();
|
||||
const entry: CacheEntry = {
|
||||
guid,
|
||||
asset,
|
||||
lastAccessTime: now,
|
||||
accessCount: 1
|
||||
};
|
||||
|
||||
// 如果已存在,更新 / Update if exists
|
||||
const oldEntry = this._cache.get(guid);
|
||||
if (oldEntry) {
|
||||
entry.accessCount = oldEntry.accessCount + 1;
|
||||
}
|
||||
|
||||
this._cache.set(guid, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is cached
|
||||
* 检查资产是否缓存
|
||||
*/
|
||||
has(guid: AssetGUID): boolean {
|
||||
return this._cache.has(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove asset from cache
|
||||
* 从缓存移除资产
|
||||
*/
|
||||
remove(guid: AssetGUID): void {
|
||||
this._cache.delete(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
* 清空所有缓存
|
||||
*/
|
||||
clear(): void {
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache size
|
||||
* 获取缓存大小
|
||||
*/
|
||||
getSize(): number {
|
||||
return this._cache.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached GUIDs
|
||||
* 获取所有缓存的GUID
|
||||
*/
|
||||
getAllGuids(): AssetGUID[] {
|
||||
return Array.from(this._cache.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* 获取缓存统计
|
||||
*/
|
||||
getStatistics(): {
|
||||
count: number;
|
||||
entries: Array<{
|
||||
guid: AssetGUID;
|
||||
accessCount: number;
|
||||
lastAccessTime: number;
|
||||
}>;
|
||||
} {
|
||||
const entries = Array.from(this._cache.values()).map((entry) => ({
|
||||
guid: entry.guid,
|
||||
accessCount: entry.accessCount,
|
||||
lastAccessTime: entry.lastAccessTime
|
||||
}));
|
||||
|
||||
return {
|
||||
count: this._cache.size,
|
||||
entries
|
||||
};
|
||||
}
|
||||
}
|
||||
431
packages/asset-system/src/core/AssetDatabase.ts
Normal file
431
packages/asset-system/src/core/AssetDatabase.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* Asset database for managing asset metadata
|
||||
* 用于管理资产元数据的资产数据库
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetGUID,
|
||||
AssetType,
|
||||
IAssetMetadata,
|
||||
IAssetCatalogEntry
|
||||
} from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* Asset database implementation
|
||||
* 资产数据库实现
|
||||
*/
|
||||
export class AssetDatabase {
|
||||
private readonly _metadata = new Map<AssetGUID, IAssetMetadata>();
|
||||
private readonly _pathToGuid = new Map<string, AssetGUID>();
|
||||
private readonly _typeToGuids = new Map<AssetType, Set<AssetGUID>>();
|
||||
private readonly _labelToGuids = new Map<string, Set<AssetGUID>>();
|
||||
private readonly _dependencies = new Map<AssetGUID, Set<AssetGUID>>();
|
||||
private readonly _dependents = new Map<AssetGUID, Set<AssetGUID>>();
|
||||
|
||||
/**
|
||||
* Add asset to database
|
||||
* 添加资产到数据库
|
||||
*/
|
||||
addAsset(metadata: IAssetMetadata): void {
|
||||
const { guid, path, type, labels, dependencies } = metadata;
|
||||
|
||||
// 存储元数据 / Store metadata
|
||||
this._metadata.set(guid, metadata);
|
||||
this._pathToGuid.set(path, guid);
|
||||
|
||||
// 按类型索引 / Index by type
|
||||
if (!this._typeToGuids.has(type)) {
|
||||
this._typeToGuids.set(type, new Set());
|
||||
}
|
||||
this._typeToGuids.get(type)!.add(guid);
|
||||
|
||||
// 按标签索引 / Index by labels
|
||||
labels.forEach((label) => {
|
||||
if (!this._labelToGuids.has(label)) {
|
||||
this._labelToGuids.set(label, new Set());
|
||||
}
|
||||
this._labelToGuids.get(label)!.add(guid);
|
||||
});
|
||||
|
||||
// 建立依赖关系 / Establish dependencies
|
||||
this.updateDependencies(guid, dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove asset from database
|
||||
* 从数据库移除资产
|
||||
*/
|
||||
removeAsset(guid: AssetGUID): void {
|
||||
const metadata = this._metadata.get(guid);
|
||||
if (!metadata) return;
|
||||
|
||||
// 清理元数据 / Clean up metadata
|
||||
this._metadata.delete(guid);
|
||||
this._pathToGuid.delete(metadata.path);
|
||||
|
||||
// 清理类型索引 / Clean up type index
|
||||
const typeSet = this._typeToGuids.get(metadata.type);
|
||||
if (typeSet) {
|
||||
typeSet.delete(guid);
|
||||
if (typeSet.size === 0) {
|
||||
this._typeToGuids.delete(metadata.type);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理标签索引 / Clean up label indices
|
||||
metadata.labels.forEach((label) => {
|
||||
const labelSet = this._labelToGuids.get(label);
|
||||
if (labelSet) {
|
||||
labelSet.delete(guid);
|
||||
if (labelSet.size === 0) {
|
||||
this._labelToGuids.delete(label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 清理依赖关系 / Clean up dependencies
|
||||
this.clearDependencies(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update asset metadata
|
||||
* 更新资产元数据
|
||||
*/
|
||||
updateAsset(guid: AssetGUID, updates: Partial<IAssetMetadata>): void {
|
||||
const metadata = this._metadata.get(guid);
|
||||
if (!metadata) return;
|
||||
|
||||
// 如果路径改变,更新索引 / Update index if path changed
|
||||
if (updates.path && updates.path !== metadata.path) {
|
||||
this._pathToGuid.delete(metadata.path);
|
||||
this._pathToGuid.set(updates.path, guid);
|
||||
}
|
||||
|
||||
// 如果类型改变,更新索引 / Update index if type changed
|
||||
if (updates.type && updates.type !== metadata.type) {
|
||||
const oldTypeSet = this._typeToGuids.get(metadata.type);
|
||||
if (oldTypeSet) {
|
||||
oldTypeSet.delete(guid);
|
||||
}
|
||||
|
||||
if (!this._typeToGuids.has(updates.type)) {
|
||||
this._typeToGuids.set(updates.type, new Set());
|
||||
}
|
||||
this._typeToGuids.get(updates.type)!.add(guid);
|
||||
}
|
||||
|
||||
// 如果依赖改变,更新关系 / Update relations if dependencies changed
|
||||
if (updates.dependencies) {
|
||||
this.updateDependencies(guid, updates.dependencies);
|
||||
}
|
||||
|
||||
// 合并更新 / Merge updates
|
||||
Object.assign(metadata, updates);
|
||||
metadata.lastModified = Date.now();
|
||||
metadata.version++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset metadata
|
||||
* 获取资产元数据
|
||||
*/
|
||||
getMetadata(guid: AssetGUID): IAssetMetadata | undefined {
|
||||
return this._metadata.get(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata by path
|
||||
* 通过路径获取元数据
|
||||
*/
|
||||
getMetadataByPath(path: string): IAssetMetadata | undefined {
|
||||
const guid = this._pathToGuid.get(path);
|
||||
return guid ? this._metadata.get(guid) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by type
|
||||
* 按类型查找资产
|
||||
*/
|
||||
findAssetsByType(type: AssetType): AssetGUID[] {
|
||||
const guids = this._typeToGuids.get(type);
|
||||
return guids ? Array.from(guids) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by label
|
||||
* 按标签查找资产
|
||||
*/
|
||||
findAssetsByLabel(label: string): AssetGUID[] {
|
||||
const guids = this._labelToGuids.get(label);
|
||||
return guids ? Array.from(guids) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find assets by multiple labels (AND operation)
|
||||
* 按多个标签查找资产(AND操作)
|
||||
*/
|
||||
findAssetsByLabels(labels: string[]): AssetGUID[] {
|
||||
if (labels.length === 0) return [];
|
||||
|
||||
let result: Set<AssetGUID> | null = null;
|
||||
|
||||
for (const label of labels) {
|
||||
const labelGuids = this._labelToGuids.get(label);
|
||||
if (!labelGuids || labelGuids.size === 0) return [];
|
||||
|
||||
if (!result) {
|
||||
result = new Set(labelGuids);
|
||||
} else {
|
||||
// 交集 / Intersection
|
||||
const intersection = new Set<AssetGUID>();
|
||||
labelGuids.forEach((guid) => {
|
||||
if (result!.has(guid)) {
|
||||
intersection.add(guid);
|
||||
}
|
||||
});
|
||||
result = intersection;
|
||||
}
|
||||
}
|
||||
|
||||
return result ? Array.from(result) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search assets by query
|
||||
* 通过查询搜索资产
|
||||
*/
|
||||
searchAssets(query: {
|
||||
name?: string;
|
||||
type?: AssetType;
|
||||
labels?: string[];
|
||||
path?: string;
|
||||
}): AssetGUID[] {
|
||||
let results = Array.from(this._metadata.keys());
|
||||
|
||||
// 按名称过滤 / Filter by name
|
||||
if (query.name) {
|
||||
const nameLower = query.name.toLowerCase();
|
||||
results = results.filter((guid) => {
|
||||
const metadata = this._metadata.get(guid)!;
|
||||
return metadata.name.toLowerCase().includes(nameLower);
|
||||
});
|
||||
}
|
||||
|
||||
// 按类型过滤 / Filter by type
|
||||
if (query.type) {
|
||||
const typeGuids = this._typeToGuids.get(query.type);
|
||||
if (!typeGuids) return [];
|
||||
results = results.filter((guid) => typeGuids.has(guid));
|
||||
}
|
||||
|
||||
// 按标签过滤 / Filter by labels
|
||||
if (query.labels && query.labels.length > 0) {
|
||||
const labelResults = this.findAssetsByLabels(query.labels);
|
||||
const labelSet = new Set(labelResults);
|
||||
results = results.filter((guid) => labelSet.has(guid));
|
||||
}
|
||||
|
||||
// 按路径过滤 / Filter by path
|
||||
if (query.path) {
|
||||
const pathLower = query.path.toLowerCase();
|
||||
results = results.filter((guid) => {
|
||||
const metadata = this._metadata.get(guid)!;
|
||||
return metadata.path.toLowerCase().includes(pathLower);
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset dependencies
|
||||
* 获取资产依赖
|
||||
*/
|
||||
getDependencies(guid: AssetGUID): AssetGUID[] {
|
||||
const deps = this._dependencies.get(guid);
|
||||
return deps ? Array.from(deps) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset dependents (assets that depend on this one)
|
||||
* 获取资产的依赖者(依赖此资产的其他资产)
|
||||
*/
|
||||
getDependents(guid: AssetGUID): AssetGUID[] {
|
||||
const deps = this._dependents.get(guid);
|
||||
return deps ? Array.from(deps) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dependencies recursively
|
||||
* 递归获取所有依赖
|
||||
*/
|
||||
getAllDependencies(guid: AssetGUID, visited = new Set<AssetGUID>()): AssetGUID[] {
|
||||
if (visited.has(guid)) return [];
|
||||
visited.add(guid);
|
||||
|
||||
const result: AssetGUID[] = [];
|
||||
const directDeps = this.getDependencies(guid);
|
||||
|
||||
for (const dep of directDeps) {
|
||||
result.push(dep);
|
||||
const transitiveDeps = this.getAllDependencies(dep, visited);
|
||||
result.push(...transitiveDeps);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for circular dependencies
|
||||
* 检查循环依赖
|
||||
*/
|
||||
hasCircularDependency(guid: AssetGUID): boolean {
|
||||
const visited = new Set<AssetGUID>();
|
||||
const recursionStack = new Set<AssetGUID>();
|
||||
|
||||
const checkCycle = (current: AssetGUID): boolean => {
|
||||
visited.add(current);
|
||||
recursionStack.add(current);
|
||||
|
||||
const deps = this.getDependencies(current);
|
||||
for (const dep of deps) {
|
||||
if (!visited.has(dep)) {
|
||||
if (checkCycle(dep)) return true;
|
||||
} else if (recursionStack.has(dep)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.delete(current);
|
||||
return false;
|
||||
};
|
||||
|
||||
return checkCycle(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dependencies
|
||||
* 更新依赖关系
|
||||
*/
|
||||
private updateDependencies(guid: AssetGUID, newDependencies: AssetGUID[]): void {
|
||||
// 清除旧的依赖关系 / Clear old dependencies
|
||||
this.clearDependencies(guid);
|
||||
|
||||
// 建立新的依赖关系 / Establish new dependencies
|
||||
if (newDependencies.length > 0) {
|
||||
this._dependencies.set(guid, new Set(newDependencies));
|
||||
|
||||
// 更新被依赖关系 / Update dependent relations
|
||||
newDependencies.forEach((dep) => {
|
||||
if (!this._dependents.has(dep)) {
|
||||
this._dependents.set(dep, new Set());
|
||||
}
|
||||
this._dependents.get(dep)!.add(guid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear dependencies
|
||||
* 清除依赖关系
|
||||
*/
|
||||
private clearDependencies(guid: AssetGUID): void {
|
||||
// 清除依赖 / Clear dependencies
|
||||
const deps = this._dependencies.get(guid);
|
||||
if (deps) {
|
||||
deps.forEach((dep) => {
|
||||
const dependents = this._dependents.get(dep);
|
||||
if (dependents) {
|
||||
dependents.delete(guid);
|
||||
if (dependents.size === 0) {
|
||||
this._dependents.delete(dep);
|
||||
}
|
||||
}
|
||||
});
|
||||
this._dependencies.delete(guid);
|
||||
}
|
||||
|
||||
// 清除被依赖 / Clear dependents
|
||||
const dependents = this._dependents.get(guid);
|
||||
if (dependents) {
|
||||
dependents.forEach((dependent) => {
|
||||
const dependencies = this._dependencies.get(dependent);
|
||||
if (dependencies) {
|
||||
dependencies.delete(guid);
|
||||
if (dependencies.size === 0) {
|
||||
this._dependencies.delete(dependent);
|
||||
}
|
||||
}
|
||||
});
|
||||
this._dependents.delete(guid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
* 获取数据库统计
|
||||
*/
|
||||
getStatistics(): {
|
||||
totalAssets: number;
|
||||
assetsByType: Map<AssetType, number>;
|
||||
totalDependencies: number;
|
||||
assetsWithDependencies: number;
|
||||
circularDependencies: number;
|
||||
} {
|
||||
const assetsByType = new Map<AssetType, number>();
|
||||
this._typeToGuids.forEach((guids, type) => {
|
||||
assetsByType.set(type, guids.size);
|
||||
});
|
||||
|
||||
let circularDependencies = 0;
|
||||
this._metadata.forEach((_, guid) => {
|
||||
if (this.hasCircularDependency(guid)) {
|
||||
circularDependencies++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
totalAssets: this._metadata.size,
|
||||
assetsByType,
|
||||
totalDependencies: Array.from(this._dependencies.values()).reduce(
|
||||
(sum, deps) => sum + deps.size,
|
||||
0
|
||||
),
|
||||
assetsWithDependencies: this._dependencies.size,
|
||||
circularDependencies
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export to catalog entries
|
||||
* 导出为目录条目
|
||||
*/
|
||||
exportToCatalog(): IAssetCatalogEntry[] {
|
||||
const entries: IAssetCatalogEntry[] = [];
|
||||
|
||||
this._metadata.forEach((metadata) => {
|
||||
entries.push({
|
||||
guid: metadata.guid,
|
||||
path: metadata.path,
|
||||
type: metadata.type,
|
||||
size: metadata.size,
|
||||
hash: metadata.hash
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear database
|
||||
* 清空数据库
|
||||
*/
|
||||
clear(): void {
|
||||
this._metadata.clear();
|
||||
this._pathToGuid.clear();
|
||||
this._typeToGuids.clear();
|
||||
this._labelToGuids.clear();
|
||||
this._dependencies.clear();
|
||||
this._dependents.clear();
|
||||
}
|
||||
}
|
||||
193
packages/asset-system/src/core/AssetLoadQueue.ts
Normal file
193
packages/asset-system/src/core/AssetLoadQueue.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Priority-based asset loading queue
|
||||
* 基于优先级的资产加载队列
|
||||
*/
|
||||
|
||||
import { AssetGUID, IAssetLoadOptions } from '../types/AssetTypes';
|
||||
import { IAssetLoadQueue } from '../interfaces/IAssetManager';
|
||||
|
||||
/**
|
||||
* Queue item
|
||||
* 队列项
|
||||
*/
|
||||
interface QueueItem {
|
||||
guid: AssetGUID;
|
||||
priority: number;
|
||||
options?: IAssetLoadOptions;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset load queue implementation
|
||||
* 资产加载队列实现
|
||||
*/
|
||||
export class AssetLoadQueue implements IAssetLoadQueue {
|
||||
private readonly _queue: QueueItem[] = [];
|
||||
private readonly _guidToIndex = new Map<AssetGUID, number>();
|
||||
|
||||
/**
|
||||
* Add to queue
|
||||
* 添加到队列
|
||||
*/
|
||||
enqueue(guid: AssetGUID, priority: number, options?: IAssetLoadOptions): void {
|
||||
// 检查是否已在队列中 / Check if already in queue
|
||||
if (this._guidToIndex.has(guid)) {
|
||||
this.reprioritize(guid, priority);
|
||||
return;
|
||||
}
|
||||
|
||||
const item: QueueItem = {
|
||||
guid,
|
||||
priority,
|
||||
options,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 二分查找插入位置 / Binary search for insertion position
|
||||
const index = this.findInsertIndex(priority);
|
||||
this._queue.splice(index, 0, item);
|
||||
|
||||
// 更新索引映射 / Update index mapping
|
||||
this.updateIndices(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove from queue
|
||||
* 从队列移除
|
||||
*/
|
||||
dequeue(): { guid: AssetGUID; options?: IAssetLoadOptions } | null {
|
||||
if (this._queue.length === 0) return null;
|
||||
|
||||
const item = this._queue.shift();
|
||||
if (!item) return null;
|
||||
|
||||
// 更新索引映射 / Update index mapping
|
||||
this._guidToIndex.delete(item.guid);
|
||||
this.updateIndices(0);
|
||||
|
||||
return {
|
||||
guid: item.guid,
|
||||
options: item.options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if queue is empty
|
||||
* 检查队列是否为空
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this._queue.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue size
|
||||
* 获取队列大小
|
||||
*/
|
||||
getSize(): number {
|
||||
return this._queue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear queue
|
||||
* 清空队列
|
||||
*/
|
||||
clear(): void {
|
||||
this._queue.length = 0;
|
||||
this._guidToIndex.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprioritize item
|
||||
* 重新设置优先级
|
||||
*/
|
||||
reprioritize(guid: AssetGUID, newPriority: number): void {
|
||||
const index = this._guidToIndex.get(guid);
|
||||
if (index === undefined) return;
|
||||
|
||||
const item = this._queue[index];
|
||||
if (!item || item.priority === newPriority) return;
|
||||
|
||||
// 移除旧项 / Remove old item
|
||||
this._queue.splice(index, 1);
|
||||
this._guidToIndex.delete(guid);
|
||||
|
||||
// 重新插入 / Reinsert with new priority
|
||||
item.priority = newPriority;
|
||||
const newIndex = this.findInsertIndex(newPriority);
|
||||
this._queue.splice(newIndex, 0, item);
|
||||
|
||||
// 更新索引 / Update indices
|
||||
this.updateIndices(Math.min(index, newIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find insertion index for priority
|
||||
* 查找优先级的插入索引
|
||||
*/
|
||||
private findInsertIndex(priority: number): number {
|
||||
let left = 0;
|
||||
let right = this._queue.length;
|
||||
|
||||
while (left < right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
// 高优先级在前 / Higher priority first
|
||||
if (this._queue[mid].priority >= priority) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update indices after modification
|
||||
* 修改后更新索引
|
||||
*/
|
||||
private updateIndices(startIndex: number): void {
|
||||
for (let i = startIndex; i < this._queue.length; i++) {
|
||||
this._guidToIndex.set(this._queue[i].guid, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue items (for debugging)
|
||||
* 获取队列项(用于调试)
|
||||
*/
|
||||
getItems(): ReadonlyArray<{
|
||||
guid: AssetGUID;
|
||||
priority: number;
|
||||
waitTime: number;
|
||||
}> {
|
||||
const now = Date.now();
|
||||
return this._queue.map((item) => ({
|
||||
guid: item.guid,
|
||||
priority: item.priority,
|
||||
waitTime: now - item.timestamp
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific item from queue
|
||||
* 从队列中移除特定项
|
||||
*/
|
||||
remove(guid: AssetGUID): boolean {
|
||||
const index = this._guidToIndex.get(guid);
|
||||
if (index === undefined) return false;
|
||||
|
||||
this._queue.splice(index, 1);
|
||||
this._guidToIndex.delete(guid);
|
||||
this.updateIndices(index);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if guid is in queue
|
||||
* 检查guid是否在队列中
|
||||
*/
|
||||
contains(guid: AssetGUID): boolean {
|
||||
return this._guidToIndex.has(guid);
|
||||
}
|
||||
}
|
||||
541
packages/asset-system/src/core/AssetManager.ts
Normal file
541
packages/asset-system/src/core/AssetManager.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
/**
|
||||
* Asset manager implementation
|
||||
* 资产管理器实现
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetGUID,
|
||||
AssetHandle,
|
||||
AssetType,
|
||||
AssetState,
|
||||
IAssetLoadOptions,
|
||||
IAssetLoadResult,
|
||||
IAssetReferenceInfo,
|
||||
IAssetPreloadGroup,
|
||||
IAssetLoadProgress,
|
||||
IAssetMetadata,
|
||||
AssetLoadError,
|
||||
IAssetCatalog
|
||||
} from '../types/AssetTypes';
|
||||
import {
|
||||
IAssetManager,
|
||||
IAssetLoadQueue
|
||||
} from '../interfaces/IAssetManager';
|
||||
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
|
||||
import { AssetCache } from './AssetCache';
|
||||
import { AssetLoadQueue } from './AssetLoadQueue';
|
||||
import { AssetLoaderFactory } from '../loaders/AssetLoaderFactory';
|
||||
import { AssetDatabase } from './AssetDatabase';
|
||||
|
||||
/**
|
||||
* Asset entry in the manager
|
||||
* 管理器中的资产条目
|
||||
*/
|
||||
interface AssetEntry {
|
||||
guid: AssetGUID;
|
||||
handle: AssetHandle;
|
||||
asset: unknown;
|
||||
metadata: IAssetMetadata;
|
||||
state: AssetState;
|
||||
referenceCount: number;
|
||||
lastAccessTime: number;
|
||||
loadPromise?: Promise<IAssetLoadResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset manager implementation
|
||||
* 资产管理器实现
|
||||
*/
|
||||
export class AssetManager implements IAssetManager {
|
||||
private readonly _assets = new Map<AssetGUID, AssetEntry>();
|
||||
private readonly _handleToGuid = new Map<AssetHandle, AssetGUID>();
|
||||
private readonly _pathToGuid = new Map<string, AssetGUID>();
|
||||
private readonly _cache: AssetCache;
|
||||
private readonly _loadQueue: IAssetLoadQueue;
|
||||
private readonly _loaderFactory: IAssetLoaderFactory;
|
||||
private readonly _database: AssetDatabase;
|
||||
|
||||
private _nextHandle: AssetHandle = 1;
|
||||
|
||||
private _statistics = {
|
||||
loadedCount: 0,
|
||||
failedCount: 0
|
||||
};
|
||||
|
||||
private _isDisposed = false;
|
||||
private _loadingCount = 0;
|
||||
|
||||
constructor(catalog?: IAssetCatalog) {
|
||||
this._cache = new AssetCache();
|
||||
this._loadQueue = new AssetLoadQueue();
|
||||
this._loaderFactory = new AssetLoaderFactory();
|
||||
this._database = new AssetDatabase();
|
||||
|
||||
// 如果提供了目录,初始化数据库 / Initialize database if catalog provided
|
||||
if (catalog) {
|
||||
this.initializeFromCatalog(catalog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize from catalog
|
||||
* 从目录初始化
|
||||
*/
|
||||
private initializeFromCatalog(catalog: IAssetCatalog): void {
|
||||
catalog.entries.forEach((entry, guid) => {
|
||||
const metadata: IAssetMetadata = {
|
||||
guid,
|
||||
path: entry.path,
|
||||
type: entry.type,
|
||||
name: entry.path.split('/').pop() || '',
|
||||
size: entry.size,
|
||||
hash: entry.hash,
|
||||
dependencies: [],
|
||||
labels: [],
|
||||
tags: new Map(),
|
||||
lastModified: Date.now(),
|
||||
version: 1
|
||||
};
|
||||
|
||||
this._database.addAsset(metadata);
|
||||
this._pathToGuid.set(entry.path, guid);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset by GUID
|
||||
* 通过GUID加载资产
|
||||
*/
|
||||
async loadAsset<T = unknown>(
|
||||
guid: AssetGUID,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>> {
|
||||
// 检查是否已加载 / Check if already loaded
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry) {
|
||||
if (entry.state === AssetState.Loaded && !options?.forceReload) {
|
||||
entry.lastAccessTime = Date.now();
|
||||
return {
|
||||
asset: entry.asset as T,
|
||||
handle: entry.handle,
|
||||
metadata: entry.metadata,
|
||||
loadTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (entry.state === AssetState.Loading && entry.loadPromise) {
|
||||
return entry.loadPromise as Promise<IAssetLoadResult<T>>;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取元数据 / Get metadata
|
||||
const metadata = this._database.getMetadata(guid);
|
||||
if (!metadata) {
|
||||
throw AssetLoadError.fileNotFound(guid, 'Unknown');
|
||||
}
|
||||
|
||||
// 创建加载器 / Create loader
|
||||
const loader = this._loaderFactory.createLoader(metadata.type);
|
||||
if (!loader) {
|
||||
throw AssetLoadError.unsupportedType(guid, metadata.type);
|
||||
}
|
||||
|
||||
// 开始加载 / Start loading
|
||||
const loadStartTime = performance.now();
|
||||
const newEntry: AssetEntry = {
|
||||
guid,
|
||||
handle: this._nextHandle++,
|
||||
asset: null,
|
||||
metadata,
|
||||
state: AssetState.Loading,
|
||||
referenceCount: 0,
|
||||
lastAccessTime: Date.now()
|
||||
};
|
||||
|
||||
this._assets.set(guid, newEntry);
|
||||
this._handleToGuid.set(newEntry.handle, guid);
|
||||
this._loadingCount++;
|
||||
|
||||
// 创建加载Promise / Create loading promise
|
||||
const loadPromise = this.performLoad<T>(loader, metadata, options, loadStartTime, newEntry);
|
||||
newEntry.loadPromise = loadPromise;
|
||||
|
||||
try {
|
||||
const result = await loadPromise;
|
||||
return result;
|
||||
} catch (error) {
|
||||
this._statistics.failedCount++;
|
||||
newEntry.state = AssetState.Failed;
|
||||
throw error;
|
||||
} finally {
|
||||
this._loadingCount--;
|
||||
delete newEntry.loadPromise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform asset loading
|
||||
* 执行资产加载
|
||||
*/
|
||||
private async performLoad<T>(
|
||||
loader: IAssetLoader,
|
||||
metadata: IAssetMetadata,
|
||||
options: IAssetLoadOptions | undefined,
|
||||
startTime: number,
|
||||
entry: AssetEntry
|
||||
): Promise<IAssetLoadResult<T>> {
|
||||
// 加载依赖 / Load dependencies
|
||||
if (metadata.dependencies.length > 0) {
|
||||
await this.loadDependencies(metadata.dependencies, options);
|
||||
}
|
||||
|
||||
// 执行加载 / Execute loading
|
||||
const result = await loader.load(metadata.path, metadata, options);
|
||||
|
||||
// 更新条目 / Update entry
|
||||
entry.asset = result.asset;
|
||||
entry.state = AssetState.Loaded;
|
||||
|
||||
// 缓存资产 / Cache asset
|
||||
this._cache.set(metadata.guid, result.asset);
|
||||
|
||||
// 更新统计 / Update statistics
|
||||
this._statistics.loadedCount++;
|
||||
|
||||
const loadResult: IAssetLoadResult<T> = {
|
||||
asset: result.asset as T,
|
||||
handle: entry.handle,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
|
||||
return loadResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dependencies
|
||||
* 加载依赖
|
||||
*/
|
||||
private async loadDependencies(
|
||||
dependencies: AssetGUID[],
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<void> {
|
||||
const promises = dependencies.map((dep) => this.loadAsset(dep, options));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset by path
|
||||
* 通过路径加载资产
|
||||
*/
|
||||
async loadAssetByPath<T = unknown>(
|
||||
path: string,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>> {
|
||||
const guid = this._pathToGuid.get(path);
|
||||
if (!guid) {
|
||||
// 尝试从数据库查找 / Try to find from database
|
||||
let metadata = this._database.getMetadataByPath(path);
|
||||
if (!metadata) {
|
||||
// 动态创建元数据 / Create metadata dynamically
|
||||
const fileExt = path.substring(path.lastIndexOf('.')).toLowerCase();
|
||||
let assetType = AssetType.Custom;
|
||||
|
||||
// 根据文件扩展名确定资产类型 / Determine asset type by file extension
|
||||
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(fileExt)) {
|
||||
assetType = AssetType.Texture;
|
||||
} else if (['.json'].includes(fileExt)) {
|
||||
assetType = AssetType.Json;
|
||||
} else if (['.txt', '.md', '.xml', '.yaml'].includes(fileExt)) {
|
||||
assetType = AssetType.Text;
|
||||
}
|
||||
|
||||
// 生成唯一GUID / Generate unique GUID
|
||||
const dynamicGuid = `dynamic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
metadata = {
|
||||
guid: dynamicGuid,
|
||||
path: path,
|
||||
type: assetType,
|
||||
name: path.split('/').pop() || path.split('\\').pop() || 'unnamed',
|
||||
size: 0, // 动态加载时未知大小 / Unknown size for dynamic loading
|
||||
hash: '',
|
||||
dependencies: [],
|
||||
labels: [],
|
||||
tags: new Map(),
|
||||
lastModified: Date.now(),
|
||||
version: 1
|
||||
};
|
||||
|
||||
// 注册到数据库 / Register to database
|
||||
this._database.addAsset(metadata);
|
||||
this._pathToGuid.set(path, metadata.guid);
|
||||
} else {
|
||||
this._pathToGuid.set(path, metadata.guid);
|
||||
}
|
||||
|
||||
return this.loadAsset<T>(metadata.guid, options);
|
||||
}
|
||||
|
||||
return this.loadAsset<T>(guid, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load multiple assets
|
||||
* 批量加载资产
|
||||
*/
|
||||
async loadAssets(
|
||||
guids: AssetGUID[],
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<Map<AssetGUID, IAssetLoadResult>> {
|
||||
const results = new Map<AssetGUID, IAssetLoadResult>();
|
||||
|
||||
// 并行加载所有资产 / Load all assets in parallel
|
||||
const promises = guids.map(async (guid) => {
|
||||
try {
|
||||
const result = await this.loadAsset(guid, options);
|
||||
results.set(guid, result);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load asset ${guid}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload asset group
|
||||
* 预加载资产组
|
||||
*/
|
||||
async preloadGroup(
|
||||
group: IAssetPreloadGroup,
|
||||
onProgress?: (progress: IAssetLoadProgress) => void
|
||||
): Promise<void> {
|
||||
const totalCount = group.assets.length;
|
||||
let loadedCount = 0;
|
||||
let loadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
// 计算总大小 / Calculate total size
|
||||
for (const guid of group.assets) {
|
||||
const metadata = this._database.getMetadata(guid);
|
||||
if (metadata) {
|
||||
totalBytes += metadata.size;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载每个资产 / Load each asset
|
||||
for (const guid of group.assets) {
|
||||
const metadata = this._database.getMetadata(guid);
|
||||
if (!metadata) continue;
|
||||
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
currentAsset: metadata.name,
|
||||
loadedCount,
|
||||
totalCount,
|
||||
loadedBytes,
|
||||
totalBytes,
|
||||
progress: loadedCount / totalCount
|
||||
});
|
||||
}
|
||||
|
||||
await this.loadAsset(guid, { priority: group.priority });
|
||||
|
||||
loadedCount++;
|
||||
loadedBytes += metadata.size;
|
||||
}
|
||||
|
||||
// 最终进度 / Final progress
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
currentAsset: '',
|
||||
loadedCount: totalCount,
|
||||
totalCount,
|
||||
loadedBytes: totalBytes,
|
||||
totalBytes,
|
||||
progress: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get loaded asset
|
||||
* 获取已加载的资产
|
||||
*/
|
||||
getAsset<T = unknown>(guid: AssetGUID): T | null {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry && entry.state === AssetState.Loaded) {
|
||||
entry.lastAccessTime = Date.now();
|
||||
return entry.asset as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset by handle
|
||||
* 通过句柄获取资产
|
||||
*/
|
||||
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null {
|
||||
const guid = this._handleToGuid.get(handle);
|
||||
if (!guid) return null;
|
||||
return this.getAsset<T>(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
isLoaded(guid: AssetGUID): boolean {
|
||||
const entry = this._assets.get(guid);
|
||||
return entry?.state === AssetState.Loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset state
|
||||
* 获取资产状态
|
||||
*/
|
||||
getAssetState(guid: AssetGUID): AssetState {
|
||||
const entry = this._assets.get(guid);
|
||||
return entry?.state || AssetState.Unloaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload asset
|
||||
* 卸载资产
|
||||
*/
|
||||
unloadAsset(guid: AssetGUID): void {
|
||||
const entry = this._assets.get(guid);
|
||||
if (!entry) return;
|
||||
|
||||
// 检查引用计数 / Check reference count
|
||||
if (entry.referenceCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取加载器以释放资源 / Get loader to dispose resources
|
||||
const loader = this._loaderFactory.createLoader(entry.metadata.type);
|
||||
if (loader) {
|
||||
loader.dispose(entry.asset);
|
||||
}
|
||||
|
||||
// 清理条目 / Clean up entry
|
||||
this._handleToGuid.delete(entry.handle);
|
||||
this._assets.delete(guid);
|
||||
this._cache.remove(guid);
|
||||
|
||||
// 更新统计 / Update statistics
|
||||
this._statistics.loadedCount--;
|
||||
|
||||
entry.state = AssetState.Unloaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload all assets
|
||||
* 卸载所有资产
|
||||
*/
|
||||
unloadAllAssets(): void {
|
||||
const guids = Array.from(this._assets.keys());
|
||||
guids.forEach((guid) => this.unloadAsset(guid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload unused assets
|
||||
* 卸载未使用的资产
|
||||
*/
|
||||
unloadUnusedAssets(): void {
|
||||
const guids = Array.from(this._assets.keys());
|
||||
guids.forEach((guid) => {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry && entry.referenceCount === 0) {
|
||||
this.unloadAsset(guid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add reference to asset
|
||||
* 增加资产引用
|
||||
*/
|
||||
addReference(guid: AssetGUID): void {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry) {
|
||||
entry.referenceCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reference from asset
|
||||
* 移除资产引用
|
||||
*/
|
||||
removeReference(guid: AssetGUID): void {
|
||||
const entry = this._assets.get(guid);
|
||||
if (entry && entry.referenceCount > 0) {
|
||||
entry.referenceCount--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reference info
|
||||
* 获取引用信息
|
||||
*/
|
||||
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null {
|
||||
const entry = this._assets.get(guid);
|
||||
if (!entry) return null;
|
||||
|
||||
return {
|
||||
guid,
|
||||
handle: entry.handle,
|
||||
referenceCount: entry.referenceCount,
|
||||
lastAccessTime: entry.lastAccessTime,
|
||||
state: entry.state
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void {
|
||||
this._loaderFactory.registerLoader(type, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset statistics
|
||||
* 获取资产统计信息
|
||||
*/
|
||||
getStatistics(): { loadedCount: number; loadQueue: number; failedCount: number } {
|
||||
return {
|
||||
loadedCount: this._statistics.loadedCount,
|
||||
loadQueue: this._loadQueue.getSize(),
|
||||
failedCount: this._statistics.failedCount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* 清空缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose manager
|
||||
* 释放管理器
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._isDisposed) return;
|
||||
|
||||
this.unloadAllAssets();
|
||||
this._cache.clear();
|
||||
this._loadQueue.clear();
|
||||
this._assets.clear();
|
||||
this._handleToGuid.clear();
|
||||
this._pathToGuid.clear();
|
||||
|
||||
this._isDisposed = true;
|
||||
}
|
||||
}
|
||||
243
packages/asset-system/src/core/AssetPathResolver.ts
Normal file
243
packages/asset-system/src/core/AssetPathResolver.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Asset path resolver for different platforms and protocols
|
||||
* 不同平台和协议的资产路径解析器
|
||||
*/
|
||||
|
||||
import { AssetPlatform } from '../types/AssetTypes';
|
||||
import { PathValidator } from '../utils/PathValidator';
|
||||
|
||||
/**
|
||||
* Asset path resolver configuration
|
||||
* 资产路径解析器配置
|
||||
*/
|
||||
export interface IAssetPathConfig {
|
||||
/** Base URL for web assets | Web资产的基础URL */
|
||||
baseUrl?: string;
|
||||
|
||||
/** Asset directory path | 资产目录路径 */
|
||||
assetDir?: string;
|
||||
|
||||
/** Asset host for asset:// protocol | 资产协议的主机名 */
|
||||
assetHost?: string;
|
||||
|
||||
/** Current platform | 当前平台 */
|
||||
platform?: AssetPlatform;
|
||||
|
||||
/** Custom path transformer | 自定义路径转换器 */
|
||||
pathTransformer?: (path: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset path resolver
|
||||
* 资产路径解析器
|
||||
*/
|
||||
export class AssetPathResolver {
|
||||
private config: IAssetPathConfig;
|
||||
|
||||
constructor(config: IAssetPathConfig = {}) {
|
||||
this.config = {
|
||||
baseUrl: '',
|
||||
assetDir: 'assets',
|
||||
platform: AssetPlatform.H5,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(config: Partial<IAssetPathConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve asset path to full URL
|
||||
* 解析资产路径为完整URL
|
||||
*/
|
||||
resolve(path: string): string {
|
||||
// Validate input path
|
||||
const validation = PathValidator.validate(path);
|
||||
if (!validation.valid) {
|
||||
console.warn(`Invalid asset path: ${path} - ${validation.reason}`);
|
||||
// Sanitize the path instead of throwing
|
||||
path = PathValidator.sanitize(path);
|
||||
if (!path) {
|
||||
throw new Error(`Cannot resolve invalid path: ${validation.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Already a full URL
|
||||
// 已经是完整URL
|
||||
if (this.isAbsoluteUrl(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Data URL
|
||||
// 数据URL
|
||||
if (path.startsWith('data:')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Normalize the path
|
||||
path = PathValidator.normalize(path);
|
||||
|
||||
// Apply custom transformer if provided
|
||||
// 应用自定义转换器(如果提供)
|
||||
if (this.config.pathTransformer) {
|
||||
path = this.config.pathTransformer(path);
|
||||
// Re-validate after transformation
|
||||
const postTransform = PathValidator.validate(path);
|
||||
if (!postTransform.valid) {
|
||||
throw new Error(`Path transformer produced invalid path: ${postTransform.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-specific resolution
|
||||
// 平台特定解析
|
||||
switch (this.config.platform) {
|
||||
case AssetPlatform.H5:
|
||||
return this.resolveH5Path(path);
|
||||
|
||||
case AssetPlatform.WeChat:
|
||||
return this.resolveWeChatPath(path);
|
||||
|
||||
case AssetPlatform.Playable:
|
||||
return this.resolvePlayablePath(path);
|
||||
|
||||
case AssetPlatform.Android:
|
||||
case AssetPlatform.iOS:
|
||||
return this.resolveMobilePath(path);
|
||||
|
||||
case AssetPlatform.Editor:
|
||||
return this.resolveEditorPath(path);
|
||||
|
||||
default:
|
||||
return this.resolveH5Path(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for H5 platform
|
||||
* 解析H5平台路径
|
||||
*/
|
||||
private resolveH5Path(path: string): string {
|
||||
// Remove leading slash if present
|
||||
// 移除开头的斜杠(如果存在)
|
||||
path = path.replace(/^\//, '');
|
||||
|
||||
// Combine with base URL and asset directory
|
||||
// 与基础URL和资产目录结合
|
||||
const base = this.config.baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
|
||||
const assetDir = this.config.assetDir || 'assets';
|
||||
|
||||
return `${base}/${assetDir}/${path}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for WeChat Mini Game
|
||||
* 解析微信小游戏路径
|
||||
*/
|
||||
private resolveWeChatPath(path: string): string {
|
||||
// WeChat mini games use relative paths
|
||||
// 微信小游戏使用相对路径
|
||||
return `${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for Playable Ads platform
|
||||
* 解析试玩广告平台路径
|
||||
*/
|
||||
private resolvePlayablePath(path: string): string {
|
||||
// Playable ads typically use base64 embedded resources or relative paths
|
||||
// 试玩广告通常使用base64内嵌资源或相对路径
|
||||
|
||||
// If custom transformer is provided (e.g., for base64 encoding)
|
||||
// 如果提供了自定义转换器(例如用于base64编码)
|
||||
if (this.config.pathTransformer) {
|
||||
return this.config.pathTransformer(path);
|
||||
}
|
||||
|
||||
// Default to relative path without directory prefix
|
||||
// 默认使用不带目录前缀的相对路径
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for mobile platform (Android/iOS)
|
||||
* 解析移动平台路径(Android/iOS)
|
||||
*/
|
||||
private resolveMobilePath(path: string): string {
|
||||
// Mobile platforms use relative paths or file:// protocol
|
||||
// 移动平台使用相对路径或file://协议
|
||||
return `./${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path for Editor platform (Tauri)
|
||||
* 解析编辑器平台路径(Tauri)
|
||||
*/
|
||||
private resolveEditorPath(path: string): string {
|
||||
// For Tauri editor, use pathTransformer if provided
|
||||
// 对于Tauri编辑器,使用pathTransformer(如果提供)
|
||||
if (this.config.pathTransformer) {
|
||||
return this.config.pathTransformer(path);
|
||||
}
|
||||
|
||||
// Use configurable asset host or default to 'localhost'
|
||||
// 使用可配置的资产主机或默认为 'localhost'
|
||||
const host = this.config.assetHost || 'localhost';
|
||||
const sanitizedPath = PathValidator.sanitize(path);
|
||||
return `asset://${host}/${sanitizedPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is absolute URL
|
||||
* 检查路径是否为绝对URL
|
||||
*/
|
||||
private isAbsoluteUrl(path: string): boolean {
|
||||
return /^(https?:\/\/|file:\/\/|asset:\/\/)/.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset directory from path
|
||||
* 从路径获取资产目录
|
||||
*/
|
||||
getAssetDirectory(path: string): string {
|
||||
const resolved = this.resolve(path);
|
||||
const lastSlash = resolved.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? resolved.substring(0, lastSlash) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset filename from path
|
||||
* 从路径获取资产文件名
|
||||
*/
|
||||
getAssetFilename(path: string): string {
|
||||
const resolved = this.resolve(path);
|
||||
const lastSlash = resolved.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? resolved.substring(lastSlash + 1) : resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join paths
|
||||
* 连接路径
|
||||
*/
|
||||
join(...paths: string[]): string {
|
||||
return paths.join('/').replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path
|
||||
* 规范化路径
|
||||
*/
|
||||
normalize(path: string): string {
|
||||
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global asset path resolver instance
|
||||
* 全局资产路径解析器实例
|
||||
*/
|
||||
export const globalPathResolver = new AssetPathResolver();
|
||||
338
packages/asset-system/src/core/AssetReference.ts
Normal file
338
packages/asset-system/src/core/AssetReference.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* Asset reference for lazy loading
|
||||
* 用于懒加载的资产引用
|
||||
*/
|
||||
|
||||
import { AssetGUID, IAssetLoadOptions, AssetState } from '../types/AssetTypes';
|
||||
import { IAssetManager } from '../interfaces/IAssetManager';
|
||||
|
||||
/**
|
||||
* Asset reference class for lazy loading
|
||||
* 懒加载资产引用类
|
||||
*/
|
||||
export class AssetReference<T = unknown> {
|
||||
private _guid: AssetGUID;
|
||||
private _asset?: T;
|
||||
private _loadPromise?: Promise<T>;
|
||||
private _manager?: IAssetManager;
|
||||
private _isReleased = false;
|
||||
private _autoRelease = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(guid: AssetGUID, manager?: IAssetManager) {
|
||||
this._guid = guid;
|
||||
this._manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset GUID
|
||||
* 获取资产GUID
|
||||
*/
|
||||
get guid(): AssetGUID {
|
||||
return this._guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
get isLoaded(): boolean {
|
||||
return this._asset !== undefined && !this._isReleased;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset synchronously (returns null if not loaded)
|
||||
* 同步获取资产(如果未加载则返回null)
|
||||
*/
|
||||
get asset(): T | null {
|
||||
return this._asset ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set asset manager
|
||||
* 设置资产管理器
|
||||
*/
|
||||
setManager(manager: IAssetManager): void {
|
||||
this._manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset asynchronously
|
||||
* 异步加载资产
|
||||
*/
|
||||
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
|
||||
if (this._isReleased) {
|
||||
throw new Error(`Asset reference ${this._guid} has been released`);
|
||||
}
|
||||
|
||||
// 如果已经加载,直接返回 / Return if already loaded
|
||||
if (this._asset !== undefined) {
|
||||
return this._asset;
|
||||
}
|
||||
|
||||
// 如果正在加载,返回加载Promise / Return loading promise if loading
|
||||
if (this._loadPromise) {
|
||||
return this._loadPromise;
|
||||
}
|
||||
|
||||
if (!this._manager) {
|
||||
throw new Error('Asset manager not set for AssetReference');
|
||||
}
|
||||
|
||||
// 开始加载 / Start loading
|
||||
this._loadPromise = this.performLoad(options);
|
||||
|
||||
try {
|
||||
const asset = await this._loadPromise;
|
||||
return asset;
|
||||
} finally {
|
||||
this._loadPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform asset loading
|
||||
* 执行资产加载
|
||||
*/
|
||||
private async performLoad(options?: IAssetLoadOptions): Promise<T> {
|
||||
if (!this._manager) {
|
||||
throw new Error('Asset manager not set');
|
||||
}
|
||||
|
||||
const result = await this._manager.loadAsset<T>(this._guid, options);
|
||||
this._asset = result.asset;
|
||||
|
||||
// 增加引用计数 / Increase reference count
|
||||
this._manager.addReference(this._guid);
|
||||
|
||||
return this._asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release asset reference
|
||||
* 释放资产引用
|
||||
*/
|
||||
release(): void {
|
||||
if (this._isReleased) return;
|
||||
|
||||
if (this._manager && this._asset !== undefined) {
|
||||
// 减少引用计数 / Decrease reference count
|
||||
this._manager.removeReference(this._guid);
|
||||
|
||||
// 如果引用计数为0,可以考虑卸载 / Consider unloading if reference count is 0
|
||||
const refInfo = this._manager.getReferenceInfo(this._guid);
|
||||
if (refInfo && refInfo.referenceCount === 0 && this._autoRelease) {
|
||||
this._manager.unloadAsset(this._guid);
|
||||
}
|
||||
}
|
||||
|
||||
this._asset = undefined;
|
||||
this._isReleased = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set auto-release mode
|
||||
* 设置自动释放模式
|
||||
*/
|
||||
setAutoRelease(autoRelease: boolean): void {
|
||||
this._autoRelease = autoRelease;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate reference
|
||||
* 验证引用
|
||||
*/
|
||||
validate(): boolean {
|
||||
if (!this._manager) return false;
|
||||
|
||||
const state = this._manager.getAssetState(this._guid);
|
||||
return state !== AssetState.Failed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset state
|
||||
* 获取资产状态
|
||||
*/
|
||||
getState(): AssetState {
|
||||
if (this._isReleased) return AssetState.Unloaded;
|
||||
if (!this._manager) return AssetState.Unloaded;
|
||||
|
||||
return this._manager.getAssetState(this._guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone reference
|
||||
* 克隆引用
|
||||
*/
|
||||
clone(): AssetReference<T> {
|
||||
const newRef = new AssetReference<T>(this._guid, this._manager);
|
||||
newRef.setAutoRelease(this._autoRelease);
|
||||
return newRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to JSON
|
||||
* 转换为JSON
|
||||
*/
|
||||
toJSON(): { guid: AssetGUID } {
|
||||
return { guid: this._guid };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from JSON
|
||||
* 从JSON创建
|
||||
*/
|
||||
static fromJSON<T = unknown>(
|
||||
json: { guid: AssetGUID },
|
||||
manager?: IAssetManager
|
||||
): AssetReference<T> {
|
||||
return new AssetReference<T>(json.guid, manager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Weak asset reference that doesn't prevent unloading
|
||||
* 不阻止卸载的弱资产引用
|
||||
*/
|
||||
export class WeakAssetReference<T = unknown> {
|
||||
private _guid: AssetGUID;
|
||||
private _manager?: IAssetManager;
|
||||
|
||||
constructor(guid: AssetGUID, manager?: IAssetManager) {
|
||||
this._guid = guid;
|
||||
this._manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset GUID
|
||||
* 获取资产GUID
|
||||
*/
|
||||
get guid(): AssetGUID {
|
||||
return this._guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try get asset without loading
|
||||
* 尝试获取资产而不加载
|
||||
*/
|
||||
tryGet(): T | null {
|
||||
if (!this._manager) return null;
|
||||
return this._manager.getAsset<T>(this._guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset if not loaded
|
||||
* 如果未加载则加载资产
|
||||
*/
|
||||
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
|
||||
if (!this._manager) {
|
||||
throw new Error('Asset manager not set');
|
||||
}
|
||||
|
||||
const result = await this._manager.loadAsset<T>(this._guid, options);
|
||||
// 不增加引用计数 / Don't increase reference count for weak reference
|
||||
return result.asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
isLoaded(): boolean {
|
||||
if (!this._manager) return false;
|
||||
return this._manager.isLoaded(this._guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set asset manager
|
||||
* 设置资产管理器
|
||||
*/
|
||||
setManager(manager: IAssetManager): void {
|
||||
this._manager = manager;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset reference array for managing multiple references
|
||||
* 用于管理多个引用的资产引用数组
|
||||
*/
|
||||
export class AssetReferenceArray<T = unknown> {
|
||||
private _references: AssetReference<T>[] = [];
|
||||
private _manager?: IAssetManager;
|
||||
|
||||
constructor(guids: AssetGUID[] = [], manager?: IAssetManager) {
|
||||
this._manager = manager;
|
||||
this._references = guids.map((guid) => new AssetReference<T>(guid, manager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add reference
|
||||
* 添加引用
|
||||
*/
|
||||
add(guid: AssetGUID): void {
|
||||
this._references.push(new AssetReference<T>(guid, this._manager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reference
|
||||
* 移除引用
|
||||
*/
|
||||
remove(guid: AssetGUID): boolean {
|
||||
const index = this._references.findIndex((ref) => ref.guid === guid);
|
||||
if (index >= 0) {
|
||||
this._references[index].release();
|
||||
this._references.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all assets
|
||||
* 加载所有资产
|
||||
*/
|
||||
async loadAllAsync(options?: IAssetLoadOptions): Promise<T[]> {
|
||||
const promises = this._references.map((ref) => ref.loadAsync(options));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all references
|
||||
* 释放所有引用
|
||||
*/
|
||||
releaseAll(): void {
|
||||
this._references.forEach((ref) => ref.release());
|
||||
this._references = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded assets
|
||||
* 获取所有已加载的资产
|
||||
*/
|
||||
getLoadedAssets(): T[] {
|
||||
return this._references
|
||||
.filter((ref) => ref.isLoaded)
|
||||
.map((ref) => ref.asset!)
|
||||
.filter((asset) => asset !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reference count
|
||||
* 获取引用数量
|
||||
*/
|
||||
get count(): number {
|
||||
return this._references.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set asset manager
|
||||
* 设置资产管理器
|
||||
*/
|
||||
setManager(manager: IAssetManager): void {
|
||||
this._manager = manager;
|
||||
this._references.forEach((ref) => ref.setManager(manager));
|
||||
}
|
||||
}
|
||||
51
packages/asset-system/src/index.ts
Normal file
51
packages/asset-system/src/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Asset System for ECS Framework
|
||||
* ECS框架的资产系统
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types/AssetTypes';
|
||||
|
||||
// Interfaces
|
||||
export * from './interfaces/IAssetLoader';
|
||||
export * from './interfaces/IAssetManager';
|
||||
|
||||
// Core
|
||||
export { AssetManager } from './core/AssetManager';
|
||||
export { AssetCache } from './core/AssetCache';
|
||||
export { AssetDatabase } from './core/AssetDatabase';
|
||||
export { AssetLoadQueue } from './core/AssetLoadQueue';
|
||||
export { AssetReference, WeakAssetReference, AssetReferenceArray } from './core/AssetReference';
|
||||
export { AssetPathResolver, globalPathResolver } from './core/AssetPathResolver';
|
||||
export type { IAssetPathConfig } from './core/AssetPathResolver';
|
||||
|
||||
// Loaders
|
||||
export { AssetLoaderFactory } from './loaders/AssetLoaderFactory';
|
||||
export { TextureLoader } from './loaders/TextureLoader';
|
||||
export { JsonLoader } from './loaders/JsonLoader';
|
||||
export { TextLoader } from './loaders/TextLoader';
|
||||
export { BinaryLoader } from './loaders/BinaryLoader';
|
||||
|
||||
// Integration
|
||||
export { EngineIntegration } from './integration/EngineIntegration';
|
||||
export type { IEngineBridge } from './integration/EngineIntegration';
|
||||
|
||||
// Default instance
|
||||
import { AssetManager } from './core/AssetManager';
|
||||
|
||||
/**
|
||||
* Default asset manager instance
|
||||
* 默认资产管理器实例
|
||||
*/
|
||||
export const assetManager = new AssetManager();
|
||||
|
||||
/**
|
||||
* Initialize asset system with catalog
|
||||
* 使用目录初始化资产系统
|
||||
*/
|
||||
export function initializeAssetSystem(catalog?: any): AssetManager {
|
||||
if (catalog) {
|
||||
return new AssetManager(catalog);
|
||||
}
|
||||
return assetManager;
|
||||
}
|
||||
217
packages/asset-system/src/integration/EngineIntegration.ts
Normal file
217
packages/asset-system/src/integration/EngineIntegration.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Engine integration for asset system
|
||||
* 资产系统的引擎集成
|
||||
*/
|
||||
|
||||
import { AssetManager } from '../core/AssetManager';
|
||||
import { AssetGUID } from '../types/AssetTypes';
|
||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* Engine bridge interface
|
||||
* 引擎桥接接口
|
||||
*/
|
||||
export interface IEngineBridge {
|
||||
/**
|
||||
* Load texture to GPU
|
||||
* 加载纹理到GPU
|
||||
*/
|
||||
loadTexture(id: number, url: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Load multiple textures
|
||||
* 批量加载纹理
|
||||
*/
|
||||
loadTextures(requests: Array<{ id: number; url: string }>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Unload texture from GPU
|
||||
* 从GPU卸载纹理
|
||||
*/
|
||||
unloadTexture(id: number): void;
|
||||
|
||||
/**
|
||||
* Get texture info
|
||||
* 获取纹理信息
|
||||
*/
|
||||
getTextureInfo(id: number): { width: number; height: number } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset system engine integration
|
||||
* 资产系统引擎集成
|
||||
*/
|
||||
export class EngineIntegration {
|
||||
private _assetManager: AssetManager;
|
||||
private _engineBridge?: IEngineBridge;
|
||||
private _textureIdMap = new Map<AssetGUID, number>();
|
||||
private _pathToTextureId = new Map<string, number>();
|
||||
|
||||
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
|
||||
this._assetManager = assetManager;
|
||||
this._engineBridge = engineBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set engine bridge
|
||||
* 设置引擎桥接
|
||||
*/
|
||||
setEngineBridge(bridge: IEngineBridge): void {
|
||||
this._engineBridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture for component
|
||||
* 为组件加载纹理
|
||||
*/
|
||||
async loadTextureForComponent(texturePath: string): Promise<number> {
|
||||
// 检查是否已有纹理ID / Check if texture ID exists
|
||||
const existingId = this._pathToTextureId.get(texturePath);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 通过资产系统加载 / Load through asset system
|
||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, texturePath);
|
||||
}
|
||||
|
||||
// 缓存映射 / Cache mapping
|
||||
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture by GUID
|
||||
* 通过GUID加载纹理
|
||||
*/
|
||||
async loadTextureByGuid(guid: AssetGUID): Promise<number> {
|
||||
// 检查是否已有纹理ID / Check if texture ID exists
|
||||
const existingId = this._textureIdMap.get(guid);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 通过资产系统加载 / Load through asset system
|
||||
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
const metadata = result.metadata;
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, metadata.path);
|
||||
}
|
||||
|
||||
// 缓存映射 / Cache mapping
|
||||
this._textureIdMap.set(guid, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch load textures
|
||||
* 批量加载纹理
|
||||
*/
|
||||
async loadTexturesBatch(paths: string[]): Promise<Map<string, number>> {
|
||||
const results = new Map<string, number>();
|
||||
|
||||
// 收集需要加载的纹理 / Collect textures to load
|
||||
const toLoad: string[] = [];
|
||||
for (const path of paths) {
|
||||
const existingId = this._pathToTextureId.get(path);
|
||||
if (existingId) {
|
||||
results.set(path, existingId);
|
||||
} else {
|
||||
toLoad.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (toLoad.length === 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// 并行加载所有纹理 / Load all textures in parallel
|
||||
const loadPromises = toLoad.map(async (path) => {
|
||||
try {
|
||||
const id = await this.loadTextureForComponent(path);
|
||||
results.set(path, id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load texture: ${path}`, error);
|
||||
results.set(path, 0); // 使用默认纹理ID / Use default texture ID
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(loadPromises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload texture
|
||||
* 卸载纹理
|
||||
*/
|
||||
unloadTexture(textureId: number): void {
|
||||
// 从引擎卸载 / Unload from engine
|
||||
if (this._engineBridge) {
|
||||
this._engineBridge.unloadTexture(textureId);
|
||||
}
|
||||
|
||||
// 清理映射 / Clean up mappings
|
||||
for (const [path, id] of this._pathToTextureId.entries()) {
|
||||
if (id === textureId) {
|
||||
this._pathToTextureId.delete(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [guid, id] of this._textureIdMap.entries()) {
|
||||
if (id === textureId) {
|
||||
this._textureIdMap.delete(guid);
|
||||
// 也从资产管理器卸载 / Also unload from asset manager
|
||||
this._assetManager.unloadAsset(guid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture ID for path
|
||||
* 获取路径的纹理ID
|
||||
*/
|
||||
getTextureId(path: string): number | null {
|
||||
return this._pathToTextureId.get(path) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload textures for scene
|
||||
* 为场景预加载纹理
|
||||
*/
|
||||
async preloadSceneTextures(texturePaths: string[]): Promise<void> {
|
||||
await this.loadTexturesBatch(texturePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all texture mappings
|
||||
* 清空所有纹理映射
|
||||
*/
|
||||
clearTextureMappings(): void {
|
||||
this._textureIdMap.clear();
|
||||
this._pathToTextureId.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
loadedTextures: number;
|
||||
} {
|
||||
return {
|
||||
loadedTextures: this._pathToTextureId.size
|
||||
};
|
||||
}
|
||||
}
|
||||
222
packages/asset-system/src/interfaces/IAssetLoader.ts
Normal file
222
packages/asset-system/src/interfaces/IAssetLoader.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Asset loader interfaces
|
||||
* 资产加载器接口
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
AssetGUID,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult
|
||||
} from '../types/AssetTypes';
|
||||
|
||||
/**
|
||||
* Base asset loader interface
|
||||
* 基础资产加载器接口
|
||||
*/
|
||||
export interface IAssetLoader<T = unknown> {
|
||||
/** 支持的资产类型 / Supported asset type */
|
||||
readonly supportedType: AssetType;
|
||||
|
||||
/** 支持的文件扩展名 / Supported file extensions */
|
||||
readonly supportedExtensions: string[];
|
||||
|
||||
/**
|
||||
* Load an asset from the given path
|
||||
* 从指定路径加载资产
|
||||
*/
|
||||
load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>>;
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, metadata: IAssetMetadata): boolean;
|
||||
|
||||
/**
|
||||
* Dispose loaded asset and free resources
|
||||
* 释放已加载的资产并释放资源
|
||||
*/
|
||||
dispose(asset: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loader factory interface
|
||||
* 资产加载器工厂接口
|
||||
*/
|
||||
export interface IAssetLoaderFactory {
|
||||
/**
|
||||
* Create loader for specific asset type
|
||||
* 为特定资产类型创建加载器
|
||||
*/
|
||||
createLoader(type: AssetType): IAssetLoader | null;
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
*/
|
||||
unregisterLoader(type: AssetType): void;
|
||||
|
||||
/**
|
||||
* Check if loader exists for type
|
||||
* 检查类型是否有加载器
|
||||
*/
|
||||
hasLoader(type: AssetType): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture asset interface
|
||||
* 纹理资产接口
|
||||
*/
|
||||
export interface ITextureAsset {
|
||||
/** WebGL纹理ID / WebGL texture ID */
|
||||
textureId: number;
|
||||
/** 宽度 / Width */
|
||||
width: number;
|
||||
/** 高度 / Height */
|
||||
height: number;
|
||||
/** 格式 / Format */
|
||||
format: 'rgba' | 'rgb' | 'alpha';
|
||||
/** 是否有Mipmap / Has mipmaps */
|
||||
hasMipmaps: boolean;
|
||||
/** 原始数据(如果可用) / Raw image data if available */
|
||||
data?: ImageData | HTMLImageElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesh asset interface
|
||||
* 网格资产接口
|
||||
*/
|
||||
export interface IMeshAsset {
|
||||
/** 顶点数据 / Vertex data */
|
||||
vertices: Float32Array;
|
||||
/** 索引数据 / Index data */
|
||||
indices: Uint16Array | Uint32Array;
|
||||
/** 法线数据 / Normal data */
|
||||
normals?: Float32Array;
|
||||
/** UV坐标 / UV coordinates */
|
||||
uvs?: Float32Array;
|
||||
/** 切线数据 / Tangent data */
|
||||
tangents?: Float32Array;
|
||||
/** 边界盒 / Axis-aligned bounding box */
|
||||
bounds: {
|
||||
min: [number, number, number];
|
||||
max: [number, number, number];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio asset interface
|
||||
* 音频资产接口
|
||||
*/
|
||||
export interface IAudioAsset {
|
||||
/** 音频缓冲区 / Audio buffer */
|
||||
buffer: AudioBuffer;
|
||||
/** 时长(秒) / Duration in seconds */
|
||||
duration: number;
|
||||
/** 采样率 / Sample rate */
|
||||
sampleRate: number;
|
||||
/** 声道数 / Number of channels */
|
||||
channels: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material asset interface
|
||||
* 材质资产接口
|
||||
*/
|
||||
export interface IMaterialAsset {
|
||||
/** 着色器名称 / Shader name */
|
||||
shader: string;
|
||||
/** 材质属性 / Material properties */
|
||||
properties: Map<string, unknown>;
|
||||
/** 纹理映射 / Texture slot mappings */
|
||||
textures: Map<string, AssetGUID>;
|
||||
/** 渲染状态 / Render states */
|
||||
renderStates: {
|
||||
cullMode?: 'none' | 'front' | 'back';
|
||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
|
||||
depthTest?: boolean;
|
||||
depthWrite?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefab asset interface
|
||||
* 预制体资产接口
|
||||
*/
|
||||
export interface IPrefabAsset {
|
||||
/** 根实体数据 / Serialized entity hierarchy */
|
||||
root: unknown;
|
||||
/** 包含的组件类型 / Component types used in prefab */
|
||||
componentTypes: string[];
|
||||
/** 引用的资产 / All referenced assets */
|
||||
referencedAssets: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene asset interface
|
||||
* 场景资产接口
|
||||
*/
|
||||
export interface ISceneAsset {
|
||||
/** 场景名称 / Scene name */
|
||||
name: string;
|
||||
/** 实体列表 / Serialized entity list */
|
||||
entities: unknown[];
|
||||
/** 场景设置 / Scene settings */
|
||||
settings: {
|
||||
/** 环境光 / Ambient light */
|
||||
ambientLight?: [number, number, number];
|
||||
/** 雾效 / Fog settings */
|
||||
fog?: {
|
||||
enabled: boolean;
|
||||
color: [number, number, number];
|
||||
density: number;
|
||||
};
|
||||
/** 天空盒 / Skybox asset */
|
||||
skybox?: AssetGUID;
|
||||
};
|
||||
/** 引用的资产 / All referenced assets */
|
||||
referencedAssets: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON asset interface
|
||||
* JSON资产接口
|
||||
*/
|
||||
export interface IJsonAsset {
|
||||
/** JSON数据 / JSON data */
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text asset interface
|
||||
* 文本资产接口
|
||||
*/
|
||||
export interface ITextAsset {
|
||||
/** 文本内容 / Text content */
|
||||
content: string;
|
||||
/** 编码格式 / Encoding */
|
||||
encoding: 'utf8' | 'utf16' | 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary asset interface
|
||||
* 二进制资产接口
|
||||
*/
|
||||
export interface IBinaryAsset {
|
||||
/** 二进制数据 / Binary data */
|
||||
data: ArrayBuffer;
|
||||
/** MIME类型 / MIME type */
|
||||
mimeType?: string;
|
||||
}
|
||||
328
packages/asset-system/src/interfaces/IAssetManager.ts
Normal file
328
packages/asset-system/src/interfaces/IAssetManager.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Asset manager interfaces
|
||||
* 资产管理器接口
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetGUID,
|
||||
AssetHandle,
|
||||
AssetType,
|
||||
AssetState,
|
||||
IAssetLoadOptions,
|
||||
IAssetLoadResult,
|
||||
IAssetReferenceInfo,
|
||||
IAssetPreloadGroup,
|
||||
IAssetLoadProgress
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader } from './IAssetLoader';
|
||||
|
||||
/**
|
||||
* Asset manager interface
|
||||
* 资产管理器接口
|
||||
*/
|
||||
export interface IAssetManager {
|
||||
/**
|
||||
* Load asset by GUID
|
||||
* 通过GUID加载资产
|
||||
*/
|
||||
loadAsset<T = unknown>(
|
||||
guid: AssetGUID,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>>;
|
||||
|
||||
/**
|
||||
* Load asset by path
|
||||
* 通过路径加载资产
|
||||
*/
|
||||
loadAssetByPath<T = unknown>(
|
||||
path: string,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<T>>;
|
||||
|
||||
/**
|
||||
* Load multiple assets
|
||||
* 批量加载资产
|
||||
*/
|
||||
loadAssets(
|
||||
guids: AssetGUID[],
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<Map<AssetGUID, IAssetLoadResult>>;
|
||||
|
||||
/**
|
||||
* Preload asset group
|
||||
* 预加载资产组
|
||||
*/
|
||||
preloadGroup(
|
||||
group: IAssetPreloadGroup,
|
||||
onProgress?: (progress: IAssetLoadProgress) => void
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get loaded asset
|
||||
* 获取已加载的资产
|
||||
*/
|
||||
getAsset<T = unknown>(guid: AssetGUID): T | null;
|
||||
|
||||
/**
|
||||
* Get asset by handle
|
||||
* 通过句柄获取资产
|
||||
*/
|
||||
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null;
|
||||
|
||||
/**
|
||||
* Check if asset is loaded
|
||||
* 检查资产是否已加载
|
||||
*/
|
||||
isLoaded(guid: AssetGUID): boolean;
|
||||
|
||||
/**
|
||||
* Get asset state
|
||||
* 获取资产状态
|
||||
*/
|
||||
getAssetState(guid: AssetGUID): AssetState;
|
||||
|
||||
/**
|
||||
* Unload asset
|
||||
* 卸载资产
|
||||
*/
|
||||
unloadAsset(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Unload all assets
|
||||
* 卸载所有资产
|
||||
*/
|
||||
unloadAllAssets(): void;
|
||||
|
||||
/**
|
||||
* Unload unused assets
|
||||
* 卸载未使用的资产
|
||||
*/
|
||||
unloadUnusedAssets(): void;
|
||||
|
||||
/**
|
||||
* Add reference to asset
|
||||
* 增加资产引用
|
||||
*/
|
||||
addReference(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Remove reference from asset
|
||||
* 移除资产引用
|
||||
*/
|
||||
removeReference(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Get reference info
|
||||
* 获取引用信息
|
||||
*/
|
||||
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null;
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
||||
|
||||
/**
|
||||
* Get asset statistics
|
||||
* 获取资产统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
loadedCount: number;
|
||||
loadQueue: number;
|
||||
failedCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* 清空缓存
|
||||
*/
|
||||
clearCache(): void;
|
||||
|
||||
/**
|
||||
* Dispose manager
|
||||
* 释放管理器
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset cache interface
|
||||
* 资产缓存接口
|
||||
*/
|
||||
export interface IAssetCache {
|
||||
/**
|
||||
* Get cached asset
|
||||
* 获取缓存的资产
|
||||
*/
|
||||
get<T = unknown>(guid: AssetGUID): T | null;
|
||||
|
||||
/**
|
||||
* Set cached asset
|
||||
* 设置缓存的资产
|
||||
*/
|
||||
set<T = unknown>(guid: AssetGUID, asset: T, size: number): void;
|
||||
|
||||
/**
|
||||
* Check if asset is cached
|
||||
* 检查资产是否已缓存
|
||||
*/
|
||||
has(guid: AssetGUID): boolean;
|
||||
|
||||
/**
|
||||
* Remove from cache
|
||||
* 从缓存中移除
|
||||
*/
|
||||
remove(guid: AssetGUID): void;
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
* 清空所有缓存
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Get cache size
|
||||
* 获取缓存大小
|
||||
*/
|
||||
getSize(): number;
|
||||
|
||||
/**
|
||||
* Get cached asset count
|
||||
* 获取缓存资产数量
|
||||
*/
|
||||
getCount(): number;
|
||||
|
||||
/**
|
||||
* Evict assets based on policy
|
||||
* 根据策略驱逐资产
|
||||
*/
|
||||
evict(targetSize: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading queue interface
|
||||
* 资产加载队列接口
|
||||
*/
|
||||
export interface IAssetLoadQueue {
|
||||
/**
|
||||
* Add to queue
|
||||
* 添加到队列
|
||||
*/
|
||||
enqueue(
|
||||
guid: AssetGUID,
|
||||
priority: number,
|
||||
options?: IAssetLoadOptions
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Remove from queue
|
||||
* 从队列移除
|
||||
*/
|
||||
dequeue(): {
|
||||
guid: AssetGUID;
|
||||
options?: IAssetLoadOptions;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* Check if queue is empty
|
||||
* 检查队列是否为空
|
||||
*/
|
||||
isEmpty(): boolean;
|
||||
|
||||
/**
|
||||
* Get queue size
|
||||
* 获取队列大小
|
||||
*/
|
||||
getSize(): number;
|
||||
|
||||
/**
|
||||
* Clear queue
|
||||
* 清空队列
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Reprioritize item
|
||||
* 重新设置优先级
|
||||
*/
|
||||
reprioritize(guid: AssetGUID, newPriority: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset dependency resolver interface
|
||||
* 资产依赖解析器接口
|
||||
*/
|
||||
export interface IAssetDependencyResolver {
|
||||
/**
|
||||
* Resolve dependencies for asset
|
||||
* 解析资产的依赖
|
||||
*/
|
||||
resolveDependencies(guid: AssetGUID): Promise<AssetGUID[]>;
|
||||
|
||||
/**
|
||||
* Get direct dependencies
|
||||
* 获取直接依赖
|
||||
*/
|
||||
getDirectDependencies(guid: AssetGUID): AssetGUID[];
|
||||
|
||||
/**
|
||||
* Get all dependencies recursively
|
||||
* 递归获取所有依赖
|
||||
*/
|
||||
getAllDependencies(guid: AssetGUID): AssetGUID[];
|
||||
|
||||
/**
|
||||
* Check for circular dependencies
|
||||
* 检查循环依赖
|
||||
*/
|
||||
hasCircularDependency(guid: AssetGUID): boolean;
|
||||
|
||||
/**
|
||||
* Build dependency graph
|
||||
* 构建依赖图
|
||||
*/
|
||||
buildDependencyGraph(guids: AssetGUID[]): Map<AssetGUID, AssetGUID[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset streaming interface
|
||||
* 资产流式加载接口
|
||||
*/
|
||||
export interface IAssetStreaming {
|
||||
/**
|
||||
* Start streaming assets
|
||||
* 开始流式加载资产
|
||||
*/
|
||||
startStreaming(guids: AssetGUID[]): void;
|
||||
|
||||
/**
|
||||
* Stop streaming
|
||||
* 停止流式加载
|
||||
*/
|
||||
stopStreaming(): void;
|
||||
|
||||
/**
|
||||
* Pause streaming
|
||||
* 暂停流式加载
|
||||
*/
|
||||
pauseStreaming(): void;
|
||||
|
||||
/**
|
||||
* Resume streaming
|
||||
* 恢复流式加载
|
||||
*/
|
||||
resumeStreaming(): void;
|
||||
|
||||
/**
|
||||
* Set streaming budget per frame
|
||||
* 设置每帧流式加载预算
|
||||
*/
|
||||
setFrameBudget(milliseconds: number): void;
|
||||
|
||||
/**
|
||||
* Get streaming progress
|
||||
* 获取流式加载进度
|
||||
*/
|
||||
getProgress(): IAssetLoadProgress;
|
||||
}
|
||||
90
packages/asset-system/src/loaders/AssetLoaderFactory.ts
Normal file
90
packages/asset-system/src/loaders/AssetLoaderFactory.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Asset loader factory implementation
|
||||
* 资产加载器工厂实现
|
||||
*/
|
||||
|
||||
import { AssetType } from '../types/AssetTypes';
|
||||
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
|
||||
import { TextureLoader } from './TextureLoader';
|
||||
import { JsonLoader } from './JsonLoader';
|
||||
import { TextLoader } from './TextLoader';
|
||||
import { BinaryLoader } from './BinaryLoader';
|
||||
|
||||
/**
|
||||
* Asset loader factory
|
||||
* 资产加载器工厂
|
||||
*/
|
||||
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
||||
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
||||
|
||||
constructor() {
|
||||
// 注册默认加载器 / Register default loaders
|
||||
this.registerDefaultLoaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register default loaders
|
||||
* 注册默认加载器
|
||||
*/
|
||||
private registerDefaultLoaders(): void {
|
||||
// 纹理加载器 / Texture loader
|
||||
this._loaders.set(AssetType.Texture, new TextureLoader());
|
||||
|
||||
// JSON加载器 / JSON loader
|
||||
this._loaders.set(AssetType.Json, new JsonLoader());
|
||||
|
||||
// 文本加载器 / Text loader
|
||||
this._loaders.set(AssetType.Text, new TextLoader());
|
||||
|
||||
// 二进制加载器 / Binary loader
|
||||
this._loaders.set(AssetType.Binary, new BinaryLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create loader for specific asset type
|
||||
* 为特定资产类型创建加载器
|
||||
*/
|
||||
createLoader(type: AssetType): IAssetLoader | null {
|
||||
return this._loaders.get(type) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom loader
|
||||
* 注册自定义加载器
|
||||
*/
|
||||
registerLoader(type: AssetType, loader: IAssetLoader): void {
|
||||
this._loaders.set(type, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister loader
|
||||
* 注销加载器
|
||||
*/
|
||||
unregisterLoader(type: AssetType): void {
|
||||
this._loaders.delete(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if loader exists for type
|
||||
* 检查类型是否有加载器
|
||||
*/
|
||||
hasLoader(type: AssetType): boolean {
|
||||
return this._loaders.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered loaders
|
||||
* 获取所有注册的加载器
|
||||
*/
|
||||
getRegisteredTypes(): AssetType[] {
|
||||
return Array.from(this._loaders.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all loaders
|
||||
* 清空所有加载器
|
||||
*/
|
||||
clear(): void {
|
||||
this._loaders.clear();
|
||||
}
|
||||
}
|
||||
165
packages/asset-system/src/loaders/BinaryLoader.ts
Normal file
165
packages/asset-system/src/loaders/BinaryLoader.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Binary asset loader
|
||||
* 二进制资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, IBinaryAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* Binary loader implementation
|
||||
* 二进制加载器实现
|
||||
*/
|
||||
export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
|
||||
readonly supportedType = AssetType.Binary;
|
||||
readonly supportedExtensions = [
|
||||
'.bin', '.dat', '.raw', '.bytes',
|
||||
'.wasm', '.so', '.dll', '.dylib'
|
||||
];
|
||||
|
||||
/**
|
||||
* Load binary asset
|
||||
* 加载二进制资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<IBinaryAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取MIME类型 / Get MIME type
|
||||
const mimeType = response.headers.get('content-type') || undefined;
|
||||
|
||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
|
||||
// 读取响应 / Read response
|
||||
let data: ArrayBuffer;
|
||||
if (options?.onProgress && total > 0) {
|
||||
data = await this.readResponseWithProgress(response, total, options.onProgress);
|
||||
} else {
|
||||
data = await response.arrayBuffer();
|
||||
}
|
||||
|
||||
const asset: IBinaryAsset = {
|
||||
data,
|
||||
mimeType
|
||||
};
|
||||
|
||||
return {
|
||||
asset,
|
||||
handle: 0,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new AssetLoadError(
|
||||
`Failed to load binary: ${error.message}`,
|
||||
metadata.guid,
|
||||
AssetType.Binary,
|
||||
error
|
||||
);
|
||||
}
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with timeout
|
||||
* 带超时的fetch
|
||||
*/
|
||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read response with progress
|
||||
* 带进度读取响应
|
||||
*/
|
||||
private async readResponseWithProgress(
|
||||
response: Response,
|
||||
total: number,
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<ArrayBuffer> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
let receivedLength = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
// 报告进度 / Report progress
|
||||
onProgress(receivedLength / total);
|
||||
}
|
||||
|
||||
// 合并chunks到ArrayBuffer / Merge chunks into ArrayBuffer
|
||||
const result = new Uint8Array(receivedLength);
|
||||
let position = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: IBinaryAsset): void {
|
||||
// ArrayBuffer无法直接释放,但可以清空引用 / Can't directly release ArrayBuffer, but clear reference
|
||||
(asset as any).data = null;
|
||||
}
|
||||
}
|
||||
162
packages/asset-system/src/loaders/JsonLoader.ts
Normal file
162
packages/asset-system/src/loaders/JsonLoader.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* JSON asset loader
|
||||
* JSON资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* JSON loader implementation
|
||||
* JSON加载器实现
|
||||
*/
|
||||
export class JsonLoader implements IAssetLoader<IJsonAsset> {
|
||||
readonly supportedType = AssetType.Json;
|
||||
readonly supportedExtensions = ['.json', '.jsonc'];
|
||||
|
||||
/**
|
||||
* Load JSON asset
|
||||
* 加载JSON资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<IJsonAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
|
||||
// 读取响应 / Read response
|
||||
let jsonData: unknown;
|
||||
if (options?.onProgress && total > 0) {
|
||||
jsonData = await this.readResponseWithProgress(response, total, options.onProgress);
|
||||
} else {
|
||||
jsonData = await response.json();
|
||||
}
|
||||
|
||||
const asset: IJsonAsset = {
|
||||
data: jsonData
|
||||
};
|
||||
|
||||
return {
|
||||
asset,
|
||||
handle: 0,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new AssetLoadError(
|
||||
`Failed to load JSON: ${error.message}`,
|
||||
metadata.guid,
|
||||
AssetType.Json,
|
||||
error
|
||||
);
|
||||
}
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with timeout
|
||||
* 带超时的fetch
|
||||
*/
|
||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read response with progress
|
||||
* 带进度读取响应
|
||||
*/
|
||||
private async readResponseWithProgress(
|
||||
response: Response,
|
||||
total: number,
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<unknown> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
let receivedLength = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
// 报告进度 / Report progress
|
||||
onProgress(receivedLength / total);
|
||||
}
|
||||
|
||||
// 合并chunks / Merge chunks
|
||||
const allChunks = new Uint8Array(receivedLength);
|
||||
let position = 0;
|
||||
for (const chunk of chunks) {
|
||||
allChunks.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
// 解码为字符串并解析JSON / Decode to string and parse JSON
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const jsonString = decoder.decode(allChunks);
|
||||
return JSON.parse(jsonString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: IJsonAsset): void {
|
||||
// JSON资产通常不需要特殊清理 / JSON assets usually don't need special cleanup
|
||||
// 但可以清空引用以帮助GC / But can clear references to help GC
|
||||
(asset as any).data = null;
|
||||
}
|
||||
}
|
||||
172
packages/asset-system/src/loaders/TextLoader.ts
Normal file
172
packages/asset-system/src/loaders/TextLoader.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Text asset loader
|
||||
* 文本资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* Text loader implementation
|
||||
* 文本加载器实现
|
||||
*/
|
||||
export class TextLoader implements IAssetLoader<ITextAsset> {
|
||||
readonly supportedType = AssetType.Text;
|
||||
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
|
||||
|
||||
/**
|
||||
* Load text asset
|
||||
* 加载文本资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<ITextAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
|
||||
// 读取响应 / Read response
|
||||
let content: string;
|
||||
if (options?.onProgress && total > 0) {
|
||||
content = await this.readResponseWithProgress(response, total, options.onProgress);
|
||||
} else {
|
||||
content = await response.text();
|
||||
}
|
||||
|
||||
// 检测编码 / Detect encoding
|
||||
const encoding = this.detectEncoding(content);
|
||||
|
||||
const asset: ITextAsset = {
|
||||
content,
|
||||
encoding
|
||||
};
|
||||
|
||||
return {
|
||||
asset,
|
||||
handle: 0,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new AssetLoadError(
|
||||
`Failed to load text: ${error.message}`,
|
||||
metadata.guid,
|
||||
AssetType.Text,
|
||||
error
|
||||
);
|
||||
}
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with timeout
|
||||
* 带超时的fetch
|
||||
*/
|
||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read response with progress
|
||||
* 带进度读取响应
|
||||
*/
|
||||
private async readResponseWithProgress(
|
||||
response: Response,
|
||||
total: number,
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<string> {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let result = '';
|
||||
let receivedLength = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
receivedLength += value.length;
|
||||
result += decoder.decode(value, { stream: true });
|
||||
|
||||
// 报告进度 / Report progress
|
||||
onProgress(receivedLength / total);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect text encoding
|
||||
* 检测文本编码
|
||||
*/
|
||||
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
|
||||
// 简单的编码检测 / Simple encoding detection
|
||||
// 检查是否包含非ASCII字符 / Check for non-ASCII characters
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const charCode = content.charCodeAt(i);
|
||||
if (charCode > 127) {
|
||||
// 包含非ASCII字符,可能是UTF-8或UTF-16 / Contains non-ASCII, likely UTF-8 or UTF-16
|
||||
return charCode > 255 ? 'utf16' : 'utf8';
|
||||
}
|
||||
}
|
||||
return 'ascii';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITextAsset): void {
|
||||
// 清空内容以帮助GC / Clear content to help GC
|
||||
(asset as any).content = '';
|
||||
}
|
||||
}
|
||||
216
packages/asset-system/src/loaders/TextureLoader.ts
Normal file
216
packages/asset-system/src/loaders/TextureLoader.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Texture asset loader
|
||||
* 纹理资产加载器
|
||||
*/
|
||||
|
||||
import {
|
||||
AssetType,
|
||||
IAssetLoadOptions,
|
||||
IAssetMetadata,
|
||||
IAssetLoadResult,
|
||||
AssetLoadError
|
||||
} from '../types/AssetTypes';
|
||||
import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader';
|
||||
|
||||
/**
|
||||
* Texture loader implementation
|
||||
* 纹理加载器实现
|
||||
*/
|
||||
export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
||||
readonly supportedType = AssetType.Texture;
|
||||
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
||||
|
||||
private static _nextTextureId = 1;
|
||||
private readonly _loadedTextures = new Map<string, ITextureAsset>();
|
||||
|
||||
/**
|
||||
* Load texture asset
|
||||
* 加载纹理资产
|
||||
*/
|
||||
async load(
|
||||
path: string,
|
||||
metadata: IAssetMetadata,
|
||||
options?: IAssetLoadOptions
|
||||
): Promise<IAssetLoadResult<ITextureAsset>> {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 检查缓存 / Check cache
|
||||
if (!options?.forceReload && this._loadedTextures.has(path)) {
|
||||
const cached = this._loadedTextures.get(path)!;
|
||||
return {
|
||||
asset: cached,
|
||||
handle: cached.textureId,
|
||||
metadata,
|
||||
loadTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建图像元素 / Create image element
|
||||
const image = await this.loadImage(path, options);
|
||||
|
||||
// 创建纹理资产 / Create texture asset
|
||||
const textureAsset: ITextureAsset = {
|
||||
textureId: TextureLoader._nextTextureId++,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
format: 'rgba', // 默认格式 / Default format
|
||||
hasMipmaps: false,
|
||||
data: image
|
||||
};
|
||||
|
||||
// 缓存纹理 / Cache texture
|
||||
this._loadedTextures.set(path, textureAsset);
|
||||
|
||||
// 触发引擎纹理加载(如果有引擎桥接) / Trigger engine texture loading if bridge exists
|
||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||
await this.uploadToGPU(textureAsset, path);
|
||||
}
|
||||
|
||||
return {
|
||||
asset: textureAsset,
|
||||
handle: textureAsset.textureId,
|
||||
metadata,
|
||||
loadTime: performance.now() - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from URL
|
||||
* 从URL加载图像
|
||||
*/
|
||||
private async loadImage(url: string, options?: IAssetLoadOptions): Promise<HTMLImageElement> {
|
||||
// For Tauri asset URLs, use fetch to load the image
|
||||
// 对于Tauri资产URL,使用fetch加载图像
|
||||
if (url.startsWith('http://asset.localhost/') || url.startsWith('asset://')) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
// Clean up blob URL after loading
|
||||
// 加载后清理blob URL
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = () => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
reject(new Error(`Failed to load image from blob: ${url}`));
|
||||
};
|
||||
image.src = blobUrl;
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load Tauri asset: ${url} - ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// For regular URLs, use standard Image loading
|
||||
// 对于常规URL,使用标准Image加载
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.crossOrigin = 'anonymous';
|
||||
|
||||
// 超时处理 / Timeout handling
|
||||
const timeout = options?.timeout || 30000;
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error(`Image load timeout: ${url}`));
|
||||
}, timeout);
|
||||
|
||||
// 进度回调 / Progress callback
|
||||
if (options?.onProgress) {
|
||||
// 图像加载没有真正的进度事件,模拟进度 / Images don't have real progress events, simulate
|
||||
let progress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
progress = Math.min(progress + 0.1, 0.9);
|
||||
options.onProgress!(progress);
|
||||
}, 100);
|
||||
|
||||
image.onload = () => {
|
||||
clearInterval(progressInterval);
|
||||
clearTimeout(timeoutId);
|
||||
options.onProgress!(1);
|
||||
resolve(image);
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
clearInterval(progressInterval);
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error(`Failed to load image: ${url}`));
|
||||
};
|
||||
} else {
|
||||
image.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(image);
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error(`Failed to load image: ${url}`));
|
||||
};
|
||||
}
|
||||
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload texture to GPU
|
||||
* 上传纹理到GPU
|
||||
*/
|
||||
private async uploadToGPU(textureAsset: ITextureAsset, path: string): Promise<void> {
|
||||
const bridge = (window as any).engineBridge;
|
||||
if (bridge && bridge.loadTexture) {
|
||||
await bridge.loadTexture(textureAsset.textureId, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the loader can handle this asset
|
||||
* 验证加载器是否可以处理此资产
|
||||
*/
|
||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
||||
return this.supportedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate memory usage for the asset
|
||||
* 估算资产的内存使用量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dispose loaded asset
|
||||
* 释放已加载的资产
|
||||
*/
|
||||
dispose(asset: ITextureAsset): void {
|
||||
// 从缓存中移除 / Remove from cache
|
||||
for (const [path, cached] of this._loadedTextures.entries()) {
|
||||
if (cached === asset) {
|
||||
this._loadedTextures.delete(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 释放GPU资源 / Release GPU resources
|
||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
||||
const bridge = (window as any).engineBridge;
|
||||
if (bridge.unloadTexture) {
|
||||
bridge.unloadTexture(asset.textureId);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理图像数据 / Clean up image data
|
||||
if (asset.data instanceof HTMLImageElement) {
|
||||
asset.data.src = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
404
packages/asset-system/src/types/AssetTypes.ts
Normal file
404
packages/asset-system/src/types/AssetTypes.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* Core asset system types and enums
|
||||
* 核心资产系统类型和枚举
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unique identifier for assets across the project
|
||||
* 项目中资产的唯一标识符
|
||||
*/
|
||||
export type AssetGUID = string;
|
||||
|
||||
/**
|
||||
* Runtime asset handle for efficient access
|
||||
* 运行时资产句柄,用于高效访问
|
||||
*/
|
||||
export type AssetHandle = number;
|
||||
|
||||
/**
|
||||
* Asset loading state
|
||||
* 资产加载状态
|
||||
*/
|
||||
export enum AssetState {
|
||||
/** 未加载 */
|
||||
Unloaded = 'unloaded',
|
||||
/** 加载中 */
|
||||
Loading = 'loading',
|
||||
/** 已加载 */
|
||||
Loaded = 'loaded',
|
||||
/** 加载失败 */
|
||||
Failed = 'failed',
|
||||
/** 释放中 */
|
||||
Disposing = 'disposing'
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset types supported by the system
|
||||
* 系统支持的资产类型
|
||||
*/
|
||||
export enum AssetType {
|
||||
/** 纹理 */
|
||||
Texture = 'texture',
|
||||
/** 网格 */
|
||||
Mesh = 'mesh',
|
||||
/** 材质 */
|
||||
Material = 'material',
|
||||
/** 着色器 */
|
||||
Shader = 'shader',
|
||||
/** 音频 */
|
||||
Audio = 'audio',
|
||||
/** 字体 */
|
||||
Font = 'font',
|
||||
/** 预制体 */
|
||||
Prefab = 'prefab',
|
||||
/** 场景 */
|
||||
Scene = 'scene',
|
||||
/** 脚本 */
|
||||
Script = 'script',
|
||||
/** 动画片段 */
|
||||
AnimationClip = 'animation',
|
||||
/** 行为树 */
|
||||
BehaviorTree = 'behaviortree',
|
||||
/** JSON数据 */
|
||||
Json = 'json',
|
||||
/** 文本 */
|
||||
Text = 'text',
|
||||
/** 二进制 */
|
||||
Binary = 'binary',
|
||||
/** 自定义 */
|
||||
Custom = 'custom'
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform variants for assets
|
||||
* 资产的平台变体
|
||||
*/
|
||||
export enum AssetPlatform {
|
||||
/** H5平台(浏览器) */
|
||||
H5 = 'h5',
|
||||
/** 微信小游戏 */
|
||||
WeChat = 'wechat',
|
||||
/** 试玩广告(Playable Ads) */
|
||||
Playable = 'playable',
|
||||
/** Android平台 */
|
||||
Android = 'android',
|
||||
/** iOS平台 */
|
||||
iOS = 'ios',
|
||||
/** 编辑器(Tauri桌面) */
|
||||
Editor = 'editor'
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality levels for asset variants
|
||||
* 资产变体的质量级别
|
||||
*/
|
||||
export enum AssetQuality {
|
||||
/** 低质量 */
|
||||
Low = 'low',
|
||||
/** 中等质量 */
|
||||
Medium = 'medium',
|
||||
/** 高质量 */
|
||||
High = 'high',
|
||||
/** 超高质量 */
|
||||
Ultra = 'ultra'
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset metadata stored in the database
|
||||
* 存储在数据库中的资产元数据
|
||||
*/
|
||||
export interface IAssetMetadata {
|
||||
/** 全局唯一标识符 */
|
||||
guid: AssetGUID;
|
||||
/** 资产路径 */
|
||||
path: string;
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 资产名称 */
|
||||
name: string;
|
||||
/** 文件大小(字节) / File size in bytes */
|
||||
size: number;
|
||||
/** 内容哈希值 / Content hash for versioning */
|
||||
hash: string;
|
||||
/** 依赖的其他资产 / Dependencies on other assets */
|
||||
dependencies: AssetGUID[];
|
||||
/** 资产标签 / User-defined labels for categorization */
|
||||
labels: string[];
|
||||
/** 自定义标签 / Custom metadata tags */
|
||||
tags: Map<string, string>;
|
||||
/** 导入设置 / Import-time settings */
|
||||
importSettings?: Record<string, unknown>;
|
||||
/** 最后修改时间 / Unix timestamp of last modification */
|
||||
lastModified: number;
|
||||
/** 版本号 / Asset version number */
|
||||
version: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset variant descriptor
|
||||
* 资产变体描述符
|
||||
*/
|
||||
export interface IAssetVariant {
|
||||
/** 目标平台 */
|
||||
platform: AssetPlatform;
|
||||
/** 质量级别 */
|
||||
quality: AssetQuality;
|
||||
/** 本地化语言 / Language code for localized assets */
|
||||
locale?: string;
|
||||
/** 主题变体 / Theme identifier (e.g., 'dark', 'light') */
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset load options
|
||||
* 资产加载选项
|
||||
*/
|
||||
export interface IAssetLoadOptions {
|
||||
/** 加载优先级(0-100,越高越优先) / Priority level 0-100, higher loads first */
|
||||
priority?: number;
|
||||
/** 是否异步加载 / Use async loading */
|
||||
async?: boolean;
|
||||
/** 指定加载的变体 / Specific variant to load */
|
||||
variant?: IAssetVariant;
|
||||
/** 强制重新加载 / Force reload even if cached */
|
||||
forceReload?: boolean;
|
||||
/** 超时时间(毫秒) / Timeout in milliseconds */
|
||||
timeout?: number;
|
||||
/** 进度回调 / Progress callback (0-1) */
|
||||
onProgress?: (progress: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset bundle manifest
|
||||
* 资产包清单
|
||||
*/
|
||||
export interface IAssetBundleManifest {
|
||||
/** 包名称 */
|
||||
name: string;
|
||||
/** 版本号 */
|
||||
version: string;
|
||||
/** 内容哈希 / Content hash for integrity check */
|
||||
hash: string;
|
||||
/** 压缩类型 */
|
||||
compression?: 'none' | 'gzip' | 'brotli';
|
||||
/** 包含的资产列表 / Assets contained in this bundle */
|
||||
assets: AssetGUID[];
|
||||
/** 依赖的其他包 / Other bundles this depends on */
|
||||
dependencies: string[];
|
||||
/** 包大小(字节) / Bundle size in bytes */
|
||||
size: number;
|
||||
/** 创建时间戳 / Creation timestamp */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading result
|
||||
* 资产加载结果
|
||||
*/
|
||||
export interface IAssetLoadResult<T = unknown> {
|
||||
/** 加载的资产实例 */
|
||||
asset: T;
|
||||
/** 资产句柄 */
|
||||
handle: AssetHandle;
|
||||
/** 资产元数据 */
|
||||
metadata: IAssetMetadata;
|
||||
/** 加载耗时(毫秒) / Load time in milliseconds */
|
||||
loadTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading error
|
||||
* 资产加载错误
|
||||
*/
|
||||
export class AssetLoadError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly guid: AssetGUID,
|
||||
public readonly type: AssetType,
|
||||
public readonly cause?: Error
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AssetLoadError';
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for file not found error
|
||||
* 文件未找到错误的工厂方法
|
||||
*/
|
||||
static fileNotFound(guid: AssetGUID, path: string): AssetLoadError {
|
||||
return new AssetLoadError(`Asset file not found: ${path}`, guid, AssetType.Custom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for unsupported type error
|
||||
* 不支持的类型错误的工厂方法
|
||||
*/
|
||||
static unsupportedType(guid: AssetGUID, type: AssetType): AssetLoadError {
|
||||
return new AssetLoadError(`Unsupported asset type: ${type}`, guid, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for load timeout error
|
||||
* 加载超时错误的工厂方法
|
||||
*/
|
||||
static loadTimeout(guid: AssetGUID, type: AssetType, timeout: number): AssetLoadError {
|
||||
return new AssetLoadError(`Asset load timeout after ${timeout}ms`, guid, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for dependency failed error
|
||||
* 依赖加载失败错误的工厂方法
|
||||
*/
|
||||
static dependencyFailed(guid: AssetGUID, type: AssetType, depGuid: AssetGUID): AssetLoadError {
|
||||
return new AssetLoadError(`Dependency failed to load: ${depGuid}`, guid, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset reference counting info
|
||||
* 资产引用计数信息
|
||||
*/
|
||||
export interface IAssetReferenceInfo {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 资产句柄 */
|
||||
handle: AssetHandle;
|
||||
/** 引用计数 */
|
||||
referenceCount: number;
|
||||
/** 最后访问时间 / Unix timestamp of last access */
|
||||
lastAccessTime: number;
|
||||
/** 当前状态 */
|
||||
state: AssetState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset import options
|
||||
* 资产导入选项
|
||||
*/
|
||||
export interface IAssetImportOptions {
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 生成Mipmap / Generate mipmaps for textures */
|
||||
generateMipmaps?: boolean;
|
||||
/** 纹理压缩格式 / Texture compression format */
|
||||
compression?: 'none' | 'dxt' | 'etc2' | 'astc';
|
||||
/** 最大纹理尺寸 / Maximum texture dimension */
|
||||
maxTextureSize?: number;
|
||||
/** 生成LOD / Generate LODs for meshes */
|
||||
generateLODs?: boolean;
|
||||
/** 优化网格 / Optimize mesh geometry */
|
||||
optimizeMesh?: boolean;
|
||||
/** 音频格式 / Audio encoding format */
|
||||
audioFormat?: 'mp3' | 'ogg' | 'wav';
|
||||
/** 自定义处理器 / Custom processor plugin name */
|
||||
customProcessor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset usage statistics
|
||||
* 资产使用统计
|
||||
*/
|
||||
export interface IAssetUsageStats {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 加载次数 */
|
||||
loadCount: number;
|
||||
/** 总加载时间(毫秒) / Total time spent loading in ms */
|
||||
totalLoadTime: number;
|
||||
/** 平均加载时间(毫秒) / Average load time in ms */
|
||||
averageLoadTime: number;
|
||||
/** 最后使用时间 / Unix timestamp of last use */
|
||||
lastUsedTime: number;
|
||||
/** 被引用的资产列表 / Assets that reference this one */
|
||||
referencedBy: AssetGUID[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset preload group
|
||||
* 资产预加载组
|
||||
*/
|
||||
export interface IAssetPreloadGroup {
|
||||
/** 组名称 */
|
||||
name: string;
|
||||
/** 包含的资产 */
|
||||
assets: AssetGUID[];
|
||||
/** 加载优先级 / Load priority 0-100 */
|
||||
priority: number;
|
||||
/** 是否必需 / Must be loaded before scene start */
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset loading progress info
|
||||
* 资产加载进度信息
|
||||
*/
|
||||
export interface IAssetLoadProgress {
|
||||
/** 当前加载的资产 */
|
||||
currentAsset: string;
|
||||
/** 已加载数量 */
|
||||
loadedCount: number;
|
||||
/** 总数量 */
|
||||
totalCount: number;
|
||||
/** 已加载字节数 */
|
||||
loadedBytes: number;
|
||||
/** 总字节数 */
|
||||
totalBytes: number;
|
||||
/** 进度百分比(0-1) / Progress value 0-1 */
|
||||
progress: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset catalog entry for runtime lookups
|
||||
* 运行时查找的资产目录条目
|
||||
*/
|
||||
export interface IAssetCatalogEntry {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 资产路径 */
|
||||
path: string;
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 所在包名称 / Bundle containing this asset */
|
||||
bundleName?: string;
|
||||
/** 可用变体 / Available variants */
|
||||
variants?: IAssetVariant[];
|
||||
/** 大小(字节) / Size in bytes */
|
||||
size: number;
|
||||
/** 内容哈希 / Content hash */
|
||||
hash: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime asset catalog
|
||||
* 运行时资产目录
|
||||
*/
|
||||
export interface IAssetCatalog {
|
||||
/** 版本号 */
|
||||
version: string;
|
||||
/** 创建时间戳 / Creation timestamp */
|
||||
createdAt: number;
|
||||
/** 所有目录条目 / All catalog entries */
|
||||
entries: Map<AssetGUID, IAssetCatalogEntry>;
|
||||
/** 此目录中的包 / Bundles in this catalog */
|
||||
bundles: Map<string, IAssetBundleManifest>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset hot-reload event
|
||||
* 资产热重载事件
|
||||
*/
|
||||
export interface IAssetHotReloadEvent {
|
||||
/** 资产GUID */
|
||||
guid: AssetGUID;
|
||||
/** 资产路径 */
|
||||
path: string;
|
||||
/** 资产类型 */
|
||||
type: AssetType;
|
||||
/** 旧版本哈希 / Previous version hash */
|
||||
oldHash: string;
|
||||
/** 新版本哈希 / New version hash */
|
||||
newHash: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
165
packages/asset-system/src/utils/PathValidator.ts
Normal file
165
packages/asset-system/src/utils/PathValidator.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Path Validator
|
||||
* 路径验证器
|
||||
*
|
||||
* Validates and sanitizes asset paths for security
|
||||
* 验证并清理资产路径以确保安全
|
||||
*/
|
||||
|
||||
export class PathValidator {
|
||||
// Dangerous path patterns
|
||||
private static readonly DANGEROUS_PATTERNS = [
|
||||
/\.\.[/\\]/g, // Path traversal attempts (..)
|
||||
/^[/\\]/, // Absolute paths on Unix
|
||||
/^[a-zA-Z]:[/\\]/, // Absolute paths on Windows
|
||||
/[<>:"|?*]/, // Invalid characters for Windows paths
|
||||
/\0/, // Null bytes
|
||||
/%00/, // URL encoded null bytes
|
||||
/\.\.%2[fF]/ // URL encoded path traversal
|
||||
];
|
||||
|
||||
// Valid path characters (alphanumeric, dash, underscore, dot, slash)
|
||||
private static readonly VALID_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@]+$/;
|
||||
|
||||
// Maximum path length
|
||||
private static readonly MAX_PATH_LENGTH = 260;
|
||||
|
||||
/**
|
||||
* Validate if a path is safe
|
||||
* 验证路径是否安全
|
||||
*/
|
||||
static validate(path: string): { valid: boolean; reason?: string } {
|
||||
// Check for null/undefined/empty
|
||||
if (!path || typeof path !== 'string') {
|
||||
return { valid: false, reason: 'Path is empty or invalid' };
|
||||
}
|
||||
|
||||
// Check length
|
||||
if (path.length > this.MAX_PATH_LENGTH) {
|
||||
return { valid: false, reason: `Path exceeds maximum length of ${this.MAX_PATH_LENGTH} characters` };
|
||||
}
|
||||
|
||||
// Check for dangerous patterns
|
||||
for (const pattern of this.DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(path)) {
|
||||
return { valid: false, reason: 'Path contains dangerous pattern' };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for valid characters
|
||||
if (!this.VALID_PATH_REGEX.test(path)) {
|
||||
return { valid: false, reason: 'Path contains invalid characters' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a path
|
||||
* 清理路径
|
||||
*/
|
||||
static sanitize(path: string): string {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove dangerous patterns
|
||||
let sanitized = path;
|
||||
|
||||
// Remove path traversal (apply repeatedly until fully removed)
|
||||
let prev;
|
||||
do {
|
||||
prev = sanitized;
|
||||
sanitized = sanitized.replace(/\.\.[/\\]/g, '');
|
||||
} while (sanitized !== prev);
|
||||
|
||||
// Remove leading slashes
|
||||
sanitized = sanitized.replace(/^[/\\]+/, '');
|
||||
|
||||
// Remove null bytes
|
||||
sanitized = sanitized.replace(/\0/g, '');
|
||||
sanitized = sanitized.replace(/%00/g, '');
|
||||
|
||||
// Remove invalid Windows characters
|
||||
sanitized = sanitized.replace(/[<>:"|?*]/g, '_');
|
||||
|
||||
// Normalize slashes
|
||||
sanitized = sanitized.replace(/\\/g, '/');
|
||||
|
||||
// Remove double slashes
|
||||
sanitized = sanitized.replace(/\/+/g, '/');
|
||||
|
||||
// Trim whitespace
|
||||
sanitized = sanitized.trim();
|
||||
|
||||
// Truncate if too long
|
||||
if (sanitized.length > this.MAX_PATH_LENGTH) {
|
||||
sanitized = sanitized.substring(0, this.MAX_PATH_LENGTH);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is trying to escape the base directory
|
||||
* 检查路径是否试图逃离基础目录
|
||||
*/
|
||||
static isPathTraversal(path: string): boolean {
|
||||
const normalized = path.replace(/\\/g, '/');
|
||||
return normalized.includes('../') || normalized.includes('..\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a path for consistent handling
|
||||
* 规范化路径以便一致处理
|
||||
*/
|
||||
static normalize(path: string): string {
|
||||
if (!path) return '';
|
||||
|
||||
// Sanitize first
|
||||
let normalized = this.sanitize(path);
|
||||
|
||||
// Convert backslashes to forward slashes
|
||||
normalized = normalized.replace(/\\/g, '/');
|
||||
|
||||
// Remove trailing slash (except for root)
|
||||
if (normalized.length > 1 && normalized.endsWith('/')) {
|
||||
normalized = normalized.slice(0, -1);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join path segments safely
|
||||
* 安全地连接路径段
|
||||
*/
|
||||
static join(...segments: string[]): string {
|
||||
const validSegments = segments
|
||||
.filter((s) => s && typeof s === 'string')
|
||||
.map((s) => this.sanitize(s))
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
if (validSegments.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.normalize(validSegments.join('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension safely
|
||||
* 安全地获取文件扩展名
|
||||
*/
|
||||
static getExtension(path: string): string {
|
||||
const sanitized = this.sanitize(path);
|
||||
const lastDot = sanitized.lastIndexOf('.');
|
||||
const lastSlash = sanitized.lastIndexOf('/');
|
||||
|
||||
if (lastDot > lastSlash && lastDot > 0) {
|
||||
return sanitized.substring(lastDot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user