Feature/UI input system fix (#243)
* feat(ui): 实现编辑器预览模式下的 UI 输入系统 * feat(platform-web): 为浏览器运行时添加 UI 输入系统绑定
This commit is contained in:
@@ -9,7 +9,7 @@ import { Core, Scene, Entity, SceneSerializer } from '@esengine/ecs-framework';
|
|||||||
import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, SpriteAnimatorSystem } from '@esengine/ecs-components';
|
import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, SpriteAnimatorSystem } from '@esengine/ecs-components';
|
||||||
import { TilemapComponent, TilemapRenderingSystem } from '@esengine/tilemap';
|
import { TilemapComponent, TilemapRenderingSystem } from '@esengine/tilemap';
|
||||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||||
import { UIRenderDataProvider, invalidateUIRenderCaches } from '@esengine/ui';
|
import { UIRenderDataProvider, invalidateUIRenderCaches, UIInputSystem } from '@esengine/ui';
|
||||||
import * as esEngine from '@esengine/engine';
|
import * as esEngine from '@esengine/engine';
|
||||||
import {
|
import {
|
||||||
AssetManager,
|
AssetManager,
|
||||||
@@ -37,6 +37,7 @@ export class EngineService {
|
|||||||
private tilemapSystem: TilemapRenderingSystem | null = null;
|
private tilemapSystem: TilemapRenderingSystem | null = null;
|
||||||
private behaviorTreeSystem: BehaviorTreeExecutionSystem | null = null;
|
private behaviorTreeSystem: BehaviorTreeExecutionSystem | null = null;
|
||||||
private uiRenderProvider: UIRenderDataProvider | null = null;
|
private uiRenderProvider: UIRenderDataProvider | null = null;
|
||||||
|
private uiInputSystem: UIInputSystem | null = null;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
private modulesInitialized = false;
|
private modulesInitialized = false;
|
||||||
private running = false;
|
private running = false;
|
||||||
@@ -244,6 +245,7 @@ export class EngineService {
|
|||||||
this.tilemapSystem = context.tilemapSystem as TilemapRenderingSystem | undefined ?? null;
|
this.tilemapSystem = context.tilemapSystem as TilemapRenderingSystem | undefined ?? null;
|
||||||
this.behaviorTreeSystem = context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined ?? null;
|
this.behaviorTreeSystem = context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined ?? null;
|
||||||
this.uiRenderProvider = context.uiRenderProvider as UIRenderDataProvider | undefined ?? null;
|
this.uiRenderProvider = context.uiRenderProvider as UIRenderDataProvider | undefined ?? null;
|
||||||
|
this.uiInputSystem = context.uiInputSystem as UIInputSystem | undefined ?? null;
|
||||||
|
|
||||||
// 设置 UI 渲染数据提供者到 EngineRenderSystem
|
// 设置 UI 渲染数据提供者到 EngineRenderSystem
|
||||||
// Set UI render data provider to EngineRenderSystem
|
// Set UI render data provider to EngineRenderSystem
|
||||||
@@ -276,12 +278,19 @@ export class EngineService {
|
|||||||
pluginManager.clearSceneSystems();
|
pluginManager.clearSceneSystems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unbind UI input system before clearing
|
||||||
|
// 清理前解绑 UI 输入系统
|
||||||
|
if (this.uiInputSystem) {
|
||||||
|
this.uiInputSystem.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
// 清空本地引用(系统的实际清理由场景管理)
|
// 清空本地引用(系统的实际清理由场景管理)
|
||||||
// Clear local references (actual system cleanup is managed by scene)
|
// Clear local references (actual system cleanup is managed by scene)
|
||||||
this.animatorSystem = null;
|
this.animatorSystem = null;
|
||||||
this.tilemapSystem = null;
|
this.tilemapSystem = null;
|
||||||
this.behaviorTreeSystem = null;
|
this.behaviorTreeSystem = null;
|
||||||
this.uiRenderProvider = null;
|
this.uiRenderProvider = null;
|
||||||
|
this.uiInputSystem = null;
|
||||||
this.modulesInitialized = false;
|
this.modulesInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +371,15 @@ export class EngineService {
|
|||||||
this.renderSystem.setPreviewMode(true);
|
this.renderSystem.setPreviewMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind UI input system to canvas for event handling
|
||||||
|
// 绑定 UI 输入系统到 canvas 以处理事件
|
||||||
|
if (this.uiInputSystem && this.canvasId) {
|
||||||
|
const canvas = document.getElementById(this.canvasId) as HTMLCanvasElement;
|
||||||
|
if (canvas) {
|
||||||
|
this.uiInputSystem.bindToCanvas(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Enable animator system and start auto-play animations
|
// Enable animator system and start auto-play animations
|
||||||
// 启用动画系统并启动自动播放的动画
|
// 启用动画系统并启动自动播放的动画
|
||||||
if (this.animatorSystem) {
|
if (this.animatorSystem) {
|
||||||
@@ -435,6 +453,12 @@ export class EngineService {
|
|||||||
this.renderSystem.setPreviewMode(false);
|
this.renderSystem.setPreviewMode(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unbind UI input system from canvas
|
||||||
|
// 从 canvas 解绑 UI 输入系统
|
||||||
|
if (this.uiInputSystem) {
|
||||||
|
this.uiInputSystem.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
// Disable animator system and stop all animations
|
// Disable animator system and stop all animations
|
||||||
// 禁用动画系统并停止所有动画
|
// 禁用动画系统并停止所有动画
|
||||||
if (this.animatorSystem) {
|
if (this.animatorSystem) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-en
|
|||||||
import { TransformComponent, SpriteAnimatorSystem, CoreRuntimeModule } from '@esengine/ecs-components';
|
import { TransformComponent, SpriteAnimatorSystem, CoreRuntimeModule } from '@esengine/ecs-components';
|
||||||
import type { SystemContext, IPluginLoader, IRuntimeModuleLoader, PluginDescriptor } from '@esengine/ecs-components';
|
import type { SystemContext, IPluginLoader, IRuntimeModuleLoader, PluginDescriptor } from '@esengine/ecs-components';
|
||||||
// Import from /runtime entry points to avoid editor dependencies (React, etc.)
|
// Import from /runtime entry points to avoid editor dependencies (React, etc.)
|
||||||
import { UIRuntimeModule, UIRenderDataProvider } from '@esengine/ui/runtime';
|
import { UIRuntimeModule, UIRenderDataProvider, UIInputSystem } from '@esengine/ui/runtime';
|
||||||
import { TilemapRuntimeModule, TilemapRenderingSystem } from '@esengine/tilemap/runtime';
|
import { TilemapRuntimeModule, TilemapRenderingSystem } from '@esengine/tilemap/runtime';
|
||||||
import { BehaviorTreeRuntimeModule, BehaviorTreeExecutionSystem } from '@esengine/behavior-tree/runtime';
|
import { BehaviorTreeRuntimeModule, BehaviorTreeExecutionSystem } from '@esengine/behavior-tree/runtime';
|
||||||
|
|
||||||
@@ -33,6 +33,8 @@ export interface RuntimeModuleConfig {
|
|||||||
enabledPlugins?: string[];
|
enabledPlugins?: string[];
|
||||||
/** 是否为编辑器模式 */
|
/** 是否为编辑器模式 */
|
||||||
isEditor?: boolean;
|
isEditor?: boolean;
|
||||||
|
/** Canvas ID 用于 UI 输入绑定 */
|
||||||
|
canvasId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -352,6 +354,15 @@ export function createRuntimeSystems(
|
|||||||
|
|
||||||
scene.addSystem(renderSystem);
|
scene.addSystem(renderSystem);
|
||||||
|
|
||||||
|
// 绑定 UIInputSystem 到 canvas(用于 UI 交互)
|
||||||
|
// Bind UIInputSystem to canvas (for UI interaction)
|
||||||
|
if (config?.canvasId && context.uiInputSystem) {
|
||||||
|
const canvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
|
||||||
|
if (canvas) {
|
||||||
|
(context.uiInputSystem as UIInputSystem).bindToCanvas(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cameraSystem,
|
cameraSystem,
|
||||||
animatorSystem: context.animatorSystem as SpriteAnimatorSystem | undefined,
|
animatorSystem: context.animatorSystem as SpriteAnimatorSystem | undefined,
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ class BrowserRuntime {
|
|||||||
private animationId: number | null = null;
|
private animationId: number | null = null;
|
||||||
private assetManager: AssetManager;
|
private assetManager: AssetManager;
|
||||||
private engineIntegration: EngineIntegration;
|
private engineIntegration: EngineIntegration;
|
||||||
|
private canvasId: string;
|
||||||
|
|
||||||
constructor(config: RuntimeConfig) {
|
constructor(config: RuntimeConfig) {
|
||||||
|
this.canvasId = config.canvasId;
|
||||||
if (!Core.Instance) {
|
if (!Core.Instance) {
|
||||||
Core.create();
|
Core.create();
|
||||||
}
|
}
|
||||||
@@ -58,8 +60,10 @@ class BrowserRuntime {
|
|||||||
// 初始化模块系统
|
// 初始化模块系统
|
||||||
await initializeRuntime(Core);
|
await initializeRuntime(Core);
|
||||||
|
|
||||||
// 创建运行时系统
|
// 创建运行时系统(传入 canvasId 用于 UI 输入绑定)
|
||||||
this.systems = createRuntimeSystems(Core.scene!, this.bridge);
|
this.systems = createRuntimeSystems(Core.scene!, this.bridge, {
|
||||||
|
canvasId: this.canvasId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadScene(sceneUrl: string): Promise<void> {
|
async loadScene(sceneUrl: string): Promise<void> {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from './components';
|
} from './components';
|
||||||
import { UILayoutSystem } from './systems/UILayoutSystem';
|
import { UILayoutSystem } from './systems/UILayoutSystem';
|
||||||
import { UIInputSystem } from './systems/UIInputSystem';
|
import { UIInputSystem } from './systems/UIInputSystem';
|
||||||
|
import { UIAnimationSystem } from './systems/UIAnimationSystem';
|
||||||
import { UIRenderDataProvider } from './systems/UIRenderDataProvider';
|
import { UIRenderDataProvider } from './systems/UIRenderDataProvider';
|
||||||
import {
|
import {
|
||||||
UIRenderBeginSystem,
|
UIRenderBeginSystem,
|
||||||
@@ -52,6 +53,9 @@ export class UIRuntimeModule implements IRuntimeModuleLoader {
|
|||||||
const layoutSystem = new UILayoutSystem();
|
const layoutSystem = new UILayoutSystem();
|
||||||
scene.addSystem(layoutSystem);
|
scene.addSystem(layoutSystem);
|
||||||
|
|
||||||
|
const animationSystem = new UIAnimationSystem();
|
||||||
|
scene.addSystem(animationSystem);
|
||||||
|
|
||||||
const renderBeginSystem = new UIRenderBeginSystem();
|
const renderBeginSystem = new UIRenderBeginSystem();
|
||||||
scene.addSystem(renderBeginSystem);
|
scene.addSystem(renderBeginSystem);
|
||||||
|
|
||||||
@@ -81,6 +85,7 @@ export class UIRuntimeModule implements IRuntimeModuleLoader {
|
|||||||
|
|
||||||
const uiRenderProvider = new UIRenderDataProvider();
|
const uiRenderProvider = new UIRenderDataProvider();
|
||||||
const inputSystem = new UIInputSystem();
|
const inputSystem = new UIInputSystem();
|
||||||
|
inputSystem.setLayoutSystem(layoutSystem);
|
||||||
scene.addSystem(inputSystem);
|
scene.addSystem(inputSystem);
|
||||||
|
|
||||||
context.uiLayoutSystem = layoutSystem;
|
context.uiLayoutSystem = layoutSystem;
|
||||||
|
|||||||
@@ -52,12 +52,16 @@ export class UIScrollViewComponent extends Component {
|
|||||||
* 内容宽度
|
* 内容宽度
|
||||||
* Content width
|
* Content width
|
||||||
*/
|
*/
|
||||||
|
@Serialize()
|
||||||
|
@Property({ type: 'number', label: 'Content Width', min: 0 })
|
||||||
public contentWidth: number = 0;
|
public contentWidth: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内容高度
|
* 内容高度
|
||||||
* Content height
|
* Content height
|
||||||
*/
|
*/
|
||||||
|
@Serialize()
|
||||||
|
@Property({ type: 'number', label: 'Content Height', min: 0 })
|
||||||
public contentHeight: number = 0;
|
public contentHeight: number = 0;
|
||||||
|
|
||||||
// ===== 滚动配置 Scroll Configuration =====
|
// ===== 滚动配置 Scroll Configuration =====
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ export type EasingName = keyof typeof Easing;
|
|||||||
@ECSSystem('UIAnimation')
|
@ECSSystem('UIAnimation')
|
||||||
export class UIAnimationSystem extends EntitySystem {
|
export class UIAnimationSystem extends EntitySystem {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 匹配任何可能有动画的组件
|
// 匹配有任何动画组件的实体
|
||||||
super(Matcher.empty());
|
super(Matcher.empty().any(UIButtonComponent, UIProgressBarComponent, UISliderComponent));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { UITransformComponent } from '../components/UITransformComponent';
|
|||||||
import { UIInteractableComponent } from '../components/UIInteractableComponent';
|
import { UIInteractableComponent } from '../components/UIInteractableComponent';
|
||||||
import { UIButtonComponent } from '../components/widgets/UIButtonComponent';
|
import { UIButtonComponent } from '../components/widgets/UIButtonComponent';
|
||||||
import { UISliderComponent } from '../components/widgets/UISliderComponent';
|
import { UISliderComponent } from '../components/widgets/UISliderComponent';
|
||||||
|
import { UIScrollViewComponent } from '../components/widgets/UIScrollViewComponent';
|
||||||
|
import type { UILayoutSystem } from './UILayoutSystem';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 鼠标按钮
|
* 鼠标按钮
|
||||||
@@ -58,6 +60,23 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
private lastClickEntity: Entity | null = null;
|
private lastClickEntity: Entity | null = null;
|
||||||
private doubleClickThreshold: number = 300; // ms
|
private doubleClickThreshold: number = 300; // ms
|
||||||
|
|
||||||
|
// ===== 滚轮状态 Wheel State =====
|
||||||
|
|
||||||
|
private wheelDeltaX: number = 0;
|
||||||
|
private wheelDeltaY: number = 0;
|
||||||
|
private hasWheelEvent: boolean = false;
|
||||||
|
|
||||||
|
// ===== ScrollView 拖拽状态 ScrollView Drag State =====
|
||||||
|
|
||||||
|
private scrollViewDragTarget: Entity | null = null;
|
||||||
|
private scrollViewDragStartX: number = 0;
|
||||||
|
private scrollViewDragStartY: number = 0;
|
||||||
|
private scrollViewDragStartScrollX: number = 0;
|
||||||
|
private scrollViewDragStartScrollY: number = 0;
|
||||||
|
private scrollViewLastDragX: number = 0;
|
||||||
|
private scrollViewLastDragY: number = 0;
|
||||||
|
private scrollViewLastDragTime: number = 0;
|
||||||
|
|
||||||
// ===== 事件监听器 Event Listeners =====
|
// ===== 事件监听器 Event Listeners =====
|
||||||
|
|
||||||
private canvas: HTMLCanvasElement | null = null;
|
private canvas: HTMLCanvasElement | null = null;
|
||||||
@@ -66,6 +85,11 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
private boundMouseUp: (e: MouseEvent) => void;
|
private boundMouseUp: (e: MouseEvent) => void;
|
||||||
private boundWheel: (e: WheelEvent) => void;
|
private boundWheel: (e: WheelEvent) => void;
|
||||||
|
|
||||||
|
// ===== UI 布局系统引用 UI Layout System Reference =====
|
||||||
|
// 用于获取 UI 画布尺寸以进行坐标转换
|
||||||
|
// Used to get UI canvas size for coordinate conversion
|
||||||
|
private layoutSystem: UILayoutSystem | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Matcher.empty().all(UITransformComponent, UIInteractableComponent));
|
super(Matcher.empty().all(UITransformComponent, UIInteractableComponent));
|
||||||
|
|
||||||
@@ -75,6 +99,17 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
this.boundWheel = this.onWheel.bind(this);
|
this.boundWheel = this.onWheel.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 UI 布局系统引用
|
||||||
|
* Set UI layout system reference
|
||||||
|
*
|
||||||
|
* 用于获取 UI 画布尺寸以进行坐标转换
|
||||||
|
* Used to get UI canvas size for coordinate conversion
|
||||||
|
*/
|
||||||
|
public setLayoutSystem(layoutSystem: UILayoutSystem): void {
|
||||||
|
this.layoutSystem = layoutSystem;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绑定到 Canvas 元素
|
* 绑定到 Canvas 元素
|
||||||
* Bind to canvas element
|
* Bind to canvas element
|
||||||
@@ -128,7 +163,30 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
|
|
||||||
private onMouseMove(e: MouseEvent): void {
|
private onMouseMove(e: MouseEvent): void {
|
||||||
const rect = this.canvas!.getBoundingClientRect();
|
const rect = this.canvas!.getBoundingClientRect();
|
||||||
this.setMousePosition(e.clientX - rect.left, e.clientY - rect.top);
|
// 屏幕坐标(CSS 像素,左上角为 0,0,Y 向下)
|
||||||
|
const screenX = e.clientX - rect.left;
|
||||||
|
const screenY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// 获取 UI 画布尺寸
|
||||||
|
const uiCanvasSize = this.layoutSystem?.getCanvasSize() ?? { width: 1920, height: 1080 };
|
||||||
|
|
||||||
|
// 转换为 UI 世界坐标(中心为 0,0,Y 向上)
|
||||||
|
// UI 坐标系:左上角是 (-width/2, +height/2),右下角是 (+width/2, -height/2)
|
||||||
|
// 屏幕坐标系:左上角是 (0, 0),右下角是 (canvasWidth, canvasHeight)
|
||||||
|
const canvasWidth = rect.width;
|
||||||
|
const canvasHeight = rect.height;
|
||||||
|
|
||||||
|
// 屏幕坐标归一化到 [0, 1]
|
||||||
|
const normalizedX = screenX / canvasWidth;
|
||||||
|
const normalizedY = screenY / canvasHeight;
|
||||||
|
|
||||||
|
// 转换为 UI 世界坐标
|
||||||
|
// X: 0 -> -uiCanvasWidth/2, 1 -> +uiCanvasWidth/2
|
||||||
|
// Y: 0 -> +uiCanvasHeight/2 (顶部), 1 -> -uiCanvasHeight/2 (底部)
|
||||||
|
const worldX = (normalizedX - 0.5) * uiCanvasSize.width;
|
||||||
|
const worldY = (0.5 - normalizedY) * uiCanvasSize.height;
|
||||||
|
|
||||||
|
this.setMousePosition(worldX, worldY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseDown(e: MouseEvent): void {
|
private onMouseDown(e: MouseEvent): void {
|
||||||
@@ -139,11 +197,17 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
this.setMouseButton(e.button as MouseButton, false);
|
this.setMouseButton(e.button as MouseButton, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWheel(_e: WheelEvent): void {
|
private onWheel(e: WheelEvent): void {
|
||||||
// TODO: 处理滚轮事件
|
e.preventDefault();
|
||||||
|
this.wheelDeltaX = e.deltaX;
|
||||||
|
this.wheelDeltaY = e.deltaY;
|
||||||
|
this.hasWheelEvent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// 如果没有绑定 canvas,不处理输入
|
||||||
|
if (!this.canvas) return;
|
||||||
|
|
||||||
const dt = Time.deltaTime;
|
const dt = Time.deltaTime;
|
||||||
|
|
||||||
// 按 zIndex 从高到低排序,确保上层元素优先处理
|
// 按 zIndex 从高到低排序,确保上层元素优先处理
|
||||||
@@ -214,6 +278,7 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
// 处理特殊控件
|
// 处理特殊控件
|
||||||
this.handleSlider(entity);
|
this.handleSlider(entity);
|
||||||
this.handleButton(entity, interactable);
|
this.handleButton(entity, interactable);
|
||||||
|
this.handleScrollView(entity, transform);
|
||||||
|
|
||||||
// 阻止事件传递到下层
|
// 阻止事件传递到下层
|
||||||
if (interactable.blockEvents) {
|
if (interactable.blockEvents) {
|
||||||
@@ -230,6 +295,9 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
if (interactable.pressed && !this.mouseButtons[MouseButton.Left]) {
|
if (interactable.pressed && !this.mouseButtons[MouseButton.Left]) {
|
||||||
interactable.pressed = false;
|
interactable.pressed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 即使鼠标不在元素上,也需要更新按钮的目标颜色(恢复到 normal)
|
||||||
|
this.handleButton(entity, interactable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +359,13 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
this.lastClickTime = 0;
|
this.lastClickTime = 0;
|
||||||
} else {
|
} else {
|
||||||
interactable.onClick?.();
|
interactable.onClick?.();
|
||||||
|
|
||||||
|
// 如果是按钮,也调用按钮的 onClick
|
||||||
|
const button = entity.getComponent(UIButtonComponent);
|
||||||
|
if (button && !button.disabled) {
|
||||||
|
button.onClick?.();
|
||||||
|
}
|
||||||
|
|
||||||
this.lastClickEntity = entity;
|
this.lastClickEntity = entity;
|
||||||
this.lastClickTime = now;
|
this.lastClickTime = now;
|
||||||
}
|
}
|
||||||
@@ -344,8 +419,11 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
const button = entity.getComponent(UIButtonComponent);
|
const button = entity.getComponent(UIButtonComponent);
|
||||||
if (!button || button.disabled) return;
|
if (!button || button.disabled) return;
|
||||||
|
|
||||||
// 更新目标颜色
|
// 更新目标颜色和当前颜色
|
||||||
button.targetColor = button.getStateColor(interactable.getState());
|
const stateColor = button.getStateColor(interactable.getState());
|
||||||
|
button.targetColor = stateColor;
|
||||||
|
// 直接设置 currentColor 以便立即看到效果(动画系统会平滑过渡)
|
||||||
|
button.currentColor = stateColor;
|
||||||
|
|
||||||
// 处理长按
|
// 处理长按
|
||||||
if (interactable.pressed) {
|
if (interactable.pressed) {
|
||||||
@@ -365,6 +443,220 @@ export class UIInputSystem extends EntitySystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleScrollView(entity: Entity, transform: UITransformComponent): void {
|
||||||
|
const scrollView = entity.getComponent(UIScrollViewComponent);
|
||||||
|
if (!scrollView) return;
|
||||||
|
|
||||||
|
const viewportWidth = transform.computedWidth;
|
||||||
|
const viewportHeight = transform.computedHeight;
|
||||||
|
const maxScrollX = scrollView.getMaxScrollX(viewportWidth);
|
||||||
|
const maxScrollY = scrollView.getMaxScrollY(viewportHeight);
|
||||||
|
|
||||||
|
// 处理滚轮事件
|
||||||
|
if (this.hasWheelEvent) {
|
||||||
|
let deltaX = 0;
|
||||||
|
let deltaY = 0;
|
||||||
|
|
||||||
|
if (scrollView.verticalScroll && maxScrollY > 0) {
|
||||||
|
deltaY = this.wheelDeltaY * (scrollView.wheelSpeed / 40);
|
||||||
|
}
|
||||||
|
if (scrollView.horizontalScroll && maxScrollX > 0) {
|
||||||
|
deltaX = this.wheelDeltaX * (scrollView.wheelSpeed / 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaX !== 0 || deltaY !== 0) {
|
||||||
|
if (scrollView.smoothScroll) {
|
||||||
|
scrollView.targetScrollX = this.clampScroll(scrollView.targetScrollX + deltaX, maxScrollX);
|
||||||
|
scrollView.targetScrollY = this.clampScroll(scrollView.targetScrollY + deltaY, maxScrollY);
|
||||||
|
} else {
|
||||||
|
scrollView.scrollX = this.clampScroll(scrollView.scrollX + deltaX, maxScrollX);
|
||||||
|
scrollView.scrollY = this.clampScroll(scrollView.scrollY + deltaY, maxScrollY);
|
||||||
|
scrollView.targetScrollX = scrollView.scrollX;
|
||||||
|
scrollView.targetScrollY = scrollView.scrollY;
|
||||||
|
}
|
||||||
|
scrollView.velocityX = 0;
|
||||||
|
scrollView.velocityY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasWheelEvent = false;
|
||||||
|
this.wheelDeltaX = 0;
|
||||||
|
this.wheelDeltaY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理内容拖拽滚动
|
||||||
|
const isLeftPressed = this.mouseButtons[MouseButton.Left]!;
|
||||||
|
const wasLeftPressed = this.prevMouseButtons[MouseButton.Left]!;
|
||||||
|
|
||||||
|
if (isLeftPressed && !wasLeftPressed && !scrollView.dragging) {
|
||||||
|
// 开始拖拽
|
||||||
|
scrollView.dragging = true;
|
||||||
|
scrollView.dragStartScrollX = scrollView.scrollX;
|
||||||
|
scrollView.dragStartScrollY = scrollView.scrollY;
|
||||||
|
this.scrollViewDragTarget = entity;
|
||||||
|
this.scrollViewDragStartX = this.mouseX;
|
||||||
|
this.scrollViewDragStartY = this.mouseY;
|
||||||
|
this.scrollViewDragStartScrollX = scrollView.scrollX;
|
||||||
|
this.scrollViewDragStartScrollY = scrollView.scrollY;
|
||||||
|
this.scrollViewLastDragX = this.mouseX;
|
||||||
|
this.scrollViewLastDragY = this.mouseY;
|
||||||
|
this.scrollViewLastDragTime = performance.now();
|
||||||
|
scrollView.velocityX = 0;
|
||||||
|
scrollView.velocityY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollView.dragging && this.scrollViewDragTarget === entity) {
|
||||||
|
if (isLeftPressed) {
|
||||||
|
// 拖拽中
|
||||||
|
const dragDeltaX = this.mouseX - this.scrollViewDragStartX;
|
||||||
|
const dragDeltaY = this.mouseY - this.scrollViewDragStartY;
|
||||||
|
|
||||||
|
let newScrollX = this.scrollViewDragStartScrollX - dragDeltaX;
|
||||||
|
let newScrollY = this.scrollViewDragStartScrollY - dragDeltaY;
|
||||||
|
|
||||||
|
if (scrollView.horizontalScroll) {
|
||||||
|
if (scrollView.elasticBounds) {
|
||||||
|
newScrollX = this.applyElasticBounds(newScrollX, maxScrollX, scrollView.elasticity);
|
||||||
|
} else {
|
||||||
|
newScrollX = this.clampScroll(newScrollX, maxScrollX);
|
||||||
|
}
|
||||||
|
scrollView.scrollX = newScrollX;
|
||||||
|
scrollView.targetScrollX = newScrollX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollView.verticalScroll) {
|
||||||
|
if (scrollView.elasticBounds) {
|
||||||
|
newScrollY = this.applyElasticBounds(newScrollY, maxScrollY, scrollView.elasticity);
|
||||||
|
} else {
|
||||||
|
newScrollY = this.clampScroll(newScrollY, maxScrollY);
|
||||||
|
}
|
||||||
|
scrollView.scrollY = newScrollY;
|
||||||
|
scrollView.targetScrollY = newScrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算速度(用于惯性)
|
||||||
|
const now = performance.now();
|
||||||
|
const timeDelta = (now - this.scrollViewLastDragTime) / 1000;
|
||||||
|
if (timeDelta > 0) {
|
||||||
|
scrollView.velocityX = (this.scrollViewLastDragX - this.mouseX) / timeDelta;
|
||||||
|
scrollView.velocityY = (this.scrollViewLastDragY - this.mouseY) / timeDelta;
|
||||||
|
}
|
||||||
|
this.scrollViewLastDragX = this.mouseX;
|
||||||
|
this.scrollViewLastDragY = this.mouseY;
|
||||||
|
this.scrollViewLastDragTime = now;
|
||||||
|
} else {
|
||||||
|
// 结束拖拽
|
||||||
|
scrollView.dragging = false;
|
||||||
|
this.scrollViewDragTarget = null;
|
||||||
|
|
||||||
|
// 应用惯性
|
||||||
|
if (!scrollView.inertia) {
|
||||||
|
scrollView.velocityX = 0;
|
||||||
|
scrollView.velocityY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新惯性滚动
|
||||||
|
this.updateScrollViewInertia(scrollView, maxScrollX, maxScrollY);
|
||||||
|
|
||||||
|
// 平滑滚动到目标位置
|
||||||
|
this.updateScrollViewSmooth(scrollView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clampScroll(value: number, max: number): number {
|
||||||
|
return Math.max(0, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyElasticBounds(value: number, max: number, elasticity: number): number {
|
||||||
|
if (value < 0) {
|
||||||
|
return value * elasticity;
|
||||||
|
}
|
||||||
|
if (value > max) {
|
||||||
|
return max + (value - max) * elasticity;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateScrollViewInertia(scrollView: UIScrollViewComponent, maxScrollX: number, maxScrollY: number): void {
|
||||||
|
if (scrollView.dragging) return;
|
||||||
|
|
||||||
|
const dt = Time.deltaTime;
|
||||||
|
const deceleration = scrollView.decelerationRate;
|
||||||
|
const minVelocity = 10;
|
||||||
|
|
||||||
|
// 应用减速
|
||||||
|
if (Math.abs(scrollView.velocityX) > minVelocity || Math.abs(scrollView.velocityY) > minVelocity) {
|
||||||
|
scrollView.velocityX *= Math.pow(deceleration, dt * 60);
|
||||||
|
scrollView.velocityY *= Math.pow(deceleration, dt * 60);
|
||||||
|
|
||||||
|
// 更新滚动位置
|
||||||
|
if (scrollView.horizontalScroll) {
|
||||||
|
scrollView.scrollX += scrollView.velocityX * dt;
|
||||||
|
scrollView.targetScrollX = scrollView.scrollX;
|
||||||
|
}
|
||||||
|
if (scrollView.verticalScroll) {
|
||||||
|
scrollView.scrollY += scrollView.velocityY * dt;
|
||||||
|
scrollView.targetScrollY = scrollView.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止条件
|
||||||
|
if (Math.abs(scrollView.velocityX) < minVelocity) scrollView.velocityX = 0;
|
||||||
|
if (Math.abs(scrollView.velocityY) < minVelocity) scrollView.velocityY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹回边界
|
||||||
|
if (scrollView.elasticBounds && !scrollView.dragging) {
|
||||||
|
const bounceSpeed = 8;
|
||||||
|
|
||||||
|
if (scrollView.scrollX < 0) {
|
||||||
|
scrollView.scrollX += (0 - scrollView.scrollX) * bounceSpeed * dt;
|
||||||
|
scrollView.targetScrollX = scrollView.scrollX;
|
||||||
|
if (Math.abs(scrollView.scrollX) < 0.5) scrollView.scrollX = 0;
|
||||||
|
} else if (scrollView.scrollX > maxScrollX) {
|
||||||
|
scrollView.scrollX += (maxScrollX - scrollView.scrollX) * bounceSpeed * dt;
|
||||||
|
scrollView.targetScrollX = scrollView.scrollX;
|
||||||
|
if (Math.abs(scrollView.scrollX - maxScrollX) < 0.5) scrollView.scrollX = maxScrollX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollView.scrollY < 0) {
|
||||||
|
scrollView.scrollY += (0 - scrollView.scrollY) * bounceSpeed * dt;
|
||||||
|
scrollView.targetScrollY = scrollView.scrollY;
|
||||||
|
if (Math.abs(scrollView.scrollY) < 0.5) scrollView.scrollY = 0;
|
||||||
|
} else if (scrollView.scrollY > maxScrollY) {
|
||||||
|
scrollView.scrollY += (maxScrollY - scrollView.scrollY) * bounceSpeed * dt;
|
||||||
|
scrollView.targetScrollY = scrollView.scrollY;
|
||||||
|
if (Math.abs(scrollView.scrollY - maxScrollY) < 0.5) scrollView.scrollY = maxScrollY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 硬边界限制
|
||||||
|
scrollView.scrollX = this.clampScroll(scrollView.scrollX, maxScrollX);
|
||||||
|
scrollView.scrollY = this.clampScroll(scrollView.scrollY, maxScrollY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateScrollViewSmooth(scrollView: UIScrollViewComponent): void {
|
||||||
|
if (scrollView.dragging) return;
|
||||||
|
if (scrollView.velocityX !== 0 || scrollView.velocityY !== 0) return;
|
||||||
|
|
||||||
|
const dt = Time.deltaTime;
|
||||||
|
const smoothSpeed = 1 / Math.max(0.01, scrollView.smoothScrollDuration);
|
||||||
|
|
||||||
|
const diffX = scrollView.targetScrollX - scrollView.scrollX;
|
||||||
|
const diffY = scrollView.targetScrollY - scrollView.scrollY;
|
||||||
|
|
||||||
|
if (Math.abs(diffX) > 0.5) {
|
||||||
|
scrollView.scrollX += diffX * smoothSpeed * dt;
|
||||||
|
} else {
|
||||||
|
scrollView.scrollX = scrollView.targetScrollX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(diffY) > 0.5) {
|
||||||
|
scrollView.scrollY += diffY * smoothSpeed * dt;
|
||||||
|
} else {
|
||||||
|
scrollView.scrollY = scrollView.targetScrollY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private updateCursor(hoveredEntity: Entity | null): void {
|
private updateCursor(hoveredEntity: Entity | null): void {
|
||||||
if (!this.canvas) return;
|
if (!this.canvas) return;
|
||||||
|
|
||||||
|
|||||||
16821
pnpm-lock.yaml
generated
16821
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user