Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

72 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

rsnav

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.

At a glance

              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   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

Documentation

Full prose documentation lives in docs/README.md, which routes by who you are and by what went wrong. Three entry points:

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.

Quick start

Interactive demo

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).

Batch-run fixtures from the CLI

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.

Dynamic-obstacles testbed

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.

Multi-agent crowd testbed

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.

Doors testbed

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.

Multi-tile world demo

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.

Programmatic use

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.

Crates

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).

File format

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.

Status

Working and tested (~205 tests pass workspace-wide):

  • βœ… CDT round-trip against triangle.c reference (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 (-q switch in triangle) β€” no facility for it.
  • Self-intersecting PSLG segments (segmentintersection in triangle) β€” on the legacy path, form_skeleton returns Err(SelfIntersection) rather than splitting the crossing. This is no longer the whole story: build_cdt_with_inset exists 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 in form_skeleton. See docs/13-authored-geometry.md.
  • Conforming-Delaunay midpoint splitting (conformingedge in triangle).
  • Visibility-region exact (Asano/Suri sweep). The shipped visibility_region uses uniform angular sampling β€” exact enough for visualization at typical resolutions.
  • Interactive pan/zoom in the demo (only auto-fit + Fit-view button).
  • TiledWorld v1: 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).

References

  • 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 against triangle.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

License

Dual-licensed under either of

  • MIT license
  • Apache License, Version 2.0

at your option.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages