Merge remote-tracking branch 'origin/main_laptop_1' into main_desktop_1
This commit is contained in:
@@ -52,7 +52,10 @@ async function withDraftWalls(
|
||||
projectId: string,
|
||||
): Promise<SectionDetailResponse> {
|
||||
const drafts = readPendingStructures(projectId);
|
||||
if (!drafts) return withStructureAreas(detail);
|
||||
// ⚠ **빈 목록도 「초안 없음」이다**(2026-09-09). 예전에는 `[]` 가 「초안이 있는데 벽이
|
||||
// 하나도 없다」로 읽혀 **아래에서 서버 저장분을 통째로 지웠다** — 구조물을 놓아도
|
||||
// 횡단도에 아무것도 안 보이던 결함의 원인이다(창 둘에서 같은 증상, 실측 확인).
|
||||
if (!drafts || !drafts.length) return withStructureAreas(detail);
|
||||
const types = await fetchStructureTypes().catch(() => []);
|
||||
const names = new Map(
|
||||
types
|
||||
|
||||
@@ -39,6 +39,8 @@ interface ServerCalcInput {
|
||||
haul_equipment_limits?: Parameters<typeof computeHaulPlan>[1];
|
||||
/** 채집석 공제(㎥, 양수) — B08 이 낸다. `null`/없음은 「아직 안 옴」이다. */
|
||||
collected_stone_deduction_m3?: number | null;
|
||||
/** 구조물 터파기 잔토(㎥, 양수) — B08 이 낸다. 사토에 **더한다**. */
|
||||
structure_spoil_m3?: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +58,7 @@ if (input.haul_plan_for) {
|
||||
// 그리기 코드가 손대지 않고 그대로 받는다. 전부 숫자·문자열이라 JSON 으로 오간다.
|
||||
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,
|
||||
});
|
||||
writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null }));
|
||||
process.exit(0);
|
||||
@@ -80,6 +83,7 @@ const result = conversion
|
||||
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,
|
||||
})
|
||||
: null;
|
||||
const massHaul = result
|
||||
|
||||
@@ -63,7 +63,10 @@ _AREA_KEYS = (
|
||||
)
|
||||
|
||||
|
||||
def _mass_haul_context(collected_stone_deduction_m3: float | None = None) -> dict[str, Any]:
|
||||
def _mass_haul_context(
|
||||
collected_stone_deduction_m3: float | None = None,
|
||||
structure_spoil_m3: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다.
|
||||
|
||||
⚠ 채집석 공제(`collected_stone_deduction_m3`)만 상수가 아니라 **B08 이 내는 값**이다.
|
||||
@@ -85,6 +88,11 @@ def _mass_haul_context(collected_stone_deduction_m3: float | None = None) -> dic
|
||||
# ⚠ B08 이 아직 이 값을 내지 않는다(2026-09-09) — 그때까지 `None`(아직 안 옴)이다.
|
||||
# 값을 내기 시작하면 여기에 실어 주기만 하면 통로가 이어진다.
|
||||
"collected_stone_deduction_m3": collected_stone_deduction_m3,
|
||||
# 구조물 터파기 잔토(㎥, 양수) — **사토에 더한다**(공제는 빼고 이것은 더한다).
|
||||
# 구조물 잔토는 사토에 한 번만 더한다.
|
||||
# B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고,
|
||||
# 더하는 자리는 유토곡선의 사토뿐이다.
|
||||
"structure_spoil_m3": structure_spoil_m3,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -256,6 +256,27 @@ export function wallStandsAt(owner: CrossSection, section: CrossSection, key: st
|
||||
return spanCovers(revetSpanOfSpec(side), deltaM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크된 관의 벽이 **이 카드에 실제로 서는가**. 하나도 안 서면 그 링크는 이 측점의
|
||||
* 그림을 막을 이유가 없다 — 독립 벽(D경로)이 그려져야 한다.
|
||||
*
|
||||
* 왜 있나(2026-09-09) — 「관이 없는 측점인데도 구조물을 놓으면 아무것도 안 보인다」가
|
||||
* 사용자에게 보이던 결함이었다. 겹침 방지 가드가 **링크가 있기만 하면** 막고 있었는데,
|
||||
* 관이 아홉·열하나인 노선에서는 링크가 거의 모든 측점을 덮어 D경로가 통째로 죽었다.
|
||||
* 가드를 없애지 않고 **좁힌다** — 벽이 실제로 서는 카드에서만 막는다.
|
||||
*/
|
||||
export function culvertWallsStandAt(owner: CrossSection, section: CrossSection): boolean {
|
||||
const keys = ["inlet", "outlet"];
|
||||
const counts = owner.design?.extra_wall_counts ?? {};
|
||||
for (const [side, count] of Object.entries(counts)) {
|
||||
const total = Math.max(Math.trunc(Number(count) || 0), 0);
|
||||
for (let index = 0; index < total; index += 1) {
|
||||
keys.push(side === "basin" ? `bextra${index}` : `extra${index}`);
|
||||
}
|
||||
}
|
||||
return keys.some((key) => wallStandsAt(owner, section, key));
|
||||
}
|
||||
|
||||
export function culvertLinkFor(
|
||||
section: CrossSection,
|
||||
sections: readonly CrossSection[],
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
appendFordPavementOverlay,
|
||||
appendFordSurfaceDropPlan,
|
||||
} from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { culvertWallsStandAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { appendRevetmentOverlay, computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
|
||||
import {
|
||||
appendCrossDesignOverlay,
|
||||
@@ -422,7 +423,12 @@ export function createCrossSectionCard(
|
||||
// 이 측점에 배관/숨김 기슭막이 세트가 직접 붙었거나(section.culvert) **연동으로
|
||||
// 옆에서 이어져 온**(culvertLink) 경우엔 배관 경로가 그린다 — 옛 D경로는 건너뛴다
|
||||
// (둘 다 그리면 이웃 카드에 벽이 겹친다 — 2026-08-28 이관 이중그리기 방지).
|
||||
if (!section.culvert && !culvertLink) {
|
||||
// ⚠ 가드를 **좁혔다**(2026-09-09) — 예전에는 링크가 **있기만 하면** 막았는데,
|
||||
// 관이 아홉·열하나인 노선에서는 링크가 거의 모든 측점을 덮어 **구조물을 놓아도
|
||||
// 횡단도에 아무것도 안 보였다**(사용자에게 보이던 결함). 겹침 방지라는 까닭은
|
||||
// 그대로 두고, **그 링크의 벽이 이 카드에 실제로 설 때만** 막는다.
|
||||
const linkedWallsHere = !!culvertLink && culvertWallsStandAt(culvertLink.source, section);
|
||||
if (!section.culvert && !linkedWallsHere) {
|
||||
const ownAdjust = revetOffset?.adjustFor(section, "own");
|
||||
const ownLayout = computeRevetmentLayout(section, ownAdjust);
|
||||
ownDesignTrim = ownLayout?.designTrim;
|
||||
|
||||
@@ -199,6 +199,13 @@ export interface HaulPlan {
|
||||
collected_stone_deduction_m3: number | null;
|
||||
/** 실제로 사토에서 뺀 양(㎥). 사토가 모자라면 받은 값보다 작을 수 있다. */
|
||||
collected_stone_deducted_m3: number;
|
||||
/**
|
||||
* 구조물 터파기가 남긴 잔토(㎥, 양수) — **사토에 더한다**. `null` 은 「아직 안 옴」이고
|
||||
* `0` 은 「없음」이다(공제와 같은 태도).
|
||||
*/
|
||||
structure_spoil_m3: number | null;
|
||||
/** 실제로 사토에 더한 양(㎥). 받을 잔량이 없으면 받은 값보다 작을 수 있다. */
|
||||
structure_spoil_added_m3: number;
|
||||
/** 블록 안에서 옮기는 양(㎥). */
|
||||
hauled_m3: number;
|
||||
/** 떨어진 구간끼리 장거리로 옮기는 양(㎥). */
|
||||
@@ -367,10 +374,46 @@ function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: numb
|
||||
return deduction - Math.max(left, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조물 잔토 — **사토에 한 번만 더한다**(2026-09-09 네 창 합의).
|
||||
*
|
||||
* 구조물 잔토는 사토에 한 번만 더한다.
|
||||
* B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고,
|
||||
* 더하는 자리는 유토곡선의 사토뿐이다.
|
||||
*
|
||||
* ⚠ **잔량 하나하나를 늘린다** — 총량만 늘리면 사토는 늘고 **운반이 안 는다**(공제 때와 같은 자리).
|
||||
* ⚠ **나누는 법** — B08 이 지금은 **총량 하나**만 준다. 어느 측점에서 나온 잔토인지 모르므로
|
||||
* **남은 사토 잔량의 크기에 비례**해 나눈다. 한 곳에 몰면 운반거리가 틀리기 때문이고,
|
||||
* 측점별 값이 오면 그때 그 자리에 얹을 것(그때는 이 함수만 고치면 된다).
|
||||
* ⚠ **자연방토(`natural_m3`)는 안 늘린다** — 구조물 잔토는 실어 내는 흙이다.
|
||||
* ⚠ **지반유형 안분(ea/rr/br)도 안 건드린다** — 어느 지반에서 나온 흙인지 모른다.
|
||||
* 합계(`volume_m3`)와 갈래 합이 어긋나는 것은 그 사실을 드러내는 표시다.
|
||||
*
|
||||
* 돌려주는 값은 **실제로 더한 양(㎥)**. 받을 사토 잔량이 하나도 없으면 0 이다.
|
||||
*/
|
||||
function applyStructureSpoil(residuals: HaulResidual[], amount: number | null): number {
|
||||
if (amount === null || !Number.isFinite(amount) || amount <= 0) return 0;
|
||||
const spoils = residuals.filter((residual) => residual.kind === "spoil");
|
||||
const total = spoils.reduce((sum, residual) => sum + residual.volume_m3, 0);
|
||||
if (!spoils.length || total <= EPSILON) return 0;
|
||||
let added = 0;
|
||||
spoils.forEach((residual, index) => {
|
||||
// 마지막 잔량은 나머지를 그대로 받아 반올림 오차가 새지 않게 한다.
|
||||
const share =
|
||||
index === spoils.length - 1 ? amount - added : amount * (residual.volume_m3 / total);
|
||||
residual.volume_m3 += share;
|
||||
added += share;
|
||||
});
|
||||
return added;
|
||||
}
|
||||
|
||||
export function computeHaulPlan(
|
||||
result: MassHaulResult,
|
||||
limits: HaulEquipmentLimit[] | undefined,
|
||||
options?: { collected_stone_deduction_m3?: number | null },
|
||||
options?: {
|
||||
collected_stone_deduction_m3?: number | null;
|
||||
structure_spoil_m3?: number | null;
|
||||
},
|
||||
): HaulPlan | null {
|
||||
const points = result.points;
|
||||
if (points.length < 2) return null;
|
||||
@@ -551,6 +594,8 @@ export function computeHaulPlan(
|
||||
|
||||
const deductionInput = options?.collected_stone_deduction_m3 ?? null;
|
||||
const deducted = applyCollectedStoneDeduction(settled, deductionInput);
|
||||
const structureSpoilInput = options?.structure_spoil_m3 ?? null;
|
||||
const structureSpoilAdded = applyStructureSpoil(settled, structureSpoilInput);
|
||||
const remaining = settled.filter((residual) => residual.volume_m3 > EPSILON);
|
||||
remaining.forEach((residual, index) => {
|
||||
residual.index = index + 1;
|
||||
@@ -577,6 +622,8 @@ export function computeHaulPlan(
|
||||
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,
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface WallSpec {
|
||||
form: string | null;
|
||||
height_m: number | null;
|
||||
side: string | null;
|
||||
/** 기초 축 — "기초유" | "기초버림". 저장 칸과 같은 글자(터파기 그림이 이 값으로 갈린다). */
|
||||
foundation: string | null;
|
||||
tiers: number | null;
|
||||
lift_m: number | null;
|
||||
shift_m: number | null;
|
||||
@@ -78,6 +80,8 @@ export function wallSpecsFrom(
|
||||
form: (options.form as string) || FORM_BY_TYPE[structure.type_id] || null,
|
||||
height_m: num(options.height_m),
|
||||
side: (options.side as string) ?? null,
|
||||
// 기초 축 — 저장 칸과 **같은 글자**. 초안 경로에서 빠지면 터파기가 안 그려진다.
|
||||
foundation: (options.foundation as string) ?? null,
|
||||
tiers: num(options.tiers),
|
||||
lift_m: num(options.lift_m),
|
||||
shift_m: num(options.shift_m),
|
||||
|
||||
Reference in New Issue
Block a user