/** A road centerline vertex in world XZ. */ export type RoadPoint = readonly [number, number]; /** Renderer-ready triangle ribbon: flat position triples plus triangle indices. */ export interface RoadRibbon { /** Interleaved xyz vertex positions. */ positions: Float32Array; /** Triangle indices into `positions`. */ indices: Uint32Array; } /** * Fillets sharp corners of a centerline into short arcs so a ribbon built from it reads as a smooth * road instead of overlapping/notching rectangles at each bend. Each interior vertex is replaced by a * quadratic-bezier arc of `radius` (clamped to half the shorter adjacent segment) sampled at * `cornerSegments` steps; endpoints and near-straight vertices pass through unchanged. * @internal — the corner-smoothing behind ``; games get it for free through the render. */ export declare function roundPathCorners(points: readonly RoadPoint[], radius: number, cornerSegments?: number): RoadPoint[]; /** * The single owning table of ground-decal Y offsets, in world units above the sampled terrain. * * Contract — the vertical stacking order is `terrain < road < junction < marking < glow`, and every * ground overlay must draw from THIS table rather than picking a local magic epsilon (the old * scatter of `0.08` ribbons, `+0.02` discs, `0.05` markings, `0.055–0.1` playground fudge factors * is exactly the z-fighting this removes). * * - `road` and `junction` deliberately share one layer: {@link buildJunctionSurface} welds its * boundary vertices onto the exact corner vertices where the trimmed ribbon ends * ({@link trimPathAtJunctions}), so the two surfaces meet at a shared seam and never stack — they * must sit at the same height or the seam would tear. Do NOT bump `junction` above `road`. * - `marking` is the ONLY permitted coplanar overlay (lane paint sits on top of asphalt at the same * XZ). Its Y separation alone is not enough to beat depth precision on sloped ground: consumers * rendering markings MUST ALSO set a negative `polygonOffset` and/or a higher `renderOrder` on the * marking material (renderer-side — this table only owns the Y term). * - `glow` sits above markings for emissive decals (crosswalk glow, selection halos). * * Values are chosen with real separation so a small terrain slope can't invert the order. */ export declare const GROUND_DECAL_LAYERS: { /** Road ribbons. */ readonly road: 0.06; /** Junction surfaces — SAME layer as `road` because they are seam-welded, never stacked. */ readonly junction: 0.06; /** Lane markings / crosswalks — the only coplanar overlay; also set polygonOffset/renderOrder. */ readonly marking: 0.11; /** Emissive decals above markings (glow, halos). */ readonly glow: 0.14; }; /** Options for {@link buildRoadRibbon}. */ export interface RoadRibbonOptions { /** Lift above the sampled ground so the ribbon never z-fights the terrain. Default {@link GROUND_DECAL_LAYERS}.road. */ elevation?: number; /** Longest centerline step before the polyline is subdivided to drape over relief. Default 4. */ maxSegmentLength?: number; } /** * Triangulate a road centerline into a ground-draped ribbon mesh: the polyline is subdivided, * each vertex is offset half a `width` along the local perpendicular, and every vertex sits at * `sampleHeight(x, z) + elevation`. Pure geometry — the shell (or any renderer) turns the result * into a mesh, and tests can assert on it directly. */ export declare function buildRoadRibbon(path: readonly RoadPoint[], width: number, sampleHeight: (x: number, z: number) => number, options?: RoadRibbonOptions): RoadRibbon; /** A circular exclusion zone: dashes whose midpoint falls inside are dropped (e.g. junction patches). */ export interface DashExclusion { center: RoadPoint; radius: number; } /** * Build a flat, ground-draped disc patch centered on `center` — a triangle fan of `segments` sides * (default 16), every vertex draped at `sampleHeight + elevation`. * * @deprecated This floats a disc slightly above the crossing ribbons, which still overlaps and * z-fights them. Prefer {@link buildJunctionSurface} (with {@link trimPathAtJunctions}, or the * all-in-one {@link buildTrimmedIntersections}): it trims the ribbons back to the junction boundary * and welds one seam-shared surface, with no overlap and no floating disc. Kept so existing callers * don't break this round. */ export declare function buildJunctionPatch(center: RoadPoint, radius: number, sampleHeight: (x: number, z: number) => number, options?: { elevation?: number; segments?: number; }): RoadRibbon; /** * Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line, * `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back * through {@link buildRoadRibbon} with a thin width to mesh the dashes. Pass `exclude` circles * (junction patches) to interrupt the center line through intersections — any dash whose midpoint * lands inside an exclusion is dropped. */ export declare function dashSegments(path: readonly RoadPoint[], dashLength?: number, gapLength?: number, exclude?: readonly DashExclusion[]): readonly (readonly RoadPoint[])[]; /** Result of {@link nearestOnPath}: closest point on the centerline plus distance and tangent. */ export interface RoadSample { /** Closest point on the centerline. */ point: RoadPoint; /** World distance from the query to `point`. */ distance: number; /** Unit tangent of the centerline at `point`. */ tangent: readonly [number, number]; } /** * Closest-point query against a road centerline — the seam traffic AI, spawn placement, and * "am I on the road" checks share. Returns null for a degenerate path. */ export declare function nearestOnPath(path: readonly RoadPoint[], x: number, z: number): RoadSample | null; /** True when the query point lies within half the road `width` of the centerline. */ export declare function isOnRoad(path: readonly RoadPoint[], width: number, x: number, z: number): boolean; /** Total arc length of a centerline in world units. */ export declare function pathLength(path: readonly RoadPoint[]): number; /** * The minimal junction shape this geometry needs — a node position plus its outgoing arms. Matches * the structural subset of `StreetJunction` (streetGenerator) so a network's `junctions` can be fed * straight in. Arm `angle` follows the generator convention `atan2(dx, dz)`, i.e. the outward unit * direction of an arm is `[sin(angle), cos(angle)]`. */ export interface RoadJunctionInput { x: number; z: number; arms: readonly { angle: number; width: number; }[]; } /** Tunables shared by {@link trimPathAtJunctions}, {@link buildJunctionSurface}, and {@link buildTrimmedIntersections}. */ export interface JunctionGeometryOptions { /** * Requested curb-return radius at exterior corners between adjacent approaches. Default 2. * Clamped per corner by the two adjacent road widths, the corner chord, and available mouth gap — * never inflates the paved conflict area the way an apron pull-back would. */ curbReturnRadius?: number; /** * Extra clearance beyond the projected crossing half-width so ribbons stop just outside the * carriageway union. Default 0.25. Does **not** include the curb-return radius (returns live only * on the exterior corner arcs). */ apronMargin?: number; /** Samples per curb-return fillet arc between two adjacent approach corners. Default 4. */ filletSegments?: number; /** Lift above sampled ground. Defaults to {@link GROUND_DECAL_LAYERS}.junction (== road: seam-shared). */ elevation?: number; /** Longest step before draping subdivision, matching {@link buildRoadRibbon}. Default 4. */ maxSegmentLength?: number; /** World-space tolerance for matching a path vertex onto a junction node. Default 1e-6. */ nodeEpsilon?: number; } /** One trim of a ribbon end back to a junction boundary — the seam the welded surface attaches to. */ export interface RoadCut { /** Index (into the `junctions` array passed to the trimmer) of the junction this cut welds to. */ junctionIndex: number; /** Whether the cut is at the start or the end of the returned sub-path. */ at: "start" | "end"; /** Trimmed centerline endpoint — the point on the junction boundary the ribbon now ends at. */ center: RoadPoint; /** Unit direction pointing from the junction center outward into the road. */ direction: readonly [number, number]; /** Left edge corner at the cut (`center` + width/2 along the local perpendicular). */ left: RoadPoint; /** Right edge corner at the cut (`center` − width/2 along the local perpendicular). */ right: RoadPoint; /** Arc-length the ribbon was pulled back from the junction center. */ apron: number; } /** A street sub-path after trimming, plus the cuts (0–2) that shortened it. Feed `path` to {@link buildRoadRibbon}. */ export interface TrimmedRoad { /** Trimmed centerline (≥2 points) ready for meshing. */ path: RoadPoint[]; /** Cuts applied to this sub-path — at most one `start` and one `end`. */ cuts: RoadCut[]; } /** * Cut a street centerline back from the junctions it passes through so its ribbon ENDS at the * junction boundary instead of ploughing through the node. Junctions are matched to path vertices * within `nodeEpsilon` (endpoints and interior vertices alike); a junction sitting on an interior * vertex SPLITS the path into two independently-trimmed sub-paths. Each cut records the exact * boundary point and the road's left/right edge corners there, so {@link buildJunctionSurface} can * weld its polygon onto the identical vertices with no overlap. * * Pure geometry, allocation-aware, bounded by the vertex and junction counts. Returns one * {@link TrimmedRoad} per surviving sub-path (a mid-polyline junction yields two). */ export declare function trimPathAtJunctions(path: readonly RoadPoint[], width: number, junctions: readonly RoadJunctionInput[], options?: JunctionGeometryOptions): TrimmedRoad[]; /** Options for {@link trimBandAtJunctions} — junction geometry tunables plus a band-specific clearance. */ export interface BandTrimOptions extends JunctionGeometryOptions { /** Extra outward clearance added to every junction's apron radius before cutting the band. Default 0. */ clearance?: number; } /** * Cut an offset band (a polyline running PARALLEL to a street — e.g. a sidewalk from * `Street.sidewalks.{left,right}`) out of every junction's apron, so the band stops at the crossing * instead of sailing straight through it. Unlike {@link trimPathAtJunctions} the band never passes * through a node, so it is clipped by DISTANCE: each junction contributes a circular apron of radius * `max arm half-width + apronMargin + bandWidth/2 + clearance` (plus a small curb-return allowance), * and any part of the band inside any apron circle is removed, splitting the band into the sub-paths * that survive outside. Entry/exit points land exactly on the apron boundary. A band that never * enters an apron passes through as a single untouched copy. * * Pure geometry, deterministic, bounded by the band-vertex and junction counts. * * @example * ```ts * // Same `junctions` you fed the roads; trim both sidewalks of a street around every node. * const left = trimBandAtJunctions(street.sidewalks.left, street.sidewalkWidth, junctions); * const right = trimBandAtJunctions(street.sidewalks.right, street.sidewalkWidth, junctions); * for (const sub of [...left, ...right]) meshes.push(buildRoadRibbon(sub, street.sidewalkWidth, sampleHeight)); * ``` * * @capability world-intersections trim offset sidewalk/parallel bands around junction aprons */ export declare function trimBandAtJunctions(bandPath: readonly RoadPoint[], bandWidth: number, junctions: readonly RoadJunctionInput[], options?: BandTrimOptions): RoadPoint[][]; /** One approach feeding {@link buildJunctionSurface}: the exact draped corner vertices a ribbon ends at. */ export interface JunctionApproach { /** Junction-boundary centerline point where the ribbon ends. */ center: RoadPoint; /** Left edge corner (draped `[x, y, z]`) — the ribbon's terminal left vertex. */ left: readonly [number, number, number]; /** Right edge corner (draped `[x, y, z]`) — the ribbon's terminal right vertex. */ right: readonly [number, number, number]; /** Unit direction from the node into this approach. Present on approaches emitted by {@link buildTrimmedIntersections}. */ direction?: readonly [number, number]; /** Unit tangent in the source street's authored path order. */ sourceTangent?: readonly [number, number]; /** Source street index, so consumers never have to infer identity from rendered vertices. */ streetIndex?: number; /** Source trimmed-ribbon index, parallel to {@link TrimmedIntersections.trimmed}. */ trimmedIndex?: number; /** Which end of the source trimmed path meets this junction. */ cutAt?: "start" | "end"; /** Full pavement width at this mouth. */ width?: number; /** Sidewalk widths attached to the source path's left and right pavement edges. */ sidewalkWidths?: IntersectionSidewalkWidths; /** Lane-paint declaration inherited from the source street. */ markings?: IntersectionMarkingIntent; } /** * Build a tangent-continuous centerline through a two-arm junction. This is the shared path for * paint, medians, curbs, or other linear dressing that should follow a bend but stop at crossings. * Returns `null` for anything other than exactly two approaches. * @capability world-intersections connect linear road dressing through two-arm bends */ export declare function buildJunctionConnector(junction: { x: number; z: number; }, approaches: readonly JunctionApproach[], segments?: number): RoadPoint[] | null; /** * Weld one triangulated junction surface onto the corner vertices its incident ribbons END at * (from {@link trimPathAtJunctions}). Corners are grouped by approach (so unequal widths cannot * interleave a neighbour between a mouth pair), ordered around the node, and bridged as follows: * * - **Road mouth** — the straight end-edge of the approach (shared seam with the ribbon). * - **Through edge (~180°)** — straight chord along the continuing curb (T-junction far side). * - **Exterior corner** — a compact circular curb-return of radius ≤ `curbReturnRadius`, clamped by * adjacent widths and chord length; bows *outward* only, never a plaza disc. * - **Two-arm turn** — both edges get the same compact returns (no diagonal cap, no remote tangent). * * The boundary is ear-clipped so every triangle faces +Y. Draped height matches * {@link buildRoadRibbon}. Mouth corners are the approach corners verbatim. */ export declare function buildJunctionSurface(junction: { x: number; z: number; }, approaches: readonly JunctionApproach[], sampleHeight: (x: number, z: number) => number, options?: JunctionGeometryOptions): RoadRibbon; /** Widths of renderer-neutral sidewalk bands on each authored side of a street. */ export interface IntersectionSidewalkWidths { /** Width beyond the left pavement edge. Omit or use zero for no band. */ left?: number; /** Width beyond the right pavement edge. Omit or use zero for no band. */ right?: number; } /** One longitudinal lane-marking ribbon, offset left (+) or right (-) from the authored centerline. */ export interface IntersectionMarkingLine { /** Signed centerline offset in world units. Default 0. */ offset?: number; /** Paint width. Default 0.15. */ width?: number; } /** Paint intent carried through trimming without renderer inference. */ export interface IntersectionMarkingIntent { /** Longitudinal marking ribbons. */ lines?: readonly IntersectionMarkingLine[]; /** Emit a transverse stop line at this street's multi-arm approaches. Default false. */ stopLine?: boolean; } /** One street to trim + mesh through {@link buildTrimmedIntersections}. */ export interface IntersectionStreet { /** Street centerline; endpoints/vertices coincide with junction nodes. */ path: readonly RoadPoint[]; /** Full road width. */ width: number; /** Optional, independently-sized bands beyond the authored left/right pavement edges. */ sidewalks?: IntersectionSidewalkWidths; /** Optional lane-paint declaration. Omission preserves the previous pavement-only output. */ markings?: IntersectionMarkingIntent; } /** Renderer-ready output of {@link buildTrimmedIntersections}: trimmed ribbons + welded junction surfaces. */ export interface TrimmedIntersections { /** One trimmed ribbon per surviving sub-path (a through-street contributes two). */ ribbons: RoadRibbon[]; /** The trimmed sub-paths, parallel to `ribbons`, for lane markings / analysis. */ trimmed: TrimmedRoad[]; /** One welded surface per junction that had ≥1 incident approach. */ junctions: RoadRibbon[]; /** Index into the input `junctions` for each surface, parallel to `junctions` above. */ junctionIndices: number[]; /** Exact ribbon-mouth approaches for each surface, parallel to `junctions`; useful for dressing. */ junctionApproaches: JunctionApproach[][]; /** Source street index for each entry in `trimmed`/`ribbons`. */ trimmedStreetIndices: number[]; /** Source pavement width for each entry in `trimmed`/`ribbons`. */ trimmedWidths: number[]; /** Source paint intent for each entry in `trimmed`/`ribbons`. */ trimmedMarkings: (IntersectionMarkingIntent | undefined)[]; /** Road-side sidewalk bands, excluding junction interiors. */ sidewalks: RoadRibbon[]; /** Sidewalk curb-return aprons around junction pavement; road-mouth edges and center fill are absent. */ sidewalkAprons: RoadRibbon[]; /** Input junction index for each entry in `sidewalkAprons`. */ sidewalkApronJunctionIndices: number[]; } /** * Trim a set of streets against a set of junctions and weld the crossing surfaces in one call — the * ergonomic entry the shell/playground consume for meshing. * * Each street is trimmed by {@link trimPathAtJunctions} (through-streets split in two), each surviving * sub-path is meshed with {@link buildRoadRibbon}, and every junction that any ribbon actually ends * at gets one {@link buildJunctionSurface}. The junction's boundary vertices are read back BITWISE * from the ribbons' terminal cross-sections, so ribbon and surface share the exact same floats at * the seam — no overlap, no z-fighting, no floating disc. Ribbons and surfaces both live on the * seam-shared {@link GROUND_DECAL_LAYERS}.road/.junction layer. Deterministic (junctions emitted in * ascending index order) and bounded by the street/junction counts. * * @capability world-intersections trim streets at junctions and weld curb-return crossing surfaces in one call */ export declare function buildTrimmedIntersections(streets: readonly IntersectionStreet[], junctions: readonly RoadJunctionInput[], sampleHeight: (x: number, z: number) => number, options?: JunctionGeometryOptions): TrimmedIntersections; /** Tunables for {@link buildIntersectionMarkings}. */ export interface IntersectionMarkingOptions { /** Clearance between a multi-arm road mouth and longitudinal paint / stop lines. Default 1. */ mouthClearance?: number; /** Default longitudinal paint width. Default 0.15. */ lineWidth?: number; /** Transverse stop-line width along the approach. Default 0.35. */ stopLineWidth?: number; /** Gap left between a stop line and each pavement edge. Default 0.5. */ stopLineEdgeClearance?: number; /** Samples in a degree-2 marking connector. Default 8. */ connectorSegments?: number; /** Painted dash length. Omit or use zero for a solid line. */ dashLength?: number; /** Gap between painted dashes. Default 3 when `dashLength` is positive. */ dashGap?: number; /** Marking lift above terrain. Default {@link GROUND_DECAL_LAYERS}.marking. */ elevation?: number; /** Longest draping step. Default 4. */ maxSegmentLength?: number; } /** * Mesh the paint declared on streets passed to {@link buildTrimmedIntersections}. Longitudinal * ribbons follow the trimmed road paths. At degree-2 nodes corresponding lines are joined by a * tangent-continuous cubic (signed offsets are preserved); at multi-arm nodes lines deliberately * stop short of the mouth and optional transverse stop lines make that termination readable. * @capability world-intersections connect sidewalks and lane guidance through generated intersections */ export declare function buildIntersectionMarkings(intersections: TrimmedIntersections, sampleHeight: (x: number, z: number) => number, options?: IntersectionMarkingOptions): RoadRibbon[];