框选多选与变换手柄
点选只解决「命中最上层一个」。编辑器还要:拖空处画矩形选、Shift 加选、包围盒手柄、旋转、对齐吸附。这些全部是 场景数据操作,Canvas 只负责画装饰层。
Camera、viewToWorld 见 相机与锚点缩放。
1. 框选:世界 AABB 相交,不要像素
export interface Aabb {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
export function viewRectToWorld(
camera: Camera,
a: { x: number; y: number },
b: { x: number; y: number },
): Aabb {
const w1 = viewToWorld(camera, a.x, a.y);
const w2 = viewToWorld(camera, b.x, b.y);
return {
minX: Math.min(w1.x, w2.x),
minY: Math.min(w1.y, w2.y),
maxX: Math.max(w1.x, w2.x),
maxY: Math.max(w1.y, w2.y),
};
}
export function aabbHits(a: Aabb, b: Aabb): boolean {
return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY;
}
从空处 pending 拖过阈值后进入 marquee。松手时对 pickable 节点的世界 AABB 做相交。旋转节点的 AABB 是 OBB 的轴对齐外包,会偏大——精确选集再对 path 做一次 isPointInPath 或 SAT。不要 getImageData 扫框。
Shift:结果与当前选集求并。Alt:求差。先后顺序写进产品规则,不要隐式。
2. 选集变换:改矩阵,不改 path 顶点
多选拖动 = 每个节点 local.e/f 加上 世界平移。若有父节点,要把世界 delta 变到父空间:
export function translateNode(
nodeLocal: DOMMatrix,
parentWorld: DOMMatrix,
worldDx: number,
worldDy: number,
): DOMMatrix {
const parentInv = parentWorld.inverse();
const delta = parentInv.transformPoint(new DOMPoint(worldDx, worldDy));
const origin = parentInv.transformPoint(new DOMPoint(0, 0));
return nodeLocal.translate(delta.x - origin.x, delta.y - origin.y);
}
缩放/旋转手柄:绕选集世界中心。手柄本身画在 装饰层,用 view 像素固定大小(handle = 8 / camera.scale 世界长度),否则缩小时手柄点不中。
命中顺序:手柄 > 节点 > 框选空处。手柄用正方形 AABB 即可,不必上 path。
3. 吸附
export function snap(value: number, grid: number): number {
return Math.round(value / grid) * grid;
}
export function snapThreshold(
value: number,
targets: readonly number[],
slopWorld: number,
): number {
let best = value;
let bestDist = slopWorld;
for (const t of targets) {
const d = Math.abs(value - t);
if (d < bestDist) {
bestDist = d;
best = t;
}
}
return best;
}
slopWorld 随相机变:8 / camera.scale,保证屏幕上大约 8 CSS 像素才吸。目标来自其它节点的边/中线,不要每帧对全部节点算——先用空间索引拿出视口附近。
Shift 拖:约束到水平或垂直(看 dx vs dy 谁大)。这是工具层,不是相机层。
4. 装饰层单独画
行军蚁、手柄、对齐参考线放动态层,每帧清。节点层只在 scene 变时重绘。见 分层。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 旋转后框选漏选 | 用了局部 AABB 去比世界框 |
| 手柄缩小时点不中 | 手柄尺寸跟了世界 scale |
| 子节点拖动方向反 | 父级有翻转,delta 没进父空间 |
| 多选缩放各飞各的 | 没有统一围绕公共中心 |
| 对齐抖 | slop 用了世界固定像素,放大后吸死 |
权威资料
核对日期:2026-08-26
需要 Camera / viewToWorld 时用 相机与锚点缩放。