diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index f2aa42d78..ae3a21e28 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -172,6 +172,8 @@ export const STATE_REGISTRY = { * 예전에는 버튼을 누를 때마다 서버가 계산해 **바로 저장**했다. 조작은 캐시에 쌓고 * [저장]·[확정]에서만 정본으로 나가야 한다(사용자 확정). */ crossdesign: { bucket: "draft", scope: "route" }, + /** 설계선 쉬프트 = 쪽마다 노견 확장 `{측점: {left, right}}`(139-6) — [저장] 때 patch 로. */ + shoulderext: { bucket: "draft", scope: "route" }, /** B08 자재 집계표 셋 사용자 값 `{units, mix, surcharge}`(고친 칸만) — [저장] 때 프로젝트 설정으로(121). */ "b08-materials": { bucket: "draft", scope: "project" }, /** B08 홍수량 계산서 입력 `{측점: 입력}` — [저장] 때 B08 자기 파일로 · B05 연결값과 같은 칸은 null(연동 · 96). */ diff --git a/B05_Profile/B05_Profile_Router_Confirm.py b/B05_Profile/B05_Profile_Router_Confirm.py index 85e6bf6c7..0866d8527 100644 --- a/B05_Profile/B05_Profile_Router_Confirm.py +++ b/B05_Profile/B05_Profile_Router_Confirm.py @@ -90,7 +90,11 @@ async def sync_uphill_overrides_into_designs( 양성(both_fill)은 측구가 없으므로 건드리지 않는다. """ # 지연 import — B06 라우터 모듈 로드는 이 함수가 실제 불릴 때만 필요하다. - from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args + from B06_Section.B06_Section_Engine_Design import ( + compute_cross_design, + curve_widening_args, + stored_shoulder_extend, + ) from B06_Section.B06_Section_Repository import get_cross_section_designs from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs from B06_Section.B06_Section_Router_Design import ( @@ -157,6 +161,7 @@ async def sync_uphill_overrides_into_designs( ditch_choice=design.get("ditch_choice"), surface_drop_m=ford_drop_at(float(chainage), ford_drops), **curve_widening_args(cross_record), + **stored_shoulder_extend(design), # 노견 확장(139-6) ) next_design["status"] = design.get("status", "provisional") next_design["pavement_suggested"] = design.get("pavement_suggested", pavement_suggested) diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 52e447655..ec9143b20 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -571,6 +571,9 @@ export interface CrossSectionPatch { display_half_width_m?: number; /** 측점별 암 절토 경사비(2026-09-07). 0 = 표준값으로 되돌림. */ cut_slope_ratio_user?: number; + /** 설계선 쉬프트 = 쪽마다 노견 확장(m · 139-6) — 0 = 되돌림. */ + shoulder_extend_left_m?: number; + shoulder_extend_right_m?: number; /** 배수관 연장(m) — 구조물 면적과 같은 길로 정본에 실린다(2026-09-08). */ pipe_length_m?: number; /** B08 하상경사 원천(110) — 관 바닥 두 끝 경사 · 박스 두 끝 원지반 경사(%) · 관 길이와 같은 길. */ diff --git a/B06_Section/B06_Section_Cross_Design_Session.ts b/B06_Section/B06_Section_Cross_Design_Session.ts index 5e5f74df3..2f38122b6 100644 --- a/B06_Section/B06_Section_Cross_Design_Session.ts +++ b/B06_Section/B06_Section_Cross_Design_Session.ts @@ -68,3 +68,45 @@ export function crossDesignChoices( } return choices; } + +/* ── 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6) — 카드 버튼 선택과 따로 담음(버튼 선택이 + * 통째로 덮어써도 안 지워지게). [저장]·[확정] 이 patch 로 싣고, 브라우저 재계산이 입력으로 씀. */ +export type ShoulderSide = "left" | "right"; +export type ShoulderExtends = Map>>; + +/** 누가거리(숫자) → 쪽마다 노견 확장(세션에 있는 쪽만). */ +export function readShoulderExtends( + projectId: string | null, + routeId: number | null, +): ShoulderExtends { + const out: ShoulderExtends = new Map(); + if (!projectId || routeId === null) return out; + const raw = readState>>>( + "shoulderext", + projectId, + routeId, + ); + for (const [key, value] of Object.entries(raw ?? {})) { + const chainage = Number(key); + if (Number.isFinite(chainage) && value) out.set(chainage, value); + } + return out; +} + +/** 한 측점 한 쪽 노견 확장을 담는다(0 = 되돌림 · 저장 때 정본의 옛 값을 지움). */ +export function writeShoulderExtend( + projectId: string, + routeId: number, + chainageM: number, + side: ShoulderSide, + valueM: number, +): void { + const all = + readState>>>( + "shoulderext", + projectId, + routeId, + ) ?? {}; + all[keyOf(chainageM)] = { ...(all[keyOf(chainageM)] ?? {}), [side]: Math.max(valueM, 0) }; + writeState("shoulderext", all, projectId, routeId); +} diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 16f1a143b..6bbfaf7d1 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -30,7 +30,7 @@ import { previewCrossDesigns } from "./B06_Section_Api_Fetch"; import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch"; import { bermSpecAt, readBermSpans, readRockBoundarySession } from "./B06_Section_UI_Page_Persist"; import type { BermSpec } from "@util/common_util_cross_berm"; -import { crossDesignChoices } from "./B06_Section_Cross_Design_Session"; +import { crossDesignChoices, readShoulderExtends } from "./B06_Section_Cross_Design_Session"; import { effectiveStandardCross, readRockBoundaryDefault, @@ -87,6 +87,9 @@ export const USER_TOUCHED_KEYS = [ "berm", // 측점별 암 절토 경사(2026-09-07) — 표준을 바꿔도 개별로 고친 측점은 그대로 둔다. "cut_slope_ratio_user", + // 설계선 쉬프트 = 쪽마다 노견 확장(139-6) — 기하 입력이라 재계산에도 넘김 + "shoulder_extend_left_m", + "shoulder_extend_right_m", ] as const; /** 다시 계산해도 살려 두는 값 — 위 사용자 값에 **상태를 나르는 둘**을 더한 것. */ @@ -257,6 +260,13 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { }; const choices = crossDesignChoices(projectId, input.routeId); const choiceAt = (chainageM: number) => choices.get(Math.round(chainageM * 100) / 100); + // 노견 확장(139-6) — 세션 → 저장분 순 · 기하 입력이라 계산에 넣고 결과 칸도 그 값으로 + const shoulderExtends = readShoulderExtends(projectId, input.routeId); + const extendAt = (chainageM: number, design: Record, side: "left" | "right") => { + const session = shoulderExtends.get(Math.round(chainageM * 100) / 100)?.[side]; + const stored = design[`shoulder_extend_${side}_m`]; + return Math.max(session ?? (typeof stored === "number" ? stored : 0), 0); + }; const updated: number[] = []; for (const section of detail.cross_sections) { @@ -274,6 +284,8 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { ? storedOffset : rockDefault; const choice = choiceAt(section.chainage_m); + const extendLeft = extendAt(section.chainage_m, design, "left"); + const extendRight = extendAt(section.chainage_m, design, "right"); let next; try { next = computeCrossDesign( @@ -318,6 +330,8 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { ? section.curve_outer_side : null, curveWideningM: section.curve_widening_m ?? null, + shoulderExtendLeftM: extendLeft, + shoulderExtendRightM: extendRight, // 사토장 — **저장분 폭을 그대로 잇는다**. 폭은 구간 용량에서 서버가 정한 값이라 // 브라우저가 다시 풀지 않는다(다시 풀면 그 측점만 폭이 달라져 작업장 모양이 깨진다). // 이어 붙이지 않으면 계획선을 만질 때마다 사토장이 그림에서 사라진다. @@ -340,6 +354,8 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { next as unknown as NonNullable, previous, ); + section.design.shoulder_extend_left_m = extendLeft; + section.design.shoulder_extend_right_m = extendRight; updated.push(section.chainage_m); } return updated; diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index 33b28829e..bd27f6d19 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -141,6 +141,20 @@ def _ground_interpolator(valid: list[tuple[float, float]]): return ground_at +def stored_shoulder_extend(stored: dict[str, Any] | None) -> dict[str, float]: + """저장분의 설계선 쉬프트(쪽마다 노견 확장 · 139-6) — `compute_cross_design(**...)` 로 넘긴다. + + ⚠ 소단 · 암 절토 경사와 같은 **기하 입력**이다 — 다시 계산한 뒤 키만 베껴 붙이면 설계선은 옛 + 노견으로 나오고 값만 남아 어긋난다. 없거나 음수 = 0. + """ + out: dict[str, float] = {} + for side in ("left", "right"): + value = (stored or {}).get(f"shoulder_extend_{side}_m") + ok = isinstance(value, (int, float)) and not isinstance(value, bool) + out[f"shoulder_extend_{side}_m"] = max(float(value), 0.0) if ok else 0.0 + return out + + def curve_widening_args(section: dict[str, Any] | None) -> dict[str, Any]: """측점 기록에서 곡선부 확폭 입력을 뽑는다 — `compute_cross_design(**...)` 로 넘긴다. @@ -178,6 +192,8 @@ def compute_cross_design( curve_widening_m: float | None = None, berm: BermSpec | None = None, spoil_fill: dict[str, Any] | None = None, + shoulder_extend_left_m: float = 0.0, + shoulder_extend_right_m: float = 0.0, ) -> dict[str, Any]: """측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다. @@ -198,6 +214,8 @@ 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 · + 139-6) — 사용자 값 · 저장분은 `stored_shoulder_extend`. TS 짝 `shoulderExtendLeftM` 등. 폭은 **노면 끝**(노견이 시작하는 자리)에서 재고, 그 바깥 성토는 **노선 몫이 아니라 사토장 몫**이라 `fill_area_m2` 에서 뺀다(2026-09-09 확정 ㉠ — 두 번 세지 않기). `slope_ratio_n` 이 비면 그 측점의 **노선 성토 기울기**를 그대로 쓴다. @@ -279,6 +297,8 @@ def compute_cross_design( widening_left_m=widening_left, widening_right_m=widening_right, berm=berm, + shoulder_extend_left_m=shoulder_extend_left_m, + shoulder_extend_right_m=shoulder_extend_right_m, ) # 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야 @@ -515,6 +535,13 @@ def compute_cross_design( "interval_m": round(float(berm.interval_m), 4), "slope_deg": round(float(berm.slope_deg), 4), } + # 노견 확장(139-6) — 넓힌 쪽만 되싣는다(사용자 값 · 다시 셀 때 입력) + for side, extend in ( + ("left", geometry.shoulder_extend_left), + ("right", geometry.shoulder_extend_right), + ): + if extend > 0: + result[f"shoulder_extend_{side}_m"] = round(extend, 4) if paved: result["pavement_thickness_m"] = round(paved_group["pavement_thickness_m"], 4) # 암 지반은 경계선 오프셋을 echo해 프론트가 세션값 없이도 오버레이·재계산에 쓰게 한다. diff --git a/B06_Section/B06_Section_Engine_Design_Geometry.py b/B06_Section/B06_Section_Engine_Design_Geometry.py index cefdf8c23..8259639b2 100644 --- a/B06_Section/B06_Section_Engine_Design_Geometry.py +++ b/B06_Section/B06_Section_Engine_Design_Geometry.py @@ -59,6 +59,8 @@ class _SectionGeometry: widening_left_m: float = 0.0, widening_right_m: float = 0.0, berm: BermSpec | None = None, + shoulder_extend_left_m: float = 0.0, + shoulder_extend_right_m: float = 0.0, ) -> None: half_road = group["road_width_m"] / 2.0 # 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽). @@ -138,6 +140,13 @@ class _SectionGeometry: #: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. self.ditch_choice = choice + # 설계선 쉬프트 = 쪽마다 노견 확장(≥ 0 · 139-6) — 단면유형 · 측구 판정은 **옮기기 전** + # 노견에서(위) · 노체 · 측구 · 사면이 같이 바깥으로 · 차도는 그대로. ⚠ 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.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/B06_Section/B06_Section_Engine_SampleExtend.py b/B06_Section/B06_Section_Engine_SampleExtend.py index 5cfe32ce1..b3523a2e6 100644 --- a/B06_Section/B06_Section_Engine_SampleExtend.py +++ b/B06_Section/B06_Section_Engine_SampleExtend.py @@ -24,6 +24,7 @@ from B06_Section.B06_Section_Engine_Design import ( _SLOPE_CLOSE_TOLERANCE_M, compute_cross_design, curve_widening_args, + stored_shoulder_extend, ) from common_util.common_util_json import atomic_write_json @@ -156,6 +157,7 @@ def extend_unclosed_sections( surface_drop_m=ford_drop_at(chainage, drops), berm=stored_berm(design), **curve_widening_args(section), + **stored_shoulder_extend(design), # 노견 확장(139-6) ) try: diff --git a/B06_Section/B06_Section_Engine_SpoilFill.py b/B06_Section/B06_Section_Engine_SpoilFill.py index b07d94d27..28b280e5c 100644 --- a/B06_Section/B06_Section_Engine_SpoilFill.py +++ b/B06_Section/B06_Section_Engine_SpoilFill.py @@ -23,7 +23,7 @@ from __future__ import annotations from pathlib import Path from typing import Any -from B06_Section.B06_Section_Engine_Design import compute_cross_design +from B06_Section.B06_Section_Engine_Design import compute_cross_design, stored_shoulder_extend from common_util.common_util_structure_face_role import structure_face_role #: 폭을 좁혀 가는 이분법 반복 수. TS 짝(`solveSpoilWidthM`)과 같은 값이다. @@ -216,6 +216,7 @@ def enforce_spoil_fills( "slope_ratio_n": slope_ratio_n, }, **curve_widening_args(section), + **stored_shoulder_extend(design), # 노견 확장(139-6) ) except (ValueError, KeyError): return None diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index bf1e7531a..3e98b06af 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -20,7 +20,11 @@ from B05_Profile.B05_Profile_Engine_Sections import ( ) from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions from B06_Section.B06_Section_Engine_Culvert import attach_culvert_sets -from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args +from B06_Section.B06_Section_Engine_Design import ( + compute_cross_design, + curve_widening_args, + stored_shoulder_extend, +) from B06_Section.B06_Section_Engine_Structures_Wall import attach_wall_structures from B06_Section.B06_Section_Repository import ( count_cross_sections, @@ -593,6 +597,7 @@ async def compute_cross_section_design( surface_drop_m=ford_drop_at(request.chainage_m, ford_surface_drops(project_root)), berm=stored_berm(stored_design or {}), **curve_widening_args(cross_record), + **stored_shoulder_extend(stored_design or {}), # 노견 확장(139-6) ) # B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다. design["status"] = "provisional" diff --git a/B06_Section/B06_Section_Router_Design.py b/B06_Section/B06_Section_Router_Design.py index 7722522ce..05ef8acb7 100644 --- a/B06_Section/B06_Section_Router_Design.py +++ b/B06_Section/B06_Section_Router_Design.py @@ -9,7 +9,11 @@ from B05_Profile.B05_Profile_Engine_Sections import find_cross_file from B05_Profile.B05_Profile_Structures_Repository import load_structures from B05_Profile.B05_Profile_Structures_Schema import structure_type_map from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets -from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args +from B06_Section.B06_Section_Engine_Design import ( + compute_cross_design, + curve_widening_args, + stored_shoulder_extend, +) from common_util.common_util_cross_berm import ( BERM_DEFAULT_INTERVAL_M, BERM_DEFAULT_SLOPE_DEG, @@ -143,6 +147,9 @@ USER_TOUCHED_KEYS = ( # 측점별 암 절토 경사 — 표준 횡단면 설정을 바꿔도 개별로 고친 측점은 그대로 둔다 # (2026-09-07 사용자 확정: 「사용자가 기본값을 사용하지 않는 값들은 변경되면 안됨」). "cut_slope_ratio_user", + # 설계선 쉬프트 = 쪽마다 노견 확장(139-6) — 기하 입력(`stored_shoulder_extend`) + "shoulder_extend_left_m", + "shoulder_extend_right_m", ) # 노선 [확인] 체인이 옛 노선에서 누가거리로 넘기는 **사용자 입력**(PLAN 74-3) — 계획고 · 면적 · @@ -216,6 +223,7 @@ def enforce_pavement_ranges( surface_drop_m=ford_drop_at(chainage, ford_drops), berm=stored_berm(design), **curve_widening_args(section), + **stored_shoulder_extend(design), # 노견 확장(139-6) ) except (ValueError, KeyError): continue @@ -270,6 +278,7 @@ def enforce_ford_surface_drops( surface_drop_m=wanted, berm=stored_berm(design), **curve_widening_args(section), + **stored_shoulder_extend(design), # 노견 확장(139-6) ) except (ValueError, KeyError): continue @@ -466,6 +475,7 @@ def recompute_designs_for_alignment( surface_drop_m=ford_drop_at(chainage, ford_drops), berm=session_berms.get(key) or stored_berm(stored), **curve_widening_args(section), + **stored_shoulder_extend(stored), # 노견 확장(139-6) ) except (ValueError, KeyError): continue diff --git a/B06_Section/B06_Section_Schema.py b/B06_Section/B06_Section_Schema.py index 3b4d9fdb9..09d6be53f 100644 --- a/B06_Section/B06_Section_Schema.py +++ b/B06_Section/B06_Section_Schema.py @@ -147,6 +147,9 @@ class CrossSectionPatch(BaseModel): # **0 은 「표준값을 씀」**(되돌리기)이라 gt=0 이 아니라 ge=0 이다 — 0 이 와야 # 정본에 남은 옛 사용자 값이 지워진다. cut_slope_ratio_user: float | None = Field(default=None, ge=0) + # 설계선 쉬프트 = 쪽마다 노견 확장(m · 139-6) — 0 = 되돌림(정본의 옛 값을 지움) + shoulder_extend_left_m: float | None = Field(default=None, ge=0, le=20) + shoulder_extend_right_m: float | None = Field(default=None, ge=0, le=20) inlet_structure: Literal["auto", "revet", "I", "L", "U"] | None = None basin_adjust: BasinAdjustPatch | None = None # 기슭막이 4축 조작값 — 키는 역할("inlet"/"outlet"/"extra0"…/"bextra0"…). diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index 9a0b0cfdf..974ec3458 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -28,7 +28,7 @@ import { STRUCTURE_ROW_KEYS, structureAreaRows, } from "./B06_Section_Structure_Layouts"; -import { crossDesignChoices } from "./B06_Section_Cross_Design_Session"; +import { crossDesignChoices, readShoulderExtends } from "./B06_Section_Cross_Design_Session"; import type { StandardCrossSection } from "./B06_Section_Api_Fetch"; import type { RockBoundaryControl } from "./B06_Section_UI_Section_View"; import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope"; @@ -386,6 +386,12 @@ export function collectSectionEdits(ctx: SectionPersistContext): { // 자동으로 되돌아가지 않으므로(켬/끔 둘 뿐) 지금은 손해가 없다(2026-09-09). if (typeof choice.ditch_choice === "boolean") patch.ditch_choice = choice.ditch_choice; }); + // 설계선 쉬프트 = 쪽마다 노견 확장(139-6) — 서버가 [저장] 때 이 값으로 설계선을 다시 냄 + readShoulderExtends(ctx.projectId, ctx.routeId()).forEach((sides, chainage) => { + const patch = patchFor(chainage); + if (sides.left !== undefined) patch.shoulder_extend_left_m = sides.left; + if (sides.right !== undefined) patch.shoulder_extend_right_m = sides.right; + }); if (detail) { // 구조물 폐회로 면적 — **카드를 그리지 않은 측점까지** 여기서 다 계산한다 // (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index 4ac4be5ae..a7a24ab93 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -113,6 +113,9 @@ 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`. */ + shoulderExtendLeftM?: number; + shoulderExtendRightM?: number; } export interface CrossDesignEdge { @@ -137,6 +140,9 @@ export interface CrossDesignResult { /** 곡선부 확폭(m) — 붙은 쪽만 값이 있다. */ widening_left_m?: number; widening_right_m?: number; + /** 노견 확장(m · 139-6) — 넓힌 쪽만. */ + shoulder_extend_left_m?: number; + shoulder_extend_right_m?: number; cross_slope_pct: number; ditch: Record; /** 결과 — **실제로 섰나**. */ @@ -356,6 +362,8 @@ export function computeCrossDesign( ditchEnabled: options.ditchEnabled ?? null, ditchChoice: options.ditchChoice ?? null, berm: options.berm ?? null, + shoulderExtendLeftM: options.shoulderExtendLeftM ?? 0, + shoulderExtendRightM: options.shoulderExtendRightM ?? 0, }); // 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). @@ -575,6 +583,11 @@ export function computeCrossDesign( slope_deg: round4(options.berm.slopeDeg), }; } + // 노견 확장(139-6) — 넓힌 쪽만 되싣는다(짝: 파이썬) + if (geometry.shoulderExtendLeft > 0) + result.shoulder_extend_left_m = round4(geometry.shoulderExtendLeft); + if (geometry.shoulderExtendRight > 0) + result.shoulder_extend_right_m = round4(geometry.shoulderExtendRight); if (paved) result.pavement_thickness_m = round4(pavedGroup.pavement_thickness_m); if (presetKey === "rock" && rockBoundaryOffsetM !== null) { result.rock_boundary_offset_m = round4(rockBoundaryOffsetM); diff --git a/common_util/common_util_cross_design_geometry.ts b/common_util/common_util_cross_design_geometry.ts index a4cb10817..3f38877d7 100644 --- a/common_util/common_util_cross_design_geometry.ts +++ b/common_util/common_util_cross_design_geometry.ts @@ -108,6 +108,9 @@ export class SectionGeometry { hasDitch: boolean; /** 사용자가 정한 선택(`null` = 자동). 결과(`hasDitch`)와 다른 값이다. */ ditchChoice: boolean | null; + /** 노견 확장(m · 139-6) — 쪽마다. */ + shoulderExtendLeft = 0; + shoulderExtendRight = 0; ditchPoints: Array<[number, number]> = []; private groundAt: ((offsetM: number) => number) | null; private rockOffset: number; @@ -138,6 +141,9 @@ export class SectionGeometry { wideningRightM?: number; /** 소단 제원 — 없으면 계단 없이 종전 사면 그대로(계획서 3-9). */ berm?: BermSpec | null; + /** 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6). */ + shoulderExtendLeftM?: number; + shoulderExtendRightM?: number; }) { const { group } = params; const halfRoad = group.road_width_m / 2; @@ -204,6 +210,13 @@ export class SectionGeometry { /** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */ this.ditchChoice = choice; + // 설계선 쉬프트 = 쪽마다 노견 확장(≥ 0 · 139-6) — 단면유형 · 측구 판정은 **옮기기 전** 노견에서 + // (위) · 노체 · 측구 · 사면이 같이 바깥으로 · 차도는 그대로. ⚠ 파이썬 짝 `_SectionGeometry`. + this.shoulderExtendLeft = Math.max(params.shoulderExtendLeftM ?? 0, 0); + this.shoulderExtendRight = Math.max(params.shoulderExtendRightM ?? 0, 0); + 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 facbdc950..4f46752ee 100644 --- a/resources/tester/test_b06_cross_design_mirror.py +++ b/resources/tester/test_b06_cross_design_mirror.py @@ -178,6 +178,32 @@ CASES = [ "spoil_fill": {"side": "left", "width_m": 8.0, "slope_ratio_n": 2.0}, }, }, + { + # 설계선 쉬프트(139-6) — 쪽마다 노견 확장 · 측구 쪽(절토)과 성토 쪽을 같이 넓힘 + "name": "노견 확장 좌 0.7 · 우 1.2", + "samples": _ground(0.35), + "design_elevation_m": 100.4, + "options": { + "ground_type": "soil", + "section_mode": "left_cut", + "ditch_side": "left", + "shoulder_extend_left_m": 0.7, + "shoulder_extend_right_m": 1.2, + }, + }, + { + # 노견 끝 근처에서 지반이 설계면을 넘는 자리 — 단면유형 · 측구 판정은 옮기기 전 노견에서 + "name": "노견 확장 — 판정은 옮기기 전 노견", + "samples": _ground(-0.05), + "design_elevation_m": 100.0, + "options": { + "ground_type": "soil", + "section_mode": "right_cut", + "ditch_side": "right", + "shoulder_extend_right_m": 3.0, + "shoulder_extend_left_m": 2.5, + }, + }, ] _TS_RUNNER = """ @@ -198,6 +224,8 @@ const camel = { plan_radius_m: "planRadiusM", curve_outer_side: "curveOuterSide", curve_widening_m: "curveWideningM", + shoulder_extend_left_m: "shoulderExtendLeftM", + shoulder_extend_right_m: "shoulderExtendRightM", }; // 사토장 제원만 칸 이름이 다르다 — 파이썬은 snake, TS 는 camel 이다. const spoilCamel = { side: "side", width_m: "widthM", slope_ratio_n: "slopeRatioN" };