feat(fairygui): FairyGUI 完整集成 (#314)

* feat(fairygui): FairyGUI ECS 集成核心架构

实现 FairyGUI 的 ECS 原生集成,完全替代旧 UI 系统:

核心类:
- GObject: UI 对象基类,支持变换、可见性、关联、齿轮
- GComponent: 容器组件,管理子对象和控制器
- GRoot: 根容器,管理焦点、弹窗、输入分发
- GGroup: 组容器,支持水平/垂直布局

抽象层:
- DisplayObject: 显示对象基类
- EventDispatcher: 事件分发
- Timer: 计时器
- Stage: 舞台,管理输入和缩放

布局系统:
- Relations: 约束关联管理
- RelationItem: 24 种关联类型

基础设施:
- Controller: 状态控制器
- Transition: 过渡动画
- ScrollPane: 滚动面板
- UIPackage: 包管理
- ByteBuffer: 二进制解析

* refactor(ui): 删除旧 UI 系统,使用 FairyGUI 替代

* feat(fairygui): 实现 UI 控件

- 添加显示类:Image、TextField、Graph
- 添加基础控件:GImage、GTextField、GGraph
- 添加交互控件:GButton、GProgressBar、GSlider
- 更新 IRenderCollector 支持 Graph 渲染
- 扩展 Controller 添加 selectedPageId
- 添加 STATE_CHANGED 事件类型

* feat(fairygui): 现代化架构重构

- 增强 EventDispatcher 支持类型安全、优先级和传播控制
- 添加 PropertyBinding 响应式属性绑定系统
- 添加 ServiceContainer 依赖注入容器
- 添加 UIConfig 全局配置系统
- 添加 UIObjectFactory 对象工厂
- 实现 RenderBridge 渲染桥接层
- 实现 Canvas2DBackend 作为默认渲染后端
- 扩展 IRenderCollector 支持更多图元类型

* feat(fairygui): 九宫格渲染和资源加载修复

- 修复 FGUIUpdateSystem 支持路径和 GUID 两种加载方式
- 修复 GTextInput 同时设置 _displayObject 和 _textField
- 实现九宫格渲染展开为 9 个子图元
- 添加 sourceWidth/sourceHeight 用于九宫格计算
- 添加 DOMTextRenderer 文本渲染层(临时方案)

* fix(fairygui): 修复 GGraph 颜色读取

* feat(fairygui): 虚拟节点 Inspector 和文本渲染支持

* fix(fairygui): 编辑器状态刷新和遗留引用修复

- 修复切换 FGUI 包后组件列表未刷新问题
- 修复切换组件后 viewport 未清理旧内容问题
- 修复虚拟节点在包加载后未刷新问题
- 重构为事件驱动架构,移除轮询机制
- 修复 @esengine/ui 遗留引用,统一使用 @esengine/fairygui

* fix: 移除 tsconfig 中的 @esengine/ui 引用
This commit is contained in:
YHH
2025-12-22 10:52:54 +08:00
committed by GitHub
parent 96b5403d14
commit a1e1189f9d
237 changed files with 30983 additions and 23563 deletions

View File

@@ -15,7 +15,8 @@ import {
ExtensionInspector,
AssetFileInspector,
RemoteEntityInspector,
PrefabInspector
PrefabInspector,
VirtualNodeInspector
} from './views';
import { EntityInspectorPanel } from '../inspector';
@@ -112,5 +113,14 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi
);
}
if (target.type === 'virtual-node') {
return (
<VirtualNodeInspector
parentEntityId={target.data.parentEntityId}
virtualNode={target.data.virtualNode}
/>
);
}
return null;
}

View File

@@ -1,5 +1,6 @@
import { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub, InspectorRegistry, CommandManager } from '@esengine/editor-core';
import type { IVirtualNode } from '@esengine/editor-core';
export interface InspectorProps {
entityStore: EntityStoreService;
@@ -20,11 +21,22 @@ export interface AssetFileInfo {
type ExtensionData = Record<string, any>;
/**
* Virtual node target data
* 虚拟节点目标数据
*/
export interface VirtualNodeTargetData {
parentEntityId: number;
virtualNodeId: string;
virtualNode: IVirtualNode;
}
export type InspectorTarget =
| { type: 'entity'; data: Entity }
| { type: 'remote-entity'; data: RemoteEntity; details?: EntityDetails }
| { type: 'asset-file'; data: AssetFileInfo; content?: string; isImage?: boolean }
| { type: 'extension'; data: ExtensionData }
| { type: 'virtual-node'; data: VirtualNodeTargetData }
| null;
export interface RemoteEntity {

View File

@@ -0,0 +1,324 @@
/**
* 虚拟节点检查器
* Virtual Node Inspector
*
* 显示 FGUI 等组件内部虚拟节点的只读属性
* Displays read-only properties of virtual nodes from components like FGUI
*/
import type { IVirtualNode } from '@esengine/editor-core';
import { Box, Eye, EyeOff, Move, Maximize2, RotateCw, Palette, Type, Image, Square, Layers, MousePointer, Sliders } from 'lucide-react';
import '../../../styles/VirtualNodeInspector.css';
interface VirtualNodeInspectorProps {
parentEntityId: number;
virtualNode: IVirtualNode;
}
/**
* Format number to fixed decimal places
* 格式化数字到固定小数位
*/
function formatNumber(value: number | undefined, decimals: number = 2): string {
if (value === undefined || value === null) return '-';
return value.toFixed(decimals);
}
/**
* Property row component
* 属性行组件
*/
function PropertyRow({ label, value, icon }: { label: string; value: React.ReactNode; icon?: React.ReactNode }) {
return (
<div className="virtual-node-property-row">
<span className="property-label">
{icon && <span className="property-icon">{icon}</span>}
{label}
</span>
<span className="property-value">{value}</span>
</div>
);
}
/**
* Section component
* 分组组件
*/
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="virtual-node-section">
<div className="section-header">{title}</div>
<div className="section-content">{children}</div>
</div>
);
}
/**
* Color swatch component for displaying colors
* 颜色色块组件
*/
function ColorSwatch({ color }: { color: string }) {
return (
<span className="color-swatch-wrapper">
<span
className="color-swatch"
style={{ backgroundColor: color }}
/>
<span className="color-value">{color}</span>
</span>
);
}
/**
* Check if a property key is for common/transform properties
* 检查属性键是否为公共/变换属性
*/
const COMMON_PROPS = new Set([
'className', 'x', 'y', 'width', 'height', 'alpha', 'visible',
'touchable', 'rotation', 'scaleX', 'scaleY', 'pivotX', 'pivotY', 'grayed'
]);
/**
* Property categories for type-specific display
* 类型特定显示的属性分类
*/
const TYPE_SPECIFIC_SECTIONS: Record<string, { title: string; icon: React.ReactNode; props: string[] }> = {
Graph: {
title: '图形属性 | Graph',
icon: <Square size={12} />,
props: ['graphType', 'lineSize', 'lineColor', 'fillColor', 'cornerRadius', 'sides', 'startAngle']
},
Image: {
title: '图像属性 | Image',
icon: <Image size={12} />,
props: ['color', 'flip', 'fillMethod', 'fillOrigin', 'fillClockwise', 'fillAmount']
},
TextField: {
title: '文本属性 | Text',
icon: <Type size={12} />,
props: ['text', 'font', 'fontSize', 'color', 'align', 'valign', 'leading', 'letterSpacing',
'bold', 'italic', 'underline', 'singleLine', 'autoSize', 'stroke', 'strokeColor']
},
Loader: {
title: '加载器属性 | Loader',
icon: <Image size={12} />,
props: ['url', 'align', 'verticalAlign', 'fill', 'shrinkOnly', 'autoSize', 'color',
'fillMethod', 'fillOrigin', 'fillClockwise', 'fillAmount']
},
Button: {
title: '按钮属性 | Button',
icon: <MousePointer size={12} />,
props: ['title', 'icon', 'mode', 'selected', 'titleColor', 'titleFontSize',
'selectedTitle', 'selectedIcon']
},
List: {
title: '列表属性 | List',
icon: <Layers size={12} />,
props: ['defaultItem', 'itemCount', 'selectedIndex', 'scrollPane']
},
ProgressBar: {
title: '进度条属性 | Progress',
icon: <Sliders size={12} />,
props: ['value', 'max']
},
Slider: {
title: '滑块属性 | Slider',
icon: <Sliders size={12} />,
props: ['value', 'max']
},
Component: {
title: '组件属性 | Component',
icon: <Layers size={12} />,
props: ['numChildren', 'numControllers', 'numTransitions']
}
};
/**
* Format a property value for display
* 格式化属性值以供显示
*/
function formatPropertyValue(key: string, value: unknown): React.ReactNode {
if (value === null || value === undefined) {
return '-';
}
// Color properties - show color swatch
if (typeof value === 'string' && (
key.toLowerCase().includes('color') ||
key === 'fillColor' ||
key === 'lineColor' ||
key === 'strokeColor' ||
key === 'titleColor'
)) {
if (value.startsWith('#') || value.startsWith('rgb')) {
return <ColorSwatch color={value} />;
}
}
if (typeof value === 'boolean') {
return value ? 'Yes' : 'No';
}
if (typeof value === 'number') {
return formatNumber(value);
}
if (typeof value === 'string') {
// Truncate long strings
if (value.length > 50) {
return value.substring(0, 47) + '...';
}
return value || '-';
}
return JSON.stringify(value);
}
export function VirtualNodeInspector({ parentEntityId, virtualNode }: VirtualNodeInspectorProps) {
const { name, type, visible, x, y, width, height, data } = virtualNode;
// Extract additional properties from data
// 从 data 中提取额外属性
const alpha = data.alpha as number | undefined;
const rotation = data.rotation as number | undefined;
const scaleX = data.scaleX as number | undefined;
const scaleY = data.scaleY as number | undefined;
const touchable = data.touchable as boolean | undefined;
const grayed = data.grayed as boolean | undefined;
const pivotX = data.pivotX as number | undefined;
const pivotY = data.pivotY as number | undefined;
// Get type-specific section config
const typeSection = TYPE_SPECIFIC_SECTIONS[type];
// Collect type-specific properties
const typeSpecificProps: Array<{ key: string; value: unknown }> = [];
const otherProps: Array<{ key: string; value: unknown }> = [];
Object.entries(data).forEach(([key, value]) => {
if (COMMON_PROPS.has(key)) {
return; // Skip common props
}
if (typeSection?.props.includes(key)) {
typeSpecificProps.push({ key, value });
} else {
otherProps.push({ key, value });
}
});
return (
<div className="entity-inspector virtual-node-inspector">
{/* Header */}
<div className="virtual-node-header">
<Box size={16} className="header-icon" />
<div className="header-info">
<div className="header-name">{name}</div>
<div className="header-type">{type}</div>
</div>
<div className="header-badge">
Virtual Node
</div>
</div>
{/* Read-only notice */}
<div className="virtual-node-notice">
</div>
{/* Basic Properties */}
<Section title="基本属性 | Basic">
<PropertyRow
label="Visible"
value={visible ? <Eye size={14} /> : <EyeOff size={14} className="disabled" />}
/>
{touchable !== undefined && (
<PropertyRow
label="Touchable"
value={touchable ? 'Yes' : 'No'}
/>
)}
{grayed !== undefined && (
<PropertyRow
label="Grayed"
value={grayed ? 'Yes' : 'No'}
/>
)}
{alpha !== undefined && (
<PropertyRow
label="Alpha"
value={formatNumber(alpha)}
icon={<Palette size={12} />}
/>
)}
</Section>
{/* Transform */}
<Section title="变换 | Transform">
<PropertyRow
label="Position"
value={`(${formatNumber(x)}, ${formatNumber(y)})`}
icon={<Move size={12} />}
/>
<PropertyRow
label="Size"
value={`${formatNumber(width)} × ${formatNumber(height)}`}
icon={<Maximize2 size={12} />}
/>
{(rotation !== undefined && rotation !== 0) && (
<PropertyRow
label="Rotation"
value={`${formatNumber(rotation)}°`}
icon={<RotateCw size={12} />}
/>
)}
{(scaleX !== undefined || scaleY !== undefined) && (
<PropertyRow
label="Scale"
value={`(${formatNumber(scaleX ?? 1)}, ${formatNumber(scaleY ?? 1)})`}
/>
)}
{(pivotX !== undefined || pivotY !== undefined) && (
<PropertyRow
label="Pivot"
value={`(${formatNumber(pivotX ?? 0)}, ${formatNumber(pivotY ?? 0)})`}
/>
)}
</Section>
{/* Type-Specific Properties */}
{typeSection && typeSpecificProps.length > 0 && (
<Section title={typeSection.title}>
{typeSpecificProps.map(({ key, value }) => (
<PropertyRow
key={key}
label={key}
value={formatPropertyValue(key, value)}
icon={key === typeSection.props[0] ? typeSection.icon : undefined}
/>
))}
</Section>
)}
{/* Other Properties */}
{otherProps.length > 0 && (
<Section title="其他属性 | Other">
{otherProps.map(({ key, value }) => (
<PropertyRow
key={key}
label={key}
value={formatPropertyValue(key, value)}
/>
))}
</Section>
)}
{/* Debug Info */}
<Section title="调试信息 | Debug">
<PropertyRow label="Parent Entity ID" value={parentEntityId} />
<PropertyRow label="Virtual Node ID" value={virtualNode.id} />
<PropertyRow label="Child Count" value={virtualNode.children?.length ?? 0} />
</Section>
</div>
);
}

View File

@@ -4,3 +4,4 @@ export { AssetFileInspector } from './AssetFileInspector';
export { RemoteEntityInspector } from './RemoteEntityInspector';
export { EntityInspector } from './EntityInspector';
export { PrefabInspector } from './PrefabInspector';
export { VirtualNodeInspector } from './VirtualNodeInspector';