- 장 나눔 = 추가 · 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>
226 lines
9.0 KiB
Python
226 lines
9.0 KiB
Python
"""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
|