feat(B07): 배수유역도 종류화 · 평면도 드롭다운 공용 꼴 · IP 표 차례 · 용지도 라벨 2배와 상자 밑 선 자름(109-1~4)

- 배수유역도 = 기본 종류(양식 watershed · 1/6,000 한 장 · 조각 없음) · 도면 id plan_watershed · × 지우기 · 「새 종류」 양식 고르기로 다시 · 옛 폴더에 한 번만 더함
- 평면도 칸 종류 · 양식 고르기 = 공용 createSelectField(compact)
- IP 표 첫 줄 맨 위 · 왼 → 오 · 위 → 아래(묶음 왼쪽 아래 기준 그대로)
- 용지도 필지 정보 글 3.6(상수 하나) · 상자 밑 등고선 · 계류 · 필지 · 읍면동 · 시군구 선 잘라 냄(clip_lines 일반화)
- 시험: 기본 종류 · 유역 한 장 · 지우기 · 다시 만들기 · IP 표 두 줄 · 상자 안 선 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
2026-10-08 16:16:27 +09:00
co-authored by Claude Opus 5.5
parent 331883043e
commit 0ca0583995
14 changed files with 351 additions and 137 deletions
@@ -110,33 +110,31 @@ def ip_blocks(
def _place(blocks: list[dict[str, Any]], width_mm: float) -> None:
"""묶음마다 표 왼쪽 위 자리(종이 mm) `origin` — 왼쪽 아래부터 나란히 · 넘치면 위로 다음 줄."""
"""묶음마다 표 왼쪽 위 자리(종이 mm) `origin` — 왼쪽 → 오른쪽 · 위 → 아래(첫 줄 맨 위).
묶음 전체는 도곽 왼쪽 아래 기준 — 줄이 늘면 위로 쌓이고 마지막 줄이 맨 아래다.
"""
block_w = sum(_COLUMNS)
per_line = max(1, int((width_mm - 2 * _MARGIN) // block_w))
lines = -(-len(blocks) // per_line)
for order, block in enumerate(blocks):
line, column = divmod(order, per_line)
top = _MARGIN + line * (_IP_ROWS * _ROW_H + _LINE_GAP) + _IP_ROWS * _ROW_H
top = _MARGIN + (lines - 1 - line) * (_IP_ROWS * _ROW_H + _LINE_GAP) + _IP_ROWS * _ROW_H
block["origin"] = [_MARGIN + column * block_w, top]
block["rows"] = _END_ROWS if "ia" not in block else _IP_ROWS
def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any) -> None:
"""표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다."""
min_x, min_y = box[0], box[1]
width = sum(_COLUMNS) / MM
area = unary_union(
[
rect(
min_x + b["origin"][0] / MM,
min_y + b["origin"][1] / MM - b["rows"] * _ROW_H / MM,
min_x + b["origin"][0] / MM + width,
min_y + b["origin"][1] / MM,
)
for b in blocks
]
)
def clip_lines(
pieces: dict[str, dict[str, Any]],
rects: list[tuple[float, float, float, float]],
names: tuple[str, ...],
) -> None:
"""상자들(사업지 m · 최소 x · y · 최대 x · y) 밑 선 조각 `names` 를 상자 밖으로 잘라 낸다."""
if not rects:
return
area = unary_union([rect(*bounds) for bounds in rects])
inside = prep(area)
for piece in _CLIPPED:
for piece in names:
lines = (pieces.get(piece) or {}).get("lines")
if not lines:
continue
@@ -155,6 +153,22 @@ def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box:
pieces[piece]["lines"] = kept
def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any) -> None:
"""표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다."""
min_x, min_y = box[0], box[1]
width = sum(_COLUMNS) / MM
rects = [
(
min_x + b["origin"][0] / MM,
min_y + b["origin"][1] / MM - b["rows"] * _ROW_H / MM,
min_x + b["origin"][0] / MM + width,
min_y + b["origin"][1] / MM,
)
for b in blocks
]
clip_lines(pieces, rects, _CLIPPED)
def ip_table_piece(
pieces: dict[str, dict[str, Any]],
curves: list[dict[str, Any]],