- 칸이 있는 글자와 그림에 모서리 그립 추가, 모서리를 끌어 칸 크기를 바꾸는 조작 신설 (다른 도면 요소에도 동일 적용) - 기본 도각(00_template_A1)의 글자 24개에 표제란 칸을 계산해 부여 — 칸 기준 가로·세로 가운데 정렬 - 도각 편집 시 도면명·도면번호도 보던 도면 값으로 미리보기 제공 - 복제 시 미리보기 값 유지 (그립 편집 후 자리표가 토큰으로 되돌아가던 문제) - CAD index.html 을 캐시하지 않도록 처리 — 빌드 후에도 옛 화면이 남던 문제 해소 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
3.9 KiB
TypeScript
120 lines
3.9 KiB
TypeScript
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
|
|
);
|
|
}
|