Merge remote-tracking branch 'origin/main_laptop_1' into main_desktop_1
This commit is contained in:
@@ -85,3 +85,49 @@ def _split_cut_areas(
|
|||||||
soil_area += (min(max(d_a, 0.0), t0) + min(max(d_b, 0.0), t0)) / 2.0 * span
|
soil_area += (min(max(d_a, 0.0), t0) + min(max(d_b, 0.0), t0)) / 2.0 * span
|
||||||
rock_area += (max(d_a - t0, 0.0) + max(d_b - t0, 0.0)) / 2.0 * span
|
rock_area += (max(d_a - t0, 0.0) + max(d_b - t0, 0.0)) / 2.0 * span
|
||||||
return soil_area, rock_area
|
return soil_area, rock_area
|
||||||
|
|
||||||
|
|
||||||
|
# 층따기 대상 판정 기울기 — 원지반 횡단기울기 1:4(=25%)보다 급한 곳에만 한다.
|
||||||
|
# 근거: 임도설치 및 관리 등에 관한 규정 별표2 · 임도기술교본 6장 4절 「경사지의 층따기에
|
||||||
|
# 있어 그 경사가 1:4보다 급한 경사를 가진 지반 위에 성토를 하는 경우 … 층따기를 설치」.
|
||||||
|
# 지식DB `01_임도/02_상세설계/성토_비탈면.md` §4 [구현] 「원지반 횡단경사 > 25% 구간의 성토부」.
|
||||||
|
_BENCH_CUT_MIN_GROUND_SLOPE = 0.25
|
||||||
|
|
||||||
|
|
||||||
|
def _bench_cut_length(offsets: list[float], grounds: list[float], diffs: list[float]) -> float:
|
||||||
|
"""층따기 밑수 — **성토부 아래 원지반 표면의 경사길이(m)**.
|
||||||
|
|
||||||
|
무엇을 재나
|
||||||
|
성토(diff<0)가 원지반에 얹히는 구간에서, 원지반 횡단기울기가 1:4 보다 급한
|
||||||
|
조각만 골라 **지표면을 따라간 길이**를 더한다. 수평 폭이 아니라 빗변이다 —
|
||||||
|
층따기는 그 경사면을 계단으로 깎는 일이라 대상 면이 곧 지표면이다.
|
||||||
|
|
||||||
|
왜 성토면이 아니라 원지반인가
|
||||||
|
층따기는 **원지반 표면**에 하는 것이다(교본 6장 4절). 성토 비탈면 길이로 재면
|
||||||
|
대상이 아닌 면을 세는 것이 된다.
|
||||||
|
|
||||||
|
단위
|
||||||
|
여기서 나오는 것은 **길이(m)** 다. 면적(㎡)은 측점 사이를 평균단면적법으로 이어
|
||||||
|
B08 이 낸다 — 사면 4계열과 같은 방식이라 계산을 두 벌로 짜지 않는다.
|
||||||
|
(2026-09-09 사용자 확정: 층따기 단위는 ㎡.)
|
||||||
|
"""
|
||||||
|
total = 0.0
|
||||||
|
for index in range(1, len(offsets)):
|
||||||
|
run = offsets[index] - offsets[index - 1]
|
||||||
|
if run <= 0:
|
||||||
|
continue
|
||||||
|
d0, d1 = diffs[index - 1], diffs[index]
|
||||||
|
# 성토 조각만 — 부호가 바뀌면 영교점까지만 성토다.
|
||||||
|
if d0 >= 0 and d1 >= 0:
|
||||||
|
continue
|
||||||
|
share = 1.0
|
||||||
|
if d0 * d1 < 0:
|
||||||
|
zero_ratio = d0 / (d0 - d1)
|
||||||
|
share = (1.0 - zero_ratio) if d0 > 0 else zero_ratio
|
||||||
|
if share <= 0:
|
||||||
|
continue
|
||||||
|
rise = grounds[index] - grounds[index - 1]
|
||||||
|
if abs(rise) / run < _BENCH_CUT_MIN_GROUND_SLOPE:
|
||||||
|
continue
|
||||||
|
total += ((run**2 + rise**2) ** 0.5) * share
|
||||||
|
return total
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from collections.abc import Callable
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from B06_Section.B06_Section_Engine_Areas import (
|
from B06_Section.B06_Section_Engine_Areas import (
|
||||||
|
_bench_cut_length,
|
||||||
_split_cut_areas,
|
_split_cut_areas,
|
||||||
_trapezoid_areas,
|
_trapezoid_areas,
|
||||||
)
|
)
|
||||||
@@ -684,17 +685,22 @@ def compute_cross_design(
|
|||||||
merged = sorted(set(round(offset, 6) for offset in merged))
|
merged = sorted(set(round(offset, 6) for offset in merged))
|
||||||
|
|
||||||
offsets: list[float] = []
|
offsets: list[float] = []
|
||||||
|
grounds: list[float] = []
|
||||||
diffs: list[float] = []
|
diffs: list[float] = []
|
||||||
design_line: list[dict[str, float]] = []
|
design_line: list[dict[str, float]] = []
|
||||||
for offset_m in merged:
|
for offset_m in merged:
|
||||||
ground_m = ground_at(offset_m)
|
ground_m = ground_at(offset_m)
|
||||||
design_z = geometry.design_z(offset_m, ground_m)
|
design_z = geometry.design_z(offset_m, ground_m)
|
||||||
offsets.append(offset_m)
|
offsets.append(offset_m)
|
||||||
|
grounds.append(ground_m)
|
||||||
diffs.append(ground_m - design_z)
|
diffs.append(ground_m - design_z)
|
||||||
design_line.append({"offset_m": round(offset_m, 4), "elevation_m": round(design_z, 4)})
|
design_line.append({"offset_m": round(offset_m, 4), "elevation_m": round(design_z, 4)})
|
||||||
|
|
||||||
# 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음 — 이중계상 방지).
|
# 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음 — 이중계상 방지).
|
||||||
cut_area, fill_area = _trapezoid_areas(offsets, diffs)
|
cut_area, fill_area = _trapezoid_areas(offsets, diffs)
|
||||||
|
# 층따기 밑수(길이 m) — 성토부 아래 원지반이 1:4 보다 급한 구간의 지표면 길이.
|
||||||
|
# 면적(㎡)은 측점 사이를 평균단면적법으로 이어 B08 이 낸다(2026-09-09 사용자 확정).
|
||||||
|
bench_cut_length = _bench_cut_length(offsets, grounds, diffs)
|
||||||
fill_ground_slope = geometry.fill_ground_slope()
|
fill_ground_slope = geometry.fill_ground_slope()
|
||||||
# 사면이 샘플 범위 끝에서도 원지반과 만나지 않으면 면적이 거기서 잘린다 — 그만큼
|
# 사면이 샘플 범위 끝에서도 원지반과 만나지 않으면 면적이 거기서 잘린다 — 그만큼
|
||||||
# 절·성토량이 실제와 다르고 유토곡선도 그 값을 그대로 쌓는다. 영원히 안 만나는
|
# 절·성토량이 실제와 다르고 유토곡선도 그 값을 그대로 쌓는다. 영원히 안 만나는
|
||||||
@@ -807,6 +813,10 @@ def compute_cross_design(
|
|||||||
"cut_rock_area_m2": round(cut_rock_area, 4),
|
"cut_rock_area_m2": round(cut_rock_area, 4),
|
||||||
"cut_rock_kind": cut_rock_kind,
|
"cut_rock_kind": cut_rock_kind,
|
||||||
"fill_area_m2": round(fill_area, 4),
|
"fill_area_m2": round(fill_area, 4),
|
||||||
|
# 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m).
|
||||||
|
# B08 이 측점 사이를 이어 ㎡ 로 만든다. 여기서 ㎥ 로 바꾸지 않는다 —
|
||||||
|
# 단의 높이·폭이 설계도서 값이라 지어낼 수 없다.
|
||||||
|
"bench_cut_length_m": round(bench_cut_length, 4),
|
||||||
# 사면이 샘플 범위 끝까지 원지반을 못 만나 면적이 잘린 측점 — 경고 표기용.
|
# 사면이 샘플 범위 끝까지 원지반을 못 만나 면적이 잘린 측점 — 경고 표기용.
|
||||||
"slope_unclosed": slope_unclosed,
|
"slope_unclosed": slope_unclosed,
|
||||||
# 성토측 자연 지반 경사(rise/run) — 자연방토 판정 입력. 성토측이 없으면 None.
|
# 성토측 자연 지반 경사(rise/run) — 자연방토 판정 입력. 성토측이 없으면 None.
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ interface ServerCalcInput {
|
|||||||
earthwork_conversion?: Parameters<typeof computeMassHaul>[1];
|
earthwork_conversion?: Parameters<typeof computeMassHaul>[1];
|
||||||
natural_spoil_min_ground_slope?: number | null;
|
natural_spoil_min_ground_slope?: number | null;
|
||||||
haul_equipment_limits?: Parameters<typeof computeHaulPlan>[1];
|
haul_equipment_limits?: Parameters<typeof computeHaulPlan>[1];
|
||||||
|
/** 채집석 공제(㎥, 양수) — B08 이 낸다. `null`/없음은 「아직 안 옴」이다. */
|
||||||
|
collected_stone_deduction_m3?: number | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +54,9 @@ const input = JSON.parse(readFileSync(inputPath, "utf8")) as ServerCalcInput;
|
|||||||
if (input.haul_plan_for) {
|
if (input.haul_plan_for) {
|
||||||
// **화면이 쓰는 꼴 그대로** 내보낸다(직렬화 형태 `haulPlanPayload` 가 아니다) — 그래야
|
// **화면이 쓰는 꼴 그대로** 내보낸다(직렬화 형태 `haulPlanPayload` 가 아니다) — 그래야
|
||||||
// 그리기 코드가 손대지 않고 그대로 받는다. 전부 숫자·문자열이라 JSON 으로 오간다.
|
// 그리기 코드가 손대지 않고 그대로 받는다. 전부 숫자·문자열이라 JSON 으로 오간다.
|
||||||
const plan = computeHaulPlan(input.haul_plan_for, input.context?.haul_equipment_limits);
|
const plan = computeHaulPlan(input.haul_plan_for, input.context?.haul_equipment_limits, {
|
||||||
|
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
||||||
|
});
|
||||||
writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null }));
|
writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null }));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
@@ -73,7 +77,11 @@ const result = conversion
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
// 배분은 **서버만** 만든다 — 그래야 그 코드가 브라우저 번들에서 빠진다(2026-09-06).
|
// 배분은 **서버만** 만든다 — 그래야 그 코드가 브라우저 번들에서 빠진다(2026-09-06).
|
||||||
const plan = result ? computeHaulPlan(result, input.context?.haul_equipment_limits) : null;
|
const plan = result
|
||||||
|
? computeHaulPlan(result, input.context?.haul_equipment_limits, {
|
||||||
|
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
const massHaul = result
|
const massHaul = result
|
||||||
? massHaulPayload(result, plan ? { haul_plan: haulPlanPayload(plan) } : null)
|
? massHaulPayload(result, plan ? { haul_plan: haulPlanPayload(plan) } : null)
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -63,8 +63,18 @@ _AREA_KEYS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _mass_haul_context() -> dict[str, Any]:
|
def _mass_haul_context(collected_stone_deduction_m3: float | None = None) -> dict[str, Any]:
|
||||||
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다."""
|
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다.
|
||||||
|
|
||||||
|
⚠ 채집석 공제(`collected_stone_deduction_m3`)만 상수가 아니라 **B08 이 내는 값**이다.
|
||||||
|
`None` 은 「아직 안 옴」이고 `0` 은 「공제 없음」이라 **서로 다르다** — 값이 안 온 것을
|
||||||
|
공제 0 으로 읽으면 조용히 넘어간다(2026-09-09 네 창 합의).
|
||||||
|
|
||||||
|
채집석 공제는 사토에서 한 번만 뺀다.
|
||||||
|
B08 은 소요량(collected_stone_deduction_m3, ㎥ 양수)을 내기만 하고 공제하지 않으며,
|
||||||
|
빼는 자리는 유토곡선의 사토뿐이다 —
|
||||||
|
실어 내는 몫(spoil_m3 − natural_spoil_m3)에서 먼저 빼고 모자라면 자연방토에서 뺀다.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"earthwork_conversion": EARTHWORK_CONVERSION_FACTORS,
|
"earthwork_conversion": EARTHWORK_CONVERSION_FACTORS,
|
||||||
"natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
"natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
||||||
@@ -72,6 +82,9 @@ def _mass_haul_context() -> dict[str, Any]:
|
|||||||
{"key": key, "max_distance_m": limit}
|
{"key": key, "max_distance_m": limit}
|
||||||
for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M
|
for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M
|
||||||
],
|
],
|
||||||
|
# ⚠ B08 이 아직 이 값을 내지 않는다(2026-09-09) — 그때까지 `None`(아직 안 옴)이다.
|
||||||
|
# 값을 내기 시작하면 여기에 실어 주기만 하면 통로가 이어진다.
|
||||||
|
"collected_stone_deduction_m3": collected_stone_deduction_m3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type { BermSpec } from "./common_util_cross_berm";
|
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).
|
// 단면 기하(노면·측구·사면 설계고)는 파일이 700줄을 넘어 떼어냈다(2026-09-04).
|
||||||
import {
|
import {
|
||||||
CURVE_WIDENING_MAX_WIDTH_M,
|
CURVE_WIDENING_MAX_WIDTH_M,
|
||||||
@@ -131,6 +131,8 @@ export interface CrossDesignResult {
|
|||||||
cut_rock_area_m2: number;
|
cut_rock_area_m2: number;
|
||||||
cut_rock_kind: string | null;
|
cut_rock_kind: string | null;
|
||||||
fill_area_m2: number;
|
fill_area_m2: number;
|
||||||
|
/** 층따기 밑수 — 성토부 아래 원지반(1:4 보다 급한 구간)의 지표면 길이(m). */
|
||||||
|
bench_cut_length_m: number;
|
||||||
slope_unclosed: boolean;
|
slope_unclosed: boolean;
|
||||||
fill_ground_slope: number | null;
|
fill_ground_slope: number | null;
|
||||||
ditch_area_m2: number;
|
ditch_area_m2: number;
|
||||||
@@ -331,18 +333,22 @@ export function computeCrossDesign(
|
|||||||
const merged = [...mergedSet].sort((a, b) => a - b);
|
const merged = [...mergedSet].sort((a, b) => a - b);
|
||||||
|
|
||||||
const offsets: number[] = [];
|
const offsets: number[] = [];
|
||||||
|
const grounds: number[] = [];
|
||||||
const diffs: number[] = [];
|
const diffs: number[] = [];
|
||||||
const designLine: CrossDesignEdge[] = [];
|
const designLine: CrossDesignEdge[] = [];
|
||||||
for (const offsetM of merged) {
|
for (const offsetM of merged) {
|
||||||
const groundM = groundAt(offsetM);
|
const groundM = groundAt(offsetM);
|
||||||
const designZ = geometry.designZ(offsetM, groundM);
|
const designZ = geometry.designZ(offsetM, groundM);
|
||||||
offsets.push(offsetM);
|
offsets.push(offsetM);
|
||||||
|
grounds.push(groundM);
|
||||||
diffs.push(groundM - designZ);
|
diffs.push(groundM - designZ);
|
||||||
designLine.push({ offset_m: round4(offsetM), elevation_m: round4(designZ) });
|
designLine.push({ offset_m: round4(offsetM), elevation_m: round4(designZ) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음).
|
// 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음).
|
||||||
const [cutArea, fillArea] = trapezoidAreas(offsets, diffs);
|
const [cutArea, fillArea] = trapezoidAreas(offsets, diffs);
|
||||||
|
// 층따기 밑수(길이 m) — 성토부 아래 원지반이 1:4 보다 급한 구간의 지표면 길이.
|
||||||
|
const benchCut = benchCutLength(offsets, grounds, diffs);
|
||||||
const fillGroundSlope = geometry.fillGroundSlope();
|
const fillGroundSlope = geometry.fillGroundSlope();
|
||||||
const slopeUnclosed =
|
const slopeUnclosed =
|
||||||
diffs.length > 0 &&
|
diffs.length > 0 &&
|
||||||
@@ -447,6 +453,7 @@ export function computeCrossDesign(
|
|||||||
cut_rock_area_m2: round4(cutRockArea),
|
cut_rock_area_m2: round4(cutRockArea),
|
||||||
cut_rock_kind: cutRockKind,
|
cut_rock_kind: cutRockKind,
|
||||||
fill_area_m2: round4(fillArea),
|
fill_area_m2: round4(fillArea),
|
||||||
|
bench_cut_length_m: round4(benchCut),
|
||||||
slope_unclosed: slopeUnclosed,
|
slope_unclosed: slopeUnclosed,
|
||||||
fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope),
|
fill_ground_slope: fillGroundSlope === null ? null : round4(fillGroundSlope),
|
||||||
ditch_area_m2: round4(ditchArea),
|
ditch_area_m2: round4(ditchArea),
|
||||||
|
|||||||
@@ -95,3 +95,34 @@ export function splitCutAreas(
|
|||||||
}
|
}
|
||||||
return [soilArea, rockArea];
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -192,6 +192,13 @@ export interface HaulPlan {
|
|||||||
borrow_m3: number;
|
borrow_m3: number;
|
||||||
/** 사토 중 자연방토 몫(㎥) — 운반비를 세지 않는다. */
|
/** 사토 중 자연방토 몫(㎥) — 운반비를 세지 않는다. */
|
||||||
natural_spoil_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;
|
hauled_m3: number;
|
||||||
/** 떨어진 구간끼리 장거리로 옮기는 양(㎥). */
|
/** 떨어진 구간끼리 장거리로 옮기는 양(㎥). */
|
||||||
@@ -316,9 +323,54 @@ function bandOutline(
|
|||||||
* 누가토량 곡선에서 토량 분배(평형선·운반 블록·장비 띠·사토/토취)를 뽑는다.
|
* 누가토량 곡선에서 토량 분배(평형선·운반 블록·장비 띠·사토/토취)를 뽑는다.
|
||||||
* 블록도 잔량도 안 나오면(평탄한 곡선) null.
|
* 블록도 잔량도 안 나오면(평탄한 곡선) 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(
|
export function computeHaulPlan(
|
||||||
result: MassHaulResult,
|
result: MassHaulResult,
|
||||||
limits: HaulEquipmentLimit[] | undefined,
|
limits: HaulEquipmentLimit[] | undefined,
|
||||||
|
options?: { collected_stone_deduction_m3?: number | null },
|
||||||
): HaulPlan | null {
|
): HaulPlan | null {
|
||||||
const points = result.points;
|
const points = result.points;
|
||||||
if (points.length < 2) return null;
|
if (points.length < 2) return null;
|
||||||
@@ -497,10 +549,17 @@ export function computeHaulPlan(
|
|||||||
residual.index = index + 1;
|
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 spoil = 0;
|
||||||
let borrow = 0;
|
let borrow = 0;
|
||||||
let naturalSpoil = 0;
|
let naturalSpoil = 0;
|
||||||
for (const residual of settled) {
|
for (const residual of remaining) {
|
||||||
if (residual.kind === "spoil") {
|
if (residual.kind === "spoil") {
|
||||||
spoil += residual.volume_m3;
|
spoil += residual.volume_m3;
|
||||||
naturalSpoil += residual.natural_m3;
|
naturalSpoil += residual.natural_m3;
|
||||||
@@ -510,12 +569,14 @@ export function computeHaulPlan(
|
|||||||
for (const point of points) fillTotal += point.fill_m3;
|
for (const point of points) fillTotal += point.fill_m3;
|
||||||
return {
|
return {
|
||||||
blocks,
|
blocks,
|
||||||
residuals: settled,
|
residuals: remaining,
|
||||||
transfers,
|
transfers,
|
||||||
steps,
|
steps,
|
||||||
spoil_m3: spoil,
|
spoil_m3: spoil,
|
||||||
borrow_m3: borrow,
|
borrow_m3: borrow,
|
||||||
natural_spoil_m3: naturalSpoil,
|
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),
|
hauled_m3: blocks.reduce((sum, block) => sum + block.volume_m3, 0),
|
||||||
transferred_m3: transfers.reduce((sum, entry) => sum + entry.volume_m3, 0),
|
transferred_m3: transfers.reduce((sum, entry) => sum + entry.volume_m3, 0),
|
||||||
fill_total_m3: fillTotal,
|
fill_total_m3: fillTotal,
|
||||||
|
|||||||
Reference in New Issue
Block a user