diff --git a/B06_Section/B06_Section_Engine_Design_Geometry.py b/B06_Section/B06_Section_Engine_Design_Geometry.py index e19dffc10..abd91a5f2 100644 --- a/B06_Section/B06_Section_Engine_Design_Geometry.py +++ b/B06_Section/B06_Section_Engine_Design_Geometry.py @@ -103,6 +103,16 @@ class _SectionGeometry: low_left = ground_at(self.left_extent) < ground_at(-self.right_extent) self.slope_per_offset = -slope if low_left else slope + # 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 노견(폭 고정) · 측구 · 사면이 + # 같이 바깥으로 · 늘어난 폭은 차도 · 단면유형 · 측구 판정은 **옮긴 뒤** 노견에서(아래 · + # 370-1 — 마루를 넘으면 그 쪽 사면 기울기로 원지반까지). ⚠ TS 짝 `SectionGeometry`. + self.shoulder_extend_left = max(shoulder_extend_left_m, 0.0) + self.shoulder_extend_right = max(shoulder_extend_right_m, 0.0) + self.half_road_left += self.shoulder_extend_left + self.half_road_right += self.shoulder_extend_right + self.left_extent += self.shoulder_extend_left + self.right_extent += self.shoulder_extend_right + # 단면 유형 자동 판정(D-2): 각 측 절/성토 역할을 노면 끝 지반이 설계면보다 # 높은지(절토)/낮은지(성토)로 결정한다. 좌절/우절/양절/양성이 모두 지형에서 # 자연 도출된다. 사용자 입력 section_mode는 측구 방향(ditch_side) 기본값에만 쓰고 @@ -156,16 +166,6 @@ class _SectionGeometry: if tilted: self.has_ditch = False - # 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전** - # 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. - # ⚠ TS 짝 `SectionGeometry`. - self.shoulder_extend_left = max(shoulder_extend_left_m, 0.0) - self.shoulder_extend_right = max(shoulder_extend_right_m, 0.0) - self.half_road_left += self.shoulder_extend_left - self.half_road_right += self.shoulder_extend_right - self.left_extent += self.shoulder_extend_left - self.right_extent += self.shoulder_extend_right - # 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용). self.ditch_points: list[tuple[float, float]] = [] edge_offset = self.left_extent if ditch_side == "left" else -self.right_extent diff --git a/common_util/common_util_cross_design_geometry.ts b/common_util/common_util_cross_design_geometry.ts index 75358fc4b..59aa1fa94 100644 --- a/common_util/common_util_cross_design_geometry.ts +++ b/common_util/common_util_cross_design_geometry.ts @@ -180,6 +180,16 @@ export class SectionGeometry { this.slopePerOffset = lowLeft ? -slope : slope; } + // 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 노견(폭 고정) · 측구 · 사면이 같이 + // 바깥으로 · 늘어난 폭은 차도 · 단면유형 · 측구 판정은 **옮긴 뒤** 노견에서(아래 · 370-1 — 마루를 + // 넘으면 그 쪽 사면 기울기로 원지반까지). ⚠ 파이썬 짝 `_SectionGeometry`. + this.shoulderExtendLeft = Math.max(params.shoulderExtendLeftM ?? 0, 0); + this.shoulderExtendRight = Math.max(params.shoulderExtendRightM ?? 0, 0); + this.halfRoadLeft += this.shoulderExtendLeft; + this.halfRoadRight += this.shoulderExtendRight; + this.leftExtent += this.shoulderExtendLeft; + this.rightExtent += this.shoulderExtendRight; + // 단면유형 자동 판정(D-2) — 노면 끝 지반이 설계면보다 높으면 절토, 낮으면 성토. const groundAt = params.groundAt; if (groundAt !== null) { @@ -226,15 +236,6 @@ export class SectionGeometry { // 물넘이포장 · 맹암거 자리(286-2 · 288-1) — 측구가 안 섬 · 선택은 그대로(파이썬 짝) if (params.tilted) this.hasDitch = false; - // 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전** - // 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`. - this.shoulderExtendLeft = Math.max(params.shoulderExtendLeftM ?? 0, 0); - this.shoulderExtendRight = Math.max(params.shoulderExtendRightM ?? 0, 0); - this.halfRoadLeft += this.shoulderExtendLeft; - this.halfRoadRight += this.shoulderExtendRight; - this.leftExtent += this.shoulderExtendLeft; - this.rightExtent += this.shoulderExtendRight; - // 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용). const edgeOffset = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent; const outward = params.ditchSide === "left" ? 1 : -1; diff --git a/resources/tester/test_b06_cross_design_mirror.py b/resources/tester/test_b06_cross_design_mirror.py index c3789c971..f0554ff98 100644 --- a/resources/tester/test_b06_cross_design_mirror.py +++ b/resources/tester/test_b06_cross_design_mirror.py @@ -61,6 +61,22 @@ def _dip_ground() -> list[dict]: return samples +def _ridge_ground() -> list[dict]: + """우측 −4 m 까지 노면보다 높은 평지(절토) · 그 너머 급히 내려가는 마루(370-1).""" + return [ + {"offset_m": o, "elevation_m": round(100.3 - 0.6 * max(0.0, -o - 4.0), 4), "valid": True} + for o in (-12.0 + i * 0.5 for i in range(49)) + ] + + +def _rise_ground() -> list[dict]: + """좌측 +3 m 까지 노면보다 낮은 평지(성토) · 그 너머 오르막(370-1).""" + return [ + {"offset_m": o, "elevation_m": round(99.6 + 0.4 * max(0.0, o - 3.0), 4), "valid": True} + for o in (-12.0 + i * 0.5 for i in range(49)) + ] + + # 갈래를 골고루 태운다 — 지반유형 셋 · 단면유형 · 측구 형식 · 포장 · 2단 경사 · 세월교 # 노면 하강 · 곡선부 확폭(바깥쪽 좌/우) · 지반선 결측 구간. CASES = [ @@ -214,8 +230,8 @@ CASES = [ }, }, { - # 노견 끝 근처에서 지반이 설계면을 넘는 자리 — 단면유형 · 측구 판정은 옮기기 전 노견에서 - "name": "노견 확장 — 판정은 옮기기 전 노견", + # 노견 끝 근처에서 지반이 설계면을 넘는 자리 — 단면유형 · 측구 판정은 옮긴 노견에서(370-1) + "name": "노견 확장 — 판정은 옮긴 노견", "samples": _ground(-0.05), "design_elevation_m": 100.0, "options": { @@ -226,6 +242,30 @@ CASES = [ "shoulder_extend_left_m": 2.5, }, }, + { + # 370-1 — 우 절토 마루를 넘겨 옮김 = 그 쪽 성토 1:n 으로 원지반까지(원지반 직선 아님) + "name": "마루 너머 = 성토 1:n", + "samples": _ridge_ground(), + "design_elevation_m": 100.0, + "options": { + "ground_type": "soil", + "section_mode": "right_cut", + "ditch_side": "right", + "shoulder_extend_right_m": 3.0, + }, + }, + { + # 370-1 — 좌 성토를 오르막으로 옮김 = 그 쪽 절토(측구 자동 판정 따름) + "name": "성 → 절 넘어감", + "samples": _rise_ground(), + "design_elevation_m": 100.0, + "options": { + "ground_type": "soil", + "section_mode": "left_cut", + "ditch_side": "left", + "shoulder_extend_left_m": 2.5, + }, + }, { # 151-2 — 곡선부 확폭과 사면 이동이 같은 쪽에 겹침(확폭 출력엔 이동 몫 안 섞임) "name": "확폭 좌 + 사면 이동 좌 0.4", @@ -490,3 +530,33 @@ def test_사면_이동은_노견째_평행이동_늘어난_폭은_차도() -> No both = by_name["확폭 좌 + 사면 이동 좌 0.4"] assert both["widening_left_m"] == curve["widening_left_m"] > 0 assert abs(both["carriageway_width_m"] - (curve["carriageway_width_m"] + 0.4)) < 1e-9 + + +def _outer_slope(result: dict, side: str, skip: float = 0.0) -> float: + """노면 끝에서 `skip`(측구 폭) 바깥 설계선 한 마디의 기울기(dz / 바깥 거리).""" + sign = 1.0 if side == "left" else -1.0 + start_offset = result["road_edges"][side]["offset_m"] + sign * skip + line = sorted(result["design_line"], key=lambda p: sign * p["offset_m"]) + index = next(i for i, p in enumerate(line) if abs(p["offset_m"] - start_offset) < 1e-6) + start, nxt = line[index], line[index + 1] + return (nxt["elevation_m"] - start["elevation_m"]) / abs(nxt["offset_m"] - start["offset_m"]) + + +def test_절성_넘어가면_그_쪽_사면_기울기로_원지반까지() -> None: + """370-1 — 옮긴 노견에서 절/성 판정 · 마루 너머 = 성토 1:n · 성 → 절 = 절토 1:n + 측구 자동.""" + by_name = dict(zip((case["name"] for case in CASES), _python_results(), strict=True)) + soil = STANDARD_CROSS_SECTION["soil"] + ridge = by_name["마루 너머 = 성토 1:n"] + assert ridge["section_mode"] == "left_cut" # 우 = 성토 + assert ridge["ditch_enabled"] is False # 우 측구 자동 판정 따라 빠짐 + assert ridge["fill_area_m2"] > 0 + assert abs(_outer_slope(ridge, "right") + 1.0 / soil["fill_slope_ratio"]) < 1e-3 + rise = by_name["성 → 절 넘어감"] + assert rise["section_mode"] == "left_cut" # 좌 = 절토 + assert rise["ditch_enabled"] is True and rise["ditch_side"] == "left" + assert ( + abs( + _outer_slope(rise, "left", soil["ditch"]["top_width_m"]) - 1.0 / soil["cut_slope_ratio"] + ) + < 1e-3 + )