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

@@ -0,0 +1,199 @@
.asset-picker-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.asset-picker-dialog {
width: 480px;
max-width: 90vw;
max-height: 70vh;
background: #1e1e1e;
border: 1px solid #333;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
}
.asset-picker-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #333;
}
.asset-picker-header h3 {
margin: 0;
font-size: 14px;
font-weight: 500;
color: #e0e0e0;
}
.asset-picker-close {
background: transparent;
border: none;
color: #888;
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.asset-picker-close:hover {
background: #333;
color: #e0e0e0;
}
.asset-picker-search {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #252525;
border-bottom: 1px solid #333;
}
.asset-picker-search svg {
color: #666;
flex-shrink: 0;
}
.asset-picker-search input {
flex: 1;
background: transparent;
border: none;
color: #e0e0e0;
font-size: 12px;
outline: none;
}
.asset-picker-search input::placeholder {
color: #666;
}
.asset-picker-content {
flex: 1;
overflow-y: auto;
min-height: 200px;
max-height: 400px;
}
.asset-picker-loading,
.asset-picker-empty {
padding: 32px;
text-align: center;
color: #666;
font-size: 12px;
}
.asset-picker-tree {
padding: 4px 0;
}
.asset-picker-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
cursor: pointer;
user-select: none;
}
.asset-picker-item:hover {
background: #2a2a2a;
}
.asset-picker-item.selected {
background: #0d47a1;
}
.asset-picker-item__icon {
display: flex;
align-items: center;
color: #888;
}
.asset-picker-item.selected .asset-picker-item__icon {
color: #90caf9;
}
.asset-picker-item__name {
font-size: 12px;
color: #e0e0e0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-picker-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-top: 1px solid #333;
background: #252525;
}
.asset-picker-selected {
flex: 1;
min-width: 0;
font-size: 11px;
color: #e0e0e0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-picker-selected .placeholder {
color: #666;
font-style: italic;
}
.asset-picker-actions {
display: flex;
gap: 8px;
margin-left: 16px;
}
.asset-picker-actions button {
padding: 6px 16px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
border: none;
}
.asset-picker-actions .btn-cancel {
background: #333;
color: #e0e0e0;
}
.asset-picker-actions .btn-cancel:hover {
background: #444;
}
.asset-picker-actions .btn-confirm {
background: #1976d2;
color: white;
}
.asset-picker-actions .btn-confirm:hover {
background: #1565c0;
}
.asset-picker-actions .btn-confirm:disabled {
background: #333;
color: #666;
cursor: not-allowed;
}

View File

@@ -0,0 +1,282 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { X, Search, Folder, FolderOpen, File, Image, FileText, Music, Video } from 'lucide-react';
import { Core } from '@esengine/ecs-framework';
import { ProjectService } from '@esengine/editor-core';
import { TauriFileSystemService } from '../../services/TauriFileSystemService';
import './AssetPickerDialog.css';
interface AssetPickerDialogProps {
isOpen: boolean;
onClose: () => void;
onSelect: (path: string) => void;
title?: string;
fileExtensions?: string[]; // e.g., ['.png', '.jpg']
placeholder?: string;
}
interface FileNode {
name: string;
path: string;
isDirectory: boolean;
children?: FileNode[];
}
export function AssetPickerDialog({
isOpen,
onClose,
onSelect,
title = 'Select Asset',
fileExtensions = [],
placeholder = 'Search assets...'
}: AssetPickerDialogProps) {
const [searchTerm, setSearchTerm] = useState('');
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [assets, setAssets] = useState<FileNode[]>([]);
const [loading, setLoading] = useState(false);
// Load project assets
useEffect(() => {
if (!isOpen) return;
const loadAssets = async () => {
setLoading(true);
try {
const projectService = Core.services.tryResolve(ProjectService);
const fileSystem = new TauriFileSystemService();
const currentProject = projectService?.getCurrentProject();
if (projectService && currentProject) {
const projectPath = currentProject.path;
const assetsPath = `${projectPath}/assets`;
const buildTree = async (dirPath: string): Promise<FileNode[]> => {
const entries = await fileSystem.listDirectory(dirPath);
const nodes: FileNode[] = [];
for (const entry of entries) {
const node: FileNode = {
name: entry.name,
path: entry.path,
isDirectory: entry.isDirectory
};
if (entry.isDirectory) {
try {
node.children = await buildTree(entry.path);
} catch {
node.children = [];
}
}
nodes.push(node);
}
// Sort: folders first, then files, alphabetically
return nodes.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name);
});
};
const tree = await buildTree(assetsPath);
setAssets(tree);
}
} catch (error) {
console.error('Failed to load assets:', error);
} finally {
setLoading(false);
}
};
loadAssets();
setSelectedPath(null);
setSearchTerm('');
}, [isOpen]);
// Filter assets based on search and file extensions
const filteredAssets = useMemo(() => {
const filterNode = (node: FileNode): FileNode | null => {
// Check file extension filter
if (!node.isDirectory && fileExtensions.length > 0) {
const hasValidExtension = fileExtensions.some((ext) =>
node.name.toLowerCase().endsWith(ext.toLowerCase())
);
if (!hasValidExtension) return null;
}
// Check search term
const matchesSearch = !searchTerm ||
node.name.toLowerCase().includes(searchTerm.toLowerCase());
if (node.isDirectory && node.children) {
const filteredChildren = node.children
.map(filterNode)
.filter((n): n is FileNode => n !== null);
if (filteredChildren.length > 0 || matchesSearch) {
return { ...node, children: filteredChildren };
}
return null;
}
return matchesSearch ? node : null;
};
return assets
.map(filterNode)
.filter((n): n is FileNode => n !== null);
}, [assets, searchTerm, fileExtensions]);
const toggleFolder = useCallback((path: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev);
if (next.has(path)) {
next.delete(path);
} else {
next.add(path);
}
return next;
});
}, []);
const handleSelect = useCallback((node: FileNode) => {
if (node.isDirectory) {
toggleFolder(node.path);
} else {
setSelectedPath(node.path);
}
}, [toggleFolder]);
const handleConfirm = useCallback(() => {
if (selectedPath) {
onSelect(selectedPath);
onClose();
}
}, [selectedPath, onSelect, onClose]);
const handleDoubleClick = useCallback((node: FileNode) => {
if (!node.isDirectory) {
onSelect(node.path);
onClose();
}
}, [onSelect, onClose]);
const getFileIcon = (name: string) => {
const ext = name.split('.').pop()?.toLowerCase();
switch (ext) {
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
case 'webp':
return <Image size={14} />;
case 'mp3':
case 'wav':
case 'ogg':
return <Music size={14} />;
case 'mp4':
case 'webm':
return <Video size={14} />;
case 'json':
case 'txt':
case 'md':
return <FileText size={14} />;
default:
return <File size={14} />;
}
};
const renderNode = (node: FileNode, depth: number = 0) => {
const isExpanded = expandedFolders.has(node.path);
const isSelected = selectedPath === node.path;
return (
<div key={node.path}>
<div
className={`asset-picker-item ${isSelected ? 'selected' : ''}`}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
onClick={() => handleSelect(node)}
onDoubleClick={() => handleDoubleClick(node)}
>
<span className="asset-picker-item__icon">
{node.isDirectory ? (
isExpanded ? <FolderOpen size={14} /> : <Folder size={14} />
) : (
getFileIcon(node.name)
)}
</span>
<span className="asset-picker-item__name">{node.name}</span>
</div>
{node.isDirectory && isExpanded && node.children && (
<div className="asset-picker-children">
{node.children.map((child) => renderNode(child, depth + 1))}
</div>
)}
</div>
);
};
if (!isOpen) return null;
return (
<div className="asset-picker-overlay" onClick={onClose}>
<div className="asset-picker-dialog" onClick={(e) => e.stopPropagation()}>
<div className="asset-picker-header">
<h3>{title}</h3>
<button className="asset-picker-close" onClick={onClose}>
<X size={16} />
</button>
</div>
<div className="asset-picker-search">
<Search size={14} />
<input
type="text"
placeholder={placeholder}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="asset-picker-content">
{loading ? (
<div className="asset-picker-loading">Loading assets...</div>
) : filteredAssets.length === 0 ? (
<div className="asset-picker-empty">No assets found</div>
) : (
<div className="asset-picker-tree">
{filteredAssets.map((node) => renderNode(node))}
</div>
)}
</div>
<div className="asset-picker-footer">
<div className="asset-picker-selected">
{selectedPath ? (
<span title={selectedPath}>
{selectedPath.split(/[\\/]/).pop()}
</span>
) : (
<span className="placeholder">No asset selected</span>
)}
</div>
<div className="asset-picker-actions">
<button className="btn-cancel" onClick={onClose}>
Cancel
</button>
<button
className="btn-confirm"
onClick={handleConfirm}
disabled={!selectedPath}
>
Select
</button>
</div>
</div>
</div>
</div>
);
}