diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts index 2202d33d..d4fd64fb 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -50,6 +50,9 @@ import { } from "./B05_wf2_Route_UI_IrregularStations"; import type { SectionStation } from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch"; import "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css"; +// SVG 차트 색상(.b06-chart__*)의 정의처는 _Style_Cross.css다. 이걸 빼면 B05로 바로 진입했을 때 +// 배경 rect가 브라우저 기본 fill(검정)로 그려진다 — B06을 먼저 방문해야 정상으로 보이던 원인. +import "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css"; const COLLAPSED_KEY = "b05-route-profile-collapsed"; /** 정보 라인을 뺀 본문 세로를 그래프 40% : 테이블 60%로 나눈다(4:6, 6이 테이블). */ diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts index 8da2bf6d..adbdfad8 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -374,6 +374,7 @@ function buildSelectedColumn( interval: number, display: { station: number; cumulative: number }, onAdjustStation?: (chainageM: number, deltaM: number) => void, + isIrregular = false, ): HTMLElement { const plan = interpolateSample(alignment.samples, chainage, "elevation_m"); const ground = interpolateSample(alignment.samples, chainage, "ground_elevation_m"); @@ -419,7 +420,10 @@ function buildSelectedColumn( { value: { text: curve ? curve.l_m.toFixed(2) : "" }, modifier: "" }, // 곡선 L { value: { text: curve ? curve.r_m.toFixed(1) : "" }, modifier: "" }, // 곡선 R ]; - const column = element("div", "b05-profile-table__irregular-col"); + const column = element( + "div", + `b05-profile-table__irregular-col${isIrregular ? " is-floating" : ""}`, + ); column.style.left = `${centerX}px`; column.style.width = `${cellWidth}px`; rows.forEach((row) => { @@ -501,15 +505,20 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { ? alignment.stations.findIndex((row) => row.station_id === options.selectedStationId) : -1; if (selectedIrregular) { + const centerX = x(selectedIrregular.chainage_m); + // 비정규 측점은 측점 격자와 무관한 위치라 오버레이가 좌우 이웃 셀을 반씩 덮어 값이 잘려 + // 보인다. 겹치는 규칙 측점 셀을 숨겨 하이라이트 창이 깨끗한 자리에 뜨게 한다. + hideCoveredCells(table, centers, centerX, cellWidth); table.append( buildSelectedColumn( selectedIrregular.chainage_m, alignment, - x(selectedIrregular.chainage_m), + centerX, cellWidth, stationInterval, display, options.onAdjustStation, + true, ), ); } else if (selectedRegularIndex >= 0) { @@ -522,8 +531,33 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { stationInterval, display, options.onAdjustStation, + false, ), ); } return table; } + +/** + * 하이라이트 열이 덮는 자리의 규칙 측점 값 셀을 숨긴다. + * 셀과 하이라이트 열 모두 폭 `cellWidth`를 중심 정렬로 쓰므로, 중심 간 거리가 `cellWidth` + * 미만이면 두 사각형이 겹친다. 겹치는 측점은 어차피 값이 잘려 읽을 수 없으므로 통째로 숨긴다. + */ +function hideCoveredCells( + table: HTMLElement, + centers: readonly number[], + columnCenter: number, + cellWidth: number, +): void { + const covered = new Set(); + centers.forEach((center, index) => { + if (Math.abs(center - columnCenter) < cellWidth) covered.add(index); + }); + if (covered.size === 0) return; + table.querySelectorAll(".b05-profile-table__row").forEach((row) => { + const cells = row.querySelectorAll( + ".b05-profile-table__cell, .b05-profile-table__curve", + ); + covered.forEach((index) => cells[index]?.classList.add("is-covered")); + }); +} diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index b9cd8f16..f475fa62 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -16,6 +16,7 @@ } .b05-route__main { + position: relative; display: flex; flex-direction: column; width: 100%; @@ -35,22 +36,28 @@ } /* 하단 종단 패널: 그래프 + 12행 도면 테이블을 담기 위해 화면 높이의 60%를 차지한다. + 3D 뷰포트를 밀어내지 않고 그 위를 덮는 오버레이로 띄운다 — 패널을 여닫아도 3D 뷰 + 크기가 변하지 않아 카메라 시점과 렌더 비용이 그대로 유지된다. 접기 핸들로 언제든 내릴 수 있어 3D 뷰를 전체 영역으로 볼 수 있다. */ .b05-route-profile { - position: relative; + position: absolute; + z-index: 3; + right: 0; + bottom: 0; + left: 0; display: flex; - flex: 0 0 60vh; - flex: 0 0 60dvh; + height: 60vh; + height: 60dvh; flex-direction: column; min-height: 0; overflow: visible; border-top: 1px solid var(--color-border); background: var(--color-surface-raised); - transition: flex-basis var(--transition-fast); + transition: height var(--transition-fast); } .b05-route-profile.is-collapsed { - flex-basis: 0; + height: 0; min-height: 0; } @@ -663,6 +670,22 @@ pointer-events: none; } +/* 비정규(구조물) 측점은 측점 격자와 무관한 위치에 떠서, 이웃 셀을 가린 자리에 뜬다. + 떠 있는 창임이 드러나게 그림자·굵은 테두리로 구분한다(규칙 측점은 격자와 일치해 불필요). */ +.b05-profile-table__irregular-col.is-floating { + border-inline-width: 2px; + box-shadow: + 0 0 0 1px var(--color-surface-raised), + 0 4px 12px rgb(0 0 0 / 28%); +} + +/* 하이라이트 창이 덮은 자리의 규칙 측점 값 셀 — 값이 잘려 읽을 수 없으므로 숨긴다. + 자리(레이아웃)는 유지해야 세로 구분선 격자가 끊기지 않으므로 visibility로 감춘다. */ +.b05-profile-table__cell.is-covered, +.b05-profile-table__curve.is-covered { + visibility: hidden; +} + .b05-profile-table__irregular-col-cell { display: flex; flex: 1 1 0;