feat(B05): BOX암거 비탈 투영 커브를 박스 방향 단일축으로 긋는다

원본·변형 성토면 투영 커브는 행마다 그 자리 노선 프레임을 써서, 곡선 구간이면
가로 방향이 행마다 틀어져 커브가 뒤틀린다. BOX암거는 날개벽처럼 박스 방향
하나로 그어야 한다(사용자).

BOX암거 세트만(`section.box`) 행 자리는 노선 위 그대로 두고 가로 방향만 기준
측점 좌향으로 고정한다. 세월교·배수관은 종전대로 그 행의 노선 프레임을 쓴다.

원본 커브의 바깥 끝은 그 행의 코리도 윤곽 점을 그대로 썼는데 가로 방향이
고정되면 어긋나므로, 고정 방향으로 쏜 선이 성토 끝선과 만나는 자리를 바깥
끝으로 삼는다(`castToToe`를 공개로 돌려 재사용). 끝은 여전히 실제 성토 끝선
위다. `BUILD_VERSION` 37 → 38.

실측(한 커브 안 행별 가로 방향 분산, ±180 감김 보정): BOX 200.92는 원본·변형
× 좌·우 4개 모두 0.00°. 배수관 84.3 11.42~50.80°, 세월교 149.73 22.84~23.02°,
배수관 275.71 2.28~8.95°로 그대로다. 행 손실 없음 — BOX 변형 6행·원본 13행.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 18:27:26 +09:00
co-authored by Claude Opus 5
parent 96be8fe233
commit 392e471dce
3 changed files with 39 additions and 8 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ function fnv1a(text: string): string {
* (2026-08-23 사용자 보고: "원복이 안 된 것 같다가 갑자기 반영됨"). 판번호를 해시에
* 섞어 두면 배포와 동시에 저장본이 만료된다.
*/
const BUILD_VERSION = 37; // 날개벽 바깥 세로선을 구체 측면에 붙임(2026-08-26).
const BUILD_VERSION = 38; // BOX암거 비탈 커브를 박스 방향 단일축으로(2026-08-26).
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
+36 -5
View File
@@ -19,7 +19,11 @@ import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
import type { CorridorStructure, RouteFrame } from "./B05_Profile_UI_Corridor_Structures";
import { structureSilhouettes } from "./B05_Profile_UI_Corridor_Station_Structure";
import { assembleStripLoops } from "./B05_Profile_UI_Corridor_Carve";
import { structureFootprintLoops, wingBoxLoops } from "./B05_Profile_UI_Corridor_Plan_Footprint";
import {
castToToe,
structureFootprintLoops,
wingBoxLoops,
} from "./B05_Profile_UI_Corridor_Plan_Footprint";
import { mixedGround, trimSlopeToGround } from "./B05_Profile_UI_Corridor_Station";
import { groundInterpolator } from "../B06_Section/B06_Section_UI_Cross_Culvert_Solve";
import type { PXY } from "./B05_Profile_UI_Corridor_Union";
@@ -187,7 +191,19 @@ export function buildPlanCurves(
leftY: section.frame.left_xy[1],
designZ: null,
};
const frameOf = (chainageM: number): RouteFrame => frameAt(chainageM) ?? fallback;
const routeFrameOf = (chainageM: number): RouteFrame => frameAt(chainageM) ?? fallback;
/**
* BOX암거는 비탈 커브도 **박스 방향 하나**로 긋는다(2026-08-26 사용자: 날개벽처럼
* 단일방향). 행 자리는 노선 위 그대로 두고 **가로 방향만** 기준 측점 좌향으로
* 고정한다 — 곡선에서 행마다 방향이 틀어져 커브가 뒤틀리던 걸 막는다.
* 세월교·배수관은 종전대로 그 행의 노선 프레임을 쓴다.
*/
const boxAxis = section.box ? section.frame.left_xy : null;
const frameOf = (chainageM: number): RouteFrame => {
const frame = routeFrameOf(chainageM);
if (!boxAxis) return frame;
return { ...frame, leftX: boxAxis[0], leftY: boxAxis[1] };
};
const inletSide: Side = (section.uphill_side ?? "left") === "left" ? "left" : "right";
const roleOf = (side: Side): PlanCurve["role"] => (side === inletSide ? "inlet" : "outlet");
// 노견 끝 편거리는 소유 측점 값을 구간 내내 쓴다 — 구간이 수 m라 확폭 변화는 무시.
@@ -266,11 +282,26 @@ export function buildPlanCurves(
push("slope-deformed", side, stripLoops(deformedRows, planZ));
// 원본 성토·절토선 — [노견 끝 → 코리도 바깥 윤곽(catch)]. 구조물이 없다고 본 면이다.
const rows: Array<[PXY, PXY] | null> = [];
// BOX암거는 가로 방향이 고정이라 그 행의 윤곽 점을 그대로 쓰면 방향이 틀어진다.
// 대신 고정 방향으로 쏜 선이 성토 끝선과 만나는 자리를 바깥 끝으로 삼는다 —
// 끝은 여전히 실제 성토 끝선 위다(2026-08-26 사용자).
const boxToe = boxAxis ? nearToe(outline, side, section.chainage_m) : null;
outline.chainages.forEach((chainageM, index) => {
if (chainageM < from - 1e-9 || chainageM > to + 1e-9) return;
const [ox, oy] = side === "left" ? outline.left[index] : outline.right[index];
const innerXY = at(frameOf(chainageM), roadEdge);
const outerXY = { x: ox, y: oy };
const frame = frameOf(chainageM);
const innerXY = at(frame, roadEdge);
let outerXY: PXY;
if (boxToe) {
const hit = castToToe(frame.cx, frame.cy, frame.leftX * sign, frame.leftY * sign, boxToe);
if (!hit) {
rows.push(null);
return;
}
outerXY = { x: hit.x, y: hit.y };
} else {
const [ox, oy] = side === "left" ? outline.left[index] : outline.right[index];
outerXY = { x: ox, y: oy };
}
rows.push(apart(innerXY, outerXY) ? [innerXY, outerXY] : null);
});
push("slope-original", side, stripLoops(rows, planZ));
@@ -143,7 +143,7 @@ export function structureFootprintLoops(
}
/** 광선이 성토 끝선(폴리라인)과 만난 자리 — 교점과 그 앞 마디 번호. */
interface ToeHit {
export interface ToeHit {
x: number;
y: number;
segment: number;
@@ -163,7 +163,7 @@ const RAY_LIMIT_M = 15;
* 쏘면 영영 안 만나 박스가 통째로 빠진다. 안쪽에서 만나도 "성토 끝선과 만나게"라는
* 규칙은 그대로 성립한다.
*/
function castToToe(
export function castToToe(
ox: number,
oy: number,
dx: number,