Files
Aislo/resources/tester/test_revet_wall_path.mjs
eomsangdonandClaude Opus 5 0ef32b5279 chore(tester): 시험을 resources/tester/ 로 옮김 — 창끼리 건너가게
⚠ **뿌리** — `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>
2026-09-09 17:12:30 +09:00

135 lines
4.9 KiB
JavaScript

// 기슭막이 벽 경로선(B05_Profile_UI_Corridor_Structures_Path) 자체검증 — 로직 복제.
// 연동을 푼 측점이 있으면 벽이 노선과 평행이 아니라 자기 경로를 따라간다(2026-08-30).
// 실행: node tmp/tests/test_revet_wall_path.mjs
import assert from "node:assert/strict";
/* ── 복제: buildWallPath / splineValue / polygonAt ───────────────────── */
function wallPolygonOf(layout, key) {
const wall = key.startsWith("bextra")
? layout.basinExtras[Number(key.slice(6))]
: key.startsWith("extra")
? layout.extraWalls[Number(key.slice(5))]
: layout.walls.find((candidate) => candidate.role === key);
if (!wall || wall.points.length < 3) return null;
return wall.points.map((point) => [point.offset, point.elevation]);
}
function buildWallPath(base, others, key) {
const path = [base];
for (const other of others) {
const polygon = wallPolygonOf(other.layout, key);
if (!polygon || polygon.length !== base.polygon.length) continue;
path.push({ at: other.at, polygon });
}
if (path.length < 2) return null;
return path.sort((a, b) => a.at - b.at);
}
function splineValue(values, ats, at) {
const last = values.length - 1;
if (last < 1) return values[0];
if (at <= ats[0]) return values[0];
if (at >= ats[last]) return values[last];
let i = 0;
while (i < last - 1 && ats[i + 1] < at) i += 1;
const span = ats[i + 1] - ats[i];
const t = span <= 1e-9 ? 0 : (at - ats[i]) / span;
const p0 = i > 0 ? values[i - 1] : values[i];
const p1 = values[i];
const p2 = values[i + 1];
const p3 = i + 2 <= last ? values[i + 2] : values[i + 1];
const t2 = t * t;
const t3 = t2 * t;
return (
0.5 *
(2 * p1 +
(-p0 + p2) * t +
(2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 +
(-p0 + 3 * p1 - 3 * p2 + p3) * t3)
);
}
function polygonAt(path, at) {
const ats = path.map((control) => control.at);
return path[0].polygon.map((_vertex, index) => [
splineValue(
path.map((control) => control.polygon[index][0]),
ats,
at,
),
splineValue(
path.map((control) => control.polygon[index][1]),
ats,
at,
),
]);
}
/* ── 시험 자료 — 사각 벽 하나. 소유 측점 200, 연동 해제 측점 205(0.5m 바깥). ── */
const wall = (offset) => ({
role: "outlet",
points: [
{ offset, elevation: 10 },
{ offset, elevation: 12 },
{ offset: offset + 0.5, elevation: 12 },
{ offset: offset + 0.5, elevation: 10 },
],
});
const base = { at: 200, polygon: wallPolygonOf({ walls: [wall(3)] }, "outlet") };
const others = [{ at: 205, layout: { walls: [wall(3.5)], extraWalls: [], basinExtras: [] } }];
const path = buildWallPath(base, others, "outlet");
assert.ok(path, "제어점 2개면 경로선이 생겨야 한다");
assert.equal(path.length, 2);
assert.deepEqual(
path.map((control) => control.at),
[200, 205],
"제어점은 누가거리 오름차순",
);
// 끝점은 그 측점 값 그대로.
assert.equal(polygonAt(path, 200)[0][0], 3);
assert.equal(polygonAt(path, 205)[0][0], 3.5);
// 범위 밖은 끝값 유지(외삽 금지 — 벽이 연장 밖으로 새면 안 된다).
assert.equal(polygonAt(path, 190)[0][0], 3);
assert.equal(polygonAt(path, 220)[0][0], 3.5);
// 사이는 두 값 사이(제어점 2개 = 선형).
const mid = polygonAt(path, 202.5)[0][0];
assert.ok(Math.abs(mid - 3.25) < 1e-9, `중간 보간 어긋남: ${mid}`);
// 표고는 두 측점이 같으므로 보간해도 그대로.
assert.equal(polygonAt(path, 202.5)[0][1], 10);
// 꼭짓점 수는 링마다 같아야 로프트가 된다.
assert.equal(polygonAt(path, 202.5).length, base.polygon.length);
// 제어점 3개 — 가운데가 튀면 스플라인이 그 값을 지난다(경로선이 측점을 통과).
const three = buildWallPath(base, [
{ at: 203, layout: { walls: [wall(4)], extraWalls: [], basinExtras: [] } },
{ at: 205, layout: { walls: [wall(3.5)], extraWalls: [], basinExtras: [] } },
], "outlet");
assert.equal(three.length, 3);
assert.equal(polygonAt(three, 203)[0][0], 4, "제어 측점 자리는 그 측점 값 그대로");
// 꼭짓점 수가 다른 측점은 제어점에서 빠진다(로프트 토폴로지 보호).
const ragged = buildWallPath(base, [
{
at: 205,
layout: {
walls: [{ role: "outlet", points: wall(3.5).points.slice(0, 3) }],
extraWalls: [],
basinExtras: [],
},
},
], "outlet");
assert.equal(ragged, null, "꼭짓점 수가 어긋나면 경로선 없이 소유 폴리곤 고정");
// 다단 키도 같은 규칙으로 짝짓는다.
const tierBase = { at: 200, polygon: wallPolygonOf({ walls: [], extraWalls: [wall(5)] }, "extra0") };
const tierPath = buildWallPath(
tierBase,
[{ at: 205, layout: { walls: [], extraWalls: [wall(5.4)], basinExtras: [] } }],
"extra0",
);
assert.ok(tierPath && Math.abs(polygonAt(tierPath, 205)[0][0] - 5.4) < 1e-9);
console.log("test_revet_wall_path.mjs OK");