feat(B05): 구조물 구간 성토면을 B06 구간 배열대로 되메운다
B06 횡단이 내는 변형 성토선을 3D 포켓에 되메운다. 다단 기슭막이면 횡단이 성토 > 구조물 > 성토 > 구조물 > 성토라 성토선이 하나가 아니다 — 합치지 않고 구간 1줄 = 리본 1장으로 낸다. - Station_Structure: StructureSilhouette.fillRuns 추가. 31점 폴리라인을 인덱스로 쪼개지 않고 B06 배열(outletFill/basinFill.segments, ford fillSegments, box fillLine)에서 바로 담는다. 벽 전면은 안 담는다 — 구조물 솔리드 몫. - Carve: 패치 리본을 실루엣당 1장이 아니라 run당 1장. 구간 배열을 쓸 때는 벽 트림을 걸지 않는다 — 바깥 끝 하나로 자르는 규칙이라 단 사이 구간이 아랫단 벽 바깥까지 통째로 잘려 사라졌다(실측: 0.86m 구간 2개 전멸). 배열이 비면 리본도 없다. - Build: FILL_PATCH_APPLY 신규(옛 CARVE_APPLY 대체). 투영 커브·절취를 다 낸 뒤 붙인다 — 먼저 넣으면 투영선이 패치에서 z를 읽어 절취 경계가 달라진다. - Viewer: snapCorridorEdges에서 패치 제외. B06 성토 구간은 벽 뒷면·구체 최상단에서 끝나는 게 정상인데 지형에 스냅해 4.1m 세로 지느러미가 생겼다(실측). - BUILD_VERSION 60 -> 63. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,7 +108,7 @@ function fnv1a(text: string): string {
|
||||
// 복원되어 화면·데이터가 하나도 안 바뀌었다. 기하를 고치면 **반드시** 올릴 것.
|
||||
// 56 = 측벽에서 **노견 쪽·성토 끝단 쪽 리본 경계 벽을 뺀다** — 거기서 잘린 게
|
||||
// 아니라 리본이 끝난 자리다(2026-08-27 사용자).
|
||||
const BUILD_VERSION = 60;
|
||||
const BUILD_VERSION = 63;
|
||||
|
||||
/** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */
|
||||
export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string {
|
||||
|
||||
@@ -124,10 +124,15 @@ export interface CorridorBuildResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* 절단 적용 여부(2026-08-25 사용자) — 지금은 **표기 모드**: 서피스를 자르지 않고
|
||||
* 원본 성토면 위에 절단 투영선만 그린다. 절단·패치로 되돌리려면 true.
|
||||
* 구조물 구간 **성토 패치 리본** 적용 여부(2026-08-27 사용자). B06 횡단이 내는
|
||||
* 변형 성토선을 3D 포켓에 되메운다 — 다단이면 `성토 > 구조물 > 성토`라 성토선이
|
||||
* 하나가 아니므로 **구간 1줄 = 리본 1장**으로 낸다(`_Carve.ts` `fillRuns`).
|
||||
*
|
||||
* 절취(`FILL_CUT_APPLY`)와 별개다: 패치는 잘린 자리를 채우기만 하고, 절취·측벽·
|
||||
* 경계 트림(0-2~0-6)은 그대로 둔다. 패치 리본은 절취 대상이 아니다
|
||||
* (`_Cut.ts` `ribbon.patch` 제외).
|
||||
*/
|
||||
const CARVE_APPLY: boolean = false;
|
||||
const FILL_PATCH_APPLY: boolean = true;
|
||||
|
||||
/** 종방향 세분 간격(m) — 곡선 각짐 방지(2026-08-23 사용자: 노선 폴리라인 따라 세분).
|
||||
* 평면 궤적을 스플라인으로 잇게 되어(2026-08-23) 0.1m까지 쪼갤 이유가 없어졌다 —
|
||||
@@ -648,14 +653,6 @@ export function buildCorridor(
|
||||
});
|
||||
if (points.length >= 2) caps.push({ points });
|
||||
});
|
||||
// 패치 리본은 표기 모드에서 끈다(2026-08-25 사용자: 절단하지 말고 투영선만) —
|
||||
// 원본 성토면이 그대로 있어 패치를 겹치면 z-fight만 난다.
|
||||
if (CARVE_APPLY) {
|
||||
ribbons.push(
|
||||
...buildStructurePatchRibbons(crossSections, structures, frameAt, profileZ ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
// 탑뷰 투영 커브(2026-08-26 사용자) — 세트별 평면(구조물 상단 +5m)에 눕힌
|
||||
// 구조물·비탈 커브. 원본 비탈선은 코리도 바깥 윤곽(outline)을 그대로 읽는다.
|
||||
// 리본을 함께 넘겨 파란 스케치 커브를 절·성토면에 얹은 빨간 투영선도 낸다.
|
||||
@@ -668,8 +665,14 @@ export function buildCorridor(
|
||||
// 되어 비스듬한 커브에서 어긋난다(2026-08-26 사용자 지적, `_Cut.ts` 머리말).
|
||||
// 잘린 자리 윤곽(`cut-merged`)은 새 데이텀에 얹어 기존 커브 **뒤에 덧붙인다**.
|
||||
const cut = FILL_CUT_APPLY ? maskFillByPlanCurves(ribbons, planCurves) : null;
|
||||
|
||||
// 성토 패치는 **투영 커브·절취를 다 낸 뒤** 붙인다 — 먼저 넣으면 투영선이 패치
|
||||
// 표면에서 z를 읽어 절취 경계가 달라지고, 절취가 패치를 다시 갉는다.
|
||||
const patchRibbons = FILL_PATCH_APPLY
|
||||
? buildStructurePatchRibbons(crossSections, structures, frameAt, profileZ ?? null)
|
||||
: [];
|
||||
return {
|
||||
ribbons: cut ? cut.ribbons : ribbons,
|
||||
ribbons: [...(cut ? cut.ribbons : ribbons), ...patchRibbons],
|
||||
outline,
|
||||
caps,
|
||||
structures,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
PIECE_COLS,
|
||||
resample,
|
||||
type CorridorKind,
|
||||
type OffsetPoint,
|
||||
} from "./B05_Profile_UI_Corridor_Station";
|
||||
import type { CorridorRibbon } from "./B05_Profile_UI_Corridor_Build";
|
||||
import { groundInterpolator } from "../B06_Section/B06_Section_UI_Cross_Culvert_Solve";
|
||||
@@ -234,97 +235,118 @@ export function buildStructurePatchRibbons(
|
||||
for (const section of crossSections) {
|
||||
const design = section.design;
|
||||
for (const silhouette of structureSilhouettes(section)) {
|
||||
// 패치 전용 점(세월교 성토부선)이 있으면 그걸 쓴다 — 벽 전면·유로를 리본으로
|
||||
// 덮으면 관 구멍·L자 통로가 막힌다(2026-08-25 사용자 ②).
|
||||
let source = silhouette.patchPoints ?? silhouette.points;
|
||||
// 노견 확장점 안쪽(수평 구간)은 노견 조각이 가져갔다 — 패치는 그 바깥부터.
|
||||
// 단 그 측에 측구가 있으면 노견을 안 넓혔으므로 스트립도 하지 않는다 — 하면
|
||||
// 패치가 노견에서 떨어져 뜬다(2026-08-25 사용자 보고).
|
||||
const ditch = design?.ditch;
|
||||
const ditchEnabled = design?.ditch_enabled ?? (ditch != null && ditch.type !== "none");
|
||||
const ditchOnSide = ditchEnabled && design?.ditch_side === silhouette.side;
|
||||
const widen = silhouette.widenTo;
|
||||
if (widen && !silhouette.patchPoints && !ditchOnSide) {
|
||||
const sign = silhouette.side === "left" ? 1 : -1;
|
||||
source = source.filter((point) => (point.offset_m - widen.offset_m) * sign > -1e-9);
|
||||
if (source.length && Math.abs(source[0].offset_m - widen.offset_m) > 1e-9) {
|
||||
source = [widen, ...source];
|
||||
}
|
||||
}
|
||||
if (source.length < 2) continue;
|
||||
// "auto" = 실루엣 가운데 표고가 지반보다 높으면 성토, 낮으면 절토.
|
||||
let kind: CorridorKind = silhouette.kind === "auto" ? "fill" : silhouette.kind;
|
||||
if (silhouette.kind === "auto") {
|
||||
const groundAt = groundInterpolator(section.samples);
|
||||
const mid = silhouette.points[Math.floor(silhouette.points.length / 2)];
|
||||
if (groundAt && mid.elevation_m < groundAt(mid.offset_m) - 0.05) kind = "cut";
|
||||
}
|
||||
const colCount = PIECE_COLS[kind];
|
||||
const from = section.chainage_m - silhouette.span.beforeM;
|
||||
const to = section.chainage_m + silhouette.span.afterM;
|
||||
if (!(to - from > 1e-6)) continue;
|
||||
const rowChainages: number[] = [];
|
||||
for (let at = from; at < to; at += 1) rowChainages.push(at);
|
||||
rowChainages.push(to);
|
||||
const baseZ = profileZ?.(section.chainage_m) ?? null;
|
||||
|
||||
// 행 버퍼 — 벽·날개벽에 통째로 덮인 행에서 끊어 별개 리본으로 내보낸다.
|
||||
let buffer: Array<{ chainage: number; frame: RouteFrame; points: typeof source }> = [];
|
||||
const flush = (): void => {
|
||||
if (buffer.length >= 2) {
|
||||
const positions = new Float64Array(buffer.length * colCount * 3);
|
||||
buffer.forEach((row, index) => {
|
||||
const shift =
|
||||
baseZ != null && row.frame.designZ != null ? row.frame.designZ - baseZ : 0;
|
||||
resample(row.points, colCount).forEach((point, col) => {
|
||||
const i = (index * colCount + col) * 3;
|
||||
positions[i] = row.frame.cx + row.frame.leftX * point.offset_m;
|
||||
positions[i + 1] = row.frame.cy + row.frame.leftY * point.offset_m;
|
||||
positions[i + 2] = point.elevation_m + shift;
|
||||
});
|
||||
});
|
||||
ribbons.push({
|
||||
kind,
|
||||
side: silhouette.side,
|
||||
colCount,
|
||||
chainages: buffer.map((row) => row.chainage),
|
||||
positions,
|
||||
patch: true,
|
||||
});
|
||||
}
|
||||
buffer = [];
|
||||
};
|
||||
|
||||
for (const at of rowChainages) {
|
||||
const frame = frameAt(at);
|
||||
if (!frame) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
let points = source;
|
||||
if (!revets.isEmpty) {
|
||||
const outerCov = revets.outerAt(
|
||||
frame.cx,
|
||||
frame.cy,
|
||||
frame.leftX,
|
||||
frame.leftY,
|
||||
silhouette.side,
|
||||
points[0].offset_m,
|
||||
points[points.length - 1].offset_m,
|
||||
);
|
||||
if (outerCov !== null) {
|
||||
const cut = cutInnerTo(points, outerCov, silhouette.side);
|
||||
if (!cut) {
|
||||
// 이 행은 벽·날개벽이 통째로 덮는다 — 리본을 끊는다(분리 서피스).
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
points = cut;
|
||||
const sign = silhouette.side === "left" ? 1 : -1;
|
||||
// B06이 낸 성토 구간 배열을 그대로 쓴다 — **구간 1줄 = 리본 1장**이라 다단에서도
|
||||
// 줄이 안 섞이고 역주행으로 접히지도 않는다(2026-08-27 사용자). 배열이 없으면
|
||||
// 옛 동작 한 줄: 패치 전용 점(세월교 성토부선)이 있으면 그걸 쓴다 — 벽 전면·유로를
|
||||
// 리본으로 덮으면 관 구멍·L자 통로가 막힌다(2026-08-25 사용자 ②).
|
||||
// 배열이 **비어 있으면** 그 측엔 성토 구간이 없다는 뜻이라 리본도 없다 —
|
||||
// 옛 한 줄로 되돌리면 축퇴 실루엣(같은 점 2개)이 폭 0 리본을 만든다.
|
||||
const runs: OffsetPoint[][] = silhouette.fillRuns ?? [
|
||||
silhouette.patchPoints ?? silhouette.points,
|
||||
];
|
||||
for (let source of runs) {
|
||||
// 노견에서 시작하는 줄만 확장점으로 다듬는다 — 다단 성토 구간처럼 이미 바깥에서
|
||||
// 시작하는 줄에 확장점을 앞에 붙이면 노견에 없는 판이 하나 생긴다(2026-08-27).
|
||||
const widen = silhouette.widenTo;
|
||||
if (
|
||||
widen &&
|
||||
!silhouette.patchPoints &&
|
||||
!ditchOnSide &&
|
||||
source.length > 0 &&
|
||||
(source[0].offset_m - widen.offset_m) * sign <= 1e-9
|
||||
) {
|
||||
source = source.filter((point) => (point.offset_m - widen.offset_m) * sign > -1e-9);
|
||||
if (source.length && Math.abs(source[0].offset_m - widen.offset_m) > 1e-9) {
|
||||
source = [widen, ...source];
|
||||
}
|
||||
}
|
||||
buffer.push({ chainage: at, frame, points });
|
||||
if (source.length < 2) continue;
|
||||
// "auto" = 실루엣 가운데 표고가 지반보다 높으면 성토, 낮으면 절토.
|
||||
let kind: CorridorKind = silhouette.kind === "auto" ? "fill" : silhouette.kind;
|
||||
if (silhouette.kind === "auto") {
|
||||
const groundAt = groundInterpolator(section.samples);
|
||||
const mid = silhouette.points[Math.floor(silhouette.points.length / 2)];
|
||||
if (groundAt && mid.elevation_m < groundAt(mid.offset_m) - 0.05) kind = "cut";
|
||||
}
|
||||
const colCount = PIECE_COLS[kind];
|
||||
const from = section.chainage_m - silhouette.span.beforeM;
|
||||
const to = section.chainage_m + silhouette.span.afterM;
|
||||
if (!(to - from > 1e-6)) continue;
|
||||
const rowChainages: number[] = [];
|
||||
for (let at = from; at < to; at += 1) rowChainages.push(at);
|
||||
rowChainages.push(to);
|
||||
const baseZ = profileZ?.(section.chainage_m) ?? null;
|
||||
|
||||
// 행 버퍼 — 벽·날개벽에 통째로 덮인 행에서 끊어 별개 리본으로 내보낸다.
|
||||
let buffer: Array<{ chainage: number; frame: RouteFrame; points: typeof source }> = [];
|
||||
const flush = (): void => {
|
||||
if (buffer.length >= 2) {
|
||||
const positions = new Float64Array(buffer.length * colCount * 3);
|
||||
buffer.forEach((row, index) => {
|
||||
const shift =
|
||||
baseZ != null && row.frame.designZ != null ? row.frame.designZ - baseZ : 0;
|
||||
resample(row.points, colCount).forEach((point, col) => {
|
||||
const i = (index * colCount + col) * 3;
|
||||
positions[i] = row.frame.cx + row.frame.leftX * point.offset_m;
|
||||
positions[i + 1] = row.frame.cy + row.frame.leftY * point.offset_m;
|
||||
positions[i + 2] = point.elevation_m + shift;
|
||||
});
|
||||
});
|
||||
ribbons.push({
|
||||
kind,
|
||||
side: silhouette.side,
|
||||
colCount,
|
||||
chainages: buffer.map((row) => row.chainage),
|
||||
positions,
|
||||
patch: true,
|
||||
});
|
||||
}
|
||||
buffer = [];
|
||||
};
|
||||
|
||||
for (const at of rowChainages) {
|
||||
const frame = frameAt(at);
|
||||
if (!frame) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
let points = source;
|
||||
// 벽 트림은 **옛 한 줄** 전용이다. 한 줄은 구조물을 가로질러 그어져 벽이 덮은
|
||||
// 안쪽을 잘라내야 했다. B06 구간 배열은 이미 벽 **사이**에 정의돼 있어 겹치는
|
||||
// 건 아랫단 벽 뒷면 0.15m 남짓뿐이고 그것도 불투명한 벽 솔리드 안에 묻힌다.
|
||||
// 반대로 트림을 걸면 바깥 끝 하나로 자르는 규칙이라 단 사이 구간이 아랫단 벽
|
||||
// 바깥까지 통째로 잘려 사라진다(2026-08-27 실측: 0.86m 구간이 벽 행에서 전멸).
|
||||
if (!revets.isEmpty && !silhouette.fillRuns) {
|
||||
const outerCov = revets.outerAt(
|
||||
frame.cx,
|
||||
frame.cy,
|
||||
frame.leftX,
|
||||
frame.leftY,
|
||||
silhouette.side,
|
||||
points[0].offset_m,
|
||||
points[points.length - 1].offset_m,
|
||||
);
|
||||
if (outerCov !== null) {
|
||||
const cut = cutInnerTo(points, outerCov, silhouette.side);
|
||||
if (!cut) {
|
||||
// 이 행은 벽·날개벽이 통째로 덮는다 — 리본을 끊는다(분리 서피스).
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
points = cut;
|
||||
}
|
||||
}
|
||||
buffer.push({ chainage: at, frame, points });
|
||||
}
|
||||
flush();
|
||||
}
|
||||
flush();
|
||||
}
|
||||
}
|
||||
return ribbons;
|
||||
|
||||
@@ -49,6 +49,17 @@ export interface StructureSilhouette {
|
||||
* 관 구멍·유로가 막힌다).
|
||||
*/
|
||||
patchPoints?: OffsetPoint[];
|
||||
/**
|
||||
* B06이 낸 **성토 구간 배열**(2026-08-27 사용자). 다단 기슭막이면 횡단이
|
||||
* `성토 > 구조물 > 성토 > 구조물 > 성토`라 성토선이 하나가 아니다 — 합치지 않고
|
||||
* 그대로 담아 패치 리본이 **구간 1줄 = 서피스 1장**으로 낸다. 벽 전면은 넣지
|
||||
* 않는다(구조물 솔리드 몫). 없으면 옛 동작(`patchPoints ?? points` 한 줄).
|
||||
*
|
||||
* 합치면 안 되는 이유: 다음 단 성토 시작점이 윗단 벽 발끝보다 **안쪽**이라
|
||||
* (윗단 벽 하단 +0.5m와 전면 경사선 1:0.3의 교차점) 편거리가 역주행한다 —
|
||||
* 한 줄 리본으로는 스스로 접힌다.
|
||||
*/
|
||||
fillRuns?: OffsetPoint[][];
|
||||
}
|
||||
|
||||
/** 실루엣에서 노견 확장점 찾기 — 첫 점(노견) 표고 그대로 바깥으로 나간 가장 먼 점. */
|
||||
@@ -102,6 +113,8 @@ export function boxSilhouettes(section: CrossSection): StructureSilhouette[] {
|
||||
kind,
|
||||
keepOrder: true,
|
||||
widenTo: widenToOf(points, side.role),
|
||||
// 구체 최상단에서 끝나는 성토선 한 줄 — BOX는 다단이 없다.
|
||||
fillRuns: [points],
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -141,16 +154,22 @@ export function fordSilhouettes(section: CrossSection): StructureSilhouette[] {
|
||||
// 리본으로 덮으면 관 구멍과 L자 통로가 막힌다(2026-08-25 사용자 ②). 벽·바닥은
|
||||
// 구조물 솔리드가 그린다.
|
||||
const patchPoints: OffsetPoint[] = [];
|
||||
// 성토 구간은 **구간별로도** 담는다 — 패치 리본이 한 장씩 낸다(2026-08-27 사용자).
|
||||
const fillRuns: OffsetPoint[][] = [];
|
||||
// 유출측 다단과 달리 세월교는 단 나눔이 없다 — 접지 후 절토선(kind="cut")은 뺀다.
|
||||
const fill = side.fillSegments.filter((segment) => segment.kind !== "cut");
|
||||
let kind: "cut" | "fill" = "fill";
|
||||
if (fill.length) {
|
||||
fill.forEach((segment) =>
|
||||
fill.forEach((segment) => {
|
||||
const run: OffsetPoint[] = [];
|
||||
segment.points.forEach((point) => {
|
||||
points.push({ offset_m: point.offset, elevation_m: point.elevation });
|
||||
patchPoints.push({ offset_m: point.offset, elevation_m: point.elevation });
|
||||
}),
|
||||
);
|
||||
const at = { offset_m: point.offset, elevation_m: point.elevation };
|
||||
points.push(at);
|
||||
patchPoints.push(at);
|
||||
run.push(at);
|
||||
});
|
||||
if (run.length >= 2) fillRuns.push(run);
|
||||
});
|
||||
} else if (side.cutLine) {
|
||||
// 바닥판이 원지반 안으로 박혔다 — 그만큼 원지반을 걷어낸다.
|
||||
const floorOuter = {
|
||||
@@ -159,10 +178,14 @@ export function fordSilhouettes(section: CrossSection): StructureSilhouette[] {
|
||||
};
|
||||
points.push(floorOuter);
|
||||
patchPoints.push(floorOuter);
|
||||
const run: OffsetPoint[] = [floorOuter];
|
||||
for (const cutPoint of [side.cutLine.from, side.cutLine.to]) {
|
||||
points.push({ offset_m: cutPoint.offset, elevation_m: cutPoint.elevation });
|
||||
patchPoints.push({ offset_m: cutPoint.offset, elevation_m: cutPoint.elevation });
|
||||
const at = { offset_m: cutPoint.offset, elevation_m: cutPoint.elevation };
|
||||
points.push(at);
|
||||
patchPoints.push(at);
|
||||
run.push(at);
|
||||
}
|
||||
fillRuns.push(run);
|
||||
kind = "cut";
|
||||
} else {
|
||||
points.push({
|
||||
@@ -179,6 +202,7 @@ export function fordSilhouettes(section: CrossSection): StructureSilhouette[] {
|
||||
keepOrder: true,
|
||||
widenTo: widenToOf(points, onLeft ? "left" : "right"),
|
||||
patchPoints,
|
||||
fillRuns,
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -212,6 +236,18 @@ export function culvertSilhouettes(section: CrossSection): StructureSilhouette[]
|
||||
);
|
||||
collected.set(side, bucket);
|
||||
};
|
||||
// 성토 구간은 **구간별로도** 담는다 — 다단이면 벽이 사이를 끊어 줄이 여러 개다
|
||||
// (2026-08-27 사용자). 벽 전면은 담지 않는다 — 구조물 솔리드가 그린다.
|
||||
const runs = new Map<"left" | "right", OffsetPoint[][]>();
|
||||
const addRun = (
|
||||
side: "left" | "right",
|
||||
points: ReadonlyArray<{ offset: number; elevation: number }>,
|
||||
): void => {
|
||||
if (points.length < 2) return;
|
||||
const bucket = runs.get(side) ?? [];
|
||||
bucket.push(points.map((point) => ({ offset_m: point.offset, elevation_m: point.elevation })));
|
||||
runs.set(side, bucket);
|
||||
};
|
||||
const trimSlopeOf = (side: "left" | "right") =>
|
||||
side === "left" ? layout.designTrim?.maxSlope : layout.designTrim?.minSlope;
|
||||
|
||||
@@ -227,11 +263,17 @@ export function culvertSilhouettes(section: CrossSection): StructureSilhouette[]
|
||||
const stepCount = Math.max(outletSegments.length, layout.extraWalls.length);
|
||||
for (let step = 0; step < stepCount; step += 1) {
|
||||
const segment = outletSegments[step];
|
||||
if (segment) push(outletSide, segment.points);
|
||||
if (segment) {
|
||||
push(outletSide, segment.points);
|
||||
addRun(outletSide, segment.points);
|
||||
}
|
||||
const wall = layout.extraWalls[step];
|
||||
if (wall) push(outletSide, [wall.topJoint, wall.bottomFront]);
|
||||
}
|
||||
layout.basinFill.segments.forEach((segment) => push(inletSide, segment.points));
|
||||
layout.basinFill.segments.forEach((segment) => {
|
||||
push(inletSide, segment.points);
|
||||
addRun(inletSide, segment.points);
|
||||
});
|
||||
layout.basinExtras.forEach((wall) => push(inletSide, [wall.topJoint, wall.bottomFront]));
|
||||
|
||||
let basinCut = false;
|
||||
@@ -253,6 +295,7 @@ export function culvertSilhouettes(section: CrossSection): StructureSilhouette[]
|
||||
// 집수정이 원지반에 박히면 절토선까지 — 그만큼 원지반을 걷어낸다.
|
||||
if (layout.basin.cutLine) {
|
||||
push(inletSide, [layout.basin.cutLine.from, layout.basin.cutLine.to]);
|
||||
addRun(inletSide, [layout.basin.cutLine.from, layout.basin.cutLine.to]);
|
||||
basinCut = true;
|
||||
}
|
||||
spans[inletSide] = basinSpanOf(section);
|
||||
@@ -271,6 +314,7 @@ export function culvertSilhouettes(section: CrossSection): StructureSilhouette[]
|
||||
// 유출측(성토 + 다단)은 수집 순서 그대로, 유입측(집수정·절토)은 종전 정렬.
|
||||
keepOrder: side === outletSide,
|
||||
widenTo: widenToOf(points, side),
|
||||
fillRuns: runs.get(side) ?? [],
|
||||
});
|
||||
});
|
||||
return result;
|
||||
|
||||
@@ -536,6 +536,10 @@ export function createRouteViewer(): RouteViewer {
|
||||
// 측구는 제외 — 바깥 상단은 노견과 같은 Z가 규칙(2026-08-23 사용자 지시).
|
||||
// 지형 스냅하면 측구 바깥벽이 원지반까지 끌려 올라가 U형이 깨진다.
|
||||
if (ribbon.kind !== "cut" && ribbon.kind !== "fill") return;
|
||||
// 구조물 구간 패치도 제외(2026-08-27) — B06 성토 구간은 벽 뒷면이나 구체
|
||||
// 최상단에서 **끝나는 게 정상**이라 바깥 끝이 지반이 아니다. 스냅하면 그
|
||||
// 자리가 원지반까지 끌려 내려가 세로 지느러미가 생긴다(실측 4.1m).
|
||||
if (ribbon.patch) return;
|
||||
const cols = ribbon.colCount;
|
||||
const outerCol = ribbon.side === "right" ? 0 : cols - 1;
|
||||
const innerCol = ribbon.side === "right" ? cols - 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user