diff --git a/.gitignore b/.gitignore index f3b61bd7..a235b0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ next-env.d.ts yarn.lock package-lock.json pnpm-lock.yaml -certificates \ No newline at end of file +certificates +.ai \ No newline at end of file diff --git a/package.json b/package.json index 7be9b3f3..676d8f4f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "chart.js": "^4.4.6", "dayjs": "^1.11.13", "env-cmd": "^10.1.0", - "fabric": "^5.3.0", + "fabric": "^5.5.2", "framer-motion": "^11.2.13", "fs": "^0.0.1-security", "iron-session": "^8.0.2", diff --git a/src/app/layout.js b/src/app/layout.js index dbd6c9a1..e19c6bb7 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -58,7 +58,8 @@ export default async function RootLayout({ children }) { pwdInitYn: session.pwdInitYn, custCd: session.custCd, isLoggedIn: session.isLoggedIn, - builderNo: session.builderNo + builderNo: session.builderNo, + custNm: session.custNm } } if (!headerPathname.includes('/login') && !session.isLoggedIn) { diff --git a/src/common/common.js b/src/common/common.js index 757dc0e8..76632014 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -61,6 +61,7 @@ export const LINE_TYPE = { */ DEFAULT: 'default', EAVES: 'eaves', + EAVE_HELP_LINE: 'eaveHelpLine', GABLE: 'gable', GABLE_LEFT: 'gableLeft', //케라바 왼쪽 GABLE_RIGHT: 'gableRight', //케라바 오른쪽 @@ -218,6 +219,9 @@ export const SAVE_KEY = [ 'originColor', 'originWidth', 'originHeight', + 'skeletonLines', + 'skeleton', + 'viewportTransform', ] export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype] diff --git a/src/components/common/draggable/WithDraggable.jsx b/src/components/common/draggable/WithDraggable.jsx index 7ebdf067..fa87910a 100644 --- a/src/components/common/draggable/WithDraggable.jsx +++ b/src/components/common/draggable/WithDraggable.jsx @@ -24,7 +24,7 @@ export default function WithDraggable({ isShow, children, pos = { x: 0, y: 0 }, handleOnDrag(e, data)} - handle= ''//{handle === '' ? '.modal-handle' : handle} //전체 handle + handle="" //{handle === '' ? '.modal-handle' : handle} //전체 handle cancel="input, button, select, textarea, [contenteditable], .sort-select" >
@@ -38,15 +38,18 @@ export default function WithDraggable({ isShow, children, pos = { x: 0, y: 0 }, ) } -function WithDraggableHeader({ title, onClose, children }) { +function WithDraggableHeader({ title, onClose, children, isFold, onFold = null }) { return (

{title}

- {onClose && ( - - )} +
+ {onFold && } + {onClose && ( + + )} +
) } diff --git a/src/components/common/input/CalcInput.jsx b/src/components/common/input/CalcInput.jsx index 32323560..17af010e 100644 --- a/src/components/common/input/CalcInput.jsx +++ b/src/components/common/input/CalcInput.jsx @@ -48,14 +48,23 @@ export const CalculatorInput = forwardRef( const calculator = calculatorRef.current let newDisplayValue = '' + // 소수점 이하 2자리 제한 로직 추가 + const shouldPreventInput = (value) => { + const decimalParts = (value || '').split('.') + return decimalParts.length > 1 && decimalParts[1].length >= 2 + } + if (hasOperation) { // 연산자 이후 숫자 입력 시 if (calculator.currentOperand === '0' || calculator.shouldResetDisplay) { calculator.currentOperand = num.toString() calculator.shouldResetDisplay = false - } else { + }else if (!shouldPreventInput(calculator.currentOperand)) { //소수점 이하2자리 calculator.currentOperand = (calculator.currentOperand || '') + num } + // else { + // calculator.currentOperand = (calculator.currentOperand || '') + num + // } newDisplayValue = calculator.previousOperand + calculator.operation + calculator.currentOperand setDisplayValue(newDisplayValue) } else { @@ -68,7 +77,7 @@ export const CalculatorInput = forwardRef( if (!hasOperation) { onChange(calculator.currentOperand) } - } else { + } else if (!shouldPreventInput(calculator.currentOperand)) { //소수점 이하2자리 calculator.currentOperand = (calculator.currentOperand || '') + num newDisplayValue = calculator.currentOperand setDisplayValue(newDisplayValue) @@ -76,6 +85,14 @@ export const CalculatorInput = forwardRef( onChange(newDisplayValue) } } + // else { + // calculator.currentOperand = (calculator.currentOperand || '') + num + // newDisplayValue = calculator.currentOperand + // setDisplayValue(newDisplayValue) + // if (!hasOperation) { + // onChange(newDisplayValue) + // } + // } } // 커서를 텍스트 끝으로 이동하고 스크롤 처리 diff --git a/src/components/community/modal/QnaRegModal.jsx b/src/components/community/modal/QnaRegModal.jsx index b26536e0..3ab03e71 100644 --- a/src/components/community/modal/QnaRegModal.jsx +++ b/src/components/community/modal/QnaRegModal.jsx @@ -22,7 +22,8 @@ export default function QnaRegModal({ setOpen, setReload, searchValue, selectPag const [sessionState, setSessionState] = useRecoilState(sessionStore) const globalLocaleState = useRecoilValue(globalLocaleStore) const [files, setFiles] = useState([]) - const [qnaData, setQnaData] = useState([]) + //const [qnaData, setQnaData] = useState([]) + const [qnaData, setQnaData] = useState({}) const [closeMdFlg, setCloseMdFlg] = useState(true) const [closeSmFlg, setCloseSmFlg] = useState(true) const [hideSmFlg, setHideSmFlg] = useState(false) @@ -44,6 +45,10 @@ export default function QnaRegModal({ setOpen, setReload, searchValue, selectPag const [isBtnDisable, setIsBtnDisable] = useState(false); const { promiseGet, post, promisePost } = useAxios(globalLocaleState) + useEffect(() => { + console.log('qnaData updated:', qnaData); + }, [qnaData]); + let fileCheck = false; const regPhoneNumber = (e) => { const result = e.target.value @@ -80,14 +85,16 @@ let fileCheck = false; //setQnaData([]) setQnaData({ - ...qnaData, compCd: "5200", siteTpCd: "QC", schNoticeClsCd: "QNA", - regId: sessionState.userId, - storeId: sessionState.userId, - qstMail : sessionState.email - }) + regId: sessionState?.userId || '', + storeId: sessionState?.storeId || '', + qstMail: sessionState?.email || '', + qnaClsLrgCd: '', + qnaClsMidCd: '', + qnaClsSmlCd: '' + }); const codeL = findCommonCode(204200) if (codeL != null) { @@ -119,43 +126,42 @@ let fileCheck = false; } const onChangeQnaTypeM = (e) => { + if (!e?.clCode) return; - if(e === undefined || e === null) return; - const codeS = findCommonCode(204400) - if (codeS != null) { - - let codeList = [] - - codeS.map((item) => { - - if (item.clRefChr1 === e.clCode) { - codeList.push(item); - - } - }) - - - setQnaData({ ...qnaData, qnaClsMidCd: e.clCode }) - setCloseSmFlg(false) - setQnaTypeSmCodeList(codeList) - qnaTypeSmCodeRef.current?.setValue(); - - if(codeList.length > 0) { - setHideSmFlg(false) - }else{ - setHideSmFlg(true) - } - + // 중분류 코드 업데이트 + setQnaData(prevState => ({ + ...prevState, + qnaClsMidCd: e.clCode, + // 소분류는 초기화 (새로 선택하도록) + qnaClsSmlCd: '' + })); + // 소분류 코드 목록 설정 + const codeS = findCommonCode(204400); + if (codeS) { + const filteredCodeList = codeS.filter(item => item.clRefChr1 === e.clCode); + setQnaTypeSmCodeList(filteredCodeList); + // 소분류가 있으면 초기화, 없으면 숨김 + const hasSubCategories = filteredCodeList.length > 0; + setCloseSmFlg(!hasSubCategories); + setHideSmFlg(!hasSubCategories); } else { setHideSmFlg(true) } - } + // 소분류 선택기 초기화 + qnaTypeSmCodeRef.current?.setValue(); + }; + + const onChangeQnaTypeS = (e) => { - if(e === undefined || e === null) return; - setQnaData({ ...qnaData, qnaClsSmlCd:e.clCode}) + if (!e?.clCode) return; + + setQnaData(prevState => ({ + ...prevState, + qnaClsSmlCd: e.clCode + })); } const onFileSave = () => { @@ -356,6 +362,8 @@ let fileCheck = false; {dayjs(new Date()).format('YYYY-MM-DD')} + Customer + {getMessage('qna.reg.header.regUserNm')}* setQnaData({...qnaData, regUserNm: e.target.value })} onBlur={(e) => setQnaData({ ...qnaData, regUserNm: e.target.value })} /> {getMessage('qna.reg.header.regUserTelNo')} - type === LINE_TYPE.WALLLINE.EAVES)) { // 용마루 -- straight-skeleton console.log('용마루 지붕') - drawRidgeRoof(this.id, this.canvas, textMode) - //drawSkeletonRidgeRoof(this.id, this.canvas, textMode); + ///drawRidgeRoof(this.id, this.canvas, textMode) + drawSkeletonRidgeRoof(this.id, this.canvas, textMode); } else if (isGableRoof(types)) { // A형, B형 박공 지붕 console.log('패턴 지붕') @@ -378,9 +379,27 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { const dy = Big(end.y).minus(Big(start.y)) const length = dx.pow(2).plus(dy.pow(2)).sqrt().times(10).round().toNumber() + const direction = getDirectionByPoint(start, end) + + let left, top + + if (direction === 'bottom') { + left = (start.x + end.x) / 2 - 50 + top = (start.y + end.y) / 2 + } else if (direction === 'top') { + left = (start.x + end.x) / 2 + 30 + top = (start.y + end.y) / 2 + } else if (direction === 'left') { + left = (start.x + end.x) / 2 + top = (start.y + end.y) / 2 - 30 + } else if (direction === 'right') { + left = (start.x + end.x) / 2 + top = (start.y + end.y) / 2 + 30 + } + let midPoint - midPoint = new fabric.Point((start.x + end.x) / 2, (start.y + end.y) / 2) + midPoint = new fabric.Point(left, top) const degree = Big(Math.atan2(dy.toNumber(), dx.toNumber())).times(180).div(Math.PI).toNumber() diff --git a/src/components/floor-plan/CanvasFrame.jsx b/src/components/floor-plan/CanvasFrame.jsx index 4b718c05..9441dc7c 100644 --- a/src/components/floor-plan/CanvasFrame.jsx +++ b/src/components/floor-plan/CanvasFrame.jsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useRef } from 'react' -import { useRecoilValue, useResetRecoilState } from 'recoil' +import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' import QContextMenu from '@/components/common/context-menu/QContextMenu' import PanelBatchStatistics from '@/components/floor-plan/modal/panelBatch/PanelBatchStatistics' @@ -11,7 +11,7 @@ import { useCanvas } from '@/hooks/useCanvas' import { usePlan } from '@/hooks/usePlan' import { useContextMenu } from '@/hooks/useContextMenu' import { useCanvasConfigInitialize } from '@/hooks/common/useCanvasConfigInitialize' -import { currentMenuState } from '@/store/canvasAtom' +import { canvasZoomState, currentMenuState } from '@/store/canvasAtom' import { totalDisplaySelector } from '@/store/settingAtom' import { POLYGON_TYPE } from '@/common/common' import { FloorPlanContext } from '@/app/floor-plan/FloorPlanProvider' @@ -32,6 +32,7 @@ import { useEvent } from '@/hooks/useEvent' import { compasDegAtom } from '@/store/orientationAtom' import { hotkeyStore } from '@/store/hotkeyAtom' import { usePopup } from '@/hooks/usePopup' +import { outerLinePointsState } from '@/store/outerLineAtom' export default function CanvasFrame() { const canvasRef = useRef(null) @@ -45,11 +46,13 @@ export default function CanvasFrame() { const totalDisplay = useRecoilValue(totalDisplaySelector) // 집계표 표시 여부 const { setIsGlobalLoading } = useContext(QcastContext) const resetModuleStatisticsState = useResetRecoilState(moduleStatisticsState) + const resetOuterLinePoints = useResetRecoilState(outerLinePointsState) const resetMakersState = useResetRecoilState(makersState) const resetSelectedMakerState = useResetRecoilState(selectedMakerState) const resetSeriesState = useResetRecoilState(seriesState) const resetModelsState = useResetRecoilState(modelsState) const resetCompasDeg = useResetRecoilState(compasDegAtom) + const [zoom, setCanvasZoom] = useRecoilState(canvasZoomState) const resetSelectedModelsState = useResetRecoilState(selectedModelsState) const resetPcsCheckState = useResetRecoilState(pcsCheckState) const { handleModuleSelectionTotal } = useCanvasPopupStatusController() @@ -67,6 +70,13 @@ export default function CanvasFrame() { canvasLoadInit() //config된 상태로 캔버스 객체를 그린다 canvas?.renderAll() // 캔버스를 다시 그립니다. + if (canvas.viewportTransform) { + if (canvas.viewportTransform[0] !== 1) { + setCanvasZoom(Number((canvas.viewportTransform[0] * 100).toFixed(0))) + } + } + canvas.originViewPortTransform = canvas.viewportTransform + if (canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE).length > 0) { setTimeout(() => { setSelectedMenu('module') @@ -129,6 +139,7 @@ export default function CanvasFrame() { const resetRecoilData = () => { // resetModuleStatisticsState() + resetOuterLinePoints() resetMakersState() resetSelectedMakerState() resetSeriesState() diff --git a/src/components/floor-plan/CanvasMenu.jsx b/src/components/floor-plan/CanvasMenu.jsx index 6ddd94c7..97d878a0 100644 --- a/src/components/floor-plan/CanvasMenu.jsx +++ b/src/components/floor-plan/CanvasMenu.jsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil' @@ -25,17 +25,18 @@ import { useCommonUtils } from '@/hooks/common/useCommonUtils' import useMenu from '@/hooks/common/useMenu' import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateController' import { useAxios } from '@/hooks/useAxios' -import { canvasSettingState, canvasState, canvasZoomState, currentMenuState, verticalHorizontalModeState, currentCanvasPlanState } from '@/store/canvasAtom' +import { + canvasSettingState, + canvasState, + canvasZoomState, + currentCanvasPlanState, + currentMenuState, + verticalHorizontalModeState, +} from '@/store/canvasAtom' import { sessionStore } from '@/store/commonAtom' import { outerLinePointsState } from '@/store/outerLineAtom' import { appMessageStore, globalLocaleStore } from '@/store/localeAtom' -import { - addedRoofsState, - basicSettingState, - corridorDimensionSelector, - selectedRoofMaterialSelector, - settingModalFirstOptionsState, -} from '@/store/settingAtom' +import { addedRoofsState, basicSettingState, selectedRoofMaterialSelector, settingModalFirstOptionsState } from '@/store/settingAtom' import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingAtom' import { commonUtilsState } from '@/store/commonUtilsAtom' import { menusState } from '@/store/menuAtom' @@ -51,6 +52,7 @@ import { QcastContext } from '@/app/QcastProvider' import { useRoofFn } from '@/hooks/common/useRoofFn' import { usePolygon } from '@/hooks/usePolygon' import { useTrestle } from '@/hooks/module/useTrestle' + export default function CanvasMenu(props) { const [currentCanvasPlan, setCurrentCanvasPlan] = useRecoilState(currentCanvasPlanState) const { selectedMenu, setSelectedMenu } = props @@ -515,7 +517,10 @@ export default function CanvasMenu(props) { if (createUser === 'T01' && sessionState.storeId !== 'T01') { setAllButtonStyles('none') } else { - setEstimateContextState({ tempFlg: estimateRecoilState.tempFlg, lockFlg: estimateRecoilState.lockFlg }) + setEstimateContextState({ + tempFlg: estimateRecoilState.tempFlg, + lockFlg: estimateRecoilState.lockFlg, + }) handleButtonStyles(estimateRecoilState.tempFlg, estimateRecoilState.lockFlg, estimateContextState.docNo) } } diff --git a/src/components/floor-plan/modal/Slope.jsx b/src/components/floor-plan/modal/Slope.jsx index df4a25d7..849920bf 100644 --- a/src/components/floor-plan/modal/Slope.jsx +++ b/src/components/floor-plan/modal/Slope.jsx @@ -4,6 +4,7 @@ import { globalPitchState, pitchSelector, pitchTextSelector } from '@/store/canv import { useRecoilState } from 'recoil' import { useRef } from 'react' import { usePopup } from '@/hooks/usePopup' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Slope({ id, pos = { x: 50, y: 230 } }) { const { getMessage } = useMessage() @@ -22,7 +23,19 @@ export default function Slope({ id, pos = { x: 50, y: 230 } }) { {getMessage('slope')}
- + {/**/} +
{pitchText}
diff --git a/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx index 938b5244..d7696a84 100644 --- a/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx +++ b/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx @@ -8,6 +8,7 @@ import { currentObjectState } from '@/store/canvasAtom' import { useAuxiliaryDrawing } from '@/hooks/roofcover/useAuxiliaryDrawing' import { useSwal } from '@/hooks/useSwal' import { normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function AuxiliaryEdit(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) @@ -66,7 +67,19 @@ export default function AuxiliaryEdit(props) {

{getMessage('length')}

- setVerticalSize(normalizeDigits(e.target.value))} /> + {/* setVerticalSize(normalizeDigits(e.target.value))} />*/} + setVerticalSize(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -88,7 +101,19 @@ export default function AuxiliaryEdit(props) {
- setHorizonSize(normalizeDigits(e.target.value))} /> + {/* setHorizonSize(normalizeDigits(e.target.value))} />*/} + setHorizonSize(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
diff --git a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx index 5a9cde6f..40fb9f76 100644 --- a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx +++ b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx @@ -8,19 +8,21 @@ import { useEffect, useState } from 'react' import Big from 'big.js' import { calcLineActualSize, calcLinePlaneSize } from '@/util/qpolygon-utils' import { normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function AuxiliarySize(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) const { id, pos = contextPopupPosition } = props const [checkedRadio, setCheckedRadio] = useState(null) - const [value1, setValue1] = useState(null) - const [value2, setValue2] = useState(null) + const [value1, setValue1] = useState('') + const [value2, setValue2] = useState('') const [size, setSize] = useState(0) const { getMessage } = useMessage() const { closePopup } = usePopup() const currentObject = useRecoilValue(currentObjectState) const canvas = useRecoilValue(canvasState) + useEffect(() => { return () => { canvas?.discardActiveObject() @@ -37,7 +39,7 @@ export default function AuxiliarySize(props) { }, [currentObject]) const handleInput = (e) => { - let value = e.target.value.replace(/^0+/, '') + let value = e.replace(/^0+/, '') if (value === '') { if (checkedRadio === 1) setValue1(value) if (checkedRadio === 2) setValue2(value) @@ -130,7 +132,20 @@ export default function AuxiliarySize(props) {
{getMessage('length')}
- + {/**/} +
mm
@@ -149,7 +164,20 @@ export default function AuxiliarySize(props) {
{getMessage('length')}
- + {/**/} +
mm
diff --git a/src/components/floor-plan/modal/basic/BasicSetting.jsx b/src/components/floor-plan/modal/basic/BasicSetting.jsx index a70b9a22..ac2b3ca4 100644 --- a/src/components/floor-plan/modal/basic/BasicSetting.jsx +++ b/src/components/floor-plan/modal/basic/BasicSetting.jsx @@ -1,4 +1,4 @@ -import { POLYGON_TYPE, MODULE_SETUP_TYPE } from '@/common/common' +import { MODULE_SETUP_TYPE, POLYGON_TYPE } from '@/common/common' import WithDraggable from '@/components/common/draggable/WithDraggable' import { Orientation } from '@/components/floor-plan/modal/basic/step/Orientation' import PitchPlacement from '@/components/floor-plan/modal/basic/step/pitch/PitchPlacement' @@ -74,6 +74,7 @@ export default function BasicSetting({ id, pos = { x: 50, y: 230 } }) { const { trigger: trestleTrigger } = useCanvasPopupStatusController(2) const { trigger: placementTrigger } = useCanvasPopupStatusController(3) const [roofsStore, setRoofsStore] = useRecoilState(roofsState) + const [isFold, setIsFold] = useState(false) // const { initEvent } = useContext(EventContext) const { manualModuleSetup, autoModuleSetup, manualFlatroofModuleSetup, autoFlatroofModuleSetup, manualModuleLayoutSetup, restoreModuleInstArea } = @@ -282,35 +283,42 @@ export default function BasicSetting({ id, pos = { x: 50, y: 230 } }) { return ( - handleClosePopup(id)} /> + handleClosePopup(id)} + onFold={() => setIsFold(!isFold)} + /> -
-
{getMessage('modal.module.basic.setting.orientation.setting')}
- - {basicSetting.roofSizeSet && basicSetting.roofSizeSet != '3' && ( - <> -
{getMessage('modal.module.basic.setting.module.setting')}
- -
{getMessage('modal.module.basic.setting.module.placement')}
- +
+
+
{getMessage('modal.module.basic.setting.orientation.setting')}
+ + {basicSetting.roofSizeSet && basicSetting.roofSizeSet != '3' && ( + <> +
{getMessage('modal.module.basic.setting.module.setting')}
+ +
{getMessage('modal.module.basic.setting.module.placement')}
+ + )} + {basicSetting.roofSizeSet && basicSetting.roofSizeSet == '3' && ( + <> +
{getMessage('modal.module.basic.setting.module.placement')}
+ + )} +
+ {tabNum === 1 && } + {/*배치면 초기설정 - 입력방법: 복시도 입력 || 실측값 입력*/} + {basicSetting.roofSizeSet && basicSetting.roofSizeSet != '3' && tabNum === 2 && } + {basicSetting.roofSizeSet && basicSetting.roofSizeSet != '3' && tabNum === 3 && ( + )} - {basicSetting.roofSizeSet && basicSetting.roofSizeSet == '3' && ( - <> -
{getMessage('modal.module.basic.setting.module.placement')}
- + {/*배치면 초기설정 - 입력방법: 육지붕*/} + {/* {basicSetting.roofSizeSet && basicSetting.roofSizeSet == '3' && tabNum === 3 && } */} + {basicSetting.roofSizeSet && basicSetting.roofSizeSet == '3' && tabNum === 2 && ( + )}
- {tabNum === 1 && } - {/*배치면 초기설정 - 입력방법: 복시도 입력 || 실측값 입력*/} - {basicSetting.roofSizeSet && basicSetting.roofSizeSet != '3' && tabNum === 2 && } - {basicSetting.roofSizeSet && basicSetting.roofSizeSet != '3' && tabNum === 3 && ( - - )} - {/*배치면 초기설정 - 입력방법: 육지붕*/} - {/* {basicSetting.roofSizeSet && basicSetting.roofSizeSet == '3' && tabNum === 3 && } */} - {basicSetting.roofSizeSet && basicSetting.roofSizeSet == '3' && tabNum === 2 && ( - - )}
{/* {tabNum === 1 && } */} diff --git a/src/components/floor-plan/modal/basic/step/Orientation.jsx b/src/components/floor-plan/modal/basic/step/Orientation.jsx index 96fbc6e4..6359163f 100644 --- a/src/components/floor-plan/modal/basic/step/Orientation.jsx +++ b/src/components/floor-plan/modal/basic/step/Orientation.jsx @@ -98,7 +98,7 @@ export const Orientation = forwardRef((props, ref) => { if (moduleSeriesList.length > 0 && foundModule.moduleSerCd) { const currentSeries = moduleSeriesList.find(series => series.moduleSerCd === foundModule.moduleSerCd) if (currentSeries && (!selectedModuleSeries || selectedModuleSeries.moduleSerCd !== currentSeries.moduleSerCd)) { - setSelectedModuleSeries(currentSeries) + //setSelectedModuleSeries(currentSeries) } }else{ setSelectedModuleSeries(allOption) diff --git a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx index 68893c23..f86a7ead 100644 --- a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx @@ -20,8 +20,8 @@ import { useEstimate } from '@/hooks/useEstimate' import { useCircuitTrestle } from '@/hooks/useCirCuitTrestle' import { useImgLoader } from '@/hooks/floorPlan/useImgLoader' import { QcastContext } from '@/app/QcastProvider' -import { fabric } from 'fabric' import { fontSelector } from '@/store/fontAtom' +import { fabric } from 'fabric' const ALLOCATION_TYPE = { AUTO: 'auto', @@ -59,6 +59,9 @@ export default function CircuitTrestleSetting({ id }) { const passivityCircuitAllocationRef = useRef() const { setIsGlobalLoading } = useContext(QcastContext) + const originCanvasViewPortTransform = useRef([]) + const [isFold, setIsFold] = useState(false) + const { makers, setMakers, @@ -83,6 +86,7 @@ export default function CircuitTrestleSetting({ id }) { } = useCircuitTrestle() // const { trigger: moduleSelectedDataTrigger } = useCanvasPopupStatusController(2) useEffect(() => { + originCanvasViewPortTransform.current = [...canvas.viewportTransform] if (!managementState) { } // setCircuitData({ @@ -171,15 +175,12 @@ export default function CircuitTrestleSetting({ id }) { }) } - canvas.renderAll() - - // roof polygon들의 중간점 계산 - const roofPolygons = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) let x, y x = canvas.width / 2 y = canvas.height / 2 - + canvas.zoomToPoint(new fabric.Point(x, y), 0.4) + changeFontSize('lengthText', '28') changeFontSize('circuitNumber', '28') changeFontSize('flowText', '28') @@ -188,9 +189,12 @@ export default function CircuitTrestleSetting({ id }) { // 캡쳐 후 처리 const afterCapture = (type) => { - setCanvasZoom(100) - canvas.set({ zoom: 1 }) - canvas.viewportTransform = [1, 0, 0, 1, 0, 0] + if (originCanvasViewPortTransform.current[0] !== 1) { + setCanvasZoom(Number((originCanvasViewPortTransform.current[0] * 100).toFixed(0))) + } + canvas.viewportTransform = [...originCanvasViewPortTransform.current] + canvas.renderAll() + changeFontSize('lengthText', lengthText.fontSize.value) changeFontSize('circuitNumber', circuitNumberText.fontSize.value) changeFontSize('flowText', flowText.fontSize.value) @@ -223,11 +227,33 @@ export default function CircuitTrestleSetting({ id }) { return } + const isMultiModule = selectedModules.itemList.length > 1 + + let isAllIndfcs = false + + if (isMultiModule) { + //INDFCS 실내집중, OUTDMULTI 옥외멀티 + // 1. 모듈이 혼합형일 경우 선택한 pcs가 실내집중인 경우 alert + if (selectedModels.length > 0) { + isAllIndfcs = selectedModels.every((model) => model.pcsTpCd === 'INDFCS') + } else { + isAllIndfcs = models.every((model) => model.pcsTpCd === 'INDFCS') + } + } + + if (isAllIndfcs) { + swalFire({ + title: getMessage('module.circuit.indoor.focused.error'), + type: 'alert', + }) + return + } + const params = { ...getOptYn(), useModuleItemList: getUseModuleItemList(), roofSurfaceList: getRoofSurfaceList(), - pcsItemList: getPcsItemList(), + pcsItemList: getPcsItemList(isMultiModule), } // 파워컨디셔너 추천 목록 조회 @@ -288,12 +314,12 @@ export default function CircuitTrestleSetting({ id }) { }) } else { // 회로 구성 가능 여부 체크 - getPcsVoltageChk({ ...params, pcsItemList: getSelectedPcsItemList() }).then((res) => { + getPcsVoltageChk({ ...params, pcsItemList: getSelectedPcsItemList(isMultiModule) }).then((res) => { if (res.resultCode === 'S') { // 회로 구성 가능 여부 체크 통과 시 승압설정 정보 조회 getPcsVoltageStepUpList({ ...params, - pcsItemList: getSelectedPcsItemList(), + pcsItemList: getSelectedPcsItemList(isMultiModule), }).then((res) => { if (res?.result.resultCode === 'S' && res?.data) { setTabNum(2) @@ -519,6 +545,7 @@ export default function CircuitTrestleSetting({ id }) { obj.circuit = null obj.pcsItemId = null obj.circuitNumber = null + obj.pcs = null }) setSelectedModels( JSON.parse(JSON.stringify(selectedModels)).map((model) => { @@ -788,20 +815,30 @@ export default function CircuitTrestleSetting({ id }) { return ( - handleClose()} /> + handleClose()} + isFold={isFold} + onFold={() => setIsFold(!isFold)} + /> -
-
{getMessage('modal.circuit.trestle.setting.power.conditional.select')}
- -
- {getMessage('modal.circuit.trestle.setting.circuit.allocation')}({getMessage('modal.circuit.trestle.setting.step.up.allocation')}) +
+
+
+
{getMessage('modal.circuit.trestle.setting.power.conditional.select')}
+ +
+ {getMessage('modal.circuit.trestle.setting.circuit.allocation')}({getMessage('modal.circuit.trestle.setting.step.up.allocation')}) +
+
+ {tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && } + {tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && ( + + )} + {tabNum === 2 && }
- {tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && } - {tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && ( - - )} - {tabNum === 2 && } + {tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && (
diff --git a/src/components/floor-plan/modal/lineTypes/Angle.jsx b/src/components/floor-plan/modal/lineTypes/Angle.jsx index 880aac04..0faad2a4 100644 --- a/src/components/floor-plan/modal/lineTypes/Angle.jsx +++ b/src/components/floor-plan/modal/lineTypes/Angle.jsx @@ -1,6 +1,7 @@ import Image from 'next/image' import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Angle({ props }) { const { getMessage } = useMessage() @@ -14,14 +15,29 @@ export default function Angle({ props }) {
{getMessage('modal.cover.outline.angle')}
- (angle1Ref.current.value = '')}*/} + {/* onChange={(e) => setAngle1(normalizeDecimalLimit(e.target.value, 2))}*/} + {/* placeholder="45"*/} + {/*/>*/} + (angle1Ref.current.value = '')} - onChange={(e) => setAngle1(normalizeDecimalLimit(e.target.value, 2))} + onChange={(value) => setAngle1(value)} placeholder="45" + onFocus={() => (angle1Ref.current.value = '')} + options={{ + allowNegative: false, + allowDecimal: true + }} />
diff --git a/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx b/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx index 92e63e07..12ed66be 100644 --- a/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx +++ b/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx @@ -1,6 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' import { getDegreeByChon } from '@/util/canvas-util' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function DoublePitch({ props }) { const { getMessage } = useMessage() @@ -50,14 +51,29 @@ export default function DoublePitch({ props }) {
{getMessage('modal.cover.outline.angle')}
- (angle1Ref.current.value = '')}*/} + {/* onChange={(e) => setAngle1(normalizeDecimalLimit(e.target.value, 2))}*/} + {/* placeholder="45"*/} + {/*/>*/} + (angle1Ref.current.value = '')} - onChange={(e) => setAngle1(normalizeDecimalLimit(e.target.value, 2))} + onChange={(value) => setAngle1(value)} placeholder="45" + onFocus={() => (angle1Ref.current.value = '')} + options={{ + allowNegative: false, + allowDecimal: true + }} />
@@ -67,14 +83,29 @@ export default function DoublePitch({ props }) {
{getMessage('modal.cover.outline.length')}
- (length1Ref.current.value = '')}*/} + {/* onChange={(e) => setLength1(normalizeDigits(e.target.value))}*/} + {/* placeholder="3000"*/} + {/*/>*/} + (length1Ref.current.value = '')} - onChange={(e) => setLength1(normalizeDigits(e.target.value))} + onChange={(value) => setLength1(value)} placeholder="3000" + onFocus={() => (length1Ref.current.value = '')} + options={{ + allowNegative: false, + allowDecimal: false + }} />
@@ -68,14 +70,27 @@ export default function Updown({ UP_DOWN_REF }) {
{getMessage('modal.movement.flow.line.movement')}
- */} + {setFilledInput(value)}} + options={{ + allowNegative: false, + allowDecimal: false + }} />
mm diff --git a/src/components/floor-plan/modal/object/DormerOffset.jsx b/src/components/floor-plan/modal/object/DormerOffset.jsx index af0ee8e0..fd03f185 100644 --- a/src/components/floor-plan/modal/object/DormerOffset.jsx +++ b/src/components/floor-plan/modal/object/DormerOffset.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { useMessage } from '@/hooks/useMessage' import WithDraggable from '@/components/common/draggable/WithDraggable' import { useRecoilValue } from 'recoil' @@ -6,6 +6,7 @@ import { contextPopupPositionState } from '@/store/popupAtom' import { usePopup } from '@/hooks/usePopup' import { useObjectBatch } from '@/hooks/object/useObjectBatch' import { canvasState } from '@/store/canvasAtom' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function DormerOffset(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) @@ -14,8 +15,10 @@ export default function DormerOffset(props) { const { closePopup } = usePopup() const [arrow1, setArrow1] = useState(null) const [arrow2, setArrow2] = useState(null) - const arrow1LengthRef = useRef() - const arrow2LengthRef = useRef() + const arrow1LengthRef = useRef(0) + const arrow2LengthRef = useRef(0) + const [arrow1Length, setArrow1Length] = useState(0) + const [arrow2Length, setArrow2Length] = useState(0) const canvas = useRecoilValue(canvasState) const { dormerOffsetKeyEvent, dormerOffset } = useObjectBatch({}) @@ -50,7 +53,20 @@ export default function DormerOffset(props) {

{getMessage('length')}

- + {/**/} + {}} // No-op function to prevent error + options={{ + allowNegative: false, + allowDecimal: false, + }} + />
mm
@@ -70,7 +86,20 @@ export default function DormerOffset(props) {
- + {/**/} + {}} // No-op function to prevent error + options={{ + allowNegative: false, + allowDecimal: false, + }} + />
mm
diff --git a/src/components/floor-plan/modal/object/SizeSetting.jsx b/src/components/floor-plan/modal/object/SizeSetting.jsx index b4a7d3e7..ce3d5fbd 100644 --- a/src/components/floor-plan/modal/object/SizeSetting.jsx +++ b/src/components/floor-plan/modal/object/SizeSetting.jsx @@ -5,10 +5,11 @@ import { useMessage } from '@/hooks/useMessage' import WithDraggable from '@/components/common/draggable/WithDraggable' import { usePopup } from '@/hooks/usePopup' import { contextPopupPositionState } from '@/store/popupAtom' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useObjectBatch } from '@/hooks/object/useObjectBatch' import { BATCH_TYPE, POLYGON_TYPE } from '@/common/common' import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function SizeSetting(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) @@ -20,7 +21,8 @@ export default function SizeSetting(props) { const { resizeSurfaceShapeBatch } = useSurfaceShapeBatch({}) const widthRef = useRef(null) const heightRef = useRef(null) - + const [width, setWidth] = useState(target?.width ? (target.width * 10).toFixed() : 0) + const [height, setHeight] = useState(target?.height ? (target.height * 10) : 0) // const { initEvent } = useEvent() // const { initEvent } = useContext(EventContext) @@ -28,6 +30,15 @@ export default function SizeSetting(props) { // initEvent() // }, []) + useEffect(() => { + if (target?.width !== undefined) { + setWidth((target.width * 10).toFixed()); + } + if (target?.height !== undefined) { + setHeight((target.height * 10).toFixed()); + } + }, [target]); + const handleReSizeObject = () => { const width = widthRef.current.value const height = heightRef.current.value @@ -47,11 +58,25 @@ export default function SizeSetting(props) {
- + setWidth(e.target.value)} readOnly={true} /> mm
- + {/**/} + setWidth(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + /> mm
@@ -60,11 +85,25 @@ export default function SizeSetting(props) {
- + setHeight(e.target.value)} readOnly={true} /> mm
- + {/**/} + setHeight(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + /> mm
diff --git a/src/components/floor-plan/modal/object/type/OpenSpace.jsx b/src/components/floor-plan/modal/object/type/OpenSpace.jsx index ab4d287d..dfda5148 100644 --- a/src/components/floor-plan/modal/object/type/OpenSpace.jsx +++ b/src/components/floor-plan/modal/object/type/OpenSpace.jsx @@ -1,10 +1,13 @@ import { forwardRef, useState, useEffect } from 'react' import { useMessage } from '@/hooks/useMessage' import { INPUT_TYPE } from '@/common/common' +import { CalculatorInput } from '@/components/common/input/CalcInput' const OpenSpace = forwardRef((props, refs) => { const { getMessage } = useMessage() const [selectedType, setSelectedType] = useState(INPUT_TYPE.FREE) + const [width, setWidth] = useState(0) + const [height, setHeight] = useState(0) useEffect(() => { if (selectedType === INPUT_TYPE.FREE) { @@ -51,12 +54,26 @@ const OpenSpace = forwardRef((props, refs) => {
- */} + setWidth(value)} disabled={selectedType !== INPUT_TYPE.DIMENSION} + options={{ + allowNegative: false, + allowDecimal: false + }} />
mm @@ -68,12 +85,26 @@ const OpenSpace = forwardRef((props, refs) => {
- */} + setHeight(value)} disabled={selectedType !== INPUT_TYPE.DIMENSION} + options={{ + allowNegative: false, + allowDecimal: false + }} />
mm diff --git a/src/components/floor-plan/modal/object/type/PentagonDormer.jsx b/src/components/floor-plan/modal/object/type/PentagonDormer.jsx index 9d82485c..30bb3a49 100644 --- a/src/components/floor-plan/modal/object/type/PentagonDormer.jsx +++ b/src/components/floor-plan/modal/object/type/PentagonDormer.jsx @@ -1,6 +1,7 @@ import Image from 'next/image' import { useMessage } from '@/hooks/useMessage' import { forwardRef, useState } from 'react' +import { CalculatorInput } from '@/components/common/input/CalcInput' const PentagonDormer = forwardRef((props, refs) => { const { getMessage } = useMessage() @@ -11,6 +12,11 @@ const PentagonDormer = forwardRef((props, refs) => { setDirection(e.target.value) refs.directionRef.current = e.target.value } + const [pitch, setPitch] = useState(4) // pitch 상태 추가, 기본값 4로 설정 + const [offsetWidth, setOffsetWidth] = useState(300) // offsetWidth 상태 추가, 기본값 300으로 + const [offsetDepth, setOffsetDepth] = useState(400) // offsetDepth 상태 추가, 기본값 400으로 + const [width, setWidth] = useState(2000) // width 상태 추가, 기본값 2000으로 + const [height, setHeight] = useState(2000) // height 상태 추가, 기본값 2000으로 return ( <> @@ -30,7 +36,20 @@ const PentagonDormer = forwardRef((props, refs) => {
- + {/**/} + setHeight(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -41,7 +60,20 @@ const PentagonDormer = forwardRef((props, refs) => {
- + {/**/} + setOffsetDepth(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -55,7 +87,20 @@ const PentagonDormer = forwardRef((props, refs) => {
- + {/**/} + setWidth(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -66,7 +111,20 @@ const PentagonDormer = forwardRef((props, refs) => {
- + {/**/} + setOffsetWidth(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -77,7 +135,20 @@ const PentagonDormer = forwardRef((props, refs) => {
- + {/**/} + setPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
diff --git a/src/components/floor-plan/modal/object/type/Shadow.jsx b/src/components/floor-plan/modal/object/type/Shadow.jsx index 0945d78c..52595fa1 100644 --- a/src/components/floor-plan/modal/object/type/Shadow.jsx +++ b/src/components/floor-plan/modal/object/type/Shadow.jsx @@ -1,11 +1,14 @@ import { forwardRef, useState, useEffect } from 'react' import { useMessage } from '@/hooks/useMessage' import { INPUT_TYPE } from '@/common/common' +import { CalculatorInput } from '@/components/common/input/CalcInput' const Shadow = forwardRef((props, refs) => { const { getMessage } = useMessage() const [selectedType, setSelectedType] = useState(INPUT_TYPE.FREE) + const [width, setWidth] = useState(0) + const [height, setHeight] = useState(0) useEffect(() => { if (selectedType === INPUT_TYPE.FREE) { @@ -51,12 +54,26 @@ const Shadow = forwardRef((props, refs) => {
- */} + setWidth(value)} disabled={selectedType !== INPUT_TYPE.DIMENSION} + options={{ + allowNegative: false, + allowDecimal: false + }} />
mm @@ -68,12 +85,26 @@ const Shadow = forwardRef((props, refs) => {
- */} + setHeight(value)} disabled={selectedType !== INPUT_TYPE.DIMENSION} + options={{ + allowNegative: false, + allowDecimal: false + }} />
mm diff --git a/src/components/floor-plan/modal/object/type/TriangleDormer.jsx b/src/components/floor-plan/modal/object/type/TriangleDormer.jsx index 65fed20b..08e31538 100644 --- a/src/components/floor-plan/modal/object/type/TriangleDormer.jsx +++ b/src/components/floor-plan/modal/object/type/TriangleDormer.jsx @@ -1,6 +1,7 @@ import Image from 'next/image' import { useMessage } from '@/hooks/useMessage' import { forwardRef, useState } from 'react' +import { CalculatorInput } from '@/components/common/input/CalcInput' const TriangleDormer = forwardRef((props, refs) => { const { getMessage } = useMessage() @@ -11,6 +12,9 @@ const TriangleDormer = forwardRef((props, refs) => { setDirection(e.target.value) refs.directionRef.current = e.target.value } +const [height, setHeight] = useState(1500) +const [offset, setOffset] = useState(400) +const [pitch, setPitch] = useState(4) return ( <> @@ -30,7 +34,20 @@ const TriangleDormer = forwardRef((props, refs) => {
- + {/**/} + setHeight(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -41,7 +58,20 @@ const TriangleDormer = forwardRef((props, refs) => {
- + {/**/} + setOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -52,7 +82,20 @@ const TriangleDormer = forwardRef((props, refs) => {
- + {/**/} + setPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true + }} + />
diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index 8b1f7dc8..4ec45dd5 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -170,8 +170,8 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla setCurrentRoof({ ...selectedRoofMaterial, - pitch: currentRoof?.pitch, - angle: currentRoof?.angle, + // pitch: currentRoof?.pitch, + // angle: currentRoof?.angle, index: 0, planNo: currentRoof.planNo, roofSizeSet: String(currentRoof.roofSizeSet), @@ -350,27 +350,29 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla label="" className="input-origin block" readOnly={currentRoof?.roofAngleSet !== item.value} - value={index === 0 ? currentRoof?.pitch || '0' : currentRoof?.angle || '0'} + value={index === 0 ? (currentRoof?.pitch ?? basicSetting?.inclBase ?? '0') : (currentRoof?.angle ?? '0')} onChange={(value) => { if (index === 0) { - const num = value === '' ? '' : Number(value) + const pitch = value === '' ? '' : Number(value); + const angle = pitch === '' ? '' : getDegreeByChon(pitch); setCurrentRoof(prev => ({ ...prev, - pitch: num === '' ? '' : num, - angle: num === '' ? '' : getDegreeByChon(num), - })) + pitch, + angle + })); } else { - const num = value === '' ? '' : Number(value) - setCurrentRoof( prev => ({ + const angle = value === '' ? '' : Number(value); + const pitch = angle === '' ? '' : getChonByDegree(angle); + setCurrentRoof(prev => ({ ...prev, - pitch: num === '' ? '' : getChonByDegree(num), - angle: num === '' ? '' : num, - })) + pitch, + angle + })); } }} options={{ allowNegative: false, - allowDecimal: false //(index !== 0), + allowDecimal: true }} />
@@ -514,13 +516,17 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla {/*/>*/} { - setCurrentRoof({ ...currentRoof, value }) + const hajebichi = value === '' ? '' : Number(value); + setCurrentRoof(prev => ({ + ...prev, + hajebichi + })); }} readOnly={currentRoof?.roofPchAuth === 'R'} disabled={currentRoof?.roofSizeSet === '3'} diff --git a/src/components/floor-plan/modal/placementSurface/PlacementSurface.jsx b/src/components/floor-plan/modal/placementSurface/PlacementSurface.jsx index 7ab89106..4ede1367 100644 --- a/src/components/floor-plan/modal/placementSurface/PlacementSurface.jsx +++ b/src/components/floor-plan/modal/placementSurface/PlacementSurface.jsx @@ -1,6 +1,7 @@ import Image from 'next/image' import { useMessage } from '@/hooks/useMessage' import { forwardRef, useState } from 'react' +import { CalculatorInput } from '@/components/common/input/CalcInput' const PlacementSurface = forwardRef((props, refs) => { const { getMessage } = useMessage() @@ -74,10 +75,32 @@ const PlacementSurface = forwardRef((props, refs) => {
- */} + + {}} ref={ line.isDiagonal ? lengthetc @@ -91,6 +114,10 @@ const PlacementSurface = forwardRef((props, refs) => { ? length4 : length5 } + options={{ + allowNegative: false, + allowDecimal: false + }} />
mm diff --git a/src/components/floor-plan/modal/roofAllocation/ContextRoofAllocationSetting.jsx b/src/components/floor-plan/modal/roofAllocation/ContextRoofAllocationSetting.jsx index 78597844..05b2759a 100644 --- a/src/components/floor-plan/modal/roofAllocation/ContextRoofAllocationSetting.jsx +++ b/src/components/floor-plan/modal/roofAllocation/ContextRoofAllocationSetting.jsx @@ -13,6 +13,7 @@ import { useCommonCode } from '@/hooks/common/useCommonCode' import { globalLocaleStore } from '@/store/localeAtom' import { currentAngleTypeSelector, pitchTextSelector } from '@/store/canvasAtom' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function ContextRoofAllocationSetting(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) @@ -204,15 +205,29 @@ export default function ContextRoofAllocationSetting(props) {
{getMessage('modal.object.setting.offset.slope')}
- {*/} + {/* e.target.value = normalizeDecimalLimit(e.target.value, 2)*/} + {/* handleChangePitch(e, index)*/} + {/* }}*/} + {/* value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}*/} + {/*/>*/} + { - e.target.value = normalizeDecimalLimit(e.target.value, 2) - handleChangePitch(e, index) - }} value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')} - /> + onChange={(value) => { + handleChangePitch(value, index) + }} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
diff --git a/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx b/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx index 0e0e09ee..79b524ef 100644 --- a/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx +++ b/src/components/floor-plan/modal/roofAllocation/RoofAllocationSetting.jsx @@ -14,6 +14,7 @@ import { useRoofShapeSetting } from '@/hooks/roofcover/useRoofShapeSetting' import { currentAngleTypeSelector, pitchTextSelector } from '@/store/canvasAtom' import { getDegreeByChon } from '@/util/canvas-util' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function RoofAllocationSetting(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) @@ -205,15 +206,29 @@ export default function RoofAllocationSetting(props) {
{getMessage('modal.object.setting.offset.slope')}
- {*/} + {/* e.target.value = normalizeDecimalLimit(e.target.value, 2)*/} + {/* handleChangePitch(e, index)*/} + {/* }}*/} + {/* value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}*/} + {/*/>*/} + { - e.target.value = normalizeDecimalLimit(e.target.value, 2) - handleChangePitch(e, index) - }} value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')} - /> + onChange={(value) => { + handleChangePitch(value, index) + }} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
diff --git a/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx b/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx index c49cb3fa..f3dd4052 100644 --- a/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx +++ b/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil' import { ANGLE_TYPE, currentAngleTypeSelector } from '@/store/canvasAtom' import { selectedRoofMaterialSelector } from '@/store/settingAtom' import { useEffect } from 'react' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Eaves({ offsetRef, pitchRef, pitchText }) { const { getMessage } = useMessage() @@ -16,12 +17,24 @@ export default function Eaves({ offsetRef, pitchRef, pitchText }) { {getMessage('slope')}
- */} + + value={currentAngleType === ANGLE_TYPE.SLOPE ? selectedRoofMaterial.pitch : selectedRoofMaterial.angle} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
@@ -30,7 +43,20 @@ export default function Eaves({ offsetRef, pitchRef, pitchText }) { {getMessage('eaves.offset')}
- + {/**/} + {}} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
diff --git a/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx b/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx index 6da1266e..fe04a65e 100644 --- a/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx +++ b/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx @@ -1,6 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { useRecoilValue } from 'recoil' import { currentAngleTypeSelector } from '@/store/canvasAtom' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Gable({ offsetRef }) { const { getMessage } = useMessage() @@ -12,7 +13,20 @@ export default function Gable({ offsetRef }) { {getMessage('gable.offset')}
- + {/**/} + {}} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
diff --git a/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx b/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx index 474c2f60..67a47bb0 100644 --- a/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx +++ b/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx @@ -1,4 +1,5 @@ import { useMessage } from '@/hooks/useMessage' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Shed({ offsetRef }) { const { getMessage } = useMessage() @@ -9,7 +10,20 @@ export default function Shed({ offsetRef }) { {getMessage('shed.width')}
- + {/**/} + {}} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/Direction.jsx b/src/components/floor-plan/modal/roofShape/type/Direction.jsx index 5ea8e635..7c3cf809 100644 --- a/src/components/floor-plan/modal/roofShape/type/Direction.jsx +++ b/src/components/floor-plan/modal/roofShape/type/Direction.jsx @@ -1,5 +1,6 @@ import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Direction({ pitch, setPitch, eavesOffset, setEavesOffset, gableOffset, setGableOffset, shedWidth, setShedWidth, pitchText }) { const { getMessage } = useMessage() @@ -10,12 +11,24 @@ export default function Direction({ pitch, setPitch, eavesOffset, setEavesOffset {getMessage('slope')}
- setPitch(normalizeDecimalLimit(e.target.value, 2))}*/} + {/*/>*/} + setPitch(normalizeDecimalLimit(e.target.value, 2))} - /> + onChange={(value) => setPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
@@ -24,12 +37,24 @@ export default function Direction({ pitch, setPitch, eavesOffset, setEavesOffset {getMessage('eaves.offset')}
- setEavesOffset(normalizeDigits(e.target.value))}*/} + {/*/>*/} + setEavesOffset(normalizeDigits(e.target.value))} - /> + onChange={(value) => setEavesOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
@@ -38,12 +63,24 @@ export default function Direction({ pitch, setPitch, eavesOffset, setEavesOffset {getMessage('gable.offset')}
- setGableOffset(normalizeDigits(e.target.value))}*/} + {/*/>*/} + setGableOffset(normalizeDigits(e.target.value))} - /> + onChange={(value) => setGableOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
@@ -52,12 +89,24 @@ export default function Direction({ pitch, setPitch, eavesOffset, setEavesOffset {getMessage('windage.width')}
- setShedWidth(normalizeDigits(e.target.value))}*/} + {/*/>*/} + setShedWidth(normalizeDigits(e.target.value))} - /> + onChange={(value) => setShedWidth(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/Pattern.jsx b/src/components/floor-plan/modal/roofShape/type/Pattern.jsx index 269cac58..46631b5c 100644 --- a/src/components/floor-plan/modal/roofShape/type/Pattern.jsx +++ b/src/components/floor-plan/modal/roofShape/type/Pattern.jsx @@ -1,5 +1,6 @@ import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Pattern(props) { const { getMessage } = useMessage() @@ -11,7 +12,20 @@ export default function Pattern(props) { {getMessage('slope')}
- setPitch(normalizeDecimalLimit(e.target.value, 2))} /> + {/* setPitch(normalizeDecimalLimit(e.target.value, 2))} />*/} + setPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
@@ -20,7 +34,20 @@ export default function Pattern(props) { {getMessage('eaves.offset')}
- setEavesOffset(normalizeDigits(e.target.value))} /> + {/* setEavesOffset(normalizeDigits(e.target.value))} />*/} + setEavesOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
@@ -29,7 +56,20 @@ export default function Pattern(props) { {getMessage('gable.offset')}
- setGableOffset(normalizeDigits(e.target.value))} /> + {/* setGableOffset(normalizeDigits(e.target.value))} />*/} + setGableOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/Ridge.jsx b/src/components/floor-plan/modal/roofShape/type/Ridge.jsx index fb0f016f..03f22936 100644 --- a/src/components/floor-plan/modal/roofShape/type/Ridge.jsx +++ b/src/components/floor-plan/modal/roofShape/type/Ridge.jsx @@ -1,6 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { useEffect } from 'react' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Ridge(props) { const { getMessage } = useMessage() @@ -13,7 +14,20 @@ export default function Ridge(props) { {getMessage('slope')}
- setPitch(normalizeDecimalLimit(e.target.value, 2))} /> + {/* setPitch(normalizeDecimalLimit(e.target.value, 2))} />*/} + setPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
@@ -22,7 +36,20 @@ export default function Ridge(props) { {getMessage('eaves.offset')}
- setEavesOffset(normalizeDigits(e.target.value))} /> + {/* setEavesOffset(normalizeDigits(e.target.value))} />*/} + setEavesOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/option/Eaves.jsx b/src/components/floor-plan/modal/roofShape/type/option/Eaves.jsx index 8e231c7e..9477adae 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/Eaves.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/Eaves.jsx @@ -1,5 +1,6 @@ import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Eaves({ pitch, setPitch, eavesOffset, setEavesOffset, pitchText }) { const { getMessage } = useMessage() @@ -10,7 +11,21 @@ export default function Eaves({ pitch, setPitch, eavesOffset, setEavesOffset, pi {getMessage('slope')}
- setPitch(normalizeDecimalLimit(e.target.value, 2))} /> + {/* setPitch(normalizeDecimalLimit(e.target.value, 2))} />*/} + setPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + > +
{pitchText}
@@ -19,7 +34,20 @@ export default function Eaves({ pitch, setPitch, eavesOffset, setEavesOffset, pi {getMessage('eaves.offset')}
- setEavesOffset(normalizeDigits(e.target.value))} /> + {/* setEavesOffset(normalizeDigits(e.target.value))} />*/} + setEavesOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/option/Gable.jsx b/src/components/floor-plan/modal/roofShape/type/option/Gable.jsx index 173c12f4..1b6c1c7f 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/Gable.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/Gable.jsx @@ -1,6 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { useEffect } from 'react' import { normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Gable({ gableOffset, setGableOffset }) { const { getMessage } = useMessage() @@ -10,7 +11,20 @@ export default function Gable({ gableOffset, setGableOffset }) {
{getMessage('gable.offset')}
- setGableOffset(normalizeDigits(e.target.value))} /> + {/* setGableOffset(normalizeDigits(e.target.value))} />*/} + setGableOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/option/HipAndGable.jsx b/src/components/floor-plan/modal/roofShape/type/option/HipAndGable.jsx index 787dd0e8..cb38560f 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/HipAndGable.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/HipAndGable.jsx @@ -1,5 +1,6 @@ import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function HipAndGable({ pitch, setPitch, eavesOffset, setEavesOffset, hipAndGableWidth, setHipAndGableWidth, pitchText }) { const { getMessage } = useMessage() @@ -10,7 +11,20 @@ export default function HipAndGable({ pitch, setPitch, eavesOffset, setEavesOffs {getMessage('slope')}
- setPitch(normalizeDecimalLimit(e.target.value, 2))} /> + {/* setPitch(normalizeDecimalLimit(e.target.value, 2))} />*/} + setPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
@@ -19,7 +33,21 @@ export default function HipAndGable({ pitch, setPitch, eavesOffset, setEavesOffs {getMessage('eaves.offset')}
- setEavesOffset(normalizeDigits(e.target.value))} /> + {/* setEavesOffset(normalizeDigits(e.target.value))} />*/} + setEavesOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + > +
mm
@@ -28,12 +56,24 @@ export default function HipAndGable({ pitch, setPitch, eavesOffset, setEavesOffs {getMessage('hipandgable.width')}
- setHipAndGableWidth(normalizeDigits(e.target.value))}*/} + {/*/>*/} + setHipAndGableWidth(normalizeDigits(e.target.value))} - /> + onChange={(value) => setHipAndGableWidth(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/option/Jerkinhead.jsx b/src/components/floor-plan/modal/roofShape/type/option/Jerkinhead.jsx index 22f0607e..ed4c11f1 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/Jerkinhead.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/Jerkinhead.jsx @@ -1,5 +1,6 @@ import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Jerkinhead({ gableOffset, @@ -18,7 +19,20 @@ export default function Jerkinhead({ {getMessage('gable.offset')}
- setGableOffset(normalizeDigits(e.target.value))} /> + {/* setGableOffset(normalizeDigits(e.target.value))} />*/} + setGableOffset(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
@@ -27,7 +41,21 @@ export default function Jerkinhead({ {getMessage('jerkinhead.width')}
- setJerkinHeadWidth(normalizeDigits(e.target.value))} /> + {/* setJerkinHeadWidth(normalizeDigits(e.target.value))} />*/} + + setJerkinHeadWidth(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
@@ -36,12 +64,24 @@ export default function Jerkinhead({ {getMessage('jerkinhead.slope')}
- setJerkinHeadPitch(normalizeDecimalLimit(e.target.value, 2))}*/} + {/*/>*/} + setJerkinHeadPitch(normalizeDecimalLimit(e.target.value, 2))} - /> + onChange={(value) => setJerkinHeadPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
diff --git a/src/components/floor-plan/modal/roofShape/type/option/Shed.jsx b/src/components/floor-plan/modal/roofShape/type/option/Shed.jsx index daacea56..6fe1d32f 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/Shed.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/Shed.jsx @@ -1,5 +1,6 @@ import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Shed({ shedWidth, setShedWidth, shedPitch, setShedPitch, pitchText }) { const { getMessage } = useMessage() @@ -8,14 +9,40 @@ export default function Shed({ shedWidth, setShedWidth, shedPitch, setShedPitch,
{getMessage('slope')}
- setShedPitch(normalizeDecimalLimit(e.target.value, 2))} /> + {/* setShedPitch(normalizeDecimalLimit(e.target.value, 2))} />*/} + setShedPitch(value)} + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
{getMessage('shed.width')}
- setShedWidth(normalizeDigits(e.target.value))} /> + {/* setShedWidth(normalizeDigits(e.target.value))} />*/} + setShedWidth(value)} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/roofShape/type/option/Wall.jsx b/src/components/floor-plan/modal/roofShape/type/option/Wall.jsx index dfdff20d..bee6e9bb 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/Wall.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/Wall.jsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { useMessage } from '@/hooks/useMessage' import { normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Wall({ sleeveOffset, setSleeveOffset, hasSleeve, setHasSleeve }) { const { getMessage } = useMessage() @@ -10,7 +11,8 @@ export default function Wall({ sleeveOffset, setSleeveOffset, hasSleeve, setHasS
- setHasSleeve(e.target.value)} /> + setHasSleeve(e.target.value)} />
@@ -18,20 +20,34 @@ export default function Wall({ sleeveOffset, setSleeveOffset, hasSleeve, setHasS
- setHasSleeve(e.target.value)} /> + setHasSleeve(e.target.value)} />
- setSleeveOffset(normalizeDigits(e.target.value))}*/} + {/* readOnly={hasSleeve === '0'}*/} + {/*/>*/} + setSleeveOffset(normalizeDigits(e.target.value))} + onChange={(value) => setSleeveOffset(value)} readOnly={hasSleeve === '0'} - /> + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/setting01/FirstOption.jsx b/src/components/floor-plan/modal/setting01/FirstOption.jsx index 51a52cdb..ef2d8e0b 100644 --- a/src/components/floor-plan/modal/setting01/FirstOption.jsx +++ b/src/components/floor-plan/modal/setting01/FirstOption.jsx @@ -1,8 +1,10 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import { useMessage } from '@/hooks/useMessage' import { POLYGON_TYPE } from '@/common/common' import { useEvent } from '@/hooks/useEvent' import { useRoofFn } from '@/hooks/common/useRoofFn' +import { outlineDisplaySelector } from '@/store/settingAtom' +import { useRecoilValue } from 'recoil' export default function FirstOption(props) { const { getMessage } = useMessage() @@ -11,6 +13,7 @@ export default function FirstOption(props) { const { option1, option2, dimensionDisplay } = settingModalFirstOptions const { initEvent } = useEvent() const { setSurfaceShapePattern } = useRoofFn() + const outlineDisplay = useRecoilValue(outlineDisplaySelector) // 데이터를 최초 한 번만 조회 useEffect(() => { @@ -18,6 +21,13 @@ export default function FirstOption(props) { setSettingsDataSave({ ...settingsData }) }, []) + useEffect(() => { + const outline = canvas.getObjects().filter((obj) => obj.name === 'originRoofOuterLine') + outline.forEach((obj) => { + obj.visible = outlineDisplay + }) + }, [outlineDisplay]) + const onClickOption = async (item) => { let dimensionDisplay = settingModalFirstOptions?.dimensionDisplay let option1 = settingModalFirstOptions?.option1 @@ -58,7 +68,12 @@ export default function FirstOption(props) { // setSettingModalFirstOptions({ ...settingModalFirstOptions, option1: [...options] }) } - setSettingsData({ ...settingsData, option1: [...option1], option2: [...option2], dimensionDisplay: [...dimensionDisplay] }) + setSettingsData({ + ...settingsData, + option1: [...option1], + option2: [...option2], + dimensionDisplay: [...dimensionDisplay], + }) } // useEffect(() => { diff --git a/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx b/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx index 76f09cc7..79ae0898 100644 --- a/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx +++ b/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx @@ -1,6 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { useEffect, useState } from 'react' import { useEvent } from '@/hooks/useEvent' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Offset({ length1Ref, arrow1Ref, currentWallLineRef }) { const { getMessage } = useMessage() @@ -74,7 +75,20 @@ export default function Offset({ length1Ref, arrow1Ref, currentWallLineRef }) {
- + {/**/} + {}} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
diff --git a/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx b/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx index 92f6a10b..d78a202f 100644 --- a/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx +++ b/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx @@ -1,6 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' import { useEvent } from '@/hooks/useEvent' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default forwardRef(function WallLine({ length1Ref, length2Ref, arrow1Ref, arrow2Ref, radioTypeRef, currentWallLineRef }, ref) { const { getMessage } = useMessage() @@ -46,7 +47,21 @@ export default forwardRef(function WallLine({ length1Ref, length2Ref, arrow1Ref,
- + {/**/} + {}} + readOnly={type !== 1} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
@@ -80,7 +95,21 @@ export default forwardRef(function WallLine({ length1Ref, length2Ref, arrow1Ref,
- + {/**/} + {}} + readOnly={type !== 2} + options={{ + allowNegative: false, + allowDecimal: false + }} + />
mm
diff --git a/src/components/header/Header.jsx b/src/components/header/Header.jsx index 8b37b134..561b8e47 100644 --- a/src/components/header/Header.jsx +++ b/src/components/header/Header.jsx @@ -133,17 +133,21 @@ export default function Header(props) { { id: 1, name: 'HANASYS ORDER', link: `${qOrderUrl}?autoLoginParam1=${encodeURIComponent(res.data)}`, target: '_blank' }, { id: 2, name: 'HANASYS Musubi', link: `${qMusubiUrl}?autoLoginParam1=${encodeURIComponent(res.data)}`, target: '_blank' }, { id: 3, name: getMessage('site.header.link2'), link: `https://q-warranty.q-cells.jp/seller_login`, target: '_blank' }, + { id: 4, name: 'Q.PARTNERS', link: `https://q-partners.q-cells.jp/qcast_login.php`, target: '_blank' }, + ] : userSession.groupId === '60000' ? [ { id: 0, name: getMessage('site.header.link1'), target: '_blank' }, { id: 1, name: 'HANASYS ORDER', link: `${qOrderUrl}?autoLoginParam1=${encodeURIComponent(res.data)}`, target: '_blank' }, { id: 2, name: getMessage('site.header.link2'), link: `https://q-warranty.q-cells.jp/seller_login`, target: '_blank' }, + { id: 3, name: 'Q.PARTNERS', link: `https://q-partners.q-cells.jp/qcast_login.php`, target: '_blank' }, ] : [ { id: 0, name: getMessage('site.header.link1'), target: '_blank' }, { id: 1, name: 'HANASYS Musubi', link: `${qMusubiUrl}?autoLoginParam1=${encodeURIComponent(res.data)}`, target: '_blank' }, { id: 2, name: getMessage('site.header.link2'), link: `https://q-warranty.q-cells.jp/seller_login`, target: '_blank' }, + { id: 3, name: 'Q.PARTNERS', link: `https://q-partners.q-cells.jp/qcast_login.php`, target: '_blank' }, ], ) onChangeSelect({ id: 0, name: getMessage('site.header.link1') }) diff --git a/src/components/simulator/Simulator.jsx b/src/components/simulator/Simulator.jsx index 5d6a7e3d..d873049f 100644 --- a/src/components/simulator/Simulator.jsx +++ b/src/components/simulator/Simulator.jsx @@ -264,15 +264,14 @@ export default function Simulator() { style={{ width: '30%' }} className="select-light" value={pwrGnrSimType} - defaultValue={`D`} onChange={(e) => { handleChartChangeData(e.target.value) setPwrGnrSimType(e.target.value) }} > - + {/**/} - + {/**/}
@@ -334,33 +333,31 @@ export default function Simulator() { - {moduleInfoList.length > 0 ? ( - moduleInfoList.map((moduleInfo) => { - return ( - <> - - {/* 지붕면 */} - {moduleInfo.roofSurface} - {/* 경사각 */} - - {convertNumberToPriceDecimal(moduleInfo.slopeAngle)} - {moduleInfo.classType == 0 ? '寸' : 'º'} - - {/* 방위각(도) */} - {convertNumberToPriceDecimal(moduleInfo.azimuth)} - {/* 태양전지모듈 */} - -
{moduleInfo.itemNo}
- - {/* 매수 */} - {convertNumberToPriceDecimal(moduleInfo.amount)} - - - ) - }) - ) : ( - - {getMessage('common.message.no.data')} + {moduleInfoList.length > 0 ? ( + moduleInfoList.map((moduleInfo) => { + return ( + + {/* 지붕면 */} + {moduleInfo.roofSurface} + {/* 경사각 */} + + {convertNumberToPriceDecimal(moduleInfo.slopeAngle)} + {moduleInfo.classType == 0 ? '寸' : 'º'} + + {/* 방위각(도) */} + {convertNumberToPriceDecimal(moduleInfo.azimuth)} + {/* 태양전지모듈 */} + +
{moduleInfo.itemNo}
+ + {/* 매수 */} + {convertNumberToPriceDecimal(moduleInfo.amount)} + + ) + }) + ) : ( + + {getMessage('common.message.no.data')} )} @@ -385,25 +382,23 @@ export default function Simulator() { - {pcsInfoList.length > 0 ? ( - pcsInfoList.map((pcsInfo) => { - return ( - <> - - {/* 파워컨디셔너 */} - -
{pcsInfo.itemNo}
- - {/* 대 */} - {convertNumberToPriceDecimal(pcsInfo.amount)} - - - ) - }) - ) : ( - - {getMessage('common.message.no.data')} - + {pcsInfoList.length > 0 ? ( + pcsInfoList.map((pcsInfo) => { + return ( + + {/* 파워컨디셔너 */} + +
{pcsInfo.itemNo}
+ + {/* 대 */} + {convertNumberToPriceDecimal(pcsInfo.amount)} + + ) + }) + ) : ( + + {getMessage('common.message.no.data')} + )} diff --git a/src/hooks/common/useCommonUtils.js b/src/hooks/common/useCommonUtils.js index 7c53f98c..9fe5a221 100644 --- a/src/hooks/common/useCommonUtils.js +++ b/src/hooks/common/useCommonUtils.js @@ -31,8 +31,11 @@ export function useCommonUtils() { useEffect(() => { commonTextMode() if (commonUtils.dimension) { + generateTempGrid() commonDimensionMode() return + } else { + removeTempGrid() } if (commonUtils.distance) { commonDistanceMode() @@ -645,6 +648,7 @@ export function useCommonUtils() { lockMovementY: true, name: obj.name, editable: false, + selectable: true, // 복사된 객체 선택 가능하도록 설정 id: uuidv4(), //복사된 객체라 새로 따준다 }) @@ -653,19 +657,25 @@ export function useCommonUtils() { //배치면일 경우 if (obj.name === 'roof') { - clonedObj.setCoords() - clonedObj.fire('modified') - clonedObj.fire('polygonMoved') + clonedObj.canvas = canvas // canvas 참조 설정 clonedObj.set({ direction: obj.direction, directionText: obj.directionText, roofMaterial: obj.roofMaterial, + stroke: 'black', // 복사된 객체는 선택 해제 상태의 색상으로 설정 + selectable: true, // 선택 가능하도록 설정 + evented: true, // 마우스 이벤트를 받을 수 있도록 설정 + isFixed: false, // containsPoint에서 특별 처리 방지 }) obj.lines.forEach((line, index) => { clonedObj.lines[index].set({ attributes: line.attributes }) }) + clonedObj.fire('polygonMoved') // 내부 좌표 재계산 (points, pathOffset) + clonedObj.fire('modified') + clonedObj.setCoords() // 모든 속성 설정 후 좌표 업데이트 + canvas.setActiveObject(clonedObj) canvas.renderAll() addLengthText(clonedObj) //수치 추가 drawDirectionArrow(clonedObj) //방향 화살표 추가 @@ -905,6 +915,45 @@ export function useCommonUtils() { } } + const generateTempGrid = () => { + if (!canvas) return + + const objects = canvas.getObjects().filter((obj) => ['QPolygon'].includes(obj.type)) + const gridLines = [] + + objects.forEach((obj) => { + const lines = obj.lines + + lines.forEach((line) => { + const gridLine = new fabric.Line([line.x1, line.y1, line.x2, line.y2], { + stroke: 'gray', + strokeWidth: 1, + selectable: false, + evented: false, + opacity: 0.5, + name: 'tempGrid', + direction: line.x1 === line.x2 ? 'vertical' : 'horizontal', + visible: false, + }) + gridLines.push(gridLine) + }) + }) + + gridLines.forEach((line) => { + canvas.add(line) + }) + + canvas.renderAll() + } + + const removeTempGrid = () => { + if (!canvas) return + + const tempGrids = canvas.getObjects().filter((obj) => obj.name === 'tempGrid' && !obj.visible) + tempGrids.forEach((grid) => canvas.remove(grid)) + canvas.renderAll() + } + return { commonFunctions, dimensionSettings, @@ -916,5 +965,7 @@ export function useCommonUtils() { editText, changeDimensionExtendLine, deleteOuterLineObject, + generateTempGrid, + removeTempGrid, } } diff --git a/src/hooks/common/useRoofFn.js b/src/hooks/common/useRoofFn.js index eb640d76..1148eed3 100644 --- a/src/hooks/common/useRoofFn.js +++ b/src/hooks/common/useRoofFn.js @@ -1,4 +1,4 @@ -import { useRecoilValue, useResetRecoilState } from 'recoil' +import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' import { canvasState, currentObjectState } from '@/store/canvasAtom' import { selectedRoofMaterialSelector } from '@/store/settingAtom' import { ROOF_MATERIAL_LAYOUT } from '@/components/floor-plan/modal/placementShape/PlacementShapeSetting' @@ -25,6 +25,7 @@ export function useRoofFn() { const { addPitchText } = useLine() const { setPolygonLinesActualSize } = usePolygon() const { changeCorridorDimensionText } = useText() + const [outerLinePoints, setOuterLinePoints] = useRecoilState(outerLinePointsState) //면형상 선택 클릭시 지붕 패턴 입히기 function setSurfaceShapePattern(polygon, mode = 'onlyBorder', trestleMode = false, roofMaterial, isForceChange = false, isDisplay = false) { @@ -263,6 +264,9 @@ export function useRoofFn() { const deltaX = roof.left - originalRoofLeft const deltaY = roof.top - originalRoofTop + const originOuterLinePoints = [...outerLinePoints] + setOuterLinePoints(originOuterLinePoints.map((point) => ({ x: point.x + deltaX, y: point.y + deltaY }))) + // Move all related objects by the delta allRoofObject.forEach((obj) => { if (obj.points !== undefined) { diff --git a/src/hooks/floorPlan/estimate/useEstimateController.js b/src/hooks/floorPlan/estimate/useEstimateController.js index 4cbe4521..b46d21ed 100644 --- a/src/hooks/floorPlan/estimate/useEstimateController.js +++ b/src/hooks/floorPlan/estimate/useEstimateController.js @@ -241,7 +241,11 @@ export const useEstimateController = (planNo, flag) => { //북면 먼저 체크 if (estimateData.fileFlg === '0') { - if (estimateData?.northArrangement === '1') { + if (estimateData?.northArrangement === '1' && + !estimateData?.moduleModel + ?.replace(/\s+/g, '') // 모든 공백 제거 + ?.toUpperCase() + ?.includes('RE.RISE-NBCAG')) { fileFlg = false setIsGlobalLoading(false) return swalFire({ text: getMessage('estimate.detail.save.requiredNorthArrangementFileUpload'), type: 'alert', icon: 'warning' }) diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index 73fb92f8..0f8c04d3 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -743,7 +743,19 @@ export const useTrestle = () => { if (!data || data.length === 0) { return } - itemList = data + //itemList = data +// itemList에 northModuleYn 추가 + itemList = data.map(item => { + if (item.itemTpCd === "MODULE") { + const matchedModule = modules.find(module => module.moduleItemId === item.itemId); + return { + ...item, + northModuleYn: matchedModule?.northModuleYn || 'N' + }; + } + return item; + }); + //northArrangement 북면 설치 여부 const northArrangement = getNorthArrangement() @@ -2586,6 +2598,7 @@ export const useTrestle = () => { return { moduleTpCd: module.moduleInfo.itemTp, moduleItemId: module.moduleInfo.itemId, + northModuleYn: module?.moduleInfo?.northModuleYn || 'N' // 기본값 'N' } }) @@ -2597,6 +2610,7 @@ export const useTrestle = () => { moduleTpCd: cur.moduleTpCd, moduleItemId: cur.moduleItemId, cnt: 0, + northModuleYn: cur.northModuleYn } } acc[key].cnt++ @@ -2609,6 +2623,11 @@ export const useTrestle = () => { moduleTpCd: groupedParam.moduleTpCd, moduleItemId: groupedParam.moduleItemId, moduleCnt: groupedParam.cnt, + northModuleYn: groupedParam.northModuleYn + // northModuleYn: params.find(p => + // p.moduleTpCd === groupedParam.moduleTpCd && + // p.moduleItemId === groupedParam.moduleItemId + // )?.northModuleYn || 'N' } }) } diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index af3cee04..072a2987 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -42,6 +42,7 @@ import { useEvent } from '@/hooks/useEvent' import { logger } from '@/util/logger' import { useText } from '@/hooks/useText' import { usePolygon } from '@/hooks/usePolygon' +import { getDegreeByChon } from '@/util/canvas-util' const defaultDotLineGridSetting = { INTERVAL: { @@ -177,8 +178,8 @@ export function useCanvasSetting(executeEffect = true) { raft: item.raftBase && parseInt(item.raftBase), layout: ['ROOF_ID_SLATE', 'ROOF_ID_SINGLE'].includes(item.roofMatlCd) ? ROOF_MATERIAL_LAYOUT.STAIRS : ROOF_MATERIAL_LAYOUT.PARALLEL, hajebichi: item.roofPchBase && parseInt(item.roofPchBase), - pitch: item.pitch ? parseInt(item.pitch) : 4, - angle: item.angle ? parseInt(item.angle) : 21.8, + pitch: item.inclBase ? parseInt(item.inclBase) : 4, + angle: getDegreeByChon(item.inclBase ? parseInt(item.inclBase): 4) //item.angle ? parseInt(item.angle) : 21.8, })) setRoofMaterials(roofLists) return roofLists diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 7d1d326a..795086fb 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -8,6 +8,7 @@ import { useSwal } from '@/hooks/useSwal' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' import { calcLinePlaneSize } from '@/util/qpolygon-utils' +import { getSelectLinePosition } from '@/util/skeleton-utils' import { useMouse } from '@/hooks/useMouse' //동선이동 형 올림 내림 @@ -91,7 +92,7 @@ export function useMovementSetting(id) { } wall.baseLines.forEach((line) => { if (type === TYPE.UP_DOWN) { - line.set({ selectable: true, visible: true, stroke: '#1083E3', strokeWidth: 5 }) + line.set({ selectable: true, visible: true, stroke: '#1085E5', strokeWidth: 5 }) line.setCoords() line.bringToFront() } else { @@ -102,7 +103,7 @@ export function useMovementSetting(id) { /** outerLines 속성처리*/ const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') - outerLines.forEach((line) => line.set({ visible: false })) + outerLines.forEach((line) => line.set({ visible: true })) canvas.renderAll() }, [type]) @@ -179,99 +180,238 @@ export function useMovementSetting(id) { name: 'followLine', }) canvas.add(followLine) + followLine.bringToFront() FOLLOW_LINE_REF.current = followLine - canvas.on('mouse:move', (event) => { - const mousePos = getIntersectMousePoint(event) - if (followLine.x1 === followLine.x2) { - followLine.left = mousePos.x - 2 - } else { - followLine.top = mousePos.y - 2 - } - canvas.renderAll() - }) - canvas.renderAll() }, [currentObject]) + const clearRef = () => { if (type === TYPE.FLOW_LINE) { - FLOW_LINE_REF.POINTER_INPUT_REF.current.value = '' - FLOW_LINE_REF.FILLED_INPUT_REF.current.value = '' - FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = true - FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked = false - } - if (type === TYPE.UP_DOWN) { - UP_DOWN_REF.POINTER_INPUT_REF.current.value = '' - UP_DOWN_REF.FILLED_INPUT_REF.current.value = '' - UP_DOWN_REF.UP_RADIO_REF.current.checked = true - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false - } - } + // 안전한 ref 접근 + if (FLOW_LINE_REF.POINTER_INPUT_REF.current) { + FLOW_LINE_REF.POINTER_INPUT_REF.current.value = '' + } + if (FLOW_LINE_REF.FILLED_INPUT_REF.current) { + FLOW_LINE_REF.FILLED_INPUT_REF.current.value = '' + } - const mouseMoveEvent = (e) => { - const target = canvas.getActiveObject() - if (!target) return + const upRightChecked = FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current?.checked || false + const downLeftChecked = FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current?.checked || false - const { top: targetTop, left: targetLeft } = target - const currentX = Big(getIntersectMousePoint(e).x) //.round(0, Big.roundUp) - const currentY = Big(getIntersectMousePoint(e).y) //.round(0, Big.roundUp) - let value = '' - if (target.y1 === target.y2) { - value = Big(targetTop).minus(currentY).times(10).round(0) - } else { - value = Big(targetLeft).minus(currentX).times(10).round(0).neg() - } - if (typeRef.current === TYPE.FLOW_LINE) { - FLOW_LINE_REF.POINTER_INPUT_REF.current.value = value.toNumber() - } else { - UP_DOWN_REF.POINTER_INPUT_REF.current.value = value.abs().toNumber() - const midX = Big(target.x1).plus(target.x2).div(2) - const midY = Big(target.y1).plus(target.y2).div(2) - const wall = canvas.getObjects().find((obj) => obj.id === target.attributes.wallId) - let checkPoint - if (target.y1 === target.y2) { - checkPoint = { x: midX.toNumber(), y: midY.plus(10).toNumber() } - if (wall.inPolygon(checkPoint)) { - if (value.s === -1) { - UP_DOWN_REF.UP_RADIO_REF.current.checked = false - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true - } else { - UP_DOWN_REF.UP_RADIO_REF.current.checked = true - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false - } - } else { - if (value.s === 1) { - UP_DOWN_REF.UP_RADIO_REF.current.checked = false - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true - } else { - UP_DOWN_REF.UP_RADIO_REF.current.checked = true - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false - } + if (upRightChecked || downLeftChecked) { + if (FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current) { + FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked = !downLeftChecked + } + if (FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current) { + FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = !upRightChecked } } else { - checkPoint = { x: midX.plus(10).toNumber(), y: midY.toNumber() } - if (wall.inPolygon(checkPoint)) { - if (value.s === 1) { - UP_DOWN_REF.UP_RADIO_REF.current.checked = false - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true - } else { - UP_DOWN_REF.UP_RADIO_REF.current.checked = true - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false - } - } else { - if (value.s === -1) { - UP_DOWN_REF.UP_RADIO_REF.current.checked = false - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true - } else { - UP_DOWN_REF.UP_RADIO_REF.current.checked = true - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false - } + if (FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current) { + FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = true + } + if (FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current) { + FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked = false } } } + + if (type === TYPE.UP_DOWN) { + // 안전한 ref 접근 + if (UP_DOWN_REF.POINTER_INPUT_REF.current) { + UP_DOWN_REF.POINTER_INPUT_REF.current.value = '' + } + if (UP_DOWN_REF.FILLED_INPUT_REF.current) { + UP_DOWN_REF.FILLED_INPUT_REF.current.value = '' + } + if (UP_DOWN_REF.UP_RADIO_REF.current) { + UP_DOWN_REF.UP_RADIO_REF.current.checked = true + } + if (UP_DOWN_REF.DOWN_RADIO_REF.current) { + UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false + } + } } + let currentCalculatedValue = 0 + + const mouseMoveEvent = (e) => { + //console.log('mouseMoveEvent:::::',e) + // 기존에는 activeObject를 사용했으나, 이 기능에서는 선택된 라인을 비선택(selectable:false) 상태로 두므로 + // 항상 selectedObject.current를 기준으로 계산한다. + const target = selectedObject.current + if (!target) return + + // 디버깅 로그 추가 + // if (typeRef.current === TYPE.UP_DOWN) { + // console.log('UP_DOWN_REF.POINTER_INPUT_REF.current:', UP_DOWN_REF.POINTER_INPUT_REF.current); + // if (!UP_DOWN_REF.POINTER_INPUT_REF.current) { + // console.warn('UP_DOWN_REF.POINTER_INPUT_REF.current is null/undefined'); + // } + // } + + const { top: targetTop, left: targetLeft } = target + const currentX = Big(getIntersectMousePoint(e).x) + const currentY = Big(getIntersectMousePoint(e).y) + + let value = '' + let direction = '' + + if (Math.abs(target.y1 - target.y2) < 0.5) { // 수평 라인 + value = Big(targetTop).minus(currentY).times(10).round(0) + + // 방향 감지 + if (value.toNumber() > 0) { + direction = 'up' // 마우스가 라인 위쪽에 있음 (위로 움직임) + } else if (value.toNumber() < 0) { + direction = 'down' // 마우스가 라인 아래쪽에 있음 (아래로 움직임) + } + } else { // 수직 라인 + value = Big(targetLeft).minus(currentX).times(10).round(0).neg() + + // 방향 감지 + if (value.toNumber() > 0) { + direction = 'right' // 마우스가 라인 오른쪽에 있음 (오른쪽으로 움직임) + } else if (value.toNumber() < 0) { + direction = 'left' // 마우스가 라인 왼쪽에 있음 (왼쪽으로 움직임) + } + } + + // followLine도 포인터를 따라가도록 동기화 (하나의 mouse:move 핸들러만 사용) + const followLine = FOLLOW_LINE_REF.current + if (followLine) { + if (followLine.x1 === followLine.x2) { + // 수직 라인: x만 이동 + followLine.left = currentX.toNumber() - 2 + } else { + // 수평 라인: y만 이동 + followLine.top = currentY.toNumber() - 2 + } + followLine.bringToFront() + followLine.setCoords && followLine.setCoords() + canvas.renderAll() + } + + // 방향 정보를 사용하여 라디오 버튼 상태 업데이트 + + currentCalculatedValue = value.toNumber() + + if (typeRef.current === TYPE.FLOW_LINE) { + // ref가 존재하는지 확인 후 값 설정 + if (FLOW_LINE_REF.POINTER_INPUT_REF.current) { + FLOW_LINE_REF.POINTER_INPUT_REF.current.value = value.toNumber() + } + } else { + // UP_DOWN 타입일 때 안전한 접근 + if (UP_DOWN_REF.POINTER_INPUT_REF.current) { + UP_DOWN_REF.POINTER_INPUT_REF.current.value = value.abs().toNumber() + } + + const midX = Big(target.x1).plus(target.x2).div(2) + const midY = Big(target.y1).plus(target.y2).div(2) + const wall = canvas.getObjects().find((obj) => obj.id === target.attributes.wallId) + + + + const result = getSelectLinePosition(wall, target, { + testDistance: 5, // 테스트 거리 + debug: true // 디버깅 로그 출력 + }); + //console.log("1111litarget:::::", target); + //console.log("1111linePosition:::::", result.position); // 'top', 'bottom', 'left', 'right' + + let linePosition = result.position; +//console.log("1111linePosition:::::", direction, linePosition); + + if (target.y1 === target.y2) { //수평벽 + + const setRadioStates = (isUp) => { + if (UP_DOWN_REF.UP_RADIO_REF.current) { + UP_DOWN_REF.UP_RADIO_REF.current.checked = isUp; + } + if (UP_DOWN_REF.DOWN_RADIO_REF.current) { + UP_DOWN_REF.DOWN_RADIO_REF.current.checked = !isUp; + } + }; + + if (linePosition === 'top') { + setRadioStates(value.s !== -1); + } else if (linePosition === 'bottom') { + setRadioStates(value.s !== 1); + } + + if(direction === 'up') { + + } + /* + checkPoint = { x: midX.toNumber(), y: midY.plus(10).toNumber() } + if (wall.inPolygon(checkPoint)) { //선택라인이 내부 + if (value.s === -1) { + console.log('1value:::', value.s) + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true + } else { + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false + } + } else { // + if (value.s === 1) { //선택라인이 외부 + console.log('2value:::', value.s) + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true + } else { + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false + } + } + */ + } else { + + const setRadioStates = (isUp) => { + if (UP_DOWN_REF.UP_RADIO_REF.current) { + UP_DOWN_REF.UP_RADIO_REF.current.checked = isUp; + } + if (UP_DOWN_REF.DOWN_RADIO_REF.current) { + UP_DOWN_REF.DOWN_RADIO_REF.current.checked = !isUp; + } + }; + + if (linePosition === 'left') { + setRadioStates(value.s !== 1); + } else if (linePosition === 'right') { + setRadioStates(value.s !== -1); + } + /* + checkPoint = { x: midX.plus(10).toNumber(), y: midY.toNumber() } + if (wall.inPolygon(checkPoint)) { + if (value.s === 1) { + console.log('3value:::', value.s) + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true + } else { + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false + + } + } else { + if (value.s === -1) { + console.log('-1value:::', value.s) + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = false + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true + } else { + if (UP_DOWN_REF.UP_RADIO_REF.current) UP_DOWN_REF.UP_RADIO_REF.current.checked = true + if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false + } + } + */ + } + } + + } + + + const mouseDownEvent = (e) => { canvas .getObjects() @@ -279,6 +419,7 @@ export function useMovementSetting(id) { .forEach((obj) => canvas.remove(obj)) canvas.renderAll() + //const target = selectedObject.current const target = selectedObject.current if (!target) return @@ -313,14 +454,37 @@ export function useMovementSetting(id) { FOLLOW_LINE_REF.current = null canvas.renderAll() } + if (UP_DOWN_REF.current !== null) { + canvas.remove(UP_DOWN_REF.current) + UP_DOWN_REF.current = null + canvas.renderAll() + } + const target = selectedObject.current !== null ? selectedObject.current : CONFIRM_LINE_REF.current?.target if (!target) return const roofId = target.attributes.roofId const roof = canvas.getObjects().find((obj) => obj.id === roofId) + + // 현이동, 동이동 추가 + let flPointValue = FLOW_LINE_REF.POINTER_INPUT_REF.current?.value ?? 0; + let flFilledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current?.value ?? 0; + flPointValue = (flFilledValue > 0 || flFilledValue < 0) ? flFilledValue : flPointValue; + const moveFlowLine = typeRef.current === TYPE.FLOW_LINE ? flPointValue : 0 + + let udPointValue = UP_DOWN_REF.POINTER_INPUT_REF.current?.value ?? 0; + let udFilledValue = UP_DOWN_REF.FILLED_INPUT_REF.current?.value ?? 0; + udPointValue = udFilledValue > 0 ? udFilledValue : udPointValue; + const moveUpDown = typeRef.current === TYPE.UP_DOWN ? udPointValue : 0 + roof.moveFlowLine = parseInt(moveFlowLine, 10) || 0; + roof.moveUpDown = parseInt(moveUpDown, 10) || 0; + roof.moveDirect = ""; + roof.moveSelectLine = target + //console.log("target::::", target, roof.moveSelectLine) const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) const baseLines = wall.baseLines + let centerPoint = wall.getCenterPoint(); let targetBaseLines = [] let isGableRoof if (typeRef.current === TYPE.FLOW_LINE) { @@ -340,7 +504,7 @@ export function useMovementSetting(id) { isGableRoof = false } const lineVector = - target.y1 === target.y2 + Math.abs(target.y1 - target.y2) < 0.2 ? FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked ? 'up' : 'down' @@ -348,6 +512,7 @@ export function useMovementSetting(id) { ? 'right' : 'left' let checkBaseLines, currentBaseLines + roof.moveDirect = lineVector switch (lineVector) { case 'up': checkBaseLines = baseLines.filter((line) => line.y1 === line.y2 && line.y1 < target.y1) @@ -406,10 +571,23 @@ export function useMovementSetting(id) { break } } else { + roof.moveDirect = UP_DOWN_REF.UP_RADIO_REF.current.checked ? 'out' : UP_DOWN_REF.DOWN_RADIO_REF.current.checked ? 'in' : 'out' targetBaseLines.push({ line: target, distance: 0 }) } - targetBaseLines.sort((a, b) => a.distance - b.distance) + // Remove duplicate lines + const uniqueLines = new Map(); + targetBaseLines = targetBaseLines.filter(item => { + const key = `${item.line.x1},${item.line.y1},${item.line.x2},${item.line.y2}`; + if (!uniqueLines.has(key)) { + uniqueLines.set(key, true); + return true; + } + return false; + }); + + // Sort by distance + targetBaseLines.sort((a, b) => a.distance - b.distance); targetBaseLines = targetBaseLines.filter((line) => line.distance === targetBaseLines[0].distance) if (isGableRoof) { @@ -442,18 +620,28 @@ export function useMovementSetting(id) { let value if (typeRef.current === TYPE.FLOW_LINE) { - value = - FLOW_LINE_REF.FILLED_INPUT_REF.current.value !== '' - ? Big(FLOW_LINE_REF.FILLED_INPUT_REF.current.value).times(2) - : Big(FLOW_LINE_REF.POINTER_INPUT_REF.current.value).times(2) + value = (() => { + const filledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current?.value; + const pointerValue = FLOW_LINE_REF.POINTER_INPUT_REF.current?.value; + + if (filledValue && !isNaN(filledValue) && filledValue.trim() !== '') { + return Big(filledValue).times(2); + } else if (pointerValue && !isNaN(pointerValue) && pointerValue.trim() !== '') { + return Big(pointerValue).times(2); + } + return Big(0); // 기본값으로 0 반환 또는 다른 적절한 기본값 + })(); if (target.y1 === target.y2) { value = value.neg() } } else { - value = - UP_DOWN_REF.FILLED_INPUT_REF.current.value !== '' - ? Big(UP_DOWN_REF.FILLED_INPUT_REF.current.value) - : Big(UP_DOWN_REF.POINTER_INPUT_REF.current.value) + console.log("error::", UP_DOWN_REF.POINTER_INPUT_REF.current.value) + value = Big( + (UP_DOWN_REF?.FILLED_INPUT_REF?.current?.value?.trim() || + UP_DOWN_REF?.POINTER_INPUT_REF?.current?.value?.trim() || + '0' + ) + ); const midX = Big(target.x1).plus(target.x2).div(2) const midY = Big(target.y1).plus(target.y2).div(2) @@ -467,17 +655,50 @@ export function useMovementSetting(id) { const inPolygon = wall.inPolygon(checkPoint) - if (UP_DOWN_REF.UP_RADIO_REF.current.checked && inPolygon) { - value = value.neg() - } else if (UP_DOWN_REF.DOWN_RADIO_REF.current.checked && !inPolygon) { - value = value.neg() - } + // if (UP_DOWN_REF.UP_RADIO_REF.current.checked && inPolygon) { + // value = value.neg() + // } else if (UP_DOWN_REF.DOWN_RADIO_REF.current.checked && !inPolygon) { + // value = value.neg() + // } } + // console.log("2222titarget:::::", target); + // console.log("2222저장된 moveSelectLine:", roof.moveSelectLine); + // console.log("222wall::::", wall.points) + const result = getSelectLinePosition(wall, target, { + testDistance: 5, // 테스트 거리 + debug: true // 디버깅 로그 출력 + }); + + //console.log("2222linePosition:::::", result.position); + //console.log("222moveDirect:::::", roof.moveDirect); + + +// 디버깅용 분류 결과 확인 + + let linePosition = result.position; + roof.movePosition = linePosition value = value.div(10) targetBaseLines .filter((line) => Math.sqrt(Math.pow(line.line.x2 - line.line.x1, 2) + Math.pow(line.line.y2 - line.line.y1, 2)) >= 1) .forEach((target) => { const currentLine = target.line + + //console.log("linePosition::::::::::::::", linePosition) + if (UP_DOWN_REF?.DOWN_RADIO_REF?.current?.checked ){ + //position확인 + if(linePosition === 'bottom' || linePosition === 'right') { + //console.log("1value::::::::::::::", value.toString()) + value = value.neg() + + } + }else { + if(linePosition === 'top' || linePosition === 'left') { + //console.log("1value::::::::::::::", value.toString()) + value = value.neg() + } + } + + //console.log("2value::::::::::::::", value.toString()) const index = baseLines.findIndex((line) => line === currentLine) const nextLine = baseLines[(index + 1) % baseLines.length] const prevLine = baseLines[(index - 1 + baseLines.length) % baseLines.length] @@ -530,6 +751,9 @@ export function useMovementSetting(id) { closePopup(id) } + // javascript + + return { TYPE, closePopup, @@ -541,3 +765,4 @@ export function useMovementSetting(id) { handleSave, } } + diff --git a/src/hooks/roofcover/useOuterLineWall.js b/src/hooks/roofcover/useOuterLineWall.js index e5f14cc1..ebe1d8ad 100644 --- a/src/hooks/roofcover/useOuterLineWall.js +++ b/src/hooks/roofcover/useOuterLineWall.js @@ -252,6 +252,7 @@ export function useOuterLineWall(id, propertiesId) { canvas?.renderAll() setOuterLineFix(true) closePopup(id) + ccwCheck() addPopup(propertiesId, 1, ) } @@ -905,6 +906,51 @@ export function useOuterLineWall(id, propertiesId) { } } + // 시계방향으로 그려진 경우 반시게방향으로 변경 + const ccwCheck = () => { + let outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') + + if (outerLines.length < 2) { + swalFire({ text: getMessage('wall.line.not.found') }) + return + } + + /** + * 외벽선이 시계방향인지 시계반대 방향인지 확인 + */ + const outerLinePoints = outerLines.map((line) => ({ x: line.x1, y: line.y1 })) + let counterClockwise = true + let signedArea = 0 + + outerLinePoints.forEach((point, index) => { + const nextPoint = outerLinePoints[(index + 1) % outerLinePoints.length] + signedArea += point.x * nextPoint.y - point.y * nextPoint.x + }) + + if (signedArea > 0) { + counterClockwise = false + } + /** 시계 방향일 경우 외벽선 reverse*/ + if (!counterClockwise) { + outerLines.reverse().forEach((line, index) => { + addLine([line.x2, line.y2, line.x1, line.y1], { + stroke: line.stroke, + strokeWidth: line.strokeWidth, + idx: index, + selectable: line.selectable, + name: 'outerLine', + x1: line.x2, + y1: line.y2, + x2: line.x1, + y2: line.y1, + visible: line.visible, + }) + canvas.remove(line) + }) + canvas.renderAll() + } + } + return { points, setPoints, diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index e6d7a825..f93e230d 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -9,6 +9,7 @@ import { basicSettingState, correntObjectNoState, corridorDimensionSelector, + outlineDisplaySelector, roofDisplaySelector, roofMaterialsSelector, selectedRoofMaterialSelector, @@ -29,6 +30,8 @@ import { QcastContext } from '@/app/QcastProvider' import { usePlan } from '@/hooks/usePlan' import { roofsState } from '@/store/roofAtom' import { useText } from '@/hooks/useText' +import { processEaveHelpLines } from '@/util/skeleton-utils' +import { QLine } from '@/components/fabric/QLine' export function useRoofAllocationSetting(id) { const canvas = useRecoilValue(canvasState) @@ -59,9 +62,11 @@ export function useRoofAllocationSetting(id) { const { saveCanvas } = usePlan() const [roofsStore, setRoofsStore] = useRecoilState(roofsState) const [moduleSelectionData, setModuleSelectionData] = useRecoilState(moduleSelectionDataState) + const outerLinePoints = useRecoilValue(outerLinePointsState) const resetPoints = useResetRecoilState(outerLinePointsState) const [corridorDimension, setCorridorDimension] = useRecoilState(corridorDimensionSelector) const { changeCorridorDimensionText } = useText() + const outlineDisplay = useRecoilValue(outlineDisplaySelector) useEffect(() => { /** 배치면 초기설정에서 선택한 지붕재 배열 설정 */ @@ -109,46 +114,54 @@ export function useRoofAllocationSetting(id) { */ const fetchBasicSettings = async (planNo) => { try { - await get({ url: `/api/canvas-management/canvas-basic-settings/by-object/${correntObjectNo}/${planNo}` }).then((res) => { - let roofsArray = {} - - if (res.length > 0) { - roofsArray = res.map((item) => { - return { - planNo: item.planNo, - roofApply: item.roofApply, - roofSeq: item.roofSeq, - roofMatlCd: item.roofMatlCd, - roofWidth: item.roofWidth, - roofHeight: item.roofHeight, - roofHajebichi: item.roofHajebichi, - roofGap: item.roofGap, - roofLayout: item.roofLayout, - roofPitch: item.roofPitch, - roofAngle: item.roofAngle, - } - }) - } else { - if (roofList.length > 0) { - roofsArray = roofList - } else { - roofsArray = [ - { - planNo: planNo, - roofApply: true, - roofSeq: 0, - roofMatlCd: 'ROOF_ID_WA_53A', - roofWidth: 265, - roofHeight: 235, - roofHajebichi: 0, - roofGap: 'HEI_455', - roofLayout: 'P', + const response = await get({ url: `/api/canvas-management/canvas-basic-settings/by-object/${correntObjectNo}/${planNo}` }); + + let roofsArray = []; + + // API에서 데이터를 성공적으로 가져온 경우 + if (response && response.length > 0) { + roofsArray = response.map((item, index) => ({ + planNo: item.planNo, + roofApply: item.roofApply, + roofSeq: item.roofSeq || index, + roofMatlCd: item.roofMatlCd, + roofWidth: item.roofWidth, + roofHeight: item.roofHeight, + roofHajebichi: item.roofHajebichi, + roofGap: item.roofGap, + roofLayout: item.roofLayout, + roofPitch: item.roofPitch, + roofAngle: item.roofAngle, + selected: index === 0, // 첫 번째 항목을 기본 선택으로 설정 + index: index + })); + } + // API에서 데이터가 없고 기존 roofList가 있는 경우 + else if (roofList && roofList.length > 0) { + roofsArray = roofList.map((roof, index) => ({ + ...roof, + selected: index === 0 // 첫 번째 항목을 기본 선택으로 설정 + })); + } + // 둘 다 없는 경우 기본값 설정 + else { + roofsArray = [ + { + planNo: planNo, + roofApply: true, + roofSeq: 0, + roofMatlCd: 'ROOF_ID_WA_53A', + roofWidth: 265, + roofHeight: 235, + roofHajebichi: 0, + roofGap: 'HEI_455', + roofLayout: 'P', roofPitch: 4, roofAngle: 21.8, }, ] } - } + /** * 데이터 설정 @@ -200,7 +213,7 @@ export function useRoofAllocationSetting(id) { angle: roof.angle ?? '', })) setCurrentRoofList(normalizedRoofs) - }) + } catch (error) { console.error('Data fetching error:', error) } @@ -303,11 +316,53 @@ export function useRoofAllocationSetting(id) { addPopup(popupId, 1, ) } else { apply() + //기존 지붕 선은 남겨둔다. + drawOriginRoofLine() resetPoints() + basicSettingSave() } } + const drawOriginRoofLine = () => { + // outerLinePoints 배열을 이용하여 빨간색 Line 객체들 생성 + if (outerLinePoints && outerLinePoints.length > 1) { + // 연속된 점들을 연결하여 라인 생성 + for (let i = 0; i < outerLinePoints.length - 1; i++) { + const point1 = outerLinePoints[i] + const point2 = outerLinePoints[i + 1] + + const line = new fabric.Line([point1.x, point1.y, point2.x, point2.y], { + stroke: 'black', + strokeDashArray: [5, 2], + strokeWidth: 1, + selectable: false, + name: 'originRoofOuterLine', + visible: outlineDisplay, + }) + + canvas.add(line) + } + + // 마지막 점과 첫 점을 연결하여 폐곡선 만들기 + if (outerLinePoints.length > 2) { + const lastPoint = outerLinePoints[outerLinePoints.length - 1] + const firstPoint = outerLinePoints[0] + + const closingLine = new fabric.Line([lastPoint.x, lastPoint.y, firstPoint.x, firstPoint.y], { + stroke: 'red', + strokeWidth: 2, + selectable: false, + name: 'originRoofOuterLine', + }) + + canvas.add(closingLine) + } + + canvas.renderAll() + } + } + /** * 지붕재 오른쪽 마우스 클릭 후 단일로 지붕재 변경 필요한 경우 */ @@ -327,11 +382,18 @@ export function useRoofAllocationSetting(id) { setBasicSetting((prev) => { return { ...prev, selectedRoofMaterial: newRoofList.find((roof) => roof.selected) } }) + const selectedRoofMaterial = newRoofList.find((roof) => roof.selected) + const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && obj.roofMaterial?.index === selectedRoofMaterial.index) + + roofs.forEach((roof) => { + setSurfaceShapePattern(roof, roofDisplay.column, false, { ...selectedRoofMaterial }, true) + drawDirectionArrow(roof) + }) setRoofList(newRoofList) setRoofMaterials(newRoofList) setRoofsStore(newRoofList) - const selectedRoofMaterial = newRoofList.find((roof) => roof.selected) + setSurfaceShapePattern(currentObject, roofDisplay.column, false, selectedRoofMaterial, true) drawDirectionArrow(currentObject) modifyModuleSelectionData() @@ -404,6 +466,22 @@ export function useRoofAllocationSetting(id) { const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) roofBases.forEach((roofBase) => { try { + + const roofEaveHelpLines = canvas.getObjects().filter((obj) => obj.lineName === 'eaveHelpLine' && obj.roofId === roofBase.id) + if (roofEaveHelpLines.length > 0) { + if (roofBase.lines) { + // Filter out any eaveHelpLines that are already in lines to avoid duplicates + const existingEaveLineIds = new Set(roofBase.lines.map((line) => line.id)) + const newEaveLines = roofEaveHelpLines.filter((line) => !existingEaveLineIds.has(line.id)) + roofBase.lines = [...newEaveLines] + } else { + roofBase.lines = [...roofEaveHelpLines] + } + if (!roofBase.innerLines) { + roofBase.innerLines = [] + } + } + if (roofBase.separatePolygon.length > 0) { splitPolygonWithSeparate(roofBase.separatePolygon) } else { @@ -569,7 +647,7 @@ export function useRoofAllocationSetting(id) { * 피치 변경 */ const handleChangePitch = (e, index) => { - let value = e.target.value + let value = e //e.target.value const reg = /^[0-9]+(\.[0-9]{0,1})?$/ if (!reg.test(value)) { diff --git a/src/hooks/roofcover/useRoofShapeSetting.js b/src/hooks/roofcover/useRoofShapeSetting.js index edf0e7b6..9e1d00ff 100644 --- a/src/hooks/roofcover/useRoofShapeSetting.js +++ b/src/hooks/roofcover/useRoofShapeSetting.js @@ -179,46 +179,6 @@ export function useRoofShapeSetting(id) { let outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') let direction - if (outerLines.length < 2) { - swalFire({ text: getMessage('wall.line.not.found') }) - return - } - - /** - * 외벽선이 시계방향인지 시계반대 방향인지 확인 - */ - const outerLinePoints = outerLines.map((line) => ({ x: line.x1, y: line.y1 })) - let counterClockwise = true - let signedArea = 0 - - outerLinePoints.forEach((point, index) => { - const nextPoint = outerLinePoints[(index + 1) % outerLinePoints.length] - signedArea += point.x * nextPoint.y - point.y * nextPoint.x - }) - - if (signedArea > 0) { - counterClockwise = false - } - /** 시계 방향일 경우 외벽선 reverse*/ - if (!counterClockwise) { - outerLines.reverse().forEach((line, index) => { - addLine([line.x2, line.y2, line.x1, line.y1], { - stroke: line.stroke, - strokeWidth: line.strokeWidth, - idx: index, - selectable: line.selectable, - name: 'outerLine', - x1: line.x2, - y1: line.y2, - x2: line.x1, - y2: line.y1, - visible: line.visible, - }) - canvas.remove(line) - }) - canvas.renderAll() - } - if ([1, 2, 3, 5, 6, 7, 8].includes(shapeNum)) { // 변별로 설정이 아닌 경우 경사를 지붕재에 적용해주어야함 setRoofPitch() @@ -507,7 +467,7 @@ export function useRoofShapeSetting(id) { originX: 'center', originY: 'center', }) - polygon.setViewLengthText(false) + // polygon.setViewLengthText(false) polygon.lines = [...outerLines] addPitchTextsByOuterLines() diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js index e42a7025..b26ee195 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -1,7 +1,13 @@ 'use client' import { 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 { degreesToRadians } from '@turf/turf' @@ -879,7 +885,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { } }) - // roof.fire('polygonMoved') + roof.fire('polygonMoved') roof.fire('modified') drawDirectionArrow(roof) changeCorridorDimensionText() @@ -1451,6 +1457,50 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { // 그룹화할 객체들 배열 (currentObject + relatedObjects) const objectsToGroup = [currentObject, ...relatedObjects] + // 회전 카운트 초기화 및 최초 상태 저장 + if (!currentObject.rotationCount) { + currentObject.rotationCount = 0 + } + + // 최초 회전일 때 (rotationCount === 0) 원본 상태 저장 + if (currentObject.rotationCount === 0) { + objectsToGroup.forEach((obj) => { + if (!obj.originalState) { + obj.originalState = { + left: obj.left, + top: obj.top, + angle: obj.angle || 0, + points: obj.type === 'QPolygon' ? JSON.parse(JSON.stringify(obj.points)) : null, + scaleX: obj.scaleX || 1, + scaleY: obj.scaleY || 1, + } + } + }) + } + + // 회전 카운트 증가 (먼저 증가시켜서 목표 각도 계산) + currentObject.rotationCount = (currentObject.rotationCount + 1) % 4 + + // 목표 회전 각도 계산 (원본 기준) + const targetAngle = currentObject.rotationCount * 90 + + // 원본 상태로 먼저 복원한 후 목표 각도만큼 회전 + objectsToGroup.forEach((obj) => { + if (obj.originalState) { + // 원본 상태로 복원 + obj.set({ + left: obj.originalState.left, + top: obj.originalState.top, + angle: obj.originalState.angle, + scaleX: obj.originalState.scaleX, + scaleY: obj.originalState.scaleY, + }) + if (obj.originalState.points && obj.type === 'QPolygon') { + obj.set({ points: JSON.parse(JSON.stringify(obj.originalState.points)) }) + } + } + }) + // 기존 객체들을 캔버스에서 제거 objectsToGroup.forEach((obj) => canvas.remove(obj)) @@ -1463,12 +1513,8 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { // 그룹을 캔버스에 추가 canvas.add(group) - // 현재 회전값에 90도 추가 - const currentAngle = group.angle || 0 - const newAngle = (currentAngle + 90) % 360 - - // 그룹 전체를 회전 - group.rotate(newAngle) + // 목표 각도로 회전 (원본 기준) + group.rotate(targetAngle) group.setCoords() // 그룹을 해제하고 개별 객체로 복원 diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js index d991a2cf..cc4ac608 100644 --- a/src/hooks/useCanvasEvent.js +++ b/src/hooks/useCanvasEvent.js @@ -402,7 +402,8 @@ export function useCanvasEvent() { } } else { zoom = canvasZoom - 10 - if (zoom < 10) { //50%->10% + if (zoom < 10) { + //50%->10% return } } @@ -412,8 +413,33 @@ export function useCanvasEvent() { const handleZoomClear = () => { setCanvasZoom(100) - canvas.set({ zoom: 1 }) - canvas.viewportTransform = [1, 0, 0, 1, 0, 0] + + zoomToAllObjects() + canvas.renderAll() + } + + const zoomToAllObjects = () => { + const objects = canvas.getObjects().filter((obj) => obj.visible) + if (objects.length === 0) return + + let minX = Infinity, + minY = Infinity + let maxX = -Infinity, + maxY = -Infinity + + objects.forEach((obj) => { + const bounds = obj.getBoundingRect() + minX = Math.min(minX, bounds.left) + minY = Math.min(minY, bounds.top) + maxX = Math.max(maxX, bounds.left + bounds.width) + maxY = Math.max(maxY, bounds.top + bounds.height) + }) + + const centerX = (minX + maxX) / 2 + const centerY = (minY + maxY) / 2 + const centerPoint = new fabric.Point(centerX, centerY) + + canvas.zoomToPoint(centerPoint, 1) canvas.renderAll() } diff --git a/src/hooks/useCirCuitTrestle.js b/src/hooks/useCirCuitTrestle.js index a35d3c8c..83bb0b84 100644 --- a/src/hooks/useCirCuitTrestle.js +++ b/src/hooks/useCirCuitTrestle.js @@ -42,7 +42,18 @@ export function useCircuitTrestle(executeEffect = false) { } } // PCS 아이템 목록 - const getPcsItemList = () => { + const getPcsItemList = (isMultiModule = false) => { + if (isMultiModule) { + return models + .filter((model) => model.pcsTpCd !== 'INDFCS') + .map((model) => { + return { + itemId: model.itemId, + pcsMkrCd: model.pcsMkrCd, + pcsSerCd: model.pcsSerCd, + } + }) + } return models.map((model) => { return { itemId: model.itemId, @@ -53,7 +64,18 @@ export function useCircuitTrestle(executeEffect = false) { } // 선택된 PCS 아이템 목록 - const getSelectedPcsItemList = () => { + const getSelectedPcsItemList = (isMultiModule = false) => { + if (isMultiModule) { + return selectedModels + .filter((model) => model.pcsTpCd !== 'INDFCS') + .map((model) => { + return { + itemId: model.itemId, + pcsMkrCd: model.pcsMkrCd, + pcsSerCd: model.pcsSerCd, + } + }) + } return selectedModels.map((model) => { return { itemId: model.itemId, @@ -95,6 +117,7 @@ export function useCircuitTrestle(executeEffect = false) { uniqueId: module.id ? module.id : null, } }), + roofSurfaceNorthYn: obj.direction === 'north' ? 'Y' : 'N', } }) .filter((surface) => surface.moduleList.length > 0) diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.js index 4580f3ee..a863fd01 100644 --- a/src/hooks/useContextMenu.js +++ b/src/hooks/useContextMenu.js @@ -123,7 +123,7 @@ export function useContextMenu() { }, [currentContextMenu]) useEffect(() => { - console.log('currentObject', currentObject) + //console.log('currentObject', currentObject) if (currentObject?.name) { switch (currentObject.name) { case 'triangleDormer': @@ -162,6 +162,7 @@ export function useContextMenu() { case 'auxiliaryLine': case 'hip': case 'ridge': + case 'eaveHelpLine': if (selectedMenu === 'surface') { setContextMenu([ [ diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 69eb955c..8eebe849 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1809,6 +1809,7 @@ export function useMode() { const currentWall = line.currentWall const nextWall = line.nextWall const index = line.index + addPoint + const direction = currentWall.direction const xDiff = Big(currentWall.x1).minus(Big(nextWall.x1)) const yDiff = Big(currentWall.y1).minus(Big(nextWall.y1)) const offsetCurrentPoint = offsetPolygon[index] @@ -1820,7 +1821,11 @@ export function useMode() { x: xDiff.eq(0) ? offsetCurrentPoint.x : nextWall.x1, y: yDiff.eq(0) ? offsetCurrentPoint.y : nextWall.y1, } - const diffOffset = Big(nextWall.attributes.offset).minus(Big(currentWall.attributes.offset)) + + let diffOffset = ['top', 'right'].includes(direction) + ? Big(nextWall.attributes.offset).minus(Big(currentWall.attributes.offset)) + : Big(currentWall.attributes.offset).minus(Big(nextWall.attributes.offset)) + const offsetPoint2 = { x: yDiff.eq(0) ? offsetPoint1.x : Big(offsetPoint1.x).plus(diffOffset).toNumber(), y: xDiff.eq(0) ? offsetPoint1.y : Big(offsetPoint1.y).plus(diffOffset).toNumber(), diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 2ae37440..fe8fbd78 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -845,6 +845,8 @@ export const usePolygon = () => { polygonLines.forEach((line) => { line.need = true }) + // 순서에 의존하지 않도록 모든 조합을 먼저 확인한 후 처리 + const innerLineMapping = new Map() // innerLine -> polygonLine 매핑 저장 // innerLines와 polygonLines의 겹침을 확인하고 type 변경 innerLines.forEach((innerLine) => { @@ -854,14 +856,26 @@ export const usePolygon = () => { if (innerLine.attributes && polygonLine.attributes.type) { // innerLine이 polygonLine보다 긴 경우 polygonLine.need를 false로 변경 if (polygonLine.length < innerLine.length) { - polygonLine.need = false + if (polygonLine.lineName !== 'eaveHelpLine') { + polygonLine.need = false + } } - innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize - innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize - innerLine.attributes.type = polygonLine.attributes.type - innerLine.direction = polygonLine.direction - innerLine.attributes.isStart = true - innerLine.parentLine = polygonLine + // innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize + // innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize + // innerLine.attributes.type = polygonLine.attributes.type + // innerLine.direction = polygonLine.direction + // innerLine.attributes.isStart = true + // innerLine.parentLine = polygonLine + + // 매핑된 innerLine의 attributes를 변경 (교차점 계산 전에 적용) + innerLineMapping.forEach((polygonLine, innerLine) => { + innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize + innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize + innerLine.attributes.type = polygonLine.attributes.type + innerLine.direction = polygonLine.direction + innerLine.attributes.isStart = true + innerLine.parentLine = polygonLine + }) } } }) @@ -1386,7 +1400,10 @@ export const usePolygon = () => { }) if (startFlag && endFlag) { - if (!representLines.includes(line) && line.attributes.type === LINE_TYPE.WALLLINE.EAVES) { + if ( + !representLines.includes(line) && + (line.attributes.type === LINE_TYPE.WALLLINE.EAVES || line.attributes.type === LINE_TYPE.WALLLINE.EAVE_HELP_LINE) + ) { representLines.push(line) } else if (!representLines.includes(line) && line.attributes.type === LINE_TYPE.WALLLINE.HIPANDGABLE) { representLines.push(line) @@ -1567,50 +1584,125 @@ export const usePolygon = () => { // ==== Dijkstra pathfinding ==== + // function findShortestPath(start, end, graph, epsilon = 1) { + // const startKey = pointToKey(start, epsilon) + // const endKey = pointToKey(end, epsilon) + // + // const distances = {} + // const previous = {} + // const visited = new Set() + // const queue = [{ key: startKey, dist: 0 }] + // + // for (const key in graph) distances[key] = Infinity + // distances[startKey] = 0 + // + // while (queue.length > 0) { + // queue.sort((a, b) => a.dist - b.dist) + // const { key } = queue.shift() + // if (visited.has(key)) continue + // visited.add(key) + // + // for (const neighbor of graph[key] || []) { + // const neighborKey = pointToKey(neighbor.point, epsilon) + // const alt = distances[key] + neighbor.distance + // if (alt < distances[neighborKey]) { + // distances[neighborKey] = alt + // previous[neighborKey] = key + // queue.push({ key: neighborKey, dist: alt }) + // } + // } + // } + // + // const path = [] + // let currentKey = endKey + // + // if (!previous[currentKey]) return null + // + // while (currentKey !== startKey) { + // const [x, y] = currentKey.split(',').map(Number) + // path.unshift({ x, y }) + // currentKey = previous[currentKey] + // } + // + // const [sx, sy] = startKey.split(',').map(Number) + // path.unshift({ x: sx, y: sy }) + // + // return path + // } + function findShortestPath(start, end, graph, epsilon = 1) { const startKey = pointToKey(start, epsilon) const endKey = pointToKey(end, epsilon) - const distances = {} + // 거리와 이전 노드 추적 + const distances = { [startKey]: 0 } const previous = {} const visited = new Set() + + // 우선순위 큐 (거리가 짧은 순으로 정렬) const queue = [{ key: startKey, dist: 0 }] - for (const key in graph) distances[key] = Infinity - distances[startKey] = 0 + // 모든 노드 초기화 + for (const key in graph) { + if (key !== startKey) { + distances[key] = Infinity + } + } - while (queue.length > 0) { + // 우선순위 큐에서 다음 노드 선택 + const getNextNode = () => { + if (queue.length === 0) return null queue.sort((a, b) => a.dist - b.dist) - const { key } = queue.shift() - if (visited.has(key)) continue - visited.add(key) + return queue.shift() + } - for (const neighbor of graph[key] || []) { + let current + while ((current = getNextNode())) { + const currentKey = current.key + + // 목적지에 도달하면 종료 + if (currentKey === endKey) break + + // 이미 방문한 노드는 건너뜀 + if (visited.has(currentKey)) continue + visited.add(currentKey) + + // 인접 노드 탐색 + for (const neighbor of graph[currentKey] || []) { const neighborKey = pointToKey(neighbor.point, epsilon) - const alt = distances[key] + neighbor.distance - if (alt < distances[neighborKey]) { + if (visited.has(neighborKey)) continue + + const alt = distances[currentKey] + neighbor.distance + + // 더 짧은 경로를 찾은 경우 업데이트 + if (alt < (distances[neighborKey] || Infinity)) { distances[neighborKey] = alt - previous[neighborKey] = key + previous[neighborKey] = currentKey + + // 우선순위 큐에 추가 queue.push({ key: neighborKey, dist: alt }) } } } + // 경로 재구성 const path = [] let currentKey = endKey - if (!previous[currentKey]) return null - - while (currentKey !== startKey) { + // 시작점에 도달할 때까지 역추적 + while (previous[currentKey] !== undefined) { const [x, y] = currentKey.split(',').map(Number) path.unshift({ x, y }) currentKey = previous[currentKey] } - const [sx, sy] = startKey.split(',').map(Number) - path.unshift({ x: sx, y: sy }) + // 시작점 추가 + if (path.length > 0) { + const [sx, sy] = startKey.split(',').map(Number) + path.unshift({ x: sx, y: sy }) + } - return path + return path.length > 0 ? path : null } // 최종 함수 diff --git a/src/lib/authActions.js b/src/lib/authActions.js index 5e7055d0..4a5b2c94 100644 --- a/src/lib/authActions.js +++ b/src/lib/authActions.js @@ -49,6 +49,7 @@ export async function setSession(data) { session.custCd = data.custCd session.isLoggedIn = true session.builderNo = data.builderNo + session.custNm = data.custNm await session.save() } diff --git a/src/locales/ja.json b/src/locales/ja.json index 2999728f..e738ebaa 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -614,7 +614,7 @@ "qna.sub.title": "お問合せリスト", "qna.reg.header.regDt": "お問い合わせ登録日", "qna.reg.header.regUserNm": "名前", - "qna.reg.header.regUserTelNo": "お問い合わせ", + "qna.reg.header.regUserTelNo": "電話番号", "qna.reg.header.type": "お問い合わせ区分", "qna.reg.header.title": "お問い合わせタイトル", "qna.reg.header.contents": "お問い合わせ内容", @@ -1089,6 +1089,7 @@ "module.circuit.minimun.error": "回路番号は1以上の数値を入力してください。", "module.already.exist.error": "回路番号が同じで異なるパワーコンディショナのモジュールがあります。 別の回路番号を設定してください。", "module.circuit.fix.not.same.roof.error": "異なる屋根面のモジュールが選択されています。 モジュールの選択をや直してください。", + "module.circuit.indoor.focused.error": "混合モジュールと屋内集中PCSを組み合わせる場合は、手動回路割り当てのみ対応可能です。", "construction.length.difference": "屋根面工法をすべて選択してください。", "menu.validation.canvas.roof": "パネルを配置するには、屋根面を入力する必要があります。", "batch.object.outside.roof": "オブジェクトは屋根に設置する必要があります。", diff --git a/src/locales/ko.json b/src/locales/ko.json index 4f6601cd..bed23c44 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1089,6 +1089,7 @@ "module.circuit.minimun.error": "회로번호는 1 이상입력해주세요.", "module.already.exist.error": "회로번호가 같은 다른 파워 컨디셔너 모듈이 있습니다. 다른 회로번호를 설정하십시오.", "module.circuit.fix.not.same.roof.error": "다른 지붕면의 모듈이 선택되어 있습니다. 모듈 선택을 다시 하세요.", + "module.circuit.indoor.focused.error": "혼합 모듈과 실내 집중형 PCS를 조합하는 경우, 수동 회로 할당만 가능합니다.", "construction.length.difference": "지붕면 공법을 모두 선택하십시오.", "menu.validation.canvas.roof": "패널을 배치하려면 지붕면을 입력해야 합니다.", "batch.object.outside.roof": "오브젝트는 지붕내에 설치해야 합니다.", diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index 4465f09a..b85f0268 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -133,8 +133,23 @@ $alert-color: #101010; color: $pop-color; font-weight: 700; } - .modal-close{ + .modal-btn-wrap{ margin-left: auto; + display: flex; + align-items: center; + gap: 15px; + } + .modal-fold{ + display: block; + width: 15px; + height: 15px; + background: url(../../public/static/images/canvas/penal_arr_white.svg)no-repeat center; + background-size: contain; + &.act{ + transform: rotate(180deg); + } + } + .modal-close{ color: transparent; font-size: 0; width: 10px; diff --git a/src/styles/_reset.scss b/src/styles/_reset.scss index c0c4a2cb..472243ed 100644 --- a/src/styles/_reset.scss +++ b/src/styles/_reset.scss @@ -460,7 +460,11 @@ button{ } } - +.table-select{ + height: 20px; + color: #fff !important; + font-size: 11px !important; +} // input .form-input{ label{ diff --git a/src/util/canvas-util.js b/src/util/canvas-util.js index 243936da..b2ee57db 100644 --- a/src/util/canvas-util.js +++ b/src/util/canvas-util.js @@ -269,7 +269,7 @@ export const getDegreeByChon = (chon) => { * @returns {number} */ export const getChonByDegree = (degree) => { - return Number((Math.tan((degree * Math.PI) / 180) * 10).toFixed(1)) + return Number((Math.tan((degree * Math.PI) / 180) * 10).toFixed(2)) } /** @@ -1036,11 +1036,11 @@ export const getDegreeInOrientation = (degree) => { { min: -51, max: -37, value: -45 }, { min: -36, max: -22, value: -30 }, { min: -21, max: -7, value: -15 }, - { min: -6, max: 0, value: 0 } + { min: -6, max: 0, value: 0 }, ] // 해당 범위에 맞는 값 찾기 - const range = degreeRanges.find(range => degree >= range.min && degree <= range.max) + const range = degreeRanges.find((range) => degree >= range.min && degree <= range.max) return range ? range.value : degree } diff --git a/src/util/fabric-extensions.js b/src/util/fabric-extensions.js index 9410f764..acccc601 100644 --- a/src/util/fabric-extensions.js +++ b/src/util/fabric-extensions.js @@ -29,22 +29,39 @@ fabric.Rect.prototype.getCurrentPoints = function () { /** * fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용) - * 그룹의 groupPoints를 다시 계산하여 반환 + * 그룹 내 객체들의 점들을 수집하여 현재 월드 좌표를 반환 */ fabric.Group.prototype.getCurrentPoints = function () { - // groupPoints를 다시 계산 + // 그룹 내 객체들로부터 실시간으로 점들을 계산 + if (this._objects && this._objects.length > 0) { + let allPoints = [] - // 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우) - if (this.groupPoints && Array.isArray(this.groupPoints)) { - const matrix = this.calcTransformMatrix() - console.log('this.groupPoints', this.groupPoints) - return this.groupPoints.map(function (p) { - const point = new fabric.Point(p.x, p.y) - return fabric.util.transformPoint(point, matrix) + // 그룹 내 모든 객체의 점들을 수집 + this._objects.forEach(function (obj) { + if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') { + const objPoints = obj.getCurrentPoints() + allPoints = allPoints.concat(objPoints) + } else if (obj.points && Array.isArray(obj.points)) { + const pathOffset = obj.pathOffset || { x: 0, y: 0 } + const matrix = obj.calcTransformMatrix() + const transformedPoints = obj.points + .map(function (p) { + return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y) + }) + .map(function (p) { + return fabric.util.transformPoint(p, matrix) + }) + allPoints = allPoints.concat(transformedPoints) + } }) + + if (allPoints.length > 0) { + // Convex Hull 알고리즘을 사용하여 외곽 점들만 반환 + return this.getConvexHull(allPoints) + } } - // groupPoints가 없으면 바운딩 박스를 사용 + // 객체가 없으면 바운딩 박스를 사용 const bounds = this.getBoundingRect() const points = [ { x: bounds.left, y: bounds.top }, diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 576926f0..9eb774e1 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -12027,7 +12027,11 @@ export const drawRidgeRoof = (roofId, canvas, textMode) => { .filter((line) => (line.x2 === ridge.x1 && line.y2 === ridge.y1) || (line.x2 === ridge.x2 && line.y2 === ridge.y2)) .filter((line) => baseLines.filter((baseLine) => baseLine.x1 === line.x1 && baseLine.y1 === line.y1).length > 0) basePoints.sort((a, b) => a.line.attributes.planeSize - b.line.attributes.planeSize) - hipSize = Big(basePoints[0].line.attributes.planeSize) + if (basePoints.length > 0 && basePoints[0].line) { + hipSize = Big(basePoints[0].line.attributes.planeSize) + } else { + hipSize = Big(0) // 또는 기본값 설정 + } } hipSize = hipSize.pow(2).div(2).sqrt().round().div(10).toNumber() diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 54cb5ba2..383c82e0 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -4,6 +4,8 @@ import { calcLineActualSize, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygo import { QLine } from '@/components/fabric/QLine' import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' +import { QPolygon } from '@/components/fabric/QPolygon' +import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -12,38 +14,259 @@ import Big from 'big.js' * @param {string} textMode - 텍스트 표시 모드 * @param pitch */ - +const EPSILON = 0.1 export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { - let roof = canvas?.getObjects().find((object) => object.id === roofId) - const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) - - const hasNonParallelLines = roof.lines.filter((line) => Big(line.x1).minus(Big(line.x2)).gt(1) && Big(line.y1).minus(Big(line.y2)).gt(1)) - if (hasNonParallelLines.length > 0) { - return - } - - const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE] - const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] - - /** 외벽선 */ - const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) - - - - //const skeletonLines = []; - // 1. 지붕 폴리곤 좌표 전처리 - const coordinates = preprocessPolygonCoordinates(roof.points); - if (coordinates.length < 3) { - console.warn("Polygon has less than 3 unique points. Cannot generate skeleton."); - return; - } - // 2. 스켈레톤 생성 및 그리기 - skeletonBuilder(roofId, canvas, textMode, roof, baseLines) + skeletonBuilder(roofId, canvas, textMode) } + +const movingLineFromSkeleton = (roofId, canvas) => { + + let roof = canvas?.getObjects().find((object) => object.id === roofId) + + + let moveDirection = roof.moveDirect; + let moveFlowLine = roof.moveFlowLine??0; + let moveUpDown = roof.moveUpDown??0; + const getSelectLine = () => roof.moveSelectLine; + const selectLine = getSelectLine(); + let movePosition = roof.movePosition; + + const startPoint = selectLine.startPoint + const endPoint = selectLine.endPoint + const orgRoofPoints = roof.points; // orgPoint를 orgPoints로 변경 + const oldPoints = canvas?.skeleton.lastPoints ?? orgRoofPoints // 여기도 변경 + const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); + + const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) + const baseLines = wall.baseLines + roof.basePoints = createOrderedBasePoints(roof.points, baseLines) + + const skeletonPolygon = canvas.getObjects().filter((object) => object.skeletonType === 'polygon' && object.parentId === roofId) + const skeletonLines = canvas.getObjects().filter((object) => object.skeletonType === 'line' && object.parentId === roofId) + + if (oppositeLine) { + console.log('Opposite line found:', oppositeLine); + } else { + console.log('No opposite line found'); + } + + if(moveFlowLine !== 0) { + return oldPoints.map((point, index) => { + console.log('Point:', point); + const newPoint = { ...point }; + const absMove = Big(moveFlowLine).times(2).div(10); + + console.log('skeletonBuilder moveDirection:', moveDirection); + + switch (moveDirection) { + case 'left': + // Move left: decrease X + if (moveFlowLine !== 0) { + for (const line of oppositeLine) { + if (line.position === 'left') { + if (isSamePoint(newPoint, line.start)) { + newPoint.x = Big(line.start.x).plus(absMove).toNumber(); + } else if (isSamePoint(newPoint, line.end)) { + newPoint.x = Big(line.end.x).plus(absMove).toNumber(); + } + + break; + + } + + } + } else if (moveUpDown !== 0) { + + } + + break; + case 'right': + for (const line of oppositeLine) { + if (line.position === 'right') { + if (isSamePoint(newPoint, line.start)) { + newPoint.x = Big(line.start.x).minus(absMove).toNumber(); + } else if (isSamePoint(newPoint, line.end)) { + newPoint.x = Big(line.end.x).minus(absMove).toNumber(); + } + break + + } + + } + + break; + case 'up': + // Move up: decrease Y (toward top of screen) + + for (const line of oppositeLine) { + if (line.position === 'top') { + if (isSamePoint(newPoint, line.start)) { + newPoint.y = Big(line.start.y).minus(absMove).toNumber(); + } else if (isSamePoint(newPoint, line.end)) { + newPoint.y = Big(line.end.y).minus(absMove).toNumber(); + } + break; + + } + } + + break; + case 'down': + // Move down: increase Y (toward bottom of screen) + for (const line of oppositeLine) { + + if (line.position === 'bottom') { + + console.log('oldPoint:', point); + + if (isSamePoint(newPoint, line.start)) { + newPoint.y = Big(line.start.y).minus(absMove).toNumber(); + + } else if (isSamePoint(newPoint, line.end)) { + newPoint.y = Big(line.end.y).minus(absMove).toNumber(); + } + + + break; + } + + } + break; + default : +// 사용 예시 + } + + console.log('newPoint:', newPoint); + //baseline 변경 + return newPoint; + }) + } else if(moveUpDown !== 0) { + + + // const selectLine = getSelectLine(); + // + // console.log("wall::::", wall.points) + // console.log("저장된 3333moveSelectLine:", roof.moveSelectLine); + // console.log("저장된 3moveSelectLine:", selectLine); + // const result = getSelectLinePosition(wall, selectLine, { + // testDistance: 5, // 테스트 거리 + // debug: true // 디버깅 로그 출력 + // }); + // console.log("3333linePosition:::::", result.position); + + const position = movePosition //result.position; + const moveUpDownLength = Big(moveUpDown).times(1).div(10); + const modifiedStartPoints = []; + // oldPoints를 복사해서 새로운 points 배열 생성 + let newPoints = oldPoints.map(point => ({...point})); + + // selectLine과 일치하는 baseLines 찾기 + const matchingLines = baseLines + .map((line, index) => ({ ...line, findIndex: index })) + .filter(line => + (isSamePoint(line.startPoint, selectLine.startPoint) && + isSamePoint(line.endPoint, selectLine.endPoint)) || + (isSamePoint(line.startPoint, selectLine.endPoint) && + isSamePoint(line.endPoint, selectLine.startPoint)) + ); + + + matchingLines.forEach(line => { + const originalStartPoint = line.startPoint; + const originalEndPoint = line.endPoint; + const offset = line.attributes.offset + // 새로운 좌표 계산 + let newStartPoint = {...originalStartPoint}; + let newEndPoint = {...originalEndPoint} +// 원본 라인 업데이트 + // newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트 + console.log('absMove::', moveUpDownLength); + newPoints.forEach((point, index) => { + if(position === 'bottom'){ + if (moveDirection === 'in') { + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).minus(moveUpDownLength).toNumber(); + } + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.y = Big(point.y).minus(absMove).toNumber(); + // } + }else if (moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).plus(moveUpDownLength).toNumber(); + } + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.y = Big(point.y).plus(absMove).toNumber(); + // } + } + + }else if (position === 'top'){ + if(moveDirection === 'in'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.y = Big(point.y).plus(moveUpDownLength).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).plus(moveUpDownLength).toNumber(); + } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.y = Big(point.y).minus(moveUpDownLength).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).minus(moveUpDownLength).toNumber(); + } + } + + }else if(position === 'left'){ + if(moveDirection === 'in'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).plus(moveUpDownLength).toNumber(); + } + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.x = Big(point.x).plus(absMove).toNumber(); + // } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).minus(moveUpDownLength).toNumber(); + } + // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + // point.x = Big(point.x).minus(absMove).toNumber(); + // } + } + + }else if(position === 'right'){ + if(moveDirection === 'in'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.x = Big(point.x).minus(moveUpDownLength).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).minus(moveUpDownLength).toNumber(); + } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.x = Big(point.x).plus(moveUpDownLength).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).plus(moveUpDownLength).toNumber(); + } + } + + } + + }); + + // 원본 baseLine도 업데이트 + line.startPoint = newStartPoint; + line.endPoint = newEndPoint; + }); + return newPoints; + } +} + + /** * SkeletonBuilder를 사용하여 스켈레톤을 생성하고 내부선을 그립니다. * @param {string} roofId - 지붕 ID @@ -52,18 +275,77 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { * @param {fabric.Object} roof - 지붕 객체 * @param baseLines */ -export const skeletonBuilder = (roofId, canvas, textMode, roof, baseLines) => { - const geoJSONPolygon = toGeoJSON(roof.points) +export const skeletonBuilder = (roofId, canvas, textMode) => { + //처마 + let roof = canvas?.getObjects().find((object) => object.id === roofId) + + // [추가] wall 객체를 찾아 roof.lines에 wallId를 직접 주입 (초기화) + // 지붕은 벽을 기반으로 생성되므로 라인의 순서(Index)가 동일합니다. + const wallObj = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + + if (roof && wallObj && roof.lines && wallObj.lines) { + // 개선된 코드 (기하학적 매칭) + // or use some other unique properties + + + roof.lines.forEach((rLine, index) => { + // 벽 라인 중에서 시작점과 끝점이 일치하는 라인 찾기 + const wLine = wallObj.lines[index] + if (wLine) { + // 안정적인 ID 생성 + rLine.attributes.wallLine = wLine.id; // Use the stable ID + + // ... + } + }) + } + + const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE] + const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] + + /** 외벽선 */ + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + //const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) + + const baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || [] + const baseLinePoints = baseLines.map((line) => ({ x: line.left, y: line.top })) + + const outerLines = canvas.getObjects().filter((object) => object.name === 'outerLinePoint') || [] + const outerLinePoints = outerLines.map((line) => ({ x: line.left, y: line.top })) + + 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. 지붕 폴리곤 좌표 전처리 + 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 + + let points = roof.points + + + //마루이동 + if (moveFlowLine !== 0 || moveUpDown !== 0) { + points = movingLineFromSkeleton(roofId, canvas) + } + + console.log('points:', points) + const geoJSONPolygon = toGeoJSON(points) try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 geoJSONPolygon.pop() const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) - console.log(`지붕 형태: ${skeleton.roof_type}`, skeleton.edge_analysis) - // 스켈레톤 데이터를 기반으로 내부선 생성 - roof.innerLines = createInnerLinesFromSkeleton(skeleton, canvas, textMode, roof, baseLines) + roof.innerLines = roof.innerLines || [] + roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { @@ -71,12 +353,36 @@ export const skeletonBuilder = (roofId, canvas, textMode, roof, baseLines) => { canvas.skeletonLines = [] } canvas.skeletonStates[roofId] = true + canvas.skeletonLines = [] + canvas.skeletonLines.push(...roof.innerLines) + roof.skeletonLines = canvas.skeletonLines + const cleanSkeleton = { + Edges: skeleton.Edges.map((edge) => ({ + X1: edge.Edge.Begin.X, + Y1: edge.Edge.Begin.Y, + X2: edge.Edge.End.X, + Y2: edge.Edge.End.Y, + Polygon: edge.Polygon, + + // Add other necessary properties, but skip circular references + })), + roofId: roofId, + // Add other necessary top-level properties + } + canvas.skeleton = [] + canvas.skeleton = cleanSkeleton + canvas.skeleton.lastPoints = points + canvas.set('skeleton', cleanSkeleton) canvas.renderAll() + + console.log('skeleton rendered.', canvas) } catch (e) { console.error('스켈레톤 생성 중 오류 발생:', e) if (canvas.skeletonStates) { canvas.skeletonStates[roofId] = false + canvas.skeletonStates = {} + canvas.skeletonLines = [] } } } @@ -90,19 +396,73 @@ export const skeletonBuilder = (roofId, canvas, textMode, roof, baseLines) => { * @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none') * @param {Array} baseLines - 원본 외벽선 QLine 객체 배열 */ -const createInnerLinesFromSkeleton = (skeleton,canvas, textMode, roof, baseLines) => { +const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if (!skeleton?.Edges) return [] - const skeletonLines = [] + const roof = canvas?.getObjects().find((object) => object.id === roofId) + const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) + let skeletonLines = [] + let findPoints = []; + const processedInnerEdges = new Set() + const textElements = {}; + + const coordinateText = (line) => { + // Generate a stable ID for this line + const lineKey = `${line.x1},${line.y1},${line.x2},${line.y2}`; + + // Remove existing text elements for this line + if (textElements[lineKey]) { + textElements[lineKey].forEach(text => { + if (canvas.getObjects().includes(text)) { + canvas.remove(text); + } + }); + } + + // Create start point text + const startText = new fabric.Text(`(${Math.round(line.x1)}, ${Math.round(line.y1)})`, { + left: line.x1 + 5, + top: line.y1 - 20, + fontSize: 10, + fill: 'magenta', + fontFamily: 'Arial', + selectable: false, + hasControls: false, + hasBorders: false + }); + + // Create end point text + const endText = new fabric.Text(`(${Math.round(line.x2)}, ${Math.round(line.y2)})`, { + left: line.x2 + 5, + top: line.y2 - 20, + fontSize: 10, + fill: 'orange', + fontFamily: 'Arial', + selectable: false, + hasControls: false, + hasBorders: false + }); + + // Add to canvas + canvas.add(startText, endText); + + // Store references + textElements[lineKey] = [startText, endText]; + + // Bring lines to front + canvas.bringToFront(startText); + canvas.bringToFront(endText); + }; // 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다. + skeleton.Edges.forEach((edgeResult, index) => { - processEavesEdge(edgeResult, processedInnerEdges, roof, skeletonLines, baseLines[index].attributes.pitch); + + + processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines); }); -/* // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다. - skeleton.Edges.forEach(edgeResult => { const { Begin, End } = edgeResult.Edge; @@ -134,52 +494,1021 @@ const createInnerLinesFromSkeleton = (skeleton,canvas, textMode, roof, baseLines } }); -* - //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. - const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, baseLines); + /* + //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. + const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); - if(disconnectedLines.length > 0) { + if(disconnectedLines.length > 0) { - disconnectedLines.forEach(dLine => { - const { index, extendedLine, p1Connected, p2Connected } = dLine; - const newPoint = extendedLine?.point; - if (!newPoint) return; - // p1이 끊어졌으면 p1을, p2가 끊어졌으면 p2를 연장된 지점으로 업데이트 - if (p1Connected) { //p2 연장 - skeletonLines[index].p2 = { ...skeletonLines[index].p2, x: newPoint.x, y: newPoint.y }; - } else if (p2Connected) {//p1 연장 - skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y }; - } - }); + disconnectedLines.forEach(dLine => { + const { index, extendedLine, p1Connected, p2Connected } = dLine; + const newPoint = extendedLine?.point; + if (!newPoint) return; + // p1이 끊어졌으면 p1을, p2가 끊어졌으면 p2를 연장된 지점으로 업데이트 + if (p1Connected) { //p2 연장 + skeletonLines[index].p2 = { ...skeletonLines[index].p2, x: newPoint.x, y: newPoint.y }; + } else if (p2Connected) {//p1 연장 + skeletonLines[index].p1 = { ...skeletonLines[index].p1, x: newPoint.x, y: newPoint.y }; + } + }); - //2-1 확장된 스켈레톤 선이 연장되다가 서로 만나면 만난점(접점)에서 멈추어야 된다. - trimIntersectingExtendedLines(skeletonLines, disconnectedLines); + //2-1 확장된 스켈레톤 선이 연장되다가 서로 만나면 만난점(접점)에서 멈추어야 된다. + trimIntersectingExtendedLines(skeletonLines, disconnectedLines); - } + } + */ -*/ + //2. 연결이 끊어진 라인이 있을경우 찾아서 추가한다(동 이동일때) // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다. const innerLines = []; - skeletonLines.forEach(line => { - const { p1, p2, attributes, lineStyle } = line; - const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], { + const addLines = [] + const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set + + //처마라인 + const roofLines = roof.lines + //벽라인 + const wallLines = wall.lines + + skeletonLines.forEach((sktLine, skIndex) => { + let { p1, p2, attributes, lineStyle } = sktLine; + + // 중복방지 - 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교) + const lineKey = [ + [p1.x, p1.y].sort().join(','), + [p2.x, p2.y].sort().join(',') + ].sort().join('|'); + + // 이미 추가된 라인인지 확인 + if (existingLines.has(lineKey)) { + return; // 이미 있는 라인이면 스킵 + } + + const direction = getLineDirection( + { x: sktLine.p1.x, y: sktLine.p1.y }, + { x: sktLine.p2.x, y: sktLine.p2.y } + ); + + //그림을 그릴때 idx 가 필요함 roof는 왼쪽부터 시작됨 - 그림그리는 순서가 필요함 + + + // roofLines.forEach((roofLine) => { + // + // if (isSameLine(p1.x, p1.y, p2.x, p2.y, roofLine) || isSameLine(p2.x, p2.y, p1.x, p1.y, roofLine)) { + // roofIdx = roofLine.idx; + // console.log("roofIdx::::::", roofIdx) + // return false; // forEach 중단 + // } + // }); + + + const skeletonLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, - stroke: lineStyle.color, + stroke: (sktLine.attributes.isOuterEdge)?'orange':lineStyle.color, strokeWidth: lineStyle.width, - name: attributes.type, - textMode: textMode, - attributes: attributes, + name: (sktLine.attributes.isOuterEdge)?'eaves': attributes.type, + attributes: { + ...attributes, + + }, + direction: direction, + isBaseLine: sktLine.attributes.isOuterEdge, + lineName: (sktLine.attributes.isOuterEdge)?'roofLine': attributes.type, + selectable:(!sktLine.attributes.isOuterEdge), + //visible: (!sktLine.attributes.isOuterEdge), }); - canvas.add(innerLine); - innerLine.bringToFront(); - innerLines.push(innerLine); + //coordinateText(skeletonLine) + canvas.add(skeletonLine); + skeletonLine.bringToFront(); + existingLines.add(lineKey); // 추가된 라인을 추적 + + + + //skeleton 라인에서 처마선은 삭제 + if(skeletonLine.lineName === 'roofLine'){ + + skeletonLine.set('visible', false); //임시 + roof.set({ + //stroke: 'black', + strokeWidth: 4 + }); + + + + + + }else{ + + + } + + innerLines.push(skeletonLine) + canvas.renderAll(); }); - canvas.renderAll(); + if((roof.moveUpDown??0 > 0) || (roof.moveFlowLine??0 > 0) ) { + + const getMoveUpDownLine = () => { + // 같은 라인이 없으므로 새 다각형 라인 생성 + //라인 편집 + // let i = 0 + const currentRoofLines = canvas.getObjects().filter((obj) => obj.lineName === 'roofLine' && obj.attributes.roofId === roofId) + let roofLineRects = canvas.getObjects().filter((obj) => obj.name === 'roofLineRect' && obj.roofId === roofId) + + + roofLineRects.forEach((roofLineRect) => { + canvas.remove(roofLineRect) + canvas.renderAll() + }) + + let helpLines = canvas.getObjects().filter((obj) => obj.lineName === 'helpLine' && obj.roofId === roofId) + helpLines.forEach((helpLine) => { + canvas.remove(helpLine) + canvas.renderAll() + }) + + function sortCurrentRoofLines(lines) { + return [...lines].sort((a, b) => { + // Get all coordinates in a consistent order + const getCoords = (line) => { + const x1 = line.x1 ?? line.get('x1'); + const y1 = line.y1 ?? line.get('y1'); + const x2 = line.x2 ?? line.get('x2'); + const y2 = line.y2 ?? line.get('y2'); + + // Sort points left-to-right, then top-to-bottom + return x1 < x2 || (x1 === x2 && y1 < y2) + ? [x1, y1, x2, y2] + : [x2, y2, x1, y1]; + }; + + const aCoords = getCoords(a); + const bCoords = getCoords(b); + + // Compare each coordinate in order + for (let i = 0; i < 4; i++) { + if (Math.abs(aCoords[i] - bCoords[i]) > 0.1) { + return aCoords[i] - bCoords[i]; + } + } + return 0; + }); + } + + // 각 라인 집합 정렬 + + // roofLines의 방향에 맞춰 currentRoofLines의 방향을 조정 + const alignLineDirection = (sourceLines, targetLines) => { + return sourceLines.map(sourceLine => { + // 가장 가까운 targetLine 찾기 + const nearestTarget = targetLines.reduce((nearest, targetLine) => { + const sourceCenter = { + x: (sourceLine.x1 + sourceLine.x2) / 2, + y: (sourceLine.y1 + sourceLine.y2) / 2 + }; + const targetCenter = { + x: (targetLine.x1 + targetLine.x2) / 2, + y: (targetLine.y1 + targetLine.y2) / 2 + }; + const distance = Math.hypot( + sourceCenter.x - targetCenter.x, + sourceCenter.y - targetCenter.y + ); + + return !nearest || distance < nearest.distance + ? { line: targetLine, distance } + : nearest; + }, null)?.line; + + if (!nearestTarget) return sourceLine; + + // 방향이 반대인지 확인 (벡터 내적을 사용) + const sourceVec = { + x: sourceLine.x2 - sourceLine.x1, + y: sourceLine.y2 - sourceLine.y1 + }; + const targetVec = { + x: nearestTarget.x2 - nearestTarget.x1, + y: nearestTarget.y2 - nearestTarget.y1 + }; + + const dotProduct = sourceVec.x * targetVec.x + sourceVec.y * targetVec.y; + + // 내적이 음수이면 방향이 반대이므로 뒤집기 + if (dotProduct < 0) { + return { + ...sourceLine, + x1: sourceLine.x2, + y1: sourceLine.y2, + x2: sourceLine.x1, + y2: sourceLine.y1 + }; + } + + return sourceLine; + }); + }; + + // const sortedWallLines = sortCurrentRoofLines(wall.lines); + // roofLines의 방향에 맞춰 currentRoofLines 조정 후 정렬 + const alignedCurrentRoofLines = alignLineDirection(currentRoofLines, roofLines); + const sortedCurrentRoofLines = sortCurrentRoofLines(alignedCurrentRoofLines); + // const sortedRoofLines = sortCurrentRoofLines(roofLines); + const sortedWallBaseLines = sortCurrentRoofLines(wall.baseLines); + // const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, wallLines); + const sortRoofLines = sortBaseLinesByWallLines(roofLines, wallLines); + + // 원본 wallLines를 복사하여 사용 + const sortedWallLines = [...wallLines]; + const sortedBaseLines = sortBaseLinesByWallLines(wall.baseLines, sortedWallLines); + const sortedRoofLines = sortBaseLinesByWallLines(roofLines, sortedWallLines); + + //wall.lines 는 기본 벽 라인 + //wall.baseLine은 움직인라인 + const movedLines = [] + + // 조건에 맞는 라인들만 필터링 + const validWallLines = wallLines.filter((wallLine, index) => wallLine.idx - 1 === index); + + + validWallLines.forEach((wallLine, index) => { + + const originalIndex = wallLines.indexOf(wallLine); + const roofLine = sortRoofLines[originalIndex]; + const currentRoofLine = currentRoofLines[originalIndex]; + const moveLine = wall.baseLines[originalIndex]; + const wallBaseLine = wall.baseLines[originalIndex]; + + // const roofLine = sortRoofLines[index]; + + if (roofLine.attributes.wallLine !== wallLine.id || (roofLine.idx - 1) !== index) { + console.log("wallLine2::::", wallLine.id) + console.log('roofLine:::', roofLine.attributes.wallLine) + console.log("w:::", wallLine.startPoint, wallLine.endPoint) + console.log("R:::", roofLine.startPoint, roofLine.endPoint) + console.log("not matching roofLine", roofLine); + return false + }//roofLines.find(line => line.attributes.wallLineId === wallLine.attributes.wallId); + + // const currentRoofLine = currentRoofLines[index]; + // const moveLine = wall.baseLines[index] + // const wallBaseLine = wall.baseLines[index] + //console.log("wallBaseLine", wallBaseLine); + + //roofline 외곽선 설정 + console.log("index::::", index) + console.log('roofLine:::', roofLine) + console.log('wallLine', wallLine) + console.log('wallBaseLine', wallBaseLine) + + + const origin = moveLine.attributes?.originPoint + if (!origin) return + + if (isSamePoint(moveLine, wallLine)) { + + return false + } + + const movedStart = Math.abs(moveLine.x1 - wallLine.x1) > EPSILON || Math.abs(moveLine.y1 - origin.y1) > EPSILON + const movedEnd = Math.abs(moveLine.x2 - wallLine.x2) > EPSILON || Math.abs(moveLine.y2 - origin.y2) > EPSILON + + + const fullyMoved = movedStart && movedEnd + + +//반시계 방향 + let newPStart //= {x:roofLine.x1, y:roofLine.y1} + let newPEnd //= {x:movedLines.x2, y:movedLines.y2} + +//현재 roof는 무조건 시계방향 + + const getAddLine = (p1, p2, stroke = '') => { + movedLines.push({ index, p1, p2 }) + +// Usage: + // let mergeLines = mergeMovedLines(movedLines); + //console.log("mergeLines:::::::", mergeLines); + const line = new QLine([p1.x, p1.y, p2.x, p2.y], { + parentId : roof.id, + fontSize : roof.fontSize, + stroke : '#3FBAE6', + strokeWidth: 4, + name : 'eaveHelpLine', + lineName : 'eaveHelpLine', + visible : true, + roofId : roofId, + selectable: true, + hoverCursor: 'pointer', + attributes : { + type : 'eaveHelpLine', + isStart: true, + pitch : wallLine.attributes.pitch, + } + }); + + //coordinateText(line) + canvas.add(line) + line.bringToFront() + canvas.renderAll(); + return line + } + + //getAddLine(roofLine.startPoint, roofLine.endPoint, ) //외곽선을 그린다 + + newPStart = { x: roofLine.x1, y: roofLine.y1 } + newPEnd = { x: roofLine.x2, y: roofLine.y2 } + + const getInnerLines = (lines, point) => { + + } + let isIn = false + let isOut = false + +//두 포인트가 변경된 라인인 + if (fullyMoved) { + //반시계방향향 + + const mLine = getSelectLinePosition(wall, wallBaseLine) + + if (getOrientation(roofLine) === 'vertical') { + + if (['left', 'right'].includes(mLine.position)) { + if (Math.abs(wallLine.x1 - wallBaseLine.x1) < 0.1 || Math.abs(wallLine.x2 - wallBaseLine.x2) < 0.1) { + return false + } + const positionType = + (mLine.position === 'left' && wallLine.x1 < wallBaseLine.x1) || + (mLine.position === 'right' && wallLine.x1 > wallBaseLine.x1) + ? 'in' : 'out'; + const condition = `${mLine.position}_${positionType}`; + let isStartEnd = findInteriorPoint(wallBaseLine, sortedBaseLines) + let sPoint, ePoint; + if (condition === 'left_in') { + isIn = true + + if (isStartEnd.start) { + newPEnd.y = roofLine.y2; + newPEnd.x = roofLine.x2; + + const moveDist = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + ePoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }; + newPStart.y = wallBaseLine.y1 + + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_start' }); + const newPointX = Big(roofLine.x1).plus(moveDist).abs().toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() + let idx = (0 > index - 1) ? roofLines.length : index + const pLineX = roofLines[idx - 1].x1 + + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + + if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + } + + if (isStartEnd.end) { + newPStart.y = roofLine.y1; + newPStart.x = roofLine.x1; + + const moveDist = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 }; + newPEnd.y = wallBaseLine.y2 + + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_end' }); + const newPointX = Big(roofLine.x1).plus(moveDist).toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y2).minus(0).abs().toNumber() + let idx = (roofLines.length < index + 1) ? 0 : index + const pLineX = roofLines[idx + 1].x2 + + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange') + + if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + } + + } else if (condition === 'left_out') { + console.log("left_out::::isStartEnd:::::", isStartEnd); + if (isStartEnd.start) { + + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y1).minus(moveDist).abs().toNumber() + const bStartY = Big(wallLine.y1).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x2 }) + + const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() + newPStart.y = aStartY + newPEnd.y = roofLine.y2 //Big(roofLine.y2).minus(eLineY).toNumber() + let idx = (0 >= index - 1) ? roofLines.length : index + const newLine = roofLines[idx - 1]; + + if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + if (inLine) { + if (inLine.x1 < inLine.x2) { + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } else { + getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: bStartY, x: wallLine.x2 }, 'pink') + } + + } + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') + getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') + findPoints.push({ y: aStartY, x: newPStart.x, position: 'left_out_start' }); + } else { + const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + newPStart.y = Big(newPStart.y).minus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.x1 < inLine.x2) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } + } else { + //newPStart.y = wallLine.y1; + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber(); + newPStart.y = Big(wallBaseLine.y1).minus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.x2 > inLine.x1) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } + } + + } + } + + + if (isStartEnd.end) { + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y2).plus(moveDist).toNumber() + const bStartY = Big(wallLine.y2).plus(moveDist).toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 }) + console.log("startLines:::::::", inLine); + const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() + newPEnd.y = aStartY + newPStart.y = roofLine.y1//Big(roofLine.y1).plus(eLineY).toNumber() + let idx = (roofLines.length < index + 1) ? 0 : index + const newLine = roofLines[idx + 1]; + + if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + if (inLine) { + if (inLine.x1 < inLine.x2) { + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: bStartY, x: wallLine.x1 }, 'pink') + } + } + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') + getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') + findPoints.push({ y: aStartY, x: newPEnd.x, position: 'left_out_end' }); + } else { + const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + newPEnd.y = Big(newPEnd.y).plus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.x1 < inLine.x2) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } else { + + // newPEnd.y = wallLine.y2 + + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber(); + newPEnd.y = Big(wallBaseLine.y2).plus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.x2 > inLine.x1) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } + } + + } + findPoints.push({ y: newPStart.y, x: newPEnd.x, position: 'left_out_end' }); + } + } else if (condition === 'right_in') { + if (isStartEnd.start) { + + newPEnd.y = roofLine.y2; + newPEnd.x = roofLine.x2; + + const moveDist = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + ePoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }; + newPStart.y = wallBaseLine.y1 + + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_start' }); + const newPointX = Big(roofLine.x1).minus(moveDist).abs().toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() + let idx = (0 >= index - 1) ? roofLines.length : index + const pLineX = roofLines[idx - 1].x1 + + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + + if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + } + + if (isStartEnd.end) { + newPStart.y = roofLine.y1; + newPStart.x = roofLine.x1; + + const moveDist = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 }; + newPEnd.y = wallBaseLine.y2 + + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_end' }); + const newPointX = Big(roofLine.x1).minus(moveDist).toNumber() + const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() + const pLineY = Big(roofLine.y2).minus(0).abs().toNumber() + let idx = (roofLines.length < index + 1) ? 0 : index + const pLineX = roofLines[idx + 1].x2 + + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue') + getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange') + + if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } + getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + } + + } else if (condition === 'right_out') { + console.log("right_out::::isStartEnd:::::", isStartEnd); + if (isStartEnd.start) { //x1 inside + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y1).plus(moveDist).abs().toNumber() + const bStartY = Big(wallLine.y1).plus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 }) + console.log("startLines:::::::", inLine); + const eLineY = Big(bStartY).minus(wallLine.y1).abs().toNumber() + newPStart.y = aStartY + newPEnd.y = roofLine.y2//Big(roofLine.y2).plus(eLineY).toNumber() + let idx = (0 >= index - 1) ? roofLines.length : index + const newLine = roofLines[idx - 1]; + + if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { + if (inLine) { + if (inLine.x2 < inLine.x1) { + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: bStartY, x: wallLine.x2 }, 'pink') + } + } + getAddLine({ y: bStartY, x: wallLine.x2 }, { y: roofLine.y1, x: wallLine.x1 }, 'magenta') + getAddLine({ y: newLine.y1, x: newLine.x1 }, { y: newLine.y2, x: wallLine.x2 }, 'Gray') + findPoints.push({ y: aStartY, x: newPEnd.x, position: 'right_out_start' }); + } else { + const cLineY = Big(wallBaseLine.x1).minus(wallLine.x1).abs().toNumber() + newPStart.y = Big(newPStart.y).plus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.x2 < inLine.x1) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } + } else { + //newPStart.y = wallLine.y1; + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.x1).minus(roofLine.x1).abs().toNumber(); + newPStart.y = Big(wallBaseLine.y1).plus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.x2 > inLine.x1) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y1, x: inLine.x1 }, 'purple') + } else { + getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } + } + + } + + } + + } + + if (isStartEnd.end) { + const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const aStartY = Big(roofLine.y2).minus(moveDist).abs().toNumber() + const bStartY = Big(wallLine.y2).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { y: aStartY, x: roofLine.x1 }) + console.log("startLines:::::::", inLine); + const eLineY = Big(bStartY).minus(wallLine.y2).abs().toNumber() + newPEnd.y = aStartY + newPStart.y = roofLine.y1//Big(roofLine.y1).minus(eLineY).toNumber() + let idx = (roofLines.length < index + 1) ? 0 : index + const newLine = roofLines[idx + 1]; + if (inLine) { + if (inLine.x2 < inLine.x1) { + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: inLine.y2, x: inLine.x2 }, 'pink') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: bStartY, x: wallLine.x1 }, 'pink') + } + } + if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { + getAddLine({ y: bStartY, x: wallLine.x1 }, { y: roofLine.y2, x: wallLine.x2 }, 'magenta') + getAddLine({ y: newLine.y2, x: newLine.x2 }, { y: newLine.y1, x: wallLine.x1 }, 'Gray') + findPoints.push({ y: aStartY, x: newPEnd.x, position: 'right_out_end' }); + } else { + const cLineY = Big(wallBaseLine.x2).minus(wallLine.x2).abs().toNumber() + newPEnd.y = Big(newPEnd.y).minus(cLineY).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.x2 < inLine.x1) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } else { + //newPEnd.y = wallLine.y2; + + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.x2).minus(roofLine.x2).abs().toNumber(); + newPEnd.y = Big(wallBaseLine.y2).minus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.x2 > inLine.x1) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y1, x: inLine.x1 }, 'purple') + } else { + getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } + + } + + } + } + } + } + } else if (getOrientation(roofLine) === 'horizontal') { //red + + if (['top', 'bottom'].includes(mLine.position)) { + if (Math.abs(wallLine.y1 - wallBaseLine.y1) < 0.1 || Math.abs(wallLine.y2 - wallBaseLine.y2) < 0.1) { + return false + } + const positionType = + (mLine.position === 'top' && wallLine.y1 < wallBaseLine.y1) || + (mLine.position === 'bottom' && wallLine.y1 > wallBaseLine.y1) + ? 'in' : 'out'; + + const condition = `${mLine.position}_${positionType}`; + let isStartEnd = findInteriorPoint(wallBaseLine, sortedBaseLines) + + let sPoint, ePoint; + + if (condition === 'top_in') { + if (isStartEnd.start) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }; + newPStart.x = wallBaseLine.x1; + + + const newPointY = Big(roofLine.y2).plus(moveDist).toNumber() + + const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber() + const pLineX = Big(roofLine.x1).minus(0).abs().toNumber() + let idx = (0 >= index - 1) ? roofLines.length : index + const pLineY = roofLines[idx - 1].y1 + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_start' }); + + if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') + + } + + if (isStartEnd.end) { + const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } + newPEnd.x = wallBaseLine.x2 + + const newPointY = Big(roofLine.y1).plus(moveDist).toNumber() + + const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber() + const pLineX = Big(roofLine.x2).minus(0).abs().toNumber() + let idx = roofLines.length < index + 1 ? 0 : index + const pLineY = roofLines[idx + 1].y2 + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_end' }); + + if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + + + //getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') + } + + } else if (condition === 'top_out') { + console.log("top_out isStartEnd:::::::", isStartEnd); + + if (isStartEnd.start) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x1).plus(moveDist).toNumber() + const bStartX = Big(wallLine.x1).plus(moveDist).toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) + + const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber() + newPEnd.x = roofLine.x2 //Big(newPEnd.x).plus(eLineX).toNumber() + newPStart.x = aStartX + let idx = (0 > index - 1) ? roofLines.length : index + const newLine = roofLines[idx - 1]; + + if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + if (inLine) { + if (inLine.y2 > inLine.y1) { + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } else { + getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink') + } + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta') + getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'top_out_start' }); + } else { + const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber() + newPStart.x = Big(newPStart.x).plus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.y2 > inLine.y1) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } + + } else { + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.y1).minus(roofLine.y1).abs().toNumber(); + newPStart.x = Big(wallBaseLine.x1).plus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.y2 > inLine.y1) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } + } + } + + } + } + if (isStartEnd.end) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x2).minus(moveDist).abs().toNumber() + const bStartX = Big(wallLine.x2).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: newPEnd.y }) + console.log("startLines:::::::", inLine); + const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() + newPStart.x = roofLine.x1;//Big(newPStart.x).minus(eLineX).abs().toNumber() + newPEnd.x = aStartX + let idx = (roofLines.length < index + 1) ? 0 : index + const newLine = roofLines[idx + 1]; + + if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + if (inLine) { + if (inLine.y2 > inLine.y1) { + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } else { + getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink') + } + + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') + getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'top_out_end' }); + } else { + const cLineX = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + newPEnd.x = Big(newPEnd.x).minus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.y2 > inLine.y1) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } else { + //newPEnd.x = wallLine.x2; + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.y2).minus(roofLine.y2).abs().toNumber(); + newPEnd.x = Big(wallBaseLine.x2).minus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.y1 > inLine.y2) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y1, x: inLine.x1 }, 'purple') + } else { + getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } + } + + } + } + } else if (condition === 'bottom_in') { + if (isStartEnd.start) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 }; + newPStart.x = wallBaseLine.x1; + + + const newPointY = Big(roofLine.y2).minus(moveDist).toNumber() + + const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber() + const pLineX = Big(roofLine.x1).minus(0).abs().toNumber() + let idx = (0 > index - 1) ? roofLines.length : index + const pLineY = roofLines[idx - 1].y1 + getAddLine({ x: newPStart.x, y: newPStart.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_start' }); + + if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') + } + + if (isStartEnd.end) { + const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 }; + newPEnd.x = wallBaseLine.x2; + + + const newPointY = Big(roofLine.y1).minus(moveDist).toNumber() + + const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber() + const pLineX = Big(roofLine.x2).minus(0).abs().toNumber() + let idx = (roofLines.length < index + 1) ? 0 : index + const pLineY = roofLines[idx + 1].y2 + getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: sPoint.x, y: sPoint.y }, 'blue') + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_end' }); + + if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } + getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') + + } + } else if (condition === 'bottom_out') { + console.log("bottom_out isStartEnd:::::::", isStartEnd); + if (isStartEnd.start) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x1).minus(moveDist).abs().toNumber() + const bStartX = Big(wallLine.x1).minus(moveDist).abs().toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 }) + console.log("startLines:::::::", inLine); + const eLineX = Big(bStartX).minus(wallLine.x1).abs().toNumber() + newPEnd.x = roofLine.x2//Big(roofLine.x2).minus(eLineX).toNumber() + newPStart.x = aStartX + let idx = (0 > index - 1) ? roofLines.length : index + const newLine = roofLines[idx - 1]; + + + if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { + if (inLine) { + if (inLine.y2 < inLine.y1) { + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } else { + getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink') + } + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x1, y: wallLine.y1 }, 'magenta') + getAddLine({ x: newLine.x1, y: newLine.y1 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'bottom_out_start' }); + } else { + const cLineX = Big(wallBaseLine.y1).minus(wallLine.y1).abs().toNumber() + newPStart.x = Big(newPStart.x).minus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.y2 < inLine.y1) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } + } else { + //newPStart.x = wallLine.x1; + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.y1).minus(roofLine.y1).abs().toNumber(); + newPStart.x = Big(wallBaseLine.x1).minus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPStart.y, x: newPStart.x }) + if (inLine) { + if (inLine.y2 > inLine.y1) { + getAddLine({ y: newPStart.y, x: newPStart.x }, { y: inLine.y1, x: inLine.x1 }, 'purple') + } else { + getAddLine({ y: inLine.y2, x: inLine.x2 }, { y: newPStart.y, x: newPStart.x }, 'purple') + } + } + } + + } + } + + if (isStartEnd.end) { + const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const aStartX = Big(roofLine.x2).plus(moveDist).toNumber() + const bStartX = Big(wallLine.x2).plus(moveDist).toNumber() + const inLine = findLineContainingPoint(innerLines, { x: aStartX, y: roofLine.y1 }) + console.log("startLines:::::::", inLine); + const eLineX = Big(bStartX).minus(wallLine.x2).abs().toNumber() + newPEnd.x = aStartX + newPStart.x = roofLine.x1;//Big(roofLine.x1).plus(eLineX).toNumber() + let idx = (roofLines.length < index + 1) ? 0 : index + const newLine = roofLines[idx + 1]; + + if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { + if (inLine) { + if (inLine.y2 < inLine.y1) { + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: inLine.x2, y: inLine.y2 }, 'pink') + } else { + getAddLine({ x: inLine.x1, y: inLine.y1 }, { x: bStartX, y: wallLine.y1 }, 'pink') + } + } + getAddLine({ x: bStartX, y: wallLine.y1 }, { x: roofLine.x2, y: wallLine.y2 }, 'magenta') + getAddLine({ x: newLine.x2, y: newLine.y2 }, { x: newLine.x1, y: wallLine.y1 }, 'Gray') + findPoints.push({ x: aStartX, y: newPEnd.y, position: 'bottom_out_end' }); + } else { + const cLineX = Big(wallBaseLine.y2).minus(wallLine.y2).abs().toNumber() + newPEnd.x = Big(newPEnd.x).plus(cLineX).toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.y2 < inLine.y1) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } else { + //newPEnd.x = wallLine.x2; + //외곽 라인 그리기 + const rLineM = Big(wallBaseLine.y2).minus(roofLine.y2).abs().toNumber(); + newPEnd.x = Big(wallBaseLine.x2).plus(rLineM).abs().toNumber(); + const inLine = findLineContainingPoint(innerLines, { y: newPEnd.y, x: newPEnd.x }) + if (inLine) { + if (inLine.y1 > inLine.y2) { + getAddLine({ y: newPEnd.y, x: newPEnd.x }, { y: inLine.y2, x: inLine.x2 }, 'purple') + } else { + getAddLine({ y: inLine.y1, x: inLine.x1 }, { y: newPEnd.y, x: newPEnd.x }, 'purple') + } + } + } + + } + } + } + } + } + + getAddLine(newPStart, newPEnd, 'red') + //canvas.remove(roofLine) + } else { + getAddLine(roofLine.startPoint, roofLine.endPoint,) + } + + + canvas.renderAll() + }); +} + getMoveUpDownLine() + + } + + if (findPoints.length > 0) { + // 모든 점에 대해 라인 업데이트를 누적 + return findPoints.reduce((innerLines, point) => { + return updateAndAddLine(innerLines, point); + }, [...innerLines]); + + } return innerLines; + } /** @@ -190,22 +1519,100 @@ const createInnerLinesFromSkeleton = (skeleton,canvas, textMode, roof, baseLines * @param roof * @param pitch */ -function processEavesEdge(edgeResult, processedInnerEdges, roof, skeletonLines, pitch) { +function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { + let roof = canvas?.getObjects().find((object) => object.id === roofId) + // [1] 벽 객체를 가져옵니다. + let wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId); + const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); - const currentDegree = getDegreeByChon(pitch) + //처마선인지 확인하고 pitch 대입 각 처마선마다 pitch가 다를수 있음 + const { Begin, End } = edgeResult.Edge; + // [2] 현재 처리 중인 엣지가 roof.lines의 몇 번째 인덱스인지 찾습니다. + const roofLineIndex = roof.lines.findIndex(line => + line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line) + ); + + let outerLine = null; + let targetWallId = null; + + // [3] 인덱스를 통해 매칭되는 벽 라인의 불변 ID(wallId)를 가져옵니다. + if (roofLineIndex !== -1) { + outerLine = roof.lines[roofLineIndex]; + if (wall && wall.lines && wall.lines[roofLineIndex]) { + targetWallId = wall.lines[roofLineIndex].attributes.wallId; + } + targetWallId = outerLine.attributes.wallId; + } + + if(!outerLine) { + outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); + console.log('Has matching line:', outerLine); + //if(outerLine === null) return + } + let pitch = outerLine?.attributes?.pitch??0 + + + const convertedPolygon = edgeResult.Polygon?.map(point => ({ + x: typeof point.X === 'number' ? parseFloat(point.X) : 0, + y: typeof point.Y === 'number' ? parseFloat(point.Y) : 0 + })).filter(point => point.x !== 0 || point.y !== 0) || []; + + if (convertedPolygon.length > 0) { + const skeletonPolygon = new QPolygon(convertedPolygon, { + type: POLYGON_TYPE.ROOF, + fill: false, + stroke: 'blue', + strokeWidth: 4, + skeletonType: 'polygon', + polygonName: '', + parentId: roof.id, + }); + //canvas?.add(skeletonPolygon) + //canvas.renderAll() + } + let eavesLines = [] for (let i = 0; i < polygonPoints.length; i++) { const p1 = polygonPoints[i]; const p2 = polygonPoints[(i + 1) % polygonPoints.length]; - // 외벽선에 해당하는 스켈레톤 선은 제외하고 내부선만 추가 - if (!isOuterEdge(p1, p2, [edgeResult.Edge])) { - addRawLine(roof.id, skeletonLines, processedInnerEdges, p1, p2, 'RIDGE', '#FF0000', 3, currentDegree); - } + + // 지붕 경계선과 교차 확인 및 클리핑 + const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine); + //console.log('clipped line', clippedLine.p1, clippedLine.p2); + const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge]) + addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine, targetWallId); + // } } } + +function findMatchingLine(edgePolygon, roof, roofPoints) { + const edgePoints = edgePolygon.map(p => ({ x: p.X, y: p.Y })); + + for (let i = 0; i < edgePoints.length; i++) { + const p1 = edgePoints[i]; + const p2 = edgePoints[(i + 1) % edgePoints.length]; + + for (let j = 0; j < roofPoints.length; j++) { + const rp1 = roofPoints[j]; + const rp2 = roofPoints[(j + 1) % roofPoints.length]; + + if ((isSamePoint(p1, rp1) && isSamePoint(p2, rp2)) || + (isSamePoint(p1, rp2) && isSamePoint(p2, rp1))) { + // 매칭되는 라인을 찾아서 반환 + return roof.lines.find(line => + (isSamePoint(line.p1, rp1) && isSamePoint(line.p2, rp2)) || + (isSamePoint(line.p1, rp2) && isSamePoint(line.p2, rp1)) + ); + } + } + } + return null; +} + + /** * GABLE(케라바) Edge를 처리하여 스켈레톤 선을 정리하고 연장합니다. * @param {object} edgeResult - 스켈레톤 Edge 데이터 @@ -217,7 +1624,7 @@ function processEavesEdge(edgeResult, processedInnerEdges, roof, skeletonLines, function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine, lastSkeletonLines) { const edgePoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); //const polygons = createPolygonsFromSkeletonLines(skeletonLines, selectBaseLine); - console.log("edgePoints::::::", edgePoints) + //console.log("edgePoints::::::", edgePoints) // 1. Initialize processedLines with a deep copy of lastSkeletonLines let processedLines = [] // 1. 케라바 면과 관련된 불필요한 스켈레톤 선을 제거합니다. @@ -232,8 +1639,8 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine, } } - console.log("skeletonLines::::::", skeletonLines) - console.log("lastSkeletonLines", lastSkeletonLines) + //console.log("skeletonLines::::::", skeletonLines) + //console.log("lastSkeletonLines", lastSkeletonLines) // 2. Find common lines between skeletonLines and lastSkeletonLines skeletonLines.forEach(line => { @@ -259,9 +1666,9 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine, // return !isEdgeLine; // }); - console.log("skeletonLines::::::", skeletonLines); - console.log("lastSkeletonLines", lastSkeletonLines); - console.log("processedLines after filtering", processedLines); + //console.log("skeletonLines::::::", skeletonLines); + //console.log("lastSkeletonLines", lastSkeletonLines); + //console.log("processedLines after filtering", processedLines); return processedLines; @@ -292,50 +1699,59 @@ function isOuterEdge(p1, p2, edges) { * 스켈레톤 라인 배열에 새로운 라인을 추가합니다. (중복 방지) * @param id * @param {Array} skeletonLines - 스켈레톤 라인 배열 - * @param {Set} processedInnerEdges - 처리된 Edge 키 Set * @param {object} p1 - 시작점 * @param {object} p2 - 끝점 * @param {string} lineType - 라인 타입 * @param {string} color - 색상 * @param {number} width - 두께 - * @param currentDegree + * @param pitch + * @param isOuterLine */ -function addRawLine(id, skeletonLines, processedInnerEdges, p1, p2, lineType, color, width, currentDegree) { - const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|'); - if (processedInnerEdges.has(edgeKey)) return; - processedInnerEdges.add(edgeKey); - +function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine, wallLineId) { + // const edgeKey = [`${p1.x.toFixed(1)},${p1.y.toFixed(1)}`, `${p2.x.toFixed(1)},${p2.y.toFixed(1)}`].sort().join('|'); + // if (processedInnerEdges.has(edgeKey)) return; + // processedInnerEdges.add(edgeKey); + const currentDegree = getDegreeByChon(pitch) const dx = Math.abs(p2.x - p1.x); const dy = Math.abs(p2.y - p1.y); const isDiagonal = dx > 0.1 && dy > 0.1; const normalizedType = isDiagonal ? LINE_TYPE.SUBLINE.HIP : lineType; - const rawLines = [] - skeletonLines.push({ + // Count existing HIP lines + const existingEavesCount = skeletonLines.filter(line => + line.lineName === LINE_TYPE.SUBLINE.RIDGE + ).length; + + // If this is a HIP line, its index will be the existing count + const eavesIndex = normalizedType === LINE_TYPE.SUBLINE.RIDGE ? existingEavesCount : undefined; + + const newLine = { p1, p2, attributes: { - roofId:id, - + roofId: id, actualSize: (isDiagonal) ? calcLineActualSize( - { - x1: p1.x, - y1: p1.y, - x2: p2.x, - y2: p2.y - }, + { + x1: p1.x, + y1: p1.y, + x2: p2.x, + y2: p2.y + }, currentDegree - ) : calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), - + ) : calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), type: normalizedType, planeSize: calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), isRidge: normalizedType === LINE_TYPE.SUBLINE.RIDGE, + isOuterEdge: isOuterLine, + pitch: pitch, + wallLineId: wallLineId, // [5] attributes에 wallId 저장 (이 정보가 최종 roofLines에 들어갑니다) + ...(eavesIndex !== undefined && { eavesIndex }) }, lineStyle: { color, width }, - }); - - console.log('skeletonLines', skeletonLines); + }; + skeletonLines.push(newLine); + //console.log('skeletonLines', skeletonLines); } /** @@ -353,6 +1769,7 @@ const preprocessPolygonCoordinates = (initialPoints) => { if (coordinates.length > 1 && coordinates[0][0] === coordinates[coordinates.length - 1][0] && coordinates[0][1] === coordinates[coordinates.length - 1][1]) { coordinates.pop(); } + return coordinates.reverse(); }; @@ -812,10 +2229,1257 @@ const isPointOnSegment = (point, segStart, segEnd) => { return dotProduct >= 0 && dotProduct <= squaredLength; }; + + // Export all necessary functions export { findAllIntersections, collectAllPoints, - createPolygonsFromSkeletonLines + createPolygonsFromSkeletonLines, + preprocessPolygonCoordinates, + findOppositeLine, + createOrderedBasePoints, + createInnerLinesFromSkeleton }; + + + +/** + * Finds the opposite line in a polygon based on the given line + * @param {Array} edges - The polygon edges from canvas.skeleton.Edges + * @param {Object} startPoint - The start point of the line to find opposite for + * @param {Object} endPoint - The end point of the line to find opposite for + * @param targetPosition + * @returns {Object|null} The opposite line if found, null otherwise + */ +function findOppositeLine(edges, startPoint, endPoint, points) { + const result = []; + // 1. 다각형 찾기 + const polygons = findPolygonsContainingLine(edges, startPoint, endPoint); + if (polygons.length === 0) return null; + + const referenceSlope = calculateSlope(startPoint, endPoint); + + // 각 다각형에 대해 처리 + for (const polygon of polygons) { + // 2. 기준 선분의 인덱스 찾기 + + let baseIndex = -1; + for (let i = 0; i < polygon.length; i++) { + const p1 = { x: polygon[i].X, y: polygon[i].Y }; + const p2 = { + x: polygon[(i + 1) % polygon.length].X, + y: polygon[(i + 1) % polygon.length].Y + }; + + + + + if ((isSamePoint(p1, startPoint) && isSamePoint(p2, endPoint)) || + (isSamePoint(p1, endPoint) && isSamePoint(p2, startPoint))) { + baseIndex = i; + break; + } + } + + if (baseIndex === -1) continue; // 현재 다각형에서 기준 선분을 찾지 못한 경우 + + // 3. 다각형의 각 선분을 순회하면서 평행한 선분 찾기 + const polyLength = polygon.length; + for (let i = 0; i < polyLength; i++) { + if (i === baseIndex) continue; // 기준 선분은 제외 + + const p1 = { x: polygon[i].X, y: polygon[i].Y }; + const p2 = { + x: polygon[(i + 1) % polyLength].X, + y: polygon[(i + 1) % polyLength].Y + }; + + + const p1Exist = points.some(p => + Math.abs(p.x - p1.x) < 0.0001 && Math.abs(p.y - p1.y) < 0.0001 + ); + + const p2Exist = points.some(p => + Math.abs(p.x - p2.x) < 0.0001 && Math.abs(p.y - p2.y) < 0.0001 + ); + + if(p1Exist && p2Exist){ + const position = getLinePosition( + { start: p1, end: p2 }, + { start: startPoint, end: endPoint } + ); + result.push({ + start: p1, + end: p2, + position: position, + polygon: polygon + }); + } + + + } + } + + return result.length > 0 ? result:[]; + +} + +function getLinePosition(line, referenceLine) { + // 대상선의 중점 + const lineMidX = (line.start.x + line.end.x) / 2; + const lineMidY = (line.start.y + line.end.y) / 2; + + // 참조선의 중점 + const refMidX = (referenceLine.start.x + referenceLine.end.x) / 2; + const refMidY = (referenceLine.start.y + referenceLine.end.y) / 2; + + // 단순히 좌표 차이로 판단 + const deltaX = lineMidX - refMidX; + const deltaY = lineMidY - refMidY; + + // 참조선의 기울기 + const refDeltaX = referenceLine.end.x - referenceLine.start.x; + const refDeltaY = referenceLine.end.y - referenceLine.start.y; + + // 참조선이 더 수평인지 수직인지 판단 + if (Math.abs(refDeltaX) > Math.abs(refDeltaY)) { + // 수평선에 가까운 경우 - Y 좌표로 판단 + return deltaY > 0 ? 'bottom' : 'top'; + } else { + // 수직선에 가까운 경우 - X 좌표로 판단 + return deltaX > 0 ? 'right' : 'left'; + } +} + +/** + * Helper function to find if two points are the same within a tolerance + */ +function isSamePoint(p1, p2, tolerance = 0.1) { + return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance; +} + +// 두 점을 지나는 직선의 기울기 계산 +function calculateSlope(p1, p2) { + // 수직선인 경우 (기울기 무한대) + if (p1.x === p2.x) return Infinity; + return (p2.y - p1.y) / (p2.x - p1.x); +} + + +/** + * Helper function to find the polygon containing the given line + */ +function findPolygonsContainingLine(edges, p1, p2) { + const polygons = []; + for (const edge of edges) { + const polygon = edge.Polygon; + for (let i = 0; i < polygon.length; i++) { + const ep1 = { x: polygon[i].X, y: polygon[i].Y }; + const ep2 = { + x: polygon[(i + 1) % polygon.length].X, + y: polygon[(i + 1) % polygon.length].Y + }; + + if ((isSamePoint(ep1, p1) && isSamePoint(ep2, p2)) || + (isSamePoint(ep1, p2) && isSamePoint(ep2, p1))) { + polygons.push(polygon); + break; // 이 다각형에 대한 검사 완료 + } + } + } + return polygons; // 일치하는 모든 다각형 반환 +} + +/** + * roof.lines로 만들어진 다각형 내부에만 선분이 존재하도록 클리핑합니다. + * @param {Object} p1 - 선분의 시작점 {x, y} + * @param {Object} p2 - 선분의 끝점 {x, y} + * @param {Array} roofLines - 지붕 경계선 배열 (QLine 객체의 배열) + * @param skeletonLines + * @returns {Object} {p1: {x, y}, p2: {x, y}} - 다각형 내부로 클리핑된 선분 + */ +function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { + if (!roofLines || !roofLines.length) { + return { p1: { ...p1 }, p2: { ...p2 } }; + } + + const dx = Math.abs(p2.x - p1.x); + const dy = Math.abs(p2.y - p1.y); + const isDiagonal = dx > 0.5 && dy > 0.5; + + // 기본값으로 원본 좌표 설정 + let clippedP1 = { x: p1.x, y: p1.y }; + let clippedP2 = { x: p2.x, y: p2.y }; + + // p1이 다각형 내부에 있는지 확인 + const p1Inside = isPointInsidePolygon(p1, roofLines); + + // p2가 다각형 내부에 있는지 확인 + const p2Inside = isPointInsidePolygon(p2, roofLines); + + //console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); + + // 두 점 모두 내부에 있으면 그대로 반환 + if (p1Inside && p2Inside) { + if(!selectLine || isDiagonal){ + return { p1: clippedP1, p2: clippedP2 }; + } + //console.log('평행선::', clippedP1, clippedP2) + return { p1: clippedP1, p2: clippedP2 }; + } + + // 선분과 다각형 경계선의 교차점들을 찾음 + const intersections = []; + + for (const line of roofLines) { + const lineP1 = { x: line.x1, y: line.y1 }; + const lineP2 = { x: line.x2, y: line.y2 }; + + const intersection = getLineIntersection(p1, p2, lineP1, lineP2); + + if (intersection) { + // 교차점이 선분 위에 있는지 확인 + const t = getParameterT(p1, p2, intersection); + if (t >= 0 && t <= 1) { + intersections.push({ + point: intersection, + t: t + }); + } + } + } + + //console.log('Found intersections:', intersections.length); + + // 교차점들을 t 값으로 정렬 + intersections.sort((a, b) => a.t - b.t); + + if (!p1Inside && !p2Inside) { + // 두 점 모두 외부에 있는 경우 + if (intersections.length >= 2) { + //console.log('Both outside, using intersection points'); + clippedP1.x = intersections[0].point.x; + clippedP1.y = intersections[0].point.y; + clippedP2.x = intersections[1].point.x; + clippedP2.y = intersections[1].point.y; + } else { + //console.log('Both outside, no valid intersections - returning original'); + // 교차점이 충분하지 않으면 원본 반환 + return { p1: clippedP1, p2: clippedP2 }; + } + } else if (!p1Inside && p2Inside) { + // p1이 외부, p2가 내부 + if (intersections.length > 0) { + //console.log('p1 outside, p2 inside - moving p1 to intersection'); + clippedP1.x = intersections[0].point.x; + clippedP1.y = intersections[0].point.y; + // p2는 이미 내부에 있으므로 원본 유지 + clippedP2.x = p2.x; + clippedP2.y = p2.y; + } + } else if (p1Inside && !p2Inside) { + // p1이 내부, p2가 외부 + if (intersections.length > 0) { + //console.log('p1 inside, p2 outside - moving p2 to intersection'); + // p1은 이미 내부에 있으므로 원본 유지 + clippedP1.x = p1.x; + clippedP1.y = p1.y; + clippedP2.x = intersections[0].point.x; + clippedP2.y = intersections[0].point.y; + } + } + + return { p1: clippedP1, p2: clippedP2 }; +} + + +function isPointInsidePolygon(point, roofLines) { + // 1. 먼저 경계선 위에 있는지 확인 (방향 무관) + if (isOnBoundaryDirectionIndependent(point, roofLines)) { + return true; + } + + // 2. 내부/외부 판단 (기존 알고리즘) + let winding = 0; + const x = point.x; + const y = point.y; + + for (let i = 0; i < roofLines.length; i++) { + const line = roofLines[i]; + const x1 = line.x1, y1 = line.y1; + const x2 = line.x2, y2 = line.y2; + + if (y1 <= y) { + if (y2 > y) { + const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1); + if (orientation > 0) winding++; + } + } else { + if (y2 <= y) { + const orientation = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1); + if (orientation < 0) winding--; + } + } + } + + return winding !== 0; +} + +// 방향에 무관한 경계선 검사 +function isOnBoundaryDirectionIndependent(point, roofLines) { + const tolerance = 1e-10; + + for (const line of roofLines) { + if (isPointOnLineSegmentDirectionIndependent(point, line, tolerance)) { + return true; + } + } + return false; +} + +// 핵심: 방향에 무관한 선분 위 점 검사 +function isPointOnLineSegmentDirectionIndependent(point, line, tolerance) { + const x = point.x, y = point.y; + const x1 = line.x1, y1 = line.y1; + const x2 = line.x2, y2 = line.y2; + + // 방향에 무관하게 경계 상자 체크 + const minX = Math.min(x1, x2); + const maxX = Math.max(x1, x2); + const minY = Math.min(y1, y2); + const maxY = Math.max(y1, y2); + + if (x < minX - tolerance || x > maxX + tolerance || + y < minY - tolerance || y > maxY + tolerance) { + return false; + } + + // 외적을 이용한 직선 위 판단 (방향 무관) + const cross = (y - y1) * (x2 - x1) - (x - x1) * (y2 - y1); + return Math.abs(cross) < tolerance; +} + +/** + * 선분 위의 점에 대한 매개변수 t를 계산합니다. + * p = p1 + t * (p2 - p1)에서 t 값을 구합니다. + * @param {Object} p1 - 선분의 시작점 + * @param {Object} p2 - 선분의 끝점 + * @param {Object} point - 선분 위의 점 + * @returns {number} 매개변수 t (0이면 p1, 1이면 p2) + */ +function getParameterT(p1, p2, point) { + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + + // x 좌표가 더 큰 변화를 보이면 x로 계산, 아니면 y로 계산 + if (Math.abs(dx) > Math.abs(dy)) { + return dx === 0 ? 0 : (point.x - p1.x) / dx; + } else { + return dy === 0 ? 0 : (point.y - p1.y) / dy; + } +} +export const convertBaseLinesToPoints = (baseLines) => { + const points = []; + const pointSet = new Set(); + + baseLines.forEach((line) => { + [ + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 } + ].forEach(point => { + const key = `${point.x},${point.y}`; + if (!pointSet.has(key)) { + pointSet.add(key); + points.push(point); + } + }); + }); + + return points; +}; + +function getLineDirection(p1, p2) { + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const angle = Math.atan2(dy, dx) * 180 / Math.PI; + + // 각도 범위에 따라 방향 반환 + if ((angle >= -45 && angle < 45)) return 'right'; + if ((angle >= 45 && angle < 135)) return 'bottom'; + if ((angle >= 135 || angle < -135)) return 'left'; + return 'top'; // (-135 ~ -45) +} + + + + + +// selectLine과 baseLines 비교하여 방향 찾기 +function findLineDirection(selectLine, baseLines) { + for (const baseLine of baseLines) { + // baseLine의 시작점과 끝점 + const baseStart = baseLine.startPoint; + const baseEnd = baseLine.endPoint; + + // selectLine의 시작점과 끝점 + const selectStart = selectLine.startPoint; + const selectEnd = selectLine.endPoint; + + // 정방향 또는 역방향으로 일치하는지 확인 + if ((isSamePoint(baseStart, selectStart) && isSamePoint(baseEnd, selectEnd)) || + (isSamePoint(baseStart, selectEnd) && isSamePoint(baseEnd, selectStart))) { + + // baseLine의 방향 계산 + const dx = baseEnd.x - baseStart.x; + const dy = baseEnd.y - baseStart.y; + + // 기울기를 바탕으로 방향 판단 + if (Math.abs(dx) > Math.abs(dy)) { + return dx > 0 ? 'right' : 'left'; + } else { + return dy > 0 ? 'down' : 'up'; + } + } + } + + return null; // 일치하는 라인이 없는 경우 +} + + +/** + * baseLines를 연결하여 다각형 순서로 정렬된 점들 반환 + * @param {Array} baseLines - 라인 배열 + * @returns {Array} 순서대로 정렬된 점들의 배열 + */ +function getOrderedBasePoints(baseLines) { + if (baseLines.length === 0) return []; + + const points = []; + const usedLines = new Set(); + + // 첫 번째 라인으로 시작 + let currentLine = baseLines[0]; + points.push({ ...currentLine.startPoint }); + points.push({ ...currentLine.endPoint }); + usedLines.add(0); + + let lastPoint = currentLine.endPoint; + + // 연결된 라인들을 찾아가며 점들 수집 + while (usedLines.size < baseLines.length) { + let foundNext = false; + + for (let i = 0; i < baseLines.length; i++) { + if (usedLines.has(i)) continue; + + const line = baseLines[i]; + + // 현재 끝점과 연결되는 라인 찾기 + if (isSamePoint(lastPoint, line.startPoint)) { + points.push({ ...line.endPoint }); + lastPoint = line.endPoint; + usedLines.add(i); + foundNext = true; + break; + } else if (isSamePoint(lastPoint, line.endPoint)) { + points.push({ ...line.startPoint }); + lastPoint = line.startPoint; + usedLines.add(i); + foundNext = true; + break; + } + } + + if (!foundNext) break; // 연결되지 않는 경우 중단 + } + + // 마지막 점이 첫 번째 점과 같으면 제거 (닫힌 다각형) + if (points.length > 2 && isSamePoint(points[0], points[points.length - 1])) { + points.pop(); + } + + return points; +} + +/** + * roof.points와 baseLines가 정확히 대응되는 경우의 간단한 버전 + */ +function createOrderedBasePoints(roofPoints, baseLines) { + const basePoints = []; + + // baseLines에서 연결된 순서대로 점들을 추출 + const orderedBasePoints = getOrderedBasePoints(baseLines); + + // roofPoints의 개수와 맞추기 + if (orderedBasePoints.length >= roofPoints.length) { + return orderedBasePoints.slice(0, roofPoints.length); + } + + // 부족한 경우 roofPoints 기반으로 보완 + roofPoints.forEach((roofPoint, index) => { + if (index < orderedBasePoints.length) { + basePoints.push(orderedBasePoints[index]); + } else { + basePoints.push({ ...roofPoint }); // fallback + } + }); + + return basePoints; +} + +export const getSelectLinePosition = (wall, selectLine, options = {}) => { + const { testDistance = 10, epsilon = 0.5, debug = false } = options; + + if (!wall || !selectLine) { + if (debug) console.log('ERROR: wall 또는 selectLine이 없음'); + return { position: 'unknown', orientation: 'unknown', error: 'invalid_input' }; + } + + // selectLine의 좌표 추출 + const lineCoords = extractLineCoords(selectLine); + if (!lineCoords.valid) { + if (debug) console.log('ERROR: selectLine 좌표가 유효하지 않음'); + return { position: 'unknown', orientation: 'unknown', error: 'invalid_coords' }; + } + + const { x1, y1, x2, y2 } = lineCoords; + + //console.log('wall.points', wall.baseLines); + for(const line of wall.baseLines) { + //console.log('line', line); + const basePoint = extractLineCoords(line); + const { x1: bx1, y1: by1, x2: bx2, y2: by2 } = basePoint; + //console.log('x1, y1, x2, y2', bx1, by1, bx2, by2); + + // 객체 비교 대신 좌표값 비교 + if (Math.abs(bx1 - x1) < 0.1 && + Math.abs(by1 - y1) < 0.1 && + Math.abs(bx2 - x2) < 0.1 && + Math.abs(by2 - y2) < 0.1) { + //console.log('basePoint 일치!!!', basePoint); + } + } + + + // 라인 방향 분석 + const lineInfo = analyzeLineOrientation(x1, y1, x2, y2, epsilon); + + // if (debug) { + // console.log('=== getSelectLinePosition ==='); + // console.log('selectLine 좌표:', lineCoords); + // console.log('라인 방향:', lineInfo.orientation); + // } + + // 라인의 중점 + const midX = (x1 + x2) / 2; + const midY = (y1 + y2) / 2; + + let position = 'unknown'; + + if (lineInfo.orientation === 'horizontal') { + // 수평선: top 또는 bottom 판단 + + // 바로 위쪽 테스트 포인트 + const topTestPoint = { x: midX, y: midY - testDistance }; + // 바로 아래쪽 테스트 포인트 + const bottomTestPoint = { x: midX, y: midY + testDistance }; + + const topIsInside = checkPointInPolygon(topTestPoint, wall); + const bottomIsInside = checkPointInPolygon(bottomTestPoint, wall); + + // if (debug) { + // console.log('수평선 테스트:'); + // console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); + // console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); + // } + + // top 조건: 위쪽이 외부, 아래쪽이 내부 + if (!topIsInside && bottomIsInside) { + position = 'top'; + } + // bottom 조건: 위쪽이 내부, 아래쪽이 외부 + else if (topIsInside && !bottomIsInside) { + position = 'bottom'; + } + + } else if (lineInfo.orientation === 'vertical') { + // 수직선: left 또는 right 판단 + + // 바로 왼쪽 테스트 포인트 + const leftTestPoint = { x: midX - testDistance, y: midY }; + // 바로 오른쪽 테스트 포인트 + const rightTestPoint = { x: midX + testDistance, y: midY }; + + const leftIsInside = checkPointInPolygon(leftTestPoint, wall); + const rightIsInside = checkPointInPolygon(rightTestPoint, wall); + + // if (debug) { + // console.log('수직선 테스트:'); + // console.log(' 왼쪽 포인트:', leftTestPoint, '-> 내부:', leftIsInside); + // console.log(' 오른쪽 포인트:', rightTestPoint, '-> 내부:', rightIsInside); + // } + + // left 조건: 왼쪽이 외부, 오른쪽이 내부 + if (!leftIsInside && rightIsInside) { + position = 'left'; + } + // right 조건: 오른쪽이 외부, 왼쪽이 내부 + else if (leftIsInside && !rightIsInside) { + position = 'right'; + } + + } else { + // 대각선 + if (debug) console.log('대각선은 지원하지 않음'); + return { position: 'unknown', orientation: 'diagonal', error: 'not_supported' }; + } + + const result = { + position, + orientation: lineInfo.orientation, + method: 'inside_outside_test', + confidence: position !== 'unknown' ? 1.0 : 0.0, + testPoints: lineInfo.orientation === 'horizontal' ? { + top: { x: midX, y: midY - testDistance }, + bottom: { x: midX, y: midY + testDistance } + } : { + left: { x: midX - testDistance, y: midY }, + right: { x: midX + testDistance, y: midY } + }, + midPoint: { x: midX, y: midY } + }; + + // if (debug) { + // console.log('최종 결과:', result); + // } + + return result; +}; + +// 점이 다각형 내부에 있는지 확인하는 함수 +const checkPointInPolygon = (point, wall) => { + + // 2. wall.baseLines를 이용한 Ray Casting Algorithm + if (!wall.baseLines || !Array.isArray(wall.baseLines)) { + console.warn('wall.baseLines가 없습니다'); + return false; + } + + return raycastingAlgorithm(point, wall.baseLines); +}; + +// Ray Casting Algorithm 구현 +const raycastingAlgorithm = (point, lines) => { + const { x, y } = point; + let intersectionCount = 0; + + for (const line of lines) { + const coords = extractLineCoords(line); + if (!coords.valid) continue; + + const { x1, y1, x2, y2 } = coords; + + // Ray casting: 점에서 오른쪽으로 수평선을 그어서 다각형 경계와의 교점 개수를 셈 + // 교점 개수가 홀수면 내부, 짝수면 외부 + + // 선분의 y 범위 확인 + if ((y1 > y) !== (y2 > y)) { + // x 좌표에서의 교점 계산 + const intersectX = (x2 - x1) * (y - y1) / (y2 - y1) + x1; + + // 점의 오른쪽에 교점이 있으면 카운트 + if (x < intersectX) { + intersectionCount++; + } + } + } + + // 홀수면 내부, 짝수면 외부 + return intersectionCount % 2 === 1; +}; + +// 라인 객체에서 좌표를 추출하는 헬퍼 함수 (중복 방지용 - 이미 있다면 제거) +const extractLineCoords = (line) => { + if (!line) { + return { x1: 0, y1: 0, x2: 0, y2: 0, valid: false }; + } + + let x1, y1, x2, y2; + + // 다양한 라인 객체 형태에 대응 + if (line.x1 !== undefined && line.y1 !== undefined && + line.x2 !== undefined && line.y2 !== undefined) { + x1 = line.x1; + y1 = line.y1; + x2 = line.x2; + y2 = line.y2; + } + else if (line.startPoint && line.endPoint) { + x1 = line.startPoint.x; + y1 = line.startPoint.y; + x2 = line.endPoint.x; + y2 = line.endPoint.y; + } + else if (line.p1 && line.p2) { + x1 = line.p1.x; + y1 = line.p1.y; + x2 = line.p2.x; + y2 = line.p2.y; + } + else { + return { x1: 0, y1: 0, x2: 0, y2: 0, valid: false }; + } + + const coords = [x1, y1, x2, y2]; + const valid = coords.every(coord => + typeof coord === 'number' && + !Number.isNaN(coord) && + Number.isFinite(coord) + ); + + return { x1, y1, x2, y2, valid }; +}; + +// 라인 방향 분석 함수 (중복 방지용 - 이미 있다면 제거) +const analyzeLineOrientation = (x1, y1, x2, y2, epsilon = 0.5) => { + const dx = x2 - x1; + const dy = y2 - y1; + const absDx = Math.abs(dx); + const absDy = Math.abs(dy); + const length = Math.sqrt(dx * dx + dy * dy); + + let orientation; + if (absDy < epsilon && absDx >= epsilon) { + orientation = 'horizontal'; + } else if (absDx < epsilon && absDy >= epsilon) { + orientation = 'vertical'; + } else { + orientation = 'diagonal'; + } + + return { + orientation, + dx, dy, absDx, absDy, length, + midX: (x1 + x2) / 2, + midY: (y1 + y2) / 2, + isHorizontal: orientation === 'horizontal', + isVertical: orientation === 'vertical' + }; +}; + + +// 점에서 선분까지의 최단 거리를 계산하는 도우미 함수 +function pointToLineDistance(point, lineP1, lineP2) { + const A = point.x - lineP1.x; + const B = point.y - lineP1.y; + const C = lineP2.x - lineP1.x; + const D = lineP2.y - lineP1.y; + + const dot = A * C + B * D; + const lenSq = C * C + D * D; + let param = -1; + + if (lenSq !== 0) { + param = dot / lenSq; + } + + let xx, yy; + + if (param < 0) { + xx = lineP1.x; + yy = lineP1.y; + } else if (param > 1) { + xx = lineP2.x; + yy = lineP2.y; + } else { + xx = lineP1.x + param * C; + yy = lineP1.y + param * D; + } + + const dx = point.x - xx; + const dy = point.y - yy; + return Math.sqrt(dx * dx + dy * dy); +} + + +const getOrientation = (line, eps = 0.1) => { + const x1 = line.get('x1') + const y1 = line.get('y1') + const x2 = line.get('x2') + const y2 = line.get('y2') + const dx = Math.abs(x2 - x1) + const dy = Math.abs(y2 - y1) + + if (dx < eps && dy >= eps) return 'vertical' + if (dy < eps && dx >= eps) return 'horizontal' + if (dx < eps && dy < eps) return 'point' + return 'diagonal' +} + + + +export const processEaveHelpLines = (lines) => { + if (!lines || lines.length === 0) return []; + + // 수직/수평 라인 분류 (부동소수점 오차 고려) + const verticalLines = lines.filter(line => Math.abs(line.x1 - line.x2) < 0.1); + const horizontalLines = lines.filter(line => Math.abs(line.y1 - line.y2) < 0.1); + + // 라인 병합 (더 엄격한 조건으로) + const mergedVertical = mergeLines(verticalLines, 'vertical'); + const mergedHorizontal = mergeLines(horizontalLines, 'horizontal'); + + // 결과 확인용 로그 + console.log('Original lines:', lines.length); + console.log('Merged vertical:', mergedVertical.length); + console.log('Merged horizontal:', mergedHorizontal.length); + + return [...mergedVertical, ...mergedHorizontal]; +}; + +const mergeLines = (lines, direction) => { + if (!lines || lines.length < 2) return lines || []; + + // 방향에 따라 정렬 (수직: y1 기준, 수평: x1 기준) + lines.sort((a, b) => { + const aPos = direction === 'vertical' ? a.y1 : a.x1; + const bPos = direction === 'vertical' ? b.y1 : b.x1; + return aPos - bPos; + }); + + const merged = []; + let current = { ...lines[0] }; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + + // 같은 선상에 있는지 확인 (부동소수점 오차 고려) + const isSameLine = direction === 'vertical' + ? Math.abs(current.x1 - line.x1) < 0.1 + : Math.abs(current.y1 - line.y1) < 0.1; + + // 연결 가능한지 확인 (약간의 겹침 허용) + const isConnected = direction === 'vertical' + ? current.y2 + 0.1 >= line.y1 // 약간의 오차 허용 + : current.x2 + 0.1 >= line.x1; + + if (isSameLine && isConnected) { + // 라인 병합 + current.y2 = Math.max(current.y2, line.y2); + current.x2 = direction === 'vertical' ? current.x1 : current.x2; + } else { + merged.push(current); + current = { ...line }; + } + } + merged.push(current); + + // 병합 결과 로그 + console.log(`Merged ${direction} lines:`, merged); + + return merged; +}; + + + +/** + * 주어진 점을 포함하는 라인을 찾는 함수 + * @param {Array} lines - 검색할 라인 배열 (각 라인은 x1, y1, x2, y2 속성을 가져야 함) + * @param {Object} point - 찾고자 하는 점 {x, y} + * @param {number} [tolerance=0.1] - 점이 선분 위에 있는지 판단할 때의 허용 오차 + * @returns {Object|null} 점을 포함하는 첫 번째 라인 또는 null + */ +function findLineContainingPoint(lines, point, tolerance = 0.1) { + if (!point || !lines || !lines.length) return null; + + return lines.find(line => { + const { x1, y1, x2, y2 } = line; + return isPointOnLineSegment(point, {x: x1, y: y1}, {x: x2, y: y2}, tolerance); + }) || null; +} + +/** + * 점이 선분 위에 있는지 확인하는 함수 + * @param {Object} point - 확인할 점 {x, y} + * @param {Object} lineStart - 선분의 시작점 {x, y} + * @param {Object} lineEnd - 선분의 끝점 {x, y} + * @param {number} tolerance - 허용 오차 + * @returns {boolean} + */ +function isPointOnLineSegment(point, lineStart, lineEnd, tolerance = 0.1) { + const { x: px, y: py } = point; + const { x: x1, y: y1 } = lineStart; + const { x: x2, y: y2 } = lineEnd; + + // 선분의 길이 + const lineLength = Math.hypot(x2 - x1, y2 - y1); + + // 점에서 선분의 양 끝점까지의 거리 합 + const dist1 = Math.hypot(px - x1, py - y1); + const dist2 = Math.hypot(px - x2, py - y2); + + // 점이 선분 위에 있는지 확인 (허용 오차 범위 내에서) + return Math.abs(dist1 + dist2 - lineLength) <= tolerance; +} + +/** + * Updates a line in the innerLines array and returns the updated array + * @param {Array} innerLines - Array of line objects to update + * @param {Object} targetPoint - The point to find the line {x, y} + * @param {Object} wallBaseLine - The base line containing new coordinates + * @param {Function} getAddLine - Function to add a new line + * @returns {Array} Updated array of lines + */ +function updateAndAddLine(innerLines, targetPoint) { + + // 1. Find the line containing the target point + const foundLine = findLineContainingPoint(innerLines, targetPoint); + if (!foundLine) { + console.warn('No line found containing the target point'); + return [...innerLines]; + } + + // 2. Create a new array without the found line + const updatedLines = innerLines.filter(line => + line !== foundLine && + !(line.x1 === foundLine.x1 && + line.y1 === foundLine.y1 && + line.x2 === foundLine.x2 && + line.y2 === foundLine.y2) + ); + + // Calculate distances to both endpoints + const distanceToStart = Math.hypot( + targetPoint.x - foundLine.x1, + targetPoint.y - foundLine.y1 + ); + const distanceToEnd = Math.hypot( + targetPoint.x - foundLine.x2, + targetPoint.y - foundLine.y2 + ); + + // 단순 거리 비교: 타겟 포인트가 시작점에 더 가까우면 시작점을 수정(isUpdatingStart = true) + //무조건 start + let isUpdatingStart = false //distanceToStart < distanceToEnd; + if(targetPoint.position === "top_in_start"){ + if(foundLine.y2 >= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "top_in_end"){ + if(foundLine.y2 >= foundLine.y1){ + isUpdatingStart = true; + } + + }else if(targetPoint.position === "bottom_in_start"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "bottom_in_end"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_in_start"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_in_end"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_in_start"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_in_end"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "top_out_start"){ + if(foundLine.y2 >= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "top_out_end"){ + if(foundLine.y2 >= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "bottom_out_start"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "bottom_out_end"){ + if(foundLine.y2 <= foundLine.y1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_out_start"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "left_out_end"){ + if(foundLine.x2 >= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_out_start"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } + }else if(targetPoint.position === "right_out_end"){ + if(foundLine.x2 <= foundLine.x1){ + isUpdatingStart = true; + } + } + + const updatedLine = { + ...foundLine, + left: isUpdatingStart ? targetPoint.x : foundLine.x1, + top: isUpdatingStart ? targetPoint.y : foundLine.y1, + x1: isUpdatingStart ? targetPoint.x : foundLine.x1, + y1: isUpdatingStart ? targetPoint.y : foundLine.y1, + x2: isUpdatingStart ? foundLine.x2 : targetPoint.x, + y2: isUpdatingStart ? foundLine.y2 : targetPoint.y, + startPoint: { + x: isUpdatingStart ? targetPoint.x : foundLine.x1, + y: isUpdatingStart ? targetPoint.y : foundLine.y1 + }, + endPoint: { + x: isUpdatingStart ? foundLine.x2 : targetPoint.x, + y: isUpdatingStart ? foundLine.y2 : targetPoint.y + } + }; + + // 4. If it's a Fabric.js object, use set method if available + if (typeof foundLine.set === 'function') { + foundLine.set({ + x1: isUpdatingStart ? targetPoint.x : foundLine.x1, + y1: isUpdatingStart ? targetPoint.y : foundLine.y1, + x2: isUpdatingStart ? foundLine.x2 : targetPoint.x, + y2: isUpdatingStart ? foundLine.y2 : targetPoint.y + }); + updatedLines.push(foundLine); + } else { + updatedLines.push(updatedLine); + } + + return updatedLines; +} + + + +/** + * 점이 선분 위에 있는지 확인 + * @param {Object} point - 확인할 점 {x, y} + * @param {Object} lineStart - 선분의 시작점 {x, y} + * @param {Object} lineEnd - 선분의 끝점 {x, y} + * @param {number} tolerance - 오차 허용 범위 + * @returns {boolean} - 점이 선분 위에 있으면 true, 아니면 false + */ +function isPointOnLineSegment2(point, lineStart, lineEnd, tolerance = 0.1) { + const { x: px, y: py } = point; + const { x: x1, y: y1 } = lineStart; + const { x: x2, y: y2 } = lineEnd; + + // 선분의 길이 + const lineLength = Math.hypot(x2 - x1, y2 - y1); + + // 점에서 선분의 양 끝점까지의 거리 + const dist1 = Math.hypot(px - x1, py - y1); + const dist2 = Math.hypot(px - x2, py - y2); + + // 점이 선분 위에 있는지 확인 (오차 허용 범위 내에서) + const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance; + + if (isOnSegment) { + console.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); + } + + return isOnSegment; +} + +/** + * 세 점(p1 -> p2 -> p3)의 방향성을 계산합니다. (2D 외적) + * 반시계 방향(CCW)으로 그려진 폴리곤(Y축 Down) 기준: + * - 결과 > 0 : 오른쪽 턴 (Right Turn) -> 골짜기 (Valley/Reflex Vertex) + * - 결과 < 0 : 왼쪽 턴 (Left Turn) -> 외곽 모서리 (Convex Vertex) + * - 결과 = 0 : 직선 + */ +function getTurnDirection(p1, p2, p3) { + // 벡터 a: p1 -> p2 + // 벡터 b: p2 -> p3 + const val = (p2.x - p1.x) * (p3.y - p2.y) - (p2.y - p1.y) * (p3.x - p2.x); + return val; +} + +/** + * 현재 점(point)을 기준으로 연결된 이전 라인과 다음 라인을 찾아 골짜기 여부 판단 + */ +function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { + const tolerance = 0.1; + + // 1. 연결된 '다른' 라인을 찾습니다. + // isStartVertex가 true면 : 이 점으로 '들어오는' 라인(Previous Line)을 찾아야 함 + // isStartVertex가 false면 : 이 점에서 '나가는' 라인(Next Line)을 찾아야 함 + + let neighborLine = null; + + if (isStartVertex) { + // targetPoint가 Start이므로, 어떤 라인의 End가 targetPoint와 같아야 함 (Previous Line) + neighborLine = allLines.find(l => + l !== connectedLine && + isSamePoint(l.endPoint || {x:l.x2, y:l.y2}, targetPoint, tolerance) + ); + } else { + // targetPoint가 End이므로, 어떤 라인의 Start가 targetPoint와 같아야 함 (Next Line) + neighborLine = allLines.find(l => + l !== connectedLine && + isSamePoint(l.startPoint || {x:l.x1, y:l.y1}, targetPoint, tolerance) + ); + } + + // 연결된 라인을 못 찾았거나 끊겨있으면 판단 불가 (일단 false) + if (!neighborLine) return false; + + // 2. 세 점을 구성하여 회전 방향(Turn) 계산 + // 순서: PrevLine.Start -> [TargetVertex] -> NextLine.End + let p1, p2, p3; + + if (isStartVertex) { + // neighbor(Prev) -> connected(Current) + p1 = neighborLine.startPoint || {x: neighborLine.x1, y: neighborLine.y1}; + p2 = targetPoint; // 접점 + p3 = connectedLine.endPoint || {x: connectedLine.x2, y: connectedLine.y2}; + } else { + // connected(Current) -> neighbor(Next) + p1 = connectedLine.startPoint || {x: connectedLine.x1, y: connectedLine.y1}; + p2 = targetPoint; // 접점 + p3 = neighborLine.endPoint || {x: neighborLine.x2, y: neighborLine.y2}; + } + + // 3. 외적 계산 (Y축이 아래로 증가하는 캔버스 좌표계 + CCW 진행 기준) + // 값이 양수(+)면 오른쪽 턴 = 골짜기 + const crossProduct = getTurnDirection(p1, p2, p3); + + return crossProduct > 0; +} + +function findInteriorPoint(line, polygonLines) { + const { x1, y1, x2, y2 } = line; + + // line 객체 포맷 통일 + const currentLine = { + ...line, + startPoint: { x: x1, y: y1 }, + endPoint: { x: x2, y: y2 } + }; + + // 1. 시작점이 골짜기인지 확인 (들어오는 라인과 나가는 라인의 각도) + const startIsValley = isValleyVertex(currentLine.startPoint, currentLine, polygonLines, true); + + // 2. 끝점이 골짜기인지 확인 + const endIsValley = isValleyVertex(currentLine.endPoint, currentLine, polygonLines, false); + + return { + start: startIsValley, + end: endIsValley + }; +} + +/** + * baseLines의 순서를 wallLines의 순서와 일치시킵니다. + * 1순위: 공통 ID(id, matchingId, parentId 등)를 이용한 직접 매칭 + * 2순위: 기하학적 유사성(기울기, 길이, 위치)을 점수화하여 매칭 + * + * @param {Array} baseLines - 정렬할 원본 baseLine 배열 + * @param {Array} wallLines - 기준이 되는 wallLine 배열 + * @returns {Array} wallLines 순서에 맞춰 정렬된 baseLines + */ +export const sortBaseLinesByWallLines = (baseLines, wallLines) => { + if (!baseLines || !wallLines || baseLines.length === 0 || wallLines.length === 0) { + return baseLines; + } + + const sortedBaseLines = new Array(wallLines.length).fill(null); + const usedBaseIndices = new Set(); + + // [1단계] ID 매칭 (기존 로직 유지 - 혹시 ID가 있는 경우를 대비) + // ... (ID 매칭 코드는 생략하거나 유지) ... + + // [2단계] 'originPoint' 또는 좌표 일치성을 이용한 강력한 기하학적 매칭 + wallLines.forEach((wLine, wIndex) => { + if (sortedBaseLines[wIndex]) return; + + // 비교할 기준 좌표 설정 (originPoint가 있으면 그것을, 없으면 현재 좌표 사용) + const wStart = wLine.attributes?.originPoint + ? { x: wLine.attributes.originPoint.x1, y: wLine.attributes.originPoint.y1 } + : { x: wLine.x1, y: wLine.y1 }; + + const wEnd = wLine.attributes?.originPoint + ? { x: wLine.attributes.originPoint.x2, y: wLine.attributes.originPoint.y2 } + : { x: wLine.x2, y: wLine.y2 }; + + // 수직/수평 여부 판단 + const isVertical = Math.abs(wStart.x - wEnd.x) < 0.1; + const isHorizontal = Math.abs(wStart.y - wEnd.y) < 0.1; + + let bestMatchIndex = -1; + let minDiff = Infinity; + + baseLines.forEach((bLine, bIndex) => { + if (usedBaseIndices.has(bIndex)) return; + + let diff = Infinity; + + // 1. 수직선인 경우: X좌표가 일치해야 함 (예: 230.8 == 230.8) + if (isVertical) { + // bLine도 수직선인지 확인 (x1, x2 차이가 거의 없어야 함) + if (Math.abs(bLine.x1 - bLine.x2) < 1.0) { + // X좌표 차이를 오차(diff)로 계산 + diff = Math.abs(wStart.x - bLine.x1); + } + } + // 2. 수평선인 경우: Y좌표가 일치해야 함 + else if (isHorizontal) { + // bLine도 수평선인지 확인 + if (Math.abs(bLine.y1 - bLine.y2) < 1.0) { + diff = Math.abs(wStart.y - bLine.y1); + } + } + // 3. 대각선인 경우: 기울기와 절편 비교 (복잡하므로 거리로 대체) + else { + // 중점 간 거리 + 기울기 차이 + // (이전 답변의 로직 사용 가능) + } + + // 오차가 매우 작으면(예: 1px 미만) 같은 라인으로 간주 + if (diff < 1.0 && diff < minDiff) { + minDiff = diff; + bestMatchIndex = bIndex; + } + }); + + if (bestMatchIndex !== -1) { + sortedBaseLines[wIndex] = baseLines[bestMatchIndex]; + usedBaseIndices.add(bestMatchIndex); + } + }); + + // [3단계] 남은 라인 처리 (Fallback) + // 매칭되지 않은 wallLine들에 대해 남은 baseLines를 순서대로 배정하거나 + // 거리 기반 근사 매칭을 수행 + // ... (기존 fallback 로직) ... + + // 빈 구멍 채우기 (null 방지) + for(let i=0; i !usedBaseIndices.has(idx)); + if(unused !== -1) { + sortedBaseLines[i] = baseLines[unused]; + usedBaseIndices.add(unused); + } else { + sortedBaseLines[i] = baseLines[0]; // 최후의 수단 + } + } + } + + return sortedBaseLines; +}; \ No newline at end of file