Files
Aislo/B06_Section/B06_Section_UI_Cross_Detail_Pick.ts
T
eomsangdonandClaude Opus 5.5 6dd6ced454 feat(b06): 상세 닫힌 영역 고름 · 면적 · 둘레 · 표 칸 → 그래프 · 표 강조 바깥 한 줄 (259-2 · 259-3)
- 나눈 조각으로 면 찾기(열린 영역 제외) · 영역 안 누름 = 고름 · 채움 강조 · 면적 · 둘레
- 선 고름 경사비율 · 닫힌 선 둘레 · 띠 고름 값 글
- 표 칸 누름 = 저장된 조각 그래프 고름 · 띠 강조 + 값 글
- 고른 칸 묶음 바깥 한 줄(겹선 없음) · 줄 물들임 없앰 · 합친 묶음 이름 칸 키 맞춤
- 259-2 · 259-3 이 같은 파일(상세 표 배선)이라 한 커밋

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
2026-10-11 14:10:35 +09:00

124 lines
6.3 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")].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);
}