# -*- coding: utf-8 -*- """140-3 B07 횡단면도 장 배치 — 장마다 열 × 행 칸(기본 4 × 4). 못박는 것 - 칸 = 그림칸(usable_area)을 똑같이 나눈 자리 · 차례 = 좌하단 → 위 → 오른쪽 열. - 측점 73개 · 4 × 4 → 16 · 16 · 16 · 16 · 9 · 17번째 측점 = 2장 첫 칸. - 장마다 다른 배치(sheets {장 번호: [열, 행]}) · 기본 바꾸기. - 단면 블록 가운데 = 칸 가운데 · 블록 크기 = 1/100 그대로(section_block_size). - 도각 = 고정 그림칸 — 내용이 작아도 커도 같은 자리. - 배치 저장 · 읽기(깨진 값 거절) · 담긴 측점이 바뀐 확정 장만 풂. """ import json from pathlib import Path import pytest from B07_DesignDetail.B07_DesignDetail_Engine_Cad import FRAME_LAYER_ID from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import ( build_cross_sheet, grid_slots, plan_cross_sheets, section_block_size, ) from B07_DesignDetail.B07_DesignDetail_Engine_Template import entities_bbox, usable_area from B07_DesignDetail.B07_DesignDetail_Router_Support import ( _cross_sheet_plan, read_cross_layout, write_cross_layout, ) from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest, _write_manifest 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 test_grid_slots_bottom_left_up_then_right() -> None: w, h = usable_area() slots = grid_slots(6, 4, 4) cw, ch = w / 4, h / 4 assert slots[0] == pytest.approx([-w / 2 + cw / 2, -h / 2 + ch / 2]) # 좌하단 assert slots[1] == pytest.approx([-w / 2 + cw / 2, -h / 2 + 1.5 * ch]) # 그 위 assert slots[4] == pytest.approx([-w / 2 + 1.5 * cw, -h / 2 + ch / 2]) # 다음 열 맨 아래 def test_73_stations_default_grid_17th_is_sheet_two() -> None: chainages = [i * 20.0 for i in range(73)] sheets = plan_cross_sheets(chainages) assert [len(s["chainages"]) for s in sheets] == [16, 16, 16, 16, 9] assert sheets[1]["chainages"][0] == chainages[16] # 17번째 측점 = 2장 첫 칸 assert sheets[1]["number"] == 2 and sheets[1]["grid"] == [4, 4] assert sheets[1]["id"] == "cross_s00320m" # 장 id = 첫 측점 def test_per_sheet_and_default_layout() -> None: chainages = [float(i) for i in range(40)] layout = {"default": [3, 3], "sheets": {"1": [2, 2], "3": [5, 4]}} sheets = plan_cross_sheets(chainages, layout) assert [s["grid"] for s in sheets] == [[2, 2], [3, 3], [5, 4], [3, 3]] assert [len(s["chainages"]) for s in sheets] == [4, 9, 20, 7] def _frame_bbox(drawing: dict) -> tuple: return entities_bbox([e for e in drawing["entities"] if e.get("layerId") == FRAME_LAYER_ID]) def test_block_centered_in_cell_at_scale_and_frame_fixed() -> None: table = {"ground": 100.0, "planned": 100.0} small = [_source(0.0, 0.05, 6)] big = [_source(0.0, 0.6, 15), _source(20.0, 0.2, 8)] drawings = [] for sources in (small, big): sheet = plan_cross_sheets([s["chainage_m"] for s in sources])[0] sections = [ {"chainage": s["chainage_m"], "source": s, "quantity_table": table, "title": ""} for s in sources ] drawing = build_cross_sheet(sheet, sections) for source, slot, placement in zip( sources, sheet["slots"], drawing["cross_placements"], strict=True ): left, bottom, right, top = placement["frame"] # 블록 가운데 = 칸 가운데 assert (left + right) / 2 == pytest.approx(slot[0]) assert (bottom + top) / 2 == pytest.approx(slot[1]) # 1/100 그대로 — 블록 크기가 자기 치수 그대로(칸 맞춤으로 줄이지 않음) width, height = section_block_size(source, None, design=None) assert right - left == pytest.approx(width, abs=0.5) drawings.append(drawing) # 도각 = 고정 그림칸 — 내용 크기와 무관하게 같은 자리 assert _frame_bbox(drawings[0]) == pytest.approx(_frame_bbox(drawings[1])) def _project(tmp_path: Path, count: int) -> tuple[Path, Path]: root = tmp_path / "project" cross_dir = root / "B06_Section" / "cross_sections" cross_dir.mkdir(parents=True) longitudinal_path = root / "B06_Section" / "longitudinal" / "longitudinal.json" longitudinal_path.parent.mkdir(parents=True) stations = [] for i in range(count): chainage = i * 20 stations.append({"chainage_m": float(chainage), "center_x": 0.0, "center_y": 0.0}) (cross_dir / f"cross_{chainage:05d}m.json").write_text( json.dumps(_source(float(chainage), 0.1)), "utf-8" ) longitudinal_path.write_text(json.dumps({"stations": stations}), "utf-8") return root, longitudinal_path def test_layout_store_and_release_changed_confirmed(tmp_path: Path) -> None: root, longitudinal_path = _project(tmp_path, 20) assert read_cross_layout(root) == {"default": [4, 4], "sheets": {}} # 파일 없으면 기본 ids = [s["id"] for s in _cross_sheet_plan(root, longitudinal_path)] assert ids == ["cross_s00000m", "cross_s00320m"] # 16 + 4 manifest = _read_manifest(root) manifest["drawings"] = {i: {"confirmed": True} for i in ids} _write_manifest(root, manifest) # 2장만 3 × 3 — 1장 측점 그대로(확정 유지) · 2장 측점 그대로(4개 < 9칸) → 풀 장 없음 assert ( write_cross_layout(root, longitudinal_path, {"default": [4, 4], "sheets": {"2": [3, 3]}}) == [] ) assert read_cross_layout(root)["sheets"] == {"2": [3, 3]} # 1장 3 × 3 — 1장 9개로 줄고 2장은 다른 측점에서 시작 → 1장 풂(옛 측점 16개 돌려줌) stale = write_cross_layout( root, longitudinal_path, {"default": [4, 4], "sheets": {"1": [3, 3]}} ) assert stale[:16] == [i * 20 for i in range(16)] drawings = _read_manifest(root)["drawings"] assert drawings["cross_s00000m"]["confirmed"] is False assert drawings["cross_s00320m"]["confirmed"] is False # 사라진 장 id 도 풂 new_ids = [s["id"] for s in _cross_sheet_plan(root, longitudinal_path)] assert new_ids == ["cross_s00000m", "cross_s00180m"] with pytest.raises(ValueError): write_cross_layout(root, longitudinal_path, {"default": [0, 4], "sheets": {}}) with pytest.raises(ValueError): write_cross_layout(root, longitudinal_path, {"default": [4, 4], "sheets": {"x": [2, 2]}})