- 옮김: 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
223 lines
8.2 KiB
JavaScript
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));
|