/* ============================================================================= * B06_Section_UI_Cross_Pick_Order.ts * 횡단 카드 고르기 차례(295-1) — 고를 자리가 좁은 것이 위(모달 `pickAt` 과 같은 잣대). * * 판정면(구조물 · 띠 · 형상)을 다 꽂은 뒤 넓이 큰 것부터 아래로 다시 세움(작은 것 위) · * 보라 선 판정(넓이 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; /** `points` 속성 다각형 넓이(px²) — 점이 셋 못 되면 0. */ export function pointsArea(points: string | null): number { const pts = (points ?? "") .trim() .split(/\s+/) .map((pair) => pair.split(",").map(Number)) .filter((p) => p.length === 2 && p.every(Number.isFinite)); if (pts.length < 3) return 0; let twice = 0; pts.forEach(([x0, y0], i) => { const [x1, y1] = pts[(i + 1) % pts.length]; twice += x0 * y1 - x1 * y0; }); return Math.abs(twice) / 2; } /** 판정면을 넓이 차례로 다시 세우고 보라 선 판정을 맨 위로 · 끌기 누름을 막음. */ 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, ...edges, ...lines); let down: { x: number; y: number } | null = null; layer.addEventListener( "pointerdown", (event) => (down = { x: (event as PointerEvent).clientX, y: (event as PointerEvent).clientY }), true, ); layer.addEventListener( "click", (event) => { const { clientX, clientY } = event as MouseEvent; if (down && Math.hypot(clientX - down.x, clientY - down.y) > DRAG_PX) event.stopPropagation(); down = null; }, true, ); }