Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into feature/redo-undo
# Conflicts: # src/hooks/roofcover/useWallLineOffsetSetting.js
This commit is contained in:
commit
2ae3c47f59
@ -2,6 +2,8 @@
|
|||||||
import { useEffect, useState, useContext, useRef } from 'react'
|
import { useEffect, useState, useContext, useRef } from 'react'
|
||||||
import { useMessage } from '@/hooks/useMessage'
|
import { useMessage } from '@/hooks/useMessage'
|
||||||
import { useAxios } from '@/hooks/useAxios'
|
import { useAxios } from '@/hooks/useAxios'
|
||||||
|
import { useRecoilValue } from 'recoil'
|
||||||
|
import { globalLocaleStore } from '@/store/localeAtom'
|
||||||
import Select from 'react-select'
|
import Select from 'react-select'
|
||||||
import { SessionContext } from '@/app/SessionProvider'
|
import { SessionContext } from '@/app/SessionProvider'
|
||||||
import { isEmptyArray, isObjectNotEmpty } from '@/util/common-utils'
|
import { isEmptyArray, isObjectNotEmpty } from '@/util/common-utils'
|
||||||
@ -10,24 +12,35 @@ import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateCon
|
|||||||
export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
const { get } = useAxios()
|
const { get } = useAxios()
|
||||||
|
const globalLocale = useRecoilValue(globalLocaleStore)
|
||||||
const { handleEstimateCopy, estimateContextState } = useEstimateController(planNo, true)
|
const { handleEstimateCopy, estimateContextState } = useEstimateController(planNo, true)
|
||||||
|
|
||||||
const { session } = useContext(SessionContext)
|
const { session } = useContext(SessionContext)
|
||||||
|
|
||||||
|
// storeLvl 기반 동적 라벨
|
||||||
|
const lvl = session?.storeLvl || '1'
|
||||||
|
const nextLvl = Number(lvl) + 1
|
||||||
|
const saleStoreLabel = globalLocale === 'ko'
|
||||||
|
? `${lvl}차 판매점명 / ID`
|
||||||
|
: `${lvl}次販売店名/ID`
|
||||||
|
const otherSaleStoreLabel = globalLocale === 'ko'
|
||||||
|
? `${nextLvl}차+하위 판매점명 / ID`
|
||||||
|
: `${nextLvl}次下位販売店名/ID`
|
||||||
|
|
||||||
const [saleStoreList, setSaleStoreList] = useState([]) // 판매점 리스트
|
const [saleStoreList, setSaleStoreList] = useState([]) // 판매점 리스트
|
||||||
const [favoriteStoreList, setFavoriteStoreList] = useState([]) //즐겨찾기한 판매점목록
|
const [favoriteStoreList, setFavoriteStoreList] = useState([]) //즐겨찾기한 판매점목록
|
||||||
const [showSaleStoreList, setShowSaleStoreList] = useState([]) //보여줄 판매점목록
|
const [showSaleStoreList, setShowSaleStoreList] = useState([]) //보여줄 판매점목록
|
||||||
const [otherSaleStoreList, setOtherSaleStoreList] = useState([])
|
const [otherSaleStoreList, setOtherSaleStoreList] = useState([])
|
||||||
const [originOtherSaleStoreList, setOriginOtherSaleStoreList] = useState([])
|
const [originOtherSaleStoreList, setOriginOtherSaleStoreList] = useState([])
|
||||||
|
|
||||||
const [saleStoreId, setSaleStoreId] = useState('') //선택한 1차점
|
const [saleStoreId, setSaleStoreId] = useState('') //선택한 상위점
|
||||||
const [otherSaleStoreId, setOtherSaleStoreId] = useState('') //선택한 1차점 이외
|
const [saleStoreName, setSaleStoreName] = useState('') //선택한 상위점명
|
||||||
|
const [otherSaleStoreId, setOtherSaleStoreId] = useState('') //선택한 하위점
|
||||||
|
|
||||||
const [sendPlanNo, setSendPlanNo] = useState('1')
|
const [sendPlanNo, setSendPlanNo] = useState('1')
|
||||||
const [copyReceiveUser, setCopyReceiveUser] = useState('')
|
const [copyReceiveUser, setCopyReceiveUser] = useState('')
|
||||||
|
|
||||||
const ref = useRef() //2차점 자동완성 초기화용
|
const ref = useRef() //하위점 자동완성 초기화용
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let url
|
let url
|
||||||
@ -40,8 +53,9 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
if (session.storeLvl === '1') {
|
if (session.storeLvl === '1') {
|
||||||
url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=1&userId=${session?.userId}`
|
url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=1&userId=${session?.userId}`
|
||||||
} else {
|
} else {
|
||||||
//T01 or 1차점만 복사버튼 노출됨으로
|
// 2차+ 판매점: 자기 자신이 상위점, 하위점 목록 조회
|
||||||
// url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=1&userId=${session?.userId}`
|
setSaleStoreId(session?.storeId)
|
||||||
|
url = `/api/object/saleStore/${session?.storeId}/childList?storeLvl=${session?.storeLvl}&userId=${session?.userId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +105,14 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
|
|
||||||
setOtherSaleStoreList(otherList)
|
setOtherSaleStoreList(otherList)
|
||||||
} else {
|
} else {
|
||||||
//T01 or 1차점만 복사버튼 노출됨으로
|
// 2차+ 판매점: 자기 자신은 상위점명으로, 나머지는 하위점 선택 목록으로 설정
|
||||||
|
const myself = res.find((row) => row.saleStoreId === session?.storeId)
|
||||||
|
if (myself) {
|
||||||
|
setSaleStoreName(myself.saleStoreName)
|
||||||
|
}
|
||||||
|
otherList = res.filter((row) => row.saleStoreId !== session?.storeId)
|
||||||
|
setOtherSaleStoreList(otherList)
|
||||||
|
setOriginOtherSaleStoreList(otherList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,7 +140,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1차점 변경 이벤트
|
// 상위점 변경 이벤트
|
||||||
const onSelectionChange = (key) => {
|
const onSelectionChange = (key) => {
|
||||||
if (isObjectNotEmpty(key)) {
|
if (isObjectNotEmpty(key)) {
|
||||||
if (key.saleStoreId === saleStoreId) {
|
if (key.saleStoreId === saleStoreId) {
|
||||||
@ -154,7 +175,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2차점 변경 이벤트
|
// 하위점 변경 이벤트
|
||||||
const onSelectionChange2 = (key) => {
|
const onSelectionChange2 = (key) => {
|
||||||
if (isObjectNotEmpty(key)) {
|
if (isObjectNotEmpty(key)) {
|
||||||
if (key.saleStoreId === otherSaleStoreId) {
|
if (key.saleStoreId === otherSaleStoreId) {
|
||||||
@ -169,7 +190,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//2차점 자동완성 초기화
|
//하위점 자동완성 초기화
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.clearValue()
|
ref.current.clearValue()
|
||||||
@ -198,7 +219,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
<div className="estimate-copy-info-wrap">
|
<div className="estimate-copy-info-wrap">
|
||||||
<div className="estimate-copy-info-item">
|
<div className="estimate-copy-info-item">
|
||||||
<div className="estimate-copy-info-tit">
|
<div className="estimate-copy-info-tit">
|
||||||
{getMessage('estimate.detail.estimateCopyPopup.label.saleStoreId')} <span className="red">*</span>
|
{saleStoreLabel} <span className="red">*</span>
|
||||||
</div>
|
</div>
|
||||||
{session.storeId === 'T01' && (
|
{session.storeId === 'T01' && (
|
||||||
<div className="estimate-copy-info-box">
|
<div className="estimate-copy-info-box">
|
||||||
@ -247,9 +268,17 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
<div className="estimate-copy-id">{saleStoreId}</div>
|
<div className="estimate-copy-id">{saleStoreId}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{session.storeId !== 'T01' && session.storeLvl !== '1' && (
|
||||||
|
<div className="estimate-copy-info-box">
|
||||||
|
<div className="estimate-copy-sel">
|
||||||
|
<input type="text" className="input-light" value={saleStoreName || ''} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="estimate-copy-id">{saleStoreId}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="estimate-copy-info-item">
|
<div className="estimate-copy-info-item">
|
||||||
<div className="estimate-copy-info-tit">{getMessage('estimate.detail.estimateCopyPopup.label.otherSaleStoreId')}</div>
|
<div className="estimate-copy-info-tit">{otherSaleStoreLabel}</div>
|
||||||
<div className="estimate-copy-info-box">
|
<div className="estimate-copy-info-box">
|
||||||
<div className="estimate-copy-sel">
|
<div className="estimate-copy-sel">
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@ -462,6 +462,7 @@ export default function CanvasMenu(props) {
|
|||||||
if (createUser && tempFlg && lockFlg) {
|
if (createUser && tempFlg && lockFlg) {
|
||||||
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
||||||
setAllButtonStyles('none')
|
setAllButtonStyles('none')
|
||||||
|
setCopyButtonStyle('')
|
||||||
} else {
|
} else {
|
||||||
handleButtonStyles(tempFlg, lockFlg, docNo)
|
handleButtonStyles(tempFlg, lockFlg, docNo)
|
||||||
}
|
}
|
||||||
@ -523,6 +524,7 @@ export default function CanvasMenu(props) {
|
|||||||
if (createUser && tempFlg && lockFlg) {
|
if (createUser && tempFlg && lockFlg) {
|
||||||
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
||||||
setAllButtonStyles('none')
|
setAllButtonStyles('none')
|
||||||
|
setCopyButtonStyle('')
|
||||||
} else {
|
} else {
|
||||||
setEstimateContextState({
|
setEstimateContextState({
|
||||||
tempFlg: estimateRecoilState.tempFlg,
|
tempFlg: estimateRecoilState.tempFlg,
|
||||||
@ -714,7 +716,7 @@ export default function CanvasMenu(props) {
|
|||||||
<span className="name">{getMessage('plan.menu.estimate.reset')}</span>
|
<span className="name">{getMessage('plan.menu.estimate.reset')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{estimateRecoilState?.docNo !== null && (sessionState.storeId === 'T01' || sessionState.storeLvl === '1') && (
|
{estimateRecoilState?.docNo !== null && (sessionState.storeId === 'T01' || sessionState.storeLvl) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={{ display: copyButtonStyle }}
|
style={{ display: copyButtonStyle }}
|
||||||
|
|||||||
@ -836,12 +836,9 @@ export default function CircuitTrestleSetting({ id }) {
|
|||||||
pcsItemId: item.itemId,
|
pcsItemId: item.itemId,
|
||||||
pscOptCd: getPcsOptCd(index),
|
pscOptCd: getPcsOptCd(index),
|
||||||
paralQty: serQty.paralQty,
|
paralQty: serQty.paralQty,
|
||||||
|
// 접속함 중복 체크: 동일 itemId는 1개만, 다르면 각각 올림
|
||||||
connections: item.connList?.length
|
connections: item.connList?.length
|
||||||
? [
|
? [...new Map(item.connList.map((conn) => [conn.itemId, { connItemId: conn.itemId, connMaxParalCnt: conn.connMaxParalCnt }])).values()]
|
||||||
{
|
|
||||||
connItemId: item.connList[0].itemId,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
: [],
|
||||||
})
|
})
|
||||||
// return {
|
// return {
|
||||||
|
|||||||
@ -134,9 +134,7 @@ export default function StepUp(props) {
|
|||||||
const moduleIds = targetSurface.modules.map((module) => module.id)
|
const moduleIds = targetSurface.modules.map((module) => module.id)
|
||||||
|
|
||||||
/** 기존 모듈 텍스트 삭제 */
|
/** 기존 모듈 텍스트 삭제 */
|
||||||
canvas
|
;[...canvas.getObjects().filter((obj) => moduleIds.includes(obj.parentId))]
|
||||||
.getObjects()
|
|
||||||
.filter((obj) => moduleIds.includes(obj.parentId))
|
|
||||||
.forEach((text) => canvas.remove(text))
|
.forEach((text) => canvas.remove(text))
|
||||||
|
|
||||||
/** 새로운 모듈 회로 정보 추가 */
|
/** 새로운 모듈 회로 정보 추가 */
|
||||||
@ -223,7 +221,6 @@ export default function StepUp(props) {
|
|||||||
useModuleItemList: props.getUseModuleItemList(),
|
useModuleItemList: props.getUseModuleItemList(),
|
||||||
pcsItemList: getSelectedPcsItemList(),
|
pcsItemList: getSelectedPcsItemList(),
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PCS 접속함 및 옵션 목록 조회 */
|
/** PCS 접속함 및 옵션 목록 조회 */
|
||||||
getPcsConnOptionItemList(params).then((res) => {
|
getPcsConnOptionItemList(params).then((res) => {
|
||||||
if (res?.result.code === 200 && res?.data) {
|
if (res?.result.code === 200 && res?.data) {
|
||||||
@ -411,61 +408,100 @@ export default function StepUp(props) {
|
|||||||
* 주어진 stepUpListData 를 기준으로 캔버스의 모든 module circuit 텍스트를 다시 그린다.
|
* 주어진 stepUpListData 를 기준으로 캔버스의 모든 module circuit 텍스트를 다시 그린다.
|
||||||
* 모든 pcsItem 의 selected serQty 를 순회해 적용한다.
|
* 모든 pcsItem 의 selected serQty 를 순회해 적용한다.
|
||||||
*/
|
*/
|
||||||
const applyCircuitsToCanvas = (stepUpListSrc) => {
|
/**
|
||||||
|
* 모듈에 circuit 텍스트를 추가하는 공통 함수
|
||||||
|
*/
|
||||||
|
const addCircuitTextToModule = (module) => {
|
||||||
|
const targetModule = canvas.getObjects().find((obj) => obj.id === module.uniqueId)
|
||||||
|
if (targetModule && module.circuit !== '' && module.circuit !== null) {
|
||||||
|
const moduleCircuitText = new fabric.Text(module.circuit, {
|
||||||
|
left: targetModule.left + targetModule.width / 2,
|
||||||
|
top: targetModule.top + targetModule.height / 2,
|
||||||
|
fontFamily: circuitNumberText.fontFamily.value,
|
||||||
|
fontWeight: circuitNumberText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal',
|
||||||
|
fontStyle: circuitNumberText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal',
|
||||||
|
fontSize: circuitNumberText.fontSize.value,
|
||||||
|
fill: circuitNumberText.fontColor.value,
|
||||||
|
width: targetModule.width,
|
||||||
|
height: targetModule.height,
|
||||||
|
textAlign: 'center',
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'center',
|
||||||
|
name: 'circuitNumber',
|
||||||
|
parentId: targetModule.id,
|
||||||
|
circuitInfo: module.pcsItemId,
|
||||||
|
visible: isDisplayCircuitNumber,
|
||||||
|
})
|
||||||
|
targetModule.circuit = moduleCircuitText
|
||||||
|
targetModule.pcsItemId = module.pcsItemId
|
||||||
|
targetModule.circuitNumber = module.circuit
|
||||||
|
canvas.add(moduleCircuitText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyCircuitsToCanvas = (stepUpListSrc, mainIdx, subIdx) => {
|
||||||
if (!stepUpListSrc?.[0]?.pcsItemList) return
|
if (!stepUpListSrc?.[0]?.pcsItemList) return
|
||||||
|
|
||||||
/** 모든 모듈 circuit 데이터 초기화 */
|
if (mainIdx >= 1) {
|
||||||
canvas
|
/**
|
||||||
.getObjects()
|
* 우측 PCS (mainIdx >= 1) 클릭 시:
|
||||||
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
* 해당 pcsItem 의 selected serQty 를 찾아서 moduleList 의 circuit 만 갱신한다.
|
||||||
.forEach((module) => {
|
* 좌측 PCS 의 circuit 은 그대로 유지.
|
||||||
|
*/
|
||||||
|
const pcsItem = stepUpListSrc[0].pcsItemList[mainIdx]
|
||||||
|
const sel = pcsItem?.serQtyList?.find((sq) => sq.selected)
|
||||||
|
if (!sel) return
|
||||||
|
|
||||||
|
// 해당 pcsItem 의 moduleList 에 포함된 모듈의 기존 circuit 텍스트만 제거
|
||||||
|
const moduleUniqueIds = new Set()
|
||||||
|
sel.roofSurfaceList.forEach((rs) => {
|
||||||
|
rs.moduleList.forEach((m) => moduleUniqueIds.add(m.uniqueId))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 해당 모듈의 circuit 데이터 초기화 + 기존 텍스트 제거
|
||||||
|
const targetModuleObjs = canvas.getObjects().filter(
|
||||||
|
(obj) => obj.name === POLYGON_TYPE.MODULE && moduleUniqueIds.has(obj.id),
|
||||||
|
)
|
||||||
|
targetModuleObjs.forEach((module) => {
|
||||||
module.circuit = null
|
module.circuit = null
|
||||||
module.circuitNumber = null
|
module.circuitNumber = null
|
||||||
module.pcsItemId = null
|
module.pcsItemId = null
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 기존 circuit 텍스트 객체 모두 제거 */
|
const textsToRemove = [...canvas.getObjects().filter(
|
||||||
canvas
|
(obj) => obj.name === 'circuitNumber' && moduleUniqueIds.has(obj.parentId),
|
||||||
.getObjects()
|
)]
|
||||||
.filter((obj) => obj.name === 'circuitNumber')
|
textsToRemove.forEach((text) => canvas.remove(text))
|
||||||
.forEach((text) => canvas.remove(text))
|
|
||||||
|
|
||||||
stepUpListSrc[0].pcsItemList.forEach((pcsItem) => {
|
// 새 circuit 적용
|
||||||
const sel = pcsItem.serQtyList?.find((sq) => sq.selected)
|
|
||||||
if (!sel) return
|
|
||||||
sel.roofSurfaceList.forEach((roofSurface) => {
|
sel.roofSurfaceList.forEach((roofSurface) => {
|
||||||
const targetSurface = canvas.getObjects().filter((obj) => obj.id === roofSurface.roofSurfaceId)[0]
|
roofSurface.moduleList.forEach((module) => addCircuitTextToModule(module))
|
||||||
if (!targetSurface) return
|
})
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* 좌측 PCS (mainIdx === 0) 클릭 시:
|
||||||
|
* 전체 모듈 circuit 초기화 후 모든 pcsItem 의 selected 를 적용
|
||||||
|
*/
|
||||||
|
canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
|
.forEach((module) => {
|
||||||
|
module.circuit = null
|
||||||
|
module.circuitNumber = null
|
||||||
|
module.pcsItemId = null
|
||||||
|
})
|
||||||
|
|
||||||
roofSurface.moduleList.forEach((module) => {
|
const circuitTexts = [...canvas.getObjects().filter((obj) => obj.name === 'circuitNumber')]
|
||||||
const targetModule = canvas.getObjects().find((obj) => obj.id === module.uniqueId)
|
circuitTexts.forEach((text) => canvas.remove(text))
|
||||||
if (targetModule && module.circuit !== '' && module.circuit !== null) {
|
|
||||||
const moduleCircuitText = new fabric.Text(module.circuit, {
|
stepUpListSrc[0].pcsItemList.forEach((pcsItem) => {
|
||||||
left: targetModule.left + targetModule.width / 2,
|
const sel = pcsItem.serQtyList?.find((sq) => sq.selected)
|
||||||
top: targetModule.top + targetModule.height / 2,
|
if (!sel) return
|
||||||
fontFamily: circuitNumberText.fontFamily.value,
|
sel.roofSurfaceList.forEach((roofSurface) => {
|
||||||
fontWeight: circuitNumberText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal',
|
roofSurface.moduleList.forEach((module) => addCircuitTextToModule(module))
|
||||||
fontStyle: circuitNumberText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal',
|
|
||||||
fontSize: circuitNumberText.fontSize.value,
|
|
||||||
fill: circuitNumberText.fontColor.value,
|
|
||||||
width: targetModule.width,
|
|
||||||
height: targetModule.height,
|
|
||||||
textAlign: 'center',
|
|
||||||
originX: 'center',
|
|
||||||
originY: 'center',
|
|
||||||
name: 'circuitNumber',
|
|
||||||
parentId: targetModule.id,
|
|
||||||
circuitInfo: module.pcsItemId,
|
|
||||||
visible: isDisplayCircuitNumber,
|
|
||||||
})
|
|
||||||
targetModule.circuit = moduleCircuitText
|
|
||||||
targetModule.pcsItemId = module.pcsItemId
|
|
||||||
targetModule.circuitNumber = module.circuit
|
|
||||||
canvas.add(moduleCircuitText)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
setModuleStatisticsData()
|
setModuleStatisticsData()
|
||||||
@ -484,8 +520,7 @@ export default function StepUp(props) {
|
|||||||
setStepUpListData(tempStepUpListData)
|
setStepUpListData(tempStepUpListData)
|
||||||
|
|
||||||
/** PCS 2개 이상 또는 첫번째 PCS 선택 시에만 실행 */
|
/** PCS 2개 이상 또는 첫번째 PCS 선택 시에만 실행 */
|
||||||
const needsRefetch =
|
const needsRefetch = tempStepUpListData[0].pcsItemList.length > 1 && mainIdx === 0
|
||||||
tempStepUpListData[0].pcsItemList.length > 1 && mainIdx === 0
|
|
||||||
|
|
||||||
if (needsRefetch) {
|
if (needsRefetch) {
|
||||||
/** 파워컨디셔너 옵션 조회 요청 파라미터 */
|
/** 파워컨디셔너 옵션 조회 요청 파라미터 */
|
||||||
@ -523,10 +558,10 @@ export default function StepUp(props) {
|
|||||||
const dataArray = Array.isArray(res.data) ? res.data : [res.data]
|
const dataArray = Array.isArray(res.data) ? res.data : [res.data]
|
||||||
const newStepUpListData = formatStepUpListData(dataArray)
|
const newStepUpListData = formatStepUpListData(dataArray)
|
||||||
setStepUpListData(newStepUpListData)
|
setStepUpListData(newStepUpListData)
|
||||||
applyCircuitsToCanvas(newStepUpListData)
|
applyCircuitsToCanvas(newStepUpListData, mainIdx, subIdx)
|
||||||
} else {
|
} else {
|
||||||
swalFire({ text: getMessage('common.message.send.error') })
|
swalFire({ text: getMessage('common.message.send.error') })
|
||||||
applyCircuitsToCanvas(tempStepUpListData)
|
applyCircuitsToCanvas(tempStepUpListData, mainIdx, subIdx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -534,7 +569,7 @@ export default function StepUp(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** API 재조회가 없는 경로: 현재 tempStepUpListData 로 즉시 캔버스 갱신 */
|
/** API 재조회가 없는 경로: 현재 tempStepUpListData 로 즉시 캔버스 갱신 */
|
||||||
applyCircuitsToCanvas(tempStepUpListData)
|
applyCircuitsToCanvas(tempStepUpListData, mainIdx, subIdx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -550,9 +585,20 @@ export default function StepUp(props) {
|
|||||||
pcsItemId: serQty.itemId,
|
pcsItemId: serQty.itemId,
|
||||||
pcsOptCd: seletedOption,
|
pcsOptCd: seletedOption,
|
||||||
paralQty: serQty.paralQty,
|
paralQty: serQty.paralQty,
|
||||||
connections: {
|
// 접속함 중복 체크: 동일 itemId는 1개만, 다르면 각각 올림
|
||||||
connItemId: item.connList[0].itemId,
|
connections: item.connList?.length
|
||||||
},
|
? [
|
||||||
|
...new Map(
|
||||||
|
item.connList.map((conn) => [
|
||||||
|
conn.itemId,
|
||||||
|
{
|
||||||
|
connItemId: conn.itemId,
|
||||||
|
connMaxParalCnt: conn.connMaxParalCnt,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).values(),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -219,7 +219,7 @@ export function useCanvasConfigInitialize() {
|
|||||||
surface.modules = modules.filter((module) => module.surfaceId === surface.id)
|
surface.modules = modules.filter((module) => module.surfaceId === surface.id)
|
||||||
})
|
})
|
||||||
modules.forEach((obj) => {
|
modules.forEach((obj) => {
|
||||||
console.log(obj)
|
//console.log(obj)
|
||||||
obj.set({
|
obj.set({
|
||||||
selectable: true,
|
selectable: true,
|
||||||
lockMovementX: true,
|
lockMovementX: true,
|
||||||
|
|||||||
@ -84,15 +84,25 @@ export function useImgLoader() {
|
|||||||
const originalBg = canvas.backgroundColor
|
const originalBg = canvas.backgroundColor
|
||||||
canvas.backgroundColor = '#ffffff'
|
canvas.backgroundColor = '#ffffff'
|
||||||
|
|
||||||
// CORS 대응: 이미지 오브젝트에 crossOrigin 설정
|
// CORS 대응: 이미지 오브젝트에 crossOrigin 설정 (로드 완료 대기)
|
||||||
canvas.getObjects('image').forEach((obj) => {
|
const imageObjects = canvas.getObjects('image').filter((obj) => obj.getSrc)
|
||||||
if (obj.getSrc) {
|
await Promise.all(
|
||||||
const img = new Image()
|
imageObjects.map(
|
||||||
img.crossOrigin = 'anonymous'
|
(obj) =>
|
||||||
img.src = obj.getSrc()
|
new Promise((resolve) => {
|
||||||
obj.setElement(img)
|
const img = new Image()
|
||||||
}
|
img.crossOrigin = 'anonymous'
|
||||||
})
|
img.onload = () => {
|
||||||
|
obj.setElement(img)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
img.onerror = () => {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
img.src = obj.getSrc()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
|
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import { useLine } from '@/hooks/useLine'
|
|||||||
import { useSwal } from '@/hooks/useSwal'
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
import Big from 'big.js'
|
import Big from 'big.js'
|
||||||
import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
|
||||||
import { outerLineFixState } from '@/store/outerLineAtom'
|
import { outerLineFixState } from '@/store/outerLineAtom'
|
||||||
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
||||||
|
|
||||||
// 외벽선 편집 및 오프셋
|
// 외벽선 편집 및 오프셋
|
||||||
export function useWallLineOffsetSetting(id) {
|
export function useWallLineOffsetSetting(id) {
|
||||||
@ -356,7 +356,10 @@ export function useWallLineOffsetSetting(id) {
|
|||||||
const prevX = currentLine.x1 < currentLine.x2 ? Math.min(currentLine.x1, currentLine.x2) : Math.max(currentLine.x1, currentLine.x2)
|
const prevX = currentLine.x1 < currentLine.x2 ? Math.min(currentLine.x1, currentLine.x2) : Math.max(currentLine.x1, currentLine.x2)
|
||||||
const nextX = currentLine.x1 < currentLine.x2 ? Math.max(currentLine.x1, currentLine.x2) : Math.min(currentLine.x1, currentLine.x2)
|
const nextX = currentLine.x1 < currentLine.x2 ? Math.max(currentLine.x1, currentLine.x2) : Math.min(currentLine.x1, currentLine.x2)
|
||||||
if (arrow1Ref.current === 'up') {
|
if (arrow1Ref.current === 'up') {
|
||||||
currentLine.set({ y1: currentLineMaxY.minus(offsetLength).toNumber(), y2: currentLineMaxY.minus(offsetLength).toNumber() })
|
currentLine.set({
|
||||||
|
y1: currentLineMaxY.minus(offsetLength).toNumber(),
|
||||||
|
y2: currentLineMaxY.minus(offsetLength).toNumber(),
|
||||||
|
})
|
||||||
if (prevVector === currentVector) {
|
if (prevVector === currentVector) {
|
||||||
const point1 = { x: prevX, y: prevLine.y2 }
|
const point1 = { x: prevX, y: prevLine.y2 }
|
||||||
const point2 = { x: prevX, y: currentLine.y1 }
|
const point2 = { x: prevX, y: currentLine.y1 }
|
||||||
@ -391,7 +394,10 @@ export function useWallLineOffsetSetting(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (arrow1Ref.current === 'down') {
|
if (arrow1Ref.current === 'down') {
|
||||||
currentLine.set({ y1: currentLineMaxY.plus(offsetLength).toNumber(), y2: currentLineMaxY.plus(offsetLength).toNumber() })
|
currentLine.set({
|
||||||
|
y1: currentLineMaxY.plus(offsetLength).toNumber(),
|
||||||
|
y2: currentLineMaxY.plus(offsetLength).toNumber(),
|
||||||
|
})
|
||||||
if (prevVector === currentVector) {
|
if (prevVector === currentVector) {
|
||||||
const point1 = { x: prevX, y: prevLine.y2 }
|
const point1 = { x: prevX, y: prevLine.y2 }
|
||||||
const point2 = { x: prevX, y: currentLine.y1 }
|
const point2 = { x: prevX, y: currentLine.y1 }
|
||||||
@ -429,7 +435,10 @@ export function useWallLineOffsetSetting(id) {
|
|||||||
const prevY = currentLine.y1 < currentLine.y2 ? Math.min(currentLine.y1, currentLine.y2) : Math.max(currentLine.y1, currentLine.y2)
|
const prevY = currentLine.y1 < currentLine.y2 ? Math.min(currentLine.y1, currentLine.y2) : Math.max(currentLine.y1, currentLine.y2)
|
||||||
const nextY = currentLine.y1 < currentLine.y2 ? Math.max(currentLine.y1, currentLine.y2) : Math.min(currentLine.y1, currentLine.y2)
|
const nextY = currentLine.y1 < currentLine.y2 ? Math.max(currentLine.y1, currentLine.y2) : Math.min(currentLine.y1, currentLine.y2)
|
||||||
if (arrow1Ref.current === 'left') {
|
if (arrow1Ref.current === 'left') {
|
||||||
currentLine.set({ x1: currentLineMaxX.minus(offsetLength).toNumber(), x2: currentLineMaxX.minus(offsetLength).toNumber() })
|
currentLine.set({
|
||||||
|
x1: currentLineMaxX.minus(offsetLength).toNumber(),
|
||||||
|
x2: currentLineMaxX.minus(offsetLength).toNumber(),
|
||||||
|
})
|
||||||
|
|
||||||
if (prevVector === currentVector) {
|
if (prevVector === currentVector) {
|
||||||
const point1 = { x: prevLine.x2, y: prevY }
|
const point1 = { x: prevLine.x2, y: prevY }
|
||||||
@ -465,7 +474,10 @@ export function useWallLineOffsetSetting(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (arrow1Ref.current === 'right') {
|
if (arrow1Ref.current === 'right') {
|
||||||
currentLine.set({ x1: currentLineMaxX.plus(offsetLength).toNumber(), x2: currentLineMaxX.plus(offsetLength).toNumber() })
|
currentLine.set({
|
||||||
|
x1: currentLineMaxX.plus(offsetLength).toNumber(),
|
||||||
|
x2: currentLineMaxX.plus(offsetLength).toNumber(),
|
||||||
|
})
|
||||||
|
|
||||||
if (prevVector === currentVector) {
|
if (prevVector === currentVector) {
|
||||||
const point1 = { x: prevLine.x2, y: prevY }
|
const point1 = { x: prevLine.x2, y: prevY }
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useSwal } from '@/hooks/useSwal'
|
|||||||
import { useMessage } from '@/hooks/useMessage'
|
import { useMessage } from '@/hooks/useMessage'
|
||||||
import { useEvent } from '@/hooks/useEvent'
|
import { useEvent } from '@/hooks/useEvent'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom'
|
import { basicSettingState, corridorDimensionSelector, roofDisplaySelector } from '@/store/settingAtom'
|
||||||
import { usePolygon } from '@/hooks/usePolygon'
|
import { usePolygon } from '@/hooks/usePolygon'
|
||||||
import { fontSelector } from '@/store/fontAtom'
|
import { fontSelector } from '@/store/fontAtom'
|
||||||
import { slopeSelector } from '@/store/commonAtom'
|
import { slopeSelector } from '@/store/commonAtom'
|
||||||
@ -48,6 +48,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
|
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
|
||||||
const { fetchSettings } = useCanvasSetting(false)
|
const { fetchSettings } = useCanvasSetting(false)
|
||||||
const [currentObject, setCurrentObject] = useRecoilState(currentObjectState)
|
const [currentObject, setCurrentObject] = useRecoilState(currentObjectState)
|
||||||
|
const [, setCorridorDimension] = useRecoilState(corridorDimensionSelector)
|
||||||
const [popupId, setPopupId] = useState(uuidv4())
|
const [popupId, setPopupId] = useState(uuidv4())
|
||||||
const { saveSnapshot } = useUndoRedo()
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
@ -1142,6 +1143,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
from: 'surface',
|
from: 'surface',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 리사이즈 후 이전 사이즈 속성 초기화 (새 기하학적 길이로 재계산하기 위해)
|
||||||
|
newPolygon.lines.forEach((line) => {
|
||||||
|
delete line.attributes.planeSize
|
||||||
|
delete line.attributes.actualSize
|
||||||
|
delete line.attributes.isCalculated
|
||||||
|
})
|
||||||
|
|
||||||
// 캔버스에 추가
|
// 캔버스에 추가
|
||||||
canvas.add(newPolygon)
|
canvas.add(newPolygon)
|
||||||
|
|
||||||
@ -1153,9 +1161,29 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
drawDirectionArrow(newPolygon)
|
drawDirectionArrow(newPolygon)
|
||||||
newPolygon.setCoords()
|
newPolygon.setCoords()
|
||||||
changeSurfaceLineType(newPolygon)
|
changeSurfaceLineType(newPolygon)
|
||||||
|
|
||||||
|
// 리사이즈 후 라인 길이 재계산 (setSurfaceShapePattern 내부 호출이 실패할 수 있으므로 명시적으로 재호출)
|
||||||
|
newPolygon.lines.forEach((line) => {
|
||||||
|
delete line.attributes.planeSize
|
||||||
|
delete line.attributes.actualSize
|
||||||
|
delete line.attributes.isCalculated
|
||||||
|
})
|
||||||
|
setPolygonLinesActualSize(newPolygon, true)
|
||||||
|
|
||||||
canvas?.renderAll()
|
canvas?.renderAll()
|
||||||
closeAll()
|
closeAll()
|
||||||
addPopup(popupId, 1, <SizeSetting id={popupId} side={side} target={newPolygon} />)
|
addPopup(popupId, 1, <SizeSetting id={popupId} side={side} target={newPolygon} />)
|
||||||
|
|
||||||
|
// React 리렌더 후에도 actualSize가 유지되도록 최종 업데이트
|
||||||
|
setTimeout(() => {
|
||||||
|
newPolygon.lines.forEach((line) => {
|
||||||
|
delete line.attributes.planeSize
|
||||||
|
delete line.attributes.actualSize
|
||||||
|
delete line.attributes.isCalculated
|
||||||
|
})
|
||||||
|
setPolygonLinesActualSize(newPolygon, true)
|
||||||
|
canvas?.renderAll()
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeSurfaceLinePropertyEvent = () => {
|
const changeSurfaceLinePropertyEvent = () => {
|
||||||
|
|||||||
@ -559,21 +559,108 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
|
|
||||||
console.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`))
|
console.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`))
|
||||||
|
|
||||||
|
// 좌표 유효성 검증
|
||||||
|
const invalidPoints = changRoofLinePoints.filter((p, i) =>
|
||||||
|
p == null || isNaN(p.x) || isNaN(p.y) || !isFinite(p.x) || !isFinite(p.y)
|
||||||
|
)
|
||||||
|
if (invalidPoints.length > 0) {
|
||||||
|
console.error('[SK_ERROR] 유효하지 않은 좌표 발견:', invalidPoints)
|
||||||
|
console.error('[SK_ERROR] 전체 좌표:', JSON.stringify(changRoofLinePoints))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인접 중복점 검출 (거리 < 0.5)
|
||||||
|
for (let i = 0; i < changRoofLinePoints.length; i++) {
|
||||||
|
const curr = changRoofLinePoints[i]
|
||||||
|
const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length]
|
||||||
|
const dist = Math.hypot(next.x - curr.x, next.y - curr.y)
|
||||||
|
if (dist < 0.5) {
|
||||||
|
console.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일직선(collinear) 점 검출
|
||||||
|
for (let i = 0; i < changRoofLinePoints.length; i++) {
|
||||||
|
const prev = changRoofLinePoints[(i - 1 + changRoofLinePoints.length) % changRoofLinePoints.length]
|
||||||
|
const curr = changRoofLinePoints[i]
|
||||||
|
const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length]
|
||||||
|
const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x)
|
||||||
|
if (Math.abs(cross) < 1.0) {
|
||||||
|
console.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다각형 오버 검출: 원본 roof.points와 비교하여 꼭짓점 방향(cross product)이 뒤집혔는지 체크
|
||||||
|
let isOverDetected = false
|
||||||
|
const origPoints = roof.points || []
|
||||||
|
const n = changRoofLinePoints.length
|
||||||
|
console.log('[SK_OVER_DEBUG] origPoints:', origPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
|
||||||
|
console.log('[SK_OVER_DEBUG] changRoofLinePoints:', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
|
||||||
|
console.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => {
|
||||||
|
const o = origPoints[i]
|
||||||
|
if (!o) return null
|
||||||
|
const dist = Math.hypot(p.x - o.x, p.y - o.y)
|
||||||
|
return dist > 1 ? `[${i}] dx=${Math.round(p.x - o.x)} dy=${Math.round(p.y - o.y)}` : null
|
||||||
|
}).filter(Boolean))
|
||||||
|
if (origPoints.length === n && n >= 3) {
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const prevOrig = origPoints[(i - 1 + n) % n]
|
||||||
|
const currOrig = origPoints[i]
|
||||||
|
const nextOrig = origPoints[(i + 1) % n]
|
||||||
|
const crossOrig = (currOrig.x - prevOrig.x) * (nextOrig.y - prevOrig.y) - (currOrig.y - prevOrig.y) * (nextOrig.x - prevOrig.x)
|
||||||
|
|
||||||
|
const prevNew = changRoofLinePoints[(i - 1 + n) % n]
|
||||||
|
const currNew = changRoofLinePoints[i]
|
||||||
|
const nextNew = changRoofLinePoints[(i + 1) % n]
|
||||||
|
const crossNew = (currNew.x - prevNew.x) * (nextNew.y - prevNew.y) - (currNew.y - prevNew.y) * (nextNew.x - prevNew.x)
|
||||||
|
|
||||||
|
if (crossOrig * crossNew < 0) {
|
||||||
|
isOverDetected = true
|
||||||
|
console.error(`[SK_OVER] 꼭짓점[${i}] 방향 뒤집힘! cross: ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}`)
|
||||||
|
console.error(`[SK_OVER] 원본: [${(i-1+n)%n}](${Math.round(prevOrig.x)},${Math.round(prevOrig.y)}) → [${i}](${Math.round(currOrig.x)},${Math.round(currOrig.y)}) → [${(i+1)%n}](${Math.round(nextOrig.x)},${Math.round(nextOrig.y)})`)
|
||||||
|
console.error(`[SK_OVER] 변경: [${(i-1+n)%n}](${Math.round(prevNew.x)},${Math.round(prevNew.y)}) → [${i}](${Math.round(currNew.x)},${Math.round(currNew.y)}) → [${(i+1)%n}](${Math.round(nextNew.x)},${Math.round(nextNew.y)})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지)
|
// changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지)
|
||||||
roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y }))
|
roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y }))
|
||||||
|
|
||||||
const perturbedPoints = perturbPolygonPoints(changRoofLinePoints)
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// [분기] 스켈레톤 입력 포인트 결정
|
||||||
|
// 정상: changRoofLinePoints 그대로 사용 (기존 로직 변경 없음)
|
||||||
|
// 오버: calcOverCorrectedPoints() 별도 함수로 보정 포인트 생성
|
||||||
|
// ※ changRoofLinePoints 자체는 절대 수정하지 않음
|
||||||
|
// ※ 오버 보정 실패 시 changRoofLinePoints 그대로 fallback
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
let skeletonInputPoints = changRoofLinePoints // 정상 경로
|
||||||
|
|
||||||
|
if (isOverDetected) {
|
||||||
|
// [예외] 오버 감지 시 → 별도 함수에서 스켈레톤 전용 보정 포인트 계산
|
||||||
|
try {
|
||||||
|
const corrected = calcOverCorrectedPoints(changRoofLinePoints, origPoints)
|
||||||
|
if (corrected && corrected.length >= 3) {
|
||||||
|
skeletonInputPoints = corrected
|
||||||
|
console.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 보정 실패 시 기존 changRoofLinePoints로 fallback → 기존 동작 보장
|
||||||
|
console.error('[SK_OVER] 보정 실패 → fallback:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const perturbedPoints = perturbPolygonPoints(skeletonInputPoints)
|
||||||
const geoJSONPolygon = toGeoJSON(perturbedPoints)
|
const geoJSONPolygon = toGeoJSON(perturbedPoints)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
|
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
|
||||||
geoJSONPolygon.pop()
|
geoJSONPolygon.pop()
|
||||||
console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
|
console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
|
||||||
|
console.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length)
|
||||||
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
||||||
|
|
||||||
// 스켈레톤 데이터를 기반으로 내부선 생성
|
// 스켈레톤 데이터를 기반으로 내부선 생성
|
||||||
roof.innerLines = roof.innerLines || []
|
roof.innerLines = roof.innerLines || []
|
||||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode)
|
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, isOverDetected)
|
||||||
//console.log("roofInnerLines:::", roof.innerLines);
|
//console.log("roofInnerLines:::", roof.innerLines);
|
||||||
// 캔버스에 스켈레톤 상태 저장
|
// 캔버스에 스켈레톤 상태 저장
|
||||||
if (!canvas.skeletonStates) {
|
if (!canvas.skeletonStates) {
|
||||||
@ -606,7 +693,9 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
|
|
||||||
//console.log('skeleton rendered.', canvas)
|
//console.log('skeleton rendered.', canvas)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('스켈레톤 생성 중 오류 발생:', e)
|
console.error('[SK_ERROR] 스켈레톤 생성 중 오류 발생:', e)
|
||||||
|
console.error('[SK_ERROR] 입력 좌표:', changRoofLinePoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`))
|
||||||
|
console.error('[SK_ERROR] geoJSON:', JSON.stringify(geoJSONPolygon))
|
||||||
if (canvas.skeletonStates) {
|
if (canvas.skeletonStates) {
|
||||||
canvas.skeletonStates[roofId] = false
|
canvas.skeletonStates[roofId] = false
|
||||||
canvas.skeletonStates = {}
|
canvas.skeletonStates = {}
|
||||||
@ -624,7 +713,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
* @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
|
* @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
|
||||||
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
|
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
|
||||||
*/
|
*/
|
||||||
const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOverDetected = false) => {
|
||||||
if (!skeleton?.Edges) return []
|
if (!skeleton?.Edges) return []
|
||||||
const roof = canvas?.getObjects().find((object) => object.id === roofId)
|
const roof = canvas?.getObjects().find((object) => object.id === roofId)
|
||||||
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
|
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
|
||||||
@ -1083,8 +1172,6 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const movedStart = Math.abs(wallBaseLine.x1 - wallLine.x1) > EPSILON || Math.abs(wallBaseLine.y1 - wallLine.y1) > EPSILON
|
const movedStart = Math.abs(wallBaseLine.x1 - wallLine.x1) > EPSILON || Math.abs(wallBaseLine.y1 - wallLine.y1) > EPSILON
|
||||||
const movedEnd = Math.abs(wallBaseLine.x2 - wallLine.x2) > EPSILON || Math.abs(wallBaseLine.y2 - wallLine.y2) > EPSILON
|
const movedEnd = Math.abs(wallBaseLine.x2 - wallLine.x2) > EPSILON || Math.abs(wallBaseLine.y2 - wallLine.y2) > EPSILON
|
||||||
|
|
||||||
@ -1684,7 +1771,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
|
|||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
getMoveUpDownLine()
|
if (!isOverDetected) {
|
||||||
|
getMoveUpDownLine()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2000,6 +2089,114 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine,
|
|||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [오버 전용 예외 처리 함수] calcOverCorrectedPoints
|
||||||
|
*
|
||||||
|
* wallLine이 인접 roofLine을 넘어섰을 때(오버 감지 시)에만 호출된다.
|
||||||
|
* changRoofLinePoints를 직접 수정하지 않고 스켈레톤 전용 보정 포인트를 새로 만들어 반환한다.
|
||||||
|
*
|
||||||
|
* 보정 방법:
|
||||||
|
* 1. 이동된 점(moved) 판별
|
||||||
|
* 2. 이동점이 인접 고정점을 넘어갔으면 → 이동점을 고정점 위치로 클램핑
|
||||||
|
* 3. 클램핑된 점에서 연결된 이동점으로 값 전파
|
||||||
|
* 4. 인접 중복점 제거 (dist < 0.5)
|
||||||
|
* 5. 일직선(collinear) 중간점 제거
|
||||||
|
*
|
||||||
|
* @param {Array<{x,y}>} points - changRoofLinePoints (수정하지 않음, 읽기 전용)
|
||||||
|
* @param {Array<{x,y}>} origPoints - roof.points (원본 폴리곤)
|
||||||
|
* @returns {Array<{x,y}>} 보정된 스켈레톤 입력 포인트 (실패 시 원본 반환)
|
||||||
|
*/
|
||||||
|
const calcOverCorrectedPoints = (points, origPoints) => {
|
||||||
|
const n = points.length
|
||||||
|
const skPoints = points.map(p => ({ x: p.x, y: p.y }))
|
||||||
|
|
||||||
|
// Step 1. 이동된 점 판별 (원본 대비 1px 초과 이동)
|
||||||
|
const moved = skPoints.map((p, i) => Math.hypot(p.x - origPoints[i].x, p.y - origPoints[i].y) > 1)
|
||||||
|
|
||||||
|
// Step 2. 이동점이 인접 고정점을 넘어갔으면 → 고정점 위치로 클램핑
|
||||||
|
const clamped = new Set()
|
||||||
|
const clampToFixed = (movedIdx, fixedIdx) => {
|
||||||
|
let didClamp = false
|
||||||
|
// 원래 수평 연결 (같은 y) → x 클램핑
|
||||||
|
if (Math.abs(origPoints[movedIdx].y - origPoints[fixedIdx].y) < 1) {
|
||||||
|
const origDir = origPoints[movedIdx].x - origPoints[fixedIdx].x
|
||||||
|
const newDir = skPoints[movedIdx].x - skPoints[fixedIdx].x
|
||||||
|
if (origDir * newDir < 0) {
|
||||||
|
console.log(`[SK_OVER] 클램핑: [${movedIdx}].x ${Math.round(skPoints[movedIdx].x)} → ${Math.round(skPoints[fixedIdx].x)}`)
|
||||||
|
skPoints[movedIdx].x = skPoints[fixedIdx].x
|
||||||
|
didClamp = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 원래 수직 연결 (같은 x) → y 클램핑
|
||||||
|
if (Math.abs(origPoints[movedIdx].x - origPoints[fixedIdx].x) < 1) {
|
||||||
|
const origDir = origPoints[movedIdx].y - origPoints[fixedIdx].y
|
||||||
|
const newDir = skPoints[movedIdx].y - skPoints[fixedIdx].y
|
||||||
|
if (origDir * newDir < 0) {
|
||||||
|
console.log(`[SK_OVER] 클램핑: [${movedIdx}].y ${Math.round(skPoints[movedIdx].y)} → ${Math.round(skPoints[fixedIdx].y)}`)
|
||||||
|
skPoints[movedIdx].y = skPoints[fixedIdx].y
|
||||||
|
didClamp = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (didClamp) clamped.add(movedIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (!moved[i]) continue
|
||||||
|
const nextIdx = (i + 1) % n
|
||||||
|
const prevIdx = (i - 1 + n) % n
|
||||||
|
if (!moved[nextIdx]) clampToFixed(i, nextIdx)
|
||||||
|
if (!moved[prevIdx]) clampToFixed(i, prevIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3. 클램핑된 점 → 연결된 미클램핑 이동점으로 값 전파 (반복)
|
||||||
|
let propagated = true
|
||||||
|
while (propagated) {
|
||||||
|
propagated = false
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (!moved[i] || clamped.has(i)) continue
|
||||||
|
const prevIdx = (i - 1 + n) % n
|
||||||
|
const nextIdx = (i + 1) % n
|
||||||
|
if (clamped.has(prevIdx) && Math.abs(origPoints[prevIdx].x - origPoints[i].x) < 1) {
|
||||||
|
skPoints[i].x = skPoints[prevIdx].x; clamped.add(i); propagated = true
|
||||||
|
}
|
||||||
|
if (clamped.has(prevIdx) && Math.abs(origPoints[prevIdx].y - origPoints[i].y) < 1) {
|
||||||
|
skPoints[i].y = skPoints[prevIdx].y; clamped.add(i); propagated = true
|
||||||
|
}
|
||||||
|
if (clamped.has(nextIdx) && Math.abs(origPoints[nextIdx].x - origPoints[i].x) < 1) {
|
||||||
|
skPoints[i].x = skPoints[nextIdx].x; clamped.add(i); propagated = true
|
||||||
|
}
|
||||||
|
if (clamped.has(nextIdx) && Math.abs(origPoints[nextIdx].y - origPoints[i].y) < 1) {
|
||||||
|
skPoints[i].y = skPoints[nextIdx].y; clamped.add(i); propagated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4. 인접 중복점 제거 (dist < 0.5)
|
||||||
|
const deduped = []
|
||||||
|
for (let i = 0; i < skPoints.length; i++) {
|
||||||
|
const next = skPoints[(i + 1) % skPoints.length]
|
||||||
|
if (Math.hypot(next.x - skPoints[i].x, next.y - skPoints[i].y) > 0.5) {
|
||||||
|
deduped.push(skPoints[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5. 일직선(collinear) 중간점 제거
|
||||||
|
const cleaned = []
|
||||||
|
for (let i = 0; i < deduped.length; i++) {
|
||||||
|
const prev = deduped[(i - 1 + deduped.length) % deduped.length]
|
||||||
|
const curr = deduped[i]
|
||||||
|
const next = deduped[(i + 1) % deduped.length]
|
||||||
|
const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x)
|
||||||
|
if (Math.abs(cross) > 1.0) cleaned.push(curr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보정 실패 시(꼭짓점 부족) 원본 반환 → 기존 동작 보장
|
||||||
|
if (cleaned.length >= 3) return cleaned
|
||||||
|
if (deduped.length >= 3) return deduped
|
||||||
|
console.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환')
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 두 점으로 이루어진 선분이 외벽선인지 확인합니다.
|
* 두 점으로 이루어진 선분이 외벽선인지 확인합니다.
|
||||||
* @param {object} p1 - 점1 {x, y}
|
* @param {object} p1 - 점1 {x, y}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user