From d0493287ece48f06e94f62be73203a584e614346 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 15 Jun 2026 15:16:02 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=84=A0=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=A7=80=EB=B6=95=20=EB=82=98?= =?UTF-8?q?=EB=88=84=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 --- src/hooks/usePolygon.js | 143 +++++++++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 47 deletions(-) diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 288caf88..5bab7229 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -4,7 +4,7 @@ import { fabric } from 'fabric' import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, isPointOnLine } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' import { calcLinePlaneSize, equalizeSymmetricHips, isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils' -import { basicSettingState, flowDisplaySelector, corridorDimensionSelector } from '@/store/settingAtom' +import { basicSettingState, corridorDimensionSelector, flowDisplaySelector } from '@/store/settingAtom' import { fontSelector } from '@/store/fontAtom' import { QLine } from '@/components/fabric/QLine' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' @@ -97,30 +97,27 @@ export const usePolygon = () => { // 기존엔 planeSize 만 표시해, 실제치수 모드에서도 이동 후 평면길이(예: 8000)가 나오던 문제. const __isCorridorDimension = corridorDimension?.column === 'corridorDimension' const __display = (__isCorridorDimension ? planeSize : +roofSizeSet === 1 ? actualSize : planeSize) ?? planeSize ?? length - const text = new fabric.Textbox( - Number(__display).toFixed(1).replace(/\.0$/, ''), - { - left: left, - top: top, - fontSize: lengthTextFontOptions.fontSize.value, - minX, - maxX, - minY, - maxY, - parentDirection: line.direction, - parentDegree: degree, - parentId: polygon.id, - planeSize: planeSize ?? length, - actualSize: actualSize ?? length, - editable: false, - selectable: true, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - parent: polygon, - name: 'lengthText', - }, - ) + const text = new fabric.Textbox(Number(__display).toFixed(1).replace(/\.0$/, ''), { + left: left, + top: top, + fontSize: lengthTextFontOptions.fontSize.value, + minX, + maxX, + minY, + maxY, + parentDirection: line.direction, + parentDegree: degree, + parentId: polygon.id, + planeSize: planeSize ?? length, + actualSize: actualSize ?? length, + editable: false, + selectable: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + parent: polygon, + name: 'lengthText', + }) polygon.texts.push(text) canvas.add(text) }) @@ -357,24 +354,27 @@ export const usePolygon = () => { const drawDirectionStringToArrow2 = (polygon, showDirectionText) => { let { direction, surfaceCompass, moduleCompass, arrow } = polygon if (moduleCompass === null || moduleCompass === undefined) { - const textObj = new fabric.Text(`${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : (arrow.roofAngle ?? getDegreeByChon(arrow.pitch))}${pitchText}`, { - fontFamily: flowFontOptions.fontFamily.value, - fontWeight: flowFontOptions.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', - fontStyle: flowFontOptions.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', - fontSize: flowFontOptions.fontSize.value, - fill: flowFontOptions.fontColor.value, - originX: 'center', - originY: 'center', - pitch: arrow.pitch, - roofAngle: arrow.roofAngle, - name: 'flowText', - selectable: false, - left: arrow.stickeyPoint.x, - top: arrow.stickeyPoint.y, - parent: arrow, - parentId: arrow.id, - visible: isFlowDisplay, - }) + const textObj = new fabric.Text( + `${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : (arrow.roofAngle ?? getDegreeByChon(arrow.pitch))}${pitchText}`, + { + fontFamily: flowFontOptions.fontFamily.value, + fontWeight: flowFontOptions.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', + fontStyle: flowFontOptions.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', + fontSize: flowFontOptions.fontSize.value, + fill: flowFontOptions.fontColor.value, + originX: 'center', + originY: 'center', + pitch: arrow.pitch, + roofAngle: arrow.roofAngle, + name: 'flowText', + selectable: false, + left: arrow.stickeyPoint.x, + top: arrow.stickeyPoint.y, + parent: arrow, + parentId: arrow.id, + visible: isFlowDisplay, + }, + ) polygon.canvas.add(textObj) return @@ -753,7 +753,9 @@ export const usePolygon = () => { // [SPLIT-ALLOC-DIAG 2026-06-01] 분할 graph 진입 전 innerLines 전수 dump. // 케라바 패턴 라인(kerabPatternRidge/ExtRidge/Hip/ExtHip)과 원래 ridge(RG-2 등) 가 // type='default' / isStart 미설정으로 graph 시드/유효점 후보에서 제외되는지 추적용. - logger.log(`[SPLIT-ALLOC-DIAG] polygon.id=${polygon.id?.slice(0, 8)} innerLines.visible=${innerLines.length} polygonLines=${polygon.lines?.length ?? 0}`) + logger.log( + `[SPLIT-ALLOC-DIAG] polygon.id=${polygon.id?.slice(0, 8)} innerLines.visible=${innerLines.length} polygonLines=${polygon.lines?.length ?? 0}`, + ) innerLines.forEach((l, i) => { logger.log( ` [SPLIT-ALLOC-DIAG ${i}] lineName=${l.lineName ?? 'none'} name=${l.name} ` + @@ -1503,6 +1505,34 @@ export const usePolygon = () => { return Math.abs(line.startPoint.x - line.endPoint.x) > 2 || Math.abs(line.startPoint.y - line.endPoint.y) > 2 }) + // [SUPERSEDED-EAVES 2026-06-15] 처마선을 바깥으로 옮겨 처마(연·軒)를 만들면, 옮긴 처마와 양끝 연결선 + // (모두 auxiliaryLine)·원래 처마선(eaves)이 닫힌 "띠 사각형"을 이룬다. 이때 원래 처마선이 면분할 + // 입력에 남아 있으면 바깥 면을 [띠 + 사다리꼴]로 갈라, 사다리꼴을 닫는 시작선이 사라져 면이 통째로 + // 누락된다(GETSPLIT-IO 진단: 위 사다리꼴 미생성). 원래 처마선은 이미 면 내부의 가상선이므로 면분할 + // 입력에서 제외 → 띠+사다리꼴이 하나의 면(예: 6각형)으로 병합된다. 띠 패턴이 없으면 아무 영향 없음. + const auxBandLines = allLines.filter((l) => l.name === 'auxiliaryLine') + if (auxBandLines.length > 0) { + const sharesPt = (l, p) => isSamePoint(l.startPoint, p) || isSamePoint(l.endPoint, p) + const otherEnd = (l, p) => (isSamePoint(l.startPoint, p) ? l.endPoint : l.startPoint) + const supersededEaves = allLines.filter((E) => { + if (E.attributes?.type !== 'eaves') return false + const A = E.startPoint + const B = E.endPoint + // A·B 각각에서 바깥으로 뻗는 연결 보조선(한쪽 끝점만 공유) + const CA = auxBandLines.find((c) => c !== E && sharesPt(c, A) && !sharesPt(c, B)) + const CB = auxBandLines.find((c) => c !== E && c !== CA && sharesPt(c, B) && !sharesPt(c, A)) + if (!CA || !CB) return false + // 두 연결선의 바깥 끝점을 잇는 평행 보조선(옮긴 처마)이 있어야 띠가 닫힌다 + const Ao = otherEnd(CA, A) + const Bo = otherEnd(CB, B) + return auxBandLines.some((c) => c !== CA && c !== CB && sharesPt(c, Ao) && sharesPt(c, Bo)) + }) + if (supersededEaves.length > 0) { + logger.log(`[SUPERSEDED-EAVES] 띠로 대체된 원래 처마선 ${supersededEaves.length}개 면분할 입력에서 제외`) + allLines = allLines.filter((l) => !supersededEaves.includes(l)) + } + } + // 나눠서 중복 제거된 roof return let newRoofs = getSplitRoofsPoints(allLines) @@ -1755,9 +1785,12 @@ export const usePolygon = () => { // 2차 매칭 (옵션 3 + α): 대각선이 외벽 중간을 분할한 sub-segment 처리 const _isCollinearWithin = (parent, p) => { - const ax = parent.startPoint.x, ay = parent.startPoint.y - const bx = parent.endPoint.x, by = parent.endPoint.y - const dx = bx - ax, dy = by - ay + const ax = parent.startPoint.x, + ay = parent.startPoint.y + const bx = parent.endPoint.x, + by = parent.endPoint.y + const dx = bx - ax, + dy = by - ay const segLenSq = dx * dx + dy * dy if (segLenSq < 1) return false const cross = (p.x - ax) * dy - (p.y - ay) * dx @@ -2091,6 +2124,22 @@ export const usePolygon = () => { } }) + // [GETSPLIT-IO 2026-06-15] getSplitRoofsPoints 의 정확한 입력(allLines, 좌표스냅·교차분할·중복제거 후)과 + // 출력(roofs 면 polygon)을 debug/debug.log 로 영속화. 면분할 오할당 진단용 — 로컬 전용. + debugCapture.log('GETSPLIT-IO', { + allLines: allLines.map((l) => ({ + name: l.name, + lineName: l.lineName ?? 'none', + type: l.attributes?.type ?? 'none', + isStart: l.attributes?.isStart ?? false, + sx: Math.round(l.startPoint.x * 100) / 100, + sy: Math.round(l.startPoint.y * 100) / 100, + ex: Math.round(l.endPoint.x * 100) / 100, + ey: Math.round(l.endPoint.y * 100) / 100, + })), + roofs: roofs.map((r) => r.map((p) => ({ x: Math.round(p.x * 100) / 100, y: Math.round(p.y * 100) / 100 }))), + }) + return removeDuplicatePolygons( roofs.filter((roof) => roof.length < 100), allLines.some((line) => line.name === 'auxiliaryLine'),