feat(B05): 곡선 생략 규칙을 쓰지 않음 — 꺾이는 자리에는 늘 곡선

사용자 지시(2026-09-07) — 「내각 155 이상이면 곡선 생략은 반영하지 말자.
사용자가 계획 평면 노선을 수정할 때 문제가 될 것 같아.」

곡선이 있다 없다 하면 잡는 손잡이도 있다 없다 하고, 조금 폈다는 이유로
곡선이 사라지면 되돌릴 길이 없음. 그래서 별표2 의 155° 생략 규칙을 쓰지 않고
꺾이는 자리에는 전부 곡선을 둠. 상수는 법정 근거를 잃지 않게 남겨만 둠.

실측(용화 예상노선 331점) — 곡선 13 → 26곳, 정점 145 → 181,
반지름 12~28 → 12~199m, 내각 109~177°.
벗어남은 그대로 최대 3.57m · 평균 1.081 → 1.102m (원본 충실도 유지).

시험 하나를 뒤집음 — 「펴진 자리는 곡선을 안 둔다」 → 「펴진 자리에도 곡선을
둔다」. 아주 완만하므로 원본과 거의 겹치는 것도 함께 확인.
pytest 420 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 08:06:00 +09:00
co-authored by Claude Opus 5
parent e593378d33
commit 9626d9e8c7
+12 -31
View File
@@ -48,7 +48,12 @@ MAX_REFINE_DEPTH = 3
# 원호를 몇 도마다 한 점씩 찍을지 — 촘촘할수록 매끄럽지만 정점이 늘어난다.
ARC_STEP_DEG = 5.0
# 이 값 이상으로 펴진 자리는 곡선을 두지 않는다(별표2: 내각 155° 이상).
# 별표2 가 「곡선을 두지 않을 수 있다」고 하는 내각(도) — **지금은 쓰지 않는다.**
#
# 왜 안 쓰나(2026-09-07 사용자 지시) — 「내각 155 이상이면 곡선 생략은 반영하지 말자.
# 사용자가 계획 평면 노선을 수정할 때 문제가 될 것 같아.」 곡선이 있다 없다 하면 잡는
# 손잡이도 있다 없다 하고, 조금 폈다는 이유로 곡선이 사라지면 되돌릴 길이 없다. 그래서
# **꺾이는 자리에는 늘 곡선을 둔다.** 값은 법정 근거를 잃지 않게 남겨만 둔다.
STRAIGHT_INNER_ANGLE_DEG = 155.0
# 한 곡선으로 **묶을지** 볼 때 쓰는 문턱(도) — 위 155° 와 쓰임이 다르다.
@@ -442,7 +447,7 @@ def _line_intersection(
def _curve_runs(
cleaned: list[tuple[float, float]], straight_inner_angle_deg: float
cleaned: list[tuple[float, float]],
) -> list[tuple[int, int]]:
"""**한 곡선으로 묶을 꺾임점 구간**을 고른다 — `[(첫 꺾임점, 끝 꺾임점)]`.
@@ -475,20 +480,8 @@ def _curve_runs(
):
break
end = following
# 별표2 의 155° 는 **묶은 뒤 전체 교각**에 적용한다 — 앞뒤 직선이 이루는 내각이
# 그만큼 펴져 있으면 곡선을 두지 않는다.
apex = (
cleaned[index]
if index == end
else _line_intersection(
cleaned[index - 1], cleaned[index], cleaned[end], cleaned[end + 1]
)
)
total_inner = (
180.0 if apex is None else _inner_angle_deg(cleaned[index - 1], apex, cleaned[end + 1])
)
if total_inner < straight_inner_angle_deg:
runs.append((index, end))
# 곡선을 생략하지 않는다(2026-09-07 사용자 지시) — 꺾이는 자리는 전부 곡선이다.
runs.append((index, end))
index = end + 1
return runs
@@ -547,7 +540,6 @@ def _split_wide_runs(
deduped: list[tuple[float, float]],
node_indices: list[int],
min_radius_m: float,
straight_inner_angle_deg: float = STRAIGHT_INNER_ANGLE_DEG,
) -> list[tuple[int, int]]:
"""묶는 것이 **손해면 도로 쪼갠다** — 한 곡선으로 펴서 원본에서 더 멀어지면 안 묶는다.
@@ -580,15 +572,7 @@ def _split_wide_runs(
if merged_mean <= apart_mean and merged_max <= max(apart_max, MERGE_MAX_GAP_M):
result.append((first, last))
else:
# 쪼갤 때 **별표2 의 155° 를 다시 본다** — 묶여 있을 때는 전체 교각이 기준을
# 넘었더라도, 낱개로 떼면 펴진 자리(내각 155° 이상)가 나온다. 다시 안 보면
# 그런 자리에도 곡선이 생긴다(2026-09-07 실측: 내각 159° 에 R 12m 원호).
result.extend(
(index, index)
for index in range(first, last + 1)
if _inner_angle_deg(cleaned[index - 1], cleaned[index], cleaned[index + 1])
< straight_inner_angle_deg
)
result.extend((index, index) for index in range(first, last + 1))
return result
@@ -597,7 +581,6 @@ def build_planned_polyline(
*,
min_radius_m: float,
hairpin_min_radius_m: float = 10.0,
straight_inner_angle_deg: float = STRAIGHT_INNER_ANGLE_DEG,
simplify: bool = True,
) -> PlannedPolyline:
"""점 묶음을 계획노선 폴리라인으로 바꾼다.
@@ -629,11 +612,9 @@ def build_planned_polyline(
cleaned[index - 1], cleaned[index], cleaned[index + 1]
)
runs = _curve_runs(cleaned, straight_inner_angle_deg)
runs = _curve_runs(cleaned)
if node_indices is not None:
runs = _split_wide_runs(
runs, cleaned, deduped, node_indices, min_radius_m, straight_inner_angle_deg
)
runs = _split_wide_runs(runs, cleaned, deduped, node_indices, min_radius_m)
vertices: list[tuple[float, float]] = [cleaned[0]]
curves: list[RouteCurve] = []
cursor = 0 # 아직 선에 안 실은 첫 꺾임점