Files
Aislo/B07_DesignDetail/openwebcad/src/integration/box-resize-drag.ts
T
eomsangdonandClaude Opus 5 d0d3f2a311 feat(B07): 자리표 마우스 크기조절·기본 도각 칸 정렬·도면명 미리보기
- 칸이 있는 글자와 그림에 모서리 그립 추가, 모서리를 끌어 칸 크기를 바꾸는 조작 신설 (다른 도면 요소에도 동일 적용)
- 기본 도각(00_template_A1)의 글자 24개에 표제란 칸을 계산해 부여 — 칸 기준 가로·세로 가운데 정렬
- 도각 편집 시 도면명·도면번호도 보던 도면 값으로 미리보기 제공
- 복제 시 미리보기 값 유지 (그립 편집 후 자리표가 토큰으로 되돌아가던 문제)
- CAD index.html 을 캐시하지 않도록 처리 — 빌드 후에도 옛 화면이 남던 문제 해소

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 15:52:36 +09:00

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
);
}