diff --git a/src/bsp.rs b/src/bsp.rs index 42369f9..3cd19b2 100644 --- a/src/bsp.rs +++ b/src/bsp.rs @@ -298,7 +298,7 @@ impl Node { let plane = self.plane.clone().unwrap(); // Split polygons in parallel - let (mut coplanar_front, mut coplanar_back, mut front, mut back) = polygons + let (mut coplanar_front, mut coplanar_back, front, back) = polygons .par_iter() .map(|p| plane.split_polygon(p)) // <-- just pass p .reduce( diff --git a/src/csg.rs b/src/csg.rs index e9f6de7..ea9c6f1 100644 --- a/src/csg.rs +++ b/src/csg.rs @@ -16,7 +16,6 @@ use geo::{ }; use nalgebra::{ Isometry3, Matrix3, Matrix4, Point3, Quaternion, Rotation3, Translation3, Unit, Vector3, - partial_max, partial_min, }; use std::fmt::Debug; use std::sync::OnceLock; @@ -26,7 +25,7 @@ use rayon::prelude::*; /// The main CSG solid structure. Contains a list of 3D polygons, 2D polylines, and some metadata. #[derive(Debug, Clone)] -pub struct CSG { +pub struct CSG { /// 3D polygons for volumetric shapes pub polygons: Vec>, @@ -40,6 +39,12 @@ pub struct CSG { pub metadata: Option, } +impl Default for CSG { + fn default() -> Self { + Self::new() + } +} + impl CSG { /// Create an empty CSG pub fn new() -> Self { @@ -968,13 +973,13 @@ impl CSG { // 1) Gather from the 3D polygons for poly in &self.polygons { for v in &poly.vertices { - min_x = *partial_min(&min_x, &v.pos.x).unwrap(); - min_y = *partial_min(&min_y, &v.pos.y).unwrap(); - min_z = *partial_min(&min_z, &v.pos.z).unwrap(); + min_x = min_x.min(v.pos.x); + min_y = min_y.min(v.pos.y); + min_z = min_z.min(v.pos.z); - max_x = *partial_max(&max_x, &v.pos.x).unwrap(); - max_y = *partial_max(&max_y, &v.pos.y).unwrap(); - max_z = *partial_max(&max_z, &v.pos.z).unwrap(); + max_x = max_x.max(v.pos.x); + max_y = max_y.max(v.pos.y); + max_z = max_z.max(v.pos.z); } } @@ -988,13 +993,13 @@ impl CSG { let max_pt = rect.max(); // Merge the 2D bounds into our existing min/max, forcing z=0 for 2D geometry. - min_x = *partial_min(&min_x, &min_pt.x).unwrap(); - min_y = *partial_min(&min_y, &min_pt.y).unwrap(); - min_z = *partial_min(&min_z, &0.0).unwrap(); + min_x = min_x.min(min_pt.x); + min_y = min_y.min(min_pt.y); + min_z = min_z.min(0.0); - max_x = *partial_max(&max_x, &max_pt.x).unwrap(); - max_y = *partial_max(&max_y, &max_pt.y).unwrap(); - max_z = *partial_max(&max_z, &0.0).unwrap(); + max_x = max_x.max(max_pt.x); + max_y = max_y.max(max_pt.y); + max_z = max_z.max(0.0); } // If still uninitialized (e.g., no polygons or geometry), return a trivial AABB at origin diff --git a/src/extrudes.rs b/src/extrudes.rs index 09d761f..7333f67 100644 --- a/src/extrudes.rs +++ b/src/extrudes.rs @@ -593,8 +593,7 @@ impl CSG { } // Build the polygon - let poly = Polygon::new(verts, metadata.clone()); - Some(poly) + Some(Polygon::new(verts, metadata.clone())) } //---------------------------------------------------------------------- diff --git a/src/io/svg.rs b/src/io/svg.rs index d9c0564..e179abe 100644 --- a/src/io/svg.rs +++ b/src/io/svg.rs @@ -377,6 +377,7 @@ impl PathBuilder { } pub trait FromSVG: Sized { + #[allow(unused)] fn from_svg(doc: &str) -> Result; } @@ -532,6 +533,7 @@ impl FromSVG for CSG<()> { } pub trait ToSVG { + #[allow(unused)] fn to_svg(&self) -> String; } @@ -884,9 +886,9 @@ fn svg_points_to_line_string(points: &str) -> Result, use nom::branch::alt; use nom::character::complete::{char, multispace0, multispace1}; use nom::combinator::opt; - use nom::multi::separated_list1; use nom::number::complete::float; - use nom::sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}; + use nom::sequence::{pair, tuple, delimited, separated_pair}; + use nom::multi::separated_list1; fn comma_wsp(i: &str) -> IResult<&str, ()> { let (i, _) = alt(( diff --git a/src/lib.rs b/src/lib.rs index 5674bc3..342ba41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! //! # Features //! #### Default -//! - **f64**: use f64 as Real +//! - **f64**: use f64 as `Real` //! - [**stl-io**](https://en.wikipedia.org/wiki/STL_(file_format)): `.stl` import/export //! - [**dxf-io**](https://en.wikipedia.org/wiki/AutoCAD_DXF): `.dxf` import/export //! - **chull-io**: convex hull and minkowski sum @@ -17,7 +17,7 @@ //! - **delaunay**: use `geo`s `spade` feature for triangulation //! //! #### Optional -//! - **f32**: use f32 as Real, this conflicts with f64 +//! - **f32**: use f32 as `Real`, this conflicts with f64 //! - **parallel**: use rayon for multithreading //! - **svg-io**: create `CSG`s from and convert `CSG`s to SVG's //! - **truetype-text**: create `CSG`s using TrueType fonts `.ttf` diff --git a/src/main.rs b/src/main.rs index a77146e..4e9c06e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,9 @@ // Here, we do not use any shared data, so we'll bind the generic S to (). use csgrs::float_types::Real; -use csgrs::plane::Plane; -use nalgebra::{Point3, Vector3}; use std::fs; +use nalgebra::{Vector3, Point3, Point2}; +use csgrs::plane::Plane; #[cfg(feature = "image")] use image::{GrayImage, ImageBuffer}; @@ -189,11 +189,11 @@ fn main() { [0.5, 1.0, 0.0], [0.5, 0.5, 1.0], ]; - let faces = vec![ - vec![0, 2, 1], // base triangle - vec![0, 1, 3], // side - vec![1, 2, 3], - vec![2, 0, 3], + let faces: Vec<&[usize]> = vec![ + &[0, 2, 1], // base triangle + &[0, 1, 3], // side + &[1, 2, 3], + &[2, 0, 3], ]; let poly = CSG::polyhedron(&points, &faces, None); #[cfg(feature = "stl-io")] @@ -224,7 +224,7 @@ fn main() { // 14) Mass properties (just printing them) let (mass, com, principal_frame) = cube.mass_properties(1.0); - println!("Cube mass = {}", mass); + println!("Cube mass = {mass}"); println!("Cube center of mass = {:?}", com); println!("Cube principal inertia local frame = {:?}", principal_frame); @@ -330,7 +330,6 @@ fn main() { let _ = fs::write("stl/pie_slice.stl", wedge.to_stl_ascii("pie_slice")); // Create a 2D "metaball" shape from 3 circles - use nalgebra::Point2; let balls_2d = vec![ (Point2::new(0.0, 0.0), 1.0), (Point2::new(1.5, 0.0), 1.0), diff --git a/src/metaballs.rs b/src/metaballs.rs index a9e6553..b2f33ce 100644 --- a/src/metaballs.rs +++ b/src/metaballs.rs @@ -99,7 +99,7 @@ fn stitch(contours: &[LineString]) -> Vec> { } // close if ends coincide - if chain.len() >= 3 && (chain[0] == *chain.last().unwrap()) == false { + if chain.len() >= 3 && chain[0] != *chain.last().unwrap() { chain.push(chain[0]); } chains.push(LineString::new(chain)); diff --git a/src/offset.rs b/src/offset.rs index 230f7f9..10d08d3 100644 --- a/src/offset.rs +++ b/src/offset.rs @@ -11,6 +11,8 @@ impl CSG { /// - If it's a Polygon, buffer it and store the result as a MultiPolygon /// - If it's a MultiPolygon, buffer it directly /// - Otherwise, ignore (exclude) it from the new collection + /// + /// Note: this does not affect the 3d polygons of the `CSG` pub fn offset(&self, distance: Real) -> CSG { let offset_geoms = self .geometry diff --git a/src/plane.rs b/src/plane.rs index b1736d1..27d3464 100644 --- a/src/plane.rs +++ b/src/plane.rs @@ -35,7 +35,16 @@ pub struct Plane { } impl Plane { - /// Tries to pick three vertices that span the largest area triangle + /// Create a plane from three points + pub const fn from_points(a: &Point3, b: &Point3, c: &Point3) -> Plane { + Plane { + point_a: *a, + point_b: *b, + point_c: *c, + } + } + + /// Tries to pick three vertices that span the largest area triangle /// (maximally well-spaced) and returns a plane defined by them. /// Care is taken to preserve the original winding of the vertices. /// @@ -212,7 +221,7 @@ impl Plane { self.normal().dot(&self.point_a.coords) } - pub fn flip(&mut self) { + pub const fn flip(&mut self) { std::mem::swap(&mut self.point_a, &mut self.point_b); } diff --git a/src/polygon.rs b/src/polygon.rs index 389266c..ecf3175 100644 --- a/src/polygon.rs +++ b/src/polygon.rs @@ -38,6 +38,43 @@ impl Polygon { } } + /// Create a polygon from vertices + pub fn new_with_plane(vertices: Vec, metadata: Option, plane: Plane) -> Self { + assert!(vertices.len() >= 3, "degenerate polygon"); + + Polygon { + vertices, + plane, + bounding_box: OnceLock::new(), + metadata, + } + } + + /// Create a polygon from three vertices + pub fn from_tri(vertices: &[Vertex; 3], metadata: Option) -> Self { + Polygon { + vertices: vertices.to_vec(), + plane: Plane::from_points(&vertices[0].pos, &vertices[1].pos, &vertices[2].pos), + bounding_box: OnceLock::new(), + metadata, + } + } + + /// Returns the [`Plane`] of a [`Polygon`] + /// + // I think a worst-case can be constructed for any heuristic here the + // only optimal solution may be to calculate which vertices are far apart + // which we would need a fast solution for. + // + // Finding the best solution is likely to be too intensive, + // but finding a "good enough" solution may still be quick. + // It may be useful to retain this version and implement a slower higher + // quality solution as a second function. + #[inline] + pub fn plane(&self) -> &Plane { + &self.plane + } + /// Axis aligned bounding box of this Polygon (cached after first call) pub fn bounding_box(&self) -> Aabb { *self.bounding_box.get_or_init(|| { @@ -179,10 +216,16 @@ impl Polygon { // 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); + + // only reserves if needed, this is unneeded as it's done ahead of time + // next_level.reserve(4); + // add to vec without copy + for sub in subs.into_iter() { + next_level.push(sub); + } } queue = next_level; } @@ -235,12 +278,12 @@ impl Polygon { } /// Returns a reference to the metadata, if any. - pub fn metadata(&self) -> Option<&S> { + pub const fn metadata(&self) -> Option<&S> { self.metadata.as_ref() } /// Returns a mutable reference to the metadata, if any. - pub fn metadata_mut(&mut self) -> Option<&mut S> { + pub const fn metadata_mut(&mut self) -> Option<&mut S> { self.metadata.as_mut() } @@ -275,13 +318,13 @@ pub fn build_orthonormal_basis(n: Vector3) -> (Vector3, Vector3 Vec<[Vertex; 3]> { +/// Helper function to subdivide a triangle into 4 +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].clone(), v01.clone(), v20.clone()], [v01.clone(), tri[1].clone(), v12.clone()], [v20.clone(), v12.clone(), tri[2].clone()], diff --git a/src/shapes2d.rs b/src/shapes2d.rs index 5181501..6d4d3d8 100644 --- a/src/shapes2d.rs +++ b/src/shapes2d.rs @@ -267,6 +267,7 @@ impl CSG { /// Teardrop shape. A simple approach: /// - a circle arc for the "round" top /// - it tapers down to a cusp at bottom. + /// /// This is just one of many possible "teardrop" definitions. // todo: center on focus of the arc pub fn teardrop_outline( diff --git a/src/shapes3d.rs b/src/shapes3d.rs index da11a08..8957afb 100644 --- a/src/shapes3d.rs +++ b/src/shapes3d.rs @@ -330,7 +330,7 @@ impl CSG { CSG::frustum_ptp( Point3::origin(), Point3::new(0.0, 0.0, height), - radius.clone(), + radius, radius, segments, metadata, @@ -373,7 +373,7 @@ impl CSG { ) -> CSG { let mut polygons = Vec::new(); - for face in faces { + for face in faces.iter() { // Skip degenerate faces if face.len() < 3 { continue; @@ -381,7 +381,7 @@ impl CSG { // Gather the vertices for this face let mut face_vertices = Vec::with_capacity(face.len()); - for &idx in face { + for &idx in face.iter() { // Ensure the index is valid if idx >= points.len() { panic!( @@ -529,7 +529,7 @@ impl CSG { /// - `direction`: the vector defining arrow length and intended pointing direction /// - `segments`: number of segments for approximating the cylinder and frustum /// - `orientation`: when false (default) the arrow points away from start (its base is at start); - /// when true the arrow points toward start (its tip is at start). + /// when true the arrow points toward start (its tip is at start). /// - `metadata`: optional metadata for the generated polygons. pub fn arrow( start: Point3, @@ -682,9 +682,7 @@ impl CSG { [9, 8, 1], ]; - let faces_vec: Vec> = faces.iter().map(|f| f.to_vec()).collect(); - - Self::polyhedron(&pts, &faces_vec, metadata).scale(factor, factor, factor) + Self::polyhedron(&pts, &faces, metadata).scale(factor, factor, factor) } /// Torus centred at the origin in the *XY* plane. diff --git a/src/tests.rs b/src/tests.rs index d9392ff..522c5a7 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -110,13 +110,13 @@ fn test_to_stl_ascii() { #[test] fn test_degenerate_polygon_after_clipping() { - let vertices = vec![ + let vertices = &[ Vertex::new(Point3::origin(), Vector3::y()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::y()), Vertex::new(Point3::new(0.5, 1.0, 0.0), Vector3::y()), ]; - let polygon: Polygon<()> = Polygon::new(vertices.clone(), None); + let polygon: Polygon<()> = Polygon::from_tri(vertices, None); let plane = Plane::from_normal(Vector3::new(0.0, 0.0, 1.0), 0.0); eprintln!("Original polygon: {:?}", polygon); @@ -133,13 +133,13 @@ fn test_degenerate_polygon_after_clipping() { #[test] fn test_valid_polygon_clipping() { - let vertices = vec![ + let vertices = &[ Vertex::new(Point3::origin(), Vector3::y()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::y()), Vertex::new(Point3::new(0.5, 1.0, 0.0), Vector3::y()), ]; - let polygon: Polygon<()> = Polygon::new(vertices, None); + let polygon: Polygon<()> = Polygon::from_tri(vertices, None); let plane = Plane::from_normal(-Vector3::y(), -0.5); @@ -365,8 +365,8 @@ fn test_node_new_and_build() { #[test] fn test_node_invert() { - let p: Polygon<()> = Polygon::new( - vec![ + let p: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::origin(), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()), @@ -401,24 +401,24 @@ fn test_node_clip_polygons2() { }; // Build the node with some polygons // We'll put a polygon in the plane exactly (z=0) and one above, one below - let poly_in_plane: Polygon<()> = Polygon::new( - vec![ + let poly_in_plane: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::origin(), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()), ], None, ); - let poly_above: Polygon<()> = Polygon::new( - vec![ + let poly_above: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::new(0.0, 0.0, 1.0), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 1.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, 1.0), Vector3::z()), ], None, ); - let poly_below: Polygon<()> = Polygon::new( - vec![ + let poly_below: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::new(0.0, 0.0, -1.0), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, -1.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, -1.0), Vector3::z()), @@ -434,8 +434,8 @@ fn test_node_clip_polygons2() { // Now node has polygons: [poly_in_plane], front child with poly_above, back child with poly_below // Clip a polygon that crosses from z=-0.5 to z=0.5 - let crossing_poly: Polygon<()> = Polygon::new( - vec![ + let crossing_poly: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::new(-1.0, -1.0, -0.5), Vector3::z()), Vertex::new(Point3::new(2.0, -1.0, 0.5), Vector3::z()), Vertex::new(Point3::new(-1.0, 2.0, 0.5), Vector3::z()), @@ -481,16 +481,16 @@ fn test_node_clip_to() { #[test] fn test_node_all_polygons() { // Build a node with multiple polygons - let poly1: Polygon<()> = Polygon::new( - vec![ + let poly1: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::origin(), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()), ], None, ); - let poly2: Polygon<()> = Polygon::new( - vec![ + let poly2: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::new(0.0, 0.0, 1.0), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 1.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, 1.0), Vector3::z()), @@ -509,8 +509,8 @@ fn test_node_all_polygons() { // ------------------------------------------------------------ #[test] fn test_csg_from_polygons_and_to_polygons() { - let poly: Polygon<()> = Polygon::new( - vec![ + let poly: Polygon<()> = Polygon::from_tri( + &[ Vertex::new(Point3::origin(), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()), @@ -537,12 +537,12 @@ fn test_csg_union() { // Check bounding box => should now at least range from -1 to (0.5+1) = 1.5 let bb = bounding_box(&polys); - assert!(approx_eq(bb[0], -1.0, 1e-8)); - assert!(approx_eq(bb[1], -1.0, 1e-8)); - assert!(approx_eq(bb[2], -1.0, 1e-8)); - assert!(approx_eq(bb[3], 1.5, 1e-8)); - assert!(approx_eq(bb[4], 1.5, 1e-8)); - assert!(approx_eq(bb[5], 1.5, 1e-8)); + assert!(approx_eq(bb[0], -1.0, 1e-9)); + assert!(approx_eq(bb[1], -1.0, 1e-9)); + assert!(approx_eq(bb[2], -1.0, 1e-9)); + assert!(approx_eq(bb[3], 1.5, 1e-9)); + assert!(approx_eq(bb[4], 1.5, 1e-9)); + assert!(approx_eq(bb[5], 1.5, 1e-9)); } #[test] @@ -713,7 +713,7 @@ fn test_csg_polyhedron() { [0.0, 1.0, 0.0], // 2 [0.0, 0.0, 1.0], // 3 ]; - let faces = vec![vec![0, 1, 2], vec![0, 1, 3], vec![1, 2, 3], vec![2, 0, 3]]; + let faces: Vec<&[usize]> = vec![&[0, 1, 2], &[0, 1, 3], &[1, 2, 3], &[2, 0, 3]]; let csg_tetra: CSG<()> = CSG::polyhedron(pts, &faces, None); // We should have exactly 4 triangular faces assert_eq!(csg_tetra.polygons.len(), 4); @@ -788,8 +788,12 @@ fn test_csg_subdivide_triangles() { let cube: CSG<()> = CSG::cube(2.0, 2.0, 2.0, None); // subdivide_triangles(1) => each polygon (quad) is triangulated => 2 triangles => each tri subdivides => 4 // So each face with 4 vertices => 2 triangles => each becomes 4 => total 8 per face => 6 faces => 48 - let subdiv = cube.subdivide_triangles(1.try_into().expect("not 0")); + let subdiv = cube.subdivide_triangles(1.try_into().expect("not zero")); assert_eq!(subdiv.polygons.len(), 6 * 8); + + // subdivide in place + cube.subdivide_triangles(1.try_into().expect("not zero")); + assert_eq!(subdiv.polygons.len(), cube.polygons.len()); } #[test] @@ -938,13 +942,9 @@ fn test_csg_text() { #[test] fn test_csg_to_trimesh() { let cube: CSG<()> = CSG::cube(2.0, 2.0, 2.0, None); - let shape = cube.to_trimesh(); + let trimesh = cube.to_trimesh().unwrap(); // Should be a TriMesh with 12 triangles - if let Some(trimesh) = shape { - assert_eq!(trimesh.indices().len(), 12); // 6 faces => 2 triangles each => 12 - } else { - panic!("Expected a TriMesh"); - } + assert_eq!(trimesh.indices().len(), 12); // 6 faces => 2 triangles each => 12 } #[test] @@ -1012,12 +1012,13 @@ struct MyMetaData { #[test] fn test_polygon_metadata_string() { // Create a simple triangle polygon with shared data = Some("triangle".to_string()). - let verts = vec![ + let verts = [ Vertex::new(Point3::origin(), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()), ]; - let mut poly = Polygon::new(verts, Some("triangle".to_string())); + // This also works with `Polygon::new` + let mut poly = Polygon::from_tri(&verts, Some("triangle".to_string())); // Check getter assert_eq!(poly.metadata(), Some(&"triangle".to_string())); @@ -1067,16 +1068,16 @@ fn test_polygon_metadata_custom_struct() { #[test] fn test_csg_construction_with_metadata() { // Build a CSG of two polygons, each with distinct shared data. - let poly_a = Polygon::new( - vec![ + let poly_a = Polygon::from_tri( + &[ Vertex::new(Point3::origin(), Vector3::z()), Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(1.0, 1.0, 0.0), Vector3::z()), ], Some("PolyA".to_string()), ); - let poly_b = Polygon::new( - vec![ + let poly_b = Polygon::from_tri( + &[ Vertex::new(Point3::new(2.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(3.0, 0.0, 0.0), Vector3::z()), Vertex::new(Point3::new(3.0, 1.0, 0.0), Vector3::z()), @@ -1136,19 +1137,19 @@ fn test_difference_metadata() { let mut cube1 = CSG::cube(2.0, 2.0, 2.0, None); for p in &mut cube1.polygons { - p.set_metadata("Cube1".to_string()); + p.set_metadata("Cube1"); } let mut cube2 = CSG::cube(2.0, 2.0, 2.0, None).translate(0.5, 0.5, 0.5); for p in &mut cube2.polygons { - p.set_metadata("Cube2".to_string()); + p.set_metadata("Cube2"); } let result = cube1.difference(&cube2); // All polygons in the result should come from "Cube1" only. for poly in &result.polygons { - assert_eq!(poly.metadata(), Some(&"Cube1".to_string())); + assert_eq!(poly.metadata(), Some(&"Cube1")); } } @@ -1217,8 +1218,10 @@ fn test_subdivide_metadata() { ], Some("LargeQuad".to_string()), ); + let csg = CSG::from_polygons(&[poly]); - let subdivided = csg.subdivide_triangles(1.try_into().expect("not 0")); // one level of subdivision + + let subdivided = csg.subdivide_triangles(1.try_into().expect("not zero")); // one level of subdivision // Now it's split into multiple triangles. Each should keep "LargeQuad" as metadata. assert!(subdivided.polygons.len() > 1); @@ -1291,23 +1294,27 @@ fn signed_area(polygon: &Polygon<()>) -> Real { #[test] fn test_square_ccw_ordering() { - let square = CSG::square(2.0, 2.0, None); - assert_eq!(square.polygons.len(), 1); - let poly = &square.polygons[0]; - let area = signed_area(poly); + use geo::Area; + + let square: CSG = CSG::square(2.0, 2.0, None); + assert_eq!(square.geometry.len(), 1); + let poly = &square.geometry[0]; + let area = poly.signed_area(); assert!(area > 0.0, "Square vertices are not CCW ordered"); } #[test] fn test_offset_2d_positive_distance_grows() { - let square = CSG::square(2.0, 2.0, None); // Centered square with size 2x2 + use geo::Area; + + let square: CSG = CSG::square(2.0, 2.0, None); // Centered square with size 2x2 let offset = square.offset(0.5); // Positive offset should grow the square // The original square has area 4.0 // The offset square should have area greater than 4.0 - assert_eq!(offset.polygons.len(), 1); - let poly = &offset.polygons[0]; - let area = signed_area(poly); + assert_eq!(offset.geometry.len(), 1); + let poly = &offset.geometry[0]; + let area = poly.signed_area(); assert!( area > 4.0, "Offset with positive distance did not grow the square" @@ -1519,22 +1526,11 @@ fn test_flatten_cube() { // The flattened cube should have 1 polygon1, now in z=0 assert_eq!( - flattened.polygons.len(), + flattened.geometry.len(), 1, "Flattened cube should have 1 face in z=0" ); - // Check that all vertices lie at z=0 - for poly in &flattened.polygons { - for v in &poly.vertices { - assert!( - (v.pos.z - 0.0).abs() < EPSILON, - "Flattened vertex must have z=0, found z={}", - v.pos.z - ); - } - } - // Optional: we can check the bounding box in z-dimension is effectively zero let bbox = flattened.bounding_box(); let thickness = bbox.maxs.z - bbox.mins.z; @@ -1556,13 +1552,13 @@ fn test_slice_cylinder() { // (unless the top or bottom also exactly intersect z=0, which they do not in this scenario). // So we expect exactly 1 polygon. assert_eq!( - cross_section.polygons.len(), + cross_section.geometry.len(), 1, "Slicing a cylinder at z=0 should yield exactly 1 cross-section polygon" ); - let poly = &cross_section.polygons[0]; - let vcount = poly.vertices.len(); + let poly: geo::Polygon = cross_section.geometry[0].clone().try_into().unwrap(); + let vcount = poly.exterior().0.len(); // We used 32 slices for the cylinder, so we expect up to 32 edges // in the cross-section circle. Some slight differences might occur @@ -1575,15 +1571,6 @@ fn test_slice_cylinder() { vcount ); - // Check all vertices are on z=0 - for v in &poly.vertices { - assert!( - (v.pos.z - 0.0).abs() < EPSILON, - "Sliced vertex must have z=0, found z={}", - v.pos.z - ); - } - // Optional: check approximate radius // The cross-section should be at radius ~1 around x=0,y=some, z=0 // (Actually, the cylinder's axis is along Y, so we expect x^2+z^2=1. @@ -1592,8 +1579,8 @@ fn test_slice_cylinder() { // the cylinder we built is from (0,-1,0) to (0,1,0) with radius=1 around that axis, // so the cross-section plane is z=0, meaning x^2 + y^2 = 1. // We can check a couple of sample vertices.) - if !poly.vertices.is_empty() { - let first_v = &poly.vertices[0].pos; + if !poly.exterior().0.is_empty() { + let first_v = &poly.exterior().0[0]; let r_approx = (first_v.x.powi(2) + first_v.y.powi(2)).sqrt(); // We expect something close to radius=1: assert!( @@ -1680,7 +1667,7 @@ fn test_flatten_and_union_two_disjoint_squares() { let csg = CSG::from_polygons(&[square_a, square_b]); let flat_csg = csg.flatten(); - assert!(!flat_csg.polygons.is_empty()); + assert!(!flat_csg.geometry.is_empty()); // Expect 2 disjoint polygons in the result assert_eq!( @@ -1710,7 +1697,7 @@ fn test_flatten_and_union_near_xy_plane() { let flat_csg = csg.flatten(); assert!( - !flat_csg.polygons.is_empty(), + !flat_csg.geometry.is_empty(), "Should flatten to a valid polygon" ); let bb = flat_csg.bounding_box();