右键拖拽平移
一、问题本质
不是平移公式写错,而是 UrhoX UI 库指针事件派发的两个不对称陷阱叠加:
| 陷阱 | 现象 | 根因文件 |
| OnPointerMove 不向父冒泡 | 右键按下 isPanning_=true,但 move 事件永远到不了 mapView | urhox-libs/UI/Core/Widget.lua(OnPointerDown/Up 有 parent 冒泡,OnPointerMove 没有) |
| HandlePointerUp 无视按键触发 OnClick | 右键抬起在物件/同学身上会误弹菜单/拾取 | urhox-libs/UI/Core/UI.lua:2078-2103(同 widget 松开就调 pressedWidget:OnClick(event),不看 button) |
加上 worldLayer 默认 pointerEvents="auto" + backgroundColor 使 findWidgetAt 在空白区命中 worldLayer 而非 mapView,三件套叠加 → 右键完全失效。


二、关键代码(4 处改动,scripts/页面/校园地图.lua)
1. worldLayer 改为 pointerEvents = "box-none"(核心修复)
lualocal worldLayer = UI.Panel { position = "absolute", left = worldOffsetX_ or 0, top = worldOffsetY_ or 0, width = canvasMetrics.worldWidth, height = canvasMetrics.worldHeight, backgroundColor = { 48, 52, 55, 255 }, overflow = "visible", -- v0.10.80:必须 box-none;否则空白区域右键时 findWidgetAt 返回 worldLayer -- (default "auto" + backgroundColor 使其 HitTest 命中), -- Widget:OnPointerMove 不像 OnPointerDown/Up 那样向父冒泡,mapView.onPointerMove -- 就收不到拖拽事件,表现为右键平移无效。 pointerEvents = "box-none", scale = zoomLevel_, transformOrigin = "top-left", children = { ... }, }
原理:box-none 让 worldLayer 自身不 HitTest(子节点仍可命中)。空白区右键时 pressedWidgets_[0] = mapView,HandlePointerMove 的 pressed-widget 拖拽回退路径(UI.lua:2035-2042)会在光标离开 mapView 时克隆 dragEvent 并用 screenToWidgetLocal(mapView, x, y) 继续派发给 mapView,所以即使拖拽途中划过学生/物件标记,平移也不会中断。
2. 三个 onClick 加左键守卫(防右键误触)
物件拾取(CreateObjectDot):
luaonClick = interactionText and function(widget, event) -- v0.10.80:仅左键触发拾取;右键是地图拖拽,不能触发交互 if event and event.button and event.button ~= MOUSEB_LEFT then return end if event then event:StopPropagation() end local ok, reason = PlayerInteractionService.PickupAny(context, object.id) ...
主角菜单(playerMarker):
luaplayerMarker.onClick = function(widget, event) -- v0.10.80:仅左键打开主角菜单;右键是地图拖拽 if event and event.button and event.button ~= MOUSEB_LEFT then return end if event then event:StopPropagation() end ...
同学菜单(nearbyStudents marker):
luaonClick = function(widget, event) -- v0.10.80:仅左键打开同学菜单;右键是地图拖拽 if event and event.button and event.button ~= MOUSEB_LEFT then return end if event then event:StopPropagation() end ...
注意守卫条件是 ~= MOUSEB_LEFT 而不是 == MOUSEB_RIGHT。这样中键(MOUSEB_MIDDLE)和未来扩展按键也不会误触交互,只有左键才触发业务逻辑。
3. Pan 逻辑(v0.10.78 写的,v0.10.80 原样保留,现在才能真正跑通)
lua-- 模块级状态 local isPanning_ = false local panStartX_, panStartY_ = 0, 0 local panOrigOffsetX_, panOrigOffsetY_ = 0, 0 local mapView = UI.Panel { ... onPointerDown = function(widget, event) if event.button == MOUSEB_RIGHT then isPanning_ = true panStartX_ = event.x panStartY_ = event.y panOrigOffsetX_ = worldOffsetX_ panOrigOffsetY_ = worldOffsetY_ end end, onPointerMove = function(widget, event) -- 缓存光标供滚轮锚点缩放用 lastMouseX_, lastMouseY_ = event.x, event.y -- v0.10.80:isPanning_ 为 true 平移(event.button 在 MOVE 事件不可靠,必须靠状态位) if isPanning_ then worldOffsetX_ = panOrigOffsetX_ + (event.x - panStartX_) worldOffsetY_ = panOrigOffsetY_ + (event.y - panStartY_) context.RebuildUI() end end, onPointerUp = function(widget, event) isPanning_ = false -- 防粘手:在控件内松开 end, onPointerCancel = function(widget, event) isPanning_ = false -- 防粘手:拖出控件外/系统中断 end, }


三、可复用的经验规则(已写入 工具准则 避雷清单)
- 父容器要接 onPointerMove 拖拽 → 子容器必须 pointerEvents="box-none"。默认 auto + 任何背景色都会让空白区 HitTest 命中子层;而 OnPointerMove 不冒泡,父层永远收不到拖拽。
- 任何会弹出菜单/拾取/提交的 onClick 都必须加左键守卫,因为 HandlePointerUp 不区分按键:
- MOVE 事件上的 event.button 不可靠,拖拽状态必须用模块级 isPanning_ 这类状态位判断,onPointerUp/onPointerCancel 都必须复位(onPointerCancel 覆盖拖出控件外松开、系统中断等场景)。
- pan 公式坐标空间:event.x/event.y 在 onPointerDown/Move 回调里已由 UI 库通过 screenToWidgetLocal(widget, x, y) 覆写为 widget-local 基准像素,与 worldLayer 的 left/top(position="absolute")同空间,直接相减即可,不要再扣 viewRect.x/y(v0.10.78 已修正)。


四、约束遵守验证
| 用户约束 | 实现 |
| 左键绝对不能拖地图 | onPointerDown 只在 event.button == MOUSEB_RIGHT 时置 isPanning_;左键行为(拾取/菜单/背包)完全保留 |
| 只有右键按住才拖 | onPointerDown 入口 + 状态位驱动 |
| 松开右键立即停 | onPointerUp 和 onPointerCancel 都复位 isPanning_ |
| 左键原有交互不破坏 | 三个 onClick 守卫仅过滤非左键,左键路径一行没改 |
LSP 41 文件 0 Error,官方 Build 一次通过。

