fix(B06): 채집석 공제도 축을 맞춰 뺌 — 갈래마다 ×C

채집석은 **벽 입적(제자리 부피)**이라 자연 축이고 유토곡선은 다짐 축임.
축이 다른 값을 그냥 빼고 있었음(리핑암이면 13% 어긋남).

- 갈래별 값(`collected_stone_by_ground_m3`)이 오면 갈래마다 ×C 해서 뺌
- 갈래를 못 가른 몫은 계수가 없어 **환산하지 않고** 그대로 뺌
- 갈래가 아예 안 오면 종전처럼 총량을 그대로 뺌 (안 온 것과 0 은 다름)
- 통로 정리 — `_mass_haul_context` 가 B08 입력 dict 를 통째로 받게 함
  (칸이 늘 때마다 인자를 늘리다 빠뜨리던 자리)

⚠ 벽 입적과 원바닥 암 부피의 관계를 정한 원문이 없음 — 그 가정은 그대로 두고
**축만** 맞춘 것임(세 창 합의, 근거 문구는 B08 이 값과 함께 보냄).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 07:27:02 +09:00
co-authored by Claude Opus 5
parent 652dbcbd12
commit 1379b7fd69
4 changed files with 73 additions and 23 deletions
+54 -5
View File
@@ -348,10 +348,22 @@ function bandOutline(
*
* 돌려주는 값은 **실제로 뺀 양(㎥)**. 사토가 모자라면 받은 값보다 작다.
*/
function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: number | null): number {
if (deduction === null || !Number.isFinite(deduction) || deduction <= 0) return 0;
function applyCollectedStoneDeduction(
residuals: HaulResidual[],
deduction: number | null,
byGround?: Record<string, number> | null,
groundUnknownM3?: number | null,
conversion?: EarthworkConversion | null,
): number {
// ⚠ **축을 맞춘 뒤 뺀다**(2026-09-09 세 창 확정) — 채집석은 **벽 입적(제자리 부피)**이라
// 자연 축이고 이 곡선은 다짐 축이다. 갈래별 값이 오면 **갈래마다 ×C** 해서 뺀다.
// 갈래를 못 가른 몫은 **계수가 없어 환산하지 않고** 그대로 뺀다(근거 없이 안 눅인다).
// 갈래가 아예 안 오면 종전처럼 총량을 그대로 뺀다 — 값이 안 오는 것과 0 은 다르다.
const converted = compactedStoneDeduction(byGround, groundUnknownM3, conversion);
const amountToTake = converted === null ? deduction : converted;
if (amountToTake === null || !Number.isFinite(amountToTake) || amountToTake <= 0) return 0;
const spoils = residuals.filter((residual) => residual.kind === "spoil");
let left = deduction;
let left = amountToTake;
const take = (residual: HaulResidual, amount: number): void => {
if (amount <= 0) return;
const before = residual.volume_m3;
@@ -375,7 +387,7 @@ function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: numb
residual.natural_m3 -= amount;
take(residual, amount);
}
return deduction - Math.max(left, 0);
return amountToTake - Math.max(left, 0);
}
/**
@@ -417,6 +429,33 @@ const GROUND_OF_BUCKET = {
br_m3: "blasting_rock",
} as const;
/** 환산계수 이름 ↔ 채집석이 오는 갈래 이름. B08 이 `soil`·`ripping_rock`·`blasting_rock` 로 보낸다. */
const STONE_GROUND_KEYS = ["soil", "ripping_rock", "blasting_rock"] as const;
/**
* 채집석 공제를 **다짐 축**으로 옮긴다 — 갈래마다 ×C, 갈래를 모르는 몫은 그대로.
* 갈래가 하나도 안 오면 `null`(= 「갈래를 안 보냈음」)이라 부르는 쪽이 총량으로 되돌아간다.
*/
function compactedStoneDeduction(
byGround: Record<string, number> | null | undefined,
groundUnknownM3: number | null | undefined,
conversion: EarthworkConversion | null | undefined,
): number | null {
if (!byGround) return null;
const entries = STONE_GROUND_KEYS.map((key) => [key, Number(byGround[key] ?? 0)] as const).filter(
([, value]) => Number.isFinite(value) && value > 0,
);
const unknown = Number(groundUnknownM3 ?? 0);
const hasUnknown = Number.isFinite(unknown) && unknown > 0;
if (!entries.length && !hasUnknown) return null;
let total = hasUnknown ? unknown : 0;
for (const [ground, value] of entries) {
const factor = conversion?.[ground]?.compacted;
total += typeof factor === "number" && factor > 0 ? value * factor : value;
}
return total;
}
/**
* **자연상태 → 다짐상태**(× C). 구조물 잔토를 곡선에 얹기 전에 한 번만 거친다.
*
@@ -545,6 +584,10 @@ export function computeHaulPlan(
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<{
@@ -752,7 +795,13 @@ export function computeHaulPlan(
options?.conversion ?? null,
);
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
const deducted = applyCollectedStoneDeduction(settled, deductionInput);
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;