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:
35
packages/rendering/fairygui/src/display/Container.ts
Normal file
35
packages/rendering/fairygui/src/display/Container.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { DisplayObject } from './DisplayObject';
|
||||
import type { IRenderCollector } from '../render/IRenderCollector';
|
||||
|
||||
/**
|
||||
* Container
|
||||
*
|
||||
* A concrete DisplayObject that can contain children but has no visual content itself.
|
||||
* Used as the display object for GComponent.
|
||||
*
|
||||
* 一个具体的 DisplayObject,可以包含子对象但本身没有可视内容。
|
||||
* 用作 GComponent 的显示对象。
|
||||
*/
|
||||
export class Container extends DisplayObject {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect render data from children
|
||||
* 从子对象收集渲染数据
|
||||
*/
|
||||
public collectRenderData(collector: IRenderCollector): void {
|
||||
if (!this._visible) return;
|
||||
|
||||
// Update transform before collecting render data
|
||||
// 收集渲染数据前更新变换
|
||||
this.updateTransform();
|
||||
|
||||
// Collect render data from all children
|
||||
// 从所有子对象收集渲染数据
|
||||
for (const child of this._children) {
|
||||
child.collectRenderData(collector);
|
||||
}
|
||||
}
|
||||
}
|
||||
638
packages/rendering/fairygui/src/display/DisplayObject.ts
Normal file
638
packages/rendering/fairygui/src/display/DisplayObject.ts
Normal file
@@ -0,0 +1,638 @@
|
||||
import { EventDispatcher } from '../events/EventDispatcher';
|
||||
import { FGUIEvents } from '../events/Events';
|
||||
import { Point, Rectangle } from '../utils/MathTypes';
|
||||
import type { IRenderCollector } from '../render/IRenderCollector';
|
||||
import type { GObject } from '../core/GObject';
|
||||
|
||||
/**
|
||||
* DisplayObject
|
||||
*
|
||||
* Abstract display object base class for all visual elements.
|
||||
*
|
||||
* 抽象显示对象基类,所有可视元素的基础
|
||||
*/
|
||||
export abstract class DisplayObject extends EventDispatcher {
|
||||
/** Name of this display object | 显示对象名称 */
|
||||
public name: string = '';
|
||||
|
||||
// Transform properties | 变换属性
|
||||
protected _x: number = 0;
|
||||
protected _y: number = 0;
|
||||
protected _width: number = 0;
|
||||
protected _height: number = 0;
|
||||
protected _scaleX: number = 1;
|
||||
protected _scaleY: number = 1;
|
||||
protected _rotation: number = 0;
|
||||
protected _pivotX: number = 0;
|
||||
protected _pivotY: number = 0;
|
||||
protected _skewX: number = 0;
|
||||
protected _skewY: number = 0;
|
||||
|
||||
// Display properties | 显示属性
|
||||
protected _alpha: number = 1;
|
||||
protected _visible: boolean = true;
|
||||
protected _touchable: boolean = true;
|
||||
protected _grayed: boolean = false;
|
||||
|
||||
// Hierarchy | 层级关系
|
||||
protected _parent: DisplayObject | null = null;
|
||||
protected _children: DisplayObject[] = [];
|
||||
|
||||
// Stage reference | 舞台引用
|
||||
protected _stage: DisplayObject | null = null;
|
||||
|
||||
// Dirty flags | 脏标记
|
||||
protected _transformDirty: boolean = true;
|
||||
protected _boundsDirty: boolean = true;
|
||||
|
||||
// Cached values | 缓存值
|
||||
protected _worldAlpha: number = 1;
|
||||
protected _worldMatrix: Float32Array = new Float32Array([1, 0, 0, 1, 0, 0]);
|
||||
protected _bounds: Rectangle = new Rectangle();
|
||||
|
||||
// User data | 用户数据
|
||||
public userData: unknown = null;
|
||||
|
||||
/** Owner GObject reference | 所属 GObject 引用 */
|
||||
public gOwner: GObject | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
// Position | 位置
|
||||
|
||||
public get x(): number {
|
||||
return this._x;
|
||||
}
|
||||
|
||||
public set x(value: number) {
|
||||
if (this._x !== value) {
|
||||
this._x = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public get y(): number {
|
||||
return this._y;
|
||||
}
|
||||
|
||||
public set y(value: number) {
|
||||
if (this._y !== value) {
|
||||
this._y = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public setPosition(x: number, y: number): void {
|
||||
if (this._x !== x || this._y !== y) {
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Size | 尺寸
|
||||
|
||||
public get width(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public set width(value: number) {
|
||||
if (this._width !== value) {
|
||||
this._width = value;
|
||||
this.markBoundsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public get height(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public set height(value: number) {
|
||||
if (this._height !== value) {
|
||||
this._height = value;
|
||||
this.markBoundsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public setSize(width: number, height: number): void {
|
||||
if (this._width !== width || this._height !== height) {
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
this.markBoundsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Scale | 缩放
|
||||
|
||||
public get scaleX(): number {
|
||||
return this._scaleX;
|
||||
}
|
||||
|
||||
public set scaleX(value: number) {
|
||||
if (this._scaleX !== value) {
|
||||
this._scaleX = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public get scaleY(): number {
|
||||
return this._scaleY;
|
||||
}
|
||||
|
||||
public set scaleY(value: number) {
|
||||
if (this._scaleY !== value) {
|
||||
this._scaleY = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public setScale(scaleX: number, scaleY: number): void {
|
||||
if (this._scaleX !== scaleX || this._scaleY !== scaleY) {
|
||||
this._scaleX = scaleX;
|
||||
this._scaleY = scaleY;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Rotation | 旋转
|
||||
|
||||
public get rotation(): number {
|
||||
return this._rotation;
|
||||
}
|
||||
|
||||
public set rotation(value: number) {
|
||||
if (this._rotation !== value) {
|
||||
this._rotation = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Pivot | 轴心点
|
||||
|
||||
public get pivotX(): number {
|
||||
return this._pivotX;
|
||||
}
|
||||
|
||||
public set pivotX(value: number) {
|
||||
if (this._pivotX !== value) {
|
||||
this._pivotX = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public get pivotY(): number {
|
||||
return this._pivotY;
|
||||
}
|
||||
|
||||
public set pivotY(value: number) {
|
||||
if (this._pivotY !== value) {
|
||||
this._pivotY = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public setPivot(pivotX: number, pivotY: number): void {
|
||||
if (this._pivotX !== pivotX || this._pivotY !== pivotY) {
|
||||
this._pivotX = pivotX;
|
||||
this._pivotY = pivotY;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Skew | 倾斜
|
||||
|
||||
public get skewX(): number {
|
||||
return this._skewX;
|
||||
}
|
||||
|
||||
public set skewX(value: number) {
|
||||
if (this._skewX !== value) {
|
||||
this._skewX = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public get skewY(): number {
|
||||
return this._skewY;
|
||||
}
|
||||
|
||||
public set skewY(value: number) {
|
||||
if (this._skewY !== value) {
|
||||
this._skewY = value;
|
||||
this.markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Alpha | 透明度
|
||||
|
||||
public get alpha(): number {
|
||||
return this._alpha;
|
||||
}
|
||||
|
||||
public set alpha(value: number) {
|
||||
if (this._alpha !== value) {
|
||||
this._alpha = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Visibility | 可见性
|
||||
|
||||
public get visible(): boolean {
|
||||
return this._visible;
|
||||
}
|
||||
|
||||
public set visible(value: boolean) {
|
||||
this._visible = value;
|
||||
}
|
||||
|
||||
// Touchable | 可触摸
|
||||
|
||||
public get touchable(): boolean {
|
||||
return this._touchable;
|
||||
}
|
||||
|
||||
public set touchable(value: boolean) {
|
||||
this._touchable = value;
|
||||
}
|
||||
|
||||
// Grayed | 灰度
|
||||
|
||||
public get grayed(): boolean {
|
||||
return this._grayed;
|
||||
}
|
||||
|
||||
public set grayed(value: boolean) {
|
||||
this._grayed = value;
|
||||
}
|
||||
|
||||
// Hierarchy | 层级
|
||||
|
||||
public get parent(): DisplayObject | null {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stage reference
|
||||
* 获取舞台引用
|
||||
*/
|
||||
public get stage(): DisplayObject | null {
|
||||
return this._stage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set stage reference (internal use)
|
||||
* 设置舞台引用(内部使用)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public setStage(stage: DisplayObject | null): void {
|
||||
this._stage = stage;
|
||||
}
|
||||
|
||||
public get numChildren(): number {
|
||||
return this._children.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a child display object
|
||||
* 添加子显示对象
|
||||
*/
|
||||
public addChild(child: DisplayObject): void {
|
||||
this.addChildAt(child, this._children.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a child at specific index
|
||||
* 在指定位置添加子显示对象
|
||||
*/
|
||||
public addChildAt(child: DisplayObject, index: number): void {
|
||||
if (child._parent === this) {
|
||||
this.setChildIndex(child, index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (child._parent) {
|
||||
child._parent.removeChild(child);
|
||||
}
|
||||
|
||||
index = Math.max(0, Math.min(index, this._children.length));
|
||||
this._children.splice(index, 0, child);
|
||||
child._parent = this;
|
||||
child.markTransformDirty();
|
||||
|
||||
// Dispatch addedToStage event if this is on stage
|
||||
// 如果当前对象在舞台上,分发 addedToStage 事件
|
||||
if (this._stage !== null) {
|
||||
this.setChildStage(child, this._stage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set stage for child and its descendants, dispatch events
|
||||
* 为子对象及其后代设置舞台,分发事件
|
||||
*/
|
||||
private setChildStage(child: DisplayObject, stage: DisplayObject | null): void {
|
||||
const wasOnStage = child._stage !== null;
|
||||
const isOnStage = stage !== null;
|
||||
|
||||
child._stage = stage;
|
||||
|
||||
if (!wasOnStage && isOnStage) {
|
||||
// Dispatch addedToStage event
|
||||
child.emit(FGUIEvents.ADDED_TO_STAGE);
|
||||
} else if (wasOnStage && !isOnStage) {
|
||||
// Dispatch removedFromStage event
|
||||
child.emit(FGUIEvents.REMOVED_FROM_STAGE);
|
||||
}
|
||||
|
||||
// Recursively set stage for all children
|
||||
for (const grandChild of child._children) {
|
||||
this.setChildStage(grandChild, stage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a child display object
|
||||
* 移除子显示对象
|
||||
*/
|
||||
public removeChild(child: DisplayObject): void {
|
||||
const index = this._children.indexOf(child);
|
||||
if (index >= 0) {
|
||||
this.removeChildAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove child at specific index
|
||||
* 移除指定位置的子显示对象
|
||||
*/
|
||||
public removeChildAt(index: number): DisplayObject | null {
|
||||
if (index < 0 || index >= this._children.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const child = this._children[index];
|
||||
|
||||
// Dispatch removedFromStage event if on stage
|
||||
// 如果在舞台上,分发 removedFromStage 事件
|
||||
if (this._stage !== null) {
|
||||
this.setChildStage(child, null);
|
||||
}
|
||||
|
||||
this._children.splice(index, 1);
|
||||
child._parent = null;
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all children
|
||||
* 移除所有子显示对象
|
||||
*/
|
||||
public removeChildren(): void {
|
||||
// Dispatch removedFromStage events if on stage
|
||||
// 如果在舞台上,分发 removedFromStage 事件
|
||||
if (this._stage !== null) {
|
||||
for (const child of this._children) {
|
||||
this.setChildStage(child, null);
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of this._children) {
|
||||
child._parent = null;
|
||||
}
|
||||
this._children.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get child at index
|
||||
* 获取指定位置的子显示对象
|
||||
*/
|
||||
public getChildAt(index: number): DisplayObject | null {
|
||||
if (index < 0 || index >= this._children.length) {
|
||||
return null;
|
||||
}
|
||||
return this._children[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get child index
|
||||
* 获取子显示对象的索引
|
||||
*/
|
||||
public getChildIndex(child: DisplayObject): number {
|
||||
return this._children.indexOf(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set child index
|
||||
* 设置子显示对象的索引
|
||||
*/
|
||||
public setChildIndex(child: DisplayObject, index: number): void {
|
||||
const currentIndex = this._children.indexOf(child);
|
||||
if (currentIndex < 0) return;
|
||||
|
||||
index = Math.max(0, Math.min(index, this._children.length - 1));
|
||||
if (currentIndex === index) return;
|
||||
|
||||
this._children.splice(currentIndex, 1);
|
||||
this._children.splice(index, 0, child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap two children
|
||||
* 交换两个子显示对象
|
||||
*/
|
||||
public swapChildren(child1: DisplayObject, child2: DisplayObject): void {
|
||||
const index1 = this._children.indexOf(child1);
|
||||
const index2 = this._children.indexOf(child2);
|
||||
if (index1 >= 0 && index2 >= 0) {
|
||||
this._children[index1] = child2;
|
||||
this._children[index2] = child1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get child by name
|
||||
* 通过名称获取子显示对象
|
||||
*/
|
||||
public getChildByName(name: string): DisplayObject | null {
|
||||
for (const child of this._children) {
|
||||
if (child.name === name) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Transform | 变换
|
||||
|
||||
/**
|
||||
* Update world matrix
|
||||
* 更新世界矩阵
|
||||
*
|
||||
* World matrix is in FGUI's coordinate system (top-left origin, Y-down).
|
||||
* Coordinate system conversion to engine (center origin, Y-up) is done in FGUIRenderDataProvider.
|
||||
*
|
||||
* 世界矩阵使用 FGUI 坐标系(左上角原点,Y 向下)。
|
||||
* 坐标系转换到引擎(中心原点,Y 向上)在 FGUIRenderDataProvider 中完成。
|
||||
*/
|
||||
public updateTransform(): void {
|
||||
if (!this._transformDirty) return;
|
||||
|
||||
const m = this._worldMatrix;
|
||||
const rad = (this._rotation * Math.PI) / 180;
|
||||
const cos = Math.cos(rad);
|
||||
const sin = Math.sin(rad);
|
||||
|
||||
m[0] = cos * this._scaleX;
|
||||
m[1] = sin * this._scaleX;
|
||||
m[2] = -sin * this._scaleY;
|
||||
m[3] = cos * this._scaleY;
|
||||
|
||||
// Keep FGUI's coordinate system (top-left origin, Y-down)
|
||||
// 保持 FGUI 坐标系(左上角原点,Y 向下)
|
||||
m[4] = this._x - this._pivotX * m[0] - this._pivotY * m[2];
|
||||
m[5] = this._y - this._pivotX * m[1] - this._pivotY * m[3];
|
||||
|
||||
if (this._parent) {
|
||||
const pm = this._parent._worldMatrix;
|
||||
const a = m[0], b = m[1], c = m[2], d = m[3], tx = m[4], ty = m[5];
|
||||
|
||||
m[0] = a * pm[0] + b * pm[2];
|
||||
m[1] = a * pm[1] + b * pm[3];
|
||||
m[2] = c * pm[0] + d * pm[2];
|
||||
m[3] = c * pm[1] + d * pm[3];
|
||||
m[4] = tx * pm[0] + ty * pm[2] + pm[4];
|
||||
m[5] = tx * pm[1] + ty * pm[3] + pm[5];
|
||||
|
||||
this._worldAlpha = this._alpha * this._parent._worldAlpha;
|
||||
} else {
|
||||
this._worldAlpha = this._alpha;
|
||||
}
|
||||
|
||||
this._transformDirty = false;
|
||||
|
||||
for (const child of this._children) {
|
||||
child.markTransformDirty();
|
||||
child.updateTransform();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local to global point conversion
|
||||
* 本地坐标转全局坐标
|
||||
*/
|
||||
public localToGlobal(localPoint: Point, outPoint?: Point): Point {
|
||||
this.updateTransform();
|
||||
|
||||
outPoint = outPoint || new Point();
|
||||
const m = this._worldMatrix;
|
||||
outPoint.x = localPoint.x * m[0] + localPoint.y * m[2] + m[4];
|
||||
outPoint.y = localPoint.x * m[1] + localPoint.y * m[3] + m[5];
|
||||
return outPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global to local point conversion
|
||||
* 全局坐标转本地坐标
|
||||
*/
|
||||
public globalToLocal(globalPoint: Point, outPoint?: Point): Point {
|
||||
this.updateTransform();
|
||||
|
||||
outPoint = outPoint || new Point();
|
||||
const m = this._worldMatrix;
|
||||
const det = m[0] * m[3] - m[1] * m[2];
|
||||
|
||||
if (det === 0) {
|
||||
outPoint.x = 0;
|
||||
outPoint.y = 0;
|
||||
} else {
|
||||
const invDet = 1 / det;
|
||||
const x = globalPoint.x - m[4];
|
||||
const y = globalPoint.y - m[5];
|
||||
outPoint.x = (x * m[3] - y * m[2]) * invDet;
|
||||
outPoint.y = (y * m[0] - x * m[1]) * invDet;
|
||||
}
|
||||
return outPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit test
|
||||
* 碰撞检测
|
||||
*/
|
||||
public hitTest(globalX: number, globalY: number): DisplayObject | null {
|
||||
if (!this._visible || !this._touchable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const localPoint = this.globalToLocal(new Point(globalX, globalY));
|
||||
|
||||
if (
|
||||
localPoint.x >= 0 &&
|
||||
localPoint.x < this._width &&
|
||||
localPoint.y >= 0 &&
|
||||
localPoint.y < this._height
|
||||
) {
|
||||
for (let i = this._children.length - 1; i >= 0; i--) {
|
||||
const hit = this._children[i].hitTest(globalX, globalY);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dirty flags | 脏标记
|
||||
|
||||
protected markTransformDirty(): void {
|
||||
this._transformDirty = true;
|
||||
this._boundsDirty = true;
|
||||
}
|
||||
|
||||
protected markBoundsDirty(): void {
|
||||
this._boundsDirty = true;
|
||||
}
|
||||
|
||||
// Render data collection | 渲染数据收集
|
||||
|
||||
/**
|
||||
* Collect render data (abstract - implemented by subclasses)
|
||||
* 收集渲染数据(抽象方法 - 由子类实现)
|
||||
*/
|
||||
public abstract collectRenderData(collector: IRenderCollector): void;
|
||||
|
||||
/**
|
||||
* Get world matrix
|
||||
* 获取世界矩阵
|
||||
*/
|
||||
public get worldMatrix(): Float32Array {
|
||||
return this._worldMatrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get world alpha
|
||||
* 获取世界透明度
|
||||
*/
|
||||
public get worldAlpha(): number {
|
||||
return this._worldAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose
|
||||
* 销毁
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this._parent) {
|
||||
this._parent.removeChild(this);
|
||||
}
|
||||
|
||||
for (const child of this._children) {
|
||||
child.dispose();
|
||||
}
|
||||
|
||||
this._children.length = 0;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
173
packages/rendering/fairygui/src/display/Graph.ts
Normal file
173
packages/rendering/fairygui/src/display/Graph.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { DisplayObject } from './DisplayObject';
|
||||
import { EGraphType } from '../core/FieldTypes';
|
||||
import type { IRenderCollector, IRenderPrimitive } from '../render/IRenderCollector';
|
||||
import { ERenderPrimitiveType } from '../render/IRenderCollector';
|
||||
|
||||
/**
|
||||
* Graph
|
||||
*
|
||||
* Display object for rendering geometric shapes.
|
||||
*
|
||||
* 用于渲染几何图形的显示对象
|
||||
*/
|
||||
export class Graph extends DisplayObject {
|
||||
/** Graph type | 图形类型 */
|
||||
private _type: EGraphType = EGraphType.Empty;
|
||||
|
||||
/** Line width | 线宽 */
|
||||
public lineSize: number = 1;
|
||||
|
||||
/** Line color | 线颜色 */
|
||||
public lineColor: string = '#000000';
|
||||
|
||||
/** Fill color | 填充颜色 */
|
||||
public fillColor: string = '#FFFFFF';
|
||||
|
||||
/** Corner radius for rect | 矩形圆角半径 */
|
||||
public cornerRadius: number[] | null = null;
|
||||
|
||||
/** Polygon points | 多边形顶点 */
|
||||
public polygonPoints: number[] | null = null;
|
||||
|
||||
/** Number of sides for regular polygon | 正多边形边数 */
|
||||
public sides: number = 3;
|
||||
|
||||
/** Start angle for regular polygon | 正多边形起始角度 */
|
||||
public startAngle: number = 0;
|
||||
|
||||
/** Distance multipliers for regular polygon | 正多边形距离乘数 */
|
||||
public distances: number[] | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get graph type
|
||||
* 获取图形类型
|
||||
*/
|
||||
public get type(): EGraphType {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw rectangle
|
||||
* 绘制矩形
|
||||
*/
|
||||
public drawRect(lineSize: number, lineColor: string, fillColor: string, cornerRadius?: number[]): void {
|
||||
this._type = EGraphType.Rect;
|
||||
this.lineSize = lineSize;
|
||||
this.lineColor = lineColor;
|
||||
this.fillColor = fillColor;
|
||||
this.cornerRadius = cornerRadius || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw ellipse
|
||||
* 绘制椭圆
|
||||
*/
|
||||
public drawEllipse(lineSize: number, lineColor: string, fillColor: string): void {
|
||||
this._type = EGraphType.Ellipse;
|
||||
this.lineSize = lineSize;
|
||||
this.lineColor = lineColor;
|
||||
this.fillColor = fillColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw polygon
|
||||
* 绘制多边形
|
||||
*/
|
||||
public drawPolygon(lineSize: number, lineColor: string, fillColor: string, points: number[]): void {
|
||||
this._type = EGraphType.Polygon;
|
||||
this.lineSize = lineSize;
|
||||
this.lineColor = lineColor;
|
||||
this.fillColor = fillColor;
|
||||
this.polygonPoints = points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw regular polygon
|
||||
* 绘制正多边形
|
||||
*/
|
||||
public drawRegularPolygon(
|
||||
lineSize: number,
|
||||
lineColor: string,
|
||||
fillColor: string,
|
||||
sides: number,
|
||||
startAngle?: number,
|
||||
distances?: number[]
|
||||
): void {
|
||||
this._type = EGraphType.RegularPolygon;
|
||||
this.lineSize = lineSize;
|
||||
this.lineColor = lineColor;
|
||||
this.fillColor = fillColor;
|
||||
this.sides = sides;
|
||||
this.startAngle = startAngle || 0;
|
||||
this.distances = distances || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear graph
|
||||
* 清除图形
|
||||
*/
|
||||
public clear(): void {
|
||||
this._type = EGraphType.Empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse color string to packed u32 (0xRRGGBBAA format)
|
||||
* 解析颜色字符串为打包的 u32(0xRRGGBBAA 格式)
|
||||
*/
|
||||
private parseColor(color: string): number {
|
||||
if (color.startsWith('#')) {
|
||||
const hex = color.slice(1);
|
||||
if (hex.length === 6) {
|
||||
return ((parseInt(hex, 16) << 8) | 0xFF) >>> 0;
|
||||
} else if (hex.length === 8) {
|
||||
return parseInt(hex, 16) >>> 0;
|
||||
}
|
||||
}
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
public collectRenderData(collector: IRenderCollector): void {
|
||||
if (!this._visible || this._alpha <= 0 || this._type === EGraphType.Empty) return;
|
||||
|
||||
this.updateTransform();
|
||||
|
||||
const fillColorNum = this.parseColor(this.fillColor);
|
||||
|
||||
const primitive: IRenderPrimitive = {
|
||||
type: ERenderPrimitiveType.Graph,
|
||||
sortOrder: 0,
|
||||
worldMatrix: this._worldMatrix,
|
||||
width: this._width,
|
||||
height: this._height,
|
||||
alpha: this._worldAlpha,
|
||||
grayed: this._grayed,
|
||||
graphType: this._type,
|
||||
lineSize: this.lineSize,
|
||||
lineColor: this.parseColor(this.lineColor),
|
||||
fillColor: fillColorNum,
|
||||
clipRect: collector.getCurrentClipRect() || undefined
|
||||
};
|
||||
|
||||
if (this.cornerRadius) {
|
||||
primitive.cornerRadius = this.cornerRadius;
|
||||
}
|
||||
|
||||
if (this._type === EGraphType.Polygon && this.polygonPoints) {
|
||||
primitive.polygonPoints = this.polygonPoints;
|
||||
}
|
||||
|
||||
if (this._type === EGraphType.RegularPolygon) {
|
||||
primitive.sides = this.sides;
|
||||
primitive.startAngle = this.startAngle;
|
||||
if (this.distances) {
|
||||
primitive.distances = this.distances;
|
||||
}
|
||||
}
|
||||
|
||||
collector.addPrimitive(primitive);
|
||||
}
|
||||
}
|
||||
201
packages/rendering/fairygui/src/display/Image.ts
Normal file
201
packages/rendering/fairygui/src/display/Image.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { DisplayObject } from './DisplayObject';
|
||||
import { Rectangle } from '../utils/MathTypes';
|
||||
import { EFillMethod, EFillOrigin } from '../core/FieldTypes';
|
||||
import type { IRenderCollector, IRenderPrimitive } from '../render/IRenderCollector';
|
||||
import { ERenderPrimitiveType } from '../render/IRenderCollector';
|
||||
|
||||
/**
|
||||
* Sprite texture info from FairyGUI package
|
||||
* FairyGUI 包中的精灵纹理信息
|
||||
*/
|
||||
export interface ISpriteTexture {
|
||||
atlas: string;
|
||||
atlasId: string;
|
||||
rect: { x: number; y: number; width: number; height: number };
|
||||
offset: { x: number; y: number };
|
||||
originalSize: { x: number; y: number };
|
||||
rotated: boolean;
|
||||
/** Atlas width for UV calculation | 图集宽度,用于 UV 计算 */
|
||||
atlasWidth: number;
|
||||
/** Atlas height for UV calculation | 图集高度,用于 UV 计算 */
|
||||
atlasHeight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image
|
||||
*
|
||||
* Display object for rendering images/textures.
|
||||
*
|
||||
* 用于渲染图像/纹理的显示对象
|
||||
*/
|
||||
export class Image extends DisplayObject {
|
||||
/** Texture ID, key, or sprite info | 纹理 ID、键或精灵信息 */
|
||||
public texture: string | number | ISpriteTexture | null = null;
|
||||
|
||||
/** Tint color (hex string like "#FFFFFF") | 着色颜色 */
|
||||
public color: string = '#FFFFFF';
|
||||
|
||||
/** Scale9 grid for 9-slice scaling | 九宫格缩放 */
|
||||
public scale9Grid: Rectangle | null = null;
|
||||
|
||||
/** Scale by tile | 平铺缩放 */
|
||||
public scaleByTile: boolean = false;
|
||||
|
||||
/** Tile grid indice | 平铺网格索引 */
|
||||
public tileGridIndice: number = 0;
|
||||
|
||||
// Fill properties | 填充属性
|
||||
private _fillMethod: EFillMethod = EFillMethod.None;
|
||||
private _fillOrigin: EFillOrigin = EFillOrigin.Top;
|
||||
private _fillClockwise: boolean = true;
|
||||
private _fillAmount: number = 1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public get fillMethod(): EFillMethod {
|
||||
return this._fillMethod;
|
||||
}
|
||||
|
||||
public set fillMethod(value: EFillMethod) {
|
||||
this._fillMethod = value;
|
||||
}
|
||||
|
||||
public get fillOrigin(): EFillOrigin {
|
||||
return this._fillOrigin;
|
||||
}
|
||||
|
||||
public set fillOrigin(value: EFillOrigin) {
|
||||
this._fillOrigin = value;
|
||||
}
|
||||
|
||||
public get fillClockwise(): boolean {
|
||||
return this._fillClockwise;
|
||||
}
|
||||
|
||||
public set fillClockwise(value: boolean) {
|
||||
this._fillClockwise = value;
|
||||
}
|
||||
|
||||
public get fillAmount(): number {
|
||||
return this._fillAmount;
|
||||
}
|
||||
|
||||
public set fillAmount(value: number) {
|
||||
this._fillAmount = Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse color string to packed u32 (0xRRGGBBAA format)
|
||||
* 解析颜色字符串为打包的 u32(0xRRGGBBAA 格式)
|
||||
*/
|
||||
private parseColor(color: string): number {
|
||||
if (color.startsWith('#')) {
|
||||
const hex = color.slice(1);
|
||||
if (hex.length === 6) {
|
||||
return ((parseInt(hex, 16) << 8) | 0xFF) >>> 0;
|
||||
} else if (hex.length === 8) {
|
||||
return parseInt(hex, 16) >>> 0;
|
||||
}
|
||||
}
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
public collectRenderData(collector: IRenderCollector): void {
|
||||
if (!this._visible || this._alpha <= 0 || !this.texture) return;
|
||||
|
||||
this.updateTransform();
|
||||
|
||||
// Determine texture ID, UV rect, and draw rect based on texture type
|
||||
let textureId: string | number;
|
||||
let uvRect: [number, number, number, number] | undefined;
|
||||
let drawWidth = this._width;
|
||||
let drawHeight = this._height;
|
||||
let drawOffsetX = 0;
|
||||
let drawOffsetY = 0;
|
||||
|
||||
if (typeof this.texture === 'object') {
|
||||
// ISpriteTexture - use atlas file as texture ID
|
||||
const sprite = this.texture as ISpriteTexture;
|
||||
textureId = sprite.atlas;
|
||||
|
||||
// Calculate normalized UV from sprite rect and atlas dimensions
|
||||
const atlasW = sprite.atlasWidth || 1;
|
||||
const atlasH = sprite.atlasHeight || 1;
|
||||
const u0 = sprite.rect.x / atlasW;
|
||||
const v0 = sprite.rect.y / atlasH;
|
||||
const u1 = (sprite.rect.x + sprite.rect.width) / atlasW;
|
||||
const v1 = (sprite.rect.y + sprite.rect.height) / atlasH;
|
||||
uvRect = [u0, v0, u1, v1];
|
||||
|
||||
// Handle trimmed sprites (offset and originalSize)
|
||||
// 处理裁剪过的精灵(偏移和原始尺寸)
|
||||
const origW = sprite.originalSize.x;
|
||||
const origH = sprite.originalSize.y;
|
||||
const regionW = sprite.rect.width;
|
||||
const regionH = sprite.rect.height;
|
||||
|
||||
if (origW !== regionW || origH !== regionH) {
|
||||
// Sprite was trimmed, calculate actual draw rect
|
||||
// 精灵被裁剪过,计算实际绘制矩形
|
||||
const sx = this._width / origW;
|
||||
const sy = this._height / origH;
|
||||
drawOffsetX = sprite.offset.x * sx;
|
||||
drawOffsetY = sprite.offset.y * sy;
|
||||
drawWidth = regionW * sx;
|
||||
drawHeight = regionH * sy;
|
||||
}
|
||||
} else {
|
||||
textureId = this.texture;
|
||||
}
|
||||
|
||||
// Create adjusted world matrix if there's an offset
|
||||
let worldMatrix = this._worldMatrix;
|
||||
if (drawOffsetX !== 0 || drawOffsetY !== 0) {
|
||||
// Apply offset to the world matrix translation
|
||||
// 将偏移应用到世界矩阵的平移部分
|
||||
worldMatrix = new Float32Array(this._worldMatrix);
|
||||
const m = this._worldMatrix;
|
||||
// Transform offset by rotation/scale part of matrix
|
||||
worldMatrix[4] = m[4] + drawOffsetX * m[0] + drawOffsetY * m[2];
|
||||
worldMatrix[5] = m[5] + drawOffsetX * m[1] + drawOffsetY * m[3];
|
||||
}
|
||||
|
||||
const primitive: IRenderPrimitive = {
|
||||
type: ERenderPrimitiveType.Image,
|
||||
sortOrder: 0,
|
||||
worldMatrix,
|
||||
width: drawWidth,
|
||||
height: drawHeight,
|
||||
alpha: this._worldAlpha,
|
||||
grayed: this._grayed,
|
||||
textureId,
|
||||
uvRect,
|
||||
color: this.parseColor(this.color),
|
||||
clipRect: collector.getCurrentClipRect() || undefined
|
||||
};
|
||||
|
||||
if (this.scale9Grid) {
|
||||
primitive.scale9Grid = this.scale9Grid;
|
||||
// Pass source dimensions for nine-slice calculation
|
||||
// 传递源尺寸用于九宫格计算
|
||||
if (typeof this.texture === 'object') {
|
||||
const sprite = this.texture as ISpriteTexture;
|
||||
primitive.sourceWidth = sprite.rect.width;
|
||||
primitive.sourceHeight = sprite.rect.height;
|
||||
} else {
|
||||
// For non-sprite textures, use the display object's original size
|
||||
// 对于非精灵纹理,使用显示对象的原始尺寸
|
||||
primitive.sourceWidth = this._width;
|
||||
primitive.sourceHeight = this._height;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scaleByTile) {
|
||||
primitive.tileMode = true;
|
||||
}
|
||||
|
||||
collector.addPrimitive(primitive);
|
||||
}
|
||||
}
|
||||
341
packages/rendering/fairygui/src/display/InputTextField.ts
Normal file
341
packages/rendering/fairygui/src/display/InputTextField.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import { TextField } from './TextField';
|
||||
|
||||
/**
|
||||
* InputTextField
|
||||
*
|
||||
* Editable text input display object.
|
||||
* Creates and manages a hidden HTML input element for text editing.
|
||||
*
|
||||
* 可编辑文本输入显示对象
|
||||
* 创建并管理隐藏的 HTML input 元素用于文本编辑
|
||||
*/
|
||||
export class InputTextField extends TextField {
|
||||
private _inputElement: HTMLInputElement | HTMLTextAreaElement | null = null;
|
||||
private _password: boolean = false;
|
||||
private _keyboardType: string = 'text';
|
||||
private _editable: boolean = true;
|
||||
private _maxLength: number = 0;
|
||||
private _promptText: string = '';
|
||||
private _promptColor: string = '#999999';
|
||||
private _restrict: string = '';
|
||||
private _multiline: boolean = false;
|
||||
private _hasFocus: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.touchable = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set password mode
|
||||
* 获取/设置密码模式
|
||||
*/
|
||||
public get password(): boolean {
|
||||
return this._password;
|
||||
}
|
||||
|
||||
public set password(value: boolean) {
|
||||
if (this._password !== value) {
|
||||
this._password = value;
|
||||
this.updateInputType();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set keyboard type
|
||||
* 获取/设置键盘类型
|
||||
*/
|
||||
public get keyboardType(): string {
|
||||
return this._keyboardType;
|
||||
}
|
||||
|
||||
public set keyboardType(value: string) {
|
||||
if (this._keyboardType !== value) {
|
||||
this._keyboardType = value;
|
||||
this.updateInputType();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set editable state
|
||||
* 获取/设置可编辑状态
|
||||
*/
|
||||
public get editable(): boolean {
|
||||
return this._editable;
|
||||
}
|
||||
|
||||
public set editable(value: boolean) {
|
||||
this._editable = value;
|
||||
if (this._inputElement) {
|
||||
if (value) {
|
||||
this._inputElement.removeAttribute('readonly');
|
||||
} else {
|
||||
this._inputElement.setAttribute('readonly', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set max length
|
||||
* 获取/设置最大长度
|
||||
*/
|
||||
public get maxLength(): number {
|
||||
return this._maxLength;
|
||||
}
|
||||
|
||||
public set maxLength(value: number) {
|
||||
this._maxLength = value;
|
||||
if (this._inputElement && value > 0) {
|
||||
this._inputElement.maxLength = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set placeholder text
|
||||
* 获取/设置占位符文本
|
||||
*/
|
||||
public get promptText(): string {
|
||||
return this._promptText;
|
||||
}
|
||||
|
||||
public set promptText(value: string) {
|
||||
this._promptText = value;
|
||||
if (this._inputElement) {
|
||||
this._inputElement.placeholder = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set placeholder color
|
||||
* 获取/设置占位符颜色
|
||||
*/
|
||||
public get promptColor(): string {
|
||||
return this._promptColor;
|
||||
}
|
||||
|
||||
public set promptColor(value: string) {
|
||||
this._promptColor = value;
|
||||
// Apply via CSS
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set character restriction pattern
|
||||
* 获取/设置字符限制模式
|
||||
*/
|
||||
public get restrict(): string {
|
||||
return this._restrict;
|
||||
}
|
||||
|
||||
public set restrict(value: string) {
|
||||
this._restrict = value;
|
||||
if (this._inputElement && value && this._inputElement instanceof HTMLInputElement) {
|
||||
this._inputElement.pattern = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set multiline mode
|
||||
* 获取/设置多行模式
|
||||
*/
|
||||
public get multiline(): boolean {
|
||||
return this._multiline;
|
||||
}
|
||||
|
||||
public set multiline(value: boolean) {
|
||||
if (this._multiline !== value) {
|
||||
this._multiline = value;
|
||||
this.recreateInputElement();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request focus
|
||||
* 请求焦点
|
||||
*/
|
||||
public focus(): void {
|
||||
this.ensureInputElement();
|
||||
if (this._inputElement) {
|
||||
this._inputElement.focus();
|
||||
this._hasFocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear focus
|
||||
* 清除焦点
|
||||
*/
|
||||
public blur(): void {
|
||||
if (this._inputElement) {
|
||||
this._inputElement.blur();
|
||||
this._hasFocus = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all text
|
||||
* 选择所有文本
|
||||
*/
|
||||
public selectAll(): void {
|
||||
if (this._inputElement) {
|
||||
this._inputElement.select();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set selection range
|
||||
* 设置选择范围
|
||||
*/
|
||||
public setSelection(start: number, end: number): void {
|
||||
if (this._inputElement) {
|
||||
this._inputElement.setSelectionRange(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text from input
|
||||
* 从输入获取文本
|
||||
*/
|
||||
public getInputText(): string {
|
||||
if (this._inputElement) {
|
||||
return this._inputElement.value;
|
||||
}
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set text to input
|
||||
* 设置文本到输入
|
||||
*/
|
||||
public setInputText(value: string): void {
|
||||
this.text = value;
|
||||
if (this._inputElement) {
|
||||
this._inputElement.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private ensureInputElement(): void {
|
||||
if (!this._inputElement) {
|
||||
this.createInputElement();
|
||||
}
|
||||
}
|
||||
|
||||
private createInputElement(): void {
|
||||
if (this._multiline) {
|
||||
this._inputElement = document.createElement('textarea');
|
||||
} else {
|
||||
this._inputElement = document.createElement('input');
|
||||
this.updateInputType();
|
||||
}
|
||||
|
||||
this.applyInputStyles();
|
||||
this.bindInputEvents();
|
||||
|
||||
document.body.appendChild(this._inputElement);
|
||||
}
|
||||
|
||||
private recreateInputElement(): void {
|
||||
const oldValue = this._inputElement?.value || '';
|
||||
this.destroyInputElement();
|
||||
this.createInputElement();
|
||||
if (this._inputElement) {
|
||||
this._inputElement.value = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
private destroyInputElement(): void {
|
||||
if (this._inputElement) {
|
||||
this._inputElement.remove();
|
||||
this._inputElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
private updateInputType(): void {
|
||||
if (this._inputElement && this._inputElement instanceof HTMLInputElement) {
|
||||
if (this._password) {
|
||||
this._inputElement.type = 'password';
|
||||
} else {
|
||||
this._inputElement.type = this._keyboardType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyInputStyles(): void {
|
||||
if (!this._inputElement) return;
|
||||
|
||||
const style = this._inputElement.style;
|
||||
style.position = 'absolute';
|
||||
style.border = 'none';
|
||||
style.outline = 'none';
|
||||
style.background = 'transparent';
|
||||
style.padding = '0';
|
||||
style.margin = '0';
|
||||
style.fontFamily = this.font || 'sans-serif';
|
||||
style.fontSize = `${this.fontSize}px`;
|
||||
style.color = this.color;
|
||||
style.opacity = '0'; // Hidden initially, shown when focused
|
||||
|
||||
if (this._maxLength > 0) {
|
||||
this._inputElement.maxLength = this._maxLength;
|
||||
}
|
||||
if (this._promptText) {
|
||||
this._inputElement.placeholder = this._promptText;
|
||||
}
|
||||
if (this._restrict && this._inputElement instanceof HTMLInputElement) {
|
||||
this._inputElement.pattern = this._restrict;
|
||||
}
|
||||
if (!this._editable) {
|
||||
this._inputElement.setAttribute('readonly', 'true');
|
||||
}
|
||||
|
||||
this._inputElement.value = this.text;
|
||||
}
|
||||
|
||||
private bindInputEvents(): void {
|
||||
if (!this._inputElement) return;
|
||||
|
||||
this._inputElement.addEventListener('input', () => {
|
||||
this.text = this._inputElement?.value || '';
|
||||
this.emit('input');
|
||||
});
|
||||
|
||||
this._inputElement.addEventListener('focus', () => {
|
||||
this._hasFocus = true;
|
||||
if (this._inputElement) {
|
||||
this._inputElement.style.opacity = '1';
|
||||
}
|
||||
this.emit('focus');
|
||||
});
|
||||
|
||||
this._inputElement.addEventListener('blur', () => {
|
||||
this._hasFocus = false;
|
||||
if (this._inputElement) {
|
||||
this._inputElement.style.opacity = '0';
|
||||
}
|
||||
this.emit('blur');
|
||||
});
|
||||
|
||||
this._inputElement.addEventListener('keydown', (e: Event) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter' && !this._multiline) {
|
||||
this.emit('submit');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update input element position based on display object position
|
||||
* 根据显示对象位置更新输入元素位置
|
||||
*/
|
||||
public updateInputPosition(globalX: number, globalY: number): void {
|
||||
if (this._inputElement) {
|
||||
this._inputElement.style.left = `${globalX}px`;
|
||||
this._inputElement.style.top = `${globalY}px`;
|
||||
this._inputElement.style.width = `${this.width}px`;
|
||||
this._inputElement.style.height = `${this.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.destroyInputElement();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
420
packages/rendering/fairygui/src/display/MovieClip.ts
Normal file
420
packages/rendering/fairygui/src/display/MovieClip.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
import { Image } from './Image';
|
||||
import { Timer } from '../core/Timer';
|
||||
import { FGUIEvents } from '../events/Events';
|
||||
import type { IRenderCollector } from '../render/IRenderCollector';
|
||||
|
||||
/**
|
||||
* Frame data for movie clip animation
|
||||
* 动画帧数据
|
||||
*/
|
||||
export interface IFrame {
|
||||
/** Additional delay for this frame | 该帧额外延迟 */
|
||||
addDelay: number;
|
||||
/** Texture ID for this frame | 该帧的纹理 ID */
|
||||
texture?: string | number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple callback handler
|
||||
* 简单回调处理器
|
||||
*/
|
||||
export type SimpleHandler = (() => void) | { run: () => void };
|
||||
|
||||
/**
|
||||
* MovieClip
|
||||
*
|
||||
* Animated sprite display object with frame-based animation.
|
||||
*
|
||||
* 基于帧的动画精灵显示对象
|
||||
*
|
||||
* Features:
|
||||
* - Frame-by-frame animation
|
||||
* - Swing (ping-pong) mode
|
||||
* - Time scale control
|
||||
* - Play range and loop control
|
||||
*/
|
||||
export class MovieClip extends Image {
|
||||
/** Frame interval in milliseconds | 帧间隔(毫秒) */
|
||||
public interval: number = 0;
|
||||
|
||||
/** Swing mode (ping-pong) | 摆动模式 */
|
||||
public swing: boolean = false;
|
||||
|
||||
/** Delay between loops | 循环间延迟 */
|
||||
public repeatDelay: number = 0;
|
||||
|
||||
/** Time scale multiplier | 时间缩放 */
|
||||
public timeScale: number = 1;
|
||||
|
||||
private _playing: boolean = true;
|
||||
private _frameCount: number = 0;
|
||||
private _frames: IFrame[] = [];
|
||||
private _frame: number = 0;
|
||||
private _start: number = 0;
|
||||
private _end: number = 0;
|
||||
private _times: number = 0;
|
||||
private _endAt: number = 0;
|
||||
private _status: number = 0; // 0-none, 1-next loop, 2-ending, 3-ended
|
||||
|
||||
private _frameElapsed: number = 0;
|
||||
private _reversed: boolean = false;
|
||||
private _repeatedCount: number = 0;
|
||||
private _endHandler: SimpleHandler | null = null;
|
||||
private _isOnStage: boolean = false;
|
||||
private _lastTime: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.touchable = false;
|
||||
this.setPlaySettings();
|
||||
|
||||
// Subscribe to stage lifecycle events
|
||||
// 订阅舞台生命周期事件
|
||||
this.on(FGUIEvents.ADDED_TO_STAGE, this.onAddToStage, this);
|
||||
this.on(FGUIEvents.REMOVED_FROM_STAGE, this.onRemoveFromStage, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get animation frames
|
||||
* 获取动画帧
|
||||
*/
|
||||
public get frames(): IFrame[] {
|
||||
return this._frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set animation frames
|
||||
* 设置动画帧
|
||||
*/
|
||||
public set frames(value: IFrame[]) {
|
||||
this._frames = value;
|
||||
this.scaleByTile = false;
|
||||
this.scale9Grid = null;
|
||||
|
||||
if (this._frames && this._frames.length > 0) {
|
||||
this._frameCount = this._frames.length;
|
||||
|
||||
if (this._end === -1 || this._end > this._frameCount - 1) {
|
||||
this._end = this._frameCount - 1;
|
||||
}
|
||||
if (this._endAt === -1 || this._endAt > this._frameCount - 1) {
|
||||
this._endAt = this._frameCount - 1;
|
||||
}
|
||||
if (this._frame < 0 || this._frame > this._frameCount - 1) {
|
||||
this._frame = this._frameCount - 1;
|
||||
}
|
||||
|
||||
this._frameElapsed = 0;
|
||||
this._repeatedCount = 0;
|
||||
this._reversed = false;
|
||||
} else {
|
||||
this._frameCount = 0;
|
||||
}
|
||||
|
||||
this.drawFrame();
|
||||
this.checkTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get frame count
|
||||
* 获取帧数
|
||||
*/
|
||||
public get frameCount(): number {
|
||||
return this._frameCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current frame index
|
||||
* 获取当前帧索引
|
||||
*/
|
||||
public get frame(): number {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current frame index
|
||||
* 设置当前帧索引
|
||||
*/
|
||||
public set frame(value: number) {
|
||||
if (this._frame !== value) {
|
||||
if (this._frames && value >= this._frameCount) {
|
||||
value = this._frameCount - 1;
|
||||
}
|
||||
|
||||
this._frame = value;
|
||||
this._frameElapsed = 0;
|
||||
this.drawFrame();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playing state
|
||||
* 获取播放状态
|
||||
*/
|
||||
public get playing(): boolean {
|
||||
return this._playing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set playing state
|
||||
* 设置播放状态
|
||||
*/
|
||||
public set playing(value: boolean) {
|
||||
if (this._playing !== value) {
|
||||
this._playing = value;
|
||||
this.checkTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind to first frame
|
||||
* 倒回到第一帧
|
||||
*/
|
||||
public rewind(): void {
|
||||
this._frame = 0;
|
||||
this._frameElapsed = 0;
|
||||
this._reversed = false;
|
||||
this._repeatedCount = 0;
|
||||
|
||||
this.drawFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync status from another MovieClip
|
||||
* 从另一个 MovieClip 同步状态
|
||||
*/
|
||||
public syncStatus(anotherMc: MovieClip): void {
|
||||
this._frame = anotherMc._frame;
|
||||
this._frameElapsed = anotherMc._frameElapsed;
|
||||
this._reversed = anotherMc._reversed;
|
||||
this._repeatedCount = anotherMc._repeatedCount;
|
||||
|
||||
this.drawFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance animation by time
|
||||
* 推进动画时间
|
||||
*
|
||||
* @param timeInMilliseconds Time to advance | 推进时间(毫秒)
|
||||
*/
|
||||
public advance(timeInMilliseconds: number): void {
|
||||
const beginFrame = this._frame;
|
||||
const beginReversed = this._reversed;
|
||||
const backupTime = timeInMilliseconds;
|
||||
|
||||
while (true) {
|
||||
let tt = this.interval + this._frames[this._frame].addDelay;
|
||||
if (this._frame === 0 && this._repeatedCount > 0) {
|
||||
tt += this.repeatDelay;
|
||||
}
|
||||
if (timeInMilliseconds < tt) {
|
||||
this._frameElapsed = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
timeInMilliseconds -= tt;
|
||||
|
||||
if (this.swing) {
|
||||
if (this._reversed) {
|
||||
this._frame--;
|
||||
if (this._frame <= 0) {
|
||||
this._frame = 0;
|
||||
this._repeatedCount++;
|
||||
this._reversed = !this._reversed;
|
||||
}
|
||||
} else {
|
||||
this._frame++;
|
||||
if (this._frame > this._frameCount - 1) {
|
||||
this._frame = Math.max(0, this._frameCount - 2);
|
||||
this._repeatedCount++;
|
||||
this._reversed = !this._reversed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._frame++;
|
||||
if (this._frame > this._frameCount - 1) {
|
||||
this._frame = 0;
|
||||
this._repeatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Completed one round
|
||||
if (this._frame === beginFrame && this._reversed === beginReversed) {
|
||||
const roundTime = backupTime - timeInMilliseconds;
|
||||
timeInMilliseconds -= Math.floor(timeInMilliseconds / roundTime) * roundTime;
|
||||
}
|
||||
}
|
||||
|
||||
this.drawFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set play settings
|
||||
* 设置播放参数
|
||||
*
|
||||
* @param start Start frame | 开始帧
|
||||
* @param end End frame (-1 for last) | 结束帧(-1 为最后一帧)
|
||||
* @param times Loop times (0 for infinite) | 循环次数(0 为无限)
|
||||
* @param endAt Stop at frame (-1 for end) | 停止帧(-1 为结束帧)
|
||||
* @param endHandler Callback on end | 结束回调
|
||||
*/
|
||||
public setPlaySettings(
|
||||
start: number = 0,
|
||||
end: number = -1,
|
||||
times: number = 0,
|
||||
endAt: number = -1,
|
||||
endHandler: SimpleHandler | null = null
|
||||
): void {
|
||||
this._start = start;
|
||||
this._end = end;
|
||||
if (this._end === -1 || this._end > this._frameCount - 1) {
|
||||
this._end = this._frameCount - 1;
|
||||
}
|
||||
this._times = times;
|
||||
this._endAt = endAt;
|
||||
if (this._endAt === -1) {
|
||||
this._endAt = this._end;
|
||||
}
|
||||
this._status = 0;
|
||||
this._endHandler = endHandler;
|
||||
this.frame = start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when added to stage
|
||||
* 添加到舞台时调用
|
||||
*/
|
||||
public onAddToStage(): void {
|
||||
this._isOnStage = true;
|
||||
this._lastTime = Timer.time;
|
||||
this.checkTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when removed from stage
|
||||
* 从舞台移除时调用
|
||||
*/
|
||||
public onRemoveFromStage(): void {
|
||||
this._isOnStage = false;
|
||||
this.checkTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update animation (called each frame)
|
||||
* 更新动画(每帧调用)
|
||||
*/
|
||||
public update(): void {
|
||||
if (!this._playing || this._frameCount === 0 || this._status === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Timer.time;
|
||||
let dt = currentTime - this._lastTime;
|
||||
this._lastTime = currentTime;
|
||||
|
||||
if (dt > 100) {
|
||||
dt = 100;
|
||||
}
|
||||
if (this.timeScale !== 1) {
|
||||
dt *= this.timeScale;
|
||||
}
|
||||
|
||||
this._frameElapsed += dt;
|
||||
let tt = this.interval + this._frames[this._frame].addDelay;
|
||||
if (this._frame === 0 && this._repeatedCount > 0) {
|
||||
tt += this.repeatDelay;
|
||||
}
|
||||
if (this._frameElapsed < tt) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._frameElapsed -= tt;
|
||||
if (this._frameElapsed > this.interval) {
|
||||
this._frameElapsed = this.interval;
|
||||
}
|
||||
|
||||
if (this.swing) {
|
||||
if (this._reversed) {
|
||||
this._frame--;
|
||||
if (this._frame <= 0) {
|
||||
this._frame = 0;
|
||||
this._repeatedCount++;
|
||||
this._reversed = !this._reversed;
|
||||
}
|
||||
} else {
|
||||
this._frame++;
|
||||
if (this._frame > this._frameCount - 1) {
|
||||
this._frame = Math.max(0, this._frameCount - 2);
|
||||
this._repeatedCount++;
|
||||
this._reversed = !this._reversed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._frame++;
|
||||
if (this._frame > this._frameCount - 1) {
|
||||
this._frame = 0;
|
||||
this._repeatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._status === 1) {
|
||||
// New loop
|
||||
this._frame = this._start;
|
||||
this._frameElapsed = 0;
|
||||
this._status = 0;
|
||||
} else if (this._status === 2) {
|
||||
// Ending
|
||||
this._frame = this._endAt;
|
||||
this._frameElapsed = 0;
|
||||
this._status = 3; // Ended
|
||||
|
||||
// Play end callback
|
||||
if (this._endHandler) {
|
||||
const handler = this._endHandler;
|
||||
this._endHandler = null;
|
||||
if (typeof handler === 'function') {
|
||||
handler();
|
||||
} else {
|
||||
handler.run();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this._frame === this._end) {
|
||||
if (this._times > 0) {
|
||||
this._times--;
|
||||
if (this._times === 0) {
|
||||
this._status = 2; // Ending
|
||||
} else {
|
||||
this._status = 1; // New loop
|
||||
}
|
||||
} else {
|
||||
this._status = 1; // New loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.drawFrame();
|
||||
}
|
||||
|
||||
private drawFrame(): void {
|
||||
if (this._frameCount > 0 && this._frame < this._frames.length) {
|
||||
const frame = this._frames[this._frame];
|
||||
this.texture = frame.texture ?? null;
|
||||
} else {
|
||||
this.texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
private checkTimer(): void {
|
||||
if (this._playing && this._frameCount > 0 && this._isOnStage) {
|
||||
Timer.add(this.update, this);
|
||||
} else {
|
||||
Timer.remove(this.update, this);
|
||||
}
|
||||
}
|
||||
|
||||
public collectRenderData(collector: IRenderCollector): void {
|
||||
super.collectRenderData(collector);
|
||||
}
|
||||
}
|
||||
270
packages/rendering/fairygui/src/display/TextField.ts
Normal file
270
packages/rendering/fairygui/src/display/TextField.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { DisplayObject } from './DisplayObject';
|
||||
import { EAutoSizeType, EAlignType, EVertAlignType } from '../core/FieldTypes';
|
||||
import type { IRenderCollector, IRenderPrimitive } from '../render/IRenderCollector';
|
||||
import { ERenderPrimitiveType } from '../render/IRenderCollector';
|
||||
|
||||
/**
|
||||
* TextField
|
||||
*
|
||||
* Display object for rendering text.
|
||||
*
|
||||
* 用于渲染文本的显示对象
|
||||
*/
|
||||
export class TextField extends DisplayObject {
|
||||
|
||||
/** Font name | 字体名称 */
|
||||
public font: string = '';
|
||||
|
||||
/** Font size | 字体大小 */
|
||||
public fontSize: number = 12;
|
||||
|
||||
/** Text color (hex string) | 文本颜色 */
|
||||
public color: string = '#000000';
|
||||
|
||||
/** Horizontal alignment | 水平对齐 */
|
||||
public align: EAlignType = EAlignType.Left;
|
||||
|
||||
/** Vertical alignment | 垂直对齐 */
|
||||
public valign: EVertAlignType = EVertAlignType.Top;
|
||||
|
||||
/** Line spacing | 行间距 */
|
||||
public leading: number = 3;
|
||||
|
||||
/** Letter spacing | 字符间距 */
|
||||
public letterSpacing: number = 0;
|
||||
|
||||
/** Bold | 粗体 */
|
||||
public bold: boolean = false;
|
||||
|
||||
/** Italic | 斜体 */
|
||||
public italic: boolean = false;
|
||||
|
||||
/** Underline | 下划线 */
|
||||
public underline: boolean = false;
|
||||
|
||||
/** Single line | 单行 */
|
||||
public singleLine: boolean = false;
|
||||
|
||||
/** Stroke width | 描边宽度 */
|
||||
public stroke: number = 0;
|
||||
|
||||
/** Stroke color | 描边颜色 */
|
||||
public strokeColor: string = '#000000';
|
||||
|
||||
/** UBB enabled | UBB 标签启用 */
|
||||
public ubbEnabled: boolean = false;
|
||||
|
||||
/** Auto size type | 自动尺寸类型 */
|
||||
public autoSize: EAutoSizeType = EAutoSizeType.Both;
|
||||
|
||||
/** Word wrap | 自动换行 */
|
||||
public wordWrap: boolean = false;
|
||||
|
||||
/** Template variables | 模板变量 */
|
||||
public templateVars: Record<string, string> | null = null;
|
||||
|
||||
/** Text width after layout | 排版后文本宽度 */
|
||||
private _textWidth: number = 0;
|
||||
|
||||
/** Text height after layout | 排版后文本高度 */
|
||||
private _textHeight: number = 0;
|
||||
|
||||
/** Text content changed flag | 文本内容变化标记 */
|
||||
private _textChanged: boolean = true;
|
||||
|
||||
/** Internal text storage | 内部文本存储 */
|
||||
private _text: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set text content
|
||||
* 获取/设置文本内容
|
||||
*/
|
||||
public get text(): string {
|
||||
return this._text;
|
||||
}
|
||||
|
||||
public set text(value: string) {
|
||||
if (this._text !== value) {
|
||||
this._text = value;
|
||||
this._textChanged = true;
|
||||
this.ensureSizeCorrect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text width
|
||||
* 获取文本宽度
|
||||
*/
|
||||
public get textWidth(): number {
|
||||
if (this._textChanged) {
|
||||
this.buildLines();
|
||||
}
|
||||
return this._textWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text height
|
||||
* 获取文本高度
|
||||
*/
|
||||
public get textHeight(): number {
|
||||
if (this._textChanged) {
|
||||
this.buildLines();
|
||||
}
|
||||
return this._textHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure text size is calculated correctly
|
||||
* 确保文本尺寸正确计算
|
||||
*/
|
||||
public ensureSizeCorrect(): void {
|
||||
if (this._textChanged && this.autoSize !== EAutoSizeType.None) {
|
||||
this.buildLines();
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared canvas context for text measurement | 共享的 Canvas 上下文用于文本测量 */
|
||||
private static _measureContext: CanvasRenderingContext2D | null = null;
|
||||
|
||||
/**
|
||||
* Get or create canvas context for text measurement
|
||||
* 获取或创建用于文本测量的 canvas 上下文
|
||||
*/
|
||||
private static getMeasureContext(): CanvasRenderingContext2D {
|
||||
if (!TextField._measureContext) {
|
||||
const canvas = document.createElement('canvas');
|
||||
TextField._measureContext = canvas.getContext('2d')!;
|
||||
}
|
||||
return TextField._measureContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build lines and calculate text dimensions
|
||||
* 构建行信息并计算文本尺寸
|
||||
*
|
||||
* 使用 Canvas 2D measureText 精确测量文本尺寸
|
||||
* Use Canvas 2D measureText for accurate text measurement
|
||||
*/
|
||||
private buildLines(): void {
|
||||
this._textChanged = false;
|
||||
|
||||
if (!this._text) {
|
||||
this._textWidth = 0;
|
||||
this._textHeight = this.fontSize;
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = TextField.getMeasureContext();
|
||||
|
||||
// 设置字体样式
|
||||
// Set font style
|
||||
const fontStyle = this.italic ? 'italic ' : '';
|
||||
const fontWeight = this.bold ? 'bold ' : '';
|
||||
const fontFamily = this.font || 'Arial, sans-serif';
|
||||
ctx.font = `${fontStyle}${fontWeight}${this.fontSize}px ${fontFamily}`;
|
||||
|
||||
const lines = this._text.split('\n');
|
||||
const lineHeight = this.fontSize + this.leading;
|
||||
|
||||
let maxWidth = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// 使用 canvas measureText 获取精确宽度
|
||||
// Use canvas measureText for accurate width
|
||||
let lineWidth = ctx.measureText(line).width;
|
||||
|
||||
// 添加字符间距
|
||||
// Add letter spacing
|
||||
if (this.letterSpacing !== 0 && line.length > 1) {
|
||||
lineWidth += this.letterSpacing * (line.length - 1);
|
||||
}
|
||||
|
||||
if (lineWidth > maxWidth) {
|
||||
maxWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// 单行模式只取第一行
|
||||
// Single line mode only takes first line
|
||||
if (this.singleLine) {
|
||||
this._textWidth = maxWidth;
|
||||
this._textHeight = lineHeight;
|
||||
} else {
|
||||
this._textWidth = maxWidth;
|
||||
this._textHeight = lines.length * lineHeight;
|
||||
}
|
||||
|
||||
// 添加 gutter 边距(参考 Unity 实现的 GUTTER_X = 2, GUTTER_Y = 2)
|
||||
// Add gutter padding (refer to Unity implementation: GUTTER_X = 2, GUTTER_Y = 2)
|
||||
this._textWidth += 4;
|
||||
this._textHeight += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set variable
|
||||
* 设置变量
|
||||
*/
|
||||
public setVar(name: string, value: string): void {
|
||||
if (!this.templateVars) {
|
||||
this.templateVars = {};
|
||||
}
|
||||
this.templateVars[name] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse color string to packed u32 (0xRRGGBBAA format)
|
||||
* 解析颜色字符串为打包的 u32(0xRRGGBBAA 格式)
|
||||
*/
|
||||
private parseColor(color: string): number {
|
||||
if (color.startsWith('#')) {
|
||||
const hex = color.slice(1);
|
||||
if (hex.length === 6) {
|
||||
return ((parseInt(hex, 16) << 8) | 0xFF) >>> 0;
|
||||
} else if (hex.length === 8) {
|
||||
return parseInt(hex, 16) >>> 0;
|
||||
}
|
||||
}
|
||||
return 0x000000FF;
|
||||
}
|
||||
|
||||
public collectRenderData(collector: IRenderCollector): void {
|
||||
if (!this._visible || this._alpha <= 0 || !this._text) return;
|
||||
|
||||
this.updateTransform();
|
||||
|
||||
const primitive: IRenderPrimitive = {
|
||||
type: ERenderPrimitiveType.Text,
|
||||
sortOrder: 0,
|
||||
worldMatrix: this._worldMatrix,
|
||||
width: this._width,
|
||||
height: this._height,
|
||||
alpha: this._worldAlpha,
|
||||
grayed: this._grayed,
|
||||
text: this._text,
|
||||
font: this.font,
|
||||
fontSize: this.fontSize,
|
||||
color: this.parseColor(this.color),
|
||||
align: this.align,
|
||||
valign: this.valign,
|
||||
leading: this.leading,
|
||||
letterSpacing: this.letterSpacing,
|
||||
bold: this.bold,
|
||||
italic: this.italic,
|
||||
underline: this.underline,
|
||||
singleLine: this.singleLine,
|
||||
wordWrap: this.wordWrap,
|
||||
clipRect: collector.getCurrentClipRect() || undefined
|
||||
};
|
||||
|
||||
if (this.stroke > 0) {
|
||||
primitive.stroke = this.stroke;
|
||||
primitive.strokeColor = this.parseColor(this.strokeColor);
|
||||
}
|
||||
|
||||
collector.addPrimitive(primitive);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user