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 3346a19a..1fb386c0 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -80,6 +80,8 @@ const TABLE_HEIGHT_KEY = "b05-route-profile-table-height"; /** 정보 라인을 뺀 본문 세로를 그래프 40% : 테이블 60%로 나눈다(4:6, 6이 테이블). */ const CHART_HEIGHT_RATIO = 0.4; const MIN_CHART_HEIGHT = 100; +/** 접힌 유토곡선 손잡이(24px)가 앉을 테이블 아래 전용 띠 — 곡선 행 클릭을 가리지 않게. */ +const MASSHAUL_HANDLE_GUTTER_PX = 26; /** 패널을 끌어 줄일 수 있는 하한(px) — 이보다 낮으면 그래프도 테이블도 못 읽는다. */ const MIN_PANEL_HEIGHT = 180; /** 상한은 3D 뷰포트가 완전히 가려지지 않도록 부모 높이의 90%까지만 허용한다. */ @@ -483,7 +485,9 @@ export function createRouteProfilePanel( const chartHeight = alignment ? Math.max(MIN_CHART_HEIGHT, available - fixedTableHeight) : available; - const tableHeight = available - chartHeight; + // 테이블 아래에 접힌 유토곡선 손잡이(24px)의 전용 띠를 남긴다 — 띠 없이는 손잡이가 + // 곡선 L·R 행 위에 떠서 입력 클릭을 가로챈다(2026-08-04 사용자 보고: 곡선 수정 불가). + const tableHeight = available - chartHeight - MASSHAUL_HANDLE_GUTTER_PX; const table = alignment ? createProfileTable({ 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 d482bdd4..f08953bb 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -73,6 +73,8 @@ const CHAR_WIDTH_EM = 0.6; const CELL_PADDING_PX = 2; /** 가로 여유가 있을 때 목표로 삼는 글자 크기. 캔버스 최소 폭 산정의 기준이 된다. */ export const TABLE_TARGET_FONT_PX = 12; +/** 곡선 셀이 이웃과 겹칠 때 세로(회전) 표기로 줄이는 셀 폭(px). */ +const CURVE_ROTATED_WIDTH_PX = 16; /** 주어진 글자 크기로 가장 긴 값을 자르지 않고 담는 데 필요한 셀 폭. */ export function tableCellWidthFor(fontPx: number): number { @@ -303,6 +305,9 @@ function buildCurveRows( structureStationKeys: ReadonlySet, /** 배관 구조물 chainage 키 — R이 필수라 곡선 L·R은 **항상** 표기한다(2026-08-04 지시). */ pipeStationKeys: ReadonlySet, + /** 기본 글자 크기(px)와 행 높이(px) — 회전 셀의 글자 축소 산식에 쓴다. */ + fontPx: number, + rowHeightPx: number, ): HTMLElement[] { const { alignment, onCurveRadiusChange } = options; const lengthRow = createRow( @@ -319,42 +324,59 @@ function buildCurveRows( alignment.curves.map((curve) => [curve.chainage_m.toFixed(3), curve]), ); - // 곡선 값이 든 셀들의 오른쪽 끝 — 배관 곡선 셀이 이웃 곡선 구간과 근접해 겹치면 - // 배관 쪽만 오른쪽으로 비켜 세운다(선택 하이라이트 오버레이는 원위치·한 열 그대로). - let lastContentRight = Number.NEGATIVE_INFINITY; - alignment.stations.forEach((station, index) => { - const lengthCell = element("span", "b05-profile-table__curve"); - const radiusCell = element("span", "b05-profile-table__curve"); + // 1차: 곡선 값이 있는 열을 모은다. 이웃 곡선 셀과 가로로 겹치면(배관 측점이 규칙 측점 + // 옆에 붙는 경우) **자리를 옮기지 않고** 그 쌍을 세로(회전) 표기로 바꾼다 — 옆으로 + // 비키면 다른 열의 값으로 오해된다(2026-08-04 사용자 지시). 값은 측점 수직선 위에 + // 그대로 서고, 글자 크기는 행 높이에 맞춰 줄인다(구배 행 is-rotated와 같은 방식). + const curveEntries = alignment.stations.map((station, index) => { const key = station.chainage_m.toFixed(3); const isPipe = pipeStationKeys.has(key); // 구조물 측점 열은 곡선 입력도 기본 표기하지 않는다(빈 칸으로 격자만 유지). // 예외: 배관 측점은 R이 필수 입력이라 곡선 L·R을 항상 보인다. const curve = structureStationKeys.has(key) && !isPipe ? undefined : curveByChainage.get(key); - let center = centers[index]; - if (curve) { - const half = options.cellWidth / 2; - if (isPipe && center - half < lastContentRight + 2) { - center = lastContentRight + 2 + half; - } - lastContentRight = center + half; + return { center: centers[index], curve, rotated: false }; + }); + const withCurve = curveEntries.filter((entry) => entry.curve); + for (let pair = 1; pair < withCurve.length; pair += 1) { + if (withCurve[pair].center - withCurve[pair - 1].center < options.cellWidth) { + withCurve[pair - 1].rotated = true; + withCurve[pair].rotated = true; } + } + + curveEntries.forEach(({ center, curve, rotated }) => { + const lengthCell = element("span", "b05-profile-table__curve"); + const radiusCell = element("span", "b05-profile-table__curve"); if (curve) { // L·R은 L = R × |대수차| 로 연동된다. 둘 다 입력 가능하고, 어느 쪽을 고쳐도 R로 환산해 // 같은 파이프라인(onCurveRadiusChange)을 태운다. 재계산 후 마지막에 입력한 값이 반영된다. // L 입력 → R = L × (r_m / l_m) (현재 곡선의 L:R 비율 = 1/|대수차|, 단위 무관). - radiusCell.append( - curveInput( - curve, - curve.r_m.toFixed(1), - `종단곡선 반경 R (m)\n${curveTitle(curve)}`, - (r) => r, - ), + const radiusInput = curveInput( + curve, + curve.r_m.toFixed(1), + `종단곡선 반경 R (m)\n${curveTitle(curve)}`, + (r) => r, ); - lengthCell.append( - curveInput(curve, curve.l_m.toFixed(2), `종단곡선 길이 L (m)\n${curveTitle(curve)}`, (l) => - curve.l_m > 1e-9 ? (l * curve.r_m) / curve.l_m : null, - ), + const lengthInput = curveInput( + curve, + curve.l_m.toFixed(2), + `종단곡선 길이 L (m)\n${curveTitle(curve)}`, + (l) => (curve.l_m > 1e-9 ? (l * curve.r_m) / curve.l_m : null), ); + radiusCell.append(radiusInput); + lengthCell.append(lengthInput); + if (rotated) { + [lengthCell, radiusCell].forEach((cell) => { + cell.classList.add("is-rotated"); + cell.style.width = `${CURVE_ROTATED_WIDTH_PX}px`; + }); + [lengthInput, radiusInput].forEach((input) => { + const byLength = input.value.length + ? (rowHeightPx - 6) / (input.value.length * CHAR_WIDTH_EM) + : fontPx; + input.style.fontSize = `${Math.max(7, Math.min(fontPx, CURVE_ROTATED_WIDTH_PX - 4, byLength))}px`; + }); + } [lengthCell, radiusCell].forEach((cell) => { if (curve.skip_allowed) cell.classList.add("is-optional"); @@ -584,7 +606,9 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement { }); table.append(row); }); - table.append(...buildCurveRows(options, centers, structureStationKeys, pipeStationKeys)); + table.append( + ...buildCurveRows(options, centers, structureStationKeys, pipeStationKeys, fontSize, rowHeight), + ); // 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다. // 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index cdd5a6e7..744004df 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -570,6 +570,16 @@ overflow: hidden; } +/* 이웃 곡선 셀과 겹치는 좁은 자리(배관 측점 등) — 셀을 얇게 줄이고 입력을 세로쓰기로 + 세운다. 자리는 측점 수직선 그대로다(옆으로 비키면 다른 열 값으로 오해, 2026-08-04). */ +.b05-profile-table__curve.is-rotated input { + writing-mode: vertical-rl; + width: 100%; + height: 100%; + padding: 0; + text-align: center; +} + /* 측점 값 셀은 측점 수직선을 중심으로 좌우 대칭 배치된다. 셀 경계(= 이웃 측점과의 중간)에 세로 구분선을 둬 구배 행과 같은 격자를 만든다. */ .b05-profile-table__cell, @@ -918,9 +928,10 @@ display: none; } -/* 좌측 가장자리 세로 중앙 핸들 — 공용 side 핸들을 패널 왼쪽 밖으로 내보낸다. */ +/* 좌측 가장자리 세로 중앙 핸들 — 공용 side 핸들을 패널 왼쪽 밖으로 내보낸다. + 유토곡선 오버레이(z-index 5)가 그 위를 덮으면 안 보인다(2026-08-04 사용자 보고) — 위로. */ .b05-drainage .ui-workflow-overlay__handle--side { - z-index: 2; + z-index: 6; right: auto; left: calc(-1 * var(--spacing-24)); border-right: 0;