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:
@@ -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.
|
||||
*
|
||||
* 预览模式(true):UI 使用屏幕空间叠加投影,独立于世界相机。
|
||||
* 编辑器模式(false):UI 在世界空间渲染,跟随编辑器相机。
|
||||
*
|
||||
* @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.
|
||||
* 获取渲染的精灵数量。
|
||||
|
||||
Reference in New Issue
Block a user