diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index f85530d0..43cc3bfd 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -84,7 +84,7 @@ function fnv1a(text: string): string { * (2026-08-23 사용자 보고: "원복이 안 된 것 같다가 갑자기 반영됨"). 판번호를 해시에 * 섞어 두면 배포와 동시에 저장본이 만료된다. */ -const BUILD_VERSION = 34; // 세월교 날개벽 노선 추종 + 바닥 에이프런(2026-08-26). +const BUILD_VERSION = 35; // 세월교 날개벽 벌어짐 방향 정정(2026-08-26). /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts index b51c7a20..84ee9c78 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts @@ -46,6 +46,35 @@ export interface WingAnchor { /** 날개 축 방향 분할 수 — 높이 체감을 담을 최소 해상도. */ const WING_STEPS = 4; +/** + * 측벽 끝단부 프레임 — 측점 ± 반폭 두 자리 중 **`along` 방향에 놓인 쪽**을 고른다. + * 누가거리 부호와 진행 방향 부호가 반대인 노선이 있어 부호를 가정하면 안 된다. + */ +function endFrameOf( + frame: StructureFrame, + endFrameAt: ((chainageM: number) => RouteFrame | null) | undefined, + chainageM: number, + halfSpanM: number, + along: number, +): RouteFrame | null { + if (!endFrameAt) return null; + const dirX = -frame.leftY; + const dirY = frame.leftX; + let best: RouteFrame | null = null; + let bestReach = 0; + for (const step of [halfSpanM, -halfSpanM]) { + const candidate = endFrameAt(chainageM + step); + if (!candidate) continue; + // 기준 측점에서 그 자리까지의 진행 방향 성분 — `along`과 부호가 같아야 한다. + const reach = ((candidate.cx - frame.cx) * dirX + (candidate.cy - frame.cy) * dirY) * along; + if (reach > bestReach) { + bestReach = reach; + best = candidate; + } + } + return best; +} + /** * 날개벽 4매(좌·우 × 전·후). `anchors`가 빈 배열이거나 제원이 꺼져 있으면 안 만든다. * @@ -79,7 +108,13 @@ export function buildWingSolids( const height1 = Math.max(wing.height_m ?? height0, 0.3); for (const along of [1, -1]) { // 뿌리를 낼 프레임 — 세월교는 측벽 끝단 자리의 노선 프레임, BOX는 기준 측점. - const end = endFrameAt?.(chainageM + along * halfSpanM) ?? null; + // + // 누가거리 부호와 `dir`(좌향을 90° 돌린 진행 방향) 부호가 같다는 보장이 없다. + // 실제로 이 프레임의 `dir`은 누가거리가 **줄어드는** 쪽을 향해, 그냥 + // `chainageM + along * halfSpanM`으로 잡으면 뿌리가 벌어짐 방향의 반대편에 + // 앉아 날개가 오므라든다(2026-08-26 사용자: "아령같이 커져야 물을 받는다"). + // 그래서 앞뒤 두 자리를 다 뽑아 **`along` 방향에 놓인 쪽**을 고른다. + const end = endFrameOf(frame, endFrameAt, chainageM, halfSpanM, along); const base = end ?? frame; // 노선 진행 방향 = 좌향 벡터를 90° 돌린 것. const dirX = -base.leftY;