diff --git a/Makefile b/Makefile
index 88ed72a..3f58df0 100644
--- a/Makefile
+++ b/Makefile
@@ -10,11 +10,23 @@ check-fmt:
cargo fmt --check
uv --project pyraydeon run ruff format --check pyraydeon
-.PHONY: render-test
-render-test:
+.PHONY: rust-render-test
+rust-render-test:
./raydeon/check-examples.sh
+
+.PHONY: reinstall-py-venv
+reinstall-py-venv:
+ echo "Reinstalling native dependencies in virtualenv..."
+ uv --project pyraydeon run --reinstall python -c 'print("Reinstalled dependencies")'
+
+
+.PHONY: py-render-test
+py-render-test: reinstall-py-venv
./pyraydeon/check-examples.sh
+.PHONY: render-test
+render-test: rust-render-test py-render-test
+
.PHONY: unit-test
unit-test:
cargo test --locked
diff --git a/pyraydeon/check-examples.sh b/pyraydeon/check-examples.sh
index 814d906..e8462df 100755
--- a/pyraydeon/check-examples.sh
+++ b/pyraydeon/check-examples.sh
@@ -22,9 +22,6 @@ if [ "$ALL_TOOLS_FOUND" = false ]; then
exit 1
fi
-echo "Reinstalling native dependencies in virtualenv..."
-uv --project ${SCRIPT_DIR} run --reinstall python -c 'print("Reinstalled dependencies")'
-
for example in ${SCRIPT_DIR}/examples/*.py; do
example_name=$(basename "$example" .py)
diff --git a/pyraydeon/examples/py_cubes.py b/pyraydeon/examples/py_cubes.py
index 3f9d971..2d62a8d 100644
--- a/pyraydeon/examples/py_cubes.py
+++ b/pyraydeon/examples/py_cubes.py
@@ -19,36 +19,6 @@
)
-class Quad(CollisionGeometry):
- def __init__(self, vertices):
- self.vertices = vertices
- self.plane = self.compute_plane(vertices)
-
- def compute_plane(self, points):
- p1, p2, p3 = points[:3]
- normal = np.cross(p2 - p1, p3 - p1)
- normal /= np.linalg.norm(normal)
- return Plane(p1, normal)
-
- def is_point_in_face(self, point):
- edge1 = self.vertices[1] - self.vertices[0]
- edge2 = self.vertices[3] - self.vertices[0]
- v = point - self.vertices[0]
- u1 = np.dot(v, edge1) / np.dot(edge1, edge1)
- u2 = np.dot(v, edge2) / np.dot(edge2, edge2)
- return 0 <= u1 <= 1 and 0 <= u2 <= 1
-
- def hit_by(self, ray) -> HitData | None:
- intersection = self.plane.hit_by(ray)
- if intersection is not None and self.is_point_in_face(intersection.hit_point):
- return intersection
-
- def bounding_box(self):
- my_min = np.minimum.reduce(self.vertices)
- my_max = np.maximum.reduce(self.vertices)
- return AABB3(my_min, my_max)
-
-
class RectPrism(Geometry):
def __init__(
self,
@@ -117,7 +87,7 @@ def __repr__(self):
return f"RectPrism(basis='[{self.right}, {self.up}, {self.fwd}]', dims='[{self.width}, {self.height}, {self.depth}]')"
def collision_geometry(self):
- return [Quad(self.vertices[face]) for face in self.faces]
+ return [PyQuad(self.vertices[face]) for face in self.faces]
def paths(self, cam):
edges = set(
@@ -134,6 +104,36 @@ def paths(self, cam):
return paths
+class PyQuad(CollisionGeometry):
+ def __init__(self, vertices):
+ self.vertices = vertices
+ self.plane = self.compute_plane(vertices)
+
+ def compute_plane(self, points):
+ p1, p2, p3 = points[:3]
+ normal = np.cross(p2 - p1, p3 - p1)
+ normal /= np.linalg.norm(normal)
+ return Plane(p1, normal)
+
+ def is_point_in_face(self, point):
+ edge1 = self.vertices[1] - self.vertices[0]
+ edge2 = self.vertices[3] - self.vertices[0]
+ v = point - self.vertices[0]
+ u1 = np.dot(v, edge1) / np.dot(edge1, edge1)
+ u2 = np.dot(v, edge2) / np.dot(edge2, edge2)
+ return 0 <= u1 <= 1 and 0 <= u2 <= 1
+
+ def hit_by(self, ray) -> HitData | None:
+ intersection = self.plane.hit_by(ray)
+ if intersection is not None and self.is_point_in_face(intersection.hit_point):
+ return intersection
+
+ def bounding_box(self):
+ my_min = np.minimum.reduce(self.vertices)
+ my_max = np.maximum.reduce(self.vertices)
+ return AABB3(my_min, my_max)
+
+
up = np.array([-1.0, 1.0, 0.0])
up = up / np.linalg.norm(up)
right = np.array([1.0, 1.0, 0.0])
diff --git a/pyraydeon/examples/py_rhombohedron.py b/pyraydeon/examples/py_rhombohedron.py
new file mode 100644
index 0000000..c2e8d0f
--- /dev/null
+++ b/pyraydeon/examples/py_rhombohedron.py
@@ -0,0 +1,155 @@
+"""Demonstrates custom objects with native collision geometry"""
+
+import numpy as np
+import svg
+
+from pyraydeon import (
+ Camera,
+ Geometry,
+ LineSegment3D,
+ Scene,
+ Quad,
+)
+
+
+class Rhombohedron(Geometry):
+ def __init__(self, origin, basis, dims):
+ basis = basis / np.linalg.norm(basis, axis=1, keepdims=True)
+
+ self.origin = origin
+ self.basis = basis
+ self.dims = dims
+
+ combinations = np.array(np.meshgrid([0, 1], [0, 1], [0, 1])).T.reshape(-1, 3)
+ scaled_combinations = combinations * dims
+
+ # Transform the scaled combinations using the basis
+ transformed_vertices = np.dot(scaled_combinations, basis)
+ # Shift by the origin
+ self.vertices = transformed_vertices + origin
+
+ # Make edges stand out slightly so as to not be intersected by their own faces
+ centroid = np.mean(self.vertices, axis=0)
+ vert_move_dirs = self.vertices - centroid
+ unit_move_dirs = vert_move_dirs / np.linalg.norm(
+ vert_move_dirs, axis=0, keepdims=True
+ )
+ move_vectors = unit_move_dirs * 0.0015
+ self.path_vertices = self.vertices + move_vectors
+
+ self.faces = [
+ [0, 1, 3, 2], # Bottom face
+ [4, 5, 7, 6], # Top face
+ [0, 1, 5, 4], # Front face
+ [2, 3, 7, 6], # Back face
+ [0, 2, 6, 4], # Left face
+ [1, 3, 7, 5], # Right face
+ ]
+ self.quads = self.compute_quads()
+
+ def __repr__(self):
+ return f"Rhomboid(origin='{self.origin}', basis='{self.basis}', dims='{self.dims}')"
+
+ def compute_quads(self):
+ quads = []
+ for face in self.faces:
+ verts = self.vertices[face]
+ origin = verts[0]
+ basis = np.array(
+ [
+ verts[1] - origin,
+ verts[3] - origin,
+ ]
+ )
+ dims = np.linalg.norm(basis, axis=1)
+
+ quads.append(Quad(origin, basis, dims))
+ return quads
+
+ def collision_geometry(self):
+ return [geom for quad in self.quads for geom in quad.collision_geometry()]
+
+ def paths(self, cam):
+ edges = set(
+ [
+ tuple(sorted((face[i], face[(i + 1) % len(face)])))
+ for face in self.faces
+ for i in range(len(face))
+ ]
+ )
+ paths = [
+ LineSegment3D(self.path_vertices[edge[0]], self.path_vertices[edge[1]])
+ for edge in edges
+ ]
+ return paths
+
+
+scene = Scene(
+ [
+ Rhombohedron(
+ origin=np.array([0.0, 0.0, 0.0]),
+ basis=np.array(
+ [
+ [0.9, 0.5, 0.0],
+ [-0.3, 1.0, 0.0],
+ [-0.5, 0.25, -0.7],
+ ]
+ ),
+ dims=np.array([1.0, 1.0, 1.0]),
+ ),
+ Rhombohedron(
+ origin=np.array([2.0, 0.0, -2.0]),
+ basis=np.array(
+ [
+ [-0.9, 0.5, 0.0],
+ [0.3, 1.0, 0.5],
+ [1.5, 0.25, -0.7],
+ ]
+ ),
+ dims=np.array([1.0, 1.0, 1.0]),
+ ),
+ ]
+)
+
+eye = np.array([0, 0.4, 5])
+focus = np.array([0, 0.4, 0])
+up = np.array([0, 1, 0])
+
+fovy = 60.0
+width = 1024
+height = 1024
+znear = 0.1
+zfar = 20.0
+
+cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)
+
+paths = scene.render(cam)
+
+canvas = svg.SVG(
+ width="8in",
+ height="8in",
+ viewBox="0 0 1024 1024",
+)
+backing_rect = svg.Rect(
+ x=0,
+ y=0,
+ width="100%",
+ height="100%",
+ fill="white",
+)
+svg_lines = [
+ svg.Line(
+ x1=f"{path.p1[0]}",
+ y1=f"{path.p1[1]}",
+ x2=f"{path.p2[0]}",
+ y2=f"{path.p2[1]}",
+ stroke_width="0.7mm",
+ stroke="black",
+ )
+ for path in paths
+]
+line_group = svg.G(transform=f"translate(0, {height}) scale(1, -1)", elements=svg_lines)
+canvas.elements = [backing_rect, line_group]
+
+
+print(canvas)
diff --git a/pyraydeon/examples/py_rhombohedron_expected.svg b/pyraydeon/examples/py_rhombohedron_expected.svg
new file mode 100644
index 0000000..23143b2
--- /dev/null
+++ b/pyraydeon/examples/py_rhombohedron_expected.svg
@@ -0,0 +1 @@
+
diff --git a/pyraydeon/src/linear.rs b/pyraydeon/src/linear.rs
index b7ef730..d3ad9fc 100644
--- a/pyraydeon/src/linear.rs
+++ b/pyraydeon/src/linear.rs
@@ -1,9 +1,6 @@
-use numpy::{Ix1, PyArray, PyReadonlyArray1};
+use numpy::PyReadonlyArray1;
use pyo3::exceptions::PyIndexError;
use pyo3::prelude::*;
-use raydeon::CollisionGeometry as _;
-
-use crate::ray::{HitData, Ray};
#[derive(Debug, Copy, Clone)]
pub struct ArbitrarySpace;
@@ -248,42 +245,8 @@ impl FloatIter {
}
}
-pywrap!(AABB3, raydeon::AABB3);
-
-#[pymethods]
-impl AABB3 {
- #[new]
- fn new(min: PyReadonlyArray1, max: PyReadonlyArray1) -> PyResult {
- let min = Point3::try_from(min)?;
- let max = Point3::try_from(max)?;
- Ok(raydeon::AABB3::new(min.0, max.0).into())
- }
-
- #[getter]
- fn min<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> {
- PyArray::from_slice_bound(py, &self.0.min.to_array())
- }
-
- #[getter]
- fn max<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> {
- PyArray::from_slice_bound(py, &self.0.max.to_array())
- }
-
- fn hit_by(&self, _py: Python, ray: Ray) -> Option {
- raydeon::shapes::AxisAlignedCuboid::from(self.0.cast_unit())
- .hit_by(&ray.0)
- .map(Into::into)
- }
-
- fn __repr__(slf: &Bound<'_, Self>) -> PyResult {
- let class_name = slf.get_type().qualname()?;
- Ok(format!("{}<{:?}>", class_name, slf.borrow().0))
- }
-}
-
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
- m.add_class::()?;
Ok(())
}
diff --git a/pyraydeon/src/ray.rs b/pyraydeon/src/ray.rs
index a27c8e2..2a0a1b9 100644
--- a/pyraydeon/src/ray.rs
+++ b/pyraydeon/src/ray.rs
@@ -1,7 +1,8 @@
use numpy::{Ix1, PyArray, PyReadonlyArray1};
use pyo3::prelude::*;
+use raydeon::CollisionGeometry as _;
-use crate::linear::{Point3, Vec3};
+use crate::linear::{ArbitrarySpace, Point3, Vec3};
pywrap!(Ray, raydeon::Ray);
@@ -56,8 +57,42 @@ impl HitData {
}
}
+pywrap!(AABB3, raydeon::AABB3);
+
+#[pymethods]
+impl AABB3 {
+ #[new]
+ fn new(min: PyReadonlyArray1, max: PyReadonlyArray1) -> PyResult {
+ let min = Point3::try_from(min)?;
+ let max = Point3::try_from(max)?;
+ Ok(raydeon::AABB3::new(min.0, max.0).into())
+ }
+
+ #[getter]
+ fn min<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> {
+ PyArray::from_slice_bound(py, &self.0.min.to_array())
+ }
+
+ #[getter]
+ fn max<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> {
+ PyArray::from_slice_bound(py, &self.0.max.to_array())
+ }
+
+ fn hit_by(&self, _py: Python, ray: Ray) -> Option {
+ raydeon::shapes::AxisAlignedCuboid::from(self.0.cast_unit())
+ .hit_by(&ray.0)
+ .map(Into::into)
+ }
+
+ fn __repr__(slf: &Bound<'_, Self>) -> PyResult {
+ let class_name = slf.get_type().qualname()?;
+ Ok(format!("{}<{:?}>", class_name, slf.borrow().0))
+ }
+}
+
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
+ m.add_class::()?;
Ok(())
}
diff --git a/pyraydeon/src/shapes/mod.rs b/pyraydeon/src/shapes/mod.rs
index aa499e1..f0df791 100644
--- a/pyraydeon/src/shapes/mod.rs
+++ b/pyraydeon/src/shapes/mod.rs
@@ -1,4 +1,4 @@
-use primitive::Plane;
+use primitive::{Plane, Quad};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
use raydeon::WorldSpace;
@@ -8,8 +8,7 @@ mod primitive;
pub(crate) use primitive::{AxisAlignedCuboid, Tri};
-use crate::linear::AABB3;
-use crate::ray::{HitData, Ray};
+use crate::ray::{HitData, Ray, AABB3};
use crate::scene::{Camera, LineSegment3D};
#[derive(Debug)]
@@ -38,7 +37,7 @@ impl Geometry {
pub(crate) fn geometry(&self, obj: PyObject) -> Arc> {
match &self.geom {
InnerGeometry::Native(ref geom) => Arc::clone(geom),
- InnerGeometry::Py => Arc::new(PythonGeometry::new(obj)),
+ InnerGeometry::Py => Arc::new(PythonGeometry::new(obj, PythonGeometryKind::Draw)),
}
}
}
@@ -82,7 +81,7 @@ impl Geometry {
fn __repr__(slf: &Bound<'_, Self>) -> PyResult {
let class_name = slf.get_type().qualname()?;
- Ok(format!("{}<{:?}>", class_name, slf.borrow().geom))
+ Ok(format!("{}<{:#?}>", class_name, slf.borrow().geom))
}
}
@@ -141,25 +140,35 @@ impl CollisionGeometry {
fn __repr__(slf: &Bound<'_, Self>) -> PyResult {
let class_name = slf.get_type().qualname()?;
- Ok(format!("{}<{:?}>", class_name, slf.borrow().geom))
+ Ok(format!("{}<{:#?}>", class_name, slf.borrow().geom))
}
}
#[derive(Debug)]
struct PythonGeometry {
slf: PyObject,
- aabb: Option,
+ kind: PythonGeometryKind,
+}
+
+#[derive(Debug)]
+enum PythonGeometryKind {
+ Draw,
+ Collision { aabb: Option },
}
impl PythonGeometry {
- fn new(slf: PyObject) -> Self {
- Self { slf, aabb: None }
+ fn new(slf: PyObject, kind: PythonGeometryKind) -> Self {
+ Self { slf, kind }
}
fn as_collision_geometry(slf: PyObject) -> Self {
- let mut ret = Self { slf, aabb: None };
+ let mut ret = Self {
+ slf,
+ kind: PythonGeometryKind::Draw,
+ };
let aabb = raydeon::CollisionGeometry::bounding_box(&ret);
- ret.aabb = aabb.map(|aabb| aabb.cast_unit().into());
+ let aabb = aabb.map(|aabb| aabb.cast_unit().into());
+ ret.kind = PythonGeometryKind::Collision { aabb };
ret
}
}
@@ -211,7 +220,7 @@ impl raydeon::Shape for PythonGeometry {
impl raydeon::CollisionGeometry for PythonGeometry {
fn hit_by(&self, ray: &raydeon::Ray) -> Option {
- if let Some(aabb) = self.aabb {
+ if let PythonGeometryKind::Collision { aabb: Some(aabb) } = self.kind {
raydeon::shapes::AxisAlignedCuboid::from(aabb.0.cast_unit()).hit_by(ray)?;
}
Python::with_gil(|py| {
@@ -243,6 +252,7 @@ pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
+ m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
Ok(())
diff --git a/pyraydeon/src/shapes/primitive.rs b/pyraydeon/src/shapes/primitive.rs
index 705fc92..ebc7629 100644
--- a/pyraydeon/src/shapes/primitive.rs
+++ b/pyraydeon/src/shapes/primitive.rs
@@ -1,5 +1,7 @@
use super::{CollisionGeometry, Geometry};
use crate::linear::{Point3, Vec3};
+use numpy::{PyArrayLike1, PyArrayLike2};
+use pyo3::exceptions::PyIndexError;
use pyo3::prelude::*;
use raydeon::WorldSpace;
use std::sync::Arc;
@@ -123,3 +125,72 @@ impl Plane {
Ok((Self(shape), geom))
}
}
+
+#[pyclass(frozen, extends=Geometry, subclass)]
+pub(crate) struct Quad(pub(crate) Arc);
+
+impl ::std::ops::Deref for Quad {
+ type Target = Arc;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl From> for Quad {
+ fn from(value: Arc) -> Self {
+ Self(value)
+ }
+}
+
+#[pymethods]
+impl Quad {
+ #[new]
+ #[pyo3(signature = (origin, basis, dims, tag=0))]
+ fn new(
+ origin: &Bound<'_, PyAny>,
+ basis: PyArrayLike2<'_, f64>,
+ dims: PyArrayLike1<'_, f64>,
+ tag: usize,
+ ) -> PyResult<(Self, Geometry)> {
+ let origin: Point3 = origin.try_into()?;
+ let basis = basis
+ .as_array()
+ .as_slice()
+ .ok_or(PyIndexError::new_err("basis must be 2x3 array"))
+ .and_then(|slice| {
+ if slice.len() != 6 {
+ Err(PyIndexError::new_err("basis must be 2x3 array"))
+ } else {
+ Ok(slice)
+ }
+ })
+ .map(|basis| {
+ [
+ raydeon::Vec3::new(basis[0], basis[1], basis[2]),
+ raydeon::Vec3::new(basis[3], basis[4], basis[5]),
+ ]
+ })?;
+ let dims = dims
+ .as_array()
+ .as_slice()
+ .ok_or(PyIndexError::new_err("dims must be 1x2 array"))
+ .and_then(|slice| {
+ if slice.len() != 2 {
+ Err(PyIndexError::new_err("dims must be 1x2 array"))
+ } else {
+ Ok(slice)
+ }
+ })
+ .map(|dims| [dims[0], dims[1]])?;
+
+ let shape = Arc::new(raydeon::shapes::Quad::tagged(
+ origin.0.cast_unit(),
+ basis,
+ dims,
+ tag,
+ ));
+ let geom = Geometry::native(Arc::clone(&shape) as Arc>);
+ Ok((Self(shape), geom))
+ }
+}
diff --git a/raydeon/src/shapes/aacuboid.rs b/raydeon/src/shapes/aacuboid.rs
index 3212cd5..3605634 100644
--- a/raydeon/src/shapes/aacuboid.rs
+++ b/raydeon/src/shapes/aacuboid.rs
@@ -1,3 +1,4 @@
+//! Provides basic drawing and collision for axis-aligned cuboids.
use std::sync::Arc;
use collision::Continuous;
diff --git a/raydeon/src/shapes/mod.rs b/raydeon/src/shapes/mod.rs
index 461f9be..2e18284 100644
--- a/raydeon/src/shapes/mod.rs
+++ b/raydeon/src/shapes/mod.rs
@@ -1,9 +1,11 @@
pub use self::aacuboid::AxisAlignedCuboid;
pub use self::plane::Plane;
+pub use self::quad::Quad;
pub use self::sphere::Sphere;
pub use self::triangle::Triangle;
pub mod aacuboid;
pub mod plane;
+pub mod quad;
pub mod sphere;
pub mod triangle;
diff --git a/raydeon/src/shapes/plane.rs b/raydeon/src/shapes/plane.rs
index 051ecf8..1f65b46 100644
--- a/raydeon/src/shapes/plane.rs
+++ b/raydeon/src/shapes/plane.rs
@@ -1,3 +1,4 @@
+//! Provides collision for 3D planes.
use crate::{CollisionGeometry, HitData, Ray, WPoint3, WVec3, WorldSpace};
#[derive(Debug, Copy, Clone)]
diff --git a/raydeon/src/shapes/quad.rs b/raydeon/src/shapes/quad.rs
new file mode 100644
index 0000000..8b0bf70
--- /dev/null
+++ b/raydeon/src/shapes/quad.rs
@@ -0,0 +1,70 @@
+use std::sync::Arc;
+
+use super::Triangle;
+use crate::path::LineSegment3D;
+use crate::{Camera, CollisionGeometry, Shape, WPoint3, WVec3, WorldSpace};
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct Quad {
+ pub origin: WPoint3,
+ pub basis: [WVec3; 2],
+ pub dims: [f64; 2],
+ pub verts: [WPoint3; 4],
+ tag: usize,
+}
+
+impl Quad {
+ pub fn new(origin: WPoint3, basis: [WVec3; 2], dims: [f64; 2]) -> Quad {
+ Self::tagged(origin, basis, dims, 0)
+ }
+
+ pub fn tagged(origin: WPoint3, mut basis: [WVec3; 2], dims: [f64; 2], tag: usize) -> Quad {
+ let verts = [
+ origin,
+ origin + basis[0] * dims[0],
+ origin + basis[0] * dims[0] + basis[1] * dims[1],
+ origin + basis[1] * dims[1],
+ ];
+ basis[0] = basis[0].normalize();
+ basis[1] = basis[1].normalize();
+ Quad {
+ origin,
+ basis,
+ dims,
+ verts,
+ tag,
+ }
+ }
+}
+
+impl Shape for Quad {
+ fn collision_geometry(&self) -> Option>>> {
+ Some(vec![
+ Arc::new(Triangle::new(self.verts[0], self.verts[1], self.verts[3])),
+ Arc::new(Triangle::new(self.verts[2], self.verts[3], self.verts[1])),
+ ])
+ }
+
+ fn paths(&self, _cam: &Camera) -> Vec> {
+ let centroid = self
+ .verts
+ .into_iter()
+ .map(WPoint3::to_vector)
+ .sum::()
+ / 4.0;
+
+ let v = self
+ .verts
+ .into_iter()
+ .map(|v| v + (v.to_vector() - centroid).normalize() * 0.0015)
+ .collect::>();
+
+ vec![
+ LineSegment3D::tagged(v[0], v[1], self.tag),
+ LineSegment3D::tagged(v[1], v[2], self.tag),
+ LineSegment3D::tagged(v[2], v[3], self.tag),
+ LineSegment3D::tagged(v[3], v[0], self.tag),
+ ]
+ }
+}
diff --git a/raydeon/src/shapes/sphere.rs b/raydeon/src/shapes/sphere.rs
index 785eaad..9ac1434 100644
--- a/raydeon/src/shapes/sphere.rs
+++ b/raydeon/src/shapes/sphere.rs
@@ -1,3 +1,4 @@
+//! Provides collision for spheres.
use crate::{CollisionGeometry, HitData, Ray, WPoint3, WVec3, WorldSpace};
#[derive(Debug, Copy, Clone)]
diff --git a/raydeon/src/shapes/triangle.rs b/raydeon/src/shapes/triangle.rs
index 130a4c4..dc7d6f0 100644
--- a/raydeon/src/shapes/triangle.rs
+++ b/raydeon/src/shapes/triangle.rs
@@ -1,3 +1,4 @@
+//! Provides basic drawing and collision for triangles.
use std::sync::Arc;
use super::plane::Plane;