fix(B07): 횡단면도 박스를 단면마다 자기 그림 + 1 mm 로 — 칸 통일 없앰

- 장 배치: 칸을 그 장 가장 큰 블록으로 통일하던 것을 없애고 블록을 자기 크기로 열마다 쌓음(좌하단부터 아래→위, 차면 오른쪽 열)
- 그리는 가로 범위를 선이 있는 데까지로 — 원지반 자료가 짧으면 빈 여유가 박스를 밀던 것
- 블록 크기에 암 경계선 포함(실제 그림과 같은 규칙)
- 옛 배치로 확정된 장은 저장본을 그대로 보이되 [수정] 안내 알림
- 검증_sub4: 박스 ↔ 그림 66개 모두 1.0 mm · 같은 조건 27장 → 20장

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
2026-10-06 23:11:14 +09:00
co-authored by Claude Opus 5.5
parent d42efb162b
commit 67bba96227
6 changed files with 196 additions and 70 deletions
@@ -32,3 +32,120 @@ def test_frame_hugs_section_by_one_mm() -> None:
width, height = section_block_size(_SOURCE, None, with_table=False)
assert height == pytest.approx(f_max_y - f_min_y)
assert width == pytest.approx(f_max_x - f_min_x)
# ── 68-1 장 배치: 단면마다 자기 박스(칸 통일 없음) ──────────────────────────────
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import CROSS_SECTION_MARGIN_M # noqa: E402
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import ( # noqa: E402
build_cross_sheet,
plan_cross_sheets,
)
def _source(chainage: float, slope: float, half: int = 10) -> dict:
return {
"chainage_m": chainage,
"samples": [
{"valid": True, "offset_m": float(x), "elevation_m": 100.0 + slope * x}
for x in range(-half, half + 1)
],
}
def _box(entity: dict) -> tuple[float, float, float, float] | None:
if entity.get("type") == "Table":
shape = entity["shapeData"]
origin = shape["origin"]
return (
origin["x"],
origin["y"] - sum(shape["rowHeights"]),
origin["x"] + sum(shape["columnWidths"]),
origin["y"],
)
return entities_bbox([entity])
_TABLE = {"ground": 100.0, "planned": 100.0}
def test_each_block_hugs_its_own_drawing_by_one_mm() -> None:
"""크기 다른 단면 셋을 한 장에 — 박스마다 자기 그림(표 포함)에서 사방 1.0 mm."""
sources = [_source(0.0, 0.05), _source(20.0, 0.6, 15), _source(40.0, 0.2, 6)]
blocks = [(s["chainage_m"], *section_block_size(s, None, design=None)) for s in sources]
sheets = plan_cross_sheets(blocks)
assert len(sheets) == 1
sections = [
{"chainage": s["chainage_m"], "source": s, "quantity_table": _TABLE, "title": ""}
for s in sources
]
drawing = build_cross_sheet(sheets[0], sections)
inner = [_box(e) for e in drawing["entities"] if e.get("layerId") != FRAME_LAYER_ID]
frames = [p["frame"] for p in drawing["cross_placements"]]
sizes = {(round(f[2] - f[0], 1), round(f[3] - f[1], 1)) for f in frames}
assert len(sizes) == 3 # 칸 통일 없음 — 박스 크기가 단면마다 다르다
for left, bottom, right, top in frames:
mine = [
b
for b in inner
if b
and b[0] >= left - 1e-6
and b[2] <= right + 1e-6
and b[1] >= bottom - 1e-6
and b[3] <= top + 1e-6
]
assert min(b[0] for b in mine) - left == pytest.approx(1.0)
assert right - max(b[2] for b in mine) == pytest.approx(1.0)
assert min(b[1] for b in mine) - bottom == pytest.approx(1.0)
assert top - max(b[3] for b in mine) == pytest.approx(1.0)
def test_small_blocks_are_not_widened_by_a_big_one() -> None:
"""큰 단면 하나 + 작은 단면 여럿 — 칸을 큰 블록으로 통일하던 때보다 한 장에 더 담긴다."""
blocks = [(0.0, 300.0, 300.0)] + [(float(i), 110.0, 60.0) for i in range(1, 30)]
sheets = plan_cross_sheets(blocks)
# 옛 배치(칸 300×300 → A1 739×499 에 2×1)는 첫 장에 2개였다.
assert len(sheets[0]["chainages"]) > 2
assert sum(len(s["chainages"]) for s in sheets) == 30
def test_window_stops_where_lines_end() -> None:
"""원지반 자료가 여유(1.5 m)보다 짧으면 범위를 선 끝에서 멈춘다 — 빈 여유가 박스를 밀지 않음."""
source = _source(0.0, 0.1, 6)
design_line = [
{"offset_m": -6.0, "elevation_m": 99.4},
{"offset_m": -5.5, "elevation_m": 98.0},
{"offset_m": 5.5, "elevation_m": 98.0},
{"offset_m": 6.0, "elevation_m": 100.6},
]
drawing = build_cross_drawing(source, "cross_00000m", design_line, None, None, "")
placement = drawing["cross_placements"][0]
assert placement["x0"] == pytest.approx(-6.0)
assert placement["x1"] == pytest.approx(6.0)
assert CROSS_SECTION_MARGIN_M == 1.5
def test_old_confirmed_cross_sheet_is_flagged(tmp_path) -> None:
"""68-1 전에 확정한 횡단 장은 저장본을 그대로 보이되 「옛 배치」 표시를 단다."""
import json
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import DRAWING_FORMAT
from B07_DesignDetail.B07_DesignDetail_Router_Support import (
CROSS_SHEET_LAYOUT,
_read_drawing,
)
stage = tmp_path / "B07_DesignDetail"
(stage / "drawings").mkdir(parents=True)
saved = {"format": DRAWING_FORMAT, "entities": [], "layers": []}
(stage / "drawings" / "cross_s00000m.json").write_text(json.dumps(saved), encoding="utf-8")
entry = {"kind": "cross", "label": "횡단면도 1장", "confirmed": True}
manifest = {"drawings": {"cross_s00000m": entry}}
(stage / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
_k, _l, drawing, confirmed, _t = _read_drawing(tmp_path, tmp_path / "x.json", "cross_s00000m")
assert confirmed and drawing.get("stale_layout") is True
entry["layout"] = CROSS_SHEET_LAYOUT
(stage / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
_k, _l, drawing, confirmed, _t = _read_drawing(tmp_path, tmp_path / "x.json", "cross_s00000m")
assert confirmed and "stale_layout" not in drawing