From 3e97eb379ba625d0cd8733cf246eaf9d3d37df9a Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 27 Jan 2026 10:24:31 +0900 Subject: [PATCH 1/4] =?UTF-8?q?type=EC=9D=B4=20null=EC=9D=B8=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useAuxiliaryDrawing.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/hooks/roofcover/useAuxiliaryDrawing.js b/src/hooks/roofcover/useAuxiliaryDrawing.js index bbf3628a..ab0af73e 100644 --- a/src/hooks/roofcover/useAuxiliaryDrawing.js +++ b/src/hooks/roofcover/useAuxiliaryDrawing.js @@ -75,13 +75,14 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { }, [arrow2]) useEffect(() => { - typeRef.current = type - clear() if (type === null) { initEvent() return } + initEvent() + typeRef.current = type + clear() addCanvasMouseEventListener('mouse:move', mouseMove) addCanvasMouseEventListener('mouse:down', mouseDown) addDocumentEventListener('keydown', document, keydown[type]) @@ -96,6 +97,10 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { return } + if (type === null) { + return + } + // 지붕의 각 꼭지점을 흡착점으로 설정 const roofsPoints = roofs.map((roof) => roof.points).flat() roofAdsorptionPoints.current = [...roofsPoints] @@ -113,6 +118,9 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { }, []) useEffect(() => { + if (type === null) { + return + } const roofs = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) if (roofs.length === 0) { return From fdaefd79e92829791c810a02ce44814bd09e5d35 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 27 Jan 2026 16:15:30 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=98=81=EC=97=AD=20=EA=BC=AC=EC=9E=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/module/useModuleBasicSetting.js | 4 +- src/util/qpolygon-utils.js | 62 ++++++++++++++++++++--- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index f298c916..45fd8a53 100644 --- a/src/hooks/module/useModuleBasicSetting.js +++ b/src/hooks/module/useModuleBasicSetting.js @@ -17,7 +17,7 @@ import { import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util' import '@/util/fabric-extensions' // fabric 객체들에 getCurrentPoints 메서드 추가 import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom' -import offsetPolygon, { calculateAngle, createLinesFromPolygon } from '@/util/qpolygon-utils' +import offsetPolygon, { calculateAngle, createLinesFromPolygon, cleanSelfIntersectingPolygon } from '@/util/qpolygon-utils' import { QPolygon } from '@/components/fabric/QPolygon' import { useEvent } from '@/hooks/useEvent' import { BATCH_TYPE, LINE_TYPE, MODULE_SETUP_TYPE, POLYGON_TYPE } from '@/common/common' @@ -384,6 +384,8 @@ export function useModuleBasicSetting(tabNum) { } else { offsetPoints = createPaddingPolygon(polygon, roof.lines).vertices } + // 자기교차(꼬임) 제거 + offsetPoints = cleanSelfIntersectingPolygon(offsetPoints) } //모듈설치영역?? 생성 diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 47e8f4e9..931362f4 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -5,6 +5,7 @@ import { getAdjacent, getDegreeByChon, isPointOnLine, isPointOnLineNew } from '@ import { QPolygon } from '@/components/fabric/QPolygon' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' +import * as turf from '@turf/turf' const TWO_PI = Math.PI * 2 const EPSILON = 1e-10 //좌표계산 시 최소 차이값 @@ -248,6 +249,47 @@ function createPaddingPolygon(polygon, offset, arcSegments = 0) { return paddingPolygon } +/** + * 자기교차(self-intersection)가 있는 폴리곤을 정리하는 함수 + * turf.js의 unkinkPolygon을 사용하여 꼬인 부분을 제거하고 가장 큰 폴리곤을 반환 + * @param {Array} vertices - [{x, y}, ...] 형태의 점 배열 + * @returns {Array} 정리된 점 배열 + */ +export function cleanSelfIntersectingPolygon(vertices) { + if (!vertices || vertices.length < 3) return vertices + + try { + // vertices를 GeoJSON 폴리곤으로 변환 + const coords = vertices.map((p) => [p.x, p.y]) + coords.push([vertices[0].x, vertices[0].y]) // ring 닫기 + + const turfPoly = turf.polygon([coords]) + + // 자기교차 검사 + const kinked = turf.kinks(turfPoly) + + if (kinked.features.length === 0) { + return vertices // 꼬임 없음 + } + + // 꼬인 폴리곤을 분리 + const unkinked = turf.unkinkPolygon(turfPoly) + + if (unkinked.features.length > 0) { + // 가장 큰 면적의 폴리곤 선택 + const largest = unkinked.features.reduce((max, f) => (turf.area(f) > turf.area(max) ? f : max)) + + // GeoJSON 좌표를 다시 {x, y} 형태로 변환 + const cleanedCoords = largest.geometry.coordinates[0] + return cleanedCoords.slice(0, -1).map((c) => ({ x: c[0], y: c[1] })) + } + } catch (e) { + console.warn('Failed to clean self-intersecting polygon:', e) + } + + return vertices +} + export default function offsetPolygon(vertices, offset) { const polygon = createPolygon(vertices) const arcSegments = 0 @@ -255,25 +297,29 @@ export default function offsetPolygon(vertices, offset) { const originPolygon = new QPolygon(vertices, { fontSize: 0 }) originPolygon.setViewLengthText(false) + let result if (offset > 0) { - let result = createMarginPolygon(polygon, offset, arcSegments).vertices - const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point)) + let marginResult = createMarginPolygon(polygon, offset, arcSegments).vertices + const allPointsOutside = marginResult.every((point) => !originPolygon.inPolygon(point)) if (allPointsOutside) { - return createMarginPolygon(polygon, offset, arcSegments).vertices + result = createMarginPolygon(polygon, offset, arcSegments).vertices } else { - return createPaddingPolygon(polygon, offset, arcSegments).vertices + result = createPaddingPolygon(polygon, offset, arcSegments).vertices } } else { - let result = createPaddingPolygon(polygon, offset, arcSegments).vertices - const allPointsInside = result.every((point) => originPolygon.inPolygon(point)) + let paddingResult = createPaddingPolygon(polygon, offset, arcSegments).vertices + const allPointsInside = paddingResult.every((point) => originPolygon.inPolygon(point)) if (allPointsInside) { - return createPaddingPolygon(polygon, offset, arcSegments).vertices + result = createPaddingPolygon(polygon, offset, arcSegments).vertices } else { - return createMarginPolygon(polygon, offset, arcSegments).vertices + result = createMarginPolygon(polygon, offset, arcSegments).vertices } } + + // 자기교차(꼬임) 제거 + return cleanSelfIntersectingPolygon(result) } function normalizePoint(point) { From 2e4bcb98cd36f5256f0081277d436972fba3e26a Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 27 Jan 2026 16:48:07 +0900 Subject: [PATCH 3/4] =?UTF-8?q?text=20=EC=99=80=20planSize=20=EB=B0=8F=20a?= =?UTF-8?q?ctualSize=EA=B0=80=20=EC=95=88=EB=A7=9E=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/usePolygon.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index b47de91a..b862047a 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -1576,6 +1576,7 @@ export const usePolygon = () => { }) roofLines.forEach((line) => { + //console.log("::::::::::",line); roof.lines.forEach((roofLine) => { if ( (isSamePoint(line.startPoint, roofLine.startPoint) && isSamePoint(line.endPoint, roofLine.endPoint)) || @@ -1952,7 +1953,16 @@ export const usePolygon = () => { } } - polygon.lines.forEach((line) => { + polygon.lines.forEach((line, index) => { + //text 와 planSize 및 actualSize가 안맞는 문제 + const nextText = polygon?.texts?.[index]?.text + const nextPlaneSize = Number(nextText) + if (nextText != null && nextText !== '' && Number.isFinite(nextPlaneSize) ) { + if(line.attributes.actualSize !== nextPlaneSize && line.attributes.planeSize !== nextPlaneSize) { + line.attributes.planeSize = nextPlaneSize + } + + } setActualSize(line, polygon.direction, +polygon.roofMaterial?.pitch) }) From b9f3b603335849fd0aa7e1ab481ddfbf041f8e9a Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 27 Jan 2026 16:48:42 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=ED=95=9C=EA=B8=80=3D>=EC=9D=BC=EB=B3=B8?= =?UTF-8?q?=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useRoofShapePassivitySetting.js | 13 +++++++------ src/locales/ja.json | 1 + src/locales/ko.json | 1 + 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/hooks/roofcover/useRoofShapePassivitySetting.js b/src/hooks/roofcover/useRoofShapePassivitySetting.js index ac36779c..6149ca39 100644 --- a/src/hooks/roofcover/useRoofShapePassivitySetting.js +++ b/src/hooks/roofcover/useRoofShapePassivitySetting.js @@ -48,9 +48,9 @@ export function useRoofShapePassivitySetting(id) { useEffect(() => { const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') if (!canvas.outerLineFix || outerLines.length === 0) { - swalFire({ text: getMessage('wall.line.not.found') }) + swalFire({ text: getMessage('wall.line.not.found'),icon: 'warning' }) closePopup(id) - return + //return } setIsLoading(true) }, []) @@ -142,8 +142,9 @@ export function useRoofShapePassivitySetting(id) { const handleConfirm = () => { if (!currentLineRef.current) { - alert('선택된 외곽선이 없습니다.') - return + //alert('선택된 외곽선이 없습니다.') + swalFire({ text: getMessage('wall.line.not.selected'), icon: 'warning' }) + //return } let attributes const offset = Number(offsetRef.current.value) / 10 @@ -210,8 +211,8 @@ export function useRoofShapePassivitySetting(id) { }) if (!checkedAllSetting) { - swalFire({ text: '설정이 완료되지 않은 외벽선이 있습니다.', icon: 'warning' }) - return + swalFire({ text: getMessage('modal.canvas.setting.roofline.properties.setting.not.setting'), icon: 'warning' }) + //return } exceptObjs.forEach((obj) => { diff --git a/src/locales/ja.json b/src/locales/ja.json index 6f762667..debbec54 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1111,6 +1111,7 @@ "module.layout.setup.has.zero.value": "モジュールの列数、段数を入力して下さい。", "modal.placement.initial.setting.plan.drawing.only.number": "(※数字は[半角]入力のみ可能です。)", "wall.line.not.found": "外壁がありません", + "wall.line.not.selected": "選択された外郭線がありません。", "roof.line.not.found": "屋根形状がありません", "roof.material.can.not.delete": "割り当てられた配置面があります。", "chidory.can.not.install": "千鳥配置できない工法です。", diff --git a/src/locales/ko.json b/src/locales/ko.json index 93b1a4f1..bbb2d7b1 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1111,6 +1111,7 @@ "module.layout.setup.has.zero.value": "모듈의 열수, 단수를 입력해 주세요.", "modal.placement.initial.setting.plan.drawing.only.number": "(※ 숫자는 [반각]입력만 가능합니다.)", "wall.line.not.found": "외벽선이 없습니다.", + "wall.line.not.selected": "선택된 외곽선이 없습니다.", "roof.line.not.found": "지붕형상이 없습니다.", "roof.material.can.not.delete": "할당된 배치면이 있습니다.", "chidory.can.not.install": "치조 불가 공법입니다.",