Feature/runtime cdn and plugin loader (#240)
* feat(ui): 完善 UI 布局系统和编辑器可视化工具 * refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统 * fix: 修复 CodeQL 警告并提升测试覆盖率 * refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题 * fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤 * docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明 * fix(ci): 修复 type-check 失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖 * fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖 * fix(ci): platform-web 添加缺失的 behavior-tree 依赖 * fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
322
packages/ui/src/systems/render/UITextRenderSystem.ts
Normal file
322
packages/ui/src/systems/render/UITextRenderSystem.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* UI Text Render System
|
||||
* UI 文本渲染系统
|
||||
*
|
||||
* Renders UITextComponent entities by generating text textures
|
||||
* and submitting them to the shared UIRenderCollector.
|
||||
* 通过生成文本纹理并提交到共享的 UIRenderCollector 来渲染 UITextComponent 实体。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { UITransformComponent } from '../../components/UITransformComponent';
|
||||
import { UITextComponent } from '../../components/UITextComponent';
|
||||
import { getUIRenderCollector, registerCacheInvalidationCallback, unregisterCacheInvalidationCallback } from './UIRenderCollector';
|
||||
|
||||
/**
|
||||
* Text texture cache entry
|
||||
* 文本纹理缓存条目
|
||||
*/
|
||||
interface TextTextureCache {
|
||||
textureId: number;
|
||||
text: string;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: string | number;
|
||||
italic: boolean;
|
||||
color: number;
|
||||
alpha: number;
|
||||
align: string;
|
||||
verticalAlign: string;
|
||||
lineHeight: number;
|
||||
width: number;
|
||||
height: number;
|
||||
dataUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Text Render System
|
||||
* UI 文本渲染系统
|
||||
*
|
||||
* Handles rendering of text components by:
|
||||
* 1. Generating text textures using Canvas 2D
|
||||
* 2. Caching textures to avoid regeneration every frame
|
||||
* 3. Submitting texture render primitives to the collector
|
||||
*
|
||||
* 处理文本组件的渲染:
|
||||
* 1. 使用 Canvas 2D 生成文本纹理
|
||||
* 2. 缓存纹理以避免每帧重新生成
|
||||
* 3. 向收集器提交纹理渲染原语
|
||||
*/
|
||||
@ECSSystem('UITextRender', { updateOrder: 120 })
|
||||
export class UITextRenderSystem extends EntitySystem {
|
||||
private textCanvas: HTMLCanvasElement | null = null;
|
||||
private textCtx: CanvasRenderingContext2D | null = null;
|
||||
private textTextureCache: Map<number, TextTextureCache> = new Map();
|
||||
private nextTextureId = 90000;
|
||||
private onTextureCreated: ((id: number, dataUrl: string) => void) | null = null;
|
||||
private cacheInvalidationBound: () => void;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(UITransformComponent, UITextComponent));
|
||||
// Bind the method for cache invalidation callback
|
||||
this.cacheInvalidationBound = this.clearTextCache.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when system is added to scene
|
||||
* 系统添加到场景时调用
|
||||
*/
|
||||
public override initialize(): void {
|
||||
super.initialize();
|
||||
// Register for cache invalidation events
|
||||
registerCacheInvalidationCallback(this.cacheInvalidationBound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when system is destroyed
|
||||
* 系统销毁时调用
|
||||
*/
|
||||
protected override onDestroy(): void {
|
||||
super.onDestroy();
|
||||
// Unregister cache invalidation callback
|
||||
unregisterCacheInvalidationCallback(this.cacheInvalidationBound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback for when a new text texture is created
|
||||
* 设置创建新文本纹理时的回调
|
||||
*/
|
||||
setTextureCallback(callback: (id: number, dataUrl: string) => void): void {
|
||||
this.onTextureCreated = callback;
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
const collector = getUIRenderCollector();
|
||||
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
const text = entity.getComponent(UITextComponent)!;
|
||||
|
||||
if (!transform.visible || !text.text) continue;
|
||||
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
const alpha = transform.worldAlpha ?? transform.alpha;
|
||||
const baseOrder = 100 + transform.zIndex;
|
||||
|
||||
// Generate or retrieve cached texture
|
||||
// 生成或获取缓存的纹理
|
||||
const textureId = this.getOrCreateTextTexture(
|
||||
entity.id, text, Math.ceil(width), Math.ceil(height)
|
||||
);
|
||||
|
||||
if (textureId === null) continue;
|
||||
|
||||
// Use top-left position with origin at (0, 0)
|
||||
// 使用左上角位置,原点在 (0, 0)
|
||||
collector.addRect(
|
||||
x, y,
|
||||
width, height,
|
||||
0xFFFFFF, // White tint (color is baked into texture)
|
||||
alpha,
|
||||
baseOrder + 1, // Text renders above background
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0,
|
||||
textureId
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create text texture
|
||||
* 获取或创建文本纹理
|
||||
*/
|
||||
private getOrCreateTextTexture(
|
||||
entityId: number,
|
||||
text: UITextComponent,
|
||||
width: number,
|
||||
height: number
|
||||
): number | null {
|
||||
const canvasData = this.getTextCanvas();
|
||||
if (!canvasData) return null;
|
||||
|
||||
const { canvas, ctx } = canvasData;
|
||||
|
||||
const cached = this.textTextureCache.get(entityId);
|
||||
|
||||
// Check if we need to regenerate the texture
|
||||
// 检查是否需要重新生成纹理
|
||||
const needsUpdate = !cached ||
|
||||
cached.text !== text.text ||
|
||||
cached.fontSize !== text.fontSize ||
|
||||
cached.fontFamily !== text.fontFamily ||
|
||||
cached.fontWeight !== text.fontWeight ||
|
||||
cached.italic !== text.italic ||
|
||||
cached.color !== text.color ||
|
||||
cached.alpha !== text.alpha ||
|
||||
cached.align !== text.align ||
|
||||
cached.verticalAlign !== text.verticalAlign ||
|
||||
cached.lineHeight !== text.lineHeight ||
|
||||
cached.width !== width ||
|
||||
cached.height !== height;
|
||||
|
||||
if (needsUpdate) {
|
||||
const canvasWidth = Math.max(1, width);
|
||||
const canvasHeight = Math.max(1, height);
|
||||
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
ctx.font = text.getCSSFont();
|
||||
ctx.fillStyle = text.getCSSColor();
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
// Handle horizontal alignment
|
||||
// 处理水平对齐
|
||||
let textX = 0;
|
||||
if (text.align === 'center') {
|
||||
ctx.textAlign = 'center';
|
||||
textX = canvasWidth / 2;
|
||||
} else if (text.align === 'right') {
|
||||
ctx.textAlign = 'right';
|
||||
textX = canvasWidth;
|
||||
} else {
|
||||
ctx.textAlign = 'left';
|
||||
textX = 0;
|
||||
}
|
||||
|
||||
// Handle vertical alignment
|
||||
// 处理垂直对齐
|
||||
const textHeight = text.fontSize * text.lineHeight;
|
||||
let textY = 0;
|
||||
|
||||
if (text.verticalAlign === 'middle') {
|
||||
textY = (canvasHeight - textHeight) / 2;
|
||||
} else if (text.verticalAlign === 'bottom') {
|
||||
textY = canvasHeight - textHeight;
|
||||
}
|
||||
|
||||
// Draw text (with or without word wrap)
|
||||
// 绘制文本(带或不带自动换行)
|
||||
if (text.wordWrap) {
|
||||
this.drawWrappedText(ctx, text.text, textX, textY, canvasWidth, text.fontSize * text.lineHeight);
|
||||
} else {
|
||||
ctx.fillText(text.text, textX, textY);
|
||||
}
|
||||
|
||||
// Get or create texture ID
|
||||
// 获取或创建纹理 ID
|
||||
const textureId = cached?.textureId ?? this.nextTextureId++;
|
||||
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
|
||||
// Notify callback of new texture
|
||||
// 通知回调新纹理
|
||||
if (this.onTextureCreated) {
|
||||
this.onTextureCreated(textureId, dataUrl);
|
||||
}
|
||||
|
||||
// Update cache
|
||||
// 更新缓存
|
||||
this.textTextureCache.set(entityId, {
|
||||
textureId,
|
||||
text: text.text,
|
||||
fontSize: text.fontSize,
|
||||
fontFamily: text.fontFamily,
|
||||
fontWeight: text.fontWeight,
|
||||
italic: text.italic,
|
||||
color: text.color,
|
||||
alpha: text.alpha,
|
||||
align: text.align,
|
||||
verticalAlign: text.verticalAlign,
|
||||
lineHeight: text.lineHeight,
|
||||
width,
|
||||
height,
|
||||
dataUrl
|
||||
});
|
||||
}
|
||||
|
||||
return this.textTextureCache.get(entityId)?.textureId ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create text canvas
|
||||
* 获取或创建文本画布
|
||||
*/
|
||||
private getTextCanvas(): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null {
|
||||
if (!this.textCanvas) {
|
||||
this.textCanvas = document.createElement('canvas');
|
||||
this.textCtx = this.textCanvas.getContext('2d');
|
||||
}
|
||||
if (!this.textCtx) return null;
|
||||
return { canvas: this.textCanvas, ctx: this.textCtx };
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw text with word wrapping
|
||||
* 绘制带自动换行的文本
|
||||
*/
|
||||
private drawWrappedText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
maxWidth: number,
|
||||
lineHeight: number
|
||||
): void {
|
||||
const words = text.split(' ');
|
||||
let line = '';
|
||||
let currentY = y;
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = line + word + ' ';
|
||||
const metrics = ctx.measureText(testLine);
|
||||
|
||||
if (metrics.width > maxWidth && line !== '') {
|
||||
ctx.fillText(line.trim(), x, currentY);
|
||||
line = word + ' ';
|
||||
currentY += lineHeight;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (line.trim()) {
|
||||
ctx.fillText(line.trim(), x, currentY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear text texture cache
|
||||
* 清除文本纹理缓存
|
||||
*/
|
||||
clearTextCache(): void {
|
||||
this.textTextureCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for a specific entity
|
||||
* 清除特定实体的缓存
|
||||
*/
|
||||
clearEntityTextCache(entityId: number): void {
|
||||
this.textTextureCache.delete(entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose resources
|
||||
* 释放资源
|
||||
*/
|
||||
dispose(): void {
|
||||
this.textCanvas = null;
|
||||
this.textCtx = null;
|
||||
this.textTextureCache.clear();
|
||||
this.onTextureCreated = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user