Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1

This commit is contained in:
2026-09-06 22:54:54 +09:00
3 changed files with 86 additions and 34 deletions
+20 -10
View File
@@ -63,8 +63,12 @@ import {
svgText,
validElevation,
} from "./B06_Section_UI_Section_Common";
import { crossPlotMetrics, effectiveCardHalfWidth } from "./B06_Section_UI_Cross_View_Metrics";
export { crossCardNaturalHeight } from "./B06_Section_UI_Cross_View_Metrics";
import {
type CrossPlotBase,
crossPlotFromBase,
crossPlotMetrics,
effectiveCardHalfWidth,
} from "./B06_Section_UI_Cross_View_Metrics";
/**
* 횡단 카드 요소. 선택 표시와 면적 강조를 **카드를 다시 만들지 않고** 갈아 끼우는 핸들을 단다
@@ -144,6 +148,8 @@ export function createCrossSectionCard(
/** 세월교 측벽·BOX암거 구체 조작 제어(2026-08-25). */
ford?: FordControl,
box?: BoxControl,
/** 행 높이를 재며 이미 만들어 둔 기하 — 있으면 다시 계산하지 않는다(2026-09-06). */
plotBase?: CrossPlotBase | null,
): CrossCardElement {
// 링크 카드 = 구조물이 옆 측점에 서 있고 그 연장이 여기까지 온 경우. 관만 숨기고
// 선택·조정은 연다 — 연동을 풀어 이 측점 위치를 따로 잡을 수 있어야 한다
@@ -274,14 +280,18 @@ export function createCrossSectionCard(
appendCardHeader(card, section, stationInterval, onDesignChange);
const metrics = crossPlotMetrics(
section,
verticalExaggeration,
widthPx,
effectiveHalfWidth,
designElevation,
forcedHeightPx,
);
// 행 높이를 재며 만든 기하가 있으면 그대로 쓴다 — 같은 측점의 기하를 두 번 계산하던
// 것이 B06 진입에서 `draw` 자기 시간의 대부분이었다(2026-09-06 CPU 프로파일).
const metrics = plotBase
? crossPlotFromBase(plotBase, forcedHeightPx)
: crossPlotMetrics(
section,
verticalExaggeration,
widthPx,
effectiveHalfWidth,
designElevation,
forcedHeightPx,
);
if (!metrics) {
card.append(emptyView(L("B06_Profile_View_NoCross")));
} else {
@@ -5,7 +5,7 @@ import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
const AREA_OVERLAY_HEADROOM_PX = 58;
interface CrossPlotMetrics {
export interface CrossPlotMetrics {
sourceSamples: SectionSample[];
minOffset: number;
maxOffset: number;
@@ -16,14 +16,28 @@ interface CrossPlotMetrics {
heightPx: number;
}
export function crossPlotMetrics(
/**
* 강제 높이를 얹기 **전**까지의 값 — 카드 두 벌(행 높이 재기 / 실제 그리기)이 나눠 쓴다.
* 예전에는 같은 측점의 기하를 두 번 계산했고, 그 값이 B06 진입에서 `draw` 자기 시간
* 213ms 의 대부분이었다(2026-09-06 CPU 프로파일, 측점 67곳 기준 3.2ms/장).
*/
export interface CrossPlotBase {
sourceSamples: SectionSample[];
minOffset: number;
maxOffset: number;
elevationMid: number;
pixelsPerMeter: number;
/** 강제 높이가 없을 때 쓰는 높이(바닥 `CROSS_HEIGHT` 적용 전). */
naturalHeight: number;
}
export function crossPlotBase(
section: CrossSection,
verticalExaggeration: number,
widthPx: number,
crossHalfWidth?: number,
designElevation?: number,
forcedHeightPx?: number,
): CrossPlotMetrics | null {
): CrossPlotBase | null {
const sourceSamples = section.samples.filter(
(sample) =>
crossHalfWidth === undefined || Math.abs(sample.offset_m ?? 0) <= crossHalfWidth + 1e-6,
@@ -48,32 +62,44 @@ export function crossPlotMetrics(
);
const naturalHeight =
rawSpan * pixelsPerMeter + CROSS_PAD.top + CROSS_PAD.bottom + AREA_OVERLAY_HEADROOM_PX;
const heightPx = forcedHeightPx ?? Math.max(naturalHeight, CROSS_HEIGHT);
const displaySpan = Math.max(heightPx - CROSS_PAD.top - CROSS_PAD.bottom, 1e-6) / pixelsPerMeter;
const displayMax = elevationMid + displaySpan / 2 + AREA_OVERLAY_HEADROOM_PX / pixelsPerMeter / 2;
return { sourceSamples, minOffset, maxOffset, elevationMid, pixelsPerMeter, naturalHeight };
}
/** 재 둔 기하에 행 높이만 얹는다 — 다시 계산하지 않는다. */
export function crossPlotFromBase(base: CrossPlotBase, forcedHeightPx?: number): CrossPlotMetrics {
const heightPx = forcedHeightPx ?? Math.max(base.naturalHeight, CROSS_HEIGHT);
const displaySpan =
Math.max(heightPx - CROSS_PAD.top - CROSS_PAD.bottom, 1e-6) / base.pixelsPerMeter;
const displayMax =
base.elevationMid + displaySpan / 2 + AREA_OVERLAY_HEADROOM_PX / base.pixelsPerMeter / 2;
return {
sourceSamples,
minOffset,
maxOffset,
elevationMid,
sourceSamples: base.sourceSamples,
minOffset: base.minOffset,
maxOffset: base.maxOffset,
elevationMid: base.elevationMid,
displaySpan,
displayMax,
pixelsPerMeter,
pixelsPerMeter: base.pixelsPerMeter,
heightPx,
};
}
export function crossCardNaturalHeight(
export function crossPlotMetrics(
section: CrossSection,
verticalExaggeration: number,
widthPx: number,
crossHalfWidth?: number,
designElevation?: number,
): number {
return (
crossPlotMetrics(section, verticalExaggeration, widthPx, crossHalfWidth, designElevation)
?.heightPx ?? CROSS_HEIGHT
forcedHeightPx?: number,
): CrossPlotMetrics | null {
const base = crossPlotBase(
section,
verticalExaggeration,
widthPx,
crossHalfWidth,
designElevation,
);
return base ? crossPlotFromBase(base, forcedHeightPx) : null;
}
/**
+23 -7
View File
@@ -35,10 +35,15 @@ import {
type RevetOffsetControl,
type RevetLinkControl,
type StructureSpanControl,
crossCardNaturalHeight,
type CrossCardElement,
type StationWidthControl,
} from "./B06_Section_UI_Cross_View";
import {
type CrossPlotBase,
crossPlotBase,
effectiveCardHalfWidth,
} from "./B06_Section_UI_Cross_View_Metrics";
import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
import { culvertLinkFor as 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";
@@ -405,7 +410,11 @@ export function createSectionView(
return planZ - (section.design?.surface_drop_m ?? 0);
};
const buildCrossCard = (section: CrossSection, forcedHeightPx?: number): HTMLElement =>
const buildCrossCard = (
section: CrossSection,
forcedHeightPx?: number,
plotBase?: CrossPlotBase | null,
): HTMLElement =>
createCrossSectionCard(
section,
section.station_id === selectedStationId,
@@ -429,6 +438,7 @@ export function createSectionView(
revetLink,
ford,
box,
plotBase,
);
/**
@@ -583,16 +593,22 @@ export function createSectionView(
if (detail.cross_sections.length) {
// 1차: 각 카드의 자연 높이(250px 바닥 적용) 측정 → 같은 행(columnCount 단위) 최댓값을 행 높이로.
const sections = detail.cross_sections;
const naturalHeights = sections.map((section) =>
crossCardNaturalHeight(
// 여기서 만든 기하를 2차에서 **그대로 넘긴다** — 예전에는 같은 측점을 두 번 계산했고
// 그것이 진입에서 `draw` 자기 시간의 대부분이었다(2026-09-06 CPU 프로파일).
// 반폭도 카드가 실제로 쓰는 값(`effectiveCardHalfWidth`, 자동 줌아웃 포함)으로 맞춰
// 재 둔다 — 카드·행 높이가 어긋나지 않는다.
const plotBases = sections.map((section) =>
crossPlotBase(
section,
currentExaggeration,
cachedCardWidth,
// 개별 표시 반폭이 있으면 그 폭 기준으로 높이를 재야 카드·행 높이가 맞는다.
stationWidth?.widthFor(section) ?? currentCrossHalfWidth,
effectiveCardHalfWidth(section, stationWidth?.widthFor(section) ?? currentCrossHalfWidth),
designElevationAt(detail.longitudinal.design_profiles, section.chainage_m),
),
);
const naturalHeights = plotBases.map((base) =>
Math.max(base?.naturalHeight ?? CROSS_HEIGHT, CROSS_HEIGHT),
);
// 2차: 행별 최댓값으로 강제 높이를 정해 같은 행 카드를 동일 높이로 렌더한다.
for (let start = 0; start < sections.length; start += columnCount) {
const rowHeight = Math.max(...naturalHeights.slice(start, start + columnCount));
@@ -602,7 +618,7 @@ export function createSectionView(
index += 1
) {
cachedRowHeight.set(sections[index].station_id, rowHeight);
grid.append(buildCrossCard(sections[index], rowHeight));
grid.append(buildCrossCard(sections[index], rowHeight, plotBases[index]));
}
}
} else {