From 02bd45bf3c7c76ff00414e4bc0e2e8ebc644f2de Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 4 Sep 2026 19:09:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(B07):=20=ED=91=9C=EC=A4=80=20=ED=9A=A1?= =?UTF-8?q?=EB=8B=A8=EB=A9=B4=EB=8F=84=20=E2=80=94=20=EC=B9=98=EC=88=98?= =?UTF-8?q?=C2=B7=EC=B8=A1=EA=B5=AC=20=ED=99=95=EB=8C=80=EB=8F=84=C2=B7?= =?UTF-8?q?=EC=95=94=EB=B0=98=EC=84=A0=202=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 빈 도각이던 표준 횡단면도를 실제 도면으로 만듦. - 본 그림: B06 좌측 패널 모식도와 같은 배치를 실치수로 그림(1/50). 노폭·노견· 측구 상단폭·노면 전폭에 치수선(눈금+치수값)을 붙이고, 절토·성토 경사비와 횡단경사를 표기. 값은 B06 표준 횡단면 설정을 그대로 읽고 없으면 config 기본값. - 측구 부분 확대도(1/10): 상단폭·저폭·깊이 치수 + 확대 축척 표기. - 암반 2단 절토: 아래는 암반 경사(1:0.4), 위는 토사 경사(1:1), 갈리는 높이에 암반선을 파선으로 긋고 각도를 함께 적음. - 암 L형 측구·포장 횡단경사는 주기(※)로 적음 — 본 그림은 토사 기준. 곁들여: Router_Support 가 700줄을 넘어 재수출 import 를 한 문장으로 합쳐 681줄로 줄임(기능 변화 없음). 검증: 치수값이 STANDARD_CROSS_SECTION 과 일치(500·3000·500·900·4000), 확대도 900·300·300 + S=1/10, 2단 절토선 꺾임점 1개, 콘텐츠 358.2x172.6 mm ≤ A1 작도영역. Co-Authored-By: Claude Opus 5 (1M context) --- .../B07_DesignDetail_Api_Fetch.ts | 2 + .../B07_DesignDetail_Engine_Cad_Standard.py | 581 ++++++++++++++++++ B07_DesignDetail/B07_DesignDetail_Router.py | 17 + .../B07_DesignDetail_Router_Support.py | 97 ++- B07_DesignDetail/B07_DesignDetail_Schema.py | 2 + .../B07_DesignDetail_UI_Panels.ts | 2 +- config/config_system.py | 4 + 7 files changed, 646 insertions(+), 59 deletions(-) create mode 100644 B07_DesignDetail/B07_DesignDetail_Engine_Cad_Standard.py diff --git a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts index b51b8f63..e842eae7 100644 --- a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts +++ b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts @@ -14,6 +14,7 @@ export interface DesignDrawingItem { | "plan" | "landuse" | "plan_lidar" + | "cross_standard" | "blank"; label: string; chainage_m: number | null; @@ -101,6 +102,7 @@ export interface DesignDrawingResponse { | "plan" | "landuse" | "plan_lidar" + | "cross_standard" | "blank"; label: string; drawing: CadDrawing; diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Standard.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Standard.py new file mode 100644 index 00000000..ce03210c --- /dev/null +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Standard.py @@ -0,0 +1,581 @@ +"""B07 표준 횡단면도 CAD 조립 — 변수 모식도에 치수를 넣고, 측구 확대도·암반선 2단을 함께 낸다. + +사용자 지시(2026-09-04) — 「표준 횡단면도 좌상단에 기본값으로 B06 좌측 패널의 변수 위치 +안내와 비슷한 그림을 넣고, 변수 이름 자리에 도면처럼 치수를 적을 것. 측구는 작으니 부분 +확대도로. 발파·암반이면 암반선과 각도를 넣어 절토측이 2단(토사 각도 + 암반 각도)으로 +표현될 것」. + +배치는 B06 좌측 패널 모식도(`B06_Section_UI_Standard_Diagram.ts`)와 같다 — 좌가 절토·측구, +우가 성토, 가운데가 계획고. 다른 점은 **실치수**라는 것이다. 모식도는 위치 안내라 비율이 +없지만 도면은 축척(본 그림 1/50, 측구 확대도 1/10)대로 그리고 치수선을 붙인다. + +값은 B06 「표준 횡단면 설정」(`standard_cross_section`)을 그대로 읽는다 — 여기서 기하를 +다시 정하지 않는다. 저장값이 없으면 config 기본값을 쓴다. + +좌표 규약: 종이 mm = 실거리 m x MM. 원점은 계획고(노면 중심). +""" + +import math +from typing import Any + +from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( + DRAWING_FORMAT, + FRAME_LAYER_ID, + TABLE_LABEL_COLOR, + _layer, + _text_entity, + polyline_entity, +) +from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( + entities_bbox, + frame_entities, + scale_fields, +) +from config.config_system import ( + DRAWING_SCALE_CROSS_STANDARD, + DRAWING_SCALE_DITCH_DETAIL, + STANDARD_CROSS_SECTION, +) + +STANDARD_KIND = "cross_standard" +STANDARD_LABEL = "표준 횡단면도" + +# 도면 좌표 = 종이 mm. 본 그림 1/50 -> 1 m = 20 mm, 측구 확대도 1/10 -> 1 m = 100 mm. +MM = 1000.0 / DRAWING_SCALE_CROSS_STANDARD +DETAIL_MM = 1000.0 / DRAWING_SCALE_DITCH_DETAIL + +SECTION_LAYER_ID = "b07-std-section" +ROCK_LAYER_ID = "b07-std-rock" +DIM_LAYER_ID = "b07-std-dim" +DETAIL_LAYER_ID = "b07-std-detail" +NOTE_LAYER_ID = "b07-std-note" +TITLE_LAYER_ID = "b07-std-title" + +SECTION_COLOR = "#111111" +ROCK_COLOR = "#a63d3d" +DIM_COLOR = "#2f6fb0" +DETAIL_COLOR = "#111111" +NOTE_COLOR = "#333333" + +_LINE_WIDTH = 2 +_TITLE_FONT_SIZE = 7.0 +_LABEL_FONT_SIZE = 3.0 +_DIM_FONT_SIZE = 2.6 +_NOTE_FONT_SIZE = 2.8 + +# 그림에 세울 절·성토 높이(m) — 표준도는 실제 지형이 없으므로 대표 높이로 그린다. +_CUT_HEIGHT_M = 3.0 +_FILL_HEIGHT_M = 3.0 +# 암반 구간: 절토 밑에서 이만큼이 암반이고 그 위가 토사다(2단 절토). +_ROCK_HEIGHT_M = 1.5 + +_DIM_TICK_MM = 1.6 # 치수선 끝 눈금 반길이 +_DIM_OFFSET_MM = 8.0 # 치수선을 그림에서 띄우는 거리 +_DIM_GAP_MM = 6.0 # 치수선 단 사이 + + +def _ground(kind: str, standard: dict[str, Any] | None) -> dict[str, Any]: + """표준 횡단면 설정에서 한 지반유형 값을 꺼낸다. 없으면 config 기본값.""" + stored = (standard or {}).get(kind) + if isinstance(stored, dict) and stored: + merged = dict(STANDARD_CROSS_SECTION.get(kind) or {}) + merged.update(stored) + return merged + return dict(STANDARD_CROSS_SECTION.get(kind) or {}) + + +def _number(value: Any, fallback: float) -> float: + return float(value) if isinstance(value, (int, float)) else fallback + + +def _dim_entities( + drawing_id: str, + tag: str, + start: tuple[float, float], + end: tuple[float, float], + label: str, + layer_id: str = DIM_LAYER_ID, +) -> list[dict[str, Any]]: + """치수선 한 벌(치수선 + 양끝 눈금 + 치수값). 좌표는 종이 mm.""" + entities: list[dict[str, Any]] = [] + line = polyline_entity(drawing_id, [start, end], layer_id, DIM_COLOR, suffix=f":dim:{tag}") + if line: + entities.append(line) + dx, dy = end[0] - start[0], end[1] - start[1] + length = math.hypot(dx, dy) or 1.0 + nx, ny = -dy / length, dx / length + for index, point in enumerate((start, end)): + tick = polyline_entity( + drawing_id, + [ + (point[0] - nx * _DIM_TICK_MM, point[1] - ny * _DIM_TICK_MM), + (point[0] + nx * _DIM_TICK_MM, point[1] + ny * _DIM_TICK_MM), + ], + layer_id, + DIM_COLOR, + suffix=f":dim:{tag}:tick:{index}", + ) + if tick: + entities.append(tick) + entities.append( + _text_entity( + f"{drawing_id}:dim:{tag}:text", + label, + (start[0] + end[0]) / 2.0 + nx * 2.2, + (start[1] + end[1]) / 2.0 + ny * 2.2, + layer_id, + _DIM_FONT_SIZE, + DIM_COLOR, + ) + ) + return entities + + +def _section_geometry(values: dict[str, Any]) -> dict[str, Any]: + """표준 단면의 실좌표(m) 꼭짓점. 좌가 절토·측구, 우가 성토(B06 모식도와 같은 배치).""" + road = _number(values.get("road_width_m"), 3.0) + shoulder_left = _number(values.get("shoulder_left_m"), 0.5) + shoulder_right = _number(values.get("shoulder_right_m"), 0.5) + ditch = values.get("ditch") if isinstance(values.get("ditch"), dict) else {} + top_width = _number(ditch.get("top_width_m"), 0.9) + bottom_width = _number(ditch.get("bottom_width_m"), 0.3) + depth = _number(ditch.get("depth_m"), 0.3) + slope = values.get("cross_slope_pct") if isinstance(values.get("cross_slope_pct"), dict) else {} + cross_pct = _number(slope.get("max"), _number(slope.get("min"), 3.0)) + cut_ratio = _number(values.get("cut_slope_ratio"), 1.0) + fill_ratio = _number(values.get("fill_slope_ratio"), 1.2) + + road_left = -(road / 2.0 + shoulder_left) + road_right = road / 2.0 + shoulder_right + # 횡단경사는 측구(좌) 쪽으로 내려간다 — 노면 좌끝이 계획고보다 낮다. + drop = abs(road_left) * cross_pct / 100.0 + surface = [(road_right, 0.0), (road_left, -drop)] + + ditch_top_left = road_left - top_width + ditch_bottom_y = -drop - depth + inset = (top_width - bottom_width) / 2.0 + ditch_line = [ + (road_left, -drop), + (road_left - inset, ditch_bottom_y), + (ditch_top_left + inset, ditch_bottom_y), + (ditch_top_left, -drop), + ] + cut_top = (ditch_top_left - _CUT_HEIGHT_M * cut_ratio, -drop + _CUT_HEIGHT_M) + fill_toe = (road_right + _FILL_HEIGHT_M * fill_ratio, -_FILL_HEIGHT_M) + return { + "road": road, + "shoulder_left": shoulder_left, + "shoulder_right": shoulder_right, + "top_width": top_width, + "bottom_width": bottom_width, + "depth": depth, + "cross_pct": cross_pct, + "cut_ratio": cut_ratio, + "fill_ratio": fill_ratio, + "road_left": road_left, + "road_right": road_right, + "drop": drop, + "surface": surface, + "ditch_line": ditch_line, + "ditch_top_left": ditch_top_left, + "cut_start": (ditch_top_left, -drop), + "cut_top": cut_top, + "fill_toe": fill_toe, + } + + +def _rock_entities( + drawing_id: str, geometry: dict[str, Any], rock_ratio: float, paper: Any +) -> list[dict[str, Any]]: + """암반선과 2단 절토(아래=암반각, 위=토사각)를 절토측에 덧그린다.""" + entities: list[dict[str, Any]] = [] + start_x, start_y = geometry["cut_start"] + soil_ratio = geometry["cut_ratio"] + # 아래 단: 암반각으로 _ROCK_HEIGHT_M 만큼 올라간다. + bench = (start_x - _ROCK_HEIGHT_M * rock_ratio, start_y + _ROCK_HEIGHT_M) + # 위 단: 그 위는 토사각. + upper = ( + bench[0] - (_CUT_HEIGHT_M - _ROCK_HEIGHT_M) * soil_ratio, + bench[1] + (_CUT_HEIGHT_M - _ROCK_HEIGHT_M), + ) + two_stage = polyline_entity( + drawing_id, + [paper((start_x, start_y)), paper(bench), paper(upper)], + ROCK_LAYER_ID, + ROCK_COLOR, + suffix=":rock:cut", + width=_LINE_WIDTH, + ) + if two_stage: + entities.append(two_stage) + # 암반선 — 2단이 갈리는 높이의 수평 파선. + boundary = polyline_entity( + drawing_id, + [paper((bench[0] - 1.5, bench[1])), paper((geometry["road_right"], bench[1]))], + ROCK_LAYER_ID, + ROCK_COLOR, + suffix=":rock:boundary", + dash=[6, 4], + ) + if boundary: + entities.append(boundary) + label_x, label_y = paper((bench[0] - 1.6, bench[1])) + entities.append( + _text_entity( + f"{drawing_id}:rock:boundary:text", + "암반선", + label_x, + label_y + 2.5, + ROCK_LAYER_ID, + _LABEL_FONT_SIZE, + ROCK_COLOR, + align="right", + ) + ) + mid_lower = paper(((start_x + bench[0]) / 2.0, (start_y + bench[1]) / 2.0)) + mid_upper = paper(((bench[0] + upper[0]) / 2.0, (bench[1] + upper[1]) / 2.0)) + entities.append( + _text_entity( + f"{drawing_id}:rock:lower", + f"암반 1:{rock_ratio:g}", + mid_lower[0] - 6.0, + mid_lower[1], + ROCK_LAYER_ID, + _DIM_FONT_SIZE, + ROCK_COLOR, + align="right", + ) + ) + entities.append( + _text_entity( + f"{drawing_id}:rock:upper", + f"토사 1:{soil_ratio:g}", + mid_upper[0] - 6.0, + mid_upper[1], + ROCK_LAYER_ID, + _DIM_FONT_SIZE, + ROCK_COLOR, + align="right", + ) + ) + return entities + + +def _ditch_detail_entities( + drawing_id: str, geometry: dict[str, Any], origin: tuple[float, float] +) -> list[dict[str, Any]]: + """측구 부분 확대도(1/10) — 작아서 본 그림에서는 치수를 읽을 수 없다.""" + entities: list[dict[str, Any]] = [] + top_width = geometry["top_width"] + bottom_width = geometry["bottom_width"] + depth = geometry["depth"] + inset = (top_width - bottom_width) / 2.0 + ox, oy = origin + + def paper(point: tuple[float, float]) -> tuple[float, float]: + return (ox + point[0] * DETAIL_MM, oy + point[1] * DETAIL_MM) + + shape = [ + (0.0, 0.0), + (inset, -depth), + (inset + bottom_width, -depth), + (top_width, 0.0), + ] + outline = polyline_entity( + drawing_id, + [paper(point) for point in shape], + DETAIL_LAYER_ID, + DETAIL_COLOR, + suffix=":detail:ditch", + width=_LINE_WIDTH, + ) + if outline: + entities.append(outline) + entities.extend( + _dim_entities( + drawing_id, + "detail-top", + paper((0.0, 0.0 + 0.06)), + paper((top_width, 0.0 + 0.06)), + f"{top_width * 1000:.0f}", + DETAIL_LAYER_ID, + ) + ) + entities.extend( + _dim_entities( + drawing_id, + "detail-bottom", + paper((inset, -depth - 0.06)), + paper((inset + bottom_width, -depth - 0.06)), + f"{bottom_width * 1000:.0f}", + DETAIL_LAYER_ID, + ) + ) + entities.extend( + _dim_entities( + drawing_id, + "detail-depth", + paper((top_width + 0.08, 0.0)), + paper((top_width + 0.08, -depth)), + f"{depth * 1000:.0f}", + DETAIL_LAYER_ID, + ) + ) + title = paper((top_width / 2.0, 0.3)) + entities.append( + _text_entity( + f"{drawing_id}:detail:title", + f"측구 상세도 (S = 1/{DRAWING_SCALE_DITCH_DETAIL})", + title[0], + title[1], + DETAIL_LAYER_ID, + _LABEL_FONT_SIZE, + TABLE_LABEL_COLOR, + ) + ) + return entities + + +def build_standard_cross_drawing( + drawing_id: str, label: str, standard: dict[str, Any] | None = None +) -> dict[str, Any]: + """표준 횡단면도 한 장을 만든다 — 본 그림 + 치수 + 측구 확대도 + 암반 2단 + 주기.""" + soil = _ground("soil", standard) + rock = _ground("rock", standard) + paved = _ground("paved", standard) + geometry = _section_geometry(soil) + + def paper(point: tuple[float, float]) -> tuple[float, float]: + return (point[0] * MM, point[1] * MM) + + entities: list[dict[str, Any]] = [] + # 본 그림: 절토면 - 측구 - 노면 - 성토면을 한 줄로 잇는다. + outline = [ + geometry["cut_top"], + *geometry["ditch_line"][::-1], + *geometry["surface"][::-1], + geometry["fill_toe"], + ] + body = polyline_entity( + drawing_id, + [paper(point) for point in outline], + SECTION_LAYER_ID, + SECTION_COLOR, + suffix=":section", + width=_LINE_WIDTH, + ) + if body: + entities.append(body) + # 중심선(계획고). + center = polyline_entity( + drawing_id, + [paper((0.0, 1.2)), paper((0.0, -1.2))], + SECTION_LAYER_ID, + SECTION_COLOR, + suffix=":center", + dash=[10, 3, 2, 3], + ) + if center: + entities.append(center) + entities.append( + _text_entity( + f"{drawing_id}:center:text", + "계획고", + *paper((0.0, 1.45)), + SECTION_LAYER_ID, + _LABEL_FONT_SIZE, + SECTION_COLOR, + ) + ) + + # 치수선 — 노면 아래 두 단(위: 노견·노폭·노견, 아래: 노면 전폭). + base_y = min(geometry["fill_toe"][1], -geometry["drop"] - geometry["depth"]) + dim_y = base_y * MM - _DIM_OFFSET_MM + half = geometry["road"] / 2.0 + entities.extend( + _dim_entities( + drawing_id, + "shoulder-left", + (geometry["road_left"] * MM, dim_y), + (-half * MM, dim_y), + f"{geometry['shoulder_left'] * 1000:.0f}", + ) + ) + entities.extend( + _dim_entities( + drawing_id, + "road", + (-half * MM, dim_y), + (half * MM, dim_y), + f"{geometry['road'] * 1000:.0f}", + ) + ) + entities.extend( + _dim_entities( + drawing_id, + "shoulder-right", + (half * MM, dim_y), + (geometry["road_right"] * MM, dim_y), + f"{geometry['shoulder_right'] * 1000:.0f}", + ) + ) + entities.extend( + _dim_entities( + drawing_id, + "ditch-top", + (geometry["ditch_top_left"] * MM, dim_y), + (geometry["road_left"] * MM, dim_y), + f"{geometry['top_width'] * 1000:.0f}", + ) + ) + roadbed_width_m = geometry["road"] + geometry["shoulder_left"] + geometry["shoulder_right"] + entities.extend( + _dim_entities( + drawing_id, + "roadbed", + (geometry["road_left"] * MM, dim_y - _DIM_GAP_MM), + (geometry["road_right"] * MM, dim_y - _DIM_GAP_MM), + f"{roadbed_width_m * 1000:.0f}", + ) + ) + + # 경사·횡단경사 표기. + cut_mid = paper( + ( + (geometry["cut_start"][0] + geometry["cut_top"][0]) / 2.0, + (geometry["cut_start"][1] + geometry["cut_top"][1]) / 2.0, + ) + ) + fill_mid = paper( + ( + (geometry["road_right"] + geometry["fill_toe"][0]) / 2.0, + (0.0 + geometry["fill_toe"][1]) / 2.0, + ) + ) + entities.append( + _text_entity( + f"{drawing_id}:cut:text", + f"절토 1:{geometry['cut_ratio']:g}", + cut_mid[0] - 4.0, + cut_mid[1] + 3.0, + SECTION_LAYER_ID, + _DIM_FONT_SIZE, + SECTION_COLOR, + align="right", + ) + ) + entities.append( + _text_entity( + f"{drawing_id}:fill:text", + f"성토 1:{geometry['fill_ratio']:g}", + fill_mid[0] + 4.0, + fill_mid[1] + 3.0, + SECTION_LAYER_ID, + _DIM_FONT_SIZE, + SECTION_COLOR, + align="left", + ) + ) + entities.append( + _text_entity( + f"{drawing_id}:cross-slope:text", + f"횡단경사 {geometry['cross_pct']:g}%", + *paper((geometry["road_left"] / 2.0, 0.6)), + SECTION_LAYER_ID, + _DIM_FONT_SIZE, + SECTION_COLOR, + ) + ) + + # 암반 구간 2단 절토. + entities.extend( + _rock_entities(drawing_id, geometry, _number(rock.get("cut_slope_ratio"), 0.4), paper) + ) + + body_bbox = entities_bbox(entities) + right = body_bbox[2] if body_bbox else 0.0 + top = body_bbox[3] if body_bbox else 0.0 + + # 측구 부분 확대도 — 본 그림 오른쪽 위. + entities.extend(_ditch_detail_entities(drawing_id, geometry, (right + 28.0, top - 30.0))) + + # 주기: 구간별로 달라지는 값만 적는다(기본은 토사). + notes = [ + "※ 본 그림은 토사 구간 기준임.", + f"※ 암 구간 — 절토 1:{_number(rock.get('cut_slope_ratio'), 0.4):g}, " + f"L형 측구 {_number((rock.get('ditch_l_type') or {}).get('width_m'), 0.5) * 1000:.0f}" + f"×{_number((rock.get('ditch_l_type') or {}).get('depth_m'), 0.1) * 1000:.0f}mm " + "(횡단면도에서 일반·L형 중 선택).", + f"※ 포장 구간 — 절·성토 경사는 토사와 같고 횡단경사만 " + f"{_number((paved.get('cross_slope_pct') or {}).get('min'), 1.5):g}~" + f"{_number((paved.get('cross_slope_pct') or {}).get('max'), 2.0):g}% 임.", + "※ 치수 단위 mm.", + ] + note_bbox = entities_bbox(entities) + note_x = note_bbox[0] if note_bbox else 0.0 + note_y = (note_bbox[1] if note_bbox else 0.0) - 12.0 + for index, note in enumerate(notes): + entities.append( + _text_entity( + f"{drawing_id}:note:{index}", + note, + note_x, + note_y - index * 5.0, + NOTE_LAYER_ID, + _NOTE_FONT_SIZE, + NOTE_COLOR, + align="left", + ) + ) + + bbox = entities_bbox(entities) + if bbox: + min_bx, _min_by, max_bx, max_by = bbox + entities.append( + _text_entity( + f"{drawing_id}:title", + label, + (min_bx + max_bx) / 2.0, + max_by + 12.0, + TITLE_LAYER_ID, + _TITLE_FONT_SIZE, + TABLE_LABEL_COLOR, + ) + ) + entities.append( + _text_entity( + f"{drawing_id}:scale", + f"S = 1/{DRAWING_SCALE_CROSS_STANDARD}", + max_bx, + max_by + 5.0, + TITLE_LAYER_ID, + _DIM_FONT_SIZE, + TABLE_LABEL_COLOR, + align="right", + ) + ) + entities.extend( + frame_entities( + drawing_id, + entities_bbox(entities) or bbox, + fit=False, + fields={ + "도면명": label, + **scale_fields(("", DRAWING_SCALE_CROSS_STANDARD)), + }, + ) + ) + + return { + "format": DRAWING_FORMAT, + "entities": entities, + "layers": [ + _layer(SECTION_LAYER_ID, "표준 단면"), + _layer(ROCK_LAYER_ID, "암반선·2단 절토"), + _layer(DIM_LAYER_ID, "치수"), + _layer(DETAIL_LAYER_ID, "측구 상세도"), + _layer(NOTE_LAYER_ID, "주기"), + _layer(TITLE_LAYER_ID, "표제"), + _layer(FRAME_LAYER_ID, "도각", locked=True), + ], + } diff --git a/B07_DesignDetail/B07_DesignDetail_Router.py b/B07_DesignDetail/B07_DesignDetail_Router.py index 5cd8a010..f4bb7d50 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router.py +++ b/B07_DesignDetail/B07_DesignDetail_Router.py @@ -17,6 +17,7 @@ from B06_Section.B06_Section_Repository import ( get_cross_section_design, get_cross_section_designs, get_longitudinal_section, + get_project_standard_cross_section, merge_cross_section_design_by_round, ) from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import ( @@ -34,6 +35,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( use_title_fields, ) from B07_DesignDetail.B07_DesignDetail_Router_Support import ( + CROSS_STANDARD_ID, LANDUSE_ID, LIDAR_ID, MASS_HAUL_ID, @@ -307,6 +309,21 @@ async def get_design_drawing( if context is None: return JSONResponse(status_code=404, content={"status": "error", "message": reason}) source_design = await asyncio.to_thread(watershed_source, context) + elif drawing_id == CROSS_STANDARD_ID: + # 표준 횡단면도는 B06 「표준 횡단면 설정」 저장값으로 그린다(없으면 config 기본값). + pool = get_db_pool() + async with pool.acquire() as connection: + async with connection.cursor() as cursor: + await cursor.execute( + "SELECT company_id FROM projects WHERE id = %s AND deleted_at IS NULL", + (str(project_id),), + ) + row = await cursor.fetchone() + source_design = ( + await get_project_standard_cross_section(connection, int(row[0]), project_id) + if row + else None + ) elif LIDAR_ID.fullmatch(drawing_id): # 라이다 계획평면도는 확정 DTM 격자로 음영기복 그림을 만들어 넘긴다. context, reason = await load_drainage_context(project_id) diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index 93936c4f..76e1627c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -47,66 +47,36 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import ( plan_cross_sheets, section_block_size, ) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Standard import ( + STANDARD_LABEL, + build_standard_cross_drawing, +) from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import ( QUANTITY_VALUE_KEYS, ) from B07_DesignDetail.B07_DesignDetail_Engine_Template import add_title_fields -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - _BASIN_MAX_DISTANCE_M as _BASIN_MAX_DISTANCE_M, -) # 유역도 배경·파일 입출력 조각은 700줄 제한으로 떼어냈다(2026-09-04). # 여기서 그대로 다시 내보내 호출부(`B07_DesignDetail_Router.py`)의 import 경로는 불변이다. -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - CONTOUR_FILE as CONTOUR_FILE, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - LANDUSE_ID as LANDUSE_ID, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - LIDAR_ID as LIDAR_ID, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - PLAN_ID as PLAN_ID, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - STREAM_FILE as STREAM_FILE, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - _basins_crs as _basins_crs, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - _clip_segment as _clip_segment, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - _geojson_features as _geojson_features, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - _geojson_payload as _geojson_payload, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - _geometry_lines as _geometry_lines, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - _too_far_from_route as _too_far_from_route, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - clip_line_to_box as clip_line_to_box, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - landuse_source as landuse_source, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - lidar_source as lidar_source, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - plan_source as plan_source, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - plan_stations as plan_stations, -) -from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( - watershed_source as watershed_source, +from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import ( # noqa: F401 + _BASIN_MAX_DISTANCE_M, + CONTOUR_FILE, + LANDUSE_ID, + LIDAR_ID, + PLAN_ID, + STREAM_FILE, + _basins_crs, + _clip_segment, + _geojson_features, + _geojson_payload, + _geometry_lines, + _too_far_from_route, + clip_line_to_box, + landuse_source, + lidar_source, + plan_source, + plan_stations, + watershed_source, ) from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import ( _cross_files, @@ -132,13 +102,12 @@ _LONG_ID = re.compile(r"^longitudinal(?:_(\d+))?$") MASS_HAUL_ID = "mass_haul" WATERSHED_ID = "watershed" COVER_ID = "cover" +# 표준 횡단면도 — 노선 자료가 아니라 B06 표준 횡단면 설정값으로 그리는 한 장. +CROSS_STANDARD_ID = "cross_standard" # 아직 내용을 만들지 않은 도면 — 도각만 실어 연다(2026-09-01 사용자 지시). # 화면 순서(DRAWING_GROUPS)와 같은 이름을 쓴다. -BLANK_DRAWINGS: tuple[tuple[str, str], ...] = ( - ("blank_cross_standard", "표준 횡단면도"), - ("blank_standard", "표준도"), -) +BLANK_DRAWINGS: tuple[tuple[str, str], ...] = (("blank_standard", "표준도"),) BLANK_LABELS: dict[str, str] = dict(BLANK_DRAWINGS) @@ -224,6 +193,7 @@ def _drawing_list( # 노선 전장 1장짜리 도면 — 자료가 없으면 여는 시점에 404로 알린다(목록에는 항상 둔다). for drawing_id, kind, label in ( (COVER_ID, "cover", "표지"), + (CROSS_STANDARD_ID, "cross_standard", STANDARD_LABEL), (MASS_HAUL_ID, "mass_haul", "토적도(유토곡선)"), (WATERSHED_ID, "watershed", "유역도(배수 유역도)"), ): @@ -449,7 +419,7 @@ def _read_drawing( # 포맷 버전이 다르면(테이블·레이어 구성 변경 전 저장본) 캐시를 버리고 # 아래에서 원본 기준으로 재생성한다. 확정 상태도 무효로 응답해 재확정 유도. if saved.get("format") == DRAWING_FORMAT: - if drawing_id in (COVER_ID, MASS_HAUL_ID, WATERSHED_ID): + if drawing_id in (COVER_ID, MASS_HAUL_ID, WATERSHED_ID, CROSS_STANDARD_ID): kind = drawing_id # id와 kind가 같은 단장 도면 elif PLAN_ID.fullmatch(drawing_id): kind = "plan" @@ -485,6 +455,17 @@ def _read_drawing( None, ) + if drawing_id == CROSS_STANDARD_ID: + # stored_design = B06 표준 횡단면 설정값(라우터가 실어 준다). 없으면 config 기본값. + standard = stored_design if isinstance(stored_design, dict) else None + return ( + "cross_standard", + STANDARD_LABEL, + build_standard_cross_drawing(drawing_id, STANDARD_LABEL, standard), + False, + None, + ) + if LIDAR_ID.fullmatch(drawing_id): # stored_design = lidar_source()가 만든 노선 + 지표면 음영기복 그림. if not isinstance(stored_design, dict): diff --git a/B07_DesignDetail/B07_DesignDetail_Schema.py b/B07_DesignDetail/B07_DesignDetail_Schema.py index dbe92954..53abb805 100644 --- a/B07_DesignDetail/B07_DesignDetail_Schema.py +++ b/B07_DesignDetail/B07_DesignDetail_Schema.py @@ -19,6 +19,7 @@ class DesignDrawingItem(BaseModel): "plan", "landuse", "plan_lidar", + "cross_standard", "blank", ] label: str @@ -52,6 +53,7 @@ class DesignDrawingResponse(BaseModel): "plan", "landuse", "plan_lidar", + "cross_standard", "blank", ] label: str diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts index 2eab42c6..8de0f675 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts @@ -36,7 +36,7 @@ export const DRAWING_GROUPS: readonly { { label: "계획평면도(배치도)", idPrefix: "plan_layout" }, { label: "계획평면도(라이다)", idPrefix: "plan_lidar" }, { label: "종단면도", kind: "longitudinal" }, - { label: "표준 횡단면도", blankId: "blank_cross_standard" }, + { label: "표준 횡단면도", kind: "cross_standard" }, { label: "횡단면도", kind: "cross" }, { label: "토적도(유토곡선)", kind: "mass_haul" }, { label: "유역도(배수 유역도)", kind: "watershed" }, diff --git a/config/config_system.py b/config/config_system.py index de4780e4..701e782e 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -178,6 +178,10 @@ DRAWING_SCALE_BASIN = 6000 # 유역도 평면 축척 분모 (실거리 1 m = 1/ # 계획평면도·용지도 평면 축척 분모 — 지식DB 「설계제원_총괄」 측량·도면 기준 1/1,200. # 횡단면도와 같은 원칙: 축척은 줄이지 않고, 한 장에 안 들어가면 **장을 나눈다**. DRAWING_SCALE_PLAN = 1200 +# 표준 횡단면도 — 상세도라 지식DB에 지정 축척이 없다. 본 그림 1/50, 측구 부분확대도 +# 1/10 (2026-09-04). 노폭 4 m 기준 본 그림이 A1 작도영역에 여유 있게 든다. +DRAWING_SCALE_CROSS_STANDARD = 50 +DRAWING_SCALE_DITCH_DETAIL = 10 # 시스템 리소스 로그 (루트 log 폴더에 단일 파일, 1개월 보관) LOG_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "log")