- common_util_mass_haul_balance 842 → 467 (_types · _apply 분리 · 원래 파일에서 타입 다시 내보냄) - B05_Profile_UI_Profile_Table 702 → 548 (_Table_Selected 분리) - test_507_3 는 옮긴 글을 새 파일에서 찾음 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
470 lines
20 KiB
TypeScript
470 lines
20 KiB
TypeScript
/* =============================================================================
|
||
* common_util_mass_haul_balance.ts
|
||
* 토량 분배(평형선 · 운반 블록 · 장비 띠) 계산 엔진 — 순수 함수, DOM 의존 없음.
|
||
*
|
||
* 누가토량 곡선(`common_util_mass_haul`)은 "얼마가 남는가"만 말한다. 이 파일은 그 곡선에서
|
||
* **어디 흙을 어디로 얼마나 옮기는가**를 뽑아 낸다: 평형선 → 운반 블록 → 장비 띠 →
|
||
* 띠별 운반토량 `Q` · 평균운반거리 `L` · 지반유형 안분.
|
||
*
|
||
* ── 작도 규칙(참고자료) ─────────────────────────────────────────────
|
||
* <https://clouds-daily.tistory.com/249>
|
||
* - 상승 = 절토, 하강 = 성토. 극대·극소점 = 절·성토 경계.
|
||
* - 산 모양 = 좌→우 운반, 골 모양 = 우→좌 운반.
|
||
* - 평형선(수평선)이 곡선을 자르는 두 점 사이는 절토량 = 성토량.
|
||
* - 운반토량 = 평형선에서 곡선 정점까지의 종거.
|
||
* - 평균운반거리 = **종거의 1/2 지점을 지나는 수평선 B′C′의 길이**(반종거 수평선).
|
||
* - 배분 3원칙 = ① 운반거리 최소 ② 높은 곳 → 낮은 곳 ③ 토량을 모아 운반.
|
||
*
|
||
* ── 평형선을 어떻게 자동으로 잡는가 ─────────────────────────────────
|
||
* 참고자료는 "가장 유리한 평형점을 도상에서 구한다"까지만 말한다. **0선 하나로 고정하면
|
||
* 안 된다** — 임도는 절토가 크게 우세해 사토가 전부 종점 한 덩어리로 몰린다. 그래서 곡선을
|
||
* 왼쪽부터 훑으며 평형선 높이를 **계단으로 올린다**:
|
||
*
|
||
* 커서(곡선 위, 높이 = 현재 평형선)에서 다음 극값을 본다.
|
||
* - 올라갔다가(절토) 다시 현재 높이 **아래**까지 내려오면 → 성토가 절토를 다 받는다.
|
||
* 평형선은 그대로, 블록은 [커서 … 곡선이 평형선으로 되돌아온 점].
|
||
* - 올라갔다가 현재 높이 **위**에서 멈추면 → 남는 절토가 생긴다. 평형선을 그 골 높이로
|
||
* 올리고, 못 받은 만큼을 **그 자리에서** 사토로 계상한다.
|
||
* - 내려가는 경우는 정확히 대칭(토취).
|
||
*
|
||
* 이렇게 하면 ① 구간이 겹치거나 비지 않고 ② 사토·토취가 발생 지점에 남으며
|
||
* ③ 단차의 합이 최종 누가토량과 정확히 일치한다.
|
||
*
|
||
* ── 블록 하나를 장비 띠로 가르는 이유 ───────────────────────────────
|
||
* 참고 도면의 토량배분도에는 수평선이 블록마다 여러 개 그려져 있고(A–A′ / B–B′ / C–C′),
|
||
* 그 사이 띠마다 번호가 붙어 종단면의 해칭 구간과 1:1로 대응한다. 하단 브래킷
|
||
* (무대운반거리 / 도자 평균운반거리 / 덤프 평균운반거리)의 폭이 각 수평선의 길이와 같다.
|
||
*
|
||
* 즉 **수평선의 높이는 그 현(弦)의 길이가 장비 경계거리와 같아지는 높이**다. 누가토량
|
||
* 그래프에서 세로축이 곧 토량이므로, 두 수평선 사이 띠의 두께가 그 장비가 옮기는 토량이다.
|
||
* 현 길이 20m 되는 높이 위쪽 → 종무대
|
||
* 거기서 60m 되는 높이까지 → 도쟈
|
||
* 그 아래 ~ 평형선 → 덤프
|
||
* 경계값 정의처는 `config_system.EARTHWORK_HAUL_EQUIPMENT_LIMITS_M` 한 곳뿐이다.
|
||
*
|
||
* ── 단위 ─────────────────────────────────────────────────────────
|
||
* 곡선이 다짐상태 기준이므로 `Q`·EA/RR/BR 안분도 **다짐상태**다. 내역서용 자연상태 수량은
|
||
* `MassHaulResult.cut_natural_m3` / 측점별 `cut_soil_m3`·`cut_rr_m3`·`cut_br_m3`가 따로 든다.
|
||
* ========================================================================== */
|
||
|
||
import type { MassHaulPoint, MassHaulResult } from "./common_util_mass_haul";
|
||
import type { EarthworkConversion } from "./common_util_mass_haul_types";
|
||
import {
|
||
crossFrom,
|
||
EPSILON,
|
||
evaluate,
|
||
extremaIndices,
|
||
pruneExtrema,
|
||
segmentOf,
|
||
} from "./common_util_mass_haul_curve";
|
||
import {
|
||
apportion,
|
||
cutMix,
|
||
naturalSpoilRatio,
|
||
settleResiduals,
|
||
sortedLimits,
|
||
} from "./common_util_mass_haul_settle";
|
||
|
||
export { haulPlanPayload } from "./common_util_mass_haul_settle";
|
||
import type {
|
||
BalanceStep,
|
||
CurvePoint,
|
||
HaulBand,
|
||
HaulBlock,
|
||
HaulDirection,
|
||
HaulEquipmentLimit,
|
||
HaulPlan,
|
||
HaulResidual,
|
||
} from "./common_util_mass_haul_balance_types";
|
||
import {
|
||
applyCollectedStoneDeduction,
|
||
applyStructureSpoil,
|
||
} from "./common_util_mass_haul_balance_apply";
|
||
|
||
export type {
|
||
BalanceStep,
|
||
CurvePoint,
|
||
HaulBand,
|
||
HaulBlock,
|
||
HaulDirection,
|
||
HaulEquipmentLimit,
|
||
HaulPlan,
|
||
HaulResidual,
|
||
HaulTransfer,
|
||
} from "./common_util_mass_haul_balance_types";
|
||
|
||
/**
|
||
* **그림에서만** 이보다 작은 진동은 블록으로 세지 않는다. 측점 하나짜리 요철까지 블록을 만들면
|
||
* balloon이 수십 개 깔려 도면을 못 읽는다. 곡선 진폭 대비 비율이라 노선 규모에 자동으로 맞는다.
|
||
* ⚠ 수량 계획에는 걸지 않는다 — 진폭이 커지면 작은 봉우리가 통째로 지워져 운반량이 0 이 됐다
|
||
* (2026-09-14 936be972 실측 474.14㎥ → 0 · 브레인 ①).
|
||
*/
|
||
const MIN_SWING_RATIO = 0.02;
|
||
|
||
/** 경계현 높이를 훑을 때 쪼개는 칸 수. 잔요철에 속지 않을 만큼 촘촘해야 한다. */
|
||
const CHORD_SCAN_STEPS = 64;
|
||
/** 훑어서 찾은 칸 안에서 좁히는 이분법 반복 수. 24회면 칸 폭의 1/1600만 남는다. */
|
||
const CHORD_SOLVE_STEPS = 24;
|
||
/** 띠 테두리를 곡선(포물선)에 붙이려고 구간 하나를 쪼개는 수. */
|
||
const OUTLINE_STEPS = 4;
|
||
|
||
/** 한 블록을 장비 띠로 가르는 데 필요한 기하 계산 묶음. */
|
||
interface BlockGeometry {
|
||
/** `ratio`(0 = 평형선, 1 = 정점)에 해당하는 누가토량 높이. */
|
||
levelAt: (ratio: number) => number;
|
||
/** 그 높이 수평선이 블록 안에서 곡선을 자르는 두 점. */
|
||
chordAt: (ratio: number) => { from: number; to: number; length: number };
|
||
}
|
||
|
||
function blockGeometry(
|
||
points: MassHaulPoint[],
|
||
fromM: number,
|
||
toM: number,
|
||
apexM: number,
|
||
base: number,
|
||
apex: number,
|
||
): BlockGeometry {
|
||
const levelAt = (ratio: number): number => base + (apex - base) * ratio;
|
||
const chordAt = (ratio: number): { from: number; to: number; length: number } => {
|
||
const level = levelAt(ratio);
|
||
const from = crossFrom(points, fromM, apexM, level) ?? fromM;
|
||
const to = crossFrom(points, apexM, toM, level) ?? toM;
|
||
return { from, to, length: Math.max(to - from, 0) };
|
||
};
|
||
return { levelAt, chordAt };
|
||
}
|
||
|
||
/**
|
||
* 현의 길이가 `target` **이하로 확정되는** 가장 낮은 높이를 찾는다(반환값은 `ratio`).
|
||
*
|
||
* 현 길이는 평형선(가장 김)에서 정점(0)으로 갈수록 줄지만 **단조가 아니다** — 가지치기로
|
||
* 블록을 하나로 묶어도 그 안의 원래 폴리라인에는 잔요철이 남아, 높이를 올리는 도중 현이
|
||
* 잠깐 다시 길어질 수 있다. 이분법만 쓰면 그 지점에서 엉뚱한 근으로 수렴한다(무작위 검증에서
|
||
* 종무대 경계현이 45m로 잡히는 사례를 잡았다).
|
||
*
|
||
* 그래서 **훑기 → 이분법** 두 단계로 간다: 먼저 전 구간을 성기게 훑어 현이 `target`을 넘는
|
||
* **가장 높은** 자리를 찾고, 그 바로 위 칸에서만 이분법으로 좁힌다. 이렇게 잡은 경계 위쪽은
|
||
* 전부 `target` 이하가 보장된다 — "이 띠는 이 장비로 옮길 수 있다"가 성립해야 하기 때문이다.
|
||
* 평형선에서 이미 `target` 이하면 그 장비가 블록 전체를 맡는다 → 0.
|
||
*/
|
||
function ratioForChord(geometry: BlockGeometry, target: number): number {
|
||
let lastOver = -1;
|
||
for (let step = 0; step <= CHORD_SCAN_STEPS; step += 1) {
|
||
if (geometry.chordAt(step / CHORD_SCAN_STEPS).length > target) lastOver = step;
|
||
}
|
||
if (lastOver < 0) return 0;
|
||
if (lastOver >= CHORD_SCAN_STEPS) return 1;
|
||
let low = lastOver / CHORD_SCAN_STEPS;
|
||
let high = (lastOver + 1) / CHORD_SCAN_STEPS;
|
||
for (let step = 0; step < CHORD_SOLVE_STEPS; step += 1) {
|
||
const middle = (low + high) / 2;
|
||
if (geometry.chordAt(middle).length > target) low = middle;
|
||
else high = middle;
|
||
}
|
||
return high;
|
||
}
|
||
|
||
/**
|
||
* 띠를 감싸는 다각형.
|
||
*
|
||
* 예전에는 「아래 현 → 왼쪽 곡선 → 위 현 → 오른쪽 곡선」을 이어 붙였는데, 곡선이 두 현 사이에서
|
||
* 요동치면 그 조각이 띠 밖으로 삐져나가 **면이 스스로 겹쳤다**(2026-08-02 사용자 지적).
|
||
*
|
||
* 정의를 바꿨다. 띠는 「아래 현의 두 끝 사이에서, 아래 높이부터 **곡선과 위 높이 중 안쪽**까지」다.
|
||
* 아래 현을 밑변으로 깔고 그 위를 훑으며 곡선값을 두 높이 사이로 **자른 값**만 찍는다.
|
||
* 이러면 어떤 지형에서도 단순 다각형이라 스스로 겹칠 수가 없다.
|
||
*/
|
||
function bandOutline(
|
||
points: MassHaulPoint[],
|
||
low: { from: number; to: number },
|
||
lowLevel: number,
|
||
highLevel: number,
|
||
): CurvePoint[] {
|
||
const floor = Math.min(lowLevel, highLevel);
|
||
const ceil = Math.max(lowLevel, highLevel);
|
||
const outline: CurvePoint[] = [{ m: low.from, v: lowLevel }];
|
||
const push = (m: number, v: number): void => {
|
||
const previous = outline[outline.length - 1];
|
||
if (previous && Math.abs(previous.m - m) < 1e-9 && Math.abs(previous.v - v) < 1e-9) return;
|
||
outline.push({ m, v });
|
||
};
|
||
for (let index = 1; index < points.length; index += 1) {
|
||
const segment = segmentOf(points, index);
|
||
if (!segment) continue;
|
||
const start = Math.max(segment.x0, low.from);
|
||
const end = Math.min(segment.x0 + segment.span, low.to);
|
||
if (!(end > start)) continue;
|
||
for (let step = 0; step <= OUTLINE_STEPS; step += 1) {
|
||
const m = start + ((end - start) * step) / OUTLINE_STEPS;
|
||
push(m, Math.min(Math.max(evaluate(segment, m - segment.x0), floor), ceil));
|
||
}
|
||
}
|
||
push(low.to, lowLevel);
|
||
return outline;
|
||
}
|
||
|
||
/**
|
||
* 누가토량 곡선에서 토량 분배(평형선·운반 블록·장비 띠·사토/토취)를 뽑는다.
|
||
* 블록도 잔량도 안 나오면(평탄한 곡선) null.
|
||
*/
|
||
|
||
export function computeHaulPlan(
|
||
result: MassHaulResult,
|
||
limits: HaulEquipmentLimit[] | undefined,
|
||
options?: {
|
||
collected_stone_deduction_m3?: number | null;
|
||
/** 갈래별 채집석(㎥) — **벽 입적(자연 축)**이라 여기서 ×C 해 다짐 축에 맞춘다. */
|
||
collected_stone_by_ground_m3?: Record<string, number> | null;
|
||
/** 갈래를 못 가른 채집석(㎥) — 계수가 없어 **환산하지 않고** 그대로 뺀다. */
|
||
collected_stone_ground_unknown_m3?: number | null;
|
||
structure_spoil_m3?: number | null;
|
||
/** 측점별 구조물 잔토 — 오면 **이쪽이 이긴다**(그 자리 잔량에 얹어 운반거리를 맞춘다). */
|
||
structure_spoil_points?: Array<{
|
||
chainage_m: number;
|
||
spoil_m3: number;
|
||
/** 그 터파기의 토질 — B08 이 `design.ground_type` 으로 이미 판정한 값(품셈 9-13 3구분).
|
||
* **새 근거를 만드는 것이 아니라 이어받는 것**이다. 섞여서 못 고른 구조물은 `null` 로
|
||
* 오고, 그 몫은 「지반 모름」(`ground_unknown_m3`)으로 남는다. */
|
||
ground_type?: string | null;
|
||
ground_label?: string | null;
|
||
ground?: string | null;
|
||
}> | null;
|
||
/** 토량환산계수 — 구조물 잔토(자연상태)를 이 곡선의 **다짐상태**로 옮길 때만 쓴다.
|
||
* 안 오면 환산 없이 담긴다(값을 지어내지 않는다). */
|
||
conversion?: EarthworkConversion | null;
|
||
/** 그림용 — 잔진동을 거른다(`MIN_SWING_RATIO`). 안 오면 **거르지 않는다**(수량 계획). */
|
||
drawing?: boolean;
|
||
},
|
||
): HaulPlan | null {
|
||
const points = result.points;
|
||
if (points.length < 2) return null;
|
||
|
||
const range = Math.max(result.max_cumulative_m3 - result.min_cumulative_m3, 0);
|
||
const extrema = options?.drawing
|
||
? pruneExtrema(points, extremaIndices(points), Math.max(range * MIN_SWING_RATIO, 1))
|
||
: extremaIndices(points);
|
||
const tiers = sortedLimits(limits);
|
||
|
||
const blocks: HaulBlock[] = [];
|
||
const residuals: HaulResidual[] = [];
|
||
const steps: BalanceStep[] = [];
|
||
let bandCounter = 0;
|
||
|
||
const pushBlock = (
|
||
fromM: number,
|
||
toM: number,
|
||
base: number,
|
||
apexM: number,
|
||
apex: number,
|
||
direction: HaulDirection,
|
||
): void => {
|
||
const volume = Math.abs(apex - base);
|
||
if (!(volume > EPSILON) || !(toM > fromM)) return;
|
||
const geometry = blockGeometry(points, fromM, toM, apexM, base, apex);
|
||
|
||
// 정점 쪽(가까운 장비)부터 평형선 쪽으로 내려오며 띠를 자른다.
|
||
const bands: HaulBand[] = [];
|
||
let upper = 1;
|
||
for (const tier of tiers) {
|
||
if (upper <= EPSILON) break;
|
||
const lower = tier.max_distance_m === null ? 0 : ratioForChord(geometry, tier.max_distance_m);
|
||
if (upper - lower <= 1e-6) {
|
||
upper = lower;
|
||
continue;
|
||
}
|
||
const lowChord = geometry.chordAt(lower);
|
||
const highChord = geometry.chordAt(upper);
|
||
const haulChord = geometry.chordAt((lower + upper) / 2);
|
||
// 절토 쪽 변에서 잘라 낸 구간이 이 띠가 퍼오는 흙이다(산이면 왼쪽 변, 골이면 오른쪽 변).
|
||
const cutFrom = direction === "forward" ? lowChord.from : highChord.to;
|
||
const cutTo = direction === "forward" ? highChord.from : lowChord.to;
|
||
const bandVolume = (upper - lower) * volume;
|
||
bandCounter += 1;
|
||
bands.push({
|
||
index: bandCounter,
|
||
equipment: tier.key || null,
|
||
level_base_m3: geometry.levelAt(lower),
|
||
level_apex_m3: geometry.levelAt(upper),
|
||
volume_m3: bandVolume,
|
||
boundary_from_m: lowChord.from,
|
||
boundary_to_m: lowChord.to,
|
||
haul_distance_m: haulChord.length,
|
||
haul_from_m: haulChord.from,
|
||
haul_to_m: haulChord.to,
|
||
outline: bandOutline(points, lowChord, geometry.levelAt(lower), geometry.levelAt(upper)),
|
||
...apportion(cutMix(points, cutFrom, cutTo, result.conversion), bandVolume),
|
||
});
|
||
upper = lower;
|
||
}
|
||
|
||
blocks.push({
|
||
index: blocks.length + 1,
|
||
base_m3: base,
|
||
from_m: fromM,
|
||
to_m: toM,
|
||
apex_m: apexM,
|
||
apex_volume_m3: apex,
|
||
volume_m3: volume,
|
||
direction,
|
||
bands,
|
||
});
|
||
steps.push({ from_m: fromM, to_m: toM, level_m3: base });
|
||
};
|
||
|
||
const pushResidual = (fromM: number, toM: number, from: number, to: number): void => {
|
||
const delta = to - from;
|
||
if (Math.abs(delta) < EPSILON) return;
|
||
const volume = Math.abs(delta);
|
||
residuals.push({
|
||
index: residuals.length + 1,
|
||
kind: delta > 0 ? "spoil" : "borrow",
|
||
from_m: fromM,
|
||
to_m: toM,
|
||
volume_m3: volume,
|
||
level_from_m3: from,
|
||
level_to_m3: to,
|
||
// 자연방토는 사토에만 해당한다 — 토취는 밖에서 사 오는 흙이다.
|
||
natural_m3: delta > 0 ? volume * naturalSpoilRatio(points, fromM, toM) : 0,
|
||
// 사토만 지반유형을 물을 수 있다(절토에서 남은 흙). 토취는 밖에서 사 온다.
|
||
...(delta > 0
|
||
? apportion(cutMix(points, fromM, toM, result.conversion), volume)
|
||
: { ea_m3: 0, rr_m3: 0, br_m3: 0 }),
|
||
});
|
||
};
|
||
|
||
let level = points[0].cumulative_volume_m3;
|
||
let cursor = points[0].chainage_m;
|
||
let k = 1;
|
||
|
||
while (k < extrema.length) {
|
||
const apexX = points[extrema[k]].chainage_m;
|
||
const apexY = points[extrema[k]].cumulative_volume_m3;
|
||
if (apexX <= cursor + EPSILON) {
|
||
k += 1;
|
||
continue;
|
||
}
|
||
const rising = apexY > level + EPSILON;
|
||
const falling = apexY < level - EPSILON;
|
||
if (!rising && !falling) {
|
||
cursor = apexX;
|
||
k += 1;
|
||
continue;
|
||
}
|
||
|
||
// 짝이 되는 다음 극값이 없으면 받아 줄 성토(또는 절토)가 없다 — 전량 사토/토취.
|
||
if (k + 1 >= extrema.length) {
|
||
steps.push({ from_m: cursor, to_m: apexX, level_m3: level });
|
||
pushResidual(cursor, apexX, level, apexY);
|
||
level = apexY;
|
||
cursor = apexX;
|
||
k += 1;
|
||
continue;
|
||
}
|
||
const nextX = points[extrema[k + 1]].chainage_m;
|
||
const nextY = points[extrema[k + 1]].cumulative_volume_m3;
|
||
|
||
if (rising) {
|
||
if (nextY <= level + EPSILON) {
|
||
// 성토가 절토를 다 받는다 — 평형선 그대로, 곡선이 되돌아온 점에서 블록을 닫는다.
|
||
const end = crossFrom(points, apexX, nextX, level) ?? nextX;
|
||
pushBlock(cursor, end, level, apexX, apexY, "forward");
|
||
cursor = end;
|
||
k += 1;
|
||
} else {
|
||
// 못 받은 절토가 남는다 — 평형선을 그 골 높이로 올리고 차액을 사토로 계상한다.
|
||
const start = crossFrom(points, cursor, apexX, nextY) ?? cursor;
|
||
steps.push({ from_m: cursor, to_m: start, level_m3: level });
|
||
pushResidual(cursor, start, level, nextY);
|
||
pushBlock(start, nextX, nextY, apexX, apexY, "forward");
|
||
level = nextY;
|
||
cursor = nextX;
|
||
k += 2;
|
||
}
|
||
} else if (nextY >= level - EPSILON) {
|
||
// 골 — 오른쪽 절토가 왼쪽 성토를 채운다(우→좌 운반).
|
||
const end = crossFrom(points, apexX, nextX, level) ?? nextX;
|
||
pushBlock(cursor, end, level, apexX, apexY, "backward");
|
||
cursor = end;
|
||
k += 1;
|
||
} else {
|
||
// 절토가 모자라 성토를 못 채운다 — 평형선을 내리고 차액을 토취로 계상한다.
|
||
const start = crossFrom(points, cursor, apexX, nextY) ?? cursor;
|
||
steps.push({ from_m: cursor, to_m: start, level_m3: level });
|
||
pushResidual(cursor, start, level, nextY);
|
||
pushBlock(start, nextX, nextY, apexX, apexY, "backward");
|
||
level = nextY;
|
||
cursor = nextX;
|
||
k += 2;
|
||
}
|
||
}
|
||
|
||
// 극값 가지치기로 잘려 나간 꼬리가 있으면 총량이 어긋난다 — 종점 잔량으로 흡수한다.
|
||
const endX = points[points.length - 1].chainage_m;
|
||
const endY = points[points.length - 1].cumulative_volume_m3;
|
||
if (Math.abs(endY - level) > EPSILON) {
|
||
steps.push({ from_m: cursor, to_m: endX, level_m3: level });
|
||
pushResidual(cursor, endX, level, endY);
|
||
}
|
||
|
||
if (!blocks.length && !residuals.length) return null;
|
||
|
||
const transfers = settleResiduals(points, residuals, result.conversion, limits);
|
||
const settled = residuals.filter((residual) => residual.volume_m3 > EPSILON);
|
||
settled.forEach((residual, index) => {
|
||
residual.index = index + 1;
|
||
});
|
||
|
||
// ⚠ **순서가 뜻을 가른다 — 「잔토를 더하고 → 공제를 뺀다」**(2026-09-09 확정).
|
||
// 유토곡선의 사토는 「현장에 남는 흙 총량」이고 **출처를 안 가린다**. 공제는 그 총량에서
|
||
// 빼는 것이라 **잔토가 담긴 뒤라야 뺄 대상이 있다.** 반대 순서로 두면 사토가 0 인
|
||
// 노선(전 구간 토취)에서 공제가 **영영 안 걸린다** — 두 창 저장분에서 실제로 그랬다.
|
||
const structureSpoilInput = options?.structure_spoil_m3 ?? null;
|
||
const structureSpoilPoints = options?.structure_spoil_points ?? null;
|
||
const structureSpoilAdded = applyStructureSpoil(
|
||
settled,
|
||
structureSpoilInput,
|
||
structureSpoilPoints,
|
||
options?.conversion ?? null,
|
||
);
|
||
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
|
||
const deducted = applyCollectedStoneDeduction(
|
||
settled,
|
||
deductionInput,
|
||
options?.collected_stone_by_ground_m3 ?? null,
|
||
options?.collected_stone_ground_unknown_m3 ?? null,
|
||
options?.conversion ?? null,
|
||
);
|
||
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 remaining) {
|
||
if (residual.kind === "spoil") {
|
||
spoil += residual.volume_m3;
|
||
naturalSpoil += residual.natural_m3;
|
||
} else borrow += residual.volume_m3;
|
||
}
|
||
let fillTotal = 0;
|
||
for (const point of points) fillTotal += point.fill_m3;
|
||
return {
|
||
blocks,
|
||
residuals: remaining,
|
||
transfers,
|
||
steps,
|
||
spoil_m3: spoil,
|
||
borrow_m3: borrow,
|
||
natural_spoil_m3: naturalSpoil,
|
||
collected_stone_deduction_m3: deductionInput,
|
||
collected_stone_deducted_m3: deducted,
|
||
structure_spoil_m3: structureSpoilInput,
|
||
structure_spoil_added_m3: structureSpoilAdded,
|
||
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,
|
||
};
|
||
}
|