feat(B07): 평면도 여러 장 맞춤선 · 이웃 구간 흐리게 · n/N장 · 돌린 맞춤 뺌(141-4)
- 장 나눔 — 돌린 방향 맞춤 뺌(그림을 안 돌리므로 그림칸 높이 넘침 흠) - 장 경계 측점에 노선 직각 맞춤선 + 「MATCH LINE NO.x · n장 이어짐」 · 글은 글 자리 잡기 장애물 - 이웃 구간 노선 = 도곽 안 흐린 파선(중심선 고를 때) · 도면명 「n/N장」 - 새 test_141_4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -18,6 +18,9 @@
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from shapely.geometry import LineString
|
||||
from shapely.geometry import box as shapely_box
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
DRAWING_FORMAT,
|
||||
FRAME_LAYER_ID,
|
||||
@@ -25,8 +28,10 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_layer,
|
||||
_text_entity,
|
||||
polyline_entity,
|
||||
station_no_label,
|
||||
station_plus_label,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import route_frame
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
compass_entities,
|
||||
entities_bbox,
|
||||
@@ -114,8 +119,8 @@ def plan_chunks(
|
||||
def fits(part: list[tuple[float, float, float]]) -> bool:
|
||||
width = max(x for _c, x, _y in part) - min(x for _c, x, _y in part)
|
||||
height = max(y for _c, _x, y in part) - min(y for _c, _x, y in part)
|
||||
# 가로로 길든 세로로 길든 도곽에만 들어가면 된다 — 두 방향 다 본다.
|
||||
return (width <= span_w and height <= span_h) or (width <= span_h and height <= span_w)
|
||||
# 그림을 돌리지 않으므로 도곽 방향 그대로만 본다(141-4 — 돌린 맞춤은 그림칸을 넘쳤음)
|
||||
return width <= span_w and height <= span_h
|
||||
|
||||
chunks: list[dict[str, Any]] = []
|
||||
start = 0
|
||||
@@ -134,6 +139,50 @@ def plan_chunks(
|
||||
return chunks
|
||||
|
||||
|
||||
MATCH_HALF_MM = 30.0 # 맞춤선 반길이(종이 mm · 노선 직각)
|
||||
|
||||
|
||||
def sheet_links(
|
||||
route: list[tuple[float, float, float]],
|
||||
chunk: dict[str, Any],
|
||||
total: int,
|
||||
box: tuple[float, float, float, float],
|
||||
interval_m: float,
|
||||
mm: float,
|
||||
neighbors: bool,
|
||||
) -> dict[str, Any]:
|
||||
"""여러 장 이음(141-4) — 장 경계 맞춤선 + 「MATCH LINE NO.x · n장 이어짐」 · 이웃 구간 노선.
|
||||
|
||||
`route` = (누가거리, x, y) 노선 전체 · 맞춤선 = 경계 측점에서 노선 직각(좌측 법선) ·
|
||||
이웃 구간 = 이 장 구간 밖 노선을 도곽으로 자른 것(흐리게 그림 · `neighbors` 일 때만).
|
||||
"""
|
||||
start_m, end_m, number = float(chunk["start_m"]), float(chunk["end_m"]), int(chunk["number"])
|
||||
half = MATCH_HALF_MM / mm
|
||||
lines = []
|
||||
for at, other in ((start_m, number - 1), (end_m, number + 1)):
|
||||
if not 1 <= other <= total or len(route) < 2:
|
||||
continue
|
||||
x, y, nx, ny = route_frame(route, at)
|
||||
no = station_no_label(at, interval_m).replace("No.", "NO.")
|
||||
lines.append(
|
||||
{
|
||||
"points": [[x - nx * half, y - ny * half], [x + nx * half, y + ny * half]],
|
||||
"label": f"MATCH LINE {no} · {other}장 이어짐",
|
||||
}
|
||||
)
|
||||
out: list[list[list[float]]] = []
|
||||
if neighbors:
|
||||
frame = shapely_box(*box)
|
||||
for part in ([p for p in route if p[0] <= start_m], [p for p in route if p[0] >= end_m]):
|
||||
if len(part) < 2:
|
||||
continue
|
||||
clipped = LineString([(x, y) for _c, x, y in part]).intersection(frame)
|
||||
for piece in getattr(clipped, "geoms", [clipped]):
|
||||
if isinstance(piece, LineString) and len(piece.coords) >= 2:
|
||||
out.append([[round(x, 4), round(y, 4)] for x, y in piece.coords])
|
||||
return {"lines": lines, "neighbors": out}
|
||||
|
||||
|
||||
def plan_drawing_id(kind: str, chunk: dict[str, Any], total: int) -> str:
|
||||
"""장이 하나면 접두어 그대로, 여럿이면 `plan_route_2` 처럼 번호를 붙인다."""
|
||||
return kind if total <= 1 else f"{kind}_{chunk['number']}"
|
||||
|
||||
@@ -44,6 +44,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanLabels import (
|
||||
label_entities,
|
||||
place_labels,
|
||||
text_rect,
|
||||
text_width,
|
||||
)
|
||||
|
||||
@@ -479,6 +480,59 @@ def _table_rect(entity: dict[str, Any]) -> tuple[float, float, float, float]:
|
||||
return (x, y - sum(shape["rowHeights"]), x + sum(shape["columnWidths"]), y)
|
||||
|
||||
|
||||
SHEET_LAYER = "b07-plan-sheet" # 여러 장 이음 — 맞춤선 · 이웃 구간 노선(141-4)
|
||||
_SHEET_COLOR = "#ff9d4d"
|
||||
_NEIGHBOR_COLOR = "#7d8590" # 이웃 구간 노선 — 흐리게
|
||||
|
||||
|
||||
def _sheet_link_entities(
|
||||
drawing_id: str, links: dict[str, Any], paper: Any
|
||||
) -> tuple[list[dict[str, Any]], list[tuple[float, float, float, float]]]:
|
||||
"""맞춤선(노선 직각 쇄선 + 「MATCH LINE …」) · 이웃 구간 노선(흐린 파선) · 글 사각 = 장애물."""
|
||||
out: list[dict[str, Any]] = []
|
||||
rects: list[tuple[float, float, float, float]] = []
|
||||
for index, points in enumerate(links.get("neighbors") or []):
|
||||
line = polyline_entity(
|
||||
drawing_id,
|
||||
[paper(p) for p in points],
|
||||
SHEET_LAYER,
|
||||
_NEIGHBOR_COLOR,
|
||||
suffix=f":n:{index}",
|
||||
dash=[3, 3],
|
||||
)
|
||||
if line:
|
||||
out.append(line)
|
||||
size = _LABEL_FONT + 0.5
|
||||
for index, item in enumerate(links.get("lines") or []):
|
||||
a, b = (paper(point) for point in item["points"])
|
||||
line = polyline_entity(
|
||||
drawing_id, [a, b], SHEET_LAYER, _SHEET_COLOR, suffix=f":m:{index}", width=2,
|
||||
dash=[8, 3, 2, 3],
|
||||
) # fmt: skip
|
||||
if line:
|
||||
out.append(line)
|
||||
length = math.hypot(b[0] - a[0], b[1] - a[1]) or 1.0
|
||||
ux, uy = (b[0] - a[0]) / length, (b[1] - a[1]) / length
|
||||
# 글은 맞춤선 끝 바깥으로 선 방향 — 거꾸로 서지 않게 오른쪽(위)쪽 끝에서
|
||||
end, (dx, dy) = (b, (ux, uy)) if ux > 1e-9 or (ux > -1e-9 and uy > 0) else (a, (-ux, -uy))
|
||||
x, y = end[0] + dx * 2.0, end[1] + dy * 2.0
|
||||
out.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:sheet:{index}",
|
||||
item["label"],
|
||||
x,
|
||||
y,
|
||||
SHEET_LAYER,
|
||||
size,
|
||||
_SHEET_COLOR,
|
||||
align="left",
|
||||
direction=(dx, dy),
|
||||
)
|
||||
)
|
||||
rects.append(text_rect(item["label"], x, y, size, "left", (dx, dy)))
|
||||
return out, rects
|
||||
|
||||
|
||||
def build_plan_kind_drawing(
|
||||
drawing_id: str,
|
||||
label: str,
|
||||
@@ -515,6 +569,11 @@ def build_plan_kind_drawing(
|
||||
for item in pieces[piece].get("items") or []:
|
||||
cx, cy = paper(item["center"])
|
||||
obstacles.append((cx - radius, cy - radius, cx + radius, cy + radius))
|
||||
links = pieces.get("sheet_links")
|
||||
if links:
|
||||
made, rects = _sheet_link_entities(drawing_id, links, paper)
|
||||
entities.extend(made)
|
||||
obstacles += rects
|
||||
entities.extend(label_entities(drawing_id, place_labels(labels, obstacles)))
|
||||
# 도곽 · 방위표 자리는 조각과 무관하게 같은 자리 — 그 장 범위를 기준으로 잡는다.
|
||||
frame_box = (0.0, 0.0, (box[2] - min_x) * mm, (box[3] - min_y) * mm)
|
||||
@@ -571,6 +630,7 @@ def build_plan_kind_drawing(
|
||||
"entities": entities,
|
||||
"layers": [
|
||||
*layers,
|
||||
*([_layer(SHEET_LAYER, "맞춤선 · 이웃 구간")] if links else []),
|
||||
_layer("b07-plan-title", "표제"),
|
||||
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
||||
],
|
||||
|
||||
@@ -31,6 +31,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
_structure_label,
|
||||
plan_area_mm,
|
||||
plan_chunks,
|
||||
sheet_links,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
|
||||
DEFAULT_KINDS,
|
||||
@@ -544,10 +545,15 @@ def build_pieces(
|
||||
pieces, _load_design_curves(project_root), route, box, chunk, total, mm
|
||||
)
|
||||
|
||||
chosen = {piece: pieces[piece] for piece in wanted if piece in pieces}
|
||||
if total > 1: # 여러 장 이음 — 조각 고름과 무관하게 늘(이웃 노선은 중심선 고를 때만 · 141-4)
|
||||
chosen["sheet_links"] = pieces["sheet_links"] = sheet_links(
|
||||
route, chunk, total, box, interval, mm, "centerline" in wanted
|
||||
)
|
||||
folder = _design_root(project_root) / "plan_pieces" / str(number)
|
||||
for piece, data in pieces.items():
|
||||
atomic_write_json(folder / f"{piece}.json", data)
|
||||
return {piece: pieces[piece] for piece in wanted if piece in pieces}, box, chunk, total
|
||||
return chosen, box, chunk, total
|
||||
|
||||
|
||||
def plan_kind_source(
|
||||
@@ -576,7 +582,7 @@ def plan_kind_source(
|
||||
)
|
||||
title = plan_title(kind["name"])
|
||||
return {
|
||||
"label": title if total <= 1 else f"{title} {chunk['number']}장",
|
||||
"label": title if total <= 1 else f"{title} {chunk['number']}/{total}장",
|
||||
"box": box,
|
||||
"pieces": pieces,
|
||||
"scale": kind["scale"],
|
||||
@@ -610,7 +616,7 @@ def plan_kind_items(project_root: Path, longitudinal: dict[str, Any]) -> list[di
|
||||
"id": f"plan_{kind['id']}"
|
||||
if single
|
||||
else f"plan_{kind['id']}_{chunk['number']}",
|
||||
"label": title if single else f"{title} {chunk['number']}장",
|
||||
"label": title if single else f"{title} {chunk['number']}/{len(sheets)}장",
|
||||
"plan_kind": kind["id"],
|
||||
"plan_kind_name": kind["name"],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""141-4 평면도 여러 장 — 노선 따라 자르기 유지 + 맞춤선 · 이웃 구간 흐리게 · 「n/N장」 · 돌린 맞춤 뺌."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
_chunk_span_m,
|
||||
plan_chunks,
|
||||
sheet_links,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
|
||||
SHEET_LAYER,
|
||||
build_plan_kind_drawing,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import plan_kind_items
|
||||
|
||||
# 남북으로 곧은 노선 1,200 m · 측점 20 m
|
||||
STATIONS = [(float(c), 0.0, float(c)) for c in range(0, 1201, 20)]
|
||||
|
||||
|
||||
def test_돌린_맞춤_빼기_남북_노선은_그림칸_높이로_나눔():
|
||||
span_w, span_h = _chunk_span_m(1200)
|
||||
assert span_h < 800 < span_w # 옛 규칙 = 돌려 보면 한 장(그림칸 높이를 넘침)
|
||||
chunks = plan_chunks(STATIONS[:41], 1200) # 800 m
|
||||
assert len(chunks) == 2
|
||||
for chunk in chunks:
|
||||
assert chunk["end_m"] - chunk["start_m"] <= span_h
|
||||
|
||||
|
||||
def test_맞춤선_경계_측점_직각_이웃_장_번호():
|
||||
chunks = plan_chunks(STATIONS, 1200)
|
||||
total = len(chunks)
|
||||
assert total == 3
|
||||
middle = chunks[1]
|
||||
box = (-300.0, middle["start_m"] - 10.0, 300.0, middle["end_m"] + 10.0)
|
||||
mm = 1000.0 / 1200
|
||||
links = sheet_links(STATIONS, middle, total, box, 20.0, mm, neighbors=True)
|
||||
first, last = links["lines"]
|
||||
start_no, end_no = int(middle["start_m"] // 20), int(middle["end_m"] // 20)
|
||||
assert first["label"] == f"MATCH LINE NO.{start_no} · 1장 이어짐"
|
||||
assert last["label"] == f"MATCH LINE NO.{end_no} · 3장 이어짐"
|
||||
(ax, ay), (bx, by) = first["points"]
|
||||
assert ay == by == middle["start_m"] # 남북 노선에 직각 = 동서
|
||||
assert math.hypot(bx - ax, by - ay) * mm == 60.0 # 종이 60 mm
|
||||
# 이웃 구간 노선 = 도곽 안만(앞 · 뒤 10 m)
|
||||
assert len(links["neighbors"]) == 2
|
||||
for line in links["neighbors"]:
|
||||
assert all(box[1] <= y <= box[3] for _x, y in line)
|
||||
# 첫 장 · 끝 장은 한쪽만 · 중심선 안 고르면 이웃 노선 없음
|
||||
only = sheet_links(STATIONS, chunks[0], total, box, 20.0, mm, neighbors=False)
|
||||
assert [item["label"].endswith("· 2장 이어짐") for item in only["lines"]] == [True]
|
||||
assert only["neighbors"] == []
|
||||
|
||||
|
||||
def test_도면에_맞춤선_층_글은_다른_글과_안_겹침():
|
||||
chunks = plan_chunks(STATIONS, 1200)
|
||||
middle = chunks[1]
|
||||
box = (-300.0, middle["start_m"] - 10.0, 300.0, middle["end_m"] + 10.0)
|
||||
mm = 1000.0 / 1200
|
||||
links = sheet_links(STATIONS, middle, 3, box, 20.0, mm, neighbors=True)
|
||||
at = middle["start_m"]
|
||||
pieces = {
|
||||
"sheet_links": links,
|
||||
# 맞춤선 글 자리에 시종점 글 후보가 처음 놓이는 견본
|
||||
"endpoints": {"items": [{"label": "시점 No.0", "x": 0.0, "y": at, "nx": 1.0, "ny": 0.0}]},
|
||||
}
|
||||
drawing = build_plan_kind_drawing("plan_x_2", "평면도(x) 2/3장", box, pieces, 1200)
|
||||
sheet = [e for e in drawing["entities"] if e["layerId"] == SHEET_LAYER]
|
||||
labels = sorted(e["shapeData"]["label"] for e in sheet if e["type"] == "Text")
|
||||
assert labels == [line["label"] for line in links["lines"]]
|
||||
assert any(layer["id"] == SHEET_LAYER for layer in drawing["layers"])
|
||||
assert sum(e["type"] == "PolyLine" for e in sheet) == 4 # 맞춤선 둘 + 이웃 노선 둘
|
||||
|
||||
|
||||
def test_목록_도면명_n_N장(tmp_path: Path):
|
||||
stations = [{"chainage_m": c, "center_x": x, "center_y": y} for c, x, y in STATIONS]
|
||||
items = [
|
||||
i for i in plan_kind_items(tmp_path, {"stations": stations}) if i["plan_kind"] == "terrain"
|
||||
]
|
||||
assert [item["label"] for item in items] == [f"평면도(지형) {n}/3장" for n in (1, 2, 3)]
|
||||
Reference in New Issue
Block a user