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,10 +7,10 @@
*/
import type { Point } from '@flatten-js/core';
import { Actor, assign, createMachine, sendTo } from 'xstate';
import { HIGHLIGHT_ENTITY_DISTANCE } from '../../App.consts';
import type { Entity } from '../../entities/Entity';
import { findClosestEntity } from '../../helpers/find-closest-entity';
import { getPointFromEvent } from '../../helpers/get-point-from-event';
import { pickRadius } from '../../helpers/pick-radius';
import { queryEntitiesNearPoint } from '../../helpers/spatial-index';
import {
getScreenCanvasDrawController,
@@ -146,8 +146,7 @@ function cursorPoint(event: StateEvent | undefined): Point {
/** 클릭 지점에서 가장 가까운 엔티티 (선택 반경 안에 있을 때만) */
function pickEntityAt(worldPoint: Point): Entity | null {
const scale = getScreenCanvasDrawController().getScreenScale() || 1;
const radius = HIGHLIGHT_ENTITY_DISTANCE / scale;
const radius = pickRadius();
const candidates = queryEntitiesNearPoint(worldPoint.x, worldPoint.y, radius);
const { distance, entity } = findClosestEntity(worldPoint, candidates);
return entity && distance <= radius ? entity : null;
@@ -175,11 +174,13 @@ export function createSequenceTool(config: SequenceToolConfig) {
setGhostHelperEntities(ghosts);
};
const pushPoint = assign(({ context, event }: { context: SequenceContext; event: StateEvent }) => {
const point = getPointFromEvent(lastPoint(context.values), event as PointInputEvent);
setAngleGuideOriginPoint(point);
return { values: [...context.values, point], picks: [...context.picks, point] };
});
const pushPoint = assign(
({ context, event }: { context: SequenceContext; event: StateEvent }) => {
const point = getPointFromEvent(lastPoint(context.values), event as PointInputEvent);
setAngleGuideOriginPoint(point);
return { values: [...context.values, point], picks: [...context.picks, point] };
}
);
const pushNumberFromEvent = assign(
({ context, event }: { context: SequenceContext; event: StateEvent }) => {
@@ -197,10 +198,12 @@ export function createSequenceTool(config: SequenceToolConfig) {
picks: [...context.picks, null],
}));
const pushText = assign(({ context, event }: { context: SequenceContext; event: StateEvent }) => ({
values: [...context.values, (event as TextInputEvent).value],
picks: [...context.picks, null],
}));
const pushText = assign(
({ context, event }: { context: SequenceContext; event: StateEvent }) => ({
values: [...context.values, (event as TextInputEvent).value],
picks: [...context.picks, null],
})
);
const pushEntity = assign(
({ context, event }: { context: SequenceContext; event: StateEvent }) => {
@@ -3,7 +3,6 @@ import { compact } from 'es-toolkit';
import { toast } from 'react-toastify';
import {
EPSILON,
HIGHLIGHT_ENTITY_DISTANCE,
SELECTION_RECTANGLE_COLOR_CONTAINS,
SELECTION_RECTANGLE_COLOR_INTERSECTION,
SELECTION_RECTANGLE_STYLE,
@@ -15,6 +14,7 @@ import { pointDistance } from '../helpers/distance-between-points';
import { expandSelectionWithGroups } from '../helpers/entity-groups';
import { findEntitiesWithinDistance } from '../helpers/find-closest-entity';
import { findGripAt, removePolylineVertex } from '../helpers/grips';
import { pickRadius } from '../helpers/pick-radius';
import {
getActiveLayerId,
getEntities,
@@ -35,17 +35,13 @@ let lastPickPoint: Point | null = null;
let pickCycleIndex = 0;
function pickEntityAt(worldPoint: Point): Entity | null {
const candidates = findEntitiesWithinDistance(
worldPoint,
getEntities(),
HIGHLIGHT_ENTITY_DISTANCE
);
const radius = pickRadius();
const candidates = findEntitiesWithinDistance(worldPoint, getEntities(), radius);
if (!candidates.length) {
lastPickPoint = null;
return null;
}
const samePlace =
!!lastPickPoint && pointDistance(lastPickPoint, worldPoint) < HIGHLIGHT_ENTITY_DISTANCE;
const samePlace = !!lastPickPoint && pointDistance(lastPickPoint, worldPoint) < radius;
pickCycleIndex = samePlace ? (pickCycleIndex + 1) % candidates.length : 0;
lastPickPoint = worldPoint;
return candidates[pickCycleIndex];
@@ -56,11 +52,7 @@ export function handleFirstSelectionPoint(
event: MouseClickEvent
): SelectContext {
// 선택된 객체의 그립이 먼저다 — 집으면 그립 편집으로 넘어간다
const gripHit = findGripAt(
getSelectedEntities(),
event.worldMouseLocation,
HIGHLIGHT_ENTITY_DISTANCE
);
const gripHit = findGripAt(getSelectedEntities(), event.worldMouseLocation, pickRadius());
if (gripHit) {
if (event.holdingCtrl) {
// Ctrl+클릭 => 폴리선 정점 제거 (다기능 그립)