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:
YHH
2025-11-27 20:42:46 +08:00
committed by GitHub
parent 71869b1a58
commit 107439d70c
367 changed files with 10661 additions and 12473 deletions

View File

@@ -8,32 +8,38 @@ import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from
*
* 计算 UI 元素的世界坐标和尺寸
* Computes world coordinates and sizes for UI elements
*
* 注意canvasWidth/canvasHeight 是 UI 设计的参考尺寸,不是实际渲染视口大小
* Note: canvasWidth/canvasHeight is the UI design reference size, not the actual render viewport size
*/
@ECSSystem('UILayout')
export class UILayoutSystem extends EntitySystem {
/**
* 视口宽度
* Viewport width
* UI 画布宽度(设计尺寸)
* UI Canvas width (design size)
*/
public viewportWidth: number = 1920;
public canvasWidth: number = 1920;
/**
* 视口高度
* Viewport height
* UI 画布高度(设计尺寸)
* UI Canvas height (design size)
*/
public viewportHeight: number = 1080;
public canvasHeight: number = 1080;
constructor() {
super(Matcher.empty().all(UITransformComponent));
}
/**
* 设置视口尺寸
* Set viewport size
* 设置 UI 画布尺寸(设计尺寸)
* Set UI canvas size (design size)
*
* 这是 UI 布局计算的参考尺寸,通常是固定的设计分辨率(如 1920x1080
* This is the reference size for UI layout calculation, usually a fixed design resolution (e.g., 1920x1080)
*/
public setViewport(width: number, height: number): void {
this.viewportWidth = width;
this.viewportHeight = height;
public setCanvasSize(width: number, height: number): void {
this.canvasWidth = width;
this.canvasHeight = height;
// 标记所有元素需要重新布局
for (const entity of this.entities) {
@@ -44,12 +50,27 @@ export class UILayoutSystem extends EntitySystem {
}
}
/**
* 获取 UI 画布尺寸
* Get UI canvas size
*/
public getCanvasSize(): { width: number; height: number } {
return { width: this.canvasWidth, height: this.canvasHeight };
}
protected process(entities: readonly Entity[]): void {
// 首先处理根元素(没有父元素的)
const rootEntities = entities.filter(e => !e.parent || !e.parent.hasComponent(UITransformComponent));
// 画布中心为原点Y 轴向上为正
// Canvas center is origin, Y axis points up
// 左上角是 (-width/2, +height/2),右下角是 (+width/2, -height/2)
// Top-left is (-width/2, +height/2), bottom-right is (+width/2, -height/2)
const parentX = -this.canvasWidth / 2;
const parentY = this.canvasHeight / 2; // Y 轴向上,所以顶部是正值
for (const entity of rootEntities) {
this.layoutEntity(entity, 0, 0, this.viewportWidth, this.viewportHeight, 1);
this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1);
}
}
@@ -69,10 +90,16 @@ export class UILayoutSystem extends EntitySystem {
if (!transform) return;
// 计算锚点位置
// X 轴向右为正anchorMinX=0 是左边anchorMinX=1 是右边
// Y 轴向上为正anchorMinY=0 是顶部anchorMinY=1 是底部
// X axis: right is positive, anchorMinX=0 is left, anchorMinX=1 is right
// Y axis: up is positive, anchorMinY=0 is top, anchorMinY=1 is bottom
const anchorMinX = parentX + parentWidth * transform.anchorMinX;
const anchorMinY = parentY + parentHeight * transform.anchorMinY;
const anchorMaxX = parentX + parentWidth * transform.anchorMaxX;
const anchorMaxY = parentY + parentHeight * transform.anchorMaxY;
// Y 轴反转parentY 是顶部(正值),向下减少
// Y axis inverted: parentY is top (positive), decreases downward
const anchorMinY = parentY - parentHeight * transform.anchorMinY;
const anchorMaxY = parentY - parentHeight * transform.anchorMaxY;
// 计算元素尺寸
let width: number;
@@ -89,7 +116,9 @@ export class UILayoutSystem extends EntitySystem {
if (transform.anchorMinY === transform.anchorMaxY) {
height = transform.height;
} else {
height = anchorMaxY - anchorMinY - transform.y;
// 拉伸模式Y 轴反转anchorMinY > anchorMaxY
// Stretch mode: Y axis inverted, anchorMinY > anchorMaxY
height = anchorMinY - anchorMaxY - transform.y;
}
// 应用尺寸约束
@@ -98,12 +127,15 @@ export class UILayoutSystem extends EntitySystem {
if (transform.minHeight > 0) height = Math.max(height, transform.minHeight);
if (transform.maxHeight > 0) height = Math.min(height, transform.maxHeight);
// 计算世界位置
// 计算世界位置(左下角,与 Gizmo origin=(0,0) 对应)
// Calculate world position (bottom-left corner, matching Gizmo origin=(0,0))
let worldX: number;
let worldY: number;
if (transform.anchorMinX === transform.anchorMaxX) {
// 固定锚点模式
// anchor 位置 + position 偏移 - pivot 偏移
// 结果是矩形左边缘的 X 坐标
worldX = anchorMinX + transform.x - width * transform.pivotX;
} else {
// 拉伸模式
@@ -111,9 +143,21 @@ export class UILayoutSystem extends EntitySystem {
}
if (transform.anchorMinY === transform.anchorMaxY) {
worldY = anchorMinY + transform.y - height * transform.pivotY;
// 固定锚点模式Y 轴向上
// Fixed anchor mode: Y axis up
// anchorMinY 是锚点 Y 位置anchor=0 在顶部Y=+540
// position.y 是从锚点的偏移(正值向上)
// pivot 决定元素哪个点对齐到 (anchor + position)
// worldY 是元素底部的 Y 坐标(与 Gizmo origin=(0,0) 对应)
// pivotY=0 意味着元素顶部对齐pivotY=1 意味着元素底部对齐
const anchorPosY = anchorMinY + transform.y; // anchor 位置 + 偏移
// pivotY=0: 顶部对齐,底部 = anchorPos - height
// pivotY=0.5: 中心对齐,底部 = anchorPos - height/2
// pivotY=1: 底部对齐,底部 = anchorPos
worldY = anchorPosY - height * (1 - transform.pivotY);
} else {
worldY = anchorMinY + transform.y;
// 拉伸模式worldY 是底部
worldY = anchorMaxY - transform.y;
}
// 更新计算后的值
@@ -131,6 +175,10 @@ export class UILayoutSystem extends EntitySystem {
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
if (children.length === 0) return;
// 计算子元素的父容器边界
// 子元素的 parentY 应该是当前元素的顶部 Y 坐标worldY 是底部,顶部 = 底部 + 高度)
const childParentY = worldY + height;
// 检查是否有布局组件
const layout = entity.getComponent(UILayoutComponent);
if (layout && layout.type !== UILayoutType.None) {
@@ -141,7 +189,7 @@ export class UILayoutSystem extends EntitySystem {
this.layoutEntity(
child,
worldX,
worldY,
childParentY,
width,
height,
transform.worldAlpha
@@ -160,7 +208,10 @@ export class UILayoutSystem extends EntitySystem {
children: Entity[]
): void {
const contentStartX = parentTransform.worldX + layout.paddingLeft;
const contentStartY = parentTransform.worldY + layout.paddingTop;
// Y-up 系统worldY 是底部,顶部 = worldY + height
// contentStartY 是内容区域的顶部 Y从顶部减去 paddingTop
const parentTopY = parentTransform.worldY + parentTransform.computedHeight;
const contentStartY = parentTopY - layout.paddingTop;
const contentWidth = parentTransform.computedWidth - layout.getHorizontalPadding();
const contentHeight = parentTransform.computedHeight - layout.getVerticalPadding();
@@ -175,12 +226,12 @@ export class UILayoutSystem extends EntitySystem {
this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
break;
default:
// 默认按正常方式递归
// 默认按正常方式递归(传递顶部 Y
for (const child of children) {
this.layoutEntity(
child,
parentTransform.worldX,
parentTransform.worldY,
parentTopY,
parentTransform.computedWidth,
parentTransform.computedHeight,
parentTransform.worldAlpha
@@ -245,30 +296,35 @@ export class UILayoutSystem extends EntitySystem {
}
// 布局每个子元素
// startY 是内容区域的顶部 YY-up 系统)
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;
// 计算子元素顶部 Y 位置(基于 alignItems
// startY 是内容区域顶部,向下布局意味着 Y 值减小
let childTopY = startY; // 默认从顶部开始
let childHeight = size.height;
switch (layout.alignItems) {
case UIAlignItems.Center:
childY = startY + (contentHeight - childHeight) / 2;
// 在内容区域垂直居中:顶部 Y = startY - (contentHeight - childHeight) / 2
childTopY = startY - (contentHeight - childHeight) / 2;
break;
case UIAlignItems.End:
childY = startY + contentHeight - childHeight;
// 对齐到底部:顶部 Y = startY - contentHeight + childHeight
childTopY = startY - contentHeight + childHeight;
break;
case UIAlignItems.Stretch:
childHeight = contentHeight;
break;
// UIAlignItems.Start: 默认从顶部开始,不需要修改
}
// 直接设置子元素的世界坐标
// 直接设置子元素的世界坐标worldY 是底部 Y
childTransform.worldX = offsetX;
childTransform.worldY = childY;
childTransform.worldY = childTopY - childHeight; // 底部 Y = 顶部 Y - 高度
childTransform.computedWidth = size.width;
childTransform.computedHeight = childHeight;
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
@@ -284,6 +340,7 @@ export class UILayoutSystem extends EntitySystem {
/**
* 垂直布局
* Vertical layout
* Y-up 系统startY 是内容区域的顶部子元素从上往下排列Y 值递减)
*/
private layoutVertical(
layout: UILayoutComponent,
@@ -304,16 +361,19 @@ export class UILayoutSystem extends EntitySystem {
const totalGap = layout.gap * (children.length - 1);
const totalHeight = totalChildHeight + totalGap;
// 计算起始位置
let offsetY = startY;
// 计算第一个子元素的顶部 YY-up 系统,从顶部开始向下)
// startY 是内容区域顶部
let currentTopY = startY; // 从顶部开始
let gap = layout.gap;
switch (layout.justifyContent) {
case UIJustifyContent.Center:
offsetY = startY + (contentHeight - totalHeight) / 2;
// 垂直居中:第一个元素的顶部 Y = startY - (contentHeight - totalHeight) / 2
currentTopY = startY - (contentHeight - totalHeight) / 2;
break;
case UIJustifyContent.End:
offsetY = startY + contentHeight - totalHeight;
// 对齐到底部:第一个元素的顶部 Y = startY - contentHeight + totalHeight
currentTopY = startY - contentHeight + totalHeight;
break;
case UIJustifyContent.SpaceBetween:
if (children.length > 1) {
@@ -324,19 +384,19 @@ export class UILayoutSystem extends EntitySystem {
if (children.length > 0) {
const space = (contentHeight - totalChildHeight) / children.length;
gap = space;
offsetY = startY + space / 2;
currentTopY = startY - space / 2;
}
break;
case UIJustifyContent.SpaceEvenly:
if (children.length > 0) {
const space = (contentHeight - totalChildHeight) / (children.length + 1);
gap = space;
offsetY = startY + space;
currentTopY = startY - space;
}
break;
}
// 布局每个子元素
// 布局每个子元素(从上往下)
for (let i = 0; i < children.length; i++) {
const child = children[i]!;
const childTransform = child.getComponent(UITransformComponent)!;
@@ -358,8 +418,9 @@ export class UILayoutSystem extends EntitySystem {
break;
}
// worldY 是底部 Y = 顶部 Y - 高度
childTransform.worldX = childX;
childTransform.worldY = offsetY;
childTransform.worldY = currentTopY - size.height;
childTransform.computedWidth = childWidth;
childTransform.computedHeight = size.height;
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
@@ -367,13 +428,15 @@ export class UILayoutSystem extends EntitySystem {
this.processChildrenRecursive(child, childTransform);
offsetY += size.height + gap;
// 移动到下一个元素的顶部位置(向下 = Y 减小)
currentTopY -= size.height + gap;
}
}
/**
* 网格布局
* Grid layout
* Y-up 系统startY 是内容区域的顶部,网格从上往下、从左往右排列
*/
private layoutGrid(
layout: UILayoutComponent,
@@ -404,7 +467,11 @@ export class UILayoutSystem extends EntitySystem {
const row = Math.floor(i / columns);
const x = startX + col * (cellWidth + gapX);
const y = startY + row * (cellHeight + gapY);
// Y-up 系统:第一行在顶部,行号增加 Y 值减小
// 单元格顶部 Y = startY - row * (cellHeight + gapY)
// 单元格底部 Y = 顶部 Y - cellHeight
const cellTopY = startY - row * (cellHeight + gapY);
const y = cellTopY - cellHeight; // worldY 是底部 Y
childTransform.worldX = x;
childTransform.worldY = y;
@@ -425,6 +492,9 @@ export class UILayoutSystem extends EntitySystem {
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
if (children.length === 0) return;
// 计算子元素的父容器顶部 YworldY 是底部,顶部 = 底部 + 高度)
const parentTopY = parentTransform.worldY + parentTransform.computedHeight;
const layout = entity.getComponent(UILayoutComponent);
if (layout && layout.type !== UILayoutType.None) {
this.layoutChildren(layout, parentTransform, children);
@@ -433,7 +503,7 @@ export class UILayoutSystem extends EntitySystem {
this.layoutEntity(
child,
parentTransform.worldX,
parentTransform.worldY,
parentTopY,
parentTransform.computedWidth,
parentTransform.computedHeight,
parentTransform.worldAlpha