Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Development_Draw.py
T

441 lines
15 KiB
Python

"""B07 전개도 그림 — 한 장 = 박스 줄바꿈 배치 · 형태 무늬 · 양 끝 측점 · 우하단 표 · 도각.
그림 241(콘크리트포장 · 전석메쌓기 전개도) 꼴:
박스 = 거리(종점 − 시점) × H(포장 B) · 좌우 끝 = 시점 · 종점 측점(붉은 지시선 + 기울인 글)
· 끝 옆 「H=2.0」(포장 「B=3.5」) · 박스 아래 「L=6.0m」
· 우하단 표(측점 · 거리 · 높이/폭 · 면적 · 계)
무늬 = 기본 도형(PolyLine · Circle · Line)으로 직접 — CAD Hatch 는 DXF 내보내면 채움이 사라짐.
흔들림 = 도면 id · 박스 열쇠 씨앗(같은 자료 = 같은 그림 · 엔티티 id 도 uuid5 결정).
좌표 = 종이 mm · 가로 축척 자동(한 장에 드는 가장 큰 그림) · 세로는 가장 높은 박스가 12~40 mm.
"""
from __future__ import annotations
import math
import random
from typing import Any
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
DRAWING_FORMAT,
FRAME_LAYER_ID,
TABLE_LABEL_COLOR,
TABLE_VALUE_COLOR,
_circle_entity,
_layer,
_line_entity,
_text_entity,
polyline_entity,
table_entity,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Development import station_text, table_rows
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
drawing_area,
frame_entities,
scale_fields,
)
BOX_LAYER_ID = "b07-dev-box"
FILL_LAYER_ID = "b07-dev-fill"
LABEL_LAYER_ID = "b07-dev-label"
TABLE_LAYER_ID = "b07-dev-table"
BOX_COLOR = "#f5f7fa"
FILL_COLOR = "#aab4c0"
LEADER_COLOR = "#ff4d4d"
TABLE_COLOR = "#e03131"
#: 가로 축척 후보(분모) — 작은 것부터 대어 보고 처음 드는 것
SCALES = (50, 100, 200, 300, 500, 600, 1000, 1200, 2000, 2500, 3000, 5000)
_MARGIN = 10.0
_TITLE_H = 26.0 # 제목 자리
_LEAD = 18.0 # 박스 위 지시선 · 측점 글 자리
_BELOW = 9.0 # 박스 아래 L= 자리
_GAP = 16.0 # 박스 사이(오른 끝 기울인 글 자리)
_MIN_H, _MAX_H = 12.0, 40.0 # 가장 높은 박스 세로 mm
_EMPTY_H = 0.6 # 높이 빈칸 박스 = 가장 높은 박스의 몫
_FONT, _TITLE_FONT = 2.5, 7.0
_TABLE_FONT = 3.2 # 표는 그림 241 처럼 크게(작도 영역 폭의 약 1/4)
_ROW_H = 9.0
_COLUMNS = (80.0, 32.0, 32.0, 40.0)
_SLANT = (0.5, math.sqrt(3) / 2) # 60°
def _fmt(value: float | None) -> str:
return "" if value is None else f"{value:,.1f}"
def _size(value: float | None) -> str:
"""높이 · 폭 — 소수 둘째 자리까지(3.55 · 3.5 · 2.0) · 면적이 쓴 값 그대로 보이게."""
if value is None:
return ""
text = f"{value:.2f}".rstrip("0")
return f"{text}0" if text.endswith(".") else text
# ── 무늬 ────────────────────────────────────────────────
def _stone(rng: random.Random, cx: float, cy: float, r: float) -> list[tuple[float, float]]:
"""흔든 7각 돌 하나(닫힌 점열)."""
turn = rng.uniform(0, math.tau)
points = [
(
cx + r * rng.uniform(0.7, 1.0) * math.cos(turn + i * math.tau / 7),
cy + r * rng.uniform(0.7, 1.0) * math.sin(turn + i * math.tau / 7),
)
for i in range(7)
]
return [*points, points[0]]
def _stones(seed, rng, box, rows, cap=500):
"""박스 안 엇갈린 격자에 흔든 돌 — 높이에 `rows` 줄(크기 = 높이 몫) · 박스를 안 넘음."""
x0, y0, x1, y1 = box
r = min((y1 - y0) / (rows * 2.3), (y1 - y0) / 2 - 0.3, (x1 - x0) / 2 - 0.3)
if r < 0.3:
return []
step = 2.3 * r
out = []
y, row = y0 + 0.3 + r, 0
while y <= y1 - 0.3 - r + 1e-9 and len(out) < cap:
x = x0 + 0.3 + r + (step / 2 if row % 2 else 0.0)
while x <= x1 - 0.3 - r + 1e-9 and len(out) < cap:
room = (x - x0 - 0.3 - r, x1 - 0.3 - r - x) # 왼 · 오 여유 — 돌이 박스를 안 넘게
jx = max(-room[0], min(rng.uniform(-0.15, 0.15) * step, room[1]))
line = polyline_entity(
seed, _stone(rng, x + jx, y, r), FILL_LAYER_ID, FILL_COLOR, f":s{len(out)}"
)
if line:
out.append(line)
x += step
y += step * 0.87
row += 1
return out
def _dots(seed, rng, box, per_mm2, radius, cap):
x0, y0, x1, y1 = box
count = min(cap, int((x1 - x0) * (y1 - y0) * per_mm2))
return [
_circle_entity(
f"{seed}:d{i}",
(rng.uniform(x0 + radius, x1 - radius), rng.uniform(y0 + radius, y1 - radius)),
radius,
FILL_LAYER_ID,
FILL_COLOR,
)
for i in range(count)
]
def _grid(seed, box, step):
x0, y0, x1, y1 = box
out = []
x = x0 + step
while x < x1 - 0.1:
out.append(
_line_entity(f"{seed}:gx{len(out)}", (x, y0), (x, y1), FILL_LAYER_ID, FILL_COLOR)
)
x += step
y = y0 + step
while y < y1 - 0.1:
out.append(
_line_entity(f"{seed}:gy{len(out)}", (x0, y), (x1, y), FILL_LAYER_ID, FILL_COLOR)
)
y += step
return out
def _logs(seed, box):
"""통나무 · 목재틀 = 가운데 원 한 줄."""
x0, y0, x1, y1 = box
r = min((y1 - y0) / 2 - 0.3, 4.0)
if r < 0.3:
return []
count = max(int((x1 - x0 - 0.4) // (2 * r + 0.4)), 0)
cy = (y0 + y1) / 2
return [
_circle_entity(
f"{seed}:o{i}", (x0 + 0.4 + r + i * (2 * r + 0.4), cy), r, FILL_LAYER_ID, FILL_COLOR
)
for i in range(count)
]
def _stakes(seed, box, step=2.5):
"""바자 = 세운 말뚝 줄."""
x0, y0, x1, y1 = box
count = max(int((x1 - x0) // step), 0)
return [
_line_entity(
f"{seed}:k{i}",
(x0 + (i + 0.5) * step, y0),
(x0 + (i + 0.5) * step, y1),
FILL_LAYER_ID,
FILL_COLOR,
)
for i in range(count)
]
def fill_entities(seed: str, form: str, group: str, box: tuple[float, float, float, float]):
"""형태 → 무늬(찰 작은 돌 · 메 큰 돌 · 콘크리트 · 쇄석 점 · 돌망태 격자+돌 · 통나무 원)."""
rng = random.Random(seed)
if group == "gravel":
return _dots(seed, rng, box, 0.025, 0.9, 900)
if group == "concrete" or form == "콘크리트":
return _dots(seed, rng, box, 0.045, 0.6, 1200)
if form == "돌쌓기(찰)":
return _stones(seed, rng, box, 5)
if form == "돌쌓기(메)":
return _stones(seed, rng, box, 2.5)
if form == "돌망태":
cells = max(round((box[3] - box[1]) / 12.0), 1) # 망태 한 단 ≈ 12 mm
return _grid(seed, box, (box[3] - box[1]) / cells) + _stones(seed, rng, box, 3 * cells)
if form == "통나무·목재틀":
return _logs(seed, box)
if form == "바자":
return _stakes(seed, box)
return []
# ── 배치 ────────────────────────────────────────────────
def _vertical(rows: list[dict[str, Any]], mm_h: float) -> float:
"""세로 mm/m — 가로와 같게 두되 가장 높은 박스가 12~40 mm 안에 들게."""
tallest = max((r["height_m"] for r in rows if r["height_m"]), default=None)
if not tallest:
return _MIN_H
return min(max(mm_h, _MIN_H / tallest), _MAX_H / tallest)
def layout(rows, area, table_box, mm_h, strict=True):
"""박스 줄바꿈 배치 → [(row, x0, y0, x1, y1)] · 영역을 넘치면 None(strict)."""
ax0, ay0, ax1, ay1 = area
v = _vertical(rows, mm_h)
tallest = max((r["height_m"] or 0.0 for r in rows), default=0.0)
empty_h = (tallest * v if tallest else _MIN_H) * _EMPTY_H
left, top = ax0 + _MARGIN, ay1 - _TITLE_H
placed, line, x, line_h = [], [], left, 0.0
def right_limit(height: float) -> float:
bottom = top - _LEAD - height - _BELOW
return table_box[0] - _GAP if bottom < table_box[3] + 4.0 else ax1 - _MARGIN
for row in rows:
w = max((row["end_m"] - row["start_m"]) * mm_h, 2.0)
h = row["height_m"] * v if row["height_m"] else empty_h
if line and x + w > right_limit(max(line_h, h)):
top -= _LEAD + line_h + _BELOW
placed += line
line, x, line_h = [], left, 0.0
if strict and (
x + w > right_limit(max(line_h, h)) or top - _LEAD - h - _BELOW < ay0 + _MARGIN
):
return None
line.append((row, x, top - _LEAD - h, x + w, top - _LEAD))
x += w + _GAP
line_h = max(line_h, h)
return placed + line
def _choose(rows, area, table_box):
"""처음 드는 축척 — 끝까지 안 들면 가장 작은 축척으로 그대로(도각이 넘침 경고)."""
for denominator in SCALES:
placed = layout(rows, area, table_box, 1000.0 / denominator)
if placed is not None:
return denominator, placed
denominator = SCALES[-1]
return denominator, layout(rows, area, table_box, 1000.0 / denominator, strict=False)
# ── 글 · 표 ─────────────────────────────────────────────
def _box_entities(drawing_id, index, row, box, label, station):
x0, y0, x1, y1 = box
seed = f"{drawing_id}:b{index}"
out = [
polyline_entity(
drawing_id,
[(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)],
BOX_LAYER_ID,
BOX_COLOR,
f":b{index}",
)
]
out += fill_entities(f"{seed}:{row['key']}", row["form"], row["group"], (x0, y0, x1, y1))
height = "미입력" if row["height_m"] is None else _size(row["height_m"])
for end, x, chainage in (("s", x0, row["start_m"]), ("e", x1, row["end_m"])):
out.append(
_line_entity(
f"{seed}:{end}:lead", (x, y1), (x, y1 + _LEAD - 2.0), LABEL_LAYER_ID, LEADER_COLOR
)
)
out.append(
_text_entity(
f"{seed}:{end}:no",
station(chainage),
x + 0.8,
y1 + 1.2,
LABEL_LAYER_ID,
_FONT,
TABLE_LABEL_COLOR,
"left",
_SLANT,
)
)
out.append(
_text_entity(
f"{seed}:{end}:h",
f"{label}={height}",
x + 3.6,
y1 + 1.2,
LABEL_LAYER_ID,
_FONT,
TABLE_VALUE_COLOR,
"left",
_SLANT,
)
)
length = row["end_m"] - row["start_m"]
out.append(
_text_entity(
f"{seed}:len",
f"L={length:.1f}m",
(x0 + x1) / 2,
y0 - 5.0,
LABEL_LAYER_ID,
_FONT,
TABLE_LABEL_COLOR,
)
)
return [entity for entity in out if entity]
def _table(drawing_id, lines, label, origin):
"""측점(NO) · 거리(M) · 높이/폭(M) · 면적(㎡) · 계."""
def cell(text, align="center", color=TABLE_LABEL_COLOR):
return {"text": text, "align": align, "color": color}
head = "폭" if label == "B" else "높이"
cells = [
[cell("측 점"), cell("거 리"), cell(head), cell("면 적")],
[cell("(NO)"), cell("(M)"), cell("(M)"), cell("(㎡)")],
]
for item in lines:
cells.append(
[
cell(item["station"]),
cell(_fmt(item["distance"]), "right", TABLE_VALUE_COLOR),
cell(_size(item["height"]), "right", TABLE_VALUE_COLOR),
cell(_fmt(item["area"]), "right", TABLE_VALUE_COLOR),
]
)
areas = [item["area"] for item in lines if item["area"] is not None]
cells.append(
[
cell("계"),
cell(_fmt(sum(item["distance"] for item in lines)), "right", TABLE_VALUE_COLOR),
cell(""),
cell(_fmt(sum(areas)) if areas else "", "right", TABLE_VALUE_COLOR),
]
)
return table_entity(
f"{drawing_id}:table",
origin,
list(_COLUMNS),
[_ROW_H] * len(cells),
cells,
TABLE_LAYER_ID,
TABLE_COLOR,
_TABLE_FONT,
TABLE_LABEL_COLOR,
)
def build_development_drawing(
drawing_id: str, sheet: dict[str, Any], interval_m: float
) -> dict[str, Any]:
"""전개도 한 장 → {format, entities, layers} — 작도 영역에 그대로 놓고 도각을 두름."""
area = drawing_area()
ax0, ay0, ax1, ay1 = area
rows = sheet["rows"]
lines = table_rows(rows, interval_m)
label = "B" if sheet["group"] in ("concrete", "gravel") else "H"
table_w, table_h = sum(_COLUMNS), _ROW_H * (len(lines) + 3)
table_box = (ax1 - _MARGIN - table_w, ay0 + _MARGIN, ax1 - _MARGIN, ay0 + _MARGIN + table_h)
denominator, placed = _choose(rows, area, table_box)
def station(chainage: float) -> str:
return station_text(chainage, interval_m)
entities: list[dict[str, Any]] = [
_text_entity(
f"{drawing_id}:title",
sheet["title"],
(ax0 + ax1) / 2,
ay1 - 14.0,
LABEL_LAYER_ID,
_TITLE_FONT,
TABLE_LABEL_COLOR,
),
_text_entity(
f"{drawing_id}:scale",
f"S=1:{denominator:,}",
ax1 - _MARGIN,
ay1 - 14.0,
LABEL_LAYER_ID,
_FONT,
TABLE_LABEL_COLOR,
"right",
),
]
for index, (row, *box) in enumerate(placed or []):
entities += _box_entities(drawing_id, index, row, tuple(box), label, station)
entities.append(_table(drawing_id, lines, label, (table_box[0], table_box[3])))
if sheet.get("missing"):
entities.append(
_text_entity(
f"{drawing_id}:missing",
f"길이 미입력 {sheet['missing']}곳 — 그리지 않음",
table_box[2],
table_box[3] + 4.0,
LABEL_LAYER_ID,
_FONT,
TABLE_VALUE_COLOR,
"right",
)
)
if not rows and not sheet.get("missing"):
entities.append(
_text_entity(
f"{drawing_id}:empty",
"그릴 구조물이 없습니다 — 목록을 새로 고침",
(ax0 + ax1) / 2,
(ay0 + ay1) / 2,
LABEL_LAYER_ID,
_FONT * 2,
TABLE_VALUE_COLOR,
)
)
title = sheet["title"]
entities += frame_entities(
drawing_id,
area,
fit=False,
bottom_left=True,
fields={"도면명": title, **scale_fields(("", denominator))},
)
return {
"format": DRAWING_FORMAT,
"entities": entities,
"layers": [
_layer(BOX_LAYER_ID, "전개도 박스"),
_layer(FILL_LAYER_ID, "재질 무늬"),
_layer(LABEL_LAYER_ID, "측점 · 치수"),
_layer(TABLE_LAYER_ID, "전개도 표"),
_layer(FRAME_LAYER_ID, "도각", locked=True),
],
}