From 38acdd02edc87907d4f4ed438f2d2df983f01730 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 21 Aug 2026 17:56:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EA=B8=B0=EC=8A=AD=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=EC=9E=90=EB=A6=AC=C2=B7=ED=95=98=EB=8B=A8=EC=84=A0?= =?UTF-8?q?=20=EA=B7=9C=EC=B9=99=20=EC=9E=AC=EC=A0=95=EB=A6=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-08-21 사용자 확정: - 기본 자리 = **성토 사면이 원지반과 만나는 지점**(사면 끝). 사면이 5m를 넘으면 기슭막이가 사면을 5m에서 끊는다 — 5m는 자리 한계가 아니라 "5m 이상이면 기슭막이 의무"라는 규정(성토_비탈면.md §2)의 표현이다. 물매 1:1.2~2.0으로 자리를 풀던 방식은 걷어냈다(자유도 축소였다). - 벽 하단선 = **원지반을 수직 0.5m 내린 선**. 수평 바닥이라 사면에서 앞 모서리가 뜨던 문제가 사라진다(배면·전면 모두 근입 0.500 확인). - 자리 기준(이동 중심) = **하단선 중점**. 배면 기준이면 벽이 기울 때 체감 위치가 어긋난다. - 관 유출단 invert = 그 자리 원지반 → 관 하단 경사가 지반과 만난다. - structureRequired = 사면길이 5m 이상(의무 발생), 종전의 "받을 수 없는 자리" 판정 대체. 그리기도 기운 하단선에 맞췄다 — 이음선 바닥·돌 해칭이 하단선 위에서 잡힌다. 검증: 두 측점 이동 10케이스 근입 배면·전면 모두 0.500, 하단선 중점이 원지반과 일치 (관 수평 클램프 2건 제외), 계획고 스윕 24케이스 문제 0. tsc 통과, pytest 148 pass. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Cross_Culvert.ts | 28 ++-- .../B06_Section_UI_Cross_Culvert_Geom.ts | 135 +++++++----------- .../B06_Section_UI_Cross_Culvert_Solve.ts | 84 +---------- .../B06_Section_UI_Cross_Culvert_Types.ts | 3 + 4 files changed, 81 insertions(+), 169 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Culvert.ts b/B06_Section/B06_Section_UI_Cross_Culvert.ts index 96045660..77d29334 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert.ts @@ -15,7 +15,6 @@ import { REVET_EMBED_DEPTH_M, REVET_LEAN_RATIO, REVET_THICKNESS_M, - REVET_TRAP_TOP_M, pipeWallThicknessM, revetHeightLimit, } from "./B06_Section_UI_Cross_Culvert_Geom"; @@ -123,10 +122,19 @@ export function appendCulvertOverlay( // 평행사변형 띠와 사다리꼴 사이 대각 이음선(내부 경계 — 사용자 스케치의 가운데 선): // 상단 변 중간점(배면+0.45)에서 전면과 나란히 바닥으로 내려온다. const joint = document.createElementNS(SVG_NS, "line"); - joint.setAttribute("x1", String(x(wall.topBack.offset + wall.outward * REVET_TRAP_TOP_M))); - joint.setAttribute("y1", String(toDisplayY(wall.topBack.elevation))); - joint.setAttribute("x2", String(x(wall.outerOffset - wall.outward * REVET_THICKNESS_M))); - joint.setAttribute("y2", String(toDisplayY(wall.base - REVET_EMBED_DEPTH_M))); + // 하단선이 원지반을 따라 기울어 있으므로 이음선 바닥도 하단선 위에서 잡는다. + const jointBaseOffset = wall.outerOffset - wall.outward * REVET_THICKNESS_M; + const bottomSpan = wall.bottomFront.offset - wall.bottomBack.offset; + const bottomAtOffset = (offset: number): number => + Math.abs(bottomSpan) > 1e-9 + ? wall.bottomBack.elevation + + (wall.bottomFront.elevation - wall.bottomBack.elevation) * + ((offset - wall.bottomBack.offset) / bottomSpan) + : wall.bottomBack.elevation; + joint.setAttribute("x1", String(x(wall.topJoint.offset))); + joint.setAttribute("y1", String(toDisplayY(wall.topJoint.elevation))); + joint.setAttribute("x2", String(x(jointBaseOffset))); + joint.setAttribute("y2", String(toDisplayY(bottomAtOffset(jointBaseOffset)))); joint.setAttribute("class", "b06-chart__culvert-stone"); layer.append(joint); // 돌 해칭 — 큰 돌이 한 줄로 쌓인 **계류측 기운 띠**(전면 평행사변형)를 따라 쌓는다. @@ -148,14 +156,14 @@ export function appendCulvertOverlay( stoneGroup.setAttribute("clip-path", `url(#${clipId})`); layer.append(clip, stoneGroup); - const embedBase = wall.base - REVET_EMBED_DEPTH_M; - const stoneSpan = wall.height + REVET_EMBED_DEPTH_M; + const centerBaseOffset = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2); + const embedBase = bottomAtOffset(centerBaseOffset); + const stoneSpan = Math.max(0.3, wall.topBack.elevation - embedBase); const stoneCount = Math.max(2, Math.round(stoneSpan / 0.45)); const stoneHeight = stoneSpan / stoneCount; // 돌 중심선 = 전면 기운 띠(이음선~전면)의 중심선. 바닥은 근입 바닥까지 연장한다. - const centerBase = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2); - const centerTop = - wall.topBack.offset + wall.outward * (REVET_TRAP_TOP_M + REVET_THICKNESS_M / 2); + const centerBase = centerBaseOffset; + const centerTop = wall.topJoint.offset + wall.outward * (REVET_THICKNESS_M / 2); // 화면 좌표 기준 기움 각(rect의 세로축을 경사축에 맞춘다). const axisX = x(centerTop) - x(centerBase); const axisY = toDisplayY(wall.topBack.elevation) - toDisplayY(embedBase); diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index 35081935..2b0c9201 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -19,7 +19,6 @@ import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section import { FILL_MIN_RISE_M, FILL_SLOPE_MAX_LENGTH_M, - FILL_STRUCTURE_HEIGHT_M, FILL_SLOPE_RATIO_MAX, FILL_SLOPE_RATIO_MIN, MIN_PIPE_COVER_M, @@ -44,6 +43,7 @@ import type { export * from "./B06_Section_UI_Cross_Culvert_Const"; export * from "./B06_Section_UI_Cross_Culvert_Types"; +import type { FillSlopeSegment } from "./B06_Section_UI_Cross_Culvert_Solve"; import { clampWallOffset, cutLength, @@ -53,10 +53,8 @@ import { designInterpolator, intersect, slopeToeOffset, - solveFillWallOffset, STRAY_LIMIT_M, } from "./B06_Section_UI_Cross_Culvert_Solve"; -import type { FillSlopeSegment, FillWallPlacement } from "./B06_Section_UI_Cross_Culvert_Solve"; /** 배수관 세트 기하 계산. 부족한 입력이면 null — 그리기와 분리(설계선 트림이 먼저 쓴다). */ export function computeCulvertLayout( @@ -120,35 +118,25 @@ export function computeCulvertLayout( Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form)); /** 요청값이 한계에 걸려 잘린 뒤의 **실제** 이동량. 조정창이 이 값을 되받는다. */ const appliedShift = { inlet: 0, outlet: 0 }; - let inletPlacement: FillWallPlacement | null = null; + // 기슭막이 기본 자리 = **성토 사면이 원지반과 만나는 지점**(사면 끝)이고, 그 자리가 + // 하단선 중점이 된다(2026-08-21 사용자 확정). 사면길이 5m는 자리 한계가 아니라 + // "5m 이상이면 기슭막이 의무"라는 뜻이라 여기서 자르지 않는다. + let inletAutoOffset: number | null = null; if (!basinReason && designAt) { - // 훑는 범위는 **성토 사면 끝까지**다 — 그 너머는 받칠 성토가 없다. 성토가 얕아 벽 - // 상단이 노견보다 높은 구간에서는 물매가 성립하지 않는데, 한계를 지반 샘플 끝까지 - // 두면 벽이 몇 m 밖으로 밀려난다(계획고 스윕에서 확인). - inletPlacement = solveFillWallOffset( - inletInfo.edge, - inletInfo.outward, + inletAutoOffset = slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit); + const inletBaseAt = (offset: number): number => + Math.min(groundAt(offset), invertCap(inletInfo.edge)); + const shifted = clampWallOffset( + inletAutoOffset, + inletAutoOffset + inletInfo.outward * (revetShift?.inlet ?? 0), wallHeightFor(culvert.inlet), - (offset) => Math.min(groundAt(offset), invertCap(inletInfo.edge)), - slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit), + inletInfo.edge, + inletBaseAt, + inletInfo.outward, ); - if (inletPlacement) { - // 사용자가 조정창으로 민 만큼 자동 자리에서 더 옮긴다 — 사면 역전 구간은 막는다. - const inletBaseAt = (offset: number): number => - Math.min(groundAt(offset), invertCap(inletInfo.edge)); - const shifted = clampWallOffset( - inletPlacement.offset, - inletPlacement.offset + inletInfo.outward * (revetShift?.inlet ?? 0), - wallHeightFor(culvert.inlet), - inletInfo.edge, - inletBaseAt, - inletInfo.outward, - ); - appliedShift.inlet = - Math.round((shifted - inletPlacement.offset) * inletInfo.outward * 10) / 10; - inlet.offset = shifted; - inlet.elevation = Math.min(groundAt(shifted), invertCap(inletInfo.edge)); - } + appliedShift.inlet = Math.round((shifted - inletAutoOffset) * inletInfo.outward * 10) / 10; + inlet.offset = shifted; + inlet.elevation = inletBaseAt(shifted); } // 유출 목표점 = 유출측 성토사면이 지반과 만나는 사면 끝(경사길이 5m 한계 — 별표2). @@ -361,57 +349,55 @@ export function computeCulvertLayout( // 높이: **관경 + 여유고**를 0.1m 눈금으로 올린 값(2026-08-21 사용자 확정). 관이 // 물매를 두어 좌·우 invert가 달라도 **관 상단 위 여유고는 같아야** 한다. 사면선은 // 높이를 정하지 않고 벽이 설 **자리**만 정한다(`solveFillWallOffset`). - const topJoint = anchor.offset + outward * (thickness / 2); // 형태별 법정·교본 높이 한계(찰 3.0 / 메 2.0 — 돌쌓기.md §1)를 넘지 못한다. const height = Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form)); if (!(height > 0.05)) return null; - const topBack: OffsetPoint = { - offset: anchor.offset, - elevation: anchor.elevation + height, - }; - // 상단 변: 사다리꼴 상단(0.45) + 띠 상단(0.45). 이음선은 상단 변 중간점(topJoint) - // 에서 1:0.3으로 바닥까지 — 사다리꼴(수직 배면, 상단 0.45 → 하단 0.45+0.3H)과 - // 평행사변형 띠(폭 0.45)가 진짜 사다리꼴·평행사변형이 된다. + // **자리 기준 = 하단선 중점**(2026-08-21 사용자 확정). 배면 기준으로 잡으면 벽이 + // 기울 때 체감 위치가 어긋난다. anchor.offset이 하단선 중점, anchor.elevation이 + // 그 자리 원지반(= 관 invert)이다. + const baseWidth = thickness * 1.5 + REVET_LEAN_RATIO * height; + const backOffset = anchor.offset - outward * (baseWidth / 2); + const frontBase = anchor.offset + outward * (baseWidth / 2); + const topJoint = backOffset + outward * (thickness / 2); + const topElevation = anchor.elevation + height; + const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation }; + // 상단 변: 사다리꼴 상단(t/2) + 띠(t). 이음선은 상단 변 중간점에서 1:0.3으로 바닥까지. const topFront = topJoint + outward * thickness; - const jointBase = topJoint + outward * REVET_LEAN_RATIO * height; - const frontBase = jointBase + outward * thickness; - // 근입: 벽 바닥은 원지반 아래 REVET_EMBED_DEPTH_M만큼 묻힌다(흙막이 "본바닥에 - // 기초" 원칙 + 실무 기초콘크리트 H=0.5). 바닥 꼭짓점 2개만 내리고 상단·이음선· - // 관 위치는 그대로 — 관은 여전히 원지반 높이로 지난다. - const embedBase = anchor.elevation - REVET_EMBED_DEPTH_M; + // 근입: 하단선은 **원지반을 수직으로 0.5m 내린 선**이다(2026-08-21 사용자 확정). + // 종전에는 수평 바닥이라 사면에서 앞 모서리가 떴다. + const bottomAt = (offset: number): number => groundAt(offset) - REVET_EMBED_DEPTH_M; + const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomAt(backOffset) }; + const bottomFront: OffsetPoint = { offset: frontBase, elevation: bottomAt(frontBase) }; const wall: WallLayout = { role: spec.role, form: spec.revet_form ?? null, lengthM: spec.revet_length_m ?? null, - backOffset: anchor.offset, + backOffset, outerOffset: frontBase, base: anchor.elevation, height, outward, topBack, - topJoint: { offset: topJoint, elevation: anchor.elevation + height }, - points: [ - { offset: anchor.offset, elevation: embedBase }, - topBack, - { offset: topFront, elevation: anchor.elevation + height }, - { offset: frontBase, elevation: embedBase }, - ], + topJoint: { offset: topJoint, elevation: topElevation }, + bottomBack, + bottomFront, + points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront], }; walls.push(wall); // 성토 사면선은 **이음선 상단점**에서 끊고, 끝단 표고도 그 점에 맞춘다(사용자 ②). if (outward > 0) { trimMax = topJoint; - trimMaxElevation = anchor.elevation + height; + trimMaxElevation = topElevation; } else { trimMin = topJoint; - trimMinElevation = anchor.elevation + height; + trimMinElevation = topElevation; } // 관 끝단 마감면 = 기슭막이의 **계류측 변**(전면)을 그대로 복사(사용자 ③). endFaces[spec.role] = { - base: { offset: frontBase, elevation: embedBase }, + base: bottomFront, direction: { - offset: topFront - frontBase, - elevation: anchor.elevation + height - embedBase, + offset: topFront - bottomFront.offset, + elevation: topElevation - bottomFront.elevation, }, }; return wall; @@ -425,29 +411,20 @@ export function computeCulvertLayout( // (2026-08-21 사용자 지적). 관 끝은 원지반 위라는 원칙대로 지반을 따르면 벽이 // 지반에 앉고 관 물매가 그 자리에 맞춰진다. 역경사는 유입 invert로 클램프. const invertAt = (offset: number): number => Math.min(groundAt(offset), inlet.elevation); + // 유출 벽 기본 자리도 **사면 끝**(= outletAnchorOffset)이고 그 자리가 하단선 중점이다. let outletWallAnchor = outletAnchor; - let outletPlacement: FillWallPlacement | null = null; + const outletAutoOffset = outletAnchorOffset; if (designAt) { - outletPlacement = solveFillWallOffset( - outletInfo.edge, - outletInfo.outward, + const shifted = clampWallOffset( + outletAutoOffset, + outletAutoOffset + outletInfo.outward * (revetShift?.outlet ?? 0), wallHeightFor(culvert.outlet), + outletInfo.edge, invertAt, - outletAnchorOffset, + outletInfo.outward, ); - if (outletPlacement) { - const shifted = clampWallOffset( - outletPlacement.offset, - outletPlacement.offset + outletInfo.outward * (revetShift?.outlet ?? 0), - wallHeightFor(culvert.outlet), - outletInfo.edge, - invertAt, - outletInfo.outward, - ); - appliedShift.outlet = - Math.round((shifted - outletPlacement.offset) * outletInfo.outward * 10) / 10; - outletWallAnchor = { offset: shifted, elevation: invertAt(shifted) }; - } + appliedShift.outlet = Math.round((shifted - outletAutoOffset) * outletInfo.outward * 10) / 10; + outletWallAnchor = { offset: shifted, elevation: invertAt(shifted) }; } let outletWall = buildWall(culvert.outlet, outletWallAnchor, outletInfo.outward, null); // 유출 벽이 사면 끝보다 안쪽으로 당겨졌으면 **관 끝도 그 자리에 맞춘다** — 관만 사면 @@ -660,13 +637,9 @@ export function computeCulvertLayout( ? outletSlope.ratio : (section.design?.fill_slope_ratio ?? FILL_SLOPE_RATIO_MIN), ratioClamped: outletSlope ? outletSlope.clamped : false, - // 벽 자리 풀이가 아예 실패했거나(성토고가 사면 5m 안에 안 들어옴), 성토고 3m 이상 - // 인데 물매가 1:1.2보다 급하면 기슭막이로는 못 받는다 — 옹벽·석축 검토 대상. - structureRequired: - outletPlacement === null || - (outletSlope != null && - outletSlope.fillHeightM >= FILL_STRUCTURE_HEIGHT_M && - outletSlope.ratio < FILL_SLOPE_RATIO_MIN), + // 사면길이 5m 이상 = **기슭막이(구조물) 의무 구간**(성토_비탈면.md §2 — + // 2026-08-21 사용자 정정: 5m는 자리 한계가 아니라 의무 발생 기준이다). + structureRequired: outletSlope != null && outletSlope.lengthM >= FILL_SLOPE_MAX_LENGTH_M, }, pitching: { top: pitchingTop, diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts index bb16575e..39b4e477 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts @@ -10,7 +10,6 @@ import { FILL_SLOPE_MAX_LENGTH_M, FILL_SLOPE_RATIO_MAX, FILL_SLOPE_RATIO_MIN, - FILL_STRUCTURE_HEIGHT_M, REVET_TRAP_TOP_M, } from "./B06_Section_UI_Cross_Culvert_Const"; import type { OffsetPoint, PipeEnd } from "./B06_Section_UI_Cross_Culvert_Types"; @@ -80,7 +79,8 @@ export function intersect( export const STRAY_LIMIT_M = 2.5; /** 노견에서 바깥으로 훑어 설계선(사면)이 지반과 만나는 첫 지점(사면 끝)을 찾는다. - * 사면 경사길이 5m 한계(별표2)에 걸리면 그 지점에서 멈춘다 — 거기가 구조물 자리다. */ + * 5m는 자리 한계가 아니다 — 사면이 5m 이상이면 **기슭막이가 의무**라는 뜻일 뿐이라 + * 여기서 끊지 않는다(2026-08-21 사용자 정정). */ export function slopeToeOffset( designAt: (offset: number) => number, groundAt: (offset: number) => number, @@ -93,6 +93,10 @@ export function slopeToeOffset( let previousDiff = designAt(startOffset) - groundAt(startOffset); let previousElevation = designAt(startOffset); let slopeLength = 0; + // 자리 = **성토선이 지반과 만나는 점**과 **사면길이 5m 지점** 중 안쪽. 사면이 5m를 + // 넘으면 기슭막이가 있어야 한다는 규정(성토_비탈면.md §2)은 곧 기슭막이가 사면을 + // 5m에서 끊어 준다는 뜻이다. 교차점이 5m 안이면 거기가 자연스러운 사면 끝이다. + // 더 밖으로 두고 싶으면 조정창으로 민다(2026-08-21 사용자 확정). for (let i = 1; i <= stepCount; i += 1) { const offset = startOffset + step * i; const elevation = designAt(offset); @@ -107,82 +111,6 @@ export function slopeToeOffset( return limitOffset; } -/** 성토측 구조물 자리 풀이 결과 — 벽 offset과 그 자리에서 성립하는 사면 물매. */ -export interface FillWallPlacement { - offset: number; - /** 사면선이 벽 이음선 상단점을 지나는 물매(1:n). 범위 밖이면 끝값으로 고정된 값. */ - ratio: number; - slopeLengthM: number; - withinLimit: boolean; - ratioClamped: boolean; - /** 그 자리의 성토 전체고(노견 − 벽 상단). 3m 이상이면 벽을 5m 쪽으로 끌어올린다. */ - fillHeightM: number; -} - -/** - * 성토측 기슭막이 자리 풀이 — 노견에서 바깥으로 훑으며 **성토 사면선이 벽 이음선 - * 상단점(평행사변형↔사다리꼴 상단 교차점)을 지나는** 자리를 찾는다. - * - * 종전에는 사면선(백엔드 1:1.2 고정)과의 교차로 벽 **높이**를 정했으나, 높이가 - * 관경+여유고로 고정된 뒤로는 그 방식이 성립하지 않는다. 물매는 고정값이 아니라 - * **1:1.2~2.0 범위**(`성토_비탈면.md` §1)이므로, 벽 자리를 정하면 물매가 따라 정해진다. - * 노견에서 멀어질수록 물매는 급 → 완만으로 단조 증가하므로 **범위에 처음 드는 자리** - * (= 사면이 가장 짧은 1:1.2 쪽)를 고른다. 사면길이 5m(별표2)를 넘기 전까지만 훑는다. - * - * 범위 안에 드는 자리가 없으면(급경사 지형) 5m 한계 직전 자리를 쓰고 물매를 끝값으로 - * 고정한다 — `ratioClamped`로 알린다. - */ -export function solveFillWallOffset( - edge: { offset_m: number; elevation_m: number }, - outward: number, - height: number, - baseAt: (offset: number) => number, - limitOffset: number, -): FillWallPlacement | null { - const span = (limitOffset - edge.offset_m) * outward; - if (!(span > 0)) return null; - const steps = 400; - const inRange: FillWallPlacement[] = []; - let last: FillWallPlacement | null = null; - for (let i = 0; i <= steps; i += 1) { - const offset = edge.offset_m + outward * ((span * i) / steps); - const joint = offset + outward * REVET_TRAP_TOP_M; - const rise = edge.elevation_m - (baseAt(offset) + height); - if (!(rise > 1e-6)) continue; - const run = Math.abs(joint - edge.offset_m); - const slopeLengthM = Math.hypot(run, rise); - // 사면길이 5m를 넘는 자리는 구조물 의무 구간이라 벽 자리 후보가 아니다(별표2). - if (slopeLengthM > FILL_SLOPE_MAX_LENGTH_M) break; - const candidate: FillWallPlacement = { - offset, - ratio: run / rise, - slopeLengthM, - withinLimit: true, - ratioClamped: false, - fillHeightM: rise, - }; - last = candidate; - if (candidate.ratio >= FILL_SLOPE_RATIO_MIN && candidate.ratio <= FILL_SLOPE_RATIO_MAX) { - inRange.push(candidate); - } - } - if (!last) return null; - if (inRange.length) { - // 성토 전체고가 3m 이상이면 1:1.2로도 사면이 5m에 육박한다 — 벽을 **사면길이 5m - // 쪽(가장 바깥 후보)**으로 끌어올린다(2026-08-21 사용자 ①). 낮은 성토는 사면을 - // 짧게 두는 **가장 안쪽 후보**를 쓴다. - return last.fillHeightM >= FILL_STRUCTURE_HEIGHT_M ? inRange[inRange.length - 1] : inRange[0]; - } - // 범위 안에 드는 자리가 없다 = 지형이 1:1.2보다 급하거나 1:2.0보다 완만하다. - // 5m 한계 직전 자리를 쓰고 물매를 가까운 끝값으로 고정한다 — 도면에 경고로 알린다. - return { - ...last, - ratio: last.ratio < FILL_SLOPE_RATIO_MIN ? FILL_SLOPE_RATIO_MIN : FILL_SLOPE_RATIO_MAX, - withinLimit: last.slopeLengthM <= FILL_SLOPE_MAX_LENGTH_M + 1e-6, - ratioClamped: true, - }; -} - /** 노견 → 벽 이음선 상단점 성토 사면 구간(단일 각도)과 그 물매·경고. */ export interface FillSlopeSegment { ratio: number; diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Types.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Types.ts index c7bbb6ca..1087b122 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Types.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Types.ts @@ -40,6 +40,9 @@ export interface WallLayout { topBack: OffsetPoint; /** 이음선 상단점(평행사변형↔사다리꼴 상단 교차점) — 성토 사면선이 여기서 끝난다. */ topJoint: OffsetPoint; + /** 하단선 양 끝 — **원지반을 수직 0.5m 내린 선**이라 사면에서는 기울어 있다. */ + bottomBack: OffsetPoint; + bottomFront: OffsetPoint; } /**