fix(B07): 집기 반경을 화면 기준으로 잡고, 누른 채 끄는 선택을 살린다

- 집기 반경이 월드 거리였다. HIGHLIGHT_ENTITY_DISTANCE를 월드 거리와 그대로 비교해
  확대할수록 화면상 반경이 같이 커졌다(1369%에서 15월드 ≈ 205px). helpers/pick-radius로
  확대율을 나눠 쓰고 상수를 화면 픽셀 10으로 낮췄다. 하이라이트·선택·그립·시퀀스 도구가
  같은 반경을 본다.
- 누른 채 끄는 선택이 없었다. 왼쪽 버튼은 mouseup에서만 클릭을 보내 누르고 끌어도 점이
  하나만 찍혔고, 그래서 좌→우(창)·우→좌(교차) 구분이 먹히지 않는 것처럼 보였다.
  선택 도구일 때만 누를 때 첫 점을 보내고, 4px 넘게 끌고 놓으면 그 자리로 사각형을 닫는다.
  다른 도구는 예전처럼 놓을 때 한 점만 받는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 15:52:25 +09:00
co-authored by Claude Opus 5
parent 3a86e02747
commit 8c828992f7
6 changed files with 105 additions and 58 deletions
@@ -7,7 +7,6 @@ import {
CANVAS_INPUT_FIELD_MOUSE_OFFSET,
CANVAS_INPUT_FIELD_TEXT_COLOR,
CANVAS_INPUT_FIELD_WIDTH,
HIGHLIGHT_ENTITY_DISTANCE,
PINCH_ZOOM_EXPONENT,
SNAP_POINT_DISTANCE,
WHEEL_LINE_PX,
@@ -25,6 +24,7 @@ import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas
import { calculateAngleGuidesAndSnapPoints } from '../helpers/calculate-angle-guides-and-snap-points.ts';
import { findClosestEntity } from '../helpers/find-closest-entity.ts';
import { getClosestSnapPointWithinRadius } from '../helpers/get-closest-snap-point.ts';
import { pickRadius } from '../helpers/pick-radius.ts';
import {
getActiveToolActor,
getAngleStep,
@@ -50,6 +50,7 @@ import {
undo,
} from '../state.ts';
import { Tool } from '../tools.ts';
import { SelectState } from '../tools/select-tool.ts';
import {
type AbsolutePointInputEvent,
ActorEvent,
@@ -74,8 +75,13 @@ function polarToPoint(distance: number, degrees: number): Point {
return new Point(distance * Math.cos(radians), distance * Math.sin(radians));
}
/** 이만큼 넘게 끌면 클릭이 아니라 드래그로 본다 (화면 픽셀) */
const DRAG_SELECT_MIN_PX = 4;
export class InputController {
private text = '';
/** 왼쪽 버튼을 누른 화면 좌표 — 놓을 때 끌었는지 판단한다 */
private leftPressLocation: Point | null = null;
constructor() {
if (typeof process === 'object' && process?.env?.NODE_ENV === 'test') {
@@ -179,29 +185,58 @@ export class InputController {
setPanStartLocation(null);
}
if (evt.button === MouseButton.Left) {
const screenCanvasDrawController = getScreenCanvasDrawController();
const closestSnapPoint = getClosestSnapPointWithinRadius(
compact([getSnapPoint(), getSnapPointOnAngleGuide()]),
screenCanvasDrawController.getWorldMouseLocation(),
SNAP_POINT_DISTANCE / screenCanvasDrawController.getScreenScale()
);
const worldMouseLocationTemp = getScreenCanvasDrawController().targetToWorld(
this.getCanvasPoint(evt)
);
const worldMouseLocation = closestSnapPoint ? closestSnapPoint.point : worldMouseLocationTemp;
const activeToolActor = getActiveToolActor();
activeToolActor?.send({
type: ActorEvent.MOUSE_CLICK,
worldMouseLocation,
screenMouseLocation: screenCanvasDrawController.worldToTarget(worldMouseLocation),
holdingCtrl: evt.ctrlKey,
holdingShift: evt.shiftKey,
} as MouseClickEvent);
if (this.isSelectToolActive()) {
// 선택 도구는 누를 때 첫 점을 이미 보냈다. 끌었으면 놓는 자리로 사각형을 닫는다.
const pressLocation = this.leftPressLocation;
this.leftPressLocation = null;
const releaseLocation = this.getCanvasPoint(evt);
const dragged =
!!pressLocation &&
Math.hypot(releaseLocation.x - pressLocation.x, releaseLocation.y - pressLocation.y) >
DRAG_SELECT_MIN_PX;
if (dragged && this.isWaitingForSecondSelectPoint()) {
this.sendMouseClick(evt);
}
return;
}
this.sendMouseClick(evt);
}
}
/** 활성 도구가 선택 도구인가 */
private isSelectToolActive(): boolean {
return getActiveToolActor()?.getSnapshot()?.context?.type === Tool.SELECT;
}
/** 선택 도구가 선택 사각형의 두 번째 점을 기다리는 중인가 */
private isWaitingForSecondSelectPoint(): boolean {
return (
getActiveToolActor()?.getSnapshot()?.value === SelectState.WAITING_FOR_SECOND_SELECT_POINT
);
}
/** 스냅을 반영한 클릭 한 번을 활성 도구에 보낸다 */
private sendMouseClick(evt: MouseEvent) {
const screenCanvasDrawController = getScreenCanvasDrawController();
const closestSnapPoint = getClosestSnapPointWithinRadius(
compact([getSnapPoint(), getSnapPointOnAngleGuide()]),
screenCanvasDrawController.getWorldMouseLocation(),
SNAP_POINT_DISTANCE / screenCanvasDrawController.getScreenScale()
);
const worldMouseLocationTemp = screenCanvasDrawController.targetToWorld(
this.getCanvasPoint(evt)
);
const worldMouseLocation = closestSnapPoint ? closestSnapPoint.point : worldMouseLocationTemp;
getActiveToolActor()?.send({
type: ActorEvent.MOUSE_CLICK,
worldMouseLocation,
screenMouseLocation: screenCanvasDrawController.worldToTarget(worldMouseLocation),
holdingCtrl: evt.ctrlKey,
holdingShift: evt.shiftKey,
} as MouseClickEvent);
}
public handleMouseEnter() {
setShouldDrawCursor(true);
}
@@ -233,7 +268,7 @@ export class InputController {
screenCanvasDrawController.targetToWorld(newScreenMouseLocation),
getEntities()
);
if (closestEntityInfo.distance < HIGHLIGHT_ENTITY_DISTANCE) {
if (closestEntityInfo.distance < pickRadius()) {
setHighlightedEntityIds([closestEntityInfo.entity.id]);
} else {
setHighlightedEntityIds([]);
@@ -279,9 +314,17 @@ export class InputController {
}
public handleMouseDown(evt: MouseEvent) {
if (evt.button !== MouseButton.Middle) return;
setPanStartLocation(this.getCanvasPoint(evt));
if (evt.button === MouseButton.Middle) {
setPanStartLocation(this.getCanvasPoint(evt));
return;
}
if (evt.button !== MouseButton.Left) return;
if ((evt.target as HTMLElement | null)?.closest('.controls')) return;
// AutoCAD처럼 누른 자리에서 사각형이 시작되도록 선택 도구에만 첫 점을 미리 보낸다.
// 다른 도구는 예전처럼 놓을 때 한 점을 받는다 (끌다가 점이 두 개 찍히지 않게).
if (!this.isSelectToolActive()) return;
this.leftPressLocation = this.getCanvasPoint(evt);
this.sendMouseClick(evt);
}
private getCanvasPoint(evt: MouseEvent): Point {