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

* refactor: reorganize package structure and decouple framework packages

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

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

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

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

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

* docs: update README to focus on framework over engine

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

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

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

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

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

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

* fix: update network tsconfig references to new paths

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

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

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

View File

@@ -0,0 +1,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);
}
}
}

View 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();
}
}

View 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)
* 解析颜色字符串为打包的 u320xRRGGBBAA 格式)
*/
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);
}
}

View 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)
* 解析颜色字符串为打包的 u320xRRGGBBAA 格式)
*/
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);
}
}

View 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();
}
}

View 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);
}
}

View 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)
* 解析颜色字符串为打包的 u320xRRGGBBAA 格式)
*/
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);
}
}