From 0418dd84ee342ee8c46681540c8552c7637d8369 Mon Sep 17 00:00:00 2001 From: sangwook yoo Date: Mon, 1 Jun 2026 09:12:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(pdf-import):=20=EC=99=B8=EA=B3=BD=EC=84=A0?= =?UTF-8?q?=20=EA=B8=B0=ED=95=98=20=ED=9B=84=EC=B2=98=EB=A6=AC(=EC=B6=95?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=8A=A4=EB=83=85=C2=B7=EB=8B=A8=EC=88=9C?= =?UTF-8?q?=ED=99=94=C2=B7=EC=9E=90=EA=B8=B0=EA=B5=90=EC=B0=A8=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - normalizeGeometry 단계 신설: snapNearAxisEdges(축정렬 스냅) → turf.simplify(과다 정점 단순화) → cleanSelfIntersectingPolygon(자기교차 복구) - applyOuterline의 중복점 제거 직후·fitAndCenter 전에 삽입(스냅→단순화 순서로 축정렬 보존) - 기존 검증 순수함수 재사용, 신규 의존성 없음 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/hooks/pdf-import/usePdfImport.js | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/hooks/pdf-import/usePdfImport.js b/src/hooks/pdf-import/usePdfImport.js index 01fa4fb2..5fc67f6f 100644 --- a/src/hooks/pdf-import/usePdfImport.js +++ b/src/hooks/pdf-import/usePdfImport.js @@ -7,10 +7,14 @@ import { usePolygon } from '@/hooks/usePolygon' import { useSwal } from '@/hooks/useSwal' import { useMessage } from '@/hooks/useMessage' import { logger } from '@/util/logger' +import { snapNearAxisEdges, cleanSelfIntersectingPolygon } from '@/util/qpolygon-utils' +import * as turf from '@turf/turf' const MM_PER_CANVAS_UNIT = 10 const MAX_DIM = 1500 const MIN_CANVAS_UNIT_LENGTH = 0.5 +const SIMPLIFY_TOLERANCE = 0.5 // canvas unit ≈ 5mm. 직선 위 잉여 정점/미세 엣지 제거(보수적) +const SNAP_ANGLE_TOL_DEG = 2 // 거의 수평/수직 엣지를 정확히 축정렬로 스냅 const toCanvasUnits = (mmPoints) => mmPoints.map((p) => ({ x: p.x / MM_PER_CANVAS_UNIT, y: p.y / MM_PER_CANVAS_UNIT })) @@ -57,6 +61,30 @@ const fitAndCenter = (points, canvas) => { })) } +const simplifyRing = (points, tolerance) => { + if (points.length < 4) return points + try { + const coords = points.map((p) => [p.x, p.y]) + coords.push([points[0].x, points[0].y]) + const simplified = turf.simplify(turf.polygon([coords]), { tolerance, highQuality: true }) + const ring = simplified.geometry.coordinates[0] + const out = ring.slice(0, -1).map((c) => ({ x: c[0], y: c[1] })) + return out.length >= 3 ? out : points + } catch (e) { + logger.warn('[usePdfImport] simplifyRing 실패', e?.message || e) + return points + } +} + +// AI 추출 좌표의 결정적 정규화: 축정렬 스냅 → 과다정점 단순화 → 자기교차 복구. +// 순서 주의 — 스냅(정점 이동) 후 단순화(정점 제거)해야 축정렬이 보존된다. +const normalizeGeometry = (points) => { + let pts = snapNearAxisEdges(points, SNAP_ANGLE_TOL_DEG) + pts = simplifyRing(pts, SIMPLIFY_TOLERANCE) + pts = cleanSelfIntersectingPolygon(pts) + return pts.length >= 3 ? pts : points +} + export function usePdfImport() { const canvas = useRecoilValue(canvasState) const setOuterLinePoints = useSetRecoilState(outerLinePointsState) @@ -133,6 +161,7 @@ export function usePdfImport() { return false } + canvasPoints = normalizeGeometry(canvasPoints) canvasPoints = fitAndCenter(canvasPoints, canvas) canvasPoints = ensureCounterClockwise(canvasPoints)