diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 5ef4a509..52fe3221 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -225,7 +225,9 @@ function fnv1a(text: string): string { // 91 = 구조물 솔리드에 **부재키**(`key`)를 싣는다 — 3D 개별 선택이 그 표로 부재를 // 가려낸다(2026-09-04). 옛 저장본엔 키가 없어 3D에서 골라도 B06 조정창으로 넘길 // 부재를 못 정하므로 만료한다. -const BUILD_VERSION = 91; +// 92 = 세월교·BOX암거 부재까지 **부재키를 일원화**한다 — 횡단도가 부르는 이름 그대로 +// (세월교 유입·유출, BOX 좌·우, 본체는 `body`). 91 저장본엔 배수관 세트 키만 있다. +const BUILD_VERSION = 92; /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts index c0b058cb..935745ce 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts @@ -461,10 +461,19 @@ export function buildCorridorStructures( ); for (const side of fordLayout.sides) { for (const part of side.parts) { - pushPierced("basin", part.points, half, half, fordBore, pipeChainages); + pushPierced( + "basin", + part.points, + half, + half, + fordBore, + pipeChainages, + null, + `${side.role}:${part.kind}`, + ); } } - pushSwept("basin", fordLayout.slabBridge, half, half); + pushSwept("basin", fordLayout.slabBridge, half, half, null, "body"); // 날개벽 4매 — BOX암거와 같은 규칙(2026-08-25 사용자). 뿌리는 **벽 외측면** // (성토 경사측면과 마주치는 면) 모서리다(2026-08-25 ⑤ — 종전 바닥판 바깥 끝은 // 틀린 자리: 거긴 날개벽 투영이 만든 에이프런 끝이다). 발치는 바닥판 밑면, @@ -494,6 +503,8 @@ export function buildCorridorStructures( (point) => [point.offset, point.elevation] as [number, number], ), }, + key: `${side.role}:wing`, + apronKey: `${side.role}:apron`, }, ]; }), diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts index ffeea745..182f5c68 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts @@ -181,6 +181,7 @@ export function boxSolids( { chainage_m: chainage, kind: "basin", + key: "body", polygons: repeat(layout.topSlab, bodyRings.length), rings: bodyRings, // 아령 외곽선의 **손잡이** — 에이프런과 합쳐 탑뷰 경계를 이룬다(2026-08-27 사용자). @@ -189,6 +190,7 @@ export function boxSolids( { chainage_m: chainage, kind: "basin", + key: "body", polygons: repeat(layout.bottomSlab, bodyRings.length), rings: bodyRings, }, @@ -204,6 +206,7 @@ export function boxSolids( solids.push({ chainage_m: chainage, kind: "basin", + key: "body", polygons: repeat(layout.barrel, wallRings.length), rings: wallRings, }); @@ -235,6 +238,7 @@ export function boxSolids( solids.push({ chainage_m: chainage, kind: "basin", + key: `${side.role}:apron`, polygons: apronPlan.map((at) => { const inner = innerAt(at); return [ @@ -281,6 +285,7 @@ function wingSolids( topElevation: side.topElevation, bottomElevation: side.bottomElevation, wing: wingOf(section, side.role) ?? box.wing_out, + key: `${side.role}:wing`, })), ); } diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts index 10777549..399ea3f6 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts @@ -30,6 +30,10 @@ export interface WingAnchor { bottomElevation: number; /** 그 측에 적용할 날개벽 제원. */ wing: FordWingSpec; + /** 날개벽 부재키 — `그측이름:wing`(예 `inlet:wing`·`left:wing`). */ + key?: string; + /** 에이프런 부재키 — `그측이름:apron`. 날개벽과 따로 밝아져야 해 나눠 둔다. */ + apronKey?: string; /** * 세월교 전용 — 바닥 에이프런(2026-08-26 사용자 ④). 있으면 **날개 끝점과 바닥판 * 종방향 끝점을 이은 직선**을 외곽으로 하는 평면 삼각형을 세운다. @@ -170,7 +174,15 @@ export function buildWingSolids( // 성토끝 박스 옆선이 노선 흐름을 타게 된다(2026-08-26 사용자). BOX는 안 싣는다. const wingMode = end ? "route" : "station"; const wingAxis: [number, number] | undefined = end ? [base.leftX, base.leftY] : undefined; - solids.push({ chainage_m: chainageM, kind, polygons, rings, wing: wingMode, wingAxis }); + solids.push({ + chainage_m: chainageM, + kind, + key: anchor.key, + polygons, + rings, + wing: wingMode, + wingAxis, + }); const apron = anchor.apron; if (!apron || apron.floor.length < 3) continue; // 밑변은 **바닥판 끝면 그 자리**에서 시작한다 — 판을 밀어넣은 뿌리(rootX)에서 @@ -203,6 +215,7 @@ export function buildWingSolids( solids.push({ chainage_m: chainageM, kind: "basin", + key: anchor.apronKey, polygons: apronPolygons, rings: apronRings, wing: wingMode, diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 6e57c4ac..b68b0765 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -31,7 +31,7 @@ import { 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 { applyStructurePick } from "./B06_Section_UI_Page_Structure_Pick"; 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"; @@ -57,9 +57,6 @@ 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 { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); let currentRouteId: number | null = null; @@ -518,24 +515,14 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { confirmButton.disabled = sectionDetail === null; } - /** - * B05 3D에서 고른 구조물을 이어받는다(2026-09-04 사용자 확정) — 세션 한 칸에 남긴 - * `{ 측점, 부재키 }`로 그 측점 카드를 고르고, 기슭막이 부재면 조정창까지 연다. - * 값은 읽는 즉시 지워지므로 진입 한 번만 따라간다. - */ + /** 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); + applyStructurePick(consumeStructurePick(projectId), sectionDetail, { + focusStation: (stationId) => sectionView.focusStation(stationId), + refreshCard: (chainageM) => sectionView.refreshCard(chainageM), + revetSelect: (chainageM, key) => stationControls.revetOffset.select(chainageM, key), + fordSelect: (chainageM, role) => stationControls.ford.select(chainageM, role), + boxSelect: (chainageM, role) => stationControls.box.select(chainageM, role), }); } diff --git a/B06_Section/B06_Section_UI_Page_Structure_Pick.ts b/B06_Section/B06_Section_UI_Page_Structure_Pick.ts new file mode 100644 index 00000000..8682eac3 --- /dev/null +++ b/B06_Section/B06_Section_UI_Page_Structure_Pick.ts @@ -0,0 +1,62 @@ +/* ============================================================================= + * B06_Section_UI_Page_Structure_Pick.ts + * B05 3D에서 고른 구조물을 **B06 화면에 얹는다**(2026-09-04 사용자 확정) — 그 측점 + * 카드를 고르고 화면에 드러낸 뒤, 부재키가 있으면 그 시설의 조정창까지 연다. + * + * 부재키는 **횡단도가 그 부재를 부르는 이름 그대로**다. 이름이 겹쳐도(배수관 유입 ↔ + * 세월교 유입) 한 측점에는 시설이 하나뿐이라 헷갈리지 않는다 — 그 측점이 세월교면 + * 세월교 조정창, BOX암거면 BOX 조정창, 아니면 기슭막이 조정창으로 보낸다. + * 본체(`body` — 세월교 월류부 상판·BOX 구체)는 고르는 부재가 아니라 카드까지만 연다. + * + * 세월교·BOX는 조정창 하나가 그 측 부재 여럿(벽·바닥·날개벽·에이프런)을 함께 다루므로 + * 부재키가 `측이름:부재`(예 `inlet:wall`) 꼴이다 — **앞칸이 조정창 이름**이다. 3D 강조는 + * 부재 하나만 켜야 해서 뒤칸까지 나눠 두었다(2026-09-04 사용자 확정). + * ========================================================================== */ + +import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; +import type { RevetKey } from "./B06_Section_UI_Cross_Culvert"; +import type { StructurePickHandoff } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session"; + +/** 기슭막이 조정창이 받는 부재키 — 집수정·배관·본체는 여기서 빠진다. */ +const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+)$/; + +interface PickTargets { + focusStation: (stationId: string) => void; + refreshCard: (chainageM: number) => void; + revetSelect: (chainageM: number, key: RevetKey) => void; + fordSelect: (chainageM: number, role: "inlet" | "outlet") => void; + boxSelect: (chainageM: number, role: "left" | "right") => void; +} + +/** + * 넘김값을 화면에 적용한다. 값이 없거나 그 측점이 이 노선에 없으면 아무것도 안 한다. + * 카드가 다 선 뒤에 고른다 — 스크롤이 자리를 잡아야 한다. + */ +export function applyStructurePick( + handoff: StructurePickHandoff | null, + detail: SectionDetailResponse | null, + targets: PickTargets, +): void { + if (!handoff) return; + const target = detail?.cross_sections.find( + (section) => Math.abs(section.chainage_m - handoff.at) < 0.01, + ); + if (!target) return; + // 조정창은 앞칸(측 이름)만 본다 — 뒤칸은 3D 강조를 부재 하나로 좁히는 몫이다. + const key = handoff.key?.split(":")[0]; + requestAnimationFrame(() => { + targets.focusStation(target.station_id); + if (!key || key === "body") return; + if (target.ford) { + if (key !== "inlet" && key !== "outlet") return; + targets.fordSelect(target.chainage_m, key); + } else if (target.box) { + if (key !== "left" && key !== "right") return; + targets.boxSelect(target.chainage_m, key); + } else { + if (!REVET_KEYS.test(key)) return; + targets.revetSelect(target.chainage_m, key as RevetKey); + } + targets.refreshCard(target.chainage_m); + }); +}