fix(B05): 3D 노선 선이 능선에서 끊겨 보이던 문제 — 2m 간격으로 지형에 붙임

원인은 선의 해상도 (2026-09-06 실측). 노선 정점(간격 중앙값 10.0m)을 곧바로
이어 지형 위 0.35m 로 띄웠는데, 볼록한 능선에서는 그 10m 직선이 지면을 뚫고
들어가 선이 땅속에 잠겼다 — 화면에서는 161+0.0~163+0.0 구간이 통째로 사라졌다.

정점 사이를 2m 간격으로 나눠 각 점의 지형고를 찍는다. 원래 정점은 모두 남기므로
평면 형상은 불변. 끼운 점과 원래 정점이 같은 지형면에서 높이를 받도록 지형고를
먼저 보고, 없을 때만 정점 z 를 쓴다. 곡선 경고 구간은 원래 정점 기준 인덱스라
조밀화 뒤 자리로 옮겨 자른다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 11:32:29 +09:00
co-authored by Claude Opus 5
parent 3def4f18f0
commit fdcd075396
+42 -11
View File
@@ -95,6 +95,14 @@ export function sceneToModel(point: THREE.Vector3, bounds: ModelBounds) {
return { x: point.x + cx, y: -point.z + cy, z: point.y + cz };
}
/** 노선 선을 지형에 붙일 때 정점 사이를 나누는 간격(m) — 2m 면 20m 측점 사이가 10토막이라
* 능선을 가로질러도 선이 지면 아래로 잠기지 않는다(2026-09-06). */
const DRAPE_STEP_M = 2;
/** 한 구간을 나누는 최대 토막 수 — 정점이 아주 멀리 떨어진 자료에서 점이 폭주하지 않게 한다. */
const MAX_DRAPE_STEPS = 64;
/** 노선 선을 지형 위로 띄우는 높이(m) — 지형과 겹쳐 깜빡이지 않을 만큼만. */
const ROUTE_LINE_LIFT_M = 0.35;
export function createRouteMarkers(
scene: THREE.Scene,
getBounds: () => ModelBounds | null,
@@ -254,17 +262,40 @@ export function createRouteMarkers(
disposeGroup(routeGroup);
const bounds = getBounds();
if (!bounds || polyline.length < 2) return;
// 정점 사이를 잘게 나눠 **각 점의 지형고를 찍어** 선을 지면에 붙인다(2026-09-06).
// 원래는 정점(간격 중앙값 10m)을 곧바로 이었는데, 볼록한 능선에서는 그 10m 직선이
// 지면을 뚫고 들어가 선이 땅속에 잠겼다 — 화면에는 폴리라인이 끊긴 것처럼 보였다
// (실측: 161+0.0~163+0.0 구간이 통째로 사라짐). 원래 정점은 모두 남기므로 평면
// 형상은 바뀌지 않는다.
const dense: Array<{ x: number; y: number; z?: number }> = [];
/** 원래 정점 i 가 조밀화 뒤 몇 번째인지 — 경고 구간이 인덱스로 자르므로 필요하다. */
const denseIndex: number[] = [];
polyline.forEach((point, index) => {
if (index > 0) {
const previous = polyline[index - 1];
const span = Math.hypot(point.x - previous.x, point.y - previous.y);
const steps = Math.min(MAX_DRAPE_STEPS, Math.ceil(span / DRAPE_STEP_M));
for (let step = 1; step < steps; step += 1) {
const ratio = step / steps;
dense.push({
x: previous.x + (point.x - previous.x) * ratio,
y: previous.y + (point.y - previous.y) * ratio,
});
}
}
denseIndex[index] = dense.length;
dense.push(point);
});
// 표고 없는 점을 0으로 삼키면 마커와 같은 함정에 빠진다(지형 한참 아래 평면에 눕는다).
// 지형 표면에서 찾아 채우고, 그래도 모르면 직전 점 높이를 이어 쓴다. 경고 구간이
// 인덱스로 이 배열을 다시 자르므로 점 개수는 그대로 두어야 한다.
// 지형 표면에서 찾아 채우고, 그래도 모르면 직전 점 높이를 이어 쓴다. 끼워 넣은 점과
// 원래 정점이 **같은 지형면**에서 높이를 받아야 선이 매끄러우므로 지형고를 먼저 본다.
let lastZ: number | null = null;
const linePoints = polyline.map((point) => {
const resolved = Number.isFinite(point.z)
? (point.z as number)
: (getTerrainZ?.(point.x, point.y) ?? lastZ);
const linePoints = dense.map((point) => {
const sampled = getTerrainZ?.(point.x, point.y) ?? null;
const resolved = sampled ?? (Number.isFinite(point.z) ? (point.z as number) : lastZ);
if (resolved !== null) lastZ = resolved;
return modelToScene({ x: point.x, y: point.y, z: resolved ?? bounds.z[0] }, bounds).add(
new THREE.Vector3(0, 0.35, 0),
new THREE.Vector3(0, ROUTE_LINE_LIFT_M, 0),
);
});
routeGroup.add(
@@ -274,10 +305,10 @@ export function createRouteMarkers(
),
);
warnings.forEach((warning) => {
const segment = linePoints.slice(
Math.max(0, warning.polyline_start_index),
warning.polyline_end_index + 1,
);
// 경고 구간의 인덱스는 **원래 정점 기준**이라 조밀화 뒤 자리로 옮겨 자른다.
const from = denseIndex[Math.max(0, warning.polyline_start_index)] ?? 0;
const to = denseIndex[Math.min(warning.polyline_end_index, polyline.length - 1)];
const segment = linePoints.slice(from, (to ?? linePoints.length - 1) + 1);
if (segment.length > 1) {
routeGroup.add(
new THREE.Line(