marquee selection
빈 캔버스에서 드래그하면 selection rectangle이 생기고, 그 영역에 걸린 도형들이 선택됩니다. 이것을 marquee selection이라고 부르겠습니다.
marquee selection을 계산하는 코드
drag 시작점과 현재점을 screen 좌표로 저장하면 overlay rectangle과 선택 판정을 같은 기준으로 맞출 수 있습니다.
function rectFromPoints(a, b) {
const x = Math.min(a.x, b.x);
const y = Math.min(a.y, b.y);
const width = Math.abs(a.x - b.x);
const height = Math.abs(a.y - b.y);
return { x, y, width, height };
}
function intersects(a, b) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
);
}
function pickByMarquee(nodes, marqueeScreenRect, camera) {
return nodes.filter((node) => {
const screenBounds = worldBoundsToScreen(nodeWorldBounds(node), camera);
return intersects(marqueeScreenRect, screenBounds);
});
}
선택 정책을 intersects에서 contains로 바꾸면 “완전히 포함된 도형만 선택” 모드가 됩니다.
drag state와 action으로 연결한다
marquee도 move처럼 시작 상태와 현재 상태를 분리합니다. 문서 상태를 바로 바꾸기보다 selection action으로 확정합니다.
function beginMarquee(input) {
return {
type: "marquee",
startScreen: input.screen,
currentScreen: input.screen
};
}
function updateMarquee(drag, input) {
return {
...drag,
currentScreen: input.screen,
rect: rectFromPoints(drag.startScreen, input.screen)
};
}
function commitMarquee(snapshot, drag, mode = "intersect") {
const rect = drag.rect ?? rectFromPoints(drag.startScreen, drag.currentScreen);
const predicate = mode === "contain" ? contains : intersects;
const ids = snapshot.drawList
.filter((node) => predicate(rect, worldBoundsToScreen(nodeWorldBounds(node), snapshot.camera)))
.map((node) => node.id);
return { type: "setSelection", ids };
}
drag rectangle을 만든다
pointerdown의 시작 screen 좌표와 pointermove의 현재 screen 좌표로 rectangle을 만듭니다.
x = min(start.x, current.x)
y = min(start.y, current.y)
width = abs(current.x - start.x)
height = abs(current.y - start.y)
이 rectangle은 overlay에 screen 좌표로 그리면 됩니다.
node bounds와 비교한다
선택 판정은 두 가지 정책이 있습니다.
intersect: 조금이라도 걸치면 선택
contain: 완전히 포함되면 선택
Figma-like editor에서는 도구 모드나 modifier key에 따라 정책을 바꿀 수 있습니다.
screen에서 비교할지 world에서 비교할지 정한다
marquee rectangle은 screen에서 만들어집니다. node bounds를 screen으로 변환해서 비교하면 overlay와 판정이 잘 맞습니다.
node world bounds -> screen bounds
screen marquee vs screen node bounds
반대로 world 기준 선택이 필요하면 marquee rectangle도 world로 변환해야 합니다.
오늘의 핵심
marquee selection은 drag rectangle과 node bounds의 관계를 선택 정책으로 바꾸는 도구입니다.
drag screen rect
node screen bounds
intersect or contain