From 01c96e9a11289c3781a6f33898d2afa1b0c6b773 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 13:10:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EC=B8=A1=EA=B5=AC=20=E3=80=8C?= =?UTF-8?q?=EC=84=A0=ED=83=9D=E3=80=8D=EA=B3=BC=20=E3=80=8C=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=E3=80=8D=EB=A5=BC=20=EA=B0=80=EB=A6=84=20=E2=80=94=20?= =?UTF-8?q?=ED=95=9C=20=EB=B2=88=20=EC=A0=80=EC=9E=A5=EB=90=98=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=9D=B4=20=EC=95=88=20=EB=8F=8C=EB=8D=98=20?= =?UTF-8?q?=EC=9E=90=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠⚠ 한 칸(`ditch_enabled`)에 **두 뜻**이 담겨 있었음. 결과(실제 섰나)를 그대로 다시 입력으로 넣어 읽었으므로 **한 번 저장되면 자동 판정이 영영 다시 안 돌았음.** 계획고를 내려 절토가 생겨도 측구가 안 서고 **아무 말도 안 나왔음.** - **선택** `ditch_choice` (없음 = 자동) · **결과** `ditch_enabled` (실제 섰나)로 가름 - 자동 판정을 **먼저** 하고 선택을 그 위에 얹음. 양성 단면은 선택보다 기하가 먼저 - ⚠ **옛 저장분은 자동값과 다를 때만 선택으로 살림** — 같으면 자동이 그렇게 냈던 것이고 다르면 사용자가 일부러 바꾼 것임. 설계 의도를 잃지 않으면서 굳은 값을 푼 것 (오늘 `length_m` 에서 겪은 「저장분이 걸린 자리」를 같은 방식으로 처리) - 화면 카드는 이제 **선택**을 읽어 상태를 세우고, 표시는 **결과**를 보임 - 파이썬·TS 짝 양쪽 + 호출부 여섯 곳(재계산·저장·확정·사토장·요청 스키마) 시험 다섯 추가 — 「결과를 다시 넣어도 자동이 계속 돎」이 그 잠금. 전체 616 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_Router_Confirm.py | 1 + B06_Section/B06_Section_Api_Types.ts | 6 +++- B06_Section/B06_Section_Cross_Refresh.ts | 3 ++ B06_Section/B06_Section_Engine_Design.py | 8 ++++- .../B06_Section_Engine_Design_Geometry.py | 32 +++++++++++++----- B06_Section/B06_Section_Engine_SpoilFill.py | 1 + B06_Section/B06_Section_Router.py | 1 + B06_Section/B06_Section_Router_Design.py | 3 ++ B06_Section/B06_Section_Schema.py | 4 ++- B06_Section/B06_Section_UI_Cross_Design.ts | 12 ++++--- B06_Section/B06_Section_UI_Page.ts | 2 +- common_util/common_util_cross_design.ts | 8 +++++ .../common_util_cross_design_geometry.ts | 33 +++++++++++++++---- 13 files changed, 91 insertions(+), 23 deletions(-) diff --git a/B05_Profile/B05_Profile_Router_Confirm.py b/B05_Profile/B05_Profile_Router_Confirm.py index a9ce488e..85e6bf6c 100644 --- a/B05_Profile/B05_Profile_Router_Confirm.py +++ b/B05_Profile/B05_Profile_Router_Confirm.py @@ -154,6 +154,7 @@ async def sync_uphill_overrides_into_designs( rock_boundary_offset_m=design.get("rock_boundary_offset_m"), two_stage_slope=bool(design.get("two_stage_slope", True)), ditch_enabled=design.get("ditch_enabled"), + ditch_choice=design.get("ditch_choice"), surface_drop_m=ford_drop_at(float(chainage), ford_drops), **curve_widening_args(cross_record), ) diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 05323078..3629355d 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -426,6 +426,8 @@ export interface CrossDesign { | { type: "none" }; /** 측구 생성 여부(엔진이 자동/override 반영해 실제 적용한 결과). */ ditch_enabled?: boolean; + /** 사용자가 정한 선택(`null` = 자동). 결과와 다른 값이다. */ + ditch_choice?: boolean | null; /** 포장 중첩 여부와 포장층 두께(포장 시). */ paved: boolean; pavement_thickness_m?: number; @@ -517,8 +519,10 @@ export interface CrossDesignRequest { two_stage_slope?: boolean; /** 이 측점만 쓰는 암 절토 경사비(1:n 의 n). 없으면 표준값(2026-09-07). */ cut_slope_ratio?: number | null; - /** 측구 생성 여부. null/미지정=자동 판정, true/false=수동 override. */ + /** 옛 이름 — 결과값이 그대로 실려 오던 자리(호환). 새 요청은 `ditch_choice` 를 쓴다. */ ditch_enabled?: boolean | null; + /** 측구를 둘지 **사용자가 정한 선택**. `null`/미지정 = 자동 판정(2026-09-09 정리). */ + ditch_choice?: boolean | null; /** 설정 패널 편집값. 요청값 → config 기본값 순으로 우선한다. */ standard_cross_section?: StandardCrossSection; } diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 875525b3..f2d44caa 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -287,6 +287,9 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { twoStageSlope: choice?.two_stage_slope ?? (design.two_stage_slope === undefined ? true : Boolean(design.two_stage_slope)), + // ⚠ **선택과 결과를 갈라 넘긴다**(2026-09-09). 선택이 있으면 그것을 따르고, + // 없으면 옛 저장분의 결과를 **자동값과 다를 때만** 선택으로 살린다. + ditchChoice: typeof design.ditch_choice === "boolean" ? design.ditch_choice : null, ditchEnabled: typeof design.ditch_enabled === "boolean" ? design.ditch_enabled : null, // 세월교 월류 하강은 계획선 편집으로 바뀌지 않는다 — 저장분 값을 그대로 잇는다. surfaceDropM: typeof design.surface_drop_m === "number" ? design.surface_drop_m : 0, diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index 849bfe3a..33b28829 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -171,6 +171,7 @@ def compute_cross_design( two_stage_slope: bool = True, cut_slope_ratio: float | None = None, ditch_enabled: bool | None = None, + ditch_choice: bool | None = None, surface_drop_m: float = 0.0, plan_radius_m: float | None = None, curve_outer_side: str | None = None, @@ -272,7 +273,9 @@ def compute_cross_design( soil_cut_ratio=soil_cut_ratio, rock_boundary_offset_m=rock_boundary_offset_m, two_stage_slope=enable_two_stage, - ditch_enabled=ditch_enabled, + # 선택은 `ditch_choice`, 옛 저장분은 `ditch_enabled` 로 온다(자동값과 다를 때만 뜻). + ditch_choice=ditch_choice, + legacy_ditch_enabled=ditch_enabled, widening_left_m=widening_left, widening_right_m=widening_right, berm=berm, @@ -425,7 +428,10 @@ def compute_cross_design( "widening_right_m": round(geometry.half_road_right - geometry.half_road, 4), "cross_slope_pct": round(cross_slope_pct, 4), "ditch": ditch_spec, + # 결과 — **실제로 섰나**. 읽는 쪽 여섯이 이 뜻으로 쓴다. "ditch_enabled": bool(geometry.has_ditch), + # 선택 — **사용자가 정한 것**(`None` = 자동). 결과와 갈라 둔다(2026-09-09). + "ditch_choice": geometry.ditch_choice, "paved": bool(paved), # 노면(노견 포함) 양 끝점 — 노면 렌더링 기준. "road_edges": { diff --git a/B06_Section/B06_Section_Engine_Design_Geometry.py b/B06_Section/B06_Section_Engine_Design_Geometry.py index 9caba13f..bc790a10 100644 --- a/B06_Section/B06_Section_Engine_Design_Geometry.py +++ b/B06_Section/B06_Section_Engine_Design_Geometry.py @@ -54,7 +54,8 @@ class _SectionGeometry: soil_cut_ratio: float | None = None, rock_boundary_offset_m: float | None = None, two_stage_slope: bool = False, - ditch_enabled: bool | None = None, + ditch_choice: bool | None = None, + legacy_ditch_enabled: bool | None = None, widening_left_m: float = 0.0, widening_right_m: float = 0.0, berm: BermSpec | None = None, @@ -108,18 +109,31 @@ class _SectionGeometry: else "fill" ) - # 측구 생성 여부(D-1): 양성은 항상 미생성. ditch_enabled가 오면 그 값을 따르고(수동 - # override), None이면 자동 판정 — 측구측 노면 끝에서 지반이 설계면보다 높으면(절토 - # 상황) 생성, 낮으면(성토 상황, 자연 배수) 미생성. ground_at 없으면 보수적으로 생성. + # 측구 생성 여부(D-1) — **자동 판정이 먼저, 사용자 선택이 그 위**(2026-09-09 정리). + # + # ⚠⚠ **한 칸에 두 뜻이 담겨 있던 자리다.** 결과(`ditch_enabled` = 실제 생성됨)를 + # 그대로 다시 입력으로 넣어 읽었으므로, **한 번 저장되면 자동 판정이 영영 다시 + # 안 돌았다.** 계획고를 내려 절토가 생겨도 측구가 안 서고 **아무 말도 안 나왔다.** + # ⇒ 이제 **선택은 `ditch_choice`**(없음 = 자동)이고 **결과는 `ditch_enabled`** 다. + # ⚠ 옛 저장분(`ditch_choice` 가 없던 것)은 `legacy_ditch_enabled` 로 온다. 그 값은 + # **자동값과 다를 때만 뜻이 있다** — 같으면 자동이 그렇게 냈던 것이고, 다르면 + # 사용자가 일부러 바꾼 것이다. 그래서 **다를 때만** 선택으로 살린다(설계 의도 보존). if section_mode == "both_fill": - self.has_ditch = False - elif ditch_enabled is not None: - self.has_ditch = ditch_enabled + auto_ditch = False elif ground_at is not None: ditch_edge = self.left_extent if ditch_side == "left" else -self.right_extent - self.has_ditch = ground_at(ditch_edge) > self.road_z(ditch_edge) + 1e-3 + auto_ditch = ground_at(ditch_edge) > self.road_z(ditch_edge) + 1e-3 else: - self.has_ditch = True + auto_ditch = True # ground_at 이 없으면 보수적으로 생성 + choice = ditch_choice + if choice is None and legacy_ditch_enabled is not None: + choice = ( + None if bool(legacy_ditch_enabled) == auto_ditch else bool(legacy_ditch_enabled) + ) + # 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저다. + self.has_ditch = auto_ditch if (choice is None or section_mode == "both_fill") else choice + #: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. + self.ditch_choice = choice # 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용). self.ditch_points: list[tuple[float, float]] = [] diff --git a/B06_Section/B06_Section_Engine_SpoilFill.py b/B06_Section/B06_Section_Engine_SpoilFill.py index f4e6bde5..02296175 100644 --- a/B06_Section/B06_Section_Engine_SpoilFill.py +++ b/B06_Section/B06_Section_Engine_SpoilFill.py @@ -202,6 +202,7 @@ def enforce_spoil_fills( two_stage_slope=bool(design.get("two_stage_slope", True)), cut_slope_ratio=stored_cut_slope(design), ditch_enabled=design.get("ditch_enabled"), + ditch_choice=design.get("ditch_choice"), surface_drop_m=float(design.get("surface_drop_m") or 0.0), berm=stored_berm(design), spoil_fill={ diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 1c445cd2..24d24e20 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -641,6 +641,7 @@ async def compute_cross_section_design( # 측점별 암 절토 경사 — 요청값이 없으면 저장분에서 잇는다(2026-09-07). cut_slope_ratio=request.cut_slope_ratio or stored_cut_slope(stored_design or {}), ditch_enabled=request.ditch_enabled, + ditch_choice=request.ditch_choice, 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), diff --git a/B06_Section/B06_Section_Router_Design.py b/B06_Section/B06_Section_Router_Design.py index 5f1aaf8a..f570baf2 100644 --- a/B06_Section/B06_Section_Router_Design.py +++ b/B06_Section/B06_Section_Router_Design.py @@ -198,6 +198,7 @@ def enforce_pavement_ranges( two_stage_slope=bool(design.get("two_stage_slope", True)), cut_slope_ratio=stored_cut_slope(design), ditch_enabled=design.get("ditch_enabled"), + ditch_choice=design.get("ditch_choice"), surface_drop_m=ford_drop_at(chainage, ford_drops), berm=stored_berm(design), **curve_widening_args(section), @@ -251,6 +252,7 @@ def enforce_ford_surface_drops( two_stage_slope=bool(design.get("two_stage_slope", True)), cut_slope_ratio=stored_cut_slope(design), ditch_enabled=design.get("ditch_enabled"), + ditch_choice=design.get("ditch_choice"), surface_drop_m=wanted, berm=stored_berm(design), **curve_widening_args(section), @@ -446,6 +448,7 @@ def recompute_designs_for_alignment( else stored_cut_slope(stored) ), ditch_enabled=stored.get("ditch_enabled"), + ditch_choice=stored.get("ditch_choice"), surface_drop_m=ford_drop_at(chainage, ford_drops), berm=session_berms.get(key) or stored_berm(stored), **curve_widening_args(section), diff --git a/B06_Section/B06_Section_Schema.py b/B06_Section/B06_Section_Schema.py index 3cbfc1b2..b6ef1a76 100644 --- a/B06_Section/B06_Section_Schema.py +++ b/B06_Section/B06_Section_Schema.py @@ -55,8 +55,10 @@ class CrossDesignRequest(BaseModel): # 이 측점만 쓰는 암 절토 경사비(1:n 의 n) — 카드에서 넣은 값(2026-09-07 사용자 지시). # None 이면 표준 횡단면 설정값을 쓴다. cut_slope_ratio: float | None = Field(default=None, gt=0) - # 측구 생성 여부. None=자동 판정(측구측 절토면만 생성), True/False=수동 override. + # 옛 이름 — **결과값**이 그대로 실려 오던 자리(호환). 자동값과 다를 때만 선택으로 산다. ditch_enabled: bool | None = None + # 측구를 둘지 **사용자가 정한 선택**. None = 자동 판정(2026-09-09 갈라냄). + ditch_choice: bool | None = None # B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 기본값 순. standard_cross_section: dict[str, Any] | None = None diff --git a/B06_Section/B06_Section_UI_Cross_Design.ts b/B06_Section/B06_Section_UI_Cross_Design.ts index e6cc50f5..1fc693c5 100644 --- a/B06_Section/B06_Section_UI_Cross_Design.ts +++ b/B06_Section/B06_Section_UI_Cross_Design.ts @@ -113,8 +113,9 @@ export interface CrossDesignChange { paved: boolean; /** 암 지반 2단계 경사(암반 경계 아래=암, 위=토사) 적용 여부. 기본 true, 토글로 해제. */ two_stage_slope: boolean; - /** 측구 생성 여부. null=자동 판정, true/false=수동 override. */ - ditch_enabled: boolean | null; + /** 측구를 둘지 **사용자가 정한 선택**. `null` = 자동 판정. + * ⚠ 결과(`design.ditch_enabled` = 실제 섰나)와 **다른 값**이다(2026-09-09 정리). */ + ditch_choice: boolean | null; } /** @@ -337,7 +338,9 @@ export function buildDesignControls( paved: design?.paved ?? false, // 암 design일 때만 저장값을 신뢰(토사는 two_stage=false echo가 무의미) — 암 전환 시 기본 복합경사(4번). twoStage: design && isRock(design.ground_type) ? (design.two_stage_slope ?? true) : true, - ditchEnabled: design?.ditch_enabled ?? null, + // ⚠ **결과가 아니라 선택을 읽는다.** 결과를 읽으면 한 번 저장된 뒤로 + // 자동 판정이 영영 안 돈다(2026-09-09에 갈라낸 자리). + ditchEnabled: design?.ditch_choice ?? null, }; const bar = document.createElement("div"); bar.className = "b06-design"; @@ -358,7 +361,7 @@ export function buildDesignControls( ditch_type: state.ditchType, paved: state.paved, two_stage_slope: state.twoStage, - ditch_enabled: state.ditchEnabled, + ditch_choice: state.ditchEnabled, }); }; @@ -386,6 +389,7 @@ export function buildDesignControls( const rockCut = isRock(state.ground) && state.mode !== "both_fill"; const hasDitch = state.mode !== "both_fill"; // 측구 생성 여부(자동 판정 or 사용자 override) — 측구형식은 측구가 있을 때만 의미 있다. + // 화면 표시는 **결과**를 보인다 — 선택이 없으면 자동으로 선 결과가 답이다. const ditchOn = state.ditchEnabled ?? design?.ditch_enabled ?? true; // 경사 방향(좌/우): 양절·양성에서 활성. 항상 인라인 노출(오버플로 대상 아님). 라벨 삭제(E-7). diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 500c100a..837ec2ce 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -273,7 +273,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { ditch_type: design.ditch_type ?? "standard", paved: design.paved, two_stage_slope: design.two_stage_slope ?? true, - ditch_enabled: design.ditch_enabled ?? null, + ditch_choice: design.ditch_choice ?? null, }; } diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index 8a521360..4ac4be5a 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -93,6 +93,9 @@ export interface CrossDesignOptions { * 짝: 파이썬 `compute_cross_design(cut_slope_ratio=…)`. */ cutSlopeRatio?: number | null; ditchEnabled?: boolean | null; + /** 측구를 둘지 **사용자가 정한 선택**(`null` = 자동 판정). + * 짝: 파이썬 `compute_cross_design(ditch_choice=…)`. */ + ditchChoice?: boolean | null; /** 세월교 월류 높이만큼 노면을 통째로 내린다(m). */ surfaceDropM?: number; /** 이 측점의 평면 곡선반경(m) — 곡선부 확폭을 정하는 입력. 직선이면 null. */ @@ -136,7 +139,10 @@ export interface CrossDesignResult { widening_right_m?: number; cross_slope_pct: number; ditch: Record; + /** 결과 — **실제로 섰나**. */ ditch_enabled: boolean; + /** 선택 — **사용자가 정한 것**(`null` = 자동). 결과와 갈라 둔다(2026-09-09). */ + ditch_choice: boolean | null; paved: boolean; road_edges: { left: CrossDesignEdge; right: CrossDesignEdge }; carriageway_edges: { left: CrossDesignEdge; right: CrossDesignEdge }; @@ -348,6 +354,7 @@ export function computeCrossDesign( rockBoundaryOffsetM, twoStageSlope: enableTwoStage, ditchEnabled: options.ditchEnabled ?? null, + ditchChoice: options.ditchChoice ?? null, berm: options.berm ?? null, }); @@ -508,6 +515,7 @@ export function computeCrossDesign( cross_slope_pct: round4(crossSlopePct), ditch: ditchSpec, ditch_enabled: geometry.hasDitch, + ditch_choice: geometry.ditchChoice, paved, road_edges: { left: { diff --git a/common_util/common_util_cross_design_geometry.ts b/common_util/common_util_cross_design_geometry.ts index adbe7b67..2dd43f0a 100644 --- a/common_util/common_util_cross_design_geometry.ts +++ b/common_util/common_util_cross_design_geometry.ts @@ -106,6 +106,8 @@ export class SectionGeometry { ditchType: string; slopePerOffset: number; hasDitch: boolean; + /** 사용자가 정한 선택(`null` = 자동). 결과(`hasDitch`)와 다른 값이다. */ + ditchChoice: boolean | null; ditchPoints: Array<[number, number]> = []; private groundAt: ((offsetM: number) => number) | null; private rockOffset: number; @@ -127,7 +129,10 @@ export class SectionGeometry { soilCutRatio: number | null; rockBoundaryOffsetM: number | null; twoStageSlope: boolean; + /** 옛 저장분의 결과값 — **자동값과 다를 때만** 선택으로 살린다. */ ditchEnabled: boolean | null; + /** 사용자가 정한 선택(없으면 자동). */ + ditchChoice?: boolean | null; /** 곡선부 확폭(m) — 붙는 쪽만 값이 있고 반대쪽은 0이다. */ wideningLeftM?: number; wideningRightM?: number; @@ -167,17 +172,33 @@ export class SectionGeometry { groundAt(-this.rightExtent) > this.roadZ(-this.rightExtent) + 1e-3 ? "cut" : "fill"; } - // 측구 생성 여부(D-1). + // 측구 생성 여부(D-1) — **자동 판정이 먼저, 사용자 선택이 그 위**(2026-09-09 정리). + // + // ⚠⚠ **한 칸에 두 뜻이 담겨 있던 자리다**(짝: 파이썬 `_SectionGeometry`). 결과 + // (`ditch_enabled` = 실제 생성됨)를 그대로 다시 입력으로 넣어 읽었으므로 **한 번 + // 저장되면 자동 판정이 영영 다시 안 돌았다.** 계획고를 내려 절토가 생겨도 측구가 + // 안 서고 아무 말도 안 나왔다. ⇒ **선택은 `ditchChoice`**(없음 = 자동), **결과는 + // `hasDitch`** 로 가른다. + // ⚠ 옛 저장분은 `ditchEnabled` 로 온다. 그 값은 **자동값과 다를 때만 뜻이 있다** — + // 같으면 자동이 그렇게 냈던 것이고, 다르면 사용자가 일부러 바꾼 것이다. + let autoDitch: boolean; if (params.sectionMode === "both_fill") { - this.hasDitch = false; - } else if (params.ditchEnabled !== null && params.ditchEnabled !== undefined) { - this.hasDitch = params.ditchEnabled; + autoDitch = false; } else if (groundAt !== null) { const ditchEdge = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent; - this.hasDitch = groundAt(ditchEdge) > this.roadZ(ditchEdge) + 1e-3; + autoDitch = groundAt(ditchEdge) > this.roadZ(ditchEdge) + 1e-3; } else { - this.hasDitch = true; + autoDitch = true; // groundAt 이 없으면 보수적으로 생성 } + let choice = params.ditchChoice ?? null; + const legacy = params.ditchEnabled; + if (choice === null && legacy !== null && legacy !== undefined) { + choice = Boolean(legacy) === autoDitch ? null : Boolean(legacy); + } + // 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저다. + this.hasDitch = choice === null || params.sectionMode === "both_fill" ? autoDitch : choice; + /** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */ + this.ditchChoice = choice; // 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용). const edgeOffset = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent;