From cc6c734250ecca244d5f6d874a990b12b50eca2d Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 23 Jun 2026 14:22:46 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=84=A0=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EC=A6=88=20=EB=B3=80=EA=B2=BD=20=ED=9B=84=20=EC=A7=80?= =?UTF-8?q?=EB=B6=95=EB=A9=B4=20=ED=95=A0=EB=8B=B9=20=EC=8B=9C=20=ED=95=A0?= =?UTF-8?q?=EB=8B=B9=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/auxiliary/AuxiliarySize.jsx | 7 +++ src/hooks/roofcover/useAuxiliaryDrawing.js | 7 ++- src/util/canvas-util.js | 57 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx index ae8d4164..3bdd9ce2 100644 --- a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx +++ b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx @@ -8,6 +8,7 @@ import { useEffect, useState } from 'react' import Image from 'next/image' import Big from 'big.js' import { calcLineActualSize, calcLinePlaneSize } from '@/util/qpolygon-utils' +import { syncOuterBoundaryTwin } from '@/util/canvas-util' import { normalizeDigits } from '@/util/input-utils' import { useUndoRedo } from '@/hooks/useUndoRedo' import { CalculatorInput } from '@/components/common/input/CalcInput' @@ -126,6 +127,12 @@ export default function AuxiliarySize(props) { degree, ) currentObject.addLengthText() + + // [BOUNDARY-SYNC 2026-06-23] currentObject 가 처마 외곽선(polygon.lines)과 좌표가 같은 "경계 복제본"이면, + // 편집 전 동일 좌표였던 처마선 쌍둥이도 변경 후 좌표로 함께 갱신해 두 경계 표현을 동기화한다. + // 미동기화 시 처마선이 옛 위치에 남아 지붕면 할당 split 에서 경계가 2벌이 되어 바깥 사각형이 가짜 면으로 잡힌다. + // (일반 보조선은 동일 좌표 처마선이 없어 매칭 안 됨 → 영향 없음) + syncOuterBoundaryTwin(canvas, currentObject, { x: x1, y: y1 }, { x: x2, y: y2 }) } canvas.renderAll() } diff --git a/src/hooks/roofcover/useAuxiliaryDrawing.js b/src/hooks/roofcover/useAuxiliaryDrawing.js index 62dab535..fbf077ef 100644 --- a/src/hooks/roofcover/useAuxiliaryDrawing.js +++ b/src/hooks/roofcover/useAuxiliaryDrawing.js @@ -5,7 +5,7 @@ import { useEvent } from '@/hooks/useEvent' import { useMouse } from '@/hooks/useMouse' import { useLine } from '@/hooks/useLine' import { useTempGrid } from '@/hooks/useTempGrid' -import { calculateIntersection, distanceBetweenPoints, findClosestPoint, isPointOnLine } from '@/util/canvas-util' +import { calculateIntersection, distanceBetweenPoints, findClosestPoint, isPointOnLine, translateOuterBoundaryTwin } from '@/util/canvas-util' import { fabric } from 'fabric' import { useAdsorptionPoint } from '@/hooks/useAdsorptionPoint' import { useSwal } from '@/hooks/useSwal' @@ -172,6 +172,11 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { roof.innerLines = roof.innerLines.filter((inner) => inner !== object) }) + // [BOUNDARY-SYNC 2026-06-23] object 가 처마 외곽선(polygon.lines)과 좌표가 같은 "경계 복제본"이면, + // 그 처마선 쌍둥이도 같은 delta(x,y)로 함께 이동시켜 두 경계 표현을 동기화한다(지붕면 할당 가짜 면 방지). + // 일반 보조선은 동일 좌표 처마선이 없어 매칭 안 됨 → 영향 없음. + translateOuterBoundaryTwin(canvas, object, x, y) + canvas.remove(object) canvas.setActiveObject(line) } diff --git a/src/util/canvas-util.js b/src/util/canvas-util.js index 7d364f4f..c97d4017 100644 --- a/src/util/canvas-util.js +++ b/src/util/canvas-util.js @@ -1124,3 +1124,60 @@ export function getTrianglePoints(triangle) { return points.map((point) => fabric.util.transformPoint(point, matrix)) } + +// [BOUNDARY-SYNC 2026-06-23] 지붕 경계는 polygon.lines(처마선)과 polygon.innerLines 안의 경계 복제본 +// 두 군데에 중복 보관된다(정상 시 좌표 동일 → split 중복제거로 합쳐짐). 보조선 사이즈 변경/이동으로 +// 한쪽 경계선만 옮기면 두 표현이 어긋나, 지붕면 할당 split 이 경계를 2벌로 인식 → 바깥 전체 사각형이 +// 가짜 면으로 잡힌다(예: 4면이어야 하는데 5면). 아래 헬퍼로 편집 시 좌표가 동일했던 "쌍둥이" 경계선을 +// 함께 갱신해 동기화한다. (일반 보조선은 동일 좌표 처마선이 없어 매칭 안 됨 → 영향 없음) +const _eqBoundaryPt = (a, b, eps = 1) => Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps + +/** 모든 지붕의 lines + innerLines 중, 끝점이 (p1,p2) 와 같은(순서 무관) 라인을 editedLine 제외하고 반환 */ +function _findBoundaryTwins(canvas, editedLine, p1, p2) { + if (!canvas) return [] + const roofs = canvas.getObjects().filter((o) => o.name === 'roof') + const twins = [] + roofs.forEach((roof) => { + ;[...(roof.lines || []), ...(roof.innerLines || [])].forEach((twin) => { + if (twin === editedLine) return + const t1 = { x: twin.x1, y: twin.y1 } + const t2 = { x: twin.x2, y: twin.y2 } + if ((_eqBoundaryPt(t1, p1) && _eqBoundaryPt(t2, p2)) || (_eqBoundaryPt(t1, p2) && _eqBoundaryPt(t2, p1))) { + twins.push({ twin, p1IsT1: _eqBoundaryPt(t1, p1) }) + } + }) + }) + return twins +} + +/** + * 사이즈 변경 등으로 끝점이 옮겨진 경우: 편집 전 좌표(origP1/origP2)가 같던 처마선 쌍둥이를 + * editedLine 의 현재(편집 후) 좌표로 맞춘다. 끝점 대응을 보존해 매핑한다. + */ +export function syncOuterBoundaryTwin(canvas, editedLine, origP1, origP2) { + if (!canvas || !editedLine) return + const newP1 = { x: editedLine.x1, y: editedLine.y1 } + const newP2 = { x: editedLine.x2, y: editedLine.y2 } + _findBoundaryTwins(canvas, editedLine, origP1, origP2).forEach(({ twin, p1IsT1 }) => { + // twin 의 t1 끝점이 origP1 에 대응하면 t1→newP1, 아니면 t1→newP2 + if (p1IsT1) twin.set({ x1: newP1.x, y1: newP1.y, x2: newP2.x, y2: newP2.y }) + else twin.set({ x1: newP2.x, y1: newP2.y, x2: newP1.x, y2: newP1.y }) + if (typeof twin.setCoords === 'function') twin.setCoords() + twin.attributes = { ...twin.attributes, planeSize: editedLine.attributes?.planeSize, actualSize: editedLine.attributes?.actualSize } + if (typeof twin.addLengthText === 'function') twin.addLengthText() + }) +} + +/** + * 평행 이동(보조선 이동)으로 옮겨진 경우: object 와 좌표가 같던 처마선 쌍둥이를 같은 delta(dx,dy)로 평행 이동한다. + */ +export function translateOuterBoundaryTwin(canvas, object, dx, dy) { + if (!canvas || !object) return + const p1 = { x: object.x1, y: object.y1 } + const p2 = { x: object.x2, y: object.y2 } + _findBoundaryTwins(canvas, object, p1, p2).forEach(({ twin }) => { + twin.set({ x1: twin.x1 + dx, y1: twin.y1 + dy, x2: twin.x2 + dx, y2: twin.y2 + dy }) + if (typeof twin.setCoords === 'function') twin.setCoords() + if (typeof twin.addLengthText === 'function') twin.addLengthText() + }) +}