From ce77e0735f6e6ff52a6174215b7e7c465bdeafb5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 26 Aug 2026 18:38:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=84=B8=EC=9B=94=EA=B5=90=20?= =?UTF-8?q?=EB=B3=B8=20=EB=B0=94=EB=8B=A5=C2=B7=EB=82=A0=EA=B0=9C=20?= =?UTF-8?q?=EB=B0=94=EB=8B=A5=C2=B7=EC=97=90=EC=9D=B4=ED=94=84=EB=9F=B0?= =?UTF-8?q?=EC=9D=84=20=ED=95=9C=20=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 원인은 종단경사 보정(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) --- B05_Profile/B05_Profile_UI_Corridor.ts | 2 +- .../B05_Profile_UI_Corridor_Structures.ts | 17 +++--- ...B05_Profile_UI_Corridor_Structures_Wing.ts | 61 +++++++++++-------- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 7cd2dc5d..5c4aca03 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 = 38; // BOX암거 비탈 커브를 박스 방향 단일축으로(2026-08-26). +const BUILD_VERSION = 40; // 세월교 날개·에이프런을 종단경사에 태움(2026-08-26). /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ 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 11854805..834ff200 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts @@ -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개만 보이지만 실물은 여러 련). diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts index 558bfefc..10777549 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Wing.ts @@ -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,