Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -17,8 +17,6 @@ import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
|||||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||||
// 가로 스크롤 고정 Y축 — B05와 같은 오버레이를 쓴다(정의처: B05 MassHaul 모듈 + 그 CSS).
|
|
||||||
import { buildStickyYAxis } from "../B05_Profile/B05_Profile_UI_Profile_MassHaul";
|
|
||||||
import type {
|
import type {
|
||||||
CrossSection,
|
CrossSection,
|
||||||
EarthworkConversion,
|
EarthworkConversion,
|
||||||
@@ -45,38 +43,19 @@ import {
|
|||||||
effectiveCardHalfWidth,
|
effectiveCardHalfWidth,
|
||||||
} from "./B06_Section_UI_Cross_View_Metrics";
|
} from "./B06_Section_UI_Cross_View_Metrics";
|
||||||
import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||||
import { LONG_PAD } from "./B06_Section_UI_Section_Common";
|
import { STRUCTURE_LANE_HEIGHT_PX } from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
||||||
|
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
||||||
|
import { attachWindowScroll, drawLongitudinalPanel } from "./B06_Section_UI_Section_View_Draw";
|
||||||
import {
|
import {
|
||||||
buildStructureLane,
|
|
||||||
STRUCTURE_LANE_HEIGHT_PX,
|
|
||||||
} from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
|
||||||
import {
|
|
||||||
mountSectionStructureMenu,
|
|
||||||
moveMarkById,
|
|
||||||
type SectionStructureEdit,
|
|
||||||
} from "./B06_Section_UI_Section_View_Menu";
|
|
||||||
import {
|
|
||||||
structureAnchorM,
|
|
||||||
type StructureInstance,
|
type StructureInstance,
|
||||||
type StructureType,
|
type StructureType,
|
||||||
} from "../B05_Profile/B05_Profile_Api_Structures";
|
} from "../B05_Profile/B05_Profile_Api_Structures";
|
||||||
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 { 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 { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
||||||
import { computeMassHaulSeries } from "@util/common_util_mass_haul";
|
import { createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
||||||
import { badgeValuesFrom, createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
import { type ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
||||||
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,
|
||||||
@@ -512,119 +491,24 @@ export function createSectionView(
|
|||||||
// 생겨 패널이 16,000px 까지 부푼다(2026-09-07 실측). 빼 두면 한 번에 수렴한다.
|
// 생겨 패널이 16,000px 까지 부푼다(2026-09-07 실측). 빼 두면 한 번에 수렴한다.
|
||||||
const laneHeight = markStructures.length && markTypes.length ? STRUCTURE_LANE_HEIGHT_PX : 0;
|
const laneHeight = markStructures.length && markTypes.length ? STRUCTURE_LANE_HEIGHT_PX : 0;
|
||||||
const heights = chartHeights(Math.max(lastChartAvailable - laneHeight, MIN_LONG_HEIGHT));
|
const heights = chartHeights(Math.max(lastChartAvailable - laneHeight, MIN_LONG_HEIGHT));
|
||||||
const moveStationMark = (markId: string, toChainageM: number): void => {
|
|
||||||
if (!structureEdit) return;
|
|
||||||
moveMarkById(markId, toChainageM, {
|
|
||||||
structures: markStructures,
|
|
||||||
stations: detail.longitudinal.stations ?? [],
|
|
||||||
edit: structureEdit,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const minWidth = longitudinalMinimumWidth(detail.longitudinal, cachedStationInterval);
|
const minWidth = longitudinalMinimumWidth(detail.longitudinal, cachedStationInterval);
|
||||||
const chartWidth = Math.max(renderWidth, minWidth);
|
updateChartWindow = drawLongitudinalPanel({
|
||||||
const chart = buildLongitudinalChart({
|
|
||||||
detail,
|
detail,
|
||||||
|
chartWrap,
|
||||||
|
renderWidth: Math.max(renderWidth, minWidth),
|
||||||
|
chartHeight: heights.long,
|
||||||
|
keepScrollLeft,
|
||||||
selectedStationId,
|
selectedStationId,
|
||||||
exaggeration: currentExaggeration,
|
exaggeration: currentExaggeration,
|
||||||
selectStation: (stationId) => selectStation(stationId, true),
|
|
||||||
stationInterval: cachedStationInterval,
|
stationInterval: cachedStationInterval,
|
||||||
chartWidth,
|
structures: markStructures,
|
||||||
chartHeight: heights.long,
|
types: markTypes,
|
||||||
minWidth,
|
edit: structureEdit,
|
||||||
scrollLeft: keepScrollLeft,
|
conversion: currentConversion,
|
||||||
viewportWidth: chartWrap.clientWidth || chartWidth,
|
naturalSpoilSlope: currentNaturalSpoilSlope,
|
||||||
// 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동.
|
selectStation: (stationId) => selectStation(stationId, true),
|
||||||
onDragStation: structureEdit
|
setMassBadge: (values) => massBadge.set(values),
|
||||||
? (stationId, toChainageM) => moveStationMark(stationId, toChainageM)
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
const longAxis = chart.axis;
|
|
||||||
const nodes: Element[] = [chart.node];
|
|
||||||
// 구조물 알약 레인 — B05 종단과 **같은 부품**(`buildStructureLane`)을 그대로 쓴다.
|
|
||||||
// 배수관·세월교도 여기 같은 알약으로 선다(2026-09-07 사용자 지시 4 표시 통일).
|
|
||||||
if (markStructures.length && markTypes.length) {
|
|
||||||
nodes.push(
|
|
||||||
buildStructureLane({
|
|
||||||
structures: markStructures,
|
|
||||||
types: markTypes,
|
|
||||||
x: chart.toX,
|
|
||||||
chainageAt: chart.toChainage,
|
|
||||||
maxChainageM: chart.maxChainageM,
|
|
||||||
widthPx: chartWidth,
|
|
||||||
axisWidthPx: LONG_PAD.left,
|
|
||||||
stationIntervalM: cachedStationInterval,
|
|
||||||
selectedId: null,
|
|
||||||
// B06 에서 알약을 누르면 그 측점 카드를 고른다 — 목록 클릭과 같은 규칙.
|
|
||||||
onSelect: (structureId) => {
|
|
||||||
if (!structureId) return;
|
|
||||||
const hit = markStructures.find((entry) => entry.structure_id === structureId);
|
|
||||||
if (!hit) return;
|
|
||||||
const at = structureAnchorM(hit);
|
|
||||||
// 그 자리에 가장 가까운 측점 카드를 고른다 — 좌측 목록 클릭과 같은 규칙.
|
|
||||||
let best: { id: string; gap: number } | null = null;
|
|
||||||
for (const section of currentDetail?.cross_sections ?? []) {
|
|
||||||
const gap = Math.abs(section.chainage_m - at);
|
|
||||||
if (!best || gap < best.gap) best = { id: section.station_id, gap };
|
|
||||||
}
|
|
||||||
if (best) selectStation(best.id, true);
|
|
||||||
},
|
|
||||||
// 알약 끌기도 B05 와 같게 받는다(2026-09-12 일원화) — 정본은 [저장]·[확정]에서.
|
|
||||||
onMove: (structureId, toChainageM) => moveStationMark(structureId, toChainageM),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
chartWrap.replaceChildren(...nodes);
|
|
||||||
// 종단 그래프 우클릭 — B05 와 같은 메뉴다(가까운 구조물이 있으면 삭제, 없으면
|
|
||||||
// 구조물군 → 종류 2단 추가). 그래프를 다시 그릴 때마다 붙인다(상태를 안 남긴다).
|
|
||||||
if (structureEdit) {
|
|
||||||
mountSectionStructureMenu(chartWrap, {
|
|
||||||
structures: markStructures,
|
|
||||||
types: markTypes,
|
|
||||||
x: chart.toX,
|
|
||||||
chainageAt: chart.toChainage,
|
|
||||||
maxChainageM: chart.maxChainageM,
|
|
||||||
edit: structureEdit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 유토곡선 그래프는 B06 에서 **그리지 않는다**(2026-09-06 사용자 지시) — 자리를 많이
|
|
||||||
// 먹는데 정작 필요한 값은 마지막 지점 누가토량 하나다. 곡선은 B05 유토곡선 패널에서
|
|
||||||
// 펼쳐 본다. 여기서는 같은 계산으로 값만 내 좌측 상단 배지에 올린다(기준: 횡단).
|
|
||||||
if (currentConversion) {
|
|
||||||
const series = computeMassHaulSeries(
|
|
||||||
detail.longitudinal,
|
|
||||||
detail.cross_sections,
|
|
||||||
currentConversion,
|
|
||||||
currentNaturalSpoilSlope,
|
|
||||||
);
|
|
||||||
const cross = series.find((entry) => entry.basis === "cross") ?? series[0];
|
|
||||||
massBadge.set(cross ? badgeValuesFrom(cross.result) : null);
|
|
||||||
} else {
|
|
||||||
massBadge.set(null);
|
|
||||||
}
|
|
||||||
// 종단 고정 Y축 — 0크기 sticky 앵커라 **첫 자식**으로 넣어야 세로 기준이 컨테이너
|
|
||||||
// 상단이 된다(SVG 뒤에 넣으면 앵커가 차트 아래로 밀린다).
|
|
||||||
if (longAxis) {
|
|
||||||
const overlay = buildStickyYAxis(longAxis, heights.long);
|
|
||||||
// 세로 창을 따라 움직이는 축임을 표시한다(유토곡선 축과 구분).
|
|
||||||
overlay.classList.add(Y_AXIS_WINDOW_CLASS);
|
|
||||||
chartWrap.prepend(overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 세로 창 갱신기 — 스크롤마다 종단은 변환으로, 유토곡선은 곡선만 다시 그려 따라온다.
|
|
||||||
updateChartWindow = () => {
|
|
||||||
const { fromM, toM } = visibleChainageRange(
|
|
||||||
chart.toChainage,
|
|
||||||
chart.maxChainageM,
|
|
||||||
chartWrap.scrollLeft,
|
|
||||||
chartWrap.clientWidth || chartWidth,
|
|
||||||
);
|
|
||||||
return applyElevationWindow(
|
|
||||||
chartWrap,
|
|
||||||
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
chartWrap.scrollLeft = keepScrollLeft;
|
|
||||||
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
||||||
syncScrollMargin();
|
syncScrollMargin();
|
||||||
|
|
||||||
@@ -641,22 +525,11 @@ export function createSectionView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 상단 패널을 통째로 다시 그리기까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
attachWindowScroll(
|
||||||
const SCROLL_SETTLE_MS = 80;
|
chartWrap,
|
||||||
let settledScrollLeft = 0;
|
() => updateChartWindow,
|
||||||
let scrollSettleTimer = 0;
|
() => drawPanel(),
|
||||||
// 세로 맞춤은 **그리기가 아니라 변환**이다(2026-09-04 사용자 확정, B05와 같은 규칙) —
|
);
|
||||||
// 스크롤마다 겹 하나의 변환만 갈아 끼우므로 실시간으로 따라온다.
|
|
||||||
chartWrap.addEventListener("scroll", () => {
|
|
||||||
const fitted = updateChartWindow?.() ?? null;
|
|
||||||
window.clearTimeout(scrollSettleTimer);
|
|
||||||
if (!needsFullRedraw(fitted)) return;
|
|
||||||
scrollSettleTimer = window.setTimeout(() => {
|
|
||||||
if (Math.abs(chartWrap.scrollLeft - settledScrollLeft) < 1) return;
|
|
||||||
settledScrollLeft = chartWrap.scrollLeft;
|
|
||||||
drawPanel();
|
|
||||||
}, SCROLL_SETTLE_MS);
|
|
||||||
});
|
|
||||||
|
|
||||||
const draw = (): void => {
|
const draw = (): void => {
|
||||||
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* B06_Section_UI_Section_View_Draw.ts
|
||||||
|
* B06 상단 패널의 **종단 그래프 한 벌**을 세운다 — 그래프·구조물 알약 레인·우클릭 메뉴·
|
||||||
|
* 누가토량 배지·고정 Y축, 그리고 가로 스크롤에 따라붙는 세로 창 갱신기까지.
|
||||||
|
*
|
||||||
|
* 왜 갈랐나 — 종단 화면 본체(`_Section_View`)가 700줄을 넘겨(CLAUDE.md 4장) 기능별로
|
||||||
|
* 나눠야 했다. 이 파일은 **그리는 몫만** 맡고, 높이 재기·되먹임 보정·선택 상태는 본체가
|
||||||
|
* 그대로 쥔다. 넘겨받는 값은 전부 인자라 여기에는 화면 상태가 남지 않는다.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import type { EarthworkConversion, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||||
|
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||||
|
import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||||
|
import { buildStructureLane } from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
||||||
|
import { buildStickyYAxis } from "../B05_Profile/B05_Profile_UI_Profile_MassHaul";
|
||||||
|
import { computeMassHaulSeries } from "@util/common_util_mass_haul";
|
||||||
|
import { badgeValuesFrom } from "@util/common_util_mass_haul_badge";
|
||||||
|
import {
|
||||||
|
applyElevationWindow,
|
||||||
|
needsFullRedraw,
|
||||||
|
Y_AXIS_WINDOW_CLASS,
|
||||||
|
type ElevationWindowResult,
|
||||||
|
} from "@util/common_util_chart_ywindow";
|
||||||
|
import { LONG_PAD } from "./B06_Section_UI_Section_Common";
|
||||||
|
import {
|
||||||
|
buildLongitudinalChart,
|
||||||
|
visibleChainageRange,
|
||||||
|
visibleElevationRange,
|
||||||
|
} from "./B06_Section_UI_Section_View_Chart";
|
||||||
|
import {
|
||||||
|
mountSectionStructureMenu,
|
||||||
|
moveMarkById,
|
||||||
|
type SectionStructureEdit,
|
||||||
|
} from "./B06_Section_UI_Section_View_Menu";
|
||||||
|
|
||||||
|
/** 그래프 한 벌을 세우는 데 필요한 값 — 본체가 재고 고른 것을 그대로 넘긴다. */
|
||||||
|
export interface LongitudinalPanelInput {
|
||||||
|
detail: SectionDetailResponse;
|
||||||
|
/** 그래프가 들어앉는 칸. 내용은 이 함수가 통째로 갈아 끼운다. */
|
||||||
|
chartWrap: HTMLElement;
|
||||||
|
/** 본체가 잰 그릴 폭·높이. */
|
||||||
|
renderWidth: number;
|
||||||
|
chartHeight: number;
|
||||||
|
/** 갈아 끼우기 전 가로 스크롤 자리 — 되돌려 놓는다. */
|
||||||
|
keepScrollLeft: number;
|
||||||
|
selectedStationId: string | null;
|
||||||
|
exaggeration: number;
|
||||||
|
stationInterval: number;
|
||||||
|
structures: ReadonlyArray<StructureInstance>;
|
||||||
|
types: ReadonlyArray<StructureType>;
|
||||||
|
/** 구조물을 넣고 빼고 옮기는 길(없으면 우클릭·끌기를 안 붙인다). */
|
||||||
|
edit: SectionStructureEdit | null;
|
||||||
|
conversion?: EarthworkConversion;
|
||||||
|
naturalSpoilSlope?: number;
|
||||||
|
selectStation: (stationId: string) => void;
|
||||||
|
/** 좌측 상단 누가토량 배지 — 값이 없으면 null 로 지운다. */
|
||||||
|
setMassBadge: (values: ReturnType<typeof badgeValuesFrom> | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 알약 레인이 설 높이(px) — 본체가 그릴 높이를 셈할 때 같은 값을 써야 한다. */
|
||||||
|
export { STRUCTURE_LANE_HEIGHT_PX } from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 종단 그래프 한 벌을 세우고 **세로 창 갱신기**를 돌려준다.
|
||||||
|
* 갱신기는 가로 스크롤마다 불러 보이는 구간에 세로를 맞추는 데 쓴다.
|
||||||
|
*/
|
||||||
|
export function drawLongitudinalPanel(
|
||||||
|
input: LongitudinalPanelInput,
|
||||||
|
): () => ElevationWindowResult | null {
|
||||||
|
const { detail, chartWrap, structures, types, edit } = input;
|
||||||
|
const chartWidth = input.renderWidth;
|
||||||
|
|
||||||
|
/** 측점선·알약을 끌어 놓았을 때 — 관인지 구조물인지는 공용 판별이 가른다. */
|
||||||
|
const moveMark = (markId: string, toChainageM: number): void => {
|
||||||
|
if (!edit) return;
|
||||||
|
moveMarkById(markId, toChainageM, {
|
||||||
|
structures,
|
||||||
|
stations: detail.longitudinal.stations ?? [],
|
||||||
|
edit,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const chart = buildLongitudinalChart({
|
||||||
|
detail,
|
||||||
|
selectedStationId: input.selectedStationId,
|
||||||
|
exaggeration: input.exaggeration,
|
||||||
|
selectStation: input.selectStation,
|
||||||
|
stationInterval: input.stationInterval,
|
||||||
|
chartWidth,
|
||||||
|
chartHeight: input.chartHeight,
|
||||||
|
minWidth: chartWidth,
|
||||||
|
scrollLeft: input.keepScrollLeft,
|
||||||
|
viewportWidth: chartWrap.clientWidth || chartWidth,
|
||||||
|
// 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동.
|
||||||
|
onDragStation: edit ? moveMark : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes: Element[] = [chart.node];
|
||||||
|
// 구조물 알약 레인 — B05 종단과 **같은 부품**을 그대로 쓴다. 배수관·세월교도 같은
|
||||||
|
// 알약으로 선다(2026-09-07 사용자 지시 4 표시 통일).
|
||||||
|
if (structures.length && types.length) {
|
||||||
|
nodes.push(
|
||||||
|
buildStructureLane({
|
||||||
|
structures,
|
||||||
|
types,
|
||||||
|
x: chart.toX,
|
||||||
|
chainageAt: chart.toChainage,
|
||||||
|
maxChainageM: chart.maxChainageM,
|
||||||
|
widthPx: chartWidth,
|
||||||
|
axisWidthPx: LONG_PAD.left,
|
||||||
|
stationIntervalM: input.stationInterval,
|
||||||
|
selectedId: null,
|
||||||
|
// 알약을 누르면 그 자리에 가장 가까운 측점 카드를 고른다 — 목록 클릭과 같은 규칙.
|
||||||
|
onSelect: (structureId) => {
|
||||||
|
if (!structureId) return;
|
||||||
|
const hit = structures.find((entry) => entry.structure_id === structureId);
|
||||||
|
if (!hit) return;
|
||||||
|
const at = structureAnchorM(hit);
|
||||||
|
let best: { id: string; gap: number } | null = null;
|
||||||
|
for (const section of detail.cross_sections) {
|
||||||
|
const gap = Math.abs(section.chainage_m - at);
|
||||||
|
if (!best || gap < best.gap) best = { id: section.station_id, gap };
|
||||||
|
}
|
||||||
|
if (best) input.selectStation(best.id);
|
||||||
|
},
|
||||||
|
// 알약 끌기도 B05 와 같게 받는다(2026-09-12 일원화) — 정본은 [저장]·[확정]에서.
|
||||||
|
onMove: moveMark,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
chartWrap.replaceChildren(...nodes);
|
||||||
|
// 종단 그래프 우클릭 — B05 와 같은 메뉴다(가까운 구조물이 있으면 삭제, 없으면 구조물군
|
||||||
|
// → 종류 2단 추가). 그래프를 다시 그릴 때마다 붙인다(상태를 안 남긴다).
|
||||||
|
if (edit) {
|
||||||
|
mountSectionStructureMenu(chartWrap, {
|
||||||
|
structures,
|
||||||
|
types,
|
||||||
|
x: chart.toX,
|
||||||
|
chainageAt: chart.toChainage,
|
||||||
|
maxChainageM: chart.maxChainageM,
|
||||||
|
edit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 유토곡선 그래프는 B06 에서 **그리지 않는다**(2026-09-06 사용자 지시) — 자리를 많이
|
||||||
|
// 먹는데 정작 필요한 값은 마지막 지점 누가토량 하나다. 곡선은 B05 유토곡선 패널에서
|
||||||
|
// 펼쳐 본다. 여기서는 같은 계산으로 값만 내 좌측 상단 배지에 올린다(기준: 횡단).
|
||||||
|
if (input.conversion) {
|
||||||
|
const series = computeMassHaulSeries(
|
||||||
|
detail.longitudinal,
|
||||||
|
detail.cross_sections,
|
||||||
|
input.conversion,
|
||||||
|
input.naturalSpoilSlope,
|
||||||
|
);
|
||||||
|
const cross = series.find((entry) => entry.basis === "cross") ?? series[0];
|
||||||
|
input.setMassBadge(cross ? badgeValuesFrom(cross.result) : null);
|
||||||
|
} else {
|
||||||
|
input.setMassBadge(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 종단 고정 Y축 — 0크기 sticky 앵커라 **첫 자식**으로 넣어야 세로 기준이 컨테이너
|
||||||
|
// 상단이 된다(SVG 뒤에 넣으면 앵커가 차트 아래로 밀린다).
|
||||||
|
if (chart.axis) {
|
||||||
|
const overlay = buildStickyYAxis(chart.axis, input.chartHeight);
|
||||||
|
// 세로 창을 따라 움직이는 축임을 표시한다(유토곡선 축과 구분).
|
||||||
|
overlay.classList.add(Y_AXIS_WINDOW_CLASS);
|
||||||
|
chartWrap.prepend(overlay);
|
||||||
|
}
|
||||||
|
chartWrap.scrollLeft = input.keepScrollLeft;
|
||||||
|
|
||||||
|
// 세로 창 갱신기 — 스크롤마다 종단을 변환으로 따라오게 한다(다시 그리지 않는다).
|
||||||
|
return () => {
|
||||||
|
const { fromM, toM } = visibleChainageRange(
|
||||||
|
chart.toChainage,
|
||||||
|
chart.maxChainageM,
|
||||||
|
chartWrap.scrollLeft,
|
||||||
|
chartWrap.clientWidth || chartWidth,
|
||||||
|
);
|
||||||
|
return applyElevationWindow(chartWrap, visibleElevationRange(detail, fromM, toM) ?? undefined);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 패널을 통째로 다시 그리기까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
||||||
|
const SCROLL_SETTLE_MS = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가로 스크롤에 세로 창을 따라붙인다 — **한 번만** 부른다(리스너를 남긴다).
|
||||||
|
*
|
||||||
|
* 세로 맞춤은 그리기가 아니라 **변환**이다(2026-09-04 사용자 확정, B05 와 같은 규칙).
|
||||||
|
* 변환으로 못 살리는 자리에서만 스크롤이 멎은 뒤 한 번 다시 그린다.
|
||||||
|
*/
|
||||||
|
export function attachWindowScroll(
|
||||||
|
chartWrap: HTMLElement,
|
||||||
|
updater: () => (() => ElevationWindowResult | null) | null,
|
||||||
|
redraw: () => void,
|
||||||
|
): void {
|
||||||
|
let settledScrollLeft = 0;
|
||||||
|
let timer = 0;
|
||||||
|
chartWrap.addEventListener("scroll", () => {
|
||||||
|
const fitted = updater()?.() ?? null;
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
if (!needsFullRedraw(fitted)) return;
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
if (Math.abs(chartWrap.scrollLeft - settledScrollLeft) < 1) return;
|
||||||
|
settledScrollLeft = chartWrap.scrollLeft;
|
||||||
|
redraw();
|
||||||
|
}, SCROLL_SETTLE_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user