Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
110 changes: 110 additions & 0 deletions PORTING_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ approximate predicate behavior, porting the same predicate surface to
`hyperreal` becomes a backend upgrade instead of a simultaneous semantic and API
rewrite.

## Branch and commit discipline during the port

Porting work should happen on a branch named `hyperreal` in each repository
touched by the migration. This applies especially to:

- `csgrs`
- `spade`
- `boolmesh`
- `curvo`
- `voxelis`

The default branches should be treated as integration targets, not active
porting branches. Before changing a repository for the port, switch that
repository to its `hyperreal` branch or create it from the appropriate upstream
base if it does not exist yet.

During the porting period, each successful change should be verified, committed,
and pushed automatically to that repository's `hyperreal` branch before moving
on to the next independent change. Cross-repo work should be split into
repository-local commits so `csgrs`, `spade`, `boolmesh`, `curvo`, and `voxelis`
each retain a reviewable history of the port.

After every successful commit and push, take another implementation turn against
the next incomplete item in this plan. Continue that cycle until the plan is
finished, tests expose a blocker, or a design decision requires human review.

## Target ownership model

The stack should have clear vertical responsibilities:
Expand All @@ -31,12 +57,24 @@ csgrs = CSG objects, modeling operations, topology, metadata, IO, and us

- exact rationals
- symbolic and computable reals
- shared reduced expression machinery with symbolic leaves
- standard-real solver variables and formal infinitesimal perturbation leaves
- lazy approximation
- sign, zero, magnitude, and structural facts
- refinement and conservative comparison support

It should not own CSG, polygon, mesh, or CAD-specific geometry types.

The machinery needed for the feature that gives `hyperreal` its name should be
developed together with the machinery needed to map a SolveSpace-style solver
onto `hyperreal`, but the semantics should remain distinct. Both need reduced
expression graphs, symbolic leaves, dependency sets, structural facts,
derivative hooks, and cached evaluation. Solver variables are unknown standard
reals that are bound by an evaluation context during iterative solving.
Infinitesimal perturbations are ordered formal terms used for exact
lexicographic signs, tie-breaking, degeneracy handling, and simulation of
simplicity; they are not ordinary variables to solve for numerically.

### `hyperlattice`

`hyperlattice` should own the general linear algebra layer:
Expand Down Expand Up @@ -228,6 +266,36 @@ new stack:
- `nalgebra::Matrix4<Real>` to the chosen `hyperlattice` transform type once
that API is stable enough

External geometry kernels should be handled on their own `hyperreal` branches.
In particular, `spade`, `boolmesh`, `curvo`, and `voxelis` should not receive
long-running uncommitted local patches during the `csgrs` migration. A verified
change in one of those libraries should be committed and pushed to that
library's `hyperreal` branch before `csgrs` is updated to depend on it.

When a needed function is missing from `hyperlattice`, `hyperlimit`,
`hypersolve`, or a future `hyperphysics` crate, the normal path should be to
borrow the algorithm from an appropriately licensed crate, port it to
`hyperreal`-compatible scalar and fact machinery, and extend the correct stack
crate here. Crates such as `nalgebra` are valid sources when their licenses,
attribution requirements, and implementation boundaries are compatible with the
target crate. The goal is not to keep permanent f64-only adapter islands; it is
to move useful, well-understood algorithms into the `hyperreal` stack so they
share consistency, structural facts, exact/perturbed predicate behavior, and
performance work with the rest of the system.

Use this path for crates that should ultimately be ported to `hyperreal` for
consistency and performance:

- linear algebra, transforms, decomposition, and dense/sparse numeric kernels
belong in `hyperlattice`
- robust geometric predicates, carriers, classification helpers, and
degeneracy policies belong in `hyperlimit`
- solver residual, Jacobian, rank, projection, and constraint scheduling
helpers belong in `hypersolve`
- physical simulation, dynamics, collision-response policy, material behavior,
or other non-geometric physics primitives should live in a separate
`hyperphysics` crate if they do not fit the existing ownership boundaries

Keep these adapters private at first. The first milestone is internal
correctness, not public API churn.

Expand Down Expand Up @@ -312,6 +380,41 @@ At this point, the semantic boundary should already be correct:
The hyperreal port should therefore focus on backend behavior, performance, and
additional certificates rather than rewriting `csgrs` modeling logic again.

This phase should align with the `hypersolve` / SolveSpace-style symbolic
variable work in `hyperreal`. The shared substrate should support:

- expression nodes with symbolic leaves
- dependency and independence facts
- structural sign, zero, magnitude, and domain facts before full evaluation
- reduced-expression caching across repeated queries
- derivative hooks for solver residuals and, where useful, perturbation
propagation
- bounded simplification so solver equations and infinitesimal series do not
grow without control

The first infinitesimal target should be CAD-useful ordered perturbations, not a
general nonstandard-analysis universe. A practical model is a finite
lexicographic perturbation tower:

```text
standard Real + a1*eps + a2*eps^2 + ...
```

or an equivalent ordered perturbation-term representation. `hyperlimit` should
be able to use these terms to decide predicate signs in degenerate cases without
inventing ad hoc epsilon constants. `hypersolve` should use the same expression
and fact infrastructure for standard-real variables, residuals, Jacobians, and
rank diagnostics. The two paths should share representation, reduction,
caching, and derivative infrastructure while keeping their policy layers
separate:

- solver symbols are bound by an evaluation context and participate in numeric
iteration
- infinitesimal symbols are ordered formal perturbations and participate in
lexicographic comparison
- `csgrs` consumes the resulting classifications and certificates, but still
owns CSG topology, metadata, and modeling policy

### Phase 10: Public API cleanup

After internals are stable:
Expand All @@ -324,6 +427,13 @@ After internals are stable:

## Design rules during the port

- Work on the `hyperreal` branch of each affected repository, especially
`csgrs`, `spade`, `boolmesh`, `curvo`, and `voxelis`.
- Commit and push each verified successful change to that repository's
`hyperreal` branch before starting the next independent change.
- After each successful commit and push, take another implementation turn on the
next unfinished porting-plan item, continuing until the plan is complete or a
blocker requires review.
- Lower crates provide facts; `csgrs` makes modeling decisions.
- Predicate uncertainty should be represented explicitly until `csgrs` decides
how to handle it.
Expand Down
2 changes: 1 addition & 1 deletion src/csg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub trait CSG: Sized + Clone {
/// # Example
/// ```
/// use csgrs::mesh::Mesh;
/// use crate::csgrs::traits::CSG;
/// use csgrs::csg::CSG;
/// let mesh = Mesh::<()>::cube(1.0, None).translate(2.0, 1.0, -2.0);
/// let floated = mesh.float();
/// assert_eq!(floated.bounding_box().mins.z, 0.0);
Expand Down
44 changes: 43 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,78 @@
use crate::float_types::Real;
use nalgebra::Point3;

/// Coordinate validation failure for a single point.
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
pub enum PointError {
#[error("point {0:?} has NaN fields")]
NaN(Point3<Real>),
#[error("point {0:?} has infinite fields")]
Infinite(Point3<Real>),
}

/// All the possible validation issues we might encounter,
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum ValidationError {
/// (RepeatedPoint) Two consecutive coords are identical
#[error("point {0:?} is repeated consecutively")]
RepeatedPoint(Point3<Real>),
/// (HoleOutsideShell) A hole is *not* contained by its outer shell
#[error("hole is not contained by its outer shell near {0:?}")]
HoleOutsideShell(Point3<Real>),
/// (NestedHoles) A hole is nested inside another hole
#[error("hole is nested inside another hole near {0:?}")]
NestedHoles(Point3<Real>),
/// (DisconnectedInterior) The interior is disconnected
#[error("interior is disconnected near {0:?}")]
DisconnectedInterior(Point3<Real>),
/// (SelfIntersection) A polygon self‐intersects
#[error("polygon self-intersects near {0:?}")]
SelfIntersection(Point3<Real>),
/// (RingSelfIntersection) A linear ring has a self‐intersection
#[error("linear ring self-intersects near {0:?}")]
RingSelfIntersection(Point3<Real>),
/// (NestedShells) Two outer shells are nested incorrectly
#[error("outer shells are nested incorrectly near {0:?}")]
NestedShells(Point3<Real>),
/// (TooFewPoints) A ring or line has fewer than the minimal #points
#[error("ring or line has too few points near {0:?}")]
TooFewPoints(Point3<Real>),
/// (InvalidCoordinate) The coordinate has a NaN or infinite
#[error("invalid coordinate {0:?}")]
InvalidCoordinate(Point3<Real>),
/// A more precise invalid-coordinate report.
#[error(transparent)]
PointError(#[from] PointError),
/// (RingNotClosed) The ring's first/last points differ
#[error("ring is not closed near {0:?}")]
RingNotClosed(Point3<Real>),
/// (MismatchedVertices) operation requires polygons with same number of vertices
#[error("operation requires polygons with the same number of vertices")]
MismatchedVertices,
/// Operation requires polygons with the same number of vertices.
#[error("operation requires polygons with the same number of vertices, {left} != {right}")]
MismatchedVertexCount { left: usize, right: usize },
/// (IndexOutOfRange) operation requires polygons with same number of vertices
#[error("index out of range")]
IndexOutOfRange,
/// A required index is outside a collection.
#[error("index {index} is out of range for length {len}")]
IndexOutOfRangeWithLen { index: usize, len: usize },
/// (InvalidArguments) operation requires polygons with same number of vertices
#[error("invalid arguments")]
InvalidArguments,
/// A named integer field is below the supported minimum.
#[error("{name} must not be less than {min}")]
FieldLessThan { name: &'static str, min: i32 },
/// A named real field is below the supported minimum.
#[error("{name} must not be less than {min}")]
FieldLessThanFloat { name: &'static str, min: Real },
/// An inconsistency while building a triangle mesh.
#[error("triangle mesh builder error: {0}")]
TriMeshError(String),
/// In general, anything else
#[error("{0}")]
Other(String, Option<Point3<Real>>),
}

Expand Down
4 changes: 2 additions & 2 deletions src/io/stl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use stl_io;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let mesh = Mesh::<()>::cube(1.0, None);
/// let bytes = mesh.to_stl_ascii("my_solid");
/// std::fs::write("stl/my_solid.stl", bytes)?;
/// assert!(bytes.starts_with("solid my_solid"));
/// # Ok(())
/// # }
/// ```
Expand Down Expand Up @@ -57,7 +57,7 @@ pub fn to_stl_ascii<T: Triangulated3D>(
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let object = Mesh::<()>::cube(1.0, None);
/// let bytes = object.to_stl_binary("my_solid")?;
/// std::fs::write("stl/my_solid.stl", bytes)?;
/// assert!(!bytes.is_empty());
/// # Ok(())
/// # }
/// ```
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ fn main() {
);

// 14) Mass properties (just printing them)
let (mass, com, principal_frame) = cube.mass_properties(1.0);
let (mass, com, principal_frame) = cube.mass_properties(1.0).unwrap();
println!("Cube mass = {}", mass);
println!("Cube center of mass = {:?}", com);
println!("Cube principal inertia local frame = {:?}", principal_frame);
Expand Down
27 changes: 15 additions & 12 deletions src/mesh/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! `Mesh` struct and implementations of the `CSGOps` trait for `Mesh`

use crate::errors::ValidationError;
use crate::float_types::{
parry3d::{
bounding_volume::Aabb,
Expand Down Expand Up @@ -325,20 +326,22 @@ impl<S: Clone + Send + Sync + Debug> Mesh<S> {
///
/// ## Errors
/// If any 3d polygon has fewer than 3 vertices, or Parry returns a `TriMeshBuilderError`
pub fn to_rapier_shape(&self) -> SharedShape {
pub fn to_rapier_shape(&self) -> Result<SharedShape, ValidationError> {
let (vertices, indices) = self.get_vertices_and_indices();
let trimesh = TriMesh::new(vertices, indices).unwrap();
SharedShape::new(trimesh)
let trimesh = TriMesh::new(vertices, indices)
.map_err(|err| ValidationError::TriMeshError(format!("{err:?}")))?;
Ok(SharedShape::new(trimesh))
}

/// Convert the polygons in this Mesh to a Parry `TriMesh`.\
/// Useful for collision detection.
///
/// ## Errors
/// If any 3d polygon has fewer than 3 vertices, or Parry returns a `TriMeshBuilderError`
pub fn to_trimesh(&self) -> Option<TriMesh> {
pub fn to_trimesh(&self) -> Result<TriMesh, ValidationError> {
let (vertices, indices) = self.get_vertices_and_indices();
TriMesh::new(vertices, indices).ok()
TriMesh::new(vertices, indices)
.map_err(|err| ValidationError::TriMeshError(format!("{err:?}")))
}

/// Uses Parry to check if a point is inside a `Mesh`'s as a `TriMesh`.\
Expand Down Expand Up @@ -371,15 +374,15 @@ impl<S: Clone + Send + Sync + Debug> Mesh<S> {
pub fn mass_properties(
&self,
density: Real,
) -> (Real, Point3<Real>, Unit<Quaternion<Real>>) {
let trimesh = self.to_trimesh().unwrap();
) -> Result<(Real, Point3<Real>, Unit<Quaternion<Real>>), ValidationError> {
let trimesh = self.to_trimesh()?;
let mp = trimesh.mass_properties(density);

(
Ok((
mp.mass(),
mp.local_com, // a Point3<Real>
mp.principal_inertia_local_frame, // a Unit<Quaternion<Real>>
)
))
}

/// Create a Rapier rigid body + collider from this Mesh, using
Expand All @@ -392,8 +395,8 @@ impl<S: Clone + Send + Sync + Debug> Mesh<S> {
translation: Vector3<Real>,
rotation: Vector3<Real>, // rotation axis scaled by angle (radians)
density: Real,
) -> RigidBodyHandle {
let shape = self.to_rapier_shape();
) -> Result<RigidBodyHandle, ValidationError> {
let shape = self.to_rapier_shape()?;

// Build a Rapier RigidBody
let rb = RigidBodyBuilder::dynamic()
Expand All @@ -407,7 +410,7 @@ impl<S: Clone + Send + Sync + Debug> Mesh<S> {
let coll = ColliderBuilder::new(shape).density(density).build();
co_set.insert_with_parent(coll, rb_handle, rb_set);

rb_handle
Ok(rb_handle)
}

/// Convert a Mesh into a Bevy `Mesh`.
Expand Down
5 changes: 4 additions & 1 deletion src/mesh/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,10 @@ impl<S: Clone + Debug + Send + Sync> Mesh<S> {
for &idx in face.iter() {
// Ensure the index is valid
if idx >= points.len() {
return Err(ValidationError::IndexOutOfRange);
return Err(ValidationError::IndexOutOfRangeWithLen {
index: idx,
len: points.len(),
});
}
let [x, y, z] = points[idx];
face_vertices.push(Vertex::new(
Expand Down
8 changes: 4 additions & 4 deletions src/polygon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ impl<S: Clone + Send + Sync> Polygon<S> {
// We'll keep a queue of triangles to process
let mut queue = vec![tri];
for _ in 0..subdivisions.get() {
let mut next_level = Vec::new();
let mut next_level = Vec::with_capacity(queue.len() * 4);
for t in queue {
let subs = subdivide_triangle(t);
next_level.extend(subs);
Expand Down Expand Up @@ -574,13 +574,13 @@ pub fn build_orthonormal_basis(n: Vector3<Real>) -> (Vector3<Real>, Vector3<Real
(u, v)
}

// Helper function to subdivide a triangle
pub fn subdivide_triangle(tri: [Vertex; 3]) -> Vec<[Vertex; 3]> {
/// Helper function to subdivide a triangle into four smaller triangles.
pub fn subdivide_triangle(tri: [Vertex; 3]) -> [[Vertex; 3]; 4] {
let v01 = tri[0].interpolate(&tri[1], 0.5);
let v12 = tri[1].interpolate(&tri[2], 0.5);
let v20 = tri[2].interpolate(&tri[0], 0.5);

vec![
[
[tri[0], v01, v20],
[v01, tri[1], v12],
[v20, v12, tri[2]],
Expand Down
Loading
Loading