feat(pdf-import): 외곽선 기하 후처리(축정렬 스냅·단순화·자기교차 복구) 추가

- normalizeGeometry 단계 신설: snapNearAxisEdges(축정렬 스냅) → turf.simplify(과다 정점 단순화) → cleanSelfIntersectingPolygon(자기교차 복구)
- applyOuterline의 중복점 제거 직후·fitAndCenter 전에 삽입(스냅→단순화 순서로 축정렬 보존)
- 기존 검증 순수함수 재사용, 신규 의존성 없음

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sangwook yoo 2026-06-01 09:12:14 +09:00
parent afda525be9
commit 0418dd84ee

View File

@ -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)