260722_4
This commit is contained in:
@@ -9,6 +9,10 @@
|
||||
* - 측점 ▲ / ▼ : 그 측점을 변화점으로 승격시켜 계획고를 ±step 만큼 꺾는다.
|
||||
* - 구간 ⇧ / ⇩ : 직선 구간 전체를 평행이동한다(구배 유지, 양 끝 변화점 동시 이동).
|
||||
* - 원복 ↺ : 그 측점의 편집 델타만 지워 자동 선형으로 되돌린다.
|
||||
*
|
||||
* 측점 버튼과 구간 버튼은 **그래프 위·아래 같은 줄**에 놓는다. 안쪽 줄에 두면 X축 제목과
|
||||
* 측점 라벨에 가려 보이지 않기 때문이다. 자리가 겹칠 땐 측점 버튼을 수직선 위에 고정하고
|
||||
* 구간 버튼만 옆으로 비킨다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type {
|
||||
@@ -19,6 +23,9 @@ import type {
|
||||
import { chainageKey, emptyEdits, hasEdits } from "./B05_wf2_Route_UI_Profile_Alignment";
|
||||
|
||||
const DRAFT_KEY_PREFIX = "b05-profile-alignment-draft";
|
||||
/** 버튼 폭(18px)에 여유를 더한 값 — 이보다 가까우면 겹친 것으로 본다. */
|
||||
const BUTTON_CLEARANCE_PX = 20;
|
||||
const BUTTON_HALF_PX = 9;
|
||||
/** 길게 누르기: 이만큼 유지하면 반복이 시작되고, 그 뒤 초당 10회(0.1m씩)로 이어진다. */
|
||||
const HOLD_DELAY_MS = 500;
|
||||
const HOLD_INTERVAL_MS = 100;
|
||||
@@ -193,6 +200,25 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
|
||||
const repeater = createHoldRepeater();
|
||||
const edited = new Set(Object.keys(alignment.edits.station_offsets));
|
||||
const stationXs = alignment.stations.map((station) => x(station.chainage_m));
|
||||
|
||||
/**
|
||||
* 구간 버튼을 측점 버튼과 같은 행에 두되, 겹치는 자리면 옆으로 비킨다.
|
||||
* 측점 버튼은 측점 수직선 위에 있어야 의미가 통하므로 **측점 쪽을 고정**하고
|
||||
* 구간 버튼만 오른쪽으로 밀어낸다(구간이 짝수 개 측점을 걸치면 중점이 측점과 겹친다).
|
||||
*/
|
||||
function avoidStations(center: number): number {
|
||||
let nearest = center;
|
||||
let best = Infinity;
|
||||
for (const stationX of stationXs) {
|
||||
const distance = Math.abs(stationX - center);
|
||||
if (distance < best) {
|
||||
best = distance;
|
||||
nearest = stationX;
|
||||
}
|
||||
}
|
||||
return best < BUTTON_CLEARANCE_PX ? nearest + BUTTON_CLEARANCE_PX : center;
|
||||
}
|
||||
alignment.stations.forEach((station) => {
|
||||
const left = x(station.chainage_m);
|
||||
const isEdited = edited.has(chainageKey(station.chainage_m));
|
||||
@@ -201,7 +227,7 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
const up = overlayButton(repeater, "is-station is-up", "▲", `${label} — ${step}m 올림`, () =>
|
||||
onStation(station.chainage_m, step),
|
||||
);
|
||||
up.style.left = `${left - 9}px`;
|
||||
up.style.left = `${left - BUTTON_HALF_PX}px`;
|
||||
const down = overlayButton(
|
||||
repeater,
|
||||
"is-station is-down",
|
||||
@@ -209,7 +235,7 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
`${label} — ${step}m 내림`,
|
||||
() => onStation(station.chainage_m, -step),
|
||||
);
|
||||
down.style.left = `${left - 9}px`;
|
||||
down.style.left = `${left - BUTTON_HALF_PX}px`;
|
||||
layer.append(up, down);
|
||||
|
||||
if (!isEdited) return;
|
||||
@@ -221,7 +247,7 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
`${label} — 자동 선형으로 원복 (현재 ${offset >= 0 ? "+" : ""}${offset.toFixed(2)}m)`,
|
||||
() => onResetStation(station.chainage_m),
|
||||
);
|
||||
reset.style.left = `${left - 9}px`;
|
||||
reset.style.left = `${left - BUTTON_HALF_PX}px`;
|
||||
layer.append(reset);
|
||||
});
|
||||
|
||||
@@ -229,14 +255,14 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
const left = x(segment.from_m);
|
||||
const right = x(segment.to_m);
|
||||
if (right - left < 36) return;
|
||||
const center = (left + right) / 2;
|
||||
const center = avoidStations((left + right) / 2);
|
||||
const label =
|
||||
`구간 ${segment.from_m.toFixed(0)}~${segment.to_m.toFixed(0)}m ` +
|
||||
`(구배 ${segment.grade_percent.toFixed(2)}%) 전체 평행이동`;
|
||||
const up = overlayButton(repeater, "is-segment is-up", "⇧", `${label} — ${step}m 올림`, () =>
|
||||
onSegment(segment, step),
|
||||
);
|
||||
up.style.left = `${center - 9}px`;
|
||||
up.style.left = `${center - BUTTON_HALF_PX}px`;
|
||||
const down = overlayButton(
|
||||
repeater,
|
||||
"is-segment is-down",
|
||||
@@ -244,7 +270,7 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
`${label} — ${step}m 내림`,
|
||||
() => onSegment(segment, -step),
|
||||
);
|
||||
down.style.left = `${center - 9}px`;
|
||||
down.style.left = `${center - BUTTON_HALF_PX}px`;
|
||||
layer.append(up, down);
|
||||
});
|
||||
|
||||
|
||||
@@ -146,15 +146,19 @@ function stationCellWidth(data: LongitudinalSection, x: (chainage: number) => nu
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 값이 목표 글자 크기로 잘리지 않으려면 캔버스가 최소 이만큼 넓어야 한다.
|
||||
* 측점 한 칸의 고정 폭. 목표 글자 크기(12px)로 7자리 값이 잘리지 않는 크기다.
|
||||
* 화면 폭에 맞춰 늘였다 줄였다 하지 않고 이 값을 유지하고, 남는 가로는 스크롤로 훑는다.
|
||||
*/
|
||||
const STATION_COLUMN_PX = tableCellWidthFor(TABLE_TARGET_FONT_PX) + CELL_GAP_PX;
|
||||
|
||||
/**
|
||||
* 측점 칸 폭을 고정했을 때 필요한 캔버스 폭.
|
||||
*
|
||||
* 종단면도 렌더러의 최소 폭(`longitudinalMinimumWidth`)은 그래프 아래 **측점 라벨**만
|
||||
* 안 겹치면 되는 기준이라 열이 48px밖에 안 돼, 7자리 값(`3000.00`)을 담기엔 좁다.
|
||||
* 가로 스크롤로 훑는 화면이므로 폭을 늘려 글자 크기를 확보하는 편이 낫다.
|
||||
*/
|
||||
function tableMinimumWidth(stationCount: number): number {
|
||||
const column = tableCellWidthFor(TABLE_TARGET_FONT_PX) + CELL_GAP_PX;
|
||||
return LONG_PAD.left + LONG_PAD.right + Math.max(1, stationCount) * column;
|
||||
return LONG_PAD.left + LONG_PAD.right + Math.max(1, stationCount) * STATION_COLUMN_PX;
|
||||
}
|
||||
|
||||
export function createRouteProfilePanel(
|
||||
@@ -312,6 +316,8 @@ export function createRouteProfilePanel(
|
||||
width,
|
||||
height: tableHeight,
|
||||
cellWidth: stationCellWidth(longitudinal, x),
|
||||
// 이름표 열을 그래프 좌측 여백과 같은 폭으로 맞춰야 그래프 시작점이 가려지지 않는다.
|
||||
labelWidth: LONG_PAD.left,
|
||||
rowCount: TABLE_ROW_COUNT,
|
||||
x,
|
||||
onCurveRadiusChange: (curve, radius) =>
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface ProfileTableOptions {
|
||||
height: number;
|
||||
/** 이웃 측점과 겹치지 않는 셀 폭(px). */
|
||||
cellWidth: number;
|
||||
/** 행 이름표 열의 폭(px). 그래프의 좌측 여백과 같아야 X축이 맞물린다. */
|
||||
labelWidth: number;
|
||||
rowCount: number;
|
||||
/** 종단면도와 공유하는 chainage → x(px) 매핑. */
|
||||
x: (chainageM: number) => number;
|
||||
@@ -35,6 +37,7 @@ export interface ProfileTableOptions {
|
||||
|
||||
interface StationRowSpec {
|
||||
label: string;
|
||||
unit: string;
|
||||
cell: (index: number) => string;
|
||||
modifier?: string;
|
||||
}
|
||||
@@ -92,17 +95,23 @@ function placeCell(row: HTMLElement, centerX: number, node: HTMLElement): void {
|
||||
row.append(node);
|
||||
}
|
||||
|
||||
function createRow(className: string, label: string): HTMLElement {
|
||||
/**
|
||||
* 행 하나. 이름표는 sticky로 좌측에 고정되며 **그래프의 좌측 여백과 같은 폭**을 쓴다.
|
||||
* 단위는 이름표를 좁게 유지하려고 툴팁으로 뺐다(넓히면 그래프 시작점을 가린다).
|
||||
*/
|
||||
function createRow(className: string, label: string, unit: string): HTMLElement {
|
||||
const row = element("div", `b05-profile-table__row ${className}`);
|
||||
row.append(element("span", "b05-profile-table__label", label));
|
||||
const caption = element("span", "b05-profile-table__label", label);
|
||||
caption.title = `${label} (${unit})`;
|
||||
row.append(caption);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 도면의 구배 블록 3행: 구간 연장(m) / 고저차(m) / 기울기(%). 단위는 행 이름표가 대신한다. */
|
||||
const SEGMENT_ROWS: SegmentRowSpec[] = [
|
||||
{ label: "구배 L", unit: "m", cell: (segment) => segment.length_m.toFixed(2) },
|
||||
{ label: "구배 H", unit: "m", cell: (segment) => segment.height_m.toFixed(2) },
|
||||
{ label: "구배 S", unit: "%", cell: (segment) => segment.grade_percent.toFixed(2) },
|
||||
{ label: "구배 L", unit: "m 구간 연장", cell: (segment) => segment.length_m.toFixed(2) },
|
||||
{ label: "구배 H", unit: "m 구간 고저차", cell: (segment) => segment.height_m.toFixed(2) },
|
||||
{ label: "구배 S", unit: "% 구간 기울기", cell: (segment) => segment.grade_percent.toFixed(2) },
|
||||
];
|
||||
|
||||
function buildStationRows(alignment: ProfileAlignment, interval: number): StationRowSpec[] {
|
||||
@@ -110,51 +119,60 @@ function buildStationRows(alignment: ProfileAlignment, interval: number): Statio
|
||||
return [
|
||||
{
|
||||
label: "절토고",
|
||||
unit: "m",
|
||||
modifier: "cut",
|
||||
cell: (index) => (stations[index].cut_m > 0.005 ? stations[index].cut_m.toFixed(2) : ""),
|
||||
},
|
||||
{
|
||||
label: "성토고",
|
||||
unit: "m",
|
||||
modifier: "fill",
|
||||
cell: (index) => (stations[index].fill_m > 0.005 ? stations[index].fill_m.toFixed(2) : ""),
|
||||
},
|
||||
{
|
||||
label: "계획고",
|
||||
unit: "m",
|
||||
modifier: "plan",
|
||||
cell: (index) => stations[index].plan_elevation_m.toFixed(2),
|
||||
},
|
||||
{ label: "지반고", cell: (index) => stations[index].ground_elevation_m.toFixed(2) },
|
||||
{ label: "누가거리", cell: (index) => stations[index].chainage_m.toFixed(2) },
|
||||
{ label: "거리", cell: (index) => (index ? stations[index].distance_m.toFixed(2) : "") },
|
||||
{ label: "측점", cell: (index) => stationLabel(stations[index].chainage_m, interval) },
|
||||
{ label: "지반고", unit: "m", cell: (index) => stations[index].ground_elevation_m.toFixed(2) },
|
||||
{ label: "누가거리", unit: "m", cell: (index) => stations[index].chainage_m.toFixed(2) },
|
||||
{
|
||||
label: "거리",
|
||||
unit: "m 전 측점 대비",
|
||||
cell: (index) => (index ? stations[index].distance_m.toFixed(2) : ""),
|
||||
},
|
||||
{
|
||||
label: "측점",
|
||||
unit: "측점번호+잔여거리",
|
||||
cell: (index) => stationLabel(stations[index].chainage_m, interval),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 구배 3행. 각 블록은 **곡선 구간(BVC~EVC)을 뺀 실제 직선 부분**만 덮는다.
|
||||
* 변화점 자리는 곡선이 차지하므로 그 구간에 구배를 적어 두면 존재하지 않는 직선의
|
||||
* 값을 읽는 셈이 된다. 값(L/H/S) 자체는 도면 관례대로 변화점 사이 기준이다.
|
||||
* 구배 3행. 각 블록은 **변화점에서 변화점까지**를 덮는다.
|
||||
*
|
||||
* 구분선을 곡선의 접선점(BVC/EVC)에 두면 곡선 길이만큼 블록 사이에 틈이 생기고,
|
||||
* 양옆 블록의 테두리가 그 틈을 감싸 "빈 셀"처럼 보인다. 변화점은 곧 **생성된 R의
|
||||
* 중심**이자 측점 수직선이므로 그 자리를 구분선으로 삼으면 블록이 빈틈없이 이어지고,
|
||||
* 표기 값(L/H/S)이 변화점 사이 기준이라는 점과도 일치한다.
|
||||
*/
|
||||
function buildSegmentRows(
|
||||
alignment: ProfileAlignment,
|
||||
x: (chainage: number) => number,
|
||||
fontPx: number,
|
||||
): HTMLElement[] {
|
||||
const curveByPvi = new Map(
|
||||
alignment.curves.filter((curve) => !curve.omitted).map((curve) => [curve.pvi_index, curve]),
|
||||
);
|
||||
return SEGMENT_ROWS.map((spec) => {
|
||||
const row = createRow("b05-profile-table__row--grade", `${spec.label} (${spec.unit})`);
|
||||
const row = createRow("b05-profile-table__row--grade", spec.label, spec.unit);
|
||||
alignment.segments.forEach((segment) => {
|
||||
const startCurve = curveByPvi.get(segment.index);
|
||||
const endCurve = curveByPvi.get(segment.index + 1);
|
||||
const left = x(startCurve ? startCurve.evc_m : segment.from_m);
|
||||
const span = x(endCurve ? endCurve.bvc_m : segment.to_m) - left;
|
||||
const left = x(segment.from_m);
|
||||
const span = x(segment.to_m) - left;
|
||||
const text = spec.cell(segment);
|
||||
// 측점을 편집해 구간이 잘게 쪼개지면 글자가 안 들어가 테두리만 남은 빈 칸이 된다.
|
||||
// 그럴 땐 블록 자체를 만들지 않는다(값은 계획고·거리 행과 그래프에서 읽을 수 있다).
|
||||
if (!segmentTextFits(text, span, fontPx)) return;
|
||||
const node = element("span", "b05-profile-table__segment", text);
|
||||
const node = element("span", "b05-profile-table__segment", "");
|
||||
// 구간이 짧아 글자가 안 들어가면 숫자만 비운다. 블록을 지우면 그 자리가 다시
|
||||
// 틈으로 남아 빈 셀처럼 보이므로, 칸은 유지하고 값은 툴팁으로 읽게 한다.
|
||||
if (segmentTextFits(text, span, fontPx)) node.textContent = text;
|
||||
node.style.left = `${left}px`;
|
||||
node.style.width = `${span}px`;
|
||||
node.title =
|
||||
@@ -182,61 +200,80 @@ function curveTitle(curve: AlignmentCurve): string {
|
||||
);
|
||||
}
|
||||
|
||||
/** 곡선 2행: 반경 R(입력·1차 값) / 곡선길이 L(= R × |대수차| 파생). */
|
||||
/**
|
||||
* 곡선 2행: 반경 R(입력·1차 값) / 곡선길이 L(= R × |대수차| 파생).
|
||||
*
|
||||
* 곡선이 없는 측점에도 **빈 칸을 만든다**. 변화점에만 셀을 두면 세로 구분선이 띄엄띄엄
|
||||
* 끊겨 위쪽 측점값 행들과 격자가 맞지 않는다.
|
||||
*/
|
||||
function buildCurveRows(options: ProfileTableOptions): HTMLElement[] {
|
||||
const { alignment, x, onCurveRadiusChange } = options;
|
||||
const lengthRow = createRow("b05-profile-table__row--curve is-group-start", "곡선 L (m)");
|
||||
const radiusRow = createRow("b05-profile-table__row--curve", "곡선 R (m)");
|
||||
|
||||
alignment.curves.forEach((curve) => {
|
||||
const lengthCell = element("span", "b05-profile-table__curve", curve.l_m.toFixed(2));
|
||||
lengthCell.title = curveTitle(curve);
|
||||
const lengthRow = createRow(
|
||||
"b05-profile-table__row--curve is-group-start",
|
||||
"곡선 L",
|
||||
"m 종단곡선 길이 (R × |대수차|)",
|
||||
);
|
||||
const radiusRow = createRow("b05-profile-table__row--curve", "곡선 R", "m 종단곡선 반경 (입력)");
|
||||
const curveByChainage = new Map(
|
||||
alignment.curves.map((curve) => [curve.chainage_m.toFixed(3), curve]),
|
||||
);
|
||||
|
||||
alignment.stations.forEach((station) => {
|
||||
const lengthCell = element("span", "b05-profile-table__curve");
|
||||
const radiusCell = element("span", "b05-profile-table__curve");
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.step = "1";
|
||||
input.min = "1";
|
||||
input.className = "b05-profile-table__radius";
|
||||
input.value = curve.r_m.toFixed(1);
|
||||
input.title = `종단곡선 반경 R (m) — 이 값이 기준이고 곡선길이 L이 따라 계산됩니다.\n${curveTitle(curve)}`;
|
||||
input.addEventListener("change", () => {
|
||||
const parsed = Number.parseFloat(input.value);
|
||||
onCurveRadiusChange(curve, Number.isFinite(parsed) && parsed > 0 ? parsed : null);
|
||||
});
|
||||
radiusCell.append(input);
|
||||
const curve = curveByChainage.get(station.chainage_m.toFixed(3));
|
||||
if (curve) {
|
||||
lengthCell.textContent = curve.l_m.toFixed(2);
|
||||
lengthCell.title = curveTitle(curve);
|
||||
|
||||
[lengthCell, radiusCell].forEach((cell) => {
|
||||
if (curve.skip_allowed) cell.classList.add("is-optional");
|
||||
if (curve.omitted) cell.classList.add("is-omitted");
|
||||
});
|
||||
placeCell(lengthRow, x(curve.chainage_m), lengthCell);
|
||||
placeCell(radiusRow, x(curve.chainage_m), radiusCell);
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.step = "1";
|
||||
input.min = "1";
|
||||
input.className = "b05-profile-table__radius";
|
||||
input.value = curve.r_m.toFixed(1);
|
||||
input.title = `종단곡선 반경 R (m) — 이 값이 기준이고 곡선길이 L이 따라 계산됩니다.\n${curveTitle(curve)}`;
|
||||
input.addEventListener("change", () => {
|
||||
const parsed = Number.parseFloat(input.value);
|
||||
onCurveRadiusChange(curve, Number.isFinite(parsed) && parsed > 0 ? parsed : null);
|
||||
});
|
||||
radiusCell.append(input);
|
||||
|
||||
[lengthCell, radiusCell].forEach((cell) => {
|
||||
if (curve.skip_allowed) cell.classList.add("is-optional");
|
||||
if (curve.omitted) cell.classList.add("is-omitted");
|
||||
});
|
||||
}
|
||||
placeCell(lengthRow, x(station.chainage_m), lengthCell);
|
||||
placeCell(radiusRow, x(station.chainage_m), radiusCell);
|
||||
});
|
||||
return [lengthRow, radiusRow];
|
||||
}
|
||||
|
||||
export function createProfileTable(options: ProfileTableOptions): HTMLElement {
|
||||
const { alignment, stationInterval, width, height, cellWidth, rowCount, x } = options;
|
||||
const { alignment, stationInterval, width, height, cellWidth, labelWidth, rowCount, x } = options;
|
||||
const table = element("div", "b05-profile-table");
|
||||
const fontSize = fitFontSize(height / Math.max(rowCount, 1), cellWidth);
|
||||
// 이름표는 "누가거리"(한글 4자)가 들어가야 하므로 값 글자보다 크지 않게 제한한다.
|
||||
const labelFontSize = Math.min(fontSize, Math.floor((labelWidth - 10) / 4));
|
||||
table.style.width = `${width}px`;
|
||||
table.style.height = `${height}px`;
|
||||
table.style.setProperty("--b05-table-font", `${fontSize}px`);
|
||||
table.style.setProperty("--b05-table-cell-width", `${cellWidth}px`);
|
||||
// 행 이름표는 가장 긴 "구배 L (m)"이 잘리지 않을 만큼만 차지한다.
|
||||
table.style.setProperty("--b05-table-label-width", `${Math.round(fontSize * 5.2 + 14)}px`);
|
||||
table.style.setProperty("--b05-table-label-width", `${labelWidth}px`);
|
||||
table.style.setProperty("--b05-table-label-font", `${Math.max(8, labelFontSize)}px`);
|
||||
|
||||
table.append(...buildSegmentRows(alignment, x, fontSize));
|
||||
buildStationRows(alignment, stationInterval).forEach((spec, index) => {
|
||||
const row = createRow(
|
||||
`${spec.modifier ? `is-${spec.modifier}` : ""}${index === 0 ? " is-group-start" : ""}`,
|
||||
spec.label,
|
||||
spec.unit,
|
||||
);
|
||||
// 값이 없는 측점(절토고/성토고 중 한쪽)도 빈 칸을 만들어야 세로 구분선이 끊기지 않는다.
|
||||
alignment.stations.forEach((station, stationIndex) => {
|
||||
const value = spec.cell(stationIndex);
|
||||
if (!value) return;
|
||||
placeCell(row, x(station.chainage_m), element("span", "b05-profile-table__cell", value));
|
||||
const cell = element("span", "b05-profile-table__cell", spec.cell(stationIndex));
|
||||
placeCell(row, x(station.chainage_m), cell);
|
||||
});
|
||||
table.append(row);
|
||||
});
|
||||
|
||||
@@ -119,6 +119,13 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 축 제목과 측점 라벨은 아래 도면 테이블(측점 행)이 그대로 담고 있어 중복이다.
|
||||
지우면 그래프 상·하단이 비어 편집 버튼이 가려지지 않고, 좌측 여백도 이름표 열로 쓸 수 있다. */
|
||||
.b05-route-profile .b06-chart__axis-label,
|
||||
.b05-route-profile .b06-chart__station-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b05-route__viewport canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -398,9 +405,10 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* 가로 스크롤 시 값들이 이름표 열을 뚫고 보이지 않도록 가장 위 층에 불투명하게 둔다. */
|
||||
.b05-profile-table__label {
|
||||
position: sticky;
|
||||
z-index: 3;
|
||||
z-index: 5;
|
||||
left: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -411,7 +419,9 @@
|
||||
border-right: 2px solid var(--color-text-muted, var(--color-plum-velvet));
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
font-size: var(--b05-table-label-font, inherit);
|
||||
font-weight: var(--font-weight-medium);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b05-profile-table__cell,
|
||||
@@ -426,13 +436,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 측점 값 셀은 측점 수직선을 중심으로 좌우 대칭 배치된다. */
|
||||
/* 측점 값 셀은 측점 수직선을 중심으로 좌우 대칭 배치된다.
|
||||
셀 경계(= 이웃 측점과의 중간)에 세로 구분선을 둬 구배 행과 같은 격자를 만든다. */
|
||||
.b05-profile-table__cell,
|
||||
.b05-profile-table__curve {
|
||||
box-sizing: border-box;
|
||||
width: var(--b05-table-cell-width, 56px);
|
||||
border-left: 1px solid var(--color-border);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.b05-profile-table__cell:last-child,
|
||||
.b05-profile-table__curve:last-child {
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b05-profile-table__row.is-cut .b05-profile-table__cell {
|
||||
color: rgb(220 38 38);
|
||||
}
|
||||
@@ -446,10 +464,15 @@
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* 구간 블록: 변화점 사이 직선 전체를 덮어 어느 구간의 값인지 한눈에 보이게 한다. */
|
||||
/* 구간 블록: 변화점 사이를 빈틈없이 채운다.
|
||||
구분선은 변화점(= 생성된 R의 중심, 측점 수직선) 위에 놓이므로 왼쪽 테두리 하나면
|
||||
충분하다. 양쪽에 다 주면 맞닿는 자리가 2px로 두꺼워진다. */
|
||||
.b05-profile-table__segment {
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b05-profile-table__segment:last-child {
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@@ -460,7 +483,6 @@
|
||||
|
||||
.b05-profile-table__curve {
|
||||
z-index: 2;
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b05-profile-table__curve.is-optional {
|
||||
@@ -532,17 +554,16 @@
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
.b05-profile-edit__btn.is-segment.is-up {
|
||||
top: 19px;
|
||||
}
|
||||
|
||||
.b05-profile-edit__btn.is-segment.is-down {
|
||||
bottom: 19px;
|
||||
/* 구간 시프트 버튼은 측점 버튼과 같은 줄(is-up/is-down)에 놓이고, 겹치는 자리에서만
|
||||
렌더러가 좌우로 비켜 배치한다. 구분을 위해 색만 달리한다. */
|
||||
.b05-route-profile:hover .b05-profile-edit__btn.is-segment {
|
||||
border-color: color-mix(in srgb, var(--color-royal-amethyst, rgb(109 40 217)) 40%, transparent);
|
||||
color: var(--color-royal-amethyst, rgb(109 40 217));
|
||||
}
|
||||
|
||||
.b05-profile-edit__btn.is-reset,
|
||||
.b05-route-profile:hover .b05-profile-edit__btn.is-reset {
|
||||
top: 36px;
|
||||
top: 20px;
|
||||
border-color: color-mix(in srgb, var(--color-royal-amethyst, rgb(109 40 217)) 45%, transparent);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-royal-amethyst, rgb(109 40 217));
|
||||
|
||||
Reference in New Issue
Block a user