Files
esengine/packages/ui/src/systems/render/UIRenderUtils.ts
YHH 536c4c5593 refactor(ui): UI 系统架构重构 (#309)
* feat(ui): 动态图集系统与渲染调试增强

## 核心功能

### 动态图集系统 (Dynamic Atlas)
- 新增 DynamicAtlasManager:运行时纹理打包,支持 MaxRects 算法
- 新增 DynamicAtlasService:自动纹理加载与图集管理
- 新增 BinPacker:高效矩形打包算法
- 支持动态/固定两种扩展策略
- 自动 UV 重映射,实现 UI 元素合批渲染

### Frame Debugger 增强
- 新增合批分析面板,显示批次中断原因
- 新增 UI 元素层级信息(depth, worldOrderInLayer)
- 新增实体高亮功能,点击可在场景中定位
- 新增动态图集可视化面板
- 改进渲染原语详情展示

### 闪光效果 (Shiny Effect)
- 新增 UIShinyEffectComponent:UI 闪光参数配置
- 新增 UIShinyEffectSystem:材质覆盖驱动的闪光动画
- 新增 ShinyEffectComponent/System(Sprite 版本)

## 引擎层改进

### Rust 纹理管理扩展
- create_blank_texture:创建空白 GPU 纹理
- update_texture_region:局部纹理更新
- 支持动态图集的 GPU 端操作

### 材质系统
- 新增 effects/ 目录:ShinyEffect 等效果实现
- 新增 interfaces/ 目录:IMaterial 等接口定义
- 新增 mixins/ 目录:可组合的材质功能

### EngineBridge 扩展
- 新增 createBlankTexture/updateTextureRegion 方法
- 改进纹理加载回调机制

## UI 渲染改进
- UIRenderCollector:支持合批调试信息
- 稳定排序:addIndex 保证渲染顺序一致性
- 九宫格渲染优化
- 材质覆盖支持

## 其他改进
- 国际化:新增 Frame Debugger 相关翻译
- 编辑器:新增渲染调试入口
- 文档:新增架构设计文档目录

* refactor(ui): 引入新基础组件架构与渲染工具函数

Phase 1 重构 - 组件职责分离与代码复用:

新增基础组件层:
- UIGraphicComponent: 所有可视 UI 元素的基类(颜色、透明度、raycast)
- UIImageComponent: 纹理显示组件(支持简单、切片、平铺、填充模式)
- UISelectableComponent: 可交互元素的基类(状态管理、颜色过渡)

新增渲染工具:
- UIRenderUtils: 提取共享的坐标计算、边框渲染、阴影渲染等工具函数
- getUIRenderTransform: 统一的变换数据提取
- renderBorder/renderShadow: 复用的边框和阴影渲染逻辑

新增渲染系统:
- UIGraphicRenderSystem: 处理新基础组件的统一渲染器

重构现有系统:
- UIRectRenderSystem: 使用新工具函数,移除重复代码
- UIButtonRenderSystem: 使用新工具函数,移除重复代码

这些改动为后续统一渲染系统奠定基础。

* refactor(ui): UIProgressBarRenderSystem 使用渲染工具函数

- 使用 getUIRenderTransform 替代手动变换计算
- 使用 renderBorder 工具函数替代重复的边框渲染
- 使用 lerpColor 工具函数替代重复的颜色插值
- 简化方法签名,使用 UIRenderTransform 类型
- 移除约 135 行重复代码

* refactor(ui): Slider 和 ScrollView 渲染系统使用工具函数

- UISliderRenderSystem: 使用 getUIRenderTransform,简化方法签名
- UIScrollViewRenderSystem: 使用 getUIRenderTransform,简化方法签名
- 统一使用 UIRenderTransform 类型减少参数传递
- 消除重复的变换计算代码

* refactor(ui): 使用 UIWidgetMarker 消除硬编码组件依赖

- 新增 UIWidgetMarker 标记组件
- UIRectRenderSystem 改为检查标记而非硬编码4种组件类型
- 各 Widget 渲染系统自动添加标记组件
- 减少模块间耦合,提高可扩展性

* feat(ui): 实现 Canvas 隔离机制

- 新增 UICanvasComponent 定义 Canvas 渲染组
- UITransformComponent 添加 Canvas 相关字段:canvasEntityId, worldSortingLayer, pixelPerfect
- UILayoutSystem 传播 Canvas 设置给子元素
- UIRenderUtils 使用 Canvas 继承的排序层
- 支持嵌套 Canvas 和不同渲染模式

* refactor(ui): 统一纹理管理工具函数

Phase 4: 纹理管理统一

新增:
- UITextureUtils.ts: 统一的纹理描述符接口和验证函数
  - UITextureDescriptor: 支持 GUID/textureId/path 多种纹理源
  - isValidTextureGuid: GUID 验证
  - getTextureKey: 获取用于合批的纹理键
  - normalizeTextureDescriptor: 规范化各种输入格式
- utils/index.ts: 工具函数导出

修改:
- UIGraphicRenderSystem: 使用新的纹理工具函数
- index.ts: 导出纹理工具类型和函数

* refactor(ui): 实现统一的脏标记机制

Phase 5: Dirty 标记机制

新增:
- UIDirtyFlags.ts: 位标记枚举和追踪工具
  - UIDirtyFlags: Visual/Layout/Transform/Material/Text 标记
  - IDirtyTrackable: 脏追踪接口
  - DirtyTracker: 辅助工具类
  - 帧级别脏状态追踪 (markFrameDirty, isFrameDirty)

修改:
- UIGraphicComponent: 实现 IDirtyTrackable
  - 属性 setter 自动设置脏标记
  - 保留 setDirty/clearDirty 向后兼容
- UIImageComponent: 所有属性支持脏追踪
  - textureGuid/imageType/fillAmount 等变化自动标记
- UIGraphicRenderSystem: 使用 clearDirtyFlags()

导出:
- UIDirtyFlags, IDirtyTrackable, DirtyTracker
- markFrameDirty, isFrameDirty, clearFrameDirty

* refactor(ui): 移除过时的 dirty flag API

移除 UIGraphicComponent 中的兼容性 API:
- 移除 _isDirty getter/setter
- 移除 setDirty() 方法
- 移除 clearDirty() 方法

现在统一使用新的 dirty flag 系统:
- isDirty() / hasDirtyFlag(flags)
- markDirty(flags) / clearDirtyFlags()

* fix(ui): 修复两个 TODO 功能

1. 滑块手柄命中测试 (UIInputSystem)
   - UISliderComponent 添加 getHandleBounds() 计算手柄边界
   - UISliderComponent 添加 isPointInHandle() 精确命中测试
   - UIInputSystem.handleSlider() 使用精确测试更新悬停状态

2. 径向填充渲染 (UIGraphicRenderSystem)
   - 实现 renderRadialFill() 方法
   - 支持 radial90/radial180/radial360 三种模式
   - 支持 fillOrigin (top/right/bottom/left) 和 fillClockwise
   - 使用多段矩形近似饼形填充效果

* feat(ui): 完善 UI 系统架构和九宫格渲染

* fix(ui): 修复文本渲染层级问题并清理调试代码

- 修复纹理就绪后调用 invalidateUIRenderCaches() 导致的无限循环
- 移除 UITextRenderSystem、UIButtonRenderSystem、UIRectRenderSystem 中的首帧调试输出
- 移除 UILayoutSystem 中的布局调试日志
- 清理所有 __UI_RENDER_DEBUG__ 条件日志

* refactor(ui): 优化渲染批处理和输入框组件

渲染系统:
- 修复 RenderBatcher 保持渲染顺序
- 优化 Rust SpriteBatch 避免合并非连续精灵
- 增强 EngineRenderSystem 纹理就绪检测

输入框组件:
- 增强 UIInputFieldComponent 功能
- 改进 UIInputSystem 输入处理
- 新增 TextMeasureService 文本测量服务

* fix(ui): 修复九宫格首帧渲染和InputField输入问题

- 修复九宫格首帧 size=0x0 问题:
  - Viewport.tsx: 预览模式读取图片尺寸存储到 importSettings
  - AssetDatabase: ISpriteSettings 添加 width/height 字段
  - AssetMetadataService: getTextureSpriteInfo 使用元数据尺寸作为后备
  - UIRectRenderSystem: 当 atlasEntry 不存在时使用 spriteInfo 尺寸
  - WebBuildPipeline: 构建时包含 importSettings
  - AssetManager: 从 catalog 初始化时复制 importSettings
  - AssetTypes: IAssetCatalogEntry 添加 importSettings 字段

- 修复 InputField 无法输入问题:
  - UIRuntimeModule: manifest 添加 pluginExport: 'UIPlugin'
  - 确保预览模式正确加载 UI 插件并绑定 UIInputSystem

- 添加调试日志用于排查纹理加载问题

* fix(sprite): 修复类型导出错误

MaterialPropertyOverride 和 MaterialOverrides 应从 @esengine/material-system 导出

* fix(ui-editor): 补充 AnchorPreset 拉伸预设的映射

添加 StretchTop, StretchMiddle, StretchBottom, StretchLeft, StretchCenter, StretchRight 的位置和锚点值映射
2025-12-19 15:33:36 +08:00

327 lines
10 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.
/**
* UI Render Utilities
* UI 渲染工具
*
* Shared utility functions for UI render systems to reduce code duplication.
* 渲染系统共享的工具函数,减少代码重复。
*/
import type { Entity } from '@esengine/ecs-framework';
import { UITransformComponent } from '../../components/UITransformComponent';
import { UIWidgetMarker } from '../../components/UIWidgetMarker';
import type { UIRenderCollector } from './UIRenderCollector';
/**
* Ensure entity has UIWidgetMarker component
* 确保实体具有 UIWidgetMarker 组件
*
* Widget components add this marker to prevent UIRectRenderSystem from
* rendering them, as they have their own specialized render systems.
*
* Widget 组件添加此标记以防止 UIRectRenderSystem 渲染它们,
* 因为它们有自己专门的渲染系统。
*
* @param entity - Entity to check/mark
*/
export function ensureUIWidgetMarker(entity: Entity): void {
if (!entity.hasComponent(UIWidgetMarker)) {
entity.addComponent(new UIWidgetMarker());
}
}
/**
* Computed transform data for rendering
* 用于渲染的计算后变换数据
*/
export interface UIRenderTransform {
/** World X position (bottom-left corner) / 世界 X 坐标(左下角) */
x: number;
/** World Y position (bottom-left corner) / 世界 Y 坐标(左下角) */
y: number;
/** Computed width with scale / 计算后的宽度(含缩放) */
width: number;
/** Computed height with scale / 计算后的高度(含缩放) */
height: number;
/** World alpha / 世界透明度 */
alpha: number;
/** World rotation in radians / 世界旋转(弧度) */
rotation: number;
/** Pivot X (0-1) / X 轴锚点 (0-1) */
pivotX: number;
/** Pivot Y (0-1) / Y 轴锚点 (0-1) */
pivotY: number;
/** Sorting layer name / 排序层名称 */
sortingLayer: string;
/** Order within layer / 层内顺序 */
orderInLayer: number;
/** Render X position (pivot-adjusted) / 渲染 X 坐标(锚点调整后) */
renderX: number;
/** Render Y position (pivot-adjusted) / 渲染 Y 坐标(锚点调整后) */
renderY: number;
}
/**
* Extract render transform data from UITransformComponent
* 从 UITransformComponent 提取渲染变换数据
*
* 使用 UILayoutSystem 计算的世界坐标。如果 layoutComputed = false回退到本地坐标。
* Uses world coordinates computed by UILayoutSystem. If layoutComputed = false, falls back to local coordinates.
*
* @param transform - UITransformComponent instance
* @param _entity - Optional entity (unused, for API compatibility)
* @returns Computed render transform, or null if not visible
*/
export function getUIRenderTransform(transform: UITransformComponent, _entity?: Entity): UIRenderTransform | null {
// 如果布局还没计算,跳过渲染(等待 UILayoutSystem 计算 worldOrderInLayer
// Skip if layout not computed yet (wait for UILayoutSystem to calculate worldOrderInLayer)
if (!transform.layoutComputed) return null;
if (!transform.worldVisible) return null;
// 使用 layoutComputed 判断是否使用世界坐标
// Use layoutComputed to determine whether to use world coordinates
const x = transform.layoutComputed ? transform.worldX : transform.x;
const y = transform.layoutComputed ? transform.worldY : transform.y;
const scaleX = transform.worldScaleX ?? transform.scaleX;
const scaleY = transform.worldScaleY ?? transform.scaleY;
const width = (transform.layoutComputed && transform.computedWidth > 0
? transform.computedWidth
: transform.width) * scaleX;
const height = (transform.layoutComputed && transform.computedHeight > 0
? transform.computedHeight
: transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
// 角度转弧度 | Convert degrees to radians
const rotationDegrees = transform.worldRotation ?? transform.rotation;
const rotation = (rotationDegrees * Math.PI) / 180;
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
// 使用继承自 Canvas 的排序层,如果没有则回退到组件本身的排序层
// Use Canvas-inherited sorting layer, fallback to component's own sortingLayer
const sortingLayer = transform.worldSortingLayer ?? transform.sortingLayer;
const orderInLayer = transform.worldOrderInLayer;
// Render position = bottom-left corner + pivot offset
// 渲染位置 = 左下角 + 锚点偏移
const renderX = x + width * pivotX;
const renderY = y + height * pivotY;
return {
x,
y,
width,
height,
alpha,
rotation,
pivotX,
pivotY,
sortingLayer,
orderInLayer,
renderX,
renderY
};
}
/**
* Border rendering options
* 边框渲染选项
*/
export interface BorderRenderOptions {
/** Border width in pixels / 边框宽度(像素) */
borderWidth: number;
/** Border color (0xRRGGBB) / 边框颜色 */
borderColor: number;
/** Border alpha (0-1) / 边框透明度 */
borderAlpha: number;
}
/**
* Render a rectangular border
* 渲染矩形边框
*
* @param collector - UIRenderCollector instance
* @param rt - Render transform data
* @param options - Border options
* @param entityId - Entity ID for debugging
* @param orderOffset - Order in layer offset (default: 0)
*/
export function renderBorder(
collector: UIRenderCollector,
rt: UIRenderTransform,
options: BorderRenderOptions,
entityId: number,
orderOffset: number = 0
): void {
const { borderWidth, borderColor, borderAlpha } = options;
if (borderWidth <= 0 || borderAlpha <= 0) return;
const alpha = borderAlpha * rt.alpha;
const orderInLayer = rt.orderInLayer + orderOffset;
// Calculate rect boundaries relative to pivot center
// 计算矩形边界(相对于 pivot 中心)
const left = rt.renderX - rt.width * rt.pivotX;
const bottom = rt.renderY - rt.height * rt.pivotY;
const right = left + rt.width;
const top = bottom + rt.height;
const centerX = (left + right) / 2;
const centerY = (top + bottom) / 2;
// Top border
collector.addRect(
centerX, top - borderWidth / 2,
rt.width, borderWidth,
borderColor, alpha, rt.sortingLayer, orderInLayer,
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
);
// Bottom border
collector.addRect(
centerX, bottom + borderWidth / 2,
rt.width, borderWidth,
borderColor, alpha, rt.sortingLayer, orderInLayer,
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
);
// Side borders (excluding corners)
const sideBorderHeight = rt.height - borderWidth * 2;
// Left border
collector.addRect(
left + borderWidth / 2, centerY,
borderWidth, sideBorderHeight,
borderColor, alpha, rt.sortingLayer, orderInLayer,
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
);
// Right border
collector.addRect(
right - borderWidth / 2, centerY,
borderWidth, sideBorderHeight,
borderColor, alpha, rt.sortingLayer, orderInLayer,
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
);
}
/**
* Shadow rendering options
* 阴影渲染选项
*/
export interface ShadowRenderOptions {
/** Shadow offset X / 阴影 X 偏移 */
offsetX: number;
/** Shadow offset Y / 阴影 Y 偏移 */
offsetY: number;
/** Shadow blur radius / 阴影模糊半径 */
blur: number;
/** Shadow color (0xRRGGBB) / 阴影颜色 */
color: number;
/** Shadow alpha (0-1) / 阴影透明度 */
alpha: number;
}
/**
* Render a shadow behind an element
* 渲染元素后的阴影
*
* @param collector - UIRenderCollector instance
* @param rt - Render transform data
* @param options - Shadow options
* @param entityId - Entity ID for debugging
* @param orderOffset - Order in layer offset (default: -1 to render below)
*/
export function renderShadow(
collector: UIRenderCollector,
rt: UIRenderTransform,
options: ShadowRenderOptions,
entityId: number,
orderOffset: number = -1
): void {
if (options.alpha <= 0) return;
collector.addRect(
rt.renderX + options.offsetX,
rt.renderY + options.offsetY,
rt.width + options.blur * 2,
rt.height + options.blur * 2,
options.color,
options.alpha * rt.alpha,
rt.sortingLayer,
rt.orderInLayer + orderOffset,
{
rotation: rt.rotation,
pivotX: rt.pivotX,
pivotY: rt.pivotY,
entityId
}
);
}
/**
* Color interpolation (linear)
* 颜色线性插值
*
* @param from - Start color (0xRRGGBB)
* @param to - End color (0xRRGGBB)
* @param t - Interpolation factor (0-1)
* @returns Interpolated color
*/
export function lerpColor(from: number, to: number, t: number): number {
const fromR = (from >> 16) & 0xFF;
const fromG = (from >> 8) & 0xFF;
const fromB = from & 0xFF;
const toR = (to >> 16) & 0xFF;
const toG = (to >> 8) & 0xFF;
const toB = to & 0xFF;
const r = Math.round(fromR + (toR - fromR) * t);
const g = Math.round(fromG + (toG - fromG) * t);
const b = Math.round(fromB + (toB - fromB) * t);
return (r << 16) | (g << 8) | b;
}
/**
* Pack color with alpha into ARGB format
* 将颜色和透明度打包为 ARGB 格式
*
* @param color - Color (0xRRGGBB)
* @param alpha - Alpha (0-1)
* @returns Packed color (0xAARRGGBB)
*/
export function packColorWithAlpha(color: number, alpha: number): number {
const a = Math.round(alpha * 255) & 0xFF;
return (a << 24) | (color & 0xFFFFFF);
}
/**
* Get nine-patch position and pivot for consistent rendering
* 获取九宫格位置和 pivot 以实现一致的渲染
*
* NinePatch now uses the same coordinate system as regular rects:
* - Position is the pivot point (same as renderX/renderY)
* - Pivot values determine rotation center
*
* 九宫格现在使用与普通矩形相同的坐标系:
* - 位置是 pivot 点(与 renderX/renderY 相同)
* - pivot 值决定旋转中心
*
* @param rt - Render transform data
* @returns Position and pivot for nine-patch rendering
*/
export function getNinePatchPosition(rt: UIRenderTransform): {
x: number;
y: number;
pivotX: number;
pivotY: number;
} {
return {
x: rt.renderX,
y: rt.renderY,
pivotX: rt.pivotX,
pivotY: rt.pivotY
};
}