- 돌 무늬: 켜마다 반 칸 엇갈림 · 이웃 · 위아래와 겹침 · 기울기 ±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
247 lines
9.4 KiB
JavaScript
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));
|