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

This commit is contained in:
2026-09-08 19:19:11 +09:00
7 changed files with 183 additions and 7 deletions
+8 -1
View File
@@ -24,7 +24,7 @@
* ========================================================================== */
import type { BermSpec } from "./common_util_cross_berm";
import { splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas";
import { benchCutLength, splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas";
// 단면 기하(노면·측구·사면 설계고)는 파일이 700줄을 넘어 떼어냈다(2026-09-04).
import {
CURVE_WIDENING_MAX_WIDTH_M,
@@ -131,6 +131,8 @@ export interface CrossDesignResult {
cut_rock_area_m2: number;
cut_rock_kind: string | null;
fill_area_m2: number;
/** 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m). */
bench_cut_length_m: number;
slope_unclosed: boolean;
fill_ground_slope: number | null;
ditch_area_m2: number;
@@ -331,18 +333,22 @@ export function computeCrossDesign(
const merged = [...mergedSet].sort((a, b) => a - b);
const offsets: number[] = [];
const grounds: number[] = [];
const diffs: number[] = [];
const designLine: CrossDesignEdge[] = [];
for (const offsetM of merged) {
const groundM = groundAt(offsetM);
const designZ = geometry.designZ(offsetM, groundM);
offsets.push(offsetM);
grounds.push(groundM);
diffs.push(groundM - designZ);
designLine.push({ offset_m: round4(offsetM), elevation_m: round4(designZ) });
}
// 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음).
const [cutArea, fillArea] = trapezoidAreas(offsets, diffs);
// 층따기 밑수(길이 m) — 성토부 아래 원지반이 1:4 보다 급한 구간의 지표면 길이.
const benchCut = benchCutLength(offsets, grounds, diffs);
const fillGroundSlope = geometry.fillGroundSlope();
const slopeUnclosed =
diffs.length > 0 &&
@@ -447,6 +453,7 @@ export function computeCrossDesign(
cut_rock_area_m2: round4(cutRockArea),
cut_rock_kind: cutRockKind,
fill_area_m2: round4(fillArea),
bench_cut_length_m: round4(benchCut),
slope_unclosed: slopeUnclosed,
fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope),
ditch_area_m2: round4(ditchArea),
@@ -95,3 +95,34 @@ export function splitCutAreas(
}
return [soilArea, rockArea];
}
/** 층따기 대상 판정 기울기 — 원지반 횡단기울기 1:4(=25%)보다 급한 곳에만 한다.
* 근거: 별표2 · 임도기술교본 6장 4절(「1:4보다 급한 경사를 가진 지반 위에 성토」).
* ⚠ 파이썬 짝: `B06_Section_Engine_Areas._BENCH_CUT_MIN_GROUND_SLOPE`. */
export const BENCH_CUT_MIN_GROUND_SLOPE = 0.25;
/**
* 층따기 밑수 — **성토부 아래 원지반 표면의 경사길이(m)**.
* ⚠ 파이썬 짝: `B06_Section_Engine_Areas._bench_cut_length`. 한 벌로 움직인다.
* 면적(㎡)은 측점 사이를 평균단면적법으로 이어 B08 이 낸다.
*/
export function benchCutLength(offsets: number[], grounds: number[], diffs: number[]): number {
let total = 0;
for (let index = 1; index < offsets.length; index += 1) {
const run = offsets[index] - offsets[index - 1];
if (run <= 0) continue;
const d0 = diffs[index - 1];
const d1 = diffs[index];
if (d0 >= 0 && d1 >= 0) continue;
let share = 1;
if (d0 * d1 < 0) {
const zeroRatio = d0 / (d0 - d1);
share = d0 > 0 ? 1 - zeroRatio : zeroRatio;
}
if (share <= 0) continue;
const rise = grounds[index] - grounds[index - 1];
if (Math.abs(rise) / run < BENCH_CUT_MIN_GROUND_SLOPE) continue;
total += Math.sqrt(run * run + rise * rise) * share;
}
return total;
}
+63 -2
View File
@@ -192,6 +192,13 @@ export interface HaulPlan {
borrow_m3: number;
/** 사토 중 자연방토 몫(㎥) — 운반비를 세지 않는다. */
natural_spoil_m3: number;
/**
* 채집석 공제로 받은 값(㎥, 양수). **`null` 은 「아직 안 옴」**이고 `0` 은 「공제 없음」이다 —
* 둘을 같게 보면 값이 안 온 것을 공제 0 으로 읽어 조용히 넘어간다(2026-09-09).
*/
collected_stone_deduction_m3: number | null;
/** 실제로 사토에서 뺀 양(㎥). 사토가 모자라면 받은 값보다 작을 수 있다. */
collected_stone_deducted_m3: number;
/** 블록 안에서 옮기는 양(㎥). */
hauled_m3: number;
/** 떨어진 구간끼리 장거리로 옮기는 양(㎥). */
@@ -316,9 +323,54 @@ function bandOutline(
* 누가토량 곡선에서 토량 분배(평형선·운반 블록·장비 띠·사토/토취)를 뽑는다.
* 블록도 잔량도 안 나오면(평탄한 곡선) null.
*/
/**
* 채집석 공제 — **사토에서 한 번만 뺀다**(2026-09-09 사용자 확정, 네 창 합의 문구).
*
* 채집석 공제는 사토에서 한 번만 뺀다.
* B08 은 소요량(collected_stone_deduction_m3, ㎥ 양수)을 내기만 하고 공제하지 않으며,
* 빼는 자리는 유토곡선의 사토뿐이다 —
* 실어 내는 몫(spoil_m3 natural_spoil_m3)에서 먼저 빼고 모자라면 자연방토에서 뺀다.
*
* ⚠ 순서가 중요하다 — 캔 돌은 **실어 낼 흙 속에 있던 것**이다. 자연방토(운반비를 안 세는 몫)
* 에서 먼저 깎으면 **줄어야 할 운반비가 안 줄어든다.**
* ⚠ 잔량 하나하나(`residuals`)를 줄인다 — 총량만 줄이면 사토 balloon·운반거리가 안 따라간다.
*
* 돌려주는 값은 **실제로 뺀 양(㎥)**. 사토가 모자라면 받은 값보다 작다.
*/
function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: number | null): number {
if (deduction === null || !Number.isFinite(deduction) || deduction <= 0) return 0;
const spoils = residuals.filter((residual) => residual.kind === "spoil");
let left = deduction;
const take = (residual: HaulResidual, amount: number): void => {
if (amount <= 0) return;
const before = residual.volume_m3;
const ratio = before > 0 ? (before - amount) / before : 0;
residual.volume_m3 = before - amount;
// 지반유형 안분도 같은 비율로 줄인다 — 남은 사토의 구성비는 그대로다.
residual.ea_m3 *= ratio;
residual.rr_m3 *= ratio;
residual.br_m3 *= ratio;
left -= amount;
};
// ① 실어 내는 몫부터
for (const residual of spoils) {
if (left <= EPSILON) break;
take(residual, Math.min(Math.max(residual.volume_m3 - residual.natural_m3, 0), left));
}
// ② 모자라면 자연방토에서
for (const residual of spoils) {
if (left <= EPSILON) break;
const amount = Math.min(residual.natural_m3, left);
residual.natural_m3 -= amount;
take(residual, amount);
}
return deduction - Math.max(left, 0);
}
export function computeHaulPlan(
result: MassHaulResult,
limits: HaulEquipmentLimit[] | undefined,
options?: { collected_stone_deduction_m3?: number | null },
): HaulPlan | null {
const points = result.points;
if (points.length < 2) return null;
@@ -497,10 +549,17 @@ export function computeHaulPlan(
residual.index = index + 1;
});
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
const deducted = applyCollectedStoneDeduction(settled, deductionInput);
const remaining = settled.filter((residual) => residual.volume_m3 > EPSILON);
remaining.forEach((residual, index) => {
residual.index = index + 1;
});
let spoil = 0;
let borrow = 0;
let naturalSpoil = 0;
for (const residual of settled) {
for (const residual of remaining) {
if (residual.kind === "spoil") {
spoil += residual.volume_m3;
naturalSpoil += residual.natural_m3;
@@ -510,12 +569,14 @@ export function computeHaulPlan(
for (const point of points) fillTotal += point.fill_m3;
return {
blocks,
residuals: settled,
residuals: remaining,
transfers,
steps,
spoil_m3: spoil,
borrow_m3: borrow,
natural_spoil_m3: naturalSpoil,
collected_stone_deduction_m3: deductionInput,
collected_stone_deducted_m3: deducted,
hauled_m3: blocks.reduce((sum, block) => sum + block.volume_m3, 0),
transferred_m3: transfers.reduce((sum, entry) => sum + entry.volume_m3, 0),
fill_total_m3: fillTotal,