fix(B07): 횡단면도 칸 크기 통일 — 축척 1/100 유지
실측 결과 축척은 문제가 아니었음. 실거리 3 m 가 모든 단면에서 30.0 mm (mm/m = 10.0 단일값). 뒤죽박죽으로 보인 원인은 칸 크기 — 한 장 안에서 폭 116~218 mm, 높이 29~178 mm 로 제각각이었음. - 장별 완전 통일: 한 장 안 모든 칸을 그 장 최대 블록 크기로 통일(_grid_for). 테두리는 build_cross_drawing(cell_frame=...) 로 칸에 맞춰 그림. - 장 경계는 전체 최소 장수가 되도록 동적계획으로 선택(_sheet_breaks). 앞에서부터 채우면 바로 뒤의 큰 단면이 칸을 키워 6칸짜리 장에 1개만 실리는 낭비가 있었음. - 축척은 지식DB 「설계제원_총괄」 기준 1/100 고정 — 어떤 경우에도 줄이지 않고 안 들어가면 장을 나눔. 장수는 세트마다 달라짐(2026-09-04 사용자 확정). 검증(용화_LAS): 장 21개 전부 칸 크기 단일값, mm/m = 10 단일, 측점 65개 누락 0, 칸 밖 이탈 0.000 mm. 실서버 API 로도 동일 확인. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -443,6 +443,7 @@ 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 수량 산출표로 만든다.
|
||||
|
||||
@@ -450,6 +451,10 @@ 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")
|
||||
@@ -511,16 +516,20 @@ def build_cross_drawing(
|
||||
)
|
||||
table_bottom = table_top - cross_table_height()
|
||||
|
||||
# 외곽 테두리: 단면 범위와 표를 함께 감싼다.
|
||||
frame_x = max((x1 - x0) / 2.0 * CROSS_MM + 4.0, cross_table_width() / 2.0 + 4.0)
|
||||
frame_top = oy + half_height + 4.0
|
||||
frame_bottom = table_bottom - 4.0
|
||||
# 외곽 테두리: 단면 범위와 표를 함께 감싼다. 칸 크기를 받았으면 그 칸에 맞춘다.
|
||||
if cell_frame is not None:
|
||||
frame_left, frame_bottom, frame_right, frame_top = cell_frame
|
||||
else:
|
||||
frame_x = max((x1 - x0) / 2.0 * CROSS_MM + 4.0, cross_table_width() / 2.0 + 4.0)
|
||||
frame_left, frame_right = center_x - frame_x, center_x + frame_x
|
||||
frame_top = oy + half_height + 4.0
|
||||
frame_bottom = table_bottom - 4.0
|
||||
corners = [
|
||||
(center_x - frame_x, frame_bottom),
|
||||
(center_x + frame_x, frame_bottom),
|
||||
(center_x + frame_x, frame_top),
|
||||
(center_x - frame_x, frame_top),
|
||||
(center_x - frame_x, frame_bottom),
|
||||
(frame_left, frame_bottom),
|
||||
(frame_right, frame_bottom),
|
||||
(frame_right, frame_top),
|
||||
(frame_left, frame_top),
|
||||
(frame_left, frame_bottom),
|
||||
]
|
||||
border = polyline_entity(drawing_id, corners, FRAME_LAYER_ID, TABLE_LINE_COLOR)
|
||||
if border:
|
||||
@@ -542,12 +551,7 @@ def build_cross_drawing(
|
||||
"x1": x1,
|
||||
# 블록 테두리(종이 mm). 프론트가 자기 그림을 이 안으로 자르고, 갈아 끼울
|
||||
# 서버 설계선을 이 안에서만 골라내는 데 쓴다.
|
||||
"frame": [
|
||||
center_x - frame_x,
|
||||
frame_bottom,
|
||||
center_x + frame_x,
|
||||
frame_top,
|
||||
],
|
||||
"frame": [frame_left, frame_bottom, frame_right, frame_top],
|
||||
}
|
||||
],
|
||||
"layers": [
|
||||
|
||||
@@ -84,72 +84,85 @@ def section_block_size(
|
||||
return (width, top + below)
|
||||
|
||||
|
||||
def _pack(
|
||||
blocks: list[tuple[int, float, float]], start: int, rows: int
|
||||
) -> tuple[int, list[float], list[float]]:
|
||||
"""blocks[start:]를 rows행 **열 우선**으로 담아 (담은 개수, 열폭, 행높이)를 낸다.
|
||||
|
||||
열폭은 그 열에 든 블록의 최대폭, 행높이는 그 행에 든 블록의 최대높이다 — 칸을
|
||||
전체 최대치로 통일하지 않으면서 행·열은 맞춘다(2026-08-30 사용자 확정).
|
||||
"""
|
||||
def _grid_for(group: list[tuple[int, float, float]]) -> tuple[float, float, int, int]:
|
||||
"""한 장에 담을 블록 묶음의 (칸폭, 칸높이, 열수, 행수) — 칸은 그 장 최대 블록 기준."""
|
||||
usable_w, usable_h = usable_area()
|
||||
col_widths: list[float] = []
|
||||
row_heights: list[float] = [0.0] * rows
|
||||
count = 0
|
||||
for index, (_chainage, width, height) in enumerate(blocks[start:]):
|
||||
column, row = divmod(index, rows)
|
||||
current = col_widths[column] if column < len(col_widths) else 0.0
|
||||
new_col = max(current, width + _BLOCK_GAP_MM)
|
||||
new_row = max(row_heights[row], height + _BLOCK_GAP_MM)
|
||||
if sum(col_widths[:column]) + new_col > usable_w:
|
||||
break
|
||||
if sum(row_heights) - row_heights[row] + new_row > usable_h:
|
||||
break
|
||||
if column < len(col_widths):
|
||||
col_widths[column] = new_col
|
||||
else:
|
||||
col_widths.append(new_col)
|
||||
row_heights[row] = new_row
|
||||
count = index + 1
|
||||
return count, col_widths, row_heights
|
||||
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)
|
||||
|
||||
|
||||
def _slots(col_widths: list[float], row_heights: list[float], count: int) -> list[list[float]]:
|
||||
def _max_take(blocks: list[tuple[int, 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:
|
||||
break
|
||||
limit = take
|
||||
return limit
|
||||
|
||||
|
||||
def _sheet_breaks(blocks: list[tuple[int, float, float]]) -> list[int]:
|
||||
"""장 경계를 **전체 최소 장수**가 되도록 고른다 (측점 순서는 유지).
|
||||
|
||||
앞에서부터 최대한 채우면 바로 뒤에 큰 단면이 오는 순간 칸이 그 단면 크기로
|
||||
튀어 그 장이 통째로 비었다(2026-09-04 실측: 6칸짜리 장에 1개만 배치). 단면
|
||||
크기는 측점마다 원지반 기울기로 달라지므로, 경계를 뒤에서부터 훑어 최소 장수
|
||||
조합을 고른다 — 큰 단면은 자기 장에 몰리고 비슷한 크기끼리 한 장에 모인다.
|
||||
같은 장수면 **앞 장을 더 많이 채우는 쪽**을 고른다(뒷장에 여백을 몰아 준다).
|
||||
"""
|
||||
total = len(blocks)
|
||||
best_sheets = [0] * (total + 1)
|
||||
best_take = [0] * (total + 1)
|
||||
for start in range(total - 1, -1, -1):
|
||||
limit = max(_max_take(blocks, start), 1)
|
||||
choice = (total + 1, 0)
|
||||
for take in range(1, limit + 1):
|
||||
candidate = (best_sheets[start + take] + 1, -take)
|
||||
if candidate < choice:
|
||||
choice = candidate
|
||||
best_sheets[start], best_take[start] = choice[0], -choice[1]
|
||||
breaks: list[int] = []
|
||||
start = 0
|
||||
while start < total:
|
||||
breaks.append(best_take[start])
|
||||
start += best_take[start]
|
||||
return breaks
|
||||
|
||||
|
||||
def _slots(cell_w: float, cell_h: float, rows: int, count: int) -> list[list[float]]:
|
||||
"""칸의 (가로 중심, 아래 변) — 좌하단부터 아래→위로 채우고, 열이 차면 오른쪽 열.
|
||||
|
||||
세로는 중심이 아니라 **아래 변**을 준다. 수량표 높이는 모든 블록이 같으므로
|
||||
아래를 맞추면 같은 행의 표가 한 줄로 선다(2026-08-30 사용자: 표는 행·열을 맞춘다).
|
||||
칸이 모두 같은 크기이므로 격자 좌표만 계산하면 된다(2026-08-29 사용자: 채우는
|
||||
순서는 좌하단부터 열 우선).
|
||||
"""
|
||||
usable_w, usable_h = usable_area()
|
||||
rows = len(row_heights)
|
||||
slots: list[list[float]] = []
|
||||
for index in range(count):
|
||||
column, row = divmod(index, rows)
|
||||
x = -usable_w / 2.0 + sum(col_widths[:column]) + col_widths[column] / 2.0
|
||||
y = -usable_h / 2.0 + sum(row_heights[:row])
|
||||
slots.append([x, y])
|
||||
slots.append(
|
||||
[
|
||||
-usable_w / 2.0 + column * cell_w + cell_w / 2.0,
|
||||
-usable_h / 2.0 + row * cell_h,
|
||||
]
|
||||
)
|
||||
return slots
|
||||
|
||||
|
||||
def plan_cross_sheets(blocks: list[tuple[int, float, float]]) -> list[dict[str, Any]]:
|
||||
"""(측점, 폭, 높이) 목록을 A1 장으로 나눈다.
|
||||
|
||||
블록 크기를 먼저 재서 **가장 많이 담기는 행 수**를 고르고, 그 행·열 격자에
|
||||
담는다(2026-08-30 사용자 지시 — 전체 최대치 통일은 여백이 너무 많았다).
|
||||
한 장 안의 칸은 모두 같은 크기(그 장 최대 블록 기준)이고 빈 곳은 여백으로 둔다.
|
||||
작성 척도는 1/100 고정 — 안 들어가면 장을 나눌 뿐 줄이지 않는다(지식DB
|
||||
「설계제원_총괄」 측량·도면 기준). 장에 담기는 측점 수는 세트마다 다르다
|
||||
(2026-09-04 사용자 확정).
|
||||
"""
|
||||
sheets: list[dict[str, Any]] = []
|
||||
start = 0
|
||||
while start < len(blocks):
|
||||
best: tuple[int, list[float], list[float]] = (0, [], [])
|
||||
for rows in range(1, len(blocks) - start + 1):
|
||||
packed = _pack(blocks, start, rows)
|
||||
if packed[0] > best[0]:
|
||||
best = packed
|
||||
count, col_widths, row_heights = best
|
||||
if count == 0: # 한 칸도 못 담을 만큼 큰 블록 — 그래도 한 장에 하나는 놓는다.
|
||||
count, col_widths, row_heights = 1, [blocks[start][1]], [blocks[start][2]]
|
||||
for count in _sheet_breaks(blocks):
|
||||
group = blocks[start : start + count]
|
||||
number = len(sheets) + 1
|
||||
cell_w, cell_h, _columns, rows = _grid_for(group)
|
||||
chainages = [chainage for chainage, _w, _h in group]
|
||||
sheets.append(
|
||||
{
|
||||
@@ -157,10 +170,12 @@ def plan_cross_sheets(blocks: list[tuple[int, float, float]]) -> list[dict[str,
|
||||
# 바뀌어 한 장에 담기는 측점 수가 달라졌을 때 같은 이름이 다른 구간을
|
||||
# 가리키고, 옛 확정 표시가 그대로 새 구간에 붙는다(2026-09-01 지적).
|
||||
"id": f"cross_s{chainages[0]:05d}m",
|
||||
"number": number,
|
||||
"number": len(sheets) + 1,
|
||||
"chainages": chainages,
|
||||
"rows": len(row_heights),
|
||||
"slots": _slots(col_widths, row_heights, count),
|
||||
"rows": max(rows, 1),
|
||||
"cell_width": cell_w,
|
||||
"cell_height": cell_h,
|
||||
"slots": _slots(cell_w, cell_h, max(rows, 1), count),
|
||||
}
|
||||
)
|
||||
start += count
|
||||
@@ -172,6 +187,8 @@ 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):
|
||||
@@ -197,6 +214,16 @@ def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) ->
|
||||
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,
|
||||
)
|
||||
placed = build_cross_drawing(
|
||||
section["source"],
|
||||
seed_id,
|
||||
@@ -205,6 +232,7 @@ 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 [])
|
||||
|
||||
Reference in New Issue
Block a user