diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index bd27f6d19..a993a1ef1 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -214,7 +214,7 @@ def compute_cross_design( 높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로 횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자). spoil_fill: 이 측점에 선 유용토운반작업장(구 사토장) — `{"side", "width_m", "slope_ratio_n"}`. - shoulder_extend_left_m · shoulder_extend_right_m: 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · + shoulder_extend_left_m · shoulder_extend_right_m: 설계선 쉬프트 = 쪽마다 사면 이동(m · ≥ 0 · 139-6) — 사용자 값 · 저장분은 `stored_shoulder_extend`. TS 짝 `shoulderExtendLeftM` 등. 폭은 **노면 끝**(노견이 시작하는 자리)에서 재고, 그 바깥 성토는 **노선 몫이 아니라 사토장 몫**이라 `fill_area_m2` 에서 뺀다(2026-09-09 확정 ㉠ — 두 번 세지 않기). @@ -444,8 +444,13 @@ def compute_cross_design( "carriageway_width_m": round(geometry.half_road_left + geometry.half_road_right, 4), # 규격 폭과 확폭을 따로 남긴다 — 횡단도 라벨·수량 산출이 둘을 나눠 쓴다. "carriageway_standard_width_m": round(group["road_width_m"], 4), - "widening_left_m": round(geometry.half_road_left - geometry.half_road, 4), - "widening_right_m": round(geometry.half_road_right - geometry.half_road, 4), + # 확폭 = 곡선부 몫만 — 사면 이동(151-2)이 넓힌 차도는 뺀다(TS 짝 같음). + "widening_left_m": round( + geometry.half_road_left - geometry.half_road - geometry.shoulder_extend_left, 4 + ), + "widening_right_m": round( + geometry.half_road_right - geometry.half_road - geometry.shoulder_extend_right, 4 + ), "cross_slope_pct": round(cross_slope_pct, 4), "ditch": ditch_spec, # 결과 — **실제로 섰나**. 읽는 쪽 여섯이 이 뜻으로 쓴다. diff --git a/B06_Section/B06_Section_Engine_Design_Geometry.py b/B06_Section/B06_Section_Engine_Design_Geometry.py index 8259639b2..9f900def6 100644 --- a/B06_Section/B06_Section_Engine_Design_Geometry.py +++ b/B06_Section/B06_Section_Engine_Design_Geometry.py @@ -140,10 +140,13 @@ class _SectionGeometry: #: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. self.ditch_choice = choice - # 설계선 쉬프트 = 쪽마다 노견 확장(≥ 0 · 139-6) — 단면유형 · 측구 판정은 **옮기기 전** - # 노견에서(위) · 노체 · 측구 · 사면이 같이 바깥으로 · 차도는 그대로. ⚠ TS 짝 `SectionGeometry`. + # 설계선 쉬프트 = 쪽마다 사면 이동(≥ 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 diff --git a/B06_Section/B06_Section_UI_Cross_Design_Line.ts b/B06_Section/B06_Section_UI_Cross_Design_Line.ts index cc9dbfae3..16ab63328 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Line.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Line.ts @@ -235,6 +235,8 @@ export function appendCrossDesignOverlay( // 노폭 라벨(2026-09-06 사용자 지시) — 확폭이 걸린 측점인지 눈으로 바로 알게 한다. // 확폭이 없으면 규격 폭만, 있으면 「4.5m (규격 3.0 + 확폭 1.5)」로 적는다. const widened = (design.widening_left_m ?? 0) + (design.widening_right_m ?? 0); + // 사면 이동(151-2) — 노견째 바깥으로 옮겨 늘어난 차도 몫. + const moved = (design.shoulder_extend_left_m ?? 0) + (design.shoulder_extend_right_m ?? 0); const standardWidth = design.carriageway_standard_width_m; const shoulderLeft = roadEdges ? roadEdges.left.offset_m - edges.left.offset_m : null; const shoulderRight = roadEdges ? edges.right.offset_m - roadEdges.right.offset_m : null; @@ -245,9 +247,13 @@ export function appendCrossDesignOverlay( ); label.setAttribute("y", String(labelAt?.y ?? toDisplayY(edges.left.elevation_m) - 6)); label.setAttribute("class", "b06-chart__carriageway-label"); + const extras = [ + widened > 0.001 ? ` + 확폭 ${widened.toFixed(2)}` : "", + moved > 0.001 ? ` + 이동 ${moved.toFixed(2)}` : "", + ].join(""); const widthText = - widened > 0.001 && typeof standardWidth === "number" - ? `노폭 ${design.carriageway_width_m.toFixed(2)}m (규격 ${standardWidth.toFixed(2)} + 확폭 ${widened.toFixed(2)})` + extras && typeof standardWidth === "number" + ? `노폭 ${design.carriageway_width_m.toFixed(2)}m (규격 ${standardWidth.toFixed(2)}${extras})` : `노폭 ${design.carriageway_width_m.toFixed(2)}m`; // 노견은 좌우가 같으면 한 번만 적는다 — 라벨이 길어지면 옆 측점 라벨과 겹친다. label.textContent = diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index a7a24ab93..d7a3a8910 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -113,7 +113,7 @@ export interface CrossDesignOptions { * 짝: 파이썬 `compute_cross_design(spoil_fill=…)`. */ spoilFill?: { side: "left" | "right"; widthM: number; slopeRatioN?: number | null } | null; - /** 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6). 짝: 파이썬 `shoulder_extend_*_m`. */ + /** 설계선 쉬프트 = 쪽마다 사면 이동(m · ≥ 0 · 139-6 · 151-2 노견째). 짝: 파이썬 `shoulder_extend_*_m`. */ shoulderExtendLeftM?: number; shoulderExtendRightM?: number; } @@ -518,8 +518,13 @@ export function computeCrossDesign( carriageway_width_m: round4(geometry.halfRoadLeft + geometry.halfRoadRight), // 규격 폭과 확폭을 따로 남긴다 — 횡단도 라벨·수량 산출이 둘을 나눠 쓴다(짝: 파이썬). carriageway_standard_width_m: round4(group.road_width_m), - widening_left_m: round4(geometry.halfRoadLeft - geometry.halfRoad), - widening_right_m: round4(geometry.halfRoadRight - geometry.halfRoad), + // 확폭 = 곡선부 몫만 — 사면 이동(151-2)이 넓힌 차도는 뺀다(짝: 파이썬). + widening_left_m: round4( + geometry.halfRoadLeft - geometry.halfRoad - geometry.shoulderExtendLeft, + ), + widening_right_m: round4( + geometry.halfRoadRight - geometry.halfRoad - geometry.shoulderExtendRight, + ), cross_slope_pct: round4(crossSlopePct), ditch: ditchSpec, ditch_enabled: geometry.hasDitch, diff --git a/common_util/common_util_cross_design_geometry.ts b/common_util/common_util_cross_design_geometry.ts index 3f38877d7..d3dbdf042 100644 --- a/common_util/common_util_cross_design_geometry.ts +++ b/common_util/common_util_cross_design_geometry.ts @@ -108,7 +108,7 @@ export class SectionGeometry { hasDitch: boolean; /** 사용자가 정한 선택(`null` = 자동). 결과(`hasDitch`)와 다른 값이다. */ ditchChoice: boolean | null; - /** 노견 확장(m · 139-6) — 쪽마다. */ + /** 사면 이동(m · 139-6 · 151-2) — 쪽마다 · 차도 반폭에 들어 있음. */ shoulderExtendLeft = 0; shoulderExtendRight = 0; ditchPoints: Array<[number, number]> = []; @@ -210,10 +210,12 @@ export class SectionGeometry { /** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */ this.ditchChoice = choice; - // 설계선 쉬프트 = 쪽마다 노견 확장(≥ 0 · 139-6) — 단면유형 · 측구 판정은 **옮기기 전** 노견에서 - // (위) · 노체 · 측구 · 사면이 같이 바깥으로 · 차도는 그대로. ⚠ 파이썬 짝 `_SectionGeometry`. + // 설계선 쉬프트 = 쪽마다 사면 이동(≥ 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; diff --git a/resources/tester/test_b06_cross_design_mirror.py b/resources/tester/test_b06_cross_design_mirror.py index 4f46752ee..151858314 100644 --- a/resources/tester/test_b06_cross_design_mirror.py +++ b/resources/tester/test_b06_cross_design_mirror.py @@ -204,6 +204,20 @@ CASES = [ "shoulder_extend_left_m": 2.5, }, }, + { + # 151-2 — 곡선부 확폭과 사면 이동이 같은 쪽에 겹침(확폭 출력엔 이동 몫 안 섞임) + "name": "확폭 좌 + 사면 이동 좌 0.4", + "samples": _ground(0.28), + "design_elevation_m": 100.3, + "options": { + "ground_type": "soil", + "section_mode": "left_cut", + "ditch_side": "left", + "plan_radius_m": 18.0, + "curve_outer_side": "left", + "shoulder_extend_left_m": 0.4, + }, + }, ] _TS_RUNNER = """ @@ -381,3 +395,32 @@ def test_시험이_빈_결과로_통과하지_않는다() -> None: assert sum(1 for item in results if item["fill_area_m2"] > 0) >= 1 assert any(item.get("widening_left_m") or item.get("widening_right_m") for item in results) assert any(item["cut_rock_area_m2"] > 0 for item in results) + + +def test_사면_이동은_노견째_평행이동_늘어난_폭은_차도() -> None: + """151-2 — 노견 폭 그대로 · 차도 끝이 이동량만큼 바깥 · 확폭 출력엔 이동 몫 안 섞임.""" + by_name = dict(zip((case["name"] for case in CASES), _python_results(), strict=True)) + base = compute_cross_design( + _ground(0.35), + 100.4, + standard=STANDARD_CROSS_SECTION, + ground_type="soil", + section_mode="left_cut", + ditch_side="left", + ) + moved = by_name["노견 확장 좌 0.7 · 우 1.2"] + for side, sign, extend in (("left", 1, 0.7), ("right", -1, 1.2)): + road = moved["road_edges"][side]["offset_m"] + lane = moved["carriageway_edges"][side]["offset_m"] + base_shoulder = ( + base["road_edges"][side]["offset_m"] - base["carriageway_edges"][side]["offset_m"] + ) + assert abs((road - lane) - base_shoulder) < 1e-9 # 노견 폭 고정 + assert abs(lane - (base["carriageway_edges"][side]["offset_m"] + sign * extend)) < 1e-9 + assert abs(moved["carriageway_width_m"] - (base["carriageway_width_m"] + 1.9)) < 1e-9 + assert moved["widening_left_m"] == 0.0 and moved["widening_right_m"] == 0.0 + + curve = by_name["곡선부 확폭 — 바깥쪽 좌"] + 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