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