feat(B05): 종단도 줌·Y 표시 표고창 조작구
- B05_Profile_UI_Profile_Zoom.ts 신규 — 배율 상태(가로 폭 배수·세로 배율·창 중심)와 버튼 7개(+ − ⇕+ ⇕− ▲ ▼ ⤢). 자리는 요약줄 오른쪽 끝(그래프 우측 상단), 설명은 툴팁, 클릭은 stopPropagation 으로 측점 선택에 번지지 않게 함 - X 줌은 폭 배수 — computeProfileLayout 에 zoomX 를 넘겨 측점 간격·폭맞춤 폭에 함께 곱함. 그래프·테이블·편집 버튼층·구조물 레인이 같은 매핑을 쓰므로 넷의 x 정렬이 유지됨 - Y 는 표시 표고창(제안 A) — createLongitudinalProfile 에 elevationOffsetRatio 인자 추가, 창 높이 = 전범위 ÷ 배율, ▲▼ 가 창 높이의 10%씩 중심 이동. 세로 스크롤 없음 - 지반선·계획선·절성토 음영을 플롯 사각형 clipPath 안으로 옮김 — 배율을 올려도 선이 축·측점 라벨 위로 흘러나오지 않음 (B06 은 배율 1·오프셋 0 이라 표시 불변) - 요약줄 렌더러에 trailing 자리 추가, .b05-profile__zoom* CSS 추가 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,8 @@ export interface BalanceBarParams {
|
||||
tools?: HTMLElement;
|
||||
/** 횡단배수 최소 계획고 위반(2026-08-23) — 배수관·BOX암거 토피 미확보 경고. */
|
||||
minCoverViolations?: MinCoverViolation[];
|
||||
/** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */
|
||||
trailing?: HTMLElement;
|
||||
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
|
||||
onResetAll: () => void;
|
||||
}
|
||||
@@ -50,6 +52,7 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
||||
"⚠ 계획선 데이터가 구버전 형식입니다 — [최적 경로 계산]을 다시 실행하세요.";
|
||||
params.balanceBar.append(note);
|
||||
}
|
||||
if (params.trailing) params.balanceBar.append(params.trailing);
|
||||
return;
|
||||
}
|
||||
const { alignment } = params;
|
||||
@@ -106,4 +109,6 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
||||
badge.textContent = "미저장 (확정 시 반영)";
|
||||
params.balanceBar.append(badge);
|
||||
}
|
||||
// 줌 조작구는 늘 오른쪽 끝 — 그래프 우측 상단 자리다(2026-09-04 사용자 지시).
|
||||
if (params.trailing) params.balanceBar.append(params.trailing);
|
||||
}
|
||||
|
||||
@@ -113,14 +113,18 @@ export function computeProfileLayout(
|
||||
data: LongitudinalSection,
|
||||
stationIntervalM: number,
|
||||
availableWidth: number,
|
||||
/** 가로 줌 배수(1 = 현행). 측점 간격 기본값과 폭맞춤 폭에 함께 곱해 넷(그래프·테이블·
|
||||
* 편집 버튼층·구조물 레인)이 같은 비율로 늘어나게 한다(2026-09-04 사용자 지시). */
|
||||
zoomX = 1,
|
||||
): ProfileLayout {
|
||||
const maxChainage = maxChainageOf(data);
|
||||
const interval = Math.max(stationIntervalM, 1e-6);
|
||||
const framePad = LONG_PAD.left + LONG_PAD.right;
|
||||
const minSpacing = STATION_SPACING_PX * PROFILE_SPACING_MULTIPLIER;
|
||||
const zoom = Math.max(zoomX, 1e-6);
|
||||
const minSpacing = STATION_SPACING_PX * PROFILE_SPACING_MULTIPLIER * zoom;
|
||||
// 기본 배수로 펼친 최소 폭 (좌우 반 칸 = 한 칸 여백 포함).
|
||||
const minWidth = framePad + ((maxChainage + interval) / interval) * minSpacing;
|
||||
const width = Math.max(minWidth, availableWidth);
|
||||
const width = Math.max(minWidth, availableWidth * zoom);
|
||||
const pxPerMeter = (width - framePad) / (maxChainage + interval);
|
||||
const spacing = interval * pxPerMeter;
|
||||
return { width, originOffset: spacing / 2, cellWidth: Math.max(1, spacing - CELL_GAP_PX) };
|
||||
|
||||
@@ -56,6 +56,7 @@ import { renderProfile } from "./B05_Profile_UI_Profile_Render";
|
||||
import { irregularStationId, type IrregularStation } from "./B05_Profile_UI_IrregularStations";
|
||||
import { createCrossPreview } from "./B05_Profile_UI_Profile_Preview";
|
||||
import { createPanelTools } from "./B05_Profile_UI_Profile_Panel_Tools";
|
||||
import { createProfileZoom } from "./B05_Profile_UI_Profile_Zoom";
|
||||
import {
|
||||
stationIdAtStructure as stationIdAtStructureOf,
|
||||
structureIdAtStation as structureIdAtStationOf,
|
||||
@@ -359,6 +360,7 @@ export function createRouteProfilePanel(
|
||||
minCoverViolations,
|
||||
balanceBar,
|
||||
tools: tools.render(),
|
||||
trailing: profileZoom.bar,
|
||||
alignment,
|
||||
legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal),
|
||||
edited: store.edited(),
|
||||
@@ -371,6 +373,9 @@ export function createRouteProfilePanel(
|
||||
});
|
||||
}
|
||||
|
||||
/* 줌·Y레인지 조작구 — 상태를 페이지가 들고 있어 편집·재계산으로 다시 그려도 유지된다. */
|
||||
const profileZoom = createProfileZoom(() => draw());
|
||||
|
||||
/* [직선화]·[쉬프트]·되돌리기·방향키 배선은 `_Panel_Tools` 로 뺐다(700줄 한계). */
|
||||
const { tools, history, handleToolPick } = createPanelTools({
|
||||
root,
|
||||
@@ -506,6 +511,7 @@ export function createRouteProfilePanel(
|
||||
selectStation,
|
||||
applyEdits,
|
||||
handleToolPick,
|
||||
zoom: profileZoom.state,
|
||||
toolActive: () => tools.mode() !== "none",
|
||||
selectedRuns: () => tools.selectedRuns(),
|
||||
stationIdAtStructure,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { createRunHighlight } from "./B05_Profile_UI_Profile_RunHighlight";
|
||||
import type { StraightRun } from "./B05_Profile_UI_Profile_Straighten";
|
||||
import type { MinCoverPoint } from "./B05_Profile_UI_Profile_MinCover";
|
||||
import { buildStickyYAxis, type RouteMassHaulDrawParams } from "./B05_Profile_UI_Profile_MassHaul";
|
||||
import type { ProfileZoomState } from "./B05_Profile_UI_Profile_Zoom";
|
||||
import { createProfileTable } from "./B05_Profile_UI_Profile_Table";
|
||||
import {
|
||||
CELL_GAP_PX,
|
||||
@@ -83,6 +84,8 @@ export interface ProfileRenderContext {
|
||||
applyEdits: (next: AlignmentEdits) => void;
|
||||
/** [직선화]·[쉬프트] 도구가 그래프 클릭을 먼저 먹는지(먹었으면 기본 선택을 건너뛴다). */
|
||||
handleToolPick: (chainageM: number | null) => boolean;
|
||||
/** 가로 폭 배수·세로 표시 표고창(줌 조작구 상태) — 2026-09-04. */
|
||||
zoom: () => ProfileZoomState;
|
||||
/** 그래프 x → chainage 역변환이 필요한 도구 판정용 — 클릭 지점의 누가거리. */
|
||||
toolActive: () => boolean;
|
||||
/** [쉬프트]로 고른 직선 구간 — 그래프에 빨갛게 강조한다(2026-09-03). */
|
||||
@@ -114,15 +117,20 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
ctx.renderBalance();
|
||||
|
||||
const longitudinal = detail.longitudinal;
|
||||
const zoom = ctx.zoom();
|
||||
// 가로 줌은 **폭 배수**다 — 캔버스가 넓어지고 가로 스크롤로 훑는다. 그래프·테이블·
|
||||
// 편집 버튼층·구조물 레인이 같은 매핑을 쓰므로 폭 하나만 키우면 넷이 함께 늘어난다.
|
||||
const availableWidth = Math.max(1, body.clientWidth - 15);
|
||||
// 계획선(편집 가능) 상태에서는 측점 간격 기본값(기준×1.5)으로 펼치되 화면이 넓으면
|
||||
// 폭맞춤으로 늘린다. 그 외(구버전·플레인 뷰)는 예전처럼 화면 폭에 맞춰 펼친다.
|
||||
const stationIntervalM = stationInterval ?? alignment?.policy.station_interval_m;
|
||||
const layout =
|
||||
alignment && stationIntervalM
|
||||
? computeProfileLayout(longitudinal, stationIntervalM, availableWidth)
|
||||
? computeProfileLayout(longitudinal, stationIntervalM, availableWidth, zoom.x)
|
||||
: {
|
||||
width: Math.max(availableWidth, longitudinalMinimumWidth(longitudinal, stationInterval)),
|
||||
width:
|
||||
Math.max(availableWidth, longitudinalMinimumWidth(longitudinal, stationInterval)) *
|
||||
zoom.x,
|
||||
originOffset: 0,
|
||||
cellWidth: STATION_SPACING_PX - CELL_GAP_PX,
|
||||
};
|
||||
@@ -219,7 +227,8 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
createLongitudinalProfile(
|
||||
graphLongitudinal,
|
||||
selectedStationId,
|
||||
1,
|
||||
// 세로 배율 = 표시 표고창 높이의 역수(1 = 표고 전범위).
|
||||
zoom.y,
|
||||
undefined,
|
||||
ctx.selectStation,
|
||||
stationInterval,
|
||||
@@ -243,6 +252,8 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
// 올린다. 19px는 라벨-버튼 사이가 너무 벌어져 70% 수준(15px)으로 줄였다
|
||||
// (2026-08-04 사용자 지시). B06은 편집 버튼이 없어 0 유지.
|
||||
15,
|
||||
// 표시 표고창의 중심 이동(창 높이 대비 비율) — ▲▼ 버튼이 옮긴다.
|
||||
zoom.offsetRatio,
|
||||
),
|
||||
);
|
||||
// 구조물 우클릭 메뉴 — 측점선 위면 삭제, 빈 자리면 배관 추가. 이동도 여기(측점선 끌기)서 한다.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Profile_Zoom.ts
|
||||
* 종단면도 줌·Y레인지 조작구 (2026-09-04 사용자 지시).
|
||||
*
|
||||
* 공사 범위가 넓고 고저차가 크면 종단 그래프가 눌려 읽히지 않는다. 조작은 두 갈래다.
|
||||
*
|
||||
* X (가로) — **폭 배수**다. SVG transform 으로 늘리면 그래프만 커지고 측점 테이블·
|
||||
* 계획고 편집 버튼층·구조물 알약 레인이 어긋난다(넷이 같은 `chainageMapper`
|
||||
* 를 쓴다). 캔버스 폭 자체를 키우고 가로 스크롤로 훑는다.
|
||||
* Y (세로) — **표시 표고창**이다(제안 A, 2026-09-04 사용자 확정). 창 높이 = 전범위 ÷ 배율,
|
||||
* 창 중심은 창 높이 대비 비율로 위·아래로 옮긴다. 세로 스크롤이 생기지 않아
|
||||
* X축·측점 라벨·편집 버튼이 항상 바닥에 남는다.
|
||||
*
|
||||
* 배율은 페이지가 들고 있다 — 편집·재계산으로 다시 그려도 유지된다(B06 `cardZoomStates` 규칙).
|
||||
* 버튼 양식은 횡단도 줌 버튼세트(`b06-cross-card__zoom-btn`)와 같고, 설명은 툴팁이다.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 한 번 누를 때 배율 배수. 횡단도 줌(1/0.85)보다 성글게 — 폭 배수라 한 칸이 크게 느껴진다. */
|
||||
const ZOOM_STEP = 1.25;
|
||||
/** 가로 폭 배수 상한 — 이 이상은 캔버스가 수만 px이 되어 브라우저가 버겁다. */
|
||||
const MAX_X = 8;
|
||||
/** 세로 배율 상한. 전범위의 1/20 까지 좁혀 본다. */
|
||||
const MAX_Y = 20;
|
||||
/** 창 중심 이동 한 번의 몫 — 창 높이의 10%. */
|
||||
const OFFSET_STEP = 0.1;
|
||||
/** 창 중심 이동 한계 — 전범위 밖으로 완전히 벗어나지 않게 창 높이의 ±2배까지. */
|
||||
const MAX_OFFSET = 2;
|
||||
|
||||
export interface ProfileZoomState {
|
||||
/** 가로 폭 배수(1 = 현행 폭맞춤). */
|
||||
x: number;
|
||||
/** 세로 배율(1 = 표고 전범위). */
|
||||
y: number;
|
||||
/** 창 중심 이동 — 창 높이 대비 비율(+ 가 위쪽). */
|
||||
offsetRatio: number;
|
||||
}
|
||||
|
||||
export interface ProfileZoom {
|
||||
/** 절·성토 요약줄 오른쪽 끝에 붙는 버튼 묶음(한 번 만들어 계속 쓴다). */
|
||||
bar: HTMLElement;
|
||||
state: () => ProfileZoomState;
|
||||
}
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number =>
|
||||
Math.min(max, Math.max(min, value));
|
||||
|
||||
export function createProfileZoom(onChange: () => void): ProfileZoom {
|
||||
const state: ProfileZoomState = { x: 1, y: 1, offsetRatio: 0 };
|
||||
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "b05-profile__zoom";
|
||||
|
||||
function add(label: string, title: string, action: () => void): void {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b05-profile__zoom-btn";
|
||||
button.textContent = label;
|
||||
button.title = title;
|
||||
button.addEventListener("click", (event) => {
|
||||
// 카드·측점 선택으로 번지면 그래프를 다시 그리며 방금 맞춘 배율이 날아간다.
|
||||
event.stopPropagation();
|
||||
action();
|
||||
onChange();
|
||||
});
|
||||
bar.append(button);
|
||||
}
|
||||
|
||||
add("+", "가로 확대 — 측점 간격을 넓혀 폅니다 (가로 스크롤로 훑음)", () => {
|
||||
state.x = clamp(state.x * ZOOM_STEP, 1, MAX_X);
|
||||
});
|
||||
add("−", "가로 축소", () => {
|
||||
state.x = clamp(state.x / ZOOM_STEP, 1, MAX_X);
|
||||
});
|
||||
add("⇕+", "세로 확대 — 표시 표고 폭을 좁혀 고저차를 크게 봅니다", () => {
|
||||
state.y = clamp(state.y * ZOOM_STEP, 1, MAX_Y);
|
||||
});
|
||||
add("⇕−", "세로 축소", () => {
|
||||
state.y = clamp(state.y / ZOOM_STEP, 1, MAX_Y);
|
||||
});
|
||||
add("▲", "표시 표고창을 위로 (창 높이의 10%)", () => {
|
||||
state.offsetRatio = clamp(state.offsetRatio + OFFSET_STEP, -MAX_OFFSET, MAX_OFFSET);
|
||||
});
|
||||
add("▼", "표시 표고창을 아래로 (창 높이의 10%)", () => {
|
||||
state.offsetRatio = clamp(state.offsetRatio - OFFSET_STEP, -MAX_OFFSET, MAX_OFFSET);
|
||||
});
|
||||
add("⤢", "가로·세로 배율과 표시 표고창을 처음 상태로", () => {
|
||||
state.x = 1;
|
||||
state.y = 1;
|
||||
state.offsetRatio = 0;
|
||||
});
|
||||
|
||||
return { bar, state: () => ({ ...state }) };
|
||||
}
|
||||
@@ -1341,3 +1341,39 @@
|
||||
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
|
||||
outline: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── 종단도 줌·Y레인지 조작구 (2026-09-04) ─────────────────────────────
|
||||
* 요약줄 오른쪽 끝(= 그래프 우측 상단)에 붙는다. 양식은 횡단도 줌 버튼세트
|
||||
* (.b06-cross-card__zoom-btn)와 같게 맞췄다 — 그쪽 CSS는 B06 페이지 전용이라
|
||||
* 여기서 같은 값으로 다시 적는다. */
|
||||
.b05-profile__zoom {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
|
||||
}
|
||||
|
||||
.b05-profile__zoom-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
background: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b05-profile__zoom-btn:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b05-profile__zoom-btn:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function longitudinalMinimumWidth(
|
||||
* 부호가 바뀌는 지점에서 끊어 절토와 성토가 섞이지 않게 한다.
|
||||
*/
|
||||
function appendCutFillBands(
|
||||
svg: SVGSVGElement,
|
||||
svg: SVGElement,
|
||||
profile: DesignProfile,
|
||||
x: (chainage: number) => number,
|
||||
planY: (index: number) => number,
|
||||
@@ -168,6 +168,12 @@ export function createLongitudinalProfile(
|
||||
* LONG_PAD.bottom을 직접 키우면 B06 종단면도 여백까지 변하므로 호출부 옵션으로 뒀다.
|
||||
*/
|
||||
bottomInsetPx = 0,
|
||||
/**
|
||||
* 세로 표시창의 중심을 위·아래로 옮기는 몫 — **창 높이 대비 비율**(0 = 현행 가운데,
|
||||
* +0.1 = 창 높이의 10%만큼 위쪽을 본다). B05 종단도의 Y 레인지 조작구가 쓴다.
|
||||
* 표고(m)가 아니라 비율이라 호출부가 노선 표고 범위를 몰라도 된다(2026-09-04).
|
||||
*/
|
||||
elevationOffsetRatio = 0,
|
||||
): HTMLElement {
|
||||
const samples = data.samples.filter(validElevation);
|
||||
if (samples.length < 2) return emptyView(L("B06_Profile_View_NoLongitudinal"));
|
||||
@@ -207,11 +213,13 @@ export function createLongitudinalProfile(
|
||||
: // 자동 스케일(B05)은 최고·최저 표고가 위아래 축선에 딱 붙지 않게 10%씩 여유를 둔다
|
||||
// (2026-08-23 사용자 지시). 공통 Y스케일(B06 yScaleOptions)은 그대로 둔다.
|
||||
Math.max(rawMax - rawMin, 1) * 1.2;
|
||||
// 화면에 담기는 표고 폭 = 전범위 ÷ 배율. 창 중심은 그 폭의 비율만큼 위·아래로 옮긴다.
|
||||
const viewCenter = elevationMid + elevationOffsetRatio * (elevationSpan / exaggeration);
|
||||
const x = (chainage: number) =>
|
||||
LONG_PAD.left + originOffsetPx + (chainage / maxChainage) * plotWidth;
|
||||
const xInverse = inverseOf(x, maxChainage);
|
||||
const y = (elevation: number) =>
|
||||
LONG_PAD.top + ((elevationMid + elevationSpan / 2 - elevation) / elevationSpan) * plotHeight;
|
||||
LONG_PAD.top + ((viewCenter + elevationSpan / 2 - elevation) / elevationSpan) * plotHeight;
|
||||
const stationInterval = configuredStationInterval ?? inferStationInterval(data.stations);
|
||||
|
||||
// Y축 눈금: 화면에 담긴 표고 범위를 10칸 안팎으로 나누되, 눈금값이 1·2·5 계열의
|
||||
@@ -219,13 +227,13 @@ export function createLongitudinalProfile(
|
||||
// 표고가 소수로 나와 어느 지점인지 못 읽었다(2026-08-23 사용자 보고).
|
||||
const yAxisTicks: Array<{ y: number; label: string }> = [];
|
||||
const rawSpan = elevationSpan / exaggeration;
|
||||
const rawTop = elevationMid + rawSpan / 2;
|
||||
const rawTop = viewCenter + rawSpan / 2;
|
||||
// 라벨 글자(최대 13px, sticky 축)가 겹치지 않게 눈금 간격은 16px 이상 띄운다.
|
||||
// 표고 눈금은 1m 아래로 내려가지 않는다(2026-08-23 사용자 지시).
|
||||
const tickStep = Math.max(niceTickStep(rawSpan, 10, Math.floor(plotHeight / 16)), 1);
|
||||
const tickDecimals = Math.max(0, Math.ceil(-Math.log10(tickStep) - 1e-9));
|
||||
for (
|
||||
let value = Math.ceil((elevationMid - rawSpan / 2) / tickStep) * tickStep;
|
||||
let value = Math.ceil((viewCenter - rawSpan / 2) / tickStep) * tickStep;
|
||||
value <= rawTop + 1e-9;
|
||||
value += tickStep
|
||||
) {
|
||||
@@ -251,12 +259,29 @@ export function createLongitudinalProfile(
|
||||
// sticky Y축 오버레이가 SVG와 동일한 눈금을 쓰도록 전달(B05 전용, B06은 콜백 없음).
|
||||
onYAxis?.({ padLeft: LONG_PAD.left, ticks: yAxisTicks });
|
||||
|
||||
// 표시 표고창(세로 배율·중심 이동) 밖으로 나간 지반선·계획선은 그리지 않는다 —
|
||||
// 안 자르면 축 라벨·측점 라벨 띠 위로 선이 흘러나온다(2026-09-04).
|
||||
const clipId = `b06-long-clip-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const clipPath = svgElement("clipPath", { id: clipId });
|
||||
clipPath.append(
|
||||
svgElement("rect", {
|
||||
x: LONG_PAD.left,
|
||||
y: LONG_PAD.top,
|
||||
width: Math.max(0, widthPx - LONG_PAD.left - LONG_PAD.right),
|
||||
height: Math.max(0, plotHeight),
|
||||
}),
|
||||
);
|
||||
const defs = svgElement("defs", {});
|
||||
defs.append(clipPath);
|
||||
const bandLayer = svgElement("g", { "clip-path": `url(#${clipId})` });
|
||||
svg.append(defs, bandLayer);
|
||||
|
||||
// 절·성토 음영과 균형 구역 경계는 측점선·프로파일선보다 아래에 깔린다.
|
||||
const toY = (elevation: number) => y(elevationMid + (elevation - elevationMid) * exaggeration);
|
||||
const toY = (elevation: number) => y(viewCenter + (elevation - viewCenter) * exaggeration);
|
||||
for (const profile of designProfiles) {
|
||||
if (profile.samples.length < 2) continue;
|
||||
appendCutFillBands(
|
||||
svg,
|
||||
bandLayer,
|
||||
profile,
|
||||
x,
|
||||
(index) => toY(profile.samples[index].elevation_m),
|
||||
@@ -264,7 +289,7 @@ export function createLongitudinalProfile(
|
||||
);
|
||||
if (profile.balance_segments.length > 1) {
|
||||
for (const segment of profile.balance_segments.slice(1)) {
|
||||
svg.append(
|
||||
bandLayer.append(
|
||||
svgElement("line", {
|
||||
x1: x(segment.start_chainage_m),
|
||||
y1: LONG_PAD.top,
|
||||
@@ -342,13 +367,14 @@ export function createLongitudinalProfile(
|
||||
|
||||
const points = samples
|
||||
.map((sample) => {
|
||||
const elevated = elevationMid + (sample.elevation_m - elevationMid) * exaggeration;
|
||||
const elevated = viewCenter + (sample.elevation_m - viewCenter) * exaggeration;
|
||||
return `${x(sample.chainage_m ?? 0)},${y(elevated)}`;
|
||||
})
|
||||
.join(" ");
|
||||
const lineLayer = svgElement("g", { "clip-path": `url(#${clipId})` });
|
||||
for (const profile of designProfiles) {
|
||||
if (profile.samples.length < 2) continue;
|
||||
svg.append(
|
||||
lineLayer.append(
|
||||
svgElement("polyline", {
|
||||
points: profile.samples
|
||||
.map((sample) => `${x(sample.chainage_m)},${toY(sample.elevation_m)}`)
|
||||
@@ -357,8 +383,9 @@ export function createLongitudinalProfile(
|
||||
}),
|
||||
);
|
||||
}
|
||||
lineLayer.append(svgElement("polyline", { points, class: "b06-chart__profile" }));
|
||||
svg.append(
|
||||
svgElement("polyline", { points, class: "b06-chart__profile" }),
|
||||
lineLayer,
|
||||
svgElement("line", {
|
||||
x1: LONG_PAD.left,
|
||||
y1: heightPx - padBottom,
|
||||
|
||||
Reference in New Issue
Block a user