Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_PlanSlope.py
T
eomsangdonandClaude Opus 5.5 f05d77d642 feat(B07): 평면도 축척 여덟 · 실거리 상수 축척화 · 막힌 글 먼 자리(141-2)
- 축척 = 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
2026-10-09 15:10:02 +09:00

193 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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},
)