diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts index 6427f480..84093cd8 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts @@ -109,6 +109,14 @@ export function boreRingChainages( return [...values].sort((a, b) => a - b); } +/** 그 측의 날개벽 제원 — 유입/유출은 상단측(uphill)이 어느 쪽이냐로 갈린다. */ +function wingOf(section: CrossSection, role: "left" | "right") { + const box = section.box; + if (!box) return null; + const inletOnLeft = (section.uphill_side ?? "left") === "left"; + return (role === "left") === inletOnLeft ? box.wing_in : box.wing_out; +} + /** 링 누가거리 목록 → 프레임(실패하면 측점 프레임으로 대체). */ function ringsAt( chainages: number[], @@ -189,14 +197,46 @@ export function boxSolids( }); } - // 에이프런 2매 — 날개벽 구간 바닥, 각 끝 표고에서 수평. + // 에이프런 2매 — 날개벽 구간 바닥. 각 끝 표고에서 수평이고, **평면에서는 사다리꼴** + // 이다(2026-08-25 사용자) — 날개벽이 각도만큼 벌어지므로 바닥도 그만큼 넓어져야 + // 날개벽 발치가 바닥에 얹힌다. for (const side of layout.sides) { if (!side.apron.length) continue; + const wing = wingOf(section, side.role); + const angle = ((wing?.angle_deg ?? 45) * Math.PI) / 180; + const length = Math.max(wing?.length_m ?? 0, 0); + const flare = length * Math.sin(angle); + const outward = side.role === "left" ? 1 : -1; + const innerX = side.apron[0].offset; + const outerX = side.apron[1].offset; + const top = side.apron[0].elevation; + const bottom = side.apron[3].elevation; + // |y| ≤ 구체 반폭이면 구체 끝면부터, 그 밖은 날개벽 선을 따라 안쪽 변이 물러난다. + const innerAt = (offsetFromCenter: number): number => { + const beyond = Math.abs(offsetFromCenter) - halfSpan; + if (beyond <= 0) return innerX; + const run = flare > 1e-6 ? (beyond / flare) * Math.abs(outerX - innerX) : 0; + return innerX + outward * Math.min(run, Math.abs(outerX - innerX)); + }; + const reach = halfSpan + flare; + const apronPlan = [-reach, -halfSpan, halfSpan, reach]; solids.push({ chainage_m: chainage, kind: "basin", - polygons: repeat(side.apron, bodyRings.length), - rings: bodyRings, + polygons: apronPlan.map((at) => { + const inner = innerAt(at); + return [ + [inner, top], + [outerX, top], + [outerX, bottom], + [inner, bottom], + ] as SectionPolygon; + }), + rings: ringsAt( + apronPlan.map((at) => chainage + at), + frameAt, + stationFrame, + ), }); } @@ -246,8 +286,11 @@ function wingSolids( const height0 = side.topElevation - side.bottomElevation; const height1 = Math.max(wing.height_m ?? height0, 0.3); for (const along of [1, -1]) { - const startX = here.cx + here.leftX * side.offset + dirX * along * halfSpan; - const startY = here.cy + here.leftY * side.offset + dirY * along * halfSpan; + // 시작점은 **그 모서리 링의 프레임**으로 잡는다 — 중앙 프레임으로 잡으면 곡선 + // 구간에서 구체 끝과 어긋난다(2026-08-25 사용자 지적). + const cornerFrame = frameAt(chainage + along * halfSpan) ?? here; + const startX = cornerFrame.cx + cornerFrame.leftX * side.offset; + const startY = cornerFrame.cy + cornerFrame.leftY * side.offset; // 날개 방향 = 구체 축을 도로 방향으로 각도만큼 튼 단위벡터. const wingX = here.leftX * axisSign * Math.cos(angle) + dirX * along * Math.sin(angle); const wingY = here.leftY * axisSign * Math.cos(angle) + dirY * along * Math.sin(angle);