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:
@@ -29,7 +29,8 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
const detailViewFileTreeRef = useRef<FileTreeHandle>(null);
|
||||
const treeOnlyViewFileTreeRef = useRef<FileTreeHandle>(null);
|
||||
const [currentPath, setCurrentPath] = useState<string | null>(null);
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
||||
const [lastSelectedPath, setLastSelectedPath] = useState<string | null>(null);
|
||||
const [assets, setAssets] = useState<AssetItem[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<AssetItem[]>([]);
|
||||
@@ -83,7 +84,7 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
} else {
|
||||
setAssets([]);
|
||||
setCurrentPath(null);
|
||||
setSelectedPath(null);
|
||||
setSelectedPaths(new Set());
|
||||
}
|
||||
}, [projectPath]);
|
||||
|
||||
@@ -92,21 +93,29 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
if (!messageHub) return;
|
||||
|
||||
const unsubscribe = messageHub.subscribe('asset:reveal', (data: any) => {
|
||||
const unsubscribe = messageHub.subscribe('asset:reveal', async (data: any) => {
|
||||
const filePath = data.path;
|
||||
if (filePath) {
|
||||
setSelectedPath(filePath);
|
||||
const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
|
||||
const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : null;
|
||||
if (dirPath) {
|
||||
setCurrentPath(dirPath);
|
||||
loadAssets(dirPath);
|
||||
// Load assets first, then set selection after list is populated
|
||||
await loadAssets(dirPath);
|
||||
setSelectedPaths(new Set([filePath]));
|
||||
|
||||
// Expand tree to reveal the file
|
||||
if (showDetailView) {
|
||||
detailViewFileTreeRef.current?.revealPath(filePath);
|
||||
} else {
|
||||
treeOnlyViewFileTreeRef.current?.revealPath(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
}, [showDetailView]);
|
||||
|
||||
const loadAssets = async (path: string) => {
|
||||
setLoading(true);
|
||||
@@ -211,8 +220,57 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
loadAssets(path);
|
||||
};
|
||||
|
||||
const handleAssetClick = (asset: AssetItem) => {
|
||||
setSelectedPath(asset.path);
|
||||
const handleTreeMultiSelect = (paths: string[], modifiers: { ctrlKey: boolean; shiftKey: boolean }) => {
|
||||
if (paths.length === 0) return;
|
||||
const path = paths[0];
|
||||
if (!path) return;
|
||||
|
||||
if (modifiers.shiftKey && paths.length > 1) {
|
||||
// Range select - paths already contains the range from FileTree
|
||||
setSelectedPaths(new Set(paths));
|
||||
} else if (modifiers.ctrlKey) {
|
||||
const newSelected = new Set(selectedPaths);
|
||||
if (newSelected.has(path)) {
|
||||
newSelected.delete(path);
|
||||
} else {
|
||||
newSelected.add(path);
|
||||
}
|
||||
setSelectedPaths(newSelected);
|
||||
setLastSelectedPath(path);
|
||||
} else {
|
||||
setSelectedPaths(new Set([path]));
|
||||
setLastSelectedPath(path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssetClick = (asset: AssetItem, e: React.MouseEvent) => {
|
||||
const filteredAssets = searchQuery.trim() ? searchResults : assets;
|
||||
|
||||
if (e.shiftKey && lastSelectedPath) {
|
||||
// Range select with Shift
|
||||
const lastIndex = filteredAssets.findIndex((a) => a.path === lastSelectedPath);
|
||||
const currentIndex = filteredAssets.findIndex((a) => a.path === asset.path);
|
||||
if (lastIndex !== -1 && currentIndex !== -1) {
|
||||
const start = Math.min(lastIndex, currentIndex);
|
||||
const end = Math.max(lastIndex, currentIndex);
|
||||
const rangePaths = filteredAssets.slice(start, end + 1).map((a) => a.path);
|
||||
setSelectedPaths(new Set(rangePaths));
|
||||
}
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
// Multi-select with Ctrl/Cmd
|
||||
const newSelected = new Set(selectedPaths);
|
||||
if (newSelected.has(asset.path)) {
|
||||
newSelected.delete(asset.path);
|
||||
} else {
|
||||
newSelected.add(asset.path);
|
||||
}
|
||||
setSelectedPaths(newSelected);
|
||||
setLastSelectedPath(asset.path);
|
||||
} else {
|
||||
// Single select
|
||||
setSelectedPaths(new Set([asset.path]));
|
||||
setLastSelectedPath(asset.path);
|
||||
}
|
||||
|
||||
messageHub?.publish('asset-file:selected', {
|
||||
fileInfo: {
|
||||
@@ -275,8 +333,11 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
}
|
||||
|
||||
// 更新选中路径
|
||||
if (selectedPath === asset.path) {
|
||||
setSelectedPath(newPath);
|
||||
if (selectedPaths.has(asset.path)) {
|
||||
const newSelected = new Set(selectedPaths);
|
||||
newSelected.delete(asset.path);
|
||||
newSelected.add(newPath);
|
||||
setSelectedPaths(newSelected);
|
||||
}
|
||||
|
||||
setRenameDialog(null);
|
||||
@@ -300,8 +361,10 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
}
|
||||
|
||||
// 清除选中状态
|
||||
if (selectedPath === asset.path) {
|
||||
setSelectedPath(null);
|
||||
if (selectedPaths.has(asset.path)) {
|
||||
const newSelected = new Set(selectedPaths);
|
||||
newSelected.delete(asset.path);
|
||||
setSelectedPaths(newSelected);
|
||||
}
|
||||
|
||||
setDeleteConfirmDialog(null);
|
||||
@@ -637,100 +700,120 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP
|
||||
</div>
|
||||
}
|
||||
rightOrBottom={
|
||||
<div className="asset-browser-list">
|
||||
<div className="asset-browser-breadcrumb">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<span key={crumb.path}>
|
||||
<span
|
||||
className="breadcrumb-item"
|
||||
onClick={() => {
|
||||
setCurrentPath(crumb.path);
|
||||
loadAssets(crumb.path);
|
||||
}}
|
||||
>
|
||||
{crumb.name}
|
||||
</span>
|
||||
{index < breadcrumbs.length - 1 && <span className="breadcrumb-separator"> / </span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{(loading || isSearching) ? (
|
||||
<div className="asset-browser-loading">
|
||||
<p>{isSearching ? '搜索中...' : t.loading}</p>
|
||||
</div>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<div className="asset-browser-empty">
|
||||
<p>{searchQuery.trim() ? '未找到匹配的资产' : t.empty}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="asset-list">
|
||||
{filteredAssets.map((asset, index) => {
|
||||
const relativePath = getRelativePath(asset.path);
|
||||
const showPath = searchQuery.trim() && relativePath;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`asset-item ${selectedPath === asset.path ? 'selected' : ''}`}
|
||||
onClick={() => handleAssetClick(asset)}
|
||||
onDoubleClick={() => handleAssetDoubleClick(asset)}
|
||||
onContextMenu={(e) => handleContextMenu(e, asset)}
|
||||
draggable={asset.type === 'file'}
|
||||
onDragStart={(e) => {
|
||||
if (asset.type === 'file') {
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
// 设置拖拽的数据
|
||||
e.dataTransfer.setData('asset-path', asset.path);
|
||||
e.dataTransfer.setData('asset-name', asset.name);
|
||||
e.dataTransfer.setData('asset-extension', asset.extension || '');
|
||||
e.dataTransfer.setData('text/plain', asset.path);
|
||||
|
||||
// 设置拖拽时的视觉效果
|
||||
const dragImage = e.currentTarget.cloneNode(true) as HTMLElement;
|
||||
dragImage.style.position = 'absolute';
|
||||
dragImage.style.top = '-9999px';
|
||||
dragImage.style.opacity = '0.8';
|
||||
document.body.appendChild(dragImage);
|
||||
e.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: asset.type === 'file' ? 'grab' : 'pointer'
|
||||
<div className="asset-browser-list">
|
||||
<div className="asset-browser-breadcrumb">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<span key={crumb.path}>
|
||||
<span
|
||||
className="breadcrumb-item"
|
||||
onClick={() => {
|
||||
setCurrentPath(crumb.path);
|
||||
loadAssets(crumb.path);
|
||||
}}
|
||||
>
|
||||
{getFileIcon(asset)}
|
||||
<div className="asset-info">
|
||||
<div className="asset-name" title={asset.path}>
|
||||
{asset.name}
|
||||
</div>
|
||||
{showPath && (
|
||||
<div className="asset-path" style={{
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{relativePath}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="asset-type">
|
||||
{asset.type === 'folder' ? t.folder : (asset.extension || t.file)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{crumb.name}
|
||||
</span>
|
||||
{index < breadcrumbs.length - 1 && <span className="breadcrumb-separator"> / </span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{(loading || isSearching) ? (
|
||||
<div className="asset-browser-loading">
|
||||
<p>{isSearching ? '搜索中...' : t.loading}</p>
|
||||
</div>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<div className="asset-browser-empty">
|
||||
<p>{searchQuery.trim() ? '未找到匹配的资产' : t.empty}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="asset-list">
|
||||
{filteredAssets.map((asset, index) => {
|
||||
const relativePath = getRelativePath(asset.path);
|
||||
const showPath = searchQuery.trim() && relativePath;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`asset-item ${selectedPaths.has(asset.path) ? 'selected' : ''}`}
|
||||
onClick={(e) => handleAssetClick(asset, e)}
|
||||
onDoubleClick={() => handleAssetDoubleClick(asset)}
|
||||
onContextMenu={(e) => handleContextMenu(e, asset)}
|
||||
draggable={asset.type === 'file'}
|
||||
onDragStart={(e) => {
|
||||
if (asset.type === 'file') {
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
|
||||
// Get all selected file assets
|
||||
const selectedFiles = selectedPaths.has(asset.path) && selectedPaths.size > 1
|
||||
? Array.from(selectedPaths)
|
||||
.filter((p) => {
|
||||
const a = assets?.find((item) => item.path === p);
|
||||
return a && a.type === 'file';
|
||||
})
|
||||
.map((p) => {
|
||||
const a = assets?.find((item) => item.path === p);
|
||||
return { type: 'file', path: p, name: a?.name, extension: a?.extension };
|
||||
})
|
||||
: [{ type: 'file', path: asset.path, name: asset.name, extension: asset.extension }];
|
||||
|
||||
// Set drag data as JSON array for multi-file support
|
||||
e.dataTransfer.setData('application/json', JSON.stringify(selectedFiles));
|
||||
e.dataTransfer.setData('asset-path', asset.path);
|
||||
e.dataTransfer.setData('asset-name', asset.name);
|
||||
e.dataTransfer.setData('asset-extension', asset.extension || '');
|
||||
e.dataTransfer.setData('text/plain', asset.path);
|
||||
|
||||
// 设置拖拽时的视觉效果
|
||||
const dragImage = e.currentTarget.cloneNode(true) as HTMLElement;
|
||||
dragImage.style.position = 'absolute';
|
||||
dragImage.style.top = '-9999px';
|
||||
dragImage.style.opacity = '0.8';
|
||||
if (selectedFiles.length > 1) {
|
||||
dragImage.textContent = `${selectedFiles.length} files`;
|
||||
}
|
||||
document.body.appendChild(dragImage);
|
||||
e.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: asset.type === 'file' ? 'grab' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{getFileIcon(asset)}
|
||||
<div className="asset-info">
|
||||
<div className="asset-name" title={asset.path}>
|
||||
{asset.name}
|
||||
</div>
|
||||
{showPath && (
|
||||
<div className="asset-path" style={{
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{relativePath}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="asset-type">
|
||||
{asset.type === 'folder' ? t.folder : (asset.extension || t.file)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="asset-browser-tree-only">
|
||||
<FileTree
|
||||
ref={treeOnlyViewFileTreeRef}
|
||||
rootPath={projectPath}
|
||||
onSelectFile={handleFolderSelect}
|
||||
selectedPath={currentPath}
|
||||
onSelectFiles={handleTreeMultiSelect}
|
||||
selectedPath={Array.from(selectedPaths)[0] || currentPath}
|
||||
selectedPaths={selectedPaths}
|
||||
messageHub={messageHub}
|
||||
searchQuery={searchQuery}
|
||||
showFiles={true}
|
||||
|
||||
Reference in New Issue
Block a user