Merge remote-tracking branch 'origin/main_laptop_1' into main_desktop_1

This commit is contained in:
2026-09-08 19:33:52 +09:00
7 changed files with 294 additions and 1 deletions
@@ -770,6 +770,15 @@
"required": true,
"phase": "detail"
},
{
"key": "stone_supply",
"label": "조달",
"input": "select",
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
@@ -844,6 +853,15 @@
"required": true,
"phase": "detail"
},
{
"key": "stone_supply",
"label": "조달",
"input": "select",
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
@@ -983,6 +1001,15 @@
"required": true,
"phase": "detail"
},
{
"key": "stone_supply",
"label": "조달",
"input": "select",
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
@@ -1079,6 +1106,15 @@
"required": true,
"phase": "detail"
},
{
"key": "stone_supply",
"label": "조달",
"input": "select",
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
},
{
"key": "length_m",
"label": "길이",
@@ -1115,6 +1151,15 @@
"required": false,
"phase": "b05"
},
{
"key": "stone_supply",
"label": "조달",
"input": "select",
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
},
{
"key": "area_m2",
"label": "면적",
@@ -1214,6 +1259,15 @@
"required": false,
"phase": "b05"
},
{
"key": "stone_supply",
"label": "조달",
"input": "select",
"choices": ["채집", "구입"],
"default": "채집",
"required": false,
"phase": "detail"
},
{
"key": "height_m",
"label": "높이",
+35
View File
@@ -131,3 +131,38 @@ def _bench_cut_length(offsets: list[float], grounds: list[float], diffs: list[fl
continue
total += ((run**2 + rise**2) ** 0.5) * share
return total
def _split_ditch_area(ditch_spec: dict, depth_to_boundary_m: float | None) -> tuple[float, float]:
"""측구 단면적을 (토사, 암반)으로 가른다 — 암반 경계선까지의 깊이 기준.
`depth_to_boundary_m` 은 **측구 상단에서 암반 경계선까지의 깊이(m)** 다.
`None` 이면 가를 근거가 없다는 뜻이라 부르는 쪽이 처리한다(여기서는 안 부른다).
⚠ 측구 단면은 **공칭 도형**(사다리꼴·L형 근사)이라 지반선을 따라 적분하지 않는다.
경계선도 그 자리 한 높이로 본다 — 폭 1m 안팎에서 지반선 기울기 차이는 도형 근사보다
작다. 절토 면적 분리(`_split_cut_areas`)가 균일 두께를 쓰는 것과 같은 태도다.
"""
kind = str(ditch_spec.get("type") or "none")
if kind == "l_type":
width = float(ditch_spec.get("width_m") or 0.0)
depth = float(ditch_spec.get("depth_m") or 0.0)
total = width * depth / 2.0
if depth <= 0 or width <= 0:
return 0.0, 0.0
d0 = min(max(depth_to_boundary_m or 0.0, 0.0), depth)
# 깊이 d 에서의 가로 폭 = W(1 − d/D). 위에서 d0 까지 적분한다.
soil = width * d0 - width * d0 * d0 / (2.0 * depth)
return soil, max(total - soil, 0.0)
if kind == "standard":
top = float(ditch_spec.get("top_width_m") or 0.0)
bottom = min(float(ditch_spec.get("bottom_width_m") or 0.0), top)
depth = float(ditch_spec.get("depth_m") or 0.0)
total = (top + bottom) / 2.0 * depth
if depth <= 0 or top <= 0:
return 0.0, 0.0
d0 = min(max(depth_to_boundary_m or 0.0, 0.0), depth)
# 깊이 d 에서의 폭 = top (topbottom)·d/depth. 위에서 d0 까지 적분한다.
soil = top * d0 - (top - bottom) * d0 * d0 / (2.0 * depth)
return soil, max(total - soil, 0.0)
return 0.0, 0.0
+29
View File
@@ -34,6 +34,7 @@ from typing import Any
from B06_Section.B06_Section_Engine_Areas import (
_bench_cut_length,
_split_cut_areas,
_split_ditch_area,
_trapezoid_areas,
)
from common_util.common_util_cross_berm import (
@@ -749,6 +750,28 @@ def compute_cross_design(
"depth_m": group["ditch_depth_m"],
}
# 측구터파기 토사/암 분리 — **새 입력을 만들지 않는다.** 절토 분리와 같은 근거
# (지반 유형 + 암반 경계선)를 그대로 쓴다. 별표2 Ⅰ.1.나.(5) 「측구터파기 단면적」이
# 횡단도 표의 법정 칸이라 반만 채워 나가면 안 된다(2026-09-09).
# 근거가 없으면 **나누지 않고** 사유를 함께 내보낸다 — 절반을 임의로 가르지 않는다.
if not geometry.has_ditch:
ditch_soil_area, ditch_rock_area = 0.0, 0.0
ditch_split_basis = "no_ditch"
elif preset_key != "rock":
# 토사 지반 — 암반 경계선 자체가 없다. 전량 토사(절토 분리와 같은 판정).
ditch_soil_area, ditch_rock_area = ditch_area, 0.0
ditch_split_basis = "soil_ground"
elif rock_boundary_offset_m is None or not geometry.ditch_points:
# 암 지반인데 경계선 값이 없다(구 데이터) — 가를 근거가 없으므로 전량 암.
ditch_soil_area, ditch_rock_area = 0.0, ditch_area
ditch_split_basis = "rock_ground_no_boundary"
else:
ditch_top_z = geometry.ditch_points[0][1]
mid_offset = sum(point[0] for point in geometry.ditch_points) / len(geometry.ditch_points)
boundary_z = ground_at(mid_offset) - abs(float(rock_boundary_offset_m))
ditch_soil_area, ditch_rock_area = _split_ditch_area(ditch_spec, ditch_top_z - boundary_z)
ditch_split_basis = "rock_boundary"
# 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo한다(D-2, 표시·저장용).
if geometry.left_role == "cut" and geometry.right_role == "cut":
resolved_mode = "both_cut"
@@ -824,6 +847,12 @@ def compute_cross_design(
round(fill_ground_slope, 4) if fill_ground_slope is not None else None
),
"ditch_area_m2": round(ditch_area, 4),
# 측구터파기 내역(합=ditch_area_m2). 가른 근거는 `ditch_split_basis` 로 함께 낸다:
# rock_boundary(암반 경계선으로 가름) · soil_ground(토사 지반이라 전량 토사) ·
# rock_ground_no_boundary(암 지반인데 경계선 없음 — 전량 암) · no_ditch(측구 없음).
"ditch_soil_area_m2": round(ditch_soil_area, 4),
"ditch_rock_area_m2": round(ditch_rock_area, 4),
"ditch_split_basis": ditch_split_basis,
"design_line": design_line,
# 절토 사면을 경사 구간별로 쪼갠 목록(소단 제외).
# ⚠ **지금 이 값을 읽는 곳은 없다**(2026-09-07). 원래 임자였던 별표2 법정 경사 검사는
@@ -0,0 +1,91 @@
"""횡단도 아래 수량 산출표 — **저장된 횡단 설계에서 칸을 채운다**.
왜 필요한가 (법정 요구)
별표2 Ⅰ.1.나.(5) 가 횡단면도에 「지반고·계획고·절토고·성토고·**단면적·지장목 제거·
측구터파기 단면적·사면보호공**」 여덟을 요구한다. 지금 나가는 것은 **앞 넷뿐**이고
뒤 넷이 빈칸으로 나갔다. 그 구멍을 메우는 자리다.
⚠ 빈칸의 까닭은 「값이 없어서」가 아니었다
표가 `source["quantities"]` 를 보는데 **그 키가 원본 파일에 아예 없다**(실측:
`cross_00960m.json` 에 `samples`·`center_z`·`frame` 뿐). 반면 **저장된 횡단 설계에는
단면적이 그대로 있고**(`cut_soil_area_m2` 등), **사면길이도 그 설계선에서 유도된다**
(`B08_Quantity_Engine_SlopeLength.station_slope`). 즉 **통로만 없었다.**
⚠ 계산을 새로 짜지 않는다 (CLAUDE.md 5장)
단면적은 B06 이 낸 저장값을 **그대로 읽고**, 사면 계열은 **B08 이 쓰는 그 함수**를 부른다.
계열 이름과 「어느 면을 쓰나」도 `B08_Quantity_Engine_SlopeArea` 의 정의를 빌려 쓴다 —
거기서 밑수가 바뀌면 이 표도 같이 움직여야 하기 때문이다.
⚠ 사면 계열 칸의 **단위**
B08 은 측점 사이를 평균단면적법으로 적분해 **면적(㎡)** 을 내지만, 그것은 두 측점이 있어야
나오는 값이라 **한 장짜리 횡단도에는 못 쓴다.** 횡단도 칸에 들어가는 것은 그 측점의
**사면길이(m)** 이고, 이는 **1m 폭 조각의 면적(㎡/m)과 수치가 같다.**
⇒ 표에 「m 로 볼 것인가 ㎡/m 로 볼 것인가」는 표기 문제이고 **값은 하나다.**
아직 안 채우는 칸 — 근거가 없다(임의로 넣지 않는다)
· 측구 토사/암석 — 저장값이 `ditch_area_m2` **한 값뿐**이라 토사·암석으로 못 가른다
· 표토제거 성토/절토 — 법은 「전량 제거」인데 **두께 칸이 없어** 물량이 안 선다
· 편책 — 별도 일위대가를 만들어 잇기로 확정(2026-09-09 ⑧-4). 밑수는 그때 붙는다
· 제근 — 입목 본수를 안 든다
· 노면다짐 — 밑수(노면 폭)는 있으나 이 표의 다른 칸과 축이 달라 뒤로 미룸
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Engine_SlopeArea import PROTECTION_SOURCE, _key, _length_of
from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slope
#: 저장된 설계 단면적을 그대로 옮기는 칸 — `표 키 → 설계 키`.
AREA_KEYS: tuple[tuple[str, str], ...] = (
("cut_soil", "cut_soil_area_m2"),
("cut_rock", "cut_rock_area_m2"),
("embankment", "fill_area_m2"),
)
#: 사면 계열에서 오는 칸 — `표 키 → (B08 계열, 면)`.
#: 성토파종·절토살포는 **법면보호공**이고, 그것은 B08 에서 면고르기를 참조한다
#: (`PROTECTION_SOURCE`). 참조를 끊으면 그쪽 한 곳만 고치면 이 표도 따라온다.
SLOPE_KEYS: tuple[tuple[str, str, str], ...] = (
("benching", "bench_cut", "fill"),
("grading_fill", "face_dressing", "fill"),
("grading_cut", "face_dressing", "cut"),
("tree_removal_fill", "tree_removal", "fill"),
("tree_removal_cut", "tree_removal", "cut"),
("fill_seeding", PROTECTION_SOURCE, "fill"),
("cut_spraying", PROTECTION_SOURCE, "cut"),
)
def _num(value: Any) -> float | None:
return float(value) if isinstance(value, (int, float)) else None
def derived_cells(chainage_m: float, design: dict[str, Any] | None) -> dict[str, float | None]:
"""저장된 횡단 설계 하나에서 **채울 수 있는 칸**만 낸다.
설계가 없거나 설계선이 없으면 **빈 dict** 를 낸다 — 0 으로 때우지 않는다.
도면에 0 이 찍히면 「없다」와 「안 쟀다」를 구별할 수 없다.
"""
if not isinstance(design, dict) or not design:
return {}
cells: dict[str, float | None] = {}
for table_key, design_key in AREA_KEYS:
value = _num(design.get(design_key))
if value is not None:
cells[table_key] = value
# 사면길이는 설계선에서 유도한다 — 설계선이 없으면 유도할 것이 없다.
if not design.get("design_line"):
return cells
slope = station_slope(float(chainage_m), design)
lengths = {
_key(series, face): _length_of(slope, series, face)
for _table_key, series, face in SLOPE_KEYS
}
for table_key, series, face in SLOPE_KEYS:
cells[table_key] = lengths[_key(series, face)]
return cells
@@ -54,6 +54,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Standard import (
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
QUANTITY_VALUE_KEYS,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Quantity import derived_cells
from B07_DesignDetail.B07_DesignDetail_Engine_Template import add_title_fields
# 유역도 배경·파일 입출력 조각은 700줄 제한으로 떼어냈다(2026-09-04).
@@ -270,6 +271,11 @@ def _quantity_table(
**계획고는 횡단 설계(design)에도 있다** — 장 배치 입력의 원본에는 그 값이 없어
계획고·절토고·성토고 세 칸이 통째로 비어 나갔다(2026-09-03 실측: 장 확정 시 21개
항목 중 지반고 하나만 채워짐). 원본에 없으면 설계에서 읽는다.
⚠ **본문 칸도 설계에서 온다**(2026-09-09) — `source["quantities"]` 키는 원본 파일에
아예 없어 열일곱 칸이 통째로 비어 나갔다. 채울 수 있는 것은 `_Engine_Cross_Quantity`
가 낸다(단면적은 저장값, 사면 계열은 B08 이 쓰는 함수 그대로). 별표2 법정 요구
여덟 중 뒤 넷이 비던 자리다.
"""
def num(value: Any) -> float | None:
@@ -289,6 +295,8 @@ def _quantity_table(
"cut": cut,
"fill": fill,
}
chainage = num(source.get("chainage_m")) or 0.0
table.update(derived_cells(chainage, design))
for key in QUANTITY_VALUE_KEYS:
table.setdefault(key, num(quantities.get(key)))
return table
+44 -1
View File
@@ -24,7 +24,12 @@
* ========================================================================== */
import type { BermSpec } from "./common_util_cross_berm";
import { benchCutLength, splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas";
import {
benchCutLength,
splitCutAreas,
splitDitchArea,
trapezoidAreas,
} from "./common_util_cross_design_areas";
// 단면 기하(노면·측구·사면 설계고)는 파일이 700줄을 넘어 떼어냈다(2026-09-04).
import {
CURVE_WIDENING_MAX_WIDTH_M,
@@ -136,6 +141,10 @@ export interface CrossDesignResult {
slope_unclosed: boolean;
fill_ground_slope: number | null;
ditch_area_m2: number;
/** 측구터파기 내역(합=ditch_area_m2)과 가른 근거. 짝: 파이썬 `ditch_split_basis`. */
ditch_soil_area_m2: number;
ditch_rock_area_m2: number;
ditch_split_basis: string;
design_line: CrossDesignEdge[];
/** ( ). : `cut_slope_segments`.
* ** **(2026-09-07) 2
@@ -395,6 +404,37 @@ export function computeCrossDesign(
};
}
// 측구터파기 토사/암 분리 — **새 입력을 만들지 않는다.** 절토 분리와 같은 근거
// (지반 유형 + 암반 경계선)를 그대로 쓴다. 근거가 없으면 나누지 않고 사유를 낸다.
// ⚠ 파이썬 짝: `B06_Section_Engine_Design` 의 같은 자리.
let ditchSoilArea: number;
let ditchRockArea: number;
let ditchSplitBasis: string;
if (!geometry.hasDitch) {
ditchSoilArea = 0;
ditchRockArea = 0;
ditchSplitBasis = "no_ditch";
} else if (presetKey !== "rock") {
ditchSoilArea = ditchArea;
ditchRockArea = 0;
ditchSplitBasis = "soil_ground";
} else if (
rockBoundaryOffsetM === null ||
rockBoundaryOffsetM === undefined ||
!geometry.ditchPoints.length
) {
ditchSoilArea = 0;
ditchRockArea = ditchArea;
ditchSplitBasis = "rock_ground_no_boundary";
} else {
const ditchTopZ = geometry.ditchPoints[0][1];
const midOffset =
geometry.ditchPoints.reduce((sum, point) => sum + point[0], 0) / geometry.ditchPoints.length;
const boundaryZ = groundAt(midOffset) - Math.abs(rockBoundaryOffsetM);
[ditchSoilArea, ditchRockArea] = splitDitchArea(ditchSpec, ditchTopZ - boundaryZ);
ditchSplitBasis = "rock_boundary";
}
// 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo 한다(D-2).
let resolvedMode: string;
if (geometry.leftRole === "cut" && geometry.rightRole === "cut") resolvedMode = "both_cut";
@@ -457,6 +497,9 @@ export function computeCrossDesign(
slope_unclosed: slopeUnclosed,
fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope),
ditch_area_m2: round4(ditchArea),
ditch_soil_area_m2: round4(ditchSoilArea),
ditch_rock_area_m2: round4(ditchRockArea),
ditch_split_basis: ditchSplitBasis,
design_line: designLine,
cut_slope_segments: geometry.cutSlopeSegments(),
};
@@ -126,3 +126,36 @@ export function benchCutLength(offsets: number[], grounds: number[], diffs: numb
}
return total;
}
/**
* [, ] (m) .
* : `B06_Section_Engine_Areas._split_ditch_area`. .
* .
*/
export function splitDitchArea(
ditchSpec: Record<string, unknown>,
depthToBoundaryM: number | null,
): [number, number] {
const kind = String(ditchSpec.type ?? "none");
const clamp = (depth: number): number => Math.min(Math.max(depthToBoundaryM ?? 0, 0), depth);
if (kind === "l_type") {
const width = Number(ditchSpec.width_m ?? 0);
const depth = Number(ditchSpec.depth_m ?? 0);
if (depth <= 0 || width <= 0) return [0, 0];
const total = (width * depth) / 2;
const d0 = clamp(depth);
const soil = width * d0 - (width * d0 * d0) / (2 * depth);
return [soil, Math.max(total - soil, 0)];
}
if (kind === "standard") {
const top = Number(ditchSpec.top_width_m ?? 0);
const bottom = Math.min(Number(ditchSpec.bottom_width_m ?? 0), top);
const depth = Number(ditchSpec.depth_m ?? 0);
if (depth <= 0 || top <= 0) return [0, 0];
const total = ((top + bottom) / 2) * depth;
const d0 = clamp(depth);
const soil = top * d0 - ((top - bottom) * d0 * d0) / (2 * depth);
return [soil, Math.max(total - soil, 0)];
}
return [0, 0];
}