- 축척 = 1/500 · 600 · 1,000 · 1,200(기본) · 2,500 · 3,000 · 5,000 · 6,000(지식DB 근거) - 비탈 눈금 간격 · 대피소 띄움 · 필지 버퍼 · 틈 = 1/1,200 실거리 × 축척 비(종이 거리 같게 · 1/6,000 눈금 10 m) - 글 후보가 다 막히면 기호에서 2 · 3 배 먼 자리 + 지시선 — 검증_sub8 1/6,000 겹친 쌍 55 → 4 - 시험 test_119 · test_141 늘림 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
193 lines
8.0 KiB
Python
193 lines
8.0 KiB
Python
"""B07 계획평면도 비탈 · 노선 따라가기 기하 — 노견선 · 성토 · 절토 외곽선 · 비탈 눈금(60 · 67장).
|
||
|
||
`_Engine_Cad_PlanKinds.py` 가 700줄을 넘어 떼어냄(141-1) — 파일 입출력 없음 · 좌표 = 사업지 m.
|
||
"""
|
||
|
||
import math
|
||
from bisect import bisect_left
|
||
from typing import Any
|
||
|
||
from config.config_system import DRAWING_SCALE_PLAN
|
||
|
||
#: 비탈 눈금 간격(1/1,200 실거리 m) — 긴 · 짧은 눈금이 번갈아 선다 · 다른 축척은 종이 간격이
|
||
#: 같게(1/6,000 = 10 m · 141-2)
|
||
TICK_STEP_M = 2.0
|
||
|
||
|
||
def tick_step_m(scale: int = DRAWING_SCALE_PLAN) -> float:
|
||
"""비탈 눈금 간격(실거리 m) — 종이 간격(1/1,200 의 2 m) × 축척."""
|
||
return TICK_STEP_M * scale / DRAWING_SCALE_PLAN
|
||
|
||
|
||
# ── 노선 따라가기 ─────────────────────────────────────
|
||
|
||
|
||
def route_frame(
|
||
route: list[tuple[float, float, float]], chainage: float
|
||
) -> tuple[float, float, float, float]:
|
||
"""노선 (누가거리, x, y) 위 그 누가거리 자리와 좌측 법선 (x, y, nx, ny)."""
|
||
chainages = [item[0] for item in route]
|
||
index = min(max(bisect_left(chainages, chainage), 1), len(route) - 1)
|
||
(c0, x0, y0), (c1, x1, y1) = route[index - 1], route[index]
|
||
dx, dy = x1 - x0, y1 - y0
|
||
length = math.hypot(dx, dy) or 1.0
|
||
ratio = (chainage - c0) / (c1 - c0) if c1 > c0 else 0.0
|
||
return (x0 + dx * ratio, y0 + dy * ratio, -dy / length, dx / length)
|
||
|
||
|
||
def _interp(records: list[dict[str, float]], chainage: float, key: str) -> float:
|
||
chainages = [record["chainage_m"] for record in records]
|
||
index = bisect_left(chainages, chainage)
|
||
if index <= 0:
|
||
return records[0][key]
|
||
if index >= len(records):
|
||
return records[-1][key]
|
||
before, after = records[index - 1], records[index]
|
||
span = after["chainage_m"] - before["chainage_m"]
|
||
ratio = (chainage - before["chainage_m"]) / span if span > 0 else 0.0
|
||
return before[key] + (after[key] - before[key]) * ratio
|
||
|
||
|
||
def _ground(samples: list[dict[str, Any]]) -> list[tuple[float, float]]:
|
||
return sorted(
|
||
(float(item["offset_m"]), float(item["elevation_m"]))
|
||
for item in samples
|
||
if item.get("valid", True) and isinstance(item.get("elevation_m"), (int, float))
|
||
)
|
||
|
||
|
||
def _ground_at(ground: list[tuple[float, float]], offset: float) -> float:
|
||
offsets = [x for x, _z in ground]
|
||
index = min(max(bisect_left(offsets, offset), 1), len(ground) - 1)
|
||
(x0, z0), (x1, z1) = ground[index - 1], ground[index]
|
||
return z0 + (z1 - z0) * ((offset - x0) / (x1 - x0) if x1 > x0 else 0.0)
|
||
|
||
|
||
def side_is_cut(samples: list[dict[str, Any]], edge: float, elevation: float) -> bool:
|
||
"""노견 끝에서 지반이 노면보다 높으면 그 쪽 비탈은 절토다(낮으면 성토)."""
|
||
ground = _ground(samples)
|
||
return len(ground) >= 2 and _ground_at(ground, edge) > elevation
|
||
|
||
|
||
def slope_end(
|
||
design_line: list[dict[str, Any]], samples: list[dict[str, Any]], edge: float, side: str
|
||
) -> float:
|
||
"""노견 끝에서 바깥으로 — 설계선이 원지반에 닿는 오프셋(비탈 끝). 못 닿으면 설계선 끝.
|
||
|
||
설계선(B06 횡단 설계 결과)은 비탈이 지반과 만난 뒤로 지반을 따라간다 — 그 첫 자리가 끝이다.
|
||
좌측은 오프셋이 커지는 쪽, 우측은 작아지는 쪽이 바깥이다.
|
||
"""
|
||
ground = _ground(samples)
|
||
points = sorted((float(p["offset_m"]), float(p["elevation_m"])) for p in design_line)
|
||
outward = (
|
||
[p for p in points if p[0] > edge]
|
||
if side == "left"
|
||
else [p for p in points[::-1] if p[0] < edge]
|
||
)
|
||
if not ground:
|
||
return outward[-1][0] if outward else edge
|
||
end, differs = edge, False
|
||
for offset, elevation in outward:
|
||
if abs(elevation - _ground_at(ground, offset)) <= 1e-3:
|
||
return offset if differs else end
|
||
differs, end = True, offset
|
||
return end
|
||
|
||
|
||
def _split_runs(
|
||
points: list[list[float]], flags: list[bool]
|
||
) -> tuple[list[list[list[float]]], list[list[list[float]]]]:
|
||
"""한 선을 참 · 거짓 구간으로 끊는다 — 바뀌는 자리 점은 앞뒤 구간이 나눠 가져 이어진다."""
|
||
runs: dict[bool, list[list[list[float]]]] = {True: [], False: []}
|
||
current: list[list[float]] = []
|
||
previous: bool | None = None
|
||
for point, flag in zip(points, flags):
|
||
if flag != previous:
|
||
if current:
|
||
current.append(point)
|
||
current = [point]
|
||
runs[flag].append(current)
|
||
previous = flag
|
||
else:
|
||
current.append(point)
|
||
return (
|
||
[run for run in runs[True] if len(run) >= 2],
|
||
[run for run in runs[False] if len(run) >= 2],
|
||
)
|
||
|
||
|
||
def slope_pieces(
|
||
route: list[tuple[float, float, float]],
|
||
records: list[dict[str, Any]],
|
||
start_m: float,
|
||
end_m: float,
|
||
step_m: float = TICK_STEP_M,
|
||
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]:
|
||
"""측점별 노견 끝 · 비탈 끝 오프셋에서 노견선 · 성토 외곽선 · 절토 외곽선 · 비탈 눈금을 만든다.
|
||
|
||
`records` = [{chainage_m, left_edge_m, left_end_m, right_edge_m, right_end_m,
|
||
left_cut?, right_cut?, (x, y, nx, ny)?}] — 좌(+) · 우(-) 오프셋. 측점 자리는 횡단 파일의
|
||
중심 · 좌축을 그대로 써서 측점에서 외곽선 폭이 횡단 설계 값과 같다. 측점 사이는 노선을
|
||
따라 보간한다. 비탈 끝 선은 그 쪽이 절토인지 성토인지로 나눠 두 조각에 싣는다.
|
||
"""
|
||
records = sorted(
|
||
(
|
||
{
|
||
**record,
|
||
**{f"{s}_cut": float(bool(record.get(f"{s}_cut"))) for s in ("left", "right")},
|
||
}
|
||
for record in records
|
||
),
|
||
key=lambda record: record["chainage_m"],
|
||
)
|
||
if len(route) < 2 or not records:
|
||
return {"lines": []}, {"lines": []}, {"lines": []}, {"ticks": []}
|
||
keys = ("left_edge_m", "left_end_m", "right_edge_m", "right_end_m")
|
||
by_chainage = {round(record["chainage_m"], 3): record for record in records}
|
||
samples = {
|
||
round(record["chainage_m"], 3)
|
||
for record in records
|
||
if start_m <= record["chainage_m"] <= end_m
|
||
}
|
||
step = start_m
|
||
while step <= end_m:
|
||
samples.add(round(step, 3))
|
||
step += step_m
|
||
lines: dict[str, list[list[float]]] = {key: [] for key in keys}
|
||
cut: dict[str, list[bool]] = {"left": [], "right": []}
|
||
ticks: list[list[list[float]]] = []
|
||
for index, chainage in enumerate(sorted(samples)):
|
||
record = by_chainage.get(chainage)
|
||
if record and all(k in record for k in ("x", "y", "nx", "ny")):
|
||
x, y, nx, ny = record["x"], record["y"], record["nx"], record["ny"]
|
||
else:
|
||
x, y, nx, ny = route_frame(route, chainage)
|
||
offsets = {key: _interp(records, chainage, key) for key in keys}
|
||
points = {
|
||
key: [round(x + nx * value, 4), round(y + ny * value, 4)]
|
||
for key, value in offsets.items()
|
||
}
|
||
for key in keys:
|
||
lines[key].append(points[key])
|
||
for side in cut:
|
||
cut[side].append(_interp(records, chainage, f"{side}_cut") > 0.5)
|
||
# 눈금은 노견 끝에서 비탈 끝으로 — 긴 것 · 짧은 것이 번갈아 선다(납품 도면 관례).
|
||
for side in ("left", "right"):
|
||
edge, end = offsets[f"{side}_edge_m"], offsets[f"{side}_end_m"]
|
||
if abs(end - edge) < 0.05:
|
||
continue
|
||
reach = end if index % 2 == 0 else edge + (end - edge) / 2.0
|
||
ticks.append([points[f"{side}_edge_m"], [x + nx * reach, y + ny * reach]])
|
||
cut_lines: list[list[list[float]]] = []
|
||
fill_lines: list[list[list[float]]] = []
|
||
for side in cut:
|
||
cut_runs, fill_runs = _split_runs(lines[f"{side}_end_m"], cut[side])
|
||
cut_lines += cut_runs
|
||
fill_lines += fill_runs
|
||
return (
|
||
{"lines": [lines["left_edge_m"], lines["right_edge_m"]]},
|
||
{"lines": fill_lines},
|
||
{"lines": cut_lines},
|
||
{"ticks": ticks},
|
||
)
|