"""비탈면 녹화 전개식 — **㎡ 넷(새심기·평떼·줄떼·비탈덮기)** (2026-09-15 브레인 12장 D 녹화 ⓐ~ⓓ). 근거 산림품셈 5-14 새심기(㎡당 · 1㎡당 10주) · 5-22 평떼(5-22-1 떼 0.3×0.3 11매/㎡ · 5-22-2 붙임 ㎡ · 5-22-3 떼꽂이 ㎡) · 5-23 줄떼(5-23-1 떼 3.33매/㎡ · 5-23-2 붙임 · 5-23-3 떼꽂이) · 5-25 거적덮기(㎡당) · 5-28 비탈덮기(5-28-1 짚망 · 5-28-2 방초·야자섬유매트) 교본 7-1 그림 7-8~7-12 는 사진·개념도(치수 없음) — 표준도가 아님. ⭐ 면적 = 구간 안 사면 면적(B06 측점 사면길이 · 평균단면적법) × 고른 면(절토면/성토면/양쪽). ⚠ 길이형(선떼·조공)은 다음 차례(선떼 = 사방기술교본 표 2-2-6 급별 m당 매수) · 나머지 공법은 사유. """ from __future__ import annotations from typing import Any from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed CUT, FILL = "b06_cut_area_m2", "b06_fill_area_m2" FACES = {"절토면": (CUT,), "성토면": (FILL,), "양쪽": (CUT, FILL)} #: 공법 → (성분 이름, 떼 매수/㎡ 또는 None, 근거). 떼 매수 = 품셈 5-22-1·5-23-1 「소요 매수」. AREA_METHODS: dict[str, tuple[str, float | None, str]] = { "새심기": ("새심기", None, "품셈 5-14 새심기(㎡당 · 1㎡당 10주 · 요소·인산은 표 안)"), "평떼": ("평떼", 11.0, "품셈 5-22 평떼 — 떼 0.3×0.3 11매/㎡ · 붙임 5-22-2 · 떼꽂이 5-22-3"), "줄떼": ( "줄떼", 3.33, "품셈 5-23 줄떼 — 떼 0.3×0.3(30㎝ 간격) 3.33매/㎡ · 붙임 5-23-2 · 떼꽂이 5-23-3", ), "비탈덮기": ( "비탈덮기", None, "품셈 5-25 거적덮기 · 5-28 비탈덮기(짚망·매트) — 덮개가 공종을 가름", ), } SOD_SPEC = "0.3×0.3" LENGTH_METHODS = ("선떼붙이기", "조공") NOTE_LENGTH = ( "{method}은(는) 길이형(10m·100m당) — 산출식 아직 없음 · 떼 매수는 품셈이" " 「사방기술교본 기준표」라 함(선떼 = 표 2-2-6 급별 m당 매수 · 다음 차례)" " · 단 직고·급수 칸은 기본값 없음(브레인 ⓒ)" ) NOTE_LATER = ( "{method}은(는) 산출식이 아직 없음 — 12장 D 녹화 차례 뒤(㎡ 넷 먼저 · 2026-09-15 브레인 ⓓ)" ) NOTE_FACE = "대상 면을 안 고름 — 절토면/성토면/양쪽 중 고를 것(공법마다 달라 기본값 없음)" NOTE_COVER = ( "비탈덮기 덮개를 안 고름 — 거적(5-25)·짚망(5-28-1)·방초·야자섬유매트(5-28-2) 중 고를 것" ) NOTE_OVERLAP = ( "⚠ 같은 사면이 초류종자살포·면고르기에도 들어 있음 — 겹친 몫을 자동으로 빼지 않음" "(파종 구간을 줄일지 설계자가 고름 · 2026-09-15 브레인 ⓑ)" ) NOTE_STAKE = ( "평떼 5-22-3 [주] 「평떼(0.06인) 품에는 떼꽂이 포함」은 5-12 품에 걸린 것으로 읽음" " — 5-22-3 떼꽂이는 따로 셈" ) NOTE_FIGURES = "교본 그림 7-8·7-10·7-11 은 사진 · 7-9·7-12 는 개념도(치수 없음) — 표준도가 아님" def _length_at(points: list[tuple[float, float]], at: float) -> float: """그 자리의 사면길이 — 앞뒤 측점 사이 직선 보간 · 끝 밖이면 가장 가까운 측점.""" if at <= points[0][0]: return points[0][1] for (s0, v0), (s1, v1) in zip(points, points[1:]): if s0 <= at <= s1: return v0 if s1 == s0 else v0 + (v1 - v0) * (at - s0) / (s1 - s0) return points[-1][1] def _interval_area(points: list[tuple[float, float]], start: float, end: float) -> float: if len(points) < 1 or end <= start: return 0.0 cuts = [start, *[s for s, _v in points if start < s < end], end] return sum( (_length_at(points, a) + _length_at(points, b)) / 2 * (b - a) for a, b in zip(cuts, cuts[1:]) ) def revegetation_from_designs( structure: dict[str, Any], designs: list[dict[str, Any]] | None, slopes: list[Any] | None = None, ) -> dict[str, Any]: """구간 안 절토·성토 사면 면적을 B06 측점에서 셈해 실은 **새** 구조물(칸 값은 안 건드림).""" if slopes is None: from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slopes slopes = station_slopes(designs or []) ordered = sorted(slopes, key=lambda s: s.chainage_m) options = dict(structure.get("options") or {}) notes = list(structure.get("notes") or []) start, end = sorted((_num(structure.get("start_m")), _num(structure.get("end_m")))) if not ordered: notes.append("B06 횡단 설계가 없어 구간 사면 면적을 못 읽음") return {**structure, "options": options, "notes": notes} for key, attr in ((CUT, "cut_length_m"), (FILL, "fill_length_m")): points = [(s.chainage_m, float(getattr(s, attr))) for s in ordered] options[key] = round(_interval_area(points, start, end), 4) notes.append( f"구간 {start:g}~{end:g}m 사면 면적 — 절토면 {options[CUT]:,.2f}㎡ · 성토면" f" {options[FILL]:,.2f}㎡ (B06 측점 사면길이 · 평균단면적법)" ) return {**structure, "options": options, "notes": notes} def _area(options: dict[str, Any]) -> float | None: keys = FACES.get(str(options.get("face") or "")) return None if keys is None else sum(_num(options.get(key)) for key in keys) def revegetation(options: dict[str, Any]) -> tuple[list[Component], list[str]]: """녹화 한 구간 — (성분, 사유).""" method = str(options.get("method") or "") if not method: return [], ["녹화 공법을 안 고름 — 공법을 고르면 섬"] if method not in AREA_METHODS: template = NOTE_LENGTH if method in LENGTH_METHODS else NOTE_LATER return [], [template.format(method=method), NOTE_FIGURES] area = _area(options) if area is None: return [], [NOTE_FACE] if method == "비탈덮기" and not options.get("cover_kind"): return [], [NOTE_COVER] name, sheets, basis = AREA_METHODS[method] notes = [NOTE_OVERLAP, basis, NOTE_FIGURES] + ([NOTE_STAKE] if method == "평떼" else []) rows: list[tuple[str, str, float, str, str]] = [ (name, "㎡", area, f"{options.get('face')} 사면 면적 {area:,.2f}㎡", "") ] if sheets is not None: rows.append(("떼", "매", area * sheets, f"면적 {area:,.2f} × {sheets:g}매/㎡", SOD_SPEC)) components = [ Component(item, unit, amount, destination, text, spec=spec) for item, unit, amount, text, spec in rows if (destination := routed(item, notes)) ] return components, notes def billing(options: dict[str, Any]) -> tuple[str, float]: """내역 줄 — 녹화 면적 ㎡.""" return "㎡", _area(options) or 0.0