Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Table.py
T

322 lines
11 KiB
Python

"""B07 횡단 수량 산출표 — 납품 양식 표 작도와 편집값 역추출.
표는 도면 좌표(종이 mm) 기준이다. 값 셀 id는 uuid5(NS, "{drawing_id}:qtable:{key}")로
정해져 있어, 사용자가 CAD에서 고친 숫자를 그대로 되읽을 수 있다.
"""
from typing import Any
from uuid import uuid5
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
_ENTITY_NS,
CROSS_TABLE_LAYER_ID,
TABLE_LABEL_COLOR,
TABLE_LINE_COLOR,
TABLE_VALUE_COLOR,
_format,
_line_entity,
_text_entity,
)
# 횡단 수량 산출표 (납품 양식). 본문 6행 × 3그룹.
# 좌: 깍기(토사/암석)·측구(토사/암석)·쌓기·층따기
# 중: 면고르기(성토/절토)·지장목제거(성토/절토)·표토제거(성토/절토)
# 우: 편책·성토파종·절토살포·제근·(공란)·노면다짐
_CROSS_HEADER_KEYS: tuple[tuple[str, str], ...] = (
("지반고", "ground"),
("계획고", "planned"),
("성토고", "fill"),
("절토고", "cut"),
)
# 좌측 그룹: (그룹 라벨 or None, 하위 라벨 or None, 값 키 or None) — 행 순서대로.
_CROSS_LEFT_ROWS: tuple[tuple[str | None, str | None, str | None], ...] = (
("깍기", "토사", "cut_soil"),
(None, "암석", "cut_rock"),
("측구", "토사", "ditch_soil"),
(None, "암석", "ditch_rock"),
("쌓기", None, "embankment"),
("층따기", None, "benching"),
)
_CROSS_MIDDLE_ROWS: tuple[tuple[str | None, str, str], ...] = (
("면고르기", "성토", "grading_fill"),
(None, "절토", "grading_cut"),
("지장목제거", "성토", "tree_removal_fill"),
(None, "절토", "tree_removal_cut"),
("표토제거", "성토", "topsoil_fill"),
(None, "절토", "topsoil_cut"),
)
_CROSS_RIGHT_ROWS: tuple[tuple[str | None, str | None], ...] = (
("편책", "fence"),
("성토파종", "fill_seeding"),
("절토살포", "cut_spraying"),
("제근", "grubbing"),
(None, None),
("노면다짐", "road_compaction"),
)
QUANTITY_VALUE_KEYS: tuple[str, ...] = (
"ground",
"planned",
"fill",
"cut",
"cut_soil",
"cut_rock",
"ditch_soil",
"ditch_rock",
"embankment",
"benching",
"grading_fill",
"grading_cut",
"tree_removal_fill",
"tree_removal_cut",
"topsoil_fill",
"topsoil_cut",
"fence",
"fill_seeding",
"cut_spraying",
"grubbing",
"road_compaction",
)
# 표 치수는 종이 밀리미터다 (도면 좌표 = mm).
_CROSS_TABLE_WIDTH = 108.0
_CROSS_TABLE_ROW_HEIGHT = 5.0
_CROSS_TABLE_FONT = 2.0
# 열 경계 비율 (좌: 그룹/하위/값/여백, 중: 그룹/하위/값/여백, 우: 라벨/값/여백)
_CROSS_COLUMN_WEIGHTS: tuple[float, ...] = (1.0, 1.7, 2.1, 1.0, 1.9, 1.4, 2.1, 1.0, 2.6, 2.1, 1.0)
def _cross_column_edges(width: float, center_x: float = 0.0) -> list[float]:
total = sum(_CROSS_COLUMN_WEIGHTS)
left = center_x - width / 2.0
edges = [left]
accumulated = 0.0
for weight in _CROSS_COLUMN_WEIGHTS:
accumulated += weight
edges.append(left + width * accumulated / total)
return edges
def _cross_table_entities(
drawing_id: str,
quantity_table: dict[str, float | None],
table_top: float,
title_label: str,
center_x: float = 0.0,
) -> list[dict[str, Any]]:
"""횡단 수량 산출표(납품 양식)를 병합 셀 그리드+텍스트로 만든다.
표는 center_x를 가운데로 놓는다 — 한 장에 여러 단면을 배치할 때 각 단면
블록의 중심으로 옮기기 위한 것이다.
"""
width = _CROSS_TABLE_WIDTH
row_h = _CROSS_TABLE_ROW_HEIGHT
font = _CROSS_TABLE_FONT
left = -width / 2.0
right = width / 2.0
edges = _cross_column_edges(width, center_x)
entities: list[dict[str, Any]] = []
def value_text(seed_key: str, x: float, y: float) -> dict[str, Any]:
value = quantity_table.get(seed_key)
return _text_entity(
f"{drawing_id}:qtable:{seed_key}",
_format(value) if isinstance(value, (int, float)) else "-",
x,
y,
CROSS_TABLE_LAYER_ID,
font,
TABLE_VALUE_COLOR,
)
def label_text(seed: str, label: str, x: float, y: float) -> dict[str, Any]:
return _text_entity(seed, label, x, y, CROSS_TABLE_LAYER_ID, font, TABLE_LABEL_COLOR)
def h_line(seed: str, x_from: float, x_to: float, y: float) -> None:
entities.append(
_line_entity(seed, (x_from, y), (x_to, y), CROSS_TABLE_LAYER_ID, TABLE_LINE_COLOR)
)
def v_line(seed: str, x: float, y_from: float, y_to: float) -> None:
entities.append(
_line_entity(seed, (x, y_from), (x, y_to), CROSS_TABLE_LAYER_ID, TABLE_LINE_COLOR)
)
# ── 행 y 좌표 (제목행, 헤더행, 본문 6행)
y_title_top = table_top
y_header_top = y_title_top - row_h
y_body_top = y_header_top - row_h
body_rows = len(_CROSS_LEFT_ROWS)
y_bottom = y_body_top - row_h * body_rows
def body_y(row_index: int) -> float:
return y_body_top - row_h * row_index
# ── 외곽/가로선
h_line(f"{drawing_id}:qgrid:top", left, right, y_title_top)
h_line(f"{drawing_id}:qgrid:title", left, right, y_header_top)
h_line(f"{drawing_id}:qgrid:header", left, right, y_body_top)
h_line(f"{drawing_id}:qgrid:bottom", left, right, y_bottom)
v_line(f"{drawing_id}:qgrid:vl", left, y_title_top, y_bottom)
v_line(f"{drawing_id}:qgrid:vr", right, y_title_top, y_bottom)
# 본문 행 사이 가로선 — 그룹 병합 셀(좌 colA, 중 colE)은 병합 지속 구간에서 끊는다.
for boundary in range(1, body_rows):
y = body_y(boundary)
left_merged = _CROSS_LEFT_ROWS[boundary][0] is None
middle_merged = _CROSS_MIDDLE_ROWS[boundary][0] is None
seed = f"{drawing_id}:qgrid:b{boundary}"
if left_merged:
h_line(f"{seed}:l", edges[1], edges[4], y)
else:
h_line(f"{seed}:l", edges[0], edges[4], y)
if middle_merged:
h_line(f"{seed}:m", edges[5], edges[8], y)
else:
h_line(f"{seed}:m", edges[4], edges[8], y)
h_line(f"{seed}:r", edges[8], edges[11], y)
# ── 헤더행 (지반고/계획고/성토고/절토고): 4쌍 균등 분할
header_cell = width / 8.0
for pair_index, (label, key) in enumerate(_CROSS_HEADER_KEYS):
x_label = left + header_cell * (pair_index * 2 + 0.5)
x_value = left + header_cell * (pair_index * 2 + 1.5)
y_mid = y_header_top - row_h * 0.5
entities.append(label_text(f"{drawing_id}:qlabel:h:{key}", label, x_label, y_mid))
entities.append(value_text(key, x_value, y_mid))
if pair_index > 0:
v_line(
f"{drawing_id}:qgrid:hv{pair_index}",
left + header_cell * pair_index * 2,
y_header_top,
y_body_top,
)
v_line(
f"{drawing_id}:qgrid:hvl{pair_index}",
left + header_cell * (pair_index * 2 + 1),
y_header_top,
y_body_top,
)
# ── 제목행 (No.측점)
entities.append(
_text_entity(
f"{drawing_id}:qtitle",
title_label,
left + width * 0.03,
y_title_top - row_h * 0.5,
CROSS_TABLE_LAYER_ID,
font * 1.15,
TABLE_LABEL_COLOR,
align="left",
)
)
# ── 본문 세로선: colA|B 경계는 그룹 라벨이 하위 라벨과 분리된 행(깍기~측구 4행)만.
ab_rows = [i for i, row in enumerate(_CROSS_LEFT_ROWS) if row[1] is not None]
if ab_rows:
v_line(
f"{drawing_id}:qgrid:vab",
edges[1],
body_y(min(ab_rows)),
body_y(max(ab_rows) + 1),
)
for edge_index in (2, 3, 4, 5, 6, 7, 8, 9, 10):
v_line(f"{drawing_id}:qgrid:vc{edge_index}", edges[edge_index], y_body_top, y_bottom)
# ── 본문 셀 (좌/중/우 그룹)
def cell_mid(edge_from: int, edge_to: int) -> float:
return (edges[edge_from] + edges[edge_to]) / 2.0
for row_index in range(body_rows):
y_mid = body_y(row_index) - row_h * 0.5
group_l, sub_l, key_l = _CROSS_LEFT_ROWS[row_index]
if group_l is not None and sub_l is not None:
# 그룹 라벨은 2행 병합 중앙 배치
y_group = body_y(row_index) - row_h # 병합 2행의 중앙
entities.append(
label_text(f"{drawing_id}:qlabel:lg:{row_index}", group_l, cell_mid(0, 1), y_group)
)
elif group_l is not None:
entities.append(
label_text(f"{drawing_id}:qlabel:lg:{row_index}", group_l, cell_mid(0, 2), y_mid)
)
if sub_l is not None:
entities.append(
label_text(f"{drawing_id}:qlabel:ls:{row_index}", sub_l, cell_mid(1, 2), y_mid)
)
if key_l is not None:
entities.append(value_text(key_l, cell_mid(2, 3), y_mid))
group_m, sub_m, key_m = _CROSS_MIDDLE_ROWS[row_index]
if group_m is not None:
y_group = body_y(row_index) - row_h
entities.append(
label_text(f"{drawing_id}:qlabel:mg:{row_index}", group_m, cell_mid(4, 5), y_group)
)
entities.append(
label_text(f"{drawing_id}:qlabel:ms:{row_index}", sub_m, cell_mid(5, 6), y_mid)
)
entities.append(value_text(key_m, cell_mid(6, 7), y_mid))
label_r, key_r = _CROSS_RIGHT_ROWS[row_index]
if label_r is not None:
entities.append(
label_text(f"{drawing_id}:qlabel:r:{row_index}", label_r, cell_mid(8, 9), y_mid)
)
if key_r is not None:
entities.append(value_text(key_r, cell_mid(9, 10), y_mid))
return entities
def extract_quantity_table(
drawing_id: str, drawing: dict[str, Any]
) -> dict[str, float | None] | None:
"""확정 도면 JSON에서 수량 산출표 Text 값을 결정적 id로 역추출한다.
값 셀 id = uuid5(NS, "{drawing_id}:qtable:{key}"). 하나도 없으면 None을
반환해 호출부가 요청 본문 quantity_table 폴백을 쓰게 한다.
"""
id_to_key = {
str(uuid5(_ENTITY_NS, f"{drawing_id}:qtable:{key}")): key for key in QUANTITY_VALUE_KEYS
}
entities = drawing.get("entities")
if not isinstance(entities, list):
return None
table: dict[str, float | None] = {}
found = False
for entity in entities:
if not isinstance(entity, dict):
continue
key = id_to_key.get(str(entity.get("id")))
if key is None:
continue
found = True
shape = entity.get("shapeData")
label = shape.get("label") if isinstance(shape, dict) else None
try:
table[key] = float(str(label).replace(",", ""))
except (TypeError, ValueError):
table[key] = None
if not found:
return None
for key in QUANTITY_VALUE_KEYS:
table.setdefault(key, None)
ground = table.get("ground")
planned = table.get("planned")
if isinstance(ground, (int, float)) and isinstance(planned, (int, float)):
table["cut"] = max(ground - planned, 0.0)
table["fill"] = max(planned - ground, 0.0)
return table
def cross_table_width() -> float:
"""수량 산출표 가로 폭(mm)."""
return _CROSS_TABLE_WIDTH
def cross_table_height() -> float:
"""수량 산출표 세로 높이(mm) — 머리 2행 + 본문 6행."""
return _CROSS_TABLE_ROW_HEIGHT * (len(_CROSS_LEFT_ROWS) + 2)