resize handles
resize는 move보다 한 단계 복잡합니다. 포인터를 움직이는 동안 크기가 바뀌고, 어느 handle을 잡았는지에 따라 고정점이 달라집니다.
local 좌표에서 resize하는 코드
오른쪽 아래 handle을 잡은 경우를 먼저 구현해보면 구조가 보입니다. pointer를 node local 좌표로 되돌린 뒤 width/height를 다시 계산합니다.
function resizeFromSouthEast(node, worldPointer, options = {}) {
const local = transformPoint(invert3(node.worldMatrix), worldPointer);
const minSize = options.minSize ?? 8;
return {
...node,
width: Math.max(minSize, local.x),
height: Math.max(minSize, local.y)
};
}
function resizeFromNorthWest(node, worldPointer, options = {}) {
const local = transformPoint(invert3(node.worldMatrix), worldPointer);
const minSize = options.minSize ?? 8;
const nextWidth = Math.max(minSize, node.width - local.x);
const nextHeight = Math.max(minSize, node.height - local.y);
return {
...node,
x: node.x + local.x,
y: node.y + local.y,
width: nextWidth,
height: nextHeight
};
}
모든 handle은 “어느 corner가 고정이고 어느 corner가 움직이는가”만 다릅니다. raw resize를 만든 뒤 aspect ratio, snap, min size 정책을 적용합니다.
handle 이름으로 일반화한다
실제 tool에서는 nw, ne, sw, se 같은 handle 이름을 받아 같은 함수에서 처리하는 편이 좋습니다.
const HANDLE_ANCHOR = {
nw: { fixed: "se", moveX: "left", moveY: "top" },
ne: { fixed: "sw", moveX: "right", moveY: "top" },
sw: { fixed: "ne", moveX: "left", moveY: "bottom" },
se: { fixed: "nw", moveX: "right", moveY: "bottom" }
};
function resizeFromHandle(node, handle, worldPointer, options = {}) {
const local = transformPoint(invert3(node.worldMatrix), worldPointer);
const minSize = options.minSize ?? 8;
const policy = HANDLE_ANCHOR[handle];
let x = node.x;
let y = node.y;
let width = node.width;
let height = node.height;
if (policy.moveX === "right") width = Math.max(minSize, local.x);
if (policy.moveY === "bottom") height = Math.max(minSize, local.y);
if (policy.moveX === "left") {
width = Math.max(minSize, node.width - local.x);
x = node.x + local.x;
}
if (policy.moveY === "top") {
height = Math.max(minSize, node.height - local.y);
y = node.y + local.y;
}
return { ...node, x, y, width, height };
}
이 버전은 축 정렬 rectangle에 맞춘 시작점입니다. 회전된 node에서 왼쪽 위를 옮길 때는 local delta를 다시 parent 좌표로 변환하는 단계가 추가됩니다.
반대편 corner를 고정한다
오른쪽 아래 handle을 잡고 드래그한다면 왼쪽 위 corner는 고정점이 됩니다.
fixed corner: top-left
moving corner: bottom-right
새 pointer 위치를 node local 좌표로 변환하고, 고정점과의 차이로 새 width/height를 계산합니다.
local 좌표에서 계산하면 단순하다
회전된 도형을 world 좌표에서 resize하려고 하면 복잡합니다. pointer를 node local 좌표로 되돌리면 width/height 계산이 쉬워집니다.
localPointer = inverse(nodeWorldMatrix) * worldPointer
그다음 handle 종류에 따라 x, y, width, height를 갱신합니다.
aspect ratio와 min size는 정책이다
Shift를 누르면 비율 고정, 너무 작아지면 최소 크기 유지 같은 규칙은 tool policy입니다. 수학 모델 위에 UX 정책을 얹는 식입니다.
raw resize
-> apply constraints
-> update model
오늘의 핵심
resize는 world pointer를 local 좌표로 되돌린 뒤, 고정점과 이동점의 관계로 새 bounds를 만드는 일입니다.
world pointer
-> local pointer
-> fixed corner
-> new size