fix(pdf-import): 좌표 축소 스케일 제거 — 치수 라벨 실측 정합(R1)

[작업내용] :
- fitAndCenter 가 큰 도면(9.6m 초과)에 scale<1 을 좌표에 곱해 'QLine 길이×10=표시 mm' 불변식을
  깨뜨리던 문제 해소 — 좌표는 실측 그대로 평행이동만 하는 centerOnCanvas 로 대체
- 화면맞춤은 fitViewport 로 분리: 캔버스 중앙 고정 viewportTransform 줌 + canvasZoomState 동기화
  (useCanvasEvent 의 setZoom 은 동일 zoom 값에서 translation 을 보존)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
sangwook yoo 2026-06-11 10:06:33 +09:00
parent 29fd4cbd59
commit 7b15b76054

View File

@ -1,6 +1,6 @@
import { useSetRecoilState, useRecoilValue } from 'recoil' import { useSetRecoilState, useRecoilValue } from 'recoil'
import { fabric } from 'fabric' import { fabric } from 'fabric'
import { canvasState } from '@/store/canvasAtom' import { canvasState, canvasZoomState } from '@/store/canvasAtom'
import { outerLineFixState, outerLinePointsState } from '@/store/outerLineAtom' import { outerLineFixState, outerLinePointsState } from '@/store/outerLineAtom'
import { outlineDisplaySelector } from '@/store/settingAtom' import { outlineDisplaySelector } from '@/store/settingAtom'
import { POLYGON_TYPE } from '@/common/common' import { POLYGON_TYPE } from '@/common/common'
@ -12,9 +12,9 @@ import { snapNearAxisEdges, cleanSelfIntersectingPolygon } from '@/util/qpolygon
import * as turf from '@turf/turf' import * as turf from '@turf/turf'
const MM_PER_CANVAS_UNIT = 10 const MM_PER_CANVAS_UNIT = 10
const MAX_DIM = 1500
// useLine.addLine 이 length < 1(=10mm) 라인을 생성하지 않으므로 동일 임계값으로 사전 제거한다. // useLine.addLine 이 length < 1(=10mm) 라인을 생성하지 않으므로 동일 임계값으로 사전 제거한다.
const MIN_CANVAS_UNIT_LENGTH = 1 const MIN_CANVAS_UNIT_LENGTH = 1
const VIEWPORT_FIT_RATIO = 0.6 // 화면맞춤 시 캔버스 대비 도면 최대 비율
const SIMPLIFY_TOLERANCE = 0.5 // canvas unit ≈ 5mm. 직선 위 잉여 정점/미세 엣지 제거(보수적) const SIMPLIFY_TOLERANCE = 0.5 // canvas unit ≈ 5mm. 직선 위 잉여 정점/미세 엣지 제거(보수적)
const SNAP_ANGLE_TOL_DEG = 2 // 거의 수평/수직 엣지를 정확히 축정렬로 스냅 const SNAP_ANGLE_TOL_DEG = 2 // 거의 수평/수직 엣지를 정확히 축정렬로 스냅
@ -50,31 +50,22 @@ const dedupShortEdges = (points) => {
return out return out
} }
const fitAndCenter = (points, canvas) => { // 좌표에 축소 scale 을 곱하면 'QLine 길이×10 = 표시 mm' 불변식이 깨져 치수 라벨이 실측과 어긋난다.
// 좌표는 실측 그대로 평행이동만 하고, 화면맞춤은 fitViewport 의 zoom(viewportTransform)으로 분리 처리한다.
const centerOnCanvas = (points, canvas) => {
const xs = points.map((p) => p.x) const xs = points.map((p) => p.x)
const ys = points.map((p) => p.y) const ys = points.map((p) => p.y)
const minX = Math.min(...xs) const minX = Math.min(...xs)
const minY = Math.min(...ys) const minY = Math.min(...ys)
const maxX = Math.max(...xs) const width = Math.max(...xs) - minX
const maxY = Math.max(...ys) const height = Math.max(...ys) - minY
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 cx = (canvas?.getWidth?.() ?? 1000) / 2
const cy = (canvas?.getHeight?.() ?? 1000) / 2 const cy = (canvas?.getHeight?.() ?? 1000) / 2
return points.map((p) => ({ return points.map((p) => ({
x: cx + (p.x - (minX + width / 2)) * scale, x: cx + (p.x - (minX + width / 2)),
y: cy + (p.y - (minY + height / 2)) * scale, y: cy + (p.y - (minY + height / 2)),
})) }))
} }
@ -106,6 +97,7 @@ export function usePdfImport() {
const canvas = useRecoilValue(canvasState) const canvas = useRecoilValue(canvasState)
const setOuterLinePoints = useSetRecoilState(outerLinePointsState) const setOuterLinePoints = useSetRecoilState(outerLinePointsState)
const setOuterLineFix = useSetRecoilState(outerLineFixState) const setOuterLineFix = useSetRecoilState(outerLineFixState)
const setCanvasZoom = useSetRecoilState(canvasZoomState)
const isOutlineDisplay = useRecoilValue(outlineDisplaySelector) const isOutlineDisplay = useRecoilValue(outlineDisplaySelector)
const { addLine } = useLine() const { addLine } = useLine()
const { swalFire } = useSwal() const { swalFire } = useSwal()
@ -168,6 +160,29 @@ export function usePdfImport() {
}) })
} }
// 도면이 화면보다 크면 캔버스 중앙 고정 줌으로 화면맞춤한다. canvasZoomState 를 함께 동기화하면
// useCanvasEvent 의 setZoom(동일 zoom 값) 은 translation 을 보존하므로 중앙 정렬이 유지된다.
const fitViewport = (points) => {
const xs = points.map((p) => p.x)
const ys = points.map((p) => p.y)
const width = Math.max(...xs) - Math.min(...xs)
const height = Math.max(...ys) - Math.min(...ys)
const targetWidth = canvas.getWidth() * VIEWPORT_FIT_RATIO
const targetHeight = canvas.getHeight() * VIEWPORT_FIT_RATIO
let zoomPercent = 100
if (width > targetWidth || height > targetHeight) {
zoomPercent = Math.max(1, Math.floor(Math.min(targetWidth / width, targetHeight / height) * 100))
}
const zoom = zoomPercent / 100
const cx = canvas.getWidth() / 2
const cy = canvas.getHeight() / 2
canvas.setViewportTransform([zoom, 0, 0, zoom, cx * (1 - zoom), cy * (1 - zoom)])
setCanvasZoom(zoomPercent)
}
const applyOuterline = async (mmPoints, options = {}) => { const applyOuterline = async (mmPoints, options = {}) => {
if (!canvas) { if (!canvas) {
logger.warn('[usePdfImport] canvas 가 준비되지 않았습니다.') logger.warn('[usePdfImport] canvas 가 준비되지 않았습니다.')
@ -202,7 +217,7 @@ export function usePdfImport() {
return false return false
} }
canvasPoints = fitAndCenter(canvasPoints, canvas) canvasPoints = centerOnCanvas(canvasPoints, canvas)
canvasPoints = ensureCounterClockwise(canvasPoints) canvasPoints = ensureCounterClockwise(canvasPoints)
const proceed = async () => { const proceed = async () => {
@ -218,6 +233,7 @@ export function usePdfImport() {
canvas.outerLineFix = true canvas.outerLineFix = true
setOuterLineFix(true) setOuterLineFix(true)
fitViewport(closedPoints)
canvas.renderAll() canvas.renderAll()
} }