Files
Aislo/resources/tester/helper_259_detail_region.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

223 lines
8.2 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 ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
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);
function loadTs(full) {
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
const localRequire = (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)));
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
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));