feat: 纹理路径稳定 ID 与架构改进 (#305)

* feat(asset-system): 实现路径稳定 ID 生成器

使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID:
- 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定
- 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID
- 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID
- clearTextureMappings() 不再清除 _pathIdCache

这解决了 Play/Stop 后纹理 ID 失效的根本问题。

* fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用

使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存:
- saveSceneSnapshot() 移除 clearTextureMappings() 调用
- restoreSceneSnapshot() 移除 clearTextureMappings() 调用
- 组件保存的 textureId 在 Play/Stop 后仍然有效

* fix(editor-core): 修复场景切换时的资源泄漏

在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性

* fix(runtime-core): 修复 PluginManager 组件注册类型错误

将 ComponentRegistry 类改为 GlobalComponentRegistry 实例:
- registerComponents() 期望 IComponentRegistry 接口实例
- GlobalComponentRegistry 是 ComponentRegistry 的全局实例

* refactor(core): 提取 IComponentRegistry 接口

将组件注册表抽象为接口,支持场景级组件注册:
- 新增 IComponentRegistry 接口定义
- Scene 持有独立的 componentRegistry 实例
- 支持从 GlobalComponentRegistry 克隆
- 各系统支持传入自定义注册表

* refactor(engine-core): 改进插件服务注册机制

- 更新 IComponentRegistry 类型引用
- 优化 PluginServiceRegistry 服务管理

* refactor(modules): 适配新的组件注册接口

更新各模块 RuntimeModule 使用 IComponentRegistry 接口:
- audio, behavior-tree, camera
- sprite, tilemap, world-streaming

* fix(physics-rapier2d): 修复物理插件组件注册

- PhysicsEditorPlugin 添加 runtimeModule 引用
- 适配 IComponentRegistry 接口
- 修复物理组件在场景加载时未注册的问题

* feat(editor-core): 添加 UserCodeService 就绪信号机制

- 新增 waitForReady()/signalReady() API
- 支持等待用户脚本编译完成
- 解决场景加载时组件未注册的时序问题

* fix(editor-app): 在编译完成后调用 signalReady()

确保用户脚本编译完成后发出就绪信号:
- 编译成功后调用 userCodeService.signalReady()
- 编译失败也要发出信号,避免阻塞场景加载

* feat(editor-core): 改进编辑器核心服务

- EntityStoreService 添加调试日志
- AssetRegistryService 优化资产注册
- PluginManager 改进插件管理
- IFileAPI 添加 getFileMtime 接口

* feat(engine): 改进 Rust 纹理管理器

- 支持任意 ID 的纹理加载(非递增)
- 添加纹理状态追踪 API
- 优化纹理缓存清理机制
- 更新 TypeScript 绑定

* feat(ui): 添加场景切换和文本闪烁组件

新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化

* feat(editor-app): 添加外部文件修改检测

- 新增 ExternalModificationDialog 组件
- TauriFileAPI 支持 getFileMtime
- 场景文件被外部修改时提示用户

* feat(editor-app): 添加渲染调试面板

- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进

* refactor(editor-app): 编辑器服务和组件优化

- EngineService 改进引擎集成
- EditorEngineSync 同步优化
- AssetFileInspector 改进
- VectorFieldEditors 优化
- InstantiatePrefabCommand 改进

* feat(i18n): 更新国际化翻译

- 添加新功能相关翻译
- 更新中文、英文、西班牙文

* feat(tauri): 添加文件修改时间查询命令

- 新增 get_file_mtime 命令
- 支持检测文件外部修改

* refactor(particle): 粒子系统改进

- 适配新的组件注册接口
- ParticleSystem 优化
- 添加单元测试

* refactor(platform): 平台适配层优化

- BrowserRuntime 改进
- 新增 RuntimeSceneManager 服务
- 导出优化

* refactor(asset-system-editor): 资产元数据改进

- AssetMetaFile 优化
- 导出调整

* fix(asset-system): 移除未使用的 TextureLoader 导入

* fix(tests): 更新测试以使用 GlobalComponentRegistry 实例

修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更:
- ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset()
- EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例
- IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例
- SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例
- ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry
- SystemTypes.test.ts: 在 Scene 创建前注册组件
- QuerySystem.test.ts: mockScene 添加 componentRegistry
This commit is contained in:
YHH
2025-12-16 12:46:14 +08:00
committed by GitHub
parent d834ca5e77
commit ed8f6e283b
107 changed files with 7399 additions and 847 deletions

View File

@@ -1,5 +1,4 @@
import type { IScene } from '@esengine/ecs-framework';
import { ComponentRegistry } from '@esengine/ecs-framework';
import type { IScene, IComponentRegistry } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { EngineBridgeToken } from '@esengine/ecs-engine-bindgen';
@@ -14,10 +13,14 @@ import {
UISliderComponent,
UIScrollViewComponent
} from './components';
import { TextBlinkComponent } from './components/TextBlinkComponent';
import { SceneLoadTriggerComponent } from './components/SceneLoadTriggerComponent';
import { UILayoutSystem } from './systems/UILayoutSystem';
import { UIInputSystem } from './systems/UIInputSystem';
import { UIAnimationSystem } from './systems/UIAnimationSystem';
import { UIRenderDataProvider } from './systems/UIRenderDataProvider';
import { TextBlinkSystem } from './systems/TextBlinkSystem';
import { SceneLoadTriggerSystem } from './systems/SceneLoadTriggerSystem';
import {
UIRenderBeginSystem,
UIRectRenderSystem,
@@ -43,7 +46,7 @@ export {
} from './tokens';
class UIRuntimeModule implements IRuntimeModule {
registerComponents(registry: typeof ComponentRegistry): void {
registerComponents(registry: IComponentRegistry): void {
registry.register(UITransformComponent);
registry.register(UIRenderComponent);
registry.register(UIInteractableComponent);
@@ -53,6 +56,8 @@ class UIRuntimeModule implements IRuntimeModule {
registry.register(UIProgressBarComponent);
registry.register(UISliderComponent);
registry.register(UIScrollViewComponent);
registry.register(TextBlinkComponent);
registry.register(SceneLoadTriggerComponent);
}
createSystems(scene: IScene, context: SystemContext): void {
@@ -65,6 +70,14 @@ class UIRuntimeModule implements IRuntimeModule {
const animationSystem = new UIAnimationSystem();
scene.addSystem(animationSystem);
// 文本闪烁系统 | Text blink system
const textBlinkSystem = new TextBlinkSystem();
scene.addSystem(textBlinkSystem);
// 场景加载触发系统 | Scene load trigger system
const sceneLoadTriggerSystem = new SceneLoadTriggerSystem();
scene.addSystem(sceneLoadTriggerSystem);
const renderBeginSystem = new UIRenderBeginSystem();
scene.addSystem(renderBeginSystem);

View File

@@ -0,0 +1,61 @@
/**
* 场景加载触发组件
* Scene Load Trigger Component
*
* 配合 UIInteractable 使用,点击时自动加载指定场景。
* Works with UIInteractable to automatically load scene on click.
*/
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
/**
* 场景加载触发组件
* Scene Load Trigger Component
*
* 添加到带有 UIInteractable 的实体上,点击时会加载 targetScene 指定的场景。
* Add to entity with UIInteractable, loads targetScene on click.
*
* @example
* ```json
* {
* "type": "SceneLoadTrigger",
* "data": {
* "targetScene": "GameScene",
* "enabled": true
* }
* }
* ```
*/
@ECSComponent('SceneLoadTrigger')
@Serializable({ version: 1, typeId: 'SceneLoadTrigger' })
export class SceneLoadTriggerComponent extends Component {
/**
* 目标场景名称
* Target scene name to load on click
*/
@Serialize()
@Property({ type: 'string', label: 'Target Scene' })
public targetScene: string = '';
/**
* 是否启用
* Whether the trigger is enabled
*/
@Serialize()
@Property({ type: 'boolean', label: 'Enabled' })
public enabled: boolean = true;
/**
* 点击后是否禁用(防止重复点击)
* Disable after click (prevent double clicks)
*/
@Serialize()
@Property({ type: 'boolean', label: 'Disable On Click' })
public disableOnClick: boolean = true;
/**
* 内部标记:回调是否已绑定
* Internal flag: whether callback is bound
*/
public _callbackBound: boolean = false;
}

View File

@@ -0,0 +1,101 @@
/**
* 文本闪烁组件
* Text Blink Component
*
* 让文本产生闪烁效果,类似 Unity 的 Animation 实现
* Creates a blinking effect for text, similar to Unity's Animation implementation
*/
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
/**
* 文本闪烁组件
* Text Blink Component
*/
@ECSComponent('TextBlink')
@Serializable({ version: 1, typeId: 'TextBlink' })
export class TextBlinkComponent extends Component {
/**
* 闪烁速度(周期/秒)
* Blink speed (cycles per second)
*/
@Serialize()
@Property({ type: 'number', label: 'Speed', min: 0.1, max: 10, step: 0.1 })
public speed: number = 1.5;
/**
* 最小透明度
* Minimum alpha
*/
@Serialize()
@Property({ type: 'number', label: 'Min Alpha', min: 0, max: 1, step: 0.05 })
public minAlpha: number = 0.3;
/**
* 最大透明度
* Maximum alpha
*/
@Serialize()
@Property({ type: 'number', label: 'Max Alpha', min: 0, max: 1, step: 0.05 })
public maxAlpha: number = 1.0;
/**
* 是否启用闪烁
* Whether blinking is enabled
*/
@Serialize()
@Property({ type: 'boolean', label: 'Enabled' })
public blinkEnabled: boolean = true;
// ============= 运行时状态(不序列化)| Runtime state (not serialized) =============
/** 当前时间 | Current time */
private _time: number = 0;
/**
* 获取当前时间
* Get current time
*/
public get time(): number {
return this._time;
}
/**
* 更新时间
* Update time
*/
public addTime(deltaTime: number): void {
this._time += deltaTime;
}
/**
* 计算当前 alpha 值
* Calculate current alpha value
*
* 使用正弦波实现平滑的闪烁效果
* Uses sine wave for smooth blinking effect
*/
public calculateAlpha(): number {
if (!this.blinkEnabled) {
return this.maxAlpha;
}
// 使用正弦波sin 从 -1 到 1映射到 minAlpha 到 maxAlpha
// Using sine wave: sin from -1 to 1, mapped to minAlpha to maxAlpha
const t = Math.sin(this._time * this.speed * Math.PI * 2);
const normalized = (t + 1) / 2; // 0 到 1
return this.minAlpha + normalized * (this.maxAlpha - this.minAlpha);
}
/**
* 重置状态
* Reset state
*/
public reset(): void {
this._time = 0;
}
override onRemovedFromEntity(): void {
this.reset();
}
}

View File

@@ -120,9 +120,36 @@ export class UIRenderComponent extends Component {
/**
* 九宫格边距 [top, right, bottom, left]
* Nine-patch margins
*
* Defines the non-stretchable borders for nine-patch rendering.
* 定义九宫格渲染时不可拉伸的边框区域。
*/
@Serialize()
@Property({ type: 'vector4', label: 'Nine-Patch Margins' })
public ninePatchMargins: [number, number, number, number] = [0, 0, 0, 0];
/**
* 源纹理宽度(像素)
* Source texture width in pixels
*
* Required for nine-patch UV calculations.
* 九宫格 UV 计算所需。
*/
@Serialize()
@Property({ type: 'number', label: 'Texture Width', min: 1 })
public textureWidth: number = 0;
/**
* 源纹理高度(像素)
* Source texture height in pixels
*
* Required for nine-patch UV calculations.
* 九宫格 UV 计算所需。
*/
@Serialize()
@Property({ type: 'number', label: 'Texture Height', min: 1 })
public textureHeight: number = 0;
// ===== 边框 Border =====
/**

View File

@@ -275,6 +275,15 @@ export class UITransformComponent extends Component implements ISortable {
*/
public worldScaleY: number = 1;
/**
* 计算后的世界层内顺序(考虑父元素和层级深度)
* Computed world order in layer (considering parent and hierarchy depth)
*
* 子元素总是渲染在父元素之上worldOrderInLayer = parentWorldOrder + depth * 1000 + localOrder
* Children always render on top of parents: worldOrderInLayer = parentWorldOrder + depth * 1000 + localOrder
*/
public worldOrderInLayer: number = 0;
/**
* 本地到世界的 2D 变换矩阵(只读,由 UILayoutSystem 计算)
* Local to world 2D transformation matrix (readonly, computed by UILayoutSystem)

View File

@@ -88,6 +88,9 @@ export {
type UIFontWeight
} from './components/UITextComponent';
export { TextBlinkComponent } from './components/TextBlinkComponent';
export { SceneLoadTriggerComponent } from './components/SceneLoadTriggerComponent';
export {
UILayoutComponent,
UILayoutType,
@@ -124,6 +127,8 @@ export { UILayoutSystem } from './systems/UILayoutSystem';
export { UIInputSystem, type UIInputEvent } from './systems/UIInputSystem';
export { UIAnimationSystem, UIEasing, type EasingFunction, type EasingName } from './systems/UIAnimationSystem';
export { UIRenderDataProvider, type IUIRenderDataProvider } from './systems/UIRenderDataProvider';
export { TextBlinkSystem } from './systems/TextBlinkSystem';
export { SceneLoadTriggerSystem } from './systems/SceneLoadTriggerSystem';
// Systems - Render (ECS-compliant render systems)
export {

View File

@@ -0,0 +1,162 @@
/**
* 场景加载触发系统
* Scene Load Trigger System
*
* 处理 SceneLoadTriggerComponent绑定 UIInteractable 点击事件到场景加载。
* Processes SceneLoadTriggerComponent, binds UIInteractable click to scene loading.
*/
import { Entity, EntitySystem, Matcher, ECSSystem, Core } from '@esengine/ecs-framework';
import { SceneLoadTriggerComponent } from '../components/SceneLoadTriggerComponent';
import { UIInteractableComponent } from '../components/UIInteractableComponent';
/**
* 场景加载函数类型(与 RuntimeSceneManager.loadScene 兼容)
* Scene load function type (compatible with RuntimeSceneManager.loadScene)
*/
type SceneLoadFunction = (sceneName: string) => Promise<void>;
/**
* 场景管理器接口(最小化,避免循环依赖)
* Scene manager interface (minimal, avoids circular dependency)
*
* 包含 IService 的 dispose 方法以兼容 ServiceContainer。
* Includes IService's dispose method for ServiceContainer compatibility.
*/
interface ISceneManager {
loadScene(sceneName: string): Promise<void>;
dispose(): void;
}
/**
* 全局场景管理器服务键
* Global scene manager service key
*
* 使用 Symbol.for 确保与 BrowserRuntime 中注册的键一致。
* Uses Symbol.for to match the key registered in BrowserRuntime.
*/
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
/**
* 场景加载触发系统
* Scene Load Trigger System
*
* 自动将 SceneLoadTriggerComponent 的配置连接到 UIInteractable 的点击事件。
* Automatically connects SceneLoadTriggerComponent config to UIInteractable click events.
*/
@ECSSystem('SceneLoadTrigger')
export class SceneLoadTriggerSystem extends EntitySystem {
private _sceneLoader: SceneLoadFunction | null = null;
constructor() {
super(Matcher.empty().all(SceneLoadTriggerComponent, UIInteractableComponent));
}
/**
* 设置场景加载函数
* Set scene load function
*
* 可以直接设置函数,或者系统会尝试从服务注册表获取 RuntimeSceneManager。
* Can set function directly, or system will try to get RuntimeSceneManager from service registry.
*/
public setSceneLoader(loader: SceneLoadFunction): void {
this._sceneLoader = loader;
}
protected override process(entities: readonly Entity[]): void {
// 如果没有设置场景加载器,尝试从服务注册表获取
// If no scene loader set, try to get from service registry
if (!this._sceneLoader) {
this._tryGetSceneManager();
}
for (const entity of entities) {
const trigger = entity.getComponent(SceneLoadTriggerComponent);
const interactable = entity.getComponent(UIInteractableComponent);
if (!trigger || !interactable) continue;
if (!trigger.enabled || !trigger.targetScene) continue;
// 只绑定一次回调
// Only bind callback once
if (trigger._callbackBound) continue;
this._bindClickHandler(entity, trigger, interactable);
}
}
/**
* 尝试从全局服务获取场景管理器
* Try to get scene manager from global services
*/
private _tryGetSceneManager(): void {
try {
// 从 Core.services 获取场景管理器
// Get scene manager from Core.services
// RuntimeSceneManager 实现了 IService 接口
// RuntimeSceneManager implements IService interface
const sceneManager = Core.services.tryResolve<ISceneManager>(GlobalSceneManagerKey);
if (sceneManager?.loadScene) {
this._sceneLoader = (sceneName: string) => sceneManager.loadScene(sceneName);
}
} catch (e) {
// 忽略错误,保持 _sceneLoader 为 null
// Ignore error, keep _sceneLoader as null
}
}
/**
* 绑定点击处理器
* Bind click handler
*/
private _bindClickHandler(
entity: Entity,
trigger: SceneLoadTriggerComponent,
interactable: UIInteractableComponent
): void {
const targetScene = trigger.targetScene;
// 保存原有的 onClick如果有
// Save original onClick (if any)
const originalOnClick = interactable.onClick;
interactable.onClick = () => {
// 调用原有回调
// Call original callback
originalOnClick?.();
// 检查是否启用
// Check if enabled
if (!trigger.enabled) return;
// 禁用(防止重复点击)
// Disable (prevent double clicks)
if (trigger.disableOnClick) {
trigger.enabled = false;
}
// 尝试获取场景加载器(可能在回调绑定后才注册)
// Try to get scene loader (may be registered after callback binding)
if (!this._sceneLoader) {
this._tryGetSceneManager();
}
// 加载场景
// Load scene
if (this._sceneLoader) {
this._sceneLoader(targetScene).catch((error) => {
console.error(`[SceneLoadTriggerSystem] Failed to load scene "${targetScene}":`, error);
// 恢复启用状态
// Restore enabled state
if (trigger.disableOnClick) {
trigger.enabled = true;
}
});
}
// 静默处理:编辑器预览模式下场景切换不可用
// Silent handling: scene switching not available in editor preview mode
};
trigger._callbackBound = true;
}
}

View File

@@ -0,0 +1,37 @@
/**
* 文本闪烁系统 - 实现 UI 元素的透明度脉冲动画
*
* Text Blink System - Implements alpha pulse animation for UI elements
*/
import { Entity, EntitySystem, Matcher, Time } from '@esengine/ecs-framework';
import { TextBlinkComponent } from '../components/TextBlinkComponent';
import { UITransformComponent } from '../components/UITransformComponent';
/**
* 处理 TextBlinkComponent驱动 UI 元素的透明度动画。
* 常用于 "TAP TO START" 等需要吸引注意力的文本效果。
*
* Processes TextBlinkComponent to drive UI element alpha animation.
* Commonly used for attention-grabbing text effects like "TAP TO START".
*/
export class TextBlinkSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(TextBlinkComponent, UITransformComponent));
}
protected override process(entities: readonly Entity[]): void {
const deltaTime = Time.deltaTime;
for (const entity of entities) {
if (!entity.enabled) continue;
const blink = entity.getComponent(TextBlinkComponent);
const uiTransform = entity.getComponent(UITransformComponent);
if (!blink || !uiTransform) continue;
blink.addTime(deltaTime);
uiTransform.alpha = blink.calculateAlpha();
}
}
}

View File

@@ -96,7 +96,7 @@ export class UILayoutSystem extends EntitySystem {
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, identityMatrix);
this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1, identityMatrix, true, 0);
}
}
@@ -112,7 +112,8 @@ export class UILayoutSystem extends EntitySystem {
parentHeight: number,
parentAlpha: number,
parentMatrix: Matrix2D,
parentVisible: boolean = true
parentVisible: boolean = true,
depth: number = 0
): void {
const transform = entity.getComponent(UITransformComponent);
if (!transform) return;
@@ -199,6 +200,12 @@ export class UILayoutSystem extends EntitySystem {
// Calculate world visibility (if parent is invisible, children are also invisible)
transform.worldVisible = parentVisible && transform.visible;
// 计算世界层内顺序(子元素总是渲染在父元素之上)
// Calculate world order in layer (children always render on top of parents)
// 公式depth * 1000 + localOrderInLayer
// Formula: depth * 1000 + localOrderInLayer
transform.worldOrderInLayer = depth * 1000 + transform.orderInLayer;
// 使用矩阵乘法计算世界变换
this.updateWorldMatrix(transform, parentMatrix);
@@ -215,7 +222,7 @@ export class UILayoutSystem extends EntitySystem {
// 检查是否有布局组件
const layout = entity.getComponent(UILayoutComponent);
if (layout && layout.type !== UILayoutType.None) {
this.layoutChildren(layout, transform, children);
this.layoutChildren(layout, transform, children, depth + 1);
} else {
// 无布局组件,直接递归处理子元素
for (const child of children) {
@@ -227,7 +234,8 @@ export class UILayoutSystem extends EntitySystem {
height,
transform.worldAlpha,
transform.localToWorldMatrix,
transform.worldVisible
transform.worldVisible,
depth + 1
);
}
}
@@ -240,7 +248,8 @@ export class UILayoutSystem extends EntitySystem {
private layoutChildren(
layout: UILayoutComponent,
parentTransform: UITransformComponent,
children: Entity[]
children: Entity[],
depth: number
): void {
const contentStartX = parentTransform.worldX + layout.paddingLeft;
// Y-up 系统worldY 是底部,顶部 = worldY + height
@@ -252,13 +261,13 @@ export class UILayoutSystem extends EntitySystem {
switch (layout.type) {
case UILayoutType.Horizontal:
this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
this.layoutHorizontal(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth);
break;
case UILayoutType.Vertical:
this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
this.layoutVertical(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth);
break;
case UILayoutType.Grid:
this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight);
this.layoutGrid(layout, parentTransform, children, contentStartX, contentStartY, contentWidth, contentHeight, depth);
break;
default:
// 默认按正常方式递归(传递顶部 Y
@@ -270,7 +279,9 @@ export class UILayoutSystem extends EntitySystem {
parentTransform.computedWidth,
parentTransform.computedHeight,
parentTransform.worldAlpha,
parentTransform.localToWorldMatrix
parentTransform.localToWorldMatrix,
parentTransform.worldVisible,
depth
);
}
}
@@ -287,7 +298,8 @@ export class UILayoutSystem extends EntitySystem {
startX: number,
startY: number,
contentWidth: number,
contentHeight: number
contentHeight: number,
depth: number
): void {
// 计算总子元素宽度
const childSizes = children.map(child => {
@@ -366,12 +378,14 @@ export class UILayoutSystem extends EntitySystem {
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 传播世界可见性 | Propagate world visibility
childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible;
// 计算世界层内顺序 | Calculate world order in layer
childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer;
// 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false;
// 递归处理子元素的子元素
this.processChildrenRecursive(child, childTransform);
this.processChildrenRecursive(child, childTransform, depth);
offsetX += size.width + gap;
}
@@ -389,7 +403,8 @@ export class UILayoutSystem extends EntitySystem {
startX: number,
startY: number,
contentWidth: number,
contentHeight: number
contentHeight: number,
depth: number
): void {
// 计算总子元素高度
const childSizes = children.map(child => {
@@ -466,11 +481,13 @@ export class UILayoutSystem extends EntitySystem {
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 传播世界可见性 | Propagate world visibility
childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible;
// 计算世界层内顺序 | Calculate world order in layer
childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer;
// 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false;
this.processChildrenRecursive(child, childTransform);
this.processChildrenRecursive(child, childTransform, depth);
// 移动到下一个元素的顶部位置(向下 = Y 减小)
currentTopY -= size.height + gap;
@@ -489,7 +506,8 @@ export class UILayoutSystem extends EntitySystem {
startX: number,
startY: number,
contentWidth: number,
_contentHeight: number
_contentHeight: number,
depth: number
): void {
const columns = layout.columns;
const gapX = layout.getHorizontalGap();
@@ -524,11 +542,13 @@ export class UILayoutSystem extends EntitySystem {
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
// 传播世界可见性 | Propagate world visibility
childTransform.worldVisible = parentTransform.worldVisible && childTransform.visible;
// 计算世界层内顺序 | Calculate world order in layer
childTransform.worldOrderInLayer = depth * 1000 + childTransform.orderInLayer;
// 使用矩阵乘法计算世界旋转和缩放
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
childTransform.layoutDirty = false;
this.processChildrenRecursive(child, childTransform);
this.processChildrenRecursive(child, childTransform, depth);
}
}
@@ -565,7 +585,7 @@ export class UILayoutSystem extends EntitySystem {
* 递归处理子元素
* Recursively process children
*/
private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void {
private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent, depth: number): void {
const children = this.getUIChildren(entity);
if (children.length === 0) return;
@@ -574,7 +594,7 @@ export class UILayoutSystem extends EntitySystem {
const layout = entity.getComponent(UILayoutComponent);
if (layout && layout.type !== UILayoutType.None) {
this.layoutChildren(layout, parentTransform, children);
this.layoutChildren(layout, parentTransform, children, depth + 1);
} else {
for (const child of children) {
this.layoutEntity(
@@ -585,7 +605,8 @@ export class UILayoutSystem extends EntitySystem {
parentTransform.computedHeight,
parentTransform.worldAlpha,
parentTransform.localToWorldMatrix,
parentTransform.worldVisible
parentTransform.worldVisible,
depth + 1
);
}
}

View File

@@ -58,9 +58,9 @@ export class UIButtonRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
// 使用排序层和层内顺序 | Use sorting layer and order in layer
// 使用排序层和世界层内顺序 | Use sorting layer and world order in layer
const sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer;
const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;

View File

@@ -55,9 +55,9 @@ export class UIProgressBarRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
// 使用排序层和层内顺序 | Use sorting layer and order in layer
// 使用排序层和世界层内顺序 | Use sorting layer and world order in layer
const sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer;
const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;

View File

@@ -10,7 +10,7 @@
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import { UITransformComponent } from '../../components/UITransformComponent';
import { UIRenderComponent } from '../../components/UIRenderComponent';
import { UIRenderComponent, UIRenderType } from '../../components/UIRenderComponent';
import { UIButtonComponent } from '../../components/widgets/UIButtonComponent';
import { UIProgressBarComponent } from '../../components/widgets/UIProgressBarComponent';
import { UISliderComponent } from '../../components/widgets/UISliderComponent';
@@ -68,9 +68,11 @@ export class UIRectRenderSystem extends EntitySystem {
const alpha = transform.worldAlpha ?? transform.alpha;
// 使用世界旋转(考虑父级旋转)
const rotation = transform.worldRotation ?? transform.rotation;
// 使用排序层和层内顺序 | Use sorting layer and order in layer
// 使用排序层和世界层内顺序 | Use sorting layer and world order in layer
const sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer;
// worldOrderInLayer 考虑了父子层级关系,确保子元素渲染在父元素之上
// worldOrderInLayer considers parent-child hierarchy, ensuring children render on top of parents
const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;
@@ -107,24 +109,56 @@ export class UIRectRenderSystem extends EntitySystem {
const textureGuid = typeof render.textureGuid === 'string' ? render.textureGuid : undefined;
const textureId = typeof render.textureGuid === 'number' ? render.textureGuid : undefined;
collector.addRect(
renderX, renderY,
width, height,
render.textureTint,
alpha,
sortingLayer,
orderInLayer,
{
rotation,
pivotX,
pivotY,
textureId,
textureGuid,
uv: render.textureUV
? [render.textureUV.u0, render.textureUV.v0, render.textureUV.u1, render.textureUV.v1]
: undefined
}
);
// Handle nine-patch rendering
// 处理九宫格渲染
if (render.type === UIRenderType.NinePatch &&
render.textureWidth > 0 &&
render.textureHeight > 0) {
// addNinePatch expects top-left corner coordinates
// Y-up coordinate system: top = bottom + height
// addNinePatch 期望左上角坐标
// Y轴向上坐标系顶部 = 底部 + 高度
const topLeftX = x;
const topLeftY = y + height;
collector.addNinePatch(
topLeftX, topLeftY,
width, height,
render.ninePatchMargins,
render.textureWidth,
render.textureHeight,
render.textureTint,
alpha,
sortingLayer,
orderInLayer,
{
rotation,
textureId,
textureGuid
}
);
} else {
// Standard image rendering
// 标准图像渲染
collector.addRect(
renderX, renderY,
width, height,
render.textureTint,
alpha,
sortingLayer,
orderInLayer,
{
rotation,
pivotX,
pivotY,
textureId,
textureGuid,
uv: render.textureUV
? [render.textureUV.u0, render.textureUV.v0, render.textureUV.u1, render.textureUV.v1]
: undefined
}
);
}
}
// Render background color if fill is enabled
// 如果启用填充,渲染背景颜色

View File

@@ -56,7 +56,7 @@ export interface UIRenderPrimitive {
rotation: number;
/** Pivot/Origin X (0-1, 0=left, 0.5=center, 1=right) | 锚点 X (0-1, 0=左, 0.5=中心, 1=右) */
pivotX: number;
/** Pivot/Origin Y (0-1, 0=top, 0.5=center, 1=bottom) | 锚点 Y (0-1, 0=, 0.5=中心, 1=下) */
/** Pivot/Origin Y (0-1, 0=bottom, 0.5=center, 1=top) in Y-up system | 锚点 Y (0-1, 0=, 0.5=中心, 1=上) Y轴向上坐标系 */
pivotY: number;
/** Packed color (0xAABBGGRR) | 打包颜色 */
color: number;
@@ -171,6 +171,136 @@ export class UIRenderCollector {
this.cache = null;
}
/**
* Add a nine-patch (9-slice) primitive
* 添加九宫格原语
*
* Nine-patch divides the texture into 9 regions:
* - Corners: Keep original size
* - Edges: Stretch in one direction
* - Center: Stretches in both directions
*
* 九宫格将纹理分为 9 个区域:
* - 角落:保持原始尺寸
* - 边缘:单向拉伸
* - 中心:双向拉伸
*
* @param x - X position | X 坐标
* @param y - Y position | Y 坐标
* @param width - Target width | 目标宽度
* @param height - Target height | 目标高度
* @param margins - Nine-patch margins [top, right, bottom, left] | 九宫格边距
* @param textureWidth - Source texture width | 源纹理宽度
* @param textureHeight - Source texture height | 源纹理高度
* @param color - Tint color | 着色颜色
* @param alpha - Alpha value | 透明度
* @param sortingLayer - Sorting layer | 排序层
* @param orderInLayer - Order in layer | 层内顺序
* @param options - Additional options | 额外选项
*/
addNinePatch(
x: number,
y: number,
width: number,
height: number,
margins: [number, number, number, number],
textureWidth: number,
textureHeight: number,
color: number,
alpha: number,
sortingLayer: string,
orderInLayer: number,
options?: {
rotation?: number;
textureId?: number;
textureGuid?: string;
}
): void {
const [marginTop, marginRight, marginBottom, marginLeft] = margins;
// Ensure minimum size to avoid negative dimensions
// 确保最小尺寸以避免负尺寸
const minWidth = marginLeft + marginRight;
const minHeight = marginTop + marginBottom;
const targetWidth = Math.max(width, minWidth);
const targetHeight = Math.max(height, minHeight);
// Calculate center dimensions
// 计算中心区域尺寸
const centerWidth = targetWidth - marginLeft - marginRight;
const centerHeight = targetHeight - marginTop - marginBottom;
// Source texture UV boundaries (normalized)
// 源纹理 UV 边界(归一化)
const uvLeft = marginLeft / textureWidth;
const uvRight = (textureWidth - marginRight) / textureWidth;
const uvTop = marginTop / textureHeight;
const uvBottom = (textureHeight - marginBottom) / textureHeight;
// Common options for all patches
// 所有 patch 的公共选项
// Note: pivotY=1 means position is top-left corner (Y-up coordinate system)
// 注意pivotY=1 表示位置是左上角Y轴向上坐标系
const baseOptions = {
rotation: options?.rotation ?? 0,
pivotX: 0,
pivotY: 1,
textureId: options?.textureId,
textureGuid: options?.textureGuid
};
// Helper to add a patch with specific UVs
// 辅助函数:添加具有特定 UV 的 patch
const addPatch = (
px: number,
py: number,
pw: number,
ph: number,
u0: number,
v0: number,
u1: number,
v1: number
) => {
if (pw <= 0 || ph <= 0) return;
this.addRect(px, py, pw, ph, color, alpha, sortingLayer, orderInLayer, {
...baseOptions,
uv: [u0, v0, u1, v1]
});
};
// Y-up coordinate system: y decreases as we go down
// Y轴向上坐标系向下移动时 y 减小
// (x, y) is top-left corner, patches extend downward (negative y direction)
// (x, y) 是左上角patch 向下延伸y 减小方向)
// Top-left corner | 左上角
addPatch(x, y, marginLeft, marginTop, 0, 0, uvLeft, uvTop);
// Top edge | 顶边
addPatch(x + marginLeft, y, centerWidth, marginTop, uvLeft, 0, uvRight, uvTop);
// Top-right corner | 右上角
addPatch(x + marginLeft + centerWidth, y, marginRight, marginTop, uvRight, 0, 1, uvTop);
// Left edge | 左边 (move down = subtract y)
addPatch(x, y - marginTop, marginLeft, centerHeight, 0, uvTop, uvLeft, uvBottom);
// Center | 中心
addPatch(x + marginLeft, y - marginTop, centerWidth, centerHeight, uvLeft, uvTop, uvRight, uvBottom);
// Right edge | 右边
addPatch(x + marginLeft + centerWidth, y - marginTop, marginRight, centerHeight, uvRight, uvTop, 1, uvBottom);
// Bottom-left corner | 左下角
addPatch(x, y - marginTop - centerHeight, marginLeft, marginBottom, 0, uvBottom, uvLeft, 1);
// Bottom edge | 底边
addPatch(x + marginLeft, y - marginTop - centerHeight, centerWidth, marginBottom, uvLeft, uvBottom, uvRight, 1);
// Bottom-right corner | 右下角
addPatch(x + marginLeft + centerWidth, y - marginTop - centerHeight, marginRight, marginBottom, uvRight, uvBottom, 1, 1);
}
/**
* Get render data
* 获取渲染数据

View File

@@ -56,9 +56,9 @@ export class UIScrollViewRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
// 使用排序层和层内顺序 | Use sorting layer and order in layer
// 使用排序层和世界层内顺序 | Use sorting layer and world order in layer
const sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer;
const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 计算位置
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;

View File

@@ -55,9 +55,9 @@ export class UISliderRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
// 使用排序层和层内顺序 | Use sorting layer and order in layer
// 使用排序层和世界层内顺序 | Use sorting layer and world order in layer
const sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer;
const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 计算中心位置
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;

View File

@@ -112,9 +112,9 @@ export class UITextRenderSystem extends EntitySystem {
const width = (transform.computedWidth ?? transform.width) * scaleX;
const height = (transform.computedHeight ?? transform.height) * scaleY;
const alpha = transform.worldAlpha ?? transform.alpha;
// 使用排序层和层内顺序 | Use sorting layer and order in layer
// 使用排序层和世界层内顺序 | Use sorting layer and world order in layer
const sortingLayer = transform.sortingLayer;
const orderInLayer = transform.orderInLayer;
const orderInLayer = transform.worldOrderInLayer;
// 使用 transform 的 pivot 作为旋转/缩放中心
const pivotX = transform.pivotX;
const pivotY = transform.pivotY;