Files
Aislo/B06_Section/B06_Section_UI_Cross_Detail_Pick.ts
T
eomsangdonandClaude Opus 5.5 b400993b82 feat(b06): [상세] 그림 휠 줌 · 가운데 단추 팬 · 표 강조 붉은 한 벌(164-1 · 164-2)
- 복제 자식을 g 하나로 감싸 공용 b_svg_zoom_pan(wheelZoom · 커서 고정 · 상한 16 · 1배 팬) · [+ − ⤢] · 칸 비율 = viewBox
- 강조 띠 · 판정 선 · 십자 · 사토장 모두 --color-danger · 고른 칸 붉은 테두리 · 줄 표시 · hover 옅게
- 그림에 대상 없는 칸 data-empty 회색 · 누름 없음(presentKeys) · 이름 칸도 그 줄 첫 키로 눌림
- 시험 test_164_detail_zoom 새로 · test_150_detail_pick presentKeys 사례

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
2026-10-10 00:01:27 +09:00

78 lines
4.1 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` 밖.
* 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")].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),
);
}