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),