feat: 预制体系统与架构改进 (#303)

* feat(prefab): 实现预制体系统和编辑器 UX 改进

## 预制体系统
- 新增 PrefabSerializer: 预制体序列化/反序列化
- 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改
- 新增 PrefabService: 预制体核心服务
- 新增 PrefabLoader: 预制体资产加载器
- 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink

## 预制体编辑模式
- 支持双击 .prefab 文件进入编辑模式
- 预制体编辑模式工具栏 (保存/退出)
- 预制体实例指示器和操作菜单

## 编辑器 UX 改进
- SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航
- 支持双击实体名称内联编辑
- 删除实体时显示子节点数量警告
- 右键菜单添加重命名/复制选项及快捷键提示
- 布局持久化和重置功能

## Bug 修复
- 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题
- 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见
- 修复 Inspector 资源字段高度不正确问题

* feat(editor): 改进编辑器 UX 交互体验

- ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计
- SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮
- PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理
- EntityInspector: 组件折叠状态持久化、属性搜索清除按钮
- Viewport: 变换操作实时数值显示
- 国际化: 添加相关文本 (en/zh)

* fix(build): 修复 Web 构建资产加载和编辑器 UX 改进

构建系统修复:
- 修复 asset-catalog.json 字段名不匹配 (entries vs assets)
- 修复 BrowserFileSystemService 支持两种目录格式
- 修复 bundle 策略检测逻辑 (空对象判断)
- 修复 module.json 中 assetExtensions 声明和类型推断

行为树修复:
- 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath
- 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree)

编辑器 UX 改进:
- 构建完成对话框添加"打开文件夹"按钮
- 构建完成对话框样式优化 (圆形图标背景、按钮布局)
- SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列)
- SceneHierarchy 隐藏滚动条

错误追踪:
- 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log)
- 添加 append_to_log Tauri 命令

* feat(render): 修复 UI 渲染和点击特效系统

## UI 渲染修复
- 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数
- 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap
- Web 运行时添加 assetPathResolver 支持 GUID 解析
- UIInteractableComponent.blockEvents 默认值改为 false

## 点击特效系统
- 新增 ClickFxComponent 和 ClickFxSystem
- 支持在点击位置播放粒子效果
- 支持多种触发模式和粒子轮换

## Camera 系统重构
- CameraSystem 从 ecs-engine-bindgen 移至 camera 包
- 新增 CameraManager 统一管理相机

## 编辑器改进
- 改进属性面板 UI 交互
- 粒子编辑器面板优化
- Transform 命令系统

* feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层

- 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay)
- 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性
- 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染
- 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer
- 更新粒子编辑器面板支持新的排序属性
- 优化 UI 渲染系统使用新的排序层级

* feat(ci): 集成 SignPath 代码签名服务

- 添加 SignPath 自动签名工作流(Windows)
- 配置 release-editor.yml 支持代码签名
- 将构建改为草稿模式,等待签名完成后发布
- 添加证书文件到 .gitignore 防止泄露

* fix(asset): 修复 Web 构建资产路径解析和全局单例移除

## 资产路径修复
- 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接
- BrowserPathResolver 支持两种模式:
  - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser)
  - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建)
- BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理

## 架构改进 - 移除全局单例
- 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入
- 移除 globalPathResolver 导出,改用 PathResolutionService
- 移除 globalPathResolutionService 导出
- ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖
- EngineService 使用 new AssetManager() 替代全局实例

## 新增服务
- PathResolutionService: 统一路径解析接口
- RuntimeModeService: 运行时模式查询服务
- SerializationContext: EntityRef 序列化上下文

## 其他改进
- 完善 ServiceToken 注释说明本地定义的意图
- 导出 BrowserPathResolveMode 类型

* fix(build): 添加 world-streaming composite 设置修复类型检查

* fix(build): 移除 world-streaming 引用避免 composite 冲突

* fix(build): 将 const enum 改为 enum 兼容 isolatedModules

* fix(build): 添加缺失的 IAssetManager 导入
This commit is contained in:
YHH
2025-12-13 19:44:08 +08:00
committed by GitHub
parent a716d8006c
commit beaa1d09de
258 changed files with 17725 additions and 3030 deletions

View File

@@ -1,6 +1,7 @@
{
"id": "asset-system",
"name": "@esengine/asset-system",
"globalKey": "assetSystem",
"displayName": "Asset System",
"description": "Asset loading, caching and management | 资源加载、缓存和管理",
"version": "1.0.0",
@@ -28,7 +29,9 @@
"TextureLoader",
"JsonLoader",
"TextLoader",
"BinaryLoader"
"BinaryLoader",
"AudioLoader",
"PrefabLoader"
],
"other": [
"AssetManager",
@@ -36,6 +39,33 @@
"AssetCache"
]
},
"assetExtensions": {
".png": "texture",
".jpg": "texture",
".jpeg": "texture",
".gif": "texture",
".webp": "texture",
".bmp": "texture",
".svg": "texture",
".mp3": "audio",
".ogg": "audio",
".wav": "audio",
".m4a": "audio",
".aac": "audio",
".flac": "audio",
".json": "data",
".xml": "data",
".yaml": "data",
".yml": "data",
".txt": "text",
".ttf": "font",
".woff": "font",
".woff2": "font",
".otf": "font",
".fnt": "font",
".atlas": "atlas",
".prefab": "prefab"
},
"requiresWasm": false,
"outputPath": "dist/index.js"
}

View File

@@ -608,6 +608,42 @@ export class AssetManager implements IAssetManager {
});
}
/**
* Unload assets by type
* 按类型卸载资产
*
* This is useful for clearing texture caches when restoring scene snapshots.
* 在恢复场景快照时清除纹理缓存时很有用。
*
* @param assetType 要卸载的资产类型 / Asset type to unload
* @param bForce 是否强制卸载(忽略引用计数)/ Whether to force unload (ignore reference count)
*/
unloadAssetsByType(assetType: AssetType, bForce: boolean = false): void {
const guids = Array.from(this._assets.keys());
guids.forEach((guid) => {
const entry = this._assets.get(guid);
if (entry && entry.metadata.type === assetType) {
if (bForce || entry.referenceCount === 0) {
// 获取加载器以释放资源 / 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;
}
}
});
}
/**
* Add reference to asset
* 增加资产引用

View File

@@ -233,9 +233,3 @@ export class AssetPathResolver {
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
}
}
/**
* Global asset path resolver instance
* 全局资产路径解析器实例
*/
export const globalPathResolver = new AssetPathResolver();

View File

@@ -11,7 +11,17 @@
*/
// Service tokens (谁定义接口,谁导出 Token)
export { AssetManagerToken, type IAssetManager } from './tokens';
export {
AssetManagerToken,
PrefabServiceToken,
PathResolutionServiceToken,
type IAssetManager,
type IPrefabService,
type IPrefabAsset,
type IPrefabData,
type IPrefabMetadata,
type IPathResolutionService
} from './tokens';
// Types
export * from './types/AssetTypes';
@@ -34,7 +44,7 @@ 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 { AssetPathResolver } from './core/AssetPathResolver';
export type { IAssetPathConfig } from './core/AssetPathResolver';
// Loaders
@@ -44,14 +54,16 @@ export { JsonLoader } from './loaders/JsonLoader';
export { TextLoader } from './loaders/TextLoader';
export { BinaryLoader } from './loaders/BinaryLoader';
export { AudioLoader } from './loaders/AudioLoader';
export { PrefabLoader } from './loaders/PrefabLoader';
// Integration
export { EngineIntegration } from './integration/EngineIntegration';
export type { IEngineBridge } from './integration/EngineIntegration';
export type { ITextureEngineBridge } from './integration/EngineIntegration';
// Services
export { SceneResourceManager } from './services/SceneResourceManager';
export type { IResourceLoader } from './services/SceneResourceManager';
export { PathResolutionService } from './services/PathResolutionService';
// Utils
export { UVHelper } from './utils/UVHelper';
@@ -62,26 +74,26 @@ export {
hashString,
hashFileInfo
} from './utils/AssetUtils';
export {
collectAssetReferences,
extractUniqueGuids,
groupByComponentType,
DEFAULT_ASSET_PATTERNS,
type SceneAssetRef,
type AssetFieldPattern
} from './utils/AssetCollector';
// Default instance
// Re-export for initializeAssetSystem
import { AssetManager } from './core/AssetManager';
/**
* Default asset manager instance
* 默认资产管理器实例
*/
export const assetManager = new AssetManager();
import type { IAssetCatalog } from './types/AssetTypes';
/**
* Initialize asset system with catalog
* 使用目录初始化资产系统
*
* @param catalog 资产目录 | Asset catalog
* @returns 新的 AssetManager 实例 | New AssetManager instance
*/
export function initializeAssetSystem(catalog?: IAssetCatalog): AssetManager {
if (catalog) {
return new AssetManager(catalog);
}
return assetManager;
return new AssetManager(catalog);
}
// Re-export IAssetCatalog for initializeAssetSystem signature
import type { IAssetCatalog } from './types/AssetTypes';

View File

@@ -4,15 +4,16 @@
*/
import { AssetManager } from '../core/AssetManager';
import { AssetGUID } from '../types/AssetTypes';
import { AssetGUID, AssetType } from '../types/AssetTypes';
import { ITextureAsset, IAudioAsset, IJsonAsset } from '../interfaces/IAssetLoader';
import { globalPathResolver } from '../core/AssetPathResolver';
import { PathResolutionService, type IPathResolutionService } from '../services/PathResolutionService';
import { TextureLoader } from '../loaders/TextureLoader';
/**
* Engine bridge interface
* 引擎桥接接口
* Texture engine bridge interface (for asset system)
* 纹理引擎桥接接口(用于资产系统)
*/
export interface IEngineBridge {
export interface ITextureEngineBridge {
/**
* Load texture to GPU
* 加载纹理到GPU
@@ -36,6 +37,36 @@ export interface IEngineBridge {
* 获取纹理信息
*/
getTextureInfo(id: number): { width: number; height: number } | null;
/**
* Get or load texture by path.
* 按路径获取或加载纹理。
*
* This is the preferred method for getting texture IDs.
* The Rust engine is the single source of truth for texture ID allocation.
* 这是获取纹理 ID 的首选方法。
* Rust 引擎是纹理 ID 分配的唯一事实来源。
*
* @param path Image path/URL | 图片路径/URL
* @returns Texture ID allocated by Rust engine | Rust 引擎分配的纹理 ID
*/
getOrLoadTextureByPath?(path: string): number;
/**
* Clear the texture path cache (optional).
* 清除纹理路径缓存(可选)。
*
* This should be called when restoring scene snapshots to ensure
* textures are reloaded with correct IDs.
* 在恢复场景快照时应调用此方法以确保纹理使用正确的ID重新加载。
*/
clearTexturePathCache?(): void;
/**
* Clear all textures and reset state (optional).
* 清除所有纹理并重置状态(可选)。
*/
clearAllTextures?(): void;
}
/**
@@ -64,7 +95,8 @@ interface DataAssetEntry {
*/
export class EngineIntegration {
private _assetManager: AssetManager;
private _engineBridge?: IEngineBridge;
private _engineBridge?: ITextureEngineBridge;
private _pathResolver: IPathResolutionService;
private _textureIdMap = new Map<AssetGUID, number>();
private _pathToTextureId = new Map<string, number>();
@@ -80,16 +112,25 @@ export class EngineIntegration {
private _dataAssets = new Map<number, DataAssetEntry>();
private static _nextDataId = 1;
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
constructor(assetManager: AssetManager, engineBridge?: ITextureEngineBridge, pathResolver?: IPathResolutionService) {
this._assetManager = assetManager;
this._engineBridge = engineBridge;
this._pathResolver = pathResolver ?? new PathResolutionService();
}
/**
* Set path resolver
* 设置路径解析器
*/
setPathResolver(resolver: IPathResolutionService): void {
this._pathResolver = resolver;
}
/**
* Set engine bridge
* 设置引擎桥接
*/
setEngineBridge(bridge: IEngineBridge): void {
setEngineBridge(bridge: ITextureEngineBridge): void {
this._engineBridge = bridge;
}
@@ -97,6 +138,9 @@ export class EngineIntegration {
* Load texture for component
* 为组件加载纹理
*
* 使用 Rust 引擎作为纹理 ID 的唯一分配源。
* Uses Rust engine as the single source of truth for texture ID allocation.
*
* AssetManager 内部会处理路径解析,这里只需传入原始路径。
* AssetManager handles path resolution internally, just pass the original path here.
*/
@@ -108,17 +152,33 @@ export class EngineIntegration {
return existingId;
}
// 通过资产系统加载AssetManager 内部会解析路径)
// Load through asset system (AssetManager resolves path internally)
// 解析路径为引擎可用的 URL
// Resolve path to engine-compatible URL
const engineUrl = this._pathResolver.catalogToRuntime(texturePath);
// 优先使用 getOrLoadTextureByPathRust 分配 ID
// Prefer getOrLoadTextureByPath (Rust allocates ID)
// 这确保纹理 ID 由 Rust 引擎统一分配,避免 JS/Rust 层 ID 不同步问题
// This ensures texture IDs are allocated by Rust engine uniformly,
// avoiding JS/Rust layer ID desync issues
if (this._engineBridge?.getOrLoadTextureByPath) {
const rustTextureId = this._engineBridge.getOrLoadTextureByPath(engineUrl);
if (rustTextureId > 0) {
// 缓存映射
// Cache mapping
this._pathToTextureId.set(texturePath, rustTextureId);
return rustTextureId;
}
}
// 回退:通过资产系统加载(兼容旧流程)
// Fallback: Load through asset system (for backward compatibility)
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
const textureAsset = result.asset;
// 如果有引擎桥接上传到GPU
// Upload to GPU if bridge exists
// 使用 globalPathResolver 将路径转换为引擎可用的 URL
// Use globalPathResolver to convert path to engine-compatible URL
if (this._engineBridge && textureAsset.data) {
const engineUrl = globalPathResolver.resolve(texturePath);
await this._engineBridge.loadTexture(textureAsset.textureId, engineUrl);
}
@@ -132,6 +192,9 @@ export class EngineIntegration {
/**
* Load texture by GUID
* 通过GUID加载纹理
*
* 使用 Rust 引擎作为纹理 ID 的唯一分配源。
* Uses Rust engine as the single source of truth for texture ID allocation.
*/
async loadTextureByGuid(guid: AssetGUID): Promise<number> {
// 检查是否已有纹理ID / Check if texture ID exists
@@ -140,14 +203,28 @@ export class EngineIntegration {
return existingId;
}
// 通过资产系统加载 / Load through asset system
// 通过资产系统加载获取元数据和路径 / Load through asset system to get metadata and path
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
const textureAsset = result.asset;
const metadata = result.metadata;
const engineUrl = this._pathResolver.catalogToRuntime(metadata.path);
// 如果有引擎桥接上传到GPU / Upload to GPU if bridge exists
// 优先使用 getOrLoadTextureByPathRust 分配 ID
// Prefer getOrLoadTextureByPath (Rust allocates ID)
if (this._engineBridge?.getOrLoadTextureByPath) {
const rustTextureId = this._engineBridge.getOrLoadTextureByPath(engineUrl);
if (rustTextureId > 0) {
// 缓存映射
// Cache mapping
this._textureIdMap.set(guid, rustTextureId);
return rustTextureId;
}
}
// 回退:使用 TextureLoader 分配的 ID兼容旧流程
// Fallback: Use TextureLoader allocated ID (for backward compatibility)
const textureAsset = result.asset;
if (this._engineBridge && textureAsset.data) {
const metadata = result.metadata;
await this._engineBridge.loadTexture(textureAsset.textureId, metadata.path);
await this._engineBridge.loadTexture(textureAsset.textureId, engineUrl);
}
// 缓存映射 / Cache mapping
@@ -486,10 +563,38 @@ export class EngineIntegration {
/**
* Clear all texture mappings
* 清空所有纹理映射
*
* This clears both local texture ID mappings and the AssetManager's
* texture cache to ensure textures are fully reloaded.
* 这会清除本地纹理 ID 映射和 AssetManager 的纹理缓存,确保纹理完全重新加载。
*
* IMPORTANT: This also clears the Rust engine's texture cache to ensure
* both JS and Rust layers are in sync.
* 重要:这也会清除 Rust 引擎的纹理缓存,确保 JS 和 Rust 层同步。
*/
clearTextureMappings(): void {
// 1. 清除本地映射
// Clear local mappings
this._textureIdMap.clear();
this._pathToTextureId.clear();
// 2. 清除 Rust 引擎的纹理缓存(如果可用)
// Clear Rust engine's texture cache (if available)
// 这确保下次加载时 Rust 会重新分配 ID
// This ensures Rust will reallocate IDs on next load
if (this._engineBridge?.clearAllTextures) {
this._engineBridge.clearAllTextures();
}
// 3. 清除 AssetManager 中的纹理资产缓存
// Clear texture asset cache in AssetManager
// 强制清除以确保纹理使用新的 ID 重新加载
// Force clear to ensure textures are reloaded with new IDs
this._assetManager.unloadAssetsByType(AssetType.Texture, true);
// 4. 重置 TextureLoader 的 ID 计数器(保持向后兼容)
// Reset TextureLoader's ID counter (for backward compatibility)
TextureLoader.resetTextureIdCounter();
}
/**

View File

@@ -109,6 +109,22 @@ export interface IAssetLoaderFactory {
* 根据文件路径获取资产类型
*/
getAssetTypeByPath(path: string): AssetType | null;
/**
* Get all supported file extensions from all registered loaders.
* 获取所有注册加载器支持的文件扩展名。
*
* @returns Array of extension patterns (e.g., ['*.png', '*.jpg', '*.particle'])
*/
getAllSupportedExtensions(): string[];
/**
* Get extension to type mapping for all registered loaders.
* 获取所有注册加载器的扩展名到类型的映射。
*
* @returns Map of extension (without dot) to asset type string
*/
getExtensionTypeMap(): Record<string, string>;
}
/**
@@ -187,18 +203,8 @@ export interface IMaterialAsset {
};
}
/**
* Prefab asset interface
* 预制体资产接口
*/
export interface IPrefabAsset {
/** 根实体数据 / Serialized entity hierarchy */
root: unknown;
/** 包含的组件类型 / Component types used in prefab */
componentTypes: string[];
/** 引用的资产 / All referenced assets */
referencedAssets: AssetGUID[];
}
// 预制体资产接口从专用文件导出 | Prefab asset interface exported from dedicated file
export type { IPrefabAsset, IPrefabData, IPrefabMetadata, IPrefabService } from './IPrefabAsset';
/**
* Scene asset interface

View File

@@ -0,0 +1,405 @@
/**
* 预制体资产接口定义
* Prefab asset interface definitions
*
* 定义预制体系统的核心类型,包括预制体数据格式、元数据、实例化选项等。
* Defines core types for the prefab system including data format, metadata, instantiation options, etc.
*/
import type { AssetGUID } from '../types/AssetTypes';
import type { SerializedEntity } from '@esengine/ecs-framework';
/**
* 预制体序列化实体(扩展自 SerializedEntity
* Serialized prefab entity (extends SerializedEntity)
*
* 在标准 SerializedEntity 基础上添加预制体特定属性。
* Adds prefab-specific properties on top of standard SerializedEntity.
*/
export interface SerializedPrefabEntity extends SerializedEntity {
/**
* 是否为预制体根节点
* Whether this is the prefab root entity
*/
isPrefabRoot?: boolean;
/**
* 嵌套预制体的 GUID如果此实体是另一个预制体的实例
* GUID of nested prefab (if this entity is an instance of another prefab)
*/
nestedPrefabGuid?: AssetGUID;
}
/**
* 预制体元数据
* Prefab metadata
*/
export interface IPrefabMetadata {
/**
* 预制体名称
* Prefab name
*/
name: string;
/**
* 资产 GUID在保存为资产后填充
* Asset GUID (populated after saving as asset)
*/
guid?: AssetGUID;
/**
* 创建时间戳
* Creation timestamp
*/
createdAt: number;
/**
* 最后修改时间戳
* Last modification timestamp
*/
modifiedAt: number;
/**
* 使用的组件类型列表
* List of component types used
*/
componentTypes: string[];
/**
* 引用的资产 GUID 列表
* List of referenced asset GUIDs
*/
referencedAssets: AssetGUID[];
/**
* 预制体描述
* Prefab description
*/
description?: string;
/**
* 预制体标签(用于分类和搜索)
* Prefab tags (for categorization and search)
*/
tags?: string[];
/**
* 缩略图数据Base64 编码)
* Thumbnail data (Base64 encoded)
*/
thumbnail?: string;
}
/**
* 组件类型注册条目
* Component type registry entry
*/
export interface IPrefabComponentTypeEntry {
/**
* 组件类型名称
* Component type name
*/
typeName: string;
/**
* 组件版本号
* Component version number
*/
version: number;
}
/**
* 预制体文件数据格式
* Prefab file data format
*
* 这是 .prefab 文件的完整结构。
* This is the complete structure of a .prefab file.
*/
export interface IPrefabData {
/**
* 预制体格式版本号
* Prefab format version number
*/
version: number;
/**
* 预制体元数据
* Prefab metadata
*/
metadata: IPrefabMetadata;
/**
* 根实体数据(包含完整的实体层级)
* Root entity data (contains full entity hierarchy)
*/
root: SerializedPrefabEntity;
/**
* 组件类型注册表(用于版本管理和兼容性检查)
* Component type registry (for versioning and compatibility checks)
*/
componentTypeRegistry: IPrefabComponentTypeEntry[];
}
/**
* 预制体资产(加载后的内存表示)
* Prefab asset (in-memory representation after loading)
*/
export interface IPrefabAsset {
/**
* 预制体数据
* Prefab data
*/
data: IPrefabData;
/**
* 资产 GUID
* Asset GUID
*/
guid: AssetGUID;
/**
* 资产路径
* Asset path
*/
path: string;
/**
* 根实体数据(快捷访问)
* Root entity data (quick access)
*/
readonly root: SerializedPrefabEntity;
/**
* 包含的组件类型列表(快捷访问)
* List of component types used (quick access)
*/
readonly componentTypes: string[];
/**
* 引用的资产列表(快捷访问)
* List of referenced assets (quick access)
*/
readonly referencedAssets: AssetGUID[];
}
/**
* 预制体实例化选项
* Prefab instantiation options
*/
export interface IPrefabInstantiateOptions {
/**
* 父实体 ID可选
* Parent entity ID (optional)
*/
parentId?: number;
/**
* 位置覆盖
* Position override
*/
position?: { x: number; y: number };
/**
* 旋转覆盖(角度)
* Rotation override (in degrees)
*/
rotation?: number;
/**
* 缩放覆盖
* Scale override
*/
scale?: { x: number; y: number };
/**
* 实体名称覆盖
* Entity name override
*/
name?: string;
/**
* 是否保留原始实体 ID默认 false生成新 ID
* Whether to preserve original entity IDs (default false, generate new IDs)
*/
preserveIds?: boolean;
/**
* 是否标记为预制体实例(默认 true
* Whether to mark as prefab instance (default true)
*/
trackInstance?: boolean;
/**
* 属性覆盖(组件属性覆盖)
* Property overrides (component property overrides)
*/
propertyOverrides?: IPrefabPropertyOverride[];
}
/**
* 预制体属性覆盖
* Prefab property override
*
* 用于记录预制体实例对原始预制体属性的修改。
* Used to record modifications to prefab properties in instances.
*/
export interface IPrefabPropertyOverride {
/**
* 目标实体路径(从根节点的相对路径,如 "Root/Child/GrandChild"
* Target entity path (relative path from root, e.g., "Root/Child/GrandChild")
*/
entityPath: string;
/**
* 组件类型名称
* Component type name
*/
componentType: string;
/**
* 属性路径(支持嵌套,如 "position.x"
* Property path (supports nesting, e.g., "position.x")
*/
propertyPath: string;
/**
* 覆盖值
* Override value
*/
value: unknown;
}
/**
* 预制体创建选项
* Prefab creation options
*/
export interface IPrefabCreateOptions {
/**
* 预制体名称
* Prefab name
*/
name: string;
/**
* 预制体描述
* Prefab description
*/
description?: string;
/**
* 预制体标签
* Prefab tags
*/
tags?: string[];
/**
* 是否包含子实体
* Whether to include child entities
*/
includeChildren?: boolean;
/**
* 保存路径(可选,用于指定保存位置)
* Save path (optional, for specifying save location)
*/
savePath?: string;
}
/**
* 预制体服务接口
* Prefab service interface
*
* 提供预制体的创建、实例化、管理等功能。
* Provides prefab creation, instantiation, management, etc.
*/
export interface IPrefabService {
/**
* 从实体创建预制体数据
* Create prefab data from entity
*
* @param entity - 源实体 | Source entity
* @param options - 创建选项 | Creation options
* @returns 预制体数据 | Prefab data
*/
createPrefab(entity: unknown, options: IPrefabCreateOptions): IPrefabData;
/**
* 实例化预制体
* Instantiate prefab
*
* @param prefab - 预制体资产 | Prefab asset
* @param scene - 目标场景 | Target scene
* @param options - 实例化选项 | Instantiation options
* @returns 创建的根实体 | Created root entity
*/
instantiate(prefab: IPrefabAsset, scene: unknown, options?: IPrefabInstantiateOptions): unknown;
/**
* 通过 GUID 实例化预制体
* Instantiate prefab by GUID
*
* @param guid - 预制体资产 GUID | Prefab asset GUID
* @param scene - 目标场景 | Target scene
* @param options - 实例化选项 | Instantiation options
* @returns 创建的根实体 | Created root entity
*/
instantiateByGuid(guid: AssetGUID, scene: unknown, options?: IPrefabInstantiateOptions): Promise<unknown>;
/**
* 检查实体是否为预制体实例
* Check if entity is a prefab instance
*
* @param entity - 要检查的实体 | Entity to check
* @returns 是否为预制体实例 | Whether it's a prefab instance
*/
isPrefabInstance(entity: unknown): boolean;
/**
* 获取预制体实例的源预制体 GUID
* Get source prefab GUID of a prefab instance
*
* @param entity - 预制体实例 | Prefab instance
* @returns 源预制体 GUID如果不是实例则返回 null | Source prefab GUID, null if not an instance
*/
getSourcePrefabGuid(entity: unknown): AssetGUID | null;
/**
* 将实例的修改应用到源预制体
* Apply instance modifications to source prefab
*
* @param instance - 预制体实例 | Prefab instance
* @returns 是否成功应用 | Whether application was successful
*/
applyToPrefab?(instance: unknown): Promise<boolean>;
/**
* 将实例还原为源预制体的状态
* Revert instance to source prefab state
*
* @param instance - 预制体实例 | Prefab instance
* @returns 是否成功还原 | Whether revert was successful
*/
revertToPrefab?(instance: unknown): Promise<boolean>;
/**
* 获取实例相对于源预制体的属性覆盖
* Get property overrides of instance relative to source prefab
*
* @param instance - 预制体实例 | Prefab instance
* @returns 属性覆盖列表 | List of property overrides
*/
getPropertyOverrides?(instance: unknown): IPrefabPropertyOverride[];
}
/**
* 预制体文件格式版本
* Prefab file format version
*/
export const PREFAB_FORMAT_VERSION = 1;
/**
* 预制体文件扩展名
* Prefab file extension
*/
export const PREFAB_FILE_EXTENSION = '.prefab';

View File

@@ -10,6 +10,7 @@ import { JsonLoader } from './JsonLoader';
import { TextLoader } from './TextLoader';
import { BinaryLoader } from './BinaryLoader';
import { AudioLoader } from './AudioLoader';
import { PrefabLoader } from './PrefabLoader';
/**
* Asset loader factory
@@ -42,6 +43,9 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
// 音频加载器 / Audio loader
this._loaders.set(AssetType.Audio, new AudioLoader());
// 预制体加载器 / Prefab loader
this._loaders.set(AssetType.Prefab, new PrefabLoader());
}
/**
@@ -142,4 +146,43 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
clear(): void {
this._loaders.clear();
}
/**
* Get all supported file extensions from all registered loaders.
* 获取所有注册加载器支持的文件扩展名。
*
* @returns Array of extension patterns (e.g., ['*.png', '*.jpg', '*.particle'])
*/
getAllSupportedExtensions(): string[] {
const extensions = new Set<string>();
for (const loader of this._loaders.values()) {
for (const ext of loader.supportedExtensions) {
// 转换为 glob 模式 | Convert to glob pattern
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
extensions.add(`*.${cleanExt}`);
}
}
return Array.from(extensions);
}
/**
* Get extension to type mapping for all registered loaders.
* 获取所有注册加载器的扩展名到类型的映射。
*
* @returns Map of extension (without dot) to asset type string
*/
getExtensionTypeMap(): Record<string, string> {
const map: Record<string, string> = {};
for (const [type, loader] of this._loaders) {
for (const ext of loader.supportedExtensions) {
const cleanExt = ext.startsWith('.') ? ext.substring(1) : ext;
map[cleanExt.toLowerCase()] = type;
}
}
return map;
}
}

View File

@@ -0,0 +1,156 @@
/**
* 预制体资产加载器
* Prefab asset loader
*/
import { AssetType } from '../types/AssetTypes';
import type { IAssetLoader, IAssetParseContext } from '../interfaces/IAssetLoader';
import type { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
import type {
IPrefabAsset,
IPrefabData,
SerializedPrefabEntity
} from '../interfaces/IPrefabAsset';
import { PREFAB_FORMAT_VERSION } from '../interfaces/IPrefabAsset';
/**
* 预制体加载器实现
* Prefab loader implementation
*/
export class PrefabLoader implements IAssetLoader<IPrefabAsset> {
readonly supportedType = AssetType.Prefab;
readonly supportedExtensions = ['.prefab'];
readonly contentType: AssetContentType = 'text';
/**
* 从文本内容解析预制体
* Parse prefab from text content
*/
async parse(content: IAssetContent, context: IAssetParseContext): Promise<IPrefabAsset> {
if (!content.text) {
throw new Error('Prefab content is empty');
}
let prefabData: IPrefabData;
try {
prefabData = JSON.parse(content.text) as IPrefabData;
} catch (error) {
throw new Error(`Failed to parse prefab JSON: ${(error as Error).message}`);
}
// 验证预制体格式 | Validate prefab format
this.validatePrefabData(prefabData);
// 版本兼容性检查 | Version compatibility check
if (prefabData.version > PREFAB_FORMAT_VERSION) {
console.warn(
`Prefab version ${prefabData.version} is newer than supported version ${PREFAB_FORMAT_VERSION}. ` +
`Some features may not work correctly.`
);
}
// 构建资产对象 | Build asset object
const prefabAsset: IPrefabAsset = {
data: prefabData,
guid: context.metadata.guid,
path: context.metadata.path,
// 快捷访问属性 | Quick access properties
get root(): SerializedPrefabEntity {
return prefabData.root;
},
get componentTypes(): string[] {
return prefabData.metadata.componentTypes;
},
get referencedAssets(): string[] {
return prefabData.metadata.referencedAssets;
}
};
return prefabAsset;
}
/**
* 释放已加载的资产
* Dispose loaded asset
*/
dispose(asset: IPrefabAsset): void {
// 清空预制体数据 | Clear prefab data
(asset as { data: IPrefabData | null }).data = null;
}
/**
* 验证预制体数据格式
* Validate prefab data format
*/
private validatePrefabData(data: unknown): asserts data is IPrefabData {
if (!data || typeof data !== 'object') {
throw new Error('Invalid prefab data: expected object');
}
const prefab = data as Partial<IPrefabData>;
// 验证版本号 | Validate version
if (typeof prefab.version !== 'number') {
throw new Error('Invalid prefab data: missing or invalid version');
}
// 验证元数据 | Validate metadata
if (!prefab.metadata || typeof prefab.metadata !== 'object') {
throw new Error('Invalid prefab data: missing or invalid metadata');
}
const metadata = prefab.metadata;
if (typeof metadata.name !== 'string') {
throw new Error('Invalid prefab data: missing or invalid metadata.name');
}
if (!Array.isArray(metadata.componentTypes)) {
throw new Error('Invalid prefab data: missing or invalid metadata.componentTypes');
}
if (!Array.isArray(metadata.referencedAssets)) {
throw new Error('Invalid prefab data: missing or invalid metadata.referencedAssets');
}
// 验证根实体 | Validate root entity
if (!prefab.root || typeof prefab.root !== 'object') {
throw new Error('Invalid prefab data: missing or invalid root entity');
}
this.validateSerializedEntity(prefab.root);
// 验证组件类型注册表 | Validate component type registry
if (!Array.isArray(prefab.componentTypeRegistry)) {
throw new Error('Invalid prefab data: missing or invalid componentTypeRegistry');
}
}
/**
* 验证序列化实体格式
* Validate serialized entity format
*/
private validateSerializedEntity(entity: unknown): void {
if (!entity || typeof entity !== 'object') {
throw new Error('Invalid entity data: expected object');
}
const e = entity as Partial<SerializedPrefabEntity>;
if (typeof e.id !== 'number') {
throw new Error('Invalid entity data: missing or invalid id');
}
if (typeof e.name !== 'string') {
throw new Error('Invalid entity data: missing or invalid name');
}
if (!Array.isArray(e.components)) {
throw new Error('Invalid entity data: missing or invalid components array');
}
if (!Array.isArray(e.children)) {
throw new Error('Invalid entity data: missing or invalid children array');
}
// 递归验证子实体 | Recursively validate child entities
for (const child of e.children) {
this.validateSerializedEntity(child);
}
}
}

View File

@@ -38,6 +38,18 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
private static _nextTextureId = 1;
/**
* Reset texture ID counter
* 重置纹理 ID 计数器
*
* This should be called when restoring scene snapshots to ensure
* textures start with fresh IDs.
* 在恢复场景快照时应调用此方法,以确保纹理从新 ID 开始。
*/
static resetTextureIdCounter(): void {
TextureLoader._nextTextureId = 1;
}
/**
* Parse texture from image content.
* 从图片内容解析纹理。

View File

@@ -0,0 +1,239 @@
/**
* 路径解析服务
* Path Resolution Service
*
* 提供统一的路径解析接口处理编辑器、Catalog、运行时三层路径转换。
* Provides unified path resolution interface for editor, catalog, and runtime path conversion.
*
* 路径格式约定 | Path Format Convention:
* - 编辑器路径 (Editor Path): 绝对路径,如 `C:\Project\assets\textures\bg.png`
* - Catalog 路径 (Catalog Path): 相对于 assets 目录,不含 `assets/` 前缀,如 `textures/bg.png`
* - 运行时 URL (Runtime URL): 完整 URL如 `./assets/textures/bg.png` 或 `https://cdn.example.com/assets/textures/bg.png`
*
* @example
* ```typescript
* import { PathResolutionServiceToken, type IPathResolutionService } from '@esengine/asset-system';
*
* // 获取服务
* const pathService = context.services.get(PathResolutionServiceToken);
*
* // Catalog 路径转运行时 URL
* const url = pathService.catalogToRuntime('textures/bg.png');
* // => './assets/textures/bg.png'
*
* // 编辑器路径转 Catalog 路径
* const catalogPath = pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
* // => 'textures/bg.png'
* ```
*/
import { createServiceToken } from '@esengine/ecs-framework';
// ============================================================================
// 接口定义 | Interface Definitions
// ============================================================================
/**
* 路径解析服务接口
* Path resolution service interface
*/
export interface IPathResolutionService {
/**
* 将 Catalog 路径转换为运行时 URL
* Convert catalog path to runtime URL
*
* @param catalogPath Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
* @returns 运行时 URL
*
* @example
* ```typescript
* // 输入: 'textures/bg.png'
* // 输出: './assets/textures/bg.png' (取决于 baseUrl 配置)
* pathService.catalogToRuntime('textures/bg.png');
* ```
*/
catalogToRuntime(catalogPath: string): string;
/**
* 将编辑器绝对路径转换为 Catalog 路径
* Convert editor absolute path to catalog path
*
* @param editorPath 编辑器绝对路径
* @param projectRoot 项目根目录
* @returns Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
*
* @example
* ```typescript
* // 输入: 'C:\\Project\\assets\\textures\\bg.png', 'C:\\Project'
* // 输出: 'textures/bg.png'
* pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
* ```
*/
editorToCatalog(editorPath: string, projectRoot: string): string;
/**
* 设置运行时基础 URL
* Set runtime base URL
*
* @param url 基础 URL通常为 './assets' 或 CDN URL
*/
setBaseUrl(url: string): void;
/**
* 获取当前基础 URL
* Get current base URL
*/
getBaseUrl(): string;
/**
* 规范化路径(统一斜杠方向,移除重复斜杠)
* Normalize path (unify slash direction, remove duplicate slashes)
*
* @param path 输入路径
* @returns 规范化后的路径
*/
normalize(path: string): string;
/**
* 检查路径是否为绝对 URL
* Check if path is absolute URL
*
* @param path 输入路径
* @returns 是否为绝对 URL
*/
isAbsoluteUrl(path: string): boolean;
}
// ============================================================================
// 服务令牌 | Service Token
// ============================================================================
/**
* 路径解析服务令牌
* Path resolution service token
*/
export const PathResolutionServiceToken = createServiceToken<IPathResolutionService>('pathResolutionService');
// ============================================================================
// 默认实现 | Default Implementation
// ============================================================================
/**
* 路径解析服务默认实现
* Default path resolution service implementation
*/
export class PathResolutionService implements IPathResolutionService {
private _baseUrl: string = './assets';
private _assetsDir: string = 'assets';
/**
* 创建路径解析服务
* Create path resolution service
*
* @param baseUrl 基础 URL默认 './assets'
*/
constructor(baseUrl?: string) {
if (baseUrl !== undefined) {
this._baseUrl = baseUrl;
}
}
/**
* 将 Catalog 路径转换为运行时 URL
* Convert catalog path to runtime URL
*/
catalogToRuntime(catalogPath: string): string {
// 空路径直接返回
if (!catalogPath) {
return catalogPath;
}
// 已经是绝对 URL 则直接返回
if (this.isAbsoluteUrl(catalogPath)) {
return catalogPath;
}
// Data URL 直接返回
if (catalogPath.startsWith('data:')) {
return catalogPath;
}
// 规范化路径
let normalized = this.normalize(catalogPath);
// 移除开头的斜杠
normalized = normalized.replace(/^\/+/, '');
// 如果路径以 'assets/' 开头,移除它(避免重复)
// Catalog 路径不应包含 assets/ 前缀
if (normalized.startsWith('assets/')) {
normalized = normalized.substring(7);
}
// 构建完整 URL
const base = this._baseUrl.replace(/\/+$/, ''); // 移除尾部斜杠
return `${base}/${normalized}`;
}
/**
* 将编辑器绝对路径转换为 Catalog 路径
* Convert editor absolute path to catalog path
*/
editorToCatalog(editorPath: string, projectRoot: string): string {
// 规范化路径
let normalizedPath = this.normalize(editorPath);
let normalizedRoot = this.normalize(projectRoot);
// 确保根路径以斜杠结尾
if (!normalizedRoot.endsWith('/')) {
normalizedRoot += '/';
}
// 移除项目根路径前缀
if (normalizedPath.startsWith(normalizedRoot)) {
normalizedPath = normalizedPath.substring(normalizedRoot.length);
}
// 移除 assets/ 前缀(如果存在)
const assetsPrefix = `${this._assetsDir}/`;
if (normalizedPath.startsWith(assetsPrefix)) {
normalizedPath = normalizedPath.substring(assetsPrefix.length);
}
return normalizedPath;
}
/**
* 设置运行时基础 URL
* Set runtime base URL
*/
setBaseUrl(url: string): void {
this._baseUrl = url;
}
/**
* 获取当前基础 URL
* Get current base URL
*/
getBaseUrl(): string {
return this._baseUrl;
}
/**
* 规范化路径
* Normalize path
*/
normalize(path: string): string {
return path
.replace(/\\/g, '/') // 反斜杠转正斜杠
.replace(/\/+/g, '/'); // 移除重复斜杠
}
/**
* 检查路径是否为绝对 URL
* Check if path is absolute URL
*/
isAbsoluteUrl(path: string): boolean {
return /^(https?:\/\/|file:\/\/|asset:\/\/|blob:)/.test(path);
}
}

View File

@@ -15,12 +15,16 @@
* ```
*/
import { createServiceToken } from '@esengine/engine-core';
import { createServiceToken } from '@esengine/ecs-framework';
import type { IAssetManager } from './interfaces/IAssetManager';
import type { IPrefabService } from './interfaces/IPrefabAsset';
import type { IPathResolutionService } from './services/PathResolutionService';
// 重新导出接口方便使用 | Re-export interface for convenience
export type { IAssetManager } from './interfaces/IAssetManager';
export type { IAssetLoadResult } from './types/AssetTypes';
export type { IPrefabService, IPrefabAsset, IPrefabData, IPrefabMetadata } from './interfaces/IPrefabAsset';
export type { IPathResolutionService } from './services/PathResolutionService';
/**
* 资产管理器服务令牌
@@ -30,3 +34,21 @@ export type { IAssetLoadResult } from './types/AssetTypes';
* For registering and getting asset manager service.
*/
export const AssetManagerToken = createServiceToken<IAssetManager>('assetManager');
/**
* 预制体服务令牌
* Prefab service token
*
* 用于注册和获取预制体服务。
* For registering and getting prefab service.
*/
export const PrefabServiceToken = createServiceToken<IPrefabService>('prefabService');
/**
* 路径解析服务令牌
* Path resolution service token
*
* 用于注册和获取路径解析服务。
* For registering and getting path resolution service.
*/
export const PathResolutionServiceToken = createServiceToken<IPathResolutionService>('pathResolutionService');

View File

@@ -0,0 +1,239 @@
/**
* 通用资产收集器
* Generic Asset Collector
*
* 从序列化的场景数据中自动收集资产引用。
* 支持基于字段名模式和 Property 元数据两种识别方式。
*
* Automatically collects asset references from serialized scene data.
* Supports both field name pattern matching and Property metadata recognition.
*/
/**
* 场景资产引用信息(用于构建时收集)
* Scene asset reference info (for build-time collection)
*/
export interface SceneAssetRef {
/** 资产 GUID | Asset GUID */
guid: string;
/** 来源组件类型 | Source component type */
componentType: string;
/** 来源字段名 | Source field name */
fieldName: string;
/** 实体名称(可选)| Entity name (optional) */
entityName?: string;
}
/**
* 资产字段模式配置
* Asset field pattern configuration
*/
export interface AssetFieldPattern {
/** 字段名模式(正则表达式)| Field name pattern (regex) */
pattern: RegExp;
/** 字段类型(用于分类)| Field type (for categorization) */
type?: string;
}
/**
* 默认资产字段模式
* Default asset field patterns
*
* 这些模式用于识别常见的资产引用字段
* These patterns are used to identify common asset reference fields
*/
export const DEFAULT_ASSET_PATTERNS: AssetFieldPattern[] = [
// GUID 类字段 | GUID-like fields
{ pattern: /^.*[Gg]uid$/, type: 'guid' },
{ pattern: /^.*[Aa]sset[Ii]d$/, type: 'guid' },
{ pattern: /^.*[Aa]ssetGuid$/, type: 'guid' },
// 纹理/贴图字段 | Texture fields
{ pattern: /^texture$/, type: 'texture' },
{ pattern: /^.*[Tt]exture[Pp]ath$/, type: 'texture' },
// 音频字段 | Audio fields
{ pattern: /^clip$/, type: 'audio' },
{ pattern: /^.*[Aa]udio[Pp]ath$/, type: 'audio' },
// 通用路径字段 | Generic path fields
{ pattern: /^.*[Pp]ath$/, type: 'path' },
];
/**
* 检查值是否像 GUID
* Check if value looks like a GUID
*/
function isGuidLike(value: unknown): value is string {
if (typeof value !== 'string') return false;
// GUID 格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// 或者简单的包含连字符的长字符串
return /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value) ||
(value.includes('-') && value.length >= 30 && value.length <= 40);
}
/**
* 从组件数据中收集资产引用
* Collect asset references from component data
*/
function collectFromComponentData(
componentType: string,
data: Record<string, unknown>,
patterns: AssetFieldPattern[],
entityName?: string
): SceneAssetRef[] {
const references: SceneAssetRef[] = [];
for (const [fieldName, value] of Object.entries(data)) {
// 检查是否匹配任何资产字段模式
// Check if matches any asset field pattern
const matchesPattern = patterns.some(p => p.pattern.test(fieldName));
if (matchesPattern) {
// 处理单个值 | Handle single value
if (isGuidLike(value)) {
references.push({
guid: value,
componentType,
fieldName,
entityName
});
}
// 处理数组 | Handle array
else if (Array.isArray(value)) {
for (const item of value) {
if (isGuidLike(item)) {
references.push({
guid: item,
componentType,
fieldName,
entityName
});
}
}
}
}
// 特殊处理已知的数组字段(如 particleAssets
// Special handling for known array fields (like particleAssets)
if (fieldName === 'particleAssets' && Array.isArray(value)) {
for (const item of value) {
if (isGuidLike(item)) {
references.push({
guid: item,
componentType,
fieldName,
entityName
});
}
}
}
}
return references;
}
/**
* 实体类型定义(支持嵌套 children
* Entity type definition (supports nested children)
*/
interface EntityData {
name?: string;
components?: Array<{ type: string; data?: Record<string, unknown> }>;
children?: EntityData[];
}
/**
* 递归处理实体及其子实体
* Recursively process entity and its children
*/
function collectFromEntity(
entity: EntityData,
patterns: AssetFieldPattern[],
references: SceneAssetRef[]
): void {
const entityName = entity.name;
// 处理当前实体的组件 | Process current entity's components
if (entity.components) {
for (const component of entity.components) {
if (!component.data) continue;
const componentRefs = collectFromComponentData(
component.type,
component.data,
patterns,
entityName
);
references.push(...componentRefs);
}
}
// 递归处理子实体 | Recursively process children
if (entity.children && Array.isArray(entity.children)) {
for (const child of entity.children) {
collectFromEntity(child, patterns, references);
}
}
}
/**
* 从序列化的场景数据中收集所有资产引用
* Collect all asset references from serialized scene data
*
* @param sceneData 序列化的场景数据JSON 对象)| Serialized scene data (JSON object)
* @param patterns 资产字段模式(可选,默认使用内置模式)| Asset field patterns (optional, defaults to built-in patterns)
* @returns 资产引用列表 | List of asset references
*
* @example
* ```typescript
* const sceneData = JSON.parse(sceneJson);
* const references = collectAssetReferences(sceneData);
* for (const ref of references) {
* console.log(`Found asset ${ref.guid} in ${ref.componentType}.${ref.fieldName}`);
* }
* ```
*/
export function collectAssetReferences(
sceneData: { entities?: EntityData[] },
patterns: AssetFieldPattern[] = DEFAULT_ASSET_PATTERNS
): SceneAssetRef[] {
const references: SceneAssetRef[] = [];
if (!sceneData.entities) {
return references;
}
// 遍历顶层实体,递归处理嵌套的子实体
// Iterate top-level entities, recursively process nested children
for (const entity of sceneData.entities) {
collectFromEntity(entity, patterns, references);
}
return references;
}
/**
* 从资产引用列表中提取唯一的 GUID 集合
* Extract unique GUID set from asset references
*/
export function extractUniqueGuids(references: SceneAssetRef[]): Set<string> {
return new Set(references.map(ref => ref.guid));
}
/**
* 按组件类型分组资产引用
* Group asset references by component type
*/
export function groupByComponentType(references: SceneAssetRef[]): Map<string, SceneAssetRef[]> {
const groups = new Map<string, SceneAssetRef[]>();
for (const ref of references) {
const existing = groups.get(ref.componentType) || [];
existing.push(ref);
groups.set(ref.componentType, existing);
}
return groups;
}

View File

@@ -3,56 +3,16 @@
* 资产工具函数
*
* Provides common utilities for asset management:
* - GUID validation and generation
* - GUID validation and generation (re-exported from core)
* - Content hashing
* 提供资产管理的通用工具:
* - GUID 验证和生成
* - GUID 验证和生成(从 core 重导出)
* - 内容哈希
*/
import type { AssetGUID } from '../types/AssetTypes';
// ============================================================================
// GUID Utilities
// GUID 工具
// ============================================================================
/**
* UUID v4 regex pattern
* UUID v4 正则表达式
*/
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
/**
* Check if a string is a valid UUID v4 format
* 检查字符串是否为有效的 UUID v4 格式
*/
export function isValidGUID(guid: string): boolean {
return UUID_REGEX.test(guid);
}
/**
* Generate a new UUID v4
* 生成新的 UUID v4
*
* Uses crypto.randomUUID() if available, otherwise falls back to manual generation.
* 如果可用则使用 crypto.randomUUID(),否则回退到手动生成。
*/
export function generateGUID(): AssetGUID {
// Use native crypto if available (Node.js, modern browsers)
// 如果可用则使用原生 cryptoNode.js、现代浏览器
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback: manual UUID v4 generation
// 回退:手动生成 UUID v4
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);
});
}
// Re-export GUID utilities from core (single source of truth)
// 从 core 重导出 GUID 工具(单一来源)
export { generateGUID, isValidGUID } from '@esengine/ecs-framework';
// ============================================================================
// Hash Utilities