fix(B05): 세월교 본 바닥·날개 바닥·에이프런을 한 면으로 맞춘다

원인은 종단경사 보정(dz)이었다. 세월교 본체(측벽·바닥판)는 dz를 타서 양끝이
들린 사발 모양인데(앞끝 +0.0907 / 측점 0 / 뒤끝 +0.1613) 날개벽·에이프런은
dz = 0으로 측점 표고에 평평히 놓였다. 접합부 틈 0.0907·0.1613m이 그 dz와
소수점까지 일치했다. 표고 자체는 이미 같았다 — 어긋난 건 dz뿐이다.

본체는 그대로 두고 날개·에이프런을 경사에 태운다(사용자 선택).

- `buildWingSolids`에 선택 인자 `baseElevationM` 추가 — 넘기면 날개·에이프런
  링이 붙는 자리의 dz(end.designZ − baseElevationM)를 탄다. 세월교만 넘기고
  BOX암거는 안 넘겨 dz = 0 현행을 유지한다.
- 에이프런을 본 바닥판과 한 장으로 만든다. ① 밑변을 판이 밀려들어간 뿌리가
  아니라 바닥판 끝면 그 자리에서 시작한다. ② 단면을 직사각형 근사 대신 바닥판
  폴리곤 그대로 쓴다(뿌리 offset 기준으로 다시 잡아 끝으로 갈수록 0으로 줄인다).
  종전엔 사다리꼴 바닥판(2.669~4.283)을 직사각형(2.747~4.283)으로 근사해
  안쪽 모서리가 0.078m 어긋났다.
- `BUILD_VERSION` 39 → 40.

실측: 에이프런 밑변 4꼭짓점이 본 바닥판 끝면 꼭짓점과 거리 0.0000m(4장 전부).
날개·에이프런 dz가 붙는 자리 값을 그대로 타고 같은 자리의 날개와 에이프런이
같은 dz다. 회귀 없음 — 아령 벌어짐 유지, 날개 바깥 세로선↔구체 측면 8장 전부
0.0000m, 성토끝 박스 4/4·4/4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 18:38:30 +09:00
co-authored by Claude Opus 5
parent 392e471dce
commit ce77e0735f
3 changed files with 45 additions and 35 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ function fnv1a(text: string): string {
* (2026-08-23 사용자 보고: "원복이 안 된 것 같다가 갑자기 반영됨"). 판번호를 해시에
* 섞어 두면 배포와 동시에 저장본이 만료된다.
*/
const BUILD_VERSION = 38; // BOX암거 비탈 커브를 박스 방향 단일축으로(2026-08-26).
const BUILD_VERSION = 40; // 세월교 날개·에이프런을 종단경사에 태움(2026-08-26).
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
@@ -438,12 +438,6 @@ export function buildCorridorStructures(
const wall = side.parts.find((part) => part.kind === "wall");
const floor = side.parts.find((part) => part.kind === "floor");
if (!wall || !floor) return [];
// 바닥판 종방향 끝의 계류측 모서리 — 그 측 바깥 방향으로 가장 먼 점이다.
const outerOffset = floor.points.reduce(
(far, point) =>
point.offset * side.outward > far * side.outward ? point.offset : far,
floor.points[0].offset,
);
return [
{
// 벽 외측면 하단 모서리(points[3] = 하단-전면) — 여기서 벌어져 나간다.
@@ -452,17 +446,20 @@ export function buildCorridorStructures(
bottomElevation: floor.points[2].elevation,
wing: side.role === "inlet" ? fordLayout.ford.wing_in : fordLayout.ford.wing_out,
// 바닥 에이프런 — 날개 끝점↔바닥판 끝점 직선이 외곽이다(2026-08-26 ④).
// 단면은 바닥판 폴리곤 그대로 — 본 바닥과 한 장으로 이어져야 한다.
apron: {
outerOffset,
topElevation: side.slabTopElevation,
bottomElevation: floor.points[2].elevation,
floor: floor.points.map(
(point) => [point.offset, point.elevation] as [number, number],
),
},
},
];
}),
"revet",
// 세월교만 — 날개 뿌리·각도를 측벽 끝단부 자리의 노선 프레임에서 뽑는다.
// 세월교만 — 날개 뿌리·각도를 측벽 끝단부 자리의 노선 프레임에서 뽑고,
// 그 자리의 종단경사 보정(dz)까지 태워 본체 끝면과 틈 없이 붙인다.
frameAt,
baseZ,
),
);
// 관은 련수만큼 폭 안에 등간격으로 놓는다(단면엔 1개만 보이지만 실물은 여러 련).
@@ -32,14 +32,16 @@ export interface WingAnchor {
wing: FordWingSpec;
/**
* 세월교 전용 — 바닥 에이프런(2026-08-26 사용자 ④). 있으면 **날개 끝점과 바닥판
* 종방향 끝점을 이은 직선**을 외곽으로 하는 평면 삼각형을 바닥 두께로 세운다.
* 종방향 끝점을 이은 직선**을 외곽으로 하는 평면 삼각형을 세운다.
* BOX암거는 안 넘긴다(현행 유지 — 사용자 ②).
*
* 단면은 **바닥판 폴리곤 그대로**다 — 본 바닥과 한 장으로 이어져야 하므로
* 직사각형으로 근사하면 안쪽 모서리가 어긋난다(2026-08-26 사용자: "본 바닥과
* 날개에 의해 생성된 신규 바닥은 하나의 바닥이어야").
*/
apron?: {
/** 바닥판 종방향 끝의 계류측 모서리 offset. */
outerOffset: number;
topElevation: number;
bottomElevation: number;
/** 그 측 바닥판 단면 폴리곤 `[offset, 표고]` — 뿌리 offset 기준으로 다시 잡는다. */
floor: Array<[number, number]>;
};
}
@@ -94,6 +96,12 @@ export function buildWingSolids(
* (2026-08-26 사용자 ③). 안 넘기면 현행 그대로 = BOX암거(사용자 ②).
*/
endFrameAt?: (chainageM: number) => RouteFrame | null,
/**
* 기준 측점의 계획고 — 넘기면 날개·에이프런이 **붙는 자리의 종단경사 보정(dz)**을
* 그대로 탄다(2026-08-26 사용자). 본체는 이미 종단경사를 타는데 날개만 측점
* 표고에 평평히 놓여 접합부에 0.09~0.16m 틈이 났다. 안 넘기면 dz = 0(BOX암거 현행).
*/
baseElevationM?: number | null,
): CorridorStructure[] {
const solids: CorridorStructure[] = [];
@@ -126,6 +134,8 @@ export function buildWingSolids(
// 날개 방향 = 구체 축을 도로 방향으로 각도만큼 튼 단위벡터.
const wingX = base.leftX * axisSign * Math.cos(angle) + dirX * along * Math.sin(angle);
const wingY = base.leftY * axisSign * Math.cos(angle) + dirY * along * Math.sin(angle);
// 붙는 자리의 종단경사 보정 — 본체 끝 링과 같은 값이라야 틈이 0이 된다.
const dz = baseElevationM != null && end?.designZ != null ? end.designZ - baseElevationM : 0;
const wingNorm = Math.hypot(wingX, wingY) || 1;
const ux = wingX / wingNorm;
const uy = wingY / wingNorm;
@@ -147,7 +157,7 @@ export function buildWingSolids(
for (let i = 0; i <= WING_STEPS; i += 1) {
const t = (length * i) / WING_STEPS;
// 패널 두께 방향 = 날개 축의 법선.
rings.push({ cx: rootX + ux * t, cy: rootY + uy * t, leftX: -uy, leftY: ux, dz: 0 });
rings.push({ cx: rootX + ux * t, cy: rootY + uy * t, leftX: -uy, leftY: ux, dz });
const top = anchor.bottomElevation + height0 + ((height1 - height0) * i) / WING_STEPS;
polygons.push([
[-thicknessM / 2, top],
@@ -162,30 +172,33 @@ export function buildWingSolids(
const wingAxis: [number, number] | undefined = end ? [base.leftX, base.leftY] : undefined;
solids.push({ chainage_m: chainageM, kind, polygons, rings, wing: wingMode, wingAxis });
const apron = anchor.apron;
if (!apron) continue;
// 바닥판 종방향 끝의 계류측 모서리까지의 폭 — 뿌리에서 그만큼 벌어져 있다.
const width = apron.outerOffset - anchor.offset;
if (Math.abs(width) < 0.01) continue;
const spread = Math.sign(width);
if (!apron || apron.floor.length < 3) continue;
// 밑변은 **바닥판 끝면 그 자리**에서 시작한다 — 판을 밀어넣은 뿌리(rootX)에서
// 시작하면 밑변이 끝면에서 어긋나 본 바닥과 사이에 단이 생긴다(2026-08-26 사용자).
const tipX = rootX + ux * length;
const tipY = rootY + uy * length;
const runX = tipX - startX;
const runY = tipY - startY;
const run = Math.hypot(runX, runY);
if (run < 0.05) continue;
// 바닥판 단면을 뿌리 offset 기준으로 다시 잡는다 — 끝으로 갈수록 0으로 줄인다.
const relFloor = apron.floor.map(
([offset, elevation]) => [offset - anchor.offset, elevation] as [number, number],
);
const apronRings: StructureFrame[] = [];
const apronPolygons: SectionPolygon[] = [];
for (let i = 0; i <= WING_STEPS; i += 1) {
const t = i / WING_STEPS;
// 링 좌향 = 뿌리→바닥판 끝 방향. 폭을 |P0P2| → 0으로 좁히면 P0·P2·P1 삼각형이다.
apronRings.push({
cx: rootX + ux * length * t,
cy: rootY + uy * length * t,
leftX: base.leftX * spread,
leftY: base.leftY * spread,
dz: 0,
cx: startX + runX * t,
cy: startY + runY * t,
leftX: base.leftX,
leftY: base.leftY,
dz,
});
const span = Math.abs(width) * (1 - t);
apronPolygons.push([
[0, apron.bottomElevation],
[span, apron.bottomElevation],
[span, apron.topElevation],
[0, apron.topElevation],
]);
apronPolygons.push(
relFloor.map(([offset, elevation]) => [offset * (1 - t), elevation] as [number, number]),
);
}
solids.push({
chainage_m: chainageM,