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:
YHH
2025-11-23 14:49:37 +08:00
committed by GitHub
parent b15cbab313
commit a3f7cc38b1
247 changed files with 33561 additions and 52047 deletions

View File

@@ -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}