feat(b07): 횡단 장 칸 = 아래 가운데 표 먼저 · 그 위 글 · 그림 도로 중심 = 칸 가운데 (174-1)
- 표 아래선 = 칸 아래 + 5 mm · 같은 행 표 윗선 · 아랫선 한 높이 · 표 가운데 x = 칸 가운데 - [상세] 글 표 바로 위 · 그림 아래 끝 = 글(없으면 표) 위 8 mm · 도로 중심(offset 0) = 칸 가운데 - 넘침 = 블록 틀이 칸 위 · 옆을 넘을 때 붉은 칸 틀(축척 그대로) - 배치 판 5 — 옛 판 확정 장 안내 글 새 배치로 - test_140 칸 가운데 판정을 새 배치로 · test_174 새 시험 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
This commit is contained in:
@@ -497,6 +497,7 @@ def build_cross_drawing(
|
||||
origin: tuple[float, float] = (0.0, 0.0),
|
||||
overlay: tuple[float, float, float, float] | None = None,
|
||||
border: bool = True,
|
||||
table_bottom: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""횡단도 한 장을 지표/설계/구조물(+암 경계) 레이어 + CAD 수량 산출표로 만든다.
|
||||
|
||||
@@ -504,6 +505,8 @@ def build_cross_drawing(
|
||||
설계선·구조물이 원지반과 갈라지는 구간 + 여유다. 세로는 이 단면 선들의
|
||||
bbox 중심을 0에 둔다(측점마다 화면 중앙 정렬). design_elevation_m는 현재
|
||||
배치에 쓰지 않지만 향후 표고 주석용으로 시그니처를 유지한다.
|
||||
table_bottom(장 칸 · 174-1)을 주면 표 아래선을 그 y 에 두고 글 · 그림을 위로 쌓는다 —
|
||||
표 · 글 가운데 x = 도로 중심(origin x) · origin y 는 쌓은 결과로 정함.
|
||||
"""
|
||||
ox, oy = origin
|
||||
raw_ground = points_from_samples(source.get("samples", []), "offset_m")
|
||||
@@ -539,6 +542,12 @@ def build_cross_drawing(
|
||||
all_ys.extend(overlay[2:])
|
||||
dy = (min(all_ys) + max(all_ys)) / 2.0 if all_ys else 0.0
|
||||
own_half_height = (max(all_ys) - min(all_ys)) / 2.0 if all_ys else 5.0
|
||||
if table_bottom is not None:
|
||||
# 아래에서 위로 — 표 → 글(150-3) → 간격 8 → 그림(174-1)
|
||||
stack = 0.0
|
||||
if quantity_table is not None:
|
||||
stack = cross_table_height() + cross_note_height(cross_note_lines(design)) + 8.0
|
||||
oy = table_bottom + stack + own_half_height * CROSS_MM
|
||||
|
||||
def paper(point: tuple[float, float]) -> tuple[float, float]:
|
||||
return (point[0] * CROSS_MM + ox, (point[1] - dy) * CROSS_MM + oy)
|
||||
@@ -560,7 +569,7 @@ def build_cross_drawing(
|
||||
|
||||
# 표는 단면 중심 아래에 붙인다 (좌표는 mm). 그리는 창이 좌우 비대칭이면
|
||||
# 단면 중심이 원점과 어긋나므로 표도 같은 중심을 쓴다.
|
||||
center_x = ox + (x0 + x1) / 2.0 * CROSS_MM
|
||||
center_x = ox if table_bottom is not None else ox + (x0 + x1) / 2.0 * CROSS_MM
|
||||
half_height = own_half_height * CROSS_MM
|
||||
table_bottom = oy - half_height
|
||||
if quantity_table is not None:
|
||||
@@ -576,8 +585,9 @@ def build_cross_drawing(
|
||||
|
||||
# 외곽 테두리: 단면 범위와 표를 함께 감싸고 1 mm 띄운다.
|
||||
pad = CROSS_BORDER_PAD_MM
|
||||
frame_x = max((x1 - x0) / 2.0 * CROSS_MM + pad, cross_table_width() / 2.0 + pad)
|
||||
frame_left, frame_right = center_x - frame_x, center_x + frame_x
|
||||
# 그림 범위와 표 중 바깥 — 장 칸에서는 표가 도로 중심이라 좌우가 다를 수 있음(174-1)
|
||||
frame_left = min(ox + x0 * CROSS_MM, center_x - cross_table_width() / 2.0) - pad
|
||||
frame_right = max(ox + x1 * CROSS_MM, center_x + cross_table_width() / 2.0) + pad
|
||||
frame_top = oy + half_height + pad
|
||||
frame_bottom = table_bottom - pad
|
||||
corners = [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
(2026-08-29 사용자 확정). 칸 = 도각 그림칸(usable_area)을 똑같이 나눈 자리 · 단면은
|
||||
칸 가운데 · 작성 척도 1/100 그대로(큰 단면이 칸을 넘으면 겹침 — 사용자 배치로 고침).
|
||||
칸 = 인쇄되는 표 격자선 · 블록 테두리 없음 · 칸보다 큰 단면은 `overflow_cells`(169-2).
|
||||
칸 안 = 표를 칸 아래 가운데에 먼저 · 그 위 글 · 그림(도로 중심 = 칸 가운데)(174-1).
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -35,7 +36,6 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
|
||||
cross_table_width,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
entities_bbox,
|
||||
frame_entities,
|
||||
scale_fields,
|
||||
usable_area,
|
||||
@@ -48,6 +48,8 @@ CROSS_SHEET_ID = re.compile(r"cross_s(\d{2,}(?:\.\d{2})?)m?")
|
||||
|
||||
# 테두리 여유 — build_cross_drawing의 테두리 값과 맞춘다. 블록은 서로 맞닿게 쌓는다.
|
||||
_BORDER_PAD_MM = CROSS_BORDER_PAD_MM
|
||||
# 칸 아래선 ~ 표 아래선 여백(mm) — 같은 행 표가 한 높이(174-1)
|
||||
CELL_TABLE_PAD_MM = 5.0
|
||||
|
||||
|
||||
def _design_points(design_line: list[Any] | None) -> list[tuple[float, float]]:
|
||||
@@ -184,36 +186,14 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
if index >= len(slots):
|
||||
break
|
||||
seed_id = f"{sheet['id']}:{section['chainage']}"
|
||||
# 1) 원점(0,0)에 한 번 만들어 블록이 원점 대비 어디에 놓이는지 잰다.
|
||||
probe = build_cross_drawing(
|
||||
section["source"],
|
||||
seed_id,
|
||||
section.get("design_line"),
|
||||
section.get("design"),
|
||||
section.get("quantity_table"),
|
||||
section.get("title", ""),
|
||||
overlay=section.get("overlay"),
|
||||
border=False,
|
||||
)
|
||||
# 블록 틀(그림 + 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]
|
||||
# 칸보다 큰 단면 — 축척 그대로 두고 그 칸을 화면 가이드로 짚음(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)
|
||||
cell = [
|
||||
center_x - cell_w / 2.0,
|
||||
center_y - cell_h / 2.0,
|
||||
center_x + cell_w / 2.0,
|
||||
center_y + cell_h / 2.0,
|
||||
]
|
||||
# 표 아래선 = 칸 아래 + 여백 · 도로 중심 = 칸 가운데 · 글 · 그림은 위로(174-1)
|
||||
placed = build_cross_drawing(
|
||||
section["source"],
|
||||
seed_id,
|
||||
@@ -221,10 +201,16 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
section.get("design"),
|
||||
section.get("quantity_table"),
|
||||
section.get("title", ""),
|
||||
origin=origin,
|
||||
origin=(center_x, 0.0),
|
||||
overlay=section.get("overlay"),
|
||||
border=False,
|
||||
table_bottom=cell[1] + CELL_TABLE_PAD_MM,
|
||||
)
|
||||
# 칸을 넘는 단면 — 축척 그대로 두고 그 칸을 화면 가이드로 짚음(169-2)
|
||||
frames = [p.get("frame") for p in placed.get("cross_placements") or []]
|
||||
left, _bottom, right, top = frames[0] if frames and frames[0] else cell
|
||||
if left < cell[0] - 1e-6 or right > cell[2] + 1e-6 or top > cell[3] + 1e-6:
|
||||
overflow.append(cell)
|
||||
entities.extend(placed["entities"])
|
||||
placements.extend(placed.get("cross_placements") or [])
|
||||
|
||||
|
||||
@@ -97,9 +97,9 @@ MASS_HAUL_ID = "mass_haul"
|
||||
COVER_ID = "cover"
|
||||
# 표준 횡단면도 — 노선 자료가 아니라 B06 표준 횡단면 설정값으로 그리는 한 장.
|
||||
CROSS_STANDARD_ID = "cross_standard"
|
||||
# 횡단 장 배치 판 — 장마다 열 × 행 칸 가운데(140-3) · 칸 격자선 · 블록 테두리 없음(169-2).
|
||||
# 이 판 전에 확정한 장은 옛 배치다.
|
||||
CROSS_SHEET_LAYOUT = 4
|
||||
# 횡단 장 배치 판 — 장마다 열 × 행 칸(140-3) · 칸 격자선 · 블록 테두리 없음(169-2) ·
|
||||
# 칸 아래 가운데 표 → 글 → 그림(174-1). 이 판 전에 확정한 장은 옛 배치다.
|
||||
CROSS_SHEET_LAYOUT = 5
|
||||
# 장마다 열 × 행 — {default: [열, 행], sheets: {장 번호: [열, 행]}}(140-3).
|
||||
CROSS_LAYOUT_FILE = "cross_layout.json"
|
||||
CROSS_GRID_DEFAULT = [4, 4]
|
||||
|
||||
@@ -340,7 +340,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
if (response.drawing.stale_layout) {
|
||||
showToast(
|
||||
"옛 배치로 확정된 장입니다. [수정]을 누르면 박스를 그림에 1 mm로 붙인 새 배치로 다시 만듭니다.",
|
||||
"옛 배치로 확정된 장입니다. [수정]을 누르면 표를 칸 아래 가운데에 맞춘 새 배치로 다시 만듭니다.",
|
||||
"info",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- 칸 = 그림칸(usable_area)을 똑같이 나눈 자리 · 차례 = 좌하단 → 위 → 오른쪽 열.
|
||||
- 측점 73개 · 4 × 4 → 16 · 16 · 16 · 16 · 9 · 17번째 측점 = 2장 첫 칸.
|
||||
- 장마다 다른 배치(sheets {장 번호: [열, 행]}) · 기본 바꾸기.
|
||||
- 단면 블록 가운데 = 칸 가운데 · 블록 크기 = 1/100 그대로(section_block_size).
|
||||
- 단면 블록 = 칸 아래 가운데(174-1) · 블록 크기 = 1/100 그대로(section_block_size).
|
||||
- 도각 = 고정 그림칸 — 내용이 작아도 커도 같은 자리.
|
||||
- 배치 저장 · 읽기(깨진 값 거절) · 담긴 측점이 바뀐 확정 장만 풂.
|
||||
"""
|
||||
@@ -17,6 +17,7 @@ import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import FRAME_LAYER_ID
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
CELL_TABLE_PAD_MM,
|
||||
build_cross_sheet,
|
||||
grid_slots,
|
||||
plan_cross_sheets,
|
||||
@@ -87,9 +88,10 @@ def test_block_centered_in_cell_at_scale_and_frame_fixed() -> None:
|
||||
sources, sheet["slots"], drawing["cross_placements"], strict=True
|
||||
):
|
||||
left, bottom, right, top = placement["frame"]
|
||||
# 블록 가운데 = 칸 가운데
|
||||
# 좌우 대칭 단면 — 블록 가운데 x = 칸 가운데 · 아래 = 칸 아래 + 표 여백(174-1)
|
||||
assert (left + right) / 2 == pytest.approx(slot[0])
|
||||
assert (bottom + top) / 2 == pytest.approx(slot[1])
|
||||
cell_bottom = slot[1] - usable_area()[1] / 8
|
||||
assert bottom == pytest.approx(cell_bottom + CELL_TABLE_PAD_MM - 1.0)
|
||||
# 1/100 그대로 — 블록 크기가 자기 치수 그대로(칸 맞춤으로 줄이지 않음)
|
||||
width, height = section_block_size(source, None, design=None)
|
||||
assert right - left == pytest.approx(width, abs=0.5)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""174-1 B07 횡단 장 — 칸 아래 가운데 표 먼저 · 그 위 글 · 그림.
|
||||
|
||||
못박는 것
|
||||
- 표 아래선 = 칸 아래 + 여백 · 같은 행 칸끼리 표 윗선 · 아랫선 한 높이(그림 · 글 크기와 무관).
|
||||
- 표 가운데 x = 칸 가운데 = 도로 중심(offset 0) — 좌우 비대칭 단면도.
|
||||
- [상세] 글은 표 바로 위 · 그림 아래 끝 = 글(없으면 표) 위 8 mm.
|
||||
- 그림이 칸 위 · 옆을 넘으면 overflow_cells(축척 그대로) · 들어가면 없음.
|
||||
- 배치 판 5(옛 판 확정 장 = 옛 배치 안내).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import GROUND_LAYER_ID
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
|
||||
CELL_TABLE_PAD_MM,
|
||||
build_cross_sheet,
|
||||
plan_cross_sheets,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
|
||||
cross_note_height,
|
||||
cross_note_lines,
|
||||
cross_table_height,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
entities_bbox,
|
||||
usable_area,
|
||||
use_project_templates,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support import CROSS_SHEET_LAYOUT
|
||||
|
||||
NOTE = {"structure_note": "L형 측구\n집수정 보강"}
|
||||
|
||||
|
||||
def _source(chainage: float, slope: float, lo: int, hi: int) -> dict:
|
||||
return {
|
||||
"chainage_m": chainage,
|
||||
"samples": [
|
||||
{"valid": True, "offset_m": float(x), "elevation_m": 100.0 + slope * x}
|
||||
for x in range(lo, hi + 1)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _sheet(sources: list[dict], designs: list[dict | None], grid: list[int]) -> tuple:
|
||||
use_project_templates(None)
|
||||
sheet = plan_cross_sheets([s["chainage_m"] for s in sources], {"default": grid})[0]
|
||||
sections = [
|
||||
{
|
||||
"chainage": s["chainage_m"],
|
||||
"source": s,
|
||||
"design": design,
|
||||
"quantity_table": {"ground": 1.0, "planned": 0.5},
|
||||
"title": "",
|
||||
}
|
||||
for s, design in zip(sources, designs, strict=True)
|
||||
]
|
||||
return sheet, build_cross_sheet(sheet, sections)
|
||||
|
||||
|
||||
def _tables(drawing: dict) -> list[tuple[float, float, float]]:
|
||||
"""표마다 (가운데 x, 아랫선 y, 윗선 y) — 배치 순서."""
|
||||
out = []
|
||||
for entity in drawing["entities"]:
|
||||
if entity["type"] != "Table":
|
||||
continue
|
||||
shape = entity["shapeData"]
|
||||
left, top = shape["origin"]["x"], shape["origin"]["y"]
|
||||
out.append((left + sum(shape["columnWidths"]) / 2, top - sum(shape["rowHeights"]), top))
|
||||
return out
|
||||
|
||||
|
||||
def test_같은_행_표_한_높이_그림_중심_칸_가운데() -> None:
|
||||
# 2 × 2 — 좌하단 · 좌상단 · 우하단 · 우상단 · 높이 · 좌우 범위 · 글 유무가 다 다름
|
||||
sources = [
|
||||
_source(0.0, 0.05, -6, 6),
|
||||
_source(20.0, 0.3, -3, 9),
|
||||
_source(40.0, 0.05, -8, 4),
|
||||
_source(60.0, 0.6, -5, 5),
|
||||
]
|
||||
designs = [None, NOTE, NOTE, None]
|
||||
sheet, drawing = _sheet(sources, designs, [2, 2])
|
||||
_w, h = usable_area()
|
||||
cell_h = h / 2
|
||||
tables = _tables(drawing)
|
||||
assert len(tables) == 4
|
||||
for (cx, bottom, top), slot in zip(tables, sheet["slots"], strict=True):
|
||||
assert cx == pytest.approx(slot[0]) # 표 가운데 x = 칸 가운데
|
||||
assert bottom == pytest.approx(slot[1] - cell_h / 2 + CELL_TABLE_PAD_MM)
|
||||
assert top - bottom == pytest.approx(cross_table_height())
|
||||
# 같은 행(아래 행 = 0 · 2번 · 위 행 = 1 · 3번) 표 윗선 · 아랫선 한 높이
|
||||
for a, b in ((0, 2), (1, 3)):
|
||||
assert tables[a][1:] == pytest.approx(tables[b][1:])
|
||||
# 같은 열 표 가운데 x 한 줄
|
||||
assert tables[0][0] == pytest.approx(tables[1][0])
|
||||
# 도로 중심(offset 0) = 칸 가운데 · 그림(원지반) 아래 끝 = 글(없으면 표) 위 8 mm
|
||||
grounds = [
|
||||
entities_bbox([e]) for e in drawing["entities"] if e.get("layerId") == GROUND_LAYER_ID
|
||||
]
|
||||
for placement, slot, table, design, ground in zip(
|
||||
drawing["cross_placements"], sheet["slots"], tables, designs, grounds, strict=True
|
||||
):
|
||||
assert placement["ox"] == pytest.approx(slot[0])
|
||||
note_h = cross_note_height(cross_note_lines(design))
|
||||
assert ground[1] == pytest.approx(table[2] + note_h + 8.0)
|
||||
assert drawing["overflow_cells"] == []
|
||||
|
||||
|
||||
def test_넘침_위_옆() -> None:
|
||||
w, h = usable_area()
|
||||
# 4 × 4 칸(192.5 × 130 mm) — 0: 높이 넘침(경사 1 · 높이 120 mm + 표) · 1: 오른쪽만 긴
|
||||
# 단면(-2 ~ 16 m · 폭 180 < 칸 폭이지만 도로 중심 고정이라 오른쪽 넘침) · 2: 들어감
|
||||
sources = [_source(0.0, 1.0, -6, 6), _source(20.0, 0.05, -2, 16), _source(40.0, 0.05, -5, 5)]
|
||||
_sheet_, drawing = _sheet(sources, [None, None, None], [4, 4])
|
||||
cw, ch = w / 4, h / 4
|
||||
cells = [[-w / 2, -h / 2 + ch * j, -w / 2 + cw, -h / 2 + ch * (j + 1)] for j in range(2)]
|
||||
assert drawing["overflow_cells"] == [pytest.approx(c) for c in cells]
|
||||
|
||||
|
||||
def test_배치_판_5() -> None:
|
||||
assert CROSS_SHEET_LAYOUT == 5
|
||||
Reference in New Issue
Block a user