From 74c190fbb9d3beaa26a85f0f8d4297924146d525 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 1 Jul 2026 10:43:14 +0900 Subject: [PATCH] =?UTF-8?q?[2296]=20=EC=88=98=EB=8F=99=EB=B0=B0=EC=B9=98?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EA=B0=84=20=EC=9D=B4=EA=B2=A9=20?= =?UTF-8?q?=EB=AF=B8=EA=B2=80=EC=A6=9D=20=EC=88=98=EC=A0=95=20=E2=80=94=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=EC=97=90=20isT?= =?UTF-8?q?ooCloseToOtherModules=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 작업내용: - isTooCloseToOtherModules 헬퍼 이식 (useModule.js [2296-2] 이동 수정과 동일 로직, getBoundingRect strokeWidth 팽창 주의 준수) - 수동배치 게이트: 외주이격(setupSurface)+겹침 통과 후 縦/横 모듈 간 이격 미달 시 배치 거부 (module.place.out) - 진단 로그([MODULE-PLACE-DIAG 2026-07-01]) 주석처리 Co-Authored-By: Claude Opus 4 --- src/hooks/module/useModuleBasicSetting.js | 87 ++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index c389f3e8..2b93bd69 100644 --- a/src/hooks/module/useModuleBasicSetting.js +++ b/src/hooks/module/useModuleBasicSetting.js @@ -34,6 +34,7 @@ import { useCircuitTrestle } from '@/hooks/useCirCuitTrestle' import { useMode } from '@/hooks/useMode' import { useTurf } from '@/hooks/common/useTurf' import { useUndoRedo } from '@/hooks/useUndoRedo' +// import { debugCapture } from '@/util/debugCapture' // [MODULE-PLACE-DIAG 2026-07-01] 수동배치 경계 초과 조사용 (재활성 시 주석 해제) export function useModuleBasicSetting(tabNum) { const canvas = useRecoilValue(canvasState) @@ -296,6 +297,44 @@ export function useModuleBasicSetting(tabNum) { } } + // [PLACE-CLEARANCE 2026-07-01] 手動配置 시 モジュール間離隔(縦 moduleIntvlVer / 横 moduleIntvlHor) 미검증 보완. + // 겹침(booleanOverlap)·外周離隔(setupSurface)만 검사해, 인접 모듈에 0간격 밀착 스냅해도 통과하던 버그. + // [2296-2] dfa90812 의 移動 수정(useModule.js:isTooCloseToOtherModules)과 동일 로직 — 진입점만 배치. + // ⚠ 간격은 stroke 를 뺀 실제 정점(getCurrentPoints)으로 잰다. getBoundingRect 는 strokeWidth 팽창 → 정상 격자도 위반 오판. + const isTooCloseToOtherModules = (module, otherModules, surface) => { + if (!otherModules || otherModules.length === 0) return false + const gapVer = Number(surface?.trestleDetail?.moduleIntvlVer ?? 0) / 10 // mm → canvas unit + const gapHor = Number(surface?.trestleDetail?.moduleIntvlHor ?? 0) / 10 + if (gapVer <= 0 && gapHor <= 0) return false + const EPS = 0.05 // 0.5mm — 좌표 반올림 잔차(±0.02) 흡수 + const bboxOf = (o) => { + const pts = o.getCurrentPoints ? o.getCurrentPoints() : null + if (pts && pts.length) { + const xs = pts.map((p) => p.x) + const ys = pts.map((p) => p.y) + return { l: Math.min(...xs), r: Math.max(...xs), t: Math.min(...ys), b: Math.max(...ys) } + } + const r = o.getBoundingRect(true, true) + return { l: r.left, r: r.left + r.width, t: r.top, b: r.top + r.height } + } + const mb = bboxOf(module) + return otherModules.some((o) => { + if (o.id === module.id) return false + const nb = bboxOf(o) + const overlapX = mb.l < nb.r - EPS && nb.l < mb.r - EPS + const overlapY = mb.t < nb.b - EPS && nb.t < mb.b - EPS + if (overlapX) { + const vGap = mb.t >= nb.b ? mb.t - nb.b : nb.t >= mb.b ? nb.t - mb.b : 0 + if (vGap < gapVer - EPS) return true + } + if (overlapY) { + const hGap = mb.l >= nb.r ? mb.l - nb.r : nb.l >= mb.r ? nb.l - mb.r : 0 + if (hGap < gapHor - EPS) return true + } + return false + }) + } + //선택 배치면 배열` let selectedModuleInstSurfaceArray = [] @@ -477,6 +516,25 @@ export function useModuleBasicSetting(tabNum) { }) setupSurface.setViewLengthText(false) + // [MODULE-PLACE-DIAG 2026-07-01] setupSurface 인셋 확인 로그 (재활성 시 주석 해제 + import 복원) + // { + // const _bbox = (pts) => { + // const xs = pts.map((p) => p.x) + // const ys = pts.map((p) => p.y) + // return { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) } + // } + // const _roofPts = roof.getCurrentPoints() + // const _rb = _bbox(_roofPts) + // const _sb = _bbox(offsetPoints) + // debugCapture.log('[MODULE-PLACE-DIAG] setupSurface 생성', { + // roofId: roof.id, roofSizeSet: canvasSetting.roofSizeSet, pitch: roof.roofMaterial?.pitch, direction: roof.direction, + // roofBboxCanvas: { w: +(_rb.maxX - _rb.minX).toFixed(2), h: +(_rb.maxY - _rb.minY).toFixed(2) }, + // setupBboxCanvas: { w: +(_sb.maxX - _sb.minX).toFixed(2), h: +(_sb.maxY - _sb.minY).toFixed(2) }, + // insetTopCanvas: +(_sb.minY - _rb.minY).toFixed(2), insetBottomCanvas: +(_rb.maxY - _sb.maxY).toFixed(2), + // insetLeftCanvas: +(_sb.minX - _rb.minX).toFixed(2), insetRightCanvas: +(_rb.maxX - _sb.maxX).toFixed(2), + // lineOffsets: roof.lines.map((l) => ({ type: l.attributes?.type, offset: l.attributes?.offset })), + // }) + // } canvas.add(setupSurface) //모듈설치면 만들기 //지붕면 선택 금지 @@ -998,13 +1056,35 @@ export function useModuleBasicSetting(tabNum) { if (!isIntersection) return tempModule.setCoords() //좌표 재정렬 + // [MODULE-PLACE-DIAG 2026-07-01] 수동배치 게이트 진단 로그 (재활성 시 주석 해제 + import 복원) + // { + // const _sc = turfPolygon?.geometry?.coordinates?.[0] || [] + // const _mc = tempTurfModule?.geometry?.coordinates?.[0] || [] + // const _bb = (c) => c.length ? { minX: Math.min(...c.map((p) => p[0])), maxX: Math.max(...c.map((p) => p[0])), minY: Math.min(...c.map((p) => p[1])), maxY: Math.max(...c.map((p) => p[1])) } : null + // const _sb = _bb(_sc), _mb = _bb(_mc) + // debugCapture.log('[MODULE-PLACE-DIAG] 배치 게이트', { + // surfaceId: trestlePolygon?.id, + // moduleBbox: _mb && { minX: +_mb.minX.toFixed(2), maxX: +_mb.maxX.toFixed(2), minY: +_mb.minY.toFixed(2), maxY: +_mb.maxY.toFixed(2) }, + // surfaceBbox: _sb && { minX: +_sb.minX.toFixed(2), maxX: +_sb.maxX.toFixed(2), minY: +_sb.minY.toFixed(2), maxY: +_sb.maxY.toFixed(2) }, + // overTopCanvas: _mb && _sb ? +(_sb.minY - _mb.minY).toFixed(2) : null, + // overBottomCanvas: _mb && _sb ? +(_mb.maxY - _sb.maxY).toFixed(2) : null, + // gateSpare0: checkModuleDisjointSurface(tempTurfModule, turfPolygon, 0), + // gateSpare05: checkModuleDisjointSurface(tempTurfModule, turfPolygon, 0.5), + // isTooClose: isTooCloseToOtherModules(tempModule, manualDrawModules, trestlePolygon), + // gapVerReq: Number(trestlePolygon?.trestleDetail?.moduleIntvlVer ?? 0) / 10, + // gapHorReq: Number(trestlePolygon?.trestleDetail?.moduleIntvlHor ?? 0) / 10, + // existingModuleCount: manualDrawModules?.length, + // }) + // } // [EDGE-PLACE-TOLERANCE 2026-06-09] 手動 1枚 배치: 端部에 딱 붙인 모듈이 소수 절단 잔차(±0.02)로 屋根 경계를 살짝 넘으면 // spare=0 판정이 屋根外로 오인 → 「取り付け面からモジュールを取り付けることはできません」 경고. 屋根 절대좌표에 따라 2번째/3번째가 들쭉날쭉. // 이동/삽입/정렬(isOutsideSurface)과 동일한 0.5 허용오차로 잔차 흡수. if (checkModuleDisjointSurface(tempTurfModule, turfPolygon, 0.5)) { //마우스 클릭시 set으로 해당 위치에 셀을 넣음 const isOverlap = manualDrawModules.some((module) => turf.booleanOverlap(tempTurfModule, polygonToTurfPolygon(module, true))) //겹치는지 확인 - if (!isOverlap) { + // [PLACE-CLEARANCE 2026-07-01] 인접 모듈과 モジュール間離隔(縦30/横3 등) 미만이면 배치 거부 + const isTooClose = isTooCloseToOtherModules(tempModule, manualDrawModules, trestlePolygon) + if (!isOverlap && !isTooClose) { canvas?.remove(tempModule) //안겹치면 넣는다 @@ -1036,8 +1116,11 @@ export function useModuleBasicSetting(tabNum) { // } // getModuleStatistics() - } else { + } else if (isOverlap) { swalFire({ text: getMessage('module.place.overlab'), icon: 'warning' }) + } else { + // [PLACE-CLEARANCE 2026-07-01] 間離隔 미달 — 설치면 밖 배치와 동일하게 취급(取り付け不可) + swalFire({ text: getMessage('module.place.out'), icon: 'warning' }) } } else { swalFire({ text: getMessage('module.place.out'), icon: 'warning' })