feat(B06): 담을 사토가 없으면 사토를 새로 세워 구조물 잔토를 담음 (확정 ㉰)
사토가 0 인 노선(전 구간 토취)에서는 잔토를 더할 자리가 없어 **물량이 그냥 사라졌다**. 파낸 흙은 어디로든 가므로 그것이 가장 나쁘다. - 담을 사토 잔량이 없으면 **그 측점에 사토를 새로 세워** 담는다. 물량이 보존되고 운반비가 붙는다(금액이 느는 방향 — 안전측). - ⚠ 토취를 줄이는 길(㉯)은 「그 잔토를 성토재로 쓸 수 있다」는 근거가 있어야 한다. 구조물 터파기 흙은 암이 섞일 수 있고 우리가 그 판정을 안 하므로, **근거 없이 금액을 내리지 않는다.** 확정되면 이 함수 한 곳만 바꾸면 된다 — 그 문장을 주석에 남겼다. - 새로 세운 사토는 자연방토 0 · 지반유형 안분 0 — 실어 내는 흙이고 지반을 모른다. - 측점별 잔토를 Node 진입점까지 이어 붙임(빠져 있어 총량 갈래로 떨어지고 있었다). 실측(route 184): 사토 0 → **12.88㎥**, 잔량이 **측점 5m 자리에 섬**(전에는 노선 전체 구간). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,8 @@ interface ServerCalcInput {
|
||||
collected_stone_deduction_m3?: number | null;
|
||||
/** 구조물 터파기 잔토(㎥, 양수) — B08 이 낸다. 사토에 **더한다**. */
|
||||
structure_spoil_m3?: number | null;
|
||||
/** 측점별 잔토 — 오면 **그 자리**에 얹는다(운반거리가 맞다). */
|
||||
structure_spoil_points?: Array<{ chainage_m: number; spoil_m3: number }> | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ if (input.haul_plan_for) {
|
||||
const plan = computeHaulPlan(input.haul_plan_for, input.context?.haul_equipment_limits, {
|
||||
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
||||
structure_spoil_m3: input.context?.structure_spoil_m3 ?? null,
|
||||
structure_spoil_points: input.context?.structure_spoil_points ?? null,
|
||||
});
|
||||
writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null }));
|
||||
process.exit(0);
|
||||
@@ -84,6 +87,7 @@ const plan = result
|
||||
? computeHaulPlan(result, input.context?.haul_equipment_limits, {
|
||||
collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null,
|
||||
structure_spoil_m3: input.context?.structure_spoil_m3 ?? null,
|
||||
structure_spoil_points: input.context?.structure_spoil_points ?? null,
|
||||
})
|
||||
: null;
|
||||
const massHaul = result
|
||||
|
||||
@@ -391,42 +391,82 @@ function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: numb
|
||||
*
|
||||
* 돌려주는 값은 **실제로 더한 양(㎥)**. 받을 사토 잔량이 하나도 없으면 0 이다.
|
||||
*/
|
||||
function newSpoilResidual(fromM: number, toM: number, volumeM3: number): HaulResidual {
|
||||
// 잔토를 담을 사토 잔량이 없을 때 **새로 세운다**(2026-09-09 확정 ㉰).
|
||||
// ⚠ 지반유형 안분·자연방토는 0 이다 — 어느 지반에서 나온 흙인지 모르고, **실어 내는** 흙이다.
|
||||
return {
|
||||
index: 0,
|
||||
kind: "spoil",
|
||||
from_m: fromM,
|
||||
to_m: toM,
|
||||
volume_m3: volumeM3,
|
||||
level_from_m3: 0,
|
||||
level_to_m3: 0,
|
||||
ea_m3: 0,
|
||||
rr_m3: 0,
|
||||
br_m3: 0,
|
||||
natural_m3: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조물 잔토 — **사토에 한 번만 더한다**(2026-09-09 네 창 합의).
|
||||
*
|
||||
* 구조물 잔토는 사토에 한 번만 더한다.
|
||||
* B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고,
|
||||
* 더하는 자리는 유토곡선의 사토뿐이다.
|
||||
*
|
||||
* ⚠ **잔량 하나하나를 늘린다** — 총량만 늘리면 사토는 늘고 **운반이 안 는다**.
|
||||
* ⚠ **담을 사토가 없으면 사토를 새로 세운다**(확정 ㉰). 파낸 흙은 어디로든 가므로
|
||||
* **물량이 사라지면 안 된다**. 토취를 줄이는 길(㉯)은 **「그 잔토를 성토재로 쓸 수 있다」**는
|
||||
* 근거가 있어야 하는데 구조물 터파기 흙은 암이 섞일 수 있고 우리가 그 판정을 안 한다 —
|
||||
* 근거 없이 금액을 내리지 않고 **내보내는 쪽(안전측)** 으로 둔다.
|
||||
* ⇒ 나중에 「성토재로 쓴다」가 확정되면 **이 함수 한 곳만** 바꾸면 된다.
|
||||
* ⚠ 자연방토·지반유형 안분은 안 건드린다 — 실어 내는 흙이고, 어느 지반에서 나온지 모른다.
|
||||
*/
|
||||
function applyStructureSpoil(
|
||||
residuals: HaulResidual[],
|
||||
amount: number | null,
|
||||
points: Array<{ chainage_m: number; spoil_m3: number }> | null,
|
||||
): number {
|
||||
const spoils = residuals.filter((residual) => residual.kind === "spoil");
|
||||
if (!spoils.length) return 0;
|
||||
const spoilsOf = (): HaulResidual[] => residuals.filter((residual) => residual.kind === "spoil");
|
||||
|
||||
// ① 측점별 값이 오면 **그 자리 잔량**에 얹는다 — 구조물이 실제로 선 자리라 운반거리가 맞다.
|
||||
// ① 측점별 값이 오면 **그 자리**에 얹는다 — 구조물이 실제로 선 자리라 운반거리가 맞다.
|
||||
if (points && points.length) {
|
||||
let added = 0;
|
||||
for (const point of points) {
|
||||
const value = Number(point?.spoil_m3);
|
||||
const chainage = Number(point?.chainage_m);
|
||||
if (!Number.isFinite(value) || value <= 0 || !Number.isFinite(chainage)) continue;
|
||||
// 그 측점을 품은 잔량 → 없으면 가장 가까운 잔량. 버리지 않는다.
|
||||
const spoils = spoilsOf();
|
||||
const covering = spoils.find(
|
||||
(residual) => chainage >= residual.from_m - 1e-9 && chainage <= residual.to_m + 1e-9,
|
||||
);
|
||||
const target =
|
||||
covering ??
|
||||
spoils.reduce((best, residual) => {
|
||||
const middle = (residual.from_m + residual.to_m) / 2;
|
||||
const bestMiddle = (best.from_m + best.to_m) / 2;
|
||||
return Math.abs(chainage - middle) < Math.abs(chainage - bestMiddle) ? residual : best;
|
||||
}, spoils[0]);
|
||||
target.volume_m3 += value;
|
||||
if (covering) {
|
||||
covering.volume_m3 += value;
|
||||
} else {
|
||||
// 그 자리를 품는 사토가 없다 — 그 측점에 사토를 새로 세워 담는다.
|
||||
residuals.push(newSpoilResidual(chainage, chainage, value));
|
||||
}
|
||||
added += value;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
// ② 총량만 오면 **남은 사토 잔량 크기에 비례**해 나눈다(어느 자리인지 모를 때의 차선).
|
||||
// ② 총량만 오면 남은 사토 잔량 크기에 **비례**해 나눈다(어느 자리인지 모를 때의 차선).
|
||||
if (amount === null || !Number.isFinite(amount) || amount <= 0) return 0;
|
||||
const spoils = spoilsOf();
|
||||
const total = spoils.reduce((sum, residual) => sum + residual.volume_m3, 0);
|
||||
if (total <= EPSILON) return 0;
|
||||
if (!spoils.length || total <= EPSILON) {
|
||||
// 담을 사토가 없다 — 있는 잔량이 덮는 구간 전체에 사토를 하나 세운다. 자리를 모르므로
|
||||
// 구간을 넓게 잡고, **물량은 보존**한다.
|
||||
const froms = residuals.map((residual) => residual.from_m);
|
||||
const tos = residuals.map((residual) => residual.to_m);
|
||||
const fromM = froms.length ? Math.min(...froms) : 0;
|
||||
const toM = tos.length ? Math.max(...tos) : 0;
|
||||
residuals.push(newSpoilResidual(fromM, toM, amount));
|
||||
return amount;
|
||||
}
|
||||
let added = 0;
|
||||
spoils.forEach((residual, index) => {
|
||||
const share =
|
||||
|
||||
Reference in New Issue
Block a user