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:
@@ -130,12 +130,17 @@ def _plan_radii(
|
||||
route_chainage: np.ndarray,
|
||||
station_chainage: np.ndarray,
|
||||
total: float,
|
||||
) -> list[float | None]:
|
||||
"""측점마다 평면 곡선반경(m). 직선·측정 불가는 None."""
|
||||
) -> tuple[list[float | None], list[str | None]]:
|
||||
"""측점마다 (평면 곡선반경 m, 곡선 **바깥쪽**). 직선·측정 불가는 (None, None).
|
||||
|
||||
바깥쪽은 확폭이 붙는 쪽이다(2026-09-06 사용자 확정). 좌회전(반시계)이면 곡선 안쪽이
|
||||
좌측이므로 바깥은 우측이다. offset 부호는 +가 좌측이라는 횡단 규약을 따른다.
|
||||
"""
|
||||
arm = min(PLAN_RADIUS_ARM_M, max(total / 2.0, 0.0))
|
||||
if arm < 0.5:
|
||||
return [None] * len(station_chainage)
|
||||
return [None] * len(station_chainage), [None] * len(station_chainage)
|
||||
radii: list[float | None] = []
|
||||
outer_sides: list[str | None] = []
|
||||
for value in station_chainage:
|
||||
center = float(value)
|
||||
back = max(0.0, center - arm)
|
||||
@@ -143,15 +148,21 @@ def _plan_radii(
|
||||
# 시·종점에서는 한쪽 팔이 짧아진다 — 양쪽이 다 확보될 때만 잰다.
|
||||
if center - back < arm * 0.5 or ahead - center < arm * 0.5:
|
||||
radii.append(None)
|
||||
outer_sides.append(None)
|
||||
continue
|
||||
trio = _interpolate_xy(points, route_chainage, np.array([back, center, ahead]))
|
||||
radius = circumradius_2d(trio[0], trio[1], trio[2])
|
||||
radii.append(
|
||||
None
|
||||
if not math.isfinite(radius) or radius >= PLAN_RADIUS_STRAIGHT_M
|
||||
else round(radius, 3)
|
||||
)
|
||||
return radii
|
||||
if not math.isfinite(radius) or radius >= PLAN_RADIUS_STRAIGHT_M:
|
||||
radii.append(None)
|
||||
outer_sides.append(None)
|
||||
continue
|
||||
radii.append(round(radius, 3))
|
||||
# 외적 z 부호로 회전 방향을 본다: 양수 = 좌회전 → 안쪽이 좌측 → 바깥은 우측.
|
||||
first = trio[1] - trio[0]
|
||||
second = trio[2] - trio[1]
|
||||
cross = float(first[0] * second[1] - first[1] * second[0])
|
||||
outer_sides.append("right" if cross > 0 else "left" if cross < 0 else None)
|
||||
return radii, outer_sides
|
||||
|
||||
|
||||
def generate_sections(
|
||||
@@ -208,7 +219,7 @@ def generate_sections(
|
||||
# 측점별 **평면 곡선반경**(m). 곡선부 확폭(별표2 Ⅰ.2.나.(4))과 최소곡선반지름 위반
|
||||
# 표시가 이 값을 쓴다(2026-09-06). 노선 폴리라인 위에서 앞뒤로 같은 거리를 떨어진 세
|
||||
# 점의 외접원 반경이며, 직선이면 무한대라 None 으로 낸다.
|
||||
plan_radii = _plan_radii(points, route_chainage, station_chainage, total)
|
||||
plan_radii, plan_outer_sides = _plan_radii(points, route_chainage, station_chainage, total)
|
||||
|
||||
offsets = np.arange(
|
||||
-options.cross_half_width_m,
|
||||
@@ -283,6 +294,8 @@ def generate_sections(
|
||||
"center_z": _float_or_none(center_z),
|
||||
"azimuth_deg": round(azimuth, 6),
|
||||
"plan_radius_m": plan_radii[index],
|
||||
# 곡선 바깥쪽 — 곡선부 확폭이 붙는 쪽(2026-09-06 사용자 확정).
|
||||
"curve_outer_side": plan_outer_sides[index],
|
||||
# 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). 사용자 변경 시 확정에서 덮어쓴다.
|
||||
"uphill_side": uphill_side,
|
||||
"frame": frame,
|
||||
|
||||
@@ -95,6 +95,7 @@ async def sync_uphill_overrides_into_designs(
|
||||
get_cross_section_designs,
|
||||
update_cross_section_design,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Design import curve_widening_args
|
||||
from B06_Section.B06_Section_Router import _read_cross_design_inputs
|
||||
from B06_Section.B06_Section_Router_Design import ford_drop_at, ford_surface_drops
|
||||
|
||||
@@ -123,7 +124,7 @@ async def sync_uphill_overrides_into_designs(
|
||||
next_ditch = side
|
||||
if next_mode == mode and design.get("ditch_side") == next_ditch:
|
||||
continue
|
||||
samples, design_elevation, pavement_suggested = await asyncio.to_thread(
|
||||
samples, design_elevation, pavement_suggested, cross_record = await asyncio.to_thread(
|
||||
_read_cross_design_inputs, project_root, longitudinal_file_path, float(chainage)
|
||||
)
|
||||
next_design = compute_cross_design(
|
||||
@@ -139,6 +140,7 @@ async def sync_uphill_overrides_into_designs(
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
surface_drop_m=ford_drop_at(float(chainage), ford_drops),
|
||||
**curve_widening_args(cross_record),
|
||||
)
|
||||
next_design["status"] = design.get("status", "provisional")
|
||||
next_design["pavement_suggested"] = design.get("pavement_suggested", pavement_suggested)
|
||||
|
||||
@@ -118,6 +118,8 @@ export interface SectionStation {
|
||||
azimuth_deg: number | null;
|
||||
/** 평면 곡선반경(m). 직선이거나 잴 수 없으면 null — 곡선부 확폭·법정 최소반경 판정용. */
|
||||
plan_radius_m?: number | null;
|
||||
/** 곡선 바깥쪽 — 확폭이 붙는 쪽(2026-09-06 사용자 확정). 직선이면 null. */
|
||||
curve_outer_side?: "left" | "right" | null;
|
||||
center_x: number;
|
||||
center_y: number;
|
||||
/** 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). B05 solve 자동 판정 + 사용자 변경. */
|
||||
@@ -401,6 +403,11 @@ export interface CrossDesign {
|
||||
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:
|
||||
| { type: "standard"; top_width_m: number; bottom_width_m: number; depth_m: number }
|
||||
|
||||
@@ -188,6 +188,12 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
ditchEnabled: typeof design.ditch_enabled === "boolean" ? design.ditch_enabled : null,
|
||||
// 세월교 월류 하강은 계획선 편집으로 바뀌지 않는다 — 저장분 값을 그대로 잇는다.
|
||||
surfaceDropM: typeof design.surface_drop_m === "number" ? design.surface_drop_m : 0,
|
||||
// 곡선부 확폭 입력 — 측점 기록에 실려 온다(서버 엔진과 같은 값, 2026-09-06).
|
||||
planRadiusM: section.plan_radius_m ?? null,
|
||||
curveOuterSide:
|
||||
section.curve_outer_side === "left" || section.curve_outer_side === "right"
|
||||
? section.curve_outer_side
|
||||
: null,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
|
||||
@@ -35,11 +35,13 @@ from B06_Section.B06_Section_Engine_Areas import (
|
||||
_trapezoid_areas,
|
||||
)
|
||||
from config.config_system import (
|
||||
CURVE_WIDENING_MAX_WIDTH_M,
|
||||
SECTION_DITCH_SIDES,
|
||||
SECTION_DITCH_TYPES,
|
||||
SECTION_GROUND_TYPE_PRESET,
|
||||
SECTION_MODES,
|
||||
STANDARD_CROSS_SECTION,
|
||||
curve_widening_m,
|
||||
)
|
||||
|
||||
|
||||
@@ -159,11 +161,17 @@ class _SectionGeometry:
|
||||
rock_boundary_offset_m: float | None = None,
|
||||
two_stage_slope: bool = False,
|
||||
ditch_enabled: bool | None = None,
|
||||
widening_left_m: float = 0.0,
|
||||
widening_right_m: float = 0.0,
|
||||
) -> None:
|
||||
half_road = group["road_width_m"] / 2.0
|
||||
self.half_road = half_road # 차도 반폭(노견 제외) — 포장 범위 기준
|
||||
self.left_extent = half_road + group["shoulder_left_m"] # 좌(+) 노면 끝
|
||||
self.right_extent = half_road + group["shoulder_right_m"] # 우(-) 노면 끝
|
||||
# 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽).
|
||||
# 그래서 반폭을 좌·우로 나눠 든다 — 확폭이 0이면 예전과 똑같은 대칭 단면이다.
|
||||
self.half_road_left = half_road + max(widening_left_m, 0.0)
|
||||
self.half_road_right = half_road + max(widening_right_m, 0.0)
|
||||
self.half_road = half_road # 규격 차도 반폭(확폭 전) — 수량·표기 기준
|
||||
self.left_extent = self.half_road_left + group["shoulder_left_m"] # 좌(+) 노면 끝
|
||||
self.right_extent = self.half_road_right + group["shoulder_right_m"] # 우(-) 노면 끝
|
||||
self.z_center = design_elevation_m
|
||||
self.cut_ratio = max(group["cut_slope_ratio"], 1e-6) # 암 구간(하단) 절토 경사
|
||||
self.fill_ratio = max(group["fill_slope_ratio"], 1e-6)
|
||||
@@ -448,6 +456,20 @@ class _SectionGeometry:
|
||||
return points
|
||||
|
||||
|
||||
def curve_widening_args(section: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""측점 기록에서 곡선부 확폭 입력을 뽑는다 — `compute_cross_design(**...)` 로 넘긴다.
|
||||
|
||||
측점마다 실려 오는 값이라 호출자마다 따로 꺼내 쓰면 빠뜨리기 쉽다(2026-09-06).
|
||||
옛 저장분에는 두 값이 없어 확폭 없이 예전과 같은 단면이 나온다.
|
||||
"""
|
||||
if not isinstance(section, dict):
|
||||
return {"plan_radius_m": None, "curve_outer_side": None}
|
||||
return {
|
||||
"plan_radius_m": section.get("plan_radius_m"),
|
||||
"curve_outer_side": section.get("curve_outer_side"),
|
||||
}
|
||||
|
||||
|
||||
def compute_cross_design(
|
||||
samples: list[dict[str, Any]],
|
||||
design_elevation_m: float | None,
|
||||
@@ -462,6 +484,8 @@ def compute_cross_design(
|
||||
two_stage_slope: bool = True,
|
||||
ditch_enabled: bool | None = None,
|
||||
surface_drop_m: float = 0.0,
|
||||
plan_radius_m: float | None = None,
|
||||
curve_outer_side: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다.
|
||||
|
||||
@@ -472,6 +496,10 @@ def compute_cross_design(
|
||||
standard: B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 순.
|
||||
rock_boundary_offset_m: 암반 경계선 오프셋(지반선 기준, 음수=하향). 암 지반 2단계 절토용.
|
||||
two_stage_slope: 암 지반에서 암반 경계 기준 2단계 경사 적용 여부(기본 True, 토글로 해제).
|
||||
plan_radius_m: 이 측점의 평면 곡선반경(m). 곡선부 확폭(별표2 Ⅰ.2.나.(4))을 정하는
|
||||
입력이며, None·45m 이상이면 확폭이 없다.
|
||||
curve_outer_side: 곡선 **바깥쪽**("left"/"right"). 확폭은 그쪽으로만 붙는다
|
||||
(2026-09-06 사용자 확정). 값이 없으면 확폭을 넣지 않는다.
|
||||
surface_drop_m: 노면을 통째로 내리는 양(m) — 세월교 월류 높이. 구체 위 노면은 월류
|
||||
높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로
|
||||
횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자).
|
||||
@@ -515,6 +543,14 @@ def compute_cross_design(
|
||||
preset_key == "rock" and two_stage_slope and rock_boundary_offset_m is not None
|
||||
)
|
||||
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 widening > 0.0:
|
||||
room = max(CURVE_WIDENING_MAX_WIDTH_M - group["road_width_m"], 0.0)
|
||||
widening = min(widening, room)
|
||||
widening_left = widening if curve_outer_side == "left" else 0.0
|
||||
widening_right = widening if curve_outer_side == "right" else 0.0
|
||||
|
||||
geometry = _SectionGeometry(
|
||||
design_elevation_m=design_elevation_m,
|
||||
group=group,
|
||||
@@ -527,6 +563,8 @@ def compute_cross_design(
|
||||
rock_boundary_offset_m=rock_boundary_offset_m,
|
||||
two_stage_slope=enable_two_stage,
|
||||
ditch_enabled=ditch_enabled,
|
||||
widening_left_m=widening_left,
|
||||
widening_right_m=widening_right,
|
||||
)
|
||||
|
||||
# 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야
|
||||
@@ -617,7 +655,11 @@ def compute_cross_design(
|
||||
"two_stage_slope": bool(geometry.two_stage),
|
||||
"fill_slope_ratio": round(geometry.fill_ratio, 4),
|
||||
"roadbed_width_m": round(geometry.left_extent + geometry.right_extent, 4),
|
||||
"carriageway_width_m": round(group["road_width_m"], 4),
|
||||
"carriageway_width_m": round(geometry.half_road_left + geometry.half_road_right, 4),
|
||||
# 규격 폭과 확폭을 따로 남긴다 — 횡단도 라벨·수량 산출이 둘을 나눠 쓴다.
|
||||
"carriageway_standard_width_m": round(group["road_width_m"], 4),
|
||||
"widening_left_m": round(geometry.half_road_left - geometry.half_road, 4),
|
||||
"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),
|
||||
@@ -636,12 +678,12 @@ def compute_cross_design(
|
||||
# 차도(노견 제외) 양 끝점 — 포장 범위 기준(D-5).
|
||||
"carriageway_edges": {
|
||||
"left": {
|
||||
"offset_m": round(geometry.half_road, 4),
|
||||
"elevation_m": round(geometry.road_z(geometry.half_road), 4),
|
||||
"offset_m": round(geometry.half_road_left, 4),
|
||||
"elevation_m": round(geometry.road_z(geometry.half_road_left), 4),
|
||||
},
|
||||
"right": {
|
||||
"offset_m": round(-geometry.half_road, 4),
|
||||
"elevation_m": round(geometry.road_z(-geometry.half_road), 4),
|
||||
"offset_m": round(-geometry.half_road_right, 4),
|
||||
"elevation_m": round(geometry.road_z(-geometry.half_road_right), 4),
|
||||
},
|
||||
},
|
||||
"design_elevation_m": round(float(design_elevation_m), 4),
|
||||
|
||||
@@ -21,7 +21,7 @@ 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
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
|
||||
from B06_Section.B06_Section_Repository import (
|
||||
count_cross_sections,
|
||||
create_longitudinal_section,
|
||||
@@ -607,7 +607,7 @@ async def compute_cross_section_design(
|
||||
)
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
samples, design_elevation, pavement_suggested = await asyncio.to_thread(
|
||||
samples, design_elevation, pavement_suggested, cross_record = await asyncio.to_thread(
|
||||
_read_cross_design_inputs,
|
||||
project_root,
|
||||
str(longitudinal["longitudinal_file_path"]),
|
||||
@@ -626,6 +626,7 @@ async def compute_cross_section_design(
|
||||
two_stage_slope=request.two_stage_slope,
|
||||
ditch_enabled=request.ditch_enabled,
|
||||
surface_drop_m=ford_drop_at(request.chainage_m, ford_surface_drops(project_root)),
|
||||
**curve_widening_args(cross_record),
|
||||
)
|
||||
# B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
|
||||
design["status"] = "provisional"
|
||||
|
||||
@@ -9,7 +9,7 @@ from B05_Profile.B05_Profile_Engine_Sections import cross_filename
|
||||
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
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
|
||||
@@ -178,6 +178,7 @@ def enforce_pavement_ranges(
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
**curve_widening_args(section),
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
@@ -228,6 +229,7 @@ def enforce_ford_surface_drops(
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
surface_drop_m=wanted,
|
||||
**curve_widening_args(section),
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
@@ -251,7 +253,8 @@ def default_section_modes(longitudinal: dict[str, Any]) -> dict[float, str]:
|
||||
|
||||
def read_cross_design_inputs(
|
||||
project_root: Path, longitudinal_file_path: str, chainage_m: float
|
||||
) -> tuple[list[dict], float | None, bool]:
|
||||
) -> tuple[list[dict], float | None, bool, dict[str, Any]]:
|
||||
"""(지반 샘플, 계획고, 포장 제안, 측점 기록) — 측점 기록은 곡선부 확폭 입력을 담고 있다."""
|
||||
root = project_root.resolve()
|
||||
longitudinal_path = (root / longitudinal_file_path).resolve()
|
||||
if root not in longitudinal_path.parents:
|
||||
@@ -269,7 +272,7 @@ def read_cross_design_inputs(
|
||||
samples = cross.get("samples") if isinstance(cross, dict) else None
|
||||
if not isinstance(samples, list):
|
||||
raise ValueError("횡단 상세 파일 형식이 올바르지 않습니다.")
|
||||
return samples, design_elevation, suggested
|
||||
return samples, design_elevation, suggested, cross
|
||||
|
||||
|
||||
def attach_default_designs(
|
||||
@@ -298,6 +301,7 @@ def attach_default_designs(
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
**curve_widening_args(section),
|
||||
)
|
||||
design.update(status="provisional", pavement_suggested=suggested)
|
||||
section["design"] = design
|
||||
@@ -349,6 +353,7 @@ def recompute_designs_for_alignment(
|
||||
two_stage_slope=bool(stored.get("two_stage_slope", True)),
|
||||
ditch_enabled=stored.get("ditch_enabled"),
|
||||
surface_drop_m=ford_drop_at(chainage, ford_drops),
|
||||
**curve_widening_args(section),
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
@@ -408,6 +413,7 @@ def compute_default_designs(
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
surface_drop_m=ford_drop_at(chainage_m, ford_drops),
|
||||
**curve_widening_args(cross),
|
||||
)
|
||||
design["status"] = "provisional"
|
||||
design["pavement_suggested"] = suggested
|
||||
|
||||
@@ -647,6 +647,19 @@ export function appendCrossDesignOverlay(
|
||||
tick.setAttribute("class", "b06-chart__carriageway-tick");
|
||||
svg.append(tick);
|
||||
}
|
||||
// 노폭 라벨(2026-09-06 사용자 지시) — 확폭이 걸린 측점인지 눈으로 바로 알게 한다.
|
||||
// 확폭이 없으면 규격 폭만, 있으면 「4.5m (규격 3.0 + 확폭 1.5)」로 적는다.
|
||||
const widened = (design.widening_left_m ?? 0) + (design.widening_right_m ?? 0);
|
||||
const standardWidth = design.carriageway_standard_width_m;
|
||||
const label = document.createElementNS(SVG_NS, "text");
|
||||
label.setAttribute("x", String((x(edges.left.offset_m) + x(edges.right.offset_m)) / 2));
|
||||
label.setAttribute("y", String(toDisplayY(edges.left.elevation_m) - 6));
|
||||
label.setAttribute("class", "b06-chart__carriageway-label");
|
||||
label.textContent =
|
||||
widened > 0.001 && typeof standardWidth === "number"
|
||||
? `노폭 ${design.carriageway_width_m.toFixed(2)}m (규격 ${standardWidth.toFixed(2)} + 확폭 ${widened.toFixed(2)})`
|
||||
: `노폭 ${design.carriageway_width_m.toFixed(2)}m`;
|
||||
svg.append(label);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,14 @@
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
/* 노폭 라벨(2026-09-06) — 차도 위 가운데. 확폭이 걸린 측점을 눈으로 가려내는 표기다. */
|
||||
.b06-chart__carriageway-label {
|
||||
fill: var(--color-royal-amethyst);
|
||||
font-size: 9px;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 암 경계선(설계선 복사 + 오프셋): 리핑암·발파암 구간 점선 */
|
||||
.b06-chart__rock-boundary {
|
||||
fill: none;
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
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, curve_widening_args
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
DRAWING_FORMAT,
|
||||
build_cross_drawing,
|
||||
@@ -345,6 +345,7 @@ def _cross_design_line(
|
||||
design_elevation_from_longitudinal(longitudinal, float(source.get("chainage_m", 0.0))),
|
||||
ground_type="soil",
|
||||
section_mode="left_cut",
|
||||
**curve_widening_args(source),
|
||||
)
|
||||
return design["design_line"]
|
||||
except (ValueError, KeyError, OSError, json.JSONDecodeError):
|
||||
@@ -677,6 +678,7 @@ def _recompute_confirmed_design(
|
||||
ground_type=designation["ground_type"],
|
||||
section_mode=designation["section_mode"],
|
||||
ditch_side=designation.get("ditch_side"),
|
||||
**curve_widening_args(source),
|
||||
)
|
||||
design["status"] = "confirmed"
|
||||
return design
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -420,6 +420,38 @@ NATURAL_SPOIL_MIN_GROUND_SLOPE = 1.0 / 1.5
|
||||
# 위 5-3의 경로탐색(평면) 기준(FOREST_ROAD_MAX_GRADE 등)과는 별개의 값이므로
|
||||
# 서로 혼용하지 않는다.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# ── 곡선부 너비 확폭 (별표2 Ⅰ.2.나.(4) / Ⅰ.3.다.(4)) ─────────────────────
|
||||
# 평면 곡선반경 R(m) 구간별 **확대 기준(m)**. 원문은 "다음의 기준 이상으로 확대"라 이 값이
|
||||
# 하한이다. 45m 이상은 확폭하지 않는다. 경계는 "이상 ~ 미만".
|
||||
# 확폭 방향 = **곡선 바깥쪽 편측**(2026-09-06 사용자 확정 — 회전 시 차량이 밀리는 쪽).
|
||||
# 법령·교본에 방향 규정이 없어 사용자가 정한 값이며, 도면 실물로 재확인할 여지가 있다.
|
||||
CURVE_WIDENING_TABLE_M: tuple[tuple[float, float, float], ...] = (
|
||||
(10.0, 13.0, 2.25),
|
||||
(13.0, 14.0, 2.00),
|
||||
(14.0, 15.0, 1.75),
|
||||
(15.0, 18.0, 1.50),
|
||||
(18.0, 20.0, 1.25),
|
||||
(20.0, 25.0, 1.00),
|
||||
(25.0, 30.0, 0.75),
|
||||
(30.0, 40.0, 0.50),
|
||||
(40.0, 45.0, 0.25),
|
||||
)
|
||||
# 확폭을 더한 뒤의 **유효너비 상한**(m, 별표2 Ⅰ.2.다.(1) 비고 — "최대 5미터까지").
|
||||
# 넘으면 그 자리에서 자르고 화면이 경고한다.
|
||||
CURVE_WIDENING_MAX_WIDTH_M = 5.0
|
||||
|
||||
|
||||
def curve_widening_m(plan_radius_m: float | None) -> float:
|
||||
"""평면 곡선반경으로 확폭량(m)을 정한다. 직선·45m 이상·값 없음은 0."""
|
||||
if plan_radius_m is None:
|
||||
return 0.0
|
||||
radius = float(plan_radius_m)
|
||||
for low, high, widening in CURVE_WIDENING_TABLE_M:
|
||||
if low <= radius < high:
|
||||
return widening
|
||||
return 0.0
|
||||
|
||||
|
||||
FOREST_ROAD_PROFILE_CRITERIA = {
|
||||
# 설계속도(km/h)별 법정 기준
|
||||
"design_speed": {
|
||||
|
||||
Reference in New Issue
Block a user