import { createOpaqueId } from "../core/ids"; import type { Vec3 } from "../core/vector"; import { createScenePathRoadEdgeSettings, type ScenePathRoadEdgeSettings } from "./paths"; export type SplineCorridorJunctionTerrainMode = | "none" | "paintOnly" | "flattenAndPaint"; export type SplineCorridorJunctionShapeMode = "straight" | "curve" | "corner"; export interface SplineCorridorJunctionConnection { id: string; pathId: string; progress: number; clipDistance: number; } export interface SplineCorridorJunction { id: string; kind: "splineCorridorJunction"; name?: string; visible: boolean; enabled: boolean; center: Vec3; radius: number; materialId: string | null; terrainMode: SplineCorridorJunctionTerrainMode; shapeMode: SplineCorridorJunctionShapeMode; edge: ScenePathRoadEdgeSettings; connections: SplineCorridorJunctionConnection[]; } export type SplineCorridorJunctionRegistry = Record< string, SplineCorridorJunction >; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_VISIBLE = true; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_ENABLED = true; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_RADIUS = 1.5; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_CLIP_DISTANCE = 1.5; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_MATERIAL_ID = null; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_TERRAIN_MODE: SplineCorridorJunctionTerrainMode = "flattenAndPaint"; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_SHAPE_MODE: SplineCorridorJunctionShapeMode = "straight"; export const DEFAULT_SPLINE_CORRIDOR_JUNCTION_EDGE_ENABLED = false; export const MIN_SPLINE_CORRIDOR_JUNCTION_RADIUS = 0.05; export const MAX_SPLINE_CORRIDOR_JUNCTION_RADIUS = 500; export const MIN_SPLINE_CORRIDOR_JUNCTION_CLIP_DISTANCE = 0.05; export const MAX_SPLINE_CORRIDOR_JUNCTION_CLIP_DISTANCE = 500; export const SPLINE_CORRIDOR_JUNCTION_TERRAIN_MODES = [ "none", "paintOnly", "flattenAndPaint" ] as const satisfies readonly SplineCorridorJunctionTerrainMode[]; export const SPLINE_CORRIDOR_JUNCTION_SHAPE_MODES = [ "straight", "curve", "corner" ] as const satisfies readonly SplineCorridorJunctionShapeMode[]; function cloneVec3(vector: Vec3): Vec3 { return { x: vector.x, y: vector.y, z: vector.z }; } function normalizePositiveFiniteNumber( value: number, min: number, max: number, label: string ): number { if (!Number.isFinite(value)) { throw new Error(`${label} must be finite.`); } if (value < min || value > max) { throw new Error(`${label} must be from ${min} to ${max}.`); } return value; } export function isSplineCorridorJunctionTerrainMode( value: unknown ): value is SplineCorridorJunctionTerrainMode { return SPLINE_CORRIDOR_JUNCTION_TERRAIN_MODES.includes( value as SplineCorridorJunctionTerrainMode ); } export function isSplineCorridorJunctionShapeMode( value: unknown ): value is SplineCorridorJunctionShapeMode { return SPLINE_CORRIDOR_JUNCTION_SHAPE_MODES.includes( value as SplineCorridorJunctionShapeMode ); } export function normalizeSplineCorridorJunctionRadius(value: number): number { return normalizePositiveFiniteNumber( value, MIN_SPLINE_CORRIDOR_JUNCTION_RADIUS, MAX_SPLINE_CORRIDOR_JUNCTION_RADIUS, "Spline corridor junction radius" ); } export function normalizeSplineCorridorJunctionClipDistance( value: number ): number { return normalizePositiveFiniteNumber( value, MIN_SPLINE_CORRIDOR_JUNCTION_CLIP_DISTANCE, MAX_SPLINE_CORRIDOR_JUNCTION_CLIP_DISTANCE, "Spline corridor junction clip distance" ); } export function normalizeSplineCorridorJunctionMaterialId( value: string | null | undefined ): string | null { if (value === undefined || value === null || value.trim().length === 0) { return null; } return value; } export function normalizeSplineCorridorJunctionName( value: string | undefined ): string | undefined { if (value === undefined) { return undefined; } const trimmed = value.trim(); return trimmed.length === 0 ? undefined : trimmed; } export function normalizeSplineCorridorJunctionTerrainMode( value: unknown ): SplineCorridorJunctionTerrainMode { if (isSplineCorridorJunctionTerrainMode(value)) { return value; } throw new Error( "Spline corridor junction terrain mode must be none, paintOnly, or flattenAndPaint." ); } export function normalizeSplineCorridorJunctionShapeMode( value: unknown ): SplineCorridorJunctionShapeMode { if (isSplineCorridorJunctionShapeMode(value)) { return value; } throw new Error( "Spline corridor junction shape mode must be straight, curve, or corner." ); } export function createSplineCorridorJunctionConnection( overrides: Partial & { pathId: string; } ): SplineCorridorJunctionConnection { if (overrides.pathId.trim().length === 0) { throw new Error("Spline corridor junction connection path id is required."); } return { id: overrides.id ?? createOpaqueId("spline_junction_connection"), pathId: overrides.pathId, progress: overrides.progress === undefined ? 0 : Math.min(1, Math.max(0, overrides.progress)), clipDistance: normalizeSplineCorridorJunctionClipDistance( overrides.clipDistance ?? DEFAULT_SPLINE_CORRIDOR_JUNCTION_CLIP_DISTANCE ) }; } export function createSplineCorridorJunction( overrides: Partial< Omit > & { connections: Array< Partial & { pathId: string } >; } ): SplineCorridorJunction { if (overrides.connections.length < 2) { throw new Error("Spline corridor junctions require at least two connections."); } return { id: overrides.id ?? createOpaqueId("spline_junction"), kind: "splineCorridorJunction", name: normalizeSplineCorridorJunctionName(overrides.name), visible: overrides.visible ?? DEFAULT_SPLINE_CORRIDOR_JUNCTION_VISIBLE, enabled: overrides.enabled ?? DEFAULT_SPLINE_CORRIDOR_JUNCTION_ENABLED, center: overrides.center === undefined ? { x: 0, y: 0, z: 0 } : cloneVec3(overrides.center), radius: normalizeSplineCorridorJunctionRadius( overrides.radius ?? DEFAULT_SPLINE_CORRIDOR_JUNCTION_RADIUS ), materialId: normalizeSplineCorridorJunctionMaterialId( overrides.materialId ?? DEFAULT_SPLINE_CORRIDOR_JUNCTION_MATERIAL_ID ), terrainMode: overrides.terrainMode === undefined ? DEFAULT_SPLINE_CORRIDOR_JUNCTION_TERRAIN_MODE : normalizeSplineCorridorJunctionTerrainMode(overrides.terrainMode), shapeMode: overrides.shapeMode === undefined ? DEFAULT_SPLINE_CORRIDOR_JUNCTION_SHAPE_MODE : normalizeSplineCorridorJunctionShapeMode(overrides.shapeMode), edge: createScenePathRoadEdgeSettings({ enabled: overrides.edge?.enabled ?? DEFAULT_SPLINE_CORRIDOR_JUNCTION_EDGE_ENABLED, collisionEnabled: overrides.edge?.collisionEnabled, kind: overrides.edge?.kind, width: overrides.edge?.width, height: overrides.edge?.height, materialId: overrides.edge?.materialId }), connections: overrides.connections.map( createSplineCorridorJunctionConnection ) }; } export function cloneSplineCorridorJunction( junction: SplineCorridorJunction ): SplineCorridorJunction { return createSplineCorridorJunction(junction); } export function getSplineCorridorJunctions( junctions: SplineCorridorJunctionRegistry ): SplineCorridorJunction[] { return Object.values(junctions).sort((left, right) => (left.name ?? left.id).localeCompare(right.name ?? right.id) ); }