Files
Aislo/resources/tester/helper_259_detail_region.cjs
T
eomsangdonandClaude Sonnet 5.5 03eddde558 test(508-1b 옛 헬퍼 공용 불러오기 2/7): 헬퍼 26 곳 loadTs 베낌 걷고 makeLoader 로
- 헬퍼 동작 그대로(풀이 함수 본문은 옮겨 담음) · 제품 코드 안 고침
- 확인: 이 묶음 헬퍼를 쓰는 시험 279 개 전 279 통과 → 후 279 통과 (시험별 junit 대조 · 어긋남 0)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 17:07:17 +09:00

219 lines
7.9 KiB
JavaScript

/* PLAN 259 단위검증 헬퍼 — [상세] 공용 팬 누름 풂 · 암 경계선 나눔 · 닫힌 영역 · 표 칸 → 그래프 고름.
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림. 입력 = 표준입력 JSON {design, samples, axes, rock} ·
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_259_detail_region.py`.
*/
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const B06 = path.join(ROOT, "B06_Section");
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
globalThis.window = { sessionStorage: new Map(), localStorage: new Map() };
globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} };
const STUBS = {
"@ui/ui_template_context_menu": { createMapContextMenu: () => ({}) },
"./B06_Section_UI_Section_Common": { CROSS_PAD },
};
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
const { loadTs } = makeLoader({
root: ROOT,
cache: false,
requireFor: (full) => (request) => {
if (STUBS[request]) return STUBS[request];
if (request.endsWith(".css")) return {};
if (request.startsWith("@util/"))
return loadTs(resolve(path.join(ROOT, "common_util", request.slice(6))));
return loadTs(resolve(path.join(path.dirname(full), request)));
},
});
const Z = loadTs(path.join(ROOT, "A00_Common", "b_svg_zoom_pan.ts"));
const G = loadTs(path.join(B06, "logic/B06_Section_UI_Cross_Detail_Geom.ts"));
const S = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Select.ts"));
const T = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Table.ts"));
const { design, samples, axes } = JSON.parse(fs.readFileSync(0, "utf8"));
const out = {};
/* ① 공용 팬 — 가운데 단추 팬(click 없음) 뒤 왼쪽 누름 click 이 안 삼켜짐 */
function panCase(panStart) {
const listeners = {};
const svg = {
addEventListener: (name, fn) => (listeners[name] ??= []).push(fn),
setAttribute: () => {},
classList: { add: () => {}, remove: () => {} },
setPointerCapture: () => {},
releasePointerCapture: () => {},
getBoundingClientRect: () => ({ left: 0, top: 0, width: 600, height: 300 }),
};
const layer = { setAttribute: () => {} };
Z.attachSvgZoomPan({
svg,
layer,
widthPx: 600,
heightPx: 300,
pad: { left: 10, right: 10, top: 10, bottom: 10 },
content: { x: -600, y: -300, width: 1800, height: 900 },
panStart,
});
const fire = (name, init = {}) => {
let stopped = false;
const event = {
button: 0,
clientX: 0,
clientY: 0,
pointerId: 1,
preventDefault: () => {},
stopPropagation: () => (stopped = true),
...init,
};
(listeners[name] ?? []).forEach((fn) => fn(event));
return stopped;
};
const drag = (button) => {
fire("pointerdown", { button, clientX: 100, clientY: 100 });
fire("pointermove", { button, clientX: 140, clientY: 120 });
fire("pointerup", { button, clientX: 140, clientY: 120 });
};
const click = (x = 50) => {
fire("pointerdown", { clientX: x, clientY: 50 });
fire("pointerup", { clientX: x, clientY: 50 });
return fire("click", { clientX: x, clientY: 50 });
};
return { drag, click, fire };
}
{
const middle = panCase();
middle.drag(1); // 가운데 단추 팬 — 브라우저는 click 을 안 보냄(auxclick)
out.panMiddle = { firstClickStopped: middle.click(), secondClickStopped: middle.click() };
// 거리이정표 꼴 — 빈 곳 왼쪽 끌기 팬(끌기로 끝난 click 은 막고 · 다음 누름은 통과)
const left = panCase((event) => event.button === 1 || event.button === 0);
left.drag(0);
// 끌기 끝 click(왼쪽은 브라우저가 보냄) — 막혀야 함
out.panLeft = { afterDragStopped: left.fire("click"), nextClickStopped: left.click() };
}
/* ② 암 경계선 — 그림 요소 → 조각(설계선과 만나는 점에서 나눔) */
const px = ([offset, elevation]) =>
[
CROSS_PAD.left + (axes.maxOffset - offset) * axes.pixelsPerMeter,
CROSS_PAD.top +
(axes.displayMax -
(axes.elevationMid + (elevation - axes.elevationMid) * axes.exaggeration)) *
axes.pixelsPerMeter,
].join(",");
const el = (classes, points, attrs = {}) => ({
tagName: "polyline",
classList: { contains: (name) => classes.includes(name) },
hasAttribute: (name) => name in attrs,
getAttribute: (name) => (name === "points" ? points.map(px).join(" ") : (attrs[name] ?? null)),
});
const designPts = design.design_line.map((p) => [p.offset_m, p.elevation_m]);
const groundPts = samples.filter((s) => s.valid !== false).map((s) => [s.offset_m, s.elevation_m]);
const rockPts = G.rockRuns(design, samples)[0];
const elements = [
el(["b06-chart__design-cross"], designPts, { "data-design-line": "" }),
el(["b06-chart__cross-profile"], groundPts),
el(["b06-chart__rock-boundary"], rockPts),
];
const pieces = S.collectPieces(elements, axes);
out.pieces = pieces;
const rockPieces = pieces.filter((p) => p.line === "rock");
out.rockLabel = S.selectionLabel(design, [rockPieces[1]]);
out.rockSource = S.selectionSource("length", [rockPieces[1]]);
out.rockLocal = T.localCellValue(out.rockSource, design, samples);
out.rockLength = G.pathLength(rockPieces[1].points);
/* ③ 닫힌 영역(259-2) — 면 찾기 · 열린 영역 제외 · 영역 고름 값 · 붙은 두 영역 = 맞닿은 변 뺌 */
const faces = G.findFaces(pieces);
out.faces = faces.map((face) => ({
area: face.area,
lines: face.edges.map((i) => pieces[i].line).sort(),
}));
const one = faces.map((face) =>
S.regionLabel(
[face],
face.edges.map((i) => pieces[i]),
),
);
out.faceLabels = one;
const union = S.regionEdges(faces);
const unionPieces = union.map((i) => pieces[i]);
out.union = {
lines: unionPieces.map((p) => p.line).sort(),
label: S.regionLabel(faces, unionPieces),
selectLabel: S.selectionLabel(design, unionPieces),
source: S.selectionSource("area", unionPieces),
};
out.union.local = T.localCellValue(out.union.source, design, samples);
// 열린 원지반만 = 면 없음
out.openFaces = G.findFaces(pieces.filter((p) => p.line === "ground")).length;
// 정사각형 하나 + 매달린 꼬리 — 꼬리 지우고 면 하나(넓이 1)
out.square = G.findFaces([
{
line: "ground",
points: [
[0, 0],
[1, 0],
],
},
{
line: "design",
points: [
[1, 0],
[1, 1],
[0, 1],
],
},
{
line: "rock",
points: [
[0, 1],
[0, 0],
],
},
{
line: "spoil",
points: [
[1, 1],
[3, 3],
],
},
]).map((face) => ({ area: face.area, edges: face.edges.sort() }));
/* ④ 표 칸 → 그래프(259-2) — 저장된 조각 짝 · 칸 그래프 짝 · 띠 값 글 */
const stored = G.pieceSource(rockPieces[1]);
out.same = {
forward: S.samePiece(rockPieces[1], stored),
reverse: S.samePiece(rockPieces[1], { ...stored, start: stored.end, end: stored.start }),
otherLine: S.samePiece(rockPieces[1], { ...stored, line: "ground" }),
otherPiece: S.samePiece(rockPieces[0], stored),
};
const table = {
cells: {
fence: out.rockSource,
cut_soil: { src: "area", key: "cut_rock" },
"r:r1:0": { src: "text", text: "글" },
},
rows: [],
columns: [],
};
out.cellGraph = ["fence", "cut_soil", "r:r1:0", "embankment"].map((key) => T.cellGraph(table, key));
out.bandLabel = [
T.bandLabel("cut_soil", { cut_soil_area_m2: 1.234 }),
T.bandLabel("cut_soil", {}),
T.bandLabel("ground", { cut_soil_area_m2: 1 }),
];
/* ⑤ 표 강조 바깥 한 줄(259-3) — 합친 묶음 이름 칸(두 줄) + 둘째 줄 이름 · 값 = ㄴ 모양 */
const P = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Pick.ts"));
out.outline = P.unionOutline([
[0, 0, 10, 20],
[10, 10, 20, 20],
[20, 10, 30, 20],
]);
process.stdout.write(JSON.stringify(out));