From 376ccab8e74de2b7a8a60aef6e8f63e8fbda73f6 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 30 Apr 2026 18:26:34 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EB=B2=A8=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roofcover/useRoofAllocationSetting.js | 43 ++++++- src/hooks/useText.js | 107 ++++++++++++++++-- src/util/skeleton-utils.js | 7 +- 3 files changed, 147 insertions(+), 10 deletions(-) diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index c5ac8d61..cd0fc1a6 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -31,6 +31,7 @@ import { usePlan } from '@/hooks/usePlan' import { roofsState } from '@/store/roofAtom' import { useText } from '@/hooks/useText' import { QLine } from '@/components/fabric/QLine' +import { calcLineActualSize2 } from '@/util/qpolygon-utils' export function useRoofAllocationSetting(id) { const canvas = useRecoilValue(canvasState) @@ -405,7 +406,43 @@ export function useRoofAllocationSetting(id) { if (roof.separatePolygon.length === 0) { roof.innerLines.forEach((line) => { if ((!line.attributes.actualSize || line.attributes?.actualSize === 0) && line.length > 1) { - line.set({ attributes: { ...line.attributes, actualSize: line.attributes.planeSize } }) + // ───────────────────────────────────────────────────────────────── + // [hip actualSize fallback 2026-04-30] + // 기존: actualSize 미설정/0 시 planeSize 로 fallback → diagonal(hip) + // 까지 actualSize=planeSize 가 되어 伏せ図/配置面 라벨 토글 무효화. + // 수정: diagonal 은 pitch 로 actualSize 산출(calcLineActualSize2), + // horizontal/vertical 만 planeSize fallback 유지. + // pitch 우선순위: line.attributes.pitch → roof.attributes.pitch + // → roof.roofMaterial.pitch → 0(편평). + // ───────────────────────────────────────────────────────────────── + const dx = Math.abs((line.x2 ?? 0) - (line.x1 ?? 0)) + const dy = Math.abs((line.y2 ?? 0) - (line.y1 ?? 0)) + const isDiagonal = dx > 0.5 && dy > 0.5 + let newActualSize + if (isDiagonal) { + const pitch = + line.attributes?.pitch ?? + roof.attributes?.pitch ?? + roof.roofMaterial?.pitch ?? + 0 + newActualSize = Number( + calcLineActualSize2( + { x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2 }, + getDegreeByChon(pitch), + ), + ) + } else { + newActualSize = line.attributes.planeSize + } + line.set({ attributes: { ...line.attributes, actualSize: newActualSize } }) + // lengthText 에 actualSize 즉시 propagate (없으면 모드 토글이 if(obj.actualSize) + // 가드에 막혀 토글 무효화). + const lengthText = canvas.getObjects().find( + (o) => o.name === 'lengthText' && o.parentId === line.id, + ) + if (lengthText) { + lengthText.set({ actualSize: newActualSize }) + } } }) } @@ -479,6 +516,10 @@ export function useRoofAllocationSetting(id) { const sk = roofBase.innerLines.find((sl) => { if (removedSk.includes(sl)) return false + // [auxiliaryLine 제외 2026-04-30] integrateExtensionLines 는 SK auto-build + // hip stub + ext 의 1:1 통합용. 사용자가 그린 auxiliaryLine 은 이미 완전한 + // 분할선이므로 ext 와 병합하면 좌표가 어긋남(코너 끝점 손실). + if (sl.name === 'auxiliaryLine') return false const skP1 = { x: sl.x1, y: sl.y1 } const skP2 = { x: sl.x2, y: sl.y2 } const sharesP1 = isSamePt(extP1, skP1) || isSamePt(extP1, skP2) diff --git a/src/hooks/useText.js b/src/hooks/useText.js index 4bd4865c..01b8d51a 100644 --- a/src/hooks/useText.js +++ b/src/hooks/useText.js @@ -1,10 +1,13 @@ import { useRecoilValue } from 'recoil' import { corridorDimensionSelector } from '@/store/settingAtom' -import { canvasState } from '@/store/canvasAtom' +import { canvasState, globalPitchState } from '@/store/canvasAtom' +import { calcLineActualSize2 } from '@/util/qpolygon-utils' +import { getDegreeByChon } from '@/util/canvas-util' export function useText() { const canvas = useRecoilValue(canvasState) const corridorDimension = useRecoilValue(corridorDimensionSelector) + const globalPitch = useRecoilValue(globalPitchState) const changeCorridorDimensionText = (columnText) => { let { column } = corridorDimension @@ -21,18 +24,94 @@ export function useText() { }) }) + // ───────────────────────────────────────────────────────────────────────── + // [라벨 토글 fallback 2026-04-30] + // 기존: obj.planeSize / obj.actualSize 가 lengthText 에 set 되어있어야만 토글 적용. + // hip(대각선)을 비롯해 일부 경로(skeleton SK 빌더, split 후 재생성 등)는 + // lengthText 의 attribute 동기화가 누락되어 토글 시 if(obj.xxxSize) 가드에 막힘. + // 수정: parent line(QLine) 의 attributes 에서 직접 fallback 으로 읽어오고, + // lengthText 에도 즉시 propagate 해서 다음 토글부터 정상 인식. + // ───────────────────────────────────────────────────────────────────────── + const resolveSize = (obj, key) => { + if (obj[key]) return obj[key] + const parent = obj.parent ?? canvas.getObjects().find((o) => o.id === obj.parentId) + const v = parent?.attributes?.[key] + if (v) { + obj.set({ [key]: v }) + return v + } + return null + } + + // [hip actualSize 즉석 재계산 2026-04-30] + // 기존 SK 가 pitch=0 으로 빌드되어 hip 의 actualSize === planeSize 가 된 경우, + // realDimension 토글 시 pitch 폴백으로 즉석 재계산. + // parent line.attributes.pitch → roof.pitch → roof.roofMaterial.pitch. + // 재계산 결과는 obj.actualSize 와 line.attributes.actualSize 양쪽에 propagate. + const recomputeHipActualIfFlat = (obj) => { + const parent = obj.parent ?? canvas.getObjects().find((o) => o.id === obj.parentId) + if (!parent) return null + // [isDiagonal 판정 — parent 좌표 사용] + // QLine lengthText 의 minX/maxX/minY/maxY 는 this.length(라인길이) 로 설정되어 + // 수평 ridge 도 dy>0 이 되는 버그. 반드시 parent.x1/y1/x2/y2 로 판정. + const px1 = Number(parent.x1) + const py1 = Number(parent.y1) + const px2 = Number(parent.x2) + const py2 = Number(parent.y2) + const dx = Number.isFinite(px1) && Number.isFinite(px2) ? Math.abs(px2 - px1) : 0 + const dy = Number.isFinite(py1) && Number.isFinite(py2) ? Math.abs(py2 - py1) : 0 + // 임계 5.0mm — addRawLine 의 HIP/RIDGE 분류 임계와 일치 (drift 흡수). + const isDiagonal = dx > 5.0 && dy > 5.0 + if (!isDiagonal) return null + const plane = obj.planeSize ?? parent?.attributes?.planeSize + const actual = obj.actualSize ?? parent?.attributes?.actualSize + if (!plane) return null + if (actual && Math.abs(actual - plane) > 0.5) return null // 이미 정상 + const roof = canvas.getObjects().find((o) => o.id === parent?.parentId) + const pitch = + (parent?.attributes?.pitch ? parent.attributes.pitch : null) ?? + (roof?.pitch ? roof.pitch : null) ?? + (roof?.roofMaterial?.pitch ? roof.roofMaterial.pitch : null) ?? + globalPitch ?? + 4 + if (!pitch) return null + // x1/x2/y1/y2 가 number 가 아니면 calcLinePlaneSize 의 Big() 호출에서 throw. + const x1 = Number(parent.x1) + const y1 = Number(parent.y1) + const x2 = Number(parent.x2) + const y2 = Number(parent.y2) + if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) { + return null + } + let newActual = null + try { + newActual = Number(calcLineActualSize2({ x1, y1, x2, y2 }, getDegreeByChon(pitch))) + } catch (e) { + return null + } + if (!newActual || Math.abs(newActual - plane) < 0.5) return null + // 표시용 obj.actualSize 만 갱신. parent.attributes 갱신 시 fabric set 사이드이펙트 + // (Big.js 사슬) 가 다른 곳을 트리거할 수 있어 회피. + obj.set({ actualSize: newActual }) + return newActual + } + switch (column) { case 'corridorDimension': lengthTexts.forEach((obj) => { - if (obj.planeSize) { - obj.set({ text: obj.planeSize.toString() }) + const v = resolveSize(obj, 'planeSize') + if (v) { + obj.set({ text: v.toString() }) } }) break case 'realDimension': lengthTexts.forEach((obj) => { - if (obj.actualSize) { - obj.set({ text: obj.actualSize.toString() }) + // hip 이고 actualSize===planeSize 면 pitch 로 즉석 재계산. + recomputeHipActualIfFlat(obj) + const v = resolveSize(obj, 'actualSize') + if (v) { + obj.set({ text: v.toString() }) } }) break @@ -85,9 +164,21 @@ export function useText() { // lengthText 객체는 QPolygon.js:712-734 에서 minX/maxX/minY/maxY 로 부모 라인의 // 바운딩박스를 저장. 둘 다 양수 차이 = 대각선. const hipTexts = lengthTexts.filter((obj) => { - const dx = (obj.maxX ?? 0) - (obj.minX ?? 0) - const dy = (obj.maxY ?? 0) - (obj.minY ?? 0) - return dx > 0.5 && dy > 0.5 + // [parent 좌표 기반 isDiagonal — QLine lengthText min/max 부정확 회피] + // QLine.js:122-125 가 minY/maxY 를 this.top/this.top+this.length 로 설정해 + // 수평 ridge 도 maxY-minY>0 이 됨. 반드시 parent.x1/y1/x2/y2 로 판정. + const parent = obj.parent ?? canvas.getObjects().find((p) => p.id === obj.parentId) + if (!parent) return false + const px1 = Number(parent.x1) + const py1 = Number(parent.y1) + const px2 = Number(parent.x2) + const py2 = Number(parent.y2) + if (!Number.isFinite(px1) || !Number.isFinite(py1) || !Number.isFinite(px2) || !Number.isFinite(py2)) { + return false + } + const dx = Math.abs(px2 - px1) + const dy = Math.abs(py2 - py1) + return dx > 5.0 && dy > 5.0 }) if (hipTexts.length >= 2) { diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index b78a5618..7ade81e8 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -2912,7 +2912,12 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { console.log('Has matching line:', outerLine); //if(outerLine === null) return } - let pitch = outerLine?.attributes?.pitch??0 + // [hip pitch fallback 2026-04-30] + // outerLine.attributes.pitch 가 없으면 0 → calcLineActualSize2(deg=0) 가 + // calcLinePlaneSize 와 동치(tan0=0) → 모든 hip 의 planeSize === actualSize. + // 결과: 伏せ図(plane) ↔ 配置面(actual) 메뉴 토글해도 hip 라벨 변화 없음. + // roof.pitch 폴백(usePolygon.js:1649 패턴) 으로 진짜 pitch 확보. + let pitch = outerLine?.attributes?.pitch ?? roof?.pitch ?? 0 const convertedPolygon = edgeResult.Polygon?.map(point => ({