qcast-front/src/hooks/useText.js

232 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}
}