feat(B05): 3D 코리도 구조물 개별 선택

- 구조물 솔리드에 부재키(key) 부여 — B06과 같은 이름(inlet·outlet·extra{i}·bextra{i}·own·basin·pipe)
- 구조물 메쉬에 신원표(userData) — 이름 규칙은 그대로 유지
- 신규 모듈 두 개 — 3D 클릭·강조(Viewer_Structure_Pick), 좌측 패널 연결·B06 넘김(Structure_Pick_Session)
- 좌측 패널·종단에서 고른 것도 3D가 따라오도록 선택 동기화에 포트 추가
- B06 진입 시 세션 넘김값으로 그 측점 카드 선택·스크롤 + 기슭막이 조정창 열기
- 저장 코리도 만료(BUILD_VERSION 90→91) — 옛 저장본엔 부재키가 없음
- 뷰어 700줄 여유 — 검증용 요약을 Viewer_Debug로 분리(동작 불변)
- 검증 수단 — __corridorScene에 camera·project 추가

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 15:04:38 +09:00
co-authored by Claude Opus 5
parent e0e9d819c4
commit 2878ab9617
10 changed files with 440 additions and 47 deletions
+27
View File
@@ -30,6 +30,8 @@ import {
} from "./B06_Section_UI_Page_Persist";
import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
import { consumeStructurePick } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
import { type CrossDesignChange, createSectionView } from "./B06_Section_UI_Section_View";
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const";
@@ -55,6 +57,9 @@ import { loadSectionDetail } from "./B06_Section_Section_Store";
import { buildGroup, createSampleWidener, L } from "./B06_Section_UI_Page_Common";
import "@util/common_util_mass_haul.css";
/** B05 3D가 넘긴 부재키 중 **기슭막이 조정창이 받는 것**(2026-09-04). 집수정·배관은 빠진다. */
const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+)$/;
export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
let currentRouteId: number | null = null;
@@ -513,6 +518,27 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
confirmButton.disabled = sectionDetail === null;
}
/**
* B05 3D에서 고른 구조물을 이어받는다(2026-09-04 사용자 확정) — 세션 한 칸에 남긴
* `{ 측점, 부재키 }`로 그 측점 카드를 고르고, 기슭막이 부재면 조정창까지 연다.
* 값은 읽는 즉시 지워지므로 진입 한 번만 따라간다.
*/
function restoreStructurePick(): void {
const handoff = consumeStructurePick(projectId);
if (!handoff) return;
const target = sectionDetail?.cross_sections.find(
(section) => Math.abs(section.chainage_m - handoff.at) < 0.01,
);
if (!target) return;
// 카드가 다 선 뒤에 고른다 — 스크롤이 자리를 잡아야 한다.
requestAnimationFrame(() => {
sectionView.focusStation(target.station_id);
if (!handoff.key || !REVET_KEYS.test(handoff.key)) return;
stationControls.revetOffset.select(target.chainage_m, handoff.key as RevetKey);
sectionView.refreshCard(target.chainage_m);
});
}
function renderSectionDetail(): void {
if (sectionDetail) {
showSectionView();
@@ -527,6 +553,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
`${projectId ?? "-"}:${currentRouteId ?? "-"}`,
context?.natural_spoil_min_ground_slope ?? undefined,
);
restoreStructurePick();
}
}