232 lines
12 KiB
JavaScript
232 lines
12 KiB
JavaScript
import { useRecoilValue } from 'recoil'
|
||
import { corridorDimensionSelector } from '@/store/settingAtom'
|
||
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
|
||
if (columnText) {
|
||
column = columnText
|
||
}
|
||
const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText')
|
||
const group = canvas.getObjects().filter((obj) => obj.type === 'group')
|
||
group.forEach((obj) => {
|
||
obj._objects
|
||
.filter((obj2) => obj2.name === 'lengthText')
|
||
.forEach((obj3) => {
|
||
lengthTexts.push(obj3)
|
||
})
|
||
})
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// [라벨 토글 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
|
||
}
|
||
|
||
// [2026-05-18 라벨 통일] planeSize/actualSize 모두 저장값 그대로 표시.
|
||
// planeSize 는 정수(calcLinePlaneSize) 가 기본, hip equalize 후 0.5 step 가능.
|
||
// actualSize 는 0.5 step. 둘 다 .toFixed(1) 후 trailing .0 제거.
|
||
switch (column) {
|
||
case 'corridorDimension':
|
||
lengthTexts.forEach((obj) => {
|
||
const v = resolveSize(obj, 'planeSize')
|
||
if (v) {
|
||
obj.set({ text: Number(v).toFixed(1).replace(/\.0$/, '') })
|
||
}
|
||
})
|
||
break
|
||
case 'realDimension':
|
||
lengthTexts.forEach((obj) => {
|
||
// hip 이고 actualSize===planeSize 면 pitch 로 즉석 재계산.
|
||
recomputeHipActualIfFlat(obj)
|
||
const v = resolveSize(obj, 'actualSize')
|
||
if (v) {
|
||
obj.set({ text: Number(v).toFixed(1).replace(/\.0$/, '') })
|
||
}
|
||
})
|
||
break
|
||
case 'noneDimension':
|
||
lengthTexts.forEach((obj) => {
|
||
obj.set({ text: '' })
|
||
})
|
||
break
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
// [hip 라벨 cross-display 통일 2026-04-30 by Claude]
|
||
//
|
||
// 배경:
|
||
// 좌우대칭 hip(대각선) 라인의 라벨이 0.5mm 차이로 갈리는 케이스 발생
|
||
// (예: 좌측 5048.5 / 우측 5048).
|
||
//
|
||
// 원인:
|
||
// 1) polygon split / ridge intersection 단계 부동소수점 연산
|
||
// → QLine.js:23 toFixed(1) 0.1mm 단위 절단 → 좌우 hip 좌표가 0.1mm 차
|
||
// 2) useLine.js setActualSize 의 axisLength * tan + sqrt 계산에서
|
||
// 좌표 0.1mm 차가 ×10 으로 1mm 차로 증폭
|
||
// 3) 마지막 `Math.round(actualSize * 2) / 2` (0.5mm 단위) boundary 에서 갈림
|
||
// → 한쪽은 5048.0, 한쪽은 5048.5 로 결정
|
||
// 4) planeSize 도 usePolygon.js 의 _equalizeIfNearSymmetric([avg, avg])
|
||
// 경로에서 0.5mm 단위 값이 만들어질 수 있음 (corridorDimension 모드에 영향)
|
||
//
|
||
// 수정 방침:
|
||
// - **표시 레이어 (lengthText.text) 만 보정**.
|
||
// - line.attributes.actualSize / line.attributes.planeSize 는 절대 안 건드림.
|
||
// → 다운스트림 read 경로 (라인 분할 비율 / 확장라인 병합 / 모듈배치 shape
|
||
// 상속 / JSON 저장 / DB / 복제 플랜) 영향 0.
|
||
//
|
||
// 처리:
|
||
// - 대각선 hip 라벨끼리 묶어서, 표시값이 ±2mm 이내이면 정수 round 한
|
||
// 공통값으로 스냅 (5048.5/5048 → 둘 다 5049 또는 5048).
|
||
// - corridorDimension / realDimension 모드 모두 적용.
|
||
// - noneDimension 모드는 빈 문자열이라 패스.
|
||
//
|
||
// 추후 근본 수정 (다른 개발자 검토 필요):
|
||
// - QLine.js:23 toFixed(1) → toFixed(2) 정밀도 상향 (영향 범위 넓어 별건)
|
||
// - skeleton-utils.js getLineIntersection 결과 좌표 round
|
||
// - useLine.js setActualSize 마지막 round 단위 변경 (0.5 → 1.0)
|
||
// 위 근본수정이 들어가면 이 보정 코드는 제거 가능.
|
||
//
|
||
// 메모리: feedback_display_vs_internal_values.md 참고.
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
if (column === 'corridorDimension' || column === 'realDimension') {
|
||
// 대각선(hip) lengthText 추출.
|
||
// lengthText 객체는 QPolygon.js:712-734 에서 minX/maxX/minY/maxY 로 부모 라인의
|
||
// 바운딩박스를 저장. 둘 다 양수 차이 = 대각선.
|
||
const hipTexts = lengthTexts.filter((obj) => {
|
||
// [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) {
|
||
// 현재 표시 텍스트를 숫자로 파싱.
|
||
const items = hipTexts
|
||
.map((obj) => ({ obj, value: parseFloat(obj.text) }))
|
||
.filter((item) => !isNaN(item.value))
|
||
|
||
// 그룹화: 표시값이 ±TOLERANCE mm 이내면 동일 그룹.
|
||
// TOLERANCE=2 는 0.5 단위 round drift + sqrt fp 노이즈 흡수.
|
||
// 대칭 hip 들의 정상 길이차 (예: 다른 지붕면의 hip) 보다는 작아야 함.
|
||
const TOLERANCE = 2
|
||
const groups = []
|
||
for (const item of items) {
|
||
const matched = groups.find((g) => Math.abs(g.representative - item.value) <= TOLERANCE)
|
||
if (matched) {
|
||
matched.items.push(item)
|
||
// 그룹 대표값은 첫 항목 값 유지 (안정적인 클러스터링).
|
||
} else {
|
||
groups.push({ representative: item.value, items: [item] })
|
||
}
|
||
}
|
||
|
||
// 각 그룹의 멤버가 2개 이상이면 정수 round 평균값으로 통일.
|
||
// 1개 그룹은 단독 hip 이므로 보정 불필요.
|
||
for (const group of groups) {
|
||
if (group.items.length < 2) continue
|
||
const avg = group.items.reduce((s, i) => s + i.value, 0) / group.items.length
|
||
// [2026-05-18 라벨 0.5 step 통일] corridor/real 모두 0.5 step.
|
||
// planeSize 도 hip equalize 후 0.5 step 가능하므로 정수 round 하지 않음.
|
||
const unified = (Math.round(avg * 2) / 2).toFixed(1).replace(/\.0$/, '')
|
||
for (const item of group.items) {
|
||
// text 만 set. obj.actualSize / obj.planeSize 는 그대로.
|
||
item.obj.set({ text: unified })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
|
||
canvas?.renderAll()
|
||
}
|
||
|
||
return {
|
||
changeCorridorDimensionText,
|
||
}
|
||
}
|