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[]; /** 횡단 장 칸(526-1) — 단면마다 점선 사각형(인쇄 칸선 대신) */ cells?: 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 | null | undefined; guide = candidate && isArea(candidate.area) ? { area: [...candidate.area] as GuideArea, marks: (candidate.marks ?? []).filter(isArea), cells: (candidate.cells ?? []).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, GUIDE_COLOR, 1, [4, 4]); for (const cell of guide.cells ?? []) strokeRect(drawController, cell); 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 ); }