feat(b07): 횡단 장 = 작도 영역 열 × 행 표 격자선 · 장 안 블록 테두리 끔 · 칸 넘침 알림 (169-2)
- 장마다 칸 격자선(세로 열 + 1 · 가로 행 + 1 · 도각 층 · 인쇄 · DXF) - 단면은 블록 틀 가운데 = 칸 가운데 그대로 · 장 안 블록 테두리 안 그림(측점 한 장은 그대로) - 칸보다 큰 단면 = overflow_cells → CAD 가이드 붉은 틀 + 화면 알림(축척 그대로) - 배치 판 4 — 옛 판 확정 장은 「옛 배치」 안내 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
This commit is contained in:
@@ -496,6 +496,7 @@ def build_cross_drawing(
|
||||
frame: dict[str, float] | None = None,
|
||||
origin: tuple[float, float] = (0.0, 0.0),
|
||||
overlay: tuple[float, float, float, float] | None = None,
|
||||
border: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""횡단도 한 장을 지표/설계/구조물(+암 경계) 레이어 + CAD 수량 산출표로 만든다.
|
||||
|
||||
@@ -586,9 +587,10 @@ def build_cross_drawing(
|
||||
(frame_left, frame_top),
|
||||
(frame_left, frame_bottom),
|
||||
]
|
||||
border = polyline_entity(drawing_id, corners, FRAME_LAYER_ID, TABLE_LINE_COLOR)
|
||||
if border:
|
||||
entities.append(border)
|
||||
# 장 안 블록은 테두리 대신 장 칸 격자선(169-2) — 측점 한 장만 테두리
|
||||
outline = polyline_entity(drawing_id, corners, FRAME_LAYER_ID, TABLE_LINE_COLOR)
|
||||
if border and outline:
|
||||
entities.append(outline)
|
||||
|
||||
return {
|
||||
"format": DRAWING_FORMAT,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
채우는 순서는 **좌하단부터 아래에서 위로, 그다음 오른쪽 열**이다
|
||||
(2026-08-29 사용자 확정). 칸 = 도각 그림칸(usable_area)을 똑같이 나눈 자리 · 단면은
|
||||
칸 가운데 · 작성 척도 1/100 그대로(큰 단면이 칸을 넘으면 겹침 — 사용자 배치로 고침).
|
||||
칸 = 인쇄되는 표 격자선 · 블록 테두리 없음 · 칸보다 큰 단면은 `overflow_cells`(169-2).
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -19,11 +20,13 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
GROUND_LAYER_ID,
|
||||
ROCK_LAYER_ID,
|
||||
STRUCTURE_LAYER_ID,
|
||||
TABLE_LINE_COLOR,
|
||||
_clip_polyline,
|
||||
_layer,
|
||||
_section_window,
|
||||
build_cross_drawing,
|
||||
points_from_samples,
|
||||
polyline_entity,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
|
||||
cross_note_height,
|
||||
@@ -145,11 +148,37 @@ def plan_cross_sheets(
|
||||
return sheets
|
||||
|
||||
|
||||
def _grid_lines(sheet_id: str, cols: int, rows: int) -> list[dict[str, Any]]:
|
||||
"""장 칸 격자선(169-2) — 세로 열 + 1 · 가로 행 + 1 · 도각 층(인쇄 · DXF 에 나감)."""
|
||||
usable_w, usable_h = usable_area()
|
||||
left, bottom = -usable_w / 2.0, -usable_h / 2.0
|
||||
lines = [
|
||||
((left + usable_w * i / cols, bottom), (left + usable_w * i / cols, -bottom), f"v{i}")
|
||||
for i in range(cols + 1)
|
||||
] + [
|
||||
((left, bottom + usable_h * j / rows), (-left, bottom + usable_h * j / rows), f"h{j}")
|
||||
for j in range(rows + 1)
|
||||
]
|
||||
return [
|
||||
entity
|
||||
for start, end, name in lines
|
||||
if (
|
||||
entity := polyline_entity(
|
||||
sheet_id, [start, end], FRAME_LAYER_ID, TABLE_LINE_COLOR, f":grid-{name}"
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""한 장을 만든다. sections는 이 장에 담을 측점 입력들(배치 순서대로)."""
|
||||
entities: list[dict[str, Any]] = []
|
||||
placements: list[dict[str, Any]] = []
|
||||
overflow: list[list[float]] = []
|
||||
slots = sheet.get("slots") or []
|
||||
cols, rows = sheet.get("grid") or (4, 4)
|
||||
usable_w, usable_h = usable_area()
|
||||
cell_w, cell_h = usable_w / cols, usable_h / rows
|
||||
|
||||
for index, section in enumerate(sections):
|
||||
if index >= len(slots):
|
||||
@@ -164,13 +193,26 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
section.get("quantity_table"),
|
||||
section.get("title", ""),
|
||||
overlay=section.get("overlay"),
|
||||
border=False,
|
||||
)
|
||||
bbox = entities_bbox(probe["entities"])
|
||||
# 블록 틀(그림 + 1 mm · 안 그림) — 테두리를 끈 뒤에도 같은 자리 · 크기로 칸에 맞춤
|
||||
frames = [p.get("frame") for p in probe.get("cross_placements") or []]
|
||||
bbox = frames[0] if frames and frames[0] else entities_bbox(probe["entities"])
|
||||
if bbox is None:
|
||||
continue
|
||||
min_x, min_y, max_x, max_y = bbox
|
||||
center_x, center_y = slots[index]
|
||||
# 2) 블록(테두리 = 자기 그림 + 1 mm) 가운데를 칸 가운데에 맞춰 다시 만든다.
|
||||
# 칸보다 큰 단면 — 축척 그대로 두고 그 칸을 화면 가이드로 짚음(169-2)
|
||||
if max_x - min_x > cell_w + 1e-6 or max_y - min_y > cell_h + 1e-6:
|
||||
overflow.append(
|
||||
[
|
||||
center_x - cell_w / 2.0,
|
||||
center_y - cell_h / 2.0,
|
||||
center_x + cell_w / 2.0,
|
||||
center_y + cell_h / 2.0,
|
||||
]
|
||||
)
|
||||
# 2) 블록 그림 가운데를 칸 가운데에 맞춰 다시 만든다(장 안은 테두리 없음).
|
||||
origin = (center_x - (min_x + max_x) / 2.0, center_y - (min_y + max_y) / 2.0)
|
||||
placed = build_cross_drawing(
|
||||
section["source"],
|
||||
@@ -181,13 +223,14 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
section.get("title", ""),
|
||||
origin=origin,
|
||||
overlay=section.get("overlay"),
|
||||
border=False,
|
||||
)
|
||||
entities.extend(placed["entities"])
|
||||
placements.extend(placed.get("cross_placements") or [])
|
||||
|
||||
if entities:
|
||||
entities.extend(_grid_lines(sheet["id"], cols, rows))
|
||||
# 도각 = 고정 그림칸 — 내용 크기와 무관하게 칸 격자와 같은 자리(140-3).
|
||||
usable_w, usable_h = usable_area()
|
||||
entities.extend(
|
||||
frame_entities(
|
||||
sheet["id"],
|
||||
@@ -201,6 +244,8 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
"format": DRAWING_FORMAT,
|
||||
"entities": entities,
|
||||
"cross_placements": placements,
|
||||
# 칸보다 큰 단면의 칸 [x0, y0, x1, y1] — 라우터가 CAD 가이드 붉은 틀로 보냄(169-2)
|
||||
"overflow_cells": overflow,
|
||||
"layers": [
|
||||
_layer(GROUND_LAYER_ID, "원지반", locked=True),
|
||||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||||
|
||||
@@ -97,8 +97,9 @@ MASS_HAUL_ID = "mass_haul"
|
||||
COVER_ID = "cover"
|
||||
# 표준 횡단면도 — 노선 자료가 아니라 B06 표준 횡단면 설정값으로 그리는 한 장.
|
||||
CROSS_STANDARD_ID = "cross_standard"
|
||||
# 횡단 장 배치 판 — 장마다 열 × 행 칸 가운데(140-3). 이 판 전에 확정한 장은 옛 배치다.
|
||||
CROSS_SHEET_LAYOUT = 3
|
||||
# 횡단 장 배치 판 — 장마다 열 × 행 칸 가운데(140-3) · 칸 격자선 · 블록 테두리 없음(169-2).
|
||||
# 이 판 전에 확정한 장은 옛 배치다.
|
||||
CROSS_SHEET_LAYOUT = 4
|
||||
# 장마다 열 × 행 — {default: [열, 행], sheets: {장 번호: [열, 행]}}(140-3).
|
||||
CROSS_LAYOUT_FILE = "cross_layout.json"
|
||||
CROSS_GRID_DEFAULT = [4, 4]
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""169 도면 그림 영역 지정 · 횡단 격자 표.
|
||||
|
||||
못박는 것
|
||||
- 169-1 그림칸 = 작도 영역 그대로(여백 없음) · 도각 편집이 영역을 싣고 · 저장(어긋나면 옛 값).
|
||||
- 169-1 도면 좌표 가이드 영역 = 도각 bbox ↔ 양식 bbox 로 옮긴 영역 · 도각 아닌 것(블록 틀)이면 None.
|
||||
- 169-2 횡단 장 = 칸 격자선(열 + 1 · 행 + 1 · 칸 경계) · 장 안 블록 테두리 없음 · 큰 단면 → 넘친 칸.
|
||||
- 169-4 구조물 도각(외곽 좌하단 0,0) 응답에 영역.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from uuid import uuid5
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail import B07_DesignDetail_Engine_Template as engine
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_POLY_NS,
|
||||
FRAME_LAYER_ID,
|
||||
build_cross_drawing,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import build_cross_sheet, plan_cross_sheets
|
||||
from B07_DesignDetail.B07_DesignDetail_Router import _drawing_guide
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Drawing import structure_frame
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TABLE = {"ground": 100.0, "planned": 100.0}
|
||||
|
||||
|
||||
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 _sheet(sources: list[dict], layout: dict | None = None) -> tuple[dict, dict]:
|
||||
sheet = plan_cross_sheets([s["chainage_m"] for s in sources], layout)[0]
|
||||
sections = [
|
||||
{"chainage": s["chainage_m"], "source": s, "quantity_table": TABLE, "title": ""}
|
||||
for s in sources
|
||||
]
|
||||
return sheet, build_cross_sheet(sheet, sections)
|
||||
|
||||
|
||||
def test_usable_area_is_drawing_area_without_margin():
|
||||
engine.use_project_templates(None)
|
||||
x0, y0, x1, y1 = engine.drawing_area()
|
||||
assert engine.usable_area() == (x1 - x0, y1 - y0) == (770.0, 520.0)
|
||||
|
||||
|
||||
def test_frame_edit_loads_and_saves_area(tmp_path):
|
||||
engine.use_project_templates(None)
|
||||
assert engine.frame_template_document()["drawing_area"] == [42.0, 47.0, 812.0, 567.0]
|
||||
path = engine.save_project_template(tmp_path, [], area=[10, 20, 410, 320])
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["drawing_area"] == [10, 20, 410, 320]
|
||||
# 어긋난 값(뒤집힘 · 글자) → 고치기 전 값을 이어 받음
|
||||
for bad in ([410, 20, 10, 320], ["a", 0, 1, 1], None):
|
||||
path = engine.save_project_template(tmp_path, [], area=bad)
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["drawing_area"] == [10, 20, 410, 320]
|
||||
# 라우터가 요청 도면의 영역을 넘김
|
||||
router = (ROOT / "B07_DesignDetail/B07_DesignDetail_Router_Frame.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert 'request.drawing.get("drawing_area")' in router and "area=area" in router
|
||||
|
||||
|
||||
def test_cross_sheet_grid_lines_no_block_border_and_guide_area():
|
||||
engine.use_project_templates(None)
|
||||
w, h = engine.usable_area()
|
||||
sources = [_source(i * 20.0, 0.05, 6) for i in range(3)]
|
||||
sheet, drawing = _sheet(sources, {"default": [3, 2]})
|
||||
by_id = {e["id"]: e for e in drawing["entities"]}
|
||||
|
||||
def line(name: str) -> tuple[dict, dict]:
|
||||
entity = by_id[str(uuid5(_POLY_NS, f"{sheet['id']}:{FRAME_LAYER_ID}:grid-{name}"))]
|
||||
assert entity["layerId"] == FRAME_LAYER_ID
|
||||
shape = entity["children"][0]["shapeData"]
|
||||
return shape["startPoint"], shape["endPoint"]
|
||||
|
||||
# 격자선 = 세로 열 + 1 · 가로 행 + 1 · 칸 경계 자리 · 그림칸 끝에서 끝까지
|
||||
for i in range(3 + 1):
|
||||
a, b = line(f"v{i}")
|
||||
assert a["x"] == b["x"] == pytest.approx(-w / 2 + w * i / 3)
|
||||
assert (a["y"], b["y"]) == pytest.approx((-h / 2, h / 2))
|
||||
for j in range(2 + 1):
|
||||
a, b = line(f"h{j}")
|
||||
assert a["y"] == b["y"] == pytest.approx(-h / 2 + h * j / 2)
|
||||
assert (a["x"], b["x"]) == pytest.approx((-w / 2, w / 2))
|
||||
assert str(uuid5(_POLY_NS, f"{sheet['id']}:{FRAME_LAYER_ID}:grid-v4")) not in by_id
|
||||
# 장 안 블록 테두리 없음(측점 한 장 테두리와 같은 id 자리)
|
||||
for source in sources:
|
||||
seed = f"{sheet['id']}:{source['chainage_m']}:{FRAME_LAYER_ID}"
|
||||
assert str(uuid5(_POLY_NS, seed)) not in by_id
|
||||
# 가이드 영역(도면 좌표) = 그림칸 그대로 · 넘침 없음
|
||||
guide = _drawing_guide(drawing)
|
||||
assert guide["area"] == pytest.approx([-w / 2, -h / 2, w / 2, h / 2], abs=1e-3)
|
||||
assert guide["marks"] == [] and drawing["overflow_cells"] == []
|
||||
|
||||
|
||||
def test_big_section_marks_its_cell():
|
||||
engine.use_project_templates(None)
|
||||
w, h = engine.usable_area()
|
||||
# 4 × 4 칸(192.5 × 130 mm)에 60 m 폭 단면(600 mm) — 첫 칸(좌하단)만 넘침
|
||||
sources = [_source(0.0, 0.05, 30), _source(20.0, 0.05, 6)]
|
||||
_sheet_, drawing = _sheet(sources, {"default": [4, 4]})
|
||||
cw, ch = w / 4, h / 4
|
||||
assert drawing["overflow_cells"] == [pytest.approx([-w / 2, -h / 2, -w / 2 + cw, -h / 2 + ch])]
|
||||
assert _drawing_guide(drawing)["marks"] == drawing["overflow_cells"]
|
||||
|
||||
|
||||
def test_station_drawing_border_is_not_a_frame():
|
||||
# 측점 한 장 — 도각 층에 블록 테두리만 있음 → 가이드 없음(틀린 선 대신)
|
||||
drawing = build_cross_drawing(_source(0.0, 0.1), "cross_00000m", None, None, TABLE, "")
|
||||
assert any(e.get("layerId") == FRAME_LAYER_ID for e in drawing["entities"])
|
||||
assert _drawing_guide(drawing) is None
|
||||
|
||||
|
||||
def test_structure_frame_area_in_frame_coords():
|
||||
engine.use_project_templates(None)
|
||||
frame = structure_frame()
|
||||
min_x, min_y, *_ = engine.entities_bbox(engine.template_entities())
|
||||
x0, y0, x1, y1 = engine.drawing_area()
|
||||
assert frame["area"] == pytest.approx(
|
||||
[x0 - min_x, y0 - min_y, x1 - min_x, y1 - min_y], abs=1e-3
|
||||
)
|
||||
Reference in New Issue
Block a user