Files
Aislo/resources/tester/test_cross_y_ticks.mjs
T
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

79 lines
3.7 KiB
JavaScript

// 횡단 Y축 눈금이 플롯 안(상단~X축선)에만 놓이는지 확인 — crossPlotMetrics + Cross_View 로직 복제.
// 실행: node tmp/tests/test_cross_y_ticks.mjs
import assert from "node:assert/strict";
const CROSS_PAD = { left: 58, right: 20, top: 10, bottom: 52 };
const CROSS_HEIGHT = 250;
const AREA_OVERLAY_HEADROOM_PX = 58;
function niceTickStep(span, targetCount, maxCount) {
if (!(span > 0)) return 1;
const exponent = Math.floor(Math.log10(span / targetCount));
let best = Math.pow(10, exponent + 2);
let bestError = Infinity;
for (const power of [exponent - 1, exponent, exponent + 1, exponent + 2]) {
for (const mantissa of [1, 2, 5]) {
const step = mantissa * Math.pow(10, power);
const count = Math.floor(span / step) + 1;
if (count > Math.max(2, maxCount)) continue;
const error = Math.abs(count - targetCount);
if (error < bestError) {
bestError = error;
best = step;
}
}
}
return best;
}
function ticksOf({ rawMin, rawMax, halfWidth, widthPx = 630, exaggeration = 1 }) {
const minOffset = -halfWidth;
const maxOffset = halfWidth;
const elevationMid = (rawMin + rawMax) / 2;
const padding = rawMax > rawMin ? (rawMax - rawMin) * 0.08 : 0.5;
const pixelsPerMeter = (widthPx - CROSS_PAD.left - CROSS_PAD.right) / (maxOffset - minOffset);
const rawSpan = Math.max((rawMax - rawMin + padding * 2) * Math.max(exaggeration, 0.1), 1e-6);
const naturalHeight =
rawSpan * pixelsPerMeter + CROSS_PAD.top + CROSS_PAD.bottom + AREA_OVERLAY_HEADROOM_PX;
const heightPx = Math.max(naturalHeight, CROSS_HEIGHT);
const displaySpan = Math.max(heightPx - CROSS_PAD.top - CROSS_PAD.bottom, 1e-6) / pixelsPerMeter;
const displayMax = elevationMid + displaySpan / 2 + AREA_OVERLAY_HEADROOM_PX / pixelsPerMeter / 2;
const y = (elevation) => CROSS_PAD.top + (displayMax - elevation) * pixelsPerMeter;
const rawDisplaySpan = displaySpan / exaggeration;
const rawTop = elevationMid + (displayMax - elevationMid) / exaggeration;
const rawBottom = rawTop - rawDisplaySpan;
const plotHeightPx = heightPx - CROSS_PAD.top - CROSS_PAD.bottom;
// 표고 눈금 최소 1m(2026-08-23) — 화면 로직과 같게 맞춘다.
const step = Math.max(niceTickStep(rawDisplaySpan, 10, Math.floor(plotHeightPx / 14)), 1);
const out = [];
for (let tick = Math.ceil(rawBottom / step) * step; tick <= rawTop + 1e-9; tick += step) {
out.push({ tick, y: y(elevationMid + (tick - elevationMid) * exaggeration) });
}
return { out, step, top: CROSS_PAD.top, bottom: heightPx - CROSS_PAD.bottom, heightPx };
}
const CASES = [
{ rawMin: 536.0, rawMax: 541.0, halfWidth: 12 }, // 스크린샷과 비슷한 성토 단면
{ rawMin: 530.0, rawMax: 530.4, halfWidth: 6 }, // 거의 평탄(1m 눈금 두세 줄만 나온다)
{ rawMin: 500.0, rawMax: 528.0, halfWidth: 20 }, // 급경사 절토
{ rawMin: 536.0, rawMax: 541.0, halfWidth: 12, exaggeration: 2 }, // 과장 2배
];
for (const c of CASES) {
const { out, step, top, bottom } = ticksOf(c);
assert.ok(out.length >= 2, `눈금 부족: ${JSON.stringify(c)}${out.length}`);
assert.ok(step >= 1 - 1e-9, `1m 미만 눈금: ${step}`);
for (const t of out) {
assert.ok(Math.abs(t.tick - Math.round(t.tick)) < 1e-6, `정수 아닌 표고 눈금 ${t.tick}`);
}
for (const t of out) {
assert.ok(
t.y >= top - 1e-6 && t.y <= bottom + 1e-6,
`플롯 밖 눈금 ${t.tick.toFixed(2)} y=${t.y.toFixed(1)} (허용 ${top}~${bottom.toFixed(1)})`,
);
}
const spacing = out.length > 1 ? Math.abs(out[1].y - out[0].y) : Infinity;
assert.ok(spacing >= 14 - 1e-6, `눈금 간격 과밀 ${spacing.toFixed(1)}px (step=${step})`);
}
console.log("ok");