Files
Aislo/B07_DesignDetail/openwebcad/src/integration/guide-area.ts
T

189 lines
7.2 KiB
TypeScript

import { Point } from '@flatten-js/core';
import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController.ts';
import { getCanvas, getScreenCanvasDrawController } from '../state.ts';
/**
* 작도 영역 가이드(169-1 · 169-3) — 도면 그림이 실제로 들어가는 사각형을 점선으로 보인다.
*
* 도면 개체가 아니라 그리기 겹판(기준점 십자와 같은 자리)이라 저장 · DXF · 인쇄에 안 섞인다.
* `editable` 이면 모서리 · 변을 끌어 영역을 고치고, 고친 값은 부모(숫자 칸)에 알린다.
* 부모 짝 = `ui_template/cad_host/cad_host.ts` `guide` 곁값 · `setGuideArea` · `onGuideChange` ·
* `setGuideVisible`(180-3 — 점선 영역 + 붉은 넘침 틀 보이기 / 숨기기 · 도각 칸선은 도면 개체라 그대로).
*/
/** [왼쪽 x, 아래 y, 오른쪽 x, 위 y] — 도면 좌표 mm */
export type GuideArea = [number, number, number, number];
export interface Guide {
area: GuideArea;
/** 넘친 칸(169-2) — 붉은 사각형으로 짚음 */
marks?: GuideArea[];
/** 끌어서 고칠 수 있나 — 도각 편집 · M02 양식만 */
editable?: boolean;
}
/** 끌 손잡이 — 모서리 넷 · 변 넷 (n = 위 · s = 아래 · e = 오른쪽 · w = 왼쪽) */
export type GuideHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
const GUIDE_COLOR = '#29b6f6';
const MARK_COLOR = '#ff5252';
/** 손잡이를 집었다고 볼 화면 거리(px) — 칸 모서리 끌기(`box-resize-drag`)와 같음 */
const GRAB_PIXELS = 9;
/** 영역 최소 폭 · 높이(mm) — 뒤집히거나 0 이 되지 않게 */
export const GUIDE_MIN_MM = 10;
let guide: Guide | null = null;
/** 안내선(점선 영역 + 붉은 틀) 보임 — 싣기 때 안 지움 · 숨김이면 그리지도 · 집지도 않음 */
let guideVisible = true;
const isArea = (value: unknown): value is GuideArea =>
Array.isArray(value) &&
value.length === 4 &&
value.every((n) => typeof n === 'number' && Number.isFinite(n)) &&
value[0] < value[2] &&
value[1] < value[3];
export const getGuide = (): Guide | null => guide;
export const isGuideVisible = (): boolean => guideVisible;
/** 부모 단추(180-3) — `true` 만 보임 · 그 밖 값은 보임으로 */
export function setGuideVisible(visible: unknown): void {
guideVisible = visible !== false;
}
/** 싣기 때 — 어긋난 값이면 가이드 없음 */
export function setGuide(next: unknown): void {
const candidate = next as Partial<Guide> | null | undefined;
guide =
candidate && isArea(candidate.area)
? {
area: [...candidate.area] as GuideArea,
marks: (candidate.marks ?? []).filter(isArea),
editable: candidate.editable === true,
}
: null;
}
/** 부모 숫자 칸이 바꾼 영역 — 가이드가 있을 때만 */
export function setGuideArea(area: unknown): void {
if (guide && isArea(area)) guide = { ...guide, area: [...area] as GuideArea };
}
/** 점에서 가장 가까운 손잡이 — 모서리가 변보다 먼저 · 거리 `tolerance` 밖이면 null */
export function pickGuideHandle(
area: GuideArea,
point: { x: number; y: number },
tolerance: number
): GuideHandle | null {
const [x0, y0, x1, y1] = area;
const nearX = (x: number) => Math.abs(point.x - x) <= tolerance;
const nearY = (y: number) => Math.abs(point.y - y) <= tolerance;
const inX = point.x >= x0 - tolerance && point.x <= x1 + tolerance;
const inY = point.y >= y0 - tolerance && point.y <= y1 + tolerance;
const ns = nearY(y1) && inX ? 'n' : nearY(y0) && inX ? 's' : '';
const ew = nearX(x1) && inY ? 'e' : nearX(x0) && inY ? 'w' : '';
return ((ns + ew) as GuideHandle) || null;
}
/** 손잡이를 점까지 끈 영역 — 반대편은 그대로 · 최소 크기 지킴 · 0.1 mm 반올림 */
export function dragGuideArea(
area: GuideArea,
handle: GuideHandle,
point: { x: number; y: number }
): GuideArea {
const round = (n: number) => Math.round(n * 10) / 10;
let [x0, y0, x1, y1] = area;
if (handle.includes('w')) x0 = Math.min(round(point.x), x1 - GUIDE_MIN_MM);
if (handle.includes('e')) x1 = Math.max(round(point.x), x0 + GUIDE_MIN_MM);
if (handle.includes('s')) y0 = Math.min(round(point.y), y1 - GUIDE_MIN_MM);
if (handle.includes('n')) y1 = Math.max(round(point.y), y0 + GUIDE_MIN_MM);
return [x0, y0, x1, y1];
}
function strokeRect(drawController: ScreenCanvasDrawController, area: GuideArea): void {
const [x0, y0, x1, y1] = area;
const corners = [new Point(x0, y0), new Point(x1, y0), new Point(x1, y1), new Point(x0, y1)].map(
(point) => drawController.worldToTarget(point)
);
corners.forEach((corner, index) =>
drawController.drawLineScreen(corner, corners[(index + 1) % corners.length])
);
}
/** 그리기 겹판 — 엔티티 장면 위에 매 장 그림(`helpers/draw.ts`) */
export function drawGuide(drawController: ScreenCanvasDrawController): void {
if (!guide || !guideVisible) return;
drawController.setOpacity(0.9);
drawController.setLineStyles(false, false, GUIDE_COLOR, 1, [8, 5]);
strokeRect(drawController, guide.area);
if (guide.editable) {
const [x0, y0, x1, y1] = guide.area;
const xs = [x0, (x0 + x1) / 2, x1];
const ys = [y0, (y0 + y1) / 2, y1];
for (const x of xs) {
for (const y of ys) {
if (x === xs[1] && y === ys[1]) continue;
const at = drawController.worldToTarget(new Point(x, y));
drawController.fillRectScreen(at.x - 3, at.y - 3, 6, 6, GUIDE_COLOR, true);
}
}
}
drawController.setLineStyles(false, false, MARK_COLOR, 2, []);
for (const mark of guide.marks ?? []) strokeRect(drawController, mark);
drawController.setOpacity(1);
}
/** 마우스 위치(화면) → 도면 좌표. 캐드는 화면 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);
}
let dragging: GuideHandle | null = null;
/** 끌기 — 손잡이를 집었을 때만 가로챔(그 밖은 그리기 도구로 흘림) · 움직일 때마다 `notify` */
export function registerGuideDrag(notify: (area: GuideArea) => void): void {
const canvas = getCanvas();
if (!canvas) return;
window.addEventListener(
'mousedown',
(event: MouseEvent) => {
if (event.button !== 0 || event.target !== canvas || !guide?.editable || !guideVisible) return;
const world = worldAt(event);
if (!world) return;
const tolerance = GRAB_PIXELS / getScreenCanvasDrawController().getScreenScale();
dragging = pickGuideHandle(guide.area, world, tolerance);
if (!dragging) return;
event.stopImmediatePropagation();
event.preventDefault();
},
true
);
window.addEventListener(
'mousemove',
(event: MouseEvent) => {
if (!dragging || !guide) return;
const world = worldAt(event);
if (!world) return;
guide = { ...guide, area: dragGuideArea(guide.area, dragging, world) };
notify(guide.area);
event.stopImmediatePropagation();
},
true
);
window.addEventListener(
'mouseup',
(event: MouseEvent) => {
if (!dragging) return;
dragging = null;
event.stopImmediatePropagation();
event.preventDefault();
},
true
);
}