Files
esengine/packages/asset-system-editor/src/meta/AssetMetaFile.ts
YHH d92c2a7b66 feat(asset): 增强资产管理系统和编辑器 UI (#291)
* fix(editor): 修复粒子实体创建和优化检视器

- 添加 effects 分类到右键菜单,修复粒子实体无法创建的问题
- 添加粒子效果的本地化标签
- 简化粒子组件检视器,优先显示资产文件选择
- 高级属性只在未选择资产时显示,且默认折叠
- 添加可折叠的属性分组提升用户体验

* fix(particle): 修复粒子系统在浏览器预览中的资产加载和渲染

- 添加粒子 Gizmo 支持,显示发射形状并响应 Transform 缩放/旋转
- 修复资产热重载:添加 reloadAsset() 方法和 assets:refresh 事件监听
- 修复 VectorFieldEditors 数值输入精度(step 改为 0.01)
- 修复浏览器预览中粒子资产加载失败的问题:
  - 将相对路径转换为绝对路径以正确复制资产文件
  - 使用原始 GUID 而非生成的 GUID 构建 asset catalog
  - 初始化全局 assetManager 单例的 catalog 和 loader
  - 在 GameRuntime 的 systemContext 中添加 engineIntegration
- 公开 AssetManager.initializeFromCatalog 方法供运行时使用

* feat(asset): 增强资产管理系统和编辑器 UI

主要改动:
- 添加 loaderType 字段支持显式指定加载器类型覆盖
- 添加 .particle 扩展名和类型映射
- 新增 MANAGED_ASSET_DIRECTORIES 常量和相关工具方法
- EngineService 使用全局 assetManager 并同步 AssetRegistry 数据
- 修复插件启用逻辑,defaultEnabled=true 的新插件不被旧配置禁用
- ContentBrowser 添加 GUID 管理目录指示和非托管目录警告
- AssetPickerDialog 和 AssetFileInspector UI 增强
2025-12-07 20:26:03 +08:00

437 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Asset Meta File (.meta) Management
* 资产元数据文件 (.meta) 管理
*
* Each asset file has a companion .meta file that stores:
* - GUID: Persistent unique identifier
* - Import settings: How to process the asset
* - Labels: User-defined tags
*
* 每个资产文件都有一个配套的 .meta 文件,存储:
* - GUID持久化唯一标识符
* - 导入设置:如何处理资产
* - 标签:用户定义的标签
*/
import { AssetGUID, AssetType } from '@esengine/asset-system';
/**
* Meta file content structure
* 元数据文件内容结构
*/
export interface IAssetMeta {
/** Persistent unique identifier | 持久化唯一标识符 */
guid: AssetGUID;
/** Asset type | 资产类型 */
type: AssetType;
/**
* Explicit loader type override
* 显式指定的加载器类型覆盖
*
* When set, this type will be used instead of extension-based detection.
* Useful when file extension doesn't match the actual content type.
*
* 设置后,将使用此类型而非基于扩展名的检测。
* 适用于文件扩展名与实际内容类型不匹配的情况。
*/
loaderType?: string;
/** Import settings | 导入设置 */
importSettings?: IImportSettings;
/** User-defined labels | 用户定义的标签 */
labels?: string[];
/** Meta file version | 元数据文件版本 */
version: number;
/** Last modified timestamp | 最后修改时间戳 */
lastModified?: number;
}
/**
* Import settings for different asset types
* 不同资产类型的导入设置
*/
export interface IImportSettings {
// Texture settings | 纹理设置
maxSize?: number;
compression?: 'none' | 'dxt' | 'etc2' | 'astc' | 'webp';
generateMipmaps?: boolean;
filterMode?: 'point' | 'bilinear' | 'trilinear';
wrapMode?: 'clamp' | 'repeat' | 'mirror';
premultiplyAlpha?: boolean;
// Audio settings | 音频设置
audioFormat?: 'mp3' | 'ogg' | 'wav';
sampleRate?: number;
channels?: 1 | 2;
normalize?: boolean;
// General settings | 通用设置
[key: string]: unknown;
}
/**
* Generate a new UUID v4
* 生成新的 UUID v4
*/
export function generateGUID(): AssetGUID {
// Use crypto.randomUUID if available (modern browsers/Node 19+)
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback implementation
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Get meta file path for an asset
* 获取资产的元数据文件路径
*/
export function getMetaFilePath(assetPath: string): string {
return `${assetPath}.meta`;
}
/**
* Infer asset type from file extension
* 根据文件扩展名推断资产类型
*/
export function inferAssetType(path: string): AssetType {
const ext = path.split('.').pop()?.toLowerCase() || '';
const typeMap: Record<string, AssetType> = {
// Textures
png: 'texture',
jpg: 'texture',
jpeg: 'texture',
gif: 'texture',
webp: 'texture',
bmp: 'texture',
svg: 'texture',
// Audio
mp3: 'audio',
wav: 'audio',
ogg: 'audio',
m4a: 'audio',
flac: 'audio',
// Data
json: 'json',
txt: 'text',
xml: 'text',
csv: 'text',
// Scenes and prefabs
ecs: 'scene',
prefab: 'prefab',
// Fonts
ttf: 'font',
otf: 'font',
woff: 'font',
woff2: 'font',
// Shaders
glsl: 'shader',
vert: 'shader',
frag: 'shader',
// Custom types (plugins)
tilemap: 'tilemap',
tileset: 'tileset',
btree: 'behavior-tree',
bp: 'blueprint',
mat: 'material',
particle: 'particle'
};
return typeMap[ext] || 'binary';
}
/**
* Get default import settings for asset type
* 获取资产类型的默认导入设置
*/
export function getDefaultImportSettings(type: AssetType): IImportSettings {
switch (type) {
case 'texture':
return {
maxSize: 2048,
compression: 'none',
generateMipmaps: false,
filterMode: 'bilinear',
wrapMode: 'clamp',
premultiplyAlpha: false
};
case 'audio':
return {
audioFormat: 'mp3',
sampleRate: 44100,
channels: 2,
normalize: false
};
default:
return {};
}
}
/**
* Create a new meta file content
* 创建新的元数据文件内容
*/
export function createAssetMeta(assetPath: string, overrides?: Partial<IAssetMeta>): IAssetMeta {
const type = overrides?.type || inferAssetType(assetPath);
return {
guid: overrides?.guid || generateGUID(),
type,
importSettings: overrides?.importSettings || getDefaultImportSettings(type),
labels: overrides?.labels || [],
version: 1,
lastModified: Date.now()
};
}
/**
* Serialize meta to JSON string
* 将元数据序列化为 JSON 字符串
*/
export function serializeAssetMeta(meta: IAssetMeta): string {
return JSON.stringify(meta, null, 2);
}
/**
* Parse meta from JSON string
* 从 JSON 字符串解析元数据
*/
export function parseAssetMeta(json: string): IAssetMeta {
const meta = JSON.parse(json) as IAssetMeta;
// Validate required fields
if (!meta.guid || typeof meta.guid !== 'string') {
throw new Error('Invalid meta file: missing or invalid guid');
}
if (!meta.type || typeof meta.type !== 'string') {
throw new Error('Invalid meta file: missing or invalid type');
}
// Set defaults for optional fields
meta.version = meta.version || 1;
meta.labels = meta.labels || [];
meta.importSettings = meta.importSettings || {};
return meta;
}
/**
* Validate GUID format (UUID v4)
* 验证 GUID 格式 (UUID v4)
*/
export function isValidGUID(guid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(guid);
}
/**
* Asset Meta File Manager
* 资产元数据文件管理器
*
* Handles reading/writing .meta files through a file system interface.
*/
export class AssetMetaManager {
private _cache = new Map<string, IAssetMeta>();
private _guidToPath = new Map<AssetGUID, string>();
/**
* File system interface for reading/writing files
* 用于读写文件的文件系统接口
*/
private _fs: IMetaFileSystem | null = null;
/**
* Set file system interface
* 设置文件系统接口
*/
setFileSystem(fs: IMetaFileSystem): void {
this._fs = fs;
}
/**
* Get or create meta for an asset
* 获取或创建资产的元数据
*/
async getOrCreateMeta(assetPath: string): Promise<IAssetMeta> {
// Check cache first
const cached = this._cache.get(assetPath);
if (cached) {
return cached;
}
const metaPath = getMetaFilePath(assetPath);
// Try to read existing meta file
if (this._fs) {
try {
if (await this._fs.exists(metaPath)) {
const content = await this._fs.readText(metaPath);
const meta = parseAssetMeta(content);
this._cache.set(assetPath, meta);
this._guidToPath.set(meta.guid, assetPath);
return meta;
}
} catch (e) {
console.warn(`Failed to read meta file: ${metaPath}`, e);
}
}
// Create new meta
const meta = createAssetMeta(assetPath);
this._cache.set(assetPath, meta);
this._guidToPath.set(meta.guid, assetPath);
// Save to file system
if (this._fs) {
try {
await this._fs.writeText(metaPath, serializeAssetMeta(meta));
} catch (e) {
console.warn(`Failed to write meta file: ${metaPath}`, e);
}
}
return meta;
}
/**
* Get meta by GUID
* 根据 GUID 获取元数据
*/
getMetaByGUID(guid: AssetGUID): IAssetMeta | undefined {
const path = this._guidToPath.get(guid);
return path ? this._cache.get(path) : undefined;
}
/**
* Get asset path by GUID
* 根据 GUID 获取资产路径
*/
getPathByGUID(guid: AssetGUID): string | undefined {
return this._guidToPath.get(guid);
}
/**
* Get GUID by asset path
* 根据资产路径获取 GUID
*/
async getGUIDByPath(assetPath: string): Promise<AssetGUID> {
const meta = await this.getOrCreateMeta(assetPath);
return meta.guid;
}
/**
* Update meta and save
* 更新元数据并保存
*/
async updateMeta(assetPath: string, updates: Partial<IAssetMeta>): Promise<void> {
const meta = await this.getOrCreateMeta(assetPath);
// Apply updates
Object.assign(meta, updates);
meta.lastModified = Date.now();
meta.version++;
// Update cache
this._cache.set(assetPath, meta);
// Handle GUID change (rare, but possible)
if (updates.guid && updates.guid !== meta.guid) {
this._guidToPath.delete(meta.guid);
this._guidToPath.set(updates.guid, assetPath);
}
// Save to file system
if (this._fs) {
const metaPath = getMetaFilePath(assetPath);
await this._fs.writeText(metaPath, serializeAssetMeta(meta));
}
}
/**
* Handle asset rename
* 处理资产重命名
*/
async handleAssetRename(oldPath: string, newPath: string): Promise<void> {
const meta = this._cache.get(oldPath);
if (meta) {
// Update cache with new path
this._cache.delete(oldPath);
this._cache.set(newPath, meta);
this._guidToPath.set(meta.guid, newPath);
// Move meta file
if (this._fs) {
const oldMetaPath = getMetaFilePath(oldPath);
const newMetaPath = getMetaFilePath(newPath);
if (await this._fs.exists(oldMetaPath)) {
const content = await this._fs.readText(oldMetaPath);
await this._fs.writeText(newMetaPath, content);
await this._fs.delete(oldMetaPath);
}
}
}
}
/**
* Handle asset delete
* 处理资产删除
*/
async handleAssetDelete(assetPath: string): Promise<void> {
const meta = this._cache.get(assetPath);
if (meta) {
this._cache.delete(assetPath);
this._guidToPath.delete(meta.guid);
// Delete meta file
if (this._fs) {
const metaPath = getMetaFilePath(assetPath);
if (await this._fs.exists(metaPath)) {
await this._fs.delete(metaPath);
}
}
}
}
/**
* Clear cache
* 清除缓存
*/
clear(): void {
this._cache.clear();
this._guidToPath.clear();
}
/**
* Get all cached metas
* 获取所有缓存的元数据
*/
getAllMetas(): Map<string, IAssetMeta> {
return new Map(this._cache);
}
}
/**
* File system interface for meta file operations
* 元数据文件操作的文件系统接口
*/
export interface IMetaFileSystem {
exists(path: string): Promise<boolean>;
readText(path: string): Promise<string>;
writeText(path: string, content: string): Promise<void>;
delete(path: string): Promise<void>;
}