Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
"name": "@esengine/ui",
|
||||
"version": "1.0.0",
|
||||
"description": "ECS-based UI system with WebGL rendering for games",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
"pluginExport": "UIPlugin",
|
||||
"category": "ui"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -10,52 +15,24 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./dist/runtime.d.ts",
|
||||
"import": "./dist/runtime.js"
|
||||
},
|
||||
"./editor": {
|
||||
"types": "./dist/editor/index.d.ts",
|
||||
"import": "./dist/editor/index.js"
|
||||
},
|
||||
"./plugin.json": "./plugin.json"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"plugin.json"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.0.0",
|
||||
"@esengine/ecs-components": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@esengine/editor-core": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"lucide-react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.7.0"
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Entity, Scene } from '@esengine/ecs-framework';
|
||||
import { Entity, Scene, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { UITransformComponent, AnchorPreset } from './components/UITransformComponent';
|
||||
import { UIRenderComponent, UIRenderType } from './components/UIRenderComponent';
|
||||
import { UIInteractableComponent } from './components/UIInteractableComponent';
|
||||
@@ -145,6 +145,9 @@ export class UIBuilder {
|
||||
private createBase(config: UIBaseConfig, defaultName: string): Entity {
|
||||
const entity = this.scene.createEntity(config.name ?? `${defaultName}_${this.idCounter++}`);
|
||||
|
||||
// 添加 HierarchyComponent 支持层级结构
|
||||
entity.addComponent(new HierarchyComponent());
|
||||
|
||||
const transform = entity.addComponent(new UITransformComponent());
|
||||
transform.x = config.x ?? 0;
|
||||
transform.y = config.y ?? 0;
|
||||
@@ -419,7 +422,10 @@ export class UIBuilder {
|
||||
* Add child to parent
|
||||
*/
|
||||
public addChild(parent: Entity, child: Entity): Entity {
|
||||
parent.addChild(child);
|
||||
const hierarchySystem = this.scene.getSystem(HierarchySystem);
|
||||
if (hierarchySystem) {
|
||||
hierarchySystem.setParent(child, parent);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
@@ -428,8 +434,11 @@ export class UIBuilder {
|
||||
* Add multiple children to parent
|
||||
*/
|
||||
public addChildren(parent: Entity, children: Entity[]): Entity[] {
|
||||
for (const child of children) {
|
||||
parent.addChild(child);
|
||||
const hierarchySystem = this.scene.getSystem(HierarchySystem);
|
||||
if (hierarchySystem) {
|
||||
for (const child of children) {
|
||||
hierarchySystem.setParent(child, parent);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
/**
|
||||
* UI Runtime Module (Pure runtime, no editor dependencies)
|
||||
* UI 运行时模块(纯运行时,无编辑器依赖)
|
||||
*/
|
||||
|
||||
import type { IScene } from '@esengine/ecs-framework';
|
||||
import { ComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components';
|
||||
import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core';
|
||||
|
||||
import {
|
||||
UITransformComponent,
|
||||
@@ -32,11 +27,14 @@ import {
|
||||
UIScrollViewRenderSystem
|
||||
} from './systems/render';
|
||||
|
||||
/**
|
||||
* UI Runtime Module
|
||||
* UI 运行时模块
|
||||
*/
|
||||
export class UIRuntimeModule implements IRuntimeModuleLoader {
|
||||
export interface UISystemContext extends SystemContext {
|
||||
uiLayoutSystem?: UILayoutSystem;
|
||||
uiRenderProvider?: UIRenderDataProvider;
|
||||
uiInputSystem?: UIInputSystem;
|
||||
uiTextRenderSystem?: UITextRenderSystem;
|
||||
}
|
||||
|
||||
class UIRuntimeModule implements IRuntimeModule {
|
||||
registerComponents(registry: typeof ComponentRegistry): void {
|
||||
registry.register(UITransformComponent);
|
||||
registry.register(UIRenderComponent);
|
||||
@@ -50,6 +48,8 @@ export class UIRuntimeModule implements IRuntimeModuleLoader {
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
const uiContext = context as UISystemContext;
|
||||
|
||||
const layoutSystem = new UILayoutSystem();
|
||||
scene.addSystem(layoutSystem);
|
||||
|
||||
@@ -77,9 +77,9 @@ export class UIRuntimeModule implements IRuntimeModuleLoader {
|
||||
const textRenderSystem = new UITextRenderSystem();
|
||||
scene.addSystem(textRenderSystem);
|
||||
|
||||
if (context.engineBridge) {
|
||||
if (uiContext.engineBridge) {
|
||||
textRenderSystem.setTextureCallback((id: number, dataUrl: string) => {
|
||||
context.engineBridge.loadTexture(id, dataUrl);
|
||||
uiContext.engineBridge.loadTexture(id, dataUrl);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,9 +88,26 @@ export class UIRuntimeModule implements IRuntimeModuleLoader {
|
||||
inputSystem.setLayoutSystem(layoutSystem);
|
||||
scene.addSystem(inputSystem);
|
||||
|
||||
context.uiLayoutSystem = layoutSystem;
|
||||
context.uiRenderProvider = uiRenderProvider;
|
||||
context.uiInputSystem = inputSystem;
|
||||
context.uiTextRenderSystem = textRenderSystem;
|
||||
uiContext.uiLayoutSystem = layoutSystem;
|
||||
uiContext.uiRenderProvider = uiRenderProvider;
|
||||
uiContext.uiInputSystem = inputSystem;
|
||||
uiContext.uiTextRenderSystem = textRenderSystem;
|
||||
}
|
||||
}
|
||||
|
||||
const descriptor: PluginDescriptor = {
|
||||
id: '@esengine/ui',
|
||||
name: 'UI',
|
||||
version: '1.0.0',
|
||||
description: 'ECS-based UI system',
|
||||
category: 'ui',
|
||||
enabledByDefault: true,
|
||||
isEnginePlugin: true
|
||||
};
|
||||
|
||||
export const UIPlugin: IPlugin = {
|
||||
descriptor,
|
||||
runtimeModule: new UIRuntimeModule()
|
||||
};
|
||||
|
||||
export { UIRuntimeModule };
|
||||
|
||||
@@ -232,6 +232,32 @@ export class UITransformComponent extends Component {
|
||||
*/
|
||||
public worldAlpha: number = 1;
|
||||
|
||||
/**
|
||||
* 计算后的世界旋转(弧度,考虑父元素旋转)
|
||||
* Computed world rotation in radians (considering parent rotation)
|
||||
*/
|
||||
public worldRotation: number = 0;
|
||||
|
||||
/**
|
||||
* 计算后的世界 X 缩放(考虑父元素缩放)
|
||||
* Computed world X scale (considering parent scale)
|
||||
*/
|
||||
public worldScaleX: number = 1;
|
||||
|
||||
/**
|
||||
* 计算后的世界 Y 缩放(考虑父元素缩放)
|
||||
* Computed world Y scale (considering parent scale)
|
||||
*/
|
||||
public worldScaleY: number = 1;
|
||||
|
||||
/**
|
||||
* 本地到世界的 2D 变换矩阵(只读,由 UILayoutSystem 计算)
|
||||
* Local to world 2D transformation matrix (readonly, computed by UILayoutSystem)
|
||||
*/
|
||||
public localToWorldMatrix: { a: number; b: number; c: number; d: number; tx: number; ty: number } = {
|
||||
a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* 布局是否需要更新
|
||||
* Flag indicating layout needs update
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* UI 统一插件
|
||||
* UI Unified Plugin
|
||||
*/
|
||||
|
||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { ComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IPluginLoader,
|
||||
IRuntimeModuleLoader,
|
||||
PluginDescriptor,
|
||||
SystemContext
|
||||
} from '@esengine/editor-core';
|
||||
|
||||
// Editor imports
|
||||
import { UIEditorModule } from './index';
|
||||
|
||||
// Runtime imports
|
||||
import {
|
||||
UITransformComponent,
|
||||
UIRenderComponent,
|
||||
UIInteractableComponent,
|
||||
UITextComponent,
|
||||
UILayoutComponent,
|
||||
UIButtonComponent,
|
||||
UIProgressBarComponent,
|
||||
UISliderComponent,
|
||||
UIScrollViewComponent
|
||||
} from '../components';
|
||||
import { UILayoutSystem } from '../systems/UILayoutSystem';
|
||||
import { UIInputSystem } from '../systems/UIInputSystem';
|
||||
import { UIRenderDataProvider } from '../systems/UIRenderDataProvider';
|
||||
// Render systems
|
||||
import {
|
||||
UIRenderBeginSystem,
|
||||
UIRectRenderSystem,
|
||||
UITextRenderSystem,
|
||||
UIButtonRenderSystem,
|
||||
UIProgressBarRenderSystem,
|
||||
UISliderRenderSystem,
|
||||
UIScrollViewRenderSystem
|
||||
} from '../systems/render';
|
||||
|
||||
/**
|
||||
* 插件描述符
|
||||
*/
|
||||
const descriptor: PluginDescriptor = {
|
||||
id: '@esengine/ui',
|
||||
name: 'UI System',
|
||||
version: '1.0.0',
|
||||
description: '游戏 UI 系统,支持布局、交互、动画等',
|
||||
category: 'ui',
|
||||
enabledByDefault: true,
|
||||
canContainContent: false,
|
||||
isEnginePlugin: true,
|
||||
modules: [
|
||||
{
|
||||
name: 'UIRuntime',
|
||||
type: 'runtime',
|
||||
loadingPhase: 'default',
|
||||
entry: './src/index.ts'
|
||||
},
|
||||
{
|
||||
name: 'UIEditor',
|
||||
type: 'editor',
|
||||
loadingPhase: 'default',
|
||||
entry: './src/editor/index.ts'
|
||||
}
|
||||
],
|
||||
dependencies: [
|
||||
{ id: '@esengine/core', version: '^1.0.0' }
|
||||
],
|
||||
icon: 'LayoutGrid'
|
||||
};
|
||||
|
||||
/**
|
||||
* UI 运行时模块
|
||||
* UI runtime module
|
||||
*/
|
||||
export class UIRuntimeModule implements IRuntimeModuleLoader {
|
||||
registerComponents(registry: typeof ComponentRegistry): void {
|
||||
registry.register(UITransformComponent);
|
||||
registry.register(UIRenderComponent);
|
||||
registry.register(UIInteractableComponent);
|
||||
registry.register(UITextComponent);
|
||||
registry.register(UILayoutComponent);
|
||||
registry.register(UIButtonComponent);
|
||||
registry.register(UIProgressBarComponent);
|
||||
registry.register(UISliderComponent);
|
||||
registry.register(UIScrollViewComponent);
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
// UI Layout System (order: 50)
|
||||
const layoutSystem = new UILayoutSystem();
|
||||
scene.addSystem(layoutSystem);
|
||||
|
||||
// UI Render Begin System - clears collector at start of frame (order: 99)
|
||||
const renderBeginSystem = new UIRenderBeginSystem();
|
||||
scene.addSystem(renderBeginSystem);
|
||||
|
||||
// UI Render Systems - collect render data (order: 100-120)
|
||||
const rectRenderSystem = new UIRectRenderSystem();
|
||||
scene.addSystem(rectRenderSystem);
|
||||
|
||||
const progressBarRenderSystem = new UIProgressBarRenderSystem();
|
||||
scene.addSystem(progressBarRenderSystem);
|
||||
|
||||
const sliderRenderSystem = new UISliderRenderSystem();
|
||||
scene.addSystem(sliderRenderSystem);
|
||||
|
||||
const scrollViewRenderSystem = new UIScrollViewRenderSystem();
|
||||
scene.addSystem(scrollViewRenderSystem);
|
||||
|
||||
const buttonRenderSystem = new UIButtonRenderSystem();
|
||||
scene.addSystem(buttonRenderSystem);
|
||||
|
||||
const textRenderSystem = new UITextRenderSystem();
|
||||
scene.addSystem(textRenderSystem);
|
||||
|
||||
// Set up text texture callback to register textures with engine
|
||||
// 设置文本纹理回调以将纹理注册到引擎
|
||||
if (context.engineBridge) {
|
||||
textRenderSystem.setTextureCallback((id: number, dataUrl: string) => {
|
||||
// Load data URL as texture
|
||||
context.engineBridge.loadTexture(id, dataUrl);
|
||||
});
|
||||
}
|
||||
|
||||
// UI Render Data Provider (not a system, just a provider)
|
||||
// Note: Don't call addRenderDataProvider here - UI provider should be set via
|
||||
// setUIRenderDataProvider for proper preview mode support
|
||||
// 注意:不要在这里调用 addRenderDataProvider - UI 提供者应该通过
|
||||
// setUIRenderDataProvider 设置以支持预览模式
|
||||
const uiRenderProvider = new UIRenderDataProvider();
|
||||
|
||||
// UI Input System
|
||||
const inputSystem = new UIInputSystem();
|
||||
scene.addSystem(inputSystem);
|
||||
|
||||
// 保存引用 | Save references
|
||||
context.uiLayoutSystem = layoutSystem;
|
||||
context.uiRenderProvider = uiRenderProvider;
|
||||
context.uiInputSystem = inputSystem;
|
||||
context.uiTextRenderSystem = textRenderSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 插件加载器
|
||||
* UI plugin loader
|
||||
*/
|
||||
export const UIPlugin: IPluginLoader = {
|
||||
descriptor,
|
||||
runtimeModule: new UIRuntimeModule(),
|
||||
editorModule: new UIEditorModule(),
|
||||
};
|
||||
|
||||
export default UIPlugin;
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { Entity } from '@esengine/ecs-framework';
|
||||
import type { IGizmoRenderData, IRectGizmoData, GizmoColor } from '@esengine/editor-core';
|
||||
import { GizmoRegistry } from '@esengine/editor-core';
|
||||
import { UITransformComponent } from '../../components';
|
||||
|
||||
const UI_GIZMO_COLOR: GizmoColor = { r: 0.2, g: 0.6, b: 1, a: 0.8 };
|
||||
const UI_GIZMO_COLOR_UNSELECTED: GizmoColor = { r: 0.2, g: 0.6, b: 1, a: 0.3 };
|
||||
|
||||
function uiTransformGizmoProvider(
|
||||
transform: UITransformComponent,
|
||||
_entity: Entity,
|
||||
isSelected: boolean
|
||||
): IGizmoRenderData[] {
|
||||
if (!transform.visible) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use world coordinates (computed by UILayoutSystem) if available
|
||||
// Otherwise fallback to local coordinates
|
||||
// 使用世界坐标(由 UILayoutSystem 计算),如果可用
|
||||
// 否则回退到本地坐标
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
|
||||
// Use bottom-left position with origin at (0, 0)
|
||||
// x, y is bottom-left corner in UITransform coordinate system (Y-up)
|
||||
// This matches Gizmo origin=(0,0) which means reference point is at bottom-left
|
||||
// 使用左下角位置,原点在 (0, 0)
|
||||
// UITransform 坐标系中 x, y 是左下角(Y 向上)
|
||||
// 这与 Gizmo origin=(0,0) 匹配,表示参考点在左下角
|
||||
const gizmo: IRectGizmoData = {
|
||||
type: 'rect',
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
rotation: transform.rotation,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
color: isSelected ? UI_GIZMO_COLOR : UI_GIZMO_COLOR_UNSELECTED,
|
||||
showHandles: isSelected
|
||||
};
|
||||
|
||||
return [gizmo];
|
||||
}
|
||||
|
||||
export function registerUITransformGizmo(): void {
|
||||
GizmoRegistry.register(UITransformComponent, uiTransformGizmoProvider);
|
||||
}
|
||||
|
||||
export function unregisterUITransformGizmo(): void {
|
||||
GizmoRegistry.unregister(UITransformComponent);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './UITransformGizmo';
|
||||
@@ -1,410 +0,0 @@
|
||||
/**
|
||||
* UI 编辑器模块入口
|
||||
* UI Editor Module Entry
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { LayoutGrid, Square, Type, MousePointer2, Sliders, BarChart3, ScrollText, PanelTop } from 'lucide-react';
|
||||
import type { ServiceContainer, Entity } from '@esengine/ecs-framework';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IEditorModuleLoader,
|
||||
PanelDescriptor,
|
||||
EntityCreationTemplate,
|
||||
ComponentAction,
|
||||
ComponentInspectorProviderDef
|
||||
} from '@esengine/editor-core';
|
||||
import {
|
||||
EntityStoreService,
|
||||
MessageHub,
|
||||
ComponentRegistry,
|
||||
ComponentInspectorRegistry
|
||||
} from '@esengine/editor-core';
|
||||
|
||||
// Local imports
|
||||
import {
|
||||
UITransformComponent,
|
||||
UIRenderComponent,
|
||||
UIInteractableComponent,
|
||||
UITextComponent,
|
||||
UILayoutComponent,
|
||||
UILayoutType,
|
||||
UIJustifyContent,
|
||||
UIAlignItems,
|
||||
UIButtonComponent,
|
||||
UIProgressBarComponent,
|
||||
UISliderComponent,
|
||||
UIScrollViewComponent
|
||||
} from '../components';
|
||||
import { UITransformInspector } from './inspectors';
|
||||
import { registerUITransformGizmo, unregisterUITransformGizmo } from './gizmos';
|
||||
|
||||
// Re-exports
|
||||
export { UITransformInspector } from './inspectors';
|
||||
export { registerUITransformGizmo, unregisterUITransformGizmo } from './gizmos';
|
||||
|
||||
/**
|
||||
* UI 编辑器模块
|
||||
* UI Editor Module
|
||||
*/
|
||||
export class UIEditorModule implements IEditorModuleLoader {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
// 注册 UI 组件到编辑器组件注册表 | Register UI components to editor component registry
|
||||
const componentRegistry = services.resolve(ComponentRegistry);
|
||||
if (componentRegistry) {
|
||||
const uiComponents = [
|
||||
{ name: 'UITransform', type: UITransformComponent, category: 'components.category.ui', description: 'UI element positioning and sizing', icon: 'Move' },
|
||||
{ name: 'UIRender', type: UIRenderComponent, category: 'components.category.ui', description: 'UI element visual appearance', icon: 'Palette' },
|
||||
{ name: 'UIInteractable', type: UIInteractableComponent, category: 'components.category.ui', description: 'UI element interaction handling', icon: 'MousePointer2' },
|
||||
{ name: 'UIText', type: UITextComponent, category: 'components.category.ui', description: 'Text rendering component', icon: 'Type' },
|
||||
{ name: 'UILayout', type: UILayoutComponent, category: 'components.category.ui', description: 'Automatic child layout (Flexbox-like)', icon: 'LayoutGrid' },
|
||||
{ name: 'UIButton', type: UIButtonComponent, category: 'components.category.ui.widgets', description: 'Interactive button component', icon: 'RectangleHorizontal' },
|
||||
{ name: 'UIProgressBar', type: UIProgressBarComponent, category: 'components.category.ui.widgets', description: 'Progress indicator component', icon: 'BarChart3' },
|
||||
{ name: 'UISlider', type: UISliderComponent, category: 'components.category.ui.widgets', description: 'Value slider component', icon: 'Sliders' },
|
||||
{ name: 'UIScrollView', type: UIScrollViewComponent, category: 'components.category.ui.widgets', description: 'Scrollable container component', icon: 'ScrollText' },
|
||||
];
|
||||
|
||||
for (const comp of uiComponents) {
|
||||
componentRegistry.register({
|
||||
name: comp.name,
|
||||
type: comp.type,
|
||||
category: comp.category,
|
||||
description: comp.description,
|
||||
icon: comp.icon
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义组件检视器 | Register custom component inspectors
|
||||
const componentInspectorRegistry = services.tryResolve(ComponentInspectorRegistry);
|
||||
if (componentInspectorRegistry) {
|
||||
componentInspectorRegistry.register(new UITransformInspector());
|
||||
}
|
||||
|
||||
// 注册 Gizmo | Register gizmo
|
||||
registerUITransformGizmo();
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
unregisterUITransformGizmo();
|
||||
}
|
||||
|
||||
getEntityCreationTemplates(): EntityCreationTemplate[] {
|
||||
return [
|
||||
// UI Canvas (Root container)
|
||||
{
|
||||
id: 'create-ui-canvas',
|
||||
label: 'UI Canvas',
|
||||
icon: 'PanelTop',
|
||||
category: 'ui',
|
||||
order: 200,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('UI Canvas', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 1920;
|
||||
transform.height = 1080;
|
||||
transform.anchorMinX = 0;
|
||||
transform.anchorMinY = 0;
|
||||
transform.anchorMaxX = 1;
|
||||
transform.anchorMaxY = 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Panel
|
||||
{
|
||||
id: 'create-ui-panel',
|
||||
label: 'Panel',
|
||||
icon: 'Square',
|
||||
category: 'ui',
|
||||
order: 201,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('Panel', (entity) => {
|
||||
const render = entity.getComponent(UIRenderComponent)!;
|
||||
render.backgroundColor = 0x2D2D2D;
|
||||
render.backgroundAlpha = 0.9;
|
||||
render.setCornerRadius(8);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Text
|
||||
{
|
||||
id: 'create-ui-text',
|
||||
label: 'Text',
|
||||
icon: 'Type',
|
||||
category: 'ui',
|
||||
order: 202,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('Text', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 200;
|
||||
transform.height = 30;
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent)!;
|
||||
render.backgroundAlpha = 0;
|
||||
|
||||
const text = new UITextComponent();
|
||||
text.text = 'Hello World';
|
||||
text.fontSize = 16;
|
||||
text.color = 0xFFFFFF;
|
||||
entity.addComponent(text);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Button
|
||||
{
|
||||
id: 'create-ui-button',
|
||||
label: 'Button',
|
||||
icon: 'MousePointer2',
|
||||
category: 'ui',
|
||||
order: 203,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('Button', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 120;
|
||||
transform.height = 40;
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent)!;
|
||||
render.setCornerRadius(4);
|
||||
|
||||
const button = new UIButtonComponent();
|
||||
button.label = 'Button';
|
||||
entity.addComponent(button);
|
||||
|
||||
const interactable = entity.getComponent(UIInteractableComponent)!;
|
||||
interactable.enabled = true;
|
||||
interactable.cursor = 'pointer';
|
||||
|
||||
const text = new UITextComponent();
|
||||
text.text = 'Button';
|
||||
text.fontSize = 14;
|
||||
text.color = 0xFFFFFF;
|
||||
text.align = 'center';
|
||||
text.verticalAlign = 'middle';
|
||||
entity.addComponent(text);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Slider
|
||||
{
|
||||
id: 'create-ui-slider',
|
||||
label: 'Slider',
|
||||
icon: 'Sliders',
|
||||
category: 'ui',
|
||||
order: 204,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('Slider', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 200;
|
||||
transform.height = 20;
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent);
|
||||
if (render) {
|
||||
entity.removeComponent(render);
|
||||
}
|
||||
|
||||
const slider = new UISliderComponent();
|
||||
slider.value = 50;
|
||||
slider.minValue = 0;
|
||||
slider.maxValue = 100;
|
||||
entity.addComponent(slider);
|
||||
|
||||
const interactable = entity.getComponent(UIInteractableComponent)!;
|
||||
interactable.enabled = true;
|
||||
interactable.cursor = 'pointer';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Progress Bar
|
||||
{
|
||||
id: 'create-ui-progressbar',
|
||||
label: 'ProgressBar',
|
||||
icon: 'BarChart3',
|
||||
category: 'ui',
|
||||
order: 205,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('ProgressBar', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 200;
|
||||
transform.height = 20;
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent);
|
||||
if (render) {
|
||||
entity.removeComponent(render);
|
||||
}
|
||||
|
||||
const progress = new UIProgressBarComponent();
|
||||
progress.value = 50;
|
||||
progress.minValue = 0;
|
||||
progress.maxValue = 100;
|
||||
progress.cornerRadius = 4;
|
||||
entity.addComponent(progress);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI ScrollView
|
||||
{
|
||||
id: 'create-ui-scrollview',
|
||||
label: 'ScrollView',
|
||||
icon: 'ScrollText',
|
||||
category: 'ui',
|
||||
order: 206,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('ScrollView', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 300;
|
||||
transform.height = 400;
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent)!;
|
||||
render.backgroundColor = 0x1A1A1A;
|
||||
render.setCornerRadius(4);
|
||||
|
||||
const scrollView = new UIScrollViewComponent();
|
||||
scrollView.verticalScroll = true;
|
||||
scrollView.horizontalScroll = false;
|
||||
scrollView.contentHeight = 800;
|
||||
entity.addComponent(scrollView);
|
||||
|
||||
const interactable = entity.getComponent(UIInteractableComponent)!;
|
||||
interactable.enabled = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Layout Container (Horizontal)
|
||||
{
|
||||
id: 'create-ui-hlayout',
|
||||
label: 'HLayout',
|
||||
icon: 'LayoutGrid',
|
||||
category: 'ui',
|
||||
order: 207,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('HLayout', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 400;
|
||||
transform.height = 100;
|
||||
|
||||
const layout = new UILayoutComponent();
|
||||
layout.type = UILayoutType.Horizontal;
|
||||
layout.gap = 10;
|
||||
layout.justifyContent = UIJustifyContent.Start;
|
||||
layout.alignItems = UIAlignItems.Center;
|
||||
entity.addComponent(layout);
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent)!;
|
||||
render.backgroundAlpha = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Layout Container (Vertical)
|
||||
{
|
||||
id: 'create-ui-vlayout',
|
||||
label: 'VLayout',
|
||||
icon: 'LayoutGrid',
|
||||
category: 'ui',
|
||||
order: 208,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('VLayout', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 200;
|
||||
transform.height = 400;
|
||||
|
||||
const layout = new UILayoutComponent();
|
||||
layout.type = UILayoutType.Vertical;
|
||||
layout.gap = 10;
|
||||
layout.justifyContent = UIJustifyContent.Start;
|
||||
layout.alignItems = UIAlignItems.Stretch;
|
||||
entity.addComponent(layout);
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent)!;
|
||||
render.backgroundAlpha = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// UI Grid Layout
|
||||
{
|
||||
id: 'create-ui-grid',
|
||||
label: 'Grid',
|
||||
icon: 'LayoutGrid',
|
||||
category: 'ui',
|
||||
order: 209,
|
||||
create: (): number => {
|
||||
return this.createUIEntity('Grid', (entity) => {
|
||||
const transform = entity.getComponent(UITransformComponent)!;
|
||||
transform.width = 400;
|
||||
transform.height = 400;
|
||||
|
||||
const layout = new UILayoutComponent();
|
||||
layout.type = UILayoutType.Grid;
|
||||
layout.columns = 3;
|
||||
layout.gap = 10;
|
||||
entity.addComponent(layout);
|
||||
|
||||
const render = entity.getComponent(UIRenderComponent)!;
|
||||
render.backgroundAlpha = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 UI 实体的辅助方法
|
||||
* Helper method to create UI entity
|
||||
*/
|
||||
private createUIEntity(baseName: string, configure?: (entity: Entity) => void): number {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('Scene not available');
|
||||
}
|
||||
|
||||
const entityStore = Core.services.resolve(EntityStoreService);
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
|
||||
if (!entityStore || !messageHub) {
|
||||
throw new Error('EntityStoreService or MessageHub not available');
|
||||
}
|
||||
|
||||
const existingCount = entityStore.getAllEntities()
|
||||
.filter((e: Entity) => e.name.startsWith(baseName)).length;
|
||||
const entityName = existingCount > 0 ? `${baseName} ${existingCount + 1}` : baseName;
|
||||
|
||||
const entity = scene.createEntity(entityName);
|
||||
|
||||
const transform = new UITransformComponent();
|
||||
transform.width = 100;
|
||||
transform.height = 100;
|
||||
entity.addComponent(transform);
|
||||
|
||||
const render = new UIRenderComponent();
|
||||
render.backgroundColor = 0x4A90D9;
|
||||
entity.addComponent(render);
|
||||
|
||||
const interactable = new UIInteractableComponent();
|
||||
entity.addComponent(interactable);
|
||||
|
||||
if (configure) {
|
||||
configure(entity);
|
||||
}
|
||||
|
||||
entityStore.addEntity(entity);
|
||||
messageHub.publish('entity:added', { entity });
|
||||
messageHub.publish('scene:modified', {});
|
||||
entityStore.selectEntity(entity);
|
||||
|
||||
return entity.id;
|
||||
}
|
||||
}
|
||||
|
||||
export const uiEditorModule = new UIEditorModule();
|
||||
|
||||
// Plugin exports
|
||||
export { UIPlugin, UIRuntimeModule } from './UIPlugin';
|
||||
export default uiEditorModule;
|
||||
@@ -1,454 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core';
|
||||
import { UITransformComponent, AnchorPreset } from '../../components';
|
||||
|
||||
const DraggableNumberInput: React.FC<{
|
||||
axis?: 'x' | 'y' | 'z' | 'w';
|
||||
label?: string;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
readOnly?: boolean;
|
||||
}> = ({ axis, label, value, onChange, min, max, step = 0.1, readOnly }) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragStartRef = useRef({ x: 0, value: 0 });
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (readOnly) return;
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
dragStartRef.current = { x: e.clientX, value: value ?? 0 };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = e.clientX - dragStartRef.current.x;
|
||||
const sensitivity = e.shiftKey ? 0.01 : step;
|
||||
let newValue = dragStartRef.current.value + delta * sensitivity;
|
||||
|
||||
if (min !== undefined) newValue = Math.max(min, newValue);
|
||||
if (max !== undefined) newValue = Math.min(max, newValue);
|
||||
|
||||
onChange(Math.round(newValue * 1000) / 1000);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => setIsDragging(false);
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging, onChange, step, min, max]);
|
||||
|
||||
const axisClass = axis ? `property-vector-axis-${axis}` : '';
|
||||
const displayLabel = label || (axis ? axis.toUpperCase() : '');
|
||||
|
||||
return (
|
||||
<div className="property-vector-axis-compact">
|
||||
<span
|
||||
className={`property-vector-axis-label ${axisClass}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: readOnly ? 'default' : 'ew-resize' }}
|
||||
>
|
||||
{displayLabel}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
className="property-input property-input-number-compact"
|
||||
value={value ?? 0}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Vector2Row: React.FC<{
|
||||
label: string;
|
||||
valueX: number;
|
||||
valueY: number;
|
||||
onChangeX: (value: number) => void;
|
||||
onChangeY: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
readOnly?: boolean;
|
||||
}> = ({ label, valueX, valueY, onChangeX, onChangeY, min, max, step, readOnly }) => (
|
||||
<div className="property-field">
|
||||
<label className="property-label">{label}</label>
|
||||
<div className="property-vector-compact">
|
||||
<DraggableNumberInput axis="x" value={valueX} onChange={onChangeX} min={min} max={max} step={step} readOnly={readOnly} />
|
||||
<DraggableNumberInput axis="y" value={valueY} onChange={onChangeY} min={min} max={max} step={step} readOnly={readOnly} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const NumberRow: React.FC<{
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
readOnly?: boolean;
|
||||
}> = ({ label, value, onChange, min, max, step = 0.1, readOnly }) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragStartX, setDragStartX] = useState(0);
|
||||
const [dragStartValue, setDragStartValue] = useState(0);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (readOnly) return;
|
||||
setIsDragging(true);
|
||||
setDragStartX(e.clientX);
|
||||
setDragStartValue(value);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = e.clientX - dragStartX;
|
||||
const sensitivity = e.shiftKey ? 0.01 : step;
|
||||
let newValue = dragStartValue + delta * sensitivity;
|
||||
|
||||
if (min !== undefined) newValue = Math.max(min, newValue);
|
||||
if (max !== undefined) newValue = Math.min(max, newValue);
|
||||
|
||||
onChange(parseFloat(newValue.toFixed(3)));
|
||||
};
|
||||
|
||||
const handleMouseUp = () => setIsDragging(false);
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging, dragStartX, dragStartValue, step, min, max, onChange]);
|
||||
|
||||
return (
|
||||
<div className="property-field">
|
||||
<label
|
||||
className="property-label property-label-draggable"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: readOnly ? 'default' : 'ew-resize' }}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="property-input property-input-number"
|
||||
value={value ?? 0}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BooleanRow: React.FC<{
|
||||
label: string;
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
readOnly?: boolean;
|
||||
}> = ({ label, value, onChange, readOnly }) => (
|
||||
<div className="property-field property-field-boolean">
|
||||
<label className="property-label">{label}</label>
|
||||
<button
|
||||
className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'}`}
|
||||
disabled={readOnly}
|
||||
onClick={() => onChange(!value)}
|
||||
>
|
||||
<span className="property-toggle-thumb" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AnchorPresetGrid: React.FC<{
|
||||
currentPreset: string;
|
||||
onSelect: (preset: AnchorPreset) => void;
|
||||
}> = ({ currentPreset, onSelect }) => {
|
||||
const presets: AnchorPreset[][] = [
|
||||
[AnchorPreset.TopLeft, AnchorPreset.TopCenter, AnchorPreset.TopRight],
|
||||
[AnchorPreset.MiddleLeft, AnchorPreset.MiddleCenter, AnchorPreset.MiddleRight],
|
||||
[AnchorPreset.BottomLeft, AnchorPreset.BottomCenter, AnchorPreset.BottomRight],
|
||||
];
|
||||
|
||||
const getAnchorPosition = (preset: AnchorPreset): { x: number; y: number } => {
|
||||
const positions: Record<AnchorPreset, { x: number; y: number }> = {
|
||||
[AnchorPreset.TopLeft]: { x: 3, y: 3 },
|
||||
[AnchorPreset.TopCenter]: { x: 10, y: 3 },
|
||||
[AnchorPreset.TopRight]: { x: 17, y: 3 },
|
||||
[AnchorPreset.MiddleLeft]: { x: 3, y: 10 },
|
||||
[AnchorPreset.MiddleCenter]: { x: 10, y: 10 },
|
||||
[AnchorPreset.MiddleRight]: { x: 17, y: 10 },
|
||||
[AnchorPreset.BottomLeft]: { x: 3, y: 17 },
|
||||
[AnchorPreset.BottomCenter]: { x: 10, y: 17 },
|
||||
[AnchorPreset.BottomRight]: { x: 17, y: 17 },
|
||||
[AnchorPreset.StretchAll]: { x: 10, y: 10 },
|
||||
};
|
||||
return positions[preset];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="property-field" style={{ alignItems: 'flex-start' }}>
|
||||
<label className="property-label">Anchor</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 24px)',
|
||||
gridTemplateRows: 'repeat(3, 24px)',
|
||||
gap: '2px',
|
||||
padding: '4px',
|
||||
background: 'var(--color-bg-inset)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--color-border-default)',
|
||||
}}>
|
||||
{presets.flat().map((preset) => {
|
||||
const pos = getAnchorPosition(preset);
|
||||
const isActive = currentPreset === preset;
|
||||
return (
|
||||
<button
|
||||
key={preset}
|
||||
onClick={() => onSelect(preset)}
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
padding: 0,
|
||||
border: '1px solid',
|
||||
borderColor: isActive ? 'var(--color-primary)' : 'var(--color-border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: isActive ? 'var(--color-primary-subtle)' : 'var(--color-bg-elevated)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all var(--transition-fast)',
|
||||
}}
|
||||
title={preset}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<rect
|
||||
x="2" y="2" width="16" height="16"
|
||||
fill="none"
|
||||
stroke={isActive ? 'var(--color-primary)' : 'var(--color-text-tertiary)'}
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2,2"
|
||||
/>
|
||||
<circle
|
||||
cx={pos.x} cy={pos.y} r="3"
|
||||
fill={isActive ? 'var(--color-primary)' : 'var(--color-text-secondary)'}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onSelect(AnchorPreset.StretchAll)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '22px',
|
||||
padding: '0 8px',
|
||||
border: '1px solid',
|
||||
borderColor: currentPreset === AnchorPreset.StretchAll ? 'var(--color-primary)' : 'var(--color-border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: currentPreset === AnchorPreset.StretchAll ? 'var(--color-primary-subtle)' : 'var(--color-bg-elevated)',
|
||||
color: currentPreset === AnchorPreset.StretchAll ? 'var(--color-primary)' : 'var(--color-text-secondary)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px',
|
||||
fontWeight: 500,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '4px',
|
||||
transition: 'all var(--transition-fast)',
|
||||
}}
|
||||
title="Stretch All"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14">
|
||||
<rect x="1" y="1" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1" />
|
||||
<line x1="3" y1="7" x2="11" y2="7" stroke="currentColor" strokeWidth="1.5" />
|
||||
<line x1="7" y1="3" x2="7" y2="11" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
Stretch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export class UITransformInspector implements IComponentInspector<UITransformComponent> {
|
||||
readonly id = 'uitransform-inspector';
|
||||
readonly name = 'UITransform Inspector';
|
||||
readonly priority = 100;
|
||||
readonly targetComponents = ['UITransform', 'UITransformComponent'];
|
||||
|
||||
canHandle(component: Component): component is UITransformComponent {
|
||||
return component instanceof UITransformComponent ||
|
||||
component.constructor.name === 'UITransformComponent';
|
||||
}
|
||||
|
||||
render(context: ComponentInspectorContext): React.ReactElement {
|
||||
const transform = context.component as UITransformComponent;
|
||||
const onChange = context.onChange;
|
||||
|
||||
const handleChange = (prop: string, value: number | boolean | string) => {
|
||||
onChange?.(prop, value);
|
||||
};
|
||||
|
||||
const detectCurrentPreset = (): string => {
|
||||
const { anchorMinX, anchorMinY, anchorMaxX, anchorMaxY } = transform;
|
||||
if (anchorMinX === 0 && anchorMinY === 0 && anchorMaxX === 1 && anchorMaxY === 1) {
|
||||
return AnchorPreset.StretchAll;
|
||||
}
|
||||
if (anchorMinX === anchorMaxX && anchorMinY === anchorMaxY) {
|
||||
if (anchorMinX === 0 && anchorMinY === 0) return AnchorPreset.TopLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 0) return AnchorPreset.TopCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 0) return AnchorPreset.TopRight;
|
||||
if (anchorMinX === 0 && anchorMinY === 0.5) return AnchorPreset.MiddleLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 0.5) return AnchorPreset.MiddleCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 0.5) return AnchorPreset.MiddleRight;
|
||||
if (anchorMinX === 0 && anchorMinY === 1) return AnchorPreset.BottomLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 1) return AnchorPreset.BottomCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 1) return AnchorPreset.BottomRight;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handlePresetSelect = (preset: AnchorPreset) => {
|
||||
const presetValues: Record<AnchorPreset, [number, number, number, number]> = {
|
||||
[AnchorPreset.TopLeft]: [0, 0, 0, 0],
|
||||
[AnchorPreset.TopCenter]: [0.5, 0, 0.5, 0],
|
||||
[AnchorPreset.TopRight]: [1, 0, 1, 0],
|
||||
[AnchorPreset.MiddleLeft]: [0, 0.5, 0, 0.5],
|
||||
[AnchorPreset.MiddleCenter]: [0.5, 0.5, 0.5, 0.5],
|
||||
[AnchorPreset.MiddleRight]: [1, 0.5, 1, 0.5],
|
||||
[AnchorPreset.BottomLeft]: [0, 1, 0, 1],
|
||||
[AnchorPreset.BottomCenter]: [0.5, 1, 0.5, 1],
|
||||
[AnchorPreset.BottomRight]: [1, 1, 1, 1],
|
||||
[AnchorPreset.StretchAll]: [0, 0, 1, 1],
|
||||
};
|
||||
|
||||
const [minX, minY, maxX, maxY] = presetValues[preset];
|
||||
handleChange('anchorMinX', minX);
|
||||
handleChange('anchorMinY', minY);
|
||||
handleChange('anchorMaxX', maxX);
|
||||
handleChange('anchorMaxY', maxY);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="property-inspector">
|
||||
<AnchorPresetGrid
|
||||
currentPreset={detectCurrentPreset()}
|
||||
onSelect={handlePresetSelect}
|
||||
/>
|
||||
|
||||
<Vector2Row
|
||||
label="Position"
|
||||
valueX={transform.x}
|
||||
valueY={transform.y}
|
||||
onChangeX={(v) => handleChange('x', v)}
|
||||
onChangeY={(v) => handleChange('y', v)}
|
||||
/>
|
||||
|
||||
<Vector2Row
|
||||
label="Size"
|
||||
valueX={transform.width}
|
||||
valueY={transform.height}
|
||||
onChangeX={(v) => handleChange('width', v)}
|
||||
onChangeY={(v) => handleChange('height', v)}
|
||||
min={0}
|
||||
/>
|
||||
|
||||
<Vector2Row
|
||||
label="Anchor Min"
|
||||
valueX={transform.anchorMinX}
|
||||
valueY={transform.anchorMinY}
|
||||
onChangeX={(v) => handleChange('anchorMinX', v)}
|
||||
onChangeY={(v) => handleChange('anchorMinY', v)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
|
||||
<Vector2Row
|
||||
label="Anchor Max"
|
||||
valueX={transform.anchorMaxX}
|
||||
valueY={transform.anchorMaxY}
|
||||
onChangeX={(v) => handleChange('anchorMaxX', v)}
|
||||
onChangeY={(v) => handleChange('anchorMaxY', v)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
|
||||
<Vector2Row
|
||||
label="Pivot"
|
||||
valueX={transform.pivotX}
|
||||
valueY={transform.pivotY}
|
||||
onChangeX={(v) => handleChange('pivotX', v)}
|
||||
onChangeY={(v) => handleChange('pivotY', v)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
|
||||
<NumberRow
|
||||
label="Rotation"
|
||||
value={transform.rotation}
|
||||
onChange={(v) => handleChange('rotation', v)}
|
||||
step={0.01}
|
||||
/>
|
||||
|
||||
<Vector2Row
|
||||
label="Scale"
|
||||
valueX={transform.scaleX}
|
||||
valueY={transform.scaleY}
|
||||
onChangeX={(v) => handleChange('scaleX', v)}
|
||||
onChangeY={(v) => handleChange('scaleY', v)}
|
||||
step={0.01}
|
||||
/>
|
||||
|
||||
<NumberRow
|
||||
label="Z Index"
|
||||
value={transform.zIndex}
|
||||
onChange={(v) => handleChange('zIndex', Math.round(v))}
|
||||
step={1}
|
||||
/>
|
||||
|
||||
<NumberRow
|
||||
label="Alpha"
|
||||
value={transform.alpha}
|
||||
onChange={(v) => handleChange('alpha', v)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
|
||||
<BooleanRow
|
||||
label="Visible"
|
||||
value={transform.visible}
|
||||
onChange={(v) => handleChange('visible', v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './UITransformInspector';
|
||||
@@ -161,8 +161,5 @@ export {
|
||||
type UIScrollViewConfig
|
||||
} from './UIBuilder';
|
||||
|
||||
// Runtime module (no editor dependencies)
|
||||
export { UIRuntimeModule } from './UIRuntimeModule';
|
||||
|
||||
// Plugin (for PluginManager - includes editor dependencies)
|
||||
export { UIPlugin } from './editor/UIPlugin';
|
||||
// Runtime module and plugin
|
||||
export { UIRuntimeModule, UIPlugin, type UISystemContext } from './UIRuntimeModule';
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* @esengine/ui Runtime Entry Point
|
||||
*
|
||||
* This entry point exports only runtime-related code without any editor dependencies.
|
||||
* Use this for standalone game runtime builds.
|
||||
*
|
||||
* 此入口点仅导出运行时相关代码,不包含任何编辑器依赖。
|
||||
* 用于独立游戏运行时构建。
|
||||
*/
|
||||
|
||||
// Components - Core
|
||||
export {
|
||||
UITransformComponent,
|
||||
AnchorPreset
|
||||
} from './components/UITransformComponent';
|
||||
|
||||
export {
|
||||
UIRenderComponent,
|
||||
UIRenderType,
|
||||
type UIBorderStyle,
|
||||
type UIShadowStyle
|
||||
} from './components/UIRenderComponent';
|
||||
|
||||
export {
|
||||
UIInteractableComponent,
|
||||
type UICursorType
|
||||
} from './components/UIInteractableComponent';
|
||||
|
||||
export {
|
||||
UITextComponent,
|
||||
type UITextAlign,
|
||||
type UITextVerticalAlign,
|
||||
type UITextOverflow,
|
||||
type UIFontWeight
|
||||
} from './components/UITextComponent';
|
||||
|
||||
export {
|
||||
UILayoutComponent,
|
||||
UILayoutType,
|
||||
UIJustifyContent,
|
||||
UIAlignItems,
|
||||
type UIPadding
|
||||
} from './components/UILayoutComponent';
|
||||
|
||||
// Components - Widgets
|
||||
export {
|
||||
UIButtonComponent,
|
||||
type UIButtonStyle,
|
||||
type UIButtonDisplayMode
|
||||
} from './components/widgets/UIButtonComponent';
|
||||
|
||||
export {
|
||||
UIProgressBarComponent,
|
||||
UIProgressDirection,
|
||||
UIProgressFillMode
|
||||
} from './components/widgets/UIProgressBarComponent';
|
||||
|
||||
export {
|
||||
UISliderComponent,
|
||||
UISliderOrientation
|
||||
} from './components/widgets/UISliderComponent';
|
||||
|
||||
export {
|
||||
UIScrollViewComponent,
|
||||
UIScrollbarVisibility
|
||||
} from './components/widgets/UIScrollViewComponent';
|
||||
|
||||
// Systems - Core
|
||||
export { UILayoutSystem } from './systems/UILayoutSystem';
|
||||
export { UIInputSystem, MouseButton, type UIInputEvent } from './systems/UIInputSystem';
|
||||
export { UIAnimationSystem, Easing, type EasingFunction, type EasingName } from './systems/UIAnimationSystem';
|
||||
export { UIRenderDataProvider, type IRenderDataProvider, type IUIRenderDataProvider } from './systems/UIRenderDataProvider';
|
||||
|
||||
// Systems - Render (ECS-compliant render systems)
|
||||
export {
|
||||
// Collector
|
||||
UIRenderCollector,
|
||||
getUIRenderCollector,
|
||||
resetUIRenderCollector,
|
||||
invalidateUIRenderCaches,
|
||||
type UIRenderPrimitive,
|
||||
type ProviderRenderData,
|
||||
// Render systems
|
||||
UIRenderBeginSystem,
|
||||
UIRectRenderSystem,
|
||||
UITextRenderSystem,
|
||||
UIButtonRenderSystem,
|
||||
UIProgressBarRenderSystem,
|
||||
UISliderRenderSystem,
|
||||
UIScrollViewRenderSystem
|
||||
} from './systems/render';
|
||||
|
||||
// Rendering
|
||||
export { WebGLUIRenderer } from './rendering/WebGLUIRenderer';
|
||||
export { TextRenderer, type TextMeasurement, type TextRenderOptions } from './rendering/TextRenderer';
|
||||
|
||||
// Builder API
|
||||
export {
|
||||
UIBuilder,
|
||||
type UIBaseConfig,
|
||||
type UIButtonConfig,
|
||||
type UITextConfig,
|
||||
type UIImageConfig,
|
||||
type UIProgressBarConfig,
|
||||
type UISliderConfig,
|
||||
type UIPanelConfig,
|
||||
type UIScrollViewConfig
|
||||
} from './UIBuilder';
|
||||
|
||||
// Runtime module
|
||||
export { UIRuntimeModule } from './UIRuntimeModule';
|
||||
@@ -1,7 +1,20 @@
|
||||
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { EntitySystem, Matcher, Entity, ECSSystem, HierarchyComponent } from '@esengine/ecs-framework';
|
||||
import { UITransformComponent } from '../components/UITransformComponent';
|
||||
import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from '../components/UILayoutComponent';
|
||||
|
||||
/**
|
||||
* 2D 变换矩阵类型
|
||||
* 2D transformation matrix type
|
||||
*/
|
||||
interface Matrix2D {
|
||||
a: number; // scaleX * cos(rotation)
|
||||
b: number; // scaleX * sin(rotation)
|
||||
c: number; // scaleY * -sin(rotation)
|
||||
d: number; // scaleY * cos(rotation)
|
||||
tx: number; // translateX
|
||||
ty: number; // translateY
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 布局系统
|
||||
* UI Layout System - Computes layout for UI elements
|
||||
@@ -9,6 +22,9 @@ import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from
|
||||
* 计算 UI 元素的世界坐标和尺寸
|
||||
* Computes world coordinates and sizes for UI elements
|
||||
*
|
||||
* 使用矩阵乘法计算世界变换:worldMatrix = parentMatrix * localMatrix
|
||||
* Uses matrix multiplication for world transforms: worldMatrix = parentMatrix * localMatrix
|
||||
*
|
||||
* 注意:canvasWidth/canvasHeight 是 UI 设计的参考尺寸,不是实际渲染视口大小
|
||||
* Note: canvasWidth/canvasHeight is the UI design reference size, not the actual render viewport size
|
||||
*/
|
||||
@@ -60,7 +76,14 @@ export class UILayoutSystem extends EntitySystem {
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 首先处理根元素(没有父元素的)
|
||||
const rootEntities = entities.filter(e => !e.parent || !e.parent.hasComponent(UITransformComponent));
|
||||
const rootEntities = entities.filter(e => {
|
||||
const hierarchy = e.getComponent(HierarchyComponent);
|
||||
if (!hierarchy || hierarchy.parentId === null) {
|
||||
return true;
|
||||
}
|
||||
const parent = this.scene?.findEntityById(hierarchy.parentId);
|
||||
return !parent || !parent.hasComponent(UITransformComponent);
|
||||
});
|
||||
|
||||
// 画布中心为原点,Y 轴向上为正
|
||||
// Canvas center is origin, Y axis points up
|
||||
@@ -69,8 +92,11 @@ export class UILayoutSystem extends EntitySystem {
|
||||
const parentX = -this.canvasWidth / 2;
|
||||
const parentY = this.canvasHeight / 2; // Y 轴向上,所以顶部是正值
|
||||
|
||||
// 根元素使用单位矩阵作为父矩阵
|
||||
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);
|
||||
this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1, identityMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +110,8 @@ export class UILayoutSystem extends EntitySystem {
|
||||
parentY: number,
|
||||
parentWidth: number,
|
||||
parentHeight: number,
|
||||
parentAlpha: number
|
||||
parentAlpha: number,
|
||||
parentMatrix: Matrix2D
|
||||
): void {
|
||||
const transform = entity.getComponent(UITransformComponent);
|
||||
if (!transform) return;
|
||||
@@ -160,19 +187,23 @@ export class UILayoutSystem extends EntitySystem {
|
||||
worldY = anchorMaxY - transform.y;
|
||||
}
|
||||
|
||||
// 更新计算后的值
|
||||
// 更新布局计算的值
|
||||
transform.worldX = worldX;
|
||||
transform.worldY = worldY;
|
||||
transform.computedWidth = width;
|
||||
transform.computedHeight = height;
|
||||
transform.worldAlpha = parentAlpha * transform.alpha;
|
||||
|
||||
// 使用矩阵乘法计算世界变换
|
||||
this.updateWorldMatrix(transform, parentMatrix);
|
||||
|
||||
transform.layoutDirty = false;
|
||||
|
||||
// 如果元素不可见,跳过子元素
|
||||
if (!transform.visible) return;
|
||||
|
||||
// 处理子元素布局
|
||||
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
|
||||
const children = this.getUIChildren(entity);
|
||||
if (children.length === 0) return;
|
||||
|
||||
// 计算子元素的父容器边界
|
||||
@@ -192,7 +223,8 @@ export class UILayoutSystem extends EntitySystem {
|
||||
childParentY,
|
||||
width,
|
||||
height,
|
||||
transform.worldAlpha
|
||||
transform.worldAlpha,
|
||||
transform.localToWorldMatrix
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -234,7 +266,8 @@ export class UILayoutSystem extends EntitySystem {
|
||||
parentTopY,
|
||||
parentTransform.computedWidth,
|
||||
parentTransform.computedHeight,
|
||||
parentTransform.worldAlpha
|
||||
parentTransform.worldAlpha,
|
||||
parentTransform.localToWorldMatrix
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -328,6 +361,8 @@ export class UILayoutSystem extends EntitySystem {
|
||||
childTransform.computedWidth = size.width;
|
||||
childTransform.computedHeight = childHeight;
|
||||
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
||||
// 使用矩阵乘法计算世界旋转和缩放
|
||||
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
|
||||
childTransform.layoutDirty = false;
|
||||
|
||||
// 递归处理子元素的子元素
|
||||
@@ -424,6 +459,8 @@ export class UILayoutSystem extends EntitySystem {
|
||||
childTransform.computedWidth = childWidth;
|
||||
childTransform.computedHeight = size.height;
|
||||
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
||||
// 使用矩阵乘法计算世界旋转和缩放
|
||||
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
|
||||
childTransform.layoutDirty = false;
|
||||
|
||||
this.processChildrenRecursive(child, childTransform);
|
||||
@@ -478,18 +515,49 @@ export class UILayoutSystem extends EntitySystem {
|
||||
childTransform.computedWidth = cellWidth;
|
||||
childTransform.computedHeight = cellHeight;
|
||||
childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha;
|
||||
// 使用矩阵乘法计算世界旋转和缩放
|
||||
this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix);
|
||||
childTransform.layoutDirty = false;
|
||||
|
||||
this.processChildrenRecursive(child, childTransform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取具有 UITransformComponent 的子实体
|
||||
* Get child entities that have UITransformComponent
|
||||
*
|
||||
* 优先使用 HierarchyComponent,如果没有则返回空数组
|
||||
*/
|
||||
private getUIChildren(entity: Entity): Entity[] {
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
|
||||
// 如果没有 HierarchyComponent,返回空数组
|
||||
// UI 实体应该通过 UIBuilder 创建,会自动添加 HierarchyComponent
|
||||
if (!hierarchy) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (hierarchy.childIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const children: Entity[] = [];
|
||||
for (const childId of hierarchy.childIds) {
|
||||
const child = this.scene?.findEntityById(childId);
|
||||
if (child && child.hasComponent(UITransformComponent)) {
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归处理子元素
|
||||
* Recursively process children
|
||||
*/
|
||||
private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void {
|
||||
const children = entity.children.filter(c => c.hasComponent(UITransformComponent));
|
||||
const children = this.getUIChildren(entity);
|
||||
if (children.length === 0) return;
|
||||
|
||||
// 计算子元素的父容器顶部 Y(worldY 是底部,顶部 = 底部 + 高度)
|
||||
@@ -506,9 +574,129 @@ export class UILayoutSystem extends EntitySystem {
|
||||
parentTopY,
|
||||
parentTransform.computedWidth,
|
||||
parentTransform.computedHeight,
|
||||
parentTransform.worldAlpha
|
||||
parentTransform.worldAlpha,
|
||||
parentTransform.localToWorldMatrix
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 矩阵计算方法 Matrix calculation methods =====
|
||||
|
||||
/**
|
||||
* 计算本地变换矩阵
|
||||
* Calculate local transformation matrix
|
||||
*
|
||||
* @param pivotX - 轴心点 X (0-1)
|
||||
* @param pivotY - 轴心点 Y (0-1)
|
||||
* @param width - 元素宽度
|
||||
* @param height - 元素高度
|
||||
* @param rotation - 旋转角度(弧度)
|
||||
* @param scaleX - X 缩放
|
||||
* @param scaleY - Y 缩放
|
||||
* @param x - 元素世界 X 位置
|
||||
* @param y - 元素世界 Y 位置
|
||||
*/
|
||||
private calculateLocalMatrix(
|
||||
pivotX: number,
|
||||
pivotY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
rotation: number,
|
||||
scaleX: number,
|
||||
scaleY: number,
|
||||
x: number,
|
||||
y: number
|
||||
): Matrix2D {
|
||||
const cos = Math.cos(rotation);
|
||||
const sin = Math.sin(rotation);
|
||||
|
||||
// 轴心点相对于元素左下角的偏移
|
||||
const px = width * pivotX;
|
||||
const py = height * pivotY;
|
||||
|
||||
// 构建变换矩阵: Translate(-pivot) -> Scale -> Rotate -> Translate(position + pivot)
|
||||
// 最终矩阵将轴心点作为旋转/缩放中心
|
||||
return {
|
||||
a: scaleX * cos,
|
||||
b: scaleX * sin,
|
||||
c: scaleY * -sin,
|
||||
d: scaleY * cos,
|
||||
tx: x + px - (scaleX * cos * px - scaleY * sin * py),
|
||||
ty: y + py - (scaleX * sin * px + scaleY * cos * py)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 矩阵乘法: result = a * b
|
||||
* Matrix multiplication: result = a * b
|
||||
*/
|
||||
private multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D {
|
||||
return {
|
||||
a: a.a * b.a + a.c * b.b,
|
||||
b: a.b * b.a + a.d * b.b,
|
||||
c: a.a * b.c + a.c * b.d,
|
||||
d: a.b * b.c + a.d * b.d,
|
||||
tx: a.a * b.tx + a.c * b.ty + a.tx,
|
||||
ty: a.b * b.tx + a.d * b.ty + a.ty
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从世界矩阵分解出旋转和缩放
|
||||
* Decompose rotation and scale from world matrix
|
||||
*/
|
||||
private decomposeMatrix(m: Matrix2D): { rotation: number; scaleX: number; scaleY: number } {
|
||||
// 计算缩放
|
||||
const scaleX = Math.sqrt(m.a * m.a + m.b * m.b);
|
||||
const scaleY = Math.sqrt(m.c * m.c + m.d * m.d);
|
||||
|
||||
// 检测负缩放(通过行列式符号)
|
||||
const det = m.a * m.d - m.b * m.c;
|
||||
const sign = det < 0 ? -1 : 1;
|
||||
|
||||
// 计算旋转(从归一化的矩阵)
|
||||
let rotation = 0;
|
||||
if (scaleX > 1e-10) {
|
||||
rotation = Math.atan2(m.b / scaleX, m.a / scaleX);
|
||||
}
|
||||
|
||||
return {
|
||||
rotation,
|
||||
scaleX,
|
||||
scaleY: scaleY * sign
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新元素的世界变换矩阵
|
||||
* Update element's world transformation matrix
|
||||
*/
|
||||
private updateWorldMatrix(transform: UITransformComponent, parentMatrix: Matrix2D | null): void {
|
||||
// 计算本地矩阵
|
||||
const localMatrix = this.calculateLocalMatrix(
|
||||
transform.pivotX,
|
||||
transform.pivotY,
|
||||
transform.computedWidth,
|
||||
transform.computedHeight,
|
||||
transform.rotation,
|
||||
transform.scaleX,
|
||||
transform.scaleY,
|
||||
transform.worldX,
|
||||
transform.worldY
|
||||
);
|
||||
|
||||
// 计算世界矩阵
|
||||
if (parentMatrix) {
|
||||
transform.localToWorldMatrix = this.multiplyMatrices(parentMatrix, localMatrix);
|
||||
} else {
|
||||
transform.localToWorldMatrix = localMatrix;
|
||||
}
|
||||
|
||||
// 从世界矩阵分解出世界旋转和缩放
|
||||
const decomposed = this.decomposeMatrix(transform.localToWorldMatrix);
|
||||
transform.worldRotation = decomposed.rotation;
|
||||
transform.worldScaleX = decomposed.scaleX;
|
||||
transform.worldScaleY = decomposed.scaleY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,20 @@ export class UIButtonRenderSystem extends EntitySystem {
|
||||
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
// 使用世界缩放和旋转
|
||||
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
||||
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const width = (transform.computedWidth ?? transform.width) * scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * scaleY;
|
||||
const alpha = transform.worldAlpha ?? transform.alpha;
|
||||
const baseOrder = 100 + transform.zIndex;
|
||||
// 使用 transform 的 pivot 作为旋转/缩放中心
|
||||
const pivotX = transform.pivotX;
|
||||
const pivotY = transform.pivotY;
|
||||
// 渲染位置 = 左下角 + pivot 偏移
|
||||
const renderX = x + width * pivotX;
|
||||
const renderY = y + height * pivotY;
|
||||
|
||||
// Render texture if in texture or both mode
|
||||
// 如果在纹理或两者模式下,渲染纹理
|
||||
@@ -59,15 +69,15 @@ export class UIButtonRenderSystem extends EntitySystem {
|
||||
const texture = button.getStateTexture('normal');
|
||||
if (texture) {
|
||||
collector.addRect(
|
||||
x, y,
|
||||
renderX, renderY,
|
||||
width, height,
|
||||
0xFFFFFF, // White tint for texture
|
||||
alpha,
|
||||
baseOrder,
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0,
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY,
|
||||
texturePath: texture
|
||||
}
|
||||
);
|
||||
@@ -80,15 +90,15 @@ export class UIButtonRenderSystem extends EntitySystem {
|
||||
const bgAlpha = render?.backgroundAlpha ?? 1;
|
||||
if (bgAlpha > 0) {
|
||||
collector.addRect(
|
||||
x, y,
|
||||
renderX, renderY,
|
||||
width, height,
|
||||
button.currentColor,
|
||||
bgAlpha * alpha,
|
||||
baseOrder + (button.useTexture() ? 0.05 : 0),
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -99,61 +109,72 @@ export class UIButtonRenderSystem extends EntitySystem {
|
||||
if (render && render.borderWidth > 0 && render.borderAlpha > 0) {
|
||||
this.renderBorder(
|
||||
collector,
|
||||
x, y, width, height,
|
||||
renderX, renderY, width, height,
|
||||
render.borderWidth,
|
||||
render.borderColor,
|
||||
render.borderAlpha * alpha,
|
||||
baseOrder + 0.1,
|
||||
transform.rotation
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render border using top-left coordinates
|
||||
* 使用左上角坐标渲染边框
|
||||
* Render border using pivot-based coordinates
|
||||
* 使用基于 pivot 的坐标渲染边框
|
||||
*/
|
||||
private renderBorder(
|
||||
collector: ReturnType<typeof getUIRenderCollector>,
|
||||
x: number, y: number,
|
||||
centerX: number, centerY: number,
|
||||
width: number, height: number,
|
||||
borderWidth: number,
|
||||
borderColor: number,
|
||||
alpha: number,
|
||||
sortOrder: number,
|
||||
rotation: number
|
||||
rotation: number,
|
||||
pivotX: number,
|
||||
pivotY: number
|
||||
): void {
|
||||
// 计算矩形的边界(相对于 pivot 中心)
|
||||
const left = centerX - width * pivotX;
|
||||
const bottom = centerY - height * pivotY;
|
||||
const right = left + width;
|
||||
const top = bottom + height;
|
||||
|
||||
// Top border
|
||||
collector.addRect(
|
||||
x, y,
|
||||
(left + right) / 2, top - borderWidth / 2,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
collector.addRect(
|
||||
x, y + height - borderWidth,
|
||||
(left + right) / 2, bottom + borderWidth / 2,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Left border (excluding corners)
|
||||
const sideBorderHeight = height - borderWidth * 2;
|
||||
collector.addRect(
|
||||
x, y + borderWidth,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
left + borderWidth / 2, (top + bottom) / 2,
|
||||
borderWidth, sideBorderHeight,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Right border (excluding corners)
|
||||
collector.addRect(
|
||||
x + width - borderWidth, y + borderWidth,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
right - borderWidth / 2, (top + bottom) / 2,
|
||||
borderWidth, sideBorderHeight,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,23 +45,33 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
// 使用世界缩放和旋转
|
||||
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
||||
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const width = (transform.computedWidth ?? transform.width) * scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * scaleY;
|
||||
const alpha = transform.worldAlpha ?? transform.alpha;
|
||||
const baseOrder = 100 + transform.zIndex;
|
||||
// 使用 transform 的 pivot 作为旋转/缩放中心
|
||||
const pivotX = transform.pivotX;
|
||||
const pivotY = transform.pivotY;
|
||||
// 渲染位置 = 左下角 + pivot 偏移
|
||||
const renderX = x + width * pivotX;
|
||||
const renderY = y + height * pivotY;
|
||||
|
||||
// Render background (x, y is top-left corner)
|
||||
// 渲染背景(x, y 是左上角)
|
||||
// Render background
|
||||
// 渲染背景
|
||||
if (progressBar.backgroundAlpha > 0) {
|
||||
collector.addRect(
|
||||
x, y, width, height,
|
||||
renderX, renderY, width, height,
|
||||
progressBar.backgroundColor,
|
||||
progressBar.backgroundAlpha * alpha,
|
||||
baseOrder,
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -70,12 +80,14 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
// 渲染边框
|
||||
if (progressBar.borderWidth > 0) {
|
||||
this.renderBorder(
|
||||
collector, x, y, width, height,
|
||||
collector, renderX, renderY, width, height,
|
||||
progressBar.borderWidth,
|
||||
progressBar.borderColor,
|
||||
alpha,
|
||||
baseOrder + 0.2,
|
||||
transform
|
||||
transform,
|
||||
pivotX,
|
||||
pivotY
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,13 +97,15 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
if (progress > 0 && progressBar.fillAlpha > 0) {
|
||||
if (progressBar.showSegments) {
|
||||
this.renderSegmentedFill(
|
||||
collector, x, y, width, height,
|
||||
progress, progressBar, alpha, baseOrder + 0.1, transform
|
||||
collector, renderX, renderY, width, height,
|
||||
progress, progressBar, alpha, baseOrder + 0.1, transform,
|
||||
pivotX, pivotY
|
||||
);
|
||||
} else {
|
||||
this.renderSolidFill(
|
||||
collector, x, y, width, height,
|
||||
progress, progressBar, alpha, baseOrder + 0.1, transform
|
||||
collector, renderX, renderY, width, height,
|
||||
progress, progressBar, alpha, baseOrder + 0.1, transform,
|
||||
pivotX, pivotY
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -102,57 +116,67 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
* Render solid fill rectangle
|
||||
* 渲染实心填充矩形
|
||||
*
|
||||
* Note: x, y is the top-left corner of the progress bar
|
||||
* 注意:x, y 是进度条的左上角
|
||||
* Note: centerX, centerY is the pivot position of the progress bar
|
||||
* 注意:centerX, centerY 是进度条的 pivot 位置
|
||||
*/
|
||||
private renderSolidFill(
|
||||
collector: ReturnType<typeof getUIRenderCollector>,
|
||||
x: number, y: number, width: number, height: number,
|
||||
centerX: number, centerY: number, width: number, height: number,
|
||||
progress: number,
|
||||
progressBar: UIProgressBarComponent,
|
||||
alpha: number,
|
||||
sortOrder: number,
|
||||
transform: UITransformComponent
|
||||
transform: UITransformComponent,
|
||||
pivotX: number,
|
||||
pivotY: number
|
||||
): void {
|
||||
let fillX = x;
|
||||
let fillY = y;
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
|
||||
// 计算进度条的边界(相对于 pivot 中心)
|
||||
const left = centerX - width * pivotX;
|
||||
const bottom = centerY - height * pivotY;
|
||||
|
||||
let fillX: number;
|
||||
let fillY: number;
|
||||
let fillWidth = width;
|
||||
let fillHeight = height;
|
||||
|
||||
// Calculate fill dimensions based on direction
|
||||
// x, y is top-left corner, so calculations are simpler
|
||||
// 根据方向计算填充尺寸
|
||||
// x, y 是左上角,所以计算更简单
|
||||
switch (progressBar.direction) {
|
||||
case UIProgressDirection.LeftToRight:
|
||||
fillWidth = width * progress;
|
||||
// Fill starts from left (fillX = x, no change)
|
||||
fillX = left + fillWidth / 2;
|
||||
fillY = bottom + height / 2;
|
||||
break;
|
||||
|
||||
case UIProgressDirection.RightToLeft:
|
||||
fillWidth = width * progress;
|
||||
// Fill starts from right
|
||||
fillX = x + width - fillWidth;
|
||||
fillX = left + width - fillWidth / 2;
|
||||
fillY = bottom + height / 2;
|
||||
break;
|
||||
|
||||
case UIProgressDirection.BottomToTop:
|
||||
fillHeight = height * progress;
|
||||
// Fill starts from bottom
|
||||
fillY = y + height - fillHeight;
|
||||
fillX = left + width / 2;
|
||||
fillY = bottom + fillHeight / 2;
|
||||
break;
|
||||
|
||||
case UIProgressDirection.TopToBottom:
|
||||
fillHeight = height * progress;
|
||||
// Fill starts from top (fillY = y, no change)
|
||||
fillX = left + width / 2;
|
||||
fillY = bottom + height - fillHeight / 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
fillX = left + fillWidth / 2;
|
||||
fillY = bottom + height / 2;
|
||||
}
|
||||
|
||||
// Determine fill color (gradient or solid)
|
||||
// 确定填充颜色(渐变或实心)
|
||||
let fillColor = progressBar.fillColor;
|
||||
if (progressBar.useGradient) {
|
||||
// Simple linear interpolation between start and end colors
|
||||
// 简单的起始和结束颜色线性插值
|
||||
fillColor = this.lerpColor(
|
||||
progressBar.gradientStartColor,
|
||||
progressBar.gradientEndColor,
|
||||
@@ -166,9 +190,9 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
progressBar.fillAlpha * alpha,
|
||||
sortOrder,
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0
|
||||
rotation,
|
||||
pivotX: 0.5,
|
||||
pivotY: 0.5
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -177,18 +201,21 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
* Render segmented fill
|
||||
* 渲染分段填充
|
||||
*
|
||||
* Note: x, y is the top-left corner of the progress bar
|
||||
* 注意:x, y 是进度条的左上角
|
||||
* Note: centerX, centerY is the pivot position of the progress bar
|
||||
* 注意:centerX, centerY 是进度条的 pivot 位置
|
||||
*/
|
||||
private renderSegmentedFill(
|
||||
collector: ReturnType<typeof getUIRenderCollector>,
|
||||
x: number, y: number, width: number, height: number,
|
||||
centerX: number, centerY: number, width: number, height: number,
|
||||
progress: number,
|
||||
progressBar: UIProgressBarComponent,
|
||||
alpha: number,
|
||||
sortOrder: number,
|
||||
transform: UITransformComponent
|
||||
transform: UITransformComponent,
|
||||
pivotX: number,
|
||||
pivotY: number
|
||||
): void {
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const segments = progressBar.segments;
|
||||
const gap = progressBar.segmentGap;
|
||||
const filledSegments = Math.ceil(progress * segments);
|
||||
@@ -196,6 +223,10 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
const isHorizontal = progressBar.direction === UIProgressDirection.LeftToRight ||
|
||||
progressBar.direction === UIProgressDirection.RightToLeft;
|
||||
|
||||
// 计算进度条的边界(相对于 pivot 中心)
|
||||
const left = centerX - width * pivotX;
|
||||
const bottom = centerY - height * pivotY;
|
||||
|
||||
// Calculate segment dimensions
|
||||
// 计算段尺寸
|
||||
let segmentWidth: number;
|
||||
@@ -209,41 +240,36 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
segmentHeight = (height - gap * (segments - 1)) / segments;
|
||||
}
|
||||
|
||||
// x, y is already top-left corner
|
||||
// x, y 已经是左上角
|
||||
const baseX = x;
|
||||
const baseY = y;
|
||||
|
||||
for (let i = 0; i < filledSegments && i < segments; i++) {
|
||||
let segX: number;
|
||||
let segY: number;
|
||||
let segCenterX: number;
|
||||
let segCenterY: number;
|
||||
|
||||
// Calculate segment position based on direction (using top-left positions)
|
||||
// 根据方向计算段位置(使用左上角位置)
|
||||
// Calculate segment center position based on direction
|
||||
// 根据方向计算段中心位置
|
||||
switch (progressBar.direction) {
|
||||
case UIProgressDirection.LeftToRight:
|
||||
segX = baseX + i * (segmentWidth + gap);
|
||||
segY = baseY;
|
||||
segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2;
|
||||
segCenterY = bottom + height / 2;
|
||||
break;
|
||||
|
||||
case UIProgressDirection.RightToLeft:
|
||||
segX = baseX + width - (i + 1) * segmentWidth - i * gap;
|
||||
segY = baseY;
|
||||
segCenterX = left + width - i * (segmentWidth + gap) - segmentWidth / 2;
|
||||
segCenterY = bottom + height / 2;
|
||||
break;
|
||||
|
||||
case UIProgressDirection.TopToBottom:
|
||||
segX = baseX;
|
||||
segY = baseY + i * (segmentHeight + gap);
|
||||
segCenterX = left + width / 2;
|
||||
segCenterY = bottom + height - i * (segmentHeight + gap) - segmentHeight / 2;
|
||||
break;
|
||||
|
||||
case UIProgressDirection.BottomToTop:
|
||||
segX = baseX;
|
||||
segY = baseY + height - (i + 1) * segmentHeight - i * gap;
|
||||
segCenterX = left + width / 2;
|
||||
segCenterY = bottom + i * (segmentHeight + gap) + segmentHeight / 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
segX = baseX + i * (segmentWidth + gap);
|
||||
segY = baseY;
|
||||
segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2;
|
||||
segCenterY = bottom + height / 2;
|
||||
}
|
||||
|
||||
// Determine segment color
|
||||
@@ -258,19 +284,19 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
);
|
||||
}
|
||||
|
||||
// Use top-left position with pivot 0,0
|
||||
// 使用左上角位置,pivot 0,0
|
||||
// Use center position with pivot 0.5, 0.5
|
||||
// 使用中心位置,pivot 0.5, 0.5
|
||||
collector.addRect(
|
||||
segX, segY,
|
||||
segCenterX, segCenterY,
|
||||
segmentWidth,
|
||||
segmentHeight,
|
||||
segmentColor,
|
||||
progressBar.fillAlpha * alpha,
|
||||
sortOrder + i * 0.001, // Slight offset for each segment
|
||||
sortOrder + i * 0.001,
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0
|
||||
rotation,
|
||||
pivotX: 0.5,
|
||||
pivotY: 0.5
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -280,51 +306,59 @@ export class UIProgressBarRenderSystem extends EntitySystem {
|
||||
* Render border
|
||||
* 渲染边框
|
||||
*
|
||||
* Note: x, y is the top-left corner of the progress bar
|
||||
* 注意:x, y 是进度条的左上角
|
||||
* Note: centerX, centerY is the pivot position of the progress bar
|
||||
* 注意:centerX, centerY 是进度条的 pivot 位置
|
||||
*/
|
||||
private renderBorder(
|
||||
collector: ReturnType<typeof getUIRenderCollector>,
|
||||
x: number, y: number, width: number, height: number,
|
||||
centerX: number, centerY: number, width: number, height: number,
|
||||
borderWidth: number,
|
||||
borderColor: number,
|
||||
alpha: number,
|
||||
sortOrder: number,
|
||||
_transform: UITransformComponent
|
||||
transform: UITransformComponent,
|
||||
pivotX: number,
|
||||
pivotY: number
|
||||
): void {
|
||||
// x, y is already top-left corner
|
||||
// x, y 已经是左上角
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
|
||||
// 计算边界(相对于 pivot 中心)
|
||||
const left = centerX - width * pivotX;
|
||||
const bottom = centerY - height * pivotY;
|
||||
const right = left + width;
|
||||
const top = bottom + height;
|
||||
|
||||
// Top border
|
||||
collector.addRect(
|
||||
x, y,
|
||||
(left + right) / 2, top - borderWidth / 2,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
collector.addRect(
|
||||
x, y + height - borderWidth,
|
||||
(left + right) / 2, bottom + borderWidth / 2,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Left border (excluding corners)
|
||||
const sideBorderHeight = height - borderWidth * 2;
|
||||
collector.addRect(
|
||||
x, y + borderWidth,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
left + borderWidth / 2, (top + bottom) / 2,
|
||||
borderWidth, sideBorderHeight,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Right border (excluding corners)
|
||||
collector.addRect(
|
||||
x + width - borderWidth, y + borderWidth,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
right - borderWidth / 2, (top + bottom) / 2,
|
||||
borderWidth, sideBorderHeight,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,33 +56,40 @@ export class UIRectRenderSystem extends EntitySystem {
|
||||
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
// 使用世界缩放(考虑父级缩放)
|
||||
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
||||
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
||||
const width = (transform.computedWidth ?? transform.width) * scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * scaleY;
|
||||
const alpha = transform.worldAlpha ?? transform.alpha;
|
||||
// 使用世界旋转(考虑父级旋转)
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const baseOrder = 100 + transform.zIndex;
|
||||
// 使用 transform 的 pivot 作为旋转/缩放中心
|
||||
const pivotX = transform.pivotX;
|
||||
const pivotY = transform.pivotY;
|
||||
|
||||
// Use top-left position with origin at (0, 0)
|
||||
// Like Sprite: x,y is anchor position, origin determines where anchor is on the rect
|
||||
// For UI: x,y is top-left corner, so origin should be (0, 0)
|
||||
// 使用左上角位置,原点在 (0, 0)
|
||||
// 类似 Sprite:x,y 是锚点位置,origin 决定锚点在矩形上的位置
|
||||
// 对于 UI:x,y 是左上角,所以 origin 应该是 (0, 0)
|
||||
// worldX/worldY 是元素左下角位置,需要转换为以 pivot 为中心的位置
|
||||
// pivot 相对于元素的偏移:(width * pivotX, height * pivotY)
|
||||
// 渲染位置 = 左下角 + pivot 偏移
|
||||
const renderX = x + width * pivotX;
|
||||
const renderY = y + height * pivotY;
|
||||
|
||||
// Render shadow if enabled
|
||||
// 如果启用,渲染阴影
|
||||
if (render.shadowEnabled && render.shadowAlpha > 0) {
|
||||
collector.addRect(
|
||||
x + render.shadowOffsetX - render.shadowBlur,
|
||||
y + render.shadowOffsetY - render.shadowBlur,
|
||||
renderX + render.shadowOffsetX,
|
||||
renderY + render.shadowOffsetY,
|
||||
width + render.shadowBlur * 2,
|
||||
height + render.shadowBlur * 2,
|
||||
render.shadowColor,
|
||||
render.shadowAlpha * alpha,
|
||||
baseOrder - 0.1,
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -94,15 +101,15 @@ export class UIRectRenderSystem extends EntitySystem {
|
||||
const textureId = typeof render.texture === 'number' ? render.texture : undefined;
|
||||
|
||||
collector.addRect(
|
||||
x, y,
|
||||
renderX, renderY,
|
||||
width, height,
|
||||
render.textureTint,
|
||||
alpha,
|
||||
baseOrder,
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0,
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY,
|
||||
textureId,
|
||||
texturePath,
|
||||
uv: render.textureUV
|
||||
@@ -115,15 +122,15 @@ export class UIRectRenderSystem extends EntitySystem {
|
||||
// 如果启用填充,渲染背景颜色
|
||||
else if (render.fillBackground && render.backgroundAlpha > 0) {
|
||||
collector.addRect(
|
||||
x, y,
|
||||
renderX, renderY,
|
||||
width, height,
|
||||
render.backgroundColor,
|
||||
render.backgroundAlpha * alpha,
|
||||
baseOrder,
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -133,61 +140,78 @@ export class UIRectRenderSystem extends EntitySystem {
|
||||
if (render.borderWidth > 0 && render.borderAlpha > 0) {
|
||||
this.renderBorder(
|
||||
collector,
|
||||
x, y, width, height,
|
||||
renderX, renderY, width, height,
|
||||
render.borderWidth,
|
||||
render.borderColor,
|
||||
render.borderAlpha * alpha,
|
||||
baseOrder + 0.1,
|
||||
transform.rotation
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render border using top-left coordinates
|
||||
* 使用左上角坐标渲染边框
|
||||
* Render border using pivot-based coordinates
|
||||
* 使用基于 pivot 的坐标渲染边框
|
||||
*/
|
||||
private renderBorder(
|
||||
collector: ReturnType<typeof getUIRenderCollector>,
|
||||
x: number, y: number,
|
||||
centerX: number, centerY: number,
|
||||
width: number, height: number,
|
||||
borderWidth: number,
|
||||
borderColor: number,
|
||||
alpha: number,
|
||||
sortOrder: number,
|
||||
rotation: number
|
||||
rotation: number,
|
||||
pivotX: number,
|
||||
pivotY: number
|
||||
): void {
|
||||
// Top border (from top-left corner)
|
||||
// 计算矩形的左下角位置(相对于 pivot 中心)
|
||||
const left = centerX - width * pivotX;
|
||||
const bottom = centerY - height * pivotY;
|
||||
const right = left + width;
|
||||
const top = bottom + height;
|
||||
|
||||
// Top border
|
||||
const topBorderCenterX = (left + right) / 2;
|
||||
const topBorderCenterY = top - borderWidth / 2;
|
||||
collector.addRect(
|
||||
x, y,
|
||||
topBorderCenterX, topBorderCenterY,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
const bottomBorderCenterY = bottom + borderWidth / 2;
|
||||
collector.addRect(
|
||||
x, y + height - borderWidth,
|
||||
topBorderCenterX, bottomBorderCenterY,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Left border (excluding corners)
|
||||
const sideBorderHeight = height - borderWidth * 2;
|
||||
const leftBorderCenterX = left + borderWidth / 2;
|
||||
const sideBorderCenterY = (top + bottom) / 2;
|
||||
collector.addRect(
|
||||
x, y + borderWidth,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
leftBorderCenterX, sideBorderCenterY,
|
||||
borderWidth, sideBorderHeight,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Right border (excluding corners)
|
||||
const rightBorderCenterX = right - borderWidth / 2;
|
||||
collector.addRect(
|
||||
x + width - borderWidth, y + borderWidth,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
rightBorderCenterX, sideBorderCenterY,
|
||||
borderWidth, sideBorderHeight,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ rotation, pivotX: 0, pivotY: 0 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +46,24 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
// 使用世界缩放
|
||||
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
||||
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const width = (transform.computedWidth ?? transform.width) * scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * scaleY;
|
||||
const alpha = transform.worldAlpha ?? transform.alpha;
|
||||
const baseOrder = 100 + transform.zIndex;
|
||||
// 使用 transform 的 pivot 计算位置
|
||||
const pivotX = transform.pivotX;
|
||||
const pivotY = transform.pivotY;
|
||||
// 渲染位置 = 左下角 + pivot 偏移
|
||||
const renderX = x + width * pivotX;
|
||||
const renderY = y + height * pivotY;
|
||||
|
||||
// x, y is already top-left corner
|
||||
// x, y 已经是左上角
|
||||
const baseX = x;
|
||||
const baseY = y;
|
||||
// 计算边界
|
||||
const baseX = renderX - width * pivotX;
|
||||
const baseY = renderY - height * pivotY;
|
||||
|
||||
// Render vertical scrollbar
|
||||
// 渲染垂直滚动条
|
||||
@@ -62,7 +71,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
this.renderVerticalScrollbar(
|
||||
collector,
|
||||
baseX, baseY, width, height,
|
||||
scrollView, alpha, baseOrder
|
||||
scrollView, alpha, baseOrder, rotation
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +81,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
this.renderHorizontalScrollbar(
|
||||
collector,
|
||||
baseX, baseY, width, height,
|
||||
scrollView, alpha, baseOrder
|
||||
scrollView, alpha, baseOrder, rotation
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +97,8 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
viewWidth: number, viewHeight: number,
|
||||
scrollView: UIScrollViewComponent,
|
||||
alpha: number,
|
||||
baseOrder: number
|
||||
baseOrder: number,
|
||||
rotation: number
|
||||
): void {
|
||||
const scrollbarWidth = scrollView.scrollbarWidth;
|
||||
const hasHorizontal = scrollView.needsHorizontalScrollbar(viewWidth);
|
||||
@@ -108,7 +118,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
scrollView.scrollbarTrackColor,
|
||||
scrollView.scrollbarTrackAlpha * alpha,
|
||||
baseOrder + 0.5,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,7 +141,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
scrollView.scrollbarColor,
|
||||
handleAlpha * alpha,
|
||||
baseOrder + 0.6,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,7 +155,8 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
viewWidth: number, viewHeight: number,
|
||||
scrollView: UIScrollViewComponent,
|
||||
alpha: number,
|
||||
baseOrder: number
|
||||
baseOrder: number,
|
||||
rotation: number
|
||||
): void {
|
||||
const scrollbarWidth = scrollView.scrollbarWidth;
|
||||
const hasVertical = scrollView.needsVerticalScrollbar(viewHeight);
|
||||
@@ -165,7 +176,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
scrollView.scrollbarTrackColor,
|
||||
scrollView.scrollbarTrackAlpha * alpha,
|
||||
baseOrder + 0.5,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,7 +199,7 @@ export class UIScrollViewRenderSystem extends EntitySystem {
|
||||
scrollView.scrollbarColor,
|
||||
handleAlpha * alpha,
|
||||
baseOrder + 0.6,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,20 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
// 使用世界缩放
|
||||
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
||||
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const width = (transform.computedWidth ?? transform.width) * scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * scaleY;
|
||||
const alpha = transform.worldAlpha ?? transform.alpha;
|
||||
const baseOrder = 100 + transform.zIndex;
|
||||
// 使用 transform 的 pivot 计算中心位置
|
||||
const pivotX = transform.pivotX;
|
||||
const pivotY = transform.pivotY;
|
||||
// 渲染位置 = 左下角 + pivot 偏移
|
||||
const renderX = x + width * pivotX;
|
||||
const renderY = y + height * pivotY;
|
||||
|
||||
const isHorizontal = slider.orientation === UISliderOrientation.Horizontal;
|
||||
const progress = slider.getProgress();
|
||||
@@ -58,10 +68,10 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
const trackLength = isHorizontal ? width : height;
|
||||
const trackThickness = slider.trackThickness;
|
||||
|
||||
// Calculate center position (x, y is top-left corner)
|
||||
// 计算中心位置(x, y 是左上角)
|
||||
const centerX = x + width / 2;
|
||||
const centerY = y + height / 2;
|
||||
// Calculate center position based on pivot
|
||||
// 基于 pivot 计算中心位置
|
||||
const centerX = renderX;
|
||||
const centerY = renderY;
|
||||
|
||||
// Render track (using center position with pivot 0.5)
|
||||
// 渲染轨道(使用中心位置,pivot 0.5)
|
||||
@@ -73,7 +83,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
slider.trackColor,
|
||||
slider.trackAlpha * alpha,
|
||||
baseOrder,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
} else {
|
||||
collector.addRect(
|
||||
@@ -82,7 +92,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
slider.trackColor,
|
||||
slider.trackAlpha * alpha,
|
||||
baseOrder,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +111,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
slider.fillColor,
|
||||
slider.fillAlpha * alpha,
|
||||
baseOrder + 0.1,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
} else {
|
||||
// Fill from bottom
|
||||
@@ -112,7 +122,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
slider.fillColor,
|
||||
slider.fillAlpha * alpha,
|
||||
baseOrder + 0.1,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +134,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
collector, centerX, centerY,
|
||||
trackLength, trackThickness,
|
||||
slider, alpha, baseOrder + 0.05,
|
||||
isHorizontal
|
||||
isHorizontal, rotation
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,7 +157,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
0x000000,
|
||||
0.3 * alpha,
|
||||
baseOrder + 0.15,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,7 +169,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
handleColor,
|
||||
alpha,
|
||||
baseOrder + 0.2,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Handle border (if any)
|
||||
@@ -172,7 +182,8 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
slider.handleBorderWidth,
|
||||
slider.handleBorderColor,
|
||||
alpha,
|
||||
baseOrder + 0.25
|
||||
baseOrder + 0.25,
|
||||
rotation
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -189,7 +200,8 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
slider: UISliderComponent,
|
||||
alpha: number,
|
||||
sortOrder: number,
|
||||
isHorizontal: boolean
|
||||
isHorizontal: boolean,
|
||||
rotation: number
|
||||
): void {
|
||||
const tickCount = slider.tickCount + 2; // Include start and end ticks
|
||||
const tickSize = slider.tickSize;
|
||||
@@ -220,7 +232,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
slider.tickColor,
|
||||
alpha,
|
||||
sortOrder,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -236,7 +248,8 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
borderWidth: number,
|
||||
borderColor: number,
|
||||
alpha: number,
|
||||
sortOrder: number
|
||||
sortOrder: number,
|
||||
rotation: number
|
||||
): void {
|
||||
const halfW = width / 2;
|
||||
const halfH = height / 2;
|
||||
@@ -247,7 +260,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
x, y - halfH + halfB,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Bottom
|
||||
@@ -255,7 +268,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
x, y + halfH - halfB,
|
||||
width, borderWidth,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Left
|
||||
@@ -263,7 +276,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
x - halfW + halfB, y,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
|
||||
// Right
|
||||
@@ -271,7 +284,7 @@ export class UISliderRenderSystem extends EntitySystem {
|
||||
x + halfW - halfB, y,
|
||||
borderWidth, height - borderWidth * 2,
|
||||
borderColor, alpha, sortOrder,
|
||||
{ pivotX: 0.5, pivotY: 0.5 }
|
||||
{ rotation, pivotX: 0.5, pivotY: 0.5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +101,20 @@ export class UITextRenderSystem extends EntitySystem {
|
||||
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
const width = (transform.computedWidth ?? transform.width) * transform.scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * transform.scaleY;
|
||||
// 使用世界缩放和旋转
|
||||
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
||||
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const width = (transform.computedWidth ?? transform.width) * scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * scaleY;
|
||||
const alpha = transform.worldAlpha ?? transform.alpha;
|
||||
const baseOrder = 100 + transform.zIndex;
|
||||
// 使用 transform 的 pivot 作为旋转/缩放中心
|
||||
const pivotX = transform.pivotX;
|
||||
const pivotY = transform.pivotY;
|
||||
// 渲染位置 = 左下角 + pivot 偏移
|
||||
const renderX = x + width * pivotX;
|
||||
const renderY = y + height * pivotY;
|
||||
|
||||
// Generate or retrieve cached texture
|
||||
// 生成或获取缓存的纹理
|
||||
@@ -114,18 +124,18 @@ export class UITextRenderSystem extends EntitySystem {
|
||||
|
||||
if (textureId === null) continue;
|
||||
|
||||
// Use top-left position with origin at (0, 0)
|
||||
// 使用左上角位置,原点在 (0, 0)
|
||||
// Use pivot position with transform's pivot values
|
||||
// 使用 transform 的 pivot 值作为旋转中心
|
||||
collector.addRect(
|
||||
x, y,
|
||||
renderX, renderY,
|
||||
width, height,
|
||||
0xFFFFFF, // White tint (color is baked into texture)
|
||||
alpha,
|
||||
baseOrder + 1, // Text renders above background
|
||||
{
|
||||
rotation: transform.rotation,
|
||||
pivotX: 0,
|
||||
pivotY: 0,
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY,
|
||||
textureId
|
||||
}
|
||||
);
|
||||
|
||||
23
packages/ui/tsconfig.build.json
Normal file
23
packages/ui/tsconfig.build.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -25,7 +25,6 @@
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../components" },
|
||||
{ "path": "../editor-core" }
|
||||
]
|
||||
}
|
||||
|
||||
7
packages/ui/tsup.config.ts
Normal file
7
packages/ui/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: false
|
||||
})
|
||||
],
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: {
|
||||
index: resolve(__dirname, 'src/index.ts'),
|
||||
runtime: resolve(__dirname, 'src/runtime.ts'),
|
||||
'editor/index': resolve(__dirname, 'src/editor/index.ts')
|
||||
},
|
||||
formats: ['es'],
|
||||
fileName: (format, entryName) => `${entryName}.js`
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/editor-core',
|
||||
'react',
|
||||
'react/jsx-runtime',
|
||||
'lucide-react',
|
||||
/^@esengine\//
|
||||
],
|
||||
output: {
|
||||
exports: 'named',
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user