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:
@@ -313,3 +313,40 @@
|
||||
.asset-save-new-folder button:last-child:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
/* ==================== Managed Directory Styles ==================== */
|
||||
.asset-picker-item.managed-root .asset-picker-item__icon {
|
||||
color: #4fc1ff;
|
||||
}
|
||||
|
||||
.asset-picker-item.managed-root .managed-icon {
|
||||
color: #4fc1ff;
|
||||
}
|
||||
|
||||
.asset-picker-item .managed-badge {
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
background: #4fc1ff22;
|
||||
color: #4fc1ff;
|
||||
border-radius: 3px;
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Disabled items (no GUID) */
|
||||
.asset-picker-item.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.asset-picker-item.disabled:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.asset-picker-item .no-guid-badge {
|
||||
margin-left: auto;
|
||||
color: #f59e0b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { X, Search, Folder, FolderOpen, File, Image, FileText, Music, Video } from 'lucide-react';
|
||||
import { X, Search, Folder, FolderOpen, File, Image, FileText, Music, Video, Database, AlertTriangle } from 'lucide-react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { ProjectService } from '@esengine/editor-core';
|
||||
import { ProjectService, AssetRegistryService } from '@esengine/editor-core';
|
||||
import { TauriFileSystemService } from '../../services/TauriFileSystemService';
|
||||
import './AssetPickerDialog.css';
|
||||
|
||||
/**
|
||||
* Directories managed by asset registry (GUID system)
|
||||
* Only files in these directories can be selected
|
||||
*
|
||||
* Note: Keep in sync with MANAGED_ASSET_DIRECTORIES in AssetRegistryService.ts
|
||||
*/
|
||||
const MANAGED_ASSET_DIRECTORIES = ['assets', 'scripts', 'scenes'] as const;
|
||||
|
||||
interface AssetPickerDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -19,6 +27,10 @@ interface FileNode {
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
children?: FileNode[];
|
||||
/** Asset GUID (only for files with registered GUIDs) */
|
||||
guid?: string;
|
||||
/** Whether this is a root managed directory */
|
||||
isRootManaged?: boolean;
|
||||
}
|
||||
|
||||
export function AssetPickerDialog({
|
||||
@@ -35,7 +47,12 @@ export function AssetPickerDialog({
|
||||
const [assets, setAssets] = useState<FileNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Load project assets
|
||||
// Get AssetRegistryService for GUID lookup
|
||||
const assetRegistry = useMemo(() => {
|
||||
return Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
|
||||
}, []);
|
||||
|
||||
// Load project assets - ONLY from managed directories (assets, scripts, scenes)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
@@ -48,13 +65,44 @@ export function AssetPickerDialog({
|
||||
const currentProject = projectService?.getCurrentProject();
|
||||
if (projectService && currentProject) {
|
||||
const projectPath = currentProject.path;
|
||||
const assetsPath = `${projectPath}/assets`;
|
||||
const normalizedProjectPath = projectPath.replace(/\\/g, '/');
|
||||
|
||||
// 排除的目录名 | Excluded directory names
|
||||
const excludedDirs = new Set([
|
||||
'node_modules', '.git', '.idea', '.vscode', 'dist', 'build',
|
||||
'temp', 'tmp', '.cache', 'coverage', '__pycache__'
|
||||
]);
|
||||
|
||||
// Helper to get relative path from absolute path
|
||||
const getRelativePath = (absPath: string): string => {
|
||||
const normalizedAbs = absPath.replace(/\\/g, '/');
|
||||
if (normalizedAbs.startsWith(normalizedProjectPath)) {
|
||||
return normalizedAbs.substring(normalizedProjectPath.length + 1);
|
||||
}
|
||||
return absPath;
|
||||
};
|
||||
|
||||
const buildTree = async (dirPath: string): Promise<FileNode[]> => {
|
||||
const entries = await fileSystem.listDirectory(dirPath);
|
||||
const nodes: FileNode[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
// 跳过排除的目录 | Skip excluded directories
|
||||
if (entry.isDirectory && excludedDirs.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过隐藏文件/目录(以.开头,除了当前目录)
|
||||
// Skip hidden files/directories (starting with ., except current dir)
|
||||
if (entry.name.startsWith('.') && entry.name !== '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip .meta files
|
||||
if (entry.name.endsWith('.meta')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const node: FileNode = {
|
||||
name: entry.name,
|
||||
path: entry.path,
|
||||
@@ -67,6 +115,15 @@ export function AssetPickerDialog({
|
||||
} catch {
|
||||
node.children = [];
|
||||
}
|
||||
} else {
|
||||
// Try to get GUID for the file
|
||||
if (assetRegistry) {
|
||||
const relativePath = getRelativePath(entry.path);
|
||||
const guid = assetRegistry.getGuidByPath(relativePath);
|
||||
if (guid) {
|
||||
node.guid = guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes.push(node);
|
||||
@@ -80,8 +137,33 @@ export function AssetPickerDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const tree = await buildTree(assetsPath);
|
||||
setAssets(tree);
|
||||
// Only load managed directories (assets, scripts, scenes)
|
||||
const sep = projectPath.includes('\\') ? '\\' : '/';
|
||||
const managedNodes: FileNode[] = [];
|
||||
|
||||
for (const dirName of MANAGED_ASSET_DIRECTORIES) {
|
||||
const dirPath = `${projectPath}${sep}${dirName}`;
|
||||
try {
|
||||
const exists = await fileSystem.exists(dirPath);
|
||||
if (exists) {
|
||||
const children = await buildTree(dirPath);
|
||||
managedNodes.push({
|
||||
name: dirName,
|
||||
path: dirPath,
|
||||
isDirectory: true,
|
||||
children,
|
||||
isRootManaged: true
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Directory doesn't exist, skip
|
||||
}
|
||||
}
|
||||
|
||||
setAssets(managedNodes);
|
||||
|
||||
// Auto-expand managed directories
|
||||
setExpandedFolders(new Set(managedNodes.map(n => n.path)));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load assets:', error);
|
||||
@@ -93,7 +175,7 @@ export function AssetPickerDialog({
|
||||
loadAssets();
|
||||
setSelectedPath(null);
|
||||
setSearchTerm('');
|
||||
}, [isOpen]);
|
||||
}, [isOpen, assetRegistry]);
|
||||
|
||||
// Filter assets based on search and file extensions
|
||||
const filteredAssets = useMemo(() => {
|
||||
@@ -141,11 +223,19 @@ export function AssetPickerDialog({
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Track selected node (to check for GUID)
|
||||
const [selectedNode, setSelectedNode] = useState<FileNode | null>(null);
|
||||
|
||||
const handleSelect = useCallback((node: FileNode) => {
|
||||
if (node.isDirectory) {
|
||||
toggleFolder(node.path);
|
||||
} else {
|
||||
setSelectedPath(node.path);
|
||||
// Only allow selecting files with GUID
|
||||
if (node.guid) {
|
||||
setSelectedPath(node.path);
|
||||
setSelectedNode(node);
|
||||
}
|
||||
// Files without GUID cannot be selected
|
||||
}
|
||||
}, [toggleFolder]);
|
||||
|
||||
@@ -172,11 +262,15 @@ export function AssetPickerDialog({
|
||||
}, [selectedPath, onSelect, onClose, toRelativePath]);
|
||||
|
||||
const handleDoubleClick = useCallback((node: FileNode) => {
|
||||
if (!node.isDirectory) {
|
||||
if (!node.isDirectory && node.guid) {
|
||||
// Double-click on file with GUID selects it
|
||||
onSelect(toRelativePath(node.path));
|
||||
onClose();
|
||||
} else if (node.isDirectory) {
|
||||
// Double-click on folder toggles expansion
|
||||
toggleFolder(node.path);
|
||||
}
|
||||
}, [onSelect, onClose, toRelativePath]);
|
||||
}, [onSelect, onClose, toRelativePath, toggleFolder]);
|
||||
|
||||
const getFileIcon = (name: string) => {
|
||||
const ext = name.split('.').pop()?.toLowerCase();
|
||||
@@ -206,23 +300,38 @@ export function AssetPickerDialog({
|
||||
const renderNode = (node: FileNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isSelected = selectedPath === node.path;
|
||||
const hasGuid = node.isDirectory || !!node.guid;
|
||||
const isDisabled = !node.isDirectory && !node.guid;
|
||||
|
||||
return (
|
||||
<div key={node.path}>
|
||||
<div
|
||||
className={`asset-picker-item ${isSelected ? 'selected' : ''}`}
|
||||
className={`asset-picker-item ${isSelected ? 'selected' : ''} ${node.isRootManaged ? 'managed-root' : ''} ${isDisabled ? 'disabled' : ''}`}
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={() => handleSelect(node)}
|
||||
onDoubleClick={() => handleDoubleClick(node)}
|
||||
title={isDisabled ? 'This file has no GUID and cannot be referenced' : undefined}
|
||||
>
|
||||
<span className="asset-picker-item__icon">
|
||||
{node.isDirectory ? (
|
||||
isExpanded ? <FolderOpen size={14} /> : <Folder size={14} />
|
||||
node.isRootManaged ? (
|
||||
<Database size={14} className="managed-icon" />
|
||||
) : (
|
||||
isExpanded ? <FolderOpen size={14} /> : <Folder size={14} />
|
||||
)
|
||||
) : (
|
||||
getFileIcon(node.name)
|
||||
)}
|
||||
</span>
|
||||
<span className="asset-picker-item__name">{node.name}</span>
|
||||
{node.isRootManaged && (
|
||||
<span className="managed-badge">GUID</span>
|
||||
)}
|
||||
{isDisabled && (
|
||||
<span className="no-guid-badge" title="No GUID - cannot be referenced">
|
||||
<AlertTriangle size={12} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{node.isDirectory && isExpanded && node.children && (
|
||||
<div className="asset-picker-children">
|
||||
|
||||
Reference in New Issue
Block a user