feat(editor): 添加 ECS UI 系统和编辑器更新优化 (#238)
This commit is contained in:
444
packages/ui/src/systems/UILayoutSystem.ts
Normal file
444
packages/ui/src/systems/UILayoutSystem.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { UITransformComponent } from '../components/UITransformComponent';
|
||||
import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from '../components/UILayoutComponent';
|
||||
|
||||
/**
|
||||
* UI 布局系统
|
||||
* UI Layout System - Computes layout for UI elements
|
||||
*
|
||||
* 计算 UI 元素的世界坐标和尺寸
|
||||
* Computes world coordinates and sizes for UI elements
|
||||
*/
|
||||
@ECSSystem('UILayout')
|
||||
export class UILayoutSystem extends EntitySystem {
|
||||
/**
|
||||
* 视口宽度
|
||||
* Viewport width
|
||||
*/
|
||||
public viewportWidth: number = 1920;
|
||||
|
||||
/**
|
||||
* 视口高度
|
||||
* Viewport height
|
||||
*/
|
||||
public viewportHeight: number = 1080;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(UITransformComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置视口尺寸
|
||||
* Set viewport size
|
||||
*/
|
||||
public setViewport(width: number, height: number): void {
|
||||
this.viewportWidth = width;
|
||||
this.viewportHeight = height;
|
||||
|
||||
// 标记所有元素需要重新布局
|
||||
for (const entity of this.entities) {
|
||||
const transform = entity.getComponent(UITransformComponent);
|
||||
if (transform) {
|
||||
transform.layoutDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 首先处理根元素(没有父元素的)
|
||||
const rootEntities = entities.filter(e => !e.parent || !e.parent.hasComponent(UITransformComponent));
|
||||
|
||||
for (const entity of rootEntities) {
|
||||
this.layoutEntity(entity, 0, 0, this.viewportWidth, this.viewportHeight, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归布局实体及其子元素
|
||||
* Recursively layout entity and its children
|
||||
*/
|
||||
private layoutEntity(
|
||||
entity: Entity,
|
||||
parentX: number,
|
||||
parentY: number,
|
||||
parentWidth: number,
|
||||
parentHeight: number,
|
||||
parentAlpha: number
|
||||
): void {
|
||||
const transform = entity.getComponent(UITransformComponent);
|
||||
if (!transform) return;
|
||||
|
||||
// 计算锚点位置
|
||||
const anchorMinX = parentX + parentWidth * transform.anchorMinX;
|
||||
const anchorMinY = parentY + parentHeight * transform.anchorMinY;
|
||||
const anchorMaxX = parentX + parentWidth * transform.anchorMaxX;
|
||||
const anchorMaxY = parentY + parentHeight * transform.anchorMaxY;
|
||||
|
||||
// 计算元素尺寸
|
||||
let width: number;
|
||||
let height: number;
|
||||
|
||||
// 如果锚点 min 和 max 相同,使用固定尺寸
|
||||
if (transform.anchorMinX === transform.anchorMaxX) {
|
||||
width = transform.width;
|
||||
} else {
|
||||
// 拉伸模式:尺寸由锚点决定
|
||||
width = anchorMaxX - anchorMinX - transform.x;
|
||||
}
|
||||
|
||||
if (transform.anchorMinY === transform.anchorMaxY) {
|
||||
height = transform.height;
|
||||
} else {
|
||||
height = anchorMaxY - anchorMinY - transform.y;
|
||||
}
|
||||
|
||||
// 应用尺寸约束
|
||||
if (transform.minWidth > 0) width = Math.max(width, transform.minWidth);
|
||||
if (transform.maxWidth > 0) width = Math.min(width, transform.maxWidth);
|
||||
if (transform.minHeight > 0) height = Math.max(height, transform.minHeight);
|
||||
if (transform.maxHeight > 0) height = Math.min(height, transform.maxHeight);
|
||||
|
||||
// 计算世界位置
|
||||
let worldX: number;
|
||||
let worldY: number;
|
||||
|
||||
if (transform.anchorMinX === transform.anchorMaxX) {
|
||||
// 固定锚点模式
|
||||
worldX = anchorMinX + transform.x - width * transform.pivotX;
|
||||
} else {
|
||||
// 拉伸模式
|
||||
worldX = anchorMinX + transform.x;
|
||||
}
|
||||
|
||||
if (transform.anchorMinY === transform.anchorMaxY) {
|
||||
worldY = anchorMinY + transform.y - height * transform.pivotY;
|
||||
} else {
|
||||
worldY = anchorMinY + transform.y;
|
||||
}
|
||||
|
||||
// 更新计算后的值
|
||||
transform.worldX = worldX;
|
||||
transform.worldY = worldY;
|
||||
transform.computedWidth = width;
|
||||
transform.computedHeight = height;
|
||||
transform.worldAlpha = parentAlpha * transform.alpha;
|
||||
transform.layoutDirty = false;
|
||||
|
||||
// 如果元素不可见,跳过子元素
|
||||
if (!transform.visible) return;
|
||||
|
||||
// 处理子元素布局
|
||||
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
|
||||
if (children.length === 0) return;
|
||||
|
||||
// 检查是否有布局组件
|
||||
const layout = entity.getComponent(UILayoutComponent);
|
||||
if (layout && layout.type !== UILayoutType.None) {
|
||||
this.layoutChildren(layout, transform, children);
|
||||
} else {
|
||||
// 无布局组件,直接递归处理子元素
|
||||
for (const child of children) {
|
||||
this.layoutEntity(
|
||||
child,
|
||||
worldX,
|
||||
worldY,
|
||||
width,
|
||||
height,
|
||||
transform.worldAlpha
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布局组件布局子元素
|
||||
* Layout children according to layout component
|
||||
*/
|
||||
private layoutChildren(
|
||||
layout: UILayoutComponent,
|
||||
parentTransform: UITransformComponent,
|
||||
children: Entity[]
|
||||
): void {
|
||||
const contentStartX = parentTransform.worldX + layout.paddingLeft;
|
||||
const contentStartY = parentTransform.worldY + layout.paddingTop;
|
||||
const contentWidth = parentTransform.computedWidth - layout.getHorizontalPadding();
|
||||
const contentHeight = parentTransform.computedHeight - layout.getVerticalPadding();
|
||||
|
||||
switch (layout.type) {
|
||||
case UILayoutType.Horizontal:
|
||||
this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
|
||||
break;
|
||||
case UILayoutType.Vertical:
|
||||
this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
|
||||
break;
|
||||
case UILayoutType.Grid:
|
||||
this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
|
||||
break;
|
||||
default:
|
||||
// 默认按正常方式递归
|
||||
for (const child of children) {
|
||||
this.layoutEntity(
|
||||
child,
|
||||
parentTransform.worldX,
|
||||
parentTransform.worldY,
|
||||
parentTransform.computedWidth,
|
||||
parentTransform.computedHeight,
|
||||
parentTransform.worldAlpha
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 水平布局
|
||||
* Horizontal layout
|
||||
*/
|
||||
private layoutHorizontal(
|
||||
layout: UILayoutComponent,
|
||||
parentTransform: UITransformComponent,
|
||||
children: Entity[],
|
||||
startX: number,
|
||||
startY: number,
|
||||
contentWidth: number,
|
||||
contentHeight: number
|
||||
): void {
|
||||
// 计算总子元素宽度
|
||||
const childSizes = children.map(child => {
|
||||
const t = child.getComponent(UITransformComponent)!;
|
||||
return { entity: child, width: t.width, height: t.height };
|
||||
});
|
||||
|
||||
const totalChildWidth = childSizes.reduce((sum, c) => sum + c.width, 0);
|
||||
const totalGap = layout.gap * (children.length - 1);
|
||||
const totalWidth = totalChildWidth + totalGap;
|
||||
|
||||
// 计算起始位置(基于 justifyContent)
|
||||
let offsetX = startX;
|
||||
let gap = layout.gap;
|
||||
|
||||
switch (layout.justifyContent) {
|
||||
case UIJustifyContent.Center:
|
||||
offsetX = startX + (contentWidth - totalWidth) / 2;
|
||||
break;
|
||||
case UIJustifyContent.End:
|
||||
offsetX = startX + contentWidth - totalWidth;
|
||||
break;
|
||||
case UIJustifyContent.SpaceBetween:
|
||||
if (children.length > 1) {
|
||||
gap = (contentWidth - totalChildWidth) / (children.length - 1);
|
||||
}
|
||||
break;
|
||||
case UIJustifyContent.SpaceAround:
|
||||
if (children.length > 0) {
|
||||
const space = (contentWidth - totalChildWidth) / children.length;
|
||||
gap = space;
|
||||
offsetX = startX + space / 2;
|
||||
}
|
||||
break;
|
||||
case UIJustifyContent.SpaceEvenly:
|
||||
if (children.length > 0) {
|
||||
const space = (contentWidth - totalChildWidth) / (children.length + 1);
|
||||
gap = space;
|
||||
offsetX = startX + space;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 布局每个子元素
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]!;
|
||||
const childTransform = child.getComponent(UITransformComponent)!;
|
||||
const size = childSizes[i]!;
|
||||
|
||||
// 计算 Y 位置(基于 alignItems)
|
||||
let childY = startY;
|
||||
let childHeight = size.height;
|
||||
|
||||
switch (layout.alignItems) {
|
||||
case UIAlignItems.Center:
|
||||
childY = startY + (contentHeight - childHeight) / 2;
|
||||
break;
|
||||
case UIAlignItems.End:
|
||||
childY = startY + contentHeight - childHeight;
|
||||
break;
|
||||
case UIAlignItems.Stretch:
|
||||
childHeight = contentHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
// 直接设置子元素的世界坐标
|
||||
childTransform.worldX = offsetX;
|
||||
childTransform.worldY = childY;
|
||||
childTransform.computedWidth = size.width;
|
||||
childTransform.computedHeight = childHeight;
|
||||
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
||||
childTransform.layoutDirty = false;
|
||||
|
||||
// 递归处理子元素的子元素
|
||||
this.processChildrenRecursive(child, childTransform);
|
||||
|
||||
offsetX += size.width + gap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 垂直布局
|
||||
* Vertical layout
|
||||
*/
|
||||
private layoutVertical(
|
||||
layout: UILayoutComponent,
|
||||
parentTransform: UITransformComponent,
|
||||
children: Entity[],
|
||||
startX: number,
|
||||
startY: number,
|
||||
contentWidth: number,
|
||||
contentHeight: number
|
||||
): void {
|
||||
// 计算总子元素高度
|
||||
const childSizes = children.map(child => {
|
||||
const t = child.getComponent(UITransformComponent)!;
|
||||
return { entity: child, width: t.width, height: t.height };
|
||||
});
|
||||
|
||||
const totalChildHeight = childSizes.reduce((sum, c) => sum + c.height, 0);
|
||||
const totalGap = layout.gap * (children.length - 1);
|
||||
const totalHeight = totalChildHeight + totalGap;
|
||||
|
||||
// 计算起始位置
|
||||
let offsetY = startY;
|
||||
let gap = layout.gap;
|
||||
|
||||
switch (layout.justifyContent) {
|
||||
case UIJustifyContent.Center:
|
||||
offsetY = startY + (contentHeight - totalHeight) / 2;
|
||||
break;
|
||||
case UIJustifyContent.End:
|
||||
offsetY = startY + contentHeight - totalHeight;
|
||||
break;
|
||||
case UIJustifyContent.SpaceBetween:
|
||||
if (children.length > 1) {
|
||||
gap = (contentHeight - totalChildHeight) / (children.length - 1);
|
||||
}
|
||||
break;
|
||||
case UIJustifyContent.SpaceAround:
|
||||
if (children.length > 0) {
|
||||
const space = (contentHeight - totalChildHeight) / children.length;
|
||||
gap = space;
|
||||
offsetY = startY + space / 2;
|
||||
}
|
||||
break;
|
||||
case UIJustifyContent.SpaceEvenly:
|
||||
if (children.length > 0) {
|
||||
const space = (contentHeight - totalChildHeight) / (children.length + 1);
|
||||
gap = space;
|
||||
offsetY = startY + space;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 布局每个子元素
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]!;
|
||||
const childTransform = child.getComponent(UITransformComponent)!;
|
||||
const size = childSizes[i]!;
|
||||
|
||||
// 计算 X 位置
|
||||
let childX = startX;
|
||||
let childWidth = size.width;
|
||||
|
||||
switch (layout.alignItems) {
|
||||
case UIAlignItems.Center:
|
||||
childX = startX + (contentWidth - childWidth) / 2;
|
||||
break;
|
||||
case UIAlignItems.End:
|
||||
childX = startX + contentWidth - childWidth;
|
||||
break;
|
||||
case UIAlignItems.Stretch:
|
||||
childWidth = contentWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
childTransform.worldX = childX;
|
||||
childTransform.worldY = offsetY;
|
||||
childTransform.computedWidth = childWidth;
|
||||
childTransform.computedHeight = size.height;
|
||||
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
||||
childTransform.layoutDirty = false;
|
||||
|
||||
this.processChildrenRecursive(child, childTransform);
|
||||
|
||||
offsetY += size.height + gap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格布局
|
||||
* Grid layout
|
||||
*/
|
||||
private layoutGrid(
|
||||
layout: UILayoutComponent,
|
||||
parentTransform: UITransformComponent,
|
||||
children: Entity[],
|
||||
startX: number,
|
||||
startY: number,
|
||||
contentWidth: number,
|
||||
_contentHeight: number
|
||||
): void {
|
||||
const columns = layout.columns;
|
||||
const gapX = layout.getHorizontalGap();
|
||||
const gapY = layout.getVerticalGap();
|
||||
|
||||
// 计算单元格尺寸
|
||||
const cellWidth = layout.cellWidth > 0
|
||||
? layout.cellWidth
|
||||
: (contentWidth - gapX * (columns - 1)) / columns;
|
||||
const cellHeight = layout.cellHeight > 0
|
||||
? layout.cellHeight
|
||||
: cellWidth; // 默认正方形
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]!;
|
||||
const childTransform = child.getComponent(UITransformComponent)!;
|
||||
|
||||
const col = i % columns;
|
||||
const row = Math.floor(i / columns);
|
||||
|
||||
const x = startX + col * (cellWidth + gapX);
|
||||
const y = startY + row * (cellHeight + gapY);
|
||||
|
||||
childTransform.worldX = x;
|
||||
childTransform.worldY = y;
|
||||
childTransform.computedWidth = cellWidth;
|
||||
childTransform.computedHeight = cellHeight;
|
||||
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
||||
childTransform.layoutDirty = false;
|
||||
|
||||
this.processChildrenRecursive(child, childTransform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归处理子元素
|
||||
* Recursively process children
|
||||
*/
|
||||
private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void {
|
||||
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
|
||||
if (children.length === 0) return;
|
||||
|
||||
const layout = entity.getComponent(UILayoutComponent);
|
||||
if (layout && layout.type !== UILayoutType.None) {
|
||||
this.layoutChildren(layout, parentTransform, children);
|
||||
} else {
|
||||
for (const child of children) {
|
||||
this.layoutEntity(
|
||||
child,
|
||||
parentTransform.worldX,
|
||||
parentTransform.worldY,
|
||||
parentTransform.computedWidth,
|
||||
parentTransform.computedHeight,
|
||||
parentTransform.worldAlpha
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user