Files
Aislo/resources/tester/helper_306_underdrain_pick.cjs
T
eomsangdonandClaude Opus 5.5 0a8008ce09 feat(B06): 306 횡단 맹암거 안 엇갈려 겹친 돌 무늬 · 그림 전용 · 고르기 차례 = 안쪽 판정면 맨 아래 · 테두리 판정 면 판정 맨 위
- 돌 무늬: 켜마다 반 칸 엇갈림 · 이웃 · 위아래와 겹침 · 기울기 ±22° 번갈아 교차 · 맹암거 색(카드 · [상세] 모달 같은 그림)
- 맹암거 무리 pointer-events none — 성토 띠 · 포장 · 넓은 관 판정면이 맹암거 안에서도 골라짐
- 차례: 보라 선 > 맹암거 테두리 > 구조물 판정면(작은 것 위) > 그림 · 띠 > 맹암거 안쪽
- 시험 test_306_underdrain_pick

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-13 18:07:53 +09:00

247 lines
9.4 KiB
JavaScript

/* PLAN 306 단위검증 헬퍼 — 맹암거 돌 무늬(306-1) · 고르기 차례(306-2).
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌.
* 짝 = `test_306_underdrain_pick.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 cache = new Map();
const load = (file) => {
if (cache.has(file)) return cache.get(file);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(file, box.exports);
const near = (m) => {
if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`));
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
return new Proxy({}, { get: () => () => {} });
};
new Function("exports", "module", "require", js)(box.exports, box, near);
return box.exports;
};
/* ── 흉내 DOM ─────────────────────────────────────────────────────────── */
class Node {
constructor(tag, name = tag) {
this.tag = tag;
this.name = name;
this.attrs = {};
this.children = [];
this.listeners = [];
const self = this;
this.classList = {
contains: (c) => (self.attrs.class ?? "").split(/\s+/).includes(c),
add: (c) => (self.attrs.class = `${self.attrs.class ?? ""} ${c}`.trim()),
};
}
setAttribute(k, v) {
this.attrs[k] = String(v);
}
getAttribute(k) {
return this.attrs[k] ?? null;
}
hasAttribute(k) {
return k in this.attrs;
}
/** `[속성]` 꼴만 — 형상 고름이 쓰는 것. */
querySelectorAll(sel) {
const attr = /^\[([\w-]+)\]$/.exec(sel)[1];
return this.children.flatMap((k) => [
...(k.hasAttribute(attr) ? [k] : []),
...k.querySelectorAll(sel),
]);
}
append(...kids) {
kids.forEach((k) => {
const at = this.children.indexOf(k);
if (at >= 0) this.children.splice(at, 1);
this.children.push(k);
});
}
prepend(...kids) {
kids.forEach((k) => {
const at = this.children.indexOf(k);
if (at >= 0) this.children.splice(at, 1);
});
this.children.unshift(...kids);
}
insertBefore(k, ref) {
const at = this.children.indexOf(k);
if (at >= 0) this.children.splice(at, 1);
const to = ref ? this.children.indexOf(ref) : -1;
this.children.splice(to >= 0 ? to : this.children.length, 0, k);
}
cloneNode() {
const copy = new Node(this.tag, `${this.name}+edge`);
copy.attrs = { ...this.attrs };
return copy;
}
addEventListener(type, fn) {
this.listeners.push({ type, fn });
}
dispatchEvent(event) {
this.listeners.filter((l) => l.type === event.type).forEach((l) => l.fn(event));
return true;
}
}
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
globalThis.MouseEvent = class {
constructor(type) {
this.type = type;
this.clientX = 0;
this.clientY = 0;
}
stopPropagation() {}
};
const clickEvent = () => ({ type: "click", clientX: 0, clientY: 0, stopPropagation() {} });
const draw = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Underdrain.ts"));
const shapePick = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts"));
const order = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Pick_Order.ts"));
const out = {};
/* ── 306-1 돌 무늬 ────────────────────────────────────────────────────── */
// 288 시험과 같은 단면 — 좌 절토 · 우 성토 · 노면 6 m · 암거 높이 1 m · 1 m = 20 px
const design = {
road_edges: {
left: { offset_m: 3, elevation_m: 100.15 },
right: { offset_m: -3, elevation_m: 99.85 },
},
design_line: [
{ offset_m: 6, elevation_m: 102.9 },
{ offset_m: 3, elevation_m: 100.15 },
{ offset_m: -3, elevation_m: 99.85 },
{ offset_m: -7, elevation_m: 97.2 },
],
};
const PPM = 20;
const X = (o) => 300 - o * PPM;
const Y = (e) => 2100 - e * PPM;
const svg = new Node("g", "layer");
const group = draw.appendUnderdrainOverlay(
svg,
{ structure_id: "ud-1", chainage_m: 40, height_m: 1, slope_pct: 5 },
design,
X,
Y,
);
const stones = group.children.find((k) => k.tag === "g").children;
const rotate = (s) => /rotate\(([-\d.]+) ([-\d.]+) ([-\d.]+)\)/.exec(s.attrs.transform);
out.stones = stones.map((s) => {
const [, deg, cx, cy] = rotate(s).map(Number);
return {
cls: s.attrs.class,
deg,
cx,
cy,
w: Number(s.attrs.width),
h: Number(s.attrs.height),
};
});
out.groupClass = group.attrs.class;
/* ── 306-2 고르기 차례 ────────────────────────────────────────────────── */
const poly = (name, cls, pts) => {
const n = new Node("polygon", name);
n.setAttribute("class", cls);
n.setAttribute("points", pts.map((p) => p.join(",")).join(" "));
return n;
};
const box = (x0, y0, x1, y1) => [
[x0, y0],
[x1, y0],
[x1, y1],
[x0, y1],
];
// 꽂힌 옛 차례(카드와 같음) — 지면선 · 면적 띠 · 포장 · 맹암거 무리 · 관 판정면 · 보라 선
const layer = new Node("g", "plot");
const band = new Node("g", "band");
band.setAttribute("class", "b06-chart__area b06-chart__area--fill");
band.append(poly("bandPoly", "", box(300, 100, 460, 160)));
const body = group.children.find((k) => k.attrs.class === "b06-chart__underdrain-body");
const bodyPts = body.attrs.points.split(" ").map((p) => p.split(",").map(Number));
layer.append(
band,
group,
poly("bigPipe", "b06-chart__culvert-revet-hit", box(100, 111, 600, 117)), // 맹암거(≈2600 px²)보다 넓음
poly("smallPipe", "b06-chart__culvert-revet-hit", box(340, 110, 346, 114)),
);
const picked = [];
shapePick.attachShapePick(layer, (id) => picked.push(id));
layer.children.find((k) => k.classList.contains("b06-chart__shape-hit")).name = "underdrain";
const line = new Node("polyline", "road");
line.setAttribute("class", "b06-chart__design-hit");
line.setAttribute("points", `${X(3)},${Y(100.15)} ${X(-3)},${Y(99.85)}`);
layer.append(line);
// 옛 차례(306 전) — 형상 판정면이 넓이 순으로 면 판정 사이에 섬
const SURF = ["b06-chart__culvert-revet-hit", "b06-chart__shape-hit"];
const area = (n) => order.pointsArea(n.attrs.points ?? null);
const oldKids = [...layer.children];
const oldOrder = [
...oldKids.filter((k) => !SURF.some((c) => k.classList.contains(c)) && k !== line),
...oldKids
.filter((k) => SURF.some((c) => k.classList.contains(c)))
.sort((a, b) => area(b) - area(a)),
line,
];
order.stackPickTargets(layer);
out.order = layer.children.map((k) => k.name);
/* 누른 자리 → 맨 위에서 받는 것(흉내) — 그림 전용(맹암거 무리)은 안 받음 */
const inside = (pts, [x, y]) =>
pts.reduce((inn, [ax, ay], i) => {
const [bx, by] = pts[(i + 1) % pts.length];
return ay > y !== by > y && x < ax + ((y - ay) * (bx - ax)) / (by - ay) ? !inn : inn;
}, false);
const segGap = ([px, py], [ax, ay], [bx, by]) => {
const dx = bx - ax;
const dy = by - ay;
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy || 1)));
return Math.hypot(px - ax - t * dx, py - ay - t * dy);
};
const ptsOf = (n) => n.attrs.points.split(" ").map((p) => p.split(",").map(Number));
const ringGap = (pts, p) => Math.min(...pts.map((a, i) => segGap(p, a, pts[(i + 1) % pts.length])));
function receiver(kids, p) {
for (const k of [...kids].reverse()) {
if (k.classList.contains("b06-chart__underdrain")) continue; // pointer-events: none
if (k.classList.contains("b06-chart__design-hit")) {
const pts = ptsOf(k);
if (segGap(p, pts[0], pts[1]) <= 5) return k.name;
} else if (k.classList.contains("b06-chart__shape-hit--edge")) {
if (ringGap(ptsOf(k), p) <= 4) return k.name;
} else if (k.classList.contains("b06-chart__area")) {
if (k.children.some((c) => inside(ptsOf(c), p))) return k.name;
} else if (k.attrs.points && inside(ptsOf(k), p)) return k.name;
}
return null;
}
// 맹암거 몸 = (240,97) (360,103) (392.7,124.6) (263.1,118.2) · 띠 = x 300 부터 · 큰 관 = y 111~117
const PROBES = {
bandInUnderdrain: [330, 107], // 띠 · 맹암거 겹침(관 밖)
pipeInUnderdrain: [330, 114], // 큰 관 · 띠 · 맹암거 겹침
smallPipe: [343, 112],
underdrainOnly: [280, 107], // 맹암거만
border: [320, 121], // 아랫변 위 · 띠 안
road: [300, 100], // 노면 = 맹암거 윗변 · 보라 선
};
out.probesNew = Object.fromEntries(
Object.entries(PROBES).map(([k, p]) => [k, receiver(layer.children, p)]),
);
out.probesOld = Object.fromEntries(
Object.entries(PROBES).map(([k, p]) => [k, receiver(oldOrder, p)]),
);
// 테두리 누름 = 안쪽 판정면 창구 → 맹암거 id
const edge = layer.children.find((k) => k.classList.contains("b06-chart__shape-hit--edge"));
edge.listeners.filter((l) => l.type === "click").forEach((l) => l.fn(clickEvent()));
out.picked = picked;
out.body = bodyPts;
out.edgeCount = layer.children.filter((k) =>
k.classList.contains("b06-chart__shape-hit--edge"),
).length;
process.stdout.write(JSON.stringify(out));