Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_IpTable.py
T
eomsangdonandClaude Opus 5.5 1c1226eb4c feat(B07): 평면도 IP 표 조각 — BP · IP · EP 자료 · 장 도곽 왼쪽 아래 표 · 표 밑 선 자름(94-1 · 94-2)
- 새 Engine_Cad_IpTable — IA 곡선 접선 재계산 · TL · CL · SL · 도-분-초 · X 북 · Y 동 · 장마다 그 장 IP
- PlanKinds 조각 ip_table 한 줄 · 그리기 갈래 · 노선배치도 기본 체크
- Router_Support_Plan build_pieces 한 갈래
- 시험 — 예시 검산 · 장 경계 · 잘림 · DXF

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
2026-10-08 13:45:35 +09:00

208 lines
8.3 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 평면도 IP 표 — 곡선표에서 BP · IP · EP 자료를 셈해 장 도곽 왼쪽 아래 표로(PLAN 94장).
IP 는 곡선이 있는 것만 · 번호는 평면도 「IPn」 과 같은 곡선표 차례다. IA 는 저장 내각(소수 2자리)
대신 곡선 자기 접선(시점→교점 · 교점→종점 방향각)으로 다시 셈하고, TL · CL · SL 은 R 과 그 IA 로
셈한다. 좌표는 측량 관례대로 X = 북(y) · Y = 동(x).
표 자리(종이 mm)는 자료를 만들 때 정한다 — 표 밑 등고선 · 노선을 같은 자리로 잘라 내야 해서다.
"""
import math
from typing import Any
import numpy as np
from shapely.geometry import LineString, MultiLineString
from shapely.geometry import box as rect
from shapely.ops import unary_union
from shapely.prepared import prep
from B05_Profile.B05_Profile_Engine_Sections_Core import (
DESIGN_CURVE_MATCH_TOLERANCE_M,
_project_chainage,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import table_entity
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import _FONT_SIZE, MM
LAYER_ID = "b07-plan-ip_table"
LINE_COLOR = "#ff4d4d" # 예시 표 — 선 빨강 · 글 파랑
TEXT_COLOR = "#4d7dff"
_COLUMNS = (7.0, 6.0, 23.0) # [머리 | X·Y | 값] 종이 mm
_ROW_H = 5.0
_MARGIN = 5.0 # 도곽 왼쪽 아래 모서리에서 띄움
_LINE_GAP = 3.0 # 한 줄 넘쳐 위로 올린 줄 사이
_IP_ROWS = 8 # IP · CO(X · Y) · IA · R · TL · CL · SL
_END_ROWS = 3 # BP · EP = IP · CO 만
_VALUE_ROWS = (("I A", "ia"), ("R", "r"), ("T L", "tl"), ("C L", "cl"), ("S L", "sl"))
_CLIPPED = ("contour", "stream", "road_edge", "fill_outline", "cut_outline", "centerline")
def intersection_angle(start: Any, apex: Any, end: Any) -> float:
"""교각 IA(도) — 시점→교점 · 교점→종점 방향각 차."""
first = math.atan2(apex[1] - start[1], apex[0] - start[0])
second = math.atan2(end[1] - apex[1], end[0] - apex[0])
return abs(math.degrees((second - first + math.pi) % (2 * math.pi) - math.pi))
def curve_values(radius: float, ia_deg: float) -> dict[str, float]:
"""TL = R·tan(IA/2) · CL = R·IA(rad) · SL = R·(sec(IA/2) − 1)."""
half = math.radians(ia_deg) / 2.0
return {
"tl": radius * math.tan(half),
"cl": radius * 2.0 * half,
"sl": radius * (1.0 / math.cos(half) - 1.0),
}
def dms(degrees: float) -> str:
"""도-분-초 「94-59-58.38」 — 분 · 초 두 자리 · 초는 0.01″ 로 반올림해 60.00 이면 올림."""
hundredths = round(degrees * 360000)
whole, rest = divmod(hundredths, 360000)
minutes, seconds = divmod(rest, 6000)
return f"{whole}-{minutes:02d}-{seconds // 100:02d}.{seconds % 100:02d}"
def ip_blocks(
curves: list[dict[str, Any]],
route: list[tuple[float, float, float]],
number: int,
total: int,
start_m: float,
end_m: float,
) -> list[dict[str, Any]]:
"""그 장의 표 묶음 [BP?, IP…, EP?] — 곡선 가운데 누가거리가 그 장 [시작, 끝) 안인 것만.
장 경계 측점은 두 장이 나눠 가지므로 끝은 열어 둔다(마지막 장만 닫음) — 한 IP 는 한 장에만.
노선에서 5 m 넘게 떨어진 옛 곡선은 버린다(B05 곡선 확폭과 같은 허용).
"""
if len(route) < 2:
return []
points = np.asarray([(x, y) for _c, x, y in route], dtype=np.float64)
chainage = np.asarray([c for c, _x, _y in route], dtype=np.float64)
blocks: list[dict[str, Any]] = []
if number == 1:
blocks.append({"name": "BP", "x": route[0][1], "y": route[0][2]})
for index, curve in enumerate(curves, start=1):
start, apex, end = curve.get("start"), curve.get("apex"), curve.get("end")
radius = curve.get("radius_m")
if not (start and apex and end) or not isinstance(radius, (int, float)):
continue
start_gap, start_c = _project_chainage(points, chainage, tuple(start[:2]))
end_gap, end_c = _project_chainage(points, chainage, tuple(end[:2]))
if max(start_gap, end_gap) > DESIGN_CURVE_MATCH_TOLERANCE_M:
continue
middle = (start_c + end_c) / 2.0
if (number > 1 and middle < start_m) or (number < total and middle >= end_m):
continue
ia = intersection_angle(start, apex, end)
blocks.append(
{
"name": str(index),
"x": float(apex[0]),
"y": float(apex[1]),
"ia": ia,
"r": float(radius),
**curve_values(float(radius), ia),
}
)
if number == total:
blocks.append({"name": "EP", "x": route[-1][1], "y": route[-1][2]})
return blocks
def _place(blocks: list[dict[str, Any]], width_mm: float) -> None:
"""묶음마다 표 왼쪽 위 자리(종이 mm) `origin` — 왼쪽 아래부터 나란히 · 넘치면 위로 다음 줄."""
block_w = sum(_COLUMNS)
per_line = max(1, int((width_mm - 2 * _MARGIN) // block_w))
for order, block in enumerate(blocks):
line, column = divmod(order, per_line)
top = _MARGIN + line * (_IP_ROWS * _ROW_H + _LINE_GAP) + _IP_ROWS * _ROW_H
block["origin"] = [_MARGIN + column * block_w, top]
block["rows"] = _END_ROWS if "ia" not in block else _IP_ROWS
def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any) -> None:
"""표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다."""
min_x, min_y = box[0], box[1]
width = sum(_COLUMNS) / MM
area = unary_union(
[
rect(
min_x + b["origin"][0] / MM,
min_y + b["origin"][1] / MM - b["rows"] * _ROW_H / MM,
min_x + b["origin"][0] / MM + width,
min_y + b["origin"][1] / MM,
)
for b in blocks
]
)
inside = prep(area)
for piece in _CLIPPED:
lines = (pieces.get(piece) or {}).get("lines")
if not lines:
continue
kept: list[list[list[float]]] = []
for points in lines:
if len(points) < 2 or not inside.intersects(LineString(points)):
kept.append(points)
continue
rest = LineString(points).difference(area)
parts = rest.geoms if isinstance(rest, MultiLineString) else [rest]
kept += [
[[round(x, 4), round(y, 4)] for x, y in part.coords]
for part in parts
if isinstance(part, LineString) and len(part.coords) >= 2
]
pieces[piece]["lines"] = kept
def ip_table_piece(
pieces: dict[str, dict[str, Any]],
curves: list[dict[str, Any]],
route: list[tuple[float, float, float]],
box: tuple[float, float, float, float],
chunk: dict[str, Any],
total: int,
) -> dict[str, Any]:
"""그 장 IP 표 조각 {blocks} — 자리를 정하고 같은 자리 밑 선 조각을 `pieces` 에서 잘라 낸다."""
blocks = ip_blocks(
curves,
route,
int(chunk["number"]),
total,
float(chunk["start_m"]),
float(chunk["end_m"]),
)
_place(blocks, (box[2] - box[0]) * MM)
if blocks:
_clip(pieces, blocks, box)
return {"blocks": blocks}
def ip_table_entities(drawing_id: str, data: dict[str, Any]) -> list[dict[str, Any]]:
"""묶음마다 표 하나 — 행 IP · CO(X · Y 2행 병합) · IA · R · TL · CL · SL(값 2 · 3열 병합)."""
out = []
for block in data.get("blocks") or []:
cells: list[list[dict[str, Any] | None]] = [
[{"text": "I P"}, {"text": block["name"], "colSpan": 2}, None],
[{"text": "C O", "rowSpan": 2}, {"text": "X"}, {"text": f"{block['y']:.5f}"}],
[None, {"text": "Y"}, {"text": f"{block['x']:.5f}"}],
]
if "ia" in block:
for label, key in _VALUE_ROWS:
value = dms(block[key]) if key == "ia" else f"{block[key]:.5f}"
cells.append([{"text": label}, {"text": value, "colSpan": 2}, None])
out.append(
table_entity(
f"{drawing_id}:ip_table:{block['name']}",
tuple(block["origin"]),
list(_COLUMNS),
[_ROW_H] * len(cells),
cells,
LAYER_ID,
LINE_COLOR,
_FONT_SIZE,
TEXT_COLOR,
)
)
return out