feat(B06): 설계선 쉬프트 = 쪽마다 노견 확장 — 엔진 짝(판정은 옮기기 전 노견) · 세션 · [저장] patch · 스키마 · 사용자 값 두 벌 · 파이썬 재계산 길 저장값(139-6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -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). */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) — 관 바닥 두 끝 경사 · 박스 두 끝 원지반 경사(%) · 관 길이와 같은 길. */
|
||||
|
||||
@@ -68,3 +68,45 @@ export function crossDesignChoices(
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
/* ── 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6) — 카드 버튼 선택과 따로 담음(버튼 선택이
|
||||
* 통째로 덮어써도 안 지워지게). [저장]·[확정] 이 patch 로 싣고, 브라우저 재계산이 입력으로 씀. */
|
||||
export type ShoulderSide = "left" | "right";
|
||||
export type ShoulderExtends = Map<number, Partial<Record<ShoulderSide, number>>>;
|
||||
|
||||
/** 누가거리(숫자) → 쪽마다 노견 확장(세션에 있는 쪽만). */
|
||||
export function readShoulderExtends(
|
||||
projectId: string | null,
|
||||
routeId: number | null,
|
||||
): ShoulderExtends {
|
||||
const out: ShoulderExtends = new Map();
|
||||
if (!projectId || routeId === null) return out;
|
||||
const raw = readState<Record<string, Partial<Record<ShoulderSide, number>>>>(
|
||||
"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<Record<string, Partial<Record<ShoulderSide, number>>>>(
|
||||
"shoulderext",
|
||||
projectId,
|
||||
routeId,
|
||||
) ?? {};
|
||||
all[keyOf(chainageM)] = { ...(all[keyOf(chainageM)] ?? {}), [side]: Math.max(valueM, 0) };
|
||||
writeState("shoulderext", all, projectId, routeId);
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>, 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<CrossSection["design"]>,
|
||||
previous,
|
||||
);
|
||||
section.design.shoulder_extend_left_m = extendLeft;
|
||||
section.design.shoulder_extend_right_m = extendRight;
|
||||
updated.push(section.chainage_m);
|
||||
}
|
||||
return updated;
|
||||
|
||||
@@ -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해 프론트가 세션값 없이도 오버레이·재계산에 쓰게 한다.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"…).
|
||||
|
||||
@@ -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 사용자 확정: 「일단은 브라우저 계산으로」).
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
/** 결과 — **실제로 섰나**. */
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" };
|
||||
|
||||
Reference in New Issue
Block a user