refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,327 @@
import { EventDispatcher } from '../events/EventDispatcher';
import { FGUIEvents } from '../events/Events';
import type { GComponent } from './GComponent';
import type { ByteBuffer } from '../utils/ByteBuffer';
/**
* Controller
*
* Manages state switching for UI components.
* Similar to a state machine, it controls which gear values are active.
*
* 管理 UI 组件的状态切换,类似状态机,控制哪些齿轮值处于活动状态
*/
export class Controller extends EventDispatcher {
/** Controller name | 控制器名称 */
public name: string = '';
/** Parent component | 父组件 */
public parent: GComponent | null = null;
/** Is changing flag | 是否正在变更中 */
public changing: boolean = false;
/** Auto radio group | 自动单选组 */
public autoRadioGroupDepth: boolean = false;
private _selectedIndex: number = 0;
private _previousIndex: number = 0;
private _pageIds: string[] = [];
private _pageNames: string[] = [];
constructor() {
super();
}
/**
* Get selected index
* 获取选中索引
*/
public get selectedIndex(): number {
return this._selectedIndex;
}
/**
* Set selected index
* 设置选中索引
*/
public set selectedIndex(value: number) {
if (this._selectedIndex !== value) {
if (value > this._pageIds.length - 1) {
throw new Error('Index out of bounds: ' + value);
}
this.changing = true;
this._previousIndex = this._selectedIndex;
this._selectedIndex = value;
this.parent?.applyController(this);
this.emit(FGUIEvents.STATUS_CHANGED);
this.changing = false;
}
}
/**
* Get selected page
* 获取选中页面名称
*/
public get selectedPage(): string {
if (this._selectedIndex === -1) {
return '';
}
return this._pageNames[this._selectedIndex] || '';
}
/**
* Set selected page
* 设置选中页面
*/
public set selectedPage(value: string) {
let index = this._pageNames.indexOf(value);
if (index === -1) {
index = this._pageIds.indexOf(value);
}
if (index !== -1) {
this.selectedIndex = index;
}
}
/**
* Get selected page ID
* 获取选中页面 ID
*/
public get selectedPageId(): string {
if (this._selectedIndex === -1) {
return '';
}
return this._pageIds[this._selectedIndex] || '';
}
/**
* Set selected page ID
* 设置选中页面 ID
*/
public set selectedPageId(value: string) {
const index = this._pageIds.indexOf(value);
if (index !== -1) {
this.selectedIndex = index;
}
}
/**
* Get previous selected index
* 获取之前选中的索引
*/
public get previousIndex(): number {
return this._previousIndex;
}
/**
* Get previous selected page
* 获取之前选中的页面
*/
public get previousPage(): string {
if (this._previousIndex === -1) {
return '';
}
return this._pageNames[this._previousIndex] || '';
}
/**
* Get page count
* 获取页面数量
*/
public get pageCount(): number {
return this._pageIds.length;
}
/**
* Get page ID at index
* 获取指定索引的页面 ID
*/
public getPageId(index: number): string {
return this._pageIds[index] || '';
}
/**
* Set page ID at index
* 设置指定索引的页面 ID
*/
public setPageId(index: number, id: string): void {
this._pageIds[index] = id;
}
/**
* Get page name at index
* 获取指定索引的页面名称
*/
public getPageName(index: number): string {
return this._pageNames[index] || '';
}
/**
* Set page name at index
* 设置指定索引的页面名称
*/
public setPageName(index: number, name: string): void {
this._pageNames[index] = name;
}
/**
* Get index by page ID
* 通过页面 ID 获取索引
*/
public getPageIndexById(id: string): number {
return this._pageIds.indexOf(id);
}
/**
* Get ID by page name
* 通过页面名称获取 ID
*/
public getPageIdByName(name: string): string {
const index = this._pageNames.indexOf(name);
if (index !== -1) {
return this._pageIds[index];
}
return '';
}
/**
* Check if the controller has the specified page
* 检查控制器是否有指定页面
*/
public hasPage(aName: string): boolean {
return this._pageNames.indexOf(aName) !== -1;
}
/**
* Add page
* 添加页面
*/
public addPage(name: string = ''): void {
this.addPageAt(name, this._pageIds.length);
}
/**
* Add page at index
* 在指定位置添加页面
*/
public addPageAt(name: string, index: number): void {
const id = '' + (this._pageIds.length > 0 ? parseInt(this._pageIds[this._pageIds.length - 1]) + 1 : 0);
if (index === this._pageIds.length) {
this._pageIds.push(id);
this._pageNames.push(name);
} else {
this._pageIds.splice(index, 0, id);
this._pageNames.splice(index, 0, name);
}
}
/**
* Remove page at index
* 移除指定索引的页面
*/
public removePage(name: string): void {
const index = this._pageNames.indexOf(name);
if (index !== -1) {
this._pageIds.splice(index, 1);
this._pageNames.splice(index, 1);
if (this._selectedIndex >= this._pageIds.length) {
this._selectedIndex = this._pageIds.length - 1;
}
}
}
/**
* Remove page at index
* 移除指定索引的页面
*/
public removePageAt(index: number): void {
this._pageIds.splice(index, 1);
this._pageNames.splice(index, 1);
if (this._selectedIndex >= this._pageIds.length) {
this._selectedIndex = this._pageIds.length - 1;
}
}
/**
* Clear all pages
* 清除所有页面
*/
public clearPages(): void {
this._pageIds.length = 0;
this._pageNames.length = 0;
this._selectedIndex = -1;
}
/**
* Run actions on page changed
* 页面改变时执行动作
*/
public runActions(): void {
// Override in subclasses or handle via events
}
/**
* Setup controller from buffer
* 从缓冲区设置控制器
*/
public setup(buffer: ByteBuffer): void {
const beginPos = buffer.pos;
buffer.seek(beginPos, 0);
this.name = buffer.readS() || '';
if (buffer.readBool()) {
this.autoRadioGroupDepth = true;
}
buffer.seek(beginPos, 1);
const cnt = buffer.getInt16();
for (let i = 0; i < cnt; i++) {
this._pageIds.push(buffer.readS() || '');
this._pageNames.push(buffer.readS() || '');
}
// Home page index (simplified - ignore advanced home page types)
let homePageIndex = 0;
const homePageType = buffer.readByte();
if (homePageType === 1) {
homePageIndex = buffer.getInt16();
} else if (homePageType === 2 || homePageType === 3) {
// Skip variable name for type 3
if (homePageType === 3) {
buffer.readS();
}
}
buffer.seek(beginPos, 2);
// Skip actions for now
const actionCount = buffer.getInt16();
for (let i = 0; i < actionCount; i++) {
let nextPos = buffer.getInt16();
nextPos += buffer.pos;
buffer.pos = nextPos;
}
if (this.parent && this._pageIds.length > 0) {
this._selectedIndex = homePageIndex;
} else {
this._selectedIndex = -1;
}
}
/**
* Dispose
* 销毁
*/
public dispose(): void {
this.parent = null;
super.dispose();
}
}

View File

@@ -0,0 +1,144 @@
import { GObject } from './GObject';
import { GRoot } from './GRoot';
import { GLoader } from '../widgets/GLoader';
import { Stage } from './Stage';
import { FGUIEvents } from '../events/Events';
import { EAlignType, EVertAlignType } from './FieldTypes';
/**
* DragDropManager
*
* Manages drag and drop operations with visual feedback.
*
* 管理带有视觉反馈的拖放操作
*
* Features:
* - Visual drag agent with icon
* - Source data carrying
* - Drop target detection
* - Singleton pattern
*
* @example
* ```typescript
* // Start drag operation
* DragDropManager.inst.startDrag(sourceObj, 'ui://pkg/icon', myData);
*
* // Listen for drop on target
* targetObj.on(FGUIEvents.DROP, (data) => {
* console.log('Dropped:', data);
* });
*
* // Cancel drag
* DragDropManager.inst.cancel();
* ```
*/
export class DragDropManager {
private static _inst: DragDropManager | null = null;
private _agent: GLoader;
private _sourceData: any = null;
/**
* Get singleton instance
* 获取单例实例
*/
public static get inst(): DragDropManager {
if (!DragDropManager._inst) {
DragDropManager._inst = new DragDropManager();
}
return DragDropManager._inst;
}
constructor() {
this._agent = new GLoader();
this._agent.draggable = true;
this._agent.touchable = false; // Important: prevent interference with drop detection
this._agent.setSize(100, 100);
this._agent.setPivot(0.5, 0.5, true);
this._agent.align = EAlignType.Center;
this._agent.verticalAlign = EVertAlignType.Middle;
this._agent.sortingOrder = 1000000;
this._agent.on(FGUIEvents.DRAG_END, this.onDragEnd, this);
}
/**
* Get drag agent object
* 获取拖拽代理对象
*/
public get dragAgent(): GObject {
return this._agent;
}
/**
* Check if currently dragging
* 检查是否正在拖拽
*/
public get dragging(): boolean {
return this._agent.parent !== null;
}
/**
* Start a drag operation
* 开始拖拽操作
*
* @param source - Source object initiating drag | 发起拖拽的源对象
* @param icon - Icon URL for drag agent | 拖拽代理的图标 URL
* @param sourceData - Data to carry during drag | 拖拽期间携带的数据
* @param touchId - Touch point ID for multi-touch | 多点触控的触摸点 ID
*/
public startDrag(source: GObject, icon: string, sourceData?: any, touchId?: number): void {
if (this._agent.parent) {
return;
}
this._sourceData = sourceData;
this._agent.url = icon;
GRoot.inst.addChild(this._agent);
const stage = Stage.inst;
const pt = GRoot.inst.globalToLocal(stage.mouseX, stage.mouseY);
this._agent.setXY(pt.x, pt.y);
this._agent.startDrag(touchId);
}
/**
* Cancel current drag operation
* 取消当前拖拽操作
*/
public cancel(): void {
if (this._agent.parent) {
this._agent.stopDrag();
GRoot.inst.removeChild(this._agent);
this._sourceData = null;
}
}
private onDragEnd(): void {
if (!this._agent.parent) {
// Already cancelled
return;
}
GRoot.inst.removeChild(this._agent);
const sourceData = this._sourceData;
this._sourceData = null;
// Find drop target
const stage = Stage.inst;
const target = GRoot.inst.hitTest(stage.mouseX, stage.mouseY);
if (target) {
// Walk up the display list to find a drop handler
let obj: GObject | null = target;
while (obj) {
if (obj.hasListener(FGUIEvents.DROP)) {
obj.emit(FGUIEvents.DROP, sourceData);
return;
}
obj = obj.parent;
}
}
}
}

View File

@@ -0,0 +1,366 @@
/**
* FairyGUI Field Types
* FairyGUI 字段类型定义
*/
/**
* Button mode
* 按钮模式
*/
export const enum EButtonMode {
Common = 0,
Check = 1,
Radio = 2
}
/**
* Auto size type
* 自动尺寸类型
*/
export const enum EAutoSizeType {
None = 0,
Both = 1,
Height = 2,
Shrink = 3,
Ellipsis = 4
}
/**
* Align type
* 水平对齐类型
*/
export const enum EAlignType {
Left = 0,
Center = 1,
Right = 2
}
/**
* Vertical align type
* 垂直对齐类型
*/
export const enum EVertAlignType {
Top = 0,
Middle = 1,
Bottom = 2
}
/**
* Loader fill type
* 加载器填充类型
*/
export const enum ELoaderFillType {
None = 0,
Scale = 1,
ScaleMatchHeight = 2,
ScaleMatchWidth = 3,
ScaleFree = 4,
ScaleNoBorder = 5
}
/**
* List layout type
* 列表布局类型
*/
export const enum EListLayoutType {
SingleColumn = 0,
SingleRow = 1,
FlowHorizontal = 2,
FlowVertical = 3,
Pagination = 4
}
/**
* List selection mode
* 列表选择模式
*/
export const enum EListSelectionMode {
Single = 0,
Multiple = 1,
MultipleSingleClick = 2,
None = 3
}
/**
* Overflow type
* 溢出类型
*/
export const enum EOverflowType {
Visible = 0,
Hidden = 1,
Scroll = 2
}
/**
* Package item type
* 包资源类型
*/
export const enum EPackageItemType {
Image = 0,
MovieClip = 1,
Sound = 2,
Component = 3,
Atlas = 4,
Font = 5,
Swf = 6,
Misc = 7,
Unknown = 8,
Spine = 9,
DragonBones = 10
}
/**
* Object type
* 对象类型
*/
export const enum EObjectType {
Image = 0,
MovieClip = 1,
Swf = 2,
Graph = 3,
Loader = 4,
Group = 5,
Text = 6,
RichText = 7,
InputText = 8,
Component = 9,
List = 10,
Label = 11,
Button = 12,
ComboBox = 13,
ProgressBar = 14,
Slider = 15,
ScrollBar = 16,
Tree = 17,
Loader3D = 18
}
/**
* Progress title type
* 进度条标题类型
*/
export const enum EProgressTitleType {
Percent = 0,
ValueAndMax = 1,
Value = 2,
Max = 3
}
/**
* ScrollBar display type
* 滚动条显示类型
*/
export const enum EScrollBarDisplayType {
Default = 0,
Visible = 1,
Auto = 2,
Hidden = 3
}
/**
* Scroll type
* 滚动类型
*/
export const enum EScrollType {
Horizontal = 0,
Vertical = 1,
Both = 2
}
/**
* Flip type
* 翻转类型
*/
export const enum EFlipType {
None = 0,
Horizontal = 1,
Vertical = 2,
Both = 3
}
/**
* Children render order
* 子对象渲染顺序
*/
export const enum EChildrenRenderOrder {
Ascent = 0,
Descent = 1,
Arch = 2
}
/**
* Group layout type
* 组布局类型
*/
export const enum EGroupLayoutType {
None = 0,
Horizontal = 1,
Vertical = 2
}
/**
* Popup direction
* 弹出方向
*/
export const enum EPopupDirection {
Auto = 0,
Up = 1,
Down = 2
}
/**
* Relation type
* 关联类型
*/
export const enum ERelationType {
LeftLeft = 0,
LeftCenter = 1,
LeftRight = 2,
CenterCenter = 3,
RightLeft = 4,
RightCenter = 5,
RightRight = 6,
TopTop = 7,
TopMiddle = 8,
TopBottom = 9,
MiddleMiddle = 10,
BottomTop = 11,
BottomMiddle = 12,
BottomBottom = 13,
Width = 14,
Height = 15,
LeftExtLeft = 16,
LeftExtRight = 17,
RightExtLeft = 18,
RightExtRight = 19,
TopExtTop = 20,
TopExtBottom = 21,
BottomExtTop = 22,
BottomExtBottom = 23,
Size = 24
}
/**
* Fill method
* 填充方法
*/
export const enum EFillMethod {
None = 0,
Horizontal = 1,
Vertical = 2,
Radial90 = 3,
Radial180 = 4,
Radial360 = 5
}
/**
* Fill origin
* 填充起点
*/
export const enum EFillOrigin {
Top = 0,
Bottom = 1,
Left = 2,
Right = 3,
TopLeft = 0,
TopRight = 1,
BottomLeft = 2,
BottomRight = 3
}
/**
* Object property ID
* 对象属性 ID
*/
export const enum EObjectPropID {
Text = 0,
Icon = 1,
Color = 2,
OutlineColor = 3,
Playing = 4,
Frame = 5,
DeltaTime = 6,
TimeScale = 7,
FontSize = 8,
Selected = 9
}
/**
* Gear type
* 齿轮类型
*/
export const enum EGearType {
Display = 0,
XY = 1,
Size = 2,
Look = 3,
Color = 4,
Animation = 5,
Text = 6,
Icon = 7,
Display2 = 8,
FontSize = 9
}
// EEaseType is re-exported from tween module
export { EEaseType } from '../tween/EaseType';
/**
* Blend mode
* 混合模式
*/
export const enum EBlendMode {
Normal = 0,
None = 1,
Add = 2,
Multiply = 3,
Screen = 4,
Erase = 5,
Mask = 6,
Below = 7,
Off = 8,
Custom1 = 9,
Custom2 = 10,
Custom3 = 11
}
/**
* Transition action type
* 过渡动作类型
*/
export const enum ETransitionActionType {
XY = 0,
Size = 1,
Scale = 2,
Pivot = 3,
Alpha = 4,
Rotation = 5,
Color = 6,
Animation = 7,
Visible = 8,
Sound = 9,
Transition = 10,
Shake = 11,
ColorFilter = 12,
Skew = 13,
Text = 14,
Icon = 15,
Unknown = 16
}
/**
* Graph type
* 图形类型
*/
export const enum EGraphType {
Empty = 0,
Rect = 1,
Ellipse = 2,
Polygon = 3,
RegularPolygon = 4
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,261 @@
import { GObject } from './GObject';
import { EGroupLayoutType } from './FieldTypes';
/**
* GGroup
*
* Group container for layout and visibility control.
* Can arrange children horizontally, vertically, or have no layout.
*
* 组容器,用于布局和可见性控制,可水平、垂直或无布局排列子元素
*/
export class GGroup extends GObject {
/** Exclude invisible children from layout | 从布局中排除不可见子元素 */
public excludeInvisibles: boolean = false;
private _layout: EGroupLayoutType = EGroupLayoutType.None;
private _lineGap: number = 0;
private _columnGap: number = 0;
private _mainGridIndex: number = -1;
private _mainGridMinSize: number = 50;
private _boundsChanged: boolean = false;
private _updating: boolean = false;
public get layout(): EGroupLayoutType {
return this._layout;
}
public set layout(value: EGroupLayoutType) {
if (this._layout !== value) {
this._layout = value;
this.setBoundsChangedFlag(true);
}
}
public get lineGap(): number {
return this._lineGap;
}
public set lineGap(value: number) {
if (this._lineGap !== value) {
this._lineGap = value;
this.setBoundsChangedFlag();
}
}
public get columnGap(): number {
return this._columnGap;
}
public set columnGap(value: number) {
if (this._columnGap !== value) {
this._columnGap = value;
this.setBoundsChangedFlag();
}
}
public get mainGridIndex(): number {
return this._mainGridIndex;
}
public set mainGridIndex(value: number) {
if (this._mainGridIndex !== value) {
this._mainGridIndex = value;
this.setBoundsChangedFlag();
}
}
public get mainGridMinSize(): number {
return this._mainGridMinSize;
}
public set mainGridMinSize(value: number) {
if (this._mainGridMinSize !== value) {
this._mainGridMinSize = value;
this.setBoundsChangedFlag();
}
}
/**
* Set bounds changed flag
* 设置边界变更标记
*/
public setBoundsChangedFlag(bPositionChanged: boolean = false): void {
if (this._updating) return;
if (bPositionChanged) {
// Position changed, need to recalculate
}
if (!this._boundsChanged) {
this._boundsChanged = true;
}
}
/**
* Ensure bounds are up to date
* 确保边界是最新的
*/
public ensureBoundsCorrect(): void {
if (this._boundsChanged) {
this.updateBounds();
}
}
private updateBounds(): void {
this._boundsChanged = false;
if (!this._parent) return;
this._updating = true;
const children = this._parent.getChildrenInGroup(this);
const count = children.length;
if (count === 0) {
this._updating = false;
return;
}
if (this._layout === EGroupLayoutType.None) {
this.updateBoundsNone(children);
} else if (this._layout === EGroupLayoutType.Horizontal) {
this.updateBoundsHorizontal(children);
} else {
this.updateBoundsVertical(children);
}
this._updating = false;
}
private updateBoundsNone(children: GObject[]): void {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const child of children) {
if (this.excludeInvisibles && !child.internalVisible3) continue;
const ax = child.xMin;
const ay = child.yMin;
if (ax < minX) minX = ax;
if (ay < minY) minY = ay;
if (ax + child.width > maxX) maxX = ax + child.width;
if (ay + child.height > maxY) maxY = ay + child.height;
}
if (minX === Infinity) {
minX = 0;
minY = 0;
maxX = 0;
maxY = 0;
}
this._width = maxX - minX;
this._height = maxY - minY;
}
private updateBoundsHorizontal(children: GObject[]): void {
let totalWidth = 0;
let maxHeight = 0;
let visibleCount = 0;
for (const child of children) {
if (this.excludeInvisibles && !child.internalVisible3) continue;
totalWidth += child.width;
if (child.height > maxHeight) maxHeight = child.height;
visibleCount++;
}
if (visibleCount > 0) {
totalWidth += (visibleCount - 1) * this._columnGap;
}
this._width = totalWidth;
this._height = maxHeight;
}
private updateBoundsVertical(children: GObject[]): void {
let maxWidth = 0;
let totalHeight = 0;
let visibleCount = 0;
for (const child of children) {
if (this.excludeInvisibles && !child.internalVisible3) continue;
totalHeight += child.height;
if (child.width > maxWidth) maxWidth = child.width;
visibleCount++;
}
if (visibleCount > 0) {
totalHeight += (visibleCount - 1) * this._lineGap;
}
this._width = maxWidth;
this._height = totalHeight;
}
/**
* Move children when group is moved
* 组移动时移动子元素
*/
public moveChildren(dx: number, dy: number): void {
if (this._updating || !this._parent) return;
this._updating = true;
const children = this._parent.getChildrenInGroup(this);
for (const child of children) {
child.setXY(child.x + dx, child.y + dy);
}
this._updating = false;
}
/**
* Resize children when group is resized
* 组调整大小时调整子元素
*/
public resizeChildren(dw: number, dh: number): void {
if (this._layout === EGroupLayoutType.None || this._updating || !this._parent) return;
this._updating = true;
const children = this._parent.getChildrenInGroup(this);
const count = children.length;
if (count > 0) {
if (this._layout === EGroupLayoutType.Horizontal) {
const remainingWidth = this._width + dw - (count - 1) * this._columnGap;
let x = children[0].xMin;
for (const child of children) {
if (this.excludeInvisibles && !child.internalVisible3) continue;
const newWidth = child._sizePercentInGroup * remainingWidth;
child.setSize(newWidth, child.height + dh);
child.xMin = x;
x += newWidth + this._columnGap;
}
} else {
const remainingHeight = this._height + dh - (count - 1) * this._lineGap;
let y = children[0].yMin;
for (const child of children) {
if (this.excludeInvisibles && !child.internalVisible3) continue;
const newHeight = child._sizePercentInGroup * remainingHeight;
child.setSize(child.width + dw, newHeight);
child.yMin = y;
y += newHeight + this._lineGap;
}
}
}
this._updating = false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
import type { GObject } from './GObject';
import { UIPackage } from '../package/UIPackage';
/**
* GObjectPool
*
* Object pool for GObject instances, used for efficient UI recycling.
* Objects are pooled by their resource URL.
*
* GObject 实例对象池,用于高效的 UI 回收。对象按资源 URL 分池管理。
*/
export class GObjectPool {
private _pool: Map<string, GObject[]> = new Map();
private _count: number = 0;
/**
* Get total pooled object count
* 获取池中对象总数
*/
public get count(): number {
return this._count;
}
/**
* Clear all pooled objects
* 清空所有池化对象
*/
public clear(): void {
for (const [, arr] of this._pool) {
for (const obj of arr) {
obj.dispose();
}
}
this._pool.clear();
this._count = 0;
}
/**
* Get object from pool or create new one
* 从池中获取对象或创建新对象
*
* @param url Resource URL | 资源 URL
* @returns GObject instance or null | GObject 实例或 null
*/
public getObject(url: string): GObject | null {
url = UIPackage.normalizeURL(url);
if (!url) return null;
const arr = this._pool.get(url);
if (arr && arr.length > 0) {
this._count--;
return arr.shift()!;
}
return UIPackage.createObjectFromURL(url);
}
/**
* Return object to pool
* 将对象归还到池中
*
* @param obj GObject to return | 要归还的 GObject
*/
public returnObject(obj: GObject): void {
const url = obj.resourceURL;
if (!url) return;
let arr = this._pool.get(url);
if (!arr) {
arr = [];
this._pool.set(url, arr);
}
this._count++;
arr.push(obj);
}
}

View File

@@ -0,0 +1,506 @@
import { GComponent } from './GComponent';
import { GObject } from './GObject';
import { Stage } from './Stage';
import { Timer } from './Timer';
import { FGUIEvents, IInputEventData } from '../events/Events';
import type { IRenderCollector } from '../render/IRenderCollector';
/**
* GRoot
*
* Root container for all UI elements.
* Manages focus, popups, tooltips, and input dispatch.
*
* 所有 UI 元素的根容器,管理焦点、弹出窗口、提示和输入分发
*/
export class GRoot extends GComponent {
private static _inst: GRoot | null = null;
private _focus: GObject | null = null;
private _tooltipWin: GObject | null = null;
private _defaultTooltipWin: GObject | null = null;
private _popupStack: GObject[] = [];
private _justClosedPopups: GObject[] = [];
private _modalLayer: GObject | null = null;
private _modalWaitPane: GObject | null = null;
private _inputProcessor: InputProcessor;
constructor() {
super();
this._inputProcessor = new InputProcessor(this);
// Set this as stage root so children receive addedToStage events
// 将自己设置为舞台根,这样子对象才能收到 addedToStage 事件
if (this.displayObject) {
this.displayObject.setStage(this.displayObject);
}
// Bind to stage events
const stage = Stage.inst;
stage.on('mousedown', this.onStageMouseDown, this);
stage.on('mouseup', this.onStageMouseUp, this);
stage.on('mousemove', this.onStageMouseMove, this);
stage.on('wheel', this.onStageWheel, this);
stage.on('resize', this.onStageResize, this);
// Set initial size
this.setSize(stage.designWidth, stage.designHeight);
}
/**
* Get singleton instance
* 获取单例实例
*/
public static get inst(): GRoot {
if (!GRoot._inst) {
GRoot._inst = new GRoot();
}
return GRoot._inst;
}
/**
* Create a new GRoot (for multi-window support)
* 创建新的 GRoot支持多窗口
*/
public static create(): GRoot {
return new GRoot();
}
// Focus management | 焦点管理
/**
* Get focused object
* 获取当前焦点对象
*/
public get focus(): GObject | null {
return this._focus;
}
/**
* Set focused object
* 设置焦点对象
*/
public set focus(value: GObject | null) {
if (this._focus !== value) {
const oldFocus = this._focus;
this._focus = value;
if (oldFocus) {
oldFocus.emit(FGUIEvents.FOCUS_OUT);
}
if (this._focus) {
this._focus.emit(FGUIEvents.FOCUS_IN);
}
}
}
// Popup management | 弹出窗口管理
/**
* Show popup at position
* 在指定位置显示弹出窗口
*/
public showPopup(popup: GObject, target?: GObject, dir?: number): void {
if (this._popupStack.indexOf(popup) === -1) {
this._popupStack.push(popup);
}
this.addChild(popup);
this.adjustModalLayer();
if (target) {
const pos = target.localToGlobal(0, 0);
popup.setXY(pos.x, pos.y + target.height);
}
popup.visible = true;
}
/**
* Toggle popup visibility
* 切换弹出窗口可见性
*/
public togglePopup(popup: GObject, target?: GObject, dir?: number): void {
if (this._justClosedPopups.indexOf(popup) !== -1) {
return;
}
if (popup.parent === this && popup.visible) {
this.hidePopup(popup);
} else {
this.showPopup(popup, target, dir);
}
}
/**
* Hide popup
* 隐藏弹出窗口
*/
public hidePopup(popup?: GObject): void {
if (popup) {
const index = this._popupStack.indexOf(popup);
if (index !== -1) {
this._popupStack.splice(index, 1);
this.closePopup(popup);
}
} else {
// Hide all popups
for (const p of this._popupStack) {
this.closePopup(p);
}
this._popupStack.length = 0;
}
}
private closePopup(popup: GObject): void {
popup.visible = false;
this._justClosedPopups.push(popup);
Timer.inst.callLater(this, () => {
const index = this._justClosedPopups.indexOf(popup);
if (index !== -1) {
this._justClosedPopups.splice(index, 1);
}
});
}
/**
* Check if popup is showing
* 检查弹出窗口是否正在显示
*/
public hasAnyPopup(): boolean {
return this._popupStack.length > 0;
}
// Modal management | 模态管理
private adjustModalLayer(): void {
// Adjust modal layer position and visibility
if (this._modalLayer) {
let hasModal = false;
for (let i = this._popupStack.length - 1; i >= 0; i--) {
// Check if popup is modal
}
this._modalLayer.visible = hasModal;
}
}
/**
* Show modal wait
* 显示模态等待
*/
public showModalWait(msg?: string): void {
if (this._modalWaitPane) {
this.addChild(this._modalWaitPane);
this._modalWaitPane.visible = true;
}
}
/**
* Close modal wait
* 关闭模态等待
*/
public closeModalWait(): void {
if (this._modalWaitPane) {
this._modalWaitPane.visible = false;
this._modalWaitPane.removeFromParent();
}
}
// Tooltip management | 提示管理
/**
* Show tooltip
* 显示提示
*/
public showTooltips(msg: string): void {
if (!this._defaultTooltipWin) return;
this._tooltipWin = this._defaultTooltipWin;
this._tooltipWin.text = msg;
this.showTooltipsWin(this._tooltipWin);
}
/**
* Show custom tooltip window
* 显示自定义提示窗口
*/
public showTooltipsWin(tooltipWin: GObject, position?: { x: number; y: number }): void {
this._tooltipWin = tooltipWin;
this.addChild(tooltipWin);
if (position) {
tooltipWin.setXY(position.x, position.y);
} else {
const stage = Stage.inst;
tooltipWin.setXY(stage.mouseX + 10, stage.mouseY + 20);
}
}
/**
* Hide tooltip
* 隐藏提示
*/
public hideTooltips(): void {
if (this._tooltipWin) {
this._tooltipWin.removeFromParent();
this._tooltipWin = null;
}
}
// Input handling | 输入处理
private onStageMouseDown(data: IInputEventData): void {
this._inputProcessor.onMouseDown(data);
// Close popups if clicking outside
if (this._popupStack.length > 0) {
const hit = this.hitTest(data.stageX, data.stageY);
if (!hit || !this.isAncestorOf(hit, this._popupStack[this._popupStack.length - 1])) {
this.hidePopup();
}
}
this.hideTooltips();
}
private onStageMouseUp(data: IInputEventData): void {
this._inputProcessor.onMouseUp(data);
}
private onStageMouseMove(data: IInputEventData): void {
this._inputProcessor.onMouseMove(data);
}
private onStageWheel(data: IInputEventData): void {
this._inputProcessor.onMouseWheel(data);
}
private onStageResize(): void {
const stage = Stage.inst;
this.setSize(stage.designWidth, stage.designHeight);
}
private isAncestorOf(obj: GObject, ancestor: GObject): boolean {
let p: GObject | null = obj;
while (p) {
if (p === ancestor) return true;
p = p.parent;
}
return false;
}
/**
* Hit test at position
* 位置碰撞检测
*/
public hitTest(stageX: number, stageY: number): GObject | null {
return this._inputProcessor.hitTest(stageX, stageY);
}
// Drag and drop | 拖放
/**
* Start dragging a source object
* 开始拖拽源对象
*/
public startDragSource(source: GObject): void {
GObject.draggingObject = source;
}
/**
* Stop dragging
* 停止拖拽
*/
public stopDragSource(): void {
GObject.draggingObject = null;
}
// Window management | 窗口管理
/**
* Show window
* 显示窗口
*/
public showWindow(win: GObject): void {
this.addChild(win);
this.adjustModalLayer();
}
/**
* Hide window immediately
* 立即隐藏窗口
*/
public hideWindowImmediately(win: GObject): void {
if (win.parent === this) {
this.removeChild(win);
}
this.adjustModalLayer();
}
/**
* Bring window to front
* 将窗口置于最前
*/
public bringToFront(win: GObject): void {
const cnt = this.numChildren;
let i: number;
if (this._modalLayer && this._modalLayer.parent === this) {
i = this.getChildIndex(this._modalLayer);
} else {
i = cnt - 1;
}
const index = this.getChildIndex(win);
if (index < i) {
this.setChildIndex(win, i);
}
}
/**
* Get top window
* 获取最上层窗口
*/
public getTopWindow(): GObject | null {
const cnt = this.numChildren;
for (let i = cnt - 1; i >= 0; i--) {
const child = this.getChildAt(i);
if (child !== this._modalLayer) {
return child;
}
}
return null;
}
// Update | 更新
/**
* Update GRoot (called each frame by ECS system)
* 更新 GRoot每帧由 ECS 系统调用)
*/
public update(): void {
// Update timers
// Update transitions
// Update scroll panes
}
// Disposal | 销毁
public dispose(): void {
const stage = Stage.inst;
stage.off('mousedown', this.onStageMouseDown);
stage.off('mouseup', this.onStageMouseUp);
stage.off('mousemove', this.onStageMouseMove);
stage.off('wheel', this.onStageWheel);
stage.off('resize', this.onStageResize);
this._inputProcessor.dispose();
if (GRoot._inst === this) {
GRoot._inst = null;
}
super.dispose();
}
// Render | 渲染
public collectRenderData(collector: IRenderCollector): void {
super.collectRenderData(collector);
}
}
/**
* InputProcessor
*
* Handles input event processing and dispatching.
*
* 处理输入事件的处理和分发
*/
class InputProcessor {
private _root: GRoot;
private _touchTarget: GObject | null = null;
private _rollOverTarget: GObject | null = null;
constructor(root: GRoot) {
this._root = root;
}
public hitTest(stageX: number, stageY: number): GObject | null {
return this.hitTestInChildren(this._root, stageX, stageY);
}
private hitTestInChildren(container: GComponent, stageX: number, stageY: number): GObject | null {
const count = container.numChildren;
for (let i = count - 1; i >= 0; i--) {
const child = container.getChildAt(i);
if (!child.visible || !child.touchable) continue;
const local = child.globalToLocal(stageX, stageY);
if (local.x >= 0 && local.x < child.width && local.y >= 0 && local.y < child.height) {
if (child instanceof GComponent) {
const deeper = this.hitTestInChildren(child, stageX, stageY);
if (deeper) return deeper;
}
return child;
}
}
return null;
}
public onMouseDown(data: IInputEventData): void {
this._touchTarget = this.hitTest(data.stageX, data.stageY);
if (this._touchTarget) {
this._root.focus = this._touchTarget;
this._touchTarget.emit(FGUIEvents.TOUCH_BEGIN, data);
}
}
public onMouseUp(data: IInputEventData): void {
if (this._touchTarget) {
const target = this.hitTest(data.stageX, data.stageY);
this._touchTarget.emit(FGUIEvents.TOUCH_END, data);
if (target === this._touchTarget) {
this._touchTarget.emit(FGUIEvents.CLICK, data);
}
this._touchTarget = null;
}
}
public onMouseMove(data: IInputEventData): void {
const target = this.hitTest(data.stageX, data.stageY);
// Handle roll over/out
if (target !== this._rollOverTarget) {
if (this._rollOverTarget) {
this._rollOverTarget.emit(FGUIEvents.ROLL_OUT, data);
}
this._rollOverTarget = target;
if (this._rollOverTarget) {
this._rollOverTarget.emit(FGUIEvents.ROLL_OVER, data);
}
}
// Handle touch move
if (this._touchTarget) {
this._touchTarget.emit(FGUIEvents.TOUCH_MOVE, data);
}
}
public onMouseWheel(data: IInputEventData): void {
const target = this.hitTest(data.stageX, data.stageY);
if (target) {
target.emit('wheel', data);
}
}
public dispose(): void {
this._touchTarget = null;
this._rollOverTarget = null;
}
}

View File

@@ -0,0 +1,268 @@
/**
* Service identifier type
* 服务标识类型
*/
export type ServiceIdentifier<T = unknown> = abstract new (...args: never[]) => T;
/**
* Service factory function
* 服务工厂函数
*/
export type ServiceFactory<T> = (container: ServiceContainer) => T;
/**
* Service lifecycle
* 服务生命周期
*/
export const enum EServiceLifecycle {
/** Single instance shared across all resolutions | 单例模式 */
Singleton = 'singleton',
/** New instance per resolution | 每次解析创建新实例 */
Transient = 'transient'
}
/**
* Service registration info
* 服务注册信息
*/
interface ServiceRegistration<T = unknown> {
factory: ServiceFactory<T>;
lifecycle: EServiceLifecycle;
instance?: T;
}
/**
* ServiceContainer
*
* Lightweight dependency injection container for FairyGUI.
*
* 轻量级依赖注入容器
*
* Features:
* - Singleton and transient lifecycles
* - Factory-based registration
* - Type-safe resolution
* - Circular dependency detection
*
* @example
* ```typescript
* const container = new ServiceContainer();
*
* // Register singleton
* container.registerSingleton(AudioService, () => new AudioService());
*
* // Register with dependencies
* container.registerSingleton(UIManager, (c) => new UIManager(
* c.resolve(AudioService)
* ));
*
* // Resolve
* const uiManager = container.resolve(UIManager);
* ```
*/
export class ServiceContainer {
private _registrations: Map<ServiceIdentifier, ServiceRegistration> = new Map();
private _resolving: Set<ServiceIdentifier> = new Set();
private _disposed: boolean = false;
/**
* Register a singleton service
* 注册单例服务
*/
public registerSingleton<T>(
identifier: ServiceIdentifier<T>,
factory: ServiceFactory<T>
): this {
this.checkDisposed();
this._registrations.set(identifier, {
factory,
lifecycle: EServiceLifecycle.Singleton
});
return this;
}
/**
* Register a singleton instance directly
* 直接注册单例实例
*/
public registerInstance<T>(identifier: ServiceIdentifier<T>, instance: T): this {
this.checkDisposed();
this._registrations.set(identifier, {
factory: () => instance,
lifecycle: EServiceLifecycle.Singleton,
instance
});
return this;
}
/**
* Register a transient service (new instance per resolution)
* 注册瞬时服务(每次解析创建新实例)
*/
public registerTransient<T>(
identifier: ServiceIdentifier<T>,
factory: ServiceFactory<T>
): this {
this.checkDisposed();
this._registrations.set(identifier, {
factory,
lifecycle: EServiceLifecycle.Transient
});
return this;
}
/**
* Resolve a service
* 解析服务
*/
public resolve<T>(identifier: ServiceIdentifier<T>): T {
this.checkDisposed();
const registration = this._registrations.get(identifier);
if (!registration) {
throw new Error(`Service not registered: ${identifier.name}`);
}
// Check for circular dependency
if (this._resolving.has(identifier)) {
throw new Error(`Circular dependency detected: ${identifier.name}`);
}
// Return cached singleton if available
if (registration.lifecycle === EServiceLifecycle.Singleton && registration.instance !== undefined) {
return registration.instance as T;
}
// Resolve
this._resolving.add(identifier);
try {
const instance = registration.factory(this) as T;
if (registration.lifecycle === EServiceLifecycle.Singleton) {
registration.instance = instance;
}
return instance;
} finally {
this._resolving.delete(identifier);
}
}
/**
* Try to resolve a service, returns null if not found
* 尝试解析服务,未找到时返回 null
*/
public tryResolve<T>(identifier: ServiceIdentifier<T>): T | null {
if (!this._registrations.has(identifier)) {
return null;
}
return this.resolve(identifier);
}
/**
* Check if a service is registered
* 检查服务是否已注册
*/
public isRegistered<T>(identifier: ServiceIdentifier<T>): boolean {
return this._registrations.has(identifier);
}
/**
* Unregister a service
* 取消注册服务
*/
public unregister<T>(identifier: ServiceIdentifier<T>): boolean {
const registration = this._registrations.get(identifier);
if (registration) {
// Dispose singleton if it has dispose method
if (registration.instance && typeof (registration.instance as IDisposable).dispose === 'function') {
(registration.instance as IDisposable).dispose();
}
this._registrations.delete(identifier);
return true;
}
return false;
}
/**
* Create a child container that inherits registrations
* 创建继承注册的子容器
*/
public createChild(): ServiceContainer {
const child = new ServiceContainer();
// Copy registrations (singletons are shared)
for (const [id, reg] of this._registrations) {
child._registrations.set(id, { ...reg });
}
return child;
}
/**
* Dispose the container and all singleton instances
* 销毁容器和所有单例实例
*/
public dispose(): void {
if (this._disposed) return;
for (const registration of this._registrations.values()) {
if (registration.instance && typeof (registration.instance as IDisposable).dispose === 'function') {
(registration.instance as IDisposable).dispose();
}
}
this._registrations.clear();
this._resolving.clear();
this._disposed = true;
}
private checkDisposed(): void {
if (this._disposed) {
throw new Error('ServiceContainer has been disposed');
}
}
}
/**
* Disposable interface
* 可销毁接口
*/
interface IDisposable {
dispose(): void;
}
/**
* Global service container instance
* 全局服务容器实例
*/
let _globalContainer: ServiceContainer | null = null;
/**
* Get global service container
* 获取全局服务容器
*/
export function getGlobalContainer(): ServiceContainer {
if (!_globalContainer) {
_globalContainer = new ServiceContainer();
}
return _globalContainer;
}
/**
* Set global service container
* 设置全局服务容器
*/
export function setGlobalContainer(container: ServiceContainer): void {
_globalContainer = container;
}
/**
* Inject decorator marker (for future decorator support)
* 注入装饰器标记(用于未来装饰器支持)
*/
export function Inject<T>(identifier: ServiceIdentifier<T>): PropertyDecorator {
return (_target: object, _propertyKey: string | symbol) => {
// Store metadata for future use
// This is a placeholder for decorator-based injection
void identifier;
};
}

View File

@@ -0,0 +1,353 @@
import { EventDispatcher } from '../events/EventDispatcher';
import { IInputEventData, createInputEventData } from '../events/Events';
/**
* Stage
*
* Represents the root container and manages input events.
*
* 表示根容器并管理输入事件
*/
export class Stage extends EventDispatcher {
private static _inst: Stage | null = null;
/** Stage width | 舞台宽度 */
public width: number = 800;
/** Stage height | 舞台高度 */
public height: number = 600;
/** Current mouse/touch X position | 当前鼠标/触摸 X 坐标 */
public mouseX: number = 0;
/** Current mouse/touch Y position | 当前鼠标/触摸 Y 坐标 */
public mouseY: number = 0;
/** Design width | 设计宽度 */
public designWidth: number = 1920;
/** Design height | 设计高度 */
public designHeight: number = 1080;
/** Scale mode | 缩放模式 */
public scaleMode: EScaleMode = EScaleMode.ShowAll;
/** Align mode | 对齐模式 */
public alignH: EAlignMode = EAlignMode.Center;
public alignV: EAlignMode = EAlignMode.Middle;
/** Is touch/pointer down | 是否按下 */
public isTouchDown: boolean = false;
/** Current touch ID | 当前触摸 ID */
public touchId: number = 0;
private _canvas: HTMLCanvasElement | null = null;
private _inputData: IInputEventData;
private _scaleX: number = 1;
private _scaleY: number = 1;
private _offsetX: number = 0;
private _offsetY: number = 0;
private constructor() {
super();
this._inputData = createInputEventData();
}
/**
* Get singleton instance
* 获取单例实例
*/
public static get inst(): Stage {
if (!Stage._inst) {
Stage._inst = new Stage();
}
return Stage._inst;
}
/**
* Bind stage to a canvas element
* 绑定舞台到画布元素
*
* @param canvas HTMLCanvasElement to bind | 要绑定的画布元素
*/
public bindToCanvas(canvas: HTMLCanvasElement): void {
if (this._canvas) {
this.unbindCanvas();
}
this._canvas = canvas;
this.updateSize();
this.bindEvents();
}
/**
* Unbind from current canvas
* 解绑当前画布
*/
public unbindCanvas(): void {
if (!this._canvas) return;
this._canvas.removeEventListener('mousedown', this.handleMouseDown);
this._canvas.removeEventListener('mouseup', this.handleMouseUp);
this._canvas.removeEventListener('mousemove', this.handleMouseMove);
this._canvas.removeEventListener('wheel', this.handleWheel);
this._canvas.removeEventListener('touchstart', this.handleTouchStart);
this._canvas.removeEventListener('touchend', this.handleTouchEnd);
this._canvas.removeEventListener('touchmove', this.handleTouchMove);
this._canvas.removeEventListener('touchcancel', this.handleTouchEnd);
this._canvas = null;
}
/**
* Update stage size from canvas
* 从画布更新舞台尺寸
*/
public updateSize(): void {
if (!this._canvas) return;
this.width = this._canvas.width;
this.height = this._canvas.height;
this.updateScale();
this.emit('resize', { width: this.width, height: this.height });
}
/**
* Set design size
* 设置设计尺寸
*/
public setDesignSize(width: number, height: number): void {
this.designWidth = width;
this.designHeight = height;
this.updateScale();
}
private updateScale(): void {
const scaleX = this.width / this.designWidth;
const scaleY = this.height / this.designHeight;
switch (this.scaleMode) {
case EScaleMode.ShowAll:
this._scaleX = this._scaleY = Math.min(scaleX, scaleY);
break;
case EScaleMode.NoBorder:
this._scaleX = this._scaleY = Math.max(scaleX, scaleY);
break;
case EScaleMode.ExactFit:
this._scaleX = scaleX;
this._scaleY = scaleY;
break;
case EScaleMode.FixedWidth:
this._scaleX = this._scaleY = scaleX;
break;
case EScaleMode.FixedHeight:
this._scaleX = this._scaleY = scaleY;
break;
case EScaleMode.NoScale:
default:
this._scaleX = this._scaleY = 1;
break;
}
const actualWidth = this.designWidth * this._scaleX;
const actualHeight = this.designHeight * this._scaleY;
switch (this.alignH) {
case EAlignMode.Left:
this._offsetX = 0;
break;
case EAlignMode.Right:
this._offsetX = this.width - actualWidth;
break;
case EAlignMode.Center:
default:
this._offsetX = (this.width - actualWidth) / 2;
break;
}
switch (this.alignV) {
case EAlignMode.Top:
this._offsetY = 0;
break;
case EAlignMode.Bottom:
this._offsetY = this.height - actualHeight;
break;
case EAlignMode.Middle:
default:
this._offsetY = (this.height - actualHeight) / 2;
break;
}
}
/**
* Convert screen coordinates to stage coordinates
* 将屏幕坐标转换为舞台坐标
*/
public screenToStage(screenX: number, screenY: number): { x: number; y: number } {
return {
x: (screenX - this._offsetX) / this._scaleX,
y: (screenY - this._offsetY) / this._scaleY
};
}
/**
* Convert stage coordinates to screen coordinates
* 将舞台坐标转换为屏幕坐标
*/
public stageToScreen(stageX: number, stageY: number): { x: number; y: number } {
return {
x: stageX * this._scaleX + this._offsetX,
y: stageY * this._scaleY + this._offsetY
};
}
private bindEvents(): void {
if (!this._canvas) return;
this._canvas.addEventListener('mousedown', this.handleMouseDown);
this._canvas.addEventListener('mouseup', this.handleMouseUp);
this._canvas.addEventListener('mousemove', this.handleMouseMove);
this._canvas.addEventListener('wheel', this.handleWheel);
this._canvas.addEventListener('touchstart', this.handleTouchStart, { passive: false });
this._canvas.addEventListener('touchend', this.handleTouchEnd);
this._canvas.addEventListener('touchmove', this.handleTouchMove, { passive: false });
this._canvas.addEventListener('touchcancel', this.handleTouchEnd);
}
private getCanvasPosition(e: MouseEvent | Touch): { x: number; y: number } {
if (!this._canvas) return { x: 0, y: 0 };
const rect = this._canvas.getBoundingClientRect();
const scaleX = this._canvas.width / rect.width;
const scaleY = this._canvas.height / rect.height;
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY
};
}
private updateInputData(e: MouseEvent | Touch, type: string): void {
const pos = this.getCanvasPosition(e);
const stagePos = this.screenToStage(pos.x, pos.y);
this._inputData.stageX = stagePos.x;
this._inputData.stageY = stagePos.y;
this.mouseX = stagePos.x;
this.mouseY = stagePos.y;
if (e instanceof MouseEvent) {
this._inputData.button = e.button;
this._inputData.ctrlKey = e.ctrlKey;
this._inputData.shiftKey = e.shiftKey;
this._inputData.altKey = e.altKey;
this._inputData.nativeEvent = e;
} else {
this._inputData.touchId = e.identifier;
this.touchId = e.identifier;
}
}
private handleMouseDown = (e: MouseEvent): void => {
this.updateInputData(e, 'mousedown');
this.isTouchDown = true;
this._inputData.touchId = 0;
this.emit('mousedown', this._inputData);
};
private handleMouseUp = (e: MouseEvent): void => {
this.updateInputData(e, 'mouseup');
this.isTouchDown = false;
this.emit('mouseup', this._inputData);
};
private handleMouseMove = (e: MouseEvent): void => {
this.updateInputData(e, 'mousemove');
this.emit('mousemove', this._inputData);
};
private handleWheel = (e: WheelEvent): void => {
this.updateInputData(e, 'wheel');
this._inputData.wheelDelta = e.deltaY;
this._inputData.nativeEvent = e;
this.emit('wheel', this._inputData);
};
private handleTouchStart = (e: TouchEvent): void => {
e.preventDefault();
if (e.touches.length > 0) {
const touch = e.touches[0];
this.updateInputData(touch, 'touchstart');
this.isTouchDown = true;
this.emit('mousedown', this._inputData);
}
};
private handleTouchEnd = (e: TouchEvent): void => {
if (e.changedTouches.length > 0) {
const touch = e.changedTouches[0];
this.updateInputData(touch, 'touchend');
this.isTouchDown = false;
this.emit('mouseup', this._inputData);
}
};
private handleTouchMove = (e: TouchEvent): void => {
e.preventDefault();
if (e.touches.length > 0) {
const touch = e.touches[0];
this.updateInputData(touch, 'touchmove');
this.emit('mousemove', this._inputData);
}
};
public get scaleX(): number {
return this._scaleX;
}
public get scaleY(): number {
return this._scaleY;
}
public get offsetX(): number {
return this._offsetX;
}
public get offsetY(): number {
return this._offsetY;
}
}
/**
* Scale mode enum
* 缩放模式枚举
*/
export const enum EScaleMode {
/** No scaling | 不缩放 */
NoScale = 'noscale',
/** Show all content (letterbox) | 显示全部内容(黑边) */
ShowAll = 'showall',
/** Fill screen, clip content | 填充屏幕,裁剪内容 */
NoBorder = 'noborder',
/** Stretch to fit | 拉伸适应 */
ExactFit = 'exactfit',
/** Fixed width, height scales | 固定宽度,高度缩放 */
FixedWidth = 'fixedwidth',
/** Fixed height, width scales | 固定高度,宽度缩放 */
FixedHeight = 'fixedheight'
}
/**
* Align mode enum
* 对齐模式枚举
*/
export const enum EAlignMode {
Left = 'left',
Center = 'center',
Right = 'right',
Top = 'top',
Middle = 'middle',
Bottom = 'bottom'
}

View File

@@ -0,0 +1,266 @@
/**
* Timer callback info
* 定时器回调信息
*/
interface TimerCallback {
id: number;
caller: any;
callback: Function;
interval: number;
elapsed: number;
repeat: boolean;
removed: boolean;
}
/**
* Call later callback info
* 延迟调用回调信息
*/
interface CallLaterItem {
caller: any;
callback: Function;
}
/**
* Timer
*
* Provides timing and scheduling functionality.
*
* 提供计时和调度功能
*/
export class Timer {
private static _inst: Timer | null = null;
/** Frame delta time in milliseconds | 帧间隔时间(毫秒) */
public delta: number = 0;
/** Current time in milliseconds | 当前时间(毫秒) */
public currentTime: number = 0;
/** Frame count | 帧数 */
public frameCount: number = 0;
private _callbacks: Map<number, TimerCallback> = new Map();
private _callLaterList: CallLaterItem[] = [];
private _callLaterPending: CallLaterItem[] = [];
private _nextId: number = 1;
private _updating: boolean = false;
private constructor() {
this.currentTime = performance.now();
}
/**
* Get singleton instance
* 获取单例实例
*/
public static get inst(): Timer {
if (!Timer._inst) {
Timer._inst = new Timer();
}
return Timer._inst;
}
/**
* Get current time (static shortcut)
* 获取当前时间(静态快捷方式)
*/
public static get time(): number {
return Timer.inst.currentTime;
}
/**
* Add a callback to be called each frame
* 添加每帧调用的回调
*/
public static add(callback: Function, caller: any): void {
Timer.inst.frameLoop(1, caller, callback);
}
/**
* Remove a callback
* 移除回调
*/
public static remove(callback: Function, caller: any): void {
Timer.inst.clear(caller, callback);
}
/**
* Update timer (called by ECS system each frame)
* 更新定时器(每帧由 ECS 系统调用)
*
* @param deltaMs Delta time in milliseconds | 间隔时间(毫秒)
*/
public update(deltaMs: number): void {
this.delta = deltaMs;
this.currentTime += deltaMs;
this.frameCount++;
this._updating = true;
// Process timers
for (const callback of this._callbacks.values()) {
if (callback.removed) continue;
callback.elapsed += deltaMs;
if (callback.elapsed >= callback.interval) {
callback.callback.call(callback.caller);
if (callback.repeat) {
callback.elapsed = 0;
} else {
callback.removed = true;
}
}
}
// Clean up removed callbacks
for (const [id, callback] of this._callbacks) {
if (callback.removed) {
this._callbacks.delete(id);
}
}
// Process callLater
const pending = this._callLaterList;
this._callLaterList = this._callLaterPending;
this._callLaterPending = [];
for (const item of pending) {
item.callback.call(item.caller);
}
pending.length = 0;
this._callLaterList = pending;
this._updating = false;
}
/**
* Execute callback after specified delay (one time)
* 延迟执行回调(一次)
*
* @param delay Delay in milliseconds | 延迟时间(毫秒)
* @param caller Callback context | 回调上下文
* @param callback Callback function | 回调函数
*/
public once(delay: number, caller: any, callback: Function): void {
this.addCallback(delay, caller, callback, false);
}
/**
* Execute callback repeatedly at interval
* 按间隔重复执行回调
*
* @param interval Interval in milliseconds | 间隔时间(毫秒)
* @param caller Callback context | 回调上下文
* @param callback Callback function | 回调函数
*/
public loop(interval: number, caller: any, callback: Function): void {
this.addCallback(interval, caller, callback, true);
}
/**
* Execute callback every frame
* 每帧执行回调
*
* @param interval Frame interval (1 = every frame) | 帧间隔
* @param caller Callback context | 回调上下文
* @param callback Callback function | 回调函数
*/
public frameLoop(interval: number, caller: any, callback: Function): void {
this.loop(interval * 16.67, caller, callback);
}
/**
* Execute callback at the end of current frame
* 在当前帧结束时执行回调
*
* @param caller Callback context | 回调上下文
* @param callback Callback function | 回调函数
*/
public callLater(caller: any, callback: Function): void {
const list = this._updating ? this._callLaterPending : this._callLaterList;
const exists = list.some(
(item) => item.caller === caller && item.callback === callback
);
if (!exists) {
list.push({ caller, callback });
}
}
/**
* Clear a specific callback
* 清除指定回调
*
* @param caller Callback context | 回调上下文
* @param callback Callback function | 回调函数
*/
public clear(caller: any, callback: Function): void {
for (const cb of this._callbacks.values()) {
if (cb.caller === caller && cb.callback === callback) {
cb.removed = true;
}
}
this._callLaterList = this._callLaterList.filter(
(item) => !(item.caller === caller && item.callback === callback)
);
this._callLaterPending = this._callLaterPending.filter(
(item) => !(item.caller === caller && item.callback === callback)
);
}
/**
* Clear all callbacks for a caller
* 清除指定对象的所有回调
*
* @param caller Callback context | 回调上下文
*/
public clearAll(caller: any): void {
for (const cb of this._callbacks.values()) {
if (cb.caller === caller) {
cb.removed = true;
}
}
this._callLaterList = this._callLaterList.filter(
(item) => item.caller !== caller
);
this._callLaterPending = this._callLaterPending.filter(
(item) => item.caller !== caller
);
}
private addCallback(
interval: number,
caller: any,
callback: Function,
repeat: boolean
): void {
this.clear(caller, callback);
const id = this._nextId++;
this._callbacks.set(id, {
id,
caller,
callback,
interval,
elapsed: 0,
repeat,
removed: false
});
}
/**
* Dispose the timer
* 销毁定时器
*/
public dispose(): void {
this._callbacks.clear();
this._callLaterList.length = 0;
this._callLaterPending.length = 0;
}
}

View File

@@ -0,0 +1,859 @@
import { EventDispatcher } from '../events/EventDispatcher';
import type { GComponent } from './GComponent';
import type { GObject } from './GObject';
import { GTween } from '../tween/GTween';
import type { GTweener } from '../tween/GTweener';
import { EEaseType } from '../tween/EaseType';
import { ByteBuffer } from '../utils/ByteBuffer';
import type { SimpleHandler } from '../display/MovieClip';
/**
* Transition action types
* 过渡动画动作类型
*/
export const enum ETransitionActionType {
XY = 0,
Size = 1,
Scale = 2,
Pivot = 3,
Alpha = 4,
Rotation = 5,
Color = 6,
Animation = 7,
Visible = 8,
Sound = 9,
Transition = 10,
Shake = 11,
ColorFilter = 12,
Skew = 13,
Text = 14,
Icon = 15,
Unknown = 16
}
/**
* Transition item value
* 过渡项值
*/
interface ITransitionValue {
f1?: number;
f2?: number;
f3?: number;
f4?: number;
b1?: boolean;
b2?: boolean;
b3?: boolean;
visible?: boolean;
playing?: boolean;
frame?: number;
sound?: string;
volume?: number;
transName?: string;
playTimes?: number;
trans?: Transition;
stopTime?: number;
amplitude?: number;
duration?: number;
offsetX?: number;
offsetY?: number;
lastOffsetX?: number;
lastOffsetY?: number;
text?: string;
audioClip?: string;
flag?: boolean;
}
/**
* Tween config
* 补间配置
*/
interface ITweenConfig {
duration: number;
easeType: EEaseType;
repeat: number;
yoyo: boolean;
startValue: ITransitionValue;
endValue: ITransitionValue;
endLabel?: string;
endHook?: SimpleHandler;
}
/**
* Transition item
* 过渡项
*/
interface ITransitionItem {
time: number;
targetId: string;
type: ETransitionActionType;
tweenConfig?: ITweenConfig;
label?: string;
value: ITransitionValue;
hook?: SimpleHandler;
tweener?: GTweener;
target?: GObject;
displayLockToken: number;
}
/** Options flags */
const OPTION_AUTO_STOP_DISABLED = 2;
const OPTION_AUTO_STOP_AT_END = 4;
/**
* Transition
*
* Animation transition system for UI components.
* Supports keyframe animations, tweening, and chained transitions.
*
* UI 组件的动画过渡系统,支持关键帧动画、补间和链式过渡
*/
export class Transition extends EventDispatcher {
/** Transition name | 过渡动画名称 */
public name: string = '';
private _owner: GComponent;
private _ownerBaseX: number = 0;
private _ownerBaseY: number = 0;
private _items: ITransitionItem[] = [];
private _totalTimes: number = 0;
private _totalTasks: number = 0;
private _playing: boolean = false;
private _paused: boolean = false;
private _onComplete: SimpleHandler | null = null;
private _options: number = 0;
private _reversed: boolean = false;
private _totalDuration: number = 0;
private _autoPlay: boolean = false;
private _autoPlayTimes: number = 1;
private _autoPlayDelay: number = 0;
private _timeScale: number = 1;
private _startTime: number = 0;
private _endTime: number = -1;
constructor(owner: GComponent) {
super();
this._owner = owner;
}
public get owner(): GComponent {
return this._owner;
}
public get playing(): boolean {
return this._playing;
}
public get autoPlay(): boolean {
return this._autoPlay;
}
public set autoPlay(value: boolean) {
this.setAutoPlay(value, this._autoPlayTimes, this._autoPlayDelay);
}
public get autoPlayRepeat(): number {
return this._autoPlayTimes;
}
public get autoPlayDelay(): number {
return this._autoPlayDelay;
}
public get timeScale(): number {
return this._timeScale;
}
public set timeScale(value: number) {
if (this._timeScale !== value) {
this._timeScale = value;
if (this._playing) {
for (const item of this._items) {
if (item.tweener) {
item.tweener.setTimeScale(value);
} else if (item.type === ETransitionActionType.Transition && item.value.trans) {
item.value.trans.timeScale = value;
}
}
}
}
}
public play(
onComplete?: SimpleHandler,
times: number = 1,
delay: number = 0,
startTime: number = 0,
endTime: number = -1
): void {
this._play(onComplete || null, times, delay, startTime, endTime, false);
}
public playReverse(
onComplete?: SimpleHandler,
times: number = 1,
delay: number = 0,
startTime: number = 0,
endTime: number = -1
): void {
this._play(onComplete || null, times, delay, startTime, endTime, true);
}
public changePlayTimes(value: number): void {
this._totalTimes = value;
}
public setAutoPlay(value: boolean, times: number = -1, delay: number = 0): void {
if (this._autoPlay !== value) {
this._autoPlay = value;
this._autoPlayTimes = times;
this._autoPlayDelay = delay;
if (this._autoPlay) {
if (this._owner.onStage) {
this.play(undefined, this._autoPlayTimes, this._autoPlayDelay);
}
} else {
if (!this._owner.onStage) {
this.stop(false, true);
}
}
}
}
public _play(
onComplete: SimpleHandler | null,
times: number,
delay: number,
startTime: number,
endTime: number,
reversed: boolean
): void {
this.stop(true, true);
this._totalTimes = times;
this._reversed = reversed;
this._startTime = startTime;
this._endTime = endTime;
this._playing = true;
this._paused = false;
this._onComplete = onComplete;
for (const item of this._items) {
if (!item.target) {
if (item.targetId) {
item.target = this._owner.getChildById(item.targetId) ?? undefined;
} else {
item.target = this._owner;
}
} else if (item.target !== this._owner && item.target.parent !== this._owner) {
item.target = undefined;
}
if (item.target && item.type === ETransitionActionType.Transition) {
let trans = (item.target as GComponent).getTransition(item.value.transName || '');
if (trans === this) trans = null;
if (trans) {
if (item.value.playTimes === 0) {
for (let j = this._items.indexOf(item) - 1; j >= 0; j--) {
const item2 = this._items[j];
if (item2.type === ETransitionActionType.Transition && item2.value.trans === trans) {
item2.value.stopTime = item.time - item2.time;
trans = null;
break;
}
}
if (trans) item.value.stopTime = 0;
} else {
item.value.stopTime = -1;
}
}
item.value.trans = trans ?? undefined;
}
}
if (delay === 0) {
this.onDelayedPlay();
} else {
GTween.delayedCall(delay).setTarget(this).onComplete(() => this.onDelayedPlay());
}
}
public stop(bSetToComplete: boolean = true, bProcessCallback: boolean = false): void {
if (!this._playing) return;
this._playing = false;
this._totalTasks = 0;
this._totalTimes = 0;
const handler = this._onComplete;
this._onComplete = null;
GTween.kill(this);
const cnt = this._items.length;
if (this._reversed) {
for (let i = cnt - 1; i >= 0; i--) {
const item = this._items[i];
if (item.target) this.stopItem(item, bSetToComplete);
}
} else {
for (let i = 0; i < cnt; i++) {
const item = this._items[i];
if (item.target) this.stopItem(item, bSetToComplete);
}
}
if (bProcessCallback && handler) {
if (typeof handler === 'function') handler();
else if (typeof handler.run === 'function') handler.run();
}
}
private stopItem(item: ITransitionItem, bSetToComplete: boolean): void {
if (item.tweener) {
item.tweener.kill(bSetToComplete);
item.tweener = undefined;
if (item.type === ETransitionActionType.Shake && !bSetToComplete && item.target) {
item.target.x -= item.value.lastOffsetX || 0;
item.target.y -= item.value.lastOffsetY || 0;
}
}
if (item.type === ETransitionActionType.Transition && item.value.trans) {
item.value.trans.stop(bSetToComplete, false);
}
}
public pause(): void {
if (!this._playing || this._paused) return;
this._paused = true;
const tweener = GTween.getTween(this);
if (tweener) tweener.setPaused(true);
for (const item of this._items) {
if (!item.target) continue;
if (item.type === ETransitionActionType.Transition && item.value.trans) {
item.value.trans.pause();
}
if (item.tweener) item.tweener.setPaused(true);
}
}
public resume(): void {
if (!this._playing || !this._paused) return;
this._paused = false;
const tweener = GTween.getTween(this);
if (tweener) tweener.setPaused(false);
for (const item of this._items) {
if (!item.target) continue;
if (item.type === ETransitionActionType.Transition && item.value.trans) {
item.value.trans.resume();
}
if (item.tweener) item.tweener.setPaused(false);
}
}
public setValue(label: string, ...values: any[]): void {
for (const item of this._items) {
if (item.label === label) {
const value = item.tweenConfig ? item.tweenConfig.startValue : item.value;
this.setItemValue(item.type, value, values);
return;
} else if (item.tweenConfig?.endLabel === label) {
this.setItemValue(item.type, item.tweenConfig.endValue, values);
return;
}
}
}
private setItemValue(type: ETransitionActionType, value: ITransitionValue, args: any[]): void {
switch (type) {
case ETransitionActionType.XY:
case ETransitionActionType.Size:
case ETransitionActionType.Pivot:
case ETransitionActionType.Scale:
case ETransitionActionType.Skew:
value.b1 = value.b2 = true;
value.f1 = parseFloat(args[0]);
value.f2 = parseFloat(args[1]);
break;
case ETransitionActionType.Alpha:
case ETransitionActionType.Rotation:
case ETransitionActionType.Color:
value.f1 = parseFloat(args[0]);
break;
case ETransitionActionType.Animation:
value.frame = parseInt(args[0]);
if (args.length > 1) value.playing = args[1];
break;
case ETransitionActionType.Visible:
value.visible = args[0];
break;
case ETransitionActionType.Sound:
value.sound = args[0];
if (args.length > 1) value.volume = parseFloat(args[1]);
break;
case ETransitionActionType.Transition:
value.transName = args[0];
if (args.length > 1) value.playTimes = parseInt(args[1]);
break;
case ETransitionActionType.Shake:
value.amplitude = parseFloat(args[0]);
if (args.length > 1) value.duration = parseFloat(args[1]);
break;
case ETransitionActionType.ColorFilter:
value.f1 = parseFloat(args[0]);
value.f2 = parseFloat(args[1]);
value.f3 = parseFloat(args[2]);
value.f4 = parseFloat(args[3]);
break;
case ETransitionActionType.Text:
case ETransitionActionType.Icon:
value.text = args[0];
break;
}
}
public setTarget(label: string, target: GObject): void {
for (const item of this._items) {
if (item.label === label) {
item.targetId = target.id;
item.target = target;
return;
}
}
}
public setHook(label: string, callback: SimpleHandler): void {
for (const item of this._items) {
if (item.label === label) {
item.hook = callback;
return;
} else if (item.tweenConfig?.endLabel === label) {
item.tweenConfig.endHook = callback;
return;
}
}
}
public clearHooks(): void {
for (const item of this._items) {
item.hook = undefined;
if (item.tweenConfig) item.tweenConfig.endHook = undefined;
}
}
public onOwnerAddedToStage(): void {
if (this._autoPlay && !this._playing) {
this.play(undefined, this._autoPlayTimes, this._autoPlayDelay);
}
}
public onOwnerRemovedFromStage(): void {
if ((this._options & OPTION_AUTO_STOP_DISABLED) === 0) {
this.stop((this._options & OPTION_AUTO_STOP_AT_END) !== 0, false);
}
}
private onDelayedPlay(): void {
this._ownerBaseX = this._owner.x;
this._ownerBaseY = this._owner.y;
this._totalTasks = 1;
const cnt = this._items.length;
for (let i = this._reversed ? cnt - 1 : 0; this._reversed ? i >= 0 : i < cnt; this._reversed ? i-- : i++) {
const item = this._items[i];
if (item.target) this.playItem(item);
}
this._totalTasks--;
this.checkAllComplete();
}
private playItem(item: ITransitionItem): void {
let time: number;
if (item.tweenConfig) {
time = this._reversed
? this._totalDuration - item.time - item.tweenConfig.duration
: item.time;
if (this._endTime === -1 || time < this._endTime) {
const startValue = this._reversed ? item.tweenConfig.endValue : item.tweenConfig.startValue;
const endValue = this._reversed ? item.tweenConfig.startValue : item.tweenConfig.endValue;
item.value.b1 = startValue.b1;
item.value.b2 = startValue.b2;
switch (item.type) {
case ETransitionActionType.XY:
case ETransitionActionType.Size:
case ETransitionActionType.Scale:
case ETransitionActionType.Skew:
item.tweener = GTween.to2(
startValue.f1 || 0, startValue.f2 || 0,
endValue.f1 || 0, endValue.f2 || 0,
item.tweenConfig.duration
);
break;
case ETransitionActionType.Alpha:
case ETransitionActionType.Rotation:
item.tweener = GTween.to(startValue.f1 || 0, endValue.f1 || 0, item.tweenConfig.duration);
break;
case ETransitionActionType.Color:
item.tweener = GTween.toColor(startValue.f1 || 0, endValue.f1 || 0, item.tweenConfig.duration);
break;
case ETransitionActionType.ColorFilter:
item.tweener = GTween.to4(
startValue.f1 || 0, startValue.f2 || 0, startValue.f3 || 0, startValue.f4 || 0,
endValue.f1 || 0, endValue.f2 || 0, endValue.f3 || 0, endValue.f4 || 0,
item.tweenConfig.duration
);
break;
}
if (item.tweener) {
item.tweener
.setDelay(time)
.setEase(item.tweenConfig.easeType)
.setRepeat(item.tweenConfig.repeat, item.tweenConfig.yoyo)
.setTimeScale(this._timeScale)
.setTarget(item)
.onStart(() => this.callHook(item, false))
.onUpdate(() => this.onTweenUpdate(item))
.onComplete(() => this.onTweenComplete(item));
if (this._endTime >= 0) item.tweener.setBreakpoint(this._endTime - time);
this._totalTasks++;
}
}
} else if (item.type === ETransitionActionType.Shake) {
time = this._reversed
? this._totalDuration - item.time - (item.value.duration || 0)
: item.time;
item.value.offsetX = item.value.offsetY = 0;
item.value.lastOffsetX = item.value.lastOffsetY = 0;
item.tweener = GTween.shake(0, 0, item.value.amplitude || 0, item.value.duration || 0)
.setDelay(time)
.setTimeScale(this._timeScale)
.setTarget(item)
.onUpdate(() => this.onTweenUpdate(item))
.onComplete(() => this.onTweenComplete(item));
if (this._endTime >= 0) item.tweener.setBreakpoint(this._endTime - item.time);
this._totalTasks++;
} else {
time = this._reversed ? this._totalDuration - item.time : item.time;
if (time <= this._startTime) {
this.applyValue(item);
this.callHook(item, false);
} else if (this._endTime === -1 || time <= this._endTime) {
this._totalTasks++;
item.tweener = GTween.delayedCall(time)
.setTimeScale(this._timeScale)
.setTarget(item)
.onComplete(() => {
item.tweener = undefined;
this._totalTasks--;
this.applyValue(item);
this.callHook(item, false);
this.checkAllComplete();
});
}
}
}
private onTweenUpdate(item: ITransitionItem): void {
if (!item.tweener) return;
const tweener = item.tweener;
switch (item.type) {
case ETransitionActionType.XY:
case ETransitionActionType.Size:
case ETransitionActionType.Scale:
case ETransitionActionType.Skew:
item.value.f1 = tweener.value.x;
item.value.f2 = tweener.value.y;
break;
case ETransitionActionType.Alpha:
case ETransitionActionType.Rotation:
item.value.f1 = tweener.value.x;
break;
case ETransitionActionType.Color:
item.value.f1 = tweener.value.color;
break;
case ETransitionActionType.ColorFilter:
item.value.f1 = tweener.value.x;
item.value.f2 = tweener.value.y;
item.value.f3 = tweener.value.z;
item.value.f4 = tweener.value.w;
break;
case ETransitionActionType.Shake:
item.value.offsetX = tweener.deltaValue.x;
item.value.offsetY = tweener.deltaValue.y;
break;
}
this.applyValue(item);
}
private onTweenComplete(item: ITransitionItem): void {
item.tweener = undefined;
this._totalTasks--;
this.callHook(item, true);
this.checkAllComplete();
}
private checkAllComplete(): void {
if (this._playing && this._totalTasks === 0) {
if (this._totalTimes < 0) {
this.internalPlay();
} else {
this._totalTimes--;
if (this._totalTimes > 0) {
this.internalPlay();
} else {
this._playing = false;
const handler = this._onComplete;
this._onComplete = null;
if (handler) {
if (typeof handler === 'function') handler();
else if (typeof handler.run === 'function') handler.run();
}
}
}
}
}
private internalPlay(): void {
this._ownerBaseX = this._owner.x;
this._ownerBaseY = this._owner.y;
this._totalTasks = 1;
for (const item of this._items) {
if (item.target) this.playItem(item);
}
this._totalTasks--;
}
private callHook(item: ITransitionItem, tweenEnd: boolean): void {
const hook = tweenEnd ? item.tweenConfig?.endHook : item.hook;
if (hook) {
if (typeof hook === 'function') hook();
else if (typeof hook.run === 'function') hook.run();
}
}
private applyValue(item: ITransitionItem): void {
if (!item.target) return;
const value = item.value;
const target = item.target;
switch (item.type) {
case ETransitionActionType.XY:
if (target === this._owner) {
if (value.b1 && value.b2) target.setXY((value.f1 || 0) + this._ownerBaseX, (value.f2 || 0) + this._ownerBaseY);
else if (value.b1) target.x = (value.f1 || 0) + this._ownerBaseX;
else target.y = (value.f2 || 0) + this._ownerBaseY;
} else if (value.b3) {
if (value.b1 && value.b2) target.setXY((value.f1 || 0) * this._owner.width, (value.f2 || 0) * this._owner.height);
else if (value.b1) target.x = (value.f1 || 0) * this._owner.width;
else if (value.b2) target.y = (value.f2 || 0) * this._owner.height;
} else {
if (value.b1 && value.b2) target.setXY(value.f1 || 0, value.f2 || 0);
else if (value.b1) target.x = value.f1 || 0;
else if (value.b2) target.y = value.f2 || 0;
}
break;
case ETransitionActionType.Size:
if (!value.b1) value.f1 = target.width;
if (!value.b2) value.f2 = target.height;
target.setSize(value.f1 || 0, value.f2 || 0);
break;
case ETransitionActionType.Pivot:
target.setPivot(value.f1 || 0, value.f2 || 0, target.pivotAsAnchor);
break;
case ETransitionActionType.Alpha:
target.alpha = value.f1 || 0;
break;
case ETransitionActionType.Rotation:
target.rotation = value.f1 || 0;
break;
case ETransitionActionType.Scale:
target.setScale(value.f1 || 0, value.f2 || 0);
break;
case ETransitionActionType.Skew:
target.setSkew(value.f1 || 0, value.f2 || 0);
break;
case ETransitionActionType.Visible:
target.visible = value.visible || false;
break;
case ETransitionActionType.Transition:
if (this._playing && value.trans) {
this._totalTasks++;
const startTime = this._startTime > item.time ? this._startTime - item.time : 0;
let endTime = this._endTime >= 0 ? this._endTime - item.time : -1;
if (value.stopTime !== undefined && value.stopTime >= 0 && (endTime < 0 || endTime > value.stopTime)) {
endTime = value.stopTime;
}
value.trans.timeScale = this._timeScale;
value.trans._play(() => { this._totalTasks--; this.checkAllComplete(); }, value.playTimes || 1, 0, startTime, endTime, this._reversed);
}
break;
case ETransitionActionType.Shake:
target.x = target.x - (value.lastOffsetX || 0) + (value.offsetX || 0);
target.y = target.y - (value.lastOffsetY || 0) + (value.offsetY || 0);
value.lastOffsetX = value.offsetX;
value.lastOffsetY = value.offsetY;
break;
case ETransitionActionType.Text:
target.text = value.text || '';
break;
case ETransitionActionType.Icon:
target.icon = value.text || '';
break;
}
}
public setup(buffer: ByteBuffer): void {
this.name = buffer.readS();
this._options = buffer.getInt32();
this._autoPlay = buffer.readBool();
this._autoPlayTimes = buffer.getInt32();
this._autoPlayDelay = buffer.getFloat32();
const cnt = buffer.getInt16();
for (let i = 0; i < cnt; i++) {
const dataLen = buffer.getInt16();
const curPos = buffer.position;
buffer.seek(curPos, 0);
const item: ITransitionItem = {
type: buffer.readByte() as ETransitionActionType,
time: buffer.getFloat32(),
targetId: '',
value: {},
displayLockToken: 0
};
const targetId = buffer.getInt16();
if (targetId >= 0) {
const child = this._owner.getChildAt(targetId);
item.targetId = child?.id || '';
}
item.label = buffer.readS();
if (buffer.readBool()) {
buffer.seek(curPos, 1);
item.tweenConfig = {
duration: buffer.getFloat32(),
easeType: buffer.readByte() as EEaseType,
repeat: buffer.getInt32(),
yoyo: buffer.readBool(),
startValue: {},
endValue: {},
endLabel: buffer.readS()
};
buffer.seek(curPos, 2);
this.decodeValue(item.type, buffer, item.tweenConfig.startValue);
buffer.seek(curPos, 3);
this.decodeValue(item.type, buffer, item.tweenConfig.endValue);
} else {
buffer.seek(curPos, 2);
this.decodeValue(item.type, buffer, item.value);
}
this._items.push(item);
buffer.position = curPos + dataLen;
}
this._totalDuration = 0;
for (const item of this._items) {
let duration = item.time;
if (item.tweenConfig) duration += item.tweenConfig.duration * (item.tweenConfig.repeat + 1);
else if (item.type === ETransitionActionType.Shake) duration += item.value.duration || 0;
if (duration > this._totalDuration) this._totalDuration = duration;
}
}
private decodeValue(type: ETransitionActionType, buffer: ByteBuffer, value: ITransitionValue): void {
switch (type) {
case ETransitionActionType.XY:
case ETransitionActionType.Size:
case ETransitionActionType.Pivot:
case ETransitionActionType.Skew:
value.b1 = buffer.readBool();
value.b2 = buffer.readBool();
value.f1 = buffer.getFloat32();
value.f2 = buffer.getFloat32();
if (buffer.version >= 2 && type === ETransitionActionType.XY) value.b3 = buffer.readBool();
break;
case ETransitionActionType.Alpha:
case ETransitionActionType.Rotation:
value.f1 = buffer.getFloat32();
break;
case ETransitionActionType.Scale:
value.f1 = buffer.getFloat32();
value.f2 = buffer.getFloat32();
break;
case ETransitionActionType.Color:
value.f1 = buffer.readColor();
break;
case ETransitionActionType.Animation:
value.playing = buffer.readBool();
value.frame = buffer.getInt32();
break;
case ETransitionActionType.Visible:
value.visible = buffer.readBool();
break;
case ETransitionActionType.Sound:
value.sound = buffer.readS();
value.volume = buffer.getFloat32();
break;
case ETransitionActionType.Transition:
value.transName = buffer.readS();
value.playTimes = buffer.getInt32();
break;
case ETransitionActionType.Shake:
value.amplitude = buffer.getFloat32();
value.duration = buffer.getFloat32();
break;
case ETransitionActionType.ColorFilter:
value.f1 = buffer.getFloat32();
value.f2 = buffer.getFloat32();
value.f3 = buffer.getFloat32();
value.f4 = buffer.getFloat32();
break;
case ETransitionActionType.Text:
case ETransitionActionType.Icon:
value.text = buffer.readS();
break;
}
}
public dispose(): void {
if (this._playing) GTween.kill(this);
for (const item of this._items) {
if (item.tweener) {
item.tweener.kill();
item.tweener = undefined;
}
item.target = undefined;
item.hook = undefined;
if (item.tweenConfig) item.tweenConfig.endHook = undefined;
}
this._items.length = 0;
super.dispose();
}
}

View File

@@ -0,0 +1,116 @@
/**
* UIConfig
*
* Global configuration for FairyGUI system.
* Centralizes all configurable settings.
*
* FairyGUI 系统的全局配置,集中管理所有可配置项
*/
export const UIConfig = {
/** Default font | 默认字体 */
defaultFont: 'Arial',
/** Default font size | 默认字体大小 */
defaultFontSize: 14,
/** Button sound URL | 按钮声音 URL */
buttonSound: '',
/** Button sound volume scale | 按钮声音音量 */
buttonSoundVolumeScale: 1,
/** Horizontal scrollbar resource | 水平滚动条资源 */
horizontalScrollBar: '',
/** Vertical scrollbar resource | 垂直滚动条资源 */
verticalScrollBar: '',
/** Default scroll step | 默认滚动步进 */
defaultScrollStep: 25,
/** Default touch scroll | 默认触摸滚动 */
defaultTouchScroll: true,
/** Default scroll bounce | 默认滚动回弹 */
defaultScrollBounce: true,
/** Default scroll bar display | 默认滚动条显示 */
defaultScrollBarDisplay: 1,
/** Touch drag sensitivity | 触摸拖拽灵敏度 */
touchDragSensitivity: 10,
/** Click drag sensitivity | 点击拖拽灵敏度 */
clickDragSensitivity: 2,
/** Allow softness on top | 允许顶部弹性 */
allowSoftnessOnTopOrLeftSide: true,
/** Global modal layer resource | 全局模态层资源 */
modalLayerResource: '',
/** Modal layer color | 模态层颜色 */
modalLayerColor: 0x333333,
/** Modal layer alpha | 模态层透明度 */
modalLayerAlpha: 0.4,
/** Popup close on click outside | 点击外部关闭弹窗 */
popupCloseOnClickOutside: true,
/** Branch for resource loading | 资源加载分支 */
branch: '',
/** Loading animation resource | 加载动画资源 */
loadingAnimation: '',
/** Loader error sign resource | 加载器错误标志资源 */
loaderErrorSign: '',
/** Popup menu resource | 弹出菜单资源 */
popupMenu: '',
/** Popup menu separator resource | 弹出菜单分隔符资源 */
popupMenuSeperator: '',
/** Window modal waiting resource | 窗口模态等待资源 */
windowModalWaiting: '',
/** Bring window to front on click | 点击时将窗口置顶 */
bringWindowToFrontOnClick: true
} as const;
/**
* Mutable config type for runtime changes
* 可变配置类型用于运行时修改
*/
export type UIConfigType = {
-readonly [K in keyof typeof UIConfig]: (typeof UIConfig)[K];
};
/** Runtime config instance | 运行时配置实例 */
const _runtimeConfig: UIConfigType = { ...UIConfig };
/**
* Get current config value
* 获取当前配置值
*/
export function getUIConfig<K extends keyof UIConfigType>(key: K): UIConfigType[K] {
return _runtimeConfig[key];
}
/**
* Set config value
* 设置配置值
*/
export function setUIConfig<K extends keyof UIConfigType>(key: K, value: UIConfigType[K]): void {
_runtimeConfig[key] = value;
}
/**
* Reset config to defaults
* 重置配置为默认值
*/
export function resetUIConfig(): void {
Object.assign(_runtimeConfig, UIConfig);
}

View File

@@ -0,0 +1,184 @@
import { GObject } from './GObject';
import { EObjectType } from './FieldTypes';
import type { PackageItem } from '../package/PackageItem';
/**
* Object creator function type
* 对象创建函数类型
*/
export type ObjectCreator = () => GObject;
/**
* Extension creator function type
* 扩展创建函数类型
*/
export type ExtensionCreator = () => GObject;
/**
* UIObjectFactory
*
* Factory for creating FairyGUI objects.
* All object types are registered via registerCreator() to avoid circular dependencies.
*
* FairyGUI 对象工厂,所有对象类型通过 registerCreator() 注册以避免循环依赖
*/
export class UIObjectFactory {
private static _creators: Map<EObjectType, ObjectCreator> = new Map();
private static _extensions: Map<string, ExtensionCreator> = new Map();
/**
* Register a creator for an object type
* 注册对象类型创建器
*/
public static registerCreator(type: EObjectType, creator: ObjectCreator): void {
UIObjectFactory._creators.set(type, creator);
}
/**
* Register an extension creator for a URL
* 注册扩展创建器
*/
public static registerExtension(url: string, creator: ExtensionCreator): void {
UIObjectFactory._extensions.set(url, creator);
}
/**
* Check if extension exists for URL
* 检查 URL 是否有扩展
*/
public static hasExtension(url: string): boolean {
return UIObjectFactory._extensions.has(url);
}
/**
* Create object by type
* 根据类型创建对象
*/
public static createObject(type: EObjectType, _userClass?: new () => GObject): GObject | null {
const creator = UIObjectFactory._creators.get(type);
if (creator) {
const obj = creator();
return obj;
}
// Fallback for component-based types
switch (type) {
case EObjectType.Component:
case EObjectType.Label:
case EObjectType.ComboBox:
case EObjectType.List:
case EObjectType.Tree:
case EObjectType.ScrollBar:
case EObjectType.MovieClip:
case EObjectType.Swf:
case EObjectType.Loader:
case EObjectType.Loader3D:
// Use Component creator if specific creator not registered
const componentCreator = UIObjectFactory._creators.get(EObjectType.Component);
if (componentCreator) {
const obj = componentCreator();
return obj;
}
break;
}
return new GObject();
}
/**
* Create new object by type (number)
* 根据类型号创建新对象
*/
public static newObject(type: number): GObject;
/**
* Create new object from package item
* 从包资源项创建新对象
*/
public static newObject(item: PackageItem): GObject;
public static newObject(arg: number | PackageItem): GObject {
if (typeof arg === 'number') {
const obj = UIObjectFactory.createObject(arg as EObjectType) || new GObject();
return obj;
} else {
const item = arg as PackageItem;
// Check for extension
if (item.owner) {
const url = 'ui://' + item.owner.id + item.id;
const extensionCreator = UIObjectFactory._extensions.get(url);
if (extensionCreator) {
const obj = extensionCreator();
obj.packageItem = item;
return obj;
}
// Also check by name
const urlByName = 'ui://' + item.owner.name + '/' + item.name;
const extensionCreatorByName = UIObjectFactory._extensions.get(urlByName);
if (extensionCreatorByName) {
const obj = extensionCreatorByName();
obj.packageItem = item;
return obj;
}
}
const obj = UIObjectFactory.createObject(item.objectType);
if (obj) {
obj.packageItem = item;
}
return obj || new GObject();
}
}
/**
* Create object from package item
* 从包资源项创建对象
*/
public static createObjectFromItem(item: PackageItem): GObject | null {
const obj = UIObjectFactory.createObject(item.objectType);
if (obj) {
obj.packageItem = item;
obj.constructFromResource();
}
return obj;
}
/**
* Create object from URL with extension support
* 从 URL 创建对象(支持扩展)
*/
public static createObjectFromURL(url: string): GObject | null {
const extensionCreator = UIObjectFactory._extensions.get(url);
if (extensionCreator) {
return extensionCreator();
}
return null;
}
/**
* Resolve package item extension
* 解析包项扩展
*/
public static resolvePackageItemExtension(item: PackageItem): void {
if (!item.owner) return;
const url = 'ui://' + item.owner.id + item.id;
if (UIObjectFactory._extensions.has(url)) {
return;
}
const urlByName = 'ui://' + item.owner.name + '/' + item.name;
if (UIObjectFactory._extensions.has(urlByName)) {
return;
}
}
/**
* Clear all registered creators and extensions
* 清除所有注册的创建器和扩展
*/
public static clear(): void {
UIObjectFactory._creators.clear();
UIObjectFactory._extensions.clear();
}
}

View File

@@ -0,0 +1,39 @@
/**
* FairyGUI Module Initialization
*
* This module registers all object type creators with UIObjectFactory.
* It must be imported after all classes are defined to break circular dependencies.
*
* FairyGUI 模块初始化,注册所有对象类型创建器以打破循环依赖
*/
import { UIObjectFactory } from './UIObjectFactory';
import { EObjectType } from './FieldTypes';
import { GGroup } from './GGroup';
import { GComponent } from './GComponent';
import { GImage } from '../widgets/GImage';
import { GGraph } from '../widgets/GGraph';
import { GTextField } from '../widgets/GTextField';
import { GTextInput } from '../widgets/GTextInput';
import { GButton } from '../widgets/GButton';
import { GProgressBar } from '../widgets/GProgressBar';
import { GSlider } from '../widgets/GSlider';
import { GMovieClip } from '../widgets/GMovieClip';
import { GLoader } from '../widgets/GLoader';
// Register all object type creators
UIObjectFactory.registerCreator(EObjectType.Image, () => new GImage());
UIObjectFactory.registerCreator(EObjectType.Graph, () => new GGraph());
UIObjectFactory.registerCreator(EObjectType.Text, () => new GTextField());
UIObjectFactory.registerCreator(EObjectType.RichText, () => new GTextField());
UIObjectFactory.registerCreator(EObjectType.InputText, () => new GTextInput());
UIObjectFactory.registerCreator(EObjectType.Group, () => new GGroup());
UIObjectFactory.registerCreator(EObjectType.Component, () => new GComponent());
UIObjectFactory.registerCreator(EObjectType.Button, () => new GButton());
UIObjectFactory.registerCreator(EObjectType.ProgressBar, () => new GProgressBar());
UIObjectFactory.registerCreator(EObjectType.Slider, () => new GSlider());
UIObjectFactory.registerCreator(EObjectType.MovieClip, () => new GMovieClip());
UIObjectFactory.registerCreator(EObjectType.Loader, () => new GLoader());
// Component-based types use GComponent as fallback (registered above)
// Label, ComboBox, List, Tree, ScrollBar, Swf, Loader3D