// 기슭막이 연동 하이라이트 짝짓기 + 경사 해제(수평) 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");