Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
206 lines
7.9 KiB
Python
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
|