import { useSetRecoilState, useRecoilValue } from 'recoil' import { canvasState } from '@/store/canvasAtom' import { outerLinePointsState } from '@/store/outerLineAtom' import { POLYGON_TYPE } from '@/common/common' import { QLine } from '@/components/fabric/QLine' import { usePolygon } from '@/hooks/usePolygon' import { useSwal } from '@/hooks/useSwal' import { useMessage } from '@/hooks/useMessage' import { logger } from '@/util/logger' const MM_PER_CANVAS_UNIT = 10 const MAX_DIM = 1500 const MIN_CANVAS_UNIT_LENGTH = 0.5 const toCanvasUnits = (mmPoints) => mmPoints.map((p) => ({ x: p.x / MM_PER_CANVAS_UNIT, y: p.y / MM_PER_CANVAS_UNIT })) const polygonArea = (points) => { let area = 0 for (let i = 0; i < points.length; i++) { const cur = points[i] const next = points[(i + 1) % points.length] area += cur.x * next.y - next.x * cur.y } return area } const ensureCounterClockwise = (points) => { // canvas 좌표계 (y 가 아래로 증가) 에서 CCW 는 signed area 가 음수가 된다. return polygonArea(points) > 0 ? [...points].reverse() : points } const fitAndCenter = (points, canvas) => { const xs = points.map((p) => p.x) const ys = points.map((p) => p.y) const minX = Math.min(...xs) const minY = Math.min(...ys) const maxX = Math.max(...xs) const maxY = Math.max(...ys) const width = maxX - minX const height = maxY - minY const targetWidth = (canvas?.getWidth?.() ?? 1000) * 0.6 const targetHeight = (canvas?.getHeight?.() ?? 1000) * 0.6 const maxAllowed = Math.min(MAX_DIM, Math.max(targetWidth, targetHeight)) let scale = 1 if (width > maxAllowed || height > maxAllowed) { scale = Math.min(maxAllowed / width, maxAllowed / height) } const cx = (canvas?.getWidth?.() ?? 1000) / 2 const cy = (canvas?.getHeight?.() ?? 1000) / 2 return points.map((p) => ({ x: cx + (p.x - (minX + width / 2)) * scale, y: cy + (p.y - (minY + height / 2)) * scale, })) } export function usePdfImport() { const canvas = useRecoilValue(canvasState) const setOuterLinePoints = useSetRecoilState(outerLinePointsState) const { addPolygonByLines } = usePolygon() const { swalFire } = useSwal() const { getMessage } = useMessage() const hasExistingOuterLine = () => { if (!canvas) return false return canvas.getObjects().some((obj) => obj.name === 'outerLine' || obj.name === POLYGON_TYPE.WALL || obj.name === POLYGON_TYPE.ROOF) } const clearExistingCanvasState = () => { if (!canvas) return const removable = canvas.getObjects().filter((obj) => { return ( obj.name === 'outerLine' || obj.name === 'outerLinePoint' || obj.name === 'startPoint' || obj.name === 'helpGuideLine' || obj.name === 'lengthText' || obj.name === 'pitchText' || obj.name === POLYGON_TYPE.WALL || obj.name === POLYGON_TYPE.ROOF ) }) removable.forEach((obj) => canvas.remove(obj)) } const drawOuterLines = (points) => { points.forEach((point, idx) => { if (idx === 0) return const prev = points[idx - 1] const line = new QLine([prev.x, prev.y, point.x, point.y], { stroke: '#000000', strokeWidth: 3, idx, selectable: true, name: 'outerLine', }) canvas.add(line) }) } const applyOuterline = async (mmPoints, options = {}) => { if (!canvas) { logger.warn('[usePdfImport] canvas 가 준비되지 않았습니다.') return false } const { unit = 'mm' } = options if (unit !== 'mm') { logger.warn('[usePdfImport] 지원하지 않는 단위:', unit) swalFire({ text: getMessage('pdf.import.error.unsupported.unit'), type: 'alert' }) return false } if (!Array.isArray(mmPoints) || mmPoints.length < 3) { swalFire({ text: getMessage('pdf.import.error.too.few.vertices'), type: 'alert' }) return false } let canvasPoints = toCanvasUnits(mmPoints) canvasPoints = canvasPoints.filter((p, idx, arr) => { if (idx === 0) return true const prev = arr[idx - 1] const dx = p.x - prev.x const dy = p.y - prev.y return Math.hypot(dx, dy) >= MIN_CANVAS_UNIT_LENGTH }) if (canvasPoints.length < 3) { swalFire({ text: getMessage('pdf.import.error.too.few.vertices'), type: 'alert' }) return false } canvasPoints = fitAndCenter(canvasPoints, canvas) canvasPoints = ensureCounterClockwise(canvasPoints) const proceed = async () => { clearExistingCanvasState() const closedPoints = [...canvasPoints, canvasPoints[0]] setOuterLinePoints(closedPoints) drawOuterLines(closedPoints) try { const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') if (lines.length >= 3) { addPolygonByLines(lines, { name: POLYGON_TYPE.WALL, fill: 'transparent', stroke: 'black' }) } } catch (e) { logger.warn('[usePdfImport] wall polygon 생성 실패', e?.message || e) } canvas.renderAll() } if (hasExistingOuterLine()) { return await new Promise((resolve) => { swalFire({ text: getMessage('pdf.import.confirm.overwrite'), type: 'confirm', confirmFn: async () => { await proceed() resolve(true) }, denyFn: () => resolve(false), }) }) } await proceed() return true } return { applyOuterline, hasExistingOuterLine } }