From 4df5d0a7eaea7dfce161494ad486e950141f7097 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 6 Sep 2026 22:54:12 +0900 Subject: [PATCH] =?UTF-8?q?perf(B06):=20=EC=B9=B4=EB=93=9C=20=EA=B8=B0?= =?UTF-8?q?=ED=95=98=EB=A5=BC=20=EB=91=90=20=EB=B2=88=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EA=B2=83=EC=9D=84=20=ED=95=9C=20=EB=B2=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 행 높이를 재는 1차와 실제 그리는 2차가 같은 측점의 crossPlotMetrics 를 각각 계산했음. 측점 67곳 기준 3.2ms/장이라 진입에서 draw 자기 시간 213ms 의 대부분이었음 (보조 창 CPU 프로파일). - crossPlotMetrics 를 crossPlotBase(강제 높이 전) + crossPlotFromBase(높이만 얹기)로 가름. 기존 시그니처는 둘을 잇는 감싸개로 유지. - Section_View 1차에서 base 를 만들어 2차 카드에 그대로 넘김. - 1차 반폭도 카드가 실제로 쓰는 값(effectiveCardHalfWidth, 자동 줌아웃 포함)으로 맞춤 — 전에는 1차가 원래 반폭으로 재 카드·행 높이 기준이 어긋났음. - 쓰이지 않게 된 crossCardNaturalHeight 와 그 재수출 삭제. 자체검증(공용 브라우저) — 카드 67장·SVG 67개 그대로, 같은 행 카드 높이 통일 유지 (rowsUniform true), 높이 범위 292~1,119px. 시험 400 통과·17 건너뜀, typecheck 통과. 효과 수치는 보조 창의 격리된 프로젝트에서 3회 중앙값으로 재기로 함(내 창은 편차가 큼). Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Cross_View.ts | 30 ++++++---- .../B06_Section_UI_Cross_View_Metrics.ts | 60 +++++++++++++------ B06_Section/B06_Section_UI_Section_View.ts | 30 +++++++--- 3 files changed, 86 insertions(+), 34 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index 7000aeac..f2ac7b13 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -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 { diff --git a/B06_Section/B06_Section_UI_Cross_View_Metrics.ts b/B06_Section/B06_Section_UI_Cross_View_Metrics.ts index 29721ee5..03c8a8a5 100644 --- a/B06_Section/B06_Section_UI_Cross_View_Metrics.ts +++ b/B06_Section/B06_Section_UI_Cross_View_Metrics.ts @@ -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; } /** diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index 1c2c1bf1..cadbcb1b 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -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 {