Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ members = [
"crates/rmg-core",
"crates/rmg-ffi",
"crates/rmg-wasm",
"crates/rmg-cli"
"crates/rmg-cli",
"crates/rmg-geom"
]
resolver = "2"

Expand Down
15 changes: 15 additions & 0 deletions crates/rmg-geom/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "rmg-geom"
version = "0.1.0"
edition = "2021"
description = "Echo geometry primitives: AABB, transforms, temporal proxies, and broad-phase scaffolding"
license = "Apache-2.0"
repository = "https://example.invalid/echo"
readme = "../../README.md"
keywords = ["echo", "geometry", "aabb", "broad-phase"]
categories = ["game-engines", "data-structures"]

[dependencies]
rmg-core = { path = "../rmg-core" }

#[dev-dependencies]
81 changes: 81 additions & 0 deletions crates/rmg-geom/src/broad/aabb_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use crate::types::aabb::Aabb;
use core::cmp::Ordering;
use std::collections::BTreeMap;

/// Broad-phase interface for inserting proxies and querying overlapping pairs.
///
/// Implementations must return pairs deterministically: the pair `(a, b)` is
/// canonicalized such that `a < b`, and the full list is sorted ascending by
/// `(a, b)`.
pub trait BroadPhase {
/// Inserts or updates the proxy with the given `id` and `aabb`.
fn upsert(&mut self, id: usize, aabb: Aabb);
/// Removes a proxy if present.
fn remove(&mut self, id: usize);
/// Returns a canonical, deterministically-ordered list of overlapping pairs.
fn pairs(&self) -> Vec<(usize, usize)>;
}

/// A minimal AABB-based broad-phase using an `O(n^2)` all-pairs sweep.
///
/// Why this exists:
/// - Serves as a correctness and determinism baseline while API surfaces
/// stabilize (canonical pair identity and ordering, inclusive face overlap).
/// - Keeps the algorithm small and easy to reason about for early tests.
///
/// Performance plan (to be replaced):
/// - Sweep-and-Prune (aka Sort-and-Sweep) with stable endpoint arrays per
/// axis. Determinism ensured via:
/// - fixed axis order (e.g., X→Y→Z) or a deterministic axis choice
/// (variance with ID tie-breakers),
/// - stable sort and explicit ID tie-breaks,
/// - final pair list sorted lexicographically by `(min_id, max_id)`.
/// - Dynamic AABB Tree (BVH): deterministic insert/rotation heuristics with
/// ID-based tie-breakers; canonical pair set post-sorted by `(min_id,max_id)`.
///
/// Complexity notes:
/// - Any broad phase degenerates to `O(n^2)` when all proxies overlap (k≈n²).
/// The goal of SAP/BVH is near-linear behavior when the true overlap count
/// `k` is small and motion is temporally coherent.
///
/// TODO(geom): replace this reference implementation with a deterministic
/// Sweep-and-Prune (Phase 1), and optionally a Dynamic AABB Tree. Preserve
/// canonical pair ordering and inclusive face-touch semantics.
#[derive(Default)]
pub struct AabbTree {
items: BTreeMap<usize, Aabb>,
}

impl AabbTree {
/// Creates an empty tree.
#[must_use]
pub fn new() -> Self { Self { items: BTreeMap::new() } }
}

impl BroadPhase for AabbTree {
fn upsert(&mut self, id: usize, aabb: Aabb) {
self.items.insert(id, aabb);
}

fn remove(&mut self, id: usize) {
self.items.remove(&id);
}

fn pairs(&self) -> Vec<(usize, usize)> {
// BTreeMap iteration is already sorted by key; copy to a vector for indexed loops.
let items: Vec<(usize, Aabb)> = self.items.iter().map(|(id, aabb)| (*id, *aabb)).collect();
let mut out: Vec<(usize, usize)> = Vec::new();
for (i, (a_id, a_bb)) in items.iter().enumerate() {
for (b_id, b_bb) in items.iter().skip(i + 1) {
if a_bb.overlaps(b_bb) {
out.push((*a_id, *b_id)); // canonical since a_id < b_id
}
}
}
out.sort_unstable_by(|x, y| match x.0.cmp(&y.0) {
Ordering::Equal => x.1.cmp(&y.1),
o => o,
});
out
}
}
13 changes: 13 additions & 0 deletions crates/rmg-geom/src/broad/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Broad-phase interfaces and a minimal reference implementation.
//!
//! Determinism contract (applies to all implementations used here):
//! - Pair identity is canonicalized as `(min_id, max_id)`.
//! - The emitted pair list is strictly sorted lexicographically by that tuple.
//! - Overlap is inclusive on faces (touching AABBs are considered overlapping).
//!
//! The current `AabbTree` is an `O(n^2)` all-pairs baseline intended only for
//! early tests. It will be replaced by a deterministic Sweep-and-Prune (and/or
//! a Dynamic AABB Tree) while preserving the ordering and overlap semantics.

#[doc = "Reference AABB-based broad-phase and trait definitions."]
pub mod aabb_tree;
35 changes: 35 additions & 0 deletions crates/rmg-geom/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#![deny(
warnings,
clippy::all,
clippy::pedantic,
clippy::nursery,
clippy::cargo,
rust_2018_idioms,
missing_docs,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
#![doc = r"Geometry primitives for Echo.

This crate provides:
- Axis-aligned bounding boxes (`Aabb`).
- Rigid transforms (`Transform`).
- Temporal utilities (`Tick`, `TemporalTransform`, `TemporalProxy`).
- A minimal broad-phase trait and an AABB-based pairing structure.

Design notes:
- Deterministic: no ambient RNG; ordering of pair outputs is canonical.
- Float32 throughout; operations favor clarity and reproducibility.
- Rustdoc is treated as part of the contract; public items are documented.
"]

/// Foundational geometric types.
pub mod types;
/// Time-aware utilities for broad-phase and motion.
pub mod temporal;
/// Broad-phase interfaces and a simple AABB-based implementation.
pub mod broad;

pub use types::aabb::Aabb;
pub use types::transform::Transform;
8 changes: 8 additions & 0 deletions crates/rmg-geom/src/temporal/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! Temporal types and helpers used for tick-based motion and broad-phase.

#[doc = "Discrete Chronos ticks (u64 newtype)."]
pub mod tick;
#[doc = "Start/end transforms over a tick and fat AABB computation."]
pub mod temporal_transform;
#[doc = "Broad-phase proxy carrying entity id, tick, and fat AABB."]
pub mod temporal_proxy;
32 changes: 32 additions & 0 deletions crates/rmg-geom/src/temporal/temporal_proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::temporal::tick::Tick;
use crate::types::aabb::Aabb;

/// Broad-phase proxy summarizing an entity’s motion window for a tick.
///
/// Stores a conservative fat AABB and the owning `entity` identifier (opaque
/// to the geometry layer). The proxy is suitable for insertion into a broad-
/// phase accelerator.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TemporalProxy {
entity: u64,
tick: Tick,
fat: Aabb,
}

impl TemporalProxy {
/// Creates a new proxy for `entity` at `tick` with precomputed `fat` AABB.
#[must_use]
pub const fn new(entity: u64, tick: Tick, fat: Aabb) -> Self { Self { entity, tick, fat } }

/// Opaque entity identifier owning this proxy.
#[must_use]
pub const fn entity(&self) -> u64 { self.entity }

/// Tick associated with the motion window.
#[must_use]
pub const fn tick(&self) -> Tick { self.tick }

/// Conservative fat AABB for this proxy.
#[must_use]
pub const fn fat(&self) -> Aabb { self.fat }
}
44 changes: 44 additions & 0 deletions crates/rmg-geom/src/temporal/temporal_transform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use crate::types::{aabb::Aabb, transform::Transform};

/// Transform at two adjacent ticks used to bound motion in the broad-phase.
///
/// - `start` corresponds to tick `n`.
/// - `end` corresponds to tick `n+1`.
///
/// Determinism and plan:
/// - `fat_aabb` below currently uses a union of the start/end AABBs. This is
/// conservative for linear motion and sufficient for pairing/CCD triggers.
/// - Future: introduce a quantized margin policy (based on velocity, `dt`, and
/// shape scale) so that fat proxies are identical across peers/branches. The
/// policy and quantization will be recorded in the graph/spec.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TemporalTransform {
start: Transform,
end: Transform,
}

impl TemporalTransform {
/// Creates a new `TemporalTransform` from start and end transforms.
#[must_use]
pub const fn new(start: Transform, end: Transform) -> Self { Self { start, end } }

/// Returns the start transform.
#[must_use]
pub const fn start(&self) -> Transform { self.start }

/// Returns the end transform.
#[must_use]
pub const fn end(&self) -> Transform { self.end }

/// Computes a conservative fat AABB for a collider with local-space `shape` AABB.
///
/// The fat box is defined as the union of the shape’s AABBs at the start and
/// end transforms. This is conservative for linear motion and suffices for
/// broad-phase pairing and CCD triggering.
#[must_use]
pub fn fat_aabb(&self, shape: &Aabb) -> Aabb {
let a0 = shape.transformed(&self.start.to_mat4());
let a1 = shape.transformed(&self.end.to_mat4());
a0.union(&a1)
}
}
19 changes: 19 additions & 0 deletions crates/rmg-geom/src/temporal/tick.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// Discrete simulation tick in Chronos time.
///
/// The engine advances in integer ticks with a fixed `dt` per branch. This
/// newtype ensures explicit tick passing across APIs.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Tick {
index: u64,
}

impl Tick {
/// Creates a new tick with the given index.
#[must_use]
pub const fn new(index: u64) -> Self { Self { index } }

/// Returns the tick index.
#[must_use]
pub const fn index(&self) -> u64 { self.index }
}

Loading
Loading