Files
esengine/packages/rendering/fairygui/src/render/RenderBridge.ts

311 lines
8.5 KiB
TypeScript
Raw Normal View History

feat(fairygui): FairyGUI 完整集成 (#314) * feat(fairygui): FairyGUI ECS 集成核心架构 实现 FairyGUI 的 ECS 原生集成,完全替代旧 UI 系统: 核心类: - GObject: UI 对象基类,支持变换、可见性、关联、齿轮 - GComponent: 容器组件,管理子对象和控制器 - GRoot: 根容器,管理焦点、弹窗、输入分发 - GGroup: 组容器,支持水平/垂直布局 抽象层: - DisplayObject: 显示对象基类 - EventDispatcher: 事件分发 - Timer: 计时器 - Stage: 舞台,管理输入和缩放 布局系统: - Relations: 约束关联管理 - RelationItem: 24 种关联类型 基础设施: - Controller: 状态控制器 - Transition: 过渡动画 - ScrollPane: 滚动面板 - UIPackage: 包管理 - ByteBuffer: 二进制解析 * refactor(ui): 删除旧 UI 系统,使用 FairyGUI 替代 * feat(fairygui): 实现 UI 控件 - 添加显示类:Image、TextField、Graph - 添加基础控件:GImage、GTextField、GGraph - 添加交互控件:GButton、GProgressBar、GSlider - 更新 IRenderCollector 支持 Graph 渲染 - 扩展 Controller 添加 selectedPageId - 添加 STATE_CHANGED 事件类型 * feat(fairygui): 现代化架构重构 - 增强 EventDispatcher 支持类型安全、优先级和传播控制 - 添加 PropertyBinding 响应式属性绑定系统 - 添加 ServiceContainer 依赖注入容器 - 添加 UIConfig 全局配置系统 - 添加 UIObjectFactory 对象工厂 - 实现 RenderBridge 渲染桥接层 - 实现 Canvas2DBackend 作为默认渲染后端 - 扩展 IRenderCollector 支持更多图元类型 * feat(fairygui): 九宫格渲染和资源加载修复 - 修复 FGUIUpdateSystem 支持路径和 GUID 两种加载方式 - 修复 GTextInput 同时设置 _displayObject 和 _textField - 实现九宫格渲染展开为 9 个子图元 - 添加 sourceWidth/sourceHeight 用于九宫格计算 - 添加 DOMTextRenderer 文本渲染层(临时方案) * fix(fairygui): 修复 GGraph 颜色读取 * feat(fairygui): 虚拟节点 Inspector 和文本渲染支持 * fix(fairygui): 编辑器状态刷新和遗留引用修复 - 修复切换 FGUI 包后组件列表未刷新问题 - 修复切换组件后 viewport 未清理旧内容问题 - 修复虚拟节点在包加载后未刷新问题 - 重构为事件驱动架构,移除轮询机制 - 修复 @esengine/ui 遗留引用,统一使用 @esengine/fairygui * fix: 移除 tsconfig 中的 @esengine/ui 引用
2025-12-22 10:52:54 +08:00
import type { IRectangle } from '../utils/MathTypes';
import type { IRenderCollector, IRenderPrimitive } from './IRenderCollector';
import type { IRenderBackend, IRenderStats, ITextureHandle, IFontHandle } from './IRenderBackend';
/**
* Texture cache entry
*
*/
interface TextureCacheEntry {
handle: ITextureHandle;
lastUsedFrame: number;
refCount: number;
}
/**
* RenderBridge
*
* Bridges FairyGUI render primitives to the graphics backend.
* Provides batching, caching, and optimization.
*
* FairyGUI
*
*
* Features:
* - Automatic batching of similar primitives
* - Texture atlas support
* - Font caching
* - Render statistics
*
* @example
* ```typescript
* const bridge = new RenderBridge(webgpuBackend);
* await bridge.initialize(canvas);
*
* // In render loop
* bridge.beginFrame();
* root.collectRenderData(collector);
* bridge.render(collector);
* bridge.endFrame();
* ```
*/
export class RenderBridge {
private _backend: IRenderBackend;
private _textureCache: Map<string, TextureCacheEntry> = new Map();
private _fontCache: Map<string, IFontHandle> = new Map();
private _currentFrame: number = 0;
private _textureCacheMaxAge: number = 60; // Frames before texture is evicted
private _clipStack: IRectangle[] = [];
private _batchBuffer: IRenderPrimitive[] = [];
constructor(backend: IRenderBackend) {
this._backend = backend;
}
/**
* Get the underlying backend
*
*/
public get backend(): IRenderBackend {
return this._backend;
}
/**
* Check if bridge is initialized
*
*/
public get isInitialized(): boolean {
return this._backend.isInitialized;
}
/**
* Initialize the bridge with a canvas
* 使
*/
public async initialize(canvas: HTMLCanvasElement): Promise<boolean> {
return this._backend.initialize(canvas);
}
/**
* Begin a new frame
*
*/
public beginFrame(): void {
this._currentFrame++;
this._clipStack.length = 0;
this._batchBuffer.length = 0;
this._backend.beginFrame();
}
/**
* End the current frame
*
*/
public endFrame(): void {
this.flushBatch();
this._backend.endFrame();
this.evictOldTextures();
}
/**
* Render primitives from a collector
*
*/
public render(collector: IRenderCollector): void {
const primitives = collector.getPrimitives();
for (const primitive of primitives) {
this.processPrimitive(primitive);
}
}
/**
* Render a single primitive
*
*/
public renderPrimitive(primitive: IRenderPrimitive): void {
this.processPrimitive(primitive);
}
/**
* Push a clip rectangle
*
*/
public pushClipRect(rect: IRectangle): void {
if (this._clipStack.length > 0) {
const current = this._clipStack[this._clipStack.length - 1];
const intersected = this.intersectRects(current, rect);
this._clipStack.push(intersected);
} else {
this._clipStack.push({ ...rect });
}
this.flushBatch();
this._backend.setClipRect(this._clipStack[this._clipStack.length - 1]);
}
/**
* Pop the current clip rectangle
*
*/
public popClipRect(): void {
if (this._clipStack.length > 0) {
this._clipStack.pop();
this.flushBatch();
this._backend.setClipRect(
this._clipStack.length > 0 ? this._clipStack[this._clipStack.length - 1] : null
);
}
}
/**
* Load or get cached texture
*
*/
public async loadTexture(
url: string,
source?: ImageBitmap | HTMLImageElement | HTMLCanvasElement | ImageData
): Promise<ITextureHandle | null> {
// Check cache first
const cached = this._textureCache.get(url);
if (cached) {
cached.lastUsedFrame = this._currentFrame;
cached.refCount++;
return cached.handle;
}
// Load or create texture
let textureSource = source;
if (!textureSource) {
try {
const response = await fetch(url);
const blob = await response.blob();
textureSource = await createImageBitmap(blob);
} catch (error) {
console.error(`Failed to load texture: ${url}`, error);
return null;
}
}
const handle = this._backend.createTexture(textureSource);
this._textureCache.set(url, {
handle,
lastUsedFrame: this._currentFrame,
refCount: 1
});
return handle;
}
/**
* Release a texture reference
*
*/
public releaseTexture(url: string): void {
const cached = this._textureCache.get(url);
if (cached) {
cached.refCount--;
}
}
/**
* Load or get cached font
*
*/
public async loadFont(family: string, url?: string): Promise<IFontHandle> {
const cached = this._fontCache.get(family);
if (cached) {
return cached;
}
const handle = await this._backend.loadFont(family, url);
this._fontCache.set(family, handle);
return handle;
}
/**
* Resize the render target
*
*/
public resize(width: number, height: number): void {
this._backend.resize(width, height);
}
/**
* Get render statistics
*
*/
public getStats(): IRenderStats & { textureCount: number; fontCount: number } {
const backendStats = this._backend.getStats();
return {
...backendStats,
textureCount: this._textureCache.size,
fontCount: this._fontCache.size
};
}
/**
* Dispose the bridge and all resources
*
*/
public dispose(): void {
// Destroy all cached textures
for (const entry of this._textureCache.values()) {
this._backend.destroyTexture(entry.handle);
}
this._textureCache.clear();
this._fontCache.clear();
this._clipStack.length = 0;
this._batchBuffer.length = 0;
this._backend.dispose();
}
private processPrimitive(primitive: IRenderPrimitive): void {
// Check if can batch with previous primitives
if (this._batchBuffer.length > 0) {
const last = this._batchBuffer[this._batchBuffer.length - 1];
if (!this.canBatch(last, primitive)) {
this.flushBatch();
}
}
this._batchBuffer.push(primitive);
}
private canBatch(a: IRenderPrimitive, b: IRenderPrimitive): boolean {
// Can batch if same type and texture
if (a.type !== b.type) return false;
if (a.textureId !== b.textureId) return false;
if (a.blendMode !== b.blendMode) return false;
return true;
}
private flushBatch(): void {
if (this._batchBuffer.length === 0) return;
this._backend.submitPrimitives(this._batchBuffer);
this._batchBuffer.length = 0;
}
private evictOldTextures(): void {
const minFrame = this._currentFrame - this._textureCacheMaxAge;
const toEvict: string[] = [];
for (const [url, entry] of this._textureCache) {
if (entry.refCount <= 0 && entry.lastUsedFrame < minFrame) {
toEvict.push(url);
}
}
for (const url of toEvict) {
const entry = this._textureCache.get(url);
if (entry) {
this._backend.destroyTexture(entry.handle);
this._textureCache.delete(url);
}
}
}
private intersectRects(a: IRectangle, b: IRectangle): IRectangle {
const x = Math.max(a.x, b.x);
const y = Math.max(a.y, b.y);
const right = Math.min(a.x + a.width, b.x + b.width);
const bottom = Math.min(a.y + a.height, b.y + b.height);
return {
x,
y,
width: Math.max(0, right - x),
height: Math.max(0, bottom - y)
};
}
}