From d51a07258e0843ce5067a6fe5ff5ad9413e2ddf2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 22 Aug 2026 09:14:22 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EA=B4=80=20=EC=B6=95=EC=9D=84=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20=EB=B2=BD=20=EC=9E=90=EB=A6=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=AC=EC=9C=A0=EB=8F=84=20=E2=80=94=20=EB=B2=BD=20=EB=B0=94?= =?UTF-8?q?=EB=8B=A5=20=EC=95=84=EB=9E=98=20=EC=82=90=EC=A7=90=20=ED=95=B4?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관 하단선은 관 시작점과 유출 벽 하단선 중점(invert)을 잇는 직선으로 고정 — 정수 맞춤이 벽을 옮길 때마다 축·법선 재유도(deriveAxis). 종전엔 옛 축을 그대로 늘려 가파른 지반(13+15.7)에서 관이 벽 바닥 아래로 삐져나갔다. - 관 시작 기준도 유입 노견점이 아니라 실제 관 시작점(집수정 내공 벽) 으로 통일. - 끝단면 교점을 마감면 선분 안으로 클램프(clampToFace) — 연장선 교차로 벽 밖에 꼭짓점이 생기는 것 차단. - 검증: 13+15.7 SVG 실좌표(관 꼭짓점 385→371, 벽 하단 379 위), 4+4.3 회귀, dz 스윕 24건 이탈 0건. Co-Authored-By: Claude Opus 5 (1M context) --- .../B06_Section_UI_Cross_Culvert_Geom.ts | 76 ++++++++++++++----- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index 81912fd7..3f0a736f 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -491,32 +491,61 @@ export function computeCulvertLayout( REVET_THICKNESS_M, wallVertical.outlet, ); - // 유출 벽이 사면 끝보다 안쪽으로 당겨졌으면 **관 끝도 그 자리에 맞춘다** — 관만 사면 - // 끝까지 남으면 벽과 떨어져 마감면 교차가 실패한다(2026-08-20 사용자 ② 원인). - if (outletWall && Math.abs(outletWallAnchor.offset - outletAnchor.offset) > 0.05) { - const runW = outletWallAnchor.offset - inlet.offset; - const riseW = outletWallAnchor.elevation - inlet.elevation; + // ── 관 축 확정. 관 하단선은 **관 시작점(집수정 내공 벽 또는 유입 벽 자리)과 + // 유출 벽 하단선 중점(invert)을 잇는 직선**이다 — 벽이 어디로 가든 관은 그 벽 + // 바닥을 지난다(2026-08-22 사용자: 기슭막이는 관을 감싸는 구조물). 종전에는 정수 + // 맞춤이 벽을 옮긴 뒤에도 옛 축을 그대로 써서, 가파른 지반에서 관이 벽 바닥 + // 아래로 삐져나갔다(13+15.7 사용자 지적). + const pipeStart = basinPipeEnd ?? inlet; + // 유출 벽이 사면 끝과 다른 자리면 관 끝도 그 자리 기준으로 맞춘다(올림 연장분은 + // 유출 쪽 — 2026-08-20 확정). 관 끝은 벽 하단선 중점을 지나 정수 길이까지 나간다. + if (outletWall) { + const runW = outletWallAnchor.offset - pipeStart.offset; + const riseW = outletWallAnchor.elevation - pipeStart.elevation; const lenW = Math.hypot(runW, riseW); if (lenW > 0.5) { const scaleW = Math.ceil(lenW - 1e-6) / lenW; - outlet.offset = inlet.offset + runW * scaleW; - outlet.elevation = inlet.elevation + riseW * scaleW; + outlet.offset = pipeStart.offset + runW * scaleW; + outlet.elevation = pipeStart.elevation + riseW * scaleW; lengthM = Math.ceil(lenW - 1e-6); } } // ── 관 단면 꼭짓점 — 끝단면을 구조물 계류측 변에 맞춰 자른다(2026-08-20 사용자 ③). // 보호공 시작점이 **관 하단 꼭짓점**을 그대로 써야 하므로 여기서 확정한다. - const pipeStart = basinPipeEnd ?? inlet; - const runP = outlet.offset - pipeStart.offset; - const riseP = outlet.elevation - pipeStart.elevation; - const axisLength = Math.hypot(runP, riseP) || 1; - const axis: OffsetPoint = { offset: runP / axisLength, elevation: riseP / axisLength }; - let normal: OffsetPoint = { offset: -axis.elevation, elevation: axis.offset }; - if (normal.elevation < 0) normal = { offset: -normal.offset, elevation: -normal.elevation }; - const topAnchor: OffsetPoint = { - offset: pipeStart.offset + normal.offset * diameter, - elevation: pipeStart.elevation + normal.elevation * diameter, + // 축·법선은 정수 맞춤이 벽(=관 끝)을 옮길 때마다 다시 유도한다. + let axis: OffsetPoint = { offset: 1, elevation: 0 }; + let normal: OffsetPoint = { offset: 0, elevation: 1 }; + let topAnchor: OffsetPoint = pipeStart; + const deriveAxis = (): void => { + const runP = outlet.offset - pipeStart.offset; + const riseP = outlet.elevation - pipeStart.elevation; + const axisLength = Math.hypot(runP, riseP) || 1; + axis = { offset: runP / axisLength, elevation: riseP / axisLength }; + normal = { offset: -axis.elevation, elevation: axis.offset }; + if (normal.elevation < 0) normal = { offset: -normal.offset, elevation: -normal.elevation }; + topAnchor = { + offset: pipeStart.offset + normal.offset * diameter, + elevation: pipeStart.elevation + normal.elevation * diameter, + }; + }; + deriveAxis(); + /** 교점을 마감면 **선분 안**으로 자른다 — 선분 밖(벽 하단 아래 등)으로 새지 않게. */ + const clampToFace = ( + point: OffsetPoint, + face: { base: OffsetPoint; direction: { offset: number; elevation: number } }, + ): OffsetPoint => { + const dot = + (point.offset - face.base.offset) * face.direction.offset + + (point.elevation - face.base.elevation) * face.direction.elevation; + const len2 = + face.direction.offset * face.direction.offset + + face.direction.elevation * face.direction.elevation; + const t = len2 > 1e-12 ? Math.min(Math.max(dot / len2, 0), 1) : 0; + return { + offset: face.base.offset + face.direction.offset * t, + elevation: face.base.elevation + face.direction.elevation * t, + }; }; const endCorners = (role: "inlet" | "outlet", endPoint: OffsetPoint): PipeEnd => { const fallback: PipeEnd = { @@ -536,7 +565,8 @@ export function computeCulvertLayout( STRAY_LIMIT_M || Math.hypot(top.offset - fallback.top.offset, top.elevation - fallback.top.elevation) > STRAY_LIMIT_M; - return strayed ? fallback : { bottom, top }; + if (strayed) return fallback; + return { bottom: clampToFace(bottom, face), top: clampToFace(top, face) }; }; let pipeCorners = { inlet: endCorners("inlet", pipeStart), @@ -565,8 +595,14 @@ export function computeCulvertLayout( null, outletThickness, ); - outlet.offset = pipeStart.offset + axis.offset * target; - outlet.elevation = pipeStart.elevation + axis.elevation * target; + // 관 하단선은 옮겨진 벽의 **하단선 중점을 지나야** 한다 — 옛 축을 그대로 늘리면 + // 가파른 지반에서 관이 벽 바닥 아래로 삐져나간다(2026-08-22 13+15.7 사용자 지적). + const runW = outletWallAnchor.offset - pipeStart.offset; + const riseW = outletWallAnchor.elevation - pipeStart.elevation; + const lenW = Math.hypot(runW, riseW) || 1; + outlet.offset = pipeStart.offset + (runW / lenW) * target; + outlet.elevation = pipeStart.elevation + (riseW / lenW) * target; + deriveAxis(); pipeCorners = { inlet: endCorners("inlet", pipeStart), outlet: endCorners("outlet", outlet),