Files
esengine/packages/fairygui/src/core/GComponent.ts
YHH a1e1189f9d 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 引用
2025-12-22 10:52:54 +08:00

1006 lines
28 KiB
TypeScript

import { GObject } from './GObject';
import { GGroup } from './GGroup';
import { Controller } from './Controller';
import { Transition } from './Transition';
import { UIObjectFactory } from './UIObjectFactory';
import { EOverflowType, EChildrenRenderOrder } from './FieldTypes';
import { Rectangle, Margin } from '../utils/MathTypes';
import { Container } from '../display/Container';
import type { ScrollPane } from '../scroll/ScrollPane';
import type { IRenderCollector } from '../render/IRenderCollector';
import type { ByteBuffer } from '../utils/ByteBuffer';
import type { PackageItem } from '../package/PackageItem';
/**
* GComponent
*
* Container class that can hold children.
* Supports controllers, transitions, scroll, and masking.
*
* 容器类,可包含子对象,支持控制器、过渡动画、滚动和遮罩
*/
export class GComponent extends GObject {
/** Opaque hit area | 不透明点击区域 */
public opaque: boolean = true;
protected _margin: Margin = new Margin();
protected _trackBounds: boolean = false;
protected _boundsChanged: boolean = false;
protected _childrenRenderOrder: EChildrenRenderOrder = EChildrenRenderOrder.Ascent;
protected _apexIndex: number = 0;
protected _children: GObject[] = [];
protected _controllers: Controller[] = [];
protected _transitions: Transition[] = [];
protected _scrollPane: ScrollPane | null = null;
public _buildingDisplayList: boolean = false;
protected _sortingChildCount: number = 0;
// Overflow and masking | 溢出和遮罩
protected _overflow: EOverflowType = EOverflowType.Visible;
protected _clipRect: Rectangle | null = null;
constructor() {
super();
}
/**
* Create display object for this component
* 为此组件创建显示对象
*/
protected createDisplayObject(): void {
this._displayObject = new Container();
this._displayObject.gOwner = this;
}
// Children management | 子对象管理
/**
* Get child count
* 获取子对象数量
*/
public get numChildren(): number {
return this._children.length;
}
/**
* Add a child
* 添加子对象
*/
public addChild(child: GObject): GObject {
return this.addChildAt(child, this._children.length);
}
/**
* Add child at specific index
* 在指定位置添加子对象
*/
public addChildAt(child: GObject, index: number): GObject {
if (!child) {
throw new Error('child is null');
}
const count = this._children.length;
if (index < 0 || index > count) {
throw new Error('Invalid child index: ' + index);
}
if (child._parent === this) {
this.setChildIndex(child, index);
} else {
child.removeFromParent();
child._parent = this;
if (this._sortingChildCount > 0) {
this._sortingChildCount++;
index = this.getInsertPosForSortingChild(child);
}
this._children.splice(index, 0, child);
this.childStateChanged(child);
this.setBoundsChangedFlag();
}
return child;
}
/**
* Remove a child
* 移除子对象
*/
public removeChild(child: GObject, bDispose: boolean = false): GObject {
const index = this._children.indexOf(child);
if (index !== -1) {
return this.removeChildAt(index, bDispose);
}
return child;
}
/**
* Remove child at index
* 移除指定位置的子对象
*/
public removeChildAt(index: number, bDispose: boolean = false): GObject {
if (index < 0 || index >= this._children.length) {
throw new Error('Invalid child index: ' + index);
}
const child = this._children[index];
child._parent = null;
if (child.sortingOrder !== 0) {
this._sortingChildCount--;
}
this._children.splice(index, 1);
if (child.internalVisible) {
const childDisplay = child.displayObject;
if (this._displayObject && childDisplay) {
this._displayObject.removeChild(childDisplay);
}
}
this.setBoundsChangedFlag();
if (bDispose) {
child.dispose();
}
return child;
}
/**
* Remove children in range
* 移除指定范围的子对象
*/
public removeChildren(beginIndex: number = 0, endIndex: number = -1, bDispose: boolean = false): void {
if (endIndex < 0 || endIndex >= this._children.length) {
endIndex = this._children.length - 1;
}
for (let i = endIndex; i >= beginIndex; i--) {
this.removeChildAt(i, bDispose);
}
}
/**
* Get child at index
* 获取指定位置的子对象
*/
public getChildAt(index: number): GObject {
if (index < 0 || index >= this._children.length) {
throw new Error('Invalid child index: ' + index);
}
return this._children[index];
}
/**
* Get child by name
* 通过名称获取子对象
*/
public getChild(name: string): GObject | null {
for (const child of this._children) {
if (child.name === name) {
return child;
}
}
return null;
}
/**
* Get visible child by name
* 通过名称获取可见子对象
*/
public getVisibleChild(name: string): GObject | null {
for (const child of this._children) {
if (child.name === name && child.internalVisible) {
return child;
}
}
return null;
}
/**
* Get child by ID
* 通过 ID 获取子对象
*/
public getChildById(id: string): GObject | null {
for (const child of this._children) {
if (child.id === id) {
return child;
}
}
return null;
}
/**
* Get child by path
* 通过路径获取子对象
*/
public getChildByPath(path: string): GObject | null {
const arr = path.split('.');
let obj: GObject | null = this;
for (const part of arr) {
if (obj instanceof GComponent) {
obj = obj.getChild(part);
} else {
return null;
}
}
return obj;
}
/**
* Get child index
* 获取子对象索引
*/
public getChildIndex(child: GObject): number {
return this._children.indexOf(child);
}
/**
* Set child index
* 设置子对象索引
*/
public setChildIndex(child: GObject, index: number): void {
const oldIndex = this._children.indexOf(child);
if (oldIndex === -1) {
throw new Error('Not a child of this container');
}
if (child.sortingOrder !== 0) {
return;
}
const count = this._children.length;
if (this._sortingChildCount > 0) {
if (index > count - this._sortingChildCount - 1) {
index = count - this._sortingChildCount - 1;
}
}
this.moveChild(child, oldIndex, index);
}
/**
* Set child index before another child
* 设置子对象索引在另一个子对象之前
*/
public setChildIndexBefore(child: GObject, beforeChild: GObject): number {
const index = this._children.indexOf(child);
if (index === -1) {
throw new Error('Not a child of this container');
}
let beforeIndex = this._children.indexOf(beforeChild);
if (beforeIndex === -1) {
throw new Error('beforeChild is not a child of this container');
}
if (child.sortingOrder !== 0 || beforeChild.sortingOrder !== 0) {
return index;
}
if (index < beforeIndex) {
beforeIndex--;
}
this.moveChild(child, index, beforeIndex);
return beforeIndex;
}
private moveChild(child: GObject, oldIndex: number, newIndex: number): void {
this._children.splice(oldIndex, 1);
this._children.splice(newIndex, 0, child);
const childDisplay = child.displayObject;
if (this._displayObject && childDisplay && child.internalVisible) {
// Update display list order
this._displayObject.setChildIndex(childDisplay, newIndex);
}
this.setBoundsChangedFlag();
}
/**
* Swap children positions
* 交换两个子对象位置
*/
public swapChildren(child1: GObject, child2: GObject): void {
const index1 = this._children.indexOf(child1);
const index2 = this._children.indexOf(child2);
if (index1 === -1 || index2 === -1) {
throw new Error('Not a child of this container');
}
this.swapChildrenAt(index1, index2);
}
/**
* Swap children at positions
* 交换指定位置的子对象
*/
public swapChildrenAt(index1: number, index2: number): void {
const child1 = this._children[index1];
const child2 = this._children[index2];
this.setChildIndex(child1, index2);
this.setChildIndex(child2, index1);
}
/**
* Check if contains a child
* 检查是否包含某个子对象
*/
public isChildInView(child: GObject): boolean {
if (this._scrollPane) {
return this._scrollPane.isChildInView(child);
}
if (this._clipRect) {
return child.x + child.width > 0 &&
child.x < this._width &&
child.y + child.height > 0 &&
child.y < this._height;
}
return true;
}
/**
* Get children in a group
* 获取某个组内的所有子对象
*/
public getChildrenInGroup(group: GGroup): GObject[] {
const result: GObject[] = [];
for (const child of this._children) {
if (child.group === group) {
result.push(child);
}
}
return result;
}
private getInsertPosForSortingChild(target: GObject): number {
const count = this._children.length;
let i: number;
for (i = 0; i < count; i++) {
const child = this._children[i];
if (child === target) continue;
if (target.sortingOrder < child.sortingOrder) break;
}
return i;
}
// Controller management | 控制器管理
/**
* Get controller count
* 获取控制器数量
*/
public get numControllers(): number {
return this._controllers.length;
}
/**
* Get controller at index
* 获取指定索引的控制器
*/
public getControllerAt(index: number): Controller {
return this._controllers[index];
}
/**
* Get controller by name
* 通过名称获取控制器
*/
public getController(name: string): Controller | null {
for (const c of this._controllers) {
if (c.name === name) {
return c;
}
}
return null;
}
/**
* Add controller
* 添加控制器
*/
public addController(controller: Controller): void {
this._controllers.push(controller);
controller.parent = this;
this.applyController(controller);
}
/**
* Remove controller
* 移除控制器
*/
public removeController(controller: Controller): void {
const index = this._controllers.indexOf(controller);
if (index !== -1) {
controller.parent = null;
this._controllers.splice(index, 1);
for (const child of this._children) {
child.handleControllerChanged(controller);
}
}
}
/**
* Apply controller changes
* 应用控制器变更
*/
public applyController(controller: Controller): void {
for (const child of this._children) {
child.handleControllerChanged(controller);
}
}
/**
* Apply all controllers
* 应用所有控制器
*/
public applyAllControllers(): void {
for (const c of this._controllers) {
this.applyController(c);
}
}
// Transition management | 过渡动画管理
/**
* Get transition at index
* 获取指定索引的过渡动画
*/
public getTransitionAt(index: number): Transition {
return this._transitions[index];
}
/**
* Get transition by name
* 通过名称获取过渡动画
*/
public getTransition(name: string): Transition | null {
for (const t of this._transitions) {
if (t.name === name) {
return t;
}
}
return null;
}
// Scroll pane | 滚动面板
public get scrollPane(): ScrollPane | null {
return this._scrollPane;
}
// Overflow | 溢出
public get overflow(): EOverflowType {
return this._overflow;
}
public set overflow(value: EOverflowType) {
if (this._overflow !== value) {
this._overflow = value;
if (value === EOverflowType.Hidden) {
this._clipRect = new Rectangle(0, 0, this._width, this._height);
} else if (value === EOverflowType.Visible) {
this._clipRect = null;
}
}
}
// Children render order | 子对象渲染顺序
public get childrenRenderOrder(): EChildrenRenderOrder {
return this._childrenRenderOrder;
}
public set childrenRenderOrder(value: EChildrenRenderOrder) {
if (this._childrenRenderOrder !== value) {
this._childrenRenderOrder = value;
this.buildNativeDisplayList();
}
}
public get apexIndex(): number {
return this._apexIndex;
}
public set apexIndex(value: number) {
if (this._apexIndex !== value) {
this._apexIndex = value;
if (this._childrenRenderOrder === EChildrenRenderOrder.Arch) {
this.buildNativeDisplayList();
}
}
}
// Bounds management | 边界管理
/**
* Set bounds changed flag
* 设置边界变更标记
*/
public setBoundsChangedFlag(): void {
if (!this._boundsChanged) {
this._boundsChanged = true;
}
}
/**
* Ensure bounds are correct
* 确保边界正确
*/
public ensureBoundsCorrect(): void {
if (this._boundsChanged) {
this.updateBounds();
}
}
protected updateBounds(): void {
let ax = 0, ay = 0, aw = 0, ah = 0;
for (const child of this._children) {
const ar = child.x + child.actualWidth;
const ab = child.y + child.actualHeight;
if (ar > aw) aw = ar;
if (ab > ah) ah = ab;
}
this.setBounds(ax, ay, aw, ah);
}
public setBounds(ax: number, ay: number, aw: number, ah: number): void {
this._boundsChanged = false;
}
// Child state | 子对象状态
/**
* Notify child state changed
* 通知子对象状态改变
*/
public childStateChanged(child: GObject): void {
if (this._buildingDisplayList) return;
const childDisplay = child.displayObject;
if (child.internalVisible) {
if (this._displayObject && childDisplay) {
if (childDisplay.parent !== this._displayObject) {
const index = this.getChildIndex(child);
this._displayObject.addChildAt(childDisplay, index);
}
}
} else {
if (this._displayObject && childDisplay) {
this._displayObject.removeChild(childDisplay);
}
}
}
/**
* Notify child sorting order changed
* 通知子对象排序顺序改变
*/
public childSortingOrderChanged(child: GObject, oldValue: number, newValue: number): void {
if (newValue === 0) {
this._sortingChildCount--;
this.setChildIndex(child, this._children.length);
} else {
if (oldValue === 0) {
this._sortingChildCount++;
}
const oldIndex = this._children.indexOf(child);
const newIndex = this.getInsertPosForSortingChild(child);
if (oldIndex < newIndex) {
this.moveChild(child, oldIndex, newIndex - 1);
} else {
this.moveChild(child, oldIndex, newIndex);
}
}
}
// Display list building | 构建显示列表
protected buildNativeDisplayList(): void {
if (!this._displayObject) return;
this._buildingDisplayList = true;
const count = this._children.length;
if (count === 0) {
this._buildingDisplayList = false;
return;
}
switch (this._childrenRenderOrder) {
case EChildrenRenderOrder.Ascent:
for (let i = 0; i < count; i++) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
break;
case EChildrenRenderOrder.Descent:
for (let i = count - 1; i >= 0; i--) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
break;
case EChildrenRenderOrder.Arch:
for (let i = 0; i < this._apexIndex; i++) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
for (let i = count - 1; i >= this._apexIndex; i--) {
const child = this._children[i];
const childDisplay = child.displayObject;
if (child.internalVisible && childDisplay) {
this._displayObject.addChild(childDisplay);
}
}
break;
}
this._buildingDisplayList = false;
}
// Setup methods | 设置方法
/**
* Setup scroll pane from buffer
* 从缓冲区设置滚动面板
*/
protected setupScroll(_buffer: ByteBuffer): void {
// ScrollPane setup will be implemented when needed
// For now, create a basic scroll pane
const { ScrollPane } = require('../scroll/ScrollPane');
this._scrollPane = new ScrollPane(this);
}
/**
* Setup overflow behavior
* 设置溢出行为
*/
protected setupOverflow(overflow: EOverflowType): void {
this._overflow = overflow;
if (overflow === EOverflowType.Hidden) {
this._clipRect = new Rectangle(0, 0, this._width, this._height);
}
}
// Construction from resource | 从资源构建
/**
* Construct from resource
* 从资源构建
*/
public constructFromResource(): void {
this.constructFromResource2(null, 0);
}
/**
* Construct from resource with object pool
* 从资源构建(使用对象池)
*/
public constructFromResource2(objectPool: GObject[] | null, poolIndex: number): void {
if (!this.packageItem) {
console.warn('[GComponent] constructFromResource2: packageItem is null');
return;
}
const contentItem = this.packageItem.getBranch ? this.packageItem.getBranch() : this.packageItem;
if (!contentItem.rawData) {
console.warn('[GComponent] constructFromResource2: rawData is null for', contentItem.name);
return;
}
const buffer = contentItem.rawData;
buffer.seek(0, 0);
this._underConstruct = true;
this.sourceWidth = buffer.getInt32();
this.sourceHeight = buffer.getInt32();
this.initWidth = this.sourceWidth;
this.initHeight = this.sourceHeight;
this.setSize(this.sourceWidth, this.sourceHeight);
if (buffer.readBool()) {
this.minWidth = buffer.getInt32();
this.maxWidth = buffer.getInt32();
this.minHeight = buffer.getInt32();
this.maxHeight = buffer.getInt32();
}
if (buffer.readBool()) {
const f1 = buffer.getFloat32();
const f2 = buffer.getFloat32();
this.internalSetPivot(f1, f2, buffer.readBool());
}
if (buffer.readBool()) {
this._margin.top = buffer.getInt32();
this._margin.bottom = buffer.getInt32();
this._margin.left = buffer.getInt32();
this._margin.right = buffer.getInt32();
}
const overflow = buffer.readByte() as EOverflowType;
if (overflow === EOverflowType.Scroll) {
const savedPos = buffer.pos;
buffer.seek(0, 7);
this.setupScroll(buffer);
buffer.pos = savedPos;
} else {
this.setupOverflow(overflow);
}
if (buffer.readBool()) {
buffer.skip(8); // Skip custom data
}
this._buildingDisplayList = true;
// Read controllers
buffer.seek(0, 1);
const controllerCount = buffer.getInt16();
for (let i = 0; i < controllerCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
const controller = new Controller();
this._controllers.push(controller);
controller.parent = this;
controller.setup(buffer);
buffer.pos = nextPos;
}
// Read children
buffer.seek(0, 2);
const childCount = buffer.getInt16();
for (let i = 0; i < childCount; i++) {
const dataLen = buffer.getInt16();
const curPos = buffer.pos;
let child: GObject;
if (objectPool) {
child = objectPool[poolIndex + i];
} else {
buffer.seek(curPos, 0);
const type = buffer.readByte();
const src = buffer.readS();
const pkgId = buffer.readS();
let pi: PackageItem | null = null;
// Note: readS() returns '' for null/empty, so check for non-empty string
if (src && contentItem.owner) {
const pkg = pkgId
? contentItem.owner.getPackageById(pkgId)
: contentItem.owner;
pi = pkg ? pkg.getItemById(src) : null;
} else {
}
if (pi) {
child = UIObjectFactory.newObject(pi);
child.constructFromResource();
} else {
child = UIObjectFactory.newObject(type);
}
}
child._underConstruct = true;
child.setup_beforeAdd(buffer, curPos);
child._parent = this;
this._children.push(child);
buffer.pos = curPos + dataLen;
}
// Setup relations for this component
buffer.seek(0, 3);
this.relations.setup(buffer, true);
// Setup relations for children
buffer.seek(0, 2);
buffer.skip(2); // Skip child count
for (let i = 0; i < childCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
buffer.seek(buffer.pos, 3);
this._children[i].relations.setup(buffer, false);
buffer.pos = nextPos;
}
// Call setup_afterAdd for children
buffer.seek(0, 2);
buffer.skip(2); // Skip child count
for (let i = 0; i < childCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
const child = this._children[i];
child.setup_afterAdd(buffer, buffer.pos);
child._underConstruct = false;
buffer.pos = nextPos;
}
// Read custom properties
buffer.seek(0, 4);
buffer.skip(2); // customData
this.opaque = buffer.readBool();
const maskId = buffer.getInt16();
if (maskId !== -1) {
// Mask setup - skip for now
buffer.readBool(); // inverted mask
}
// Hit test - skip for now
buffer.readS(); // hitTestId
buffer.getInt32(); // i1
buffer.getInt32(); // i2
// Read transitions
buffer.seek(0, 5);
const transitionCount = buffer.getInt16();
for (let i = 0; i < transitionCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
const trans = new Transition(this);
trans.setup(buffer);
this._transitions.push(trans);
buffer.pos = nextPos;
}
// Apply all controllers
if (this._transitions.length > 0) {
for (const trans of this._transitions) {
if (trans.autoPlay) {
trans.play();
}
}
}
this.applyAllControllers();
this._buildingDisplayList = false;
this._underConstruct = false;
this.buildNativeDisplayList();
this.setBoundsChangedFlag();
if (contentItem.objectType !== undefined) {
this.constructExtension(buffer);
}
this.onConstruct();
}
/**
* Internal set pivot
* 内部设置轴心
*/
protected internalSetPivot(xv: number, yv: number, bAsAnchor: boolean): void {
this._pivotX = xv;
this._pivotY = yv;
this._pivotAsAnchor = bAsAnchor;
}
/**
* Construct extension (override in subclasses)
* 构建扩展(子类重写)
*/
protected constructExtension(_buffer: ByteBuffer): void {
// Override in subclasses
}
/**
* Called after construction is complete
* 构建完成后调用
*/
protected onConstruct(): void {
// Override in subclasses
}
// Size handling | 尺寸处理
protected handleSizeChanged(): void {
super.handleSizeChanged();
if (this._clipRect) {
this._clipRect.width = this._width;
this._clipRect.height = this._height;
}
if (this._scrollPane) {
this._scrollPane.onOwnerSizeChanged();
}
}
// Disposal | 销毁
public dispose(): void {
for (const t of this._transitions) {
t.dispose();
}
this._transitions.length = 0;
for (const c of this._controllers) {
c.dispose();
}
this._controllers.length = 0;
if (this._scrollPane) {
this._scrollPane.dispose();
this._scrollPane = null;
}
this.removeChildren(0, -1, true);
super.dispose();
}
// Render data collection | 渲染数据收集
public collectRenderData(collector: IRenderCollector): void {
if (!this._visible) return;
// Update this component's display object transform
// 更新此组件的显示对象变换
if (this._displayObject) {
this._displayObject.updateTransform();
}
if (this._clipRect) {
collector.pushClipRect(this._clipRect);
}
for (const child of this._children) {
if (child.internalVisible) {
child.collectRenderData(collector);
}
}
if (this._clipRect) {
collector.popClipRect();
}
}
}