feat(B06): 곡선부 노폭 자동 확폭 — 평면 R 기준, 곡선 바깥쪽 편측
2026-09-06 사용자 확정. 별표2 Ⅰ.2.나.(4) 확폭표(R 10~45m → 2.25~0.25m)를 측점별 평면 곡선반경에 물려 차도 폭을 넓힌다. - 확폭 방향은 **곡선 바깥쪽 편측** — 노선 폴리라인의 외적 부호로 회전 방향을 보고 바깥쪽을 정한다(좌회전이면 우측). 측점 기록에 `curve_outer_side` 로 실린다. - 차도 반폭을 좌·우로 나눠 들어 한쪽만 넓어지게 함. 확폭이 0이면 예전과 같은 대칭 단면이다. 노견·측구·사면은 그 바깥으로 그대로 밀린다. - 확폭을 더한 유효너비는 법정 상한 5m 에서 자른다(규격 3.0m 면 최대 2.0m 까지). - 계산 짝을 함께 고침 — 파이썬 `compute_cross_design` 과 브라우저 `computeCrossDesign`, 표는 양쪽에 두되 짝임을 주석으로 못 박음. 확폭 입력은 측점 기록에서 뽑는 헬퍼 하나로 9개 호출부(횡단·확정·B07 도면)에 같은 값이 가게 함. - 횡단도에 노폭 라벨 — 확폭이 걸리면 「노폭 4.5m (규격 3.0 + 확폭 1.5)」로 적는다. - 확인: 표 경계·편측 적용·5m 상한·회전 방향 판정 5건(pytest) + 브라우저 표 15건 일치. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,12 @@
|
||||
|
||||
import { splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas";
|
||||
// 단면 기하(노면·측구·사면 설계고)는 파일이 700줄을 넘어 떼어냈다(2026-09-04).
|
||||
import { SectionGeometry, type ResolvedGroup } from "./common_util_cross_design_geometry";
|
||||
import {
|
||||
CURVE_WIDENING_MAX_WIDTH_M,
|
||||
SectionGeometry,
|
||||
curveWideningM,
|
||||
type ResolvedGroup,
|
||||
} from "./common_util_cross_design_geometry";
|
||||
|
||||
/** 지반유형 → 표준단면 프리셋 키. 짝: config `SECTION_GROUND_TYPE_PRESET`. */
|
||||
const GROUND_TYPE_PRESET: Record<string, string> = {
|
||||
@@ -77,6 +82,10 @@ export interface CrossDesignOptions {
|
||||
ditchEnabled?: boolean | null;
|
||||
/** 세월교 월류 높이만큼 노면을 통째로 내린다(m). */
|
||||
surfaceDropM?: number;
|
||||
/** 이 측점의 평면 곡선반경(m) — 곡선부 확폭을 정하는 입력. 직선이면 null. */
|
||||
planRadiusM?: number | null;
|
||||
/** 곡선 **바깥쪽**("left"/"right") — 확폭이 붙는 쪽(2026-09-06 사용자 확정). */
|
||||
curveOuterSide?: "left" | "right" | null;
|
||||
}
|
||||
|
||||
export interface CrossDesignEdge {
|
||||
@@ -96,6 +105,11 @@ export interface CrossDesignResult {
|
||||
fill_slope_ratio: number;
|
||||
roadbed_width_m: number;
|
||||
carriageway_width_m: number;
|
||||
/** 규격 차도 폭(확폭 전, m) — 확폭 라벨·수량이 둘을 나눠 쓴다(2026-09-06). */
|
||||
carriageway_standard_width_m?: number;
|
||||
/** 곡선부 확폭(m) — 붙은 쪽만 값이 있다. */
|
||||
widening_left_m?: number;
|
||||
widening_right_m?: number;
|
||||
cross_slope_pct: number;
|
||||
ditch: Record<string, unknown>;
|
||||
ditch_enabled: boolean;
|
||||
@@ -254,8 +268,19 @@ export function computeCrossDesign(
|
||||
// 2단계 절토는 암 프리셋에서만, 암반 경계 오프셋이 있을 때만 켠다.
|
||||
const enableTwoStage =
|
||||
presetKey === "rock" && (options.twoStageSlope ?? true) && rockBoundaryOffsetM !== null;
|
||||
// 곡선부 확폭 — 표는 하한이고, 확폭을 더한 유효너비가 법정 상한(5m)을 넘지 않게 자른다.
|
||||
// 짝: 파이썬 `compute_cross_design`. 표·상한 값은 config 한 곳에서 온다.
|
||||
const outerSide = options.curveOuterSide;
|
||||
let widening =
|
||||
outerSide === "left" || outerSide === "right" ? curveWideningM(options.planRadiusM) : 0;
|
||||
if (widening > 0) {
|
||||
widening = Math.min(widening, Math.max(CURVE_WIDENING_MAX_WIDTH_M - group.road_width_m, 0));
|
||||
}
|
||||
|
||||
const geometry = new SectionGeometry({
|
||||
designElevationM: centerElevation,
|
||||
wideningLeftM: outerSide === "left" ? widening : 0,
|
||||
wideningRightM: outerSide === "right" ? widening : 0,
|
||||
group,
|
||||
sectionMode,
|
||||
ditchSide: resolvedDitchSide,
|
||||
@@ -355,7 +380,11 @@ export function computeCrossDesign(
|
||||
two_stage_slope: geometry.twoStage,
|
||||
fill_slope_ratio: round4(geometry.fillRatio),
|
||||
roadbed_width_m: round4(geometry.leftExtent + geometry.rightExtent),
|
||||
carriageway_width_m: round4(group.road_width_m),
|
||||
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),
|
||||
cross_slope_pct: round4(crossSlopePct),
|
||||
ditch: ditchSpec,
|
||||
ditch_enabled: geometry.hasDitch,
|
||||
@@ -372,12 +401,12 @@ export function computeCrossDesign(
|
||||
},
|
||||
carriageway_edges: {
|
||||
left: {
|
||||
offset_m: round4(geometry.halfRoad),
|
||||
elevation_m: round4(geometry.roadZ(geometry.halfRoad)),
|
||||
offset_m: round4(geometry.halfRoadLeft),
|
||||
elevation_m: round4(geometry.roadZ(geometry.halfRoadLeft)),
|
||||
},
|
||||
right: {
|
||||
offset_m: round4(-geometry.halfRoad),
|
||||
elevation_m: round4(geometry.roadZ(-geometry.halfRoad)),
|
||||
offset_m: round4(-geometry.halfRoadRight),
|
||||
elevation_m: round4(geometry.roadZ(-geometry.halfRoadRight)),
|
||||
},
|
||||
},
|
||||
design_elevation_m: round4(centerElevation),
|
||||
|
||||
@@ -35,9 +35,42 @@ export function sideRole(sectionMode: string): [string, string] {
|
||||
throw new Error(`지원하지 않는 단면유형입니다: ${sectionMode}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 곡선부 너비 확폭표 — **짝: `config_system_design.CURVE_WIDENING_TABLE_M`**
|
||||
* (별표2 Ⅰ.2.나.(4)). `[반경 하한, 반경 상한(미만), 확폭(m)]`이고 45m 이상은 확폭이 없다.
|
||||
* 두 벌이 한 세트이므로 값을 고칠 때는 파이썬 쪽도 함께 고친다.
|
||||
*/
|
||||
const CURVE_WIDENING_TABLE_M: ReadonlyArray<readonly [number, number, number]> = [
|
||||
[10, 13, 2.25],
|
||||
[13, 14, 2.0],
|
||||
[14, 15, 1.75],
|
||||
[15, 18, 1.5],
|
||||
[18, 20, 1.25],
|
||||
[20, 25, 1.0],
|
||||
[25, 30, 0.75],
|
||||
[30, 40, 0.5],
|
||||
[40, 45, 0.25],
|
||||
];
|
||||
|
||||
/** 확폭을 더한 뒤의 유효너비 상한(m) — 짝: `CURVE_WIDENING_MAX_WIDTH_M`. */
|
||||
export const CURVE_WIDENING_MAX_WIDTH_M = 5.0;
|
||||
|
||||
/** 평면 곡선반경으로 확폭량(m)을 정한다. 직선·45m 이상·값 없음은 0. */
|
||||
export function curveWideningM(planRadiusM: number | null | undefined): number {
|
||||
if (planRadiusM === null || planRadiusM === undefined || !Number.isFinite(planRadiusM)) return 0;
|
||||
const found = CURVE_WIDENING_TABLE_M.find(
|
||||
([low, high]) => planRadiusM >= low && planRadiusM < high,
|
||||
);
|
||||
return found ? found[2] : 0;
|
||||
}
|
||||
|
||||
/** 짝: `_SectionGeometry`. 노면 → 측구 → 사면 순으로 offset 의 설계고를 계산한다. */
|
||||
export class SectionGeometry {
|
||||
/** 규격 차도 반폭(확폭 전) — 수량·표기 기준. */
|
||||
halfRoad: number;
|
||||
/** 좌(+)·우(−) 차도 반폭 — 곡선부 확폭이 **한쪽에만** 붙어 좌우가 갈린다(2026-09-06). */
|
||||
halfRoadLeft: number;
|
||||
halfRoadRight: number;
|
||||
leftExtent: number;
|
||||
rightExtent: number;
|
||||
zCenter: number;
|
||||
@@ -70,12 +103,17 @@ export class SectionGeometry {
|
||||
rockBoundaryOffsetM: number | null;
|
||||
twoStageSlope: boolean;
|
||||
ditchEnabled: boolean | null;
|
||||
/** 곡선부 확폭(m) — 붙는 쪽만 값이 있고 반대쪽은 0이다. */
|
||||
wideningLeftM?: number;
|
||||
wideningRightM?: number;
|
||||
}) {
|
||||
const { group } = params;
|
||||
const halfRoad = group.road_width_m / 2;
|
||||
this.halfRoad = halfRoad;
|
||||
this.leftExtent = halfRoad + group.shoulder_left_m;
|
||||
this.rightExtent = halfRoad + group.shoulder_right_m;
|
||||
this.halfRoadLeft = halfRoad + Math.max(params.wideningLeftM ?? 0, 0);
|
||||
this.halfRoadRight = halfRoad + Math.max(params.wideningRightM ?? 0, 0);
|
||||
this.leftExtent = this.halfRoadLeft + group.shoulder_left_m;
|
||||
this.rightExtent = this.halfRoadRight + group.shoulder_right_m;
|
||||
this.zCenter = params.designElevationM;
|
||||
this.cutRatio = Math.max(group.cut_slope_ratio, 1e-6);
|
||||
this.fillRatio = Math.max(group.fill_slope_ratio, 1e-6);
|
||||
|
||||
Reference in New Issue
Block a user