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:
327
packages/rendering/fairygui/src/core/Controller.ts
Normal file
327
packages/rendering/fairygui/src/core/Controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
144
packages/rendering/fairygui/src/core/DragDropManager.ts
Normal file
144
packages/rendering/fairygui/src/core/DragDropManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
366
packages/rendering/fairygui/src/core/FieldTypes.ts
Normal file
366
packages/rendering/fairygui/src/core/FieldTypes.ts
Normal 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
|
||||
}
|
||||
1005
packages/rendering/fairygui/src/core/GComponent.ts
Normal file
1005
packages/rendering/fairygui/src/core/GComponent.ts
Normal file
File diff suppressed because it is too large
Load Diff
261
packages/rendering/fairygui/src/core/GGroup.ts
Normal file
261
packages/rendering/fairygui/src/core/GGroup.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
1086
packages/rendering/fairygui/src/core/GObject.ts
Normal file
1086
packages/rendering/fairygui/src/core/GObject.ts
Normal file
File diff suppressed because it is too large
Load Diff
77
packages/rendering/fairygui/src/core/GObjectPool.ts
Normal file
77
packages/rendering/fairygui/src/core/GObjectPool.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
506
packages/rendering/fairygui/src/core/GRoot.ts
Normal file
506
packages/rendering/fairygui/src/core/GRoot.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
268
packages/rendering/fairygui/src/core/ServiceContainer.ts
Normal file
268
packages/rendering/fairygui/src/core/ServiceContainer.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
353
packages/rendering/fairygui/src/core/Stage.ts
Normal file
353
packages/rendering/fairygui/src/core/Stage.ts
Normal 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'
|
||||
}
|
||||
266
packages/rendering/fairygui/src/core/Timer.ts
Normal file
266
packages/rendering/fairygui/src/core/Timer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
859
packages/rendering/fairygui/src/core/Transition.ts
Normal file
859
packages/rendering/fairygui/src/core/Transition.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
116
packages/rendering/fairygui/src/core/UIConfig.ts
Normal file
116
packages/rendering/fairygui/src/core/UIConfig.ts
Normal 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);
|
||||
}
|
||||
184
packages/rendering/fairygui/src/core/UIObjectFactory.ts
Normal file
184
packages/rendering/fairygui/src/core/UIObjectFactory.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
39
packages/rendering/fairygui/src/core/init.ts
Normal file
39
packages/rendering/fairygui/src/core/init.ts
Normal 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
|
||||
Reference in New Issue
Block a user