From 7b15b760547c2049247f68d6fd4cd628fc362bb1 Mon Sep 17 00:00:00 2001 From: sangwook yoo Date: Thu, 11 Jun 2026 10:06:33 +0900 Subject: [PATCH] =?UTF-8?q?fix(pdf-import):=20=EC=A2=8C=ED=91=9C=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C=20=EC=8A=A4=EC=BC=80=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=E2=80=94=20=EC=B9=98=EC=88=98=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?=20=EC=8B=A4=EC=B8=A1=20=EC=A0=95=ED=95=A9(R1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [작업내용] : - fitAndCenter 가 큰 도면(9.6m 초과)에 scale<1 을 좌표에 곱해 'QLine 길이×10=표시 mm' 불변식을 깨뜨리던 문제 해소 — 좌표는 실측 그대로 평행이동만 하는 centerOnCanvas 로 대체 - 화면맞춤은 fitViewport 로 분리: 캔버스 중앙 고정 viewportTransform 줌 + canvasZoomState 동기화 (useCanvasEvent 의 setZoom 은 동일 zoom 값에서 translation 을 보존) Co-Authored-By: Claude Fable 5 --- src/hooks/pdf-import/usePdfImport.js | 54 ++++++++++++++++++---------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/hooks/pdf-import/usePdfImport.js b/src/hooks/pdf-import/usePdfImport.js index e9ecb484..57dab43e 100644 --- a/src/hooks/pdf-import/usePdfImport.js +++ b/src/hooks/pdf-import/usePdfImport.js @@ -1,6 +1,6 @@ import { useSetRecoilState, useRecoilValue } from 'recoil' import { fabric } from 'fabric' -import { canvasState } from '@/store/canvasAtom' +import { canvasState, canvasZoomState } from '@/store/canvasAtom' import { outerLineFixState, outerLinePointsState } from '@/store/outerLineAtom' import { outlineDisplaySelector } from '@/store/settingAtom' import { POLYGON_TYPE } from '@/common/common' @@ -12,9 +12,9 @@ import { snapNearAxisEdges, cleanSelfIntersectingPolygon } from '@/util/qpolygon import * as turf from '@turf/turf' const MM_PER_CANVAS_UNIT = 10 -const MAX_DIM = 1500 // useLine.addLine 이 length < 1(=10mm) 라인을 생성하지 않으므로 동일 임계값으로 사전 제거한다. const MIN_CANVAS_UNIT_LENGTH = 1 +const VIEWPORT_FIT_RATIO = 0.6 // 화면맞춤 시 캔버스 대비 도면 최대 비율 const SIMPLIFY_TOLERANCE = 0.5 // canvas unit ≈ 5mm. 직선 위 잉여 정점/미세 엣지 제거(보수적) const SNAP_ANGLE_TOL_DEG = 2 // 거의 수평/수직 엣지를 정확히 축정렬로 스냅 @@ -50,31 +50,22 @@ const dedupShortEdges = (points) => { 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 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 width = Math.max(...xs) - minX + const height = Math.max(...ys) - minY 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, + x: cx + (p.x - (minX + width / 2)), + y: cy + (p.y - (minY + height / 2)), })) } @@ -106,6 +97,7 @@ export function usePdfImport() { const canvas = useRecoilValue(canvasState) const setOuterLinePoints = useSetRecoilState(outerLinePointsState) const setOuterLineFix = useSetRecoilState(outerLineFixState) + const setCanvasZoom = useSetRecoilState(canvasZoomState) const isOutlineDisplay = useRecoilValue(outlineDisplaySelector) const { addLine } = useLine() 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 = {}) => { if (!canvas) { logger.warn('[usePdfImport] canvas 가 준비되지 않았습니다.') @@ -202,7 +217,7 @@ export function usePdfImport() { return false } - canvasPoints = fitAndCenter(canvasPoints, canvas) + canvasPoints = centerOnCanvas(canvasPoints, canvas) canvasPoints = ensureCounterClockwise(canvasPoints) const proceed = async () => { @@ -218,6 +233,7 @@ export function usePdfImport() { canvas.outerLineFix = true setOuterLineFix(true) + fitViewport(closedPoints) canvas.renderAll() }