- 나눈 조각으로 면 찾기(열린 영역 제외) · 영역 안 누름 = 고름 · 채움 강조 · 면적 · 둘레 - 선 고름 경사비율 · 닫힌 선 둘레 · 띠 고름 값 글 - 표 칸 누름 = 저장된 조각 그래프 고름 · 띠 강조 + 값 글 - 고른 칸 묶음 바깥 한 줄(겹선 없음) · 줄 물들임 없앰 · 합친 묶음 이름 칸 키 맞춤 - 259-2 · 259-3 이 같은 파일(상세 표 배선)이라 한 커밋 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
124 lines
6.3 KiB
TypeScript
124 lines
6.3 KiB
TypeScript
/* =============================================================================
|
||
* 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);
|
||
}
|