Feature/editor optimization (#251)

* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
YHH
2025-12-01 22:28:51 +08:00
committed by GitHub
parent 189714c727
commit b42a7b4e43
468 changed files with 18301 additions and 9075 deletions

View File

@@ -1,7 +1,20 @@
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import { EntitySystem, Matcher, Entity, ECSSystem, HierarchyComponent } from '@esengine/ecs-framework';
import { UITransformComponent } from '../components/UITransformComponent';
import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from '../components/UILayoutComponent';
/**
* 2D 变换矩阵类型
* 2D transformation matrix type
*/
interface Matrix2D {
a: number; // scaleX * cos(rotation)
b: number; // scaleX * sin(rotation)
c: number; // scaleY * -sin(rotation)
d: number; // scaleY * cos(rotation)
tx: number; // translateX
ty: number; // translateY
}
/**
* UI 布局系统
* UI Layout System - Computes layout for UI elements
@@ -9,6 +22,9 @@ import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from
* 计算 UI 元素的世界坐标和尺寸
* Computes world coordinates and sizes for UI elements
*
* 使用矩阵乘法计算世界变换worldMatrix = parentMatrix * localMatrix
* Uses matrix multiplication for world transforms: worldMatrix = parentMatrix * localMatrix
*
* 注意canvasWidth/canvasHeight 是 UI 设计的参考尺寸,不是实际渲染视口大小
* Note: canvasWidth/canvasHeight is the UI design reference size, not the actual render viewport size
*/
@@ -60,7 +76,14 @@ export class UILayoutSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
// 首先处理根元素(没有父元素的)
const rootEntities = entities.filter(e => !e.parent || !e.parent.hasComponent(UITransformComponent));
const rootEntities = entities.filter(e => {
const hierarchy = e.getComponent(HierarchyComponent);
if (!hierarchy || hierarchy.parentId === null) {
return true;
}
const parent = this.scene?.findEntityById(hierarchy.parentId);
return !parent || !parent.hasComponent(UITransformComponent);
});
// 画布中心为原点Y 轴向上为正
// Canvas center is origin, Y axis points up
@@ -69,8 +92,11 @@ export class UILayoutSystem extends EntitySystem {
const parentX = -this.canvasWidth / 2;
const parentY = this.canvasHeight / 2; // Y 轴向上,所以顶部是正值
// 根元素使用单位矩阵作为父矩阵
const identityMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
for (const entity of rootEntities) {
this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1);
this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1, identityMatrix);
}
}
@@ -84,7 +110,8 @@ export class UILayoutSystem extends EntitySystem {
parentY: number,
parentWidth: number,
parentHeight: number,
parentAlpha: number
parentAlpha: number,
parentMatrix: Matrix2D
): void {
const transform = entity.getComponent(UITransformComponent);
if (!transform) return;
@@ -160,19 +187,23 @@ export class UILayoutSystem extends EntitySystem {
worldY = anchorMaxY - transform.y;
}
// 更新计算的值
// 更新布局计算的值
transform.worldX = worldX;
transform.worldY = worldY;
transform.computedWidth = width;
transform.computedHeight = height;
transform.worldAlpha = parentAlpha * transform.alpha;
// 使用矩阵乘法计算世界变换
this.updateWorldMatrix(transform, parentMatrix);
transform.layoutDirty = false;
// 如果元素不可见,跳过子元素
if (!transform.visible) return;
// 处理子元素布局
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
const children = this.getUIChildren(entity);
if (children.length === 0) return;
// 计算子元素的父容器边界
@@ -192,7 +223,8 @@ export class UILayoutSystem extends EntitySystem {
childParentY,
width,
height,
transform.worldAlpha
transform.worldAlpha,
transform.localToWorldMatrix
);
}
}
@@ -234,7 +266,8 @@ export class UILayoutSystem extends EntitySystem {
parentTopY,
parentTransform.computedWidth,
parentTransform.computedHeight,
parentTransform.worldAlpha
parentTransform.worldAlpha,
parentTransform.localToWorldMatrix
);
}
}
@@ -328,6 +361,8 @@ export class UILayoutSystem extends EntitySystem {
childTransform.computedWidth = size.width;
childTransform.computedHeight = childHeight;
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false;
// 递归处理子元素的子元素
@@ -424,6 +459,8 @@ export class UILayoutSystem extends EntitySystem {
childTransform.computedWidth = childWidth;
childTransform.computedHeight = size.height;
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false;
this.processChildrenRecursive(child, childTransform);
@@ -478,18 +515,49 @@ export class UILayoutSystem extends EntitySystem {
childTransform.computedWidth = cellWidth;
childTransform.computedHeight = cellHeight;
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false;
this.processChildrenRecursive(child, childTransform);
}
}
/**
* 获取具有 UITransformComponent 的子实体
* Get child entities that have UITransformComponent
*
* 优先使用 HierarchyComponent如果没有则返回空数组
*/
private getUIChildren(entity: Entity): Entity[] {
const hierarchy = entity.getComponent(HierarchyComponent);
// 如果没有 HierarchyComponent返回空数组
// UI 实体应该通过 UIBuilder 创建,会自动添加 HierarchyComponent
if (!hierarchy) {
return [];
}
if (hierarchy.childIds.length === 0) {
return [];
}
const children: Entity[] = [];
for (const childId of hierarchy.childIds) {
const child = this.scene?.findEntityById(childId);
if (child && child.hasComponent(UITransformComponent)) {
children.push(child);
}
}
return children;
}
/**
* 递归处理子元素
* Recursively process children
*/
private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void {
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
const children = this.getUIChildren(entity);
if (children.length === 0) return;
// 计算子元素的父容器顶部 YworldY 是底部,顶部 = 底部 + 高度)
@@ -506,9 +574,129 @@ export class UILayoutSystem extends EntitySystem {
parentTopY,
parentTransform.computedWidth,
parentTransform.computedHeight,
parentTransform.worldAlpha
parentTransform.worldAlpha,
parentTransform.localToWorldMatrix
);
}
}
}
// ===== 矩阵计算方法 Matrix calculation methods =====
/**
* 计算本地变换矩阵
* Calculate local transformation matrix
*
* @param pivotX - 轴心点 X (0-1)
* @param pivotY - 轴心点 Y (0-1)
* @param width - 元素宽度
* @param height - 元素高度
* @param rotation - 旋转角度(弧度)
* @param scaleX - X 缩放
* @param scaleY - Y 缩放
* @param x - 元素世界 X 位置
* @param y - 元素世界 Y 位置
*/
private calculateLocalMatrix(
pivotX: number,
pivotY: number,
width: number,
height: number,
rotation: number,
scaleX: number,
scaleY: number,
x: number,
y: number
): Matrix2D {
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
// 轴心点相对于元素左下角的偏移
const px = width * pivotX;
const py = height * pivotY;
// 构建变换矩阵: Translate(-pivot) -> Scale -> Rotate -> Translate(position + pivot)
// 最终矩阵将轴心点作为旋转/缩放中心
return {
a: scaleX * cos,
b: scaleX * sin,
c: scaleY * -sin,
d: scaleY * cos,
tx: x + px - (scaleX * cos * px - scaleY * sin * py),
ty: y + py - (scaleX * sin * px + scaleY * cos * py)
};
}
/**
* 矩阵乘法: result = a * b
* Matrix multiplication: result = a * b
*/
private multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D {
return {
a: a.a * b.a + a.c * b.b,
b: a.b * b.a + a.d * b.b,
c: a.a * b.c + a.c * b.d,
d: a.b * b.c + a.d * b.d,
tx: a.a * b.tx + a.c * b.ty + a.tx,
ty: a.b * b.tx + a.d * b.ty + a.ty
};
}
/**
* 从世界矩阵分解出旋转和缩放
* Decompose rotation and scale from world matrix
*/
private decomposeMatrix(m: Matrix2D): { rotation: number; scaleX: number; scaleY: number } {
// 计算缩放
const scaleX = Math.sqrt(m.a * m.a + m.b * m.b);
const scaleY = Math.sqrt(m.c * m.c + m.d * m.d);
// 检测负缩放(通过行列式符号)
const det = m.a * m.d - m.b * m.c;
const sign = det < 0 ? -1 : 1;
// 计算旋转(从归一化的矩阵)
let rotation = 0;
if (scaleX > 1e-10) {
rotation = Math.atan2(m.b / scaleX, m.a / scaleX);
}
return {
rotation,
scaleX,
scaleY: scaleY * sign
};
}
/**
* 更新元素的世界变换矩阵
* Update element's world transformation matrix
*/
private updateWorldMatrix(transform: UITransformComponent, parentMatrix: Matrix2D | null): void {
// 计算本地矩阵
const localMatrix = this.calculateLocalMatrix(
transform.pivotX,
transform.pivotY,
transform.computedWidth,
transform.computedHeight,
transform.rotation,
transform.scaleX,
transform.scaleY,
transform.worldX,
transform.worldY
);
// 计算世界矩阵
if (parentMatrix) {
transform.localToWorldMatrix = this.multiplyMatrices(parentMatrix, localMatrix);
} else {
transform.localToWorldMatrix = localMatrix;
}
// 从世界矩阵分解出世界旋转和缩放
const decomposed = this.decomposeMatrix(transform.localToWorldMatrix);
transform.worldRotation = decomposed.rotation;
transform.worldScaleX = decomposed.scaleX;
transform.worldScaleY = decomposed.scaleY;
}
}

View File

@@ -48,10 +48,20 @@ export class UIButtonRenderSystem extends EntitySystem {
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 scaleX = transform.worldScaleX ?? transform.scaleX;
const scaleY = transform.worldScaleY ?? transform.scaleY;
const rotation = transform.worldRotation ?? transform.rotation;
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
const baseOrder = 100 + transform.zIndex;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
// 渲染位置 = 左下角 + pivot 偏移
const renderX = x + width * pivotX;
const renderY = y + height * pivotY;
// Render texture if in texture or both mode
// 如果在纹理或两者模式下,渲染纹理
@@ -59,15 +69,15 @@ export class UIButtonRenderSystem extends EntitySystem {
const texture = button.getStateTexture('normal');
if (texture) {
collector.addRect(
x, y,
renderX, renderY,
width, height,
0xFFFFFF, // White tint for texture
alpha,
baseOrder,
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0,
rotation,
pivotX,
pivotY,
texturePath: texture
}
);
@@ -80,15 +90,15 @@ export class UIButtonRenderSystem extends EntitySystem {
const bgAlpha = render?.backgroundAlpha ?? 1;
if (bgAlpha > 0) {
collector.addRect(
x, y,
renderX, renderY,
width, height,
button.currentColor,
bgAlpha * alpha,
baseOrder + (button.useTexture() ? 0.05 : 0),
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0
rotation,
pivotX,
pivotY
}
);
}
@@ -99,61 +109,72 @@ export class UIButtonRenderSystem extends EntitySystem {
if (render && render.borderWidth > 0 && render.borderAlpha > 0) {
this.renderBorder(
collector,
x, y, width, height,
renderX, renderY, width, height,
render.borderWidth,
render.borderColor,
render.borderAlpha * alpha,
baseOrder + 0.1,
transform.rotation
rotation,
pivotX,
pivotY
);
}
}
}
/**
* Render border using top-left coordinates
* 使用左上角坐标渲染边框
* Render border using pivot-based coordinates
* 使用基于 pivot 的坐标渲染边框
*/
private renderBorder(
collector: ReturnType<typeof getUIRenderCollector>,
x: number, y: number,
centerX: number, centerY: number,
width: number, height: number,
borderWidth: number,
borderColor: number,
alpha: number,
sortOrder: number,
rotation: number
rotation: number,
pivotX: number,
pivotY: number
): void {
// 计算矩形的边界(相对于 pivot 中心)
const left = centerX - width * pivotX;
const bottom = centerY - height * pivotY;
const right = left + width;
const top = bottom + height;
// Top border
collector.addRect(
x, y,
(left + right) / 2, top - borderWidth / 2,
width, borderWidth,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Bottom border
collector.addRect(
x, y + height - borderWidth,
(left + right) / 2, bottom + borderWidth / 2,
width, borderWidth,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Left border (excluding corners)
const sideBorderHeight = height - borderWidth * 2;
collector.addRect(
x, y + borderWidth,
borderWidth, height - borderWidth * 2,
left + borderWidth / 2, (top + bottom) / 2,
borderWidth, sideBorderHeight,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Right border (excluding corners)
collector.addRect(
x + width - borderWidth, y + borderWidth,
borderWidth, height - borderWidth * 2,
right - borderWidth / 2, (top + bottom) / 2,
borderWidth, sideBorderHeight,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
}

View File

@@ -45,23 +45,33 @@ export class UIProgressBarRenderSystem extends EntitySystem {
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 scaleX = transform.worldScaleX ?? transform.scaleX;
const scaleY = transform.worldScaleY ?? transform.scaleY;
const rotation = transform.worldRotation ?? transform.rotation;
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
const baseOrder = 100 + transform.zIndex;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
// 渲染位置 = 左下角 + pivot 偏移
const renderX = x + width * pivotX;
const renderY = y + height * pivotY;
// Render background (x, y is top-left corner)
// 渲染背景x, y 是左上角)
// Render background
// 渲染背景
if (progressBar.backgroundAlpha > 0) {
collector.addRect(
x, y, width, height,
renderX, renderY, width, height,
progressBar.backgroundColor,
progressBar.backgroundAlpha * alpha,
baseOrder,
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0
rotation,
pivotX,
pivotY
}
);
}
@@ -70,12 +80,14 @@ export class UIProgressBarRenderSystem extends EntitySystem {
// 渲染边框
if (progressBar.borderWidth > 0) {
this.renderBorder(
collector, x, y, width, height,
collector, renderX, renderY, width, height,
progressBar.borderWidth,
progressBar.borderColor,
alpha,
baseOrder + 0.2,
transform
transform,
pivotX,
pivotY
);
}
@@ -85,13 +97,15 @@ export class UIProgressBarRenderSystem extends EntitySystem {
if (progress > 0 && progressBar.fillAlpha > 0) {
if (progressBar.showSegments) {
this.renderSegmentedFill(
collector, x, y, width, height,
progress, progressBar, alpha, baseOrder + 0.1, transform
collector, renderX, renderY, width, height,
progress, progressBar, alpha, baseOrder + 0.1, transform,
pivotX, pivotY
);
} else {
this.renderSolidFill(
collector, x, y, width, height,
progress, progressBar, alpha, baseOrder + 0.1, transform
collector, renderX, renderY, width, height,
progress, progressBar, alpha, baseOrder + 0.1, transform,
pivotX, pivotY
);
}
}
@@ -102,57 +116,67 @@ export class UIProgressBarRenderSystem extends EntitySystem {
* Render solid fill rectangle
* 渲染实心填充矩形
*
* Note: x, y is the top-left corner of the progress bar
* 注意:x, y 是进度条的左上角
* Note: centerX, centerY is the pivot position of the progress bar
* 注意:centerX, centerY 是进度条的 pivot 位置
*/
private renderSolidFill(
collector: ReturnType<typeof getUIRenderCollector>,
x: number, y: number, width: number, height: number,
centerX: number, centerY: number, width: number, height: number,
progress: number,
progressBar: UIProgressBarComponent,
alpha: number,
sortOrder: number,
transform: UITransformComponent
transform: UITransformComponent,
pivotX: number,
pivotY: number
): void {
let fillX = x;
let fillY = y;
const rotation = transform.worldRotation ?? transform.rotation;
// 计算进度条的边界(相对于 pivot 中心)
const left = centerX - width * pivotX;
const bottom = centerY - height * pivotY;
let fillX: number;
let fillY: number;
let fillWidth = width;
let fillHeight = height;
// Calculate fill dimensions based on direction
// x, y is top-left corner, so calculations are simpler
// 根据方向计算填充尺寸
// x, y 是左上角,所以计算更简单
switch (progressBar.direction) {
case UIProgressDirection.LeftToRight:
fillWidth = width * progress;
// Fill starts from left (fillX = x, no change)
fillX = left + fillWidth / 2;
fillY = bottom + height / 2;
break;
case UIProgressDirection.RightToLeft:
fillWidth = width * progress;
// Fill starts from right
fillX = x + width - fillWidth;
fillX = left + width - fillWidth / 2;
fillY = bottom + height / 2;
break;
case UIProgressDirection.BottomToTop:
fillHeight = height * progress;
// Fill starts from bottom
fillY = y + height - fillHeight;
fillX = left + width / 2;
fillY = bottom + fillHeight / 2;
break;
case UIProgressDirection.TopToBottom:
fillHeight = height * progress;
// Fill starts from top (fillY = y, no change)
fillX = left + width / 2;
fillY = bottom + height - fillHeight / 2;
break;
default:
fillX = left + fillWidth / 2;
fillY = bottom + height / 2;
}
// Determine fill color (gradient or solid)
// 确定填充颜色(渐变或实心)
let fillColor = progressBar.fillColor;
if (progressBar.useGradient) {
// Simple linear interpolation between start and end colors
// 简单的起始和结束颜色线性插值
fillColor = this.lerpColor(
progressBar.gradientStartColor,
progressBar.gradientEndColor,
@@ -166,9 +190,9 @@ export class UIProgressBarRenderSystem extends EntitySystem {
progressBar.fillAlpha * alpha,
sortOrder,
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0
rotation,
pivotX: 0.5,
pivotY: 0.5
}
);
}
@@ -177,18 +201,21 @@ export class UIProgressBarRenderSystem extends EntitySystem {
* Render segmented fill
* 渲染分段填充
*
* Note: x, y is the top-left corner of the progress bar
* 注意:x, y 是进度条的左上角
* Note: centerX, centerY is the pivot position of the progress bar
* 注意:centerX, centerY 是进度条的 pivot 位置
*/
private renderSegmentedFill(
collector: ReturnType<typeof getUIRenderCollector>,
x: number, y: number, width: number, height: number,
centerX: number, centerY: number, width: number, height: number,
progress: number,
progressBar: UIProgressBarComponent,
alpha: number,
sortOrder: number,
transform: UITransformComponent
transform: UITransformComponent,
pivotX: number,
pivotY: number
): void {
const rotation = transform.worldRotation ?? transform.rotation;
const segments = progressBar.segments;
const gap = progressBar.segmentGap;
const filledSegments = Math.ceil(progress * segments);
@@ -196,6 +223,10 @@ export class UIProgressBarRenderSystem extends EntitySystem {
const isHorizontal = progressBar.direction === UIProgressDirection.LeftToRight ||
progressBar.direction === UIProgressDirection.RightToLeft;
// 计算进度条的边界(相对于 pivot 中心)
const left = centerX - width * pivotX;
const bottom = centerY - height * pivotY;
// Calculate segment dimensions
// 计算段尺寸
let segmentWidth: number;
@@ -209,41 +240,36 @@ export class UIProgressBarRenderSystem extends EntitySystem {
segmentHeight = (height - gap * (segments - 1)) / segments;
}
// x, y is already top-left corner
// x, y 已经是左上角
const baseX = x;
const baseY = y;
for (let i = 0; i < filledSegments && i < segments; i++) {
let segX: number;
let segY: number;
let segCenterX: number;
let segCenterY: number;
// Calculate segment position based on direction (using top-left positions)
// 根据方向计算段位置(使用左上角位置)
// Calculate segment center position based on direction
// 根据方向计算段中心位置
switch (progressBar.direction) {
case UIProgressDirection.LeftToRight:
segX = baseX + i * (segmentWidth + gap);
segY = baseY;
segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2;
segCenterY = bottom + height / 2;
break;
case UIProgressDirection.RightToLeft:
segX = baseX + width - (i + 1) * segmentWidth - i * gap;
segY = baseY;
segCenterX = left + width - i * (segmentWidth + gap) - segmentWidth / 2;
segCenterY = bottom + height / 2;
break;
case UIProgressDirection.TopToBottom:
segX = baseX;
segY = baseY + i * (segmentHeight + gap);
segCenterX = left + width / 2;
segCenterY = bottom + height - i * (segmentHeight + gap) - segmentHeight / 2;
break;
case UIProgressDirection.BottomToTop:
segX = baseX;
segY = baseY + height - (i + 1) * segmentHeight - i * gap;
segCenterX = left + width / 2;
segCenterY = bottom + i * (segmentHeight + gap) + segmentHeight / 2;
break;
default:
segX = baseX + i * (segmentWidth + gap);
segY = baseY;
segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2;
segCenterY = bottom + height / 2;
}
// Determine segment color
@@ -258,19 +284,19 @@ export class UIProgressBarRenderSystem extends EntitySystem {
);
}
// Use top-left position with pivot 0,0
// 使用左上角位置pivot 0,0
// Use center position with pivot 0.5, 0.5
// 使用中心位置pivot 0.5, 0.5
collector.addRect(
segX, segY,
segCenterX, segCenterY,
segmentWidth,
segmentHeight,
segmentColor,
progressBar.fillAlpha * alpha,
sortOrder + i * 0.001, // Slight offset for each segment
sortOrder + i * 0.001,
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0
rotation,
pivotX: 0.5,
pivotY: 0.5
}
);
}
@@ -280,51 +306,59 @@ export class UIProgressBarRenderSystem extends EntitySystem {
* Render border
* 渲染边框
*
* Note: x, y is the top-left corner of the progress bar
* 注意:x, y 是进度条的左上角
* Note: centerX, centerY is the pivot position of the progress bar
* 注意:centerX, centerY 是进度条的 pivot 位置
*/
private renderBorder(
collector: ReturnType<typeof getUIRenderCollector>,
x: number, y: number, width: number, height: number,
centerX: number, centerY: number, width: number, height: number,
borderWidth: number,
borderColor: number,
alpha: number,
sortOrder: number,
_transform: UITransformComponent
transform: UITransformComponent,
pivotX: number,
pivotY: number
): void {
// x, y is already top-left corner
// x, y 已经是左上角
const rotation = transform.worldRotation ?? transform.rotation;
// 计算边界(相对于 pivot 中心)
const left = centerX - width * pivotX;
const bottom = centerY - height * pivotY;
const right = left + width;
const top = bottom + height;
// Top border
collector.addRect(
x, y,
(left + right) / 2, top - borderWidth / 2,
width, borderWidth,
borderColor, alpha, sortOrder,
{ pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Bottom border
collector.addRect(
x, y + height - borderWidth,
(left + right) / 2, bottom + borderWidth / 2,
width, borderWidth,
borderColor, alpha, sortOrder,
{ pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Left border (excluding corners)
const sideBorderHeight = height - borderWidth * 2;
collector.addRect(
x, y + borderWidth,
borderWidth, height - borderWidth * 2,
left + borderWidth / 2, (top + bottom) / 2,
borderWidth, sideBorderHeight,
borderColor, alpha, sortOrder,
{ pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Right border (excluding corners)
collector.addRect(
x + width - borderWidth, y + borderWidth,
borderWidth, height - borderWidth * 2,
right - borderWidth / 2, (top + bottom) / 2,
borderWidth, sideBorderHeight,
borderColor, alpha, sortOrder,
{ pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}

View File

@@ -56,33 +56,40 @@ export class UIRectRenderSystem extends EntitySystem {
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 scaleX = transform.worldScaleX ?? transform.scaleX;
const scaleY = transform.worldScaleY ?? transform.scaleY;
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
// 使用世界旋转(考虑父级旋转)
const rotation = transform.worldRotation ?? transform.rotation;
const baseOrder = 100 + transform.zIndex;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
// Use top-left position with origin at (0, 0)
// Like Sprite: x,y is anchor position, origin determines where anchor is on the rect
// For UI: x,y is top-left corner, so origin should be (0, 0)
// 使用左上角位置,原点在 (0, 0)
// 类似 Spritex,y 是锚点位置origin 决定锚点在矩形上的位置
// 对于 UIx,y 是左上角,所以 origin 应该是 (0, 0)
// worldX/worldY 是元素左下角位置,需要转换为以 pivot 为中心的位置
// pivot 相对于元素的偏移:(width * pivotX, height * pivotY)
// 渲染位置 = 左下角 + pivot 偏移
const renderX = x + width * pivotX;
const renderY = y + height * pivotY;
// Render shadow if enabled
// 如果启用,渲染阴影
if (render.shadowEnabled && render.shadowAlpha > 0) {
collector.addRect(
x + render.shadowOffsetX - render.shadowBlur,
y + render.shadowOffsetY - render.shadowBlur,
renderX + render.shadowOffsetX,
renderY + render.shadowOffsetY,
width + render.shadowBlur * 2,
height + render.shadowBlur * 2,
render.shadowColor,
render.shadowAlpha * alpha,
baseOrder - 0.1,
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0
rotation,
pivotX,
pivotY
}
);
}
@@ -94,15 +101,15 @@ export class UIRectRenderSystem extends EntitySystem {
const textureId = typeof render.texture === 'number' ? render.texture : undefined;
collector.addRect(
x, y,
renderX, renderY,
width, height,
render.textureTint,
alpha,
baseOrder,
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0,
rotation,
pivotX,
pivotY,
textureId,
texturePath,
uv: render.textureUV
@@ -115,15 +122,15 @@ export class UIRectRenderSystem extends EntitySystem {
// 如果启用填充,渲染背景颜色
else if (render.fillBackground && render.backgroundAlpha > 0) {
collector.addRect(
x, y,
renderX, renderY,
width, height,
render.backgroundColor,
render.backgroundAlpha * alpha,
baseOrder,
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0
rotation,
pivotX,
pivotY
}
);
}
@@ -133,61 +140,78 @@ export class UIRectRenderSystem extends EntitySystem {
if (render.borderWidth > 0 && render.borderAlpha > 0) {
this.renderBorder(
collector,
x, y, width, height,
renderX, renderY, width, height,
render.borderWidth,
render.borderColor,
render.borderAlpha * alpha,
baseOrder + 0.1,
transform.rotation
rotation,
pivotX,
pivotY
);
}
}
}
/**
* Render border using top-left coordinates
* 使用左上角坐标渲染边框
* Render border using pivot-based coordinates
* 使用基于 pivot 的坐标渲染边框
*/
private renderBorder(
collector: ReturnType<typeof getUIRenderCollector>,
x: number, y: number,
centerX: number, centerY: number,
width: number, height: number,
borderWidth: number,
borderColor: number,
alpha: number,
sortOrder: number,
rotation: number
rotation: number,
pivotX: number,
pivotY: number
): void {
// Top border (from top-left corner)
// 计算矩形的左下角位置(相对于 pivot 中心)
const left = centerX - width * pivotX;
const bottom = centerY - height * pivotY;
const right = left + width;
const top = bottom + height;
// Top border
const topBorderCenterX = (left + right) / 2;
const topBorderCenterY = top - borderWidth / 2;
collector.addRect(
x, y,
topBorderCenterX, topBorderCenterY,
width, borderWidth,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Bottom border
const bottomBorderCenterY = bottom + borderWidth / 2;
collector.addRect(
x, y + height - borderWidth,
topBorderCenterX, bottomBorderCenterY,
width, borderWidth,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Left border (excluding corners)
const sideBorderHeight = height - borderWidth * 2;
const leftBorderCenterX = left + borderWidth / 2;
const sideBorderCenterY = (top + bottom) / 2;
collector.addRect(
x, y + borderWidth,
borderWidth, height - borderWidth * 2,
leftBorderCenterX, sideBorderCenterY,
borderWidth, sideBorderHeight,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Right border (excluding corners)
const rightBorderCenterX = right - borderWidth / 2;
collector.addRect(
x + width - borderWidth, y + borderWidth,
borderWidth, height - borderWidth * 2,
rightBorderCenterX, sideBorderCenterY,
borderWidth, sideBorderHeight,
borderColor, alpha, sortOrder,
{ rotation, pivotX: 0, pivotY: 0 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
}

View File

@@ -46,15 +46,24 @@ export class UIScrollViewRenderSystem extends EntitySystem {
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 scaleX = transform.worldScaleX ?? transform.scaleX;
const scaleY = transform.worldScaleY ?? transform.scaleY;
const rotation = transform.worldRotation ?? transform.rotation;
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
const baseOrder = 100 + transform.zIndex;
// 使用 transform 的 pivot 计算位置
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
// 渲染位置 = 左下角 + pivot 偏移
const renderX = x + width * pivotX;
const renderY = y + height * pivotY;
// x, y is already top-left corner
// x, y 已经是左上角
const baseX = x;
const baseY = y;
// 计算边界
const baseX = renderX - width * pivotX;
const baseY = renderY - height * pivotY;
// Render vertical scrollbar
// 渲染垂直滚动条
@@ -62,7 +71,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
this.renderVerticalScrollbar(
collector,
baseX, baseY, width, height,
scrollView, alpha, baseOrder
scrollView, alpha, baseOrder, rotation
);
}
@@ -72,7 +81,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
this.renderHorizontalScrollbar(
collector,
baseX, baseY, width, height,
scrollView, alpha, baseOrder
scrollView, alpha, baseOrder, rotation
);
}
}
@@ -88,7 +97,8 @@ export class UIScrollViewRenderSystem extends EntitySystem {
viewWidth: number, viewHeight: number,
scrollView: UIScrollViewComponent,
alpha: number,
baseOrder: number
baseOrder: number,
rotation: number
): void {
const scrollbarWidth = scrollView.scrollbarWidth;
const hasHorizontal = scrollView.needsHorizontalScrollbar(viewWidth);
@@ -108,7 +118,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
scrollView.scrollbarTrackColor,
scrollView.scrollbarTrackAlpha * alpha,
baseOrder + 0.5,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
@@ -131,7 +141,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
scrollView.scrollbarColor,
handleAlpha * alpha,
baseOrder + 0.6,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
@@ -145,7 +155,8 @@ export class UIScrollViewRenderSystem extends EntitySystem {
viewWidth: number, viewHeight: number,
scrollView: UIScrollViewComponent,
alpha: number,
baseOrder: number
baseOrder: number,
rotation: number
): void {
const scrollbarWidth = scrollView.scrollbarWidth;
const hasVertical = scrollView.needsVerticalScrollbar(viewHeight);
@@ -165,7 +176,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
scrollView.scrollbarTrackColor,
scrollView.scrollbarTrackAlpha * alpha,
baseOrder + 0.5,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
@@ -188,7 +199,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
scrollView.scrollbarColor,
handleAlpha * alpha,
baseOrder + 0.6,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
}

View File

@@ -45,10 +45,20 @@ export class UISliderRenderSystem extends EntitySystem {
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 scaleX = transform.worldScaleX ?? transform.scaleX;
const scaleY = transform.worldScaleY ?? transform.scaleY;
const rotation = transform.worldRotation ?? transform.rotation;
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
const baseOrder = 100 + transform.zIndex;
// 使用 transform 的 pivot 计算中心位置
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
// 渲染位置 = 左下角 + pivot 偏移
const renderX = x + width * pivotX;
const renderY = y + height * pivotY;
const isHorizontal = slider.orientation === UISliderOrientation.Horizontal;
const progress = slider.getProgress();
@@ -58,10 +68,10 @@ export class UISliderRenderSystem extends EntitySystem {
const trackLength = isHorizontal ? width : height;
const trackThickness = slider.trackThickness;
// Calculate center position (x, y is top-left corner)
// 计算中心位置x, y 是左上角)
const centerX = x + width / 2;
const centerY = y + height / 2;
// Calculate center position based on pivot
// 基于 pivot 计算中心位置
const centerX = renderX;
const centerY = renderY;
// Render track (using center position with pivot 0.5)
// 渲染轨道使用中心位置pivot 0.5
@@ -73,7 +83,7 @@ export class UISliderRenderSystem extends EntitySystem {
slider.trackColor,
slider.trackAlpha * alpha,
baseOrder,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
} else {
collector.addRect(
@@ -82,7 +92,7 @@ export class UISliderRenderSystem extends EntitySystem {
slider.trackColor,
slider.trackAlpha * alpha,
baseOrder,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
}
@@ -101,7 +111,7 @@ export class UISliderRenderSystem extends EntitySystem {
slider.fillColor,
slider.fillAlpha * alpha,
baseOrder + 0.1,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
} else {
// Fill from bottom
@@ -112,7 +122,7 @@ export class UISliderRenderSystem extends EntitySystem {
slider.fillColor,
slider.fillAlpha * alpha,
baseOrder + 0.1,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
}
@@ -124,7 +134,7 @@ export class UISliderRenderSystem extends EntitySystem {
collector, centerX, centerY,
trackLength, trackThickness,
slider, alpha, baseOrder + 0.05,
isHorizontal
isHorizontal, rotation
);
}
@@ -147,7 +157,7 @@ export class UISliderRenderSystem extends EntitySystem {
0x000000,
0.3 * alpha,
baseOrder + 0.15,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
@@ -159,7 +169,7 @@ export class UISliderRenderSystem extends EntitySystem {
handleColor,
alpha,
baseOrder + 0.2,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Handle border (if any)
@@ -172,7 +182,8 @@ export class UISliderRenderSystem extends EntitySystem {
slider.handleBorderWidth,
slider.handleBorderColor,
alpha,
baseOrder + 0.25
baseOrder + 0.25,
rotation
);
}
}
@@ -189,7 +200,8 @@ export class UISliderRenderSystem extends EntitySystem {
slider: UISliderComponent,
alpha: number,
sortOrder: number,
isHorizontal: boolean
isHorizontal: boolean,
rotation: number
): void {
const tickCount = slider.tickCount + 2; // Include start and end ticks
const tickSize = slider.tickSize;
@@ -220,7 +232,7 @@ export class UISliderRenderSystem extends EntitySystem {
slider.tickColor,
alpha,
sortOrder,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
}
@@ -236,7 +248,8 @@ export class UISliderRenderSystem extends EntitySystem {
borderWidth: number,
borderColor: number,
alpha: number,
sortOrder: number
sortOrder: number,
rotation: number
): void {
const halfW = width / 2;
const halfH = height / 2;
@@ -247,7 +260,7 @@ export class UISliderRenderSystem extends EntitySystem {
x, y - halfH + halfB,
width, borderWidth,
borderColor, alpha, sortOrder,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Bottom
@@ -255,7 +268,7 @@ export class UISliderRenderSystem extends EntitySystem {
x, y + halfH - halfB,
width, borderWidth,
borderColor, alpha, sortOrder,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Left
@@ -263,7 +276,7 @@ export class UISliderRenderSystem extends EntitySystem {
x - halfW + halfB, y,
borderWidth, height - borderWidth * 2,
borderColor, alpha, sortOrder,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
// Right
@@ -271,7 +284,7 @@ export class UISliderRenderSystem extends EntitySystem {
x + halfW - halfB, y,
borderWidth, height - borderWidth * 2,
borderColor, alpha, sortOrder,
{ pivotX: 0.5, pivotY: 0.5 }
{ rotation, pivotX: 0.5, pivotY: 0.5 }
);
}
}

View File

@@ -101,10 +101,20 @@ export class UITextRenderSystem extends EntitySystem {
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 scaleX = transform.worldScaleX ?? transform.scaleX;
const scaleY = transform.worldScaleY ?? transform.scaleY;
const rotation = transform.worldRotation ?? transform.rotation;
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
const baseOrder = 100 + transform.zIndex;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
// 渲染位置 = 左下角 + pivot 偏移
const renderX = x + width * pivotX;
const renderY = y + height * pivotY;
// Generate or retrieve cached texture
// 生成或获取缓存的纹理
@@ -114,18 +124,18 @@ export class UITextRenderSystem extends EntitySystem {
if (textureId === null) continue;
// Use top-left position with origin at (0, 0)
// 使用左上角位置,原点在 (0, 0)
// Use pivot position with transform's pivot values
// 使用 transform 的 pivot 值作为旋转中心
collector.addRect(
x, y,
renderX, renderY,
width, height,
0xFFFFFF, // White tint (color is baked into texture)
alpha,
baseOrder + 1, // Text renders above background
{
rotation: transform.rotation,
pivotX: 0,
pivotY: 0,
rotation,
pivotX,
pivotY,
textureId
}
);