diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index c48c2d21..cc315e32 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, cleanSelfIntersectingPolygon, createLinesFromPolygon } from '@/util/qpolygon-utils' +import offsetPolygon, { calculateAngle, cleanSelfIntersectingPolygon, clipOffsetToOriginal, createLinesFromPolygon } 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' @@ -55,7 +55,7 @@ export function useModuleBasicSetting(tabNum) { const setCurrentObject = useSetRecoilState(currentObjectState) const { setModuleStatisticsData } = useCircuitTrestle() - const { createRoofPolygon, createMarginPolygon, createPaddingPolygon } = useMode() + const { createRoofPolygon, createMarginPolygon, createPaddingPolygon, removeShortEdges } = useMode() const moduleSetupOption = useRecoilValue(moduleSetupOptionState) const setManualSetupMode = useSetRecoilState(toggleManualSetupModeState) @@ -388,10 +388,14 @@ export function useModuleBasicSetting(tabNum) { setSurfaceShapePattern(roof, roofDisplay.column, true, roof.roofMaterial) //패턴 변경 // let offsetPoints = createPaddingPolygon(createRoofPolygon(roof.points), roof.lines).vertices //안쪽 offset let offsetPoints = null - const polygon = createRoofPolygon(roof.getCurrentPoints()) + + // 겹치는 선(같은 방향) 중 짧은 edge를 제거하여 폴리곤 단순화 + const cleaned = removeShortEdges(roof.getCurrentPoints(), roof.lines) + const polygon = createRoofPolygon(cleaned.vertices) + const cleanedLines = cleaned.lines const originPolygon = new QPolygon(roof.getCurrentPoints(), { fontSize: 0 }) - let result = createPaddingPolygon(polygon, roof.lines).vertices + let result = createPaddingPolygon(polygon, cleanedLines).vertices //margin polygon 의 point가 기준 polygon의 밖에 있는지 판단한다. const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point)) @@ -404,12 +408,14 @@ export function useModuleBasicSetting(tabNum) { } else { //육지붕이 아닐때 if (allPointsOutside) { - offsetPoints = createMarginPolygon(polygon, roof.lines).vertices + offsetPoints = createMarginPolygon(polygon, cleanedLines).vertices } else { - offsetPoints = createPaddingPolygon(polygon, roof.lines).vertices + offsetPoints = createPaddingPolygon(polygon, cleanedLines).vertices } // 자기교차(꼬임) 제거 offsetPoints = cleanSelfIntersectingPolygon(offsetPoints) + // 둔각 꼭짓점에서 offset 선이 지붕 바깥으로 튀어나가는 문제 수정 + offsetPoints = clipOffsetToOriginal(offsetPoints, roof.getCurrentPoints()) } //모듈설치영역?? 생성 diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 8eebe849..69c2db78 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1675,14 +1675,81 @@ export function useMode() { * @param arcSegments * @returns {{vertices, edges: *[], minX: *, minY: *, maxX: *, maxY: *}} */ + /** + * 같은 방향(각도)으로 겹치는 edge들을 감지하여, 가장 긴 선만 유지한다. + * 겹치는 짧은 edge의 꼭짓점을 제거하고 폴리곤을 단순화한다. + * @param {Array} vertices - 폴리곤 꼭짓점 배열 + * @param {Array} lines - 대응하는 QLine 배열 + * @returns {{ vertices: Array, lines: Array }} 정리된 꼭짓점과 lines + */ + function removeShortEdges(vertices, lines) { + if (!vertices || vertices.length < 4) return { vertices, lines } + + // 각 edge의 각도와 길이를 계산 + function edgeAngle(v1, v2) { + const rad = Math.atan2(v2.y - v1.y, v2.x - v1.x) + return Math.round(rad * (180 / Math.PI)) + } + function edgeLength(v1, v2) { + const dx = v2.x - v1.x + const dy = v2.y - v1.y + return Math.sqrt(dx * dx + dy * dy) + } + + // 같은 방향인지 판단 (동일 각도 또는 정반대 180도) + function isSameDirection(angle1, angle2) { + const diff = Math.abs(angle1 - angle2) % 360 + return diff <= 1 || Math.abs(diff - 180) <= 1 + } + + // 연속된 같은 방향 edge를 그룹으로 묶기 + const n = vertices.length + const edges = [] + for (let i = 0; i < n; i++) { + const next = (i + 1) % n + edges.push({ + index: i, + angle: edgeAngle(vertices[i], vertices[next]), + length: edgeLength(vertices[i], vertices[next]), + }) + } + + const removeIndices = new Set() + + for (let i = 0; i < n; i++) { + const next = (i + 1) % n + // 인접한 두 edge가 같은 방향이면 겹치는 선 + if (isSameDirection(edges[i].angle, edges[next].angle)) { + // 짧은 쪽을 제거 대상으로 표시 + if (edges[i].length <= edges[next].length) { + removeIndices.add(i) + } else { + removeIndices.add(next) + } + } + } + + if (removeIndices.size === 0) return { vertices, lines } + + // 폴리곤이 3개 미만이 되지 않도록 보호 + if (n - removeIndices.size < 3) return { vertices, lines } + + const cleanedVertices = [] + const cleanedLines = [] + + for (let i = 0; i < n; i++) { + if (removeIndices.has(i)) continue + cleanedVertices.push(vertices[i]) + cleanedLines.push(lines[i % lines.length]) + } + + return { vertices: cleanedVertices, lines: cleanedLines } + } + function createMarginPolygon(polygon, lines, arcSegments = 0) { const offsetEdges = [] polygon.edges.forEach((edge, i) => { - /* const offset = - lines[i % lines.length].attributes.offset === undefined || lines[i % lines.length].attributes.offset === 0 - ? 0.1 - : lines[i % lines.length].attributes.offset*/ const offset = lines[i % lines.length].attributes.offset const dx = edge.outwardNormal.x * offset const dy = edge.outwardNormal.y * offset @@ -1718,10 +1785,6 @@ export function useMode() { const offsetEdges = [] polygon.edges.forEach((edge, i) => { - /*const offset = - lines[i % lines.length].attributes.offset === undefined || lines[i % lines.length].attributes.offset === 0 - ? 0.1 - : lines[i % lines.length].attributes.offset*/ const offset = lines[i % lines.length].attributes.offset const dx = edge.inwardNormal.x * offset @@ -5774,5 +5837,6 @@ export function useMode() { createRoofPolygon, createMarginPolygon, createPaddingPolygon, + removeShortEdges, } } diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 4937a0ed..bf4c87cb 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -255,6 +255,40 @@ function createPaddingPolygon(polygon, offset, arcSegments = 0) { * @param {Array} vertices - [{x, y}, ...] 형태의 점 배열 * @returns {Array} 정리된 점 배열 */ +/** + * offset 폴리곤을 원본 폴리곤 경계 안으로 클리핑한다. + * 둔각 꼭짓점에서 offset 교차점이 원본 바깥으로 튀어나가는 문제를 해결한다. + * @param {Array} offsetVertices - offset된 점 배열 [{x, y}, ...] + * @param {Array} originalVertices - 원본 폴리곤 점 배열 [{x, y}, ...] + * @returns {Array} 클리핑된 점 배열 + */ +export function clipOffsetToOriginal(offsetVertices, originalVertices) { + if (!offsetVertices || offsetVertices.length < 3) return offsetVertices + if (!originalVertices || originalVertices.length < 3) return offsetVertices + + try { + const offsetCoords = offsetVertices.map((p) => [p.x, p.y]) + offsetCoords.push([offsetVertices[0].x, offsetVertices[0].y]) + + const origCoords = originalVertices.map((p) => [p.x, p.y]) + origCoords.push([originalVertices[0].x, originalVertices[0].y]) + + const offsetPoly = turf.polygon([offsetCoords]) + const origPoly = turf.polygon([origCoords]) + + const clipped = turf.intersect(turf.featureCollection([offsetPoly, origPoly])) + + if (clipped) { + const clippedCoords = clipped.geometry.coordinates[0] + return clippedCoords.slice(0, -1).map((c) => ({ x: c[0], y: c[1] })) + } + } catch (e) { + console.warn('Failed to clip offset polygon to original:', e) + } + + return offsetVertices +} + export function cleanSelfIntersectingPolygon(vertices) { if (!vertices || vertices.length < 3) return vertices