라벨변경 #810

Merged
ysCha merged 1 commits from dev into dev-deploy 2026-04-30 18:27:14 +09:00
3 changed files with 147 additions and 10 deletions

View File

@ -31,6 +31,7 @@ import { usePlan } from '@/hooks/usePlan'
import { roofsState } from '@/store/roofAtom' import { roofsState } from '@/store/roofAtom'
import { useText } from '@/hooks/useText' import { useText } from '@/hooks/useText'
import { QLine } from '@/components/fabric/QLine' import { QLine } from '@/components/fabric/QLine'
import { calcLineActualSize2 } from '@/util/qpolygon-utils'
export function useRoofAllocationSetting(id) { export function useRoofAllocationSetting(id) {
const canvas = useRecoilValue(canvasState) const canvas = useRecoilValue(canvasState)
@ -405,7 +406,43 @@ export function useRoofAllocationSetting(id) {
if (roof.separatePolygon.length === 0) { if (roof.separatePolygon.length === 0) {
roof.innerLines.forEach((line) => { roof.innerLines.forEach((line) => {
if ((!line.attributes.actualSize || line.attributes?.actualSize === 0) && line.length > 1) { 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) => { const sk = roofBase.innerLines.find((sl) => {
if (removedSk.includes(sl)) return false 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 skP1 = { x: sl.x1, y: sl.y1 }
const skP2 = { x: sl.x2, y: sl.y2 } const skP2 = { x: sl.x2, y: sl.y2 }
const sharesP1 = isSamePt(extP1, skP1) || isSamePt(extP1, skP2) const sharesP1 = isSamePt(extP1, skP1) || isSamePt(extP1, skP2)

View File

@ -1,10 +1,13 @@
import { useRecoilValue } from 'recoil' import { useRecoilValue } from 'recoil'
import { corridorDimensionSelector } from '@/store/settingAtom' 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() { export function useText() {
const canvas = useRecoilValue(canvasState) const canvas = useRecoilValue(canvasState)
const corridorDimension = useRecoilValue(corridorDimensionSelector) const corridorDimension = useRecoilValue(corridorDimensionSelector)
const globalPitch = useRecoilValue(globalPitchState)
const changeCorridorDimensionText = (columnText) => { const changeCorridorDimensionText = (columnText) => {
let { column } = corridorDimension 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) { switch (column) {
case 'corridorDimension': case 'corridorDimension':
lengthTexts.forEach((obj) => { lengthTexts.forEach((obj) => {
if (obj.planeSize) { const v = resolveSize(obj, 'planeSize')
obj.set({ text: obj.planeSize.toString() }) if (v) {
obj.set({ text: v.toString() })
} }
}) })
break break
case 'realDimension': case 'realDimension':
lengthTexts.forEach((obj) => { lengthTexts.forEach((obj) => {
if (obj.actualSize) { // hip 이고 actualSize===planeSize 면 pitch 로 즉석 재계산.
obj.set({ text: obj.actualSize.toString() }) recomputeHipActualIfFlat(obj)
const v = resolveSize(obj, 'actualSize')
if (v) {
obj.set({ text: v.toString() })
} }
}) })
break break
@ -85,9 +164,21 @@ export function useText() {
// lengthText 객체는 QPolygon.js:712-734 에서 minX/maxX/minY/maxY 로 부모 라인의 // lengthText 객체는 QPolygon.js:712-734 에서 minX/maxX/minY/maxY 로 부모 라인의
// 바운딩박스를 저장. 둘 다 양수 차이 = 대각선. // 바운딩박스를 저장. 둘 다 양수 차이 = 대각선.
const hipTexts = lengthTexts.filter((obj) => { const hipTexts = lengthTexts.filter((obj) => {
const dx = (obj.maxX ?? 0) - (obj.minX ?? 0) // [parent 좌표 기반 isDiagonal — QLine lengthText min/max 부정확 회피]
const dy = (obj.maxY ?? 0) - (obj.minY ?? 0) // QLine.js:122-125 가 minY/maxY 를 this.top/this.top+this.length 로 설정해
return dx > 0.5 && dy > 0.5 // 수평 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) { if (hipTexts.length >= 2) {

View File

@ -2912,7 +2912,12 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
console.log('Has matching line:', outerLine); console.log('Has matching line:', outerLine);
//if(outerLine === null) return //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 => ({ const convertedPolygon = edgeResult.Polygon?.map(point => ({