- 헬퍼 동작 그대로(풀이 함수 본문은 옮겨 담음) · 제품 코드 안 고침 - 확인: 이 묶음 헬퍼를 쓰는 시험 279 개 전 279 통과 → 후 279 통과 (시험별 junit 대조 · 어긋남 0) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
219 lines
7.9 KiB
JavaScript
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));
|