dev #930
@ -1360,11 +1360,10 @@ export default function Estimate({}) {
|
||||
<col />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{session?.storeLvl !== '2' && (
|
||||
<tr>
|
||||
{/* 1차 판매점명 */}
|
||||
<th>{getMessage('estimate.detail.saleStoreId')}</th>
|
||||
<td>{estimateContextState?.firstSaleStoreName}</td>
|
||||
{/* 견적일 */}
|
||||
<th>
|
||||
{getMessage('estimate.detail.estimateDate')} <span className="important">*</span>
|
||||
</th>
|
||||
@ -1374,6 +1373,19 @@ export default function Estimate({}) {
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{session?.storeLvl === '2' && (
|
||||
<tr>
|
||||
<th >
|
||||
{getMessage('estimate.detail.estimateDate')} <span className="important">*</span>
|
||||
</th>
|
||||
<td colSpan={3}>
|
||||
<div className="date-picker" style={{ width: '350px' }}>
|
||||
<SingleDatePicker {...singleDatePickerProps} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
{/* 2차 판매점명 */}
|
||||
<th>{getMessage('estimate.detail.otherSaleStoreId')}</th>
|
||||
|
||||
@ -7,6 +7,7 @@ import { currentAngleTypeSelector, pitchTextSelector } from '@/store/canvasAtom'
|
||||
import { roofsState } from '@/store/roofAtom'
|
||||
import { moduleSelectionDataState, selectedModuleState } from '@/store/selectedModuleOptions'
|
||||
import { forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { logger } from '@/util/logger'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import Swal from 'sweetalert2'
|
||||
import { normalizeDigits } from '@/util/input-utils'
|
||||
@ -175,6 +176,19 @@ const Trestle = forwardRef((props, ref) => {
|
||||
const existingConstruction = constructionList.find((construction) => construction.constTp === trestleState.constTp)
|
||||
if (existingConstruction) {
|
||||
setSelectedConstruction(existingConstruction)
|
||||
// 설치 불가능한 공법이면 강제 해제
|
||||
if (existingConstruction.cvrYn !== 'Y') setCvrChecked(false)
|
||||
if (existingConstruction.snowGdPossYn !== 'Y') setSnowGdChecked(false)
|
||||
const resolvedCvrChecked = existingConstruction.cvrYn === 'Y' ? cvrChecked : false
|
||||
const resolvedSnowGdChecked = existingConstruction.snowGdPossYn === 'Y' ? snowGdChecked : false
|
||||
logger.debug('[Trestle handleConstruction] 복원', {
|
||||
index: constructionList.findIndex((c) => c.constTp === existingConstruction.constTp),
|
||||
constNm: existingConstruction.constNm ?? existingConstruction.constTp,
|
||||
'cvrYn (설치가능여부)': existingConstruction.cvrYn,
|
||||
'cvrChecked (사용자선택)': resolvedCvrChecked,
|
||||
'snowGdPossYn (설치가능여부)': existingConstruction.snowGdPossYn,
|
||||
'snowGdChecked (사용자선택)': resolvedSnowGdChecked,
|
||||
})
|
||||
} else if (autoSelectStep === 'construction') {
|
||||
// 자동 선택: 첫 번째 가능한 construction 선택
|
||||
const availableConstructions = constructionList.filter((construction) => construction.constPossYn === 'Y')
|
||||
@ -411,10 +425,24 @@ const Trestle = forwardRef((props, ref) => {
|
||||
},
|
||||
})
|
||||
|
||||
setCvrYn(constructionList[index].cvrYn)
|
||||
setSnowGdPossYn(constructionList[index].snowGdPossYn)
|
||||
setCvrChecked(true)
|
||||
setSnowGdChecked(false)
|
||||
const newCvrYn = constructionList[index].cvrYn
|
||||
const newCvrChecked = newCvrYn === 'Y'
|
||||
const newSnowGdPossYn = constructionList[index].snowGdPossYn
|
||||
const newSnowGdChecked = newSnowGdPossYn === 'Y'
|
||||
|
||||
setCvrYn(newCvrYn)
|
||||
setSnowGdPossYn(newSnowGdPossYn)
|
||||
setCvrChecked(newCvrChecked)
|
||||
setSnowGdChecked(newSnowGdChecked)
|
||||
|
||||
logger.debug('[Trestle handleConstruction]', {
|
||||
index,
|
||||
constNm: constructionList[index].constNm,
|
||||
'cvrYn (설치가능여부)': newCvrYn,
|
||||
'cvrChecked (사용자선택)': newCvrChecked,
|
||||
'snowGdPossYn (설치가능여부)': newSnowGdPossYn,
|
||||
'snowGdChecked (사용자선택)': newSnowGdChecked,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -442,10 +470,10 @@ const Trestle = forwardRef((props, ref) => {
|
||||
...constructionList.find((data) => data.constTp === trestleState.constTp),
|
||||
cvrYn: cvrYn,
|
||||
snowGdPossYn: snowGdPossYn,
|
||||
cvrChecked: cvrChecked,
|
||||
snowGdChecked: snowGdChecked,
|
||||
setupCover: cvrChecked ?? false,
|
||||
setupSnowCover: snowGdChecked ?? false,
|
||||
cvrChecked: cvrYn === 'Y' ? cvrChecked : false,
|
||||
snowGdChecked: snowGdPossYn === 'Y' ? snowGdChecked : false,
|
||||
setupCover: cvrYn === 'Y' ? (cvrChecked ?? false) : false,
|
||||
setupSnowCover: snowGdPossYn === 'Y' ? (snowGdChecked ?? false) : false,
|
||||
},
|
||||
trestleDetail: trestleDetail,
|
||||
}
|
||||
@ -481,10 +509,10 @@ const Trestle = forwardRef((props, ref) => {
|
||||
...constructionList.find((data) => trestleState.constTp === data.constTp),
|
||||
cvrYn,
|
||||
snowGdPossYn,
|
||||
cvrChecked,
|
||||
snowGdChecked,
|
||||
setupCover: cvrChecked ?? false,
|
||||
setupSnowCover: snowGdChecked ?? false,
|
||||
cvrChecked: cvrYn === 'Y' ? cvrChecked : false,
|
||||
snowGdChecked: snowGdPossYn === 'Y' ? snowGdChecked : false,
|
||||
setupCover: cvrYn === 'Y' ? (cvrChecked ?? false) : false,
|
||||
setupSnowCover: snowGdPossYn === 'Y' ? (snowGdChecked ?? false) : false,
|
||||
},
|
||||
trestleDetail: trestleDetail,
|
||||
}
|
||||
@ -861,7 +889,11 @@ const Trestle = forwardRef((props, ref) => {
|
||||
disabled={!cvrYn || cvrYn === 'N'}
|
||||
checked={!cvrYn || cvrYn === 'N' ? false : cvrChecked ?? true}
|
||||
// onChange={() => dispatch({ type: 'SET_TRESTLE_DETAIL', roof: { ...trestleState, cvrChecked: !trestleState.cvrChecked } })}
|
||||
onChange={() => setCvrChecked(!cvrChecked)}
|
||||
onChange={() => {
|
||||
const next = !cvrChecked
|
||||
setCvrChecked(next)
|
||||
logger.debug('[처마커버 체크 변경]', { 'cvrYn (설치가능여부)': cvrYn, 'cvrChecked (사용자선택)': next })
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`ch01`}>{getMessage('modal.module.basic.setting.module.eaves.bar.fitting')}</label>
|
||||
</div>
|
||||
@ -872,7 +904,11 @@ const Trestle = forwardRef((props, ref) => {
|
||||
disabled={!snowGdPossYn || snowGdPossYn === 'N'}
|
||||
checked={snowGdChecked || false}
|
||||
// onChange={() => dispatch({ type: 'SET_TRESTLE_DETAIL', roof: { ...trestleState, snowGdChecked: !trestleState.snowGdChecked } })}
|
||||
onChange={() => setSnowGdChecked(!snowGdChecked)}
|
||||
onChange={() => {
|
||||
const next = !snowGdChecked
|
||||
setSnowGdChecked(next)
|
||||
logger.debug('[눈막이 체크 변경]', { 'snowGdPossYn (설치가능여부)': snowGdPossYn, 'snowGdChecked (사용자선택)': next })
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`ch02`}>{getMessage('modal.module.basic.setting.module.blind.metal.fitting')}</label>
|
||||
</div>
|
||||
|
||||
@ -149,9 +149,9 @@ export default function CircuitTrestleSetting({ id }) {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width: Math.floor(width),
|
||||
height: Math.floor(height),
|
||||
area: Math.floor(width) * Math.floor(height),
|
||||
width: Math.round(width),
|
||||
height: Math.round(height),
|
||||
area: Math.round(width) * Math.round(height),
|
||||
}
|
||||
})
|
||||
|
||||
@ -227,7 +227,7 @@ export default function CircuitTrestleSetting({ id }) {
|
||||
// 인접 모듈 여부 확인
|
||||
const isAdjacent = (current, target) => {
|
||||
const gap = getGap(current, target)
|
||||
return gap >= 0 && gap <= primaryInterval + 1
|
||||
return gap >= 0 && gap <= primaryInterval + 2
|
||||
}
|
||||
|
||||
// 정렬된 모듈 순회
|
||||
@ -238,7 +238,7 @@ export default function CircuitTrestleSetting({ id }) {
|
||||
const directTargets = findTargetModules(current, secondaryInterval)
|
||||
const closestTarget = getClosestTarget(directTargets)
|
||||
|
||||
if (closestTarget && isAdjacent(current, closestTarget) && closestTarget.area > current.area) {
|
||||
if (closestTarget && isAdjacent(current, closestTarget) && closestTarget.area - current.area > 1) {
|
||||
return false // 검증 실패
|
||||
}
|
||||
|
||||
@ -264,7 +264,7 @@ export default function CircuitTrestleSetting({ id }) {
|
||||
})
|
||||
|
||||
const closestHalf = getClosestTarget(findHalfTarget)
|
||||
if (closestHalf && isAdjacent(current, closestHalf) && closestHalf.area > current.area) {
|
||||
if (closestHalf && isAdjacent(current, closestHalf) && closestHalf.area - current.area > 1) {
|
||||
return false // 검증 실패
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { useMasterController } from '@/hooks/common/useMasterController'
|
||||
import { useCommonCode } from '@/hooks/common/useCommonCode'
|
||||
import { moduleSelectionDataState, moduleSelectionInitParamsState, selectedModuleState } from '@/store/selectedModuleOptions'
|
||||
import { isObjectNotEmpty, isEqualObjects } from '@/util/common-utils'
|
||||
import { logger } from '@/util/logger'
|
||||
|
||||
export function useModuleTabContents({ tabIndex, addRoof, setAddedRoofs, roofTab, tempModuleSelectionData, setTempModuleSelectionData }) {
|
||||
const globalPitchText = useRecoilValue(pitchTextSelector) //피치 텍스트
|
||||
@ -285,17 +286,58 @@ export function useModuleTabContents({ tabIndex, addRoof, setAddedRoofs, roofTab
|
||||
selectedConstruction.setupSnowCover = false //눈막이금구 설치 여부
|
||||
selectedConstruction.selectedIndex = index
|
||||
|
||||
logger.debug('[handleConstruction] 공법 선택', {
|
||||
index,
|
||||
constNm: selectedConstruction.constNm,
|
||||
cvrYn: selectedConstruction.cvrYn,
|
||||
snowGdPossYn: selectedConstruction.snowGdPossYn,
|
||||
moduleConstructionSelectionData,
|
||||
})
|
||||
|
||||
setCvrYn(selectedConstruction.cvrYn)
|
||||
setSnowGdPossYn(selectedConstruction.snowGdPossYn)
|
||||
|
||||
//기존에 선택된 데이터가 있으면 체크한다
|
||||
//기존에 선택된 데이터가 있으면 복원
|
||||
if (moduleConstructionSelectionData && moduleConstructionSelectionData.construction) {
|
||||
selectedConstruction.setupCover = moduleConstructionSelectionData.construction.setupCover || false
|
||||
selectedConstruction.setupSnowCover = moduleConstructionSelectionData.construction.setupSnowCover || false
|
||||
setCvrChecked(selectedConstruction.setupCover)
|
||||
setSnowGdChecked(selectedConstruction.setupSnowCover)
|
||||
logger.debug('[handleConstruction] 기존 데이터 복원', {
|
||||
setupCover: selectedConstruction.setupCover,
|
||||
setupSnowCover: selectedConstruction.setupSnowCover,
|
||||
})
|
||||
} else {
|
||||
// 최초 선택: 설치 가능하면 디폴트 체크
|
||||
const defaultCvr = selectedConstruction.cvrYn === 'Y'
|
||||
const defaultSnowGd = selectedConstruction.snowGdPossYn === 'Y'
|
||||
selectedConstruction.setupCover = defaultCvr
|
||||
selectedConstruction.setupSnowCover = defaultSnowGd
|
||||
setCvrChecked(defaultCvr)
|
||||
setSnowGdChecked(defaultSnowGd)
|
||||
logger.debug('[handleConstruction] 최초 선택 디폴트', {
|
||||
defaultCvr,
|
||||
defaultSnowGd,
|
||||
})
|
||||
}
|
||||
|
||||
// 설치 불가능한 공법이면 강제 해제
|
||||
if (selectedConstruction.cvrYn !== 'Y') {
|
||||
selectedConstruction.setupCover = false
|
||||
setCvrChecked(false)
|
||||
logger.debug('[handleConstruction] 처마커버 강제 해제 (cvrYn !== Y)')
|
||||
}
|
||||
if (selectedConstruction.snowGdPossYn !== 'Y') {
|
||||
selectedConstruction.setupSnowCover = false
|
||||
setSnowGdChecked(false)
|
||||
logger.debug('[handleConstruction] 눈막이 강제 해제 (snowGdPossYn !== Y)')
|
||||
}
|
||||
|
||||
logger.debug('[handleConstruction] 최종 상태', {
|
||||
setupCover: selectedConstruction.setupCover,
|
||||
setupSnowCover: selectedConstruction.setupSnowCover,
|
||||
})
|
||||
|
||||
setSelectedConstruction(selectedConstruction)
|
||||
} else {
|
||||
constructionRef.current.forEach((ref) => {
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'
|
||||
import {
|
||||
canvasSettingState,
|
||||
canvasState,
|
||||
currentCanvasPlanState,
|
||||
currentObjectState,
|
||||
globalPitchState,
|
||||
} from '@/store/canvasAtom'
|
||||
import { canvasSettingState, canvasState, currentCanvasPlanState, currentObjectState, globalPitchState } from '@/store/canvasAtom'
|
||||
import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
|
||||
import { getIntersectionPoint } from '@/util/canvas-util'
|
||||
import { getDegreeByChon, getIntersectionPoint } 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 { basicSettingState, roofDisplaySelector } from '@/store/settingAtom'
|
||||
import { usePolygon } from '@/hooks/usePolygon'
|
||||
import { fontSelector } from '@/store/fontAtom'
|
||||
import { slopeSelector } from '@/store/commonAtom'
|
||||
@ -31,10 +25,12 @@ import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useState } from 'react'
|
||||
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||
import { fabric } from 'fabric'
|
||||
|
||||
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
const { getMessage } = useMessage()
|
||||
const { drawDirectionArrow, addPolygon, addLengthText, setPolygonLinesActualSize } = usePolygon()
|
||||
const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet
|
||||
const lengthTextFont = useRecoilValue(fontSelector('lengthText'))
|
||||
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
|
||||
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
|
||||
@ -94,9 +90,76 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
let obj = null
|
||||
let points = []
|
||||
|
||||
// azimuth로부터 direction 결정
|
||||
let direction = 'south'
|
||||
if (azimuth === 'left') direction = 'west'
|
||||
else if (azimuth === 'right') direction = 'east'
|
||||
else if (azimuth === 'up') direction = 'north'
|
||||
|
||||
// after:render 콜백 - 매 렌더링 후 temp 수치 텍스트를 ctx에 직접 그림
|
||||
let tempLengthData = null // { transformedPoints, direction }
|
||||
const afterRenderHandler = () => {
|
||||
if (!tempLengthData) return
|
||||
const ctx = canvas?.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const { tPoints, dir } = tempLengthData
|
||||
const vpt = canvas.viewportTransform
|
||||
const zoom = vpt[0]
|
||||
const oX = vpt[4]
|
||||
const oY = vpt[5]
|
||||
|
||||
ctx.save()
|
||||
ctx.font = `bold ${(lengthTextFont.fontSize.value || 14) * zoom}px sans-serif`
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
tPoints.forEach((start, i) => {
|
||||
const end = tPoints[(i + 1) % tPoints.length]
|
||||
const dx = end.x - start.x
|
||||
const dy = end.y - start.y
|
||||
const planeSize = Math.round(Math.sqrt(dx * dx + dy * dy) * 10)
|
||||
|
||||
let actualSize = planeSize
|
||||
const lineDir = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'bottom' : 'top'
|
||||
if (+roofSizeSet === 1 && globalPitch > 0) {
|
||||
const pitchDeg = getDegreeByChon(globalPitch)
|
||||
const isV = Math.abs(start.x - end.x) < 1
|
||||
const isH = Math.abs(start.y - end.y) < 1
|
||||
const isD = !isH && !isV
|
||||
if (dir === 'south' || dir === 'north') {
|
||||
if (isV) actualSize = Math.round(planeSize / Math.cos((pitchDeg * Math.PI) / 180))
|
||||
else if (isD) {
|
||||
const h = Math.abs(dy) * 10 * Math.tan((pitchDeg * Math.PI) / 180)
|
||||
actualSize = Math.round(Math.sqrt(h * h + planeSize * planeSize))
|
||||
}
|
||||
} else if (dir === 'west' || dir === 'east') {
|
||||
if (isH) actualSize = Math.round(planeSize / Math.cos((pitchDeg * Math.PI) / 180))
|
||||
else if (isD) {
|
||||
const h = Math.abs(dx) * 10 * Math.tan((pitchDeg * Math.PI) / 180)
|
||||
actualSize = Math.round(Math.sqrt(h * h + planeSize * planeSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const displayValue = (+roofSizeSet === 1 ? actualSize : planeSize).toString()
|
||||
let tx = (start.x + end.x) / 2
|
||||
let ty = (start.y + end.y) / 2
|
||||
if (lineDir === 'right') ty += 15
|
||||
else if (lineDir === 'left') ty -= 15
|
||||
else if (lineDir === 'top') tx += 15
|
||||
else tx -= 25
|
||||
|
||||
ctx.fillText(displayValue, tx * zoom + oX, ty * zoom + oY)
|
||||
})
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
//일단 팝업을 가린다
|
||||
|
||||
if (checkSurfaceShape(surfaceId, { length1, length2, length3, length4, length5 })) {
|
||||
canvas?.on('after:render', afterRenderHandler)
|
||||
addCanvasMouseEventListener('mouse:move', (e) => {
|
||||
if (!isDrawing) {
|
||||
return
|
||||
@ -127,7 +190,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
pitch: globalPitch,
|
||||
}
|
||||
|
||||
obj = new QPolygon(points, options)
|
||||
obj = new fabric.Polygon(points, options)
|
||||
|
||||
let imageRotate = 0
|
||||
if (xInversion && !yInversion) {
|
||||
@ -161,12 +224,25 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
obj.setCoords() //좌표 변경 적용
|
||||
|
||||
canvas?.add(obj)
|
||||
|
||||
// 회전/반전이 적용된 실제 좌표를 계산하여 after:render에서 그릴 데이터 설정
|
||||
const matrix = obj.calcTransformMatrix()
|
||||
const tPoints = obj.get('points').map((p) => {
|
||||
const pt = new fabric.Point(p.x - obj.pathOffset.x, p.y - obj.pathOffset.y)
|
||||
return fabric.util.transformPoint(pt, matrix)
|
||||
})
|
||||
tempLengthData = { tPoints, dir: direction }
|
||||
|
||||
canvas?.renderAll()
|
||||
})
|
||||
|
||||
addCanvasMouseEventListener('mouse:down', (e) => {
|
||||
isDrawing = false
|
||||
|
||||
// after:render 핸들러 해제 및 temp 수치 데이터 초기화
|
||||
tempLengthData = null
|
||||
canvas?.off('after:render', afterRenderHandler)
|
||||
|
||||
const { xInversion, yInversion } = surfaceRefs
|
||||
canvas?.remove(obj)
|
||||
|
||||
@ -188,8 +264,12 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
}
|
||||
|
||||
//회전, flip등이 먹은 기준으로 새로생성
|
||||
// const batchSurface = addPolygon(reorderedPoints, {
|
||||
const batchSurface = addPolygon(obj.getCurrentPoints(), {
|
||||
const objMatrix = obj.calcTransformMatrix()
|
||||
const finalPoints = obj.get('points').map((p) => {
|
||||
const pt = new fabric.Point(p.x - obj.pathOffset.x, p.y - obj.pathOffset.y)
|
||||
return fabric.util.transformPoint(pt, objMatrix)
|
||||
})
|
||||
const batchSurface = addPolygon(finalPoints, {
|
||||
fill: 'transparent',
|
||||
stroke: 'red',
|
||||
strokeWidth: 3,
|
||||
@ -231,6 +311,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
// console.log('yInversion', yInversion) //좌우반전
|
||||
|
||||
changeSurfaceLineType(batchSurface)
|
||||
setPolygonLinesActualSize(batchSurface, true)
|
||||
|
||||
if (setIsHidden) setIsHidden(false)
|
||||
})
|
||||
@ -1589,6 +1670,11 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
drawDirectionArrow(currentObject)
|
||||
setTimeout(() => {
|
||||
changeSurfaceLineType(currentObject)
|
||||
// 회전된 배치면뿐 아니라 모든 배치면의 길이를 재계산
|
||||
const allRoofs = canvas.getObjects().filter((o) => o.name === POLYGON_TYPE.ROOF && o.type === 'QPolygon')
|
||||
allRoofs.forEach((roof) => {
|
||||
setPolygonLinesActualSize(roof, true)
|
||||
})
|
||||
currentObject.dirty = true
|
||||
currentObject.setCoords()
|
||||
canvas.renderAll()
|
||||
|
||||
@ -324,7 +324,7 @@ const movingLineFromSkeleton = (roofId, canvas) => {
|
||||
|
||||
|
||||
const cleaned = removeNonOrthogonalPoints(newPoints);
|
||||
console.log(cleaned); // 결과: 4개의 점만 남음 (P1, P2, P3, P4)
|
||||
// console.log(cleaned)
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
@ -380,127 +380,98 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
const hipLines = canvas.getObjects().filter((object) => object.name === 'hip' && object.parentId === roofId) || []
|
||||
const ridgeLines = canvas.getObjects().filter((object) => object.name === 'ridge' && object.parentId === roofId) || []
|
||||
|
||||
//const skeletonLines = [];
|
||||
// 1. 지붕 폴리곤 좌표 전처리
|
||||
// 1. 지붕 폴리곤 유효성 검사
|
||||
const coordinates = preprocessPolygonCoordinates(roof.points)
|
||||
if (coordinates.length < 3) {
|
||||
console.warn('Polygon has less than 3 unique points. Cannot generate skeleton.')
|
||||
return
|
||||
}
|
||||
|
||||
const moveFlowLine = roof.moveFlowLine || 0 // Provide a default value
|
||||
const moveUpDown = roof.moveUpDown || 0 // Provide a default value
|
||||
const moveFlowLine = roof.moveFlowLine || 0
|
||||
const moveUpDown = roof.moveUpDown || 0
|
||||
|
||||
// 닫힌 폴리곤(첫점 = 끝점)인 경우 마지막 중복 점 제거
|
||||
const isClosedPolygon = (points) =>
|
||||
points.length > 1 && isSamePoint(points[0], points[points.length - 1])
|
||||
|
||||
// roofLinePoints : drawRoofPolygon에서 baseLine + 각 변의 offset으로 계산된 실제 처마 외곽선
|
||||
// orderedBaseLinePoints: 외벽선(baseLine) 꼭짓점을 연결 순서대로 정렬
|
||||
const roofLinePoints = isClosedPolygon(roof.points) ? roof.points.slice(0, -1) : roof.points
|
||||
const orderedBaseLinePoints = isClosedPolygon(baseLinePoints) ? baseLinePoints.slice(0, -1) : baseLinePoints
|
||||
|
||||
// baseLine 폴리곤의 중심 계산 (offset 방향 결정용)
|
||||
// 2. 각 baseLine 꼭짓점 → 대응하는 roofLine 꼭짓점 방향벡터 계산
|
||||
// 각 변의 offset이 달라도 방향은 항상 baseLine → roofLine (바깥쪽)
|
||||
const contactData = orderedBaseLinePoints.map((point, index) => {
|
||||
const roofPoint = roofLinePoints[index]
|
||||
if (!roofPoint) return { dx: 0, dy: 0, signDx: 0, signDy: 0 }
|
||||
|
||||
const dx = roofPoint.x - point.x
|
||||
const dy = roofPoint.y - point.y
|
||||
return { dx, dy, signDx: Math.sign(dx), signDy: Math.sign(dy) }
|
||||
})
|
||||
|
||||
// 3. maxStep: 모든 꼭짓점 중 가장 큰 45도 대각 step 값
|
||||
// - 각 꼭짓점의 min(|dx|, |dy|) = 해당 꼭짓점에서 45도 방향으로 확장 가능한 거리
|
||||
// - 전체에서 max를 취해 SkeletonBuilder 입력 다각형이 충분히 확장되도록 보장
|
||||
// - 모든 꼭짓점에 동일한 maxStep을 적용하므로 각 변의 offset이 달라도
|
||||
// SkeletonBuilder 입력은 항상 대칭 다각형이 되어 스켈레톤이 안정적으로 생성됨
|
||||
const maxStep = contactData.reduce((max, data) => {
|
||||
return Math.max(max, Math.min(Math.abs(data.dx), Math.abs(data.dy)))
|
||||
}, 0)
|
||||
|
||||
// baseLine 폴리곤 중심점 (dx 또는 dy가 0인 꼭짓점의 확장 방향 보정에 사용)
|
||||
const centroid = orderedBaseLinePoints.reduce((acc, p) => {
|
||||
acc.x += p.x / orderedBaseLinePoints.length
|
||||
acc.y += p.y / orderedBaseLinePoints.length
|
||||
return acc
|
||||
}, { x: 0, y: 0 })
|
||||
|
||||
// baseLine을 offset만큼 바깥으로 평행이동
|
||||
const offsetLine = (line, index) => {
|
||||
const sp = line.startPoint, ep = line.endPoint
|
||||
let offset = line.attributes?.offset ?? 0
|
||||
// 4. 모든 꼭짓점을 동일한 maxStep으로 45도 대각 방향 확장
|
||||
// - dx 또는 dy가 0인 경우(한 축 이동만 있는 꼭짓점): centroid 기준 바깥 방향으로 보정
|
||||
let roofLineContactPoints = orderedBaseLinePoints.map((point, index) => {
|
||||
const data = contactData[index]
|
||||
|
||||
// 모든 offset이 동일할 때 수치적 안정성을 위해 약간의 변화 추가
|
||||
if (baseLines.every(l => (l.attributes?.offset ?? 0) === offset)) {
|
||||
offset += index * 0.01 // 각 라인에 약간의 차이 추가
|
||||
}
|
||||
|
||||
const edx = ep.x - sp.x, edy = ep.y - sp.y
|
||||
const len = Math.hypot(edx, edy)
|
||||
if (len === 0) return { sp: { ...sp }, ep: { ...ep } }
|
||||
|
||||
// 수직 벡터 계산 (항상 바깥 방향)
|
||||
let nx = -edy / len, ny = edx / len
|
||||
|
||||
// 방향 결정 로직 제거 - 항상 바깥으로 확장
|
||||
// if (nx * (centroid.x - mid.x) + ny * (centroid.y - mid.y) > 0) { nx = -nx; ny = -ny }
|
||||
let signDx = data.signDx
|
||||
let signDy = data.signDy
|
||||
if (signDx === 0) signDx = point.x >= centroid.x ? 1 : -1
|
||||
if (signDy === 0) signDy = point.y >= centroid.y ? 1 : -1
|
||||
|
||||
return {
|
||||
sp: { x: sp.x + nx * offset, y: sp.y + ny * offset },
|
||||
ep: { x: ep.x + nx * offset, y: ep.y + ny * offset }
|
||||
x: point.x + signDx * maxStep,
|
||||
y: point.y + signDy * maxStep
|
||||
}
|
||||
}
|
||||
|
||||
// 두 직선의 교차점
|
||||
const lineIntersect = (a1, a2, b1, b2) => {
|
||||
const denom = (a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x)
|
||||
if (Math.abs(denom) < 0.001) return null
|
||||
const t = ((a1.x - b1.x) * (b1.y - b2.y) - (a1.y - b1.y) * (b1.x - b2.x)) / denom
|
||||
return { x: a1.x + t * (a2.x - a1.x), y: a1.y + t * (a2.y - a1.y) }
|
||||
}
|
||||
|
||||
// 각 꼭짓점에서 인접 두 baseLine의 offset 교차점으로 확장 좌표 계산
|
||||
let changRoofLinePoints = orderedBaseLinePoints.map((point) => {
|
||||
const adjLines = baseLines.filter(line =>
|
||||
isSamePoint(point, line.startPoint) || isSamePoint(point, line.endPoint)
|
||||
)
|
||||
if (adjLines.length < 2) return { ...point }
|
||||
const idx1 = baseLines.indexOf(adjLines[0])
|
||||
const idx2 = baseLines.indexOf(adjLines[1])
|
||||
const oL1 = offsetLine(adjLines[0], idx1), oL2 = offsetLine(adjLines[1], idx2)
|
||||
return lineIntersect(oL1.sp, oL1.ep, oL2.sp, oL2.ep) || { ...point }
|
||||
})
|
||||
|
||||
// 중복 좌표 및 일직선 위의 불필요한 점 제거 (L자 확장 시 사각형으로 단순화)
|
||||
// const simplifyPolygon = (pts) => {
|
||||
// let result = [...pts]
|
||||
// 5. changRoofLinePoints: SkeletonBuilder에 입력할 최종 다각형
|
||||
// - roofLineContactPoints 방향으로 maxContactDistance(= √2 × maxStep) 만큼 확장
|
||||
// - 45도 대각 방향(signDx, signDy 각 ±1)이므로 실제 x·y 이동량은 각 축으로 maxStep
|
||||
const maxContactDistance = Math.hypot(maxStep, maxStep)
|
||||
|
||||
// // 복잡한 형태 보호를 위해 최소 6개 점 유지
|
||||
// if (result.length <= 6) return result
|
||||
let changRoofLinePoints = orderedBaseLinePoints.map((point, index) => {
|
||||
const contactPoint = roofLineContactPoints[index]
|
||||
if (!contactPoint) return point
|
||||
|
||||
// let changed = true
|
||||
// while (changed) {
|
||||
// changed = false
|
||||
// for (let i = result.length - 1; i >= 0; i--) {
|
||||
// const next = result[(i + 1) % result.length]
|
||||
// if (Math.abs(result[i].x - next.x) < 1.0 && Math.abs(result[i].y - next.y) < 1.0) {
|
||||
// result.splice(i, 1)
|
||||
// changed = true
|
||||
// }
|
||||
// }
|
||||
// // 최소 6개 점 유지 확인
|
||||
// if (result.length <= 6) break
|
||||
const dx = contactPoint.x - point.x
|
||||
const dy = contactPoint.y - point.y
|
||||
const len = Math.hypot(dx, dy)
|
||||
if (len === 0 || maxContactDistance === 0) return point
|
||||
|
||||
// for (let i = result.length - 1; i >= 0 && result.length > 3; i--) {
|
||||
// const prev = result[(i - 1 + result.length) % result.length]
|
||||
// const curr = result[i]
|
||||
// const next = result[(i + 1) % result.length]
|
||||
// const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x)
|
||||
// // 복잡한 형태 보호를 위해 임계값 대폭 증가
|
||||
// if (Math.abs(cross) < 50) {
|
||||
// result.splice(i, 1)
|
||||
// changed = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
// changRoofLinePoints = simplifyPolygon(changRoofLinePoints)
|
||||
return {
|
||||
x: point.x + (dx / len) * maxContactDistance,
|
||||
y: point.y + (dy / len) * maxContactDistance
|
||||
}
|
||||
})
|
||||
|
||||
let roofLineContactPoints = [...changRoofLinePoints]
|
||||
|
||||
console.log('baseLinePoints:', orderedBaseLinePoints)
|
||||
console.log('changRoofLinePoints:', changRoofLinePoints)
|
||||
|
||||
//마루이동
|
||||
// 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체
|
||||
if (moveFlowLine !== 0 || moveUpDown !== 0) {
|
||||
const movedPoints = movingLineFromSkeleton(roofId, canvas)
|
||||
roofLineContactPoints = movedPoints
|
||||
changRoofLinePoints = movedPoints
|
||||
}
|
||||
|
||||
// changRoofLinePoints 좌표를 roof.skeletonPoints에 저장 (원본 roof.points는 유지)
|
||||
// changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지)
|
||||
roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y }))
|
||||
|
||||
console.log('points:', changRoofLinePoints)
|
||||
const geoJSONPolygon = toGeoJSON(changRoofLinePoints)
|
||||
|
||||
try {
|
||||
@ -511,7 +482,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
// 스켈레톤 데이터를 기반으로 내부선 생성
|
||||
roof.innerLines = roof.innerLines || []
|
||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
|
||||
console.log("roofInnerLines:::", roof.innerLines);
|
||||
//console.log("roofInnerLines:::", roof.innerLines);
|
||||
// 캔버스에 스켈레톤 상태 저장
|
||||
if (!canvas.skeletonStates) {
|
||||
canvas.skeletonStates = {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user