Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cross_Table_Edit.py
T

206 lines
7.9 KiB
Python

"""횡단 수량표 손질(179-4) — 횡단 설계 patch `detail_table` → 칸 값 · 표 배치 한 벌.
모달(`B06_Section_Router_CrossTable`)과 B07 도면 표(`_Engine_Cad_Table`)가 같은 함수를 쓴다.
칸 출처(`detail_table.cells` · 표 키 → 출처) — 그릴 때마다 서버가 다시 셈(브라우저 값 안 받음):
area = 설계 면적 칸 그대로(`AREA_KEYS` · 띠 키 = 표 키)
length = 두 끝(m · offset · 표고)이 설계선 위면 hypot · 아니면 비움 → 「다시 고름」
text = 글 그대로 — 더한 칸(`r:` 행 · `c:` 열)만
행(`rows`) = 묶음 안(이 행 아래 · 묶음이면 하위 줄) 또는 끝에 항목 줄(좌 · 중 · 우 이름 + 값).
열(`columns`) = 오른쪽 끝 · 모든 행(비고 등).
짝(① 파이썬 · TS) = `B06_Section/B06_Section_UI_Cross_Detail_Table.ts` `localCellValue`
(조작 중 즉시 표시) · 거울 시험 `resources/tester/test_179_4_detail_table.py`.
"""
from __future__ import annotations
from math import hypot
from typing import Any
from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Quantity import AREA_KEYS
_AREA_DESIGN = dict(AREA_KEYS)
#: 두 끝이 설계선에서 이만큼(m) 안이면 「선 위」
LINE_TOLERANCE_M = 0.05
#: 더한 행 · 열 상한 — 도면 표가 장 칸을 넘지 않게
MAX_ROWS = 12
MAX_COLUMNS = 4
Slot = tuple[str | None, str | None, str | None]
def _edit(design: dict[str, Any] | None) -> dict[str, Any]:
edit = design.get("detail_table") if isinstance(design, dict) else None
return edit if isinstance(edit, dict) else {}
def _num(value: Any) -> float | None:
return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
def _point(value: Any) -> tuple[float, float] | None:
if not isinstance(value, list) or len(value) != 2:
return None
x, y = _num(value[0]), _num(value[1])
return (x, y) if x is not None and y is not None else None
def _design_line(design: dict[str, Any]) -> list[tuple[float, float]]:
points = []
for point in design.get("design_line") or []:
if isinstance(point, dict):
x, y = _num(point.get("offset_m")), _num(point.get("elevation_m"))
if x is not None and y is not None:
points.append((x, y))
return points
def _on_line(point: tuple[float, float], line: list[tuple[float, float]]) -> bool:
"""점이 설계선(꺾은선) 조각 하나에서 `LINE_TOLERANCE_M` 안인가."""
px, py = point
for (ax, ay), (bx, by) in zip(line, line[1:]):
dx, dy = bx - ax, by - ay
length2 = dx * dx + dy * dy
t = 0.0 if length2 == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / length2))
if hypot(px - (ax + t * dx), py - (ay + t * dy)) <= LINE_TOLERANCE_M:
return True
return False
def cell_value(
source: dict[str, Any], design: dict[str, Any] | None
) -> tuple[float | None, str | None, bool]:
"""출처 하나 → (숫자, 글, 다시 고름)."""
design = design if isinstance(design, dict) else {}
kind = source.get("src")
if kind == "area":
return _num(design.get(_AREA_DESIGN.get(str(source.get("key")), ""))), None, False
if kind == "length":
start, end = _point(source.get("start")), _point(source.get("end"))
line = _design_line(design)
if start and end and _on_line(start, line) and _on_line(end, line):
return hypot(end[0] - start[0], end[1] - start[1]), None, False
return None, None, True
if kind == "text":
text = source.get("text")
return None, text if isinstance(text, str) else "", False
return None, None, False
def edited_cells(
design: dict[str, Any] | None,
) -> tuple[dict[str, float | None], dict[str, str], list[str]]:
"""patch 칸 출처 → (숫자 칸, 글 칸, 다시 고를 칸). 글은 더한 칸만."""
values: dict[str, float | None] = {}
texts: dict[str, str] = {}
repick: list[str] = []
cells = _edit(design).get("cells")
for key, source in (cells if isinstance(cells, dict) else {}).items():
if not isinstance(source, dict):
continue
number, text, again = cell_value(source, design)
if text is not None:
if key[:2] in ("r:", "c:"):
texts[key] = text
continue
values[key] = number
if again:
repick.append(key)
return values, texts, repick
def _labels(row: dict[str, Any]) -> list[str]:
raw = row.get("labels")
raw = raw if isinstance(raw, list) else []
return [str(raw[i]) if i < len(raw) and isinstance(raw[i], str) else "" for i in range(3)]
def _added_rows(edit: dict[str, Any]) -> list[dict[str, Any]]:
"""쓸 수 있는 더한 행 — id = 「r…」 · 겹친 id 는 앞 것만 · 상한 `MAX_ROWS`."""
raw = edit.get("rows")
rows: list[dict[str, Any]] = []
for row in raw if isinstance(raw, list) else []:
row_id = row.get("id") if isinstance(row, dict) else None
if (
isinstance(row_id, str)
and row_id.startswith("r")
and all(r["id"] != row_id for r in rows)
):
rows.append(row)
return rows[:MAX_ROWS]
def added_row_count(design: dict[str, Any] | None) -> int:
"""더한 행 수 — 도면 표 높이(`cross_table_height`)가 씀."""
return len(_added_rows(_edit(design)))
def _slot(anchor: Slot | None, label: str, key: str) -> Slot:
"""묶음(하위 이름이 있는 줄) 아래면 하위 줄 · 아니면 홀로 선 이름."""
if anchor is not None and anchor[1] is not None:
return (None, label, key)
return (label, None, key)
def table_layout(
design: dict[str, Any] | None,
header: tuple[tuple[str, str], ...],
left: tuple[Slot, ...],
middle: tuple[Slot, ...],
right: tuple[tuple[str | None, str | None], ...],
) -> dict[str, Any]:
"""납품 양식(머리 넷 쌍 + 본문 좌 · 중 · 우) + 더한 행 · 열 = 실제 표 배치."""
edit = _edit(design)
rows: list[tuple[str, Slot, Slot, tuple[str | None, str | None]]] = [
(f"b{index}", left[index], middle[index], right[index]) for index in range(len(left))
]
for row in _added_rows(edit):
row_id = row["id"]
labels = _labels(row)
index = next((i for i, r in enumerate(rows) if r[0] == row.get("after")), None)
anchor = rows[index] if index is not None and row.get("grouped") else None
rows.insert(
len(rows) if index is None else index + 1,
(
row_id,
_slot(anchor[1] if anchor else None, labels[0], f"r:{row_id}:0"),
_slot(anchor[2] if anchor else None, labels[1], f"r:{row_id}:1"),
(labels[2], f"r:{row_id}:2"),
),
)
columns = []
raw_columns = edit.get("columns")
for column in (raw_columns if isinstance(raw_columns, list) else [])[:MAX_COLUMNS]:
column_id = column.get("id") if isinstance(column, dict) else None
if not isinstance(column_id, str) or not column_id:
continue
label = column.get("label")
columns.append(
{
"id": column_id,
"label": label if isinstance(label, str) else "",
"keys": [f"c:{column_id}:{r[0]}" for r in rows],
}
)
return {
"header": [list(pair) for pair in header],
"left": [list(r[1]) for r in rows],
"middle": [list(r[2]) for r in rows],
"right": [list(r[3]) for r in rows],
"rows": [r[0] for r in rows],
"columns": columns,
}
def group_span(slots: list[list[Any]] | list[Slot], index: int) -> int:
"""묶음 이름 칸이 덮는 줄 수 — 이 줄 + 뒤따르는 하위 줄(이름 없음 · 하위 이름 있음)."""
span = 1
while (
index + span < len(slots)
and slots[index + span][0] is None
and slots[index + span][1] is not None
):
span += 1
return span