Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Section_Columns.ts
|
||||
* 횡단 카드 한 줄 카드 수 고르기 — 보기 칸 높이에 첫 줄 + 둘째 줄 절반이 들어가는
|
||||
* 가장 적은 카드 수. 카드 폭은 최소~최대 폭 안(그림은 축척 그대로 · 폭만 바뀜).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
CROSS_CARD_CHROME,
|
||||
CROSS_GRID_GAP,
|
||||
CROSS_GRID_MAX_WIDTH,
|
||||
CROSS_GRID_MIN_WIDTH,
|
||||
} from "./B06_Section_UI_Section_Common";
|
||||
|
||||
export interface CrossColumns {
|
||||
columns: number;
|
||||
cardWidth: number;
|
||||
}
|
||||
|
||||
const widthFor = (columns: number, viewWidth: number): number =>
|
||||
Math.min(CROSS_GRID_MAX_WIDTH, (viewWidth - (columns - 1) * CROSS_GRID_GAP) / columns);
|
||||
|
||||
/**
|
||||
* `heightsFor(cardWidth, count)` — 그 폭일 때 앞 `count` 장의 카드 자연 높이.
|
||||
* `viewHeight` 가 0 이하(숨김 · 아직 안 잰 칸)면 가장 큰 카드 쪽을 고른다.
|
||||
*/
|
||||
export function pickCrossColumns(
|
||||
viewWidth: number,
|
||||
viewHeight: number,
|
||||
heightsFor: (cardWidth: number, count: number) => number[],
|
||||
): CrossColumns {
|
||||
const unit = CROSS_GRID_MIN_WIDTH + CROSS_GRID_GAP;
|
||||
const most = Math.max(1, Math.floor((viewWidth + CROSS_GRID_GAP) / unit));
|
||||
const fewest = Math.min(
|
||||
most,
|
||||
Math.max(1, Math.ceil((viewWidth + CROSS_GRID_GAP) / (CROSS_GRID_MAX_WIDTH + CROSS_GRID_GAP))),
|
||||
);
|
||||
for (let columns = fewest; columns < most; columns += 1) {
|
||||
const cardWidth = widthFor(columns, viewWidth);
|
||||
if (viewHeight <= 0) return { columns, cardWidth };
|
||||
const heights = heightsFor(cardWidth, columns * 2);
|
||||
const first = Math.max(0, ...heights.slice(0, columns)) + CROSS_CARD_CHROME;
|
||||
const second = Math.max(0, ...heights.slice(columns)) + CROSS_CARD_CHROME;
|
||||
const needed = heights.length > columns ? first + CROSS_GRID_GAP + second / 2 : first;
|
||||
if (needed <= viewHeight) return { columns, cardWidth };
|
||||
}
|
||||
return { columns: most, cardWidth: widthFor(most, viewWidth) };
|
||||
}
|
||||
@@ -35,8 +35,11 @@ export const LONG_WIDTH = 1200;
|
||||
export const LONG_HEIGHT = 220;
|
||||
export const CROSS_WIDTH = 560;
|
||||
export const CROSS_HEIGHT = 250;
|
||||
// 한 행 맞춤 기준 최소 카드 폭 — 조금 더 넓은 화면 필요(480→560, 약 +17%).
|
||||
export const CROSS_GRID_MIN_WIDTH = 560;
|
||||
// 카드 폭 범위 — 한 줄 카드 수는 보기 칸 높이로 정한다(`_Section_Columns`).
|
||||
export const CROSS_GRID_MIN_WIDTH = 400;
|
||||
export const CROSS_GRID_MAX_WIDTH = 800;
|
||||
// 카드에서 그림 높이 밖 몫 — 머리 · 설계 줄 · 꼬리(실측 135).
|
||||
export const CROSS_CARD_CHROME = 135;
|
||||
export const CROSS_GRID_GAP = 16;
|
||||
// bottom은 측점 라벨 한 줄 몫이다. X축 제목을 그래프 상단으로 올린 뒤 남던 여백을 걷어내,
|
||||
// 종단면도 아래 라벨이 그 밑 유토곡선과의 틈 **가운데**에 오도록 좁혔다(2026-08-02 사용자 지시).
|
||||
|
||||
@@ -42,9 +42,8 @@ import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
||||
import { culvertLinkFor as culvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { pickCrossColumns } from "./B06_Section_UI_Section_Columns";
|
||||
import {
|
||||
CROSS_GRID_GAP,
|
||||
CROSS_GRID_MIN_WIDTH,
|
||||
CROSS_WIDTH,
|
||||
type CrossDesignChange,
|
||||
type DesignChangeHandler,
|
||||
@@ -93,6 +92,8 @@ export function createSectionView(
|
||||
let currentStationInterval: number | undefined;
|
||||
let structureEdit: SectionStructureEdit | null = null;
|
||||
let renderWidth = 0;
|
||||
let renderHeight = 0;
|
||||
let drawnColumns = 0;
|
||||
let resizeTimer = 0;
|
||||
// 카드 단위 재빌드에 재사용하는 렌더 컨텍스트 (draw에서 갱신)
|
||||
let cachedStationInterval = 1;
|
||||
@@ -136,6 +137,33 @@ export function createSectionView(
|
||||
return Number.isFinite(width) ? Math.max(0, width) : 0;
|
||||
};
|
||||
|
||||
const contentHeight = (): number => {
|
||||
const style = getComputedStyle(cards);
|
||||
const pad = (value: string): number => Number.parseFloat(value) || 0;
|
||||
const height = cards.clientHeight - pad(style.paddingTop) - pad(style.paddingBottom);
|
||||
return Number.isFinite(height) ? Math.max(0, height) : 0;
|
||||
};
|
||||
|
||||
/** 카드 폭에서 앞 `count` 장의 기하 — 한 줄 카드 수 고르기 · 그리기가 함께 쓴다. */
|
||||
const plotBasesFor = (detail: SectionDetailResponse, cardWidth: number, count: number) =>
|
||||
detail.cross_sections
|
||||
.slice(0, count)
|
||||
.map((section) =>
|
||||
crossPlotBase(
|
||||
section,
|
||||
currentExaggeration,
|
||||
cardWidth,
|
||||
effectiveCardHalfWidth(section, stationWidth?.widthFor(section) ?? currentCrossHalfWidth),
|
||||
designElevationAt(detail.longitudinal.design_profiles, section.chainage_m),
|
||||
),
|
||||
);
|
||||
const naturalHeightsOf = (bases: ReadonlyArray<CrossPlotBase | null | undefined>): number[] =>
|
||||
bases.map((base) => Math.max(base?.naturalHeight ?? CROSS_HEIGHT, CROSS_HEIGHT));
|
||||
const pickColumns = (detail: SectionDetailResponse) =>
|
||||
pickCrossColumns(renderWidth, renderHeight, (cardWidth, count) =>
|
||||
naturalHeightsOf(plotBasesFor(detail, cardWidth, count)),
|
||||
);
|
||||
|
||||
/**
|
||||
* 선택 카드를 위 칸 안에서 보이게 한다 — `start`로 카드 **상단**(측점 라벨)을 칸 위에
|
||||
* 붙인다(2026-08-04 사용자 지적). 목록 맨 끝 카드는 스크롤이 바닥에 걸려 그 자리까지 못
|
||||
@@ -316,14 +344,14 @@ export function createSectionView(
|
||||
grid.addEventListener("click", (event) => {
|
||||
if (!(event.target as HTMLElement).closest(".b06-cross-card")) clearSelection();
|
||||
});
|
||||
const columnCount = Math.max(
|
||||
1,
|
||||
Math.floor((renderWidth + CROSS_GRID_GAP) / (CROSS_GRID_MIN_WIDTH + CROSS_GRID_GAP)),
|
||||
);
|
||||
// 열 수 = 보기 칸 높이에 1.5줄이 드는 가장 적은 수 · 카드 폭 400~800(`_Section_Columns`).
|
||||
const picked = pickColumns(detail);
|
||||
const columnCount = picked.columns;
|
||||
drawnColumns = columnCount;
|
||||
// CSS auto-fill의 자체 열 수와 JS columnCount가 어긋나면 같은 행 높이 그룹핑이 실제
|
||||
// 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수를 JS가 명시해 일치시킨다.
|
||||
grid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(0, 1fr))`;
|
||||
cachedCardWidth = (renderWidth - (columnCount - 1) * CROSS_GRID_GAP) / columnCount;
|
||||
// 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수 · 폭을 JS가 명시해 일치시킨다.
|
||||
grid.style.gridTemplateColumns = `repeat(${columnCount}, ${picked.cardWidth}px)`;
|
||||
cachedCardWidth = picked.cardWidth;
|
||||
cachedRowHeight.clear();
|
||||
if (detail.cross_sections.length) {
|
||||
// 1차: 각 카드의 자연 높이(250px 바닥 적용) 측정 → 같은 행(columnCount 단위) 최댓값을 행 높이로.
|
||||
@@ -332,18 +360,8 @@ export function createSectionView(
|
||||
// 그것이 진입에서 `draw` 자기 시간의 대부분이었다(2026-09-06 CPU 프로파일).
|
||||
// 반폭도 카드가 실제로 쓰는 값(`effectiveCardHalfWidth`, 자동 줌아웃 포함)으로 맞춰
|
||||
// 재 둔다 — 카드·행 높이가 어긋나지 않는다.
|
||||
const plotBases = sections.map((section) =>
|
||||
crossPlotBase(
|
||||
section,
|
||||
currentExaggeration,
|
||||
cachedCardWidth,
|
||||
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),
|
||||
);
|
||||
const plotBases = plotBasesFor(detail, cachedCardWidth, sections.length);
|
||||
const naturalHeights = naturalHeightsOf(plotBases);
|
||||
// 2차: 행별 최댓값으로 강제 높이를 정해 같은 행 카드를 동일 높이로 렌더한다.
|
||||
for (let start = 0; start < sections.length; start += columnCount) {
|
||||
const rowHeight = Math.max(...naturalHeights.slice(start, start + columnCount));
|
||||
@@ -401,11 +419,17 @@ export function createSectionView(
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const nextWidth = contentWidth();
|
||||
if (!Number.isFinite(nextWidth) || nextWidth <= 0 || Math.abs(nextWidth - renderWidth) < 1)
|
||||
return;
|
||||
const nextHeight = contentHeight();
|
||||
if (!Number.isFinite(nextWidth) || nextWidth <= 0) return;
|
||||
const widthMoved = Math.abs(nextWidth - renderWidth) >= 1;
|
||||
if (!widthMoved && Math.abs(nextHeight - renderHeight) < 1) return;
|
||||
window.clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(() => {
|
||||
renderWidth = nextWidth;
|
||||
renderHeight = nextHeight;
|
||||
// 높이만 바뀌면(아래 패널 끌기 · 접기) 한 줄 카드 수가 바뀔 때만 다시 그린다.
|
||||
if (!widthMoved && currentDetail && pickColumns(currentDetail).columns === drawnColumns)
|
||||
return;
|
||||
draw();
|
||||
}, 150);
|
||||
});
|
||||
@@ -442,6 +466,7 @@ export function createSectionView(
|
||||
// 진입 시 측점을 자동으로 고르지 않는다(2026-08-02 사용자 지시) — 0측점이 선택된 채로
|
||||
// 시작하면 사용자가 고르지도 않은 카드가 강조돼 있고 유토곡선 말풍선도 떠 있다.
|
||||
renderWidth = contentWidth();
|
||||
renderHeight = contentHeight();
|
||||
draw();
|
||||
if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(cards));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user