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

@@ -34,6 +34,22 @@ export interface IRenderDataProvider {
getRenderData(): readonly ProviderRenderData[];
}
/**
* Interface for UI render data providers
* UI 渲染数据提供者接口
*
* All UI is rendered in Screen Space with independent orthographic projection.
* 所有 UI 都在屏幕空间渲染,使用独立的正交投影。
*/
export interface IUIRenderDataProvider extends IRenderDataProvider {
/** Get UI render data | 获取 UI 渲染数据 */
getRenderData(): readonly ProviderRenderData[];
/** @deprecated Use getRenderData() instead */
getScreenSpaceRenderData?(): readonly ProviderRenderData[];
/** @deprecated World space UI is no longer supported */
getWorldSpaceRenderData?(): readonly ProviderRenderData[];
}
/**
* Internal gizmo color interface (duck-typed, compatible with editor-core GizmoColor)
* 内部 gizmo 颜色接口(鸭子类型,与 editor-core GizmoColor 兼容)
@@ -145,6 +161,22 @@ export class EngineRenderSystem extends EntitySystem {
private gizmoDataProvider: GizmoDataProviderFn | null = null;
private hasGizmoProvider: HasGizmoProviderFn | null = null;
// UI Canvas boundary settings
// UI 画布边界设置
private uiCanvasWidth: number = 0;
private uiCanvasHeight: number = 0;
private showUICanvasBoundary: boolean = true;
// UI render data provider (supports screen space and world space)
// UI 渲染数据提供者(支持屏幕空间和世界空间)
private uiRenderDataProvider: IUIRenderDataProvider | null = null;
// Preview mode flag: when true, UI uses screen space overlay projection
// when false (editor mode), UI renders in world space following editor camera
// 预览模式标志:为 true 时UI 使用屏幕空间叠加投影
// 为 false编辑器模式UI 在世界空间渲染,跟随编辑器相机
private previewMode: boolean = false;
/**
* Create a new engine render system.
* 创建新的引擎渲染系统。
@@ -190,6 +222,14 @@ export class EngineRenderSystem extends EntitySystem {
* Process all matched entities.
* 处理所有匹配的实体。
*
* Rendering is done in two passes:
* 1. World Pass: World sprites, tilemaps, gizmos (affected by world camera)
* 2. UI Pass: Screen space UI (independent orthographic projection, overlaid on world)
*
* 渲染分两个阶段进行:
* 1. 世界阶段:世界 Sprite、瓦片地图、Gizmo受世界相机影响
* 2. UI 阶段:屏幕空间 UI独立正交投影叠加在世界之上
*
* @param entities - Entities to process | 要处理的实体
*/
protected override process(entities: readonly Entity[]): void {
@@ -197,6 +237,11 @@ export class EngineRenderSystem extends EntitySystem {
// 清空并重用映射用于绘制gizmo
this.entityRenderMap.clear();
// ===== Pass 1: World Space Rendering =====
// ===== 阶段 1世界空间渲染 =====
// This includes world sprites, tilemaps, and world space UI
// 包括世界 Sprite、瓦片地图和世界空间 UI
// Collect all render items with sorting order
// 收集所有渲染项及其排序顺序
const renderItems: Array<{ sortingOrder: number; sprites: SpriteRenderData[] }> = [];
@@ -259,7 +304,6 @@ export class EngineRenderSystem extends EntitySystem {
}
// Collect render data from providers (e.g., tilemap)
// 收集来自提供者的渲染数据(如瓦片地图)
for (const provider of this.renderDataProviders) {
const renderDataList = provider.getRenderData();
for (const data of renderDataList) {
@@ -297,6 +341,18 @@ export class EngineRenderSystem extends EntitySystem {
}
}
// Collect UI render data if in editor mode (renders in world space)
// 如果在编辑器模式,收集 UI 渲染数据(在世界空间渲染)
if (!this.previewMode && this.uiRenderDataProvider) {
const uiRenderData = this.uiRenderDataProvider.getRenderData();
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
renderItems.push({ sortingOrder: data.sortingOrder, sprites: uiSprites });
}
}
}
// Sort by sortingOrder (lower values render first, appear behind)
// 按 sortingOrder 排序(值越小越先渲染,显示在后面)
renderItems.sort((a, b) => a.sortingOrder - b.sortingOrder);
@@ -332,7 +388,127 @@ export class EngineRenderSystem extends EntitySystem {
this.drawCameraFrustums();
}
// Draw UI canvas boundary
// 绘制 UI 画布边界
if (this.showGizmos && this.showUICanvasBoundary && this.uiCanvasWidth > 0 && this.uiCanvasHeight > 0) {
this.drawUICanvasBoundary();
}
// ===== World Pass: Render world content =====
// ===== 世界阶段:渲染世界内容 =====
this.bridge.render();
// ===== Pass 2: Screen Space UI Rendering (Preview Mode Only) =====
// ===== 阶段 2屏幕空间 UI 渲染(仅预览模式)=====
// UI is rendered on top of world content with independent projection
// UI 使用独立投影渲染在世界内容之上
// Only in preview mode - in editor mode, UI is rendered in world space above
// 仅在预览模式 - 在编辑器模式UI 在上面的世界空间渲染
if (this.previewMode) {
this.renderScreenSpaceUI();
}
}
/**
* Render screen space UI with fixed orthographic projection.
* 使用固定正交投影渲染屏幕空间 UI。
*
* Screen space UI is rendered with an independent orthographic projection
* based on the UI canvas size, not affected by the world camera.
* 屏幕空间 UI 使用基于 UI 画布尺寸的独立正交投影渲染,不受世界相机影响。
*/
private renderScreenSpaceUI(): void {
if (!this.uiRenderDataProvider) {
return;
}
// Get all UI render data (now only screen space)
// 获取所有 UI 渲染数据(现在只有屏幕空间)
const uiRenderData = this.uiRenderDataProvider.getRenderData();
if (uiRenderData.length === 0) {
return;
}
// Switch to screen space projection
// 切换到屏幕空间投影
// Use UI canvas size for the orthographic projection
// 使用 UI 画布尺寸进行正交投影
const canvasWidth = this.uiCanvasWidth > 0 ? this.uiCanvasWidth : 1920;
const canvasHeight = this.uiCanvasHeight > 0 ? this.uiCanvasHeight : 1080;
// Save current camera state and switch to screen space mode
// 保存当前相机状态并切换到屏幕空间模式
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
// Clear batcher for screen space content
this.batcher.clear();
// Collect screen space UI render items
const screenSpaceItems: Array<{ sortingOrder: number; sprites: SpriteRenderData[] }> = [];
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
screenSpaceItems.push({ sortingOrder: data.sortingOrder, sprites: uiSprites });
}
}
// Sort by sortingOrder
screenSpaceItems.sort((a, b) => a.sortingOrder - b.sortingOrder);
// Submit screen space UI sprites
for (const item of screenSpaceItems) {
for (const sprite of item.sprites) {
this.batcher.addSprite(sprite);
}
}
if (!this.batcher.isEmpty) {
const sprites = this.batcher.getSprites();
this.bridge.submitSprites(sprites);
// Render overlay (without clearing screen)
// 渲染叠加层(不清屏)
this.bridge.renderOverlay();
}
// Restore world space camera
// 恢复世界空间相机
this.bridge.popScreenSpaceMode();
}
/**
* Convert provider render data to sprite render data array.
* 将提供者渲染数据转换为 Sprite 渲染数据数组。
*/
private convertProviderDataToSprites(data: ProviderRenderData): SpriteRenderData[] {
// Get texture ID - load from path if needed
let textureId = data.textureIds[0] || 0;
if (textureId === 0 && data.texturePath) {
textureId = this.bridge.getOrLoadTextureByPath(data.texturePath);
}
const sprites: SpriteRenderData[] = [];
for (let i = 0; i < data.tileCount; i++) {
const tOffset = i * 7;
const uvOffset = i * 4;
const renderData: SpriteRenderData = {
x: data.transforms[tOffset],
y: data.transforms[tOffset + 1],
rotation: data.transforms[tOffset + 2],
scaleX: data.transforms[tOffset + 3],
scaleY: data.transforms[tOffset + 4],
originX: data.transforms[tOffset + 5],
originY: data.transforms[tOffset + 6],
textureId,
uv: [data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3]],
color: data.colors[i]
};
sprites.push(renderData);
}
return sprites;
}
/**
@@ -675,6 +851,78 @@ export class EngineRenderSystem extends EntitySystem {
}
}
/**
* Draw UI canvas boundary.
* 绘制 UI 画布边界。
*
* Shows the design resolution boundary of the UI canvas.
* 显示 UI 画布的设计分辨率边界。
*/
private drawUICanvasBoundary(): void {
const w = this.uiCanvasWidth;
const h = this.uiCanvasHeight;
// Canvas is centered at (0, 0) in Y-up coordinate system
// 画布以 (0, 0) 为中心Y 轴向上坐标系
// Bottom-left: (-w/2, -h/2), Top-right: (w/2, h/2)
// Draw the boundary as a rectangle
// 绘制边界矩形
// Using origin (0, 0) means position is bottom-left corner
// 使用 origin (0, 0) 表示位置是左下角
this.bridge.addGizmoRect(
-w / 2, // x: left edge
-h / 2, // y: bottom edge (in Y-up system)
w, // width
h, // height
0, // rotation
0, // originX: left
0, // originY: bottom
0.5, 0.8, 1.0, 0.6, // Light blue color for UI canvas boundary
false // Don't show transform handles
);
// Draw corner markers for better visibility
// 绘制角标记以提高可见性
const markerSize = 20;
const markerColor = { r: 0.5, g: 0.8, b: 1.0, a: 1.0 };
// Top-left corner marker (L shape)
const corners = [
// Top-left
{ x: -w / 2, y: h / 2 - markerSize, ex: -w / 2, ey: h / 2 },
{ x: -w / 2, y: h / 2, ex: -w / 2 + markerSize, ey: h / 2 },
// Top-right
{ x: w / 2 - markerSize, y: h / 2, ex: w / 2, ey: h / 2 },
{ x: w / 2, y: h / 2, ex: w / 2, ey: h / 2 - markerSize },
// Bottom-right
{ x: w / 2, y: -h / 2 + markerSize, ex: w / 2, ey: -h / 2 },
{ x: w / 2, y: -h / 2, ex: w / 2 - markerSize, ey: -h / 2 },
// Bottom-left
{ x: -w / 2 + markerSize, y: -h / 2, ex: -w / 2, ey: -h / 2 },
{ x: -w / 2, y: -h / 2, ex: -w / 2, ey: -h / 2 + markerSize },
];
for (const line of corners) {
const dx = line.ex - line.x;
const dy = line.ey - line.y;
const length = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
this.bridge.addGizmoRect(
(line.x + line.ex) / 2,
(line.y + line.ey) / 2,
length,
2, // Line thickness
angle,
0.5,
0.5,
markerColor.r, markerColor.g, markerColor.b, markerColor.a,
false
);
}
}
/**
* Set gizmo registry functions.
* 设置 gizmo 注册表函数。
@@ -711,6 +959,42 @@ export class EngineRenderSystem extends EntitySystem {
return this.showGizmos;
}
/**
* Set UI canvas size for boundary display.
* 设置 UI 画布尺寸以显示边界。
*
* @param width - Canvas width (design resolution) | 画布宽度(设计分辨率)
* @param height - Canvas height (design resolution) | 画布高度(设计分辨率)
*/
setUICanvasSize(width: number, height: number): void {
this.uiCanvasWidth = width;
this.uiCanvasHeight = height;
}
/**
* Get UI canvas size.
* 获取 UI 画布尺寸。
*/
getUICanvasSize(): { width: number; height: number } {
return { width: this.uiCanvasWidth, height: this.uiCanvasHeight };
}
/**
* Set UI canvas boundary visibility.
* 设置 UI 画布边界可见性。
*/
setShowUICanvasBoundary(show: boolean): void {
this.showUICanvasBoundary = show;
}
/**
* Get UI canvas boundary visibility.
* 获取 UI 画布边界可见性。
*/
getShowUICanvasBoundary(): boolean {
return this.showUICanvasBoundary;
}
/**
* Set selected entity IDs.
* 设置选中的实体ID。
@@ -798,6 +1082,51 @@ export class EngineRenderSystem extends EntitySystem {
}
}
/**
* Set the UI render data provider.
* 设置 UI 渲染数据提供者。
*
* The UI render data provider supports both screen space and world space UI.
* UI 渲染数据提供者支持屏幕空间和世界空间 UI。
*
* @param provider - UI render data provider | UI 渲染数据提供者
*/
setUIRenderDataProvider(provider: IUIRenderDataProvider | null): void {
this.uiRenderDataProvider = provider;
}
/**
* Get the UI render data provider.
* 获取 UI 渲染数据提供者。
*/
getUIRenderDataProvider(): IUIRenderDataProvider | null {
return this.uiRenderDataProvider;
}
/**
* Set preview mode.
* 设置预览模式。
*
* In preview mode (true): UI uses screen space overlay projection, independent of world camera.
* In editor mode (false): UI renders in world space, following the editor camera.
*
* 预览模式trueUI 使用屏幕空间叠加投影,独立于世界相机。
* 编辑器模式falseUI 在世界空间渲染,跟随编辑器相机。
*
* @param mode - True for preview mode, false for editor mode
*/
setPreviewMode(mode: boolean): void {
this.previewMode = mode;
}
/**
* Get preview mode.
* 获取预览模式。
*/
isPreviewMode(): boolean {
return this.previewMode;
}
/**
* Get the number of sprites rendered.
* 获取渲染的精灵数量。