fix(B05): 곡선 셀 회전 표기·손잡이 클릭 가로채기·배수유역 손잡이 z(31~33)

- 31: 곡선 L·R 셀이 이웃 곡선 셀과 겹치면 자리 이동 대신 측점선 위 세로쓰기
  (writing-mode vertical-rl) + 행 높이 맞춤 글자 축소 — 다른 열 값 오해 방지
- 32: 곡선 L·R 수정 불가 원인 = 접힌 유토곡선 손잡이가 테이블 곡선 행 위에서 클릭
  가로챔. 테이블 아래 26px 손잡이 전용 띠(MASSHAUL_HANDLE_GUTTER_PX) 확보.
  검증: 배관 곡선 R 8.0→55.0 편집 정상 반영
- 33: 배수유역 side 손잡이 z 2→6 — 유토곡선 오버레이(z5) 위로, hit-test 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 20:02:37 +09:00
co-authored by Claude Opus 5
parent 6bcd6d1209
commit f41b223079
3 changed files with 67 additions and 28 deletions
@@ -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({
+49 -25
View File
@@ -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<string>,
/** 배관 구조물 chainage 키 — R이 필수라 곡선 L·R은 **항상** 표기한다(2026-08-04 지시). */
pipeStationKeys: ReadonlySet<string>,
/** 기본 글자 크기(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)`를 중심으로 쓴다.
+13 -2
View File
@@ -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;