Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_IpTable.py
T
eomsangdonandClaude Opus 5.5 1827cd8bb5 feat(b07): 평면도 종류마다 축척 고르기 — 1/600 · 1/1,000 · 1/1,200 기본 · 유역도 1/6,000 고정(119-1)
- 종류 파일 scale(없거나 목록 밖이면 1200) · 추가 · 업데이트 인자 · 목록 밖 거절
- 장 나눔 · 도곽 범위 · 배경 절취 · 축척 글 · 도곽 칸 · IP 표 · 필지 라벨 · 측점 눈금 실거리 환산이 축척 따라감(함수 인자)
- 「종류」 폼에 공용 「축척」 고르기 · 유역도 양식은 잠금 표시
- 시험 test_119_b07_plan_scale 새로 · test_b07_plan_kinds 종류 dict 에 scale

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
2026-10-08 18:29:18 +09:00

225 lines
8.9 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))
lines = -(-len(blocks) // per_line)
for order, block in enumerate(blocks):
line, column = divmod(order, per_line)
top = _MARGIN + (lines - 1 - 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_lines(
pieces: dict[str, dict[str, Any]],
rects: list[tuple[float, float, float, float]],
names: tuple[str, ...],
) -> None:
"""상자들(사업지 m · 최소 x · y · 최대 x · y) 밑 선 조각 `names` 를 상자 밖으로 잘라 낸다."""
if not rects:
return
area = unary_union([rect(*bounds) for bounds in rects])
inside = prep(area)
for piece in names:
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 _clip(
pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any, mm: float = MM
) -> None:
"""표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다."""
min_x, min_y = box[0], box[1]
width = sum(_COLUMNS) / mm
rects = [
(
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
]
clip_lines(pieces, rects, _CLIPPED)
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,
mm: float = MM,
) -> 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, mm)
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