From 9d58c1153b6a91d98d72afef204526b198731f9f Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 13 May 2026 16:37:33 +0200 Subject: [PATCH] Add unit tests for spline corridor junction mesh generation --- .../spline-corridor-junction-mesh.test.ts | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tests/geometry/spline-corridor-junction-mesh.test.ts diff --git a/tests/geometry/spline-corridor-junction-mesh.test.ts b/tests/geometry/spline-corridor-junction-mesh.test.ts new file mode 100644 index 00000000..a683da49 --- /dev/null +++ b/tests/geometry/spline-corridor-junction-mesh.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, it } from "vitest"; + +import { createScenePath } from "../../src/document/paths"; +import { createSplineCorridorJunction } from "../../src/document/spline-corridor-junctions"; +import { + buildSplineCorridorJunctionFootprint, + sampleSplineCorridorJunctionFootprintInfluence +} from "../../src/geometry/spline-corridor-junction-footprint"; +import { buildSplineCorridorJunctionMeshGeometry } from "../../src/geometry/spline-corridor-junction-mesh"; + +function createRoadPath( + id: string, + points: Array<{ x: number; y?: number; z: number }> +) { + return createScenePath({ + id, + road: { + enabled: true, + width: 2, + shoulderWidth: 1, + heightOffset: 0.1 + }, + points: points.map((point, index) => ({ + id: `${id}-point-${index}`, + position: { + x: point.x, + y: point.y ?? 0, + z: point.z + } + })) + }); +} + +describe("spline corridor junction mesh generation", () => { + it("builds a connection-shaped footprint from clipped corridor boundaries", () => { + const pathA = createRoadPath("path-junction-footprint-a", [ + { x: 0, z: 0 }, + { x: 10, z: 0 } + ]); + const pathB = createRoadPath("path-junction-footprint-b", [ + { x: 5, z: -5 }, + { x: 5, z: 5 } + ]); + const junction = createSplineCorridorJunction({ + id: "junction-footprint", + center: { x: 5, y: 0, z: 0 }, + radius: 3, + connections: [ + { + pathId: pathA.id, + progress: 0.5, + clipDistance: 2 + }, + { + pathId: pathB.id, + progress: 0.5, + clipDistance: 2 + } + ] + }); + + const footprint = buildSplineCorridorJunctionFootprint({ + junction, + paths: [pathA, pathB] + }); + + expect(footprint).not.toBeNull(); + expect(footprint?.points).toHaveLength(8); + expect(footprint?.bounds).toEqual({ + minX: 3, + maxX: 7, + minZ: -2, + maxZ: 2 + }); + }); + + it("builds junction fill geometry from the connection footprint instead of a circle", () => { + const pathA = createRoadPath("path-junction-mesh-a", [ + { x: 0, z: 0 }, + { x: 10, z: 0 } + ]); + const pathB = createRoadPath("path-junction-mesh-b", [ + { x: 5, z: -5 }, + { x: 5, z: 5 } + ]); + const junction = createSplineCorridorJunction({ + id: "junction-mesh", + center: { x: 5, y: 0, z: 0 }, + radius: 3, + connections: [ + { + pathId: pathA.id, + progress: 0.5, + clipDistance: 2 + }, + { + pathId: pathB.id, + progress: 0.5, + clipDistance: 2 + } + ] + }); + + const geometry = buildSplineCorridorJunctionMeshGeometry({ + junction, + paths: [pathA, pathB] + }); + const positionAttribute = geometry?.getAttribute("position"); + + expect(positionAttribute?.count).toBe(9); + expect(Array.from(positionAttribute?.array ?? [])).toEqual( + expect.arrayContaining([ + 3, + expect.closeTo(0.1, 5), + -1, + 7, + expect.closeTo(0.1, 5), + 1, + 4, + expect.closeTo(0.1, 5), + -2, + 6, + expect.closeTo(0.1, 5), + 2 + ]) + ); + }); + + it("samples full terrain influence inside the connection footprint and falloff outside it", () => { + const pathA = createRoadPath("path-junction-influence-a", [ + { x: 0, z: 0 }, + { x: 10, z: 0 } + ]); + const pathB = createRoadPath("path-junction-influence-b", [ + { x: 5, z: -5 }, + { x: 5, z: 5 } + ]); + const junction = createSplineCorridorJunction({ + id: "junction-influence", + center: { x: 5, y: 0, z: 0 }, + radius: 3, + connections: [ + { + pathId: pathA.id, + progress: 0.5, + clipDistance: 2 + }, + { + pathId: pathB.id, + progress: 0.5, + clipDistance: 2 + } + ] + }); + const footprint = buildSplineCorridorJunctionFootprint({ + junction, + paths: [pathA, pathB] + }); + + if (footprint === null) { + throw new Error("Expected a junction footprint."); + } + + expect( + sampleSplineCorridorJunctionFootprintInfluence({ + footprint, + x: 5, + z: 0, + falloffDistance: 1 + }) + ).toBe(1); + expect( + sampleSplineCorridorJunctionFootprintInfluence({ + footprint, + x: 2.5, + z: 0, + falloffDistance: 1 + }) + ).toBeGreaterThan(0); + expect( + sampleSplineCorridorJunctionFootprintInfluence({ + footprint, + x: 1, + z: 0, + falloffDistance: 1 + }) + ).toBe(0); + }); +});