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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user