Files
esengine/packages/rendering/fairygui/src/render/FGUIRenderDataProvider.ts
YHH 155411e743 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
2025-12-26 14:50:35 +08:00

1184 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* FGUIRenderDataProvider
*
* Converts FairyGUI render primitives to engine render data format.
* Implements IUIRenderDataProvider for integration with the engine render system.
*
* 将 FairyGUI 渲染图元转换为引擎渲染数据格式
* 实现 IUIRenderDataProvider 以与引擎渲染系统集成
*/
import type { IRenderPrimitive } from './IRenderCollector';
import { ERenderPrimitiveType } from './IRenderCollector';
import type { RenderCollector } from './RenderCollector';
import { Stage } from '../core/Stage';
import { getMSDFFontManager, MSDFFont } from '../text/MSDFFont';
import { getDynamicFontManager } from '../text/DynamicFont';
import { layoutText } from '../text/TextLayout';
import { createTextBatch } from '../text/TextBatch';
import { EAlignType, EVertAlignType, EGraphType } from '../core/FieldTypes';
import { GraphMeshGenerator, type GraphMeshData } from './GraphMeshGenerator';
/**
* Engine render data format (matches ProviderRenderData from ecs-engine-bindgen)
* 引擎渲染数据格式(匹配 ecs-engine-bindgen 的 ProviderRenderData
*/
export interface IEngineRenderData {
transforms: Float32Array;
textureIds: Uint32Array;
uvs: Float32Array;
colors: Uint32Array;
tileCount: number;
sortingLayer: string;
orderInLayer: number;
textureGuid?: string;
bScreenSpace?: boolean;
clipRect?: { x: number; y: number; width: number; height: number };
}
/**
* MSDF text render data format
* MSDF 文本渲染数据格式
*/
export interface ITextRenderData {
/** Vertex positions [x, y, ...] for each vertex (4 per glyph) | 顶点位置 */
positions: Float32Array;
/** Texture coordinates [u, v, ...] for each vertex | 纹理坐标 */
texCoords: Float32Array;
/** Colors [r, g, b, a, ...] for each vertex | 颜色 */
colors: Float32Array;
/** Outline colors [r, g, b, a, ...] for each vertex | 描边颜色 */
outlineColors: Float32Array;
/** Outline widths for each vertex | 描边宽度 */
outlineWidths: Float32Array;
/** Font texture ID | 字体纹理 ID */
textureId: number;
/** Pixel range for MSDF shader | 着色器像素范围 */
pxRange: number;
/** Glyph count | 字形数量 */
glyphCount: number;
}
/**
* Mesh render data format for arbitrary 2D geometry
* 用于任意 2D 几何体的网格渲染数据格式
*/
export interface IMeshRenderData {
/** Vertex positions [x, y, ...] | 顶点位置 */
positions: Float32Array;
/** Texture coordinates [u, v, ...] | 纹理坐标 */
uvs: Float32Array;
/** Vertex colors (packed RGBA) | 顶点颜色 */
colors: Uint32Array;
/** Triangle indices | 三角形索引 */
indices: Uint16Array;
/** Texture ID (0 = white pixel) | 纹理 ID */
textureId: number;
}
/**
* Render data provider interface (matches IRenderDataProvider from ecs-engine-bindgen)
* 渲染数据提供者接口
*/
export interface IFGUIRenderDataProvider {
getRenderData(): readonly IEngineRenderData[];
getTextRenderData(): readonly ITextRenderData[];
getMeshRenderData(): readonly IMeshRenderData[];
}
/**
* Texture resolver callback
* 纹理解析回调
*/
export type TextureResolverFn = (textureId: string | number) => number;
/**
* FGUIRenderDataProvider
*
* Converts FairyGUI render collector data to engine render data format.
*
* 将 FairyGUI 渲染收集器数据转换为引擎渲染数据格式
*/
export class FGUIRenderDataProvider implements IFGUIRenderDataProvider {
/** Sorting layer for UI | UI 排序层 */
private _sortingLayer: string = 'UI';
/** Order within the sorting layer | 层内排序顺序 */
private _orderInLayer: number = 0;
/** Render collector reference | 渲染收集器引用 */
private _collector: RenderCollector | null = null;
/** Texture resolver function | 纹理解析函数 */
private _textureResolver: TextureResolverFn | null = null;
/** Cached render data | 缓存的渲染数据 */
private _cachedData: IEngineRenderData[] = [];
/** Cached text render data | 缓存的文本渲染数据 */
private _cachedTextData: ITextRenderData[] = [];
/** Cached mesh render data | 缓存的网格渲染数据 */
private _cachedMeshData: IMeshRenderData[] = [];
/** Default texture ID when texture not found | 找不到纹理时的默认纹理 ID */
private _defaultTextureId: number = 0;
/** Canvas width for coordinate conversion | 画布宽度,用于坐标转换 */
private _canvasWidth: number = 0;
/** Canvas height for coordinate conversion | 画布高度,用于坐标转换 */
private _canvasHeight: number = 0;
/**
* Set the render collector
* 设置渲染收集器
*/
public setCollector(collector: RenderCollector): void {
this._collector = collector;
}
/**
* Set sorting layer and order
* 设置排序层和顺序
*/
public setSorting(layer: string, order: number): void {
this._sortingLayer = layer;
this._orderInLayer = order;
}
/**
* Set texture resolver function
* 设置纹理解析函数
*/
public setTextureResolver(resolver: TextureResolverFn): void {
this._textureResolver = resolver;
}
/**
* Set default texture ID
* 设置默认纹理 ID
*/
public setDefaultTextureId(id: number): void {
this._defaultTextureId = id;
}
/**
* Set canvas size for coordinate conversion
* 设置画布尺寸,用于坐标转换
*
* FGUI uses top-left origin with Y-down coordinate system.
* Engine uses center origin with visible area [-width/2, width/2] and [-height/2, height/2].
*
* FGUI 使用左上角为原点、Y 轴向下的坐标系统。
* 引擎使用中心为原点,可见区域为 [-width/2, width/2] 和 [-height/2, height/2]。
*/
public setCanvasSize(width: number, height: number): void {
this._canvasWidth = width;
this._canvasHeight = height;
}
/**
* Get effective canvas size for coordinate conversion
* Uses Stage.designWidth/designHeight as fallback if not explicitly set
*
* 获取坐标转换的有效画布尺寸
* 如果未显式设置,则使用 Stage.designWidth/designHeight 作为回退
*/
private getEffectiveCanvasSize(): { width: number; height: number } {
if (this._canvasWidth > 0 && this._canvasHeight > 0) {
return { width: this._canvasWidth, height: this._canvasHeight };
}
// Fallback to Stage design size
// 回退到 Stage 设计尺寸
const stage = Stage.inst;
return {
width: stage.designWidth,
height: stage.designHeight
};
}
/**
* Get render data for the engine
* 获取引擎渲染数据
*/
public getRenderData(): readonly IEngineRenderData[] {
if (!this._collector) {
return [];
}
const primitives = this._collector.getPrimitives();
if (primitives.length === 0) {
return [];
}
this._cachedData.length = 0;
// Group primitives by texture for batching
const batches = this.groupByTexture(primitives);
for (const [_textureKey, batch] of batches) {
const renderData = this.convertBatch(batch);
if (renderData) {
this._cachedData.push(renderData);
}
}
return this._cachedData;
}
/**
* Get text render data for the engine
* 获取引擎文本渲染数据
*
* Note: MSDF text rendering requires font atlas to be loaded.
* Text primitives are converted to MSDF glyph batches.
*
* 注意MSDF 文本渲染需要加载字体图集。
* 文本图元被转换为 MSDF 字形批次。
*/
public getTextRenderData(): readonly ITextRenderData[] {
if (!this._collector) {
return [];
}
const primitives = this._collector.getPrimitives();
this._cachedTextData.length = 0;
// Get canvas size for coordinate conversion
const canvasSize = this.getEffectiveCanvasSize();
const halfWidth = canvasSize.width / 2;
const halfHeight = canvasSize.height / 2;
// Get font managers
const msdfFontManager = getMSDFFontManager();
const dynamicFontManager = getDynamicFontManager();
// Group text primitives by font for batching
const textBatches = new Map<string, IRenderPrimitive[]>();
for (const primitive of primitives) {
if (primitive.type !== ERenderPrimitiveType.Text) continue;
if (!primitive.text) continue;
const fontName = primitive.font || '';
let batch = textBatches.get(fontName);
if (!batch) {
batch = [];
textBatches.set(fontName, batch);
}
batch.push(primitive);
}
// Convert each font batch to render data
for (const [fontName, batch] of textBatches) {
// Try MSDF font first
let font: MSDFFont | undefined = msdfFontManager.getFont(fontName);
// Try dynamic font if MSDF not available
if (!font) {
// Try exact name first, then fallback to 'default'
let dynamicFont = dynamicFontManager.getFont(fontName);
if (!dynamicFont && fontName !== 'default') {
dynamicFont = dynamicFontManager.getFont('default');
}
if (dynamicFont) {
// Request characters for all text in this batch
for (const primitive of batch) {
if (primitive.text) {
dynamicFont.requestCharacters(primitive.text);
}
}
// Register as MSDF font if not already
font = msdfFontManager.getFont(dynamicFont.name);
if (!font) {
font = dynamicFont.registerAsMSDFFont();
}
}
}
if (!font) {
// Skip if no font available - will fall back to DOM renderer
continue;
}
for (const primitive of batch) {
const textRenderData = this.convertTextPrimitive(
primitive,
font,
halfWidth,
halfHeight
);
if (textRenderData) {
this._cachedTextData.push(textRenderData);
}
}
}
return this._cachedTextData;
}
/**
* Get mesh render data for the engine
* 获取引擎网格渲染数据
*
* Used for rendering complex shapes like ellipses, polygons, etc.
* 用于渲染椭圆、多边形等复杂形状
*/
public getMeshRenderData(): readonly IMeshRenderData[] {
if (!this._collector) {
return [];
}
const primitives = this._collector.getPrimitives();
this._cachedMeshData.length = 0;
// Get canvas size for coordinate conversion
const canvasSize = this.getEffectiveCanvasSize();
const halfWidth = canvasSize.width / 2;
const halfHeight = canvasSize.height / 2;
for (const primitive of primitives) {
if (primitive.type !== ERenderPrimitiveType.Graph) continue;
const graphType = primitive.graphType;
// Skip simple rectangles - they're handled by getRenderData using white pixel
if (graphType === EGraphType.Rect) continue;
// Generate mesh for complex shapes
const meshData = this.generateGraphMesh(primitive, halfWidth, halfHeight);
if (meshData) {
this._cachedMeshData.push(meshData);
}
// Generate outline mesh if needed
if (primitive.lineSize && primitive.lineSize > 0) {
const outlineMesh = this.generateGraphOutlineMesh(primitive, halfWidth, halfHeight);
if (outlineMesh) {
this._cachedMeshData.push(outlineMesh);
}
}
}
return this._cachedMeshData;
}
/**
* Generate mesh data for a graph primitive
* 为图形图元生成网格数据
*/
private generateGraphMesh(
primitive: IRenderPrimitive,
halfWidth: number,
halfHeight: number
): IMeshRenderData | null {
const graphType = primitive.graphType;
const width = primitive.width;
const height = primitive.height;
const fillColor = primitive.fillColor ?? 0xFFFFFFFF;
let meshData: GraphMeshData | null = null;
switch (graphType) {
case EGraphType.Ellipse:
meshData = GraphMeshGenerator.generateEllipse(width, height, fillColor);
break;
case EGraphType.Polygon:
if (primitive.polygonPoints) {
meshData = GraphMeshGenerator.generatePolygon(
primitive.polygonPoints,
width,
height,
fillColor
);
}
break;
case EGraphType.RegularPolygon:
// Generate regular polygon points
const sides = primitive.sides ?? 6;
const points = this.generateRegularPolygonPoints(width, height, sides);
meshData = GraphMeshGenerator.generatePolygon(points, width, height, fillColor);
break;
}
if (!meshData || meshData.positions.length === 0) {
return null;
}
// Get world position from matrix
const m = primitive.worldMatrix;
const baseX = m ? m[4] : 0;
const baseY = m ? m[5] : 0;
// Convert FGUI coordinates to engine coordinates
const engineBaseX = baseX - halfWidth;
const engineBaseY = halfHeight - baseY;
// Apply coordinate transformation to positions
const positions = new Float32Array(meshData.positions.length);
for (let i = 0; i < meshData.positions.length; i += 2) {
positions[i] = meshData.positions[i] + engineBaseX;
// Flip Y for engine coordinate system
positions[i + 1] = engineBaseY - meshData.positions[i + 1];
}
return {
positions,
uvs: new Float32Array(meshData.uvs),
colors: new Uint32Array(meshData.colors),
indices: new Uint16Array(meshData.indices),
textureId: 0 // White pixel texture
};
}
/**
* Generate outline mesh data for a graph primitive
* 为图形图元生成轮廓网格数据
*/
private generateGraphOutlineMesh(
primitive: IRenderPrimitive,
halfWidth: number,
halfHeight: number
): IMeshRenderData | null {
const graphType = primitive.graphType;
const width = primitive.width;
const height = primitive.height;
const lineWidth = primitive.lineSize ?? 1;
const lineColor = primitive.lineColor ?? 0x000000FF;
let meshData: GraphMeshData | null = null;
switch (graphType) {
case EGraphType.Ellipse:
meshData = GraphMeshGenerator.generateEllipseOutline(width, height, lineWidth, lineColor);
break;
case EGraphType.Polygon:
if (primitive.polygonPoints) {
meshData = GraphMeshGenerator.generateOutline(
primitive.polygonPoints,
lineWidth,
lineColor,
true
);
}
break;
case EGraphType.RegularPolygon:
const sides = primitive.sides ?? 6;
const points = this.generateRegularPolygonPoints(width, height, sides);
meshData = GraphMeshGenerator.generateOutline(points, lineWidth, lineColor, true);
break;
}
if (!meshData || meshData.positions.length === 0) {
return null;
}
// Get world position from matrix
const m = primitive.worldMatrix;
const baseX = m ? m[4] : 0;
const baseY = m ? m[5] : 0;
// Convert FGUI coordinates to engine coordinates
const engineBaseX = baseX - halfWidth;
const engineBaseY = halfHeight - baseY;
// Apply coordinate transformation to positions
const positions = new Float32Array(meshData.positions.length);
for (let i = 0; i < meshData.positions.length; i += 2) {
positions[i] = meshData.positions[i] + engineBaseX;
positions[i + 1] = engineBaseY - meshData.positions[i + 1];
}
return {
positions,
uvs: new Float32Array(meshData.uvs),
colors: new Uint32Array(meshData.colors),
indices: new Uint16Array(meshData.indices),
textureId: 0 // White pixel texture
};
}
/**
* Generate points for a regular polygon
* 生成正多边形的点
*/
private generateRegularPolygonPoints(width: number, height: number, sides: number): number[] {
const points: number[] = [];
const centerX = width / 2;
const centerY = height / 2;
const radiusX = width / 2;
const radiusY = height / 2;
const angleStep = (Math.PI * 2) / sides;
const startAngle = -Math.PI / 2; // Start from top
for (let i = 0; i < sides; i++) {
const angle = startAngle + angleStep * i;
points.push(
centerX + Math.cos(angle) * radiusX,
centerY + Math.sin(angle) * radiusY
);
}
return points;
}
/**
* Convert a text primitive to MSDF render data
* 将文本图元转换为 MSDF 渲染数据
*/
private convertTextPrimitive(
primitive: IRenderPrimitive,
font: MSDFFont,
halfWidth: number,
halfHeight: number
): ITextRenderData | null {
const text = primitive.text;
if (!text) return null;
// Get text properties
const fontSize = primitive.fontSize ?? 12;
const maxWidth = primitive.width;
const maxHeight = primitive.height;
// Convert alignment types
let align = EAlignType.Left;
if (primitive.align !== undefined) {
align = typeof primitive.align === 'string'
? this.parseHAlign(primitive.align)
: primitive.align;
} else if (primitive.textAlign !== undefined) {
align = this.parseHAlign(primitive.textAlign as string);
}
let valign = EVertAlignType.Top;
if (primitive.valign !== undefined) {
valign = typeof primitive.valign === 'string'
? this.parseVAlign(primitive.valign)
: primitive.valign;
} else if (primitive.textVAlign !== undefined) {
valign = this.parseVAlign(primitive.textVAlign as string);
}
// Layout text
const layoutResult = layoutText({
font,
text,
fontSize,
maxWidth,
maxHeight,
align,
valign,
lineHeight: 1.2,
letterSpacing: primitive.letterSpacing ?? 0,
wordWrap: primitive.wordWrap ?? false,
singleLine: primitive.singleLine ?? false
});
if (layoutResult.glyphs.length === 0) {
return null;
}
// Get world position from matrix
const m = primitive.worldMatrix;
const baseX = m ? m[4] : (primitive.x ?? 0);
const baseY = m ? m[5] : (primitive.y ?? 0);
// Convert FGUI coordinates to engine coordinates
// FGUI: top-left origin, Y-down
// Engine: center origin, Y-up
const engineBaseX = baseX - halfWidth;
const engineBaseY = halfHeight - baseY;
// Get text color
const textColor = primitive.textColor ?? primitive.color ?? 0xFFFFFFFF;
// Create text batch with coordinate offset
const batchData = createTextBatch(
layoutResult.glyphs,
font.textureId,
font.pxRange,
{
color: textColor,
alpha: primitive.alpha,
outlineColor: primitive.outlineColor,
outlineWidth: primitive.outlineWidth ?? primitive.stroke,
offsetX: engineBaseX,
offsetY: engineBaseY
}
);
// Convert Y coordinates (flip for engine coordinate system)
// The batch was created in FGUI Y-down space, need to flip
this.flipTextYCoordinates(batchData.positions, engineBaseY);
return {
positions: batchData.positions,
texCoords: batchData.texCoords,
colors: batchData.colors,
outlineColors: batchData.outlineColors,
outlineWidths: batchData.outlineWidths,
textureId: batchData.textureId,
pxRange: batchData.pxRange,
glyphCount: batchData.glyphCount
};
}
/**
* Flip Y coordinates for engine coordinate system
* 翻转 Y 坐标以适配引擎坐标系
*/
private flipTextYCoordinates(positions: Float32Array, baseY: number): void {
// Each glyph has 4 vertices, each vertex has 2 floats (x, y)
// Flip Y: newY = baseY - (y - baseY) = 2 * baseY - y
// But since we already offset, we just negate the Y relative to base
for (let i = 1; i < positions.length; i += 2) {
// Y is at odd indices
// Convert from Y-down to Y-up by negating the local Y offset
positions[i] = baseY - (positions[i] - baseY);
}
}
/**
* Parse horizontal alignment string
* 解析水平对齐字符串
*/
private parseHAlign(align: string): EAlignType {
switch (align.toLowerCase()) {
case 'center': return EAlignType.Center;
case 'right': return EAlignType.Right;
default: return EAlignType.Left;
}
}
/**
* Parse vertical alignment string
* 解析垂直对齐字符串
*/
private parseVAlign(align: string): EVertAlignType {
switch (align.toLowerCase()) {
case 'middle': return EVertAlignType.Middle;
case 'bottom': return EVertAlignType.Bottom;
default: return EVertAlignType.Top;
}
}
/**
* Group primitives by texture for batching
* 按纹理分组图元以进行批处理
*/
private groupByTexture(primitives: readonly IRenderPrimitive[]): Map<string | number | undefined, IRenderPrimitive[]> {
const batches = new Map<string | number | undefined, IRenderPrimitive[]>();
for (const primitive of primitives) {
// Handle image primitives
// 处理图像图元
if (primitive.type === ERenderPrimitiveType.Image) {
const key = primitive.textureId ?? 'none';
let batch = batches.get(key);
if (!batch) {
batch = [];
batches.set(key, batch);
}
// Expand nine-slice primitives into 9 sub-primitives
// 将九宫格图元展开为 9 个子图元
if (primitive.scale9Grid) {
const subPrimitives = this.expandScale9Grid(primitive);
batch.push(...subPrimitives);
} else {
batch.push(primitive);
}
}
// Handle graph primitives (rect, ellipse, polygon)
// 处理图形图元(矩形、椭圆、多边形)
else if (primitive.type === ERenderPrimitiveType.Graph) {
// Use special key for graph primitives (white pixel texture)
// 为图形图元使用特殊键(白色像素纹理)
const key = '__fgui_white_pixel__';
let batch = batches.get(key);
if (!batch) {
batch = [];
batches.set(key, batch);
}
// Convert graph fill to primitive for rendering
// 将图形填充转换为图元进行渲染
const fillPrimitive = this.convertGraphToPrimitive(primitive);
if (fillPrimitive) {
batch.push(fillPrimitive);
}
// Generate outline (stroke) if lineSize > 0
// 如果 lineSize > 0生成轮廓线描边
if (primitive.lineSize && primitive.lineSize > 0) {
const outlinePrimitives = this.convertGraphOutlineToPrimitive(primitive);
batch.push(...outlinePrimitives);
}
}
}
return batches;
}
/**
* Convert a graph primitive to image-like primitives for rendering
* 将图形图元转换为类似图像的图元进行渲染
*
* Currently supports only filled rectangles using a white pixel texture.
* Ellipse, polygon, and rounded rectangles require mesh rendering support
* which needs to be added to the engine.
*
* 目前只支持使用白色像素纹理的填充矩形
* 椭圆、多边形和圆角矩形需要网格渲染支持,需要在引擎中添加
*/
private convertGraphToPrimitive(primitive: IRenderPrimitive): IRenderPrimitive | null {
const graphType = primitive.graphType;
// Only support simple filled rectangles for now
// Other shapes (ellipse, polygon, rounded rect) need mesh rendering support in the engine
// 目前只支持简单填充矩形
// 其他形状(椭圆、多边形、圆角矩形)需要引擎中的网格渲染支持
if (graphType !== EGraphType.Rect) {
// TODO: Add mesh batch rendering to engine for complex shapes
// TODO: 在引擎中添加网格批处理渲染以支持复杂形状
return null;
}
// Simple filled rectangle - use white pixel texture with fillColor
// 简单填充矩形 - 使用白色像素纹理配合 fillColor
return {
type: ERenderPrimitiveType.Image,
sortOrder: primitive.sortOrder,
worldMatrix: primitive.worldMatrix,
width: primitive.width,
height: primitive.height,
alpha: primitive.alpha,
grayed: primitive.grayed,
blendMode: primitive.blendMode,
clipRect: primitive.clipRect,
textureId: '__fgui_white_pixel__',
uvRect: [0, 0, 1, 1],
color: primitive.fillColor ?? 0xFFFFFFFF
};
}
/**
* Convert graph outline to primitive for rendering
* 将图形轮廓线转换为图元进行渲染
*
* Currently only supports rectangle outlines (rendered as 4 thin rectangles).
* Other shapes need mesh rendering support in the engine.
*
* 目前只支持矩形轮廓(渲染为 4 个细矩形)
* 其他形状需要引擎中的网格渲染支持
*/
private convertGraphOutlineToPrimitive(primitive: IRenderPrimitive): IRenderPrimitive[] {
const graphType = primitive.graphType;
// Only support rectangle outlines for now
// 目前只支持矩形轮廓
if (graphType !== EGraphType.Rect) {
return [];
}
const lineWidth = primitive.lineSize ?? 1;
const lineColor = primitive.lineColor ?? 0x000000FF;
const width = primitive.width;
const height = primitive.height;
// Get position from world matrix
const m = primitive.worldMatrix;
const baseX = m ? m[4] : 0;
const baseY = m ? m[5] : 0;
const result: IRenderPrimitive[] = [];
// Create 4 thin rectangles for the outline
// Top edge
result.push(this.createOutlineRect(baseX, baseY, width, lineWidth, lineColor, primitive));
// Bottom edge
result.push(this.createOutlineRect(baseX, baseY + height - lineWidth, width, lineWidth, lineColor, primitive));
// Left edge (excluding corners already covered by top/bottom)
result.push(this.createOutlineRect(baseX, baseY + lineWidth, lineWidth, height - lineWidth * 2, lineColor, primitive));
// Right edge
result.push(this.createOutlineRect(baseX + width - lineWidth, baseY + lineWidth, lineWidth, height - lineWidth * 2, lineColor, primitive));
return result;
}
/**
* Create a single outline rectangle primitive
* 创建单个轮廓矩形图元
*/
private createOutlineRect(
x: number,
y: number,
width: number,
height: number,
color: number,
source: IRenderPrimitive
): IRenderPrimitive {
// Create new world matrix with the position
const matrix = new Float32Array(6);
matrix[0] = 1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 1;
matrix[4] = x;
matrix[5] = y;
return {
type: ERenderPrimitiveType.Image,
sortOrder: source.sortOrder + 0.1,
worldMatrix: matrix,
width,
height,
alpha: source.alpha,
grayed: source.grayed,
blendMode: source.blendMode,
clipRect: source.clipRect,
textureId: '__fgui_white_pixel__',
uvRect: [0, 0, 1, 1],
color
};
}
/**
* Expand a nine-slice primitive into 9 sub-primitives
* 将九宫格图元展开为 9 个子图元
*
* Nine-slice grid divides the image into 9 regions:
* +-------+---------------+-------+
* | 0 | 1 | 2 | (top row)
* | (TL) | (TC) | (TR) |
* +-------+---------------+-------+
* | 3 | 4 | 5 | (middle row)
* | (ML) | (center) | (MR) |
* +-------+---------------+-------+
* | 6 | 7 | 8 | (bottom row)
* | (BL) | (BC) | (BR) |
* +-------+---------------+-------+
*
* Corners (0,2,6,8): Keep original size
* Edges (1,3,5,7): Stretch in one direction
* Center (4): Stretch in both directions
*/
private expandScale9Grid(primitive: IRenderPrimitive): IRenderPrimitive[] {
const grid = primitive.scale9Grid!;
const result: IRenderPrimitive[] = [];
// Source dimensions (original texture region)
// 源尺寸(原始纹理区域)
const srcWidth = primitive.sourceWidth ?? primitive.width;
const srcHeight = primitive.sourceHeight ?? primitive.height;
// Grid boundaries in source space
// 源空间中的九宫格边界
const left = grid.x;
const top = grid.y;
const right = grid.x + grid.width;
const bottom = grid.y + grid.height;
// Target dimensions (stretched)
// 目标尺寸(拉伸后)
const targetWidth = primitive.width;
const targetHeight = primitive.height;
// Calculate stretched middle section sizes
const cornerLeftWidth = left;
const cornerRightWidth = srcWidth - right;
const cornerTopHeight = top;
const cornerBottomHeight = srcHeight - bottom;
// Middle section in target space
const middleWidth = Math.max(0, targetWidth - cornerLeftWidth - cornerRightWidth);
const middleHeight = Math.max(0, targetHeight - cornerTopHeight - cornerBottomHeight);
// UV coordinates from primitive
const uvRect = primitive.uvRect || [0, 0, 1, 1];
const u0 = uvRect[0];
const v0 = uvRect[1];
const u1 = uvRect[2];
const v1 = uvRect[3];
// Calculate UV deltas per pixel
const uPerPixel = (u1 - u0) / srcWidth;
const vPerPixel = (v1 - v0) / srcHeight;
// UV boundaries for nine-slice
const uLeft = u0 + left * uPerPixel;
const uRight = u0 + right * uPerPixel;
const vTop = v0 + top * vPerPixel;
const vBottom = v0 + bottom * vPerPixel;
// World matrix for positioning
const m = primitive.worldMatrix;
const baseX = m ? m[4] : (primitive.x ?? 0);
const baseY = m ? m[5] : (primitive.y ?? 0);
// Extract scale from matrix
const matrixScaleX = m ? Math.sqrt(m[0] * m[0] + m[1] * m[1]) : 1;
const matrixScaleY = m ? Math.sqrt(m[2] * m[2] + m[3] * m[3]) : 1;
// Helper to create a sub-primitive
const createSubPrimitive = (
offsetX: number,
offsetY: number,
width: number,
height: number,
uvX0: number,
uvY0: number,
uvX1: number,
uvY1: number
): IRenderPrimitive | null => {
if (width <= 0 || height <= 0) return null;
// Create new world matrix with offset position
const subMatrix = new Float32Array(6);
if (m) {
subMatrix[0] = m[0];
subMatrix[1] = m[1];
subMatrix[2] = m[2];
subMatrix[3] = m[3];
// Apply offset in local space, then transform
subMatrix[4] = baseX + offsetX * matrixScaleX;
subMatrix[5] = baseY + offsetY * matrixScaleY;
} else {
subMatrix[0] = 1;
subMatrix[1] = 0;
subMatrix[2] = 0;
subMatrix[3] = 1;
subMatrix[4] = baseX + offsetX;
subMatrix[5] = baseY + offsetY;
}
return {
type: primitive.type,
sortOrder: primitive.sortOrder,
worldMatrix: subMatrix,
width: width,
height: height,
alpha: primitive.alpha,
grayed: primitive.grayed,
blendMode: primitive.blendMode,
clipRect: primitive.clipRect,
textureId: primitive.textureId,
uvRect: [uvX0, uvY0, uvX1, uvY1],
color: primitive.color
};
};
// Row positions (Y offsets)
const rowY = [0, cornerTopHeight, cornerTopHeight + middleHeight];
// Row heights
const rowH = [cornerTopHeight, middleHeight, cornerBottomHeight];
// Row UV Y coordinates
const rowUV = [
[v0, vTop],
[vTop, vBottom],
[vBottom, v1]
];
// Column positions (X offsets)
const colX = [0, cornerLeftWidth, cornerLeftWidth + middleWidth];
// Column widths
const colW = [cornerLeftWidth, middleWidth, cornerRightWidth];
// Column UV X coordinates
const colUV = [
[u0, uLeft],
[uLeft, uRight],
[uRight, u1]
];
// Generate 9 sub-primitives
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
const sub = createSubPrimitive(
colX[col],
rowY[row],
colW[col],
rowH[row],
colUV[col][0],
rowUV[row][0],
colUV[col][1],
rowUV[row][1]
);
if (sub) {
result.push(sub);
}
}
}
return result;
}
/**
* Convert a batch of primitives to engine render data
* 将一批图元转换为引擎渲染数据
*/
private convertBatch(batch: IRenderPrimitive[]): IEngineRenderData | null {
if (batch.length === 0) return null;
const tileCount = batch.length;
// Allocate arrays
// Transform: [x, y, rotation, scaleX, scaleY, originX, originY] per tile (7 floats)
const transforms = new Float32Array(tileCount * 7);
const textureIds = new Uint32Array(tileCount);
const uvs = new Float32Array(tileCount * 4); // [u, v, uWidth, vHeight] per tile
const colors = new Uint32Array(tileCount);
let clipRect: { x: number; y: number; width: number; height: number } | undefined;
// Get effective canvas size for coordinate conversion
// 获取坐标转换的有效画布尺寸
const canvasSize = this.getEffectiveCanvasSize();
const halfWidth = canvasSize.width / 2;
const halfHeight = canvasSize.height / 2;
for (let i = 0; i < tileCount; i++) {
const primitive = batch[i];
const baseTransformIdx = i * 7;
const baseUvIdx = i * 4;
// Extract transform from world matrix
// Convert from FGUI coordinate system (top-left origin, Y-down)
// to engine coordinate system (center origin, Y-up)
//
// FGUI: (0, 0) = top-left, Y increases downward
// Engine: (0, 0) = center, Y increases upward
//
// Conversion formula:
// engineX = fguiX - canvasWidth/2
// engineY = canvasHeight/2 - fguiY (flip Y and offset)
//
// 从 FGUI 坐标系左上角原点Y 向下转换到引擎坐标系中心原点Y 向上)
const m = primitive.worldMatrix;
if (m) {
// Convert position from FGUI to engine coordinates
// 将位置从 FGUI 坐标转换为引擎坐标
transforms[baseTransformIdx + 0] = m[4] - halfWidth;
transforms[baseTransformIdx + 1] = halfHeight - m[5]; // Flip Y: halfHeight - fguiY
// Extract rotation from matrix (negate for Y-flip)
// 从矩阵提取旋转(因 Y 翻转而取反)
transforms[baseTransformIdx + 2] = -Math.atan2(m[1], m[0]);
// Extract scale from matrix and multiply by sprite dimensions
const matrixScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
const matrixScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
transforms[baseTransformIdx + 3] = matrixScaleX * primitive.width;
transforms[baseTransformIdx + 4] = matrixScaleY * primitive.height;
// Origin: In FGUI, position refers to top-left corner of the sprite
// In engine Y-up system: origin (0, 1) = top-left corner
// The sprite extends from position downward (negative Y in engine)
// 原点:在 FGUI 中,位置指精灵的左上角
// 在引擎 Y 向上坐标系中origin (0, 1) = 左上角
// 精灵从位置点向下延伸(引擎中 Y 负方向)
transforms[baseTransformIdx + 5] = 0; // originX: left
transforms[baseTransformIdx + 6] = 1; // originY: top
} else {
// Use position and dimensions directly (fallback path)
// 直接使用位置和尺寸(回退路径)
const x = primitive.x ?? 0;
const y = primitive.y ?? 0;
transforms[baseTransformIdx + 0] = x - halfWidth;
transforms[baseTransformIdx + 1] = halfHeight - y; // Flip Y
transforms[baseTransformIdx + 2] = 0;
transforms[baseTransformIdx + 3] = primitive.width;
transforms[baseTransformIdx + 4] = primitive.height;
transforms[baseTransformIdx + 5] = 0; // originX: left
transforms[baseTransformIdx + 6] = 1; // originY: top
}
// Resolve texture ID
if (this._textureResolver && primitive.textureId !== undefined) {
textureIds[i] = this._textureResolver(primitive.textureId);
} else {
textureIds[i] = this._defaultTextureId;
}
// UVs - engine expects [u0, v0, u1, v1] format in image coordinate system
// Engine vertex layout maps tex_coords as:
// vertex 0 (top-left) -> [u0, v0]
// vertex 1 (top-right) -> [u1, v0]
// vertex 2 (bottom-right) -> [u1, v1]
// vertex 3 (bottom-left) -> [u0, v1]
// This means v0 = top, v1 = bottom (image coordinate system, NOT OpenGL)
// FGUI sprite.rect uses image coordinates (y=0 at top), so no flip needed
//
// UV 格式:引擎期望图片坐标系的 [u0, v0, u1, v1]
// 引擎顶点布局将纹理坐标映射为v0 = 顶部v1 = 底部(图片坐标系)
// FGUI 的 sprite.rect 使用图片坐标y=0 在顶部),所以不需要翻转
if (primitive.uvRect) {
uvs[baseUvIdx + 0] = primitive.uvRect[0]; // u0
uvs[baseUvIdx + 1] = primitive.uvRect[1]; // v0 (top)
uvs[baseUvIdx + 2] = primitive.uvRect[2]; // u1
uvs[baseUvIdx + 3] = primitive.uvRect[3]; // v1 (bottom)
} else {
// Default full UV
uvs[baseUvIdx + 0] = 0;
uvs[baseUvIdx + 1] = 0;
uvs[baseUvIdx + 2] = 1;
uvs[baseUvIdx + 3] = 1;
}
// Color (0xRRGGBBAA format)
if (primitive.color !== undefined) {
colors[i] = primitive.color;
} else {
// White with alpha (0xRRGGBBAA)
const alpha = Math.floor(primitive.alpha * 255);
colors[i] = (255 << 24) | (255 << 16) | (255 << 8) | alpha;
}
// Clip rect (use first primitive's clip rect for the batch)
if (!clipRect && primitive.clipRect) {
clipRect = {
x: primitive.clipRect.x,
y: primitive.clipRect.y,
width: primitive.clipRect.width,
height: primitive.clipRect.height
};
}
}
return {
transforms,
textureIds,
uvs,
colors,
tileCount,
sortingLayer: this._sortingLayer,
orderInLayer: this._orderInLayer,
bScreenSpace: true, // FairyGUI always renders in screen space
clipRect
};
}
}
/**
* Create a default FGUI render data provider
* 创建默认的 FGUI 渲染数据提供者
*/
export function createFGUIRenderDataProvider(): FGUIRenderDataProvider {
return new FGUIRenderDataProvider();
}