diff --git a/B06_Section/B06_Section_UI_Cross_Axes.ts b/B06_Section/B06_Section_UI_Cross_Axes.ts index f7e5d72b2..72b8c20cf 100644 --- a/B06_Section/B06_Section_UI_Cross_Axes.ts +++ b/B06_Section/B06_Section_UI_Cross_Axes.ts @@ -26,6 +26,8 @@ export interface CrossAxesOptions { /** 표고 과장 기준점과 배율 — 화면 표고 → 실제 표고 환산에 쓴다. */ elevationMid: number; exaggeration: number; + /** 중심고(실제 표고) — Y축 라벨 자리에 둥근 상자로(318-2 · 소수 첫째 자리). 없으면 안 그림. */ + centerZ?: number | null; } /** X 눈금 라벨("-12" 등)이 겹치지 않는 최소 간격(px). */ @@ -36,6 +38,11 @@ const X_TARGET_TICKS = 7; /** 표고(Y) 눈금 최소 간격(m) — 1m 아래 단위는 표기하지 않는다. */ const MIN_ELEVATION_STEP_M = 1; const Y_TARGET_TICKS = 10; +/** Y 눈금 글 오른끝 ↔ Y 축선(px). */ +const Y_LABEL_GAP_PX = 7; +/** 중심고 상자 — 글자 좌우 여백 · 높이(px). 글자 크기는 눈금과 같음(10px). */ +const CENTER_PAD_X = 3; +const CENTER_BOX_H = 14; /** * 축 레이어를 그리는 함수를 만든다. 줌·팬이 바뀔 때마다 그 상태로 다시 부르면 된다. @@ -108,9 +115,16 @@ export function createCrossAxes( MIN_ELEVATION_STEP_M, ); const yDecimals = Math.max(0, Math.ceil(-Math.log10(yStep) - 1e-9)); + const screenYOf = (raw: number) => + ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration); + // 중심고 상자 자리 — 보이는 범위 안일 때만 · 그 높이 눈금 글자는 상자에 양보 + const centerZ = options.centerZ; + const centerY = + typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop + ? screenYOf(centerZ) + : null; for (let raw = Math.ceil(rawBottom / yStep) * yStep; raw <= rawTop + 1e-9; raw += yStep) { - const display = options.elevationMid + (raw - options.elevationMid) * options.exaggeration; - const screenY = ty + scale * y(display); + const screenY = screenYOf(raw); layer.append( svgElement("line", { x1: plotLeft, @@ -119,13 +133,41 @@ export function createCrossAxes( y2: screenY, class: "b06-chart__grid", }), + ); + if (centerY !== null && Math.abs(screenY - centerY) < CENTER_BOX_H) continue; + layer.append( svgText(raw.toFixed(yDecimals), { - x: plotLeft - 7, + x: plotLeft - Y_LABEL_GAP_PX, y: screenY + 3, "text-anchor": "end", class: "b06-chart__tick", }), ); } + if (centerY !== null) appendCenterTag(layer, (centerZ as number).toFixed(1), plotLeft, centerY); }; } + +/** 중심고 둥근 상자 — 글 오른끝 = 다른 눈금 자리 · 상자는 Y 축선(`axisX`) 4px 앞에서 멈춤 · + * 글 폭은 잴 수 있으면 재고(붙은 뒤) 아니면 어림. */ +function appendCenterTag(layer: SVGGElement, text: string, axisX: number, centerY: number): void { + const textRight = axisX - Y_LABEL_GAP_PX; + const box = svgElement("rect", { + y: centerY - CENTER_BOX_H / 2, + height: CENTER_BOX_H, + rx: 3, + ry: 3, + class: "b06-chart__center-box", + }); + const label = svgText(text, { + x: textRight, + y: centerY + 3.5, + "text-anchor": "end", + class: "b06-chart__tick b06-chart__center-text", + }); + layer.append(box, label); + const measured = (label as SVGTextElement).getComputedTextLength?.() ?? 0; + const width = (measured > 0 ? measured : text.length * 5.8) + CENTER_PAD_X * 2; + box.setAttribute("x", String(textRight + CENTER_PAD_X - width)); + box.setAttribute("width", String(width)); +} diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts index 063233d09..e3e67923c 100644 --- a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -172,11 +172,9 @@ export function appendCardFooter( rockBoundary?: RockBoundaryControl, cutSlope?: CutSlopeControl, ): void { + // 중심고는 그래프 Y축 라벨 상자로 옮김(318-2 · `_Cross_Axes`) — 하단 띠는 암 제어만 · 비어도 띠 높이 그대로 const footer = document.createElement("footer"); - const center = document.createElement("span"); - center.textContent = `${L("B06_Profile_View_CenterElevation")} ${section.center_z?.toFixed(2) ?? "-"}m`; - footer.append(center); - // 암 경계선 제어는 하단 정보 행 가운데(2026-08-02) — 암 지반만. + // 암 경계선 제어는 하단 정보 행 왼쪽(318-2) — 암 지반만. if (rockBoundary && section.design?.geometry_preset === "rock") { const rockControl = buildRockBoundaryControl(section, rockBoundary); rockControl.classList.add("b06-cross-card__rockb"); diff --git a/B06_Section/B06_Section_UI_Cross_Design.ts b/B06_Section/B06_Section_UI_Cross_Design.ts index 88b037ea8..2fc78b270 100644 --- a/B06_Section/B06_Section_UI_Cross_Design.ts +++ b/B06_Section/B06_Section_UI_Cross_Design.ts @@ -346,8 +346,12 @@ export function buildDesignControls( }; const bar = document.createElement("div"); bar.className = "b06-design"; - // 컨트롤 상호작용이 카드 선택 클릭으로 전파되지 않게 한다. - bar.addEventListener("click", (event) => event.stopPropagation()); + // 조작(단추 · 입력 · ⋯ 펼침)만 카드 선택 클릭으로 안 넘김 — 빈자리 누름은 카드를 고름(318-1). + bar.addEventListener("click", (event) => { + if ((event.target as Element).closest("button, input, select, label, details")) { + event.stopPropagation(); + } + }); // 경사 방향 선택이 필요한 경우 = 양절(both_cut)·양성(both_fill). 편절은 절토측 자동. // 양성은 측구가 없어도 노면 기울기 방향(생략된 측구 방향)을 여기서 지정한다(N-2-3). diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index dd7f71188..b78abd6d5 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -387,6 +387,7 @@ export function createCrossSectionCard( displayMax, elevationMid, exaggeration, + centerZ: section.center_z ?? null, }); renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW); diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index e0570dda1..ebe180f12 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -599,6 +599,7 @@ export function createSectionView( displayMax: plot.displayMax, elevationMid: plot.elevationMid, exaggeration: Math.max(currentExaggeration, 0.1), + centerZ: section.center_z ?? null, }; // 칸 맞춤이 넓힌 가로 범위를 처음 배율로 되좁힘 = 카드 반폭(266-1) const homeScale = base && fitted ? base.pixelsPerMeter / fitted.pixelsPerMeter : 1; diff --git a/B06_Section/B06_Section_UI_Style_Cross.css b/B06_Section/B06_Section_UI_Style_Cross.css index e0288d254..2b23f1c65 100644 --- a/B06_Section/B06_Section_UI_Style_Cross.css +++ b/B06_Section/B06_Section_UI_Style_Cross.css @@ -121,6 +121,8 @@ display: grid; grid-template-columns: repeat(auto-fill, minmax(min(480px, 100%), 1fr)); gap: var(--spacing-16); + /* 카드 = 내용 높이만큼 — 같은 줄 큰 카드에 맞춰 늘어나 아래 띠 밑이 비던 것(318-3 · 띠 높이 그대로) */ + align-items: start; } /* 알림 줄(138-3) — 카드 칸 맨 위 고정 · 알림 없어도 줄 유지 · 오른쪽 끝 열 수 칸. @@ -344,6 +346,11 @@ html.is-bm-route .b06-section__notice-bar { border-top: 1px solid var(--color-border); } +/* 빈 띠(토사 측점 · 중심고는 Y축으로 옮김 318-2)도 글 한 줄 높이 그대로 */ +.b06-cross-card > footer:empty::before { + content: "\200b"; +} + .b06-section__empty { display: flex; min-height: 180px; @@ -388,6 +395,18 @@ html.is-bm-route .b06-section__notice-bar { font-size: 10px; } +/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 */ +.b06-chart__center-box { + fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised)); + stroke: var(--color-accent); + stroke-width: 1; +} + +.b06-chart__center-text { + fill: var(--color-accent); + font-weight: var(--font-weight-medium); +} + /* 구조물(비정규) 측점 이름 — 종단면도 측점선 상단 표기. */ .b06-chart__structure-label { font-size: 9px; @@ -607,24 +626,14 @@ html.is-bm-route .b06-section__notice-bar { font-family: var(--font-mono); } -/* 암 경계선 제어(E-7) — 하단 정보 행 **가운데 맞춤**(2026-08-06 사용자 지시). - 방위각 삭제로 space-between 3분할 배치가 깨져 좌측에 붙었다 — 절대 배치로 - 행 중앙에 고정한다(중심고·반폭 버튼과 겹치지 않는 폭). */ -.b06-cross-card > footer { - position: relative; -} - +/* 암 경계선 제어(E-7) — 하단 정보 행 **왼쪽 맞춤**(318-2 · 중심고는 그래프 Y축 라벨 상자로 옮김). */ .b06-cross-card__rockb { - position: absolute; - left: 50%; - transform: translateX(-50%); flex: 0 0 auto; border-radius: var(--radius-inputs); padding: 1px 4px; } -/* 측점별 암 절토 경사(2026-09-07) — 암 경계선이 행 가운데를 절대 배치로 쓰므로 - 이 칸은 행의 **오른쪽 끝**에 둔다. 중심고(왼쪽)와 겹치지 않는다. */ +/* 측점별 암 절토 경사(2026-09-07) — 행의 **오른쪽 끝**(암 경계 = 왼쪽). */ .b06-cross-card__cutslope { margin-left: auto; flex: 0 0 auto;