perf(B05,B06): 유토곡선·B06 페이지도 세로 창을 실시간으로 따라오게
사용자 지적(2026-09-04) — 「유토곡선도 반영해야지. B06 페이지도 있잖아. 같은 로직일 텐데 왜 반영이 안 된 거야」. 그리는 쪽(종단 렌더러)은 공용이라 이미 바뀌어 있었고, **스크롤을 받아 창을 옮기는 쪽이 화면마다 따로**라 B05 만 연결돼 있던 것. - 세로 창 모듈을 `common_util_chart_ywindow.ts` 로 옮겨 B05·B06 이 함께 씀. 좌측 고정 축은 표식(`b05-profile__yaxis--ywindow`)이 붙은 것만 옮김 — B06 은 한 컨테이너에 종단·유토곡선 축이 함께 살아 남의 축까지 옮기면 안 됨. - 세로 과장(B06 조절값)을 각인·반영 — 예전에는 과장이 1일 때만 대상이었음. - 유토곡선은 변환이 아니라 **곡선만 다시 그림**(스크롤마다). 곡선 위 말풍선·EP 표·측점 점처럼 세로로 늘어나면 안 되는 것이 많아, 그것만 따로 옮기는 값이 다시 그리는 값보다 큼 (유토 SVG 234개 = 종단의 1/3). - B06 상단 패널을 통째로 다시 그리던 스크롤 처리를 걷어내고, 종단은 변환·유토곡선은 제자리 교체로 바꿈. 700줄 제한에 맞춰 종단 조립을 `_View_Chart.ts`(116줄)로, 유토곡선 조립기를 `_View_MassHaul.ts` 로 분리(727 → 718줄). 자체검증(공용 브라우저 임시 탭): ① B05 유토곡선 눈금이 스크롤을 따라 4.3㎥ → −1,348.8㎥ 로 이동(예전에는 안 따라옴) ② B06 종단 눈금 830/840/850m → 824/826/828m, 유토곡선 68.2㎥ → −3,375.1㎥ 로 동시 이동 ③ B06 최대 프레임 **35.9 → 23.5ms**, 33ms 초과 1 → **0건**(중앙값 16.7ms 유지) ④ B05 최대 프레임 27.4ms, 33ms 초과 0건 ⑤ 전체 시험 381 통과·17 건너뜀. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,7 @@ const CROSS_PREVIEW_DEBOUNCE_MS = 60;
|
|||||||
* 동작하도록 여기서 다시 내보낸다(2026-09-04). */
|
* 동작하도록 여기서 다시 내보낸다(2026-09-04). */
|
||||||
import type { RouteProfilePanelCallbacks } from "./B05_Profile_UI_Profile_Panel_Types";
|
import type { RouteProfilePanelCallbacks } from "./B05_Profile_UI_Profile_Panel_Types";
|
||||||
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
||||||
import { needsFullRedraw, type ElevationWindowResult } from "./B05_Profile_UI_Profile_YWindow";
|
import { needsFullRedraw, type ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
||||||
export type { RouteProfilePanelCallbacks } from "./B05_Profile_UI_Profile_Panel_Types";
|
export type { RouteProfilePanelCallbacks } from "./B05_Profile_UI_Profile_Panel_Types";
|
||||||
|
|
||||||
export function createRouteProfilePanel(
|
export function createRouteProfilePanel(
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ import {
|
|||||||
} from "./B05_Profile_UI_Profile_Alignment";
|
} from "./B05_Profile_UI_Profile_Alignment";
|
||||||
import { createEditOverlay } from "./B05_Profile_UI_Profile_Edit";
|
import { createEditOverlay } from "./B05_Profile_UI_Profile_Edit";
|
||||||
import { createRunHighlight } from "./B05_Profile_UI_Profile_RunHighlight";
|
import { createRunHighlight } from "./B05_Profile_UI_Profile_RunHighlight";
|
||||||
import { applyElevationWindow, type ElevationWindowResult } from "./B05_Profile_UI_Profile_YWindow";
|
import {
|
||||||
|
applyElevationWindow,
|
||||||
|
Y_AXIS_WINDOW_CLASS,
|
||||||
|
type ElevationWindowResult,
|
||||||
|
} from "@util/common_util_chart_ywindow";
|
||||||
import type { StraightRun } from "./B05_Profile_UI_Profile_Straighten";
|
import type { StraightRun } from "./B05_Profile_UI_Profile_Straighten";
|
||||||
import type { MinCoverPoint } from "./B05_Profile_UI_Profile_MinCover";
|
import type { MinCoverPoint } from "./B05_Profile_UI_Profile_MinCover";
|
||||||
import { buildStickyYAxis, type RouteMassHaulDrawParams } from "./B05_Profile_UI_Profile_MassHaul";
|
import { buildStickyYAxis, type RouteMassHaulDrawParams } from "./B05_Profile_UI_Profile_MassHaul";
|
||||||
@@ -329,6 +333,8 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
|||||||
});
|
});
|
||||||
// 세로 창 갱신기 — 가로로 스크롤할 때마다 패널이 이것을 부른다. 도형은 그대로 두고
|
// 세로 창 갱신기 — 가로로 스크롤할 때마다 패널이 이것을 부른다. 도형은 그대로 두고
|
||||||
// 겹 하나의 변환만 갈아 끼우므로 매 프레임 불러도 된다(2026-09-04 사용자 확정).
|
// 겹 하나의 변환만 갈아 끼우므로 매 프레임 불러도 된다(2026-09-04 사용자 확정).
|
||||||
|
// 유토곡선 갱신기 — 아래에서 유토곡선을 그린 뒤 채워진다(그리기 순서상 여기서는 아직 없다).
|
||||||
|
let massHaulUpdater: ((fromM: number, toM: number) => void) | null = null;
|
||||||
ctx.setElevationWindowUpdater(() => {
|
ctx.setElevationWindowUpdater(() => {
|
||||||
const from = Math.max(0, toChainage(body.scrollLeft));
|
const from = Math.max(0, toChainage(body.scrollLeft));
|
||||||
const to = Math.min(maxChainageM, toChainage(body.scrollLeft + body.clientWidth));
|
const to = Math.min(maxChainageM, toChainage(body.scrollLeft + body.clientWidth));
|
||||||
@@ -339,13 +345,19 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
|||||||
to,
|
to,
|
||||||
) ?? null,
|
) ?? null,
|
||||||
);
|
);
|
||||||
|
massHaulUpdater?.(from, to);
|
||||||
return applyElevationWindow(chartWrap, next);
|
return applyElevationWindow(chartWrap, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 가로 스크롤에도 고정되는 sticky Y축 오버레이(SVG와 같은 눈금·불투명 배경으로 값 누출 차단).
|
// 가로 스크롤에도 고정되는 sticky Y축 오버레이(SVG와 같은 눈금·불투명 배경으로 값 누출 차단).
|
||||||
// 앵커(0크기 sticky)는 **첫 자식**이어야 한다 — SVG 뒤에 붙이면 흐름 위치가 차트
|
// 앵커(0크기 sticky)는 **첫 자식**이어야 한다 — SVG 뒤에 붙이면 흐름 위치가 차트
|
||||||
// 아래로 밀려 스크롤 시 축이 화면에 안 보인다(2026-08-04 확인, B06과 같은 규칙).
|
// 아래로 밀려 스크롤 시 축이 화면에 안 보인다(2026-08-04 확인, B06과 같은 규칙).
|
||||||
if (yAxis) chartWrap.prepend(buildStickyYAxis(yAxis, chartHeight));
|
if (yAxis) {
|
||||||
|
const axisOverlay = buildStickyYAxis(yAxis, chartHeight);
|
||||||
|
// 세로 창을 따라 움직이는 축임을 표시한다(유토곡선 축과 구분).
|
||||||
|
axisOverlay.classList.add(Y_AXIS_WINDOW_CLASS);
|
||||||
|
chartWrap.prepend(axisOverlay);
|
||||||
|
}
|
||||||
// 쉬프트로 고른 구간 강조 — 편집 버튼층보다 아래에 깔아 버튼을 가리지 않는다.
|
// 쉬프트로 고른 구간 강조 — 편집 버튼층보다 아래에 깔아 버튼을 가리지 않는다.
|
||||||
if (alignment) {
|
if (alignment) {
|
||||||
const highlight = createRunHighlight({
|
const highlight = createRunHighlight({
|
||||||
@@ -392,7 +404,7 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
|||||||
// 유토곡선 오버레이 — 종단도·테이블 위를 덮는 서브패널(2026-08-04 사용자 확정).
|
// 유토곡선 오버레이 — 종단도·테이블 위를 덮는 서브패널(2026-08-04 사용자 확정).
|
||||||
// X 매핑(누가거리 최댓값·여백·폭)을 종단 그래프와 똑같이 넘겨야 측점 세로선이 맞물린다.
|
// X 매핑(누가거리 최댓값·여백·폭)을 종단 그래프와 똑같이 넘겨야 측점 세로선이 맞물린다.
|
||||||
if (alignment && designProfiles[0]) {
|
if (alignment && designProfiles[0]) {
|
||||||
massHaul.draw({
|
const massHaulParams: RouteMassHaulDrawParams = {
|
||||||
stationSource: graphLongitudinal,
|
stationSource: graphLongitudinal,
|
||||||
// 종단 개략 곡선은 편집이 반영된 현재 계획선을, 정식 곡선은 상세 조회가 내려준
|
// 종단 개략 곡선은 편집이 반영된 현재 계획선을, 정식 곡선은 상세 조회가 내려준
|
||||||
// 측점별 횡단 설계(기본 프리뷰 포함)를 입력으로 쓴다 — B06과 같은 재료다.
|
// 측점별 횡단 설계(기본 프리뷰 포함)를 입력으로 쓴다 — B06과 같은 재료다.
|
||||||
@@ -429,6 +441,17 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
|||||||
onClearSelection: () => {
|
onClearSelection: () => {
|
||||||
if (ctx.selectedStationId() !== null) ctx.selectStation(null);
|
if (ctx.selectedStationId() !== null) ctx.selectStation(null);
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
massHaul.draw(massHaulParams);
|
||||||
|
// 유토곡선도 **같은 창**을 본다(2026-09-04 사용자 확정) — 스크롤할 때마다 곡선만 다시
|
||||||
|
// 그린다. 종단처럼 변환으로 옮기지 않는 이유: 곡선 위에 앉는 말풍선·EP 표·측점 점이
|
||||||
|
// 세로로 늘어나면 안 되는 것들이라, 그것만 따로 옮기는 값이 곡선 자체를 다시 그리는
|
||||||
|
// 값보다 크다(요소 수가 종단의 1/3).
|
||||||
|
massHaulUpdater = (fromM, toM) => {
|
||||||
|
massHaul.draw({
|
||||||
|
...massHaulParams,
|
||||||
|
axis: { ...massHaulParams.axis, viewRange: { fromM, toM } },
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const Y_WINDOW_ATTRS = {
|
|||||||
/** 플롯 상단 y(px)와 높이(px) — 창 밖으로 나간 글자를 감추는 데 쓴다. */
|
/** 플롯 상단 y(px)와 높이(px) — 창 밖으로 나간 글자를 감추는 데 쓴다. */
|
||||||
top: "data-yw-top",
|
top: "data-yw-top",
|
||||||
height: "data-yw-height",
|
height: "data-yw-height",
|
||||||
|
/** 세로 과장(B06 조절값). 새 창의 px 환산에 그대로 곱한다. */
|
||||||
|
exaggeration: "data-yw-exag",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -290,8 +292,10 @@ export function createLongitudinalProfile(
|
|||||||
svg.setAttribute(Y_WINDOW_ATTRS.zero, String(LONG_PAD.top + plotHeight / 2));
|
svg.setAttribute(Y_WINDOW_ATTRS.zero, String(LONG_PAD.top + plotHeight / 2));
|
||||||
svg.setAttribute(Y_WINDOW_ATTRS.top, String(LONG_PAD.top));
|
svg.setAttribute(Y_WINDOW_ATTRS.top, String(LONG_PAD.top));
|
||||||
svg.setAttribute(Y_WINDOW_ATTRS.height, String(plotHeight));
|
svg.setAttribute(Y_WINDOW_ATTRS.height, String(plotHeight));
|
||||||
// 고정 배율(B06 공통 Y스케일)·세로 과장이 걸린 그래프는 창을 옮기지 않는다.
|
svg.setAttribute(Y_WINDOW_ATTRS.exaggeration, String(exaggeration));
|
||||||
if (!yScaleOptions && exaggeration === 1) svg.setAttribute(Y_WINDOW_ATTRS.auto, "1");
|
// 고정 배율(B06 공통 Y스케일)에 묶인 그래프만 창을 옮기지 않는다 — 세로 과장은 새 창의
|
||||||
|
// px 환산에 그대로 곱하면 되므로 대상이다(B06 종단, 2026-09-04).
|
||||||
|
if (!yScaleOptions) svg.setAttribute(Y_WINDOW_ATTRS.auto, "1");
|
||||||
const stationInterval = configuredStationInterval ?? inferStationInterval(data.stations);
|
const stationInterval = configuredStationInterval ?? inferStationInterval(data.stations);
|
||||||
|
|
||||||
// Y축 눈금: 화면에 담긴 표고 범위를 10칸 안팎으로 나누되, 눈금값이 1·2·5 계열의
|
// Y축 눈금: 화면에 담긴 표고 범위를 10칸 안팎으로 나누되, 눈금값이 1·2·5 계열의
|
||||||
|
|||||||
@@ -40,11 +40,22 @@ import {
|
|||||||
} from "./B06_Section_UI_Cross_View";
|
} from "./B06_Section_UI_Cross_View";
|
||||||
import { culvertLinkFor as culvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
import { culvertLinkFor as culvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||||
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||||
import { createLongitudinalProfile, longitudinalMinimumWidth } from "./B06_Section_UI_Longitudinal";
|
import { longitudinalMinimumWidth } from "./B06_Section_UI_Longitudinal";
|
||||||
|
import {
|
||||||
|
buildLongitudinalChart,
|
||||||
|
visibleChainageRange,
|
||||||
|
visibleElevationRange,
|
||||||
|
} from "./B06_Section_UI_Section_View_Chart";
|
||||||
import { applyLegendToggle } from "@util/common_util_mass_haul";
|
import { applyLegendToggle } from "@util/common_util_mass_haul";
|
||||||
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
||||||
import { MASS_HAUL_MIN_HEIGHT } from "@util/common_util_mass_haul_view";
|
import { MASS_HAUL_MIN_HEIGHT } from "@util/common_util_mass_haul_view";
|
||||||
import { buildMassHaulPanel } from "./B06_Section_UI_Section_View_MassHaul";
|
import { createMassHaulRenderer } from "./B06_Section_UI_Section_View_MassHaul";
|
||||||
|
import {
|
||||||
|
applyElevationWindow,
|
||||||
|
needsFullRedraw,
|
||||||
|
Y_AXIS_WINDOW_CLASS,
|
||||||
|
type ElevationWindowResult,
|
||||||
|
} from "@util/common_util_chart_ywindow";
|
||||||
import {
|
import {
|
||||||
BASE_PANEL_HEIGHT,
|
BASE_PANEL_HEIGHT,
|
||||||
chartHeights,
|
chartHeights,
|
||||||
@@ -56,7 +67,6 @@ import {
|
|||||||
PANEL_COLLAPSED_KEY,
|
PANEL_COLLAPSED_KEY,
|
||||||
PANEL_HEIGHT_KEY,
|
PANEL_HEIGHT_KEY,
|
||||||
readVisibleSeries,
|
readVisibleSeries,
|
||||||
unwrapChart,
|
|
||||||
} from "./B06_Section_UI_Section_View_Panel";
|
} from "./B06_Section_UI_Section_View_Panel";
|
||||||
import {
|
import {
|
||||||
CROSS_GRID_GAP,
|
CROSS_GRID_GAP,
|
||||||
@@ -68,9 +78,6 @@ import {
|
|||||||
emptyView,
|
emptyView,
|
||||||
inferStationInterval,
|
inferStationInterval,
|
||||||
L,
|
L,
|
||||||
LONG_PAD,
|
|
||||||
longitudinalMaxChainage,
|
|
||||||
windowElevationRange,
|
|
||||||
} from "./B06_Section_UI_Section_Common";
|
} from "./B06_Section_UI_Section_Common";
|
||||||
|
|
||||||
export type { CrossDesignChange, DesignChangeHandler, RockBoundaryControl };
|
export type { CrossDesignChange, DesignChangeHandler, RockBoundaryControl };
|
||||||
@@ -455,6 +462,11 @@ export function createSectionView(
|
|||||||
drawPanel();
|
drawPanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 유토곡선만 새 창으로 다시 그리는 함수 — 그릴 때마다 새로 만든다(붙인 노드는 그 안). */
|
||||||
|
let renderMassHaul: ((fromM: number, toM: number) => void) | null = null;
|
||||||
|
/** 세로 창 갱신기 — 종단은 변환, 유토곡선은 곡선만. */
|
||||||
|
let updateChartWindow: (() => ElevationWindowResult | null) | null = null;
|
||||||
|
|
||||||
/** 상단 패널 내용(종단면도 + 유토곡선 + 요약)만 다시 그린다. */
|
/** 상단 패널 내용(종단면도 + 유토곡선 + 요약)만 다시 그린다. */
|
||||||
function drawPanel(): void {
|
function drawPanel(): void {
|
||||||
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
||||||
@@ -473,93 +485,69 @@ export function createSectionView(
|
|||||||
const heights = chartHeights(lastChartAvailable);
|
const heights = chartHeights(lastChartAvailable);
|
||||||
const minWidth = longitudinalMinimumWidth(detail.longitudinal, cachedStationInterval);
|
const minWidth = longitudinalMinimumWidth(detail.longitudinal, cachedStationInterval);
|
||||||
const chartWidth = Math.max(renderWidth, minWidth);
|
const chartWidth = Math.max(renderWidth, minWidth);
|
||||||
// 종단 그래프의 세로는 **보이는 구간에 자동으로 맞춘다**(2026-09-04 사용자 확정,
|
const chart = buildLongitudinalChart({
|
||||||
// B05 와 같은 규칙·같은 함수). 가로 스크롤 위치와 컨테이너 폭이 곧 보이는 구간이다.
|
|
||||||
const maxChainageM = longitudinalMaxChainage(detail.longitudinal);
|
|
||||||
const plotWidth = Math.max(1, chartWidth - LONG_PAD.left - LONG_PAD.right);
|
|
||||||
const toChainage = (px: number): number => ((px - LONG_PAD.left) / plotWidth) * maxChainageM;
|
|
||||||
const viewFromM = Math.max(0, toChainage(keepScrollLeft));
|
|
||||||
const viewToM = Math.min(
|
|
||||||
maxChainageM,
|
|
||||||
toChainage(keepScrollLeft + (chartWrap.clientWidth || chartWidth)),
|
|
||||||
);
|
|
||||||
const longElevationRange = windowElevationRange(
|
|
||||||
[
|
|
||||||
detail.longitudinal.samples,
|
|
||||||
...(detail.longitudinal.design_profiles ?? []).map((profile) => profile.samples),
|
|
||||||
],
|
|
||||||
viewFromM,
|
|
||||||
viewToM,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Y축 눈금을 렌더러에서 받아 가로 스크롤 고정 오버레이로 얹는다(2026-08-04 사용자
|
|
||||||
// 지시 — 테이블 행 이름표처럼 스크롤해도 계속 보이게, B05와 같은 방식).
|
|
||||||
let longAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null;
|
|
||||||
const nodes: Element[] = [
|
|
||||||
unwrapChart(
|
|
||||||
createLongitudinalProfile(
|
|
||||||
detail.longitudinal,
|
|
||||||
selectedStationId,
|
|
||||||
currentExaggeration,
|
|
||||||
// 공통 Y 스케일(횡단 카드 몫)을 여기 넘기면 종단이 전 구간 축에 묶여 눌린다.
|
|
||||||
undefined,
|
|
||||||
(stationId) => selectStation(stationId, true),
|
|
||||||
cachedStationInterval,
|
|
||||||
chartWidth,
|
|
||||||
heights.long,
|
|
||||||
minWidth,
|
|
||||||
detail.longitudinal.design_profiles ?? [],
|
|
||||||
0,
|
|
||||||
(axis) => {
|
|
||||||
longAxis = axis;
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
longElevationRange,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 유토곡선 몫은 전용 모듈이 만든다(2026-09-03 · 700줄 제한) — 곡선 계산부터
|
|
||||||
// 범례·요약줄까지 한 덩어리라 붙이는 자리만 여기서 정한다.
|
|
||||||
const massHaul = buildMassHaulPanel({
|
|
||||||
detail,
|
detail,
|
||||||
conversion: currentConversion,
|
|
||||||
haulLimits: currentHaulLimits,
|
|
||||||
naturalSpoilSlope: currentNaturalSpoilSlope,
|
|
||||||
visibleSeries,
|
|
||||||
selectedStationId,
|
selectedStationId,
|
||||||
|
exaggeration: currentExaggeration,
|
||||||
|
selectStation: (stationId) => selectStation(stationId, true),
|
||||||
stationInterval: cachedStationInterval,
|
stationInterval: cachedStationInterval,
|
||||||
chartWidth,
|
chartWidth,
|
||||||
|
chartHeight: heights.long,
|
||||||
minWidth,
|
minWidth,
|
||||||
massHeight: heights.mass,
|
scrollLeft: keepScrollLeft,
|
||||||
longHeight: heights.long,
|
viewportWidth: chartWrap.clientWidth || chartWidth,
|
||||||
selectStation: (stationId) => selectStation(stationId, true),
|
|
||||||
toggleSeries,
|
|
||||||
redraw: drawPanel,
|
|
||||||
// 유토곡선도 종단과 같은 창을 본다(2026-09-04).
|
|
||||||
viewRange: { fromM: viewFromM, toM: viewToM },
|
|
||||||
});
|
});
|
||||||
if (massHaul.chart) nodes.push(massHaul.chart);
|
const longAxis = chart.axis;
|
||||||
|
const nodes: Element[] = [chart.node];
|
||||||
|
const { viewFromM, viewToM } = chart;
|
||||||
|
|
||||||
chartWrap.replaceChildren(...nodes);
|
chartWrap.replaceChildren(...nodes);
|
||||||
// 고정 Y축 오버레이 — 0크기 sticky 앵커라 **첫 자식**으로 넣어야 세로 기준이 컨테이너
|
// 유토곡선 몫은 전용 모듈이 만들어 붙인다(2026-09-03 · 700줄 제한). 가로로 스크롤할
|
||||||
// 상단이 된다(SVG 뒤에 넣으면 앵커가 차트 아래로 밀린다). 유토곡선 축은 종단 높이만큼
|
// 때는 **이 함수만** 다시 불러 곡선의 세로 창을 따라오게 한다 — 종단면도는 건드리지
|
||||||
// 안쪽(inner)을 내려 자기 그래프 구간만 덮는다 — 불투명 배경이 지나간 눈금 노출을 막는다.
|
// 않는다(2026-09-04: 상단 패널을 통째로 다시 그리면 화면이 한 번 끊긴다).
|
||||||
if (massHaul.axis) {
|
renderMassHaul = createMassHaulRenderer(
|
||||||
const overlay = buildStickyYAxis(massHaul.axis, heights.mass);
|
{
|
||||||
(overlay.firstElementChild as HTMLElement).style.top = `${heights.long}px`;
|
detail,
|
||||||
|
conversion: currentConversion,
|
||||||
|
haulLimits: currentHaulLimits,
|
||||||
|
naturalSpoilSlope: currentNaturalSpoilSlope,
|
||||||
|
visibleSeries,
|
||||||
|
selectedStationId,
|
||||||
|
stationInterval: cachedStationInterval,
|
||||||
|
chartWidth,
|
||||||
|
minWidth,
|
||||||
|
massHeight: heights.mass,
|
||||||
|
longHeight: heights.long,
|
||||||
|
selectStation: (stationId) => selectStation(stationId, true),
|
||||||
|
toggleSeries,
|
||||||
|
redraw: drawPanel,
|
||||||
|
},
|
||||||
|
{ chartWrap, panel, panelBody, statusNode: panelCount },
|
||||||
|
);
|
||||||
|
renderMassHaul(viewFromM, viewToM);
|
||||||
|
// 종단 고정 Y축 — 0크기 sticky 앵커라 **첫 자식**으로 넣어야 세로 기준이 컨테이너
|
||||||
|
// 상단이 된다(SVG 뒤에 넣으면 앵커가 차트 아래로 밀린다).
|
||||||
|
if (longAxis) {
|
||||||
|
const overlay = buildStickyYAxis(longAxis, heights.long);
|
||||||
|
// 세로 창을 따라 움직이는 축임을 표시한다(유토곡선 축과 구분).
|
||||||
|
overlay.classList.add(Y_AXIS_WINDOW_CLASS);
|
||||||
chartWrap.prepend(overlay);
|
chartWrap.prepend(overlay);
|
||||||
}
|
}
|
||||||
if (longAxis) chartWrap.prepend(buildStickyYAxis(longAxis, heights.long));
|
|
||||||
|
|
||||||
panel.querySelector(".b06-masshaul__legend")?.remove();
|
// 세로 창 갱신기 — 스크롤마다 종단은 변환으로, 유토곡선은 곡선만 다시 그려 따라온다.
|
||||||
panel.querySelector(".b06-masshaul__summary")?.remove();
|
updateChartWindow = () => {
|
||||||
// 범례는 스크롤 컨테이너 **밖**에 절대 위치로 띄운다 — 안에 넣으면 컨테이너 폭 계산에
|
const { fromM, toM } = visibleChainageRange(
|
||||||
// 끼어들어 두 그래프의 측점 세로선이 어긋난다(1차 수정에서 겪은 문제).
|
chart.toChainage,
|
||||||
panelBody.append(...massHaul.overlays);
|
chart.maxChainageM,
|
||||||
panelCount.textContent = massHaul.statusText;
|
chartWrap.scrollLeft,
|
||||||
|
chartWrap.clientWidth || chartWidth,
|
||||||
|
);
|
||||||
|
renderMassHaul?.(fromM, toM);
|
||||||
|
return applyElevationWindow(
|
||||||
|
chartWrap,
|
||||||
|
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
||||||
|
);
|
||||||
|
};
|
||||||
chartWrap.scrollLeft = keepScrollLeft;
|
chartWrap.scrollLeft = keepScrollLeft;
|
||||||
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
||||||
syncScrollMargin();
|
syncScrollMargin();
|
||||||
@@ -577,13 +565,16 @@ export function createSectionView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 가로 스크롤이 이만큼 멈춰 있으면 보이는 구간이 정해진 것으로 보고 세로를 다시 맞춘다. */
|
/** 상단 패널을 통째로 다시 그리기까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
||||||
const SCROLL_SETTLE_MS = 160;
|
const SCROLL_SETTLE_MS = 80;
|
||||||
let settledScrollLeft = 0;
|
let settledScrollLeft = 0;
|
||||||
let scrollSettleTimer = 0;
|
let scrollSettleTimer = 0;
|
||||||
// 스크롤·팬 하는 내내 축이 출렁이면 어지럽다 — 멈춘 뒤 한 번만 다시 그린다(2026-09-04).
|
// 세로 맞춤은 **그리기가 아니라 변환**이다(2026-09-04 사용자 확정, B05와 같은 규칙) —
|
||||||
|
// 스크롤마다 겹 하나의 변환만 갈아 끼우므로 실시간으로 따라온다.
|
||||||
chartWrap.addEventListener("scroll", () => {
|
chartWrap.addEventListener("scroll", () => {
|
||||||
|
const fitted = updateChartWindow?.() ?? null;
|
||||||
window.clearTimeout(scrollSettleTimer);
|
window.clearTimeout(scrollSettleTimer);
|
||||||
|
if (!needsFullRedraw(fitted)) return;
|
||||||
scrollSettleTimer = window.setTimeout(() => {
|
scrollSettleTimer = window.setTimeout(() => {
|
||||||
if (Math.abs(chartWrap.scrollLeft - settledScrollLeft) < 1) return;
|
if (Math.abs(chartWrap.scrollLeft - settledScrollLeft) < 1) return;
|
||||||
settledScrollLeft = chartWrap.scrollLeft;
|
settledScrollLeft = chartWrap.scrollLeft;
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B06_Section_UI_Section_View_Chart.ts
|
||||||
|
* B06 상단 패널의 **종단면도 몫**만 떼어 낸 조립기 (2026-09-04 · 700줄 제한).
|
||||||
|
*
|
||||||
|
* 세로는 **보이는 구간에 자동으로 맞춘다**(2026-09-04 사용자 확정, B05 와 같은 규칙·같은
|
||||||
|
* 함수). 가로 스크롤 위치와 컨테이너 폭이 곧 보이는 구간이다. 스크롤할 때 그래프를 다시
|
||||||
|
* 만들지 않고 세로 창을 **변환으로** 옮기는 것도 B05 와 같다
|
||||||
|
* (`common_util_chart_ywindow`).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||||
|
import { createLongitudinalProfile } from "./B06_Section_UI_Longitudinal";
|
||||||
|
import {
|
||||||
|
LONG_PAD,
|
||||||
|
longitudinalMaxChainage,
|
||||||
|
windowElevationRange,
|
||||||
|
} from "./B06_Section_UI_Section_Common";
|
||||||
|
import { unwrapChart } from "./B06_Section_UI_Section_View_Panel";
|
||||||
|
|
||||||
|
export interface LongitudinalChartInput {
|
||||||
|
detail: SectionDetailResponse;
|
||||||
|
selectedStationId: string | null;
|
||||||
|
/** 세로 과장(사용자 조절값). */
|
||||||
|
exaggeration: number;
|
||||||
|
selectStation: (stationId: string) => void;
|
||||||
|
stationInterval: number;
|
||||||
|
chartWidth: number;
|
||||||
|
chartHeight: number;
|
||||||
|
minWidth: number;
|
||||||
|
/** 지금 보이는 구간을 정하는 값 — 가로 스크롤 위치와 컨테이너 안쪽 폭. */
|
||||||
|
scrollLeft: number;
|
||||||
|
viewportWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LongitudinalChartResult {
|
||||||
|
node: Element;
|
||||||
|
axis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null;
|
||||||
|
/** 화면 x(px) → 누가거리(m). 스크롤 갱신이 보이는 구간을 다시 잴 때 쓴다. */
|
||||||
|
toChainage: (px: number) => number;
|
||||||
|
maxChainageM: number;
|
||||||
|
viewFromM: number;
|
||||||
|
viewToM: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 지금 보이는 구간(누가거리 m)을 잰다 — 그릴 때와 스크롤 갱신 때가 **같은 식**을 쓴다. */
|
||||||
|
export function visibleChainageRange(
|
||||||
|
toChainage: (px: number) => number,
|
||||||
|
maxChainageM: number,
|
||||||
|
scrollLeft: number,
|
||||||
|
viewportWidth: number,
|
||||||
|
): { fromM: number; toM: number } {
|
||||||
|
return {
|
||||||
|
fromM: Math.max(0, toChainage(scrollLeft)),
|
||||||
|
toM: Math.min(maxChainageM, toChainage(scrollLeft + viewportWidth)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 보이는 구간의 표고 범위 — 종단 세로 창의 기준. */
|
||||||
|
export function visibleElevationRange(
|
||||||
|
detail: SectionDetailResponse,
|
||||||
|
fromM: number,
|
||||||
|
toM: number,
|
||||||
|
): { min: number; max: number } | null {
|
||||||
|
return (
|
||||||
|
windowElevationRange(
|
||||||
|
[
|
||||||
|
detail.longitudinal.samples,
|
||||||
|
...(detail.longitudinal.design_profiles ?? []).map((profile) => profile.samples),
|
||||||
|
],
|
||||||
|
fromM,
|
||||||
|
toM,
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildLongitudinalChart(input: LongitudinalChartInput): LongitudinalChartResult {
|
||||||
|
const { detail } = input;
|
||||||
|
const maxChainageM = longitudinalMaxChainage(detail.longitudinal);
|
||||||
|
const plotWidth = Math.max(1, input.chartWidth - LONG_PAD.left - LONG_PAD.right);
|
||||||
|
const toChainage = (px: number): number => ((px - LONG_PAD.left) / plotWidth) * maxChainageM;
|
||||||
|
const { fromM, toM } = visibleChainageRange(
|
||||||
|
toChainage,
|
||||||
|
maxChainageM,
|
||||||
|
input.scrollLeft,
|
||||||
|
input.viewportWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Y축 눈금을 렌더러에서 받아 가로 스크롤 고정 오버레이로 얹는다(2026-08-04 사용자
|
||||||
|
// 지시 — 테이블 행 이름표처럼 스크롤해도 계속 보이게, B05와 같은 방식).
|
||||||
|
let axis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null;
|
||||||
|
const node = unwrapChart(
|
||||||
|
createLongitudinalProfile(
|
||||||
|
detail.longitudinal,
|
||||||
|
input.selectedStationId,
|
||||||
|
input.exaggeration,
|
||||||
|
// 공통 Y 스케일(횡단 카드 몫)을 여기 넘기면 종단이 전 구간 축에 묶여 눌린다.
|
||||||
|
undefined,
|
||||||
|
input.selectStation,
|
||||||
|
input.stationInterval,
|
||||||
|
input.chartWidth,
|
||||||
|
input.chartHeight,
|
||||||
|
input.minWidth,
|
||||||
|
detail.longitudinal.design_profiles ?? [],
|
||||||
|
0,
|
||||||
|
(next) => {
|
||||||
|
axis = next;
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return { node, axis, toChainage, maxChainageM, viewFromM: fromM, viewToM: toM };
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
* 사용자가 옛 그림을 본다.
|
* 사용자가 옛 그림을 본다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { buildStickyYAxis } from "../B05_Profile/B05_Profile_UI_Profile_MassHaul";
|
||||||
import type { EarthworkConversion, HaulEquipmentLimit } from "./B06_Section_Api_Fetch";
|
import type { EarthworkConversion, HaulEquipmentLimit } from "./B06_Section_Api_Fetch";
|
||||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||||
import {
|
import {
|
||||||
@@ -142,3 +143,77 @@ export function buildMassHaulPanel(input: MassHaulPanelInput): MassHaulPanelResu
|
|||||||
: L(series.length ? "B06_MassHaul_AllHidden" : "B06_MassHaul_Empty");
|
: L(series.length ? "B06_MassHaul_AllHidden" : "B06_MassHaul_Empty");
|
||||||
return { chart, axis, overlays, statusText };
|
return { chart, axis, overlays, statusText };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 유토곡선을 붙일 자리 — 뷰 컨트롤러가 들고 있는 DOM 이다. */
|
||||||
|
export interface MassHaulMountTargets {
|
||||||
|
/** 차트가 들어가는 가로 스크롤 컨테이너. 종단면도와 **같은 부모의 형제**여야 한다. */
|
||||||
|
chartWrap: HTMLElement;
|
||||||
|
/** 옛 범례·요약줄을 찾아 지울 뿌리. */
|
||||||
|
panel: HTMLElement;
|
||||||
|
/** 새 범례·요약줄을 붙일 자리(스크롤 컨테이너 밖). */
|
||||||
|
panelBody: HTMLElement;
|
||||||
|
/** 곡선을 못 그린 이유를 적는 상태줄. */
|
||||||
|
statusNode: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 붙여 둔 유토곡선 노드 — 다음 갱신에서 **제자리 교체**하는 데 쓴다. */
|
||||||
|
export interface MountedMassHaul {
|
||||||
|
chart: Element | null;
|
||||||
|
axis: HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유토곡선을 만들어 붙인다. 이미 붙어 있으면 **그 자리에서 갈아 끼운다** — 종단면도는
|
||||||
|
* 건드리지 않는다(2026-09-04). 가로로 스크롤할 때마다 곡선의 세로 창이 따라와야 하는데,
|
||||||
|
* 상단 패널을 통째로 다시 그리면 종단 그래프까지 새로 만들어져 화면이 한 번 끊긴다.
|
||||||
|
*/
|
||||||
|
export function mountMassHaulPanel(
|
||||||
|
input: MassHaulPanelInput,
|
||||||
|
targets: MassHaulMountTargets,
|
||||||
|
previous: MountedMassHaul,
|
||||||
|
): MountedMassHaul {
|
||||||
|
const built = buildMassHaulPanel(input);
|
||||||
|
let chart = previous.chart;
|
||||||
|
if (built.chart) {
|
||||||
|
if (chart?.isConnected) chart.replaceWith(built.chart);
|
||||||
|
else targets.chartWrap.append(built.chart);
|
||||||
|
chart = built.chart;
|
||||||
|
} else if (chart?.isConnected) {
|
||||||
|
chart.remove();
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let axis = previous.axis;
|
||||||
|
if (built.axis) {
|
||||||
|
// 고정 Y축은 0크기 sticky 앵커라 **첫 자식**이어야 세로 기준이 컨테이너 상단이 된다.
|
||||||
|
// 안쪽(inner)은 종단 높이만큼 내려 자기 그래프 구간만 덮는다.
|
||||||
|
const overlay = buildStickyYAxis(built.axis, input.massHeight);
|
||||||
|
(overlay.firstElementChild as HTMLElement).style.top = `${input.longHeight}px`;
|
||||||
|
if (axis?.isConnected) axis.replaceWith(overlay);
|
||||||
|
else targets.chartWrap.prepend(overlay);
|
||||||
|
axis = overlay;
|
||||||
|
} else if (axis?.isConnected) {
|
||||||
|
axis.remove();
|
||||||
|
axis = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.panel.querySelector(".b06-masshaul__legend")?.remove();
|
||||||
|
targets.panel.querySelector(".b06-masshaul__summary")?.remove();
|
||||||
|
targets.panelBody.append(...built.overlays);
|
||||||
|
targets.statusNode.textContent = built.statusText;
|
||||||
|
return { chart, axis };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「보이는 구간만 바꿔 다시 그리는」 함수를 만든다 — 스크롤 갱신이 부를 것이다.
|
||||||
|
* 붙여 둔 노드는 이 함수가 스스로 들고 있으므로 부르는 쪽은 구간만 넘기면 된다.
|
||||||
|
*/
|
||||||
|
export function createMassHaulRenderer(
|
||||||
|
base: Omit<MassHaulPanelInput, "viewRange">,
|
||||||
|
targets: MassHaulMountTargets,
|
||||||
|
): (fromM: number, toM: number) => void {
|
||||||
|
let mounted: MountedMassHaul = { chart: null, axis: null };
|
||||||
|
return (fromM, toM) => {
|
||||||
|
mounted = mountMassHaulPanel({ ...base, viewRange: { fromM, toM } }, targets, mounted);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
+18
-5
@@ -1,5 +1,5 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B05_Profile_UI_Profile_YWindow.ts
|
* common_util_chart_ywindow.ts
|
||||||
* 종단 그래프의 **세로 창을 변환으로 갈아 끼운다** — 다시 그리지 않는다(2026-09-04 확정).
|
* 종단 그래프의 **세로 창을 변환으로 갈아 끼운다** — 다시 그리지 않는다(2026-09-04 확정).
|
||||||
*
|
*
|
||||||
* 예전에는 스크롤을 멈출 때마다 그래프를 통째로 새로 만들었다(SVG 요소 457개, 실측
|
* 예전에는 스크롤을 멈출 때마다 그래프를 통째로 새로 만들었다(SVG 요소 457개, 실측
|
||||||
@@ -28,6 +28,9 @@ const TICK_KEEP_MIN = 0.8;
|
|||||||
const TICK_KEEP_MAX = 1.25;
|
const TICK_KEEP_MAX = 1.25;
|
||||||
/** 플롯 안에 남아야 하는 최소 눈금 수. */
|
/** 플롯 안에 남아야 하는 최소 눈금 수. */
|
||||||
const TICK_KEEP_MIN_COUNT = 4;
|
const TICK_KEEP_MIN_COUNT = 4;
|
||||||
|
/** 세로 창을 따라 움직이는 좌측 고정 축에 붙이는 표식 — 한 컨테이너에 축이 둘 이상일 때
|
||||||
|
* (B06: 종단 + 유토곡선) 남의 축까지 옮기지 않으려는 것이다. */
|
||||||
|
export const Y_AXIS_WINDOW_CLASS = "b05-profile__yaxis--ywindow";
|
||||||
/** 눈금 글자는 선보다 이만큼 아래에 앉는다(그리는 쪽과 같은 값). */
|
/** 눈금 글자는 선보다 이만큼 아래에 앉는다(그리는 쪽과 같은 값). */
|
||||||
const TICK_TEXT_DROP_PX = 4;
|
const TICK_TEXT_DROP_PX = 4;
|
||||||
|
|
||||||
@@ -38,6 +41,8 @@ interface Baked {
|
|||||||
zero: number;
|
zero: number;
|
||||||
top: number;
|
top: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
/** 세로 과장 — 새 창의 px 환산에 그대로 곱한다(B06 조절값, B05 는 1). */
|
||||||
|
exaggeration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElevationWindowResult {
|
export interface ElevationWindowResult {
|
||||||
@@ -60,6 +65,7 @@ function readBaked(root: ParentNode): Baked | null {
|
|||||||
zero: read(Y_WINDOW_ATTRS.zero),
|
zero: read(Y_WINDOW_ATTRS.zero),
|
||||||
top: read(Y_WINDOW_ATTRS.top),
|
top: read(Y_WINDOW_ATTRS.top),
|
||||||
height: read(Y_WINDOW_ATTRS.height),
|
height: read(Y_WINDOW_ATTRS.height),
|
||||||
|
exaggeration: read(Y_WINDOW_ATTRS.exaggeration) || 1,
|
||||||
};
|
};
|
||||||
const numbers = [baked.center, baked.pxPerM, baked.zero, baked.top, baked.height];
|
const numbers = [baked.center, baked.pxPerM, baked.zero, baked.top, baked.height];
|
||||||
if (!numbers.every(Number.isFinite) || baked.pxPerM <= 0 || baked.height <= 0) return null;
|
if (!numbers.every(Number.isFinite) || baked.pxPerM <= 0 || baked.height <= 0) return null;
|
||||||
@@ -81,13 +87,18 @@ function rebuildTicks(baked: Baked, root: ParentNode, center: number, span: numb
|
|||||||
const lineClass = sampleLine.getAttribute("class") ?? "";
|
const lineClass = sampleLine.getAttribute("class") ?? "";
|
||||||
const textX = sampleText.getAttribute("x") ?? "0";
|
const textX = sampleText.getAttribute("x") ?? "0";
|
||||||
const textClass = sampleText.getAttribute("class") ?? "";
|
const textClass = sampleText.getAttribute("class") ?? "";
|
||||||
const axis = root.querySelector<HTMLElement>(".b05-profile__yaxis-inner");
|
const axis = root.querySelector<HTMLElement>(`.${Y_AXIS_WINDOW_CLASS} .b05-profile__yaxis-inner`);
|
||||||
|
|
||||||
const ns = "http://www.w3.org/2000/svg";
|
const ns = "http://www.w3.org/2000/svg";
|
||||||
const lines: SVGLineElement[] = [];
|
const lines: SVGLineElement[] = [];
|
||||||
const texts: SVGTextElement[] = [];
|
const texts: SVGTextElement[] = [];
|
||||||
const spans: HTMLElement[] = [];
|
const spans: HTMLElement[] = [];
|
||||||
for (const { value, label } of yWindowTickValues(center, span, baked.height)) {
|
// 눈금값은 **과장을 뺀** 표고 폭 기준이다(그리는 쪽 `rawSpan` 과 같은 값).
|
||||||
|
for (const { value, label } of yWindowTickValues(
|
||||||
|
center,
|
||||||
|
span / baked.exaggeration,
|
||||||
|
baked.height,
|
||||||
|
)) {
|
||||||
const y = baked.zero - (value - baked.center) * baked.pxPerM;
|
const y = baked.zero - (value - baked.center) * baked.pxPerM;
|
||||||
const line = document.createElementNS(ns, "line");
|
const line = document.createElementNS(ns, "line");
|
||||||
line.setAttribute("x1", x1);
|
line.setAttribute("x1", x1);
|
||||||
@@ -133,7 +144,7 @@ export function applyElevationWindow(
|
|||||||
// 새 창 — 그리는 쪽과 **같은 규칙**으로 만든다(같은 상수를 같이 쓴다).
|
// 새 창 — 그리는 쪽과 **같은 규칙**으로 만든다(같은 상수를 같이 쓴다).
|
||||||
const span = Math.max(range.max - range.min, 1) * Y_WINDOW_SPAN_PAD;
|
const span = Math.max(range.max - range.min, 1) * Y_WINDOW_SPAN_PAD;
|
||||||
const center = (range.min + range.max) / 2;
|
const center = (range.min + range.max) / 2;
|
||||||
const pxPerM = baked.height / span;
|
const pxPerM = (baked.exaggeration * baked.height) / span;
|
||||||
const scale = pxPerM / baked.pxPerM;
|
const scale = pxPerM / baked.pxPerM;
|
||||||
|
|
||||||
// 배율이 크게 벌어지면 눈금 간격이 원래 의도와 어긋난다 — 눈금만 새로 만든다(요소 20개
|
// 배율이 크게 벌어지면 눈금 간격이 원래 의도와 어긋난다 — 눈금만 새로 만든다(요소 20개
|
||||||
@@ -161,7 +172,9 @@ export function applyElevationWindow(
|
|||||||
if (inside(moved)) visibleTicks += 1;
|
if (inside(moved)) visibleTicks += 1;
|
||||||
});
|
});
|
||||||
root
|
root
|
||||||
.querySelectorAll<HTMLElement>(`.b05-profile__yaxis-tick[${Y_WINDOW_BAKED_ATTR}]`)
|
.querySelectorAll<HTMLElement>(
|
||||||
|
`.${Y_AXIS_WINDOW_CLASS} .b05-profile__yaxis-tick[${Y_WINDOW_BAKED_ATTR}]`,
|
||||||
|
)
|
||||||
.forEach((tick) => {
|
.forEach((tick) => {
|
||||||
const moved = place(Number(tick.getAttribute(Y_WINDOW_BAKED_ATTR)));
|
const moved = place(Number(tick.getAttribute(Y_WINDOW_BAKED_ATTR)));
|
||||||
tick.style.top = `${moved}px`;
|
tick.style.top = `${moved}px`;
|
||||||
Reference in New Issue
Block a user