Files
Aislo/resources/tester/helper_b05_routeedit_pick.cjs
T
eomsangdonandClaude Sonnet 5.5 90194b2849 feat(B05): 계획노선 편집 창 — 측점 라벨 · 호 고르기 판정 함수(22-2 · 22-3 1차)
- 측점 라벨 글자 상자를 그릴 때 기록(drawStationTicks 의 onLabel · B04 지도 · 배수유역도 동작 그대로)
- stationAtScreen 이 눈금 다음 라벨 상자를 봄 · 솎여 안 보이는 눈금은 안 잡음
- 곡선 호 점열(curveArcVertices)과 점-호 판정(arcAtScreen) 더함
- 시험 resources/tester/test_b05_routeedit_pick.py 8건
- RouteEdit.ts 배선은 sub6 나누기 뒤 새 파일에 잇는 다음 커밋

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
2026-09-29 19:32:57 +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", "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 }),
);