Files
Aislo/resources/tester/helper_295_pick_order_detail.cjs
T

274 lines
10 KiB
JavaScript

/* PLAN 295 단위검증 헬퍼 — 횡단 카드 고르기 차례 · [상세] 모달 고르기 차례 · 첫 보기 · 구조물 선 안 나눔.
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌.
* 짝 = `test_295_pick_order_detail.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 P = loadTs(path.join(B06, "B06_Section_UI_Cross_Pick_Order.ts"));
const G = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Geom.ts"));
const S = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Select.ts"));
const H = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Home.ts"));
const out = {};
/* ── 카드: 흉내 층 ───────────────────────────────────────────────────── */
function node(name, classes, points) {
return {
name,
parent: null,
tagName: points && classes.includes("b06-chart__design-hit") ? "polyline" : "polygon",
classList: { contains: (c) => classes.includes(c) },
getAttribute: (a) => (a === "points" ? points : null),
};
}
function layer(kids) {
const listeners = [];
const self = {
children: [],
append: (...nodes) =>
nodes.forEach((n) => {
const at = self.children.indexOf(n);
if (at >= 0) self.children.splice(at, 1);
self.children.push(n);
}),
addEventListener: (type, fn, capture) => listeners.push({ type, fn, capture }),
fire: (type, event) => {
let stopped = false;
const full = { ...event, stopPropagation: () => (stopped = true) };
listeners.filter((l) => l.type === type && l.capture).forEach((l) => l.fn(full));
return stopped;
},
};
self.append(...kids);
return self;
}
const box = (x0, y0, x1, y1) => `${x0},${y0} ${x1},${y0} ${x1},${y1} ${x0},${y1}`;
const HIT = "b06-chart__culvert-revet-hit";
// 꽂힌 옛 차례 — 면적 띠(맨 아래) · 보라 선(구조물 판정면 앞) · 형상 · 도수로 띠 · 벽 · 관
const kids = [
node("band", ["b06-chart__area"], box(0, 0, 600, 300)),
node("slope", ["b06-chart__design-hit"], "400,100 500,200"),
node("top", ["b06-chart__design-hit"], "300,100 400,100"),
node("shape", ["b06-chart__shape-hit"], box(300, 100, 310, 110)), // 100
node("chute", [HIT, "b06-chart__chute-hit"], box(390, 90, 510, 210)), // 14400 — 성토 사면을 덮음
node("wall", [HIT], box(200, 100, 220, 300)), // 4000
node("pipe", [HIT], box(250, 250, 255, 255)), // 25
];
const L = layer(kids);
P.stackPickTargets(L);
out.cardOrder = L.children.map((n) => n.name);
// 누른 자리에서 맨 위 판정(뒤에서부터) — 다각형 안 · 선은 5 px 안
const inside = (p, pts) => {
let inn = false;
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
const [ax, ay] = pts[i];
const [bx, by] = pts[j];
if (ay > p[1] !== by > p[1] && p[0] < ((bx - ax) * (p[1] - ay)) / (by - ay) + ax) inn = !inn;
}
return inn;
};
const gap = (p, [a, b]) => {
const d = [b[0] - a[0], b[1] - a[1]];
const t = Math.max(
0,
Math.min(1, ((p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1]) / (d[0] ** 2 + d[1] ** 2)),
);
return Math.hypot(p[0] - a[0] - t * d[0], p[1] - a[1] - t * d[1]);
};
const ptsOf = (n) =>
n
.getAttribute("points")
.split(" ")
.map((q) => q.split(",").map(Number));
const topAt = (p, order) =>
[...order]
.reverse()
.find((n) =>
n.classList.contains("b06-chart__design-hit") ? gap(p, ptsOf(n)) <= 5 : inside(p, ptsOf(n)),
)?.name;
out.slopeClick = topAt([450, 150], L.children); // 띠 안 성토 사면 위
out.slopeClickOld = topAt([450, 150], kids); // 옛 차례
out.chuteClick = topAt([480, 120], L.children); // 띠 안 · 선 밖
out.pipeOverWall = topAt([252, 252], [...L.children]);
out.area = P.pointsArea(box(0, 0, 10, 20));
// 끌기 = 고름 아님
L.fire("pointerdown", { clientX: 10, clientY: 10 });
out.dragStopped = L.fire("click", { clientX: 20, clientY: 10 });
L.fire("pointerdown", { clientX: 10, clientY: 10 });
out.clickStopped = L.fire("click", { clientX: 11, clientY: 11 });
/* ── 모달: 고르기 차례 ───────────────────────────────────────────────── */
const sq = (x0, y0, x1, y1) => [
[x0, y0],
[x1, y0],
[x1, y1],
[x0, y1],
];
const big = sq(0, 0, 100, 100);
const small = sq(40, 40, 60, 60);
const edges = (ring) => ring.map((p, i) => [p, ring[(i + 1) % ring.length]]);
const paths = [...edges(big), ...edges(small)];
const pieces = paths.map((points) => ({ line: "design", points }));
const rings = [big, small];
const areas = [10000, 400];
const pick = (p, band = null) => S.pickAt(p, 1, paths, rings, areas, pieces, band);
out.modal = {
line: pick([50, 41]), // 작은 면 둘레 1 px
small: pick([50, 50]),
big: pick([20, 20]),
bandSmaller: pick([50, 50], 100),
bandBigger: pick([50, 50], 5000),
bandBigFace: pick([20, 20], 5000),
bandOnly: S.pickAt([500, 500], 1, paths, rings, areas, pieces, 50),
lineOverBand: pick([50, 41], 1),
};
/* ── 모달: 첫 보기 ───────────────────────────────────────────────────── */
const plot = { x: 50, y: 10, width: 500, height: 280 };
const center = [300, 150];
const drawn = [
[100, 120],
[500, 140],
[300, 360], // 아래로 210 — 큰 쪽
[200, 100],
[5000, 9000], // 가로 창 밖 = 셈 안 함
];
const home = H.detailHomeView(drawn, center, plot, 1, 16);
const map = ([x, y]) => [home.tx + home.scale * x, home.ty + home.scale * y];
out.home = {
scale: home.scale,
center: map(center),
plotCenter: [plot.x + plot.width / 2, plot.y + plot.height / 2],
fits: drawn.slice(0, 4).every(([x, y]) => {
const [sx, sy] = map([x, y]);
return (
sx >= plot.x - 1e-6 &&
sx <= plot.x + plot.width + 1e-6 &&
sy >= plot.y - 1e-6 &&
sy <= plot.y + plot.height + 1e-6
);
}),
flat: H.detailHomeView([[300, 150]], center, plot, 2, 16).scale,
};
/* ── 모달: 구조물 선 안 나눔 ─────────────────────────────────────────── */
// 상자(구조물 네 변 · 변마다 묶음) + 가로지르는 원지반선
const boxLines = edges(sq(0, 0, 10, 10)).map((points, group) => ({
line: "structure",
points,
group,
}));
const split = G.splitAtCrossings([
...boxLines,
{
line: "ground",
points: [
[-5, 5],
[15, 5],
],
},
]);
const left = split.findIndex(
(p) => p.line === "structure" && p.points.every(([x]) => Math.abs(x) < 1e-9),
);
const members = S.groupMembers(split, left);
const chosen = S.toggleMembers([], members, false);
out.structure = {
pieces: split.length,
groundPieces: split.filter((p) => p.line === "ground").length,
members: members.length,
chosen: chosen.length,
again: S.toggleMembers(chosen, members, false).length,
additiveOff: S.toggleMembers([...chosen, 99], members, true),
label: S.selectionLabel(
undefined,
chosen.map((i) => split[i]),
),
faces: G.findFaces(split)
.map((f) => Math.round(f.area * 100) / 100)
.sort(),
// 옛 저장 조각(구조물 선 아래 반쪽) — 그대로 다시 고름
oldPick: split.filter((p) => S.samePiece(p, { line: "structure", start: [0, 0], end: [0, 5] }))
.length,
wholeValue: G.piecesValue(
"length",
chosen.map((i) => G.pieceSource(split[i])),
{},
),
};
// collectPieces — 구조물 선(설계선 표식 없음)에 묶음 · 설계선은 없음
const axes = {
widthPx: 600,
heightPx: 300,
pixelsPerMeter: 10,
maxOffset: 20,
displayMax: 120,
elevationMid: 110,
exaggeration: 1,
};
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 collected = S.collectPieces(
[
el(
["b06-chart__design-cross"],
[
[-10, 110],
[10, 110],
],
{ "data-design-line": "" },
),
el(
["b06-chart__design-cross"],
[
[0, 105],
[0, 115],
],
),
],
axes,
);
out.collected = collected.map((p) => ({ line: p.line, group: p.group ?? null }));
process.stdout.write(JSON.stringify(out));