⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt` 가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는 시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음. - `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침** - `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시) - 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q` 옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0 ⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
5.2 KiB
JavaScript
96 lines
5.2 KiB
JavaScript
// 기슭막이 연동 하이라이트 짝짓기 + 경사 해제(수평) d 역산 자체검증 — 로직 복제.
|
||
// 2026-08-30 사용자 확정: ① 한 기슭막이는 연장 안 모든 횡단도에서 같이 강조된다
|
||
// ② 경사를 풀면 벽 상단 표고가 소유 측점 값과 같아진다(차이는 노견 확장이 흡수).
|
||
// 실행: node tmp/tests/test_revet_link_level.mjs
|
||
import assert from "node:assert/strict";
|
||
|
||
const FILL_SLOPE_RATIO_MIN = 1.2;
|
||
|
||
/* ── 복제: spanCovers / tierSpanOf / wallStandsAt ────────────────────── */
|
||
function spanCovers(span, deltaM) {
|
||
return deltaM < 0 ? -deltaM <= span.beforeM + 1e-9 : deltaM <= span.afterM + 1e-9;
|
||
}
|
||
function revetSpanOfSpec(spec) {
|
||
const length = Math.max(spec?.revet_length_m ?? 10, 0.1);
|
||
const beforeM = Math.max(spec?.revet_before_m ?? length / 2, 0);
|
||
const afterM = Math.max(spec?.revet_after_m ?? Math.max(length - beforeM, 0), 0);
|
||
return { beforeM, afterM };
|
||
}
|
||
function tierSpanOf(section, key) {
|
||
const stored = section.design?.extra_spans?.[key];
|
||
if (stored) return { beforeM: Math.max(stored.before_m, 0), afterM: Math.max(stored.after_m, 0) };
|
||
const spec = key.startsWith("bextra") ? section.culvert?.inlet : section.culvert?.outlet;
|
||
if (!spec) return { beforeM: 5, afterM: 5 };
|
||
return revetSpanOfSpec(spec);
|
||
}
|
||
function wallStandsAt(owner, section, key) {
|
||
const deltaM = section.chainage_m - owner.chainage_m;
|
||
if (Math.abs(deltaM) < 0.01) return true;
|
||
const spec = owner.culvert;
|
||
if (!spec) return false;
|
||
if (key.startsWith("extra") || key.startsWith("bextra")) {
|
||
return spanCovers(tierSpanOf(owner, key), deltaM);
|
||
}
|
||
const side = key === "inlet" ? spec.inlet : key === "outlet" ? spec.outlet : null;
|
||
if (!side) return false;
|
||
if (side.structure === "집수정" && !spec.hidden_pipe) return false;
|
||
return spanCovers(revetSpanOfSpec(side), deltaM);
|
||
}
|
||
|
||
/* ── ① 하이라이트 짝짓기 ─────────────────────────────────────────────── */
|
||
const owner = {
|
||
chainage_m: 200,
|
||
culvert: {
|
||
inlet: { structure: "기슭막이", revet_length_m: 10, revet_before_m: 5, revet_after_m: 5 },
|
||
outlet: { structure: "기슭막이", revet_length_m: 10, revet_before_m: 5, revet_after_m: 5 },
|
||
},
|
||
design: { extra_spans: { extra0: { length_m: 20, before_m: 12, after_m: 8 } } },
|
||
};
|
||
const at = (chainage) => ({ chainage_m: chainage });
|
||
|
||
// 기준벽 — 전 5·후 5 안의 측점에서만 선다.
|
||
assert.equal(wallStandsAt(owner, at(200), "outlet"), true, "소유 측점은 언제나");
|
||
assert.equal(wallStandsAt(owner, at(205), "outlet"), true, "후 5m 끝");
|
||
assert.equal(wallStandsAt(owner, at(195), "outlet"), true, "전 5m 끝");
|
||
assert.equal(wallStandsAt(owner, at(206), "outlet"), false, "연장 밖");
|
||
// 다단은 **단별 구간값**을 따른다 — 기준벽보다 길 수 있다(전 12·후 8).
|
||
assert.equal(wallStandsAt(owner, at(190), "extra0"), true, "다단 전 12m");
|
||
assert.equal(wallStandsAt(owner, at(208), "extra0"), true, "다단 후 8m");
|
||
assert.equal(wallStandsAt(owner, at(209), "extra0"), false, "다단 연장 밖");
|
||
// 단별 값이 없는 단은 소유 벽 연장을 상속한다.
|
||
assert.equal(wallStandsAt(owner, at(205), "extra1"), true);
|
||
assert.equal(wallStandsAt(owner, at(206), "extra1"), false);
|
||
// 집수정이 선 쪽은 소유 측점 하나뿐 — 옆 카드에는 없다(강조도 없다).
|
||
const basinOwner = {
|
||
chainage_m: 200,
|
||
culvert: {
|
||
inlet: { structure: "집수정", basin_length_m: 2, basin_before_m: 1, basin_after_m: 1 },
|
||
outlet: { structure: "기슭막이" },
|
||
},
|
||
};
|
||
assert.equal(wallStandsAt(basinOwner, at(201), "inlet"), false, "집수정은 옆 측점에 안 선다");
|
||
assert.equal(wallStandsAt(basinOwner, at(201), "outlet"), true);
|
||
|
||
/* ── ② 경사 해제(수평) d 역산 ────────────────────────────────────────── */
|
||
// 배관 벽 자리 규칙: 상단 = 노견 표고 − d/1.2(placePipeWall). 이 측점 d를
|
||
// `소유 d + 1.2 × dz`로 밀면 상단 표고가 소유 측점 값 그대로다.
|
||
const levelDrop = (ownerD, dz) => ownerD + FILL_SLOPE_RATIO_MIN * dz;
|
||
const topOf = (edgeElevation, d) => edgeElevation - d / FILL_SLOPE_RATIO_MIN;
|
||
|
||
const edgeOwner = 100.0;
|
||
const ownerD = 4.8;
|
||
const topOwner = topOf(edgeOwner, ownerD);
|
||
for (const dz of [-1.5, -0.4, 0, 0.4, 2.0]) {
|
||
const edgeHere = edgeOwner + dz;
|
||
const here = topOf(edgeHere, levelDrop(ownerD, dz));
|
||
assert.ok(Math.abs(here - topOwner) < 1e-9, `dz=${dz} 상단 어긋남: ${here} != ${topOwner}`);
|
||
}
|
||
// 경사를 켜 두면(보정 없음) 벽은 종단을 탄다 — 상단이 dz만큼 따라 움직인다.
|
||
const sloped = topOf(edgeOwner + 1.2, ownerD);
|
||
assert.ok(Math.abs(sloped - topOwner - 1.2) < 1e-9, "경사 반영이면 상단이 계획고를 따라간다");
|
||
// 이 측점 노견이 소유 벽 상단보다 낮으면 d가 음수 — 기하가 0에서 자른다(노견까지만).
|
||
const dNegative = levelDrop(ownerD, -(ownerD / FILL_SLOPE_RATIO_MIN) - 1);
|
||
assert.ok(dNegative < 0, "노견보다 위로는 못 올라간다 — 0에서 잘릴 요청");
|
||
|
||
console.log("test_revet_link_level.mjs OK");
|