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:
@@ -1,12 +1,14 @@
|
||||
"""B07 계획평면도 종류 · 정보 조각 파일 (PLAN 60장).
|
||||
|
||||
저장 자리(프로젝트 저장소 `B07_DesignDetail/`):
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order} — M02 양식 층처럼 파일 하나씩
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout} — 파일 하나씩
|
||||
plan_pieces/{장}/{조각}.json 정보 조각 하나 = 그 장 범위의 기하(사업지 좌표 m)
|
||||
plan_marks.json 사용자가 정한 대피소 · 사토장 자리
|
||||
|
||||
`plan_kinds/` 가 없으면 기본 넷(지형 · 노선배치도 · 대피소 · 라이다)을 쓴다. 처음 고칠 때
|
||||
기본 넷을 파일로 내려 두고 그 위에서 추가 · 업데이트 · 지운다 — 기본 넷도 고치고 지울 수 있다.
|
||||
`plan_kinds/` 가 없으면 기본 종류(지형 · 노선배치도 · 대피소 · 라이다 · 용지도 · 배수유역도)를
|
||||
쓴다. 양식(layout) = plan(1/1,200 장 나눔 · 조각 고름) · watershed(유역도 1/6,000 한 장 · 조각
|
||||
없음). 처음 고칠 때 기본 종류를 파일로 내려 두고 그 위에서 추가 · 업데이트 · 지운다 — 기본 종류도
|
||||
고치고 지울 수 있다.
|
||||
조각은 서버가 도면을 열 때마다 원자료(B05 노선 · 곡선표 · 구조물, B06 횡단 설계, B04 배경)
|
||||
에서 다시 계산해 파일로 쓰고, 도면은 그 파일을 읽어 고른 조각만 얹는다.
|
||||
"""
|
||||
@@ -24,7 +26,7 @@ from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import infer_station_interval, station_no_label
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import ip_table_piece
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import clip_lines, ip_table_piece
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
_structure_label,
|
||||
plan_area_mm,
|
||||
@@ -47,6 +49,7 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import (
|
||||
_sheet_box,
|
||||
map_background,
|
||||
plan_stations,
|
||||
watershed_source,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
|
||||
_design_root,
|
||||
@@ -55,7 +58,9 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
|
||||
_write_manifest,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import (
|
||||
LABEL_CLIPPED,
|
||||
LANDUSE_PIECES,
|
||||
label_rects,
|
||||
landuse_pieces,
|
||||
)
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
@@ -65,6 +70,8 @@ from config.config_system import DRAWING_SCALE_PLAN
|
||||
KIND_ID = re.compile(r"^[a-z][a-z0-9]{0,31}$")
|
||||
PLAN_KIND_ID = re.compile(r"^plan_([a-z][a-z0-9]{0,31})(?:_(\d+))?$")
|
||||
_NAME_MAX = 40
|
||||
WATERSHED_LAYOUT = "watershed"
|
||||
_LAYOUTS = ("plan", WATERSHED_LAYOUT)
|
||||
_REFUGE_TYPES = {"refuge": "대피소", "spoil_bank": "사토장"}
|
||||
_REFUGE_OFFSET_M = 8.0 # 대피소 · 사토장 라벨을 중심선에서 띄우는 거리(좌측)
|
||||
|
||||
@@ -80,16 +87,24 @@ def _kinds_dir(project_root: Path) -> Path:
|
||||
return _design_root(project_root) / "plan_kinds"
|
||||
|
||||
|
||||
# 이관 표시 — 있으면 용지도 종류를 이미 더해 준 폴더(지운 뒤 되살리지 않음).
|
||||
_LANDUSE_MARK = "_landuse_added"
|
||||
# 이관 표시 `_{id}_added` — 있으면 그 기본 종류를 이미 더해 준 폴더(지운 뒤 되살리지 않음).
|
||||
_ADDED_ONCE = ("landuse", "watershed")
|
||||
|
||||
|
||||
def _add_landuse_once(folder: Path) -> None:
|
||||
"""93장 앞에 종류 파일을 내린 프로젝트 — 기본 종류 「용지도」를 한 번 더한다."""
|
||||
mark = folder / _LANDUSE_MARK
|
||||
if mark.exists():
|
||||
def _document(kind: dict[str, Any], order: int) -> dict[str, Any]:
|
||||
return {
|
||||
"name": kind["name"],
|
||||
"pieces": list(kind["pieces"]),
|
||||
"order": order,
|
||||
"layout": kind.get("layout", "plan"),
|
||||
}
|
||||
|
||||
|
||||
def _add_defaults_once(folder: Path) -> None:
|
||||
"""기본 종류가 늘기 전에 종류 파일을 내린 프로젝트 — 늘어난 기본 종류를 한 번 더한다."""
|
||||
pending = [kind_id for kind_id in _ADDED_ONCE if not (folder / f"_{kind_id}_added").exists()]
|
||||
if not pending:
|
||||
return
|
||||
landuse = next(kind for kind in DEFAULT_KINDS if kind["id"] == "landuse")
|
||||
documents = []
|
||||
for path in folder.glob("*.json"):
|
||||
try:
|
||||
@@ -97,21 +112,23 @@ def _add_landuse_once(folder: Path) -> None:
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
names = {str(document.get("name")) for document in documents}
|
||||
if not (folder / "landuse.json").exists() and landuse["name"] not in names:
|
||||
order = max((int(document.get("order") or 0) for document in documents), default=-1) + 1
|
||||
atomic_write_json(
|
||||
folder / "landuse.json",
|
||||
{"name": landuse["name"], "pieces": list(landuse["pieces"]), "order": order},
|
||||
)
|
||||
mark.touch()
|
||||
order = max((int(document.get("order") or 0) for document in documents), default=-1) + 1
|
||||
for kind_id in pending:
|
||||
kind = next(kind for kind in DEFAULT_KINDS if kind["id"] == kind_id)
|
||||
if not (folder / f"{kind_id}.json").exists() and kind["name"] not in names:
|
||||
atomic_write_json(folder / f"{kind_id}.json", _document(kind, order))
|
||||
order += 1
|
||||
(folder / f"_{kind_id}_added").touch()
|
||||
|
||||
|
||||
def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
"""종류 목록 [{id, name, pieces}] — 차례대로. 파일이 없으면 기본 종류."""
|
||||
"""종류 목록 [{id, name, pieces, layout}] — 차례대로. 파일이 없으면 기본 종류."""
|
||||
folder = _kinds_dir(project_root)
|
||||
if not folder.is_dir():
|
||||
return [{**kind, "pieces": list(kind["pieces"])} for kind in DEFAULT_KINDS]
|
||||
_add_landuse_once(folder)
|
||||
return [
|
||||
{"layout": "plan", **kind, "pieces": list(kind["pieces"])} for kind in DEFAULT_KINDS
|
||||
]
|
||||
_add_defaults_once(folder)
|
||||
rows: list[tuple[int, str, dict[str, Any]]] = []
|
||||
for path in folder.glob("*.json"):
|
||||
if not KIND_ID.fullmatch(path.stem):
|
||||
@@ -123,67 +140,76 @@ def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
stored = set()
|
||||
for piece in document.get("pieces") or []:
|
||||
stored.update(LEGACY_PIECES.get(piece, (piece,)))
|
||||
pieces = [piece for piece in PIECE_IDS if piece in stored]
|
||||
layout = document.get("layout") if document.get("layout") in _LAYOUTS else "plan"
|
||||
pieces = [] if layout == WATERSHED_LAYOUT else [p for p in PIECE_IDS if p in stored]
|
||||
rows.append(
|
||||
(
|
||||
int(document.get("order") or 0),
|
||||
path.stem,
|
||||
{"id": path.stem, "name": str(document.get("name") or path.stem), "pieces": pieces},
|
||||
{
|
||||
"id": path.stem,
|
||||
"name": str(document.get("name") or path.stem),
|
||||
"pieces": pieces,
|
||||
"layout": layout,
|
||||
},
|
||||
)
|
||||
)
|
||||
return [row for _order, _id, row in sorted(rows, key=lambda item: item[:2])]
|
||||
|
||||
|
||||
def _materialize(project_root: Path) -> list[dict[str, Any]]:
|
||||
"""기본 넷을 파일로 내려 둔다(이미 있으면 그대로) — 고치기 전에 한 번."""
|
||||
"""기본 종류를 파일로 내려 둔다(이미 있으면 그대로) — 고치기 전에 한 번."""
|
||||
kinds = list_kinds(project_root)
|
||||
folder = _kinds_dir(project_root)
|
||||
if not folder.is_dir():
|
||||
for order, kind in enumerate(kinds):
|
||||
atomic_write_json(
|
||||
folder / f"{kind['id']}.json",
|
||||
{"name": kind["name"], "pieces": kind["pieces"], "order": order},
|
||||
)
|
||||
(folder / _LANDUSE_MARK).touch()
|
||||
atomic_write_json(folder / f"{kind['id']}.json", _document(kind, order))
|
||||
for kind_id in _ADDED_ONCE:
|
||||
(folder / f"_{kind_id}_added").touch()
|
||||
return kinds
|
||||
|
||||
|
||||
def _checked(name: str, pieces: list[str]) -> tuple[str, list[str]]:
|
||||
def _checked(name: str, pieces: list[str], layout: str = "plan") -> tuple[str, list[str]]:
|
||||
text = str(name or "").strip()
|
||||
if not text or len(text) > _NAME_MAX:
|
||||
raise ValueError(f"종류 이름은 1~{_NAME_MAX}자로 적어 주세요.")
|
||||
if layout not in _LAYOUTS:
|
||||
raise ValueError("올바르지 않은 양식입니다.")
|
||||
if layout == WATERSHED_LAYOUT:
|
||||
return text, [] # 유역도 양식은 그림이 정해져 있다 — 조각을 고르지 않음
|
||||
chosen = [piece for piece in PIECE_IDS if piece in set(pieces or [])]
|
||||
if not chosen:
|
||||
raise ValueError("얹을 정보를 하나 이상 고르세요.")
|
||||
return text, chosen
|
||||
|
||||
|
||||
def add_kind(project_root: Path, name: str, pieces: list[str]) -> dict[str, Any]:
|
||||
text, chosen = _checked(name, pieces)
|
||||
def add_kind(
|
||||
project_root: Path, name: str, pieces: list[str], layout: str = "plan"
|
||||
) -> dict[str, Any]:
|
||||
text, chosen = _checked(name, pieces, layout)
|
||||
kinds = _materialize(project_root)
|
||||
if any(kind["name"] == text for kind in kinds):
|
||||
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
|
||||
kind_id = f"k{uuid.uuid4().hex[:8]}"
|
||||
atomic_write_json(
|
||||
_kinds_dir(project_root) / f"{kind_id}.json",
|
||||
{"name": text, "pieces": chosen, "order": len(kinds)},
|
||||
)
|
||||
return {"id": kind_id, "name": text, "pieces": chosen}
|
||||
kind = {"id": f"k{uuid.uuid4().hex[:8]}", "name": text, "pieces": chosen, "layout": layout}
|
||||
atomic_write_json(_kinds_dir(project_root) / f"{kind['id']}.json", _document(kind, len(kinds)))
|
||||
return kind
|
||||
|
||||
|
||||
def update_kind(project_root: Path, kind_id: str, name: str, pieces: list[str]) -> dict[str, Any]:
|
||||
"""종류 이름 · 조각을 고친다(차례는 그대로). 그 종류의 확정본은 풀어 새 조각으로 다시 그린다."""
|
||||
"""종류 이름 · 조각을 고친다(차례 · 양식은 그대로). 그 종류의 확정본은 풀어 다시 그린다."""
|
||||
if not KIND_ID.fullmatch(kind_id):
|
||||
raise ValueError("올바르지 않은 종류 id 입니다.")
|
||||
text, chosen = _checked(name, pieces)
|
||||
kinds = _materialize(project_root)
|
||||
current = next((kind for kind in kinds if kind["id"] == kind_id), None)
|
||||
path = _kinds_dir(project_root) / f"{kind_id}.json"
|
||||
if current is None or not path.is_file():
|
||||
raise FileNotFoundError("고칠 종류를 찾을 수 없습니다.")
|
||||
text, chosen = _checked(name, pieces, current["layout"])
|
||||
if any(kind["name"] == text and kind["id"] != kind_id for kind in kinds):
|
||||
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
|
||||
path = _kinds_dir(project_root) / f"{kind_id}.json"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("고칠 종류를 찾을 수 없습니다.")
|
||||
order = int(_read_json(path).get("order") or 0)
|
||||
atomic_write_json(path, {"name": text, "pieces": chosen, "order": order})
|
||||
kind = {"id": kind_id, "name": text, "pieces": chosen, "layout": current["layout"]}
|
||||
atomic_write_json(path, _document(kind, order))
|
||||
manifest = _read_manifest(project_root)
|
||||
released = False
|
||||
for drawing_id, entry in manifest["drawings"].items():
|
||||
@@ -193,7 +219,7 @@ def update_kind(project_root: Path, kind_id: str, name: str, pieces: list[str])
|
||||
released = True
|
||||
if released:
|
||||
_write_manifest(project_root, manifest)
|
||||
return {"id": kind_id, "name": text, "pieces": chosen}
|
||||
return kind
|
||||
|
||||
|
||||
def delete_kind(project_root: Path, kind_id: str) -> None:
|
||||
@@ -449,6 +475,8 @@ def build_pieces(
|
||||
pieces.update(
|
||||
landuse_pieces(project_root, context.crs, box, [(x, y) for _c, x, y in route])
|
||||
)
|
||||
if "parcel_info" in wanted:
|
||||
clip_lines(pieces, label_rects(pieces["parcel_info"]["items"]), LABEL_CLIPPED)
|
||||
if "ip_table" in wanted:
|
||||
pieces["ip_table"] = ip_table_piece(
|
||||
pieces, _load_design_curves(project_root), route, box, chunk, total
|
||||
@@ -477,6 +505,9 @@ def plan_kind_source(
|
||||
)
|
||||
if kind is None:
|
||||
raise FileNotFoundError("계획평면도 종류를 찾을 수 없습니다.")
|
||||
if kind["layout"] == WATERSHED_LAYOUT:
|
||||
# 유역도 양식 — B04 배수유역 산출물 + 도엽 배경(사업지 CRS) 한 장.
|
||||
return {"layout": WATERSHED_LAYOUT, "label": kind["name"], **watershed_source(context)}
|
||||
number = int(match.group(2)) if match.group(2) else 1
|
||||
pieces, box, chunk, total = build_pieces(
|
||||
context, longitudinal, longitudinal_path, designs, number, kind["pieces"]
|
||||
@@ -494,6 +525,16 @@ def plan_kind_items(project_root: Path, longitudinal: dict[str, Any]) -> list[di
|
||||
sheets = plan_chunks(plan_stations(longitudinal))
|
||||
items = []
|
||||
for kind in list_kinds(project_root):
|
||||
if kind["layout"] == WATERSHED_LAYOUT: # 1/6,000 한 장 — 장 나눔 없음
|
||||
items.append(
|
||||
{
|
||||
"id": f"plan_{kind['id']}",
|
||||
"label": kind["name"],
|
||||
"plan_kind": kind["id"],
|
||||
"plan_kind_name": kind["name"],
|
||||
}
|
||||
)
|
||||
continue
|
||||
title = plan_title(kind["name"])
|
||||
for chunk in sheets:
|
||||
single = len(sheets) <= 1
|
||||
|
||||
Reference in New Issue
Block a user