Files
Aislo/resources/tester/helper_b05_routeedit_pick.cjs
T
eomsangdonandClaude Opus 5.5 281c71b55b refactor(logic): 348-1 순수 계산 TS 49 파일 <페이지>/logic/ 로 옮김 — 이름 앞머리 그대로 · 동작 고침 없음
- 옮김: B04 2 · B05 23 · B06 11 · A00_Common/design 13 (9,299 줄) · 이미 logic/ 에 있던 16 파일 그대로
- 안 옮김: 묶음 입구(package.json build:*) · 파이썬 · 순수지만 계산 아닌 8 (보기 결선 · 콜백 · API 타입 · 갈아끼움 알림판)
- import 266 줄(142 파일) · 시험 · 주석 · 짝 표시 문자열 경로 고침 · B05 · B06 CLAUDE.md 짝 줄 logic/
- 시험 도우미 새 경로: 가짜 require 열쇠 · B05()/B06() 부름 · 형제 import 자리 · tsc 묶어 돌리는 시험 · 정규식 · rglob
- design_view_cross 줄 한도 시험 685 → 688 (긴 import 경로 줄바꿈 3줄)
- 확인: typecheck 0 · 서버 묶음 넷(corridor · server-calc · b07-extent · b07-note) 다시 지어 코드 같음(//#region 경로 주석만 다름) · 관련 시험 270 파일 — 남은 실패 19 는 옮기기 전과 같음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-16 13:44:50 +09:00

162 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 계획노선 편집 창 **측점 라벨 · 호 고르기**(계획서 22-2 · 22-3) 단위검증 헬퍼 —
* `_RouteEdit_Input.ts` · `_RouteEdit_Curve.ts` · B04 `drawStationTicks` 를 트랜스파일해 Node 로 돌린다.
* 브라우저 전용 팔레트·표기 함수는 가짜로 채운다 — 셈만 본다.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
const STUBS = {
"@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback },
"@util/common_util_svg": {
stationLabel: (chainage, interval) => `No.${Math.round(chainage / interval)}`,
},
};
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const input = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Input.ts"));
const curve = loadTs(path.join(ROOT, "B05_Profile", "logic", "B05_Profile_UI_RouteEdit_Curve.ts"));
const overlays = loadTs(path.join(ROOT, "B04_PreProcess", "B04_PreProcess_UI_MapOverlays.ts"));
// 화면 = 사업지 m × 2, y 뒤집음(실제 `metricToScreen` 처럼 y 가 화면에서 뒤집힌다).
const toScreen = ([x, y]) => [x * 2, 400 - y * 2];
const near = (a, b, tol) => Math.hypot(a[0] - b[0], a[1] - b[1]) <= tol;
// 노드 세 개 · 가운데 R=30 곡선 하나.
const nodes = [
[0, 0],
[100, 0],
[100, 100],
];
const built = curve.buildEditedPolyline(nodes, [true, true, true], [null, 30, null], 12);
const c = built.curves[0];
// ① 호 점열 == 그려지는 선에서 시작~끝점 사이 조각(거울).
const drawn = built.vertices;
const iStart = drawn.findIndex((v) => near(v, c.start, 1e-9));
const iEnd = drawn.findIndex((v) => near(v, c.end, 1e-9));
const arc = curve.curveArcVertices(c);
const arcMatchesDrawn =
arc.length === iEnd - iStart + 1 && arc.every((v, i) => near(v, drawn[iStart + i], 1e-9));
// ② 호 누름 — 호 가운데 · 호에서 5px 비킨 곳(잡힘) · 곧은 선 · 빈 곳.
const mid = drawn[Math.floor((iStart + iEnd) / 2)];
const [mx, my] = toScreen(mid);
const arcAt = (px, py) => input.arcAtScreen(built.curves, toScreen, px, py);
const pick = {
onArc: arcAt(mx, my),
nearArc: arcAt(mx + 3, my + 3),
farFromArc: arcAt(mx - 60, my + 60),
onStraight: arcAt(...toScreen([50, 0])),
empty: arcAt(5, 5),
noCurves: input.arcAtScreen([], toScreen, mx, my),
};
// ③ 반대로 도는(오른쪽 꺾임) 곡선도 같은 판정.
const built2 = curve.buildEditedPolyline(
[
[0, 0],
[100, 0],
[100, -100],
],
[true, true, true],
[null, 30, null],
12,
);
const mid2 = built2.vertices[Math.floor(built2.vertices.length / 2)];
const pick2 = input.arcAtScreen(built2.curves, toScreen, ...toScreen(mid2));
// ④ 측점 라벨 — 가짜 캔버스로 `drawStationTicks` 를 돌려 상자를 받고, 그린 사각형과 같은지 본다.
const rects = [];
const fakeContext = {
save() {},
restore() {},
beginPath() {},
moveTo() {},
lineTo() {},
stroke() {},
translate() {},
rotate() {},
fillText() {},
fillRect: (x, y, w, h) => rects.push([x, y, w, h]),
measureText: (text) => ({ width: text.length * 6 }),
};
const line = [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
];
const boxes = [];
overlays.drawStationTicks(fakeContext, line, {
intervalM: 20,
pxPerMeter: 4, // 간격 80px → 두 칸에 하나(step 2)
toScreen: (x, y) => toScreen([x, y]),
uprightRad: 0.5,
onLabel: (box) => boxes.push(box),
});
const labelsMatchRects =
boxes.length === rects.length &&
boxes.every(
(box, i) =>
Math.abs(rects[i][0] - (box.x - box.half)) < 1e-9 &&
Math.abs(rects[i][1] - (box.y - 8)) < 1e-9 &&
Math.abs(rects[i][2] - box.half * 2) < 1e-9,
);
// ⑤ 라벨 누름 — 라벨 가운데 · 라벨 밖 · 돌린 상자 모서리 안팎 · 눈금 우선.
const target = boxes[1];
const stationLine = [
[0, 0],
[100, 0],
];
const stationAt = (px, py, labels) =>
input.stationAtScreen(stationLine, toScreen, 20, px, py, 11, labels);
const rot = (dx, dy, rad) => [
dx * Math.cos(rad) - dy * Math.sin(rad),
dx * Math.sin(rad) + dy * Math.cos(rad),
];
// 돌린 상자의 오른쪽 위 모서리 안쪽(-1px) · 바깥쪽(+3px) — 세워 본 좌표(half, 8)를 상자 각만큼 돌린 자리.
const insideCorner = rot(target.half - 1, 7, target.uprightRad);
const outsideCorner = rot(target.half + 3, 11, target.uprightRad);
const tickScreen = toScreen([40, 0]); // 라벨이 그려진 측점(0 · 40 · 80)의 눈금
const hiddenTickScreen = toScreen([20, 0]); // 솎여 라벨이 없는 측점의 눈금
const label = {
count: boxes.length,
chainages: boxes.map((b) => b.chainage),
onLabel: stationAt(target.x, target.y, boxes),
insideCorner: stationAt(target.x + insideCorner[0], target.y + insideCorner[1], boxes),
outsideCorner: stationAt(target.x + outsideCorner[0], target.y + outsideCorner[1], boxes),
// 눈금 자리를 누르면 다른 측점 라벨 상자와 겹쳐도 눈금 몫이다.
tickBeatsLabel: stationAt(tickScreen[0], tickScreen[1], [
target,
{ chainage: 80, x: tickScreen[0], y: tickScreen[1], half: 30, uprightRad: 0 },
]),
// 안 보이는(솎인) 눈금은 누름을 가로채지 않는다 — 라벨 정보가 없을 때만 전부 본다.
hiddenTick: stationAt(hiddenTickScreen[0], hiddenTickScreen[1], boxes),
hiddenTickNoInfo: stationAt(hiddenTickScreen[0], hiddenTickScreen[1], []),
emptyNoLabels: stationAt(target.x, target.y, []),
emptyElsewhere: stationAt(5, 5, boxes),
};
process.stdout.write(
JSON.stringify({ arcMatchesDrawn, pick, pick2, node: c.node_first, labelsMatchRects, label }),
);