Files
Aislo/B06_Section/B06_Section_UI_Cross_Detail_Pick.ts
T
eomsangdonandClaude Opus 5.5 fe025b6db2 feat(B06 횡단): 암 경계선 고름 · 방향키 · 세로 끌기 · 아래 띠 공용 칸 (370-3)
- 새 B06_Section_UI_Cross_Line_Cell.ts — 아래 띠 왼쪽 공용 칸(제목 · ◀▲↺▼▶ 한 벌 · 값 한 줄 말줄임 · 길게 누름 반복) · 숨은 버튼은 방향키 슬롯 뗌
- Design_Pick — 암 점선 위 판정 선(키 rock · ▲▼↺ · ◀▶ 막음 · 방향키 = 암만) · 떠 있는 선 조정창 · 우측 위 치수 라벨 걷음 · 안 고르면 암 칸 · 고르면 그 선(노면 · 사면 · 소단 · 중심점 · 교차점 · 암)
- Center.ts bindAxisDrag(한 축 끌기 · 놓을 때 한 번 · click 한 번 막음) — 중심점 · 암 세로 끌기 같이 씀
- buildRockBoundaryControl = 공용 칸 「안 고름」 꼴(Cross_Design) · Card_Chrome 띠 왼쪽 = 공용 칸 · Detail_Pick 표 강조에 암 판정 선 뺌
- CSS(Cross.css 공용 칸 · Areas.css 암 고름 띠 · Controls.css 주석) · 시험 test_370_3 새로 · 137 · 139_6 · 179_1 · 193_1 · 194_1 · 318 · 356 · 370_2 고쳐 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-17 12:14:43 +09:00

126 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B06_Section_UI_Cross_Detail_Pick.ts
* [상세] 수량표 칸 누름 → 그림 복제 안 그 항목 강조(150-4).
*
* 칸 키 → 그림 요소: 면적 띠(깍기 · 쌓기) · 사토장 선 · 중심 십자(높이 넷) · 보라 판정 선 구역
* (사면 · 노면 · 측구). 판정 선은 화면 좌표라 십자(x(0)) · `data-px-per-m` 로 m 로 되돌려
* `crossZoneAt` 으로 가름 — 측구 = 단면유형을 성토 양쪽으로 둬도 절토로 남는 자리.
* 편책 · 제근 · 표토제거는 늘 빈칸 = 누름 없음(짝 표에서 뺌) · 대상이 그 카드에 없는 칸 = `presentKeys` 밖.
* 고른 칸 묶음 = 바깥 한 줄(`outlinePicked` · 259-3).
* ponytail: 판정 선의 구조물 트림 여부는 복제에서 모름 → 자리로만 가름(트림 선은 놓인 구역을 따름).
* ========================================================================== */
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import { crossZoneAt, type CrossZone } from "./B06_Section_UI_Cross_Design_Pick";
type LineZone = CrossZone | "ditch" | "cut_slope";
/** 칸 키 → 강조 대상(선택자 = 그 요소 · zone = 그 구역 판정 선). 없는 키 = 누름 없음. */
export const DETAIL_TARGETS: Readonly<Record<string, { selector: string } | { zone: LineZone }>> = {
ground: { selector: "g.b06-chart__center" },
planned: { selector: "g.b06-chart__center" },
fill: { selector: "g.b06-chart__center" },
cut: { selector: "g.b06-chart__center" },
cut_soil: { selector: "g.b06-chart__area--cut_soil" },
cut_rock: { selector: "g.b06-chart__area--cut_rock" },
embankment: { selector: "g.b06-chart__area--fill" },
spoil_fill: { selector: ".b06-chart__spoil-fill" },
ditch_soil: { zone: "ditch" },
ditch_rock: { zone: "ditch" },
benching: { zone: "fill" },
grading_fill: { zone: "fill" },
tree_removal_fill: { zone: "fill" },
fill_seeding: { zone: "fill" },
grading_cut: { zone: "cut_slope" },
tree_removal_cut: { zone: "cut_slope" },
cut_spraying: { zone: "cut_slope" },
road_compaction: { zone: "road" },
};
/** 판정 선 하나의 구역 — 측구 · 절토 사면 · 성토 · 도로. */
export function lineZone(design: CrossDesign, offsetM: number): LineZone {
const { zone } = crossZoneAt(design, offsetM);
if (zone !== "cut") return zone;
const ditch = crossZoneAt({ ...design, section_mode: "both_fill" }, offsetM).zone === "cut";
return ditch ? "ditch" : "cut_slope";
}
/** `key` 의 강조 대상 요소 — 그 카드에 없으면 빈 목록(164-2 `data-empty`). */
function targetsOf(svg: Element, design: CrossDesign | undefined, key: string): Element[] {
const target = DETAIL_TARGETS[key];
if (!target) return [];
if ("selector" in target) return [...svg.querySelectorAll(target.selector)];
const center = svg.querySelector("g.b06-chart__center line");
const ppm = Number(svg.getAttribute("data-px-per-m"));
if (!design || !center || !(ppm > 0)) return [];
const x0 = Number(center.getAttribute("x1"));
return [...svg.querySelectorAll(".b06-chart__design-hit:not(.b06-chart__rock-hit)")].filter(
(hit) => {
const xs = (hit.getAttribute("points") ?? "")
.trim()
.split(/\s+/)
.map((pair) => Number(pair.split(",")[0]));
const mid = (xs[0] + xs[xs.length - 1]) / 2;
// x = 왼쪽 여백 + (최대 offset − offset) × ppm → offset = (x(0) − x) ÷ ppm
return lineZone(design, (x0 - mid) / ppm) === target.zone;
},
);
}
/** 그림 복제 안 강조 — 묻어 온 `.is-active` 를 지우고 `key` 대상만 켬(null = 모두 끔). */
export function highlightDetail(svg: Element, design: CrossDesign | undefined, key: string | null) {
svg.querySelectorAll(".is-active").forEach((el) => el.classList.remove("is-active"));
if (key) targetsOf(svg, design, key).forEach((el) => el.classList.add("is-active"));
}
/** 그 카드에 강조 대상이 있는 키(164-2) — 없는 칸(암석 띠 없음 · 면적 0 · 사토장 없음)은 빠짐. */
export function presentKeys(svg: Element, design: CrossDesign | undefined): Set<string> {
return new Set(
Object.keys(DETAIL_TARGETS).filter((key) => targetsOf(svg, design, key).length > 0),
);
}
/* ── 표 강조 바깥 한 줄(259-3) ──────────────────────────────────────────── */
/** 직사각형들(좌 · 위 · 우 · 아래)의 합친 모양 바깥 줄 — 격자로 나눠 덮인 칸과 안 덮인 칸 사이 변만(SVG path). */
export function unionOutline(rects: number[][]): string {
const xs = [...new Set(rects.flatMap((r) => [r[0], r[2]]))].sort((a, b) => a - b);
const ys = [...new Set(rects.flatMap((r) => [r[1], r[3]]))].sort((a, b) => a - b);
const covered = (i: number, j: number): boolean =>
i >= 0 &&
j >= 0 &&
i + 1 < xs.length &&
j + 1 < ys.length &&
rects.some((r) => r[0] <= xs[i] && xs[i + 1] <= r[2] && r[1] <= ys[j] && ys[j + 1] <= r[3]);
const parts: string[] = [];
for (let i = 0; i + 1 < xs.length; i++)
for (let j = 0; j + 1 < ys.length; j++) {
if (!covered(i, j)) continue;
const [x0, x1, y0, y1] = [xs[i], xs[i + 1], ys[j], ys[j + 1]];
if (!covered(i, j - 1)) parts.push(`M${x0} ${y0}H${x1}`);
if (!covered(i, j + 1)) parts.push(`M${x0} ${y1}H${x1}`);
if (!covered(i - 1, j)) parts.push(`M${x0} ${y0}V${y1}`);
if (!covered(i + 1, j)) parts.push(`M${x1} ${y0}V${y1}`);
}
return parts.join("");
}
/** 고른 칸(`.is-picked`) 묶음에 바깥 한 줄 — 칸마다 테두리를 그리면 붙은 칸에 겹선이 생김. */
export function outlinePicked(box: HTMLElement): void {
box.querySelector(".b06-detail__pick-outline")?.remove();
const cells = [...box.querySelectorAll(".is-picked")];
if (!cells.length) return;
const base = box.getBoundingClientRect();
const rects = cells.map((cell) => {
const r = cell.getBoundingClientRect();
return [r.left - base.left, r.top - base.top, r.right - base.left, r.bottom - base.top].map(
(v) => Math.round(v),
);
});
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("class", "b06-detail__pick-outline");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", unionOutline(rects));
svg.append(path);
box.append(svg);
}