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

@@ -130,6 +130,7 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi
const unsubAssetFileSelect = messageHub.subscribe('asset-file:selected', handleAssetFileSelection);
const unsubComponentAdded = messageHub.subscribe('component:added', handleComponentChange);
const unsubComponentRemoved = messageHub.subscribe('component:removed', handleComponentChange);
const unsubPropertyChanged = messageHub.subscribe('component:property:changed', handleComponentChange);
window.addEventListener('profiler:entity-details', handleEntityDetails);
@@ -140,6 +141,7 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi
unsubAssetFileSelect();
unsubComponentAdded();
unsubComponentRemoved();
unsubPropertyChanged();
window.removeEventListener('profiler:entity-details', handleEntityDetails);
};
}, [messageHub]);

View File

@@ -38,7 +38,7 @@ export function ComponentItem({ component, decimalPlaces = 4 }: ComponentItemPro
}}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<Settings size={14} style={{ marginLeft: "4px", color: "#888" }} />
<Settings size={14} style={{ marginLeft: '4px', color: '#888' }} />
<span
style={{
marginLeft: '6px',

View File

@@ -51,4 +51,4 @@ export function PropertyField({
</span>
</div>
);
}
}

View File

@@ -1,10 +1,10 @@
import React, { useState, useRef, useCallback } from 'react';
import { FileText, Search, X, FolderOpen, ArrowRight, Package } from 'lucide-react';
import { open } from '@tauri-apps/plugin-dialog';
import { AssetPickerDialog } from '../../../components/dialogs/AssetPickerDialog';
import './AssetField.css';
interface AssetFieldProps {
label: string;
label?: string;
value: string | null;
onChange: (value: string | null) => void;
fileExtension?: string; // 例如: '.btree'
@@ -24,6 +24,7 @@ export function AssetField({
}: AssetFieldProps) {
const [isDragging, setIsDragging] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [showPicker, setShowPicker] = useState(false);
const inputRef = useRef<HTMLDivElement>(null);
const handleDragEnter = useCallback((e: React.DragEvent) => {
@@ -54,7 +55,7 @@ export function AssetField({
// 处理从文件系统拖入的文件
const files = Array.from(e.dataTransfer.files);
const file = files.find(f =>
const file = files.find((f) =>
!fileExtension || f.name.endsWith(fileExtension)
);
@@ -78,25 +79,15 @@ export function AssetField({
}
}, [onChange, fileExtension, readonly]);
const handleBrowse = useCallback(async () => {
const handleBrowse = useCallback(() => {
if (readonly) return;
setShowPicker(true);
}, [readonly]);
try {
const selected = await open({
multiple: false,
filters: fileExtension ? [{
name: `${fileExtension} Files`,
extensions: [fileExtension.replace('.', '')]
}] : []
});
if (selected) {
onChange(selected as string);
}
} catch (error) {
console.error('Failed to open file dialog:', error);
}
}, [onChange, fileExtension, readonly]);
const handlePickerSelect = useCallback((path: string) => {
onChange(path);
setShowPicker(false);
}, [onChange]);
const handleClear = useCallback(() => {
if (!readonly) {
@@ -111,7 +102,7 @@ export function AssetField({
return (
<div className="asset-field">
<label className="asset-field__label">{label}</label>
{label && <label className="asset-field__label">{label}</label>}
<div
className={`asset-field__container ${isDragging ? 'dragging' : ''} ${isHovered ? 'hovered' : ''}`}
onMouseEnter={() => setIsHovered(true)}
@@ -160,17 +151,21 @@ export function AssetField({
</button>
)}
{/* 导航按钮 */}
{value && onNavigate && (
{/* 导航/定位按钮 */}
{onNavigate && (
<button
className="asset-field__button"
onClick={(e) => {
e.stopPropagation();
onNavigate(value);
if (value) {
onNavigate(value);
} else {
handleBrowse();
}
}}
title="在资产浏览器中显示"
title={value ? '在资产浏览器中显示' : '选择资产'}
>
<ArrowRight size={12} />
{value ? <ArrowRight size={12} /> : <FolderOpen size={12} />}
</button>
)}
@@ -189,6 +184,14 @@ export function AssetField({
)}
</div>
</div>
<AssetPickerDialog
isOpen={showPicker}
onClose={() => setShowPicker(false)}
onSelect={handlePickerSelect}
title="Select Asset"
fileExtensions={fileExtension ? [fileExtension] : []}
/>
</div>
);
}
}

View File

@@ -54,8 +54,8 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
{fileInfo.isDirectory
? '文件夹'
: fileInfo.extension
? `.${fileInfo.extension}`
: '文件'}
? `.${fileInfo.extension}`
: '文件'}
</span>
</div>
{fileInfo.size !== undefined && !fileInfo.isDirectory && (

View File

@@ -1,10 +1,12 @@
import { useState } from 'react';
import { Settings, ChevronDown, ChevronRight, X, Plus } from 'lucide-react';
import { Entity, Component, Core } from '@esengine/ecs-framework';
import { Settings, ChevronDown, ChevronRight, X, Plus, Box } from 'lucide-react';
import { Entity, Component, Core, getComponentDependencies, getComponentTypeName } from '@esengine/ecs-framework';
import { MessageHub, CommandManager, ComponentRegistry } from '@esengine/editor-core';
import { PropertyInspector } from '../../PropertyInspector';
import { NotificationService } from '../../../services/NotificationService';
import { RemoveComponentCommand, UpdateComponentCommand, AddComponentCommand } from '../../../application/commands/component';
import '../../../styles/EntityInspector.css';
import * as LucideIcons from 'lucide-react';
interface EntityInspectorProps {
entity: Entity;
@@ -16,6 +18,7 @@ interface EntityInspectorProps {
export function EntityInspector({ entity, messageHub, commandManager, componentVersion }: EntityInspectorProps) {
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
const [showComponentMenu, setShowComponentMenu] = useState(false);
const [localVersion, setLocalVersion] = useState(0);
const componentRegistry = Core.services.resolve(ComponentRegistry);
const availableComponents = componentRegistry?.getAllComponents() || [];
@@ -40,14 +43,42 @@ export function EntityInspector({ entity, messageHub, commandManager, componentV
const handleRemoveComponent = (index: number) => {
const component = entity.components[index];
if (component) {
const command = new RemoveComponentCommand(
messageHub,
entity,
component
);
commandManager.execute(command);
if (!component) return;
const componentName = getComponentTypeName(component.constructor as any);
console.log('Removing component:', componentName);
// Check if any other component depends on this one
const dependentComponents: string[] = [];
for (const otherComponent of entity.components) {
if (otherComponent === component) continue;
const dependencies = getComponentDependencies(otherComponent.constructor as any);
const otherName = getComponentTypeName(otherComponent.constructor as any);
console.log('Checking', otherName, 'dependencies:', dependencies);
if (dependencies && dependencies.includes(componentName)) {
dependentComponents.push(otherName);
}
}
console.log('Dependent components:', dependentComponents);
if (dependentComponents.length > 0) {
const notificationService = Core.services.tryResolve(NotificationService) as NotificationService | null;
if (notificationService) {
notificationService.warning(
'无法删除组件',
`${componentName} 被以下组件依赖: ${dependentComponents.join(', ')}。请先删除这些组件。`
);
}
return;
}
const command = new RemoveComponentCommand(
messageHub,
entity,
component
);
commandManager.execute(command);
};
const handlePropertyChange = (component: Component, propertyName: string, value: unknown) => {
@@ -61,6 +92,34 @@ export function EntityInspector({ entity, messageHub, commandManager, componentV
commandManager.execute(command);
};
const handlePropertyAction = async (actionId: string, _propertyName: string, component: Component) => {
if (actionId === 'nativeSize' && component.constructor.name === 'SpriteComponent') {
const sprite = component as unknown as { texture: string; width: number; height: number };
if (!sprite.texture) {
console.warn('No texture set for sprite');
return;
}
try {
const { convertFileSrc } = await import('@tauri-apps/api/core');
const assetUrl = convertFileSrc(sprite.texture);
const img = new Image();
img.onload = () => {
handlePropertyChange(component, 'width', img.naturalWidth);
handlePropertyChange(component, 'height', img.naturalHeight);
setLocalVersion((v) => v + 1);
};
img.onerror = () => {
console.error('Failed to load texture for native size:', sprite.texture);
};
img.src = assetUrl;
} catch (error) {
console.error('Error getting texture size:', error);
}
}
};
return (
<div className="entity-inspector">
<div className="inspector-header">
@@ -82,149 +141,123 @@ export function EntityInspector({ entity, messageHub, commandManager, componentV
</div>
<div className="inspector-section">
<div className="section-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="section-title section-title-with-action">
<span></span>
<div style={{ position: 'relative' }}>
<div className="component-menu-container">
<button
className="add-component-trigger"
onClick={() => setShowComponentMenu(!showComponentMenu)}
style={{
background: 'transparent',
border: '1px solid #4a4a4a',
borderRadius: '4px',
color: '#e0e0e0',
cursor: 'pointer',
padding: '2px 6px',
display: 'flex',
alignItems: 'center',
gap: '4px',
fontSize: '11px'
}}
>
<Plus size={12} />
</button>
{showComponentMenu && (
<div
style={{
position: 'absolute',
top: '100%',
right: 0,
marginTop: '4px',
backgroundColor: '#2a2a2a',
border: '1px solid #4a4a4a',
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
zIndex: 1000,
minWidth: '150px',
maxHeight: '200px',
overflowY: 'auto'
}}
>
{availableComponents.length === 0 ? (
<div style={{ padding: '8px 12px', color: '#888', fontSize: '11px' }}>
</div>
) : (
availableComponents.map((info) => (
<button
key={info.name}
onClick={() => info.type && handleAddComponent(info.type)}
style={{
display: 'block',
width: '100%',
padding: '6px 12px',
background: 'transparent',
border: 'none',
color: '#e0e0e0',
fontSize: '12px',
textAlign: 'left',
cursor: 'pointer'
}}
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#3a3a3a')}
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
>
{info.name}
</button>
))
)}
</div>
<>
<div className="component-dropdown-overlay" onClick={() => setShowComponentMenu(false)} />
<div className="component-dropdown">
<div className="component-dropdown-header"></div>
{availableComponents.length === 0 ? (
<div className="component-dropdown-empty">
</div>
) : (
<div className="component-dropdown-list">
{/* 按分类分组显示 */}
{(() => {
const categories = new Map<string, typeof availableComponents>();
availableComponents.forEach((info) => {
const cat = info.category || 'components.category.other';
if (!categories.has(cat)) {
categories.set(cat, []);
}
categories.get(cat)!.push(info);
});
return Array.from(categories.entries()).map(([category, components]) => (
<div key={category} className="component-category-group">
<div className="component-category-label">{category}</div>
{components.map((info) => (
<button
key={info.name}
className="component-dropdown-item"
onClick={() => info.type && handleAddComponent(info.type)}
>
<span className="component-dropdown-item-name">{info.name}</span>
</button>
))}
</div>
));
})()}
</div>
)}
</div>
</>
)}
</div>
</div>
{entity.components.map((component: Component, index: number) => {
{entity.components.length === 0 ? (
<div className="empty-state-small"></div>
) : (
entity.components.map((component: Component, index: number) => {
const isExpanded = expandedComponents.has(index);
const componentName = component.constructor?.name || 'Component';
const componentInfo = componentRegistry?.getComponent(componentName);
const iconName = (componentInfo as { icon?: string } | undefined)?.icon;
const IconComponent = iconName && (LucideIcons as unknown as Record<string, React.ComponentType<{ size?: number }>>)[iconName];
return (
<div
key={`${componentName}-${index}-${componentVersion}`}
style={{
marginBottom: '2px',
backgroundColor: '#2a2a2a',
borderRadius: '4px',
overflow: 'hidden'
}}
key={`${componentName}-${index}`}
className={`component-item-card ${isExpanded ? 'expanded' : ''}`}
>
<div
className="component-item-header"
onClick={() => toggleComponentExpanded(index)}
style={{
display: 'flex',
alignItems: 'center',
padding: '6px 8px',
backgroundColor: '#3a3a3a',
cursor: 'pointer',
userSelect: 'none',
borderBottom: isExpanded ? '1px solid #4a4a4a' : 'none'
}}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<span
style={{
marginLeft: '6px',
fontSize: '12px',
fontWeight: 500,
color: '#e0e0e0',
flex: 1
}}
>
<span className="component-expand-icon">
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
{IconComponent ? (
<span className="component-icon">
<IconComponent size={14} />
</span>
) : (
<span className="component-icon">
<Box size={14} />
</span>
)}
<span className="component-item-name">
{componentName}
</span>
<button
className="component-remove-btn"
onClick={(e) => {
e.stopPropagation();
handleRemoveComponent(index);
}}
title="移除组件"
style={{
background: 'transparent',
border: 'none',
color: '#888',
cursor: 'pointer',
padding: '2px',
borderRadius: '3px',
display: 'flex',
alignItems: 'center'
}}
onMouseEnter={(e) => (e.currentTarget.style.color = '#dc2626')}
onMouseLeave={(e) => (e.currentTarget.style.color = '#888')}
>
<X size={12} />
</button>
</div>
{isExpanded && (
<div style={{ padding: '6px 8px' }}>
<div className="component-item-content">
<PropertyInspector
component={component}
entity={entity}
version={componentVersion + localVersion}
onChange={(propName: string, value: unknown) =>
handlePropertyChange(component, propName, value)
}
onAction={handlePropertyAction}
/>
</div>
)}
</div>
);
})}
})
)}
</div>
</div>
</div>

View File

@@ -242,33 +242,33 @@ export function RemoteEntityInspector({
details.components &&
Array.isArray(details.components) &&
details.components.length > 0 && (
<div className="inspector-section">
<div className="section-title"> ({details.components.length})</div>
{details.components.map((comp, index) => {
const registry = Core.services.resolve(PropertyRendererRegistry);
const context: PropertyContext = {
name: comp.typeName || `Component ${index}`,
decimalPlaces,
readonly: true,
expandByDefault: true,
depth: 0
};
const rendered = registry.render(comp, context);
return rendered ? <div key={index}>{rendered}</div> : null;
})}
</div>
)}
<div className="inspector-section">
<div className="section-title"> ({details.components.length})</div>
{details.components.map((comp, index) => {
const registry = Core.services.resolve(PropertyRendererRegistry);
const context: PropertyContext = {
name: comp.typeName || `Component ${index}`,
decimalPlaces,
readonly: true,
expandByDefault: true,
depth: 0
};
const rendered = registry.render(comp, context);
return rendered ? <div key={index}>{rendered}</div> : null;
})}
</div>
)}
{details &&
Object.entries(details).filter(([key]) => key !== 'components' && key !== 'componentTypes')
.length > 0 && (
<div className="inspector-section">
<div className="section-title"></div>
{Object.entries(details)
.filter(([key]) => key !== 'components' && key !== 'componentTypes')
.map(([key, value]) => renderRemoteProperty(key, value))}
</div>
)}
<div className="inspector-section">
<div className="section-title"></div>
{Object.entries(details)
.filter(([key]) => key !== 'components' && key !== 'componentTypes')
.map(([key, value]) => renderRemoteProperty(key, value))}
</div>
)}
</div>
</div>
);