fix(B05): 변형 실루엣 투영 커브를 행마다 지반선에 트림
실루엣은 소유 측점 단면 한 장이라 그대로 밀면 구간 내내 같은 폭이 됐다. 0.5m 행마다 종단경사만큼 올린 뒤 `trimSlopeToGround`로 그 행 지반과 만나는 자리까지 잘라 실제 발끝을 따라가게 한다. 표기 커브만 바뀐다 — 서피스 절단· 패치는 손대지 않았다(`CARVE_APPLY` 계속 false, 사용자 지시). 4+4.3 우측 실측: 바깥끝 offset −3.25 고정 → −2.96~−4.42(지형 따라), 행 11 → 21. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@ import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { CorridorStructure, RouteFrame } from "./B05_Profile_UI_Corridor_Structures";
|
||||
import { structureSilhouettes } from "./B05_Profile_UI_Corridor_Station_Structure";
|
||||
import { assembleStripLoops, CarveFootprint, ringEdges } from "./B05_Profile_UI_Corridor_Carve";
|
||||
import { mixedGround, trimSlopeToGround } from "./B05_Profile_UI_Corridor_Station";
|
||||
import { groundInterpolator } from "../B06_Section/B06_Section_UI_Cross_Culvert_Solve";
|
||||
import type { PXY } from "./B05_Profile_UI_Corridor_Union";
|
||||
|
||||
export type PlanCurveSource = "structure" | "slope-deformed" | "slope-original";
|
||||
@@ -40,7 +42,7 @@ export interface PlanCurve {
|
||||
const PLANE_LIFT_M = 5;
|
||||
|
||||
/** 비탈 스트립 종방향 걸음(m). */
|
||||
const STEP_M = 1.0;
|
||||
const STEP_M = 0.5;
|
||||
|
||||
/** 안팎 폭이 이만큼은 돼야 커브를 만든다(m). */
|
||||
const MIN_WIDTH_M = 0.05;
|
||||
@@ -62,6 +64,30 @@ interface PlanFrame {
|
||||
leftY: number;
|
||||
}
|
||||
|
||||
/** 누가거리 → 그 행의 지반선. 인접 두 측점 지반을 섞는다(코리도 빌드와 같은 방식). */
|
||||
function buildGroundAt(
|
||||
crossSections: ReadonlyArray<CrossSection>,
|
||||
): (chainageM: number) => ((offset: number) => number) | null {
|
||||
const rows = crossSections
|
||||
.map((section) => ({
|
||||
chainage: section.chainage_m,
|
||||
ground: groundInterpolator(section.samples),
|
||||
}))
|
||||
.sort((a, b) => a.chainage - b.chainage);
|
||||
return (chainageM) => {
|
||||
if (!rows.length) return null;
|
||||
for (let i = 0; i < rows.length - 1; i += 1) {
|
||||
const lo = rows[i];
|
||||
const hi = rows[i + 1];
|
||||
if (chainageM < lo.chainage || chainageM > hi.chainage) continue;
|
||||
const span = hi.chainage - lo.chainage;
|
||||
const t = span > 1e-6 ? (chainageM - lo.chainage) / span : 0;
|
||||
return mixedGround(lo.ground, hi.ground, t);
|
||||
}
|
||||
return chainageM <= rows[0].chainage ? rows[0].ground : rows[rows.length - 1].ground;
|
||||
};
|
||||
}
|
||||
|
||||
function at(frame: PlanFrame, offset: number): PXY {
|
||||
return { x: frame.cx + frame.leftX * offset, y: frame.cy + frame.leftY * offset };
|
||||
}
|
||||
@@ -165,6 +191,7 @@ export function buildPlanCurves(
|
||||
outline: CorridorOutline,
|
||||
): PlanCurve[] {
|
||||
const curves: PlanCurve[] = [];
|
||||
const groundAt = buildGroundAt(crossSections);
|
||||
for (const section of crossSections) {
|
||||
const set = structures.filter(
|
||||
(structure) => Math.abs(structure.chainage_m - section.chainage_m) < 1e-6,
|
||||
@@ -213,30 +240,41 @@ export function buildPlanCurves(
|
||||
const side = silhouette.side;
|
||||
const sign = side === "left" ? 1 : -1;
|
||||
const roadEdge = roadEdgeOf(side);
|
||||
// 변형 실루엣 — [노견 확장끝 → 실루엣 바깥끝].
|
||||
// 변형 실루엣 — [노견 확장끝 → 실루엣이 그 행 지반과 만나는 자리].
|
||||
// 실루엣은 소유 측점 단면 한 장이라 그대로 밀면 구간 내내 같은 폭이 된다
|
||||
// (2026-08-26 사용자 ②) — 행마다 지반선에 트림해 실제 발끝을 따라가게 한다.
|
||||
// 서피스는 여전히 안 건드린다 — 표기 커브만 바뀐다.
|
||||
const inner = silhouette.widenTo?.offset_m ?? roadEdge;
|
||||
let outer = inner;
|
||||
for (const point of silhouette.points) {
|
||||
if ((point.offset_m - outer) * sign > 0) outer = point.offset_m;
|
||||
}
|
||||
const from = section.chainage_m - silhouette.span.beforeM;
|
||||
const to = section.chainage_m + silhouette.span.afterM;
|
||||
if ((outer - inner) * sign > MIN_WIDTH_M) {
|
||||
const steps: number[] = [];
|
||||
for (let atM = from; atM < to; atM += STEP_M) steps.push(atM);
|
||||
steps.push(to);
|
||||
push(
|
||||
"slope-deformed",
|
||||
side,
|
||||
stripLoops(
|
||||
steps.map((atM) => {
|
||||
const frame = frameOf(atM);
|
||||
return [at(frame, inner), at(frame, outer)] as [PXY, PXY];
|
||||
}),
|
||||
planZ,
|
||||
),
|
||||
);
|
||||
// "auto" = 실루엣 가운데가 지반보다 높으면 성토, 낮으면 절토(패치 리본과 같은 판정).
|
||||
let kind: "cut" | "fill" = silhouette.kind === "auto" ? "fill" : silhouette.kind;
|
||||
if (silhouette.kind === "auto") {
|
||||
const stationGround = groundInterpolator(section.samples);
|
||||
const mid = silhouette.points[Math.floor(silhouette.points.length / 2)];
|
||||
if (stationGround && mid.elevation_m < stationGround(mid.offset_m) - 0.05) kind = "cut";
|
||||
}
|
||||
const baseZ = frameOf(section.chainage_m).designZ;
|
||||
const steps: number[] = [];
|
||||
for (let atM = from; atM < to; atM += STEP_M) steps.push(atM);
|
||||
steps.push(to);
|
||||
const deformedRows = steps.map((atM): [PXY, PXY] | null => {
|
||||
const frame = frameOf(atM);
|
||||
const ground = groundAt(atM);
|
||||
if (!ground) return null;
|
||||
// 종단 경사만큼 실루엣을 통째로 올리고 내린다 — 측점 단면은 그 측점 계획고 기준이다.
|
||||
const shift = baseZ != null && frame.designZ != null ? frame.designZ - baseZ : 0;
|
||||
const placed = silhouette.points.map((point) => ({
|
||||
offset_m: point.offset_m,
|
||||
elevation_m: point.elevation_m + shift,
|
||||
}));
|
||||
const trimmed = trimSlopeToGround(placed, kind, ground);
|
||||
if (!trimmed) return null;
|
||||
const outer = trimmed[trimmed.length - 1].offset_m;
|
||||
if ((outer - inner) * sign <= MIN_WIDTH_M) return null;
|
||||
return [at(frame, inner), at(frame, outer)];
|
||||
});
|
||||
push("slope-deformed", side, stripLoops(deformedRows, planZ));
|
||||
// 원본 성토·절토선 — [노견 끝 → 코리도 바깥 윤곽(catch)]. 구조물이 없다고 본 면이다.
|
||||
const rows: Array<[PXY, PXY] | null> = [];
|
||||
outline.chainages.forEach((chainageM, index) => {
|
||||
|
||||
Reference in New Issue
Block a user