- A00_Common/design/design_profile.ts 본체 · design_profile_slots.ts 판 자리 약속 · Profile_Panel 827 → 감싸개 37줄(Guard · Drag 로 나눔) - B06: 옛 붙박이 종단(Section_View_Draw · Chart · Panel) 걷음 · Section_View_Profile 이음(선택 · 구조물 편집 · 카드 갱신 · 최소 흙덮기 관 목록) - 크기 · 접기 키 하나(profile-height · profile-collapsed) · 옛 section-panel-* 값 옮겨 받음 - 소스 문자열 시험 넷을 옮긴 자리로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
478 lines
21 KiB
TypeScript
478 lines
21 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Section_View.ts
|
|
* 횡단 보기 컨트롤러: 위 칸 = 횡단 카드 그리드(그 안에서 스크롤) · 가운데 아래쪽 =
|
|
* 종단도 한 벌(종단 보기와 같은 자리 · 같은 도구, 계획서 24-0 · 24-2c)을 조립하고,
|
|
* 측점 선택/스크롤, 반응형 폭 측정(ResizeObserver), 같은 행 높이 통일, 단건 카드 갱신을
|
|
* 관장한다.
|
|
*
|
|
* 나눈 자리:
|
|
* - 공통 상수·유틸: `_UI_Section_Common`
|
|
* - 종단도 이음: `_UI_Section_View_Profile`(종단도 한 벌 `design_profile`)
|
|
* - 횡단 카드 렌더러: `_UI_Cross_View`
|
|
* 외부(Page)에는 `createSectionView`와 `CrossDesignChange` 타입만 노출한다.
|
|
* ========================================================================== */
|
|
|
|
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
|
import type {
|
|
CrossSection,
|
|
EarthworkConversion,
|
|
HaulEquipmentLimit,
|
|
SectionDetailResponse,
|
|
} from "./B06_Section_Api_Fetch";
|
|
import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design";
|
|
import type { CrossAreaKey } from "./B06_Section_UI_Cross_Areas";
|
|
import type { FordControl } from "./B06_Section_UI_Cross_Ford_Panel";
|
|
import type { BoxControl } from "./B06_Section_UI_Cross_Box_Panel";
|
|
import {
|
|
createCrossSectionCard,
|
|
type ExtraWallControl,
|
|
type InletStructureControl,
|
|
type RevetOffsetControl,
|
|
type RevetLinkControl,
|
|
type StructureSpanControl,
|
|
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 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 {
|
|
CROSS_GRID_GAP,
|
|
CROSS_GRID_MIN_WIDTH,
|
|
CROSS_WIDTH,
|
|
type CrossDesignChange,
|
|
type DesignChangeHandler,
|
|
designElevationAt,
|
|
emptyView,
|
|
inferStationInterval,
|
|
L,
|
|
} from "./B06_Section_UI_Section_Common";
|
|
import type { SectionViewController } from "./B06_Section_UI_Section_View_Types";
|
|
import { createFillSlopeNotice } from "./B06_Section_UI_Cross_FillSlope_Notice";
|
|
import { createSectionProfile } from "./B06_Section_UI_Section_View_Profile";
|
|
|
|
export type { CrossDesignChange, DesignChangeHandler, RockBoundaryControl, SectionViewController };
|
|
|
|
export function createSectionView(
|
|
onDesignChange?: DesignChangeHandler,
|
|
rockBoundary?: RockBoundaryControl,
|
|
/** 측점 개별 표시 반폭(2026-08-06)·기슭막이 X 자리(2026-08-21) 제어. */
|
|
stationWidth?: StationWidthControl,
|
|
revetOffset?: RevetOffsetControl,
|
|
inletStructure?: InletStructureControl, // 유입측 구조물 형식(2026-08-22)
|
|
extraWalls?: ExtraWallControl, // 유출측 추가 기슭막이 단 수(2026-08-22)
|
|
/** 구간값·연동(2026-08-24), 세월교 측벽·BOX암거 구체(2026-08-25) 제어. */
|
|
structureSpan?: StructureSpanControl,
|
|
revetLink?: RevetLinkControl,
|
|
ford?: FordControl,
|
|
box?: BoxControl,
|
|
/** 측점별 암 절토 경사 입력(2026-09-07). */
|
|
cutSlope?: CutSlopeControl,
|
|
): SectionViewController {
|
|
const root = document.createElement("div");
|
|
root.className = "b06-section";
|
|
/** 위 칸 — 횡단 카드는 **이 안에서** 스크롤한다(종단도는 아래에 붙박이, 계획서 24-0). */
|
|
const cards = document.createElement("div");
|
|
cards.className = "b06-section__cards";
|
|
root.append(cards);
|
|
let currentDetail: SectionDetailResponse | null = null;
|
|
let selectedStationId: string | null = null;
|
|
/** 선택 카드에서 강조 중인 면적. 카드를 다시 그려도 그대로 살아나야 하므로 여기 둔다. */
|
|
let activeAreaKey: CrossAreaKey | null = null;
|
|
let currentExaggeration = 1;
|
|
let currentCrossHalfWidth: number | undefined;
|
|
let currentStationInterval: number | undefined;
|
|
let structureEdit: SectionStructureEdit | null = null;
|
|
let renderWidth = 0;
|
|
let resizeTimer = 0;
|
|
// 카드 단위 재빌드에 재사용하는 렌더 컨텍스트 (draw에서 갱신)
|
|
let cachedStationInterval = 1;
|
|
let cachedCardWidth = CROSS_WIDTH;
|
|
// 같은 행 카드는 같은 높이가 되도록 draw에서 측점별 행 높이를 계산해 둔다(단건 갱신도 이 값 재사용).
|
|
const cachedRowHeight = new Map<string, number>();
|
|
/** 종단도에 넘길 값 — 다시 그릴 때마다 같은 값을 다시 준다. */
|
|
let profileInput: {
|
|
balloonScope: string;
|
|
conversion?: EarthworkConversion;
|
|
haulLimits?: HaulEquipmentLimit[];
|
|
naturalSpoilMinSlope?: number;
|
|
} = { balloonScope: "-" };
|
|
|
|
// 성토사면 5m 초과 경고 줄(2026-09-14 브레인 (나)) — 카드 위 · 측점 누르면 그 카드로.
|
|
const fillSlopeNotice = createFillSlopeNotice((id) =>
|
|
selectedStationId === id ? revealCard(id, "smooth") : selectStation(id, true),
|
|
);
|
|
|
|
/** 종단도 한 벌 — 첫 그리기에서 프로젝트를 알게 되면 만든다(`_Section_View_Profile`). */
|
|
const profile = createSectionProfile({
|
|
selectCard: (stationId) => {
|
|
if (stationId === null) clearSelection();
|
|
else if (stationId !== selectedStationId) selectStation(stationId, true);
|
|
else revealCard(stationId, "smooth");
|
|
},
|
|
edit: () => structureEdit,
|
|
refreshCards: (chainages) => refreshCards(chainages),
|
|
});
|
|
|
|
const contentWidth = (): number => {
|
|
// 요소가 DOM 밖(detached)이면 computed padding이 ""라 parseFloat이 NaN을 만든다.
|
|
// NaN이 renderWidth로 흘러가면 카드 폭이 NaN이 되므로 0으로 방어한다.
|
|
const style = getComputedStyle(cards);
|
|
const pad = (value: string): number => Number.parseFloat(value) || 0;
|
|
const width = cards.clientWidth - pad(style.paddingLeft) - pad(style.paddingRight);
|
|
return Number.isFinite(width) ? Math.max(0, width) : 0;
|
|
};
|
|
|
|
/**
|
|
* 선택 카드를 위 칸 안에서 보이게 한다 — `start`로 카드 **상단**(측점 라벨)을 칸 위에
|
|
* 붙인다(2026-08-04 사용자 지적). 목록 맨 끝 카드는 스크롤이 바닥에 걸려 그 자리까지 못
|
|
* 올라오며, 그대로 둔다(브라우저가 알아서 멈춘다).
|
|
*/
|
|
const revealCard = (stationId: string | null, behavior: ScrollBehavior): void => {
|
|
if (!stationId) return;
|
|
document.getElementById(`cross-${stationId}`)?.scrollIntoView({ behavior, block: "start" });
|
|
};
|
|
|
|
/** 카드 하나의 선택 표시를 갱신한다 — **다시 만들지 않는다**(줌·팬이 살아남아야 한다). */
|
|
const markCard = (stationId: string | null): void => {
|
|
if (!stationId) return;
|
|
const card = document.getElementById(`cross-${stationId}`) as CrossCardElement | null;
|
|
card?.applySelection?.(stationId === selectedStationId, activeAreaKey);
|
|
};
|
|
|
|
/**
|
|
* 연동으로 이어진 카드들의 **벽 강조**를 다시 칠한다(2026-08-30 사용자: 한 기슭막이는
|
|
* 그 연장 안 모든 횡단도에서 같이 강조된다). 조정창은 고른 카드 하나에만 남는다.
|
|
* 카드는 다시 만들지 않고 클래스만 갈아 끼운다 — 줌·팬 보존.
|
|
*/
|
|
const syncRevetHighlights = (): void => {
|
|
if (!currentDetail || !revetOffset) return;
|
|
for (const section of currentDetail.cross_sections) {
|
|
const card = document.getElementById(
|
|
`cross-${section.station_id}`,
|
|
) as CrossCardElement | null;
|
|
card?.applyRevetHighlight?.(revetOffset.highlightFor(section));
|
|
}
|
|
};
|
|
if (revetOffset) revetOffset.onSelectionChange = syncRevetHighlights;
|
|
|
|
/** 카드 선택 변경 수신자(좌측 구조물 폼). 페이지가 등록한다. */
|
|
let stationSelectListener: ((stationId: string | null) => void) | null = null;
|
|
|
|
/**
|
|
* 선택이 바뀐 흔적만 화면에 반영한다 — **재렌더 금지**.
|
|
* 카드를 새로 만들면(전체든 한 장이든) 사용자가 맞춰 둔 휠 줌·팬(viewBox)이 초기화된다
|
|
* (2026-08-02 사용자 지적). 선택 표시가 바뀌는 카드는 이전·현재 둘뿐이고, 그 둘도 클래스와
|
|
* 강조 상태만 갈아 끼운다. 종단도 선택 표시는 카드를 따라간다(계획서 24-0).
|
|
*/
|
|
const applySelection = (previousStationId: string | null): void => {
|
|
markCard(previousStationId);
|
|
if (selectedStationId !== previousStationId) markCard(selectedStationId);
|
|
profile.showCard(
|
|
currentDetail?.cross_sections.find((entry) => entry.station_id === selectedStationId) ?? null,
|
|
);
|
|
if (selectedStationId !== previousStationId) stationSelectListener?.(selectedStationId);
|
|
};
|
|
|
|
/** 같은 측점을 다시 고르면 선택을 푼다(2026-08-02 사용자 지시) — 카드·측점선 어느 쪽이든. */
|
|
const selectStation = (stationId: string, scroll: boolean): void => {
|
|
const previous = selectedStationId;
|
|
const deselect = selectedStationId === stationId;
|
|
selectedStationId = deselect ? null : stationId;
|
|
// 측점이 바뀌면 면적 강조는 따라가지 않는다(다른 측점의 강조를 물려받으면 오해를 부른다).
|
|
activeAreaKey = null;
|
|
applySelection(previous);
|
|
if (scroll && !deselect) revealCard(stationId, "smooth");
|
|
};
|
|
|
|
/**
|
|
* 횡단 카드의 면적 값을 눌렀을 때. **선택되지 않은 카드에서 눌러도** 그 카드를 고르면서
|
|
* 누른 면적까지 한 번에 강조한다(2026-08-02 사용자 지시).
|
|
* 이미 선택된 카드라면 카드가 자기 DOM에서 강조를 켜고 끄므로 여기서는 상태만 받아 둔다
|
|
* (다시 그릴 때 그대로 되살리기 위해).
|
|
*/
|
|
const selectArea = (stationId: string, key: CrossAreaKey | null): void => {
|
|
if (selectedStationId === stationId) {
|
|
activeAreaKey = key;
|
|
return;
|
|
}
|
|
const previous = selectedStationId;
|
|
selectedStationId = stationId;
|
|
activeAreaKey = key;
|
|
applySelection(previous);
|
|
revealCard(stationId, "smooth");
|
|
};
|
|
|
|
/** 카드 바깥(그리드 빈 자리)을 누르면 선택을 푼다(2026-08-02 사용자 지시). */
|
|
const clearSelection = (): void => {
|
|
if (!selectedStationId && !activeAreaKey) return;
|
|
const previous = selectedStationId;
|
|
selectedStationId = null;
|
|
activeAreaKey = null;
|
|
applySelection(previous);
|
|
};
|
|
|
|
/** 십자선이 설 계획고 — 세월교 측점은 구체 위 노면이 월류 높이만큼 낮게 앉으므로
|
|
* 그만큼 내려 찍는다(2026-08-30 사용자 확정). 하강량은 백엔드가 계획고를 내려
|
|
* 잡으며 실어 준 값(`design.surface_drop_m`)이고, 원래 노면은 점선이 남긴다. */
|
|
const cardDesignElevation = (
|
|
detail: SectionDetailResponse,
|
|
section: CrossSection,
|
|
): number | undefined => {
|
|
const planZ = designElevationAt(detail.longitudinal.design_profiles, section.chainage_m);
|
|
if (planZ === undefined) return undefined;
|
|
return planZ - (section.design?.surface_drop_m ?? 0);
|
|
};
|
|
|
|
const buildCrossCard = (
|
|
section: CrossSection,
|
|
forcedHeightPx?: number,
|
|
plotBase?: CrossPlotBase | null,
|
|
): HTMLElement =>
|
|
createCrossSectionCard(
|
|
section,
|
|
section.station_id === selectedStationId,
|
|
currentExaggeration,
|
|
(stationId) => selectStation(stationId, false),
|
|
cachedStationInterval,
|
|
currentCrossHalfWidth,
|
|
cachedCardWidth,
|
|
forcedHeightPx,
|
|
currentDetail ? cardDesignElevation(currentDetail, section) : undefined,
|
|
onDesignChange,
|
|
rockBoundary,
|
|
section.station_id === selectedStationId ? activeAreaKey : null,
|
|
selectArea,
|
|
stationWidth,
|
|
revetOffset,
|
|
inletStructure,
|
|
extraWalls,
|
|
culvertLinkFor(section),
|
|
structureSpan,
|
|
revetLink,
|
|
ford,
|
|
box,
|
|
plotBase,
|
|
cutSlope,
|
|
);
|
|
|
|
/**
|
|
* 옆 측점 구조물이 이 측점까지 넘어왔는지 — 기슭막이 기본 연장 10m(전 5·후 5)면
|
|
* 배수관 측점 하나로 끝나지 않는다(2026-08-24 사용자). 3D는 이미 그 구간을 소유
|
|
* 측점 단면으로 채우므로 횡단도도 같은 규칙으로 링크한다.
|
|
*/
|
|
const culvertLinkFor = (section: CrossSection): CulvertLink | undefined => {
|
|
const detail = currentDetail;
|
|
if (!detail) return undefined;
|
|
return culvertLink(
|
|
section,
|
|
detail.cross_sections,
|
|
(chainageM) => designElevationAt(detail.longitudinal.design_profiles, chainageM) ?? null,
|
|
);
|
|
};
|
|
|
|
/** 카드 면적이 바뀌었다 — 경고 줄 · 유토곡선 · 뱃지만 다시 싣는다. */
|
|
function redrawProfile(): void {
|
|
if (!currentDetail) return;
|
|
fillSlopeNotice.update(currentDetail.cross_sections, cachedStationInterval);
|
|
profile.redraw();
|
|
}
|
|
|
|
/** 종단도 · 경고 줄을 지금 자료로 다시 맞춘다(카드는 안 건드린다). */
|
|
function syncProfile(): void {
|
|
if (!currentDetail) return;
|
|
fillSlopeNotice.update(currentDetail.cross_sections, cachedStationInterval);
|
|
profile.render({
|
|
detail: currentDetail,
|
|
stationInterval: cachedStationInterval,
|
|
...profileInput,
|
|
});
|
|
const profileRoot = profile.root();
|
|
if (profileRoot && profileRoot.parentElement !== root) root.append(profileRoot);
|
|
}
|
|
|
|
const draw = (): void => {
|
|
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
|
const detail = currentDetail;
|
|
cachedStationInterval =
|
|
currentStationInterval ?? inferStationInterval(detail.longitudinal.stations);
|
|
|
|
// 횡단면도 제목·개수 표기는 두지 않는다(2026-08-02 사용자 지시) — 카드마다 측점이
|
|
// 적혀 있어 무엇을 보는지는 이미 분명하다.
|
|
const grid = document.createElement("div");
|
|
grid.className = "b06-section__grid";
|
|
// 카드 사이 빈 자리를 누르면 선택 해제 — 카드 안 클릭은 카드가 먼저 처리한다.
|
|
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)),
|
|
);
|
|
// 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;
|
|
cachedRowHeight.clear();
|
|
if (detail.cross_sections.length) {
|
|
// 1차: 각 카드의 자연 높이(250px 바닥 적용) 측정 → 같은 행(columnCount 단위) 최댓값을 행 높이로.
|
|
const sections = detail.cross_sections;
|
|
// 여기서 만든 기하를 2차에서 **그대로 넘긴다** — 예전에는 같은 측점을 두 번 계산했고
|
|
// 그것이 진입에서 `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),
|
|
);
|
|
// 2차: 행별 최댓값으로 강제 높이를 정해 같은 행 카드를 동일 높이로 렌더한다.
|
|
for (let start = 0; start < sections.length; start += columnCount) {
|
|
const rowHeight = Math.max(...naturalHeights.slice(start, start + columnCount));
|
|
for (
|
|
let index = start;
|
|
index < Math.min(start + columnCount, sections.length);
|
|
index += 1
|
|
) {
|
|
cachedRowHeight.set(sections[index].station_id, rowHeight);
|
|
grid.append(buildCrossCard(sections[index], rowHeight, plotBases[index]));
|
|
}
|
|
}
|
|
} else {
|
|
grid.append(emptyView(L("B06_Profile_View_NoCross")));
|
|
}
|
|
// 위 칸 스크롤 자리를 지킨다 — 다시 그렸다고 보던 카드가 맨 앞으로 튀면 못 쓴다.
|
|
const keepScrollTop = cards.scrollTop;
|
|
cards.replaceChildren(fillSlopeNotice.root, grid);
|
|
cards.scrollTop = keepScrollTop;
|
|
syncProfile();
|
|
};
|
|
|
|
/** 카드 한 장만 갈아 끼운다(종단도는 안 건드린다). 못 찾으면 false. */
|
|
const rebuildCard = (chainageM: number): boolean => {
|
|
if (!currentDetail) return false;
|
|
const section = currentDetail.cross_sections.find(
|
|
(candidate) => Math.abs(candidate.chainage_m - chainageM) < 0.01,
|
|
);
|
|
if (!section) return false;
|
|
const existing = document.getElementById(`cross-${section.station_id}`);
|
|
// 단건 갱신은 draw에서 정해둔 행 높이를 재사용해 같은 행 카드와 높이를 유지한다.
|
|
if (existing)
|
|
existing.replaceWith(buildCrossCard(section, cachedRowHeight.get(section.station_id)));
|
|
return true;
|
|
};
|
|
|
|
const refreshCard = (chainageM: number): void => {
|
|
// 단면적이 바뀌면 유토곡선도 함께 흔들리므로 종단도만 다시 그린다(카드 전체 재렌더 없음).
|
|
if (rebuildCard(chainageM)) redrawProfile();
|
|
};
|
|
|
|
/**
|
|
* 여러 측점을 한 번에 갈아 끼운다 — **종단도는 끝에 한 번만** 다시 그린다.
|
|
*
|
|
* 측점마다 `refreshCard` 를 부르면 측점 수만큼 종단 그래프·유토곡선을 다시 그려,
|
|
* 계획선 ▲▼ 한 번에 화면이 몇 백 ms 씩 멈췄다(2026-09-12 B05 대비 느림 원인).
|
|
*/
|
|
const refreshCards = (chainages: ReadonlyArray<number>): void => {
|
|
let touched = false;
|
|
for (const chainageM of chainages) if (rebuildCard(chainageM)) touched = true;
|
|
if (touched) redrawProfile();
|
|
};
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
const nextWidth = contentWidth();
|
|
if (!Number.isFinite(nextWidth) || nextWidth <= 0 || Math.abs(nextWidth - renderWidth) < 1)
|
|
return;
|
|
window.clearTimeout(resizeTimer);
|
|
resizeTimer = window.setTimeout(() => {
|
|
renderWidth = nextWidth;
|
|
draw();
|
|
}, 150);
|
|
});
|
|
resizeObserver.observe(cards);
|
|
|
|
return {
|
|
root,
|
|
render(
|
|
detail,
|
|
verticalExaggeration,
|
|
crossHalfWidth,
|
|
stationInterval,
|
|
earthworkConversion,
|
|
haulEquipmentLimits,
|
|
balloonScope,
|
|
naturalSpoilMinSlope,
|
|
) {
|
|
currentDetail = detail;
|
|
currentExaggeration = Math.max(verticalExaggeration, 0.1);
|
|
currentCrossHalfWidth =
|
|
crossHalfWidth !== undefined && crossHalfWidth > 0 ? crossHalfWidth : undefined;
|
|
currentStationInterval =
|
|
stationInterval !== undefined && stationInterval > 0 ? stationInterval : undefined;
|
|
// 유토곡선 · 뱃지 설정과 balloon 자리 scope(프로젝트:노선)는 종단도 한 벌이 받는다 —
|
|
// 영구저장소 값(detail.balloon_offsets)이 이기는 규칙도 그쪽에 있다.
|
|
profileInput = {
|
|
balloonScope: balloonScope ?? "-",
|
|
conversion: earthworkConversion ?? profileInput.conversion,
|
|
haulLimits: haulEquipmentLimits ?? profileInput.haulLimits,
|
|
naturalSpoilMinSlope: Number.isFinite(naturalSpoilMinSlope)
|
|
? naturalSpoilMinSlope
|
|
: profileInput.naturalSpoilMinSlope,
|
|
};
|
|
// 진입 시 측점을 자동으로 고르지 않는다(2026-08-02 사용자 지시) — 0측점이 선택된 채로
|
|
// 시작하면 사용자가 고르지도 않은 카드가 강조돼 있고 유토곡선 말풍선도 떠 있다.
|
|
renderWidth = contentWidth();
|
|
draw();
|
|
if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(cards));
|
|
},
|
|
refreshCard,
|
|
refreshCards,
|
|
focusStation(stationId) {
|
|
if (selectedStationId !== stationId) selectStation(stationId, true);
|
|
else revealCard(stationId, "smooth");
|
|
},
|
|
setStationSelectListener(listener) {
|
|
stationSelectListener = listener;
|
|
},
|
|
setStructureEdit(edit) {
|
|
structureEdit = edit;
|
|
},
|
|
// 계획선 편집(▲/▼)은 종단도 한 벌이 맡는다(계획서 24-2c) — 같은 초안 가게 · 같은
|
|
// 막기 규칙. 옛 제공자는 받기만 하고 쓰지 않는다(페이지 쪽 정리는 24-2a 몫).
|
|
setGradeEdit() {},
|
|
setStructureMarks(structures, types) {
|
|
profile.setMarks(structures, types);
|
|
// 검증용 훅 — 알약 레인이 무엇을 받았는지 화면 밖에서 수치로 본다
|
|
// (`__corridorBuild` 등과 같은 용도).
|
|
(window as unknown as { __b06Marks?: unknown }).__b06Marks = {
|
|
structures: structures.length,
|
|
types: types.length,
|
|
drawn: !!currentDetail,
|
|
};
|
|
},
|
|
clear() {
|
|
currentDetail = null;
|
|
selectedStationId = null;
|
|
cards.replaceChildren();
|
|
},
|
|
dispose() {
|
|
window.clearTimeout(resizeTimer);
|
|
resizeObserver.disconnect();
|
|
profile.dispose();
|
|
},
|
|
};
|
|
}
|