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 增强
This commit is contained in:
YHH
2025-12-07 20:26:03 +08:00
committed by GitHub
parent 568b327425
commit d92c2a7b66
9 changed files with 1013 additions and 86 deletions

View File

@@ -1,5 +1,9 @@
import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive } from 'lucide-react';
import { useState, useEffect, useCallback } from 'react';
import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive, Settings2 } from 'lucide-react';
import { convertFileSrc } from '@tauri-apps/api/core';
import { Core } from '@esengine/ecs-framework';
import { AssetRegistryService } from '@esengine/editor-core';
import { assetManager as globalAssetManager } from '@esengine/asset-system';
import { AssetFileInfo } from '../types';
import { ImagePreview, CodePreview, getLanguageFromExtension } from '../common';
import '../../../styles/EntityInspector.css';
@@ -10,6 +14,18 @@ interface AssetFileInspectorProps {
isImage?: boolean;
}
/**
* Built-in loader types (always available)
* 内置加载器类型(始终可用)
*/
const BUILTIN_LOADER_TYPES = [
'texture',
'audio',
'json',
'text',
'binary'
];
function formatFileSize(bytes?: number): string {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
@@ -38,6 +54,68 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
const IconComponent = fileInfo.isDirectory ? Folder : isImage ? ImageIcon : FileIcon;
const iconColor = fileInfo.isDirectory ? '#dcb67a' : isImage ? '#a78bfa' : '#90caf9';
// State for loader type selector
const [currentLoaderType, setCurrentLoaderType] = useState<string | null>(null);
const [availableLoaderTypes, setAvailableLoaderTypes] = useState<string[]>([]);
const [detectedType, setDetectedType] = useState<string | null>(null);
const [isUpdating, setIsUpdating] = useState(false);
// Load meta info and available loader types
useEffect(() => {
if (fileInfo.isDirectory) return;
const loadMetaInfo = async () => {
try {
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
if (!assetRegistry?.isReady) return;
const metaManager = assetRegistry.metaManager;
const meta = await metaManager.getOrCreateMeta(fileInfo.path);
// Get current loader type from meta
setCurrentLoaderType(meta.loaderType || null);
setDetectedType(meta.type);
// Get available loader types from assetManager
const loaderFactory = globalAssetManager.getLoaderFactory();
const registeredTypes = loaderFactory?.getRegisteredTypes() || [];
// Combine built-in types with registered types (deduplicated)
const allTypes = new Set([...BUILTIN_LOADER_TYPES, ...registeredTypes]);
setAvailableLoaderTypes(Array.from(allTypes).sort());
} catch (error) {
console.warn('Failed to load meta info:', error);
}
};
loadMetaInfo();
}, [fileInfo.path, fileInfo.isDirectory]);
// Handle loader type change
const handleLoaderTypeChange = useCallback(async (newType: string) => {
if (fileInfo.isDirectory || isUpdating) return;
setIsUpdating(true);
try {
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
if (!assetRegistry?.isReady) return;
const metaManager = assetRegistry.metaManager;
// Update meta with new loader type
// Empty string means use auto-detection (remove override)
const loaderType = newType === '' ? undefined : newType;
await metaManager.updateMeta(fileInfo.path, { loaderType });
setCurrentLoaderType(loaderType || null);
console.log(`[AssetFileInspector] Updated loader type for ${fileInfo.name}: ${loaderType || '(auto)'}`);
} catch (error) {
console.error('Failed to update loader type:', error);
} finally {
setIsUpdating(false);
}
}, [fileInfo.path, fileInfo.name, fileInfo.isDirectory, isUpdating]);
return (
<div className="entity-inspector">
<div className="inspector-header">
@@ -92,6 +170,56 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
</div>
</div>
{/* Loader Type Section - only for files, not directories */}
{!fileInfo.isDirectory && availableLoaderTypes.length > 0 && (
<div className="inspector-section">
<div className="section-title">
<Settings2 size={14} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
</div>
<div className="property-field">
<label className="property-label"></label>
<select
className="property-select"
value={currentLoaderType || ''}
onChange={(e) => handleLoaderTypeChange(e.target.value)}
disabled={isUpdating}
style={{
flex: 1,
padding: '4px 8px',
fontSize: '12px',
borderRadius: '4px',
border: '1px solid #444',
backgroundColor: '#2a2a2a',
color: '#e0e0e0',
cursor: isUpdating ? 'wait' : 'pointer'
}}
>
<option value="">
{detectedType ? `(${detectedType})` : ''}
</option>
{availableLoaderTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
{currentLoaderType && (
<div
style={{
marginTop: '4px',
fontSize: '11px',
color: '#888',
fontStyle: 'italic'
}}
>
使 "{currentLoaderType}"
</div>
)}
</div>
)}
{isImage && (
<div className="inspector-section">
<div className="section-title"></div>