[작업내용] : - canvas.remove()에 배열을 통째로 넘겨 Fabric이 무시(silent no-op)하던 것을 spread로 개별 전달하도록 수정 - addPitchText의 중복 방지가 무력화되어 같은 자리에 경사/출폭 텍스트가 겹겹이 쌓이던 문제 해결 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
244 lines
7.5 KiB
JavaScript
244 lines
7.5 KiB
JavaScript
import { useRecoilValue } from 'recoil'
|
|
import {
|
|
canvasState,
|
|
currentAngleTypeSelector,
|
|
fontFamilyState,
|
|
fontSizeState,
|
|
globalPitchState,
|
|
pitchTextSelector,
|
|
showAngleUnitSelector,
|
|
} from '@/store/canvasAtom'
|
|
import { QLine } from '@/components/fabric/QLine'
|
|
import { basicSettingState } from '@/store/settingAtom'
|
|
import { calcLineActualSizeByLineLength, calcLinePlaneSize } from '@/util/qpolygon-utils'
|
|
import { getDegreeByChon } from '@/util/canvas-util'
|
|
import { useText } from '@/hooks/useText'
|
|
import { fontSelector } from '@/store/fontAtom'
|
|
|
|
export const useLine = () => {
|
|
const canvas = useRecoilValue(canvasState)
|
|
const fontSize = useRecoilValue(fontSizeState)
|
|
const fontFamily = useRecoilValue(fontFamilyState)
|
|
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
|
|
const pitchText = useRecoilValue(pitchTextSelector)
|
|
const angleUnit = useRecoilValue(showAngleUnitSelector)
|
|
const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet
|
|
const globalPitch = useRecoilValue(globalPitchState)
|
|
const lengthText = useRecoilValue(fontSelector('lengthText'))
|
|
|
|
const { changeCorridorDimensionText } = useText()
|
|
|
|
const addLine = (points = [], options) => {
|
|
const line = new QLine(points, {
|
|
...options,
|
|
attributes: {},
|
|
fontSize: lengthText.fontSize.value,
|
|
fontFamily: lengthText.fontFamily.value,
|
|
})
|
|
|
|
if (line.length < 1) {
|
|
return null
|
|
}
|
|
line.attributes.planeSize = calcLinePlaneSize(line)
|
|
canvas?.add(line)
|
|
return line
|
|
}
|
|
|
|
const hideLine = (line) => {
|
|
line.set({
|
|
visible: false,
|
|
})
|
|
const obj = canvas?.getObjects().find((obj) => obj.parentId === line.id)
|
|
if (obj) {
|
|
obj.set({
|
|
visible: false,
|
|
})
|
|
}
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const showLine = (line) => {
|
|
line.set({
|
|
visible: true,
|
|
})
|
|
canvas
|
|
?.getObjects()
|
|
?.find((obj) => obj.parentId === line.id)
|
|
?.set({
|
|
visible: true,
|
|
})
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const removeLine = (line) => {
|
|
removeLineText(line)
|
|
canvas?.remove(line)
|
|
canvas?.renderAll()
|
|
}
|
|
|
|
const removeLineText = (line) => {
|
|
canvas?.remove(canvas?.getObjects().find((obj) => obj.parent === line))
|
|
}
|
|
|
|
const getLengthByLine = (line) => {
|
|
const scaleX = line.scaleX
|
|
const scaleY = line.scaleY
|
|
const x1 = line.left
|
|
const y1 = line.top
|
|
const x2 = line.left + line.width * scaleX
|
|
const y2 = line.top + line.height * scaleY
|
|
const dx = x2 - x1
|
|
const dy = y2 - y1
|
|
|
|
return Number(Math.sqrt(dx * dx + dy * dy).toFixed(1)) * 10
|
|
}
|
|
|
|
const addPitchText = (line) => {
|
|
removePitchText(line)
|
|
const { startPoint, endPoint, direction, attributes } = line
|
|
const { offset, onlyOffset = false } = attributes
|
|
if (offset === 0) {
|
|
return
|
|
}
|
|
|
|
let left, top
|
|
|
|
const textStr = `${attributes.offset ? attributes.offset * 10 : attributes.width * 10}`
|
|
|
|
// currentAngleType === ANGLE_TYPE.SLOPE
|
|
// ? `${attributes.offset ? attributes.offset * 10 : attributes.width * 10}${!onlyOffset && attributes.pitch ? '-∠' + attributes.pitch + angleUnit : ''}`
|
|
// : `${attributes.offset ? attributes.offset * 10 : attributes.width * 10}${!onlyOffset && attributes.pitch ? '-∠' + getDegreeByChon(attributes.pitch) + angleUnit : ''}`
|
|
|
|
if (direction === 'top') {
|
|
left = (startPoint.x + endPoint.x) / 2
|
|
top = (startPoint.y + endPoint.y) / 2 - 50
|
|
} else if (direction === 'bottom') {
|
|
left = (startPoint.x + endPoint.x) / 2
|
|
top = (startPoint.y + endPoint.y) / 2 - 50
|
|
} else if (direction === 'left') {
|
|
left = (startPoint.x + endPoint.x) / 2 + 50
|
|
top = (startPoint.y + endPoint.y) / 2 - 30
|
|
} else if (direction === 'right') {
|
|
left = (startPoint.x + endPoint.x) / 2 + 50
|
|
top = (startPoint.y + endPoint.y) / 2 - 30
|
|
}
|
|
|
|
if (!attributes.pitch) {
|
|
return
|
|
}
|
|
|
|
const text = new fabric.Text(`${textStr}`, {
|
|
left,
|
|
top,
|
|
fontSize: 20,
|
|
originText: `${attributes.offset ? attributes.offset * 10 : attributes.width * 10}`,
|
|
fill: 'black',
|
|
name: 'pitchText',
|
|
parentId: line.id,
|
|
pitch: attributes.pitch,
|
|
onlyOffset,
|
|
})
|
|
|
|
canvas.add(text)
|
|
}
|
|
|
|
const removePitchText = (line) => {
|
|
const { id } = line
|
|
const pitchText = canvas.getObjects().filter((obj) => obj.name === 'pitchText' && obj.parentId === id)
|
|
|
|
// 기존 pitchText가 있으면 삭제
|
|
if (pitchText.length > 0) {
|
|
canvas.remove(...pitchText)
|
|
}
|
|
}
|
|
|
|
const addPitchTextsByOuterLines = () => {
|
|
canvas
|
|
.getObjects()
|
|
.filter((obj) => obj.name === 'outerLine')
|
|
.forEach((line) => {
|
|
addPitchText(line)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 복도치수, 실제치수에 따라 actualSize를 설정한다.
|
|
* @param line
|
|
* @param direction polygon의 방향
|
|
* @param pitch
|
|
* @param forceUpdate
|
|
*/
|
|
const setActualSize = (line, direction, pitch = globalPitch, forceUpdate = false) => {
|
|
if (line.attributes.isCalculated && !forceUpdate) {
|
|
return
|
|
}
|
|
const { x1, y1, x2, y2 } = line
|
|
|
|
const isHorizontal = y1 === y2
|
|
const isVertical = x1 === x2
|
|
const isDiagonal = !isHorizontal && !isVertical
|
|
const lineLength = line.attributes.planeSize ?? line.getLength()
|
|
|
|
line.attributes = { ...line.attributes, planeSize: lineLength, actualSize: lineLength }
|
|
|
|
if (+roofSizeSet === 1) {
|
|
if (direction === 'south' || direction === 'north') {
|
|
if (isVertical) {
|
|
line.attributes = {
|
|
...line.attributes,
|
|
actualSize: calcLineActualSizeByLineLength(lineLength, getDegreeByChon(pitch)),
|
|
}
|
|
} else if (isDiagonal) {
|
|
// [face 비대칭 보정 2026-04-30] 같은 hip 가 인접 두 face 에서 다른 축(yLength vs xLength)
|
|
// 으로 계산되어 좌표 round drift 가 actualSize 라벨에 1mm 차로 노출(예: 5048 vs 5049).
|
|
// axis 평균을 써서 양 face 가 동일값을 산출하도록 통일.
|
|
const yLength = Math.abs(y2 - y1) * 10
|
|
const xLength = Math.abs(x2 - x1) * 10
|
|
const axisLength = (yLength + xLength) / 2
|
|
|
|
const h = axisLength * Math.tan(getDegreeByChon(pitch) * (Math.PI / 180))
|
|
|
|
const actualSize = Math.sqrt(h ** 2 + lineLength ** 2)
|
|
line.attributes = { ...line.attributes, actualSize: actualSize }
|
|
}
|
|
} else if (direction === 'west' || direction === 'east') {
|
|
if (isHorizontal) {
|
|
line.attributes = {
|
|
...line.attributes,
|
|
actualSize: calcLineActualSizeByLineLength(lineLength, getDegreeByChon(pitch)),
|
|
}
|
|
} else if (isDiagonal) {
|
|
// [face 비대칭 보정 2026-04-30] south/north 분기와 동일하게 axis 평균 사용.
|
|
const yLength = Math.abs(y2 - y1) * 10
|
|
const xLength = Math.abs(x2 - x1) * 10
|
|
const axisLength = (yLength + xLength) / 2
|
|
|
|
const h = axisLength * Math.tan(getDegreeByChon(pitch) * (Math.PI / 180))
|
|
|
|
const actualSize = Math.sqrt(h ** 2 + lineLength ** 2)
|
|
line.attributes = { ...line.attributes, actualSize: actualSize }
|
|
}
|
|
}
|
|
}
|
|
|
|
line.attributes = {
|
|
...line.attributes,
|
|
// 0.5 단위 정밀도 유지 (대칭 분할 시 1637.5 같은 값 보존). 그 외에는 정수.
|
|
actualSize: Number((Math.round(line.attributes.actualSize * 2) / 2).toFixed(1)),
|
|
isCalculated: true,
|
|
}
|
|
}
|
|
|
|
return {
|
|
addLine,
|
|
removeLine,
|
|
hideLine,
|
|
showLine,
|
|
addPitchText,
|
|
removePitchText,
|
|
addPitchTextsByOuterLines,
|
|
getLengthByLine,
|
|
setActualSize,
|
|
}
|
|
}
|