Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_LongPlan.py
T
eomsangdonandClaude Opus 5.5 1398625d2d feat(B07): 종단도 장 = 일반측점 30 간격 600 m · Y축 여유 10 mm · 그림 자리 +50 mm (525-1 · 요구 343-3)
- 장 나눔 = 추가 · MC 측점 안 세고 일반측점 30 간격(20 m 면 600 m) · 경계 측점 나눠 가짐 · 넘치면 줄이는 규칙 그대로
- Y축 · 측점표 머리 = 첫 측점 왼쪽 10 mm
- 그림 = 작도 영역 좌하단에서 오른쪽 50 · 위 50 mm(도각 · 평면 칸 · 넘침 판정이 같은 장 bbox)
- 장 나눔 · 도각을 Engine_Cad_LongSheet.py 로 뗌(Engine_Cad_Long.py 700줄 한도)
- 옛 종단 확정본 풂 = 칸 층 찾기 → 매니페스트 long_sheet_rule(2) 비교 — 521-2 뒤 확정본도 자리가 바뀌어 한 번 더 풂 · 낡은 시험(칸 층 기준) 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 14:25:57 +09:00

226 lines
9.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 종단도 우측 상단 평면 위치도(521-2 · 요구 340-2) — 장마다 작도 영역 우상단 칸 하나.
칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm(작도 영역 좌하단 = 장 bbox 좌하단 = 그림 좌하단
− 50 mm · `LongSheet._sheet_bbox` · `frame_entities` bottom_left). 척도 = 종단 축척 `plan` 칸
(None = 자동). 방위표만 도각 층.
- 자동: 전체 노선을 칸에 맞춤(분모 500 단위 올림) · 전체 노선 가는 선 · 이 장 구간 굵게.
- 척도 정함: 이 장 중심 확대 · 이 장 굵게 · 밖 노선 흐린 파선 · 칸 밖은 잘림.
칸 내용 = 노선 · 이 장 시 · 종점 측점 글 · 방위표 · 「평면도 S=1/n」 · 「n/N 장」.
좌표 = 종단 측점 center_x · center_y — 없으면 칸 안 그림. 서버 단독(CLAUDE.md 5장 ③).
"""
import math
from collections.abc import Iterator
from typing import Any
from shapely.geometry import LineString, Point
from shapely.geometry import box as shapely_box
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
DESIGN_COLOR,
TABLE_LABEL_COLOR,
TABLE_LINE_COLOR,
_stations,
_text_entity,
infer_station_interval,
polyline_entity,
station_no_label,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanLabels import text_rect
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
compass_entities,
drawing_scale,
usable_area,
)
LONG_PLAN_LAYER_ID = "b07-long-plan"
BOX_W, BOX_H = 150.0, 100.0 # 칸 크기(종이 mm)
_INSET = 5.0 # 작도 영역 테두리에서 안쪽
_MAP_PAD = (4.0, 10.0, 4.0, 4.0) # 칸 안 노선 자리 여백(좌 · 하 · 우 · 상) — 아래는 글 줄
_AUTO_STEP = 500 # 자동 분모 올림 단위
_FONT = 2.5
_COMPASS = 12.0
_ROUTE_COLOR = "#7d8590" # 전체 · 밖 노선 — 흐리게
def plan_box(content_bbox: tuple[float, float, float, float]) -> tuple[float, float, float, float]:
"""칸 (x0, y0, x1, y1) — 그림 좌표(작도 영역 좌하단 = 장 bbox 좌하단)."""
usable_w, usable_h = usable_area()
x1 = content_bbox[0] + usable_w - _INSET
y1 = content_bbox[1] + usable_h - _INSET
return (x1 - BOX_W, y1 - BOX_H, x1, y1)
def _plan_points(longitudinal: dict[str, Any]) -> list[tuple[float, float, float]]:
"""종단 측점의 (누가거리 m, x, y) — `Router_Support_Basin.plan_stations` 와 같은 셈."""
points: list[tuple[float, float, float]] = []
for station in _stations(longitudinal):
x, y = station.get("center_x"), station.get("center_y")
if all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in (x, y)):
points.append((float(station["chainage_m"]), float(x), float(y)))
return sorted(points)
def _shapes(entities: list[dict[str, Any]]) -> Iterator[Any]:
"""엔티티 → shapely 도형(선 · 글 사각 · 원) — 칸 겹침 판정용."""
for entity in entities:
yield from _shapes(entity.get("children") or [])
shape = entity.get("shapeData") or {}
if "startPoint" in shape:
a, b = shape["startPoint"], shape["endPoint"]
yield LineString([(a["x"], a["y"]), (b["x"], b["y"])])
elif "basePoint" in shape:
base, options = shape["basePoint"], shape.get("options") or {}
direction = options.get("textDirection") or {}
yield shapely_box(
*text_rect(
str(shape.get("label") or ""),
base["x"],
base["y"],
float(options.get("fontSize", _FONT)),
str(options.get("textAlign", "center")),
(float(direction.get("x", 1.0)), float(direction.get("y", 0.0))),
)
)
elif "center" in shape:
center = shape["center"]
yield Point(center["x"], center["y"]).buffer(float(shape.get("radius", 0.0)))
def plan_box_overlaps(
longitudinal: dict[str, Any],
entities: list[dict[str, Any]],
content_bbox: tuple[float, float, float, float] | None,
) -> bool:
"""그래프 · 측점표가 칸을 덮는가 — 칸을 안 그리는 종단(좌표 없음)은 늘 거짓."""
if content_bbox is None or len(_plan_points(longitudinal)) < 2:
return False
x0, y0, x1, y1 = plan_box(content_bbox)
if content_bbox[2] < x0 or content_bbox[3] < y0:
return False
region = shapely_box(x0, y0, x1, y1)
return any(region.intersects(shape) for shape in _shapes(entities))
def _auto_denominator(points: list[tuple[float, float, float]], map_w: float, map_h: float) -> int:
width = max(p[1] for p in points) - min(p[1] for p in points)
height = max(p[2] for p in points) - min(p[2] for p in points)
need = max(width / map_w, height / map_h) * 1000.0
return max(_AUTO_STEP, math.ceil(need / _AUTO_STEP) * _AUTO_STEP)
def _clipped(
drawing_id: str,
points: list[tuple[float, float]],
region: Any,
suffix: str,
color: str,
dash: list[int] | None = None,
width: int = 1,
) -> list[dict[str, Any]]:
"""종이 점열을 칸 노선 자리로 잘라 PolyLine 들로."""
if len(points) < 2:
return []
clipped = LineString(points).intersection(region)
parts = getattr(clipped, "geoms", [clipped])
out = []
for index, part in enumerate(parts):
if isinstance(part, LineString) and not part.is_empty:
line = polyline_entity(
drawing_id,
list(part.coords),
LONG_PLAN_LAYER_ID,
color,
suffix=f"{suffix}:{index}",
dash=dash,
width=width,
)
if line:
out.append(line)
return out
def long_plan_entities(
longitudinal: dict[str, Any],
drawing_id: str,
chunk: dict[str, Any],
content_bbox: tuple[float, float, float, float] | None,
) -> list[dict[str, Any]]:
"""이 장의 평면 위치도 칸 — 좌표 없거나 그림이 비면 빈 목록."""
points = _plan_points(longitudinal)
if content_bbox is None or len(points) < 2:
return []
start_m, end_m = chunk.get("start_m"), chunk.get("end_m")
if start_m is None or end_m is None:
start_m, end_m = points[0][0], points[-1][0]
inside = [p for p in points if start_m - 1e-6 <= p[0] <= end_m + 1e-6]
x0, y0, x1, y1 = plan_box(content_bbox)
left, bottom, right, top = _MAP_PAD
map_box = (x0 + left, y0 + bottom, x1 - right, y1 - top)
map_w, map_h = map_box[2] - map_box[0], map_box[3] - map_box[1]
fixed = drawing_scale("longitudinal").get("plan")
denominator = fixed or _auto_denominator(points, map_w, map_h)
focus = inside if fixed and inside else points
center_x = (min(p[1] for p in focus) + max(p[1] for p in focus)) / 2.0
center_y = (min(p[2] for p in focus) + max(p[2] for p in focus)) / 2.0
mm = 1000.0 / denominator
box_cx, box_cy = (map_box[0] + map_box[2]) / 2.0, (map_box[1] + map_box[3]) / 2.0
def paper(x: float, y: float) -> tuple[float, float]:
return (box_cx + (x - center_x) * mm, box_cy + (y - center_y) * mm)
region = shapely_box(*map_box)
border = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]
entities = [polyline_entity(drawing_id, border, LONG_PLAN_LAYER_ID, TABLE_LINE_COLOR, ":box")]
entities += _clipped(
drawing_id,
[paper(x, y) for _c, x, y in points],
region,
":all",
_ROUTE_COLOR,
dash=[3, 3] if fixed else None,
)
entities += _clipped(
drawing_id, [paper(x, y) for _c, x, y in inside], region, ":this", DESIGN_COLOR, width=3
)
interval_m = infer_station_interval(_stations(longitudinal))
for name, (chainage, x, y) in (("start", inside[0]), ("end", inside[-1])) if inside else ():
px, py = paper(x, y)
if region.contains(Point(px, py)):
entities.append(
_text_entity(
f"{drawing_id}:lplan:{name}",
station_no_label(chainage, interval_m),
px,
py + _FONT,
LONG_PLAN_LAYER_ID,
_FONT,
TABLE_LABEL_COLOR,
)
)
number, total = int(chunk.get("number") or 1), int(chunk.get("total") or 1)
for name, label, x, align in (
("scale", f"평면도 S=1/{denominator:,}", x0 + left, "left"),
("page", f"{number}/{total} 장", x1 - right, "right"),
):
entities.append(
_text_entity(
f"{drawing_id}:lplan:{name}",
label,
x,
y0 + bottom / 2.0,
LONG_PLAN_LAYER_ID,
_FONT,
TABLE_LABEL_COLOR,
align=align,
)
)
# 방위표 = 다른 평면 도면처럼 도각 층(잠금)
compass = compass_entities(
f"{drawing_id}:lplan",
(x0 + left + _COMPASS / 2.0, y1 - top - _COMPASS / 2.0),
_COMPASS,
)
return [entity for entity in entities if entity] + compass