- 새 Engine_Cad_LongPlan — 작도 영역 우상단 5 mm 안쪽 150×100 mm 칸 · 자동 = 전체 노선 맞춤(분모 500 올림) · 척도 정함 = 이 장 중심 확대 · 밖 노선 흐린 파선 · 칸 밖 잘림 · 이 장 굵게 · 시 · 종점 측점 · 방위표 · 「평면도 S=1/n」 · 「n/N 장」 · 좌표 없으면 안 그림 - 장 나눔 — 그래프가 칸을 덮으면 측점 줄여 장 더 나눔 · 장 목록에 number · total - 축척 — longitudinal.plan 기본 자동 · 범위 1/500~1/50,000 · ⚙ 칸 = 서버 기본값 열쇠 그대로(셋째 칸 + 자동) - 칸 앞 확정 종단 저장본 — 목록 받을 때 종단만 한 번 모두 풂(DRAWING_FORMAT 그대로) - 시험 — 칸 자리 · 안 겹침 · 장마다 강조 · 척도 확대 · 좌표 없음 · 한 번 풂 · plan 범위 · ⚙ 평면 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
225 lines
8.9 KiB
Python
225 lines
8.9 KiB
Python
"""B07 종단도 우측 상단 평면 위치도(521-2 · 요구 340-2) — 장마다 작도 영역 우상단 칸 하나.
|
|
|
|
칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm(작도 영역 좌하단 = 그림 좌하단 · `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) — 그림 좌표(작도 영역 좌하단 = 그림 좌하단)."""
|
|
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
|