Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -36,6 +36,8 @@ export interface CadDrawing {
|
||||
x0: number;
|
||||
x1: number;
|
||||
}[];
|
||||
/** 횡단 장 — 옛 배치(칸 통일)로 확정된 저장본. [수정]을 누르면 새 배치로 다시 만든다. */
|
||||
stale_layout?: boolean;
|
||||
layers: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -450,6 +450,10 @@ def _section_window(
|
||||
if left is None or right is None:
|
||||
xs = [x for x, _y in ground] or [-6.0, 6.0]
|
||||
left, right = min(xs), max(xs)
|
||||
# 여유는 선이 있는 데까지만 — 원지반 자료가 짧으면 빈 여유가 테두리를 밀어낸다(68-1).
|
||||
drawn = [x for x, _y in ground] + [x for x, _y in design]
|
||||
if drawn:
|
||||
return (max(left - margin_m, min(drawn)), min(right + margin_m, max(drawn)))
|
||||
return (left - margin_m, right + margin_m)
|
||||
|
||||
|
||||
@@ -483,7 +487,6 @@ def build_cross_drawing(
|
||||
design_elevation_m: float | None = None,
|
||||
frame: dict[str, float] | None = None,
|
||||
origin: tuple[float, float] = (0.0, 0.0),
|
||||
cell_frame: tuple[float, float, float, float] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""횡단도 한 장을 지표/설계/구조물(+암 경계) 레이어 + CAD 수량 산출표로 만든다.
|
||||
|
||||
@@ -491,10 +494,6 @@ def build_cross_drawing(
|
||||
설계선·구조물이 원지반과 갈라지는 구간 + 여유다. 세로는 이 단면 선들의
|
||||
bbox 중심을 0에 둔다(측점마다 화면 중앙 정렬). design_elevation_m는 현재
|
||||
배치에 쓰지 않지만 향후 표고 주석용으로 시그니처를 유지한다.
|
||||
|
||||
cell_frame(왼쪽, 아래, 오른쪽, 위 — 종이 mm)을 주면 테두리를 그 칸에 맞춰
|
||||
그린다. 장 배치에서 한 장 안의 칸을 같은 크기로 통일할 때 쓴다(2026-09-04
|
||||
사용자 확정 — 축척 1/100은 그대로, 칸만 통일).
|
||||
"""
|
||||
ox, oy = origin
|
||||
raw_ground = points_from_samples(source.get("samples", []), "offset_m")
|
||||
@@ -556,15 +555,12 @@ def build_cross_drawing(
|
||||
)
|
||||
table_bottom = table_top - cross_table_height()
|
||||
|
||||
# 외곽 테두리: 단면 범위와 표를 함께 감싼다. 칸 크기를 받았으면 그 칸에 맞춘다.
|
||||
if cell_frame is not None:
|
||||
frame_left, frame_bottom, frame_right, frame_top = cell_frame
|
||||
else:
|
||||
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
|
||||
frame_top = oy + half_height + pad
|
||||
frame_bottom = table_bottom - pad
|
||||
# 외곽 테두리: 단면 범위와 표를 함께 감싸고 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
|
||||
frame_top = oy + half_height + pad
|
||||
frame_bottom = table_bottom - pad
|
||||
corners = [
|
||||
(frame_left, frame_bottom),
|
||||
(frame_right, frame_bottom),
|
||||
|
||||
@@ -109,6 +109,16 @@ def _parcel_info_lines(props: dict[str, Any]) -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
def _enclosing_note(props: dict[str, Any]) -> str:
|
||||
"""구역 전체가 한 필지 안일 때 정보 묶음 맨 위 줄 — 소재지(시군구 읍면동 리)를 붙인다."""
|
||||
place = " ".join(
|
||||
str(props[key]).strip()
|
||||
for key in ("sgg_nm", "emd_nm", "ri_nm")
|
||||
if isinstance(props.get(key), str) and props[key].strip()
|
||||
)
|
||||
return f"구역 전체가 이 필지 안 ({place})" if place else "구역 전체가 이 필지 안"
|
||||
|
||||
|
||||
def _legend_entities(drawing_id: str, origin: tuple[float, float]) -> list[dict[str, Any]]:
|
||||
"""범례 — 유역도에서 유역 정보표가 있던 자리(오른쪽 칸)에 놓는다."""
|
||||
entities: list[dict[str, Any]] = []
|
||||
@@ -217,6 +227,7 @@ def build_landuse_drawing(
|
||||
# 필지 경계 + 필지 정보. 도곽에 잘려 조각이 여럿인 필지도 정보는 한 번만 —
|
||||
# 가장 큰 조각 가운데에 적는다(구역 안 필지 수 = 정보 묶음 수).
|
||||
spots: dict[str, tuple[float, tuple[float, float], dict[str, Any]]] = {}
|
||||
enclosing: set[str] = set() # 도곽을 통째로 감싼 필지
|
||||
for index, parcel in enumerate(parcels):
|
||||
ring = parcel.get("ring") or []
|
||||
props = parcel.get("props") or {}
|
||||
@@ -234,14 +245,19 @@ def build_landuse_drawing(
|
||||
elif label_at is not None:
|
||||
# 도곽을 통째로 감싼 필지 — 경계선이 없으니 지정된 자리에 정보만 적는다.
|
||||
size, center = 0.0, paper(tuple(label_at))
|
||||
enclosing.add(str(props.get("pnu") or f"#{index}"))
|
||||
else:
|
||||
continue
|
||||
key = str(props.get("pnu") or f"#{index}")
|
||||
if key not in spots or size > spots[key][0]:
|
||||
spots[key] = (size, center, props)
|
||||
# 필지 경계가 하나도 안 보이면(구역이 한 필지 안) 어느 필지인지 글로 밝힌다(68-2).
|
||||
no_outline = not any(len(parcel.get("ring") or []) >= 2 for parcel in parcels)
|
||||
jibun: list[dict[str, Any]] = []
|
||||
for key, (_size, center, props) in spots.items():
|
||||
lines = _parcel_info_lines(props)
|
||||
if no_outline and key in enclosing:
|
||||
lines = [_enclosing_note(props), *lines]
|
||||
top = center[1] + (len(lines) - 1) / 2.0 * _INFO_LINE_GAP
|
||||
for row, text in enumerate(lines):
|
||||
if text:
|
||||
|
||||
@@ -42,9 +42,7 @@ from config.config_system import DRAWING_SCALE_CROSS
|
||||
# 옛 순번 형식(cross_s01)도 읽어 준다 — 이미 확정한 매니페스트가 그 이름을 갖고 있다.
|
||||
CROSS_SHEET_ID = re.compile(r"cross_s(\d{2,}(?:\.\d{2})?)m?")
|
||||
|
||||
# 블록 사이 여백(mm)과 테두리 여유 — build_cross_drawing의 테두리 값과 맞춘다.
|
||||
# 여백 0 = 이웃 칸의 테두리가 맞닿는다(2026-09-04 사용자 지시 — 박스 사이 간격 제거).
|
||||
_BLOCK_GAP_MM = 0.0
|
||||
# 테두리 여유 — build_cross_drawing의 테두리 값과 맞춘다. 블록은 서로 맞닿게 쌓는다.
|
||||
_BORDER_PAD_MM = CROSS_BORDER_PAD_MM
|
||||
|
||||
|
||||
@@ -61,18 +59,25 @@ def _design_points(design_line: list[Any] | None) -> list[tuple[float, float]]:
|
||||
|
||||
|
||||
def section_block_size(
|
||||
source: dict[str, Any], design_line: list[Any] | None, with_table: bool = True
|
||||
source: dict[str, Any],
|
||||
design_line: list[Any] | None,
|
||||
with_table: bool = True,
|
||||
design: dict[str, Any] | None = None,
|
||||
) -> tuple[float, float]:
|
||||
"""단면 블록 하나의 종이 크기(mm) — 엔티티를 만들지 않고 치수만 잰다.
|
||||
|
||||
build_cross_drawing의 테두리 계산과 같은 규칙을 쓴다: 가로는 그리는 범위와
|
||||
수량표 중 넓은 쪽, 세로는 단면 높이 + 수량표.
|
||||
수량표 중 넓은 쪽, 세로는 단면 높이(암 경계선 포함) + 수량표.
|
||||
"""
|
||||
design_spec = design if isinstance(design, dict) else None
|
||||
ground = points_from_samples(source.get("samples", []), "offset_m")
|
||||
design = _design_points(design_line)
|
||||
x0, x1 = _section_window(ground, design)
|
||||
ground_clipped = _clip_polyline(ground, x0, x1)
|
||||
ys = [y for _x, y in ground_clipped] + [y for x, y in design if x0 <= x <= x1]
|
||||
rock_offset = (design_spec or {}).get("rock_boundary_offset_m")
|
||||
if isinstance(rock_offset, (int, float)):
|
||||
ys.extend(y + float(rock_offset) for _x, y in ground_clipped)
|
||||
half_height = ((max(ys) - min(ys)) / 2.0 if ys else 5.0) * CROSS_MM
|
||||
|
||||
width = 2.0 * max(
|
||||
@@ -87,20 +92,34 @@ def section_block_size(
|
||||
return (width, top + below)
|
||||
|
||||
|
||||
def _grid_for(group: list[tuple[int, float, float]]) -> tuple[float, float, int, int]:
|
||||
"""한 장에 담을 블록 묶음의 (칸폭, 칸높이, 열수, 행수) — 칸은 그 장 최대 블록 기준."""
|
||||
def _stack_columns(group: list[tuple[int | float, float, float]]) -> list[list[int]]:
|
||||
"""블록 순번을 열로 묶는다 — 아래에서 위로 쌓다 높이가 넘치면 오른쪽 새 열."""
|
||||
_usable_w, usable_h = usable_area()
|
||||
columns: list[list[int]] = []
|
||||
height = 0.0
|
||||
for index, (_c, _w, block_h) in enumerate(group):
|
||||
if not columns or height + block_h > usable_h + 1e-6:
|
||||
columns.append([])
|
||||
height = 0.0
|
||||
columns[-1].append(index)
|
||||
height += block_h
|
||||
return columns
|
||||
|
||||
|
||||
def _fits(group: list[tuple[int | float, float, float]]) -> bool:
|
||||
"""group 을 한 장에 담을 수 있나 — 열 폭 합이 가로를, 열마다 높이 합이 세로를 넘지 않는다."""
|
||||
usable_w, usable_h = usable_area()
|
||||
cell_w = max(width for _c, width, _h in group) + _BLOCK_GAP_MM
|
||||
cell_h = max(height for _c, _w, height in group) + _BLOCK_GAP_MM
|
||||
return cell_w, cell_h, int(usable_w // cell_w), int(usable_h // cell_h)
|
||||
columns = _stack_columns(group)
|
||||
width = sum(max(group[i][1] for i in column) for column in columns)
|
||||
tallest = max(sum(group[i][2] for i in column) for column in columns)
|
||||
return width <= usable_w + 1e-6 and tallest <= usable_h + 1e-6
|
||||
|
||||
|
||||
def _max_take(blocks: list[tuple[int, float, float]], start: int) -> int:
|
||||
"""blocks[start:] 를 한 장에 담을 수 있는 최대 개수(칸 통일 기준)."""
|
||||
def _max_take(blocks: list[tuple[int | float, float, float]], start: int) -> int:
|
||||
"""blocks[start:] 를 한 장에 담을 수 있는 최대 개수."""
|
||||
limit = 0
|
||||
for take in range(1, len(blocks) - start + 1):
|
||||
_cw, _ch, columns, rows = _grid_for(blocks[start : start + take])
|
||||
if columns * rows < take:
|
||||
if not _fits(blocks[start : start + take]):
|
||||
break
|
||||
limit = take
|
||||
return limit
|
||||
@@ -134,38 +153,37 @@ def _sheet_breaks(blocks: list[tuple[int, float, float]]) -> list[int]:
|
||||
return breaks
|
||||
|
||||
|
||||
def _slots(cell_w: float, cell_h: float, rows: int, count: int) -> list[list[float]]:
|
||||
"""칸의 (가로 중심, 아래 변) — 좌하단부터 아래→위로 채우고, 열이 차면 오른쪽 열.
|
||||
def _slots(group: list[tuple[int | float, float, float]]) -> list[list[float]]:
|
||||
"""블록의 (가로 중심, 아래 변) — 좌하단부터 아래→위로 쌓고, 열이 차면 오른쪽 열.
|
||||
|
||||
칸이 모두 같은 크기이므로 격자 좌표만 계산하면 된다(2026-08-29 사용자: 채우는
|
||||
순서는 좌하단부터 열 우선).
|
||||
블록은 자기 크기 그대로 맞닿게 쌓는다 — 칸을 장 최대 블록으로 통일하면 작은
|
||||
단면의 테두리가 그림에서 수십 mm 떨어졌다(68-1). 열 폭은 그 열 최대 블록 폭이고
|
||||
블록은 열 가운데에 선다.
|
||||
"""
|
||||
usable_w, usable_h = usable_area()
|
||||
slots: list[list[float]] = []
|
||||
for index in range(count):
|
||||
column, row = divmod(index, rows)
|
||||
slots.append(
|
||||
[
|
||||
-usable_w / 2.0 + column * cell_w + cell_w / 2.0,
|
||||
-usable_h / 2.0 + row * cell_h,
|
||||
]
|
||||
)
|
||||
slots: list[list[float]] = [[0.0, 0.0] for _ in group]
|
||||
left = -usable_w / 2.0
|
||||
for column in _stack_columns(group):
|
||||
column_w = max(group[i][1] for i in column)
|
||||
bottom = -usable_h / 2.0
|
||||
for i in column:
|
||||
slots[i] = [left + column_w / 2.0, bottom]
|
||||
bottom += group[i][2]
|
||||
left += column_w
|
||||
return slots
|
||||
|
||||
|
||||
def plan_cross_sheets(blocks: list[tuple[int | float, float, float]]) -> list[dict[str, Any]]:
|
||||
"""(측점, 폭, 높이) 목록을 A1 장으로 나눈다.
|
||||
|
||||
한 장 안의 칸은 모두 같은 크기(그 장 최대 블록 기준)이고 빈 곳은 여백으로 둔다.
|
||||
작성 척도는 1/100 고정 — 안 들어가면 장을 나눌 뿐 줄이지 않는다(지식DB
|
||||
「설계제원_총괄」 측량·도면 기준). 장에 담기는 측점 수는 세트마다 다르다
|
||||
(2026-09-04 사용자 확정).
|
||||
블록은 자기 크기(그림 + 테두리 1 mm)로 열마다 쌓는다. 작성 척도는 1/100 고정 — 안
|
||||
들어가면 장을 나눌 뿐 줄이지 않는다(지식DB 「설계제원_총괄」 측량·도면 기준). 장에
|
||||
담기는 측점 수는 세트마다 다르다(2026-09-04 사용자 확정).
|
||||
"""
|
||||
sheets: list[dict[str, Any]] = []
|
||||
start = 0
|
||||
for count in _sheet_breaks(blocks):
|
||||
group = blocks[start : start + count]
|
||||
cell_w, cell_h, _columns, rows = _grid_for(group)
|
||||
chainages = [chainage for chainage, _w, _h in group]
|
||||
sheets.append(
|
||||
{
|
||||
@@ -175,10 +193,7 @@ def plan_cross_sheets(blocks: list[tuple[int | float, float, float]]) -> list[di
|
||||
"id": f"cross_s{cross_key(chainages[0])}m",
|
||||
"number": len(sheets) + 1,
|
||||
"chainages": chainages,
|
||||
"rows": max(rows, 1),
|
||||
"cell_width": cell_w,
|
||||
"cell_height": cell_h,
|
||||
"slots": _slots(cell_w, cell_h, max(rows, 1), count),
|
||||
"slots": _slots(group),
|
||||
}
|
||||
)
|
||||
start += count
|
||||
@@ -190,8 +205,6 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
entities: list[dict[str, Any]] = []
|
||||
placements: list[dict[str, Any]] = []
|
||||
slots = sheet.get("slots") or []
|
||||
cell_w = float(sheet.get("cell_width") or 0.0)
|
||||
cell_h = float(sheet.get("cell_height") or 0.0)
|
||||
|
||||
for index, section in enumerate(sections):
|
||||
if index >= len(slots):
|
||||
@@ -211,22 +224,9 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
continue
|
||||
min_x, min_y, max_x, max_y = bbox
|
||||
center_x, bottom_y = slots[index]
|
||||
# 2) 가로는 칸 가운데, 세로는 칸 아래 변에 맞춰 원점을 옮겨 다시 만든다.
|
||||
# 아래를 맞춰야 같은 행의 수량표가 한 줄로 선다.
|
||||
origin = (
|
||||
center_x - (min_x + max_x) / 2.0,
|
||||
bottom_y + _BLOCK_GAP_MM / 2.0 - min_y,
|
||||
)
|
||||
# 3) 테두리는 칸 크기로 통일한다 — 단면 크기와 무관하게 한 장 안에서 같은 크기.
|
||||
cell_frame = None
|
||||
if cell_w > 0.0 and cell_h > 0.0:
|
||||
half = (cell_w - _BLOCK_GAP_MM) / 2.0
|
||||
cell_frame = (
|
||||
center_x - half,
|
||||
bottom_y + _BLOCK_GAP_MM / 2.0,
|
||||
center_x + half,
|
||||
bottom_y + cell_h - _BLOCK_GAP_MM / 2.0,
|
||||
)
|
||||
# 2) 가로는 칸 가운데, 세로는 칸 아래 변에 테두리 아래 변을 맞춰 다시 만든다.
|
||||
# 테두리는 자기 그림 + 1 mm — 칸 크기로 넓히지 않는다(68-1).
|
||||
origin = (center_x - (min_x + max_x) / 2.0, bottom_y - min_y)
|
||||
placed = build_cross_drawing(
|
||||
section["source"],
|
||||
seed_id,
|
||||
@@ -235,7 +235,6 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
section.get("quantity_table"),
|
||||
section.get("title", ""),
|
||||
origin=origin,
|
||||
cell_frame=cell_frame,
|
||||
)
|
||||
entities.extend(placed["entities"])
|
||||
placements.extend(placed.get("cross_placements") or [])
|
||||
|
||||
@@ -103,6 +103,8 @@ WATERSHED_ID = "watershed"
|
||||
COVER_ID = "cover"
|
||||
# 표준 횡단면도 — 노선 자료가 아니라 B06 표준 횡단면 설정값으로 그리는 한 장.
|
||||
CROSS_STANDARD_ID = "cross_standard"
|
||||
# 횡단 장 배치 판 — 단면마다 자기 그림 + 1 mm 박스(68-1). 이 판 전에 확정한 장은 옛 배치다.
|
||||
CROSS_SHEET_LAYOUT = 2
|
||||
|
||||
|
||||
def _drawing_list(
|
||||
@@ -229,7 +231,7 @@ def _cross_sheet_plan(
|
||||
chainage = cross_station(source.get("chainage_m") or parse_cross_key(match.group(1)))
|
||||
design = (designs or {}).get(chainage)
|
||||
design_line = _cross_design_line(longitudinal_path, source, design)
|
||||
width, height = section_block_size(source, design_line)
|
||||
width, height = section_block_size(source, design_line, design=design)
|
||||
blocks.append((chainage, width, height))
|
||||
return plan_cross_sheets(blocks)
|
||||
|
||||
@@ -413,6 +415,8 @@ def _read_drawing(
|
||||
else:
|
||||
kind = "longitudinal" if _LONG_ID.fullmatch(drawing_id) else "cross"
|
||||
label = str(manifest_entry.get("label") or drawing_id)
|
||||
if kind == "cross" and manifest_entry.get("layout") != CROSS_SHEET_LAYOUT:
|
||||
saved["stale_layout"] = True # 화면이 [수정] → 새 배치로 다시 만들기를 안내
|
||||
stored_table = manifest_entry.get("quantity_table")
|
||||
table = stored_table if kind == "cross" and isinstance(stored_table, dict) else None
|
||||
return kind, label, saved, True, table
|
||||
@@ -585,6 +589,8 @@ def _store_confirmed_drawing(
|
||||
"confirmed": True,
|
||||
"file": f"drawings/{item.id}.json",
|
||||
}
|
||||
if item.kind == "cross":
|
||||
entry["layout"] = CROSS_SHEET_LAYOUT
|
||||
if item.kind == "cross" and isinstance(quantity_table, dict):
|
||||
entry["quantity_table"] = quantity_table
|
||||
if isinstance(quantity_tables, dict) and quantity_tables:
|
||||
|
||||
@@ -272,6 +272,12 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
applyConfirmButtonState();
|
||||
updateInfoPanel(drawing, response);
|
||||
cad.load(response.drawing, buildMeta(drawing, response, index));
|
||||
if (response.drawing.stale_layout) {
|
||||
showToast(
|
||||
"옛 배치로 확정된 장입니다. [수정]을 누르면 박스를 그림에 1 mm로 붙인 새 배치로 다시 만듭니다.",
|
||||
"info",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
cad.fail(error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.");
|
||||
if (currentDrawing) highlightActive(currentDrawing.id);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,3 +35,45 @@ def test_one_label_group_per_parcel() -> None:
|
||||
e["shapeData"]["basePoint"]["x"] for e in texts if e["shapeData"]["label"] == "산1임"
|
||||
}
|
||||
assert split_x.pop() == pytest.approx(215.0 * MM)
|
||||
|
||||
|
||||
def test_zone_inside_one_parcel_is_named_once(tmp_path) -> None:
|
||||
"""작은 구역이 큰 필지 하나 안에 통째로 — 경계선이 없어도 어느 필지인지 한 번 적는다 (68-2)."""
|
||||
import json
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import _clip_parcels
|
||||
|
||||
props = {
|
||||
"pnu": "P1",
|
||||
"sgg_nm": "봉화군",
|
||||
"emd_nm": "재산면",
|
||||
"ri_nm": "갈산리",
|
||||
"jibun": "산77-1임",
|
||||
"jimok": "임야",
|
||||
"parea": 1234567,
|
||||
"owner_nm": "국",
|
||||
}
|
||||
big = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]
|
||||
geojson = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": props,
|
||||
"geometry": {"type": "Polygon", "coordinates": [big]},
|
||||
}
|
||||
],
|
||||
}
|
||||
path = tmp_path / "parcels.geojson"
|
||||
path.write_text(json.dumps(geojson, ensure_ascii=False), encoding="utf-8")
|
||||
parcels = _clip_parcels(path, "EPSG:4326", (4.0, 4.0, 6.0, 6.0))
|
||||
assert len(parcels) == 1 and parcels[0]["ring"] == []
|
||||
|
||||
route = [(4.5, 4.5), (5.5, 5.5)]
|
||||
drawing = build_landuse_drawing("landuse", "용지도", route, [], parcels, [], [])
|
||||
labels = [
|
||||
e["shapeData"]["label"] for e in drawing["entities"] if e.get("layerId") == JIBUN_LAYER_ID
|
||||
]
|
||||
notes = [label for label in labels if label.startswith("구역 전체가 이 필지 안")]
|
||||
assert notes == ["구역 전체가 이 필지 안 (봉화군 재산면 갈산리)"]
|
||||
assert labels[1:] == ["산77-1임", "임야", "1,234,567㎡", "국"]
|
||||
|
||||
Reference in New Issue
Block a user