From 18982984729b60290960eebb6f6e86e3575b04a7 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 13 May 2026 13:18:10 +0200 Subject: [PATCH] Implement visibility clipping and segmenting logic for spline road meshes --- src/geometry/spline-road-mesh.ts | 127 ++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 2 deletions(-) diff --git a/src/geometry/spline-road-mesh.ts b/src/geometry/spline-road-mesh.ts index d51c8cea..46f80f34 100644 --- a/src/geometry/spline-road-mesh.ts +++ b/src/geometry/spline-road-mesh.ts @@ -13,6 +13,7 @@ import { sampleTerrainHeightAtWorldPosition, type Terrain } from "../document/terrains"; +import type { SplineCorridorPathClipInterval } from "../spline-corridor/spline-corridor-clips"; export interface SplineRoadPathPointLike { id?: string; @@ -93,6 +94,10 @@ function addVec3(left: Vec3, right: Vec3): Vec3 { }; } +function lerp(start: number, end: number, alpha: number): number { + return start + (end - start) * alpha; +} + function normalizePathPoint( point: SplineRoadPathPointLike, index: number @@ -216,6 +221,118 @@ function buildRoadStations( return stations; } +function interpolateStation( + start: RoadStation, + end: RoadStation, + distance: number +): RoadStation { + const length = end.distance - start.distance; + const alpha = length <= 1e-8 ? 0 : (distance - start.distance) / length; + + return { + center: { + x: lerp(start.center.x, end.center.x, alpha), + y: lerp(start.center.y, end.center.y, alpha), + z: lerp(start.center.z, end.center.z, alpha) + }, + distance, + tangent: cloneVec3(start.tangent) + }; +} + +function subtractClipIntervalsFromRange( + startDistance: number, + endDistance: number, + clipIntervals: readonly SplineCorridorPathClipInterval[] +): Array<{ startDistance: number; endDistance: number }> { + let ranges = [{ startDistance, endDistance }]; + + for (const clipInterval of clipIntervals) { + const nextRanges: typeof ranges = []; + + for (const range of ranges) { + if ( + clipInterval.endDistance <= range.startDistance || + clipInterval.startDistance >= range.endDistance + ) { + nextRanges.push(range); + continue; + } + + if (clipInterval.startDistance > range.startDistance) { + nextRanges.push({ + startDistance: range.startDistance, + endDistance: Math.min(clipInterval.startDistance, range.endDistance) + }); + } + + if (clipInterval.endDistance < range.endDistance) { + nextRanges.push({ + startDistance: Math.max(clipInterval.endDistance, range.startDistance), + endDistance: range.endDistance + }); + } + } + + ranges = nextRanges; + } + + return ranges.filter( + (range) => range.endDistance - range.startDistance > 1e-8 + ); +} + +function buildVisibleRoadStationRuns( + stations: readonly RoadStation[], + clipIntervals: readonly SplineCorridorPathClipInterval[] +): RoadStation[][] { + if (clipIntervals.length === 0) { + return [[...stations]]; + } + + const runs: RoadStation[][] = []; + let currentRun: RoadStation[] = []; + + for (let index = 0; index < stations.length - 1; index += 1) { + const start = stations[index]!; + const end = stations[index + 1]!; + + if (end.distance - start.distance <= 1e-8) { + continue; + } + + const visibleRanges = subtractClipIntervalsFromRange( + start.distance, + end.distance, + clipIntervals + ); + + for (const range of visibleRanges) { + const rangeStart = interpolateStation(start, end, range.startDistance); + const rangeEnd = interpolateStation(start, end, range.endDistance); + const previous = currentRun[currentRun.length - 1]; + + if ( + previous === undefined || + Math.abs(previous.distance - rangeStart.distance) > 1e-6 + ) { + if (currentRun.length >= 2) { + runs.push(currentRun); + } + currentRun = [rangeStart]; + } + + currentRun.push(rangeEnd); + } + } + + if (currentRun.length >= 2) { + runs.push(currentRun); + } + + return runs; +} + function createGeometryFromMeshData(meshData: SplineRoadMeshData) { const geometry = new BufferGeometry(); geometry.setAttribute( @@ -274,17 +391,23 @@ function buildEdgeProfile(options: { function addEdgeProfileCaps(options: { indices: number[]; profileVertexCount: number; + vertexOffset: number; stationCount: number; }) { if (options.profileVertexCount < 3 || options.stationCount < 2) { return; } - const lastRow = (options.stationCount - 1) * options.profileVertexCount; + const lastRow = + options.vertexOffset + (options.stationCount - 1) * options.profileVertexCount; for (let index = 1; index < options.profileVertexCount - 1; index += 1) { // Start cap faces backward along the spline; end cap faces forward. - options.indices.push(0, index + 1, index); + options.indices.push( + options.vertexOffset, + options.vertexOffset + index + 1, + options.vertexOffset + index + ); options.indices.push(lastRow, lastRow + index, lastRow + index + 1); } }