Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"""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
|
||||
@@ -21,6 +21,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_text_entity,
|
||||
polyline_entity,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import ip_table_entities
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
_COMPASS_MARGIN,
|
||||
_COMPASS_SIZE,
|
||||
@@ -54,6 +55,7 @@ PIECES: tuple[tuple[str, str, bool], ...] = (
|
||||
("centerline", "중심선", False),
|
||||
("ip", "IP", False),
|
||||
("radius", "R", False),
|
||||
("ip_table", "IP 표", False),
|
||||
("station", "측점 NO", False),
|
||||
("endpoints", "시종점", False),
|
||||
("refuge", "대피소 · 사토장", False),
|
||||
@@ -73,7 +75,7 @@ DEFAULT_KINDS: tuple[dict[str, Any], ...] = (
|
||||
{
|
||||
"id": "route",
|
||||
"name": "노선배치도",
|
||||
"pieces": [p for p in _TERRAIN if p != "radius"] + ["station", "endpoints"],
|
||||
"pieces": [p for p in _TERRAIN if p != "radius"] + ["station", "endpoints", "ip_table"],
|
||||
},
|
||||
{
|
||||
"id": "layout",
|
||||
@@ -411,6 +413,8 @@ def _piece_entities(
|
||||
elif piece == "radius":
|
||||
for index, item in enumerate(data.get("items") or []):
|
||||
text(f"{index}", item["label"], paper(item["at"]), _LABEL_FONT)
|
||||
elif piece == "ip_table":
|
||||
out.extend(ip_table_entities(drawing_id, data))
|
||||
elif piece == "station":
|
||||
for index, item in enumerate(data.get("items") or []):
|
||||
cx, cy = paper((item["x"], item["y"]))
|
||||
|
||||
@@ -24,6 +24,7 @@ from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import infer_station_interval, station_no_label
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import ip_table_piece
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
_structure_label,
|
||||
plan_area_mm,
|
||||
@@ -411,6 +412,10 @@ def build_pieces(
|
||||
pieces["lidar"] = {"image": image, "box": list(image_box)}
|
||||
except (FileNotFoundError, ValueError, OSError):
|
||||
pieces["lidar"] = {"image": None, "box": None}
|
||||
if "ip_table" in wanted:
|
||||
pieces["ip_table"] = ip_table_piece(
|
||||
pieces, _load_design_curves(project_root), route, box, chunk, total
|
||||
)
|
||||
|
||||
folder = _design_root(project_root) / "plan_pieces" / str(number)
|
||||
for piece, data in pieces.items():
|
||||
|
||||
@@ -6,21 +6,30 @@
|
||||
④ 고른 조각만 도면 층으로 얹힘
|
||||
⑤ 조각 잘게(67장) — 노견선 · 성토 외곽선 · 절토 외곽선 따로 · 기본 넷 처음 체크 = 옛 그림 ·
|
||||
옛 종류 파일의 slope_outline 펼침 · 종류 업데이트(기본 넷 포함) · 확정본 풀림 · 배수시설 기호
|
||||
⑥ IP 표(94장) — 예시 검산 · 도-분-초 올림 · 장마다 그 장 IP 한 번 · 표 밑 선 자름 · 층
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import ezdxf
|
||||
import pytest
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import (
|
||||
curve_values,
|
||||
dms,
|
||||
ip_blocks,
|
||||
ip_table_piece,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
|
||||
DEFAULT_KINDS,
|
||||
build_plan_kind_drawing,
|
||||
layer_id,
|
||||
slope_pieces,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import drawing_to_dxf
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest, _write_manifest
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
_kinds_dir,
|
||||
@@ -230,3 +239,61 @@ def test_drainage_symbol_piece() -> None:
|
||||
mine = [e for e in drawing["entities"] if e.get("layerId") == layer_id("drainage")]
|
||||
assert {e["type"] for e in mine} >= {"Text"} and len(mine) >= 2
|
||||
assert any(e["type"] == "Text" and e["shapeData"]["label"] == "D800" for e in mine)
|
||||
|
||||
|
||||
def test_ip_table_example_values_and_dms() -> None:
|
||||
ia = 94 + 59 / 60 + 58.38 / 3600 # 예시 그림 IP1
|
||||
values = curve_values(15.0, ia)
|
||||
assert dms(ia) == "94-59-58.38"
|
||||
assert [f"{values[k]:.5f}" for k in ("tl", "cl", "sl")] == ["16.36950", "24.87082", "7.20271"]
|
||||
assert dms(1 + 59 / 60 + 59.9999 / 3600) == "2-00-00.00" # 초 60.00 올림
|
||||
assert dms(5 + 3 / 60 + 7.2 / 3600) == "5-03-07.20"
|
||||
|
||||
|
||||
# ㄱ자 노선 — (0,0)→(100,0)→(100,100) · 모서리 R 15 곡선(가운데 누가거리 100) · 앞에 노선 밖 옛 곡선
|
||||
_ROUTE = [(0.0, 0.0, 0.0), (85.0, 85.0, 0.0), (115.0, 100.0, 15.0), (200.0, 100.0, 100.0)]
|
||||
_CURVES = [
|
||||
{"apex": [500.0, 500.0], "start": [490.0, 500.0], "end": [500.0, 510.0], "radius_m": 10.0},
|
||||
{"apex": [100.0, 0.0], "start": [85.0, 0.0], "end": [100.0, 15.0], "radius_m": 15.0},
|
||||
]
|
||||
|
||||
|
||||
def test_ip_blocks_one_sheet_each_and_ends() -> None:
|
||||
first = ip_blocks(_CURVES, _ROUTE, 1, 2, 0.0, 100.0)
|
||||
second = ip_blocks(_CURVES, _ROUTE, 2, 2, 100.0, 200.0)
|
||||
assert [b["name"] for b in first] == ["BP"] # 경계 IP 는 뒤 장에만
|
||||
assert [b["name"] for b in second] == ["2", "EP"] # 번호 = 곡선표 차례(옛 곡선은 빠짐)
|
||||
ip = second[0]
|
||||
assert dms(ip["ia"]) == "90-00-00.00" and ip["tl"] == pytest.approx(15.0)
|
||||
assert [b["name"] for b in ip_blocks(_CURVES, _ROUTE, 1, 1, 0.0, 200.0)] == ["BP", "2", "EP"]
|
||||
|
||||
|
||||
def test_ip_table_drawing_clips_lines_under_table(tmp_path: Path) -> None:
|
||||
box = (-20.0, -20.0, 300.0, 200.0)
|
||||
contour = [[-20.0, 0.0], [300.0, 0.0]] # 표(왼쪽 아래 · 높이 48 m) 밑을 지나는 등고선
|
||||
pieces = {
|
||||
"contour": {"lines": [contour]},
|
||||
"centerline": {"lines": [[[0.0, 100.0], [9.0, 100.0]]]},
|
||||
}
|
||||
pieces["ip_table"] = ip_table_piece(
|
||||
pieces, _CURVES, _ROUTE, box, {"number": 1, "start_m": 0.0, "end_m": 200.0}, 1
|
||||
)
|
||||
# IP 표 칸(x 29.2 ~ 72.4 · y -14 ~ 34)만 빠짐 · BP · EP 칸(3행)은 y 0 위라 안 자름
|
||||
assert pieces["contour"]["lines"] == [[[-20.0, 0.0], [29.2, 0.0]], [[72.4, 0.0], [300.0, 0.0]]]
|
||||
assert pieces["centerline"]["lines"] == [[[0.0, 100.0], [9.0, 100.0]]] # 표 밖은 그대로
|
||||
drawing = build_plan_kind_drawing("plan_ip", "시험", box, pieces)
|
||||
mine = [e for e in drawing["entities"] if e.get("layerId") == layer_id("ip_table")]
|
||||
assert [e["type"] for e in mine] == ["Table"] * 3
|
||||
bp, ip, _ep = (e["shapeData"] for e in mine)
|
||||
assert len(bp["rowHeights"]) == 3 and len(ip["rowHeights"]) == 8
|
||||
assert bp["cells"][1][2]["text"] == "0.00000" and ip["cells"][1][2]["text"] == "0.00000"
|
||||
assert ip["cells"][2][2]["text"] == "100.00000" # Y = 동(x)
|
||||
assert ip["cells"][3][1]["text"] == "90-00-00.00"
|
||||
assert {layer["id"] for layer in drawing["layers"]} >= {layer_id("ip_table")}
|
||||
assert "ip_table" in next(k for k in DEFAULT_KINDS if k["id"] == "route")["pieces"]
|
||||
# DXF — 표 → 선 + 글(IP 묶음: 바깥 4 · 세로 10 · 가로 20 · 글 17)
|
||||
raw, skipped = drawing_to_dxf({"entities": [mine[1]], "layers": drawing["layers"]})
|
||||
(tmp_path / "ip.dxf").write_bytes(raw)
|
||||
space = ezdxf.readfile(tmp_path / "ip.dxf").modelspace()
|
||||
assert skipped == {} and len(space.query("LINE")) == 34 and len(space.query("TEXT")) == 17
|
||||
assert {"90-00-00.00", "15.00000"} <= {t.dxf.text for t in space.query("TEXT")}
|
||||
|
||||
Reference in New Issue
Block a user