From 92fd17ed7192fbdef0ffce1f7690288754b911c4 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 14 Oct 2025 10:59:10 +0900 Subject: [PATCH 01/88] =?UTF-8?q?=EB=AC=B8=EC=9D=98=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=ED=8C=90=20=EC=A0=80=EC=9E=A5=EC=88=98=EC=A0=95=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/modal/QnaRegModal.jsx | 76 ++++++++++--------- src/locales/ja.json | 2 +- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/components/community/modal/QnaRegModal.jsx b/src/components/community/modal/QnaRegModal.jsx index b26536e0..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,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 = () => { 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": "お問い合わせ内容", From 1385683bce44686fa0115b75ff52394598fd5580 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 14 Oct 2025 11:05:50 +0900 Subject: [PATCH 02/88] =?UTF-8?q?hipSize=20=EA=B0=84=ED=98=B9=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=EB=B0=9C=EC=83=9D=20-=20=EA=B8=B0=EB=B3=B8=EA=B0=92?= =?UTF-8?q?=200=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 33737856..6e25c18d 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -7665,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() From 41de001986cf47a6cd84bfe2f8f7cd7389b8aedd Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 14 Oct 2025 13:58:15 +0900 Subject: [PATCH 03/88] =?UTF-8?q?=EC=88=9C=EC=84=9C=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20abs=EC=A0=81=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useMode.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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(), From 8b2cf6a9d3cc431578d0bba3d732dc3029822756 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 14 Oct 2025 14:16:02 +0900 Subject: [PATCH 04/88] =?UTF-8?q?=EC=8B=9C=EA=B3=84=EB=B0=A9=ED=96=A5=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useOuterLineWall.js | 46 ++++++++++++++++++++++ src/hooks/roofcover/useRoofShapeSetting.js | 42 +------------------- 2 files changed, 47 insertions(+), 41 deletions(-) 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() From ddf326ca6b2493fc2900dc16e3ff5c6885082eca Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 15 Oct 2025 09:54:13 +0900 Subject: [PATCH 05/88] =?UTF-8?q?=EB=8F=99,=20=ED=98=84=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 24 +++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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() } From d548b0e1f4a4bedadf035fd68ec188a7a4d494dd Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 15 Oct 2025 16:15:21 +0900 Subject: [PATCH 06/88] =?UTF-8?q?360=EB=8F=84=20=ED=9A=8C=EC=A0=84?= =?UTF-8?q?=EC=8B=9C=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/surface/useSurfaceShapeBatch.js | 52 ++++++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) 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() // 그룹을 해제하고 개별 객체로 복원 From 3a3ff7c156639cebcbaf4a75af5468d8b2a0b740 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 16 Oct 2025 18:04:09 +0900 Subject: [PATCH 07/88] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EB=82=98=EC=98=A4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=8C=20=3D>=20obj.name=20!=3D=3D=20'lengthText'=20&&=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 6021afc2..46fbb931 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -251,6 +251,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { obj.parentId === this.id && obj.name !== POLYGON_TYPE.WALL && obj.name !== POLYGON_TYPE.ROOF && + obj.name !== 'lengthText' && obj.name !== 'outerLine' && obj.name !== 'baseLine', // && obj.name !== 'outerLinePoint', From a2cd08484db2977c0e99297d08a109b810def4b5 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 17 Oct 2025 18:57:47 +0900 Subject: [PATCH 08/88] skeleton --- src/common/common.js | 2 + src/util/skeleton-utils.js | 717 +++++++++++++++++++++++++++++++++---- 2 files changed, 650 insertions(+), 69 deletions(-) diff --git a/src/common/common.js b/src/common/common.js index 757dc0e8..4220e5f5 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -218,6 +218,8 @@ export const SAVE_KEY = [ 'originColor', 'originWidth', 'originHeight', + 'skeletonLines', + 'skeleton' ] export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype] diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 54cb5ba2..34d8153e 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -4,6 +4,7 @@ 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' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -15,33 +16,144 @@ import Big from 'big.js' 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 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 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'); + } + + return oldPoints.map((point) => { + const newPoint = { ...point }; + const absMove = Big(moveFlowLine).abs().times(2).div(10); + //console.log('absMove:', absMove); + + 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); + } + + + + if(moveFlowLine > 0) { + if(moveDirection === 'down'){ + moveDirection = 'up'; + }else if(moveDirection === 'left'){ + moveDirection = 'right'; + } + } + + 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; + }) } /** @@ -52,18 +164,65 @@ 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) + //벽 + 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) 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 +230,35 @@ export const skeletonBuilder = (roofId, canvas, textMode, roof, baseLines) => { 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 = [] } } } @@ -90,16 +272,36 @@ 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 [] + let roof = canvas?.getObjects().find((object) => object.id === roofId) const skeletonLines = [] const processedInnerEdges = new Set() // 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다. + skeleton.Edges.forEach((edgeResult, index) => { - processEavesEdge(edgeResult, processedInnerEdges, roof, skeletonLines, baseLines[index].attributes.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){ + // + // 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); }); + /* // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다. @@ -161,24 +363,45 @@ const createInnerLinesFromSkeleton = (skeleton,canvas, textMode, roof, baseLines // 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: attributes.type, - textMode: textMode, + name: (line.attributes.isOuterEdge)?'eaves': attributes.type, attributes: attributes, + isBaseLine: line.attributes.isOuterEdge, + lineName: (line.attributes.isOuterEdge)?'outerLine': attributes.type, + selectable:(!line.attributes.isOuterEdge), + roofId: roofId }); - canvas.add(innerLine); - innerLine.bringToFront(); - innerLines.push(innerLine); + //skeleton 라인에서 처마선은 삭제 + if(innerLine.lineName !== 'outerLine'){ + canvas.add(innerLine); + innerLine.bringToFront(); + existingLines.add(lineKey); // 추가된 라인을 추적 + } + innerLines.push(innerLine) + canvas.renderAll(); }); - canvas.renderAll(); return innerLines; } @@ -190,22 +413,65 @@ 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) const polygonPoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); - const currentDegree = getDegreeByChon(pitch) + //처마선인지 확인하고 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 + + 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); - } + // 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)) + ); + } + } + } + return null; +} + + /** * GABLE(케라바) Edge를 처리하여 스켈레톤 선을 정리하고 연장합니다. * @param {object} edgeResult - 스켈레톤 Edge 데이터 @@ -217,7 +483,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 +498,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 +525,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; @@ -300,42 +566,50 @@ function isOuterEdge(p1, p2, edges) { * @param {number} width - 두께 * @param currentDegree */ -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) { + // 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, + ...(eavesIndex !== undefined && { eavesIndex }) }, lineStyle: { color, width }, - }); - - console.log('skeletonLines', skeletonLines); + }; + skeletonLines.push(newLine); + //console.log('skeletonLines', skeletonLines); } /** @@ -812,6 +1086,8 @@ const isPointOnSegment = (point, segStart, segEnd) => { return dotProduct >= 0 && dotProduct <= squaredLength; }; + + // Export all necessary functions export { findAllIntersections, @@ -819,3 +1095,306 @@ 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 함수를 사용하거나, 없으면 아래 구현 사용 From c60da7ddc977fbf8fbcc3dcae8b3c741da23d1c3 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 21 Oct 2025 10:50:41 +0900 Subject: [PATCH 09/88] =?UTF-8?q?[1305]Re.RISE-NBC=20AG270=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=EC=9D=84=20=EB=B6=81=EB=A9=B4=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=ED=96=88=EC=9D=84=20=EB=95=8C=20=EB=B6=81=EB=A9=B4=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98=EC=9A=A9=20=EC=B2=A8=EB=B6=80=EC=9E=90=EB=A3=8C?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B3=A0=20=EC=8B=B6=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/floorPlan/estimate/useEstimateController.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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' }) From 396d8bc70889501a1e5e0fde5d50b22d095181cc Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 22 Oct 2025 10:05:38 +0900 Subject: [PATCH 10/88] =?UTF-8?q?=EB=AA=A8=EB=93=88=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=A6=88=20=EC=B5=9C=EC=B4=88=20=EC=A0=84=EC=B2=B4=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/modal/basic/step/Orientation.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 7baa1985013f00f5744530f10204979539d2b443 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 22 Oct 2025 13:36:37 +0900 Subject: [PATCH 11/88] =?UTF-8?q?[1258]=20api/v1/master/pcsMakerList=20?= =?UTF-8?q?=EC=97=90=20moduleMatlCds=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/circuitTrestle/step/PowerConditionalSelect.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/floor-plan/modal/circuitTrestle/step/PowerConditionalSelect.jsx b/src/components/floor-plan/modal/circuitTrestle/step/PowerConditionalSelect.jsx index 2009e63f..a7aee15a 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/PowerConditionalSelect.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/PowerConditionalSelect.jsx @@ -200,6 +200,7 @@ export default function PowerConditionalSelect(props) { const param = { pcsMkrCd: option.pcsMkrCd, mixMatlNo: moduleSelectionData.module.mixMatlNo, + moduleMatlCds: moduleSelectionData.module.itemList.map((item) => item.itemId).join(','), } getPcsMakerList(param).then((res) => { From 7cc20f33c6497e15255298394f06b51cbe243f1e Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 22 Oct 2025 17:28:16 +0900 Subject: [PATCH 12/88] =?UTF-8?q?=EB=8F=99,=ED=98=84=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=EB=9D=BC=EC=9D=B8=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20=EC=83=81=EC=9A=B0=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=8B=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 08b31f4d..5e801912 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -198,8 +198,16 @@ export function useMovementSetting(id) { 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 (FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked || FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked) { + // If one is checked, uncheck the other + FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked = !FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked; + FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = !FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked; + }else{ + 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 = '' @@ -348,7 +356,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' From 82698a5d031117dcdbd9a95fe674551cf3e623a3 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 23 Oct 2025 18:34:11 +0900 Subject: [PATCH 13/88] =?UTF-8?q?=EB=8F=99,=ED=98=84=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=EB=9D=BC=EC=9D=B8=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20=EC=83=81=EC=9A=B0=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=8B=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 281 ++++++++++++++++++++++++++----------- 1 file changed, 198 insertions(+), 83 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 34d8153e..1132e9da 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -31,7 +31,8 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { const startPoint = selectLine.startPoint const endPoint = selectLine.endPoint - const oldPoints = canvas?.movePoints?.points ?? roof.points + const orgPoints = roof.points; // orgPoint를 orgPoints로 변경 + const oldPoints = canvas?.skeleton.lastPoints ?? orgPoints // 여기도 변경 const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); if (oppositeLine) { @@ -40,50 +41,80 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { console.log('No opposite line found'); } - return oldPoints.map((point) => { + + 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) + + let baseLinePoints = []; + const pointSet = new Set(); +/* + walls.forEach((wall) => { + if (wall.baseLines.length === 0) { + wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id) + } + + // Extract points from each baseLine + wall.baseLines.forEach(line => { + console.log("useSk:::", line.x1, line.y1, line.x2, line.y2); +// 시작점과 끝점을 배열에 추가 + const points = [ + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 } + ]; + + points.forEach(point => { + const key = `${point.x},${point.y}`; + if (!pointSet.has(key)) { + pointSet.add(key); + baseLinePoints.push(point); + } + }); + }); + + }) + return [...baseLinePoints]; +*/ + + return oldPoints.map((point, index) => { + + console.log('oldPoint:', point); + const originalPoint = orgPoints[index]; // orgPoint를 originalPoint로 변경 + console.log('originalPoint:', originalPoint); const newPoint = { ...point }; - const absMove = Big(moveFlowLine).abs().times(2).div(10); + const absMove = Big(moveFlowLine).times(2).div(10); //console.log('absMove:', absMove); 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); - } + //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); + // } - if(moveFlowLine > 0) { - if(moveDirection === 'down'){ - moveDirection = 'up'; - }else if(moveDirection === 'left'){ - moveDirection = 'right'; - } - } - console.log('skeletonBuilder moveDirection:', moveDirection); switch (moveDirection) { @@ -92,14 +123,22 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { 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') + newPoint.x = Big(line.start.x).plus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { - newPoint.x = Big(line.end.x).minus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') + newPoint.x = Big(line.end.x).plus(absMove).toNumber(); } - break + + break; + // } else if (line.position === 'right') { + // if (isSamePoint(newPoint, line.start)) { + // newPoint.x = Big(line.start.x).minus(absMove).toNumber(); + // (newPoint.x < originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 + // }else if(isSamePoint(newPoint, line.end)) { + // newPoint.x = Big(line.end.x).minus(absMove).toNumber(); + // (newPoint.x < originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 + // } } + } break; @@ -107,29 +146,46 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { 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') + newPoint.x = Big(line.start.x).minus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { - newPoint.x = Big(line.end.x).plus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') + newPoint.x = Big(line.end.x).minus(absMove).toNumber(); } break + // }else if(line.position === 'left') { + // if (isSamePoint(newPoint, line.start)) { + // newPoint.x = Big(line.start.x).plus(absMove).toNumber(); + // (newPoint.x > originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 + // } else if (isSamePoint(newPoint, line.end)) { + // newPoint.x= Big(line.end.x).plus(absMove).toNumber(); + // (newPoint.x > originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 + // } } + } 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') + newPoint.y = Big(line.start.y).plus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { - newPoint.y = Big(line.end.y).minus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') + newPoint.y = Big(line.end.y).plus(absMove).toNumber(); } - break + break; + + // }else if(line.position === 'bottom') { + // if(newPoint.y !== originalPoint.y) { + // if (isSamePoint(newPoint, line.start)) { + // newPoint.y = Big(line.start.y).minus(absMove).toNumber(); + // (newPoint.y < originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 + // }else if(isSamePoint(newPoint, line.end)) { + // newPoint.y = Big(line.end.y).minus(absMove).toNumber(); + // (newPoint.y < originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 + // } + // } } } @@ -137,23 +193,38 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { 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') + if (line.position === 'bottom') { + + 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).plus(absMove).toNumber(); - //changeSkeletonLine(canvas, line.end, newPoint, 'end') + newPoint.y = Big(line.end.y).minus(absMove).toNumber(); } - break + + // }else if(line.position === 'top') { + // + // if(newPoint.y !== originalPoint.y) { + // + // if (isSamePoint(newPoint, line.start)) { + // newPoint.y = Big(line.start.y).plus(absMove).toNumber(); + // (newPoint.y > originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 + // } else if (isSamePoint(newPoint, line.end)) { + // newPoint.y = Big(line.end.y).plus(absMove).toNumber(); + // (newPoint.y > originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 + // } + // } + break; } + } break; } + console.log('newPoint:', newPoint); return newPoint; }) + } /** @@ -168,18 +239,13 @@ 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 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 skeletonLines = []; @@ -197,22 +263,20 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { let points = roof.points; + + //마루이동 if (moveFlowLine !== 0) { - points = movingRidgeFromSkeleton(roofId, canvas) - const movePoints = { - points: points, - roofId: roofId, - } - canvas.set("movePoints", movePoints) + + points = movingRidgeFromSkeleton(roofId, canvas) } //처마 if(moveUpDown !== 0) { } - + console.log('points:', points); const geoJSONPolygon = toGeoJSON(points) try { @@ -232,7 +296,6 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { canvas.skeletonStates[roofId] = true canvas.skeletonLines = []; canvas.skeletonLines.push(...roof.innerLines) - canvas.set("skeletonLines", canvas.skeletonLines) const cleanSkeleton = { Edges: skeleton.Edges.map(edge => ({ @@ -249,9 +312,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { }; canvas.skeleton = []; canvas.skeleton = cleanSkeleton - + canvas.skeleton.lastPoints = points canvas.set("skeleton", cleanSkeleton); + + + canvas.renderAll() } catch (e) { console.error('스켈레톤 생성 중 오류 발생:', e) @@ -397,6 +463,23 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { canvas.add(innerLine); innerLine.bringToFront(); existingLines.add(lineKey); // 추가된 라인을 추적 + }else{ + const coordinateText = new fabric.Text(`(${Math.round(p1.x)}, ${Math.round(p1.y)})`, { + left: p1.x + 5, // 좌표점에서 약간 오른쪽으로 이동 + top: p1.y - 20, // 좌표점에서 약간 위로 이동 + fontSize: 13, + fill: 'red', + fontFamily: 'Arial', + selectable: true, + lockMovementX: false, + lockMovementY: false, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + name: 'lengthText' + }) + + canvas?.add(coordinateText) } innerLines.push(innerLine) canvas.renderAll(); @@ -439,7 +522,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { //외벽선 밖으로 나간 선을 정리한다(roof.line의 교점까지 정리한다) // 지붕 경계선과 교차 확인 및 클리핑 const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines); - console.log('clipped line', clippedLine.p1, clippedLine.p2); + //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); // } @@ -627,6 +710,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(); }; @@ -1280,14 +1364,28 @@ function getLinePosition(line, referenceLine) { 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'; + // 참조선에 대한 벡터를 계산하여 법선 벡터 구하기 + const refVecX = referenceLine.end.x - referenceLine.start.x; + const refVecY = referenceLine.end.y - referenceLine.start.y; + + // 법선 벡터 (참조선에 수직) - 방향을 수정 + const normalX = refVecY; // -refVecY에서 refVecY로 변경 + const normalY = -refVecX; // refVecX에서 -refVecX로 변경 + + // 중점에서 중점으로의 벡터 + const midVecX = lineMidX - refMidX; + const midVecY = lineMidY - refMidY; + + // 내적을 이용해 위치 판단 + const dotProduct = midVecX * normalX + midVecY * normalY; + + // 참조선의 방향에 따라 위치 결정 + if (Math.abs(refVecX) > Math.abs(refVecY)) { + // 수평에 가까운 선분인 경우 + return dotProduct > 0 ? 'top' : 'bottom'; + } else { + // 수직에 가까운 선분인 경우 + return dotProduct > 0 ? 'right' : 'left'; } } @@ -1396,5 +1494,22 @@ function clipLineToRoofBoundary(p1, p2, roofLines) { // 교차점이 없으면 원래 선분 반환 return { p1: originalP1, p2: originalP2 }; } +export const convertBaseLinesToPoints = (baseLines) => { + const points = []; + const pointSet = new Set(); -// 기존 getLineIntersection 함수를 사용하거나, 없으면 아래 구현 사용 + 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; +}; \ No newline at end of file From 0ad18e4f154c4e39ba8ecd34ff5a6162f4ca0e23 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 24 Oct 2025 13:04:56 +0900 Subject: [PATCH 14/88] =?UTF-8?q?roof.moveFlowLine=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 6 ++++-- src/hooks/useContextMenu.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 5e801912..5857707b 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -102,7 +102,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]) @@ -329,7 +329,9 @@ export function useMovementSetting(id) { 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 + let pointValue = FLOW_LINE_REF.POINTER_INPUT_REF.current.value; + let filledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current.value; + const moveFlowLine = typeRef.current === TYPE.FLOW_LINE ? (pointValue===''?filledValue:pointValue) : 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; diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.js index 4580f3ee..46ced154 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': From affef782f35532038bfc2b629850740e0f79904a Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 24 Oct 2025 18:36:38 +0900 Subject: [PATCH 15/88] =?UTF-8?q?roof.moveFlowLine=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 20 +- src/util/skeleton-utils.js | 338 +++++++++++++++------- 2 files changed, 245 insertions(+), 113 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 5857707b..20485b73 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -102,7 +102,7 @@ export function useMovementSetting(id) { /** outerLines 속성처리*/ const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') - outerLines.forEach((line) => line.set({ visible: true })) + outerLines.forEach((line) => line.set({ visible: false })) canvas.renderAll() }, [type]) @@ -321,6 +321,12 @@ 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 @@ -329,10 +335,14 @@ export function useMovementSetting(id) { const roof = canvas.getObjects().find((obj) => obj.id === roofId) // 현이동, 동이동 추가 - let pointValue = FLOW_LINE_REF.POINTER_INPUT_REF.current.value; - let filledValue = FLOW_LINE_REF.FILLED_INPUT_REF.current.value; - const moveFlowLine = typeRef.current === TYPE.FLOW_LINE ? (pointValue===''?filledValue:pointValue) : 0 - const moveUpDown = typeRef.current === TYPE.UP_DOWN ? UP_DOWN_REF.POINTER_INPUT_REF.current.value : 0 + 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 = ""; diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 1132e9da..d7168e74 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -5,6 +5,7 @@ import { QLine } from '@/components/fabric/QLine' import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' import { line } from 'framer-motion/m' +import { QPolygon } from '@/components/fabric/QPolygon' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -25,61 +26,62 @@ export const drawSkeletonRidgeRoof = (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 startPoint = selectLine.startPoint const endPoint = selectLine.endPoint - const orgPoints = roof.points; // orgPoint를 orgPoints로 변경 - const oldPoints = canvas?.skeleton.lastPoints ?? orgPoints // 여기도 변경 + const orgRoofPoints = roof.points; // orgPoint를 orgPoints로 변경 + const oldPoints = canvas?.skeleton.lastPoints ?? orgRoofPoints // 여기도 변경 const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); + 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'); } + let baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || []; + console.log('baseLines::::', baseLines); + let baseLinePoints = baseLines.map((line) => ({x:line.x1, y:line.y1})); - 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) - let baseLinePoints = []; - const pointSet = new Set(); -/* - walls.forEach((wall) => { - if (wall.baseLines.length === 0) { - wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id) - } - // Extract points from each baseLine - wall.baseLines.forEach(line => { - console.log("useSk:::", line.x1, line.y1, line.x2, line.y2); -// 시작점과 끝점을 배열에 추가 - const points = [ - { x: line.x1, y: line.y1 }, - { x: line.x2, y: line.y2 } - ]; + /* + walls.forEach((wall) => { + if (wall.baseLines.length === 0) { + wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id) + } - points.forEach(point => { - const key = `${point.x},${point.y}`; - if (!pointSet.has(key)) { - pointSet.add(key); - baseLinePoints.push(point); - } + // Extract points from each baseLine + wall.baseLines.forEach(line => { + console.log("useSk:::", line.x1, line.y1, line.x2, line.y2); + // 시작점과 끝점을 배열에 추가 + const points = [ + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 } + ]; + + points.forEach(point => { + const key = `${point.x},${point.y}`; + if (!pointSet.has(key)) { + pointSet.add(key); + baseLinePoints.push(point); + } + }); }); - }); - }) - return [...baseLinePoints]; -*/ + }) + return [...baseLinePoints]; + */ return oldPoints.map((point, index) => { - console.log('oldPoint:', point); - const originalPoint = orgPoints[index]; // orgPoint를 originalPoint로 변경 - console.log('originalPoint:', originalPoint); const newPoint = { ...point }; const absMove = Big(moveFlowLine).times(2).div(10); //console.log('absMove:', absMove); @@ -170,9 +172,9 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { for (const line of oppositeLine) { if (line.position === 'top') { if (isSamePoint(newPoint, line.start)) { - newPoint.y = Big(line.start.y).plus(absMove).toNumber(); + newPoint.y = Big(line.start.y).minus(absMove).toNumber(); } else if (isSamePoint(newPoint, line.end)) { - newPoint.y = Big(line.end.y).plus(absMove).toNumber(); + newPoint.y = Big(line.end.y).minus(absMove).toNumber(); } break; @@ -196,6 +198,8 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { 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)) { @@ -221,7 +225,9 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { break; } + console.log('newPoint:', newPoint); + //baseline 변경 return newPoint; }) @@ -246,7 +252,16 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { /** 외벽선 */ 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 = 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. 지붕 폴리곤 좌표 전처리 @@ -271,6 +286,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { points = movingRidgeFromSkeleton(roofId, canvas) + + } //처마 if(moveUpDown !== 0) { @@ -314,11 +331,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { 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) { @@ -347,23 +361,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // 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); }); @@ -431,6 +430,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const innerLines = []; const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set + skeletonLines.forEach(line => { const { p1, p2, attributes, lineStyle } = line; @@ -445,6 +445,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { return; // 이미 있는 라인이면 스킵 } + const direction = getLineDirection( + { x: line.p1.x, y: line.p1.y }, + { x: line.p2.x, y: line.p2.y } + ); + const innerLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, @@ -452,10 +457,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { strokeWidth: lineStyle.width, name: (line.attributes.isOuterEdge)?'eaves': attributes.type, attributes: attributes, + direction: direction, isBaseLine: line.attributes.isOuterEdge, lineName: (line.attributes.isOuterEdge)?'outerLine': attributes.type, selectable:(!line.attributes.isOuterEdge), - roofId: roofId + roofId: roofId, }); //skeleton 라인에서 처마선은 삭제 @@ -504,6 +510,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { 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); @@ -512,6 +519,25 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { 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: 8, + 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]; @@ -523,8 +549,8 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { // 지붕 경계선과 교차 확인 및 클리핑 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); + const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge]) + addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', 'red', 5, pitch, isOuterLine); // } } } @@ -1359,33 +1385,29 @@ function findOppositeLine(edges, startPoint, endPoint, points) { } 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 refVecX = referenceLine.end.x - referenceLine.start.x; - const refVecY = referenceLine.end.y - referenceLine.start.y; - // 법선 벡터 (참조선에 수직) - 방향을 수정 - const normalX = refVecY; // -refVecY에서 refVecY로 변경 - const normalY = -refVecX; // refVecX에서 -refVecX로 변경 + // 단순히 좌표 차이로 판단 + const deltaX = lineMidX - refMidX; + const deltaY = lineMidY - refMidY; - // 중점에서 중점으로의 벡터 - const midVecX = lineMidX - refMidX; - const midVecY = lineMidY - refMidY; + // 참조선의 기울기 + const refDeltaX = referenceLine.end.x - referenceLine.start.x; + const refDeltaY = referenceLine.end.y - referenceLine.start.y; - // 내적을 이용해 위치 판단 - const dotProduct = midVecX * normalX + midVecY * normalY; - - // 참조선의 방향에 따라 위치 결정 - if (Math.abs(refVecX) > Math.abs(refVecY)) { - // 수평에 가까운 선분인 경우 - return dotProduct > 0 ? 'top' : 'bottom'; + // 참조선이 더 수평인지 수직인지 판단 + if (Math.abs(refDeltaX) > Math.abs(refDeltaY)) { + // 수평선에 가까운 경우 - Y 좌표로 판단 + return deltaY > 0 ? 'bottom' : 'top'; } else { - // 수직에 가까운 선분인 경우 - return dotProduct > 0 ? 'right' : 'left'; + // 수직선에 가까운 경우 - X 좌표로 판단 + return deltaX > 0 ? 'right' : 'left'; } } @@ -1445,54 +1467,142 @@ function findPolygonsContainingLine(edges, p1, p2) { } /** - * roof.lines와 교차하는 선분(p1, p2)을 찾아 교차점에서 자릅니다. + * roof.lines로 만들어진 다각형 내부에만 선분이 존재하도록 클리핑합니다. * @param {Object} p1 - 선분의 시작점 {x, y} * @param {Object} p2 - 선분의 끝점 {x, y} * @param {Array} roofLines - 지붕 경계선 배열 (QLine 객체의 배열) - * @returns {Object} {p1: {x, y}, p2: {x, y}} - 교차점에서 자른 선분 또는 원래 선분 + * @returns {Object} {p1: {x, y}, p2: {x, y}} - 다각형 내부로 클리핑된 선분 */ function clipLineToRoofBoundary(p1, p2, roofLines) { - if (!roofLines || !roofLines.length) return { p1, p2 }; + if (!roofLines || !roofLines.length) { + return { p1: { ...p1 }, p2: { ...p2 } }; + } - let closestIntersection = null; - let minDistSq = Infinity; - const originalP1 = { ...p1 }; - const originalP2 = { ...p2 }; + // 기본값으로 원본 좌표 설정 + 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) { + 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 - ); + 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; + // 교차점이 선분 위에 있는지 확인 + const t = getParameterT(p1, p2, intersection); + if (t >= 0 && t <= 1) { + intersections.push({ + point: intersection, + t: t + }); } } } - // 교차점이 있으면 p2를 가장 가까운 교차점으로 업데이트 - if (closestIntersection) { - return { - p1: originalP1, - p2: closestIntersection - }; + 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: originalP1, p2: originalP2 }; + return { p1: clippedP1, p2: clippedP2 }; +} + +/** + * 점이 다각형 내부에 있는지 확인합니다 (Ray Casting 알고리즘 사용). + * @param {Object} point - 확인할 점 {x, y} + * @param {Array} roofLines - 다각형을 구성하는 선분들 + * @returns {boolean} 점이 다각형 내부에 있으면 true + */ +function isPointInsidePolygon(point, roofLines) { + let inside = false; + const x = point.x; + const y = point.y; + + for (const line of roofLines) { + const x1 = line.x1; + const y1 = line.y1; + const x2 = line.x2; + const y2 = line.y2; + + // Ray casting: 점에서 오른쪽으로 수평선을 그었을 때 다각형 경계와 교차하는 횟수 확인 + if (((y1 > y) !== (y2 > y)) && (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1)) { + inside = !inside; + } + } + + return inside; +} + +/** + * 선분 위의 점에 대한 매개변수 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 = []; @@ -1512,4 +1622,16 @@ export const convertBaseLinesToPoints = (baseLines) => { }); return points; -}; \ No newline at end of file +}; + +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) +} \ No newline at end of file From 2080c8bf20da95cd71a54aeb3b6be9ffd0a42236 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 27 Oct 2025 13:17:56 +0900 Subject: [PATCH 16/88] =?UTF-8?q?target=20=EC=9D=98=20=EC=A2=8C=ED=91=9C?= =?UTF-8?q?=EB=B9=84=EA=B5=90=20=EB=B3=80=EA=B2=BD=20Math.abs(target.y1=20?= =?UTF-8?q?-=20target.y2)=20<=200.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 103 ++++++++++++---------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 20485b73..d4c9ff9f 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -217,6 +217,8 @@ export function useMovementSetting(id) { } } + let currentCalculatedValue = 0 + const mouseMoveEvent = (e) => { const target = canvas.getActiveObject() if (!target) return @@ -224,61 +226,71 @@ export function useMovementSetting(id) { 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) { + if (Math.abs(target.y1 - target.y2) < 0.5) { + // 가로라인의 경우 value = Big(targetTop).minus(currentY).times(10).round(0) + console.log('가로라인 계산:', `${targetTop} - ${currentY.toNumber()} = ${value.toNumber()}`) } else { + // 세로라인의 경우 value = Big(targetLeft).minus(currentX).times(10).round(0).neg() + console.log('세로라인 계산:', `-(${targetLeft} - ${currentX.toNumber()}) = ${value.toNumber()}`) } - 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 - } + + currentCalculatedValue = value.toNumber() + + if (typeRef.current === TYPE.FLOW_LINE) { + FLOW_LINE_REF.POINTER_INPUT_REF.current.value = value.toNumber() } else { - if (value.s === 1) { - UP_DOWN_REF.UP_RADIO_REF.current.checked = false - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = true + 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 + } + } } else { - UP_DOWN_REF.UP_RADIO_REF.current.checked = true - UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false - } - } - } 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 + 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 + } + } } } } - } - } + + const mouseDownEvent = (e) => { canvas @@ -287,6 +299,7 @@ export function useMovementSetting(id) { .forEach((obj) => canvas.remove(obj)) canvas.renderAll() + //const target = selectedObject.current const target = selectedObject.current if (!target) return From befa12b00b3674e08bc5767d0744b259f55257f9 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 27 Oct 2025 14:38:25 +0900 Subject: [PATCH 17/88] =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 46fbb931..1c2e3d65 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -6,7 +6,6 @@ import { calculateAngle, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON } import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' -import { drawSkeletonRidgeRoof } from '@/util/skeleton-utils' export const QPolygon = fabric.util.createClass(fabric.Polygon, { type: 'QPolygon', @@ -376,9 +375,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() From 8cd67a92a848d8993be8ed1902768b0531cfadb3 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 28 Oct 2025 16:39:17 +0900 Subject: [PATCH 18/88] =?UTF-8?q?Q.PARTNERS=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/header/Header.jsx | 4 ++++ 1 file changed, 4 insertions(+) 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') }) From a75ea4c98abdcfa7d6970319813989184d3c6ace Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 29 Oct 2025 16:02:28 +0900 Subject: [PATCH 19/88] =?UTF-8?q?=ED=98=84=EC=9D=98=20=EC=83=81(out),=20?= =?UTF-8?q?=ED=95=98(in)=20=EC=9D=98=EB=AF=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 200 ++++++++++++++-------- 1 file changed, 124 insertions(+), 76 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index d4c9ff9f..f3a30473 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -91,7 +91,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 +102,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]) @@ -194,101 +194,144 @@ export function useMovementSetting(id) { 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 = '' - - if (FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked || FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked) { - // If one is checked, uncheck the other - FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked = !FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked; - FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = !FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current.checked; - }else{ - FLOW_LINE_REF.DOWN_LEFT_RADIO_REF.current.checked = true - FLOW_LINE_REF.UP_RIGHT_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 upRightChecked = FLOW_LINE_REF.UP_RIGHT_RADIO_REF.current?.checked || false + const downLeftChecked = FLOW_LINE_REF.DOWN_LEFT_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 { + 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) { - 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 (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) => { + const target = canvas.getActiveObject() 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) //.round(0, Big.roundUp) - const currentY = Big(getIntersectMousePoint(e).y) //.round(0, Big.roundUp) + const currentX = Big(getIntersectMousePoint(e).x) + const currentY = Big(getIntersectMousePoint(e).y) let value = '' if (Math.abs(target.y1 - target.y2) < 0.5) { - // 가로라인의 경우 value = Big(targetTop).minus(currentY).times(10).round(0) - console.log('가로라인 계산:', `${targetTop} - ${currentY.toNumber()} = ${value.toNumber()}`) } else { - // 세로라인의 경우 value = Big(targetLeft).minus(currentX).times(10).round(0).neg() - console.log('세로라인 계산:', `-(${targetLeft} - ${currentX.toNumber()}) = ${value.toNumber()}`) } currentCalculatedValue = value.toNumber() - 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 (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) + let checkPoint + + + if (target.y1 === target.y2) { //수평벽 + 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 { - 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 (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 + } + } + } else { + checkPoint = { x: midX.plus(10).toNumber(), y: midY.toNumber() } + if (wall.inPolygon(checkPoint)) { + if (value.s !== 1) { + 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) { + 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 } } } + } + } @@ -348,14 +391,15 @@ export function useMovementSetting(id) { 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; + 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; + + 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 + const moveUpDown = typeRef.current === TYPE.UP_DOWN ? udPointValue : 0 roof.moveFlowLine = parseInt(moveFlowLine, 10) || 0; roof.moveUpDown = parseInt(moveUpDown, 10) || 0; roof.moveDirect = ""; @@ -448,6 +492,7 @@ 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 }) } @@ -516,9 +561,12 @@ 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) { + // 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.DOWN_RADIO_REF.current.checked){ value = value.neg() } } From baeb2be0a18a9fb2a80096502618379236cde46c Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 30 Oct 2025 16:35:42 +0900 Subject: [PATCH 20/88] =?UTF-8?q?=EB=8F=99,=ED=98=84=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=EB=9D=BC=EC=9D=B8=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20=EC=83=81=EC=9A=B0=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=8B=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 148 +++++++++++++++++++--- 1 file changed, 131 insertions(+), 17 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index f3a30473..3b285e77 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -245,19 +245,17 @@ export function useMovementSetting(id) { let currentCalculatedValue = 0 const mouseMoveEvent = (e) => { - + //console.log('mouseMoveEvent:::::',e) const target = canvas.getActiveObject() 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'); - } - } - - + // 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) @@ -286,14 +284,38 @@ export function useMovementSetting(id) { 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 + wall.lines.forEach(line => { + + console.log(classifyWallLine(wall, line)) + + }) + + let linePosition = classifyWallLine(wall, target).position; 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); + } + + + /* checkPoint = { x: midX.toNumber(), y: midY.plus(10).toNumber() } if (wall.inPolygon(checkPoint)) { //선택라인이 내부 - if (value.s !== -1) { - console.log('-1value:::', value.s) + 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 { @@ -302,7 +324,7 @@ export function useMovementSetting(id) { } } else { // if (value.s === 1) { //선택라인이 외부 - console.log('+1value:::', value.s) + 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 { @@ -310,18 +332,38 @@ export function useMovementSetting(id) { 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) { + 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 { @@ -329,6 +371,7 @@ export function useMovementSetting(id) { if (UP_DOWN_REF.DOWN_RADIO_REF.current) UP_DOWN_REF.DOWN_RADIO_REF.current.checked = false } } + */ } } } @@ -406,6 +449,7 @@ export function useMovementSetting(id) { roof.moveSelectLine = target; 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) { @@ -544,6 +588,7 @@ export function useMovementSetting(id) { value = value.neg() } } else { + console.log("error::", UP_DOWN_REF) value = UP_DOWN_REF.FILLED_INPUT_REF.current.value !== '' ? Big(UP_DOWN_REF.FILLED_INPUT_REF.current.value) @@ -566,15 +611,33 @@ export function useMovementSetting(id) { // } else if (UP_DOWN_REF.DOWN_RADIO_REF.current.checked && !inPolygon) { // value = value.neg() // } - if (UP_DOWN_REF.DOWN_RADIO_REF.current.checked){ - value = value.neg() - } } + + let linePosition = classifyWallLine(wall, target).position; + + 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] @@ -627,6 +690,55 @@ export function useMovementSetting(id) { closePopup(id) } + // javascript + const classifyWallLine = (wall, line, epsilon = 0.5, offset = 10)=> { + if (!wall || !line) return { orientation: 'unknown', position: 'unknown' } + + const center = wall.getCenterPoint() + const dx = Math.abs(line.x2 - line.x1) + const dy = Math.abs(line.y2 - line.y1) + + let orientation = 'slanted' + if (dy < epsilon && dx >= epsilon) orientation = 'horizontal' + else if (dx < epsilon && dy >= epsilon) orientation = 'vertical' + else orientation = dx > dy ? 'horizontal' : 'vertical' + + const midX = (line.x1 + line.x2) / 2 + const midY = (line.y1 + line.y2) / 2 + + // checkPoint: mid에서 center 방향으로 약간 이동한 점을 계산 + let checkX = midX + let checkY = midY + if (orientation === 'horizontal') { + const dir = Math.sign(center.y - midY) || 1 + checkY = midY + dir * offset + } else if (orientation === 'vertical') { + const dir = Math.sign(center.x - midX) || 1 + checkX = midX + dir * offset + } else { + const dirX = Math.sign(center.x - midX) || 1 + const dirY = Math.sign(center.y - midY) || 1 + checkX = midX + dirX * offset + checkY = midY + dirY * offset + } + + const inside = typeof wall.inPolygon === 'function' ? wall.inPolygon({ x: checkX, y: checkY }) : true + + let position = 'unknown' + if (orientation === 'horizontal') { + const dir = center.y - midY + if (inside) position = dir > 0 ? 'top' : 'bottom' + else position = dir > 0 ? 'bottom' : 'top' + } else if (orientation === 'vertical') { + const dir = center.x - midX + if (inside) position = dir > 0 ? 'left' : 'right' + else position = dir > 0 ? 'right' : 'left' + } + + return { orientation, position, mid: { x: midX, y: midY }, center, checkPoint: { x: checkX, y: checkY }, inside } + } + + return { TYPE, closePopup, @@ -636,5 +748,7 @@ export function useMovementSetting(id) { FLOW_LINE_REF, UP_DOWN_REF, handleSave, + classifyWallLine } } + From a428cc31e89c57acd757bdab636eea41ccfd4836 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 30 Oct 2025 16:36:53 +0900 Subject: [PATCH 21/88] =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/modal/movement/type/Updown.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/floor-plan/modal/movement/type/Updown.jsx b/src/components/floor-plan/modal/movement/type/Updown.jsx index 2a3f2393..f03685e7 100644 --- a/src/components/floor-plan/modal/movement/type/Updown.jsx +++ b/src/components/floor-plan/modal/movement/type/Updown.jsx @@ -12,7 +12,7 @@ const UP_DOWN_TYPE = { export default function Updown({ UP_DOWN_REF }) { const { getMessage } = useMessage() const [type, setType] = useState(UP_DOWN_TYPE.UP) - const [filledInput, setFilledInput] = useState('') + const [filledInput, setFilledInput] = useState('100') const currentObject = useRecoilValue(currentObjectState) const handleFocus = () => { if (currentObject === null) { @@ -71,7 +71,6 @@ export default function Updown({ UP_DOWN_REF }) { Date: Thu, 30 Oct 2025 16:38:48 +0900 Subject: [PATCH 22/88] =?UTF-8?q?log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 3b285e77..f421a3bb 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -285,12 +285,6 @@ export function useMovementSetting(id) { const midY = Big(target.y1).plus(target.y2).div(2) const wall = canvas.getObjects().find((obj) => obj.id === target.attributes.wallId) - wall.lines.forEach(line => { - - console.log(classifyWallLine(wall, line)) - - }) - let linePosition = classifyWallLine(wall, target).position; if (target.y1 === target.y2) { //수평벽 @@ -622,22 +616,22 @@ export function useMovementSetting(id) { .forEach((target) => { const currentLine = target.line - console.log("linePosition::::::::::::::", linePosition) + //console.log("linePosition::::::::::::::", linePosition) if (UP_DOWN_REF.DOWN_RADIO_REF.current.checked ){ //position확인 if(linePosition === 'bottom' || linePosition === 'right') { - console.log("1value::::::::::::::", value.toString()) + //console.log("1value::::::::::::::", value.toString()) value = value.neg() } }else { if(linePosition === 'top' || linePosition === 'left') { - console.log("1value::::::::::::::", value.toString()) + //console.log("1value::::::::::::::", value.toString()) value = value.neg() } } - console.log("2value::::::::::::::", value.toString()) + //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] From 03c31c82ff7e8ce7c71d0d29aa4bc773dd6ac5c4 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 31 Oct 2025 18:08:23 +0900 Subject: [PATCH 23/88] =?UTF-8?q?log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 78 +- src/util/skeleton-utils.js | 1104 +++++++++++++++++---- 2 files changed, 941 insertions(+), 241 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index f421a3bb..91ca0093 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' //동선이동 형 올림 내림 @@ -285,7 +286,14 @@ export function useMovementSetting(id) { const midY = Big(target.y1).plus(target.y2).div(2) const wall = canvas.getObjects().find((obj) => obj.id === target.attributes.wallId) - let linePosition = classifyWallLine(wall, target).position; + 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; if (target.y1 === target.y2) { //수평벽 @@ -440,7 +448,8 @@ export function useMovementSetting(id) { roof.moveFlowLine = parseInt(moveFlowLine, 10) || 0; roof.moveUpDown = parseInt(moveUpDown, 10) || 0; roof.moveDirect = ""; - roof.moveSelectLine = target; + 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(); @@ -582,7 +591,7 @@ export function useMovementSetting(id) { value = value.neg() } } else { - console.log("error::", UP_DOWN_REF) + //console.log("error::", UP_DOWN_REF) value = UP_DOWN_REF.FILLED_INPUT_REF.current.value !== '' ? Big(UP_DOWN_REF.FILLED_INPUT_REF.current.value) @@ -606,10 +615,22 @@ export function useMovementSetting(id) { // 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 // 디버깅 로그 출력 + }); - let linePosition = classifyWallLine(wall, target).position; + //console.log("2222linePosition:::::", result.position); + +// 디버깅용 분류 결과 확인 + + 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) @@ -617,7 +638,7 @@ export function useMovementSetting(id) { const currentLine = target.line //console.log("linePosition::::::::::::::", linePosition) - if (UP_DOWN_REF.DOWN_RADIO_REF.current.checked ){ + if (UP_DOWN_REF?.DOWN_RADIO_REF?.current?.checked ){ //position확인 if(linePosition === 'bottom' || linePosition === 'right') { //console.log("1value::::::::::::::", value.toString()) @@ -685,52 +706,6 @@ export function useMovementSetting(id) { } // javascript - const classifyWallLine = (wall, line, epsilon = 0.5, offset = 10)=> { - if (!wall || !line) return { orientation: 'unknown', position: 'unknown' } - - const center = wall.getCenterPoint() - const dx = Math.abs(line.x2 - line.x1) - const dy = Math.abs(line.y2 - line.y1) - - let orientation = 'slanted' - if (dy < epsilon && dx >= epsilon) orientation = 'horizontal' - else if (dx < epsilon && dy >= epsilon) orientation = 'vertical' - else orientation = dx > dy ? 'horizontal' : 'vertical' - - const midX = (line.x1 + line.x2) / 2 - const midY = (line.y1 + line.y2) / 2 - - // checkPoint: mid에서 center 방향으로 약간 이동한 점을 계산 - let checkX = midX - let checkY = midY - if (orientation === 'horizontal') { - const dir = Math.sign(center.y - midY) || 1 - checkY = midY + dir * offset - } else if (orientation === 'vertical') { - const dir = Math.sign(center.x - midX) || 1 - checkX = midX + dir * offset - } else { - const dirX = Math.sign(center.x - midX) || 1 - const dirY = Math.sign(center.y - midY) || 1 - checkX = midX + dirX * offset - checkY = midY + dirY * offset - } - - const inside = typeof wall.inPolygon === 'function' ? wall.inPolygon({ x: checkX, y: checkY }) : true - - let position = 'unknown' - if (orientation === 'horizontal') { - const dir = center.y - midY - if (inside) position = dir > 0 ? 'top' : 'bottom' - else position = dir > 0 ? 'bottom' : 'top' - } else if (orientation === 'vertical') { - const dir = center.x - midX - if (inside) position = dir > 0 ? 'left' : 'right' - else position = dir > 0 ? 'right' : 'left' - } - - return { orientation, position, mid: { x: midX, y: midY }, center, checkPoint: { x: checkX, y: checkY }, inside } - } return { @@ -742,7 +717,6 @@ export function useMovementSetting(id) { FLOW_LINE_REF, UP_DOWN_REF, handleSave, - classifyWallLine } } diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index d7168e74..0571569b 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -6,6 +6,7 @@ import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' import { line } from 'framer-motion/m' import { QPolygon } from '@/components/fabric/QPolygon' +import { point } from '@turf/turf' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -23,13 +24,17 @@ export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { } -const movingRidgeFromSkeleton = (roofId, canvas) => { +const movingLineFromSkeleton = (roofId, canvas) => { let roof = canvas?.getObjects().find((object) => object.id === roofId) + let moveDirection = roof.moveDirect; let moveFlowLine = roof.moveFlowLine??0; - const selectLine = roof.moveSelectLine; + let moveUpDown = roof.moveUpDown??0; + const getSelectLine = () => roof.moveSelectLine; + const selectLine = getSelectLine(); + let movePosition = roof.movePosition; const startPoint = selectLine.startPoint const endPoint = selectLine.endPoint @@ -37,6 +42,10 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { 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) @@ -46,193 +55,261 @@ const movingRidgeFromSkeleton = (roofId, canvas) => { console.log('No opposite line found'); } - let baseLines = canvas.getObjects().filter((object) => object.name === 'baseLine' && object.parentId === roofId) || []; - console.log('baseLines::::', baseLines); - let baseLinePoints = baseLines.map((line) => ({x:line.x1, y:line.y1})); + 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; + } - /* - walls.forEach((wall) => { - if (wall.baseLines.length === 0) { - wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id) + } + break; + default : +// 사용 예시 } - // Extract points from each baseLine - wall.baseLines.forEach(line => { - console.log("useSk:::", line.x1, line.y1, line.x2, line.y2); - // 시작점과 끝점을 배열에 추가 - const points = [ - { x: line.x1, y: line.y1 }, - { x: line.x2, y: line.y2 } - ]; - - points.forEach(point => { - const key = `${point.x},${point.y}`; - if (!pointSet.has(key)) { - pointSet.add(key); - baseLinePoints.push(point); - } - }); - }); - + console.log('newPoint:', newPoint); + //baseline 변경 + return newPoint; }) - return [...baseLinePoints]; - */ - - return oldPoints.map((point, index) => { - - const newPoint = { ...point }; - const absMove = Big(moveFlowLine).times(2).div(10); - //console.log('absMove:', absMove); - - 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); - // } + } 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); - console.log('skeletonBuilder moveDirection:', moveDirection); + const position = movePosition //result.position; + const absMove = Big(moveUpDown).times(1).div(10); + const modifiedStartPoints = []; + // oldPoints를 복사해서 새로운 points 배열 생성 + let newPoints = oldPoints.map(point => ({...point})); - 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).plus(absMove).toNumber(); - } else if (isSamePoint(newPoint, line.end)) { - newPoint.x = Big(line.end.x).plus(absMove).toNumber(); - } + // 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)) + ); - break; - // } else if (line.position === 'right') { - // if (isSamePoint(newPoint, line.start)) { - // newPoint.x = Big(line.start.x).minus(absMove).toNumber(); - // (newPoint.x < originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 - // }else if(isSamePoint(newPoint, line.end)) { - // newPoint.x = Big(line.end.x).minus(absMove).toNumber(); - // (newPoint.x < originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 - // } - } - } + matchingLines.forEach(line => { + const originalStartPoint = line.startPoint; + const originalEndPoint = line.endPoint; + const offset = line.attributes.offset + // 새로운 좌표 계산 + let newStartPoint = {...originalStartPoint}; + let newEndPoint = {...originalEndPoint}; - 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 - // }else if(line.position === 'left') { - // if (isSamePoint(newPoint, line.start)) { - // newPoint.x = Big(line.start.x).plus(absMove).toNumber(); - // (newPoint.x > originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 - // } else if (isSamePoint(newPoint, line.end)) { - // newPoint.x= Big(line.end.x).plus(absMove).toNumber(); - // (newPoint.x > originalPoint.x)? newPoint.x = originalPoint.x : newPoint.x; // 변경된 이름 사용 - // } - } - - } - - 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(); + // 위치와 방향에 따라 좌표 조정 +/* + switch (position) { + case 'left': + if (moveDirection === 'up') { + newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber(); + } + break; + case 'right': + if (moveDirection === 'up') { + newStartPoint.x = Big(line.startPoint.x).plus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).plus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.x = Big(line.startPoint.x).minus(absMove).toNumber(); + newEndPoint.x = Big(line.endPoint.x).minus(absMove).toNumber(); + } + break; + case 'top': + if (moveDirection === 'up') { + newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber(); + } + break; + case 'bottom': + if (moveDirection === 'up') { + newStartPoint.y = Big(line.startPoint.y).plus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).plus(absMove).toNumber(); + } else if (moveDirection === 'down') { + newStartPoint.y = Big(line.startPoint.y).minus(absMove).toNumber(); + newEndPoint.y = Big(line.endPoint.y).minus(absMove).toNumber(); } break; - - // }else if(line.position === 'bottom') { - // if(newPoint.y !== originalPoint.y) { - // if (isSamePoint(newPoint, line.start)) { - // newPoint.y = Big(line.start.y).minus(absMove).toNumber(); - // (newPoint.y < originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 - // }else if(isSamePoint(newPoint, line.end)) { - // newPoint.y = Big(line.end.y).minus(absMove).toNumber(); - // (newPoint.y < originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 - // } - // } - } } - - 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(); +*/ +// 원본 라인 업데이트 + // newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트 + console.log('absMove::', absMove); + newPoints.forEach((point, index) => { + if(position === 'bottom'){ + if (moveDirection === 'in') { + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.y = Big(point.y).minus(absMove).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)) { + point.y = Big(point.y).plus(absMove).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(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).plus(absMove).toNumber(); + } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.y = Big(point.y).minus(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.y = Big(point.y).minus(absMove).toNumber(); + } + } + + }else if(position === 'left'){ + if(moveDirection === 'in'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.x = Big(point.x).plus(absMove).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)) { + point.x = Big(point.x).minus(absMove).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(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).minus(absMove).toNumber(); + } + }else if(moveDirection === 'out'){ + if(isSamePoint(roof.basePoints[index], originalStartPoint)) { + point.x = Big(point.x).plus(absMove).toNumber(); + } + if (isSamePoint(roof.basePoints[index], originalEndPoint)) { + point.x = Big(point.x).plus(absMove).toNumber(); + } } - // }else if(line.position === 'top') { - // - // if(newPoint.y !== originalPoint.y) { - // - // if (isSamePoint(newPoint, line.start)) { - // newPoint.y = Big(line.start.y).plus(absMove).toNumber(); - // (newPoint.y > originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 - // } else if (isSamePoint(newPoint, line.end)) { - // newPoint.y = Big(line.end.y).plus(absMove).toNumber(); - // (newPoint.y > originalPoint.y)? newPoint.y = originalPoint.y : newPoint.y; // 변경된 이름 사용 - // } - // } - break; } - } - break; - } - - - console.log('newPoint:', newPoint); - //baseline 변경 - return newPoint; - }) + }); + // 원본 baseLine도 업데이트 + line.startPoint = newStartPoint; + line.endPoint = newEndPoint; + }); + return newPoints; + } } + /** * SkeletonBuilder를 사용하여 스켈레톤을 생성하고 내부선을 그립니다. * @param {string} roofId - 지붕 ID @@ -281,18 +358,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { //마루이동 - if (moveFlowLine !== 0) { - - - points = movingRidgeFromSkeleton(roofId, canvas) - - + if (moveFlowLine !== 0 || moveUpDown !== 0) { + points = movingLineFromSkeleton(roofId, canvas) } -//처마 - if(moveUpDown !== 0) { - } + console.log('points:', points); const geoJSONPolygon = toGeoJSON(points) @@ -313,6 +384,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { canvas.skeletonStates[roofId] = true canvas.skeletonLines = []; canvas.skeletonLines.push(...roof.innerLines) + roof.skeletonLines = canvas.skeletonLines; const cleanSkeleton = { Edges: skeleton.Edges.map(edge => ({ @@ -355,7 +427,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if (!skeleton?.Edges) return [] let roof = canvas?.getObjects().find((object) => object.id === roofId) - const skeletonLines = [] + let skeletonLines = [] const processedInnerEdges = new Set() // 1. 모든 Edge를 순회하며 기본 스켈레톤 선(용마루)을 수집합니다. @@ -366,10 +438,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines); }); - -/* // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다. - skeleton.Edges.forEach(edgeResult => { const { Begin, End } = edgeResult.Edge; @@ -401,9 +470,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } }); -* + //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. - const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, baseLines); + const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); if(disconnectedLines.length > 0) { @@ -424,7 +493,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } -*/ + // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다. const innerLines = []; @@ -514,7 +583,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { ); if(!outerLine) { outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); - console.log('Has matching line:', outerLine); + //console.log('Has matching line:', outerLine); } let pitch = outerLine?.attributes?.pitch??0 @@ -543,11 +612,9 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { 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); + 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', 'red', 5, pitch, isOuterLine); @@ -1471,13 +1538,18 @@ function findPolygonsContainingLine(edges, p1, p2) { * @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) { +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 }; @@ -1492,6 +1564,10 @@ function clipLineToRoofBoundary(p1, p2, roofLines) { // 두 점 모두 내부에 있으면 그대로 반환 if (p1Inside && p2Inside) { + if(!selectLine || isDiagonal){ + return { p1: clippedP1, p2: clippedP2 }; + } + console.log('평행선::', clippedP1, clippedP2) return { p1: clippedP1, p2: clippedP2 }; } @@ -1516,7 +1592,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines) { } } - console.log('Found intersections:', intersections.length); + //console.log('Found intersections:', intersections.length); // 교차점들을 t 값으로 정렬 intersections.sort((a, b) => a.t - b.t); @@ -1634,4 +1710,654 @@ function getLineDirection(p1, p2) { 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; // 일치하는 라인이 없는 경우 +} + +function getLinePositionRelativeToWall(selectLine, wall) { + // wall의 경계를 가져옵니다. + const bounds = wall.getBoundingRect(); + const { left, top, width, height } = bounds; + const right = left + width; + const bottom = top + height; + + // selectLine의 중간점을 계산합니다. + const midX = (selectLine.startPoint.x + selectLine.endPoint.x) / 2; + const midY = (selectLine.startPoint.y + selectLine.endPoint.y) / 2; + + // 경계로부터의 거리를 계산합니다. + const distanceToLeft = Math.abs(midX - left); + const distanceToRight = Math.abs(midX - right); + const distanceToTop = Math.abs(midY - top); + const distanceToBottom = Math.abs(midY - bottom); + + // 가장 가까운 경계를 찾습니다. + const minDistance = Math.min( + distanceToLeft, + distanceToRight, + distanceToTop, + distanceToBottom + ); + + // 가장 가까운 경계를 반환합니다. + if (minDistance === distanceToLeft) return 'left'; + if (minDistance === distanceToRight) return 'right'; + if (minDistance === distanceToTop) return 'top'; + return 'bottom'; +} + +/** + * Convert a line into an array of coordinate points + * @param {Object} line - Line object with startPoint and endPoint + * @param {Object} line.startPoint - Start point with x, y coordinates + * @param {Object} line.endPoint - End point with x, y coordinates + * @param {number} [step=1] - Distance between points (default: 1) + * @returns {Array} Array of points [{x, y}, ...] + */ +function lineToPoints(line, step = 1) { + const { startPoint, endPoint } = line; + const points = []; + + // Add start point + points.push({ x: startPoint.x, y: startPoint.y }); + + // Calculate distance between points + const dx = endPoint.x - startPoint.x; + const dy = endPoint.y - startPoint.y; + const distance = Math.sqrt(dx * dx + dy * dy); + const steps = Math.ceil(distance / step); + + // Add intermediate points + for (let i = 1; i < steps; i++) { + const t = i / steps; + points.push({ + x: startPoint.x + dx * t, + y: startPoint.y + dy * t + }); + } + + // Add end point + points.push({ x: endPoint.x, y: endPoint.y }); + + return points; +} + +/** + * 다각형의 모든 좌표를 offset만큼 안쪽/바깥쪽으로 이동 + * @param {Array} points - 다각형 좌표 배열 [{x, y}, ...] + * @param {number} offset - offset 값 (양수: 안쪽, 음수: 바깥쪽) + * @returns {Array} offset이 적용된 새로운 좌표 배열 + */ +function offsetPolygon(points, offset) { + if (points.length < 3) return points; + + const offsetPoints = []; + const numPoints = points.length; + + for (let i = 0; i < numPoints; i++) { + const prevIndex = (i - 1 + numPoints) % numPoints; + const currentIndex = i; + const nextIndex = (i + 1) % numPoints; + + const prevPoint = points[prevIndex]; + const currentPoint = points[currentIndex]; + const nextPoint = points[nextIndex]; + + // 이전 변의 방향 벡터 + const prevVector = { + x: currentPoint.x - prevPoint.x, + y: currentPoint.y - prevPoint.y + }; + + // 다음 변의 방향 벡터 + const nextVector = { + x: nextPoint.x - currentPoint.x, + y: nextPoint.y - currentPoint.y + }; + + // 정규화 + const prevLength = Math.sqrt(prevVector.x * prevVector.x + prevVector.y * prevVector.y); + const nextLength = Math.sqrt(nextVector.x * nextVector.x + nextVector.y * nextVector.y); + + if (prevLength === 0 || nextLength === 0) continue; + + const prevNormal = { + x: -prevVector.y / prevLength, + y: prevVector.x / prevLength + }; + + const nextNormal = { + x: -nextVector.y / nextLength, + y: nextVector.x / nextLength + }; + + // 평균 법선 벡터 계산 + const avgNormal = { + x: (prevNormal.x + nextNormal.x) / 2, + y: (prevNormal.y + nextNormal.y) / 2 + }; + + // 평균 법선 벡터 정규화 + const avgLength = Math.sqrt(avgNormal.x * avgNormal.x + avgNormal.y * avgNormal.y); + if (avgLength === 0) continue; + + const normalizedAvg = { + x: avgNormal.x / avgLength, + y: avgNormal.y / avgLength + }; + + // 각도 보정 (예각일 때 offset 조정) + const cosAngle = prevNormal.x * nextNormal.x + prevNormal.y * nextNormal.y; + const adjustedOffset = Math.abs(cosAngle) > 0.1 ? offset / Math.abs(cosAngle) : offset; + + // 새로운 점 계산 + const offsetPoint = { + x: currentPoint.x + normalizedAvg.x * adjustedOffset, + y: currentPoint.y + normalizedAvg.y * adjustedOffset + }; + + offsetPoints.push(offsetPoint); + } + + return offsetPoints; +} + +/** + * 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; + + // 라인 방향 분석 + 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) => { + // 1. wall의 inPolygon 메서드가 있으면 사용 + if (typeof wall.inPolygon === 'function') { + return wall.inPolygon(point); + } + + // 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 extendLineToBoundary(p1, p2, roofLines) { + // 1. Calculate line direction and length + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const length = Math.sqrt(dx * dx + dy * dy); + if (length === 0) return { p1: { ...p1 }, p2: { ...p2 } }; + + // 2. Get all polygon points + const points = []; + const seen = new Set(); + + for (const line of roofLines) { + const p1 = { x: line.x1, y: line.y1 }; + const p2 = { x: line.x2, y: line.y2 }; + + const key1 = `${p1.x},${p1.y}`; + const key2 = `${p2.x},${p2.y}`; + + if (!seen.has(key1)) { + points.push(p1); + seen.add(key1); + } + if (!seen.has(key2)) { + points.push(p2); + seen.add(key2); + } + } + + // 3. Find the bounding box + let minX = Infinity, minY = Infinity; + let maxX = -Infinity, maxY = -Infinity; + + for (const p of points) { + minX = Math.min(minX, p.x); + minY = Math.min(minY, p.y); + maxX = Math.max(maxX, p.x); + maxY = Math.max(maxY, p.y); + } + + // 4. Extend line to bounding box + const bboxLines = [ + { x1: minX, y1: minY, x2: maxX, y2: minY }, // top + { x1: maxX, y1: minY, x2: maxX, y2: maxY }, // right + { x1: maxX, y1: maxY, x2: minX, y2: maxY }, // bottom + { x1: minX, y1: maxY, x2: minX, y2: minY } // left + ]; + + const intersections = []; + + // 5. Find intersections with bounding box + for (const line of bboxLines) { + const intersect = getLineIntersection( + p1, p2, + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 } + ); + + if (intersect) { + const t = ((intersect.x - p1.x) * dx + (intersect.y - p1.y) * dy) / (length * length); + if (t >= 0 && t <= 1) { + intersections.push({ x: intersect.x, y: intersect.y, t }); + } + } + } + + // 6. If we have two intersections, use them + if (intersections.length >= 2) { + // Sort by t value + intersections.sort((a, b) => a.t - b.t); + return { + p1: { x: intersections[0].x, y: intersections[0].y }, + p2: { + x: intersections[intersections.length - 1].x, + y: intersections[intersections.length - 1].y + } + }; + } + + // 7. Fallback to original points + return { p1: { ...p1 }, p2: { ...p2 } }; +} + +/** + * 점에서 특정 방향으로 경계선과의 교차점을 찾습니다. + * @param {Object} point - 시작점 {x, y} + * @param {Object} direction - 방향 벡터 {x, y} (정규화된 값) + * @param {Array} roofLines - 지붕 경계선 배열 + * @returns {Object|null} 교차점 {x, y} 또는 null + */ +function findBoundaryIntersection(point, direction, roofLines) { + let closestIntersection = null; + let minDistance = Infinity; + + // 충분히 긴 거리로 광선 생성 (임의로 큰 값 사용) + const rayLength = 10000; + const rayEnd = { + x: point.x + direction.x * rayLength, + y: point.y + direction.y * rayLength + }; + + // 모든 경계선과의 교차점 확인 + for (const line of roofLines) { + const lineP1 = { x: line.x1, y: line.y1 }; + const lineP2 = { x: line.x2, y: line.y2 }; + + const intersection = getLineIntersection(point, rayEnd, lineP1, lineP2); + + if (intersection) { + // 교차점까지의 거리 계산 + const distance = Math.sqrt( + Math.pow(intersection.x - point.x, 2) + + Math.pow(intersection.y - point.y, 2) + ); + + // 가장 가까운 교차점 저장 (거리가 0보다 큰 경우만) + if (distance > 0.01 && distance < minDistance) { + minDistance = distance; + closestIntersection = intersection; + } + } + } + + return closestIntersection; +} + +/** + * 점이 다른 스켈레톤 라인과의 교점인지 확인합니다. + * @param {Object} point - 확인할 점 {x, y} + * @param {Array} skeletonLines - 모든 스켈레톤 라인 배열 + * @param {Object} currentLine - 현재 라인 {p1, p2} (자기 자신 제외용) + * @param {number} tolerance - 허용 오차 + * @returns {boolean} 교점이면 true + */ +function hasIntersectionWithOtherLines(point, skeletonLines, currentLine, tolerance = 0.5) { + if (!skeletonLines || skeletonLines.length === 0) { + return false; + } + + let connectionCount = 0; + + for (const line of skeletonLines) { + // 자기 자신과의 비교는 제외 + if (line.p1 && line.p2 && currentLine.p1 && currentLine.p2) { + const isSameLineCheck = + (isSamePoint(line.p1, currentLine.p1, tolerance) && isSamePoint(line.p2, currentLine.p2, tolerance)) || + (isSamePoint(line.p1, currentLine.p2, tolerance) && isSamePoint(line.p2, currentLine.p1, tolerance)); + + if (isSameLineCheck) continue; + } + + // 다른 라인의 끝점이 현재 점과 일치하는지 확인 + if (line.p1 && isSamePoint(point, line.p1, tolerance)) { + connectionCount++; + } + if (line.p2 && isSamePoint(point, line.p2, tolerance)) { + connectionCount++; + } + } + + // 1개 이상의 다른 라인과 연결되어 있으면 교점으로 간주 + return connectionCount >= 1; } \ No newline at end of file From fecc4e999a39fb4b263a5a9f405964cea917b3ff Mon Sep 17 00:00:00 2001 From: Cha Date: Sun, 2 Nov 2025 00:48:53 +0900 Subject: [PATCH 24/88] =?UTF-8?q?baseLine=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 35 ++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index 91ca0093..e52003b3 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -263,12 +263,33 @@ export function useMovementSetting(id) { const currentY = Big(getIntersectMousePoint(e).y) let value = '' - if (Math.abs(target.y1 - target.y2) < 0.5) { + let direction = '' + + if (Math.abs(target.y1 - target.y2) < 0.5) { // 수평 라인 value = Big(targetTop).minus(currentY).times(10).round(0) - } else { + + // 방향 감지 + 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' // 마우스가 라인 왼쪽에 있음 (왼쪽으로 움직임) + } } + // 방향 정보를 사용하여 라디오 버튼 상태 업데이트 + + console.log(`방향: ${direction}, 값: ${value.toNumber()}`) + + currentCalculatedValue = value.toNumber() if (typeRef.current === TYPE.FLOW_LINE) { @@ -286,6 +307,8 @@ export function useMovementSetting(id) { 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 // 디버깅 로그 출력 @@ -294,6 +317,7 @@ export function useMovementSetting(id) { //console.log("1111linePosition:::::", result.position); // 'top', 'bottom', 'left', 'right' let linePosition = result.position; +console.log("1111linePosition:::::", direction, linePosition); if (target.y1 === target.y2) { //수평벽 @@ -312,7 +336,9 @@ export function useMovementSetting(id) { setRadioStates(value.s !== 1); } + if(direction === 'up') { + } /* checkPoint = { x: midX.toNumber(), y: midY.plus(10).toNumber() } if (wall.inPolygon(checkPoint)) { //선택라인이 내부 @@ -376,6 +402,7 @@ export function useMovementSetting(id) { */ } } + } @@ -623,8 +650,8 @@ export function useMovementSetting(id) { debug: true // 디버깅 로그 출력 }); - //console.log("2222linePosition:::::", result.position); - + console.log("2222linePosition:::::", result.position); + console.log("222moveDirect:::::", roof.moveDirect); // 디버깅용 분류 결과 확인 From a3d1704390613ee9bfd133dc558d20576c236689 Mon Sep 17 00:00:00 2001 From: Cha Date: Sun, 2 Nov 2025 00:51:49 +0900 Subject: [PATCH 25/88] =?UTF-8?q?baseLine=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useMovementSetting.js | 8 ++++---- src/util/skeleton-utils.js | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index e52003b3..cb5c0f02 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -287,7 +287,7 @@ export function useMovementSetting(id) { // 방향 정보를 사용하여 라디오 버튼 상태 업데이트 - console.log(`방향: ${direction}, 값: ${value.toNumber()}`) + //console.log(`방향: ${direction}, 값: ${value.toNumber()}`) currentCalculatedValue = value.toNumber() @@ -317,7 +317,7 @@ export function useMovementSetting(id) { //console.log("1111linePosition:::::", result.position); // 'top', 'bottom', 'left', 'right' let linePosition = result.position; -console.log("1111linePosition:::::", direction, linePosition); +//console.log("1111linePosition:::::", direction, linePosition); if (target.y1 === target.y2) { //수평벽 @@ -650,8 +650,8 @@ console.log("1111linePosition:::::", direction, linePosition); debug: true // 디버깅 로그 출력 }); - console.log("2222linePosition:::::", result.position); - console.log("222moveDirect:::::", roof.moveDirect); + //console.log("2222linePosition:::::", result.position); + //console.log("222moveDirect:::::", roof.moveDirect); // 디버깅용 분류 결과 확인 diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 0571569b..6294faaf 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -1989,6 +1989,23 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { 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); @@ -2086,10 +2103,6 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { // 점이 다각형 내부에 있는지 확인하는 함수 const checkPointInPolygon = (point, wall) => { - // 1. wall의 inPolygon 메서드가 있으면 사용 - if (typeof wall.inPolygon === 'function') { - return wall.inPolygon(point); - } // 2. wall.baseLines를 이용한 Ray Casting Algorithm if (!wall.baseLines || !Array.isArray(wall.baseLines)) { From ee79c0e680bc8be74f3137e0e525921a89d38f2e Mon Sep 17 00:00:00 2001 From: Cha Date: Thu, 6 Nov 2025 23:06:35 +0900 Subject: [PATCH 26/88] =?UTF-8?q?=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 81 +++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 6294faaf..a05866f1 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -532,12 +532,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { selectable:(!line.attributes.isOuterEdge), roofId: roofId, }); - + canvas.add(innerLine); + innerLine.bringToFront(); + existingLines.add(lineKey); // 추가된 라인을 추적 //skeleton 라인에서 처마선은 삭제 if(innerLine.lineName !== 'outerLine'){ - canvas.add(innerLine); - innerLine.bringToFront(); - existingLines.add(lineKey); // 추가된 라인을 추적 + // canvas.add(innerLine); + // innerLine.bringToFront(); + // existingLines.add(lineKey); // 추가된 라인을 추적 }else{ const coordinateText = new fabric.Text(`(${Math.round(p1.x)}, ${Math.round(p1.y)})`, { left: p1.x + 5, // 좌표점에서 약간 오른쪽으로 이동 @@ -615,7 +617,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { // 지붕 경계선과 교차 확인 및 클리핑 const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine); - //console.log('clipped line', clippedLine.p1, clippedLine.p2); + 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', 'red', 5, pitch, isOuterLine); // } @@ -1641,7 +1643,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { * @param {Array} roofLines - 다각형을 구성하는 선분들 * @returns {boolean} 점이 다각형 내부에 있으면 true */ -function isPointInsidePolygon(point, roofLines) { +function isPointInsidePolygon2(point, roofLines) { let inside = false; const x = point.x; const y = point.y; @@ -1656,11 +1658,78 @@ function isPointInsidePolygon(point, roofLines) { if (((y1 > y) !== (y2 > y)) && (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1)) { inside = !inside; } + } return inside; } +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 값을 구합니다. From 7377b06e2af7df3e7a30d998c3d6c4eb9725153b Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 7 Nov 2025 07:57:41 +0900 Subject: [PATCH 27/88] =?UTF-8?q?log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QLine.js | 1 + src/components/fabric/QPolygon.js | 5 +- src/util/skeleton-utils.js | 843 +++++++++++++++++++++++++++--- 3 files changed, 788 insertions(+), 61 deletions(-) diff --git a/src/components/fabric/QLine.js b/src/components/fabric/QLine.js index 77254d5d..02448cd7 100644 --- a/src/components/fabric/QLine.js +++ b/src/components/fabric/QLine.js @@ -16,6 +16,7 @@ export const QLine = fabric.util.createClass(fabric.Line, { children: [], padding: 5, textVisible: true, + textBaseline: 'alphabetic', initialize: function (points, options, length = 0) { // 소수점 전부 제거 diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 1c2e3d65..3ca095a8 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -6,6 +6,7 @@ import { calculateAngle, drawGableRoof, drawRidgeRoof, drawShedRoof, toGeoJSON } import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' +import { drawSkeletonRidgeRoof } from '@/util/skeleton-utils' export const QPolygon = fabric.util.createClass(fabric.Polygon, { type: 'QPolygon', @@ -335,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/util/skeleton-utils.js b/src/util/skeleton-utils.js index 6294faaf..7f7ffcd7 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -376,6 +376,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { roof.innerLines = roof.innerLines || []; roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) + + // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {} @@ -385,7 +387,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { canvas.skeletonLines = []; canvas.skeletonLines.push(...roof.innerLines) roof.skeletonLines = canvas.skeletonLines; - + canvas.renderAll() const cleanSkeleton = { Edges: skeleton.Edges.map(edge => ({ X1: edge.Edge.Begin.X, @@ -434,8 +436,10 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { skeleton.Edges.forEach((edgeResult, index) => { - + canvas.skeletonLines = []; processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines); + + }); // 2. 케라바(Gable) 속성을 가진 외벽선에 해당하는 스켈레톤을 후처리합니다. @@ -470,7 +474,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } }); - +/* //2. 연결이 끊어진 스켈레톤 선을 찾아 연장합니다. const { disconnectedLines } = findDisconnectedSkeletonLines(skeletonLines, roof.lines); @@ -492,15 +496,16 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { trimIntersectingExtendedLines(skeletonLines, disconnectedLines); } +*/ - + //2. 연결이 끊어진 라인이 있을경우 찾아서 추가한다(동 이동일때) // 3. 최종적으로 정리된 스켈레톤 선들을 QLine 객체로 변환하여 캔버스에 추가합니다. const innerLines = []; const existingLines = new Set(); // 이미 추가된 라인을 추적하기 위한 Set - - skeletonLines.forEach(line => { + console.log("length::::::::", skeletonLines.length); + skeletonLines.forEach((line, index) => { const { p1, p2, attributes, lineStyle } = line; // 라인을 고유하게 식별할 수 있는 키 생성 (정규화된 좌표로 정렬하여 비교) @@ -510,9 +515,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { ].sort().join('|'); // 이미 추가된 라인인지 확인 - if (existingLines.has(lineKey)) { - return; // 이미 있는 라인이면 스킵 - } + // if (existingLines.has(lineKey)) { + // return; // 이미 있는 라인이면 스킵 + // } + + const direction = getLineDirection( { x: line.p1.x, y: line.p1.y }, @@ -533,18 +540,159 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { roofId: roofId, }); + console.log('Processing line:', { + p1: {x: p1.x, y: p1.y}, + p2: {x: p2.x, y: p2.y}, + attributes, + lineName: (line.attributes.isOuterEdge) ? 'outerLine' : attributes.type + }); + + console.log('innerLines', innerLine.lineName); + console.log("index::::",index) //skeleton 라인에서 처마선은 삭제 + if(innerLine.lineName !== 'outerLine'){ canvas.add(innerLine); innerLine.bringToFront(); - existingLines.add(lineKey); // 추가된 라인을 추적 + //existingLines.add(lineKey); // 추가된 라인을 추적 + }else{ + + + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + const wallLines = wall.baseLines + // 현재 지점과 다음 지점을 비교하기 위한 변수 + let changedLine = roof.moveSelectLine; + const roofLines = []; + + + if (!wall.lines || !wall.baseLines) { + return wall.baseLines || wall.lines || []; + } + + // 길이가 다른 경우 baseLines 반환 + if (wall.lines.length !== wall.baseLines.length) { + return wall.baseLines; + } + + for (let i = 0; i < wall.baseLines.length; i++) { + const baseLine = wall.baseLines[i]; + const line = wall.lines[i]; + + if (!line || + ((!isSamePoint(baseLine.startPoint, line.startPoint)) && // 시작점이 다르고 + (!isSamePoint(baseLine.endPoint, line.endPoint)))) { // 끝점도 다른 경우 + + } + } + + + const startClosest = findClosestRoofLine(p1, roof.lines); + const endClosest = findClosestRoofLine(p2, roof.lines); + + + const { point, closest, selectPoint, otherPoint } = + startClosest.distance > endClosest.distance + ? { + point : p2, + closest : endClosest, + otherPoint: p1 + } + : { + point : p1, + closest : startClosest, + otherPoint: p2 + }; + +// Log the relevant information + console.log("Point:", point); + console.log("Closest intersection:", closest); + console.log("moveSelectLinePoint:", selectPoint); + let isTarget = false; + for(const roofLine of roof.lines){ + if( isSamePoint(p1, roofLine.startPoint) || + isSamePoint(p2, roofLine.endPoint) || + isSamePoint(p1, roofLine.endPoint) || + isSamePoint(p2, roofLine.startPoint) ) { + isTarget = true ; + break + } + } + if (isTarget) { + console.warn("matching line found in roof.lines"); + return; // 또는 적절한 오류 처리 + } + + const innerLine2 = new QLine([p1.x, p1.y, p2.x, p2.y], { + parentId: roof.id, + fontSize: roof.fontSize, + stroke: 'red', + strokeWidth: lineStyle.width, + name: (line.attributes.isOuterEdge)?'eaves': attributes.type, + attributes: attributes, + direction: direction, + isBaseLine: line.attributes.isOuterEdge, + lineName: (line.attributes.isOuterEdge)?'addLine': attributes.type, + selectable:(!line.attributes.isOuterEdge), + roofId: roofId, + }); + + canvas.add(innerLine2); + //existingLines.add(lineKey); // 추가된 라인을 추적 + /* + //라인추가(까지 못할때때) 외벽선에서 추가 + const wall = canvas.getObjects().find((object) => object.name === POLYGON_TYPE.WALL && object.attributes.roofId === roofId) + const wallLines = wall.baseLines + // 현재 지점과 다음 지점을 비교하기 위한 변수 + let changedLine = roof.moveSelectLine; + const roofLines = []; + + + if (!wall.lines || !wall.baseLines) { + return wall.baseLines || wall.lines || []; + } + + // 길이가 다른 경우 baseLines 반환 + if (wall.lines.length !== wall.baseLines.length) { + return wall.baseLines; + } + + + //그려지는 처마라인이 처마 && 포인터모두가 wall.baseLine에 들어가 있는 경우 + const checkPoint = {x1:line.x1, y1:line.y1, x2:line.x2, y2:line.y2} + if(line.attributes.type === 'hip' && !checkPointInPolygon(checkPoint, wall)) { + const startClosest = findClosestRoofLine(p1, roof.lines); + const endClosest = findClosestRoofLine(p2, roof.lines); + console.log("Lindd::::",line) + const { point, closest, selectPoint, otherPoint } = + startClosest.distance > endClosest.distance + ? { + point : p2, + closest : endClosest, + //selectPoint : changedLine.endPoint, + otherPoint: p1 + } + : { + point : p1, + closest : startClosest, + //selectPoint : changedLine.startPoint, + otherPoint: p2 + }; + +// Log the relevant information + console.log("Point:", point); + console.log("Closest intersection:", closest); + console.log("moveSelectLinePoint:", selectPoint); + } +*/ + const coordinateText = new fabric.Text(`(${Math.round(p1.x)}, ${Math.round(p1.y)})`, { left: p1.x + 5, // 좌표점에서 약간 오른쪽으로 이동 top: p1.y - 20, // 좌표점에서 약간 위로 이동 fontSize: 13, fill: 'red', fontFamily: 'Arial', + textBaseline: 'alphabetic', // 올바른 값으로 설정 selectable: true, lockMovementX: false, lockMovementY: false, @@ -553,7 +701,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { lockScalingY: true, name: 'lengthText' }) - +//좌표점(임시) canvas?.add(coordinateText) } innerLines.push(innerLine) @@ -581,10 +729,10 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { 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); - } + // if(!outerLine) { + // outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); + // console.log('Has matching line:', outerLine); + // } let pitch = outerLine?.attributes?.pitch??0 @@ -608,6 +756,10 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { } let eavesLines = [] + // 이 다각형이 roof.lines와 일치하는 변을 가지고 있는지 확인 + + const isolatedPolygons = []; + for (let i = 0; i < polygonPoints.length; i++) { const p1 = polygonPoints[i]; const p2 = polygonPoints[(i + 1) % polygonPoints.length]; @@ -615,11 +767,28 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { // 지붕 경계선과 교차 확인 및 클리핑 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]) + + //현이동에 의해 스켈레톤 라인이 내부에서 끝난경우 roof.lines까지 수평은 수직, 수직은 수평되게 설정 + + // 다각형이 roof.lines와 일치하는 변이 하나도 없는 경우에만 확장 + // 이 다각형이 roof.lines와 일치하는 변을 가지고 있는지 확인 + // 다각형의 모든 변이 roof.lines와 일치하지 않는 경우에만 확장 + //const extendLine = extendLineToRoofBoundary(clippedLine, roof, convertedPolygon) + + + const isIsolated = !convertedPolygon.some(pp => + roof.points.some(rp => + Math.abs(pp.x - rp.x) < 0.5 && Math.abs(pp.y - rp.y) < 0.5 + )); + //console.log("skeletonLines::::",skeletonLines); addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', 'red', 5, pitch, isOuterLine); - // } + } + + //그려진 + } @@ -730,17 +899,64 @@ function isOuterEdge(p1, p2, edges) { }); } +function isOuterRoofLine(p1, p2, lines) { + const tolerance = 0.5; + let foundLine = null; + let isForward = false; + let isBackward = false; + + for (const line of lines) { + console.log("lines of line::::", line.startPoint, line.endPoint); + const lineStart = { x: line.startPoint.x, y: line.startPoint.y }; + const lineEnd = { x: line.endPoint.x, y: line.endPoint.y }; + + + let p1X = Math.abs(lineStart.x - p1.x) < tolerance && Math.abs(lineEnd.x - p2.x) < tolerance + let p2X = Math.abs(lineEnd.x - p1.x) < tolerance && Math.abs(lineStart.x - p2.x) < tolerance + let p1Y = Math.abs(lineStart.y - p1.y) < tolerance && Math.abs(lineEnd.y - p2.y) < tolerance + let p2Y = Math.abs(lineEnd.y - p2.y) < tolerance && Math.abs(lineStart.y - p2.y) < tolerance + + if (p1X || p2X) { + foundLine = line; + p1.y = p1X? lineStart.y : lineEnd.y + p2.y = p2X? lineEnd.y : lineStart.y + break; // 매칭되는 라인을 찾으면 루프 종료 + }else if(p1Y || p2Y) { + foundLine = line; + p1.x = p1Y? lineStart.x : lineEnd.x + p2.x = p2Y? lineEnd.x : lineStart.x + break; // 매칭되는 라인을 찾으면 루프 종료 + } + } + + if (foundLine) { + return { + result: true, + line: foundLine, + p1: p1, + p2: p2 + }; + } + + return { + result: false, + line: null, + pX: false, + p2: false, + }; +} + /** * 스켈레톤 라인 배열에 새로운 라인을 추가합니다. (중복 방지) * @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, 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('|'); @@ -1492,23 +1708,27 @@ function calculateSlope(p1, p2) { 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; -// } +// Helper function to calculate slope of a line +function getSlope(line) { + const dx = line.endPoint.x - line.startPoint.x; + // Avoid division by zero for vertical lines + return dx === 0 ? Infinity : (line.endPoint.y - line.startPoint.y) / dx; +} + +// Check if two lines are parallel +function areLinesParallel(line1, line2) { + const slope1 = getSlope(line1); + const slope2 = getSlope(line2); + + // Both lines are vertical + if (slope1 === Infinity && slope2 === Infinity) return true; + + // Check if slopes are approximately equal + 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 */ @@ -1560,14 +1780,14 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { // p2가 다각형 내부에 있는지 확인 const p2Inside = isPointInsidePolygon(p2, roofLines); - console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); + //console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); // 두 점 모두 내부에 있으면 그대로 반환 if (p1Inside && p2Inside) { if(!selectLine || isDiagonal){ return { p1: clippedP1, p2: clippedP2 }; } - console.log('평행선::', clippedP1, clippedP2) + //console.log('평행선::', clippedP1, clippedP2) return { p1: clippedP1, p2: clippedP2 }; } @@ -1600,20 +1820,20 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { if (!p1Inside && !p2Inside) { // 두 점 모두 외부에 있는 경우 if (intersections.length >= 2) { - console.log('Both outside, using intersection points'); + //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'); + //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'); + //console.log('p1 outside, p2 inside - moving p1 to intersection'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; // p2는 이미 내부에 있으므로 원본 유지 @@ -1623,7 +1843,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { } else if (p1Inside && !p2Inside) { // p1이 내부, p2가 외부 if (intersections.length > 0) { - console.log('p1 inside, p2 outside - moving p2 to intersection'); + //console.log('p1 inside, p2 outside - moving p2 to intersection'); // p1은 이미 내부에 있으므로 원본 유지 clippedP1.x = p1.x; clippedP1.y = p1.y; @@ -1641,7 +1861,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { * @param {Array} roofLines - 다각형을 구성하는 선분들 * @returns {boolean} 점이 다각형 내부에 있으면 true */ -function isPointInsidePolygon(point, roofLines) { +function isPointInsidePolygon2(point, roofLines) { let inside = false; const x = point.x; const y = point.y; @@ -1661,6 +1881,61 @@ function isPointInsidePolygon(point, roofLines) { return inside; } + +/** + * 점이 다각형 내부에 있는지 확인합니다 (Ray Casting 알고리즘 사용). + * @param {Object} point - 확인할 점 {x, y} + * @param {Array} polygonLines - 다각형을 구성하는 선분들의 배열 + * @returns {boolean} 점이 다각형 내부에 있으면 true + */ +function isPointInsidePolygon(point, polygonLines) { + if (!polygonLines || polygonLines.length < 3) { + return false; + } + + let inside = false; + const x = point.x; + const y = point.y; + const n = polygonLines.length; + + // 경계 박스(bounding box) 체크로 빠르게 필터링 + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; + for (const line of polygonLines) { + minX = Math.min(minX, line.x1, line.x2); + maxX = Math.max(maxX, line.x1, line.x2); + minY = Math.min(minY, line.y1, line.y2); + maxY = Math.max(maxY, line.y1, line.y2); + } + + // 점이 경계 박스 밖에 있으면 바로 false 반환 + if (x < minX || x > maxX || y < minY || y > maxY) { + return false; + } + + // Ray Casting 알고리즘 + for (let i = 0, j = n - 1; i < n; j = i++) { + const xi = polygonLines[i].x1; + const yi = polygonLines[i].y1; + const xj = polygonLines[j].x1; + const yj = polygonLines[j].y1; + + // 점이 정점 위에 있는 경우 + if ((xi === x && yi === y) || (xj === x && yj === y)) { + return true; + } + + // 수평선과 교차하는지 확인 + const intersect = ((yi > y) !== (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + + if (intersect) { + inside = !inside; + } + } + + return inside; +} + /** * 선분 위의 점에 대한 매개변수 t를 계산합니다. * p = p1 + t * (p2 - p1)에서 t 값을 구합니다. @@ -1989,19 +2264,19 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const { x1, y1, x2, y2 } = lineCoords; - console.log('wall.points', wall.baseLines); + //console.log('wall.points', wall.baseLines); for(const line of wall.baseLines) { - console.log('line', line); + //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); + //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); + //console.log('basePoint 일치!!!', basePoint); } } @@ -2009,11 +2284,11 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { // 라인 방향 분석 const lineInfo = analyzeLineOrientation(x1, y1, x2, y2, epsilon); - if (debug) { - console.log('=== getSelectLinePosition ==='); - console.log('selectLine 좌표:', lineCoords); - console.log('라인 방향:', lineInfo.orientation); - } + // if (debug) { + // console.log('=== getSelectLinePosition ==='); + // console.log('selectLine 좌표:', lineCoords); + // console.log('라인 방향:', lineInfo.orientation); + // } // 라인의 중점 const midX = (x1 + x2) / 2; @@ -2032,11 +2307,11 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const topIsInside = checkPointInPolygon(topTestPoint, wall); const bottomIsInside = checkPointInPolygon(bottomTestPoint, wall); - if (debug) { - console.log('수평선 테스트:'); - console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); - console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); - } + // if (debug) { + // console.log('수평선 테스트:'); + // console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); + // console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); + // } // top 조건: 위쪽이 외부, 아래쪽이 내부 if (!topIsInside && bottomIsInside) { @@ -2094,9 +2369,9 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { midPoint: { x: midX, y: midY } }; - if (debug) { - console.log('최종 결과:', result); - } + // if (debug) { + // console.log('최종 결과:', result); + // } return result; }; @@ -2373,4 +2648,454 @@ function hasIntersectionWithOtherLines(point, skeletonLines, currentLine, tolera // 1개 이상의 다른 라인과 연결되어 있으면 교점으로 간주 return connectionCount >= 1; +} + +/** + * 대각선의 양 끝점을 roof.lines의 가장 가까운 접점까지 확장합니다. + * @param {Object} p1 - 대각선의 시작점 {x, y} + * @param {Object} p2 - 대각선의 끝점 {x, y} + * @param {Array} roofLines - 지붕 경계선 배열 + * @returns {Object|null} 확장된 라인 {p1: {x, y}, p2: {x, y}} 또는 null + */ +function extendDiagonalToRoofBoundary(p1, p2, roofLines) { + if (!roofLines || roofLines.length === 0) { + return null; + } + + const tolerance = 0.5; + const extendedLine = { + p1: { ...p1 }, + p2: { ...p2 } + }; + + // p1이 roof.lines 위에 있는지 확인 + const p1OnBoundary = roofLines.some(line => { + return isPointOnLineSegment(p1, + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 }, + tolerance + ); + }); + + // p2가 roof.lines 위에 있는지 확인 + const p2OnBoundary = roofLines.some(line => { + return isPointOnLineSegment(p2, + { x: line.x1, y: line.y1 }, + { x: line.x2, y: line.y2 }, + tolerance + ); + }); + + // p1을 확장해야 하는 경우 + if (!p1OnBoundary) { + const extendedP1 = findClosestBoundaryPoint(p1, p2, roofLines, 'backward'); + if (extendedP1) { + extendedLine.p1 = extendedP1; + } + } + + // p2를 확장해야 하는 경우 + if (!p2OnBoundary) { + const extendedP2 = findClosestBoundaryPoint(p2, p1, roofLines, 'forward'); + if (extendedP2) { + extendedLine.p2 = extendedP2; + } + } + + return extendedLine; +} + +/** + * 점이 선분 위에 있는지 확인합니다 (허용 오차 포함). + * @param {Object} point - 확인할 점 {x, y} + * @param {Object} lineStart - 선분 시작점 {x, y} + * @param {Object} lineEnd - 선분 끝점 {x, y} + * @param {number} tolerance - 허용 오차 + * @returns {boolean} 선분 위에 있으면 true + */ +function isPointOnLineSegment(point, lineStart, lineEnd, tolerance = 0.5) { + const dist = Math.sqrt( + Math.pow(lineEnd.x - lineStart.x, 2) + + Math.pow(lineEnd.y - lineStart.y, 2) + ); + + const dist1 = Math.sqrt( + Math.pow(point.x - lineStart.x, 2) + + Math.pow(point.y - lineStart.y, 2) + ); + + const dist2 = Math.sqrt( + Math.pow(point.x - lineEnd.x, 2) + + Math.pow(point.y - lineEnd.y, 2) + ); + + return Math.abs(dist - (dist1 + dist2)) < tolerance; +} + +/** + * 한 점에서 다른 점 방향으로 연장하여 가장 가까운 roof.lines 교차점을 찾습니다. + * @param {Object} fromPoint - 연장할 시작점 + * @param {Object} directionPoint - 방향을 결정하는 점 + * @param {Array} roofLines - 지붕 경계선 배열 + * @param {string} direction - 'forward' 또는 'backward' + * @returns {Object|null} 교차점 {x, y} 또는 null + */ +function findClosestBoundaryPoint(fromPoint, directionPoint, roofLines, direction = 'forward') { + // 방향 벡터 계산 + const dx = directionPoint.x - fromPoint.x; + const dy = directionPoint.y - fromPoint.y; + const length = Math.sqrt(dx * dx + dy * dy); + + if (length === 0) return null; + + // 정규화된 방향 벡터 + const dirVec = { + x: dx / length, + y: dy / length + }; + + // backward 방향인 경우 벡터를 반대로 + if (direction === 'backward') { + dirVec.x = -dirVec.x; + dirVec.y = -dirVec.y; + } + + let closestIntersection = null; + let minDistance = Infinity; + + // 모든 roof.lines와의 교차점 찾기 + for (const line of roofLines) { + const lineP1 = { x: line.x1, y: line.y1 }; + const lineP2 = { x: line.x2, y: line.y2 }; + + // 무한 직선과의 교차점 계산 + const intersection = getRayIntersectionWithSegment( + fromPoint, + dirVec, + lineP1, + lineP2 + ); + + if (intersection && intersection.t > 0.1) { // 약간의 여유를 둠 + const distance = Math.sqrt( + Math.pow(intersection.point.x - fromPoint.x, 2) + + Math.pow(intersection.point.y - fromPoint.y, 2) + ); + + if (distance < minDistance) { + minDistance = distance; + closestIntersection = intersection.point; + } + } + } + + return closestIntersection; +} + + +/** + * 다각형이 roof.lines와 일치하는 변(edge)을 하나 이상 가지고 있는지 확인합니다. + * @param {Array} polygonPoints - 다각형의 점들 배열 [{x, y}, ...] + * @param {Array} roofLines - 지붕 경계선 배열 + * @returns {boolean} 일치하는 변이 하나라도 있으면 true, 하나도 없으면 false + */ +function polygonHasMatchingRoofLine(polygonPoints, roofLines) { + if (!polygonPoints || polygonPoints.length < 2) return false; + if (!roofLines || roofLines.length === 0) return false; + + const tolerance = 0.5; + + // 다각형의 각 변을 순회 + for (let i = 0; i < polygonPoints.length; i++) { + const p1 = polygonPoints[i]; + const p2 = polygonPoints[(i + 1) % polygonPoints.length]; + + // 이 변이 roof.lines 중 하나와 일치하는지 확인 + for (const roofLine of roofLines) { + const rp1 = { x: roofLine.x1, y: roofLine.y1 }; + const rp2 = { x: roofLine.x2, y: roofLine.y2 }; + + // 정방향 또는 역방향으로 일치하는지 확인 + const forwardMatch = + isSamePoint(p1, rp1, tolerance) && isSamePoint(p2, rp2, tolerance); + const backwardMatch = + isSamePoint(p1, rp2, tolerance) && isSamePoint(p2, rp1, tolerance); + + if (forwardMatch || backwardMatch) { + console.log('Found matching edge - this polygon touches roof.lines:', { + polygonEdge: { p1, p2 }, + roofLine: { p1: rp1, p2: rp2 } + }); + return true; // 하나라도 일치하면 즉시 true 반환 + } + } + } + + console.log('No matching edges - this polygon is isolated from roof.lines'); + return false; // 모든 변을 확인했는데 일치하는 게 없음 +} + + +function extendLineToRoofBoundary(clippedLine, roof, convertedPolygon) { + + const isIsolated = !convertedPolygon.some(pp => + roof.points.some(rp => + Math.abs(pp.x - rp.x) < 0.5 && Math.abs(pp.y - rp.y) < 0.5 + )); + + // 대각선 라인 선택해서 가장 가까운 경계선 까지 확장 + const dx = Math.abs(clippedLine.p2.x - clippedLine.p1.x); + const dy = Math.abs(clippedLine.p2.y - clippedLine.p1.y); + const isDiagonal = dx > 0.5 && dy > 0.5; + + if (isIsolated && isDiagonal) { + // moveSelectLine의 방향 벡터 계산 + const selectDx = roof.moveSelectLine.endPoint.x - roof.moveSelectLine.startPoint.x; + const selectDy = roof.moveSelectLine.endPoint.y - roof.moveSelectLine.startPoint.y; + const selectLength = Math.sqrt(selectDx * selectDx + selectDy * selectDy); + const selectDir = { x: selectDx / selectLength, y: selectDy / selectLength }; + + // moveSelectLine의 중점 + const selectMidX = (roof.moveSelectLine.startPoint.x + roof.moveSelectLine.endPoint.x) / 2; + const selectMidY = (roof.moveSelectLine.startPoint.y + roof.moveSelectLine.endPoint.y) / 2; + + // 이동 방향 확인 + const moveDirection = roof.moveDirect; // 'up', 'down', 'left', 'right', 'in', 'out' + const movePosiotn = roof.movePosition + // moveSelectLine과 평행한 경계선 찾기 + let closestParallelLine = null; + let minDistToLine = Infinity; + + // 대각선의 중점 + const midX = (clippedLine.p1.x + clippedLine.p2.x) / 2; + const midY = (clippedLine.p1.y + clippedLine.p2.y) / 2; + + for (const roofLine of roof.lines) { + const lineP1 = { x: roofLine.x1, y: roofLine.y1 }; + const lineP2 = { x: roofLine.x2, y: roofLine.y2 }; + + // 경계선의 방향 벡터 계산 + const lineDx = lineP2.x - lineP1.x; + const lineDy = lineP2.y - lineP1.y; + const lineLength = Math.sqrt(lineDx * lineDx + lineDy * lineDy); + const lineDir = { x: lineDx / lineLength, y: lineDy / lineLength }; + + // moveSelectLine과 평행한지 확인 + const dotProduct = Math.abs(selectDir.x * lineDir.x + selectDir.y * lineDir.y); + const isParallel = dotProduct > 0.95; + + if (!isParallel) continue; + + // 대각선 중점에서 경계선까지의 거리 + const lineMidX = (lineP1.x + lineP2.x) / 2; + const lineMidY = (lineP1.y + lineP2.y) / 2; + const dist = Math.sqrt( + Math.pow(midX - lineMidX, 2) + + Math.pow(midY - lineMidY, 2) + ); + + if (dist < minDistToLine) { + minDistToLine = dist; + closestParallelLine = roofLine; + } + } + + if (closestParallelLine) { + const lineP1 = { x: closestParallelLine.x1, y: closestParallelLine.y1 }; + const lineP2 = { x: closestParallelLine.x2, y: closestParallelLine.y2 }; + const lineMidX = (lineP1.x + lineP2.x) / 2; + const lineMidY = (lineP1.y + lineP2.y) / 2; + + // 이동 방향에 따라 확장할 끝점 결정 + let shouldExtendP1 = false; + + switch (movePosiotn) { + case 'down': + // moveSelectLine이 아래/밖으로 이동 -> 아래/밖에 있는 점 확장 + //shouldExtendP1 = (lineMidY > selectMidY) ? (clippedLine.p1.y > clippedLine.p2.y) : (clippedLine.p1.y < clippedLine.p2.y); + break; + case 'up': + // moveSelectLine이 위/안으로 이동 -> 위/안에 있는 점 확장 + //shouldExtendP1 = (lineMidY < selectMidY) ? (clippedLine.p1.y < clippedLine.p2.y) : (clippedLine.p1.y > clippedLine.p2.y); + break; + case 'left': + // moveSelectLine이 왼쪽으로 이동 -> 왼쪽에 있는 점 확장 + //shouldExtendP1 = (lineMidX < selectMidX) ? (clippedLine.p1.x < clippedLine.p2.x) : (clippedLine.p1.x > clippedLine.p2.x); + break; + case 'right': + // moveSelectLine이 오른쪽으로 이동 -> 오른쪽에 있는 점 확장 + //shouldExtendP1 = (lineMidX > selectMidX) ? (clippedLine.p1.x > clippedLine.p2.x) : (clippedLine.p1.x < clippedLine.p2.x); + break; + } + + const endPoint = shouldExtendP1 ? clippedLine.p1 : clippedLine.p2; + const startPoint = shouldExtendP1 ? clippedLine.p2 : clippedLine.p1; + + // 대각선 방향 벡터 + const dirX = endPoint.x - startPoint.x; + const dirY = endPoint.y - startPoint.y; + + // endPoint에서 방향으로 연장 + const farEnd = { + x: endPoint.x + dirX * 10000, + y: endPoint.y + dirY * 10000 + }; + + const intersection = getLineIntersection(startPoint, farEnd, lineP1, lineP2); + + if (intersection) { + // endPoint를 교차점으로 확장 + if (shouldExtendP1) { + clippedLine.p1 = intersection; + } else { + clippedLine.p2 = intersection; + } + console.log('고립된 다각형 대각선 확장됨 (방향 고려):', clippedLine); + return clippedLine + + } + } + return null; + } + + + return undefined +} + +function getIntersectionDetails(p1, p2, line) { + const { startPoint: lineStart, endPoint: lineEnd } = line; + const lineIsVertical = Math.abs(lineEnd.x - lineStart.x) < 0.0001; + const lineIsHorizontal = Math.abs(lineEnd.y - lineStart.y) < 0.0001; + + // Calculate intersection point + const d1 = (p2.x - p1.x) * (lineStart.y - p1.y) - (p2.y - p1.y) * (lineStart.x - p1.x); + const d2 = (p2.x - p1.x) * (lineEnd.y - p1.y) - (p2.y - p1.y) * (lineEnd.x - p1.x); + const d3 = (lineEnd.x - lineStart.x) * (p1.y - lineStart.y) - (lineEnd.y - lineStart.y) * (p1.x - lineStart.x); + const d4 = (lineEnd.x - lineStart.x) * (p2.y - lineStart.y) - (lineEnd.y - lineStart.y) * (p2.x - lineStart.x); + + if ((d1 * d2 < 0) && (d3 * d4 < 0)) { + // Calculate intersection point + const t = d3 / (d3 - d4); + const ix = p1.x + t * (p2.x - p1.x); + const iy = p1.y + t * (p2.y - p1.y); + + // Calculate distances to determine which point is closer + const distToP1 = Math.hypot(ix - p1.x, iy - p1.y); + const distToP2 = Math.hypot(ix - p2.x, iy - p2.y); + + // Determine if intersection is closer to start or end of the line segment + const distToLineStart = Math.hypot(ix - lineStart.x, iy - lineStart.y); + const distToLineEnd = Math.hypot(ix - lineEnd.x, iy - lineEnd.y); + + return { + intersects: true, + point: { x: ix, y: iy }, + // Which point of the segment (p1 or p2) is closer to intersection + segmentPoint: distToP1 < distToP2 ? 'p1' : 'p2', + // Which point of the moveSelectLine is closer to intersection + linePoint: distToLineStart < distToLineEnd ? 'start' : 'end', + // Line orientation + lineOrientation: lineIsVertical ? 'vertical' : lineIsHorizontal ? 'horizontal' : 'diagonal' + }; + } + + return { intersects: false }; +} + +function findClosestParallelLine(intersection, roofLines, orientation) { + if (!intersection || !roofLines?.length) return null; + + let closest = null; + let minDist = Infinity; + + for (const line of roofLines) { + const p1 = line.x1 !== undefined ? {x: line.x1, y: line.y1} : line.startPoint; + const p2 = line.x2 !== undefined ? {x: line.x2, y: line.y2} : line.endPoint; + + // Check line orientation + const dx = Math.abs(p2.x - p1.x); + const dy = Math.abs(p2.y - p1.y); + const lineOrientation = dx < 0.0001 ? 'vertical' : + dy < 0.0001 ? 'horizontal' : 'diagonal'; + + if (lineOrientation !== orientation) continue; + + // Calculate distance to line + const dist = Math.min( + Math.hypot(p1.x - intersection.x, p1.y - intersection.y), + Math.hypot(p2.x - intersection.x, p2.y - intersection.y) + ); + + if (dist < minDist) { + minDist = dist; + closest = { startPoint: p1, endPoint: p2 }; + } + } + + return closest; +} + +function findClosestRoofLine(point, roofLines) { + let closestLine = null; + let minDistance = Infinity; + let roofLineIndex = 0; + let interPoint = null; + + roofLines.forEach((roofLine, index) => { + const lineP1 = roofLine.startPoint; + const lineP2 = roofLine.endPoint; + + // 점에서 선분까지의 최단 거리 계산 + const distance = pointToLineDistance(point, lineP1, lineP2); + + // 점에서 수직으로 내린 교점 계산 + const intersection = getProjectionPoint(point, { + x1: lineP1.x, + y1: lineP1.y, + x2: lineP2.x, + y2: lineP2.y + }); + + if (distance < minDistance) { + minDistance = distance; + closestLine = roofLine; + roofLineIndex = index + interPoint = intersection; + } + }); + + return { line: closestLine, distance: minDistance, index: roofLineIndex, intersectionPoint: interPoint }; +} + +// 점에서 선분까지의 최단 거리를 계산하는 도우미 함수 +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); } \ No newline at end of file From 3c094b3c5ad2550dc5e3861b2f760591e50032e2 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 7 Nov 2025 08:32:41 +0900 Subject: [PATCH 28/88] =?UTF-8?q?log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/placementShape/PlacementShapeSetting.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index 8b1f7dc8..51c16c1a 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -370,7 +370,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla }} options={{ allowNegative: false, - allowDecimal: false //(index !== 0), + allowDecimal: true //(index !== 0), }} /> From 88632a461903bff2682c59229891b08564d9d816 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 7 Nov 2025 08:45:51 +0900 Subject: [PATCH 29/88] =?UTF-8?q?sk=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 3ca095a8..8186b449 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('패턴 지붕') From 2b8de3d0d00de09d58cb024949cb2261b5ba1da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=B0=BD=EC=88=98?= Date: Fri, 7 Nov 2025 15:46:04 +0900 Subject: [PATCH 30/88] =?UTF-8?q?style:=20=EB=AA=A8=EB=8B=AC=20=EC=A0=91?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/draggable/WithDraggable.jsx | 3 +++ src/styles/_modal.scss | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/common/draggable/WithDraggable.jsx b/src/components/common/draggable/WithDraggable.jsx index 7ebdf067..e962eb16 100644 --- a/src/components/common/draggable/WithDraggable.jsx +++ b/src/components/common/draggable/WithDraggable.jsx @@ -42,11 +42,14 @@ function WithDraggableHeader({ title, onClose, children }) { return (

{title}

+
+ {onClose && ( )} +
) } 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; From 9af16ac047eea1b84c868d0fd50f1414a09d6267 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 11 Nov 2025 17:20:44 +0900 Subject: [PATCH 31/88] =?UTF-8?q?=EC=88=98=EB=8F=99=EC=A7=80=EB=B6=95?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=95=88=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useMode.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 6030f43f..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,12 +1821,10 @@ export function useMode() { x: xDiff.eq(0) ? offsetCurrentPoint.x : nextWall.x1, y: yDiff.eq(0) ? offsetCurrentPoint.y : nextWall.y1, } - 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)) - } + + 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(), From f63202877f3afb5132de3c9d0a40126abcfa6afa Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 18 Nov 2025 13:11:59 +0900 Subject: [PATCH 32/88] =?UTF-8?q?[1314][HANASYS=20DESIGN]Simulation?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=EC=9D=98=20=EB=B3=80=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=B4=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/simulator/Simulator.jsx | 4 ++-- src/hooks/module/useTrestle.js | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/simulator/Simulator.jsx b/src/components/simulator/Simulator.jsx index 5d6a7e3d..9831d1b3 100644 --- a/src/components/simulator/Simulator.jsx +++ b/src/components/simulator/Simulator.jsx @@ -270,9 +270,9 @@ export default function Simulator() { setPwrGnrSimType(e.target.value) }} > - + {/**/} - + {/**/} 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' } }) } From 43f70f9f79da59656f2f10b56503ffba5dfa9293 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 24 Nov 2025 14:03:10 +0900 Subject: [PATCH 33/88] =?UTF-8?q?=EC=86=8C=EC=88=98=EC=A0=90=EC=9D=B4?= =?UTF-8?q?=ED=95=98=202=EC=9E=90=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/input/CalcInput.jsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) 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) + // } + // } } // 커서를 텍스트 끝으로 이동하고 스크롤 처리 From 3071c0ddc5512dc2e1e72e667f2ae0e371b1680b Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 24 Nov 2025 14:06:01 +0900 Subject: [PATCH 34/88] =?UTF-8?q?[1308]=20=EC=B4=8C=EC=97=90=20=EC=86=8C?= =?UTF-8?q?=EC=88=98=EC=A0=90=20=EC=9E=85=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/roofShape/type/Direction.jsx | 81 +++++++++++++++---- .../modal/roofShape/type/Pattern.jsx | 46 ++++++++++- .../floor-plan/modal/roofShape/type/Ridge.jsx | 31 ++++++- .../modal/roofShape/type/option/Eaves.jsx | 32 +++++++- .../modal/roofShape/type/option/Gable.jsx | 16 +++- .../roofShape/type/option/HipAndGable.jsx | 27 +++++-- .../roofShape/type/option/Jerkinhead.jsx | 52 ++++++++++-- .../modal/roofShape/type/option/Shed.jsx | 6 +- .../modal/roofShape/type/option/Wall.jsx | 28 +++++-- 9 files changed, 275 insertions(+), 44 deletions(-) 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..674525a8 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,8 @@ export default function HipAndGable({ pitch, setPitch, eavesOffset, setEavesOffs {getMessage('slope')}
- setPitch(normalizeDecimalLimit(e.target.value, 2))} /> + setPitch(normalizeDecimalLimit(e.target.value, 2))} />
{pitchText} @@ -19,7 +21,8 @@ export default function HipAndGable({ pitch, setPitch, eavesOffset, setEavesOffs {getMessage('eaves.offset')}
- setEavesOffset(normalizeDigits(e.target.value))} /> + setEavesOffset(normalizeDigits(e.target.value))} />
mm @@ -28,12 +31,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..c4f8736f 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) => jerkinHeadPitch(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..7326204e 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/Shed.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/Shed.jsx @@ -8,14 +8,16 @@ export default function Shed({ shedWidth, setShedWidth, shedPitch, setShedPitch,
{getMessage('slope')}
- setShedPitch(normalizeDecimalLimit(e.target.value, 2))} /> + setShedPitch(normalizeDecimalLimit(e.target.value, 2))} />
{pitchText}
{getMessage('shed.width')}
- setShedWidth(normalizeDigits(e.target.value))} /> + setShedWidth(normalizeDigits(e.target.value))} />
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
From 0a97166b06d1b46f703a87a2be6e1e35c188ecc5 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 24 Nov 2025 14:23:45 +0900 Subject: [PATCH 35/88] =?UTF-8?q?[1308]=20=EC=B4=8C=EC=97=90=20=EC=86=8C?= =?UTF-8?q?=EC=88=98=EC=A0=90=20=EC=9E=85=EB=A0=A52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roofShape/type/option/HipAndGable.jsx | 33 ++++++++++++++++--- .../roofShape/type/option/Jerkinhead.jsx | 4 +-- .../modal/roofShape/type/option/Shed.jsx | 33 ++++++++++++++++--- 3 files changed, 60 insertions(+), 10 deletions(-) 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 674525a8..cb38560f 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/HipAndGable.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/HipAndGable.jsx @@ -11,8 +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}
@@ -21,8 +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
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 c4f8736f..30c1a9bd 100644 --- a/src/components/floor-plan/modal/roofShape/type/option/Jerkinhead.jsx +++ b/src/components/floor-plan/modal/roofShape/type/option/Jerkinhead.jsx @@ -19,8 +19,8 @@ export default function Jerkinhead({ {getMessage('gable.offset')}
- setGableOffset(normalizeDigits(e.target.value))} /> + {/* setGableOffset(normalizeDigits(e.target.value))} />*/} {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
From d9b68f301233f81197ba6031cda0f35d36956367 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 25 Nov 2025 09:49:04 +0900 Subject: [PATCH 36/88] =?UTF-8?q?[1308]=20=EC=B4=8C=EC=97=90=20=EC=86=8C?= =?UTF-8?q?=EC=88=98=EC=A0=90=20=EC=9E=85=EB=A0=A5=20-=20=EC=B2=98?= =?UTF-8?q?=EB=A7=88,=EC=BC=80=EB=9D=BC=EB=B0=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/eavesGable/type/Eaves.jsx | 96 ++++++++++++++----- .../modal/eavesGable/type/Gable.jsx | 54 +++++++++-- .../floor-plan/modal/eavesGable/type/Shed.jsx | 15 ++- .../modal/eavesGable/type/WallMerge.jsx | 16 +++- 4 files changed, 148 insertions(+), 33 deletions(-) diff --git a/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx b/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx index f9548dad..fb4db8cd 100644 --- a/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { useRecoilValue } from 'recoil' import { ANGLE_TYPE, currentAngleTypeSelector } from '@/store/canvasAtom' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pitchText }) { const { getMessage } = useMessage() @@ -21,17 +22,32 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit {getMessage('slope')}
- {*/} + {/* const v = normalizeDecimalLimit(e.target.value, 2)*/} + {/* e.target.value = v*/} + {/* if (pitchRef?.current) pitchRef.current.value = v*/} + {/* }}*/} + {/*/>*/} + { - const v = normalizeDecimalLimit(e.target.value, 2) - e.target.value = v - if (pitchRef?.current) pitchRef.current.value = v + value={currentAngleType === ANGLE_TYPE.SLOPE ? 4 : 21.8} + onChange={(value) => { + if (pitchRef?.current) pitchRef.current.value = value }} - /> + options={{ + allowNegative: false, + allowDecimal: true //(index !== 0), + }} + >
{pitchText}
@@ -40,17 +56,32 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit {getMessage('offset')}
- {*/} + {/* const v = normalizeDigits(e.target.value)*/} + {/* e.target.value = v*/} + {/* if (offsetRef?.current) offsetRef.current.value = v*/} + {/* }}*/} + {/*/>*/} + { - const v = normalizeDigits(e.target.value) - e.target.value = v - if (offsetRef?.current) offsetRef.current.value = v + value={500} + onChange={(value) => { + if (offsetRef?.current) offsetRef.current.value = value }} - /> + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm @@ -91,18 +122,33 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
- {*/} + {/* const v = normalizeDigits(e.target.value)*/} + {/* e.target.value = v*/} + {/* if (widthRef?.current) widthRef.current.value = v*/} + {/* }}*/} + {/*/>*/} + { - const v = normalizeDigits(e.target.value) - e.target.value = v - if (widthRef?.current) widthRef.current.value = v + value={500} + onChange={(value) => { + if (widthRef?.current) widthRef.current.value = value }} - /> + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
mm
diff --git a/src/components/floor-plan/modal/eavesGable/type/Gable.jsx b/src/components/floor-plan/modal/eavesGable/type/Gable.jsx index 7dd21539..c5496a1e 100644 --- a/src/components/floor-plan/modal/eavesGable/type/Gable.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/Gable.jsx @@ -3,6 +3,7 @@ import Image from 'next/image' import { useState } from 'react' import { useRecoilValue } from 'recoil' import { ANGLE_TYPE, currentAngleTypeSelector } from '@/store/canvasAtom' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pitchText }) { const { getMessage } = useMessage() @@ -21,7 +22,19 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit {getMessage('offset')}
- + {/**/} +
mm
@@ -65,13 +78,29 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit {getMessage('slope')}
- */} + + onChange={(value) => { + if (pitchRef?.current) pitchRef.current.value = value + }} + options={{ + allowNegative: false, + allowDecimal: false //(index !== 0), + }} + >
{pitchText} @@ -91,7 +120,20 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit {getMessage('offset')}
- + {/**/} +
mm diff --git a/src/components/floor-plan/modal/eavesGable/type/Shed.jsx b/src/components/floor-plan/modal/eavesGable/type/Shed.jsx index 436316a3..8ce6038a 100644 --- a/src/components/floor-plan/modal/eavesGable/type/Shed.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/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() @@ -10,7 +11,19 @@ export default function Shed({ offsetRef }) { {getMessage('offset')}
- + {/**/} +
mm diff --git a/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx b/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx index 0ed01ede..e398a2f1 100644 --- a/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx @@ -1,6 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import Image from 'next/image' import { useState } from 'react' +import { CalculatorInput } from '@/components/common/input/CalcInput' export default function WallMerge({ offsetRef, radioTypeRef }) { const { getMessage } = useMessage() @@ -51,7 +52,20 @@ export default function WallMerge({ offsetRef, radioTypeRef }) { {getMessage('offset')}
- + {/**/} +
mm From f5b68894eab4420bb6f93982a2414c42d2d24a8b Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 25 Nov 2025 13:32:36 +0900 Subject: [PATCH 37/88] sessionState?.storeId --- src/components/community/modal/QnaRegModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/community/modal/QnaRegModal.jsx b/src/components/community/modal/QnaRegModal.jsx index 8f9437f9..00c444c6 100644 --- a/src/components/community/modal/QnaRegModal.jsx +++ b/src/components/community/modal/QnaRegModal.jsx @@ -89,7 +89,7 @@ let fileCheck = false; siteTpCd: "QC", schNoticeClsCd: "QNA", regId: sessionState?.userId || '', - storeId: sessionState?.userId || '', + storeId: sessionState?.storeId || '', qstMail: sessionState?.email || '', qnaClsLrgCd: '', qnaClsMidCd: '', From 7ddbc40cd493cb737fa1e7a85f0a2409f3446c4e Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 25 Nov 2025 13:41:35 +0900 Subject: [PATCH 38/88] =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=80=20=EC=A0=91=ED=9E=98=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/draggable/WithDraggable.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/common/draggable/WithDraggable.jsx b/src/components/common/draggable/WithDraggable.jsx index e962eb16..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,17 +38,17 @@ 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 && ( + + )}
) From aeb457e5ec5a8659d3514ea44d10ffd35aa26565 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 26 Nov 2025 09:18:01 +0900 Subject: [PATCH 39/88] =?UTF-8?q?[1308]=20=EC=B4=8C=EC=97=90=20=EC=86=8C?= =?UTF-8?q?=EC=88=98=EC=A0=90=20=EC=9E=85=EB=A0=A5=20-=20=EC=B2=98?= =?UTF-8?q?=EB=A7=88,=EC=BC=80=EB=9D=BC=EB=B0=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/modal/eavesGable/type/Gable.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/floor-plan/modal/eavesGable/type/Gable.jsx b/src/components/floor-plan/modal/eavesGable/type/Gable.jsx index c5496a1e..b8eab8a7 100644 --- a/src/components/floor-plan/modal/eavesGable/type/Gable.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/Gable.jsx @@ -98,7 +98,7 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit }} options={{ allowNegative: false, - allowDecimal: false //(index !== 0), + allowDecimal: true //(index !== 0), }} >
From 34871397eee7ddd75126217af66e168f28b5bbe1 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 26 Nov 2025 09:29:17 +0900 Subject: [PATCH 40/88] =?UTF-8?q?[1308]=20=EC=B4=8C=EC=97=90=20=EC=86=8C?= =?UTF-8?q?=EC=88=98=EC=A0=90=20=EC=9E=85=EB=A0=A5=20-=20{pitchText}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/modal/Slope.jsx | 15 +++++++++- .../ContextRoofAllocationSetting.jsx | 30 ++++++++++++++----- .../roofAllocation/RoofAllocationSetting.jsx | 30 ++++++++++++++----- .../modal/roofShape/passivity/Eaves.jsx | 21 ++++++++++--- 4 files changed, 77 insertions(+), 19 deletions(-) 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/roofAllocation/ContextRoofAllocationSetting.jsx b/src/components/floor-plan/modal/roofAllocation/ContextRoofAllocationSetting.jsx index 78597844..82abfa3a 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,30 @@ 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) - }} + ref={pitchRef} 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..2e8d415b 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,30 @@ 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) - }} + ref={pitchRef} 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..fac8701e 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} From 189dd82c5071b7fb1d0db86514fbd9666a301249 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 26 Nov 2025 09:47:06 +0900 Subject: [PATCH 41/88] =?UTF-8?q?=EB=AA=A8=EB=93=88,=20=ED=9A=8C=EB=A1=9C?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20=ED=8C=9D=EC=97=85=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/basic/BasicSetting.jsx | 60 +++++++++++-------- .../circuitTrestle/CircuitTrestleSetting.jsx | 52 +++++++++------- 2 files changed, 66 insertions(+), 46 deletions(-) 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/circuitTrestle/CircuitTrestleSetting.jsx b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx index 68893c23..002a2932 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) @@ -788,20 +792,28 @@ 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/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 + }} />