작성 척도(횡단 1/100)를 도면 좌표에 넣고, A1 한 장에 들어가는 만큼 단면을 담는다. - 횡단 좌표를 종이 mm로(x10). 수량표도 종이 치수로 재조정(폭 108mm, 행 5mm) - 단면 폭 = 설계선이 원지반에서 갈라졌다 만나는 구간 + 여유 1.5m. 지표선도 그 범위로 자른다. 실측 갈라짐 폭 4.0~15.0m (종전 전폭 40m) - Engine_Cad_Sheet 신설: 칸 크기는 최대 블록 기준, 좌하단부터 아래→위, 그다음 오른쪽 열로 채우고 넘치면 다음 장. 23측점 -> 4장(3열x2행) - 목록·확정을 장 단위로. 확정 시 담긴 측점 전부의 B06 단면적을 재계산하고 수량표는 측점별로 매니페스트에 보관 - 700줄 제한: 수량표를 Engine_Cad_Table로, 라우터 동기 헬퍼를 Router_Support로 분리 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
322 lines
12 KiB
Python
322 lines
12 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 = center_x - width / 2.0
|
|
right = center_x + 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)
|