feat(B05): 세월교 날개벽을 노선 흐름에 태우고 바닥 에이프런 추가
세월교 측벽·바닥판은 ringsOf(half, half)로 노선 스플라인을 따라가는데 날개벽만 기준 측점 프레임에서 직선으로 ±월류폭/2 자리에 뿌리를 잡아, 곡선 구간에서 측벽 끝단과 어긋났다. BOX암거는 직선 규칙이 맞으므로 건드리지 않는다(사용자 ②). - `buildWingSolids`에 선택 인자 `endFrameAt` 추가 — 넘기면 날개 뿌리와 축을 측벽 끝단부 자리의 노선 프레임에서 뽑는다. 세월교만 `frameAt`을 넘긴다. 안 넘기면 계산이 옛 식과 완전히 같다(BOX 무변화). - `WingAnchor.apron` 추가 — 세월교 바닥 에이프런. 날개 끝점과 바닥판 종방향 끝점을 이은 직선을 외곽으로 하는 평면 삼각형을 바닥 두께로 세운다(사용자 ④). 링을 뿌리→날개끝으로 깔고 링 좌향을 뿌리→바닥판끝으로 잡아 폭을 |P0P2| → 0으로 좁히는 방식이라 기존 로프트 모델을 그대로 쓴다. - 에이프런도 `wing` 표식을 달므로 성토끝 박스 대상은 `kind === "revet"`로 좁혔다. - `BUILD_VERSION` 33 → 34. 실측: BOX 200.92 날개 뿌리 이동량 0.000m·에이프런 0개(무변화 확인). 세월교 149.73 뿌리 이동량 0.102~1.119m, 뿌리가 측벽 끝단 링 횡단선 위(종방향 잔차 0). 에이프런 4장, 바깥 모서리가 기준 직선 위 이탈 0.0000m. 성토끝 박스는 두 세트 모두 4/4 — 앞서 막혀 있던 149.73 우측 날개 1장도 뿌리 이동으로 풀렸다. 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 = 33; // 날개벽 판 투영 + 성토끝 박스(2026-08-26).
|
||||
const BUILD_VERSION = 34; // 세월교 날개벽 노선 추종 + 바닥 에이프런(2026-08-26).
|
||||
|
||||
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
|
||||
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
|
||||
|
||||
@@ -214,7 +214,8 @@ export function wingBoxLoops(
|
||||
const loops: Array<Array<[number, number, number]>> = [];
|
||||
for (const structure of set) {
|
||||
const rings = structure.rings;
|
||||
if (!structure.wing || !rings || rings.length < 2) continue;
|
||||
// 날개벽(revet)만 — 세월교 바닥 에이프런(basin)도 `wing` 표식을 달지만 박스는 없다.
|
||||
if (!structure.wing || structure.kind !== "revet" || !rings || rings.length < 2) continue;
|
||||
if (wingSide(rings, axis) !== side) continue;
|
||||
const start = rings[0];
|
||||
const end = rings[rings.length - 1];
|
||||
|
||||
@@ -428,6 +428,12 @@ 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] = 하단-전면) — 여기서 벌어져 나간다.
|
||||
@@ -435,9 +441,18 @@ export function buildCorridorStructures(
|
||||
topElevation: wall.points[2].elevation,
|
||||
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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
"revet",
|
||||
// 세월교만 — 날개 뿌리·각도를 측벽 끝단부 자리의 노선 프레임에서 뽑는다.
|
||||
frameAt,
|
||||
),
|
||||
);
|
||||
// 관은 련수만큼 폭 안에 등간격으로 놓는다(단면엔 1개만 보이지만 실물은 여러 련).
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { FordWingSpec } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { CorridorStructure, StructureFrame } from "./B05_Profile_UI_Corridor_Structures";
|
||||
import type {
|
||||
CorridorStructure,
|
||||
RouteFrame,
|
||||
StructureFrame,
|
||||
} from "./B05_Profile_UI_Corridor_Structures";
|
||||
import type { SectionPolygon } from "./B05_Profile_UI_Corridor_Structures_Box";
|
||||
|
||||
/** 날개벽이 붙는 구조물 한쪽 끝 — 좌·우 각각 하나. */
|
||||
@@ -26,6 +30,17 @@ export interface WingAnchor {
|
||||
bottomElevation: number;
|
||||
/** 그 측에 적용할 날개벽 제원. */
|
||||
wing: FordWingSpec;
|
||||
/**
|
||||
* 세월교 전용 — 바닥 에이프런(2026-08-26 사용자 ④). 있으면 **날개 끝점과 바닥판
|
||||
* 종방향 끝점을 이은 직선**을 외곽으로 하는 평면 삼각형을 바닥 두께로 세운다.
|
||||
* BOX암거는 안 넘긴다(현행 유지 — 사용자 ②).
|
||||
*/
|
||||
apron?: {
|
||||
/** 바닥판 종방향 끝의 계류측 모서리 offset. */
|
||||
outerOffset: number;
|
||||
topElevation: number;
|
||||
bottomElevation: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 날개 축 방향 분할 수 — 높이 체감을 담을 최소 해상도. */
|
||||
@@ -44,10 +59,13 @@ export function buildWingSolids(
|
||||
thicknessM: number,
|
||||
anchors: WingAnchor[],
|
||||
kind: CorridorStructure["kind"] = "revet",
|
||||
/**
|
||||
* 넘기면 날개 뿌리·축을 **측벽 끝단부 자리의 노선 프레임**에서 뽑는다 —
|
||||
* 세월교는 곡선에서 측벽이 노선을 따라 휘므로 직선 규칙이면 끝단과 어긋난다
|
||||
* (2026-08-26 사용자 ③). 안 넘기면 현행 그대로 = BOX암거(사용자 ②).
|
||||
*/
|
||||
endFrameAt?: (chainageM: number) => RouteFrame | null,
|
||||
): CorridorStructure[] {
|
||||
// 노선 진행 방향 = 좌향 벡터를 90° 돌린 것.
|
||||
const dirX = -frame.leftY;
|
||||
const dirY = frame.leftX;
|
||||
const solids: CorridorStructure[] = [];
|
||||
|
||||
for (const anchor of anchors) {
|
||||
@@ -60,12 +78,19 @@ export function buildWingSolids(
|
||||
const height0 = anchor.topElevation - anchor.bottomElevation;
|
||||
const height1 = Math.max(wing.height_m ?? height0, 0.3);
|
||||
for (const along of [1, -1]) {
|
||||
// 모서리 = 측점 프레임에서 진행 방향으로 반폭, 좌향으로 구체 끝 offset.
|
||||
const startX = frame.cx + dirX * along * halfSpanM + frame.leftX * anchor.offset;
|
||||
const startY = frame.cy + dirY * along * halfSpanM + frame.leftY * anchor.offset;
|
||||
// 뿌리를 낼 프레임 — 세월교는 측벽 끝단 자리의 노선 프레임, BOX는 기준 측점.
|
||||
const end = endFrameAt?.(chainageM + along * halfSpanM) ?? null;
|
||||
const base = end ?? frame;
|
||||
// 노선 진행 방향 = 좌향 벡터를 90° 돌린 것.
|
||||
const dirX = -base.leftY;
|
||||
const dirY = base.leftX;
|
||||
// 끝단 프레임을 쓰면 그 자리가 이미 측점 ± 반폭이라 진행 방향 이동이 없다.
|
||||
const reach = end ? 0 : halfSpanM;
|
||||
const startX = base.cx + dirX * along * reach + base.leftX * anchor.offset;
|
||||
const startY = base.cy + dirY * along * reach + base.leftY * anchor.offset;
|
||||
// 날개 방향 = 구체 축을 도로 방향으로 각도만큼 튼 단위벡터.
|
||||
const wingX = frame.leftX * axisSign * Math.cos(angle) + dirX * along * Math.sin(angle);
|
||||
const wingY = frame.leftY * axisSign * Math.cos(angle) + dirY * along * Math.sin(angle);
|
||||
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);
|
||||
const wingNorm = Math.hypot(wingX, wingY) || 1;
|
||||
const ux = wingX / wingNorm;
|
||||
const uy = wingY / wingNorm;
|
||||
@@ -84,6 +109,39 @@ export function buildWingSolids(
|
||||
]);
|
||||
}
|
||||
solids.push({ chainage_m: chainageM, kind, polygons, rings, wing: true });
|
||||
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);
|
||||
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: startX + ux * length * t,
|
||||
cy: startY + uy * length * t,
|
||||
leftX: base.leftX * spread,
|
||||
leftY: base.leftY * spread,
|
||||
dz: 0,
|
||||
});
|
||||
const span = Math.abs(width) * (1 - t);
|
||||
apronPolygons.push([
|
||||
[0, apron.bottomElevation],
|
||||
[span, apron.bottomElevation],
|
||||
[span, apron.topElevation],
|
||||
[0, apron.topElevation],
|
||||
]);
|
||||
}
|
||||
solids.push({
|
||||
chainage_m: chainageM,
|
||||
kind: "basin",
|
||||
polygons: apronPolygons,
|
||||
rings: apronRings,
|
||||
wing: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return solids;
|
||||
|
||||
Reference in New Issue
Block a user