框选多选与变换手柄
点选只解决最上层一个节点。编辑器还要:空处拖橡皮筋、Shift 加选、包围盒手柄。橡皮筋画在 屏幕空间,相交算在 用户空间。两套坐标不要用同一份 rect 的 x/y/width/height 混着写。
clientToLocal 见 getScreenCTM。状态机见 指针捕获。
1. 橡皮筋:屏幕空间
用 HTML overlay 或一层 不受相机矩阵编辑 的装饰 <g>(固定对着 css 像素)。按下和移动都用 clientX/Y 画矩形:
export interface ScreenRect {
left: number;
top: number;
width: number;
height: number;
}
export function clientMarquee(
a: { x: number; y: number },
b: { x: number; y: number },
): ScreenRect {
const left = Math.min(a.x, b.x);
const top = Math.min(a.y, b.y);
return { left, top, width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
}
不要把橡皮筋写成 viewBox 用户单位里的 rect 再指望线宽恒定——除非加上 vector-effect: non-scaling-stroke,否则一缩相机框线就成钢丝。更省事:HTML div position:fixed 用 client 定位。
2. 命中:用户空间 AABB
松手时把屏幕矩形两个对角经 根 svg 的 getScreenCTM 逆变换,得到用户空间盒,再和节点世界 AABB 相交:
export interface Aabb {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
export function marqueeToUser(
svg: SVGSVGElement,
rect: ScreenRect,
): Aabb {
const p1 = new DOMPoint(rect.left, rect.top);
const p2 = new DOMPoint(rect.left + rect.width, rect.top + rect.height);
const ctm = svg.getScreenCTM();
if (!ctm) {
throw new Error("svg is not in the rendering tree");
}
const inv = ctm.inverse();
const a = p1.matrixTransform(inv);
const b = p2.matrixTransform(inv);
return {
minX: Math.min(a.x, b.x),
minY: Math.min(a.y, b.y),
maxX: Math.max(a.x, b.x),
maxY: Math.max(a.y, b.y),
};
}
节点盒:getBBox({ fill: true, stroke: true, markers: false, clipped: false }) 再把四角乘节点 getCTM() 变到根用户空间(不要用 getScreenCTM 去比已经是用户空间的 marquee)。旋转节点的 AABB 偏大,精确选集再 isPointInFill。
Shift 并选、Alt 减选写进产品规则。命中顺序:手柄 > 节点 > 空处框选。
3. 手柄:屏幕尺寸 + non-scaling-stroke
手柄画在装饰层。视觉线宽用 vector-effect: non-scaling-stroke(生产只用这个值)。热区按 屏幕像素 定,再反推用户单位边长:
export function handleSizeUser(
svg: SVGSVGElement,
cssPx: number,
): number {
const a = new DOMPoint(0, 0);
const b = new DOMPoint(cssPx, 0);
const ctm = svg.getScreenCTM();
if (!ctm) return cssPx;
const inv = ctm.inverse();
const pa = a.matrixTransform(inv);
const pb = b.matrixTransform(inv);
return Math.hypot(pb.x - pa.x, pb.y - pa.y);
}
缩小时手柄仍约 8–12 CSS 像素,否则点不中。不要让手柄几何跟节点一起写进导出文件。
多选变换绕公共中心改每个节点的 transform 矩阵,不要改 d 顶点。父级有翻转时,世界 delta 要进父空间,与 Canvas 体系同一陷阱。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 放大后框选斜了 | 橡皮筋用了用户单位 rect,meet 留白没算 |
| 旋转节点漏选 | 用了局部 getBBox 直接比屏幕矩形 |
| 缩小时手柄点不中 | 手柄尺寸跟了用户单位 |
| 框线变成巨粗 | 没 non-scaling-stroke,线宽在用户空间 |
| 子节点拖动方向反 | 父级 scale/flip,delta 没进父空间 |
权威资料
核对日期:2026-08-26