Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_4' and 'origin/sub_laptop_5' into sub_laptop_3
This commit is contained in:
@@ -273,7 +273,7 @@ export function saveDrawingScales(
|
||||
});
|
||||
}
|
||||
|
||||
/** 계획평면도 종류 · 고를 조각 · 대피소 자리 (PLAN 60장). */
|
||||
/** 계획평면도 종류 · 고를 조각 (PLAN 60장). */
|
||||
export interface PlanKind {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -286,11 +286,6 @@ export interface PlanKind {
|
||||
contour_interval_m?: number;
|
||||
}
|
||||
export type PlanKindLayout = "plan" | "watershed";
|
||||
export interface PlanMark {
|
||||
chainage_m: number;
|
||||
refuge: boolean;
|
||||
spoil: boolean;
|
||||
}
|
||||
export interface PlanKindsState {
|
||||
pieces: { id: string; name: string }[];
|
||||
/** 고를 수 있는 평면도 축척 분모(서버 `DRAWING_SCALE_PLAN_CHOICES`) · 기본 · 유역도 고정. */
|
||||
@@ -301,9 +296,6 @@ export interface PlanKindsState {
|
||||
contour_intervals: number[];
|
||||
default_contour_interval: number;
|
||||
kinds: PlanKind[];
|
||||
marks: PlanMark[];
|
||||
/** 종단 측점 간격(m) — 측점 NO + 거리 입력용(GET 만 · 종단이 없으면 null). */
|
||||
station_interval_m?: number | null;
|
||||
}
|
||||
|
||||
export function fetchPlanKinds(projectId: string): Promise<PlanKindsState> {
|
||||
@@ -352,13 +344,6 @@ export function deletePlanKind(projectId: string, kindId: string): Promise<PlanK
|
||||
});
|
||||
}
|
||||
|
||||
export function savePlanMarks(projectId: string, marks: PlanMark[]): Promise<PlanKindsState> {
|
||||
return requestJson(`/projects/${projectId}/plan-marks`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ marks }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 도각 편집 화면이 싣는 도각 한 장 (실치수 1:1, 잠금 없는 도면층). */
|
||||
export interface FrameTemplateResponse {
|
||||
status: string;
|
||||
|
||||
@@ -35,7 +35,7 @@ TABLE_LINE_COLOR = "#8ea0b5"
|
||||
TABLE_LABEL_COLOR = "#e8edf4"
|
||||
TABLE_VALUE_COLOR = "#ffe066"
|
||||
|
||||
# 종단도 분할 기준: 측점 30개 초과 시 30개 단위(경계 1측점 중복)로 나눈다.
|
||||
# 종단도 분할 기준: 일반측점 30 간격(20 m 면 600 m) 단위(경계 1측점 중복) · 추가 · MC 측점은 안 셈.
|
||||
LONG_SPLIT_STATION_COUNT = 30
|
||||
|
||||
_ENTITY_NS = UUID("f15df4cc-fbb1-4bc9-b04c-63052fe43f96")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B07 종단도 CAD 조립 — 30측점 N분할 + 그래프 축·격자 + 하단 측점 테이블.
|
||||
"""B07 종단도 CAD 그림 — 그래프 축·격자 + 하단 측점 테이블(장 나눔 · 자리 · 도각 = `_LongSheet`).
|
||||
|
||||
납품 양식:
|
||||
- 그래프 영역: 좌측 Y축(표고 눈금·라벨), 기준선(X축, 최저 표고에서 5m 이상
|
||||
@@ -17,11 +17,8 @@ from typing import Any
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
DESIGN_COLOR,
|
||||
DESIGN_LAYER_ID,
|
||||
DRAWING_FORMAT,
|
||||
FRAME_LAYER_ID,
|
||||
GROUND_COLOR,
|
||||
GROUND_LAYER_ID,
|
||||
LONG_SPLIT_STATION_COUNT,
|
||||
LONG_TABLE_LAYER_ID,
|
||||
TABLE_LABEL_COLOR,
|
||||
TABLE_LINE_COLOR,
|
||||
@@ -31,7 +28,6 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_design_profile_points,
|
||||
_format,
|
||||
_interpolate,
|
||||
_layer,
|
||||
_line_entity,
|
||||
_profile_alignment,
|
||||
_stations,
|
||||
@@ -42,18 +38,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
polyline_entity,
|
||||
station_no_label,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import (
|
||||
LONG_PLAN_LAYER_ID,
|
||||
long_plan_entities,
|
||||
plan_box_overlaps,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
drawing_scale,
|
||||
entities_bbox,
|
||||
frame_entities,
|
||||
scale_fields,
|
||||
usable_area,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import drawing_scale
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
|
||||
|
||||
@@ -88,55 +73,7 @@ _LONG_TABLE_ROWS: tuple[tuple[str, str, float], ...] = (
|
||||
)
|
||||
_FONT_SIZE = 2.2
|
||||
_TICK_LEN = 0.8 # 측점 눈금 길이
|
||||
|
||||
|
||||
def _span(stations: list[dict[str, Any]], index: int, take: int) -> dict[str, float]:
|
||||
return {
|
||||
"start_m": float(stations[index]["chainage_m"]),
|
||||
"end_m": float(stations[index + take - 1]["chainage_m"]),
|
||||
}
|
||||
|
||||
|
||||
def _fits_sheet(longitudinal: dict[str, Any], span: dict[str, float]) -> bool:
|
||||
"""이 구간을 고정 축척으로 그렸을 때 A1 작도 영역에 들고 평면 위치도 칸을 안 덮는가."""
|
||||
content = _long_content(longitudinal, "probe", span)
|
||||
bbox = entities_bbox(content)
|
||||
if bbox is None:
|
||||
return True
|
||||
usable_w, usable_h = usable_area()
|
||||
fits = bbox[2] - bbox[0] <= usable_w + 1e-6 and bbox[3] - bbox[1] <= usable_h + 1e-6
|
||||
return fits and not plan_box_overlaps(longitudinal, content, bbox)
|
||||
|
||||
|
||||
def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""측점 30개 단위 분할 목록. 각 항목: {id, label, start_m, end_m}.
|
||||
|
||||
한 장이면 단일 도면(id="longitudinal", 기존 manifest 호환). 여러 장이면
|
||||
경계 측점 1개를 중복시켜 도면 간 선이 이어지게 한다(납품 도면 관례).
|
||||
축척(H 1/1,000 · V 1/200)은 모든 장이 같다 — 작도 영역을 넘으면 측점을 줄여
|
||||
장을 더 나눌 뿐 줄이지 않는다.
|
||||
"""
|
||||
stations = sorted(_stations(longitudinal), key=lambda s: float(s["chainage_m"]))
|
||||
if not stations:
|
||||
return [{"id": "longitudinal", "label": "종단도", "start_m": None, "end_m": None}]
|
||||
spans: list[dict[str, float]] = []
|
||||
index = 0
|
||||
while True:
|
||||
take = min(LONG_SPLIT_STATION_COUNT, len(stations) - index)
|
||||
# ponytail: 한 측점씩 줄이며 다시 잰다 — 넘치는 장에서만 돎
|
||||
while take > 2 and not _fits_sheet(longitudinal, _span(stations, index, take)):
|
||||
take -= 1
|
||||
spans.append(_span(stations, index, take))
|
||||
if index + take >= len(stations):
|
||||
break
|
||||
index += take - 1 # 경계 1측점 중복
|
||||
if len(spans) == 1:
|
||||
return [{"id": "longitudinal", "label": "종단도", "number": 1, "total": 1, **spans[0]}]
|
||||
return [
|
||||
{"id": f"longitudinal_{number}", "label": f"종단도({number})", "number": number, **span}
|
||||
| {"total": len(spans)}
|
||||
for number, span in enumerate(spans, start=1)
|
||||
]
|
||||
_AXIS_GAP = 10.0 # Y축 · 측점표 머리 ↔ 첫 측점 여유(종이 mm · 525-1)
|
||||
|
||||
|
||||
def _long_table_values(
|
||||
@@ -474,7 +411,8 @@ def _long_table_entities(
|
||||
"""종단 테이블: 가로 구분선 + 측점 눈금(세로선 없음) + 세로쓰기 값."""
|
||||
chainages = [float(s["chainage_m"]) * _mm_h() for s in stations]
|
||||
header_width = max(interval_m * _mm_h(), 12.0)
|
||||
left = chainages[0] - header_width
|
||||
header_x = chainages[0] - _AXIS_GAP
|
||||
left = header_x - header_width
|
||||
right = chainages[-1]
|
||||
entities: list[dict[str, Any]] = []
|
||||
|
||||
@@ -507,7 +445,7 @@ def _long_table_entities(
|
||||
)
|
||||
)
|
||||
# 외곽·헤더 구분 세로선 (측점 세로선은 없음)
|
||||
for seed, x in (("v-left", left), ("v-header", chainages[0]), ("v-right", right)):
|
||||
for seed, x in (("v-left", left), ("v-header", header_x), ("v-right", right)):
|
||||
entities.append(
|
||||
_line_entity(
|
||||
f"{drawing_id}:ltgrid:{seed}",
|
||||
@@ -637,7 +575,9 @@ def _long_content(
|
||||
if stations:
|
||||
chainages = [float(s["chainage_m"]) * mm_h for s in stations]
|
||||
entities.extend(
|
||||
_graph_grid_entities(drawing_id, chainages, chainages[0], chainages[-1], datum_m, top_m)
|
||||
_graph_grid_entities(
|
||||
drawing_id, chainages, chainages[0] - _AXIS_GAP, chainages[-1], datum_m, top_m
|
||||
)
|
||||
)
|
||||
|
||||
values = _long_table_values(stations, all_stations, ground_all, longitudinal, interval_m)
|
||||
@@ -657,41 +597,3 @@ def _long_content(
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
def build_longitudinal_drawing(
|
||||
longitudinal: dict[str, Any], drawing_id: str, chunk: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""종단도 청크 하나를 지반선+계획선+그래프 축·격자+측점 테이블로 만든다."""
|
||||
entities = _long_content(longitudinal, drawing_id, chunk)
|
||||
scale = drawing_scale("longitudinal")
|
||||
# A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다.
|
||||
# 그림 좌하단 = 작도영역 좌하단 — 장마다 같은 자리에서 시작한다.
|
||||
bbox = entities_bbox(entities)
|
||||
entities.extend(long_plan_entities(longitudinal, drawing_id, chunk, bbox)) # 521-2
|
||||
if bbox:
|
||||
entities.extend(
|
||||
frame_entities(
|
||||
drawing_id,
|
||||
bbox,
|
||||
fit=False,
|
||||
bottom_left=True,
|
||||
fields={
|
||||
"도면명": "종단면도",
|
||||
**scale_fields(("H", scale["x"]), ("V", scale["y"])),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"format": DRAWING_FORMAT,
|
||||
"entities": entities,
|
||||
"layers": [
|
||||
_layer(GROUND_LAYER_ID, "원지반", locked=True),
|
||||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||||
_layer(LONG_GRID_LAYER_ID, "그래프 격자", locked=True),
|
||||
_layer(LONG_TABLE_LAYER_ID, "측점표"),
|
||||
_layer(LONG_PLAN_LAYER_ID, "평면 위치도"),
|
||||
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""B07 종단도 우측 상단 평면 위치도(521-2 · 요구 340-2) — 장마다 작도 영역 우상단 칸 하나.
|
||||
|
||||
칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm(작도 영역 좌하단 = 그림 좌하단 · `frame_entities`
|
||||
bottom_left). 척도 = 종단 축척 `plan` 칸(None = 자동). 방위표만 도각 층.
|
||||
칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm(작도 영역 좌하단 = 장 bbox 좌하단 = 그림 좌하단
|
||||
− 50 mm · `LongSheet._sheet_bbox` · `frame_entities` bottom_left). 척도 = 종단 축척 `plan` 칸
|
||||
(None = 자동). 방위표만 도각 층.
|
||||
- 자동: 전체 노선을 칸에 맞춤(분모 500 단위 올림) · 전체 노선 가는 선 · 이 장 구간 굵게.
|
||||
- 척도 정함: 이 장 중심 확대 · 이 장 굵게 · 밖 노선 흐린 파선 · 칸 밖은 잘림.
|
||||
칸 내용 = 노선 · 이 장 시 · 종점 측점 글 · 방위표 · 「평면도 S=1/n」 · 「n/N 장」.
|
||||
@@ -43,7 +44,7 @@ _ROUTE_COLOR = "#7d8590" # 전체 · 밖 노선 — 흐리게
|
||||
|
||||
|
||||
def plan_box(content_bbox: tuple[float, float, float, float]) -> tuple[float, float, float, float]:
|
||||
"""칸 (x0, y0, x1, y1) — 그림 좌표(작도 영역 좌하단 = 그림 좌하단)."""
|
||||
"""칸 (x0, y0, x1, y1) — 그림 좌표(작도 영역 좌하단 = 장 bbox 좌하단)."""
|
||||
usable_w, usable_h = usable_area()
|
||||
x1 = content_bbox[0] + usable_w - _INSET
|
||||
y1 = content_bbox[1] + usable_h - _INSET
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""B07 종단도 장 — 일반측점 30 간격(600 m) 나눔 · 장 자리(그림 = 작도 영역 좌하단 + 50 mm) · 도각.
|
||||
|
||||
추가 · MC 측점은 세지 않고 구간 안이면 함께 그린다(525-1 · 요구 343-3). 그림 본체는
|
||||
`Engine_Cad_Long._long_content`.
|
||||
"""
|
||||
|
||||
from bisect import bisect_right
|
||||
from typing import Any
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
DESIGN_LAYER_ID,
|
||||
DRAWING_FORMAT,
|
||||
FRAME_LAYER_ID,
|
||||
GROUND_LAYER_ID,
|
||||
LONG_SPLIT_STATION_COUNT,
|
||||
LONG_TABLE_LAYER_ID,
|
||||
_layer,
|
||||
_stations,
|
||||
infer_station_interval,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import LONG_GRID_LAYER_ID, _long_content
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import (
|
||||
LONG_PLAN_LAYER_ID,
|
||||
long_plan_entities,
|
||||
plan_box_overlaps,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
drawing_scale,
|
||||
entities_bbox,
|
||||
frame_entities,
|
||||
scale_fields,
|
||||
usable_area,
|
||||
)
|
||||
|
||||
_SHEET_SHIFT = 50.0 # 그림 = 작도 영역 좌하단에서 오른쪽 · 위로(종이 mm · 525-1)
|
||||
|
||||
|
||||
def _sheet_bbox(
|
||||
bbox: tuple[float, float, float, float] | None,
|
||||
) -> tuple[float, float, float, float] | None:
|
||||
"""그림 bbox 를 좌 · 하로 넓힌 장 기준 — 도각 · 평면 칸 · 넘침 판정이 이 좌하단에 맞춤."""
|
||||
if bbox is None:
|
||||
return None
|
||||
return (bbox[0] - _SHEET_SHIFT, bbox[1] - _SHEET_SHIFT, bbox[2], bbox[3])
|
||||
|
||||
|
||||
def _span(stations: list[dict[str, Any]], index: int, take: int) -> dict[str, float]:
|
||||
return {
|
||||
"start_m": float(stations[index]["chainage_m"]),
|
||||
"end_m": float(stations[index + take - 1]["chainage_m"]),
|
||||
}
|
||||
|
||||
|
||||
def _fits_sheet(longitudinal: dict[str, Any], span: dict[str, float]) -> bool:
|
||||
"""이 구간을 고정 축척으로 그렸을 때 A1 작도 영역에 들고 평면 위치도 칸을 안 덮는가."""
|
||||
content = _long_content(longitudinal, "probe", span)
|
||||
bbox = _sheet_bbox(entities_bbox(content))
|
||||
if bbox is None:
|
||||
return True
|
||||
usable_w, usable_h = usable_area()
|
||||
fits = bbox[2] - bbox[0] <= usable_w + 1e-6 and bbox[3] - bbox[1] <= usable_h + 1e-6
|
||||
return fits and not plan_box_overlaps(longitudinal, content, bbox)
|
||||
|
||||
|
||||
def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""일반측점 30 간격(20 m 면 600 m) 단위 분할 목록. 각 항목: {id, label, start_m, end_m}.
|
||||
|
||||
추가 · MC 측점은 세지 않고 구간 안이면 함께 그린다(525-1 · 요구 343-3).
|
||||
|
||||
한 장이면 단일 도면(id="longitudinal", 기존 manifest 호환). 여러 장이면
|
||||
경계 측점 1개를 중복시켜 도면 간 선이 이어지게 한다(납품 도면 관례).
|
||||
축척(H 1/1,000 · V 1/200)은 모든 장이 같다 — 작도 영역을 넘으면 측점을 줄여
|
||||
장을 더 나눌 뿐 줄이지 않는다.
|
||||
"""
|
||||
stations = sorted(_stations(longitudinal), key=lambda s: float(s["chainage_m"]))
|
||||
if not stations:
|
||||
return [{"id": "longitudinal", "label": "종단도", "start_m": None, "end_m": None}]
|
||||
chainages = [float(s["chainage_m"]) for s in stations]
|
||||
length_m = LONG_SPLIT_STATION_COUNT * infer_station_interval(stations)
|
||||
spans: list[dict[str, float]] = []
|
||||
index = 0
|
||||
while True:
|
||||
take = max(2, bisect_right(chainages, chainages[index] + length_m + 1e-6) - index)
|
||||
take = min(take, len(stations) - index)
|
||||
# ponytail: 한 측점씩 줄이며 다시 잰다 — 넘치는 장에서만 돎
|
||||
while take > 2 and not _fits_sheet(longitudinal, _span(stations, index, take)):
|
||||
take -= 1
|
||||
spans.append(_span(stations, index, take))
|
||||
if index + take >= len(stations):
|
||||
break
|
||||
index += take - 1 # 경계 1측점 중복
|
||||
if len(spans) == 1:
|
||||
return [{"id": "longitudinal", "label": "종단도", "number": 1, "total": 1, **spans[0]}]
|
||||
return [
|
||||
{"id": f"longitudinal_{number}", "label": f"종단도({number})", "number": number, **span}
|
||||
| {"total": len(spans)}
|
||||
for number, span in enumerate(spans, start=1)
|
||||
]
|
||||
|
||||
|
||||
def build_longitudinal_drawing(
|
||||
longitudinal: dict[str, Any], drawing_id: str, chunk: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""종단도 청크 하나를 지반선+계획선+그래프 축·격자+측점 테이블로 만든다."""
|
||||
entities = _long_content(longitudinal, drawing_id, chunk)
|
||||
scale = drawing_scale("longitudinal")
|
||||
# A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다.
|
||||
# 그림 좌하단 = 작도영역 좌하단 + (50, 50) mm — 장마다 같은 자리에서 시작한다.
|
||||
bbox = _sheet_bbox(entities_bbox(entities))
|
||||
entities.extend(long_plan_entities(longitudinal, drawing_id, chunk, bbox)) # 521-2
|
||||
if bbox:
|
||||
entities.extend(
|
||||
frame_entities(
|
||||
drawing_id,
|
||||
bbox,
|
||||
fit=False,
|
||||
bottom_left=True,
|
||||
fields={
|
||||
"도면명": "종단면도",
|
||||
**scale_fields(("H", scale["x"]), ("V", scale["y"])),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"format": DRAWING_FORMAT,
|
||||
"entities": entities,
|
||||
"layers": [
|
||||
_layer(GROUND_LAYER_ID, "원지반", locked=True),
|
||||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||||
_layer(LONG_GRID_LAYER_ID, "그래프 격자", locked=True),
|
||||
_layer(LONG_TABLE_LAYER_ID, "측점표"),
|
||||
_layer(LONG_PLAN_LAYER_ID, "평면 위치도"),
|
||||
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
||||
],
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"""B07 계획평면도 종류 — 정보 조각을 골라 한 장으로 얹는다(PLAN 60장).
|
||||
|
||||
계획평면도는 여러 종류(지형 · 노선배치도 · 대피소 · 라이다 · 사용자 추가)가 **같은 양식**이다.
|
||||
계획평면도는 여러 종류(지형도 · 사용자 추가)가 **같은 양식**이다.
|
||||
정보 조각(등고선 · 중심선 · 노견선 · 성토 · 절토 외곽선 · 비탈 눈금 · IP · R · 측점 NO ·
|
||||
시종점 · 대피소/사토장 · 배수시설 기호 · 라이다 음영)마다 프로젝트 저장소에 파일이 하나씩
|
||||
시종점 · 배수시설 기호 · 라이다 음영)마다 프로젝트 저장소에 파일이 하나씩
|
||||
있고, 종류는 그중 무엇을 얹을지 고른 목록이다. 이 모듈은 조각 기하 계산과 도면 조립만
|
||||
한다(파일 입출력 없음).
|
||||
|
||||
@@ -83,7 +83,6 @@ PIECES: tuple[tuple[str, str, bool], ...] = (
|
||||
("ip_table", "IP 표", False),
|
||||
("station", "측점 NO", False),
|
||||
("endpoints", "시종점", False),
|
||||
("refuge", "대피소 · 사토장", False),
|
||||
("drainage", "배수시설 기호", False),
|
||||
# 용지도(93장) — 연속지적도 · 행정구역
|
||||
("parcel", "필지 경계", False),
|
||||
@@ -99,33 +98,9 @@ LEGACY_PIECES: dict[str, tuple[str, ...]] = {
|
||||
_OUTLINE = LEGACY_PIECES["slope_outline"]
|
||||
|
||||
_TERRAIN = ("contour", "stream", "centerline", *_OUTLINE, "slope_ticks", "ip", "radius")
|
||||
# 기본 종류 — 지울 수 있다. id 는 옛 도면 id(plan_terrain …)와 같아 확정본이 이어진다.
|
||||
# 기본 종류 — 지울 수 있다. id 는 옛 도면 id(plan_terrain)와 같아 확정본이 이어진다.
|
||||
DEFAULT_KINDS: tuple[dict[str, Any], ...] = (
|
||||
{"id": "terrain", "name": "지형", "pieces": [*_TERRAIN, "station"]},
|
||||
{
|
||||
"id": "route",
|
||||
"name": "노선배치도",
|
||||
"pieces": [p for p in _TERRAIN if p != "radius"] + ["station", "endpoints", "ip_table"],
|
||||
},
|
||||
{
|
||||
"id": "layout",
|
||||
"name": "대피소",
|
||||
"pieces": [p for p in _TERRAIN if p != "radius"] + ["station", "refuge"],
|
||||
},
|
||||
# 라이다 = 지형과 같은 정보 · 배경만 라이다(등고선 · 계류 대신).
|
||||
{
|
||||
"id": "lidar",
|
||||
"name": "라이다",
|
||||
"pieces": ["lidar", *[p for p in _TERRAIN if p not in ("contour", "stream")], "station"],
|
||||
},
|
||||
# 용지도 = 옛 따로 도면과 같은 정보(등고선 · 노선 · 지적 · 행정 경계).
|
||||
{
|
||||
"id": "landuse",
|
||||
"name": "용지도",
|
||||
"pieces": ["contour", "centerline", "parcel", "parcel_info", "emd", "sgg"],
|
||||
},
|
||||
# 배수유역도 = 양식이 다른 종류(1/6,000 한 장 · 정보표) — 조각을 고르지 않는다(109장).
|
||||
{"id": "watershed", "name": "배수유역도", "pieces": [], "layout": "watershed"},
|
||||
{"id": "terrain", "name": "지형도", "pieces": [*_TERRAIN, "station"]},
|
||||
)
|
||||
|
||||
_COLORS = {
|
||||
@@ -140,7 +115,6 @@ _COLORS = {
|
||||
"radius": "#4d7dff",
|
||||
"station": "#ffe066",
|
||||
"endpoints": "#ff9d4d",
|
||||
"refuge": "#7ddc7d",
|
||||
"drainage": STRUCTURE_COLOR,
|
||||
"parcel": "#8c6b4f",
|
||||
"parcel_info": "#a8703f", # 밝음 · 어두움 바탕 둘 다 읽히는 중간 밤색
|
||||
@@ -422,14 +396,6 @@ def _piece_entities(
|
||||
],
|
||||
(cx, cy),
|
||||
)
|
||||
elif piece == "refuge":
|
||||
size = 2.0 # 네모 기호 반크기(종이 mm)
|
||||
for index, item in enumerate(data.get("items") or []):
|
||||
x, y, half = item["x"], item["y"], size / mm
|
||||
corners = [(-1, -1), (1, -1), (1, 1), (-1, 1), (-1, -1)]
|
||||
line([(x + sx * half, y + sy * half) for sx, sy in corners], f":{index}")
|
||||
cx, cy = paper((x, y))
|
||||
text(f"{index}", item["label"], _LABEL_FONT, _side_spots(cx, cy, size), (cx, cy))
|
||||
elif piece == "drainage":
|
||||
size = _STRUCTURE_SIZE_MM # 마름모 기호 반크기(종이 mm)
|
||||
for index, item in enumerate(data.get("items") or []):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
조각(`_piece_entities`)은 글을 바로 그리지 않고 후보 자리째 모은다. 여기서 우선순위 차례로
|
||||
후보 중 첫 빈 자리를 고르고(못 찾으면 첫 후보) 멀리 놓인 글엔 지시선을 단다.
|
||||
우선순위 = 시종점 > 측점 NO > IP > R > 배수 > 대피소. 서버 단독 계산(CLAUDE.md 5장 ③).
|
||||
우선순위 = 시종점 > 측점 NO > IP > R > 배수. 서버 단독 계산(CLAUDE.md 5장 ③).
|
||||
|
||||
좌표 = 종이 mm. 글 사각 = 글 크기 × (한글 1.0 · 그 밖 0.6) 폭 · 글 크기 높이(세로 가운데 기준 —
|
||||
CAD 가 textBaseline middle 로 그림) · 방향이 있으면 돌린 사각의 바깥 상자.
|
||||
@@ -13,7 +13,7 @@ from typing import Any
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import _text_entity, polyline_entity
|
||||
|
||||
PRIORITY = ("endpoints", "station", "ip", "radius", "drainage", "refuge")
|
||||
PRIORITY = ("endpoints", "station", "ip", "radius", "drainage")
|
||||
_GAP_MM = 0.6 # 글 사이 틈(종이 mm)
|
||||
_PUSH = (2.0, 3.0) # 먼 고리 — 후보 거리의 배수
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""B07 계획평면도 종류 · 대피소 자리 라우터 (PLAN 60장).
|
||||
"""B07 계획평면도 종류 라우터 (PLAN 60장).
|
||||
|
||||
종류 = 정보 조각 고르기 목록. 추가 · 업데이트 · 지우기는 파일 하나씩(`plan_kinds/{id}.json`)이다.
|
||||
대피소 · 사토장 자리는 구조물 정본 밖에서 사용자가 더 정한 자리(`plan_marks.json`)다.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -13,19 +12,14 @@ from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import infer_station_interval
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import PIECES
|
||||
from B07_DesignDetail.B07_DesignDetail_Router import _confirmed_source
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Frame import _project_root
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_json
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
add_kind,
|
||||
delete_kind,
|
||||
list_kinds,
|
||||
read_marks,
|
||||
reorder_kinds,
|
||||
update_kind,
|
||||
write_marks,
|
||||
)
|
||||
from config.config_system import (
|
||||
BASIN_CONTOUR_INTERVAL_CHOICES,
|
||||
@@ -54,16 +48,6 @@ class PlanKindsOrderRequest(BaseModel):
|
||||
ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PlanMark(BaseModel):
|
||||
chainage_m: float = Field(ge=0)
|
||||
refuge: bool = False
|
||||
spoil: bool = False
|
||||
|
||||
|
||||
class PlanMarksRequest(BaseModel):
|
||||
marks: list[PlanMark] = Field(default_factory=list)
|
||||
|
||||
|
||||
def _state(root: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -74,7 +58,6 @@ def _state(root: Any) -> dict[str, Any]:
|
||||
"contour_intervals": list(BASIN_CONTOUR_INTERVAL_CHOICES),
|
||||
"default_contour_interval": BASIN_CONTOUR_INTERVAL_M,
|
||||
"kinds": list_kinds(root),
|
||||
"marks": read_marks(root),
|
||||
}
|
||||
|
||||
|
||||
@@ -82,22 +65,11 @@ def _error(status: int, exc: Exception) -> JSONResponse:
|
||||
return JSONResponse(status_code=status, content={"status": "error", "message": str(exc)})
|
||||
|
||||
|
||||
async def _station_interval(project_id: UUID) -> float | None:
|
||||
"""종단 측점 간격(m) — 측점 NO + 거리 입력을 누가거리로 바꾸는 기준. 종단이 없으면 None."""
|
||||
try:
|
||||
_route_id, _root, longitudinal_path, _bypassed = await _confirmed_source(project_id)
|
||||
longitudinal = await asyncio.to_thread(_read_json, longitudinal_path)
|
||||
except (FileNotFoundError, PermissionError, OSError, ValueError):
|
||||
return None
|
||||
return infer_station_interval(longitudinal.get("stations") or [])
|
||||
|
||||
|
||||
@router.get("/{project_id}/plan-kinds")
|
||||
async def get_plan_kinds(project_id: UUID) -> Any:
|
||||
"""고를 수 있는 조각 · 지금 종류 · 대피소 자리 · 측점 간격."""
|
||||
"""고를 수 있는 조각 · 지금 종류."""
|
||||
root = await _project_root(project_id)
|
||||
state = await asyncio.to_thread(_state, root)
|
||||
return {**state, "station_interval_m": await _station_interval(project_id)}
|
||||
return await asyncio.to_thread(_state, root)
|
||||
|
||||
|
||||
@router.post("/{project_id}/plan-kinds")
|
||||
@@ -153,7 +125,7 @@ async def put_plan_kind(project_id: UUID, kind_id: str, request: PlanKindRequest
|
||||
|
||||
@router.delete("/{project_id}/plan-kinds/{kind_id}")
|
||||
async def delete_plan_kind(project_id: UUID, kind_id: str) -> Any:
|
||||
"""종류 지우기 — 기본 넷도 지울 수 있다."""
|
||||
"""종류 지우기 — 기본 종류도 지울 수 있다."""
|
||||
try:
|
||||
root = await _project_root(project_id)
|
||||
await asyncio.to_thread(delete_kind, root, kind_id)
|
||||
@@ -162,15 +134,3 @@ async def delete_plan_kind(project_id: UUID, kind_id: str) -> Any:
|
||||
return _error(400, exc)
|
||||
except FileNotFoundError as exc:
|
||||
return _error(404, exc)
|
||||
|
||||
|
||||
@router.put("/{project_id}/plan-marks")
|
||||
async def put_plan_marks(project_id: UUID, request: PlanMarksRequest) -> Any:
|
||||
"""사용자가 정한 대피소 · 사토장 자리를 통째로 갈아 쓴다."""
|
||||
try:
|
||||
root = await _project_root(project_id)
|
||||
marks = [mark.model_dump() for mark in request.marks]
|
||||
await asyncio.to_thread(write_marks, root, marks)
|
||||
return await asyncio.to_thread(_state, root)
|
||||
except ValueError as exc:
|
||||
return _error(400, exc)
|
||||
|
||||
@@ -40,7 +40,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Development import (
|
||||
development_list,
|
||||
read_development,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import (
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongSheet import (
|
||||
build_longitudinal_drawing,
|
||||
longitudinal_chunks,
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ _STAGE_DIR = "B07_DesignDetail"
|
||||
|
||||
_CROSS_ID = re.compile(r"^cross_(\d+)m$")
|
||||
_LONG_ID = re.compile(r"^longitudinal(?:_(\d+))?$")
|
||||
# 종단 장 규칙 — 장 나눔 · 자리가 바뀌면 올림(옛 종단 확정본을 한 번 풂 · 521-2 칸 · 525-1 600 m)
|
||||
LONG_SHEET_RULE = 2
|
||||
# 노선 전장에 한 장씩만 나오는 도면 — 라우터가 원본 자료를 따로 실어 넘긴다.
|
||||
MASS_HAUL_ID = "mass_haul"
|
||||
COVER_ID = "cover"
|
||||
@@ -83,28 +85,16 @@ def _write_manifest(project_root: Path, manifest: dict[str, Any]) -> None:
|
||||
|
||||
|
||||
def release_old_long_sheets(project_root: Path) -> None:
|
||||
"""평면 위치도(521-2) 앞 확정 종단 저장본 — 종단만 모두 한 번 풂(칸 · 장 나눔이 바뀜).
|
||||
"""종단 장 규칙이 바뀌기 전 확정 저장본 — 종단만 모두 한 번 풂(칸 · 장 나눔 · 자리가 바뀜).
|
||||
|
||||
풀린 장의 저장본은 `_write_manifest` 가 걷으니 다음부터는 새 저장본(칸 층 있음)만 남음.
|
||||
매니페스트 `long_sheet_rule` 이 지금 규칙과 다르면 풀고 적음 — 다음부터는 새 규칙
|
||||
저장본만 남음. 풀린 장의 저장본은 `_write_manifest` 가 걷음.
|
||||
"""
|
||||
# 엔진 → 양식 → 이 모듈 차례라 위에서 부르면 돌고 돎
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import LONG_PLAN_LAYER_ID
|
||||
|
||||
manifest = _read_manifest(project_root)
|
||||
confirmed = [
|
||||
(drawing_id, entry)
|
||||
for drawing_id, entry in manifest["drawings"].items()
|
||||
if _LONG_ID.fullmatch(drawing_id) and entry.get("confirmed")
|
||||
]
|
||||
if not confirmed:
|
||||
if manifest.get("long_sheet_rule") == LONG_SHEET_RULE:
|
||||
return
|
||||
# ponytail: 첫 장만 봄 — 옛 장은 한꺼번에 풀리므로 새 장과 섞이지 않음
|
||||
saved = _design_root(project_root) / "drawings" / f"{confirmed[0][0]}.json"
|
||||
try:
|
||||
if f'"{LONG_PLAN_LAYER_ID}"' in saved.read_text(encoding="utf-8"):
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
for _drawing_id, entry in confirmed:
|
||||
entry["confirmed"] = False
|
||||
for drawing_id, entry in manifest["drawings"].items():
|
||||
if _LONG_ID.fullmatch(drawing_id):
|
||||
entry["confirmed"] = False
|
||||
manifest["long_sheet_rule"] = LONG_SHEET_RULE
|
||||
_write_manifest(project_root, manifest)
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout, scale} — 파일 하나씩
|
||||
(유역도 양식은 + contour_interval_m 등고선 간격 5 · 10 m)
|
||||
plan_pieces/{장}/{조각}.json 정보 조각 하나 = 그 장 범위의 기하(사업지 좌표 m)
|
||||
plan_marks.json 사용자가 정한 대피소 · 사토장 자리
|
||||
|
||||
`plan_kinds/` 가 없으면 기본 종류(지형 · 노선배치도 · 대피소 · 라이다 · 용지도 · 배수유역도)를
|
||||
쓴다. 양식(layout) = plan(1/1,200 장 나눔 · 조각 고름) · watershed(유역도 1/6,000 한 장 · 조각
|
||||
`plan_kinds/` 가 없으면 기본 종류(지형도 하나)를 쓴다. 양식(layout) = plan(1/1,200 장 나눔 · 조각 고름) · watershed(유역도 1/6,000 한 장 · 조각
|
||||
없음). 처음 고칠 때 기본 종류를 파일로 내려 두고 그 위에서 추가 · 업데이트 · 지운다 — 기본 종류도
|
||||
고치고 지울 수 있다.
|
||||
조각은 서버가 도면을 열 때마다 원자료(B05 노선 · 곡선표 · 구조물, B06 횡단 설계, B04 배경)
|
||||
@@ -24,7 +22,6 @@ from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
cross_station,
|
||||
find_cross_file,
|
||||
)
|
||||
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,
|
||||
@@ -86,8 +83,6 @@ 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 # 대피소 · 사토장 기호를 중심선에서 띄우는 거리(좌측 · 1/1,200 실거리)
|
||||
|
||||
|
||||
def plan_title(name: str) -> str:
|
||||
@@ -101,8 +96,9 @@ def _kinds_dir(project_root: Path) -> Path:
|
||||
return _design_root(project_root) / "plan_kinds"
|
||||
|
||||
|
||||
# 이관 표시 `_{id}_added` — 있으면 그 기본 종류를 이미 더해 준 폴더(지운 뒤 되살리지 않음).
|
||||
_ADDED_ONCE = ("landuse", "watershed")
|
||||
def _marks_path(project_root: Path) -> Path:
|
||||
"""옛 대피소 · 사토장 자리 파일 — 읽지 않음 · [초기화]가 지우기만."""
|
||||
return _design_root(project_root) / "plan_marks.json"
|
||||
|
||||
|
||||
def _scale(layout: str, value: Any) -> int:
|
||||
@@ -132,27 +128,6 @@ def _document(kind: dict[str, Any], order: int) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
documents = []
|
||||
for path in folder.glob("*.json"):
|
||||
try:
|
||||
documents.append(_read_json(path))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
names = {str(document.get("name")) for document in documents}
|
||||
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, layout, scale}] — 차례대로. 파일이 없으면 기본 종류."""
|
||||
folder = _kinds_dir(project_root)
|
||||
@@ -168,7 +143,6 @@ def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
}
|
||||
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):
|
||||
@@ -206,8 +180,6 @@ def _materialize(project_root: Path) -> list[dict[str, Any]]:
|
||||
if not folder.is_dir():
|
||||
for order, kind in enumerate(kinds):
|
||||
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
|
||||
|
||||
|
||||
@@ -350,40 +322,6 @@ def delete_kind(project_root: Path, kind_id: str) -> None:
|
||||
path.unlink()
|
||||
|
||||
|
||||
# ── 대피소 · 사토장 자리(사용자 지정) ─────────────────
|
||||
|
||||
|
||||
def _marks_path(project_root: Path) -> Path:
|
||||
return _design_root(project_root) / "plan_marks.json"
|
||||
|
||||
|
||||
def read_marks(project_root: Path) -> list[dict[str, Any]]:
|
||||
path = _marks_path(project_root)
|
||||
if not path.is_file():
|
||||
return []
|
||||
try:
|
||||
marks = _read_json(path).get("marks")
|
||||
except (OSError, ValueError):
|
||||
return []
|
||||
return [mark for mark in marks or [] if isinstance(mark, dict)]
|
||||
|
||||
|
||||
def write_marks(project_root: Path, marks: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
clean: list[dict[str, Any]] = []
|
||||
for mark in marks:
|
||||
chainage = mark.get("chainage_m")
|
||||
if not isinstance(chainage, (int, float)) or chainage < 0:
|
||||
raise ValueError("대피소 · 사토장 자리의 측점(m)이 올바르지 않습니다.")
|
||||
refuge, spoil = bool(mark.get("refuge")), bool(mark.get("spoil"))
|
||||
if refuge or spoil:
|
||||
clean.append(
|
||||
{"chainage_m": round(float(chainage), 2), "refuge": refuge, "spoil": spoil}
|
||||
)
|
||||
clean.sort(key=lambda mark: mark["chainage_m"])
|
||||
atomic_write_json(_marks_path(project_root), {"marks": clean})
|
||||
return clean
|
||||
|
||||
|
||||
# ── 정보 조각 ─────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -457,50 +395,6 @@ def slope_records(
|
||||
return records
|
||||
|
||||
|
||||
def _refuge_items(
|
||||
project_root: Path,
|
||||
route: list[tuple[float, float, float]],
|
||||
start_m: float,
|
||||
end_m: float,
|
||||
scale: int = DRAWING_SCALE_PLAN,
|
||||
) -> dict[str, Any]:
|
||||
"""대피소 · 사토장 = 구조물 정본의 그 종류 + 사용자가 정한 자리. 한 자리에 둘 다 가능.
|
||||
|
||||
띄움은 종이 거리가 같게 축척 따라(141-2)."""
|
||||
offset = _REFUGE_OFFSET_M * scale / DRAWING_SCALE_PLAN
|
||||
spots: dict[float, set[str]] = {}
|
||||
_revision, structures = load_structures(str(project_root))
|
||||
for structure in structures:
|
||||
label = _REFUGE_TYPES.get(structure.type_id)
|
||||
if not label:
|
||||
continue
|
||||
chainage = structure.chainage_m
|
||||
if chainage is None and structure.start_m is not None and structure.end_m is not None:
|
||||
chainage = (structure.start_m + structure.end_m) / 2.0
|
||||
if chainage is not None:
|
||||
spots.setdefault(round(float(chainage), 1), set()).add(label)
|
||||
for mark in read_marks(project_root):
|
||||
key = round(float(mark["chainage_m"]), 1)
|
||||
if mark.get("refuge"):
|
||||
spots.setdefault(key, set()).add("대피소")
|
||||
if mark.get("spoil"):
|
||||
spots.setdefault(key, set()).add("사토장")
|
||||
items = []
|
||||
for chainage, labels in sorted(spots.items()):
|
||||
if not (start_m <= chainage <= end_m) or len(route) < 2:
|
||||
continue
|
||||
x, y, nx, ny = route_frame(route, chainage)
|
||||
items.append(
|
||||
{
|
||||
"chainage_m": chainage,
|
||||
"label": " · ".join(name for name in ("대피소", "사토장") if name in labels),
|
||||
"x": x + nx * offset,
|
||||
"y": y + ny * offset,
|
||||
}
|
||||
)
|
||||
return {"items": items}
|
||||
|
||||
|
||||
def build_pieces(
|
||||
context: Any,
|
||||
longitudinal: dict[str, Any],
|
||||
@@ -582,8 +476,6 @@ def build_pieces(
|
||||
label += f" L={length:.1f}m"
|
||||
items.append({"label": label, "x": x, "y": y, "nx": nx, "ny": ny})
|
||||
pieces["endpoints"] = {"items": items}
|
||||
if "refuge" in wanted:
|
||||
pieces["refuge"] = _refuge_items(project_root, route, start_m, end_m, scale)
|
||||
if "drainage" in wanted:
|
||||
min_x, min_y, max_x, max_y = box
|
||||
pieces["drainage"] = {
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
/* =============================================================================
|
||||
* B07_DesignDetail_UI_PlanKinds.ts
|
||||
* 좌측 도면 목록의 「평면도」 한 묶음 — 종류(지형 · 노선배치도 · 대피소 · 라이다 ·
|
||||
* 좌측 도면 목록의 「평면도」 한 묶음 — 종류(지형도 ·
|
||||
* 사용자 추가)별 도면 단추 + [평면도 관리] 단추 (PLAN 60 · 67 · 130장).
|
||||
* 종류 추가 · 업데이트 폼 · 대피소 · 사토장 자리 폼은 [평면도 관리] 모달(`_UI_PlanManage.ts`)이 씀.
|
||||
* 종류 추가 · 업데이트 폼은 [평면도 관리] 모달(`_UI_PlanManage.ts`)이 씀.
|
||||
*
|
||||
* 종류 = 정보 조각 고르기 목록이다. 고치면 서버 파일이 바뀌고 `onChanged` 로 화면을 다시 세운다.
|
||||
* 드롭다운 · 체크박스는 공용(`createSelectField` · `ui_template_checkbox`) · 측점 입력은 B05 구조물 폼과 같은 측점 NO + 거리 두 칸.
|
||||
* 드롭다운 · 체크박스는 공용(`createSelectField` · `ui_template_checkbox`).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createSelectField, showToast } from "@ui/ui_template_elements";
|
||||
import { stationFields } from "../B05_Profile/B05_Profile_UI_Structures_Fields";
|
||||
import {
|
||||
addPlanKind,
|
||||
deletePlanKind,
|
||||
savePlanMarks,
|
||||
updatePlanKind,
|
||||
type DesignDrawingItem,
|
||||
type PlanKindLayout,
|
||||
type PlanKindsState,
|
||||
type PlanMark,
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
|
||||
/** 종단이 없어 측점 간격을 못 받았을 때 — 서버 `infer_station_interval` 기본값과 같음. */
|
||||
const DEFAULT_STATION_INTERVAL_M = 20;
|
||||
|
||||
export interface PlanKindsControls {
|
||||
projectId: string;
|
||||
/** 종류 · 대피소 자리를 고친 뒤 — 목록과 도면을 다시 받는다. */
|
||||
/** 종류를 고친 뒤 — 목록과 도면을 다시 받는다. */
|
||||
onChanged: () => void;
|
||||
}
|
||||
|
||||
@@ -56,6 +50,24 @@ export async function run(
|
||||
}
|
||||
}
|
||||
|
||||
/** 항목 체크박스 그룹 — 그룹 사이는 구분선만 · 그룹 추가 · 바꿈은 이 목록 한 곳.
|
||||
* 어느 그룹에도 없는 항목은 맨 끝 그룹에 붙는다. */
|
||||
const PIECE_GROUPS: string[][] = [
|
||||
["lidar", "contour", "stream", "parcel", "parcel_info", "emd", "sgg"], // 수치지형
|
||||
["basin", "basin_no", "drainage"], // 배수유역
|
||||
[
|
||||
"centerline",
|
||||
"road_edge",
|
||||
"fill_outline",
|
||||
"cut_outline",
|
||||
"slope_ticks",
|
||||
"ip",
|
||||
"radius",
|
||||
"ip_table",
|
||||
], // 노선
|
||||
["station", "endpoints"], // 측점 관련
|
||||
];
|
||||
|
||||
/** 양식 — 평면도(조각 고름 · 축척 고름 · 장 나눔) · 유역도(1/6,000 한 장 · 그림 고정). */
|
||||
const LAYOUTS: { value: PlanKindLayout; text: string }[] = [
|
||||
{ value: "plan", text: "평면도" },
|
||||
@@ -114,7 +126,16 @@ export function kindForm(
|
||||
name.maxLength = 40;
|
||||
const boxes = state.pieces.map((piece) => createCheckbox(piece.name, { value: piece.id }));
|
||||
const pieceList = el("div", "b07-plan-tools__pieces");
|
||||
pieceList.append(...boxes.map((box) => box.root));
|
||||
const grouped = PIECE_GROUPS.map((ids) => boxes.filter(({ input }) => ids.includes(input.value)));
|
||||
grouped[grouped.length - 1].push(
|
||||
...boxes.filter((box) => !PIECE_GROUPS.some((ids) => ids.includes(box.input.value))),
|
||||
);
|
||||
grouped
|
||||
.filter((group) => group.length)
|
||||
.forEach((group, index) => {
|
||||
if (index) pieceList.append(el("hr", "b07-plan-tools__divider"));
|
||||
pieceList.append(...group.map((box) => box.root));
|
||||
});
|
||||
const action = el("button", "b07-plan-tools__button");
|
||||
action.type = "button";
|
||||
const remove = el("button", "b07-plan-tools__button", "지우기");
|
||||
@@ -147,6 +168,7 @@ export function kindForm(
|
||||
// 양식은 새로 만들 때만 고른다 — 있는 종류는 그 양식을 보이기만.
|
||||
layout.select.value = kind?.layout ?? "plan";
|
||||
layout.select.disabled = Boolean(kind);
|
||||
layout.root.hidden = kind?.layout === "plan"; // 평면도는 늘 같은 값 — 새 종류 · 유역도만 보임
|
||||
action.textContent = kind ? "업데이트" : "추가";
|
||||
remove.hidden = !kind;
|
||||
showPieces();
|
||||
@@ -177,7 +199,9 @@ export function kindForm(
|
||||
});
|
||||
const actions = el("div", "b07-plan-tools__actions");
|
||||
actions.append(action, remove);
|
||||
form.append(pick.root, layout.root, scale.root, contour.root, name, pieceList, actions);
|
||||
const fields = el("div", "b07-plan-tools__fields");
|
||||
fields.append(pick.root, layout.root, scale.root, contour.root);
|
||||
form.append(fields, name, pieceList, actions);
|
||||
return {
|
||||
root: form,
|
||||
pick: (kindId) => {
|
||||
@@ -187,61 +211,6 @@ export function kindForm(
|
||||
};
|
||||
}
|
||||
|
||||
export function marksForm(state: PlanKindsState, controls: PlanKindsControls): HTMLElement {
|
||||
const form = el("div", "b07-plan-tools__block");
|
||||
form.append(el("strong", "", "대피소 · 사토장 자리"));
|
||||
form.append(
|
||||
el(
|
||||
"p",
|
||||
"b07-plan-tools__hint",
|
||||
"구조물로 놓은 대피소 · 사토장은 저절로 실립니다. 더 놓을 측점(NO + 거리)만 적으세요.",
|
||||
),
|
||||
);
|
||||
const interval = state.station_interval_m || DEFAULT_STATION_INTERVAL_M;
|
||||
const rows = el("div", "b07-plan-tools__marks");
|
||||
const readers = new Map<HTMLElement, () => PlanMark | null>();
|
||||
const addRow = (mark: PlanMark): void => {
|
||||
const row = el("div", "b07-plan-tools__mark");
|
||||
const station = stationFields("측점", () => interval);
|
||||
station.write(mark.chainage_m, interval);
|
||||
const refuge = createCheckbox("대피소", { checked: mark.refuge });
|
||||
const spoil = createCheckbox("사토장", { checked: mark.spoil });
|
||||
const remove = el("button", "b07-plan-tools__remove", "×");
|
||||
remove.type = "button";
|
||||
remove.title = "이 자리 빼기";
|
||||
remove.addEventListener("click", () => {
|
||||
readers.delete(row);
|
||||
row.remove();
|
||||
});
|
||||
row.append(station.wrap, refuge.root, spoil.root, remove);
|
||||
readers.set(row, () => {
|
||||
const chainage = station.read(interval, true);
|
||||
return chainage === null
|
||||
? null
|
||||
: { chainage_m: chainage, refuge: refuge.input.checked, spoil: spoil.input.checked };
|
||||
});
|
||||
rows.append(row);
|
||||
};
|
||||
state.marks.forEach(addRow);
|
||||
const more = el("button", "b07-plan-tools__button", "자리 추가");
|
||||
more.type = "button";
|
||||
more.addEventListener("click", () => addRow({ chainage_m: 0, refuge: true, spoil: false }));
|
||||
const save = el("button", "b07-plan-tools__button", "업데이트");
|
||||
save.type = "button";
|
||||
save.addEventListener("click", () => {
|
||||
const marks = [...readers.values()].map((read) => read());
|
||||
if (marks.some((mark) => mark === null)) {
|
||||
showToast("측점 NO + 거리를 바르게 적어 주세요.", "error");
|
||||
return;
|
||||
}
|
||||
void run(() => savePlanMarks(controls.projectId, marks as PlanMark[]), controls);
|
||||
});
|
||||
const actions = el("div", "b07-plan-tools__actions");
|
||||
actions.append(more, save);
|
||||
form.append(rows, actions);
|
||||
return form;
|
||||
}
|
||||
|
||||
/** 장을 종류별로 묶음 — 여러 장이면 「이름 N장」 · 한 장이면 이름(좌측 목록 · 관리 모달 공용). */
|
||||
export function groupPlanSheets(
|
||||
items: DesignDrawingItem[],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* =============================================================================
|
||||
* B07_DesignDetail_UI_PlanManage.ts
|
||||
* [평면도 관리] 모달(PLAN 130-1) — 좌측 패널이 좁아 종류 · 대피소 자리 · 장 보기를 한 창에 모음.
|
||||
* [평면도 관리] 모달(PLAN 130-1) — 좌측 패널이 좁아 종류 · 장 보기를 한 창에 모음.
|
||||
*
|
||||
* 왼쪽 = [종류 ▼] 상세 폼(추가 · 업데이트 · 지우기) · 평면도 장(종류마다 한 줄 [≡] 이름 [1][2] ·
|
||||
* 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 저장 140-2) · 대피소 · 사토장 자리
|
||||
* 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 저장 140-2)
|
||||
* 오른쪽 = 고른 장 미리보기(보기 전용 CAD · 캔버스만 `bare`) · 확정 배지 · [CAD 에서 열기]
|
||||
* 고치기는 지금 서버 길 그대로(종류 파일) — 고칠 때마다 모달 안을 다시 받고,
|
||||
* 닫을 때 `onClosed(changed)` 로 좌측 목록 · 도면을 다시 읽음.
|
||||
@@ -25,7 +25,6 @@ import { openDrawing, syncDrawings } from "./B07_DesignDetail_Prefetch";
|
||||
import {
|
||||
groupPlanSheets,
|
||||
kindForm,
|
||||
marksForm,
|
||||
run,
|
||||
type KindForm,
|
||||
type PlanKindsControls,
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
|
||||
export interface PlanManageControls {
|
||||
projectId: string;
|
||||
/** 모달을 닫음 — changed = 종류 · 자리 · 확정을 고쳤나 · openId = [CAD 에서 열기]로 고른 장. */
|
||||
/** 모달을 닫음 — changed = 종류 · 확정을 고쳤나 · openId = [CAD 에서 열기]로 고른 장. */
|
||||
onClosed: (changed: boolean, openId?: string) => void;
|
||||
/** 머리 단추 — ⚙ 로 열 때 [원본에서 다시 그리기](280-5). */
|
||||
actions?: HTMLElement[];
|
||||
@@ -80,7 +79,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo
|
||||
const view = el("div", "b07-plan-manage__view");
|
||||
view.append(head, preview.element);
|
||||
|
||||
// 종류 · 자리 폼은 좌측에 있던 것 그대로 — 고친 뒤 모달 안만 다시 세움(화면은 닫을 때).
|
||||
// 종류 폼은 좌측에 있던 것 그대로 — 고친 뒤 모달 안만 다시 세움(화면은 닫을 때).
|
||||
const edits: PlanKindsControls = {
|
||||
projectId,
|
||||
onChanged: () => {
|
||||
@@ -181,9 +180,6 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo
|
||||
try {
|
||||
const response = await openDrawing(drawing);
|
||||
if (closed || picked !== drawing.id) return;
|
||||
const badge = el("span", "b07-plan-manage__badge", response.confirmed ? "확정" : "확정 전");
|
||||
badge.dataset.confirmed = String(response.confirmed);
|
||||
head.append(badge);
|
||||
head.append(button("b07-plan-tools__button", "CAD 에서 열기", () => finish(drawing.id)));
|
||||
preview.load(
|
||||
response.drawing,
|
||||
@@ -233,7 +229,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo
|
||||
if (first) void show(first.drawing, first.label);
|
||||
else markActive();
|
||||
});
|
||||
side.replaceChildren(form.root, sheetBlock(state), marksForm(state, edits));
|
||||
side.replaceChildren(form.root, sheetBlock(state));
|
||||
if (keep) void show(keep.drawing, keep.label);
|
||||
else {
|
||||
picked = undefined;
|
||||
|
||||
@@ -269,7 +269,6 @@
|
||||
}
|
||||
|
||||
.b07-plan-kind__head,
|
||||
.b07-plan-tools__mark,
|
||||
.b07-plan-tools__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -322,15 +321,6 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-plan-manage__badge {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b07-plan-manage__badge[data-confirmed="true"] {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
/* 「평면도 장」 한 블록(140-2) — 종류마다 한 줄 [≡] 이름 [1][2] · 줄 끌어 놓기 = 종류 순서 */
|
||||
.b07-plan-manage__row {
|
||||
padding: 2px var(--spacing-4);
|
||||
@@ -393,6 +383,24 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
}
|
||||
|
||||
.b07-plan-tools__fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b07-plan-tools__fields > [hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b07-plan-tools__divider {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
margin: var(--spacing-4) 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b07-plan-tools__pieces[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -401,40 +409,7 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b07-plan-tools__hint {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 측점 NO + 거리 두 칸(B05 stationFields) — 한 줄: [측점][NO][+m] */
|
||||
.b07-plan-tools__mark .b05-route__field {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
min-width: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b07-plan-tools__mark .b05-structure__station-pair {
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b07-plan-tools__mark .b05-structure__station-pair input {
|
||||
width: 52px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b07-plan-tools__mark input.is-invalid {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.b07-plan-tools__button,
|
||||
.b07-plan-tools__remove {
|
||||
.b07-plan-tools__button {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
@@ -443,10 +418,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b07-plan-tools__remove {
|
||||
padding: 0 var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 목록에서 연 도면 탭 줄 + CAD(82-4) */
|
||||
.b07-cad-stack {
|
||||
display: flex;
|
||||
|
||||
@@ -144,9 +144,6 @@ const STUBS = {
|
||||
currentLanguageIndex: 0,
|
||||
ui_locales: new Proxy({}, { get: (_, key) => [String(key)] }),
|
||||
},
|
||||
"../B05_Profile/B05_Profile_UI_Structures_Fields": {
|
||||
stationFields: () => ({ wrap: new FakeNode("div"), write: () => {}, read: () => 0 }),
|
||||
},
|
||||
};
|
||||
|
||||
// ── 서버 길 가짜 — 종류 파일 · 장 목록이 메모리에 ───────────────────────────────
|
||||
@@ -165,8 +162,6 @@ const state = {
|
||||
{ id: "k_terrain", name: "지형", pieces: ["terrain"], layout: "plan", scale: 1200 },
|
||||
{ id: "k_route", name: "노선배치도", pieces: ["route"], layout: "plan", scale: 1200 },
|
||||
],
|
||||
marks: [],
|
||||
station_interval_m: 20,
|
||||
};
|
||||
let confirmed = new Set(["plan_k_route"]);
|
||||
/** 종류 하나 = 장 하나 · 「지형」만 두 장. */
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"""130-1 B07 [평면도 관리] 모달.
|
||||
|
||||
못박는 것
|
||||
- 좌측 「평면도」 = 장 단추 + [평면도 관리] 하나 · 종류 폼 · 대피소 칸 · × 없음.
|
||||
- 단추 → 공용 모달(큰 창) · 안에 [종류 ▼] 폼 · 평면도 장 블록(종류마다 한 줄 · 작은 장 단추) · 대피소 · 사토장 자리 ·
|
||||
- 좌측 「평면도」 = 장 단추 + [평면도 관리] 하나 · 종류 폼 · × 없음.
|
||||
- 단추 → 공용 모달(큰 창) · 안에 [종류 ▼] 폼 · 평면도 장 블록(종류마다 한 줄 · 작은 장 단추) ·
|
||||
첫 장 미리보기(보기 전용).
|
||||
- 종류 추가 · 업데이트(축척 포함) · 지우기(폼 안) = 지금 서버 길 · 고칠 때마다 모달 안을 다시 받음.
|
||||
- 장 단추 → 드롭다운 따라감 · 드롭다운 → 그 종류 첫 장 · 줄 끌어 놓기 = 종류 순서(놓는 즉시 · 140-2).
|
||||
- 미리보기 장 바꾸기 · 캔버스만(bare) · 확정 배지만(확정 풀기 없음 140-1) · 닫으면 changed=True · [CAD 에서 열기] = 그 장 id.
|
||||
- 미리보기 장 바꾸기 · 캔버스만(bare) · 확정 배지 없음(524) · 닫으면 changed=True · [CAD 에서 열기] = 그 장 id.
|
||||
|
||||
- 522 처음 = 페이지 목록 · 미리받기 장 · 고친 뒤만 목록 새로 + 미리받기 맞춤 · 같은 목록 다시 열기 = 요청 없음 · iframe `?bare`.
|
||||
|
||||
@@ -52,7 +52,7 @@ def test_단추를_누르면_모달(out):
|
||||
assert opened["dialogClass"] == "b07-plan-manage"
|
||||
assert opened["kinds"] == ["k_terrain", "k_route"]
|
||||
assert opened["sheets"] == ["plan_k_terrain", "plan_k_terrain_2", "plan_k_route"]
|
||||
assert opened["hasKindForm"] and opened["hasMarks"]
|
||||
assert opened["hasKindForm"] and not opened["hasMarks"] # 524 대피소 · 사토장 자리 지움
|
||||
|
||||
|
||||
def test_첫_장_미리보기는_보기_전용(out):
|
||||
@@ -101,7 +101,7 @@ def test_미리보기_장_바꾸기(out):
|
||||
s2 = out["sheet2"]
|
||||
assert s2["preview"] == "plan_k_terrain_2"
|
||||
assert s2["active"] == ["plan_k_terrain_2"]
|
||||
assert "확정 전" in s2["head"] and "확정 풀기" not in s2["head"]
|
||||
assert "확정" not in s2["head"] # 524 배지 지움 · 풀기도 없음(140-1)
|
||||
assert s2["sheetTexts"] == ["1", "2"] # 여러 장 = 작은 장 단추
|
||||
assert s2["dropdown"] == "k_terrain" and s2["activeRow"] == ["k_terrain"]
|
||||
assert out["routeDropdown"] == "k_route"
|
||||
@@ -119,10 +119,9 @@ def test_줄_끌어_놓기는_종류_순서(out):
|
||||
assert drag["sheets"] == ["plan_k_route", "plan_k_terrain", "plan_k_terrain_2"]
|
||||
|
||||
|
||||
def test_확정이면_배지만(out):
|
||||
# 140-1 확정 풀기는 메인 [수정] — 모달엔 배지 · [CAD 에서 열기]만
|
||||
assert "확정" in out["routeHead"] and "CAD 에서 열기" in out["routeHead"]
|
||||
assert "확정 풀기" not in out["routeHead"]
|
||||
def test_확정이어도_배지_없음(out):
|
||||
# 524 배지 지움 — 모달 머리엔 장 이름 · [CAD 에서 열기]만
|
||||
assert "확정" not in out["routeHead"] and "CAD 에서 열기" in out["routeHead"]
|
||||
|
||||
|
||||
def test_닫으면_좌측_다시_읽기_신호(out):
|
||||
|
||||
@@ -80,4 +80,4 @@ def test_목록_도면명_n_N장(tmp_path: Path):
|
||||
items = [
|
||||
i for i in plan_kind_items(tmp_path, {"stations": stations}) if i["plan_kind"] == "terrain"
|
||||
]
|
||||
assert [item["label"] for item in items] == [f"평면도(지형) {n}/3장" for n in (1, 2, 3)]
|
||||
assert [item["label"] for item in items] == [f"평면도(지형도) {n}/3장" for n in (1, 2, 3)]
|
||||
|
||||
@@ -19,7 +19,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import build_cross_drawing
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Development_Draw import (
|
||||
build_development_drawing,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import (
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongSheet import (
|
||||
build_longitudinal_drawing,
|
||||
longitudinal_chunks,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ sys.path.insert(0, str(ROOT))
|
||||
from B07_DesignDetail import B07_DesignDetail_Router_Support_Fingerprint as fp # noqa: E402
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import _metric_lines # noqa: E402
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import ( # noqa: E402
|
||||
_kinds_dir,
|
||||
list_kinds,
|
||||
update_kind,
|
||||
)
|
||||
@@ -46,6 +47,14 @@ def test_간격_배수_높이만(tmp_path):
|
||||
|
||||
|
||||
def _watershed(root: Path) -> dict:
|
||||
folder = _kinds_dir(root)
|
||||
if not folder.is_dir(): # 기본 종류는 지형도뿐(524-1) — 유역도 종류를 파일로 둠
|
||||
folder.mkdir(parents=True)
|
||||
for order, (name, layout) in enumerate((("지형도", "plan"), ("배수유역도", "watershed"))):
|
||||
document = {"name": name, "pieces": ["contour"], "order": order, "layout": layout}
|
||||
(folder / f"{'terrain' if layout == 'plan' else 'watershed'}.json").write_text(
|
||||
json.dumps(document), "utf-8"
|
||||
)
|
||||
return next(kind for kind in list_kinds(root) if kind["layout"] == "watershed")
|
||||
|
||||
|
||||
@@ -88,9 +97,9 @@ def test_지문_유역도만(tmp_path, monkeypatch):
|
||||
def changed(before: dict, after: dict) -> set:
|
||||
return {key for key in before if before[key] != after[key]}
|
||||
|
||||
first = prints()
|
||||
kind = _watershed(root)
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 5) # 기본 종류를 파일로 내림 · 같은 값
|
||||
first = prints()
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 5) # 같은 값
|
||||
assert changed(first, prints()) == set()
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 10)
|
||||
ten = prints()
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
"""B07 종단도 — 장마다 그림 좌하단 = 작도영역 좌하단 · 고정 축척 · 넘치면 장을 더 나눔 (59-3).
|
||||
"""B07 종단도 — 장마다 그림 좌하단 = 작도영역 좌하단 + 50 mm · 고정 축척 · 넘치면 장 더 나눔 (59-3).
|
||||
|
||||
장 = 일반측점 30 간격(600 m) · 추가 · MC 측점은 안 셈 · Y축 ↔ 첫 측점 10 mm(525-1).
|
||||
|
||||
우측 상단 평면 위치도(521-2) — 칸 자리 · 그래프와 안 겹침 · 장마다 강조 구간 · 척도.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
from uuid import uuid5
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import FRAME_LAYER_ID
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import (
|
||||
build_longitudinal_drawing,
|
||||
longitudinal_chunks,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import _ENTITY_NS, FRAME_LAYER_ID
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import (
|
||||
LONG_PLAN_LAYER_ID,
|
||||
plan_box_overlaps,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongSheet import (
|
||||
build_longitudinal_drawing,
|
||||
longitudinal_chunks,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
drawing_scales_path,
|
||||
entities_bbox,
|
||||
frame_guide_area,
|
||||
usable_area,
|
||||
use_project_templates,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
|
||||
LONG_SHEET_RULE,
|
||||
_read_manifest,
|
||||
_write_manifest,
|
||||
release_old_long_sheets,
|
||||
@@ -121,13 +126,14 @@ def test_평면_칸_작도영역_우상단_안_겹침_장마다_강조() -> None
|
||||
usable_w, usable_h = usable_area()
|
||||
lows = []
|
||||
for number, sheet in enumerate(sheets, start=1):
|
||||
x0, y0 = sheet["bbox"][0], sheet["bbox"][1]
|
||||
# 작도 영역 좌하단 = 그림 좌하단 − 50 mm(525-1)
|
||||
x0, y0 = sheet["bbox"][0] - 50.0, sheet["bbox"][1] - 50.0
|
||||
box = entities_bbox(sheet["plan"])
|
||||
# 칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm
|
||||
assert box == pytest.approx(
|
||||
(x0 + usable_w - 155.0, y0 + usable_h - 105.0, x0 + usable_w - 5.0, y0 + usable_h - 5.0)
|
||||
)
|
||||
assert not plan_box_overlaps(longitudinal, sheet["graph"], sheet["bbox"])
|
||||
assert not plan_box_overlaps(longitudinal, sheet["graph"], (x0, y0, *sheet["bbox"][2:]))
|
||||
assert f"{number}/{len(sheets)} 장" in sheet["texts"]
|
||||
assert any(text.startswith("평면도 S=1/") for text in sheet["texts"])
|
||||
assert sheet["bold"] # 이 장 구간 굵게
|
||||
@@ -142,7 +148,8 @@ def test_칸을_덮으면_측점_줄여_장_더_나눔() -> None:
|
||||
with_box = _with_xy(_longitudinal(60, 25.0, 0.1))
|
||||
assert longitudinal_chunks(with_box)[0]["end_m"] < plain[0]["end_m"]
|
||||
for sheet in _plan_sheets(with_box):
|
||||
assert not plan_box_overlaps(with_box, sheet["graph"], sheet["bbox"])
|
||||
x0, y0, x1, y1 = sheet["bbox"]
|
||||
assert not plan_box_overlaps(with_box, sheet["graph"], (x0 - 50.0, y0 - 50.0, x1, y1))
|
||||
|
||||
|
||||
def test_좌표_없으면_칸_없음() -> None:
|
||||
@@ -168,32 +175,77 @@ def test_척도_정하면_이_장_확대_밖_흐린_파선(tmp_path) -> None:
|
||||
assert (min(xs) + max(xs)) / 2.0 == pytest.approx((box[0] + box[2]) / 2.0, abs=1.0)
|
||||
|
||||
|
||||
def test_칸_앞_확정_종단만_한_번_풂(tmp_path) -> None:
|
||||
def test_장_규칙_앞_확정_종단만_한_번_풂(tmp_path) -> None:
|
||||
drawings = tmp_path / "B07_DesignDetail" / "drawings"
|
||||
drawings.mkdir(parents=True)
|
||||
old = {"kind": "longitudinal", "confirmed": True, "file": "x"}
|
||||
_write_manifest(
|
||||
tmp_path,
|
||||
{
|
||||
"long_sheet_rule": LONG_SHEET_RULE - 1, # 옛 규칙(521-2 칸까지)
|
||||
"drawings": {
|
||||
"longitudinal_1": dict(old),
|
||||
"longitudinal_2": dict(old),
|
||||
"cross_00020m": {"kind": "cross", "confirmed": True, "file": "x"},
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
for name in ("longitudinal_1", "longitudinal_2", "cross_00020m"):
|
||||
(drawings / f"{name}.json").write_text('{"layers": []}', encoding="utf-8")
|
||||
release_old_long_sheets(tmp_path)
|
||||
after = _read_manifest(tmp_path)["drawings"]
|
||||
manifest = _read_manifest(tmp_path)
|
||||
after = manifest["drawings"]
|
||||
assert not after["longitudinal_1"]["confirmed"] and not after["longitudinal_2"]["confirmed"]
|
||||
assert after["cross_00020m"]["confirmed"]
|
||||
assert not (drawings / "longitudinal_1.json").exists() # 옛 저장본 걷힘
|
||||
# 새 확정본(칸 층 있음)은 그대로
|
||||
assert manifest["long_sheet_rule"] == LONG_SHEET_RULE
|
||||
# 새 규칙 뒤 확정본은 그대로
|
||||
after["longitudinal_1"] = dict(old)
|
||||
_write_manifest(tmp_path, {"drawings": after})
|
||||
(drawings / "longitudinal_1.json").write_text(
|
||||
json.dumps({"layers": [{"id": LONG_PLAN_LAYER_ID}]}), encoding="utf-8"
|
||||
)
|
||||
_write_manifest(tmp_path, manifest)
|
||||
release_old_long_sheets(tmp_path)
|
||||
assert _read_manifest(tmp_path)["drawings"]["longitudinal_1"]["confirmed"]
|
||||
|
||||
|
||||
# ── 일반측점 30 간격 = 600 m · 축 여유 10 mm · 그림 자리 +50 mm(525-1 · 요구 343-3) ─────────
|
||||
def _with_extras(count: int) -> dict:
|
||||
"""20 m 일반측점 + 추가(irregular) · MC(curve) 측점을 섞은 종단."""
|
||||
longitudinal = _longitudinal(count, 20.0, 0.0)
|
||||
extras = [{"chainage_m": x + 7.0, "kind": "irregular"} for x in range(0, count * 20 - 20, 60)]
|
||||
extras += [{"chainage_m": x + 13.0, "kind": "curve"} for x in range(0, count * 20 - 20, 100)]
|
||||
longitudinal["stations"] = sorted(
|
||||
longitudinal["stations"] + extras, key=lambda s: s["chainage_m"]
|
||||
)
|
||||
return longitudinal
|
||||
|
||||
|
||||
def test_추가_MC_측점은_안_세고_600m_로_나눔() -> None:
|
||||
longitudinal = _with_extras(70) # 0 ~ 1,380 m
|
||||
chunks = longitudinal_chunks(longitudinal)
|
||||
assert [(c["start_m"], c["end_m"]) for c in chunks] == [
|
||||
(0.0, 600.0),
|
||||
(600.0, 1200.0),
|
||||
(1200.0, 1380.0),
|
||||
]
|
||||
regular = [s["chainage_m"] for s in longitudinal["stations"] if "kind" not in s]
|
||||
for chunk in chunks[:-1]:
|
||||
inside = [x for x in regular if chunk["start_m"] < x <= chunk["end_m"]]
|
||||
assert len(inside) == 30 # 경계 측점은 앞 장과 나눠 가짐
|
||||
|
||||
|
||||
def test_Y축_첫_측점_사이_10mm_그림_자리_50mm() -> None:
|
||||
longitudinal = _longitudinal(40, 20.0, 0.05)
|
||||
chunk = longitudinal_chunks(longitudinal)[0]
|
||||
entities = build_longitudinal_drawing(longitudinal, "t", chunk)["entities"]
|
||||
by_id = {e["id"]: e for e in entities}
|
||||
|
||||
def start_x(seed: str) -> float:
|
||||
return by_id[str(uuid5(_ENTITY_NS, seed))]["shapeData"]["startPoint"]["x"]
|
||||
|
||||
assert start_x("t:lgv0") - start_x("t:lgy") == pytest.approx(10.0)
|
||||
assert start_x("t:ltgrid:v-header") == pytest.approx(start_x("t:lgy"))
|
||||
content = entities_bbox(
|
||||
[e for e in entities if e["layerId"] not in (FRAME_LAYER_ID, LONG_PLAN_LAYER_ID)]
|
||||
)
|
||||
area = frame_guide_area(entities)
|
||||
assert content[0] - area[0] == pytest.approx(50.0)
|
||||
assert content[1] - area[1] == pytest.approx(50.0)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""B07 계획평면도 종류 · 정보 조각 (PLAN 60장).
|
||||
|
||||
① 측점 둘에서 성토사면 외곽선 폭 = 횡단 설계 값(노견 끝 · 설계선이 지반에 닿는 자리)
|
||||
② 종류 더하기 · 지우기(기본 넷 포함) · 다시 읽어 같음
|
||||
③ 대피소 · 사토장 자리 — 한 자리에 둘 다
|
||||
② 종류 더하기 · 지우기(기본 종류 포함) · 다시 읽어 같음 · 기본 = 지형도 하나
|
||||
③ (없앰 — 대피소 · 사토장 자리는 524-1 에서 지움)
|
||||
④ 고른 조각만 도면 층으로 얹힘
|
||||
⑤ 조각 잘게(67장) — 노견선 · 성토 외곽선 · 절토 외곽선 따로 · 기본 넷 처음 체크 = 옛 그림 ·
|
||||
옛 종류 파일의 slope_outline 펼침 · 종류 업데이트(기본 넷 포함) · 확정본 풀림 · 배수시설 기호
|
||||
⑤ 조각 잘게(67장) — 노견선 · 성토 외곽선 · 절토 외곽선 따로 · 기본 처음 체크 = 옛 그림 ·
|
||||
옛 종류 파일의 slope_outline 펼침 · 종류 업데이트(기본 포함) · 확정본 풀림 · 배수시설 기호
|
||||
⑥ IP 표(94장) — 예시 검산 · 도-분-초 올림 · 장마다 그 장 IP 한 번 · 표 밑 선 자름 · 층
|
||||
⑦ 용지도 종류(93장) — 기본 종류 · 옛 종류 폴더에 한 번 더함(지우면 안 살아남) · 용지 조각 층
|
||||
⑧ 배수유역도 종류(109장) — 양식 watershed · 한 장 목록 · 지우기 · 다시 만들기 · IP 표 두 줄 차례 ·
|
||||
⑦ 용지도 조각(93장) — 용지 조각 층
|
||||
⑧ 배수유역도 종류(109장) — 새 종류로 더함 · 양식 watershed · 한 장 목록 · 지우기 · 다시 만들기 · IP 표 두 줄 차례 ·
|
||||
용지도 상자 안 선 0
|
||||
⑨ 종류 차례(140-2) — 끌어 놓은 차례 그대로 · 다시 읽어 같음 · 앞선 종류가 바뀐 종류만 확정 풂 · 목록 어긋나면 거절
|
||||
"""
|
||||
@@ -38,16 +38,13 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import drawing_to_dxf
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest, _write_manifest
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
_kinds_dir,
|
||||
_refuge_items,
|
||||
add_kind,
|
||||
delete_kind,
|
||||
list_kinds,
|
||||
plan_kind_items,
|
||||
read_marks,
|
||||
reorder_kinds,
|
||||
slope_records,
|
||||
update_kind,
|
||||
write_marks,
|
||||
)
|
||||
|
||||
|
||||
@@ -139,54 +136,23 @@ def test_outline_width_equals_cross_design_at_two_stations(tmp_path: Path) -> No
|
||||
|
||||
def test_kinds_add_delete_persist(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
assert [k["name"] for k in list_kinds(root)] == [
|
||||
"지형",
|
||||
"노선배치도",
|
||||
"대피소",
|
||||
"라이다",
|
||||
"용지도",
|
||||
"배수유역도",
|
||||
]
|
||||
assert [k["name"] for k in list_kinds(root)] == ["지형도"] # 기본 = 하나
|
||||
assert [k["id"] for k in DEFAULT_KINDS] == ["terrain"]
|
||||
assert "refuge" not in DEFAULT_KINDS[0]["pieces"]
|
||||
added = add_kind(root, "내 평면도", ["centerline", "station", "nope"])
|
||||
assert added["pieces"] == ["centerline", "station"]
|
||||
with pytest.raises(ValueError):
|
||||
add_kind(root, "내 평면도", ["centerline"]) # 같은 이름
|
||||
with pytest.raises(ValueError):
|
||||
add_kind(root, "빈 것", [])
|
||||
delete_kind(root, "terrain") # 기본 넷도 지움
|
||||
delete_kind(root, "terrain") # 기본 종류도 지움
|
||||
again = list_kinds(root) # 다시 읽어 같음
|
||||
assert [k["name"] for k in again] == [
|
||||
"노선배치도",
|
||||
"대피소",
|
||||
"라이다",
|
||||
"용지도",
|
||||
"배수유역도",
|
||||
"내 평면도",
|
||||
]
|
||||
assert [k["name"] for k in again] == ["내 평면도"]
|
||||
assert again[-1]["pieces"] == ["centerline", "station"]
|
||||
with pytest.raises(FileNotFoundError):
|
||||
delete_kind(root, "terrain")
|
||||
for kind in list(again):
|
||||
delete_kind(root, kind["id"])
|
||||
assert list_kinds(root) == [] # 다 지우면 비어 있음(기본 넷이 되살아나지 않음)
|
||||
assert {k["id"] for k in DEFAULT_KINDS} == {
|
||||
"terrain",
|
||||
"route",
|
||||
"layout",
|
||||
"lidar",
|
||||
"landuse",
|
||||
"watershed",
|
||||
}
|
||||
|
||||
|
||||
def test_refuge_marks_same_spot(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
write_marks(root, [{"chainage_m": 20, "refuge": True, "spoil": True}, {"chainage_m": 5}])
|
||||
assert read_marks(root) == [{"chainage_m": 20.0, "refuge": True, "spoil": True}]
|
||||
route = [(0.0, 0.0, 0.0), (40.0, 0.0, 40.0)]
|
||||
items = _refuge_items(root, route, 0.0, 40.0)["items"]
|
||||
assert [item["label"] for item in items] == ["대피소 · 사토장"]
|
||||
assert items[0]["y"] == pytest.approx(20.0)
|
||||
delete_kind(root, again[0]["id"])
|
||||
assert list_kinds(root) == [] # 다 지우면 비어 있음(기본이 되살아나지 않음)
|
||||
|
||||
|
||||
def test_drawing_has_only_chosen_pieces() -> None:
|
||||
@@ -223,23 +189,23 @@ def test_default_kinds_keep_old_picture_and_legacy_file(tmp_path: Path) -> None:
|
||||
outline = {"road_edge", "fill_outline", "cut_outline"}
|
||||
# 옛 slope_outline 자리를 나눈 셋이 그대로 채움 · 배수시설은 옛 그림에 없음(용지도는 따로)
|
||||
for kind in DEFAULT_KINDS:
|
||||
if kind["id"] not in ("landuse", "watershed"):
|
||||
assert outline <= set(kind["pieces"]) and "drainage" not in kind["pieces"]
|
||||
assert outline <= set(kind["pieces"]) and "drainage" not in kind["pieces"]
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "old.json").write_text(
|
||||
json.dumps({"name": "옛것", "pieces": ["slope_outline", "station"], "order": 0}), "utf-8"
|
||||
)
|
||||
kind = next(k for k in list_kinds(root) if k["id"] == "old") # 용지도는 이관으로 더해짐
|
||||
kind = next(k for k in list_kinds(root) if k["id"] == "old")
|
||||
assert set(kind["pieces"]) == outline | {"station"}
|
||||
|
||||
|
||||
def test_update_kind_default_and_release_confirmed(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
route = add_kind(root, "노선배치도", ["centerline"])["id"]
|
||||
manifest = _read_manifest(root)
|
||||
manifest["drawings"] = {
|
||||
"plan_terrain": {"confirmed": True},
|
||||
"plan_route": {"confirmed": True},
|
||||
f"plan_{route}": {"confirmed": True},
|
||||
}
|
||||
_write_manifest(root, manifest)
|
||||
update_kind(root, "terrain", "지형2", ["contour", "centerline"]) # 기본 종류 고침
|
||||
@@ -254,7 +220,7 @@ def test_update_kind_default_and_release_confirmed(tmp_path: Path) -> None:
|
||||
assert [k["id"] for k in list_kinds(root)][0] == "terrain" # 차례 그대로
|
||||
drawings = _read_manifest(root)["drawings"]
|
||||
assert drawings["plan_terrain"]["confirmed"] is False
|
||||
assert drawings["plan_route"]["confirmed"] is True
|
||||
assert drawings[f"plan_{route}"]["confirmed"] is True
|
||||
with pytest.raises(ValueError):
|
||||
update_kind(root, "terrain", "노선배치도", ["contour"]) # 다른 종류와 같은 이름
|
||||
with pytest.raises(ValueError):
|
||||
@@ -320,7 +286,6 @@ def test_ip_table_drawing_clips_lines_under_table(tmp_path: Path) -> None:
|
||||
assert ip["cells"][2][2]["text"] == "100.00000" # Y = 동(x)
|
||||
assert ip["cells"][3][1]["text"] == "90-00-00.00"
|
||||
assert {layer["id"] for layer in drawing["layers"]} >= {layer_id("ip_table")}
|
||||
assert "ip_table" in next(k for k in DEFAULT_KINDS if k["id"] == "route")["pieces"]
|
||||
# DXF — 표 → 선 + 글(IP 묶음: 바깥 4 · 세로 10 · 가로 20 · 글 17)
|
||||
raw, skipped = drawing_to_dxf({"entities": [mine[1]], "layers": drawing["layers"]})
|
||||
(tmp_path / "ip.dxf").write_bytes(raw)
|
||||
@@ -329,22 +294,6 @@ def test_ip_table_drawing_clips_lines_under_table(tmp_path: Path) -> None:
|
||||
assert {"90-00-00.00", "15.00000"} <= {t.dxf.text for t in space.query("TEXT")}
|
||||
|
||||
|
||||
def test_landuse_kind_added_once_to_old_folder(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
for order, kind in enumerate(DEFAULT_KINDS[:4]): # 93장 앞에 내려 둔 기본 넷
|
||||
(folder / f"{kind['id']}.json").write_text(
|
||||
json.dumps({"name": kind["name"], "pieces": kind["pieces"], "order": order}), "utf-8"
|
||||
)
|
||||
kinds = list_kinds(root)
|
||||
assert [k["id"] for k in kinds][-2:] == ["landuse", "watershed"] # 늘어난 기본 종류 둘
|
||||
assert kinds[-2]["pieces"] == ["contour", "centerline", "parcel", "parcel_info", "emd", "sgg"]
|
||||
assert len(list_kinds(root)) == 6 # 두 번 읽어도 한 번
|
||||
delete_kind(root, "landuse")
|
||||
assert "landuse" not in [k["id"] for k in list_kinds(root)] # 지운 것은 안 살아남
|
||||
|
||||
|
||||
def test_landuse_pieces_become_layers() -> None:
|
||||
pieces = {
|
||||
"parcel": {"lines": [[[0.0, 0.0], [10.0, 0.0]]]},
|
||||
@@ -370,11 +319,11 @@ def test_landuse_pieces_become_layers() -> None:
|
||||
|
||||
|
||||
def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
|
||||
"""배수유역도 = 기본 종류 · 양식 watershed · 조각 없음 · 장이 여럿이어도 목록은 한 장."""
|
||||
"""배수유역도 = 새 종류 · 양식 watershed · 조각 없음 · 장이 여럿이어도 목록은 한 장."""
|
||||
root = tmp_path / "project"
|
||||
watershed = next(k for k in list_kinds(root) if k["id"] == "watershed")
|
||||
assert watershed == {
|
||||
"id": "watershed",
|
||||
added = add_kind(root, "배수유역도", [], "watershed")
|
||||
assert next(k for k in list_kinds(root) if k["id"] == added["id"]) == {
|
||||
"id": added["id"],
|
||||
"name": "배수유역도",
|
||||
"pieces": [],
|
||||
"layout": "watershed",
|
||||
@@ -387,13 +336,13 @@ def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
|
||||
]
|
||||
items = plan_kind_items(root, {"stations": stations})
|
||||
terrain = [item for item in items if item["plan_kind"] == "terrain"]
|
||||
mine = [item for item in items if item["plan_kind"] == "watershed"]
|
||||
mine = [item for item in items if item["plan_kind"] == added["id"]]
|
||||
assert len(terrain) > 1
|
||||
assert mine == [
|
||||
{
|
||||
"id": "plan_watershed",
|
||||
"id": f"plan_{added['id']}",
|
||||
"label": "배수유역도",
|
||||
"plan_kind": "watershed",
|
||||
"plan_kind": added["id"],
|
||||
"plan_kind_name": "배수유역도",
|
||||
}
|
||||
]
|
||||
@@ -402,9 +351,10 @@ def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
|
||||
def test_watershed_kind_delete_and_add_again(tmp_path: Path) -> None:
|
||||
"""× 로 지우면 안 살아남 · 「새 종류」 양식 유역도로 다시 만듦 · 업데이트는 양식 그대로."""
|
||||
root = tmp_path / "project"
|
||||
delete_kind(root, "watershed")
|
||||
assert "watershed" not in [k["id"] for k in list_kinds(root)]
|
||||
assert "watershed" not in [k["id"] for k in list_kinds(root)] # 다시 읽어도 안 살아남
|
||||
first = add_kind(root, "배수유역도", [], "watershed")
|
||||
delete_kind(root, first["id"])
|
||||
assert first["id"] not in [k["id"] for k in list_kinds(root)]
|
||||
assert first["id"] not in [k["id"] for k in list_kinds(root)] # 다시 읽어도 안 살아남
|
||||
added = add_kind(root, "배수유역도", ["contour"], "watershed") # 조각은 버림
|
||||
assert added["layout"] == "watershed" and added["pieces"] == []
|
||||
again = next(k for k in list_kinds(root) if k["id"] == added["id"])
|
||||
@@ -418,21 +368,6 @@ def test_watershed_kind_delete_and_add_again(tmp_path: Path) -> None:
|
||||
add_kind(root, "이상한 양식", ["contour"], "nope")
|
||||
|
||||
|
||||
def test_watershed_kind_added_once_to_folder_with_landuse(tmp_path: Path) -> None:
|
||||
"""용지도 표시가 있는 옛 폴더 — 배수유역도만 한 번 더해짐 · 지우면 안 살아남."""
|
||||
root = tmp_path / "project"
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
for order, kind in enumerate(DEFAULT_KINDS[:5]):
|
||||
(folder / f"{kind['id']}.json").write_text(
|
||||
json.dumps({"name": kind["name"], "pieces": kind["pieces"], "order": order}), "utf-8"
|
||||
)
|
||||
(folder / "_landuse_added").touch()
|
||||
assert [k["id"] for k in list_kinds(root)][-1] == "watershed"
|
||||
delete_kind(root, "watershed")
|
||||
assert [k["id"] for k in list_kinds(root)][-1] == "landuse"
|
||||
|
||||
|
||||
def test_ip_table_two_lines_left_to_right_top_to_bottom() -> None:
|
||||
"""한 줄 셋까지 들어가는 폭 · 묶음 넷 — 첫 줄(1 · 2 · 3) 맨 위 왼 → 오 · 넷째는 아랫줄 왼쪽."""
|
||||
blocks = [{"name": str(i), "ia": 1.0} for i in range(1, 5)]
|
||||
@@ -449,22 +384,23 @@ def test_ip_table_two_lines_left_to_right_top_to_bottom() -> None:
|
||||
|
||||
def test_reorder_kinds_order_and_release_renumbered(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
second, third = (add_kind(root, name, ["centerline"])["id"] for name in ("둘째", "셋째"))
|
||||
manifest = _read_manifest(root)
|
||||
manifest["drawings"] = {
|
||||
"plan_terrain": {"confirmed": True},
|
||||
"plan_route": {"confirmed": True},
|
||||
"plan_layout": {"confirmed": True},
|
||||
f"plan_{second}": {"confirmed": True},
|
||||
f"plan_{third}": {"confirmed": True},
|
||||
}
|
||||
_write_manifest(root, manifest)
|
||||
before = [k["id"] for k in list_kinds(root)]
|
||||
assert before[:3] == ["terrain", "route", "layout"]
|
||||
after = ["route", "terrain", *before[2:]] # 앞 둘만 바꿈
|
||||
assert before == ["terrain", second, third]
|
||||
after = [second, "terrain", third] # 앞 둘만 바꿈
|
||||
reorder_kinds(root, after)
|
||||
assert [k["id"] for k in list_kinds(root)] == after # 다시 읽어 같음
|
||||
drawings = _read_manifest(root)["drawings"]
|
||||
assert drawings["plan_terrain"]["confirmed"] is False # 번호 바뀜
|
||||
assert drawings["plan_route"]["confirmed"] is False
|
||||
assert drawings["plan_layout"]["confirmed"] is True # 앞선 종류 묶음 그대로 = 번호 그대로
|
||||
assert drawings[f"plan_{second}"]["confirmed"] is False
|
||||
assert drawings[f"plan_{third}"]["confirmed"] is True # 앞선 종류 묶음 그대로 = 번호 그대로
|
||||
with pytest.raises(ValueError):
|
||||
reorder_kinds(root, after[:-1]) # 빠진 종류
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
Reference in New Issue
Block a user