feat(B06): 곡선부 확폭에 앞뒤 10m 테이퍼 추가
별표2 는 확폭량 표만 주고 붙이는 방식이 없어, 곡선 시·종점에서 폭이 뚝 끊겼다. 곡선 앞뒤 CURVE_WIDENING_TAPER_M(10m) 구간에서 0 → W 로 잇는다. - 측점 생성 때 확폭량을 한 번에 계산해 curve_widening_m 으로 저장. 테이퍼는 이웃 측점을 봐야 하므로 측점 단위 계산으로는 낼 수 없다. - 설계 엔진(파이썬·TS 짝)이 저장된 확폭량을 우선 쓰고, 없으면 반경 표값으로 되돌아간다 — 옛 저장분도 그대로 돈다. 검증: pytest 389 passed(테이퍼 경계값 테스트 2건 추가), tsc --noEmit 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,11 +14,13 @@ import numpy as np
|
||||
from B05_Profile.B05_Profile_Engine_Geometry import circumradius_2d
|
||||
from common_util.common_util_surface_sampler import SurfaceElevationSampler
|
||||
from config.config_system import (
|
||||
CURVE_WIDENING_TAPER_M,
|
||||
SECTION_CROSS_HALF_WIDTH_M,
|
||||
SECTION_CROSS_SAMPLE_INTERVAL_M,
|
||||
SECTION_INCLUDE_ENDPOINT,
|
||||
SECTION_LONG_SAMPLE_INTERVAL_M,
|
||||
SECTION_STATION_INTERVAL_M,
|
||||
curve_widening_m,
|
||||
)
|
||||
|
||||
SECTION_SCHEMA_VERSION = 1
|
||||
@@ -165,6 +167,52 @@ def _plan_radii(
|
||||
return radii, outer_sides
|
||||
|
||||
|
||||
def _curve_widenings(
|
||||
station_chainage: np.ndarray,
|
||||
radii: list[float | None],
|
||||
outer_sides: list[str | None],
|
||||
) -> tuple[list[float], list[str | None]]:
|
||||
"""측점별 확폭량(m)과 그것이 붙는 쪽 — 곡선 앞뒤 테이퍼까지 반영한다.
|
||||
|
||||
표(별표2 Ⅰ.2.나.(4))는 곡선 안에서의 확폭량만 준다. 곡선 시·종점에서 폭이 뚝
|
||||
끊기면 안 되므로, 앞뒤 `CURVE_WIDENING_TAPER_M` 구간에서 0 → W 로 잇는다
|
||||
(2026-09-06 사용자 지시). 측점 간격이 테이퍼보다 넓으면 이 함수가 낼 중간값이
|
||||
없고, 화면·3D 가 측점 사이를 이어 그리는 것으로 대신한다.
|
||||
"""
|
||||
base = [curve_widening_m(radius) for radius in radii]
|
||||
widenings = list(base)
|
||||
sides: list[str | None] = list(outer_sides)
|
||||
# 확폭이 붙는 측점의 연속 덩어리(=곡선 구간)를 찾아 그 바깥으로 테이퍼를 편다.
|
||||
runs: list[tuple[int, int]] = []
|
||||
start: int | None = None
|
||||
for index, value in enumerate(base):
|
||||
if value > 0.0 and outer_sides[index] in ("left", "right"):
|
||||
if start is None:
|
||||
start = index
|
||||
elif start is not None:
|
||||
runs.append((start, index - 1))
|
||||
start = None
|
||||
if start is not None:
|
||||
runs.append((start, len(base) - 1))
|
||||
|
||||
for first, last in runs:
|
||||
for edge, step in ((first, -1), (last, 1)):
|
||||
edge_chainage = float(station_chainage[edge])
|
||||
index = edge + step
|
||||
while 0 <= index < len(base):
|
||||
distance = abs(float(station_chainage[index]) - edge_chainage)
|
||||
if distance > CURVE_WIDENING_TAPER_M or base[index] > 0.0:
|
||||
break
|
||||
ratio = max(0.0, 1.0 - distance / CURVE_WIDENING_TAPER_M)
|
||||
tapered = round(base[edge] * ratio, 4)
|
||||
# 양쪽 곡선 사이에 낀 측점은 넓은 쪽을 따른다.
|
||||
if tapered > widenings[index]:
|
||||
widenings[index] = tapered
|
||||
sides[index] = outer_sides[edge]
|
||||
index += step
|
||||
return widenings, sides
|
||||
|
||||
|
||||
def generate_sections(
|
||||
polyline: np.ndarray | list[list[float]],
|
||||
sampler: SurfaceElevationSampler,
|
||||
@@ -220,6 +268,10 @@ def generate_sections(
|
||||
# 표시가 이 값을 쓴다(2026-09-06). 노선 폴리라인 위에서 앞뒤로 같은 거리를 떨어진 세
|
||||
# 점의 외접원 반경이며, 직선이면 무한대라 None 으로 낸다.
|
||||
plan_radii, plan_outer_sides = _plan_radii(points, route_chainage, station_chainage, total)
|
||||
# 확폭량은 여기서 한 번에 낸다 — 테이퍼가 이웃 측점을 봐야 하므로 측점 단위로는 못 낸다.
|
||||
plan_widenings, plan_outer_sides = _curve_widenings(
|
||||
station_chainage, plan_radii, plan_outer_sides
|
||||
)
|
||||
|
||||
offsets = np.arange(
|
||||
-options.cross_half_width_m,
|
||||
@@ -296,6 +348,9 @@ def generate_sections(
|
||||
"plan_radius_m": plan_radii[index],
|
||||
# 곡선 바깥쪽 — 곡선부 확폭이 붙는 쪽(2026-09-06 사용자 확정).
|
||||
"curve_outer_side": plan_outer_sides[index],
|
||||
# 확폭량(m) — 표값에 곡선 앞뒤 테이퍼를 얹은 값. 설계는 반경이 아니라
|
||||
# 이 값을 쓴다(테이퍼 측점은 반경이 없거나 커도 확폭이 남아 있다).
|
||||
"curve_widening_m": plan_widenings[index],
|
||||
# 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). 사용자 변경 시 확정에서 덮어쓴다.
|
||||
"uphill_side": uphill_side,
|
||||
"frame": frame,
|
||||
|
||||
@@ -120,6 +120,8 @@ export interface SectionStation {
|
||||
plan_radius_m?: number | null;
|
||||
/** 곡선 바깥쪽 — 확폭이 붙는 쪽(2026-09-06 사용자 확정). 직선이면 null. */
|
||||
curve_outer_side?: "left" | "right" | null;
|
||||
/** 확폭량(m) — 표값에 곡선 앞뒤 테이퍼를 얹은 값. 설계는 이 값을 우선 쓴다. */
|
||||
curve_widening_m?: number | null;
|
||||
center_x: number;
|
||||
center_y: number;
|
||||
/** 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). B05 solve 자동 판정 + 사용자 변경. */
|
||||
|
||||
@@ -194,6 +194,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
section.curve_outer_side === "left" || section.curve_outer_side === "right"
|
||||
? section.curve_outer_side
|
||||
: null,
|
||||
curveWideningM: section.curve_widening_m ?? null,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
|
||||
@@ -41,9 +41,10 @@ from config.config_system import (
|
||||
SECTION_GROUND_TYPE_PRESET,
|
||||
SECTION_MODES,
|
||||
STANDARD_CROSS_SECTION,
|
||||
curve_widening_m,
|
||||
)
|
||||
|
||||
from config.config_system import (
|
||||
curve_widening_m as _curve_widening_m,
|
||||
)
|
||||
|
||||
# 사면이 원지반과 만났다고 볼 높이차(m). 이보다 크면 샘플 끝에서 잘린 것으로 본다.
|
||||
_SLOPE_CLOSE_TOLERANCE_M = 0.01
|
||||
@@ -463,10 +464,12 @@ def curve_widening_args(section: dict[str, Any] | None) -> dict[str, Any]:
|
||||
옛 저장분에는 두 값이 없어 확폭 없이 예전과 같은 단면이 나온다.
|
||||
"""
|
||||
if not isinstance(section, dict):
|
||||
return {"plan_radius_m": None, "curve_outer_side": None}
|
||||
return {"plan_radius_m": None, "curve_outer_side": None, "curve_widening_m": None}
|
||||
return {
|
||||
"plan_radius_m": section.get("plan_radius_m"),
|
||||
"curve_outer_side": section.get("curve_outer_side"),
|
||||
# 곡선 앞뒤 테이퍼가 얹힌 값 — 있으면 반경 표값 대신 이걸 쓴다(2026-09-06).
|
||||
"curve_widening_m": section.get("curve_widening_m"),
|
||||
}
|
||||
|
||||
|
||||
@@ -486,6 +489,7 @@ def compute_cross_design(
|
||||
surface_drop_m: float = 0.0,
|
||||
plan_radius_m: float | None = None,
|
||||
curve_outer_side: str | None = None,
|
||||
curve_widening_m: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다.
|
||||
|
||||
@@ -544,7 +548,15 @@ def compute_cross_design(
|
||||
)
|
||||
soil_cut_ratio = _resolve_group("soil", standard)["cut_slope_ratio"]
|
||||
# 곡선부 확폭 — 표는 하한이고, 확폭을 더한 유효너비가 법정 상한(5m)을 넘지 않게 자른다.
|
||||
widening = curve_widening_m(plan_radius_m) if curve_outer_side in ("left", "right") else 0.0
|
||||
# 저장된 확폭량(테이퍼 포함)이 있으면 그것을 쓰고, 없으면 반경 표값으로 되돌아간다.
|
||||
if curve_outer_side in ("left", "right"):
|
||||
widening = (
|
||||
float(curve_widening_m)
|
||||
if isinstance(curve_widening_m, (int, float))
|
||||
else _curve_widening_m(plan_radius_m)
|
||||
)
|
||||
else:
|
||||
widening = 0.0
|
||||
if widening > 0.0:
|
||||
room = max(CURVE_WIDENING_MAX_WIDTH_M - group["road_width_m"], 0.0)
|
||||
widening = min(widening, room)
|
||||
|
||||
@@ -86,6 +86,8 @@ export interface CrossDesignOptions {
|
||||
planRadiusM?: number | null;
|
||||
/** 곡선 **바깥쪽**("left"/"right") — 확폭이 붙는 쪽(2026-09-06 사용자 확정). */
|
||||
curveOuterSide?: "left" | "right" | null;
|
||||
/** 저장된 확폭량(m) — 곡선 앞뒤 테이퍼가 얹힌 값. 있으면 반경 표값 대신 쓴다. */
|
||||
curveWideningM?: number | null;
|
||||
}
|
||||
|
||||
export interface CrossDesignEdge {
|
||||
@@ -271,8 +273,13 @@ export function computeCrossDesign(
|
||||
// 곡선부 확폭 — 표는 하한이고, 확폭을 더한 유효너비가 법정 상한(5m)을 넘지 않게 자른다.
|
||||
// 짝: 파이썬 `compute_cross_design`. 표·상한 값은 config 한 곳에서 온다.
|
||||
const outerSide = options.curveOuterSide;
|
||||
// 저장된 확폭량(테이퍼 포함)이 있으면 그것을 쓰고, 없으면 반경 표값으로 되돌아간다.
|
||||
let widening =
|
||||
outerSide === "left" || outerSide === "right" ? curveWideningM(options.planRadiusM) : 0;
|
||||
outerSide === "left" || outerSide === "right"
|
||||
? typeof options.curveWideningM === "number" && Number.isFinite(options.curveWideningM)
|
||||
? options.curveWideningM
|
||||
: curveWideningM(options.planRadiusM)
|
||||
: 0;
|
||||
if (widening > 0) {
|
||||
widening = Math.min(widening, Math.max(CURVE_WIDENING_MAX_WIDTH_M - group.road_width_m, 0));
|
||||
}
|
||||
|
||||
@@ -440,6 +440,11 @@ CURVE_WIDENING_TABLE_M: tuple[tuple[float, float, float], ...] = (
|
||||
# 넘으면 그 자리에서 자르고 화면이 경고한다.
|
||||
CURVE_WIDENING_MAX_WIDTH_M = 5.0
|
||||
|
||||
# 곡선 앞뒤에서 확폭을 0 → W 로 잇는 길이(m). 별표2에는 확폭량 표만 있고 붙이는 방식이
|
||||
# 없어, 실무 관행(곡선 시·종점 앞뒤 10m 직선 테이퍼)을 따른다(2026-09-06 사용자 지시).
|
||||
# 측점 간격(기본 20m)이 이보다 넓으면 화면에서는 측점 간 보간이 대신 이어 준다.
|
||||
CURVE_WIDENING_TAPER_M = 10.0
|
||||
|
||||
|
||||
def curve_widening_m(plan_radius_m: float | None) -> float:
|
||||
"""평면 곡선반경으로 확폭량(m)을 정한다. 직선·45m 이상·값 없음은 0."""
|
||||
|
||||
Reference in New Issue
Block a user