import { Point } from '@flatten-js/core'; import { HtmlEvent } from '../App.types.ts'; import { ImageEntity } from '../entities/ImageEntity.ts'; import { TextEntity } from '../entities/TextEntity.ts'; import { getCanvas, getEntities, getScreenCanvasDrawController, getSelectedEntities, isDrawingReadOnly, setEntities, } from '../state.ts'; /** * 칸 모서리를 **끌어서** 크기를 바꾼다 (2026-09-06 사용자 지시). * * 대상은 「칸이 있는 글자(도각 자리표)」와 「그림」이다. 캐드 본래의 그립은 집었다 놓는 * 방식이라 도각 칸을 맞출 때 손이 많이 갔다 — 끌기는 여기서 따로 받는다. * * 그리기 도구와 부딪히지 않게 **모서리를 집었을 때만** 이벤트를 가로챈다(그 밖에는 그대로 * 흘려보낸다). 확정한 도면은 읽기 전용이라 손대지 않는다. */ /** 모서리를 집었다고 볼 화면 거리(px). 그립 크기(8px)보다 조금 넉넉하게 잡는다. */ const GRAB_PIXELS = 9; type Resizable = TextEntity | ImageEntity; function resizableSelection(): Resizable | null { const selected = getSelectedEntities(); if (selected.length !== 1) return null; const entity = selected[0]; if (entity instanceof TextEntity && entity.hasBox()) return entity; if (entity instanceof ImageEntity) return entity; return null; } /** 마우스 위치(화면) → 도면 좌표. 캐드는 화면 y 를 아래에서 위로 잰다. */ function worldAt(event: MouseEvent): Point | null { const canvas = getCanvas(); if (!canvas) return null; const bounds = canvas.getBoundingClientRect(); const screenPoint = new Point(event.clientX - bounds.left, bounds.bottom - event.clientY); return getScreenCanvasDrawController().targetToWorld(screenPoint); } function corners(entity: Resizable): Point[] { const box = entity.getBoundingBox(); return [ new Point(box.xmin, box.ymin), new Point(box.xmax, box.ymin), new Point(box.xmax, box.ymax), new Point(box.xmin, box.ymax), ]; } let dragging: { entity: Resizable; opposite: Point } | null = null; export function registerBoxResizeDrag(): void { const canvas = getCanvas(); if (!canvas) return; // 캡처 단계에서 먼저 받는다 — 모서리를 집은 경우에만 선택 도구로 넘어가지 않게 막는다. window.addEventListener( 'mousedown', (event: MouseEvent) => { if (event.button !== 0 || event.target !== canvas || isDrawingReadOnly()) return; const entity = resizableSelection(); const world = entity ? worldAt(event) : null; if (!entity || !world) return; const scale = getScreenCanvasDrawController().getScreenScale(); const grabDistance = GRAB_PIXELS / scale; const points = corners(entity); let index = -1; let best = grabDistance; points.forEach((corner, seq) => { const distance = corner.distanceTo(world)[0]; if (distance <= best) { best = distance; index = seq; } }); if (index < 0) return; dragging = { entity, opposite: points[(index + 2) % points.length] }; event.stopImmediatePropagation(); event.preventDefault(); }, true ); window.addEventListener( 'mousemove', (event: MouseEvent) => { if (!dragging) return; const world = worldAt(event); if (!world) return; dragging.entity.setBoxFromCorners(world, dragging.opposite); // 끄는 동안에는 되돌리기 스택에 쌓지 않는다 — 손을 뗄 때 한 번만 쌓는다. setEntities([...getEntities()], false); // 자리표 패널의 칸 크기 숫자도 따라 움직이게 알린다. window.dispatchEvent(new Event(HtmlEvent.UPDATE_STATE)); event.stopImmediatePropagation(); }, true ); window.addEventListener( 'mouseup', (event: MouseEvent) => { if (!dragging) return; dragging = null; setEntities([...getEntities()], true); window.dispatchEvent(new Event(HtmlEvent.UPDATE_STATE)); event.stopImmediatePropagation(); event.preventDefault(); }, true ); }