'use client' import { useEffect } from 'react' import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' import { canvasSettingState, canvasState, currentCanvasPlanState, globalPitchState } from '@/store/canvasAtom' import { MENU, POLYGON_TYPE, LINE_TYPE } from '@/common/common' import { getIntersectionPoint, toFixedWithoutRounding } from '@/util/canvas-util' import { degreesToRadians } from '@turf/turf' import { QPolygon } from '@/components/fabric/QPolygon' import { useSwal } from '@/hooks/useSwal' import { useMessage } from '@/hooks/useMessage' import { useEvent } from '@/hooks/useEvent' import { usePopup } from '@/hooks/usePopup' import { roofDisplaySelector } from '@/store/settingAtom' import { usePolygon } from '@/hooks/usePolygon' import { fontSelector } from '@/store/fontAtom' import { slopeSelector } from '@/store/commonAtom' import { QLine } from '@/components/fabric/QLine' import { useRoofFn } from '@/hooks/common/useRoofFn' import { outerLinePointsState } from '@/store/outerLineAtom' import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingAtom' import { getBackGroundImage } from '@/lib/imageActions' import PlacementSurfaceLineProperty from '@/components/floor-plan/modal/placementShape/PlacementSurfaceLineProperty' import { v4 as uuidv4 } from 'uuid' import { useCanvasSetting } from '@/hooks/option/useCanvasSetting' export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { const { getMessage } = useMessage() const { drawDirectionArrow, addPolygon } = usePolygon() const lengthTextFont = useRecoilValue(fontSelector('lengthText')) const resetOuterLinePoints = useResetRecoilState(outerLinePointsState) const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState) const canvasSetting = useRecoilValue(canvasSettingState) const canvas = useRecoilValue(canvasState) const globalPitch = useRecoilValue(globalPitchState) const roofDisplay = useRecoilValue(roofDisplaySelector) const slope = useRecoilValue(slopeSelector(globalPitch)) const { swalFire } = useSwal() const { addCanvasMouseEventListener, initEvent } = useEvent() // const { addCanvasMouseEventListener, initEvent } = useContext(EventContext) const { addPopup, closePopup } = usePopup() const { setSurfaceShapePattern } = useRoofFn() const currentCanvasPlan = useRecoilValue(currentCanvasPlanState) const { fetchSettings } = useCanvasSetting(false) const applySurfaceShape = (surfaceRefs, selectedType, id) => { let length1, length2, length3, length4, length5 const surfaceId = selectedType?.id const azimuth = surfaceRefs.azimuth.current if (surfaceId === 1) { length1 = surfaceRefs.length1.current.value length2 = surfaceRefs.length2.current.value length3 = surfaceRefs.lengthetc.current.value //대각선 } else if ([2, 4].includes(surfaceId)) { length1 = surfaceRefs.length1.current.value length2 = surfaceRefs.length2.current.value } else if ([3, 5, 6, 15, 18].includes(surfaceId)) { length1 = surfaceRefs.length1.current.value length2 = surfaceRefs.length2.current.value length3 = surfaceRefs.length3.current.value } else if ([8, 12, 13, 16, 17].includes(surfaceId)) { length1 = surfaceRefs.length1.current.value length2 = surfaceRefs.length2.current.value length3 = surfaceRefs.length3.current.value length4 = surfaceRefs.length4.current.value } else if ([7, 9, 10, 11, 14].includes(surfaceId)) { length1 = surfaceRefs.length1.current.value length2 = surfaceRefs.length2.current.value length3 = surfaceRefs.length3.current.value length4 = surfaceRefs.length4.current.value length5 = surfaceRefs.length5.current.value } length1 = parseFloat(length1 === undefined ? 0 : Number(length1 / 10).toFixed(1)) length2 = parseFloat(length2 === undefined ? 0 : Number(length2 / 10).toFixed(1)) length3 = parseFloat(length3 === undefined ? 0 : Number(length3 / 10).toFixed(1)) length4 = parseFloat(length4 === undefined ? 0 : Number(length4 / 10).toFixed(1)) length5 = parseFloat(length5 === undefined ? 0 : Number(length5 / 10).toFixed(1)) let isDrawing = true let obj = null let points = [] //일단 팝업을 가린다 if (checkSurfaceShape(surfaceId, { length1, length2, length3, length4, length5 })) { addCanvasMouseEventListener('mouse:move', (e) => { if (!isDrawing) { return } const pointer = canvas?.getPointer(e.e) canvas?.remove(...canvas?.getObjects().filter((obj) => obj.name === MENU.BATCH_CANVAS.SURFACE_SHAPE_BATCH_TEMP)) points = getSurfaceShape(surfaceId, pointer, { length1, length2, length3, length4, length5 }) const { xInversion, yInversion, rotate } = surfaceRefs const options = { fill: 'transparent', stroke: 'black', strokeWidth: 1, strokeDasharray: [10, 4], fontSize: 12, selectable: true, lockMovementX: true, // X 축 이동 잠금 lockMovementY: true, // Y 축 이동 잠금 lockRotation: true, // 회전 잠금 lockScalingX: true, // X 축 크기 조정 잠금 lockScalingY: true, // Y 축 크기 조정 잠금 name: MENU.BATCH_CANVAS.SURFACE_SHAPE_BATCH_TEMP, // flipX: xInversion !== yInversion, // angle: xInversion && yInversion ? Math.abs((rotate + 180) % 360) : Math.abs(rotate), // angle: rotate, originX: 'center', originY: 'center', pitch: globalPitch, } obj = new QPolygon(points, options) let imageRotate = 0 if (xInversion && !yInversion) { if (rotate % 180 === 0 || rotate < 0) { imageRotate = Math.abs(rotate % 360) } else { if (rotate < 0) { imageRotate = Math.abs((rotate - 180) % 360) } else { imageRotate = Math.abs((rotate + 180) % 360) } } } else if (xInversion && yInversion) { imageRotate = Math.abs((rotate + 360) % 360) } else if (xInversion !== yInversion && rotate < 0) { imageRotate = Math.abs(rotate) } else if (!xInversion && yInversion) { if (rotate % 180 === 0 || rotate < 0) { imageRotate = Math.abs(rotate % 360) } else { if (rotate < 0) { imageRotate = Math.abs((rotate - 180) % 360) } else { imageRotate = Math.abs((rotate + 180) % 360) } } } else { imageRotate = (rotate + 360) % 360 } obj.set({ angle: imageRotate, flipX: xInversion !== yInversion }) obj.setCoords() //좌표 변경 적용 canvas?.add(obj) canvas?.renderAll() }) addCanvasMouseEventListener('mouse:down', (e) => { isDrawing = false const { xInversion, yInversion } = surfaceRefs canvas?.remove(obj) //각도 추가 let originAngle = 0 //기본 남쪽 let direction = 'south' if (azimuth === 'left') { //서 originAngle = 90 direction = 'west' } else if (azimuth === 'right') { //동 originAngle = 270 direction = 'east' } else if (azimuth === 'up') { //북 originAngle = 180 direction = 'north' } //회전, flip등이 먹은 기준으로 새로생성 // const batchSurface = addPolygon(reorderedPoints, { const batchSurface = addPolygon(obj.getCurrentPoints(), { fill: 'transparent', stroke: 'red', strokeWidth: 3, strokeDasharray: [10, 4], fontSize: 12, selectable: true, lockMovementX: true, // X 축 이동 잠금 lockMovementY: true, // Y 축 이동 잠금 lockRotation: true, // 회전 잠금 lockScalingX: true, // X 축 크기 조정 잠금 lockScalingY: true, // Y 축 크기 조정 잠금 name: POLYGON_TYPE.ROOF, originX: 'center', originY: 'center', pitch: globalPitch, surfaceId: surfaceId, direction: direction, isXInversion: xInversion, isYInversion: yInversion, }) canvas.setActiveObject(batchSurface) setSurfaceShapePattern(batchSurface, roofDisplay.column) drawDirectionArrow(batchSurface) // closePopup(id) initEvent() // if (+canvasSetting?.roofSizeSet === 3) return // const popupId = uuidv4() // addPopup(popupId, 2, ) // console.log('xInversion', xInversion) //상하반전 // console.log('yInversion', yInversion) //좌우반전 changeSurfaceLineType(batchSurface) if (setIsHidden) setIsHidden(false) }) } else { if (setIsHidden) setIsHidden(false) } } //면형상 입력 validate const checkSurfaceShape = (surfaceId, lengths) => { const { length1, length2, length3, length4, length5 } = lengths let check = true if (surfaceId === 1) { if (length1 === 0) { swalFire({ text: getMessage('common.canvas.validate.size'), icon: 'error' }) check = false } if (length2 === 0) { if (length3 === 0) { swalFire({ text: getMessage('common.canvas.validate.size'), icon: 'error' }) check = false } } } else if ([2, 4].includes(surfaceId)) { if (length1 === 0 || length2 === 0) { swalFire({ text: getMessage('common.canvas.validate.size'), icon: 'error' }) check = false } } else if ([3, 5, 6, 15, 18].includes(surfaceId)) { if (length1 === 0 || length2 === 0 || length3 === 0) { swalFire({ text: getMessage('common.canvas.validate.size'), icon: 'error' }) check = false } if (surfaceId === 3 && length3 >= length1) { swalFire({ text: getMessage('surface.shape.validate.size.1to3'), icon: 'error' }) check = false } if (surfaceId === 5 || surfaceId === 15) { if (length3 >= length2) { swalFire({ text: getMessage('surface.shape.validate.size.2to3'), icon: 'error' }) check = false } } if (surfaceId === 18) { if (length2 >= length3) { swalFire({ text: getMessage('surface.shape.validate.size.3to2'), icon: 'error' }) check = false } } if (surfaceId === 6) { if (length2 >= length3) { swalFire({ text: getMessage('surface.shape.validate.size.3to2'), icon: 'error' }) check = false } } } else if ([8, 12, 13, 16, 17].includes(surfaceId)) { if (length1 === 0 || length2 === 0 || length3 === 0 || length4 === 0) { swalFire({ text: getMessage('common.canvas.validate.size'), icon: 'error' }) check = false } if (surfaceId === 8) { if (length2 >= length1) { swalFire({ text: getMessage('surface.shape.validate.size.1to2'), icon: 'error' }) check = false } if (length4 >= length3) { swalFire({ text: getMessage('surface.shape.validate.size.3to4'), icon: 'error' }) check = false } } if (surfaceId === 12 || surfaceId === 13 || surfaceId === 16) { if (length2 >= length1) { swalFire({ text: getMessage('surface.shape.validate.size.1to2'), icon: 'error' }) check = false } if (length4 >= length3) { swalFire({ text: getMessage('surface.shape.validate.size.3to4'), icon: 'error' }) check = false } } if (surfaceId === 17) { if (length2 >= length1) { swalFire({ text: getMessage('surface.shape.validate.size.1to2'), icon: 'error' }) check = false } if (length3 >= length4) { swalFire({ text: getMessage('surface.shape.validate.size.4to3'), icon: 'error' }) check = false } } } else if ([7, 9, 10, 11, 14].includes(surfaceId)) { if (length1 === 0 || length2 === 0 || length3 === 0 || length4 === 0 || length5 === 0) { swalFire({ text: getMessage('common.canvas.validate.size'), icon: 'error' }) check = false } if (surfaceId === 9 || surfaceId === 10) { if (length2 + length3 >= length1) { swalFire({ text: getMessage('surface.shape.validate.size.1to23'), icon: 'error' }) check = false } if (length5 >= length4) { swalFire({ text: getMessage('surface.shape.validate.size.4to5'), icon: 'error' }) check = false } } if (surfaceId === 11) { if (length1 > length2 + length3) { swalFire({ text: getMessage('surface.shape.validate.size.1to23low'), icon: 'error' }) check = false } if (length5 >= length4) { swalFire({ text: getMessage('surface.shape.validate.size.4to5'), icon: 'error' }) check = false } } if (surfaceId === 14) { if (length2 + length3 >= length1) { swalFire({ text: getMessage('surface.shape.validate.size.1to23'), icon: 'error' }) check = false } if (length5 >= length4) { swalFire({ text: getMessage('surface.shape.validate.size.4to5'), icon: 'error' }) check = false } } } return check } //면형상 가져오기 const getSurfaceShape = (surfaceId, pointer, lengths) => { let points = [] const { length1, length2, length3, length4, length5 } = lengths switch (surfaceId) { case 1: { let newLength2 = length2 if (length3 !== 0) { newLength2 = Math.sqrt(length3 ** 2 - (length1 / 2) ** 2) } points = [ { x: pointer.x, y: pointer.y - parseInt(newLength2) / 2 }, { x: pointer.x - parseInt(length1) / 2, y: pointer.y + parseInt(newLength2) / 2 }, { x: pointer.x + parseInt(length1) / 2, y: pointer.y + parseInt(newLength2) / 2 }, ] break } case 2: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y - length2 / 2 }, { x: pointer.x - length1 / 2, y: pointer.y - length2 / 2 }, ] break } case 3: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length3 / 2, y: pointer.y - length2 / 2 }, { x: pointer.x - length3 / 2, y: pointer.y - length2 / 2 }, ] break } case 4: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y - length2 / 2 }, ] break } case 5: { points = [ { x: pointer.x - length1 / 2, y: pointer.y - length3 / 2 }, { x: pointer.x - length1 / 2, y: pointer.y + length3 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length3 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length3 / 2 - length2 }, ] break } case 6: { const angleInRadians = Math.asin(length2 / length3) points = [ { x: pointer.x - length1 / 2, y: pointer.y + length2 / 2 }, { x: pointer.x - length1 / 2 + length3 * Math.cos(angleInRadians), y: pointer.y + length2 / 2 - length3 * Math.sin(angleInRadians), }, { x: pointer.x + length1 / 2 + length3 * Math.cos(angleInRadians), y: pointer.y + length2 / 2 - length3 * Math.sin(angleInRadians), }, { x: pointer.x + length1 / 2, y: pointer.y + length2 / 2 }, ] break } case 7: { points = [ { x: pointer.x - (length1 + length2 + length3) / 2, y: pointer.y - (length4 + length5) / 2 }, { x: pointer.x - (length1 + length2 + length3) / 2, y: pointer.y + (length4 + length5) / 2 }, { x: pointer.x - (length1 + length2 + length3) / 2 + length1, y: pointer.y + (length4 + length5) / 2 }, { x: pointer.x - (length1 + length2 + length3) / 2 + length1, y: pointer.y + (length4 + length5) / 2 - length5, }, { x: pointer.x - (length1 + length2 + length3) / 2 + length1 + length2, y: pointer.y + (length4 + length5) / 2 - length5, }, { x: pointer.x - (length1 + length2 + length3) / 2 + length1 + length2, y: pointer.y + (length4 + length5) / 2 - length5 + length5, }, { x: pointer.x - (length1 + length2 + length3) / 2 + length1 + length2 + length3, y: pointer.y + (length4 + length5) / 2 - length5 + length5, }, { x: pointer.x - (length1 + length2 + length3) / 2 + length1 + length2 + length3, y: pointer.y + (length4 + length5) / 2 - length5 + length5 - (length4 + length5), }, ] break } case 8: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length3 }, { x: pointer.x - length1 / 2 + length1 - (length1 - length2), y: pointer.y + length4 / 2 - length3 }, { x: pointer.x - length1 / 2 + length1 - (length1 - length2), y: pointer.y + length4 / 2 - length3 + (length3 - length4), }, { x: pointer.x - length1 / 2 + length1 - (length1 - length2) - length2, y: pointer.y + length4 / 2 - length3 + (length3 - length4), }, ] break } case 9: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length4 }, { x: pointer.x - length1 / 2 + length1 - length3, y: pointer.y + length4 / 2 - length4 }, { x: pointer.x - length1 / 2 + length1 - length3, y: pointer.y + length4 / 2 - length4 + (length4 - length5), }, { x: pointer.x - length1 / 2 + length1 - length3 - length2, y: pointer.y + length4 / 2 - length4 + (length4 - length5), }, ] break } case 10: { points = [ { x: pointer.x + length1 / 2 - length1, y: pointer.y + length4 / 2 - length5 }, { x: pointer.x + length1 / 2 - length1, y: pointer.y + length4 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length4 / 2 }, { x: pointer.x + length1 / 2 - length1 + length2 + length3, y: pointer.y + length4 / 2 - length5 - (length4 - length5), }, { x: pointer.x + length1 / 2 - length1 + length2, y: pointer.y + length4 / 2 - length5 - (length4 - length5), }, { x: pointer.x + length1 / 2 - length1 + length2, y: pointer.y + length4 / 2 - length5 }, ] break } case 11: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length5 }, { x: pointer.x - length1 / 2 + length1 - length2, y: pointer.y + length4 / 2 - length5 }, { x: pointer.x - length1 / 2 + length1 - length2, y: pointer.y + length4 / 2 - length5 - (length4 - length5), }, { x: pointer.x - length1 / 2 + length1 - length2 - length3, y: pointer.y + length4 / 2 - length5 - (length4 - length5), }, ] break } case 12: { const leftHypotenuse = Math.sqrt(((length1 - length2) / 2) ** 2 + length3 ** 2) const rightHypotenuse = (length4 / length3) * leftHypotenuse const leftAngle = Math.acos((length1 - length2) / 2 / leftHypotenuse) points = [ { x: pointer.x - length1 / 2 + leftHypotenuse * Math.cos(leftAngle), y: pointer.y + length3 / 2 - leftHypotenuse * Math.sin(leftAngle), }, { x: pointer.x - length1 / 2, y: pointer.y + length3 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length3 / 2 }, { x: pointer.x + length1 / 2 - rightHypotenuse * Math.cos(leftAngle), y: pointer.y + length3 / 2 - rightHypotenuse * Math.sin(leftAngle), }, { x: pointer.x + length1 / 2 - rightHypotenuse * Math.cos(leftAngle) - length2, y: pointer.y + length3 / 2 - rightHypotenuse * Math.sin(leftAngle), }, ] break } case 13: { const pointsArray = [ { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, ] const tmpPolygon = new QPolygon( [ { x: 0, y: length3 }, { x: length1 - length2, y: length3 }, { x: (length1 - length2) / 2, y: length3 - length3 }, ], { fill: 'transparent', stroke: 'black', //black strokeWidth: 1, selectable: false, fontSize: 0, }, ) const coord = getIntersectionPoint(tmpPolygon.lines[1].startPoint, tmpPolygon.lines[2].startPoint, length3 - length4) const scale = (length1 - length2) / coord.x tmpPolygon.set({ scaleX: scale }) tmpPolygon.setViewLengthText(false) pointsArray[0].x = 0 pointsArray[0].y = length3 //바닥면부터 시작하게 pointsArray[1].x = pointsArray[0].x + length1 pointsArray[1].y = pointsArray[0].y pointsArray[2].x = pointsArray[1].x pointsArray[2].y = pointsArray[1].y - length4 pointsArray[3].x = pointsArray[2].x - length2 pointsArray[3].y = pointsArray[2].y pointsArray[4].x = tmpPolygon.getCurrentPoints()[2].x pointsArray[4].y = tmpPolygon.getCurrentPoints()[2].y points = [ { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2, }, { x: pointer.x + length1 / 2, y: pointer.y + length4 / 2, }, { x: pointer.x + length1 / 2, y: pointer.y - length4 / 2, }, { x: pointer.x - (length2 - length1 / 2), y: pointer.y - length4 / 2, }, { x: pointer.x - length1 / 2 + pointsArray[4].x, y: pointer.y - length3 + length4 / 2, }, ] break } case 14: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 - length4 }, { x: pointer.x - length1 / 2, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length2, y: pointer.y + length4 / 2 }, { x: pointer.x - length1 / 2 + length2 + (length1 - length2 - length3) / 2, y: pointer.y + length4 / 2 - length4 + length5, }, { x: pointer.x - length1 / 2 + length1 - length3, y: pointer.y + length4 / 2 - length4 + length4 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length4 + length4 }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length4 / 2 - length4 }, ] break } case 15: { points = [ { x: pointer.x - length1 / 2, y: pointer.y + length2 - length2 / 2 - length3 }, { x: pointer.x - length1 / 2, y: pointer.y + length2 - length2 / 2 }, { x: pointer.x + length1 / 2, y: pointer.y + length2 - length2 / 2 - length3 + length3 }, { x: pointer.x + length1 / 2, y: pointer.y + length2 - length2 / 2 - length3 }, { x: pointer.x, y: pointer.y + length2 - length2 / 2 - length3 - (length2 - length3) }, ] break } case 16: { points = [ { x: pointer.x - length1 / 2 + (length1 - length2) / 2, y: pointer.y + length3 / 2 - (length3 - length4) - length4, }, { x: pointer.x - length1 / 2 + (length1 - length2) / 2, y: pointer.y + length3 / 2 - (length3 - length4), }, { x: pointer.x - length1 / 2, y: pointer.y + length3 / 2, }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length3 / 2, }, { x: pointer.x - length1 / 2 + (length1 - length2) / 2 + length2, y: pointer.y + length3 / 2 - (length3 - length4) - length4 + length4, }, { x: pointer.x - length1 / 2 + (length1 - length2) / 2 + length2, y: pointer.y + length3 / 2 - (length3 - length4) - length4, }, ] break } case 17: { const angle = (Math.asin(length3 / length4) * 180) / Math.PI // 높이와 빗변으로 먼저 각도구하기 const topL = (length1 - length2) / 2 / Math.cos((angle * Math.PI) / 180) // 꺽이는부분 윗쪽 길이 points = [ { x: pointer.x - length1 / 2, y: pointer.y + length3 / 2, }, { x: pointer.x - length1 / 2 + length1, y: pointer.y + length3 / 2, }, { x: pointer.x - length1 / 2 + length4 * Math.cos(degreesToRadians(angle)) + length2 + topL * Math.cos(degreesToRadians(angle)), y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)) + topL * Math.sin(degreesToRadians(angle)), }, { x: pointer.x - length1 / 2 + length4 * Math.cos(degreesToRadians(angle)) + length2, y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)), }, { x: pointer.x - length1 / 2 + length4 * Math.cos(degreesToRadians(angle)), y: pointer.y + length3 / 2 - length4 * Math.sin(degreesToRadians(angle)), }, ] break } case 18: { const a = Math.sqrt(length3 * length3 - length2 * length2) // 입력된 밑변과 높이 const sinA = a / length3 const angleInRadians = Math.asin(sinA) const angleInDegrees = angleInRadians * (180 / Math.PI) const b = a - length1 / 2 const c = b / Math.tan(angleInRadians) const d = Math.sqrt(b * b + c * c) const newAngleInRadians = (90 - angleInDegrees) * (Math.PI / 180) points = [ { x: pointer.x - (length1 + b) / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length1 / 2 - b / 2, y: pointer.y + length2 / 2 }, { x: pointer.x + length1 / 2 - b / 2 + d * Math.cos(newAngleInRadians), y: pointer.y + length2 / 2 - d * Math.sin(newAngleInRadians), }, { x: pointer.x - (length1 + b) / 2 + length3 * Math.cos(newAngleInRadians), y: pointer.y + length2 / 2 - length3 * Math.sin(newAngleInRadians), }, ] break } } return points } const deleteAllSurfacesAndObjects = async () => { const backgroundImage = await getBackGroundImage({ objectId: currentCanvasPlan.id, planNo: currentCanvasPlan.planNo, }) console.log('🚀 ~ deleteAllSurfacesAndObjects ~ backgroundImage:', backgroundImage) swalFire({ text: getMessage('batch.canvas.delete.all'), type: 'confirm', confirmFn: () => { canvas.clear() if (backgroundImage) { fabric.Image.fromURL(`${backgroundImage.path}`, function (img) { console.log('🚀 ~ img:', img) img.set({ left: 0, top: 0, width: img.width, height: img.height, name: 'backGroundImage', selectable: false, hasRotatingPoint: false, // 회전 핸들 활성화 lockMovementX: false, lockMovementY: false, lockRotation: false, lockScalingX: false, lockScalingY: false, }) // image = img canvas?.add(img) canvas?.sendToBack(img) canvas?.renderAll() // setBackImg(img) }) } resetOuterLinePoints() resetPlacementShapeDrawingPoints() fetchSettings() swalFire({ text: getMessage('plan.message.delete') }) }, // denyFn: () => { // swalFire({ text: '취소되었습니다.', icon: 'error' }) // }, }) } const findAllChildren = (parentId) => { let allChildren = [] // 직계 자식 객체들 찾기 const directChildren = canvas.getObjects().filter((obj) => obj.parentId === parentId) directChildren.forEach((child) => { allChildren.push(child) // 현재 자식 추가 // 자식이 그룹인 경우 if (child.type === 'group') { // 그룹 내부의 객체들 추가 child.getObjects().forEach((groupItem) => { allChildren.push(groupItem) // 그룹 내부 객체의 자식들도 찾기 const nestedChildren = findAllChildren(groupItem.id) allChildren.push(...nestedChildren) }) } // 현재 자식의 하위 자식들 찾기 const childrenOfChild = findAllChildren(child.id) allChildren.push(...childrenOfChild) }) // 중복 제거하여 반환 return [...new Set(allChildren)] } const findGroupObjects = (parentId) => { let groupObjectsArray = [] // 직계 자식 객체들 찾기 const directChildren = canvas.getObjects().filter((obj) => obj.parentId === parentId) // 각 자식 객체에 대해 처리 directChildren.forEach((child) => { groupObjectsArray.push(child) // 현재 자식 추가 // 자식이 그룹인 경우 그룹 내부 객체들도 처리 if (child.type === 'group') { child.getObjects().forEach((groupItem) => { // 그룹 내부 각 아이템의 하위 객체들 찾기 const nestedObjects = findGroupObjects(groupItem.id) groupObjectsArray.push(...nestedObjects) }) } // 일반 자식의 하위 객체들 찾기 const childObjects = findGroupObjects(child.id) groupObjectsArray.push(...childObjects) }) return groupObjectsArray } const moveSurfaceShapeBatch = () => { const roof = canvas.getActiveObject() if (roof) { const childrenObjects = canvas.getObjects().filter((obj) => obj.parentId === roof.id) const selectionArray = [roof, ...childrenObjects] const selection = new fabric.ActiveSelection(selectionArray, { canvas: canvas, draggable: true, lockMovementX: false, // X축 이동 허용 lockMovementY: false, // Y축 이동 허용 originX: 'center', originY: 'center', }) canvas.setActiveObject(selection) addCanvasMouseEventListener('mouse:up', (e) => { canvas.selection = true canvas.discardActiveObject() // 모든 선택 해제 canvas.requestRenderAll() // 화면 업데이트 selection.getObjects().forEach((obj) => { obj.set({ lockMovementX: true, lockMovementY: true, }) obj.setCoords() if (obj.type === 'group') { reGroupObject(obj) } }) canvas.renderAll() roof.fire('polygonMoved') drawDirectionArrow(roof) initEvent() }) } } const reGroupObject = (groupObj) => { groupObj._restoreObjectsState() //이건 실행만 되도 그룹이 변경됨 const reGroupObjects = [] groupObj._objects.forEach((obj) => { const newObj = new QPolygon(obj.getCurrentPoints(), { ...obj, points: obj.getCurrentPoints(), scaleX: 1, scaleY: 1, }) reGroupObjects.push(newObj) canvas.remove(obj) if (obj.direction) { drawDirectionArrow(obj) } }) const reGroup = new fabric.Group(reGroupObjects, { subTargetCheck: true, name: groupObj.name, id: groupObj.id, groupYn: true, parentId: groupObj.parentId, }) canvas?.add(reGroup) canvas?.remove(groupObj) } const resizeSurfaceShapeBatch = (side, target, width, height) => { const originTarget = { ...target } const objectWidth = target.width const objectHeight = target.height const changeWidth = width / 10 / objectWidth const changeHeight = height / 10 / objectHeight let sideX = 'left' let sideY = 'top' //그룹 중심점 변경 if (side === 2) { sideX = 'right' sideY = 'top' } else if (side === 3) { sideX = 'left' sideY = 'bottom' } else if (side === 4) { sideX = 'right' sideY = 'bottom' } //변경 전 좌표 const newCoords = target.getPointByOrigin(sideX, sideY) target.set({ originX: sideX, originY: sideY, left: newCoords.x, top: newCoords.y, }) target.scaleX = changeWidth target.scaleY = changeHeight const currentPoints = target.getCurrentPoints() target.set({ scaleX: 1, scaleY: 1, width: toFixedWithoutRounding(width / 10, 1), height: toFixedWithoutRounding(height / 10, 1), }) //크기 변경후 좌표를 재 적용 const changedCoords = target.getPointByOrigin(originTarget.originX, originTarget.originY) target.set({ originX: originTarget.originX, originY: originTarget.originY, left: changedCoords.x, top: changedCoords.y, }) canvas.renderAll() //면형상 리사이즈시에만 target.fire('polygonMoved') target.points = currentPoints target.fire('modified') setSurfaceShapePattern(target, roofDisplay.column, false, target.roofMaterial, true) if (target.direction) { drawDirectionArrow(target) } target.setCoords() canvas.renderAll() } const changeSurfaceLinePropertyEvent = () => { let tmpLines = [] const roof = canvas.getActiveObject() if (roof) { roof.set({ selectable: false, }) roof.lines.forEach((obj, index) => { const tmpLine = new QLine([obj.x1, obj.y1, obj.x2, obj.y2], { ...obj, stroke: 'rgb(3, 255, 0)', strokeWidth: 8, selectable: true, name: 'lineProperty', lineIndex: index, }) tmpLines.push(tmpLine) canvas.add(tmpLine) }) addCanvasMouseEventListener('mouse:down', (e) => { const selectedLine = e.target if (selectedLine && selectedLine.name !== 'roof') { tmpLines.forEach((line) => { line.set({ stroke: 'rgb(3, 255, 0)', name: 'lineProperty', }) }) selectedLine.set({ stroke: 'red', name: 'selectedLineProperty', }) } else { tmpLines.forEach((line) => { line.set({ stroke: 'rgb(3, 255, 0)', name: 'lineProperty', }) }) } }) canvas.renderAll() } canvas.discardActiveObject() } const changeSurfaceLineProperty = (property, roof) => { if (!property) { swalFire({ text: getMessage('modal.line.property.change'), icon: 'error' }) return } const selectedLine = canvas.getActiveObjects()[0] //배열로 떨어짐 한개만 선택가능 if (selectedLine && selectedLine.name === 'selectedLineProperty') { swalFire({ text: getMessage('modal.line.property.change.confirm'), type: 'confirm', confirmFn: () => { const lineIndex = selectedLine.lineIndex roof.lines[lineIndex].attributes = { ...roof.lines[lineIndex].attributes, type: property.value, } canvas.renderAll() }, }) } else { swalFire({ text: getMessage('modal.line.property.change.unselect'), icon: 'error' }) } } const changeSurfaceLinePropertyReset = (roof) => { const lines = canvas.getObjects().filter((obj) => obj.name === 'lineProperty' || obj.name === 'selectedLineProperty') lines.forEach((line) => { canvas.remove(line) }) if (roof) { roof.set({ selectable: true, }) } canvas?.renderAll() } /** * 면형상 작도시 라인 속성 넣는 로직 * 폴리곤으로 보면 직선방향에 따라 아래쪽인지 윗쪽인지 판단이 가능하다고 생각하여 * south -> 밑면은 무조건 right direction이라 가정하고 작업함 좌우반전시 반대로 그려지는 경우도 생기지만 그럴땐 흐름방향에 따라 최대값(최소값)을 찾아 * 해당 하는 흐름에 맞게 변경함 * @param { } polygon */ //폴리곤, 상하반전, 좌우반전 const changeSurfaceLineType = (polygon) => { const { isXInversion, isYInversion } = polygon //상하반전, 좌우반전 polygon.lines.forEach((line) => { line.attributes.type = LINE_TYPE.WALLLINE.GABLE }) const directionConfig = { south: { evaesDirection: 'right', ridgeDirection: 'left', coord1: 'y1', coord2: 'y2' }, north: { evaesDirection: 'left', ridgeDirection: 'right', coord1: 'y1', coord2: 'y2' }, east: { evaesDirection: 'top', ridgeDirection: 'bottom', coord1: 'x1', coord2: 'x2' }, west: { evaesDirection: 'bottom', ridgeDirection: 'top', coord1: 'x1', coord2: 'x2' }, } const { evaesDirection, ridgeDirection, coord1, coord2 } = directionConfig[polygon.direction] || directionConfig.west polygon.lines.forEach((line) => { if (line[coord1] === line[coord2]) { if (line.direction === evaesDirection) { line.attributes.type = LINE_TYPE.WALLLINE.EAVES } else if (line.direction === ridgeDirection) { line.attributes.type = LINE_TYPE.SUBLINE.RIDGE } } }) /** * 진짜 처마 라인인지 확인하는 로직 -> 특정 모양에 따라 처마가 없는 경우가 있는데 위에 로직으로는 * 용마루도 처마로 만들어서 재보정 */ //직선 찾는 로직 const maxLine = polygon.lines.filter((line) => line[coord1] === line[coord2]) if (maxLine.length > 0) { const maxLineSorted = maxLine.reduce((a, b) => { return (polygon.direction === 'south' || polygon.direction === 'east' ? b : a)[coord1] > (polygon.direction === 'south' || polygon.direction === 'east' ? a : b)[coord1] ? b : a }) //정렬된 폴리곤이 아니면(대각선이 존재하는 폴리곤일때) if (!polygon.isSortedPoints) { //좌우 반전을 했으면 반대로 정의함 if (isYInversion || isXInversion) { polygon.lines.forEach((line) => { if (line.attributes.type === LINE_TYPE.WALLLINE.EAVES) { line.attributes.type = LINE_TYPE.SUBLINE.RIDGE } else if (line.attributes.type === LINE_TYPE.SUBLINE.RIDGE) { line.attributes.type = LINE_TYPE.WALLLINE.EAVES } }) } } if (maxLine.length === 1) { const maxLineCoord = polygon.lines.reduce((a, b) => { return (polygon.direction === 'south' || polygon.direction === 'east' ? b : a)[coord1] > (polygon.direction === 'south' || polygon.direction === 'east' ? a : b)[coord1] ? b : a }) const isRealEavesLine = polygon.lines.filter((line) => line.attributes.type === LINE_TYPE.WALLLINE.EAVES) if (isRealEavesLine.length > 0) { isRealEavesLine.forEach((line) => { if (polygon.direction === 'south' || polygon.direction === 'north') { const targetCoord = polygon.direction === 'south' ? Math.max(maxLineCoord.y1, maxLineCoord.y2) : Math.min(maxLineCoord.y1, maxLineCoord.y2) const realLineCoord = polygon.direction === 'south' ? Math.max(line.y1, line.y2) : Math.min(line.y1, line.y2) if (targetCoord !== realLineCoord) { line.attributes.type = LINE_TYPE.SUBLINE.RIDGE } } else if (polygon.direction === 'east' || polygon.direction === 'west') { const targetCoord = polygon.direction === 'east' ? Math.max(maxLineCoord.x1, maxLineCoord.x2) : Math.min(maxLineCoord.x1, maxLineCoord.x2) const realLineCoord = polygon.direction === 'east' ? Math.max(line.x1, line.x2) : Math.min(line.x1, line.x2) if (targetCoord !== realLineCoord) { line.attributes.type = LINE_TYPE.SUBLINE.RIDGE } } }) } } } } function findCentroid(points) { let sumX = 0, sumY = 0 for (let i = 0; i < points.length; i++) { sumX += points[i].x sumY += points[i].y } return { x: sumX / points.length, y: sumY / points.length } } // 도형의 포인트를 왼쪽부터 반시계 방향으로 정렬하는 함수 /** * 다각형의 점들을 시계 반대 방향으로 정렬하는 함수 * @param {Array} points - {x, y} 좌표 객체 배열 * @param {Object} startPoint - 시작점 (제공되지 않으면 가장 왼쪽 아래 점을 사용) * @returns {Array} 시계 반대 방향으로 정렬된 점들의 배열 */ function orderPointsCounterClockwise(points, startPoint = null) { if (points.length <= 3) { return points // 점이 3개 이하면 이미 다각형의 모든 점이므로 그대로 반환 } // 시작점이 제공되지 않았다면 가장 왼쪽 아래 점을 찾음 let start = startPoint if (!start) { start = points[0] for (let i = 1; i < points.length; i++) { if (points[i].x < start.x || (points[i].x === start.x && points[i].y < start.y)) { start = points[i] } } } // 다각형의 중심점 계산 let centerX = 0, centerY = 0 for (let i = 0; i < points.length; i++) { centerX += points[i].x centerY += points[i].y } centerX /= points.length centerY /= points.length // 시작점에서 시계 반대 방향으로 각도 계산 let angles = [] for (let i = 0; i < points.length; i++) { // 시작점은 제외 if (points[i] === start) continue // 시작점을 기준으로 각 점의 각도 계산 let angle = Math.atan2(points[i].y - start.y, points[i].x - start.x) // 각도가 음수면 2π를 더해 0~2π 범위로 변환 if (angle < 0) angle += 2 * Math.PI angles.push({ point: points[i], angle: angle, }) } // 각도에 따라 정렬 (시계 반대 방향) angles.sort((a, b) => a.angle - b.angle) // 정렬된 배열 생성 (시작점을 첫 번째로) let orderedPoints = [start] for (let i = 0; i < angles.length; i++) { orderedPoints.push(angles[i].point) } return orderedPoints } /** * 특정 점에서 시작하여 시계 반대 방향으로 다음 점을 찾는 함수 * @param {Object} currentPoint - 현재 점 {x, y} * @param {Array} points - 모든 점들의 배열 * @param {Array} visited - 방문한 점들의 인덱스 배열 * @param {Object} prevVector - 이전 벡터 방향 (첫 호출에서는 null) * @returns {Object} 다음 점의 인덱스와 객체 */ function findNextCounterClockwisePoint(currentPoint, points, visited, prevVector = null) { let minAngle = Infinity let nextIndex = -1 // 이전 벡터가 없으면 (첫 점인 경우) 아래쪽을 향하는 벡터 사용 if (!prevVector) { prevVector = { x: 0, y: -1 } } for (let i = 0; i < points.length; i++) { // 이미 방문했거나 현재 점이면 건너뜀 if (visited.includes(i) || (points[i].x === currentPoint.x && points[i].y === currentPoint.y)) { continue } // 현재 점에서 다음 후보 점으로의 벡터 let vector = { x: points[i].x - currentPoint.x, y: points[i].y - currentPoint.y, } // 벡터의 크기 let magnitude = Math.sqrt(vector.x * vector.x + vector.y * vector.y) // 단위 벡터로 정규화 vector.x /= magnitude vector.y /= magnitude // 이전 벡터와 현재 벡터 사이의 각도 계산 (내적 사용) let dotProduct = prevVector.x * vector.x + prevVector.y * vector.y let crossProduct = prevVector.x * vector.y - prevVector.y * vector.x // 각도 계산 (atan2 사용) let angle = Math.atan2(crossProduct, dotProduct) // 시계 반대 방향으로 가장 작은 각도를 가진 점 찾기 // 각도가 음수면 2π를 더해 0~2π 범위로 변환 if (angle < 0) angle += 2 * Math.PI if (angle < minAngle) { minAngle = angle nextIndex = i } } return nextIndex !== -1 ? { index: nextIndex, point: points[nextIndex] } : null } /** * 다각형의 점들을 시계 반대 방향으로 추적하는 함수 * @param {Array} points - {x, y} 좌표 객체 배열 * @param {Object} startPoint - 시작점 (제공되지 않으면 가장 왼쪽 아래 점을 사용) * @returns {Array} 시계 반대 방향으로 정렬된 점들의 배열 */ function tracePolygonCounterClockwise(points, startPoint = null) { if (points.length <= 3) { return orderPointsCounterClockwise(points, startPoint) } // 시작점이 제공되지 않았다면 가장 왼쪽 아래 점을 찾음 let startIndex = 0 if (!startPoint) { for (let i = 1; i < points.length; i++) { if (points[i].x < points[startIndex].x || (points[i].x === points[startIndex].x && points[i].y < points[startIndex].y)) { startIndex = i } } startPoint = points[startIndex] } else { // 시작점이 제공된 경우 해당 점의 인덱스 찾기 for (let i = 0; i < points.length; i++) { if (points[i].x === startPoint.x && points[i].y === startPoint.y) { startIndex = i break } } } // 결과 배열 초기화 let orderedPoints = [startPoint] let visited = [startIndex] let currentPoint = startPoint let prevVector = null // 모든 점을 방문할 때까지 반복 while (visited.length < points.length) { let next = findNextCounterClockwisePoint(currentPoint, points, visited, prevVector) if (!next) break // 더 이상 찾을 점이 없으면 종료 orderedPoints.push(next.point) visited.push(next.index) // 이전 벡터 업데이트 (현재 점에서 다음 점으로의 벡터) prevVector = { x: next.point.x - currentPoint.x, y: next.point.y - currentPoint.y, } // 벡터 정규화 let magnitude = Math.sqrt(prevVector.x * prevVector.x + prevVector.y * prevVector.y) prevVector.x /= magnitude prevVector.y /= magnitude currentPoint = next.point } return orderedPoints } return { applySurfaceShape, deleteAllSurfacesAndObjects, moveSurfaceShapeBatch, resizeSurfaceShapeBatch, changeSurfaceLinePropertyEvent, changeSurfaceLineProperty, changeSurfaceLinePropertyReset, changeSurfaceLineType, } }