- 시험: 무늬는 박스 안(형태 다섯 · 견본 서른) · 확정본 kind development · 3.55 폭 표기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
441 lines
15 KiB
Python
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),
|
|
],
|
|
}
|