⚠ **뿌리** — `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>
135 lines
4.9 KiB
JavaScript
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");
|