fix(pdf-import): PDF 외곽선을 수동 확정 상태와 동일하게 생성 — 지붕형상 설정 차단 해소
[작업내용] : - useLine.addLine 경유로 outerLine 생성 — attributes/planeSize/fontSize 부여(지붕형상 설정·처마/케라바 편집·재로드 복원에 필수) - canvas.outerLineFix + outerLineFixState 설정 — 미설정 시 지붕형상 설정 진입이 wall.line.not.found 로 즉시 차단되던 문제 해소 - skeletonBuilder 가 소비하는 outerLinePoint 정점 circle 생성(수동 플로우와 동일) - 조기 WALL 폴리곤 생성 제거 — 수동 플로우와 동일하게 지붕형상 설정 적용 시점에 WALL/ROOF 생성 - 짧은엣지 dedup 을 '직전 유지 정점' 기준으로 수정하고 닫힘 엣지(Gemini 닫힌 링 반환 포함)까지 검사, 임계값을 addLine 의 1unit 과 일치 - PDF 적용 후 외벽선 그리기 메뉴(진입 시 재드로잉 강등) 대신 지붕형상 설정 메뉴로 직행 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ef55c819d5
commit
c7757fabda
@ -439,7 +439,12 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
|||||||
const hasRoofs = canvas.getObjects().some((obj) => obj.name === POLYGON_TYPE.ROOF)
|
const hasRoofs = canvas.getObjects().some((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||||||
|
|
||||||
// roofSizeSet에 따라 메뉴 설정
|
// roofSizeSet에 따라 메뉴 설정
|
||||||
if (currentRoof?.roofSizeSet === '2') {
|
if (inputMode === INPUT_MODE.PDF) {
|
||||||
|
// PDF 임포트는 외곽선이 이미 확정(outerLineFix) 상태 — 외벽선 그리기 메뉴로 보내면
|
||||||
|
// useOuterLineWall 이 라인을 재드로잉 모드로 강등시키므로 지붕형상 설정으로 직행한다.
|
||||||
|
setSelectedMenu('outline')
|
||||||
|
setCurrentMenu(MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS)
|
||||||
|
} else if (currentRoof?.roofSizeSet === '2') {
|
||||||
setSelectedMenu('surface')
|
setSelectedMenu('surface')
|
||||||
setCurrentMenu(MENU.BATCH_CANVAS.BATCH_DRAWING)
|
setCurrentMenu(MENU.BATCH_CANVAS.BATCH_DRAWING)
|
||||||
} else if (currentRoof?.roofSizeSet === '1') {
|
} else if (currentRoof?.roofSizeSet === '1') {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useSetRecoilState, useRecoilValue } from 'recoil'
|
import { useSetRecoilState, useRecoilValue } from 'recoil'
|
||||||
|
import { fabric } from 'fabric'
|
||||||
import { canvasState } from '@/store/canvasAtom'
|
import { canvasState } from '@/store/canvasAtom'
|
||||||
import { outerLinePointsState } from '@/store/outerLineAtom'
|
import { outerLineFixState, outerLinePointsState } from '@/store/outerLineAtom'
|
||||||
|
import { outlineDisplaySelector } from '@/store/settingAtom'
|
||||||
import { POLYGON_TYPE } from '@/common/common'
|
import { POLYGON_TYPE } from '@/common/common'
|
||||||
import { QLine } from '@/components/fabric/QLine'
|
import { useLine } from '@/hooks/useLine'
|
||||||
import { usePolygon } from '@/hooks/usePolygon'
|
|
||||||
import { useSwal } from '@/hooks/useSwal'
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
import { useMessage } from '@/hooks/useMessage'
|
import { useMessage } from '@/hooks/useMessage'
|
||||||
import { logger } from '@/util/logger'
|
import { logger } from '@/util/logger'
|
||||||
@ -12,7 +13,8 @@ import * as turf from '@turf/turf'
|
|||||||
|
|
||||||
const MM_PER_CANVAS_UNIT = 10
|
const MM_PER_CANVAS_UNIT = 10
|
||||||
const MAX_DIM = 1500
|
const MAX_DIM = 1500
|
||||||
const MIN_CANVAS_UNIT_LENGTH = 0.5
|
// useLine.addLine 이 length < 1(=10mm) 라인을 생성하지 않으므로 동일 임계값으로 사전 제거한다.
|
||||||
|
const MIN_CANVAS_UNIT_LENGTH = 1
|
||||||
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 // 거의 수평/수직 엣지를 정확히 축정렬로 스냅
|
||||||
|
|
||||||
@ -33,6 +35,21 @@ const ensureCounterClockwise = (points) => {
|
|||||||
return polygonArea(points) > 0 ? [...points].reverse() : points
|
return polygonArea(points) > 0 ? [...points].reverse() : points
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 직전 '유지된' 정점과의 거리로 짧은 엣지를 제거한다. 닫힘 엣지(마지막→첫)도 동일 기준으로 검사 —
|
||||||
|
// Gemini 가 닫힌 링(첫점=끝점 중복)을 반환하는 경우가 여기서 함께 정리된다.
|
||||||
|
const dedupShortEdges = (points) => {
|
||||||
|
const out = []
|
||||||
|
points.forEach((p) => {
|
||||||
|
const prev = out[out.length - 1]
|
||||||
|
if (prev && Math.hypot(p.x - prev.x, p.y - prev.y) < MIN_CANVAS_UNIT_LENGTH) return
|
||||||
|
out.push(p)
|
||||||
|
})
|
||||||
|
while (out.length >= 3 && Math.hypot(out[out.length - 1].x - out[0].x, out[out.length - 1].y - out[0].y) < MIN_CANVAS_UNIT_LENGTH) {
|
||||||
|
out.pop()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
const fitAndCenter = (points, canvas) => {
|
const fitAndCenter = (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)
|
||||||
@ -88,7 +105,9 @@ const normalizeGeometry = (points) => {
|
|||||||
export function usePdfImport() {
|
export function usePdfImport() {
|
||||||
const canvas = useRecoilValue(canvasState)
|
const canvas = useRecoilValue(canvasState)
|
||||||
const setOuterLinePoints = useSetRecoilState(outerLinePointsState)
|
const setOuterLinePoints = useSetRecoilState(outerLinePointsState)
|
||||||
const { addPolygonByLines } = usePolygon()
|
const setOuterLineFix = useSetRecoilState(outerLineFixState)
|
||||||
|
const isOutlineDisplay = useRecoilValue(outlineDisplaySelector)
|
||||||
|
const { addLine } = useLine()
|
||||||
const { swalFire } = useSwal()
|
const { swalFire } = useSwal()
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
|
|
||||||
@ -114,18 +133,38 @@ export function usePdfImport() {
|
|||||||
removable.forEach((obj) => canvas.remove(obj))
|
removable.forEach((obj) => canvas.remove(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 수동 그리기(useOuterLineWall.drawLine)와 동일하게 useLine.addLine 경유로 생성한다 —
|
||||||
|
// attributes/planeSize/fontSize 가 없으면 지붕형상 설정·처마/케라바 편집·재로드 복원이 깨진다.
|
||||||
const drawOuterLines = (points) => {
|
const drawOuterLines = (points) => {
|
||||||
points.forEach((point, idx) => {
|
points.forEach((point, idx) => {
|
||||||
if (idx === 0) return
|
if (idx === 0) return
|
||||||
const prev = points[idx - 1]
|
const prev = points[idx - 1]
|
||||||
const line = new QLine([prev.x, prev.y, point.x, point.y], {
|
addLine([prev.x, prev.y, point.x, point.y], {
|
||||||
stroke: '#000000',
|
stroke: 'black',
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
idx,
|
idx,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
name: 'outerLine',
|
name: 'outerLine',
|
||||||
|
x1: prev.x,
|
||||||
|
y1: prev.y,
|
||||||
|
x2: point.x,
|
||||||
|
y2: point.y,
|
||||||
|
visible: isOutlineDisplay,
|
||||||
})
|
})
|
||||||
canvas.add(line)
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// skeletonBuilder 등 지붕 생성 로직이 정점 위치를 'outerLinePoint' circle 에서 읽는다 — 수동 플로우와 동일하게 생성
|
||||||
|
const drawOuterLinePoints = (points) => {
|
||||||
|
points.forEach((point) => {
|
||||||
|
canvas.add(
|
||||||
|
new fabric.Circle({
|
||||||
|
left: point.x,
|
||||||
|
top: point.y,
|
||||||
|
visible: false,
|
||||||
|
name: 'outerLinePoint',
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,13 +187,7 @@ export function usePdfImport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let canvasPoints = toCanvasUnits(mmPoints)
|
let canvasPoints = toCanvasUnits(mmPoints)
|
||||||
canvasPoints = canvasPoints.filter((p, idx, arr) => {
|
canvasPoints = dedupShortEdges(canvasPoints)
|
||||||
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) {
|
if (canvasPoints.length < 3) {
|
||||||
swalFire({ text: getMessage('pdf.import.error.too.few.vertices'), type: 'alert' })
|
swalFire({ text: getMessage('pdf.import.error.too.few.vertices'), type: 'alert' })
|
||||||
@ -162,6 +195,13 @@ export function usePdfImport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvasPoints = normalizeGeometry(canvasPoints)
|
canvasPoints = normalizeGeometry(canvasPoints)
|
||||||
|
canvasPoints = dedupShortEdges(canvasPoints) // 정규화 과정에서 생긴 미세 엣지 제거
|
||||||
|
|
||||||
|
if (canvasPoints.length < 3) {
|
||||||
|
swalFire({ text: getMessage('pdf.import.error.too.few.vertices'), type: 'alert' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
canvasPoints = fitAndCenter(canvasPoints, canvas)
|
canvasPoints = fitAndCenter(canvasPoints, canvas)
|
||||||
canvasPoints = ensureCounterClockwise(canvasPoints)
|
canvasPoints = ensureCounterClockwise(canvasPoints)
|
||||||
|
|
||||||
@ -170,15 +210,13 @@ export function usePdfImport() {
|
|||||||
const closedPoints = [...canvasPoints, canvasPoints[0]]
|
const closedPoints = [...canvasPoints, canvasPoints[0]]
|
||||||
setOuterLinePoints(closedPoints)
|
setOuterLinePoints(closedPoints)
|
||||||
drawOuterLines(closedPoints)
|
drawOuterLines(closedPoints)
|
||||||
|
drawOuterLinePoints(closedPoints)
|
||||||
|
|
||||||
try {
|
// 수동 확정(useOuterLineWall.handleFix) 직후와 동일한 상태로 만든다 — 이 플래그가 없으면
|
||||||
const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
|
// 지붕형상 설정(useRoofShapeSetting/useRoofShapePassivitySetting) 진입이 'wall.line.not.found' 로 차단된다.
|
||||||
if (lines.length >= 3) {
|
// WALL/ROOF 폴리곤은 수동 플로우와 동일하게 지붕형상 설정 적용 시점에 생성된다.
|
||||||
addPolygonByLines(lines, { name: POLYGON_TYPE.WALL, fill: 'transparent', stroke: 'black' })
|
canvas.outerLineFix = true
|
||||||
}
|
setOuterLineFix(true)
|
||||||
} catch (e) {
|
|
||||||
logger.warn('[usePdfImport] wall polygon 생성 실패', e?.message || e)
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user