feat(B06): 횡단 줌 조작 일원화 + 교차점 맞춤 자동 확대

- 카드 하단 ◀/▶/↺(개별 표시 반폭) 삭제. 조작구는 우측 상단 줌 버튼 하나로.
- 줌 버튼 역할 확장(원배율에서만 반폭 조작):
  − 배율>1이면 축소, 원배율이면 표시 반폭 +1m
  + 배율>1이면 확대, 원배율이면 표시 반폭 −1m(하한 2m)
  ⤢ 배율 원복 + 절·성토 교차점이 다 보이는 폭으로 맞춤
- 교차점 맞춤(_UI_Cross_Fit): 보유 샘플(계산 반폭 20m) 안에서 교차점을 찾고,
  샘플 끝까지 안 만나면 끝단 사면·지반 기울기 차로 만나는 자리를 추세 외삽해
  올림 +1m을 요청 반폭으로 쓴다(상한 60m).
- 요청 반폭 > 보유 샘플이면 그 폭으로 재생성(regenerate) 후 해당 측점만 세션
  반폭으로 넓게 표시. 계산 기본 20m·캐시는 그대로, 확정 시 기존 경로대로
  display_half_width_m에 영구 저장.
- 재생성 로직을 _UI_Page_Common.createSampleWidener로 분리(700줄 제한),
  [전체 측점 반영]도 같은 경로 사용. 연타 중복 요청 차단.

자체검증: node tmp/tests/test_toe_extrapolate.mjs (외삽 좌/우·발산 지형·+1m 올림),
tsc --noEmit 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 18:16:31 +09:00
co-authored by Claude Opus 5
parent ec514a4560
commit f2ad5508f5
7 changed files with 224 additions and 96 deletions
+21 -29
View File
@@ -33,7 +33,8 @@ import type {
} from "./B06_Section_UI_Cross_Culvert_Wire";
import { buildStructurePanel } from "./B06_Section_UI_Cross_Structure_Panel";
import { attachZoomPan, buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
import type { ZoomPanState } from "./B06_Section_UI_Cross_View_Zoom";
import type { CrossWidthActions, ZoomPanState } from "./B06_Section_UI_Cross_View_Zoom";
import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
import type { RevetHighlightSetter, RevetKey } from "./B06_Section_UI_Cross_Culvert";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
@@ -76,8 +77,11 @@ export interface CrossCardElement extends HTMLElement {
export interface StationWidthControl {
/** 개별값(세션→저장값) 우선. 없으면 undefined — 카드가 전역 반폭으로 그린다. */
widthFor: (section: CrossSection) => number | undefined;
adjust: (chainageM: number, deltaM: number) => void;
reset: (chainageM: number) => void;
/**
* 이 측점 표시 반폭을 지정한다(2026-08-23 개편). 계산 반폭(기본 20m)을 넘는 값이면
* 페이지가 백엔드 재생성으로 샘플을 넓힌 뒤 이 카드만 그 폭으로 다시 그린다.
*/
request: (chainageM: number, widthM: number) => void;
}
// 기슭막이·유입 구조물·다단 제어 인터페이스는 Wire에 있다(700줄 제한) — 재수출.
@@ -106,7 +110,7 @@ export function createCrossSectionCard(
initialAreaKey?: CrossAreaKey | null,
/** 면적 값을 눌렀을 때. 선택되지 않은 카드에서도 눌릴 수 있어 측점 id를 함께 넘긴다. */
onAreaSelect?: (stationId: string, key: CrossAreaKey | null) => void,
/** 개별 표시 반폭 제어 — 있으면 카드 하단에 ◀/▶/↺ 버튼 그룹을 우측 맞춤으로 단다. */
/** 개별 표시 반폭 제어 — 있으면 우측 상단 줌 버튼이 원배율에서 반폭을 조절한다. */
stationWidth?: StationWidthControl,
/** 기슭막이 X 자리 제어 — 있으면 벽이 선택 가능해지고 선택 시 ◀/▶/↺가 뜬다. */
revetOffset?: RevetOffsetControl,
@@ -628,7 +632,19 @@ export function createCrossSectionCard(
});
showRevetControl = (visible) => panel.show(visible ? activeRevet : null);
showRevetControl(activeRevet !== null);
chartWrap.append(svg, readout.root, buildZoomControls(zoomPan), panel.root);
// 우측 상단 줌 버튼이 원배율에서 표시 반폭까지 다룬다(하단 ◀/▶/↺ 폐지, 2026-08-23).
const widthActions: CrossWidthActions | undefined = stationWidth && {
step: (deltaM) =>
stationWidth.request(
section.chainage_m,
Math.round((effectiveHalfWidth ?? maxOffset) + deltaM),
),
fit: () => {
const fitted = toeFitHalfWidth(section);
if (fitted !== null) stationWidth.request(section.chainage_m, fitted);
},
};
chartWrap.append(svg, readout.root, buildZoomControls(zoomPan, widthActions), panel.root);
if (activeArea) {
setBandActive(activeArea);
setChipActive(activeArea);
@@ -646,30 +662,6 @@ export function createCrossSectionCard(
rockControl.classList.add("b06-cross-card__rockb");
footer.append(rockControl);
}
// 개별 표시 반폭 ◀/▶/↺ — 우측 맞춤, 초기화는 전역 반폭 복귀(2026-08-06).
if (stationWidth) {
const widthControl = document.createElement("div");
widthControl.className = "b06-cross-card__widthctl";
const makeButton = (label: string, title: string, onClick: () => void): HTMLButtonElement => {
const button = document.createElement("button");
button.type = "button";
button.className = "b06-design__rockb-btn";
button.textContent = label;
button.title = title;
button.addEventListener("click", (event) => {
// 카드 선택 클릭으로 번지면 재렌더로 줌·팬이 초기화된다.
event.stopPropagation();
onClick();
});
return button;
};
widthControl.append(
makeButton("◀", L("B06_Cross_Width_Dec"), () => stationWidth.adjust(section.chainage_m, -1)),
makeButton("▶", L("B06_Cross_Width_Inc"), () => stationWidth.adjust(section.chainage_m, 1)),
makeButton("↺", L("B06_Cross_Width_Reset"), () => stationWidth.reset(section.chainage_m)),
);
footer.append(widthControl);
}
card.append(footer);
return card;
}