Skip to main content

Chapter 133: Selection, Picking, and Direct Manipulation

选择交互把屏幕上的一次点击、一次触摸或一次拖拽变成场景中的对象状态变化。读完本章后,读者应能追踪一个屏幕坐标如何进入 viewport、camera、ray、depth buffer、object id、hit record、selection state 和 transform gizmo,并能排查“点到 A 却选中 B”“拖动 X 轴却沿奇怪方向移动”“轮廓高亮和实际对象对不上”这类交互错位。

本章的贯穿材料是一类常见编辑器 viewport:中央显示一个 3D 机械装配模型,左上角叠加 2D 标注,模型内部有半透明 volume 数据,选中对象后出现平移、旋转、缩放 gizmo,最终用轮廓和状态面板反馈选择结果。这个场景同时包含 2D UI、3D mesh、volume、overlay 和 transform handle,因此能暴露选择系统的核心问题:输入坐标、可见性、命中优先级、对象状态和视觉反馈必须使用同一套规则闭合。

Picking 的工作定义是:把输入设备产生的二维位置转换为场景中的候选对象,并输出一个可复查的命中记录。Direct manipulation 的工作定义是:用户在可见对象上连续操作,系统即时给出增量反馈,并把操作转换为可撤销的对象状态变化。二者合在一起,构成图形编辑器、CAD、DCC、地图、数据可视化和游戏工具的基础交互层。

本章最终建立一条判断顺序:先固定输入坐标和 viewport,随后固定 camera 与 projection,再选择 picking 路径,接着用 hit test 策略解决对象冲突,之后由 selection state 驱动反馈和 gizmo,最后用屏幕坐标、深度、对象 id 和变换矩阵复盘错位来源。

133.1 Picking as a Screen-to-Scene Query

Picking 的核心任务是执行一次 screen-to-scene query。输入端来自鼠标、触摸、触控笔或手柄射线;输出端应是一个结构化 hit record,而非只返回一个对象指针。稳定的 hit record 至少包含 object id、命中距离或深度、世界空间命中点、法线、primitive id、所属 layer、命中来源和优先级。这样后续的 selection、hover、drag、tooltip 和 gizmo 都能共享同一条证据链。

在贯穿场景中,用户点击机械装配体上的一颗螺栓。事件坐标首先进入窗口系统,再扣除 canvas 或 viewport 的左上角偏移,随后换算到 viewport 内部像素坐标。Web 场景还要处理 CSS 像素和 device pixel ratio 的差异;桌面编辑器还要处理多 viewport、scissor rect、停靠面板和高 DPI 缩放。这个阶段的输出应是 viewport-local coordinate,例如 (xViewport, yViewport)

viewport-local coordinate 再转换为 normalized device coordinate。典型透视 viewport 中,横向映射到 [-1, 1],纵向也映射到 [-1, 1],并且很多图形 API、UI 系统和数学库对 Y 轴方向采用不同约定。一次可靠的 picking 实现会把这个转换集中到一个函数里,并在 debug overlay 中显示输入点、viewport rect 和 NDC。三维 ray 的起点通常来自 camera position,方向来自 near plane 与 far plane 上两个反投影点的差值。

下面的简化 TypeScript 片段只表达坐标链和反投影关系。实际工程还需要处理矩阵存储约定、深度范围、viewport Y 方向、right-handed / left-handed 坐标系和 API 的 clip-space 差异。

interface Viewport {
left: number;
top: number;
width: number;
height: number;
}

interface Ray3 {
origin: Vec3;
direction: Vec3;
}

function buildPickingRay(
clientX: number,
clientY: number,
viewport: Viewport,
inverseViewProjection: Mat4,
cameraPosition: Vec3
): Ray3 {
const x = (clientX - viewport.left) / viewport.width;
const y = (clientY - viewport.top) / viewport.height;

const ndcX = x * 2.0 - 1.0;
const ndcY = 1.0 - y * 2.0;

const nearPoint = inverseViewProjection.multiplyPoint(new Vec3(ndcX, ndcY, 0.0));
const farPoint = inverseViewProjection.multiplyPoint(new Vec3(ndcX, ndcY, 1.0));

return {
origin: cameraPosition,
direction: farPoint.subtract(nearPoint).normalize(),
};
}

这段代码能说明一个边界:picking ray 属于 camera 与 viewport 状态的派生结果。相机矩阵、projection matrix、viewport 尺寸或 DPI 任一项更新滞后,ray 都会偏离用户看到的像素。交互错位排查时,先检查 ray 和视图状态同步,收益高于直接怀疑 mesh intersection 算法。

工程中常见 picking 路径有三类。CPU ray picking 在 CPU 侧用 ray 与 bounding volume、triangle、curve 或 collider 求交,适合编辑器、静态模型、层级对象和需要详细命中信息的场景。GPU id picking 用一次离屏 pass 把 object id 或 primitive id 写入整数 render target,再读取点击像素,适合大量物体、屏幕可见性优先和实例化对象。Depth picking 读取 depth buffer 后反投影到世界空间,适合定位表面点、放置标记和生成测量点。

这三类路径回答的问题不同。CPU ray picking 关注“这条射线在几何上先碰到谁”;GPU id picking 关注“当前画面这个像素实际绘出了谁”;Depth picking 关注“这个像素的可见表面点在哪里”。在有透明物体、后处理轮廓、overlay、裁剪平面和自定义 depth pass 的场景中,三者可能给出不同结果。稳定系统会明确当前工具使用哪条 picking 路径,并把路径写入 hit record。

下面的图把一次点击到 hit record 的路径压缩成可检查流程。它的边界是单 viewport 输入;多窗口、多相机和 XR 控制器可以扩展同一条链,但不改变核心依赖。

图中最容易出错的是 Viewport local coordinateHit record 两个位置。前者决定输入点是否对应用户看到的像素;后者决定后续系统是否能解释选择结果。很多编辑器把命中对象直接写入 selection set,随后 hover、高亮、属性面板和 gizmo 分别重新计算自己的对象,最终形成多套互相矛盾的状态。更稳的做法是先产出 hit record,再由 selection policy 决定它是否进入选中集合。

如果使用现成库,API 名称也体现了这些边界。three.js 的 Raycaster 代表 CPU 侧 ray 与对象求交的典型入口;WebGPU、Vulkan 或 Metal 项目常用离屏整数 attachment 维护 object id;编辑器工具还会把两者组合,用 GPU id 找到对象,再用 CPU ray 求出对象局部命中点。库封装降低了代码量,但坐标转换、viewport 状态和 hit record 语义仍由工程负责。

133.2 Hit Testing in 2D, 3D, and Hybrid Scenes

Hit testing 解决候选对象之间的裁决问题。Picking 产出“可能碰到谁”,hit testing 决定“本次交互应该命中谁”。在贯穿场景中,同一个屏幕点可能覆盖 2D 标注、透明 volume、机械外壳、内部螺栓、选中对象轮廓和 gizmo 轴。系统需要明确各对象进入命中测试的空间、遮挡规则、优先级和容差。

2D UI 的命中通常工作在屏幕空间或局部 UI 空间。按钮、标签、面板、曲线编辑器和 overlay 图标使用矩形、圆形、多边形或 signed distance 函数判断输入点是否落入区域。它们的优先级通常由 z-order、modal state、事件捕获和 pointer capture 决定。2D 层命中结果应写明 UI element id 与所属 layer,因为它常常会拦截对 3D 场景的选择。

3D mesh 的命中通常工作在对象空间或世界空间。系统会先用 bounding sphere、axis-aligned bounding box、oriented bounding box 或 BVH 做粗筛,再对 triangle、curve、point sprite 或 collider 做精筛。输出距离时应使用 ray parameter 或 camera depth,随后按照最近可见、工具优先级、对象锁定状态和 layer mask 排序。对 skinned mesh、morph target 或 GPU procedural geometry,CPU 侧几何可能和最终画面存在差异,此时要说明使用 bind-pose、proxy collider、GPU id pass 还是 readback 数据。

Volume 命中需要额外区分几何外壳和体数据采样。用户点到半透明医学体数据或流体云图时,ray 可能先进入 volume bounding box,但真正可交互的位置取决于 density threshold、transfer function、前向累计透明度和最大步数。为了让交互可解释,hit record 应同时记录 volume entry point、first significant sample、采样阈值和当前 transfer function 版本。这样用户调整 opacity 后,点击结果变化才有可复查原因。

Hybrid scene 的策略应先分层,再排序。贯穿场景可采用 modal tool → gizmo handle → 2D overlay → selectable mesh → volume sample → background 的顺序。这个顺序服务交互意图:拖拽 gizmo 时,handle 拥有输入;打开标注编辑模式时,2D label 先于 3D mesh;常规选择模式下,可见 mesh 先于透明 volume。排序策略需要写入工具配置或文档,不能散落在多个事件回调里。

对象类型命中空间主要证据常见容差冲突裁决
2D 标注屏幕或 UI 局部空间UI rect、shape path、z-order触摸半径、文字外扩边距modal state 与 z-order 优先
3D Mesh世界或对象空间ray distance、triangle id、depth细线与小物体可使用屏幕半径最近可见对象优先
Volumeray marching 空间entry depth、density threshold、opacity采样步长和阈值窗口显著采样点优先
Gizmo屏幕空间加世界空间handle id、axis、active mode轴线屏幕距离active tool 优先
Overlay 轮廓屏幕空间selection state、stencil、depth通常不进入选择只作为反馈层

小物体和细线需要屏幕空间容差。螺栓、边线、控制点、骨骼关节和曲线手柄在高分辨率 viewport 中可能只有几个像素宽。直接做几何 ray intersection 会让用户难以命中。常见做法是在屏幕空间计算点到投影线段的距离,或者把对象包围体按像素半径扩张,再回到 3D 候选集合排序。容差必须进入 hit record,否则用户看到“点在边线旁边也选中”的结果时,系统无法解释原因。

透明与遮挡需要独立策略。机械装配模型的半透明外壳可能允许选择内部螺栓,也可能只允许选择外壳;这取决于当前工具意图。稳定做法是给材质或对象设置 pick policy,例如 opaqueOnlytransparentSelectablepassThroughlockedhidden。渲染透明度和可选择性保持独立字段,能减少材质调整对交互语义的连带影响。

多选框、套索和区域选择属于批量 hit testing。它们通常先把对象包围体或候选顶点投影到屏幕,判断是否落入矩形或多边形;精确模式再使用 depth、可见性或 triangle coverage 过滤。区域选择的核心边界是“包含中心点”“包含任意像素”“完全包围对象”三种语义差异。每种语义对用户预期不同,必须在工具反馈中显示清楚,例如框选时实时显示候选数量和即将进入 selection set 的对象。

Hit testing 的最终产物应是按优先级排序的候选列表,而非单一结果。候选列表支持循环选择、右键菜单、悬停预览、重叠对象弹窗和调试视图。贯穿场景中,如果螺栓、标注和内部 volume 同处一个屏幕点,系统可以显示候选栈:当前默认命中标注,按住修饰键切换到 mesh,进入 volume 工具后选择采样点。这样交互策略从隐式猜测变成可见规则。

133.3 Manipulator Gizmos and Transform Handles

Manipulator gizmo 把屏幕拖拽转换成对象变换。它的输入是 active selection、pivot、坐标空间、handle id、pointer ray 和约束规则;输出是一个增量 transform,再由 undo 系统、约束系统、层级系统和渲染反馈共同消费。gizmo 的关键难点在于:用户拖动的是屏幕上的轴、环或平面,实际更新的是对象在某个空间中的 translation、rotation 或 scale。

贯穿场景中,选中一颗螺栓后出现三轴平移 gizmo。用户按下红色 X 轴 handle 时,系统先命中 handle id,再锁定 active axis,并记录 drag start 状态:对象初始 world matrix、局部坐标轴、pivot、pointer ray、camera matrix、snap 设置和约束模式。后续 pointer move 事件都基于这份起始状态计算增量,不能每帧用已经改变的对象状态重新定义起始轴,否则拖拽会产生累积漂移。

平移 handle 通常有两种求解方式。轴向拖拽把 pointer ray 与世界空间或局部空间中的约束轴求最近点,得到轴上标量增量;平面拖拽把 pointer ray 与约束平面求交,得到平面内二维增量。轴向求解在相机方向接近平行约束轴时会变得不稳定,系统应切换到更合适的拖拽平面或提示当前轴难以判定。平面求解则依赖 plane normal,常见选择是轴法线、camera-facing 平面或对象局部平面。

旋转 handle 把拖拽转换为绕某个轴的角度变化。常见算法是在 pivot 附近定义旋转平面,把 pointer ray 与平面求交,得到起点向量和当前向量,再计算绕轴 signed angle。屏幕空间圆环的视觉位置应和这个旋转平面一致;如果渲染圆环使用 camera-facing billboarding,求解却使用 object local plane,用户会看到拖动方向和旋转反馈脱节。

缩放 handle 需要处理负缩放、非均匀缩放和层级矩阵。轴向缩放可以把拖拽距离映射为某个轴的 scale ratio;统一缩放可以使用屏幕空间距离或相机平面距离。工程中应把 scale delta 与 rotation、translation 分开记录,再组合成 TRS。直接修改 4x4 matrix 的元素会掩盖 shear、父级非均匀缩放和 pivot offset,后续 gizmo 显示也会出现偏差。

坐标空间是 gizmo 语义的中心。world space gizmo 的轴固定对齐世界坐标,适合场景摆放;local space gizmo 的轴跟随对象旋转,适合沿零件自身方向调整;view space gizmo 的平面跟随相机,适合屏幕构图;parent space gizmo 跟随层级父节点,适合骨骼、装配体和 UI 容器。空间切换时,gizmo 的可视轴、求解轴、snap 规则和属性面板数值应使用同一套 space id。

下面的状态图描述一次 gizmo 拖拽生命周期。它的重点是把命中 handle、锁定约束、计算增量、提交历史拆开,便于定位错位发生在哪个阶段。

状态图中的 Capture 阶段决定后续拖拽是否稳定。它必须保存起始 pointer、起始 ray、起始 transform、起始 pivot 和 active handle。Preview 阶段只计算从起始状态到当前输入的增量,并驱动临时渲染反馈。Commit 阶段才写入 undo record、通知属性面板、刷新层级和触发资源更新。这样拖拽中断、输入捕获丢失或用户按下取消键时,系统能恢复到起始状态。

现成工具的 API 也体现了这种分工。three.js 的 TransformControls 把 gizmo、camera 和 selected object 绑定在一起;Unity Editor 的 Handles 提供 position、rotation、scale 这类编辑器手柄入口。它们都能作为实现参照,但工程仍要明确 active selection、space、pivot、snap、undo 和视觉反馈如何接入自己的状态系统。

133.4 Selection State and Visual Feedback

Selection state 是交互语义状态,render state 是画面输出状态。二者相互驱动,但应保持清晰边界。selection state 记录对象是否 hover、selected、active、locked、hidden、multi-selected、preview-selected 或 editing;render state 决定材质、outline、stencil、depth test、overlay、label 和 gizmo 如何显示。把选择状态直接塞进材质实例会让 undo、撤销选择、批量选择和多 viewport 同步变得脆弱。

贯穿场景中,点击螺栓后应产生三个层次的反馈。第一层是 hover 或 selected highlight,让用户知道命中对象;第二层是属性面板和状态栏,显示 object name、id、layer、材质和 hit path;第三层是 gizmo 和可操作 handle,告诉用户下一步能拖动什么。三层反馈都应由同一个 selection state 派生,才能保证“画面高亮对象”“属性面板对象”和“gizmo 操作对象”一致。

高亮方式有多种。材质 tint 修改对象自身着色,成本低,但会影响 PBR 材质判断;outline pass 用 stencil、depth 或 normal/depth 边缘生成轮廓,适合复杂模型;x-ray overlay 能显示被遮挡对象,适合装配体和层级选择;bounding box 与 pivot marker 适合表达对象范围和变换中心。选择系统应为不同对象类型配置反馈方式:mesh 用 outline,label 用描边或背景,volume 用采样点和切片标记,gizmo handle 用颜色和加粗。

遮挡关系决定反馈是否可信。被外壳遮住的内部螺栓如果被选中,纯轮廓可能完全不可见。此时可使用 depth-aware outline、半透明遮挡提示、ghost overlay、object path breadcrumb 或局部剖切提示。反馈目标是让用户能回答三个问题:当前选中谁、它在哪里、为什么这次输入命中了它。只显示一个醒目的颜色无法覆盖这些信息。

多选状态需要明确 active object。多选集合表示将被一起操作的对象;active object 表示属性面板、pivot、对齐、父级关系或默认材质编辑的主对象。区域选择后,系统应显示 selection count,同时把 active object 与 selected set 分开记录。gizmo pivot 可以取 active object origin、selection bounding box center、median center 或 custom pivot。不同选择会影响拖拽结果,因此 pivot mode 应显示在 viewport 或工具栏中。

直接操作强调连续、可逆和增量反馈。用户拖动 gizmo 时,画面应实时显示 preview transform,属性面板可以显示临时数值,状态栏可以显示轴向增量和 snap 信息。松开输入后,系统提交一个可撤销操作。Microsoft 的 Direct Manipulation 文档把输入、viewport 和内容变换组织成可组合的交互路径;图形编辑器虽然常有自定义渲染管线,但同样需要把输入捕获、增量更新、反馈和提交拆成稳定阶段。

选择状态还要处理权限与生命周期。locked object 可以显示 hover 提示但不进入 selection set;hidden object 不参与常规 picking;frozen layer 可以被命中但只显示不可编辑状态;删除对象时 selection set 要清理失效 id;实例化对象要记录 instance id;组合对象要记录 group path。没有这些字段,用户会在大型场景中遇到“看得见却点不了”“选中后属性丢失”“删除后 gizmo 悬空”的问题。

一个可维护的 selection model 可以写成三层数据:hitCandidate[] 表示本次输入的候选栈,selectionSet 表示当前持久选择,interactionState 表示当前 hover、capture、drag、preview 和 commit。渲染层只订阅这三层状态并输出 feedback pass。这样选择策略可以单独测试,反馈 pass 可以单独替换,工具状态也能跨 viewport 保持一致。

133.5 Debugging Interaction Mismatch

Interaction mismatch 指用户感知的输入目标、系统命中的对象和最终视觉反馈之间出现偏差。排查这类问题要按数据链向下走,而非先改 hit test 阈值。贯穿场景中的典型故障是:用户点到螺栓,系统选中后方标注;用户拖动本地 X 轴,零件沿世界 Z 方向移动;用户框选装配体,属性面板显示 12 个对象,画面只高亮 11 个。

第一步检查输入坐标。记录原始 event coordinate、canvas bounding rect、viewport rect、device pixel ratio、framebuffer size 和 NDC。把这些数值显示到 debug overlay,并在点击位置画一个十字。如果十字没有落在用户点击的像素,问题位于窗口、DPI、viewport 或 Y 轴翻转。多 viewport 编辑器还要检查当前 pointer 是否进入了正确 viewport,以及 scissor rect 是否和渲染区域一致。

第二步检查 camera 与矩阵。记录 view matrix、projection matrix、inverse view-projection matrix、camera position、near/far、FOV、aspect 和当前帧编号。交互层使用上一帧相机矩阵时,快速 orbit 后的点击会偏移;属性面板改变 FOV 后,picking ray 如果仍用旧 projection,也会偏移。debug 视图可以把 ray 画进 scene,并把 near plane 命中点、far plane 命中点和 candidate bounding box 显示出来。

第三步检查 picking path。CPU ray picking 需要确认对象 transform、BVH、collider、skinned pose、layer mask 和 triangle winding 是否更新;GPU id picking 需要确认 id pass 的 viewport、depth test、MSAA resolve、integer format、object id 写入和 readback 坐标;Depth picking 需要确认 depth range、linearization、reverse-Z、clear value 和 readback 同步。每条路径都要输出 path type,这样日志能说明当前结果来自几何求交、id buffer 还是 depth buffer。

第四步检查 hit test policy。把候选栈按顺序打印出来:object id、type、layer、distance、depth、priority、pick policy、locked/hidden 状态和容差来源。很多错选问题出在排序策略,例如 2D overlay 拥有过高优先级、透明外壳没有 pass-through、gizmo handle 在非编辑模式仍参与命中、区域选择使用中心点策略导致细长对象漏选。候选栈能把这些策略差异显性化。

第五步检查 selection state 与 feedback pass。确认 selection set、active object、hover object、drag capture object、outline pass 输入、stencil mask、depth-aware overlay 和属性面板绑定的是同一组 id。画面高亮少一个对象时,常见原因是对象不可渲染、outline pass 过滤了材质、instance id 没有展开、被遮挡对象没有 x-ray 反馈,或者反馈 pass 使用了旧 selection snapshot。

症状优先检查关键证据修复方向
点击整体偏移viewport 与 DPIoverlay 十字、NDC、framebuffer size统一坐标转换入口
orbit 后短暂错选camera 同步ray 帧编号、view-projection 版本输入与渲染共用相机快照
透明外壳总是抢选hit policycandidate stack、material pick policy配置透明对象 pass-through 或工具优先级
gizmo 轴方向异常space 与 pivotactive axis、space id、pivot matrix统一显示轴和求解轴
框选数量与高亮不一致selection 与 feedbackselection set、outline pass 输入让反馈 pass 订阅同一状态快照

排查时应保留可复现的最小场景。一个 cube、一个 overlay label、一个 transparent shell、一个 child object 和一个 gizmo 已经能覆盖多数错位类型。测试步骤固定为:点击 cube 中心,点击边界,点击 overlay 重叠处,打开透明对象,切换 local/world gizmo,拖动轴,框选对象,撤销操作。每一步都记录输入坐标、hit record、selection state 和视觉反馈。这个最小场景比大型工程场景更适合验证选择系统的因果链。

最终判断标准是:用户看到的可交互对象、系统产出的 hit record、selection state 中的对象集合、gizmo 使用的 pivot 与 space、feedback pass 绘制的高亮对象必须互相对应。任何一个环节需要特殊规则时,规则应写入 hit record 或 selection state,而非隐藏在某个渲染 pass 或事件回调里。做到这一点后,选择、拾取和直接操作才能在复杂图形系统中保持可解释、可调试和可维护。

最小自检任务

在一个编辑器 viewport 中放置四个对象:一个 3D cube、一个覆盖在 cube 上方的 2D label、一个半透明 outer shell、一个 world-space translation gizmo。用户点击 cube 中心位置时,系统选中了 2D label;用户按住修饰键后能选中 cube,但拖动 gizmo 的 X 轴时,cube 沿相机右方向移动。请设计一次排查顺序,并说明每一步要记录哪些证据。

答案要点

先记录原始 pointer coordinate、viewport rect、device pixel ratio、framebuffer size 和 NDC,并在点击像素绘制 debug 十字,确认输入位置和画面位置一致。随后记录 camera view-projection 快照和 picking ray,把 ray 画进场景,确认 ray 穿过 cube 中心。

接着打印 hit candidate stack,字段包括 object id、type、layer、distance、depth、priority、pick policy 和 path type。若 label 排在 cube 前面,需要检查当前工具是否处于 label 编辑模式、2D overlay 的优先级是否覆盖常规 3D 选择、透明 shell 的 pick policy 是否改变了候选排序。修饰键能选中 cube,说明候选栈中存在 cube,主要问题位于 hit test policy。

然后进入 gizmo 排查。记录 active handle id、space id、pivot matrix、axis vector、drag start ray、当前 ray、求解平面或约束轴、delta transform 和 preview transform。X 轴拖拽却沿相机右方向移动,说明渲染显示的轴和求解使用的约束空间可能不一致,或者工具处于 view space 模式但 UI 仍显示 world/local 轴。修复方向是让 gizmo 可视轴、active axis、求解轴和属性面板 space id 来自同一份 interaction state。

最后检查 selection state 与 feedback pass。确认选中 cube 后,selection set、active object、gizmo target 和高亮 pass 输入使用同一个 object id。若状态一致但反馈错位,再检查 outline pass 的 depth、stencil、instance id 和透明对象遮挡策略。

本章知识点总结

  • Picking 查询:Picking 把屏幕输入转换成结构化 hit record,后续选择、悬停、拖拽和反馈都应复用这条证据。
  • 坐标链路:输入坐标需要经过 viewport、DPI、NDC、camera 和反投影,任一状态滞后都会造成点击偏移。
  • 路径差异:CPU ray picking、GPU id picking 和 depth picking 分别回答几何求交、当前像素对象和可见表面点问题。
  • 命中策略:Hit testing 根据对象类型、layer、可见性、透明策略、容差和工具模式裁决候选对象。
  • 混合场景:2D UI、3D mesh、volume、overlay 和 gizmo 应先分层处理,再按交互意图排序。
  • 候选栈:候选列表比单一结果更适合调试重叠对象、循环选择和修饰键切换。
  • Gizmo 状态:Manipulator gizmo 需要保存起始 transform、pivot、space、handle 和 pointer ray,再从起始状态计算增量。
  • 约束空间:平移、旋转和缩放手柄的可视轴、求解轴、snap 规则和属性面板数值应使用同一 space id。
  • 选择状态:Selection state 记录交互语义,render state 负责输出高亮、轮廓、标签和 gizmo。
  • 反馈层级:高亮、属性面板和 gizmo 应由同一个 selection state 派生,保证用户看到、面板显示和实际操作对象一致。
  • 遮挡反馈:被遮挡对象需要 depth-aware outline、ghost overlay 或路径提示,帮助用户解释当前选中对象的位置。
  • 错位排查:排查 interaction mismatch 时按输入坐标、camera、picking path、hit policy、selection state 和 feedback pass 顺序检查。