- 측점 라벨 글자 상자를 그릴 때 기록(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
162 lines
6.2 KiB
JavaScript
162 lines
6.2 KiB
JavaScript
/* 계획노선 편집 창 **측점 라벨 · 호 고르기**(계획서 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 }),
|
||
);
|