「구조물을 놓아도 횡단도에 아무것도 안 보인다」가 창 둘에서 같은 증상이었다. 화면에 표식을 심어 재 보니 **브라우저 쪽 단면에 `revetment` 자체가 없었다**. - 원인: `withDraftWalls` 가 미저장 초안 목록이 **빈 배열**일 때도 「초안 있음」으로 읽고 **서버 저장분(`revetment`)을 통째로 지운 뒤** 아무것도 안 얹었다. ⇒ `!drafts || !drafts.length` 로 고쳤다 — 빈 목록도 「초안 없음」이다. - 초안 경로가 `foundation` 을 안 실어 터파기가 안 그려지던 것도 함께 고침 (`WallSpec.foundation` 추가, 거울 시험도 같이 갱신). - 겹침 방지 가드를 **좁혔다** — 링크가 있기만 하면 막던 것을, 그 링크의 벽이 **이 카드에 실제로 설 때만** 막게 했다(`culvertWallsStandAt`). 관이 아홉·열하나인 노선에서는 링크가 거의 모든 측점을 덮어 독립 벽 경로가 통째로 죽어 있었다. 화면 확인: 벽 터파기 「기초유 · 폭 1.62m · 깊이 0.50m (수직)」, 관 터파기 Φ1000 1.80m · Φ800 1.60m, 그려진 선 전부 좌우 변이 수직. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
/* =============================================================================
|
|
* common_util_structure_walls.ts
|
|
* 구조물 정본(C군 사면안정 벽)을 횡단 측점 제원으로 바꾸는 자리 —
|
|
* 파이썬 `B06_Section_Engine_Structures_Wall.py` 의 **짝**이다(거울 테스트로 대조).
|
|
*
|
|
* 왜 두 벌인가(CLAUDE.md 5장) — 서버는 상세를 내려보낼 때 얹어야 하고(저장분 기준),
|
|
* 브라우저는 사용자가 **아직 저장하지 않은** 목록으로 즉시 얹어야 한다. 값을 만드는
|
|
* 산식이 같아야 하므로 두 파일 머리에 짝임을 적고 거울 테스트를 둔다.
|
|
*
|
|
* 얹는 것은 제원뿐이다 — 도형·면적은 `B06_Section_UI_Cross_Revetment` 가 그린다.
|
|
* ========================================================================== */
|
|
|
|
/** 구조물 정본 한 건(필요한 칸만). `structures.json` 과 같은 이름을 쓴다. */
|
|
export interface WallStructureInput {
|
|
structure_id?: string | null;
|
|
type_id: string;
|
|
placement?: string | null;
|
|
chainage_m: number;
|
|
start_m?: number | null;
|
|
end_m?: number | null;
|
|
options?: Record<string, unknown> | null;
|
|
}
|
|
|
|
/** 측점에 얹는 벽 제원 — `section.revetment` 와 같은 꼴. */
|
|
export interface WallSpec {
|
|
structure_id: string | null;
|
|
type_id: string;
|
|
name: string;
|
|
start_m: number;
|
|
end_m: number;
|
|
anchor_m: number;
|
|
form: string | null;
|
|
height_m: number | null;
|
|
side: string | null;
|
|
/** 기초 축 — "기초유" | "기초버림". 저장 칸과 같은 글자(터파기 그림이 이 값으로 갈린다). */
|
|
foundation: string | null;
|
|
tiers: number | null;
|
|
lift_m: number | null;
|
|
shift_m: number | null;
|
|
}
|
|
|
|
/** 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 파이썬 `_EDGE_TOLERANCE_M`. */
|
|
const EDGE_TOLERANCE_M = 0.02;
|
|
|
|
/** 구조물 종류 → 횡단 기하가 아는 형태 이름 — 파이썬 `_FORM_BY_TYPE` 와 같은 표. */
|
|
export const FORM_BY_TYPE: Record<string, string> = {
|
|
masonry_wet: "돌쌓기(찰)",
|
|
masonry_dry: "돌쌓기(메)",
|
|
boulder_masonry: "돌쌓기(메)",
|
|
retaining_wall: "콘크리트",
|
|
soil_guard: "통나무·목재틀",
|
|
};
|
|
|
|
const num = (value: unknown): number | null =>
|
|
typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
|
|
/**
|
|
* C군 벽 구조물을 제원 목록으로 바꾼다. 이름표(`names`)는 타입 레지스트리에서 온다.
|
|
* 구간(start·end)이 없는 항목은 건너뛴다 — 벽은 구간형이다.
|
|
*/
|
|
export function wallSpecsFrom(
|
|
structures: readonly WallStructureInput[],
|
|
names: ReadonlyMap<string, string>,
|
|
): WallSpec[] {
|
|
const specs: WallSpec[] = [];
|
|
for (const structure of structures) {
|
|
const name = names.get(structure.type_id);
|
|
if (!name) continue;
|
|
const start = num(structure.start_m);
|
|
const end = num(structure.end_m);
|
|
if (start === null || end === null) continue;
|
|
const options = (structure.options ?? {}) as Record<string, unknown>;
|
|
specs.push({
|
|
structure_id: structure.structure_id ?? null,
|
|
type_id: structure.type_id,
|
|
name,
|
|
start_m: Math.min(start, end),
|
|
end_m: Math.max(start, end),
|
|
anchor_m: num(structure.chainage_m) ?? Math.min(start, end),
|
|
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),
|
|
});
|
|
}
|
|
return specs;
|
|
}
|
|
|
|
/**
|
|
* 구간 안 측점에 제원을 얹는다(파이썬 `attach_wall_structures`). 얹은 개수를 돌려준다.
|
|
* 관 세트가 이미 붙은 측점은 건드리지 않는다 — 한 자리에 두 벽이 겹치면 읽히지 않는다.
|
|
*/
|
|
export function attachWallSpecs(
|
|
sections: Array<Record<string, unknown>>,
|
|
specs: readonly WallSpec[],
|
|
): number {
|
|
if (!specs.length) return 0;
|
|
let attached = 0;
|
|
for (const section of sections) {
|
|
const chainage = num(section.chainage_m);
|
|
if (chainage === null) continue;
|
|
if (section.culvert || section.revetment) continue;
|
|
for (const spec of specs) {
|
|
if (
|
|
spec.start_m - EDGE_TOLERANCE_M <= chainage &&
|
|
chainage <= spec.end_m + EDGE_TOLERANCE_M
|
|
) {
|
|
section.revetment = spec;
|
|
attached += 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return attached;
|
|
}
|