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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export { AssetFileInspector } from './AssetFileInspector';
|
||||
export { RemoteEntityInspector } from './RemoteEntityInspector';
|
||||
export { EntityInspector } from './EntityInspector';
|
||||
export { PrefabInspector } from './PrefabInspector';
|
||||
export { VirtualNodeInspector } from './VirtualNodeInspector';
|
||||
|
||||
Reference in New Issue
Block a user