- 성토사면 외곽선 → 노견선 · 성토 외곽선 · 절토 외곽선 셋 · 배수시설 기호 조각 신설 · 라이다 음영
- 기본 넷 처음 체크 = 지금 그림(배수시설 기호는 빼 둠) · 옛 종류 파일 slope_outline 은 셋으로 펼침
- PUT /plan-kinds/{id} — 이름 · 조각 고침(기본 넷 포함) · 그 종류 확정본 풀림
- GET /plan-kinds 에 측점 간격 실음 — 측점 NO + 거리 입력용
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
556 lines
21 KiB
Python
556 lines
21 KiB
Python
"""B07 계획평면도 종류 — 정보 조각을 골라 한 장으로 얹는다(PLAN 60장).
|
|
|
|
계획평면도는 여러 종류(지형 · 노선배치도 · 대피소 · 라이다 · 사용자 추가)가 **같은 양식**이다.
|
|
정보 조각(등고선 · 중심선 · 노견선 · 성토 · 절토 외곽선 · 비탈 눈금 · IP · R · 측점 NO ·
|
|
시종점 · 대피소/사토장 · 배수시설 기호 · 라이다 음영)마다 프로젝트 저장소에 파일이 하나씩
|
|
있고, 종류는 그중 무엇을 얹을지 고른 목록이다. 이 모듈은 조각 기하 계산과 도면 조립만
|
|
한다(파일 입출력 없음).
|
|
|
|
좌표 규약: 종이 mm = (사업지 좌표 m - 그 장 도곽 최소점) x MM (1/1,200).
|
|
"""
|
|
|
|
import math
|
|
from bisect import bisect_left
|
|
from typing import Any
|
|
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
|
DRAWING_FORMAT,
|
|
FRAME_LAYER_ID,
|
|
TABLE_LABEL_COLOR,
|
|
_layer,
|
|
_text_entity,
|
|
polyline_entity,
|
|
)
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
|
_COMPASS_MARGIN,
|
|
_COMPASS_SIZE,
|
|
_FONT_SIZE,
|
|
_ROUTE_WIDTH,
|
|
_STRUCTURE_FONT_SIZE,
|
|
_STRUCTURE_SIZE_MM,
|
|
_TITLE_FONT_SIZE,
|
|
CONTOUR_COLOR,
|
|
MM,
|
|
STREAM_COLOR,
|
|
STRUCTURE_COLOR,
|
|
)
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
|
compass_entities,
|
|
entities_bbox,
|
|
frame_entities,
|
|
scale_fields,
|
|
)
|
|
from config.config_system import DRAWING_SCALE_PLAN
|
|
|
|
# 조각 (id, 이름, 도면층 잠금) — 그리는 차례(배경이 먼저).
|
|
PIECES: tuple[tuple[str, str, bool], ...] = (
|
|
("lidar", "라이다 음영", True),
|
|
("contour", "등고선", True),
|
|
("stream", "계류", True),
|
|
("slope_ticks", "비탈 눈금", False),
|
|
("road_edge", "노견선", False),
|
|
("fill_outline", "성토 외곽선", False),
|
|
("cut_outline", "절토 외곽선", False),
|
|
("centerline", "중심선", False),
|
|
("ip", "IP", False),
|
|
("radius", "R", False),
|
|
("station", "측점 NO", False),
|
|
("endpoints", "시종점", False),
|
|
("refuge", "대피소 · 사토장", False),
|
|
("drainage", "배수시설 기호", False),
|
|
)
|
|
PIECE_IDS: tuple[str, ...] = tuple(piece for piece, _name, _locked in PIECES)
|
|
# 옛 조각 id → 나눈 조각들 — 옛 종류 파일을 읽을 때 펼친다.
|
|
LEGACY_PIECES: dict[str, tuple[str, ...]] = {
|
|
"slope_outline": ("road_edge", "fill_outline", "cut_outline"),
|
|
}
|
|
_OUTLINE = LEGACY_PIECES["slope_outline"]
|
|
|
|
_TERRAIN = ("contour", "stream", "centerline", *_OUTLINE, "slope_ticks", "ip", "radius")
|
|
# 기본 종류 넷 — 지울 수 있다. id 는 옛 도면 id(plan_terrain …)와 같아 확정본이 이어진다.
|
|
DEFAULT_KINDS: tuple[dict[str, Any], ...] = (
|
|
{"id": "terrain", "name": "지형", "pieces": [*_TERRAIN, "station"]},
|
|
{
|
|
"id": "route",
|
|
"name": "노선배치도",
|
|
"pieces": [p for p in _TERRAIN if p != "radius"] + ["station", "endpoints"],
|
|
},
|
|
{
|
|
"id": "layout",
|
|
"name": "대피소",
|
|
"pieces": [p for p in _TERRAIN if p != "radius"] + ["station", "refuge"],
|
|
},
|
|
# 라이다 = 지형과 같은 정보 · 배경만 라이다(등고선 · 계류 대신).
|
|
{
|
|
"id": "lidar",
|
|
"name": "라이다",
|
|
"pieces": ["lidar", *[p for p in _TERRAIN if p not in ("contour", "stream")], "station"],
|
|
},
|
|
)
|
|
|
|
_COLORS = {
|
|
"contour": CONTOUR_COLOR,
|
|
"stream": STREAM_COLOR,
|
|
"centerline": "#ff4d4d",
|
|
"road_edge": "#4dd8e6",
|
|
"fill_outline": "#4dd8e6",
|
|
"cut_outline": "#4dd8e6",
|
|
"slope_ticks": "#ff4d4d",
|
|
"ip": "#4d7dff",
|
|
"radius": "#4d7dff",
|
|
"station": "#ffe066",
|
|
"endpoints": "#ff9d4d",
|
|
"refuge": "#7ddc7d",
|
|
"drainage": STRUCTURE_COLOR,
|
|
}
|
|
TICK_STEP_M = 2.0 # 비탈 눈금 간격(실거리 m) — 긴 · 짧은 눈금이 번갈아 선다
|
|
_MAJOR_EVERY = 5 # 측점 NO 큰 글자 · 동그라미 간격(측점 수)
|
|
_LABEL_FONT = 2.5
|
|
_MAJOR_FONT = 4.5
|
|
_TICK_MM = 1.5
|
|
_CIRCLE_MM = 1.2
|
|
|
|
|
|
def layer_id(piece: str) -> str:
|
|
return f"b07-plan-{piece}"
|
|
|
|
|
|
# ── 노선 따라가기 ─────────────────────────────────────
|
|
|
|
|
|
def route_frame(
|
|
route: list[tuple[float, float, float]], chainage: float
|
|
) -> tuple[float, float, float, float]:
|
|
"""노선 (누가거리, x, y) 위 그 누가거리 자리와 좌측 법선 (x, y, nx, ny)."""
|
|
chainages = [item[0] for item in route]
|
|
index = min(max(bisect_left(chainages, chainage), 1), len(route) - 1)
|
|
(c0, x0, y0), (c1, x1, y1) = route[index - 1], route[index]
|
|
dx, dy = x1 - x0, y1 - y0
|
|
length = math.hypot(dx, dy) or 1.0
|
|
ratio = (chainage - c0) / (c1 - c0) if c1 > c0 else 0.0
|
|
return (x0 + dx * ratio, y0 + dy * ratio, -dy / length, dx / length)
|
|
|
|
|
|
def _interp(records: list[dict[str, float]], chainage: float, key: str) -> float:
|
|
chainages = [record["chainage_m"] for record in records]
|
|
index = bisect_left(chainages, chainage)
|
|
if index <= 0:
|
|
return records[0][key]
|
|
if index >= len(records):
|
|
return records[-1][key]
|
|
before, after = records[index - 1], records[index]
|
|
span = after["chainage_m"] - before["chainage_m"]
|
|
ratio = (chainage - before["chainage_m"]) / span if span > 0 else 0.0
|
|
return before[key] + (after[key] - before[key]) * ratio
|
|
|
|
|
|
def _ground(samples: list[dict[str, Any]]) -> list[tuple[float, float]]:
|
|
return sorted(
|
|
(float(item["offset_m"]), float(item["elevation_m"]))
|
|
for item in samples
|
|
if item.get("valid", True) and isinstance(item.get("elevation_m"), (int, float))
|
|
)
|
|
|
|
|
|
def _ground_at(ground: list[tuple[float, float]], offset: float) -> float:
|
|
offsets = [x for x, _z in ground]
|
|
index = min(max(bisect_left(offsets, offset), 1), len(ground) - 1)
|
|
(x0, z0), (x1, z1) = ground[index - 1], ground[index]
|
|
return z0 + (z1 - z0) * ((offset - x0) / (x1 - x0) if x1 > x0 else 0.0)
|
|
|
|
|
|
def side_is_cut(samples: list[dict[str, Any]], edge: float, elevation: float) -> bool:
|
|
"""노견 끝에서 지반이 노면보다 높으면 그 쪽 비탈은 절토다(낮으면 성토)."""
|
|
ground = _ground(samples)
|
|
return len(ground) >= 2 and _ground_at(ground, edge) > elevation
|
|
|
|
|
|
def slope_end(
|
|
design_line: list[dict[str, Any]], samples: list[dict[str, Any]], edge: float, side: str
|
|
) -> float:
|
|
"""노견 끝에서 바깥으로 — 설계선이 원지반에 닿는 오프셋(비탈 끝). 못 닿으면 설계선 끝.
|
|
|
|
설계선(B06 횡단 설계 결과)은 비탈이 지반과 만난 뒤로 지반을 따라간다 — 그 첫 자리가 끝이다.
|
|
좌측은 오프셋이 커지는 쪽, 우측은 작아지는 쪽이 바깥이다.
|
|
"""
|
|
ground = _ground(samples)
|
|
points = sorted((float(p["offset_m"]), float(p["elevation_m"])) for p in design_line)
|
|
outward = (
|
|
[p for p in points if p[0] > edge]
|
|
if side == "left"
|
|
else [p for p in points[::-1] if p[0] < edge]
|
|
)
|
|
if not ground:
|
|
return outward[-1][0] if outward else edge
|
|
end, differs = edge, False
|
|
for offset, elevation in outward:
|
|
if abs(elevation - _ground_at(ground, offset)) <= 1e-3:
|
|
return offset if differs else end
|
|
differs, end = True, offset
|
|
return end
|
|
|
|
|
|
def _split_runs(
|
|
points: list[list[float]], flags: list[bool]
|
|
) -> tuple[list[list[list[float]]], list[list[list[float]]]]:
|
|
"""한 선을 참 · 거짓 구간으로 끊는다 — 바뀌는 자리 점은 앞뒤 구간이 나눠 가져 이어진다."""
|
|
runs: dict[bool, list[list[list[float]]]] = {True: [], False: []}
|
|
current: list[list[float]] = []
|
|
previous: bool | None = None
|
|
for point, flag in zip(points, flags):
|
|
if flag != previous:
|
|
if current:
|
|
current.append(point)
|
|
current = [point]
|
|
runs[flag].append(current)
|
|
previous = flag
|
|
else:
|
|
current.append(point)
|
|
return (
|
|
[run for run in runs[True] if len(run) >= 2],
|
|
[run for run in runs[False] if len(run) >= 2],
|
|
)
|
|
|
|
|
|
def slope_pieces(
|
|
route: list[tuple[float, float, float]],
|
|
records: list[dict[str, Any]],
|
|
start_m: float,
|
|
end_m: float,
|
|
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
"""측점별 노견 끝 · 비탈 끝 오프셋에서 노견선 · 성토 외곽선 · 절토 외곽선 · 비탈 눈금을 만든다.
|
|
|
|
`records` = [{chainage_m, left_edge_m, left_end_m, right_edge_m, right_end_m,
|
|
left_cut?, right_cut?, (x, y, nx, ny)?}] — 좌(+) · 우(-) 오프셋. 측점 자리는 횡단 파일의
|
|
중심 · 좌축을 그대로 써서 측점에서 외곽선 폭이 횡단 설계 값과 같다. 측점 사이는 노선을
|
|
따라 보간한다. 비탈 끝 선은 그 쪽이 절토인지 성토인지로 나눠 두 조각에 싣는다.
|
|
"""
|
|
records = sorted(
|
|
(
|
|
{
|
|
**record,
|
|
**{f"{s}_cut": float(bool(record.get(f"{s}_cut"))) for s in ("left", "right")},
|
|
}
|
|
for record in records
|
|
),
|
|
key=lambda record: record["chainage_m"],
|
|
)
|
|
if len(route) < 2 or not records:
|
|
return {"lines": []}, {"lines": []}, {"lines": []}, {"ticks": []}
|
|
keys = ("left_edge_m", "left_end_m", "right_edge_m", "right_end_m")
|
|
by_chainage = {round(record["chainage_m"], 3): record for record in records}
|
|
samples = {
|
|
round(record["chainage_m"], 3)
|
|
for record in records
|
|
if start_m <= record["chainage_m"] <= end_m
|
|
}
|
|
step = start_m
|
|
while step <= end_m:
|
|
samples.add(round(step, 3))
|
|
step += TICK_STEP_M
|
|
lines: dict[str, list[list[float]]] = {key: [] for key in keys}
|
|
cut: dict[str, list[bool]] = {"left": [], "right": []}
|
|
ticks: list[list[list[float]]] = []
|
|
for index, chainage in enumerate(sorted(samples)):
|
|
record = by_chainage.get(chainage)
|
|
if record and all(k in record for k in ("x", "y", "nx", "ny")):
|
|
x, y, nx, ny = record["x"], record["y"], record["nx"], record["ny"]
|
|
else:
|
|
x, y, nx, ny = route_frame(route, chainage)
|
|
offsets = {key: _interp(records, chainage, key) for key in keys}
|
|
points = {
|
|
key: [round(x + nx * value, 4), round(y + ny * value, 4)]
|
|
for key, value in offsets.items()
|
|
}
|
|
for key in keys:
|
|
lines[key].append(points[key])
|
|
for side in cut:
|
|
cut[side].append(_interp(records, chainage, f"{side}_cut") > 0.5)
|
|
# 눈금은 노견 끝에서 비탈 끝으로 — 긴 것 · 짧은 것이 번갈아 선다(납품 도면 관례).
|
|
for side in ("left", "right"):
|
|
edge, end = offsets[f"{side}_edge_m"], offsets[f"{side}_end_m"]
|
|
if abs(end - edge) < 0.05:
|
|
continue
|
|
reach = end if index % 2 == 0 else edge + (end - edge) / 2.0
|
|
ticks.append([points[f"{side}_edge_m"], [x + nx * reach, y + ny * reach]])
|
|
cut_lines: list[list[list[float]]] = []
|
|
fill_lines: list[list[list[float]]] = []
|
|
for side in cut:
|
|
cut_runs, fill_runs = _split_runs(lines[f"{side}_end_m"], cut[side])
|
|
cut_lines += cut_runs
|
|
fill_lines += fill_runs
|
|
return (
|
|
{"lines": [lines["left_edge_m"], lines["right_edge_m"]]},
|
|
{"lines": fill_lines},
|
|
{"lines": cut_lines},
|
|
{"ticks": ticks},
|
|
)
|
|
|
|
|
|
def curve_pieces(
|
|
curves: list[dict[str, Any]],
|
|
route: list[tuple[float, float, float]],
|
|
box: tuple[float, float, float, float],
|
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
"""곡선표에서 IP(교각점 · 접선)와 R 라벨을 만든다. 번호는 노선 전체 차례(IP1 …)."""
|
|
ips: list[dict[str, Any]] = []
|
|
radii: list[dict[str, Any]] = []
|
|
min_x, min_y, max_x, max_y = box
|
|
for number, curve in enumerate(curves, start=1):
|
|
apex, start, end = curve.get("apex"), curve.get("start"), curve.get("end")
|
|
radius = curve.get("radius_m")
|
|
if not (apex and start and end) or not isinstance(radius, (int, float)):
|
|
continue
|
|
if not (min_x <= apex[0] <= max_x and min_y <= apex[1] <= max_y):
|
|
continue
|
|
ips.append(
|
|
{
|
|
"name": f"IP{number}",
|
|
"apex": list(apex[:2]),
|
|
"start": list(start[:2]),
|
|
"end": list(end[:2]),
|
|
}
|
|
)
|
|
# R 라벨은 곡선 한가운데에서 교각점 반대쪽(곡선 안쪽)으로 조금 띄운다.
|
|
middle = min(
|
|
((x, y) for _c, x, y in route),
|
|
key=lambda point: math.hypot(point[0] - apex[0], point[1] - apex[1]),
|
|
default=(apex[0], apex[1]),
|
|
)
|
|
dx, dy = middle[0] - apex[0], middle[1] - apex[1]
|
|
length = math.hypot(dx, dy) or 1.0
|
|
radii.append(
|
|
{
|
|
"label": f"R={round(float(radius), 1):g}",
|
|
"at": [middle[0] + dx / length * 4.0, middle[1] + dy / length * 4.0],
|
|
}
|
|
)
|
|
return {"items": ips}, {"items": radii}
|
|
|
|
|
|
# ── 도면 조립 ─────────────────────────────────────────
|
|
|
|
|
|
def _circle(center: tuple[float, float], radius: float) -> list[tuple[float, float]]:
|
|
return [
|
|
(center[0] + radius * math.cos(a), center[1] + radius * math.sin(a))
|
|
for a in (2 * math.pi * i / 24 for i in range(25))
|
|
]
|
|
|
|
|
|
def _reading(nx: float, ny: float) -> tuple[float, float]:
|
|
"""글자 방향 — 노선 방향(법선을 돌린 것)으로 쓰되 거꾸로 서지 않게."""
|
|
tx, ty = ny, -nx
|
|
return (tx, ty) if tx > 0 or (abs(tx) < 1e-9 and ty > 0) else (-tx, -ty)
|
|
|
|
|
|
def _piece_entities(
|
|
drawing_id: str, piece: str, data: dict[str, Any], paper: Any
|
|
) -> list[dict[str, Any]]:
|
|
layer, color = layer_id(piece), _COLORS.get(piece, TABLE_LABEL_COLOR)
|
|
out: list[dict[str, Any]] = []
|
|
|
|
def line(points: Any, suffix: str, width: int = 1, dash: list[int] | None = None) -> None:
|
|
entity = polyline_entity(
|
|
drawing_id,
|
|
[paper(p) for p in points],
|
|
layer,
|
|
color,
|
|
suffix=suffix,
|
|
width=width,
|
|
dash=dash,
|
|
)
|
|
if entity:
|
|
out.append(entity)
|
|
|
|
def text(seed: str, label: str, at: tuple[float, float], size: float, **kw: Any) -> None:
|
|
out.append(
|
|
_text_entity(
|
|
f"{drawing_id}:{piece}:{seed}", label, at[0], at[1], layer, size, color, **kw
|
|
)
|
|
)
|
|
|
|
if piece == "lidar":
|
|
image, box = data.get("image"), data.get("box")
|
|
if image and box:
|
|
left, bottom = paper((box[0], box[1]))
|
|
right, top = paper((box[2], box[3]))
|
|
corners = [(left, bottom), (right, bottom), (right, top), (left, top)]
|
|
out.append(
|
|
{
|
|
"id": f"{drawing_id}:lidar",
|
|
"type": "Image",
|
|
"lineColor": "#ffffff",
|
|
"lineWidth": 1,
|
|
"layerId": layer,
|
|
"shapeData": {
|
|
"points": [{"x": x, "y": y} for x, y in corners],
|
|
"imageData": image,
|
|
},
|
|
}
|
|
)
|
|
elif piece in ("contour", "stream", *_OUTLINE):
|
|
for index, points in enumerate(data.get("lines") or []):
|
|
line(points, f":{index}")
|
|
elif piece == "centerline":
|
|
for index, points in enumerate(data.get("lines") or []):
|
|
line(points, f":{index}", width=_ROUTE_WIDTH)
|
|
elif piece == "slope_ticks":
|
|
for index, points in enumerate(data.get("ticks") or []):
|
|
line(points, f":{index}")
|
|
elif piece == "ip":
|
|
for index, item in enumerate(data.get("items") or []):
|
|
line([item["start"], item["apex"], item["end"]], f":{index}", dash=[4, 2])
|
|
apex = paper(item["apex"])
|
|
out_circle = polyline_entity(
|
|
drawing_id, _circle(apex, _CIRCLE_MM), layer, color, suffix=f":c:{index}"
|
|
)
|
|
if out_circle:
|
|
out.append(out_circle)
|
|
text(f"{index}", item["name"], (apex[0], apex[1] - _CIRCLE_MM - 3.0), _LABEL_FONT)
|
|
elif piece == "radius":
|
|
for index, item in enumerate(data.get("items") or []):
|
|
text(f"{index}", item["label"], paper(item["at"]), _LABEL_FONT)
|
|
elif piece == "station":
|
|
for index, item in enumerate(data.get("items") or []):
|
|
cx, cy = paper((item["x"], item["y"]))
|
|
nx, ny = item["nx"], item["ny"]
|
|
line(
|
|
[
|
|
(item["x"] - nx * _TICK_MM / MM, item["y"] - ny * _TICK_MM / MM),
|
|
(item["x"] + nx * _TICK_MM / MM, item["y"] + ny * _TICK_MM / MM),
|
|
],
|
|
f":{index}",
|
|
)
|
|
if item.get("major"):
|
|
circle = polyline_entity(
|
|
drawing_id, _circle((cx, cy), _CIRCLE_MM), layer, color, suffix=f":c:{index}"
|
|
)
|
|
if circle:
|
|
out.append(circle)
|
|
gap = _MAJOR_FONT * 1.8
|
|
text(
|
|
f"{index}",
|
|
item["label"],
|
|
(cx + nx * gap, cy + ny * gap),
|
|
_MAJOR_FONT,
|
|
direction=_reading(nx, ny),
|
|
)
|
|
elif piece == "endpoints":
|
|
for index, item in enumerate(data.get("items") or []):
|
|
nx, ny = item["nx"], item["ny"]
|
|
reach = 6.0 / MM # 종이 6mm 막대
|
|
line(
|
|
[
|
|
(item["x"] - nx * reach, item["y"] - ny * reach),
|
|
(item["x"] + nx * reach, item["y"] + ny * reach),
|
|
],
|
|
f":{index}",
|
|
width=2,
|
|
)
|
|
cx, cy = paper((item["x"], item["y"]))
|
|
text(f"{index}", item["label"], (cx + nx * 9.0, cy + ny * 9.0), _LABEL_FONT + 0.5)
|
|
elif piece == "refuge":
|
|
size = 2.0 # 네모 기호 반크기(종이 mm)
|
|
for index, item in enumerate(data.get("items") or []):
|
|
x, y, half = item["x"], item["y"], size / MM
|
|
corners = [(-1, -1), (1, -1), (1, 1), (-1, 1), (-1, -1)]
|
|
line([(x + sx * half, y + sy * half) for sx, sy in corners], f":{index}")
|
|
cx, cy = paper((x, y))
|
|
text(f"{index}", item["label"], (cx + size + 1.0, cy), _LABEL_FONT, align="left")
|
|
elif piece == "drainage":
|
|
size = _STRUCTURE_SIZE_MM # 마름모 기호 반크기(종이 mm)
|
|
for index, item in enumerate(data.get("items") or []):
|
|
cx, cy = paper((item["x"], item["y"]))
|
|
corners = [(0, 1), (1, 0), (0, -1), (-1, 0), (0, 1)]
|
|
diamond = polyline_entity(
|
|
drawing_id,
|
|
[(cx + sx * size, cy + sy * size) for sx, sy in corners],
|
|
layer,
|
|
color,
|
|
suffix=f":{index}",
|
|
)
|
|
if diamond:
|
|
out.append(diamond)
|
|
text(
|
|
f"{index}", item["label"], (cx + size + 1.0, cy), _STRUCTURE_FONT_SIZE, align="left"
|
|
)
|
|
return out
|
|
|
|
|
|
def build_plan_kind_drawing(
|
|
drawing_id: str,
|
|
label: str,
|
|
box: tuple[float, float, float, float],
|
|
pieces: dict[str, dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
"""고른 조각만 얹어 계획평면도 한 장을 만든다. `box` = 그 장 도곽 범위(실좌표 m)."""
|
|
min_x, min_y = box[0], box[1]
|
|
|
|
def paper(point: Any) -> tuple[float, float]:
|
|
return ((point[0] - min_x) * MM, (point[1] - min_y) * MM)
|
|
|
|
entities: list[dict[str, Any]] = []
|
|
for piece in PIECE_IDS:
|
|
if piece in pieces:
|
|
entities.extend(_piece_entities(drawing_id, piece, pieces[piece], paper))
|
|
# 도곽 · 방위표 자리는 조각과 무관하게 같은 자리 — 그 장 범위를 기준으로 잡는다.
|
|
frame_box = (0.0, 0.0, (box[2] - min_x) * MM, (box[3] - min_y) * MM)
|
|
entities.extend(
|
|
compass_entities(
|
|
drawing_id,
|
|
(
|
|
frame_box[2] + _COMPASS_MARGIN + _COMPASS_SIZE / 2.0,
|
|
frame_box[3] - _COMPASS_SIZE / 2.0,
|
|
),
|
|
_COMPASS_SIZE,
|
|
)
|
|
)
|
|
bbox = entities_bbox(entities) or frame_box
|
|
min_bx, _min_by, max_bx, max_by = bbox
|
|
entities.append(
|
|
_text_entity(
|
|
f"{drawing_id}:title",
|
|
label,
|
|
(min_bx + max_bx) / 2.0,
|
|
max_by + 12.0,
|
|
"b07-plan-title",
|
|
_TITLE_FONT_SIZE,
|
|
TABLE_LABEL_COLOR,
|
|
)
|
|
)
|
|
entities.append(
|
|
_text_entity(
|
|
f"{drawing_id}:scale",
|
|
f"S = 1/{DRAWING_SCALE_PLAN:,}",
|
|
max_bx,
|
|
max_by + 5.0,
|
|
"b07-plan-title",
|
|
_FONT_SIZE,
|
|
TABLE_LABEL_COLOR,
|
|
align="right",
|
|
)
|
|
)
|
|
entities.extend(
|
|
frame_entities(
|
|
drawing_id,
|
|
entities_bbox(entities) or bbox,
|
|
fit=False,
|
|
fields={"도면명": label, **scale_fields(("", DRAWING_SCALE_PLAN))},
|
|
)
|
|
)
|
|
layers = [
|
|
_layer(layer_id(piece), name, locked=locked)
|
|
for piece, name, locked in PIECES
|
|
if piece in pieces
|
|
]
|
|
return {
|
|
"format": DRAWING_FORMAT,
|
|
"entities": entities,
|
|
"layers": [
|
|
*layers,
|
|
_layer("b07-plan-title", "표제"),
|
|
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
|
],
|
|
}
|