A Rust constrained-Delaunay triangulator (an independent implementation inspired by Jonathan Shewchuk's Triangle, not a port of it) plus the runtime pieces you actually need to ship navigation: a navmesh binary format, A* + funnel path search with wall-clearance, a BVH for fast point queries, a background worker for dynamic obstacles, a multi-agent crowd with local avoidance, and authoring/probing demos.
This is a pure-Rust reimplementation β no FFI, no C dependencies, no unsafe.
Every crate but one is also free of external Rust dependencies; rsnav-dynamic takes
arc-swap for the lock-free poll_swap handoff.
Built around an independent CDT implementation, validated for agreement against
Shewchuk's triangle.c β the included A.poly round-trips byte-for-byte against
the triangle binary's .ele output (29 triangles) β but not derived from its
source. See References for the attribution and the license note.
ββββββββββββββββββββββββββ
bitfield β β PSLG
βββββββββΆ β polygon-extract β βββββββββ
β (true/false grid β β β
β polygons + holes) β β
ββββββββββββββββββββββββββ βΌ
ββββββββββββββββββββββββ
β triangle β
β (Shewchuk-inspired, β
β CDT-only subset: β
β D&C Delaunay, β
β constrained edges, β
β hole carving) β
ββββββββββββ¬ββββββββββββ
β CdtMesh
βΌ
ββββββββββββββββββββββββ
β navmesh β binary file
β (compact, indexed, β (.navmesh,
β region-labelled) β see FORMAT.md)
ββββββ¬ββββββββββββ¬ββββββ
β β
query index ββββββββββ βββββββββββΆ runtime queries
ββββββββββββ ββββββββββββββββββββββββ
β bsp β β navigation β
β (BVH for β βββββ used by βββββΆ β A* + funnel + β
β locate, β β LOS + nearest; β
β nearest) β β distance_from_wall β
ββββββββββββ ββββββββββββ¬ββββββββββββ
β path polyline
βΌ
ββββββββββββββββββββββ
β pathing β
β (PathFollower: β
β lookahead + β
β anti-shortcut) β
ββββββββββββββββββββββ
That diagram is the legacy path: polygons go to the CDT as-is. There is a second,
crossing-tolerant path through triangle, taken when BuildOptions::inset is set β the
contours are first offset inward (common::offset), the resulting self- and
mutually-crossing rings are planarized into a non-crossing arrangement
(common::planarize), and the interior is selected by winding number rather than by
hole-seed flood fill (triangle::winding). It is the only path that accepts input rings
that cross. See docs/13-authored-geometry.md for when to
use it and docs/06-clearance.md for what it costs.
For game-loop integration with dynamic obstacles, rsnav-dynamic wraps the
pipeline in a background-thread worker that consumes Bitfield snapshots
and publishes results lock-free:
game thread worker thread (NavWorker)
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ
β edit Bitfield β β polygon-extract β CDT β navmesh β
β submit_snapshot ββββββΌββββ coalesce ββββββΆ β β BSP β
β β β β
β poll_swap() β βββ ArcSwap βββββββ β publish Arc<NavBuild> β
β β³ Arc<NavBuild> β β dispatch NavEvent β listener β
β stats() β NavStats β βββ atomics βββββββ β bump counters β
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ
For multi-agent simulation, rsnav-crowd sits on top of Arc<NavBuild>:
each agent owns a funnel-pulled corridor through the shared navmesh and a
sampled velocity-obstacle solver picks per-tick velocities that follow
the corridor while side-stepping other agents.
Arc<NavBuild> βββΆ Crowd βββΆ tick(dt)
β βββ replan / arrive (find_path per agent, radius-aware)
β βββ rebuild hash (spatial grid of agent positions)
β βββ choose velocities (sampled-VO: align minus TTC penalty)
β βββ integrate (advance + snap to mesh)
β
βββ set_nav(Arc<NavBuild>) βββ only invalidates corridors
that the new mesh broke
Full prose documentation lives in docs/README.md, which routes by who you are and by what went wrong. Three entry points:
- Never written pathfinding β 01-quickstart, then 02-concepts.
- You have grid A*, Recast, or a waypoint graph β 01-quickstart, then 03-from-grid-astar, which names the habits that will mislead you here.
- You know navmeshes and want the API β 17-api-map, then the decision table in 06-clearance.
All three converge on 04-units-and-conventions and
06-clearance. Those two pages carry the material that costs people
days: false means wall, BuildOptions::inset is in cells while
PathOptions::distance_from_wall is in world units, grid erosion quantizes a 0.128
request to a full 1.0 peel, and distance_from_wall is a portal shift rather than a
Euclidean clearance guarantee.
One choice is worth making before you write any code, because reversing it later means
rebuilding your whole content pipeline: BuildOptions::inset cannot be used with
TiledWorld. Per-tile contour erosion pulls each tile's boundary off the seam line, so
stitch_all matches nothing and cross-seam paths silently fail. If your world will ever be
tiled, clearance has to come from global grid erosion applied before slicing, which is
cell-quantized β so sub-cell agent radii and tiling are mutually exclusive in v1. See
06-clearance and 12-large-worlds.
Shipping a baked mesh to clients or between processes? 14-saving-and-loading covers encode/decode; 18-interop is the same two calls over a socket, with a runnable plain-TCP demonstration.
Something already broken? docs/README.md opens with a symptom index β nearly every failure mode here is silent, producing wrong geometry rather than an error β and 16-troubleshooting is written to be entered cold.
cargo run -p rsnav-demo --release
Author polygons with the mouse, press Create navmesh, then probe with right-click (set source) / left-click (set goal) for paths. The left panel has a Fixtures browser; the path field is pre-filled with ./testdata and is editable β point it at any directory of .json maps. Also in exploration mode: Doors (place a runtime door by clicking the highlighted edge, or by drawing a segment across a passage; toggle open/closed and watch paths/LOS/visibility react with no rebuild) and Zones (a NavMetadata demo that tints triangles and annotates where a path crosses a zone boundary).
cargo run -p rsnav-fixtures --release -- --testdata ./testdata
cargo run -p rsnav-fixtures --release -- --testdata ./broken.json -v
--testdata <PATH> is a file or a directory of .json fixtures; if omitted it defaults to ./testdata. Prints a status table (triangle count, region count, build_ms per fixture) and exits non-zero on any failure β drop-in for CI.
cargo run -p rsnav-rtsim --release
RTS-style harness: a 128Γ128 cell bitfield is the ground truth; mouse tools paint walls, clear them, or harvest forest cells one at a time. A background NavWorker (from rsnav-dynamic) keeps the navmesh in sync by re-running the full pipeline on each bitfield snapshot, coalescing rapid changes so it never falls behind. ~10 agents path between random walkable points and re-plan after every navmesh swap, demonstrating that game systems can keep operating while the mesh churns. The side panel surfaces live worker stats (submitted / coalesced / in-flight / completed / build_ms) and a scrolling event log via the typed NavListener API.
cargo run -p rsnav-crowd-demo --release
Small RTS world (town hall + mine + forest blob) on a 96Γ64 bitfield with a mix of peon roles: mine peons loop mine ring slot β harvest β hall slot β deposit β repeat, forest peons loop nearest tree β harvest from its walkable neighbor β hall slot β deposit β repeat (each harvest flips one tree cell back to walkable and the navmesh follows), and a few wanderers path to random walkable points to keep the avoidance solver under load. Forest blobs respawn when fully chewed. Side panel has per-role spawn buttons, mine/hall slot usage, forest cells remaining, eviction counter, and the standard NavWorker stats. The peon FSM and slot-reservation logic live entirely in the demo β rsnav-crowd itself ships only the per-agent crowd primitive.
cargo run -p rsnav-door-demo --release
A 76Γ48 bitfield split into four rooms by a cross of walls, each wall holding two doors. A door is a pure obstacle: open, its cells are walkable; closed, they are carved out of the bitfield and the NavWorker rebuilds the mesh without the gap. A handful of actors patrol back and forth between two fixed points (home β away); click a door on the map β or use the side-panel checkboxes β to open or close it. When a door toggles mid-route, Crowd::set_nav revalidates each actor's remaining path by line-of-sight: actors the door just blocked drop the stale corridor and replan, unaffected actors keep walking. A fully closed-off actor gets a red ring and retries every tick until a route reopens. Door::rect / Door::horizontal / Door::vertical are the demo's door-authoring helpers.
cargo run -p rsnav-world-demo --release
Place independent navmesh tiles in a shared world and path across the seams. Spawn tiles from the palette (Open / Holed / Pillars), then with the Move tool drag them edge-to-edge β seams re-stitch live, links shown as green segments. The Path tool (right-click source, left-click goal) routes across tiles; the Vis tool (right-click source, move cursor) draws a cross-tile line-of-sight ray, green clear / red blocked. Links are created purely by placement (overlapping border edges auto-connect) β there's no manual weld. Built on rsnav_navigation::TiledWorld.
Every crate ships runnable examples. The complete, maintained inventory β 23 examples with
their exact cargo run lines β lives in
docs/README.md Β§ Runnable examples. Start with:
cargo run -p rsnav-navigation --example first_path
Bitfield in, path polyline out, in forty lines β walked through in docs/01-quickstart.md.
| name | what it provides |
|---|---|
rsnav-common |
Vertex, Polygon, Triangle, Aabb, Mesh2d, IDs. Geometry helpers (orient2d, incircle, segment intersection, Polygon::interior_point β used for safe hole seeds on concave polygons). Also offset (contour offsetting, no arc joins), planarize (snap-rounded arrangement of crossing rings), par (the hand-rolled work-splitting used by every parallel stage) and rng (deterministic sampling). |
rsnav-triangle |
Constrained Delaunay triangulator. Independent implementation of the CDT algorithms inspired by Shewchuk's Triangle (no Steiner-point quality refinement, no Voronoi), validated against triangle.c but not derived from it. D&C Delaunay, segment insertion, hole carving, .poly/.node/.ele I/O. |
rsnav-polygon-extract |
Bitfield β PolygonWithHoles. 4-connectivity region detection, optional collinear-vertex removal, optional zigzag β diagonal smoothing, min-area culling. |
rsnav-navmesh |
Runtime mesh: flat vertices + triangles, per-triangle adjacency, edge constraint markers, area, centroid, connected-component region IDs. Per-region accessors (triangles / area / centroid / bounds), area-weighted random_point sampling for spawns, boundary_edges outline iteration. Versioned little-endian binary format (FORMAT.md). |
rsnav-bsp |
BVH (AABB-tree) over a NavMesh. locate(point) and nearest(point), both O(log n) average, plus query_aabb broad-phase range queries. |
rsnav-navigation |
A* across triangle adjacency, Simple Stupid Funnel string-pull, triangle-walk line-of-sight, nearest-point. distance_from_wall rejects narrow portals and pulls portal endpoints inward at wall vertices. Doors: runtime edge-cuts (DoorSet) β closing a door promotes internal portal edges to walls via one shared WallInfo oracle, so A*/LOS/visibility/clearance all react with no mesh or BSP rebuild. NavWorld<M>: owns (NavMesh, Bsp, DoorSet, WallInfo) + user metadata (NavMetadata trait, zone_crossings for "entered/left zone" path events). TiledWorld: multi-tile worlds β independent navmeshes placed by world offset, seams auto-stitched by overlapping border edges, cross-tile A* + LOS. |
rsnav-pathing |
PathFollower: lookahead + monotone arc-progress projection + anti-shortcut bias at corners. No navmesh dependency β operates on any polyline. |
rsnav-dynamic |
NavWorker: background-thread navmesh updates driven by Bitfield snapshots, with lock-free poll_swap for game loops. Typed NavListener events (BuildStarted / Completed / Failed) and a polling NavStats accessor for HUDs and ops dashboards. Coalesces rapid submissions β only the newest snapshot is built. |
rsnav-crowd |
Multi-agent crowd primitive: per-agent funnel-pulled corridor + sampled velocity-obstacle local avoidance with per-agent radius. Crowd::set_nav keeps still-valid paths across navmesh swaps instead of replanning the whole population. Snaps agents back to the mesh if avoidance pushes them off. No FSM, no formation logic, no slot reservation β those live in user code. |
rsnav-demo |
egui authoring + probing app (the Quick start demo above). |
rsnav-fixtures |
CLI runner for .json PSLG fixtures (the Batch-run tool above). |
rsnav-rtsim |
RTS-style dynamic-obstacles testbed (the Dynamic-obstacles app above). |
rsnav-crowd-demo |
Multi-agent peon-economy testbed (the Multi-agent crowd app above). |
rsnav-door-demo |
Togglable-doors testbed (the Doors app above). |
rsnav-world-demo |
Multi-tile world editor: place navmesh tiles, drag them to stitch seams, path / line-of-sight across tiles (the Multi-tile world app below). |
navmesh v1 is a section-based little-endian binary format. The full normative spec is in crates/navmesh/FORMAT.md. It's designed to be implementable in any language β fixed-width records, no varints/compression/alignment-tricks, unknown section types silently skipped for forward compatibility.
Required sections: META, VERTICES, TRIANGLES. Optional (recomputed if absent): ADJACENCY, EDGE_MARKERS, TRI_INFO. The minimum portable file is META + VERTICES + TRIANGLES + EDGE_MARKERS (the markers can't be re-derived without losing the wall information).
Same bytes go over the wire: NavMesh::to_bytes / from_bytes are all you need to ship a mesh from a server to clients or between processes. rsnav stops at encode/decode β the framing and protocol are your game's. See docs/18-interop.md, with a runnable plain-TCP demonstration.
Working and tested (~205 tests pass workspace-wide):
- β
CDT round-trip against
triangle.creference (A.polyβ 29 triangles, byte-exact) - β 200-point random Delaunay stress passes the empty-circumcircle test
- β Wall-distance clearance pathing (rejects narrow portals, pulls funnel endpoints off walls)
- β Runtime doors: closing an edge-cut blocks A*/LOS at the door, opening restores it, mesh & BSP never rebuilt
- β Multi-tile worlds: independent tiles auto-stitch on overlapping borders (incl. mismatched seam triangulations) and path/LOS across seams
- β
Real gonav-format fixtures (representative loads: ~1900 tris / 6 regions and ~1250 tris / 23 regions) build clean when pointed at via
--testdata
Deliberate v1 omissions:
- Steiner point quality refinement (
-qswitch intriangle) β no facility for it. - Self-intersecting PSLG segments (
segmentintersectionintriangle) β on the legacy path,form_skeletonreturnsErr(SelfIntersection)rather than splitting the crossing. This is no longer the whole story:build_cdt_with_insetexists precisely to accept crossing rings, resolving them by snap-rounded planarization plus a winding-number interior test instead of by segment splitting. Crossing input is supported there, not inform_skeleton. See docs/13-authored-geometry.md. - Conforming-Delaunay midpoint splitting (
conformingedgeintriangle). - Visibility-region exact (Asano/Suri sweep). The shipped
visibility_regionuses uniform angular sampling β exact enough for visualization at typical resolutions. - Interactive pan/zoom in the demo (only auto-fit + Fit-view button).
TiledWorldv1: translation-only tile offsets (no rotation), links always open (no per-seam doors yet), no agent clearance across seams, and a slight funnel soft-corner at T-junction seams where one tile's edge links to two of its neighbor's (aligned grids are exact; the fix is merging collinear link portals).
- Triangle: Jonathan Richard Shewchuk, "Triangle: Engineering a 2D Quality Mesh Generator and Delaunay Triangulator," in Applied Computational Geometry: Towards Geometric Engineering, vol. 1148 of LNCS, pp. 203β222, Springer, 1996.
https://www.cs.cmu.edu/~quake/triangle.htmlβ rsnav's CDT is an independent implementation inspired by this work and validated againsttriangle.c; it is not a port or copy of Triangle's source and is not covered by Triangle's license. Credit gladly given. - Robust predicates: Shewchuk, "Adaptive Precision Floating-Point Arithmetic and Fast Robust Geometric Predicates," Discrete & Computational Geometry 18:305β363, 1997.
- Simple Stupid Funnel: Mikko Mononen, "Simple Stupid Funnel Algorithm," 2010.
https://digestingduck.blogspot.com/2010/03/simple-stupid-funnel-algorithm.html
Dual-licensed under either of
- MIT license
- Apache License, Version 2.0
at your option.