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
This commit is contained in:
@@ -5,12 +5,19 @@
|
||||
* 판정면(구조물 · 띠 · 형상)을 다 꽂은 뒤 넓이 큰 것부터 아래로 다시 세움(작은 것 위) ·
|
||||
* 보라 선 판정(넓이 0 · 요구 185 사면 손잡이)은 맨 위 · 면적 띠(절 · 성토)는 맨 아래 그대로.
|
||||
* 끌고 뗀 누름(3 px 넘게 움직임)은 고름 아님.
|
||||
* 맹암거(306-2) = 안쪽 판정면은 맨 아래(겹친 띠 · 구조물이 위) · 테두리 판정(투명 굵은 선)이 면 판정 맨 위.
|
||||
* ========================================================================== */
|
||||
|
||||
import { thickenDown } from "./B06_Section_UI_Cross_Shape_Pick";
|
||||
|
||||
/** 면 판정 — 구조물(벽 · 관 · 박스 · 도수로 띠 등) · 물넘이포장 · 맹암거 형상. */
|
||||
const SURFACE_HIT_CLASSES = ["b06-chart__culvert-revet-hit", "b06-chart__shape-hit"];
|
||||
/** 선 판정 — 보라 설계선 조각(`_Cross_Design_Pick`). */
|
||||
const LINE_HIT_CLASS = "b06-chart__design-hit";
|
||||
/** 맹암거 몸(그림) · 테두리 판정 클래스(306-2). */
|
||||
const UNDERDRAIN_GROUP = "b06-chart__underdrain";
|
||||
const UNDERDRAIN_BODY = "b06-chart__underdrain-body";
|
||||
const EDGE_CLASS = "b06-chart__shape-hit--edge";
|
||||
/** 끌기로 볼 움직임(px) — 형상 고름(`SHAPE_DRAG_PX`)과 같은 값. */
|
||||
const DRAG_PX = 3;
|
||||
|
||||
@@ -33,13 +40,36 @@ export function pointsArea(points: string | null): number {
|
||||
/** 판정면을 넓이 차례로 다시 세우고 보라 선 판정을 맨 위로 · 끌기 누름을 막음. */
|
||||
export function stackPickTargets(layer: SVGElement): void {
|
||||
const kids = [...layer.children];
|
||||
// 맹암거 판정면 = 형상 고름이 몸 둘레(`thickenDown`)로 꽂은 것
|
||||
const bodies = new Set(
|
||||
kids
|
||||
.filter((el) => el.classList.contains(UNDERDRAIN_GROUP))
|
||||
.flatMap((group) => [...group.children])
|
||||
.filter((el) => el.classList.contains(UNDERDRAIN_BODY))
|
||||
.map((el) => thickenDown(el.getAttribute("points") ?? "")),
|
||||
);
|
||||
const inners = kids.filter(
|
||||
(el) =>
|
||||
el.classList.contains("b06-chart__shape-hit") && bodies.has(el.getAttribute("points") ?? ""),
|
||||
);
|
||||
const edges = inners.map((inner) => {
|
||||
layer.prepend(inner);
|
||||
const edge = inner.cloneNode() as SVGElement;
|
||||
edge.classList.add(EDGE_CLASS);
|
||||
edge.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
inner.dispatchEvent(new MouseEvent("click")); // 고름은 안쪽 판정면 창구 하나
|
||||
});
|
||||
return edge;
|
||||
});
|
||||
const surfaces = kids
|
||||
.filter((el) => SURFACE_HIT_CLASSES.some((name) => el.classList.contains(name)))
|
||||
.filter((el) => !inners.includes(el))
|
||||
.map((el) => ({ el, area: pointsArea(el.getAttribute("points")) }))
|
||||
.sort((a, b) => b.area - a.area)
|
||||
.map(({ el }) => el);
|
||||
const lines = kids.filter((el) => el.classList.contains(LINE_HIT_CLASS));
|
||||
layer.append(...surfaces, ...lines);
|
||||
layer.append(...surfaces, ...edges, ...lines);
|
||||
let down: { x: number; y: number } | null = null;
|
||||
layer.addEventListener(
|
||||
"pointerdown",
|
||||
|
||||
@@ -5,17 +5,39 @@
|
||||
* · 노면 = 설계 노면 — 서버 · TS 짝이 암거 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음.
|
||||
* · 옵셋선 = 노면을 암거 높이만큼 아래로 같은 각도.
|
||||
* · 성토 쪽 = 옵셋선을 성토 사면과 만날 때까지 · 절토 쪽 = 절토 사면을 아래로 늘여 옵셋선과 만남.
|
||||
* · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(흙막이 돌 그리기 `drawStones`).
|
||||
* · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(306-1) — 켜마다 반 칸 엇갈리고 이웃과 겹친 돌 ·
|
||||
* 기울기를 번갈아 서로 교차 · 그림 전용(누름 안 받음 · CSS `.b06-chart__underdrain`).
|
||||
* 높이가 비면 그리지 않음(수치를 지어내지 않음).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossDesign } from "./B06_Section_Api_Fetch";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { drawStones } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
import { cell, courses, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
let clipSeq = 0;
|
||||
/** 돌 한 칸(m · 보기용) · 이웃과 겹침 배수 · 번갈아 기울기(도). */
|
||||
const STONE_M = 0.25;
|
||||
const STONE_OVERLAP = 1.4;
|
||||
const STONE_TILT_DEG = 22;
|
||||
|
||||
/** 엇갈려 겹친 돌 무늬(306-1) — 켜(가로줄)마다 반 칸 밀고 · 돌은 칸보다 커서 이웃 · 위아래와 겹침 ·
|
||||
* 기울기를 번갈아 서로 교차. `frame` v = −0.5(노면) ~ 0.5(옵셋선). */
|
||||
export function drawCrossedStones(group: SVGElement, frame: BandFrame, heightM: number): void {
|
||||
const rows = courses(heightM, STONE_M);
|
||||
const count = courses(frame.spanM, STONE_M);
|
||||
const across = (heightM / rows) * STONE_OVERLAP;
|
||||
const along = (frame.spanM / count) * STONE_OVERLAP;
|
||||
for (let r = 0; r < rows; r += 1) {
|
||||
const shift = r % 2 ? 0 : 0.5;
|
||||
for (let i = 0; i + shift <= count; i += 1) {
|
||||
const tilt = (i + r) % 2 ? STONE_TILT_DEG : -STONE_TILT_DEG;
|
||||
const lean = { ...frame, leanDegrees: frame.leanDegrees + tilt };
|
||||
group.append(cell(lean, (i + shift) / count, (r + 0.5) / rows - 0.5, across, along, 0.45));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Edge = { offset_m: number; elevation_m: number };
|
||||
|
||||
@@ -120,7 +142,7 @@ export function appendUnderdrainOverlay(
|
||||
};
|
||||
const [ax, ay] = px(at(0, 0));
|
||||
const [bx, by] = px(at(1, 0));
|
||||
drawStones(
|
||||
drawCrossedStones(
|
||||
stones,
|
||||
{
|
||||
spanM: Math.hypot(hi - lo, grade * (hi - lo)),
|
||||
@@ -128,7 +150,6 @@ export function appendUnderdrainOverlay(
|
||||
point: (u, v) => px(at(u, v)),
|
||||
leanDegrees: (Math.atan2(bx - ax, -(by - ay)) * 180) / Math.PI,
|
||||
},
|
||||
false,
|
||||
height,
|
||||
);
|
||||
group.append(body, clip, stones);
|
||||
|
||||
@@ -267,7 +267,25 @@
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* 맹암거(288-1): 노면 아래 돌 무늬 형상 */
|
||||
/* 맹암거(288-1): 노면 아래 돌 무늬 형상 · 그림 전용 — 누름은 판정면(306-2) */
|
||||
.b06-chart__underdrain {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 엇갈려 겹친 돌(306-1) — 맹암거 색 · 밝음 · 어두움 모두 읽힘 */
|
||||
.b06-chart__underdrain .b06-chart__culvert-stone {
|
||||
stroke: #3aa0a8;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* 맹암거 테두리 판정(306-2) — 면 판정 맨 위 · 안쪽 판정면은 맨 아래 */
|
||||
.b06-chart__shape-hit.b06-chart__shape-hit--edge {
|
||||
fill: none;
|
||||
stroke: transparent;
|
||||
stroke-width: 8;
|
||||
pointer-events: stroke;
|
||||
}
|
||||
|
||||
.b06-chart__underdrain-body {
|
||||
fill: color-mix(in srgb, #3aa0a8 16%, transparent);
|
||||
stroke: #3aa0a8;
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
/* 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));
|
||||
@@ -0,0 +1,106 @@
|
||||
"""PLAN 306 — B06 횡단 맹암거 안 돌 무늬 · 고르기 차례(요구 200-1).
|
||||
|
||||
- 306-1 돌 무늬 = 켜마다 반 칸 엇갈리고 이웃 · 위아래와 겹친 돌 · 기울기 번갈아 교차 · 그림 전용(누름 안 받음)
|
||||
- 306-2 고르기 차례 = 맹암거 안쪽 판정면 맨 아래(겹친 띠 · 관이 위) · 테두리 판정 = 면 판정 맨 위 ·
|
||||
보라 선 맨 위 그대로 · 다른 구조물 차례(작은 것 위) 그대로
|
||||
|
||||
실제 TS 를 `helper_306_underdrain_pick.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
B06 = os.path.join(ROOT, "B06_Section")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_306_underdrain_pick.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _rows(out: dict) -> list[list[dict]]:
|
||||
"""돌을 가로(따라) · 노면 아래 깊이(켜) 좌표로 — 깊이 같은 것끼리 한 켜."""
|
||||
(ax, ay), (bx, by) = out["body"][0], out["body"][1]
|
||||
rows: dict[float, list[dict]] = defaultdict(list)
|
||||
for s in out["stones"]:
|
||||
depth = s["cy"] - (ay + (s["cx"] - ax) * (by - ay) / (bx - ax))
|
||||
rows[round(depth, 1)].append({**s, "along": s["cx"]})
|
||||
return [sorted(rows[k], key=lambda s: s["along"]) for k in sorted(rows)]
|
||||
|
||||
|
||||
def test_돌_무늬_엇갈려_겹침_306_1(out):
|
||||
assert out["groupClass"] == "b06-chart__underdrain"
|
||||
assert out["stones"] and all(s["cls"] == "b06-chart__culvert-stone" for s in out["stones"])
|
||||
rows = _rows(out)
|
||||
assert len(rows) == 4 # 높이 1 m ÷ 0.25
|
||||
step = rows[0][1]["along"] - rows[0][0]["along"]
|
||||
gap = abs(rows[1][0]["along"] - rows[0][0]["along"]) % step
|
||||
assert abs(gap - step / 2) < 1e-6 # 이웃 켜는 반 칸 엇갈림
|
||||
for row in rows:
|
||||
# 돌 길이(따라) > 간격 · 돌 높이(가로질러) > 켜 간격 → 이웃 · 위아래와 겹침
|
||||
assert all(s["h"] > step * 1.3 for s in row)
|
||||
degs = [s["deg"] for s in row]
|
||||
assert all(a != b for a, b in zip(degs, degs[1:])) # 기울기 번갈아 → 교차
|
||||
row_gap = 20 / len(rows) # 암거 높이 1 m = 20 px
|
||||
assert all(s["w"] > row_gap for s in out["stones"])
|
||||
assert len({s["deg"] for s in out["stones"]}) == 2
|
||||
|
||||
|
||||
def test_돌_무늬는_누름_안_받음_306_1():
|
||||
with open(os.path.join(B06, "B06_Section_UI_Style_Cross_Areas.css"), encoding="utf-8") as f:
|
||||
css = f.read()
|
||||
assert re.search(r"\.b06-chart__underdrain \{\s*pointer-events: none;", css)
|
||||
assert re.search(r"\.b06-chart__shape-hit--edge \{[^}]*pointer-events: stroke;", css)
|
||||
|
||||
|
||||
def test_고르기_차례_306_2(out):
|
||||
# 안쪽 판정면 맨 아래 · 구조물 판정면 넓이 차례(작은 관 위) 그대로 · 테두리 = 면 판정 맨 위 · 보라 선 맨 위
|
||||
assert out["order"] == [
|
||||
"underdrain",
|
||||
"band",
|
||||
"g",
|
||||
"bigPipe",
|
||||
"smallPipe",
|
||||
"underdrain+edge",
|
||||
"road",
|
||||
]
|
||||
assert out["edgeCount"] == 1
|
||||
|
||||
|
||||
def test_누른_자리_받는_것_306_2(out):
|
||||
old, new = out["probesOld"], out["probesNew"]
|
||||
# 옛 차례 — 맹암거 안 성토 띠 · 맹암거보다 넓은 관이 막힘
|
||||
assert old["bandInUnderdrain"] == "underdrain" and old["pipeInUnderdrain"] == "underdrain"
|
||||
assert new == {
|
||||
"bandInUnderdrain": "band",
|
||||
"pipeInUnderdrain": "bigPipe",
|
||||
"smallPipe": "smallPipe",
|
||||
"underdrainOnly": "underdrain",
|
||||
"border": "underdrain+edge",
|
||||
"road": "road",
|
||||
}
|
||||
|
||||
|
||||
def test_테두리_누름은_맹암거_고름_306_2(out):
|
||||
assert out["picked"] == ["ud-1"]
|
||||
Reference in New Issue
Block a user