diff --git a/src/components/common/input/CalcInput.jsx b/src/components/common/input/CalcInput.jsx index 3da3a7c5..32323560 100644 --- a/src/components/common/input/CalcInput.jsx +++ b/src/components/common/input/CalcInput.jsx @@ -3,7 +3,7 @@ import { createCalculator } from '@/util/calc-utils' import '@/styles/calc.scss' export const CalculatorInput = forwardRef( - ({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder }, ref) => { + ({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder, name='', disabled = false }, ref) => { const [showKeypad, setShowKeypad] = useState(false) const [displayValue, setDisplayValue] = useState(value || '0') const [hasOperation, setHasOperation] = useState(false) @@ -353,6 +353,7 @@ export const CalculatorInput = forwardRef( ref={inputRef} type="text" id={id} + name={name} value={displayValue} readOnly={readOnly} className={className} @@ -363,6 +364,7 @@ export const CalculatorInput = forwardRef( tabIndex={readOnly ? -1 : 0} placeholder={placeholder} autoComplete={'off'} + disabled={disabled} /> {showKeypad && !readOnly && ( diff --git a/src/components/community/modal/QnaDetailModal.jsx b/src/components/community/modal/QnaDetailModal.jsx index 0e25d3a8..760a65d9 100644 --- a/src/components/community/modal/QnaDetailModal.jsx +++ b/src/components/community/modal/QnaDetailModal.jsx @@ -25,6 +25,7 @@ export default function QnaDetailModal({ qnaNo, setOpen, qnaType }) { compCd : 5200, loginId : sessionState.userId, langCd : 'JA', + siteTpCd : 'QC', }) const apiUrl = `${url}?${params.toString()}` diff --git a/src/components/community/modal/QnaRegModal.jsx b/src/components/community/modal/QnaRegModal.jsx index d04f00fa..8f9437f9 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?.userId || '', + qstMail: sessionState?.email || '', + qnaClsLrgCd: '', + qnaClsMidCd: '', + qnaClsSmlCd: '' + }); const codeL = findCommonCode(204200) if (codeL != null) { @@ -119,41 +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 = () => { diff --git a/src/components/estimate/Estimate.jsx b/src/components/estimate/Estimate.jsx index f6517960..d8b127fc 100644 --- a/src/components/estimate/Estimate.jsx +++ b/src/components/estimate/Estimate.jsx @@ -138,7 +138,27 @@ export default function Estimate({}) { updatedRes = [...res] } - setOriginDisplayItemList(res) + const groupByItemGroup = (items) => { + const grouped = items.reduce((acc, item) => { + const group = item.itemGroup || '기타'; + if (!acc[group]) { + acc[group] = { + label: group, + options: [] + }; + } + acc[group].options.push({ + value: item.itemId, + label: `${item.itemNo} - ${item.itemName}`, + ...item + }); + return acc; + }, {}); + + return Object.values(grouped); + }; + const groupedItems = groupByItemGroup(res); + setOriginDisplayItemList(groupedItems) setDisplayItemList(updatedRes) } }) @@ -153,6 +173,19 @@ export default function Estimate({}) { }) } + const groupStyles = { + groupHeading: (provided) => ({ + ...provided, + fontSize: '14px', + fontWeight: 'bold', + color: '#333', + backgroundColor: '#f5f5f5', + padding: '8px 12px', + marginBottom: '4px', + borderBottom: '2px solid #ddd' + }) + }; + useEffect(() => { // console.log('🚀 ~ Estimate ~ selectedPlan:', selectedPlan) if (selectedPlan) initEstimate(selectedPlan?.planNo?? currentPid) @@ -1998,6 +2031,7 @@ export default function Estimate({}) { classNamePrefix="custom" placeholder="Select" options={originDisplayItemList} + styles={groupStyles} onChange={(e) => { if (isObjectNotEmpty(e)) { onChangeDisplayItem(e.itemId, item.dispOrder, index, false) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 88fd0125..46fbb931 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -336,8 +336,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { if (types.every((type) => 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('패턴 지붕') diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index a8ee7102..8b1f7dc8 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -346,6 +346,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla /> */} W
- changeInput(normalizeDigits(e.target.value), e)}*/} + {/* readOnly={currentRoof?.widAuth === 'R'}*/} + {/* disabled={currentRoof?.roofSizeSet === '3'}*/} + {/*/>*/} + + changeInput(normalizeDigits(e.target.value), e)} + value={currentRoof?.width||0} + onChange={(value) => { + setCurrentRoof({ ...currentRoof, value }) + }} readOnly={currentRoof?.widAuth === 'R'} disabled={currentRoof?.roofSizeSet === '3'} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} />
@@ -429,15 +448,33 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
L
- changeInput(normalizeDigits(e.target.value), e)}*/} + {/* readOnly={currentRoof?.lenAuth === 'R'}*/} + {/* disabled={currentRoof?.roofSizeSet === '3'}*/} + {/*/>*/} + + changeInput(normalizeDigits(e.target.value), e)} + value={currentRoof?.length||0} + onChange={(value) => { + setCurrentRoof({ ...currentRoof, value }) + }} readOnly={currentRoof?.lenAuth === 'R'} disabled={currentRoof?.roofSizeSet === '3'} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} />
@@ -465,16 +502,34 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
{getMessage('hajebichi')}
- changeInput(normalizeDigits(e.target.value), e)}*/} + {/* readOnly={currentRoof?.roofPchAuth === 'R'}*/} + {/* disabled={currentRoof?.roofSizeSet === '3'}*/} + {/*/>*/} + changeInput(normalizeDigits(e.target.value), e)} + value={currentRoof?.hajebichi||0} + onChange={(value) => { + setCurrentRoof({ ...currentRoof, value }) + }} readOnly={currentRoof?.roofPchAuth === 'R'} disabled={currentRoof?.roofSizeSet === '3'} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} /> +
)} diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index 9b060fde..af3cee04 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -632,9 +632,12 @@ export function useCanvasSetting(executeEffect = true) { originHorizon: res.originHorizon, originVertical: res.originVertical, }) - canvas.setWidth(res.originHorizon) - canvas.setHeight(res.originVertical) - canvas.renderAll() + + if (canvas) { + canvas.setWidth(res.originHorizon) + canvas.setHeight(res.originVertical) + canvas.renderAll() + } /** 데이터 설정 */ setSettingModalFirstOptions({ diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 7d1d326a..08b31f4d 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -319,6 +319,14 @@ export function useMovementSetting(id) { const roofId = target.attributes.roofId const roof = canvas.getObjects().find((obj) => obj.id === roofId) + + // 현이동, 동이동 추가 + const moveFlowLine = typeRef.current === TYPE.FLOW_LINE ? FLOW_LINE_REF.POINTER_INPUT_REF.current.value : 0 + const moveUpDown = typeRef.current === TYPE.UP_DOWN ? UP_DOWN_REF.POINTER_INPUT_REF.current.value : 0 + roof.moveFlowLine = parseInt(moveFlowLine, 10) || 0; + roof.moveUpDown = parseInt(moveUpDown, 10) || 0; + roof.moveDirect = ""; + roof.moveSelectLine = target; const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) const baseLines = wall.baseLines let targetBaseLines = [] @@ -348,6 +356,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) @@ -442,10 +451,17 @@ 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() } 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/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..54721d94 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -1451,6 +1451,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 +1507,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/useMode.js b/src/hooks/useMode.js index 69eb955c..6030f43f 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1820,7 +1820,13 @@ 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 + if (nextWall.index > currentWall.index) { + diffOffset = Big(nextWall.attributes.offset).minus(Big(currentWall.attributes.offset)).abs() + } else { + diffOffset = 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/locales/ja.json b/src/locales/ja.json index 2999728f..db27c2ef 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": "お問い合わせ内容", diff --git a/src/util/canvas-util.js b/src/util/canvas-util.js index 0af492a1..243936da 100644 --- a/src/util/canvas-util.js +++ b/src/util/canvas-util.js @@ -260,7 +260,7 @@ export const getDegreeByChon = (chon) => { // tan(theta) = height / base const radians = Math.atan(chon / 10) // 라디안을 도 단위로 변환 - return Number((radians * (180 / Math.PI)).toFixed(1)) + return Number((radians * (180 / Math.PI)).toFixed(2)) } /** diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 15b9e569..6e25c18d 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -7579,7 +7579,12 @@ export const drawRidgeRoof = (roofId, canvas, textMode) => { hipBasePoint = { x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2 } point = [mergePoint[0].x, mergePoint[0].y, mergePoint[3].x, mergePoint[3].y] - const theta = Big(Math.acos(Big(line.line.attributes.planeSize).div(line.line.attributes.actualSize))) + const theta = Big(Math.acos(Big(line.line.attributes.planeSize).div( + line.line.attributes.actualSize === 0 || + line.line.attributes.actualSize === '' || + line.line.attributes.actualSize === undefined ? + line.line.attributes.planeSize : line.line.attributes.actualSize + ))) .times(180) .div(Math.PI) .round(1) @@ -7660,7 +7665,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() @@ -9223,7 +9232,11 @@ const getSortedPoint = (points, lines) => { const reCalculateSize = (line) => { const oldPlaneSize = line.attributes.planeSize const oldActualSize = line.attributes.actualSize - const theta = Big(Math.acos(Big(oldPlaneSize).div(oldActualSize))) + const theta = Big(Math.acos(Big(oldPlaneSize).div( + oldActualSize === 0 || oldActualSize === '' || oldActualSize === undefined ? + oldPlaneSize : + oldActualSize + ))) .times(180) .div(Math.PI) const planeSize = calcLinePlaneSize({ diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 34d8153e..54cb5ba2 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -4,7 +4,6 @@ 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 { line } from 'framer-motion/m' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -16,144 +15,33 @@ import { line } from 'framer-motion/m' export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { - - // 2. 스켈레톤 생성 및 그리기 - skeletonBuilder(roofId, canvas, textMode) -} - - -const movingRidgeFromSkeleton = (roofId, canvas) => { - let roof = canvas?.getObjects().find((object) => object.id === roofId) - let moveDirection = roof.moveDirect; - let moveFlowLine = roof.moveFlowLine??0; - const selectLine = roof.moveSelectLine; + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) - const startPoint = selectLine.startPoint - const endPoint = selectLine.endPoint - const oldPoints = canvas?.movePoints?.points ?? roof.points - const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); - - if (oppositeLine) { - console.log('Opposite line found:', oppositeLine); - } else { - console.log('No opposite line found'); + 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 } - return oldPoints.map((point) => { - const newPoint = { ...point }; - const absMove = Big(moveFlowLine).abs().times(2).div(10); - //console.log('absMove:', absMove); + const eavesType = [LINE_TYPE.WALLLINE.EAVES, LINE_TYPE.WALLLINE.HIPANDGABLE] + const gableType = [LINE_TYPE.WALLLINE.GABLE, LINE_TYPE.WALLLINE.JERKINHEAD] - const skeletonLines = canvas.skeletonLines; - - console.log('skeleton line:', canvas.skeletonLines); - const changeSkeletonLine = (canvas, oldPoint, newPoint, str) => { - for (const line of canvas.skeletonLines) { - if (str === 'start' && isSamePoint(line.startPoint, oldPoint)) { - // Fabric.js 객체의 set 메서드로 속성 업데이트 - line.set({ - x1: newPoint.x, - y1: newPoint.y, - x2: line.x2 || line.endPoint?.x, - y2: line.y2 || line.endPoint?.y - }); - line.startPoint = newPoint; // 참조 업데이트 - } - else if (str === 'end' && isSamePoint(line.endPoint, oldPoint)) { - line.set({ - x1: line.x1 || line.startPoint?.x, - y1: line.y1 || line.startPoint?.y, - x2: newPoint.x, - y2: newPoint.y - }); - line.endPoint = newPoint; // 참조 업데이트 - } - } - canvas.requestRenderAll(); - console.log('skeleton line:', canvas.skeletonLines); - } + /** 외벽선 */ + const baseLines = wall.baseLines.filter((line) => line.attributes.planeSize > 0) - if(moveFlowLine > 0) { - if(moveDirection === 'down'){ - moveDirection = 'up'; - }else if(moveDirection === 'left'){ - moveDirection = 'right'; - } - } + //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; + } - console.log('skeletonBuilder moveDirection:', moveDirection); - switch (moveDirection) { - case 'left': - // Move left: decrease X - for (const line of oppositeLine) { - if (line.position === 'left') { - if (isSamePoint(newPoint, line.start)) { - newPoint.x = Big(line.start.x).minus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.start, newPoint, 'start') - } else if (isSamePoint(newPoint, line.end)) { - newPoint.x = Big(line.end.x).minus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') - } - break - } - } - - break; - case 'right': - for (const line of oppositeLine) { - if (line.position === 'right') { - if (isSamePoint(newPoint, line.start)) { - newPoint.x = Big(line.start.x).plus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.start, newPoint, 'start') - } else if (isSamePoint(newPoint, line.end)) { - newPoint.x = Big(line.end.x).plus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') - } - 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(); - //changeSkeletonLine(canvas, line.start, newPoint, 'start') - } else if (isSamePoint(newPoint, line.end)) { - newPoint.y = Big(line.end.y).minus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') - } - break - } - } - - break; - case 'down': - // Move down: increase Y (toward bottom of screen) - for (const line of oppositeLine) { - if (line.position === 'bottom') { - if (isSamePoint(newPoint, line.start)) { - newPoint.y = Big(line.start.y).plus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.start, newPoint, 'start') - - } else if (isSamePoint(newPoint, line.end)) { - newPoint.y = Big(line.end.y).plus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') - } - break - } - } - break; - } - - return newPoint; - }) + // 2. 스켈레톤 생성 및 그리기 + skeletonBuilder(roofId, canvas, textMode, roof, baseLines) } /** @@ -164,65 +52,18 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { * @param {fabric.Object} roof - 지붕 객체 * @param baseLines */ -export const skeletonBuilder = (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; - } - - 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) { - points = movingRidgeFromSkeleton(roofId, canvas) - - const movePoints = { - points: points, - roofId: roofId, - } - canvas.set("movePoints", movePoints) - - } -//처마 - if(moveUpDown !== 0) { - - } - - const geoJSONPolygon = toGeoJSON(points) +export const skeletonBuilder = (roofId, canvas, textMode, roof, baseLines) => { + const geoJSONPolygon = toGeoJSON(roof.points) try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 geoJSONPolygon.pop() const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) + console.log(`지붕 형태: ${skeleton.roof_type}`, skeleton.edge_analysis) + // 스켈레톤 데이터를 기반으로 내부선 생성 - roof.innerLines = roof.innerLines || []; - roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) + roof.innerLines = createInnerLinesFromSkeleton(skeleton, canvas, textMode, roof, baseLines) // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { @@ -230,35 +71,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { canvas.skeletonLines = [] } canvas.skeletonStates[roofId] = true - canvas.skeletonLines = []; - canvas.skeletonLines.push(...roof.innerLines) - canvas.set("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.set("skeleton", cleanSkeleton); canvas.renderAll() } catch (e) { console.error('스켈레톤 생성 중 오류 발생:', e) if (canvas.skeletonStates) { canvas.skeletonStates[roofId] = false - canvas.skeletonStates = {} - canvas.skeletonLines = [] } } } @@ -272,36 +90,16 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { * @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none') * @param {Array} baseLines - 원본 외벽선 QLine 객체 배열 */ -const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { +const createInnerLinesFromSkeleton = (skeleton,canvas, textMode, roof, baseLines) => { if (!skeleton?.Edges) return [] - let roof = canvas?.getObjects().find((object) => object.id === roofId) const skeletonLines = [] const processedInnerEdges = new Set() // 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다. - skeleton.Edges.forEach((edgeResult, index) => { - // const { Begin, End } = edgeResult.Edge; - // let outerLine = roof.lines.find(line => - // line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line) - // ); - // if(!outerLine){ - // - // for (const line of canvas.skeletonLines) { - // if (line.lineName === 'hip' && line.attributes.hipIndex === index) - // { - // outerLine = line; - // break; // Found the matching line, exit the loop - // } - // } - // - // } - // const pitch = outerLine.attributes?.pitch??0 - // console.log("pitch", pitch) - processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines); + processEavesEdge(edgeResult, processedInnerEdges, roof, skeletonLines, baseLines[index].attributes.pitch); }); - /* // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다. @@ -363,45 +161,24 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다. const innerLines = []; - const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set - skeletonLines.forEach(line => { const { p1, p2, attributes, lineStyle } = line; - - // 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교) - const lineKey = [ - [p1.x, p1.y].sort().join(','), - [p2.x, p2.y].sort().join(',') - ].sort().join('|'); - - // 이미 추가된 라인인지 확인 - if (existingLines.has(lineKey)) { - return; // 이미 있는 라인이면 스킵 - } - const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, stroke: lineStyle.color, strokeWidth: lineStyle.width, - name: (line.attributes.isOuterEdge)?'eaves': attributes.type, + name: attributes.type, + textMode: textMode, attributes: attributes, - isBaseLine: line.attributes.isOuterEdge, - lineName: (line.attributes.isOuterEdge)?'outerLine': attributes.type, - selectable:(!line.attributes.isOuterEdge), - roofId: roofId }); - //skeleton 라인에서 처마선은 삭제 - if(innerLine.lineName !== 'outerLine'){ - canvas.add(innerLine); - innerLine.bringToFront(); - existingLines.add(lineKey); // 추가된 라인을 추적 - } - innerLines.push(innerLine) - canvas.renderAll(); + canvas.add(innerLine); + innerLine.bringToFront(); + innerLines.push(innerLine); }); + canvas.renderAll(); return innerLines; } @@ -413,65 +190,22 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { * @param roof * @param pitch */ -function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { - let roof = canvas?.getObjects().find((object) => object.id === roofId) +function processEavesEdge(edgeResult, processedInnerEdges, roof, skeletonLines, pitch) { const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); - //처마선인지 확인하고 pitch 대입 각 처마선마다 pitch가 다를수 있음 - const { Begin, End } = edgeResult.Edge; - let outerLine = roof.lines.find(line => - line.attributes.type === 'eaves' && isSameLine(Begin.X, Begin.Y, End.X, End.Y, line) - ); - if(!outerLine) { - outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); - console.log('Has matching line:', outerLine); - } - let pitch = outerLine?.attributes?.pitch??0 - - + const currentDegree = getDegreeByChon(pitch) 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])) { - //외벽선 밖으로 나간 선을 정리한다(roof.line의 교점까지 정리한다) - // 지붕 경계선과 교차 확인 및 클리핑 - const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines); - console.log('clipped line', clippedLine.p1, clippedLine.p2); - const isOuterLine = isOuterEdge(p1, p2, [edgeResult.Edge]) - addRawLine(roof.id, skeletonLines, p1, p2, 'ridge', '#FF0000', 3, pitch, isOuterLine); - // } - } -} - - -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)) - ); - } + if (!isOuterEdge(p1, p2, [edgeResult.Edge])) { + addRawLine(roof.id, skeletonLines, processedInnerEdges, p1, p2, 'RIDGE', '#FF0000', 3, currentDegree); } } - return null; } - /** * GABLE(케라바) Edge를 처리하여 스켈레톤 선을 정리하고 연장합니다. * @param {object} edgeResult - 스켈레톤 Edge 데이터 @@ -483,7 +217,7 @@ function findMatchingLine(edgePolygon, roof, roofPoints) { 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. 케라바 면과 관련된 불필요한 스켈레톤 선을 제거합니다. @@ -498,8 +232,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 => { @@ -525,9 +259,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; @@ -566,50 +300,42 @@ function isOuterEdge(p1, p2, edges) { * @param {number} width - 두께 * @param currentDegree */ -function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, isOuterLine) { - // 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) +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); + 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 = [] - // 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 = { + skeletonLines.push({ 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, - ...(eavesIndex !== undefined && { eavesIndex }) }, lineStyle: { color, width }, - }; + }); + + console.log('skeletonLines', skeletonLines); - skeletonLines.push(newLine); - //console.log('skeletonLines', skeletonLines); } /** @@ -1086,8 +812,6 @@ const isPointOnSegment = (point, segStart, segEnd) => { return dotProduct >= 0 && dotProduct <= squaredLength; }; - - // Export all necessary functions export { findAllIntersections, @@ -1095,306 +819,3 @@ export { createPolygonsFromSkeletonLines }; - -/** - * Finds lines in the roof that match certain criteria based on the given points - * @param {Array} lines - The roof lines to search through - * @param {Object} startPoint - The start point of the reference line - * @param {Object} endPoint - The end point of the reference line - * @param {Array} oldPoints - The old points to compare against - * @returns {Array} Array of matching line objects with their properties - */ -function findMatchingRoofLines(lines, startPoint, endPoint, oldPoints) { - const result = []; - - // If no lines provided, return empty array - if (!lines || !lines.length) return result; - - // Process each line in the roof - for (const line of lines) { - // Get the start and end points of the current line - const p1 = { x: line.x1, y: line.y1 }; - const p2 = { x: line.x2, y: line.y2 }; - - // Check if both points exist in the oldPoints array - const p1Exists = oldPoints.some(p => - Math.abs(p.x - p1.x) < 0.0001 && Math.abs(p.y - p1.y) < 0.0001 - ); - - const p2Exists = oldPoints.some(p => - Math.abs(p.x - p2.x) < 0.0001 && Math.abs(p.y - p2.y) < 0.0001 - ); - - // If both points exist in oldPoints, add to results - if (p1Exists && p2Exists) { - // Calculate line position relative to the reference line - const position = getLinePosition( - { start: p1, end: p2 }, - { start: startPoint, end: endPoint } - ); - - result.push({ - start: p1, - end: p2, - position: position, - line: line - }); - } - } - - return result; -} - -/** - * 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 - }); - } - - // // 현재 선분의 기울기 계산 - // const currentSlope = calculateSlope(p1, p2); - // - // // 기울기가 같은지 확인 (평행한 선분) - // if (areLinesParallel(referenceSlope, currentSlope)) { - // // 동일한 선분이 아닌지 확인 - // if (!areSameLine(p1, p2, startPoint, endPoint)) { - // const position = getLinePosition( - // { start: p1, end: p2 }, - // { start: startPoint, end: endPoint } - // ); - // - // const lineMid = { - // x: (p1.x + p2.x) / 2, - // y: (p1.y + p2.y) / 2 - // }; - // - // const baseMid = { - // x: (startPoint.x + endPoint.x) / 2, - // y: (startPoint.y + endPoint.y) / 2 - // }; - // const distance = Math.sqrt( - // Math.pow(lineMid.x - baseMid.x, 2) + - // Math.pow(lineMid.y - baseMid.y, 2) - // ); - // - // const existingIndex = result.findIndex(line => line.position === position); - // - // if (existingIndex === -1) { - // // If no line with this position exists, add it - // result.push({ - // start: p1, - // end: p2, - // position: position, - // polygon: polygon, - // distance: distance - // }); - // } else if (distance > result[existingIndex].distance) { - // // If a line with this position exists but is closer, replace it - // result[existingIndex] = { - // start: p1, - // end: p2, - // position: position, - // polygon: polygon, - // distance: distance - // }; - // } - // } - // } - } - } - - 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; - - // Y축 차이가 더 크면 위/아래로 판단 - // Y축 차이가 더 크면 위/아래로 판단 - if (Math.abs(lineMidY - refMidY) > Math.abs(lineMidX - refMidX)) { - return lineMidY > refMidY ? 'bottom' : 'top'; - } - // X축 차이가 더 크면 왼쪽/오른쪽으로 판단 - else { - return lineMidX > refMidX ? '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); -} - -// 두 직선이 평행한지 확인 -// function areLinesParallel(slope1, slope2) { -// // 두 직선 모두 수직선인 경우 -// if (slope1 === Infinity && slope2 === Infinity) return true; -// -// // 기울기의 차이가 매우 작으면 평행한 것으로 간주 -// const epsilon = 0.0001; -// return Math.abs(slope1 - slope2) < epsilon; -// } - -// 두 선분이 동일한지 확인 -// function areSameLine(p1, p2, p3, p4) { -// return ( -// (isSamePoint(p1, p3) && isSamePoint(p2, p4)) || -// (isSamePoint(p1, p4) && isSamePoint(p2, p3)) -// ); -// } -/** - * 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와 교차하는 선분(p1, p2)을 찾아 교차점에서 자릅니다. - * @param {Object} p1 - 선분의 시작점 {x, y} - * @param {Object} p2 - 선분의 끝점 {x, y} - * @param {Array} roofLines - 지붕 경계선 배열 (QLine 객체의 배열) - * @returns {Object} {p1: {x, y}, p2: {x, y}} - 교차점에서 자른 선분 또는 원래 선분 - */ -function clipLineToRoofBoundary(p1, p2, roofLines) { - if (!roofLines || !roofLines.length) return { p1, p2 }; - - let closestIntersection = null; - let minDistSq = Infinity; - const originalP1 = { ...p1 }; - const originalP2 = { ...p2 }; - - // 모든 지붕 경계선과의 교차점을 찾음 - 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) { - // 교차점과 p1 사이의 거리 계산 - const dx = intersection.x - p1.x; - const dy = intersection.y - p1.y; - const distSq = dx * dx + dy * dy; - - // p1에 가장 가까운 교차점 찾기 - if (distSq < minDistSq) { - minDistSq = distSq; - closestIntersection = intersection; - } - } - } - - // 교차점이 있으면 p2를 가장 가까운 교차점으로 업데이트 - if (closestIntersection) { - return { - p1: originalP1, - p2: closestIntersection - }; - } - - // 교차점이 없으면 원래 선분 반환 - return { p1: originalP1, p2: originalP2 }; -} - -// 기존 getLineIntersection 함수를 사용하거나, 없으면 아래 구현 사용