From 1c3d94014910d051301a299ba4f95cdeec96cb5e Mon Sep 17 00:00:00 2001 From: ryancinsight Date: Thu, 10 Jul 2025 13:41:27 -0400 Subject: [PATCH 1/3] Refactor BSP module with dependency inversion and iterators - Implement dependency inversion pattern using BspOps trait - Create separate SerialBspOps and ParallelBspOps implementations - Replace manual loops with iterator patterns throughout - Maintain backward compatibility with existing Node API - Add SplittingPlaneStrategy trait for algorithm flexibility - Organize code into clean submodule structure: - traits.rs: Core trait definitions - node.rs: Node data structure only - serial.rs: Serial BSP operations - parallel.rs: Parallel BSP operations (with rayon) - mod.rs: Public API and backward compatibility - Fix doctest import issues and create missing directories - All tests passing (98 lib tests + 28 doc tests) This refactoring separates algorithms from data structures, enables easy testing of different BSP strategies, and follows modern Rust patterns while maintaining full API compatibility. --- src/mesh/bsp.rs | 338 --------------------------------------- src/mesh/bsp/mod.rs | 121 ++++++++++++++ src/mesh/bsp/node.rs | 41 +++++ src/mesh/bsp/parallel.rs | 274 +++++++++++++++++++++++++++++++ src/mesh/bsp/serial.rs | 248 ++++++++++++++++++++++++++++ src/mesh/bsp/traits.rs | 82 ++++++++++ src/mesh/bsp_parallel.rs | 247 ---------------------------- src/mesh/mod.rs | 1 - src/sketch/shapes.rs | 3 +- 9 files changed, 768 insertions(+), 587 deletions(-) delete mode 100644 src/mesh/bsp.rs create mode 100644 src/mesh/bsp/mod.rs create mode 100644 src/mesh/bsp/node.rs create mode 100644 src/mesh/bsp/parallel.rs create mode 100644 src/mesh/bsp/serial.rs create mode 100644 src/mesh/bsp/traits.rs delete mode 100644 src/mesh/bsp_parallel.rs diff --git a/src/mesh/bsp.rs b/src/mesh/bsp.rs deleted file mode 100644 index 0bfa4f5..0000000 --- a/src/mesh/bsp.rs +++ /dev/null @@ -1,338 +0,0 @@ -//! [BSP](https://en.wikipedia.org/wiki/Binary_space_partitioning) tree node structure and operations - -#[cfg(not(feature = "parallel"))] -use crate::float_types::EPSILON; - -#[cfg(not(feature = "parallel"))] -use crate::mesh::vertex::Vertex; - -use crate::float_types::Real; -use crate::mesh::plane::{BACK, COPLANAR, FRONT, Plane, SPANNING}; -use crate::mesh::polygon::Polygon; -use std::fmt::Debug; - -/// A [BSP](https://en.wikipedia.org/wiki/Binary_space_partitioning) tree node, containing polygons plus optional front/back subtrees -#[derive(Debug, Clone)] -pub struct Node { - /// Splitting plane for this node *or* **None** for a leaf that - /// only stores polygons. - pub plane: Option, - - /// Polygons in *front* half‑spaces. - pub front: Option>>, - - /// Polygons in *back* half‑spaces. - pub back: Option>>, - - /// Polygons that lie *exactly* on `plane` - /// (after the node has been built). - pub polygons: Vec>, -} - -impl Default for Node { - fn default() -> Self { - Self::new() - } -} - -impl Node { - /// Create a new empty BSP node - pub const fn new() -> Self { - Self { - plane: None, - front: None, - back: None, - polygons: Vec::new(), - } - } - - /// Creates a new BSP node from polygons - pub fn from_polygons(polygons: &[Polygon]) -> Self { - let mut node = Self::new(); - if !polygons.is_empty() { - node.build(polygons); - } - node - } - - /// Invert all polygons in the BSP tree - #[cfg(not(feature = "parallel"))] - pub fn invert(&mut self) { - // Flip all polygons and plane in this node - self.polygons.iter_mut().for_each(|p| p.flip()); - if let Some(ref mut plane) = self.plane { - plane.flip(); - } - - if let Some(ref mut front) = self.front { - front.invert(); - } - if let Some(ref mut back) = self.back { - back.invert(); - } - - std::mem::swap(&mut self.front, &mut self.back); - } - - pub fn pick_best_splitting_plane(&self, polygons: &[Polygon]) -> Plane { - const K_SPANS: Real = 8.0; // Weight for spanning polygons - const K_BALANCE: Real = 1.0; // Weight for front/back balance - - let mut best_plane = polygons[0].plane.clone(); - let mut best_score = Real::MAX; - - // Take a sample of polygons as candidate planes - let sample_size = polygons.len().min(20); - for p in polygons.iter().take(sample_size) { - let plane = &p.plane; - let mut num_front = 0; - let mut num_back = 0; - let mut num_spanning = 0; - - for poly in polygons { - match plane.classify_polygon(poly) { - COPLANAR => {}, // Not counted for balance - FRONT => num_front += 1, - BACK => num_back += 1, - SPANNING => num_spanning += 1, - _ => num_spanning += 1, // Treat any other combination as spanning - } - } - - let score = K_SPANS * num_spanning as Real - + K_BALANCE * ((num_front - num_back) as Real).abs(); - - if score < best_score { - best_score = score; - best_plane = plane.clone(); - } - } - best_plane - } - - /// Recursively remove all polygons in `polygons` that are inside this BSP tree - /// **Mathematical Foundation**: Uses plane classification to determine polygon visibility. - /// Polygons entirely in BACK half-space are clipped (removed). - /// **Algorithm**: O(n log d) where n is polygon count, d is tree depth. - #[cfg(not(feature = "parallel"))] - pub fn clip_polygons(&self, polygons: &[Polygon]) -> Vec> { - // If this node has no plane (i.e. it’s empty), just return - if self.plane.is_none() { - return polygons.to_vec(); - } - - let plane = self.plane.as_ref().unwrap(); - - // Pre-allocate for better performance - let mut front_polys = Vec::with_capacity(polygons.len()); - let mut back_polys = Vec::with_capacity(polygons.len()); - - // Optimized polygon splitting with iterator patterns - for polygon in polygons { - let (coplanar_front, coplanar_back, mut front_parts, mut back_parts) = - plane.split_polygon(polygon); - - // Efficient coplanar polygon classification using iterator chain - for coplanar_poly in coplanar_front.into_iter().chain(coplanar_back.into_iter()) { - if plane.orient_plane(&coplanar_poly.plane) == FRONT { - front_parts.push(coplanar_poly); - } else { - back_parts.push(coplanar_poly); - } - } - - front_polys.append(&mut front_parts); - back_polys.append(&mut back_parts); - } - - // Recursively clip with optimized pattern - let mut result = if let Some(front_node) = &self.front { - front_node.clip_polygons(&front_polys) - } else { - front_polys - }; - - if let Some(back_node) = &self.back { - result.extend(back_node.clip_polygons(&back_polys)); - } - - result - } - - /// Remove all polygons in this BSP tree that are inside the other BSP tree - #[cfg(not(feature = "parallel"))] - pub fn clip_to(&mut self, bsp: &Node) { - self.polygons = bsp.clip_polygons(&self.polygons); - if let Some(ref mut front) = self.front { - front.clip_to(bsp); - } - if let Some(ref mut back) = self.back { - back.clip_to(bsp); - } - } - - /// Return all polygons in this BSP tree using an iterative approach, - /// avoiding potential stack overflow of recursive approach - pub fn all_polygons(&self) -> Vec> { - let mut result = Vec::new(); - let mut stack = vec![self]; - - while let Some(node) = stack.pop() { - result.extend_from_slice(&node.polygons); - - // Use iterator to add child nodes more efficiently - stack.extend( - [&node.front, &node.back] - .iter() - .filter_map(|child| child.as_ref().map(|boxed| boxed.as_ref())), - ); - } - result - } - - /// Build a BSP tree from the given polygons - #[cfg(not(feature = "parallel"))] - pub fn build(&mut self, polygons: &[Polygon]) { - if polygons.is_empty() { - return; - } - - // Choose the best splitting plane using a heuristic if not already set. - if self.plane.is_none() { - self.plane = Some(self.pick_best_splitting_plane(polygons)); - } - let plane = self.plane.as_ref().unwrap(); - - // Pre-allocate with estimated capacity for better performance - let mut front = Vec::with_capacity(polygons.len() / 2); - let mut back = Vec::with_capacity(polygons.len() / 2); - - // Optimized polygon classification using iterator pattern - // **Mathematical Theorem**: Each polygon is classified relative to the splitting plane - for polygon in polygons { - let (coplanar_front, coplanar_back, mut front_parts, mut back_parts) = - plane.split_polygon(polygon); - - // Extend collections efficiently with iterator chains - self.polygons.extend(coplanar_front); - self.polygons.extend(coplanar_back); - front.append(&mut front_parts); - back.append(&mut back_parts); - } - - // Build child nodes using lazy initialization pattern for memory efficiency - if !front.is_empty() { - self.front - .get_or_insert_with(|| Box::new(Node::new())) - .build(&front); - } - - if !back.is_empty() { - self.back - .get_or_insert_with(|| Box::new(Node::new())) - .build(&back); - } - } - - /// Slices this BSP node with `slicing_plane`, returning: - /// - All polygons that are coplanar with the plane (within EPSILON), - /// - A list of line‐segment intersections (each a [Vertex; 2]) from polygons that span the plane. - #[cfg(not(feature = "parallel"))] - pub fn slice(&self, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { - let all_polys = self.all_polygons(); - - let mut coplanar_polygons = Vec::new(); - let mut intersection_edges = Vec::new(); - - for poly in &all_polys { - let vcount = poly.vertices.len(); - if vcount < 2 { - continue; // degenerate polygon => skip - } - - // Use iterator chain to compute vertex types more efficiently - let types: Vec<_> = poly - .vertices - .iter() - .map(|vertex| slicing_plane.orient_point(&vertex.pos)) - .collect(); - - let polygon_type = types.iter().fold(0, |acc, &vertex_type| acc | vertex_type); - - // Based on the combined classification of its vertices: - match polygon_type { - COPLANAR => { - // The entire polygon is in the plane, so push it to the coplanar list. - coplanar_polygons.push(poly.clone()); - }, - - FRONT | BACK => { - // Entirely on one side => no intersection. We skip it. - }, - - SPANNING => { - // The polygon crosses the plane. We'll gather the intersection points - // (the new vertices introduced on edges that cross the plane). - let crossing_points: Vec<_> = (0..vcount) - .filter_map(|i| { - let j = (i + 1) % vcount; - let ti = types[i]; - let tj = types[j]; - let vi = &poly.vertices[i]; - let vj = &poly.vertices[j]; - - if (ti | tj) == SPANNING { - let denom = slicing_plane.normal().dot(&(vj.pos - vi.pos)); - if denom.abs() > EPSILON { - let intersection = (slicing_plane.offset() - - slicing_plane.normal().dot(&vi.pos.coords)) - / denom; - Some(vi.interpolate(vj, intersection)) - } else { - None - } - } else { - None - } - }) - .collect(); - - // Convert crossing points to intersection edges - intersection_edges.extend( - crossing_points - .chunks_exact(2) - .map(|chunk| [chunk[0].clone(), chunk[1].clone()]), - ); - }, - - _ => { - // Shouldn't happen in a typical classification, but we can ignore - }, - } - } - - (coplanar_polygons, intersection_edges) - } -} - -#[cfg(test)] -mod tests { - use crate::mesh::bsp::Node; - use crate::mesh::polygon::Polygon; - use crate::mesh::vertex::Vertex; - use nalgebra::{Point3, Vector3}; - - #[test] - fn test_bsp_basic_functionality() { - let vertices = vec![ - Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0)), - Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0)), - Vertex::new(Point3::new(0.5, 1.0, 0.0), Vector3::new(0.0, 0.0, 1.0)), - ]; - let polygon: Polygon = Polygon::new(vertices, None); - let polygons = vec![polygon]; - - let node = Node::from_polygons(&polygons); - assert!(!node.all_polygons().is_empty()); - } -} diff --git a/src/mesh/bsp/mod.rs b/src/mesh/bsp/mod.rs new file mode 100644 index 0000000..d439951 --- /dev/null +++ b/src/mesh/bsp/mod.rs @@ -0,0 +1,121 @@ +//! Binary Space Partitioning (BSP) tree implementation +//! +//! This module provides BSP tree operations with dependency inversion, +//! allowing for different algorithm implementations (serial/parallel). + +pub mod node; +pub mod traits; + +#[cfg(not(feature = "parallel"))] +pub mod serial; + +#[cfg(feature = "parallel")] +pub mod parallel; + +// Re-export core types for backward compatibility +pub use node::Node; +pub use traits::{BspOps, SplittingPlaneStrategy, BalancedSplittingStrategy}; + +#[cfg(not(feature = "parallel"))] +pub use serial::SerialBspOps; + +#[cfg(feature = "parallel")] +pub use parallel::ParallelBspOps; + +// Backward compatibility implementations on Node +use std::fmt::Debug; +use crate::mesh::plane::Plane; +use crate::mesh::polygon::Polygon; +use crate::mesh::vertex::Vertex; + +impl Node { + /// Creates a new BSP node from polygons + pub fn from_polygons(polygons: &[Polygon]) -> Self { + let mut node = Self::new(); + if !polygons.is_empty() { + node.build(polygons); + } + node + } + + /// Invert all polygons in the BSP tree + #[cfg(not(feature = "parallel"))] + pub fn invert(&mut self) { + let ops = SerialBspOps::new(); + ops.invert(self); + } + + #[cfg(feature = "parallel")] + pub fn invert(&mut self) { + let ops = ParallelBspOps::new(); + ops.invert(self); + } + + /// Pick the best splitting plane using the default strategy + pub fn pick_best_splitting_plane(&self, polygons: &[Polygon]) -> Plane { + let strategy = BalancedSplittingStrategy::default(); + strategy.pick_best_splitting_plane(polygons) + } + + /// Recursively remove all polygons that are inside this BSP tree + #[cfg(not(feature = "parallel"))] + pub fn clip_polygons(&self, polygons: &[Polygon]) -> Vec> { + let ops = SerialBspOps::new(); + ops.clip_polygons(self, polygons) + } + + #[cfg(feature = "parallel")] + pub fn clip_polygons(&self, polygons: &[Polygon]) -> Vec> { + let ops = ParallelBspOps::new(); + ops.clip_polygons(self, polygons) + } + + /// Remove all polygons in this BSP tree that are inside the other BSP tree + #[cfg(not(feature = "parallel"))] + pub fn clip_to(&mut self, bsp: &Node) { + let ops = SerialBspOps::new(); + ops.clip_to(self, bsp); + } + + #[cfg(feature = "parallel")] + pub fn clip_to(&mut self, bsp: &Node) { + let ops = ParallelBspOps::new(); + ops.clip_to(self, bsp); + } + + /// Return all polygons in this BSP tree + pub fn all_polygons(&self) -> Vec> { + #[cfg(not(feature = "parallel"))] + let ops = SerialBspOps::new(); + #[cfg(feature = "parallel")] + let ops = ParallelBspOps::new(); + + ops.all_polygons(self) + } + + /// Build a BSP tree from the given polygons + #[cfg(not(feature = "parallel"))] + pub fn build(&mut self, polygons: &[Polygon]) { + let ops = SerialBspOps::new(); + ops.build(self, polygons); + } + + #[cfg(feature = "parallel")] + pub fn build(&mut self, polygons: &[Polygon]) { + let ops = ParallelBspOps::new(); + ops.build(self, polygons); + } + + /// Slices this BSP node with the given plane + #[cfg(not(feature = "parallel"))] + pub fn slice(&self, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + let ops = SerialBspOps::new(); + ops.slice(self, slicing_plane) + } + + #[cfg(feature = "parallel")] + pub fn slice(&self, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + let ops = ParallelBspOps::new(); + ops.slice(self, slicing_plane) + } +} \ No newline at end of file diff --git a/src/mesh/bsp/node.rs b/src/mesh/bsp/node.rs new file mode 100644 index 0000000..bd73308 --- /dev/null +++ b/src/mesh/bsp/node.rs @@ -0,0 +1,41 @@ +//! BSP tree node data structure + +use crate::mesh::plane::Plane; +use crate::mesh::polygon::Polygon; +use std::fmt::Debug; + +/// A BSP tree node, containing polygons plus optional front/back subtrees +#[derive(Debug, Clone)] +pub struct Node { + /// Splitting plane for this node *or* **None** for a leaf that + /// only stores polygons. + pub plane: Option, + + /// Polygons in *front* half‑spaces. + pub front: Option>>, + + /// Polygons in *back* half‑spaces. + pub back: Option>>, + + /// Polygons that lie *exactly* on `plane` + /// (after the node has been built). + pub polygons: Vec>, +} + +impl Default for Node { + fn default() -> Self { + Self::new() + } +} + +impl Node { + /// Create a new empty BSP node + pub const fn new() -> Self { + Self { + plane: None, + front: None, + back: None, + polygons: Vec::new(), + } + } +} \ No newline at end of file diff --git a/src/mesh/bsp/parallel.rs b/src/mesh/bsp/parallel.rs new file mode 100644 index 0000000..fd1c5b9 --- /dev/null +++ b/src/mesh/bsp/parallel.rs @@ -0,0 +1,274 @@ +//! Parallel implementation of BSP operations + +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +use crate::float_types::EPSILON; +use crate::mesh::bsp::node::Node; +use crate::mesh::bsp::traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; +use crate::mesh::plane::{Plane, BACK, COPLANAR, FRONT, SPANNING}; +use crate::mesh::polygon::Polygon; +use crate::mesh::vertex::Vertex; +use std::fmt::Debug; + +/// Parallel implementation of BSP operations +#[cfg(feature = "parallel")] +pub struct ParallelBspOps = BalancedSplittingStrategy, S: Clone = ()> { + splitting_strategy: SP, + _phantom: std::marker::PhantomData, +} + +#[cfg(feature = "parallel")] +impl ParallelBspOps { + pub fn new() -> Self { + Self { + splitting_strategy: BalancedSplittingStrategy::default(), + _phantom: std::marker::PhantomData, + } + } +} + +#[cfg(feature = "parallel")] +impl, S: Clone> ParallelBspOps { + pub fn with_strategy(strategy: SP) -> Self { + Self { + splitting_strategy: strategy, + _phantom: std::marker::PhantomData, + } + } +} + +#[cfg(feature = "parallel")] +impl + Sync, S: Clone + Send + Sync + Debug> BspOps for ParallelBspOps { + fn invert(&self, node: &mut Node) { + // Use iterative approach with a stack to avoid stack overflow + let mut stack = vec![node]; + + while let Some(current) = stack.pop() { + // Flip all polygons and plane in this node + current.polygons.par_iter_mut().for_each(|p| p.flip()); + if let Some(ref mut plane) = current.plane { + plane.flip(); + } + + // Swap front and back children + std::mem::swap(&mut current.front, &mut current.back); + + // Add children to stack for processing + if let Some(ref mut front) = current.front { + stack.push(front.as_mut()); + } + if let Some(ref mut back) = current.back { + stack.push(back.as_mut()); + } + } + } + + fn clip_polygons(&self, node: &Node, polygons: &[Polygon]) -> Vec> { + // If this node has no plane, just return the original set + if node.plane.is_none() { + return polygons.to_vec(); + } + let plane = node.plane.as_ref().unwrap(); + + // Split each polygon in parallel; gather results + let (coplanar_front, coplanar_back, mut front, mut back) = polygons + .par_iter() + .map(|poly| plane.split_polygon(poly)) + .reduce( + || (Vec::new(), Vec::new(), Vec::new(), Vec::new()), + |mut acc, x| { + acc.0.extend(x.0); + acc.1.extend(x.1); + acc.2.extend(x.2); + acc.3.extend(x.3); + acc + }, + ); + + // Decide where to send the coplanar polygons + coplanar_front + .into_iter() + .chain(coplanar_back.into_iter()) + .for_each(|cp| { + if plane.orient_plane(&cp.plane) == FRONT { + front.push(cp); + } else { + back.push(cp); + } + }); + + // Process front and back using parallel iterators to avoid recursive join + let mut result = if let Some(ref f) = node.front { + self.clip_polygons(f, &front) + } else { + front + }; + + if let Some(ref b) = node.back { + result.extend(self.clip_polygons(b, &back)); + } + + result + } + + fn clip_to(&self, node: &mut Node, bsp: &Node) { + // Use iterative approach with a stack to avoid recursive stack overflow + let mut stack = vec![node]; + + while let Some(current) = stack.pop() { + // Clip polygons at this node + current.polygons = self.clip_polygons(bsp, ¤t.polygons); + + // Add children to stack for processing + if let Some(ref mut front) = current.front { + stack.push(front.as_mut()); + } + if let Some(ref mut back) = current.back { + stack.push(back.as_mut()); + } + } + } + + fn build(&self, node: &mut Node, polygons: &[Polygon]) { + if polygons.is_empty() { + return; + } + + // Choose splitting plane if not already set + if node.plane.is_none() { + node.plane = Some(self.splitting_strategy.pick_best_splitting_plane(polygons)); + } + let plane = node.plane.as_ref().unwrap(); + + // Split polygons in parallel + let (mut coplanar_front, mut coplanar_back, front, back) = + polygons.par_iter().map(|p| plane.split_polygon(p)).reduce( + || (Vec::new(), Vec::new(), Vec::new(), Vec::new()), + |mut acc, x| { + acc.0.extend(x.0); + acc.1.extend(x.1); + acc.2.extend(x.2); + acc.3.extend(x.3); + acc + }, + ); + + // Append coplanar fronts/backs to node.polygons + node.polygons.append(&mut coplanar_front); + node.polygons.append(&mut coplanar_back); + + // Build children sequentially to avoid stack overflow from recursive join + if !front.is_empty() { + let mut front_node = node.front.take().unwrap_or_else(|| Box::new(Node::new())); + self.build(&mut front_node, &front); + node.front = Some(front_node); + } + + if !back.is_empty() { + let mut back_node = node.back.take().unwrap_or_else(|| Box::new(Node::new())); + self.build(&mut back_node, &back); + node.back = Some(back_node); + } + } + + fn all_polygons(&self, node: &Node) -> Vec> { + // Use serial version as parallel collection doesn't provide significant benefit here + let mut result = Vec::new(); + let mut stack = vec![node]; + + while let Some(current) = stack.pop() { + result.extend_from_slice(¤t.polygons); + + // Use iterator to add child nodes more efficiently + stack.extend( + [¤t.front, ¤t.back] + .iter() + .filter_map(|child| child.as_ref().map(|boxed| boxed.as_ref())), + ); + } + result + } + + fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + // Collect all polygons + let all_polys = self.all_polygons(node); + + // Process polygons in parallel + let (coplanar_polygons, intersection_edges) = all_polys + .par_iter() + .map(|poly| { + let vcount = poly.vertices.len(); + if vcount < 2 { + // Degenerate => skip + return (Vec::new(), Vec::new()); + } + + let types: Vec<_> = poly + .vertices + .iter() + .map(|vertex| slicing_plane.orient_point(&vertex.pos)) + .collect(); + + let polygon_type = types.iter().fold(0, |acc, &vertex_type| acc | vertex_type); + + match polygon_type { + COPLANAR => { + // Entire polygon in plane + (vec![poly.clone()], Vec::new()) + }, + FRONT | BACK => { + // Entirely on one side => no intersection + (Vec::new(), Vec::new()) + }, + SPANNING => { + // The polygon crosses the plane => gather intersection edges + let crossing_points: Vec<_> = (0..vcount) + .filter_map(|i| { + let j = (i + 1) % vcount; + let ti = types[i]; + let tj = types[j]; + let vi = &poly.vertices[i]; + let vj = &poly.vertices[j]; + + if (ti | tj) == SPANNING { + // The param intersection at which plane intersects the edge + let denom = slicing_plane.normal().dot(&(vj.pos - vi.pos)); + if denom.abs() > EPSILON { + let intersection = (slicing_plane.offset() + - slicing_plane.normal().dot(&vi.pos.coords)) + / denom; + // Interpolate: + Some(vi.interpolate(vj, intersection)) + } else { + None + } + } else { + None + } + }) + .collect(); + + // Pair up intersection points => edges + let edges: Vec<_> = crossing_points + .chunks_exact(2) + .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) + .collect(); + + (Vec::new(), edges) + }, + _ => (Vec::new(), Vec::new()), + } + }) + .reduce( + || (Vec::new(), Vec::new()), + |mut acc, x| { + acc.0.extend(x.0); + acc.1.extend(x.1); + acc + }, + ); + + (coplanar_polygons, intersection_edges) + } +} \ No newline at end of file diff --git a/src/mesh/bsp/serial.rs b/src/mesh/bsp/serial.rs new file mode 100644 index 0000000..04a79ec --- /dev/null +++ b/src/mesh/bsp/serial.rs @@ -0,0 +1,248 @@ +//! Serial implementation of BSP operations + +use crate::float_types::EPSILON; +use crate::mesh::bsp::node::Node; +use crate::mesh::bsp::traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; +use crate::mesh::plane::{Plane, BACK, COPLANAR, FRONT, SPANNING}; +use crate::mesh::polygon::Polygon; +use crate::mesh::vertex::Vertex; +use std::fmt::Debug; + +/// Serial implementation of BSP operations +pub struct SerialBspOps = BalancedSplittingStrategy, S: Clone = ()> { + splitting_strategy: SP, + _phantom: std::marker::PhantomData, +} + +impl SerialBspOps { + pub fn new() -> Self { + Self { + splitting_strategy: BalancedSplittingStrategy::default(), + _phantom: std::marker::PhantomData, + } + } +} + +impl, S: Clone> SerialBspOps { + pub fn with_strategy(strategy: SP) -> Self { + Self { + splitting_strategy: strategy, + _phantom: std::marker::PhantomData, + } + } +} + + + +impl, S: Clone + Send + Sync + Debug> BspOps for SerialBspOps { + fn invert(&self, node: &mut Node) { + // Use iterative approach with a stack + let mut stack = vec![node]; + + while let Some(current) = stack.pop() { + // Flip all polygons and plane in this node + current.polygons.iter_mut().for_each(|p| p.flip()); + if let Some(ref mut plane) = current.plane { + plane.flip(); + } + + // Swap front and back + std::mem::swap(&mut current.front, &mut current.back); + + // Add children to stack + if let Some(ref mut front) = current.front { + stack.push(front.as_mut()); + } + if let Some(ref mut back) = current.back { + stack.push(back.as_mut()); + } + } + } + + fn clip_polygons(&self, node: &Node, polygons: &[Polygon]) -> Vec> { + // If this node has no plane, just return + if node.plane.is_none() { + return polygons.to_vec(); + } + + let plane = node.plane.as_ref().unwrap(); + + // Pre-allocate for better performance + let mut front_polys = Vec::with_capacity(polygons.len()); + let mut back_polys = Vec::with_capacity(polygons.len()); + + // Optimized polygon splitting with iterator patterns + for polygon in polygons { + let (coplanar_front, coplanar_back, mut front_parts, mut back_parts) = + plane.split_polygon(polygon); + + // Efficient coplanar polygon classification using iterator chain + coplanar_front + .into_iter() + .chain(coplanar_back.into_iter()) + .for_each(|coplanar_poly| { + if plane.orient_plane(&coplanar_poly.plane) == FRONT { + front_parts.push(coplanar_poly); + } else { + back_parts.push(coplanar_poly); + } + }); + + front_polys.append(&mut front_parts); + back_polys.append(&mut back_parts); + } + + // Recursively clip with optimized pattern + let mut result = if let Some(front_node) = &node.front { + self.clip_polygons(front_node, &front_polys) + } else { + front_polys + }; + + if let Some(back_node) = &node.back { + result.extend(self.clip_polygons(back_node, &back_polys)); + } + + result + } + + fn clip_to(&self, node: &mut Node, bsp: &Node) { + node.polygons = self.clip_polygons(bsp, &node.polygons); + + if let Some(ref mut front) = node.front { + self.clip_to(front, bsp); + } + + if let Some(ref mut back) = node.back { + self.clip_to(back, bsp); + } + } + + fn all_polygons(&self, node: &Node) -> Vec> { + let mut result = Vec::new(); + let mut stack = vec![node]; + + while let Some(current) = stack.pop() { + result.extend_from_slice(¤t.polygons); + + // Use iterator to add child nodes more efficiently + stack.extend( + [¤t.front, ¤t.back] + .iter() + .filter_map(|child| child.as_ref().map(|boxed| boxed.as_ref())), + ); + } + result + } + + fn build(&self, node: &mut Node, polygons: &[Polygon]) { + if polygons.is_empty() { + return; + } + + // Choose the best splitting plane if not already set + if node.plane.is_none() { + node.plane = Some(self.splitting_strategy.pick_best_splitting_plane(polygons)); + } + let plane = node.plane.as_ref().unwrap(); + + // Pre-allocate with estimated capacity + let mut front = Vec::with_capacity(polygons.len() / 2); + let mut back = Vec::with_capacity(polygons.len() / 2); + + // Optimized polygon classification using iterator pattern + for polygon in polygons { + let (coplanar_front, coplanar_back, mut front_parts, mut back_parts) = + plane.split_polygon(polygon); + + // Extend collections efficiently with iterator chains + node.polygons.extend(coplanar_front); + node.polygons.extend(coplanar_back); + front.append(&mut front_parts); + back.append(&mut back_parts); + } + + // Build child nodes using lazy initialization pattern + if !front.is_empty() { + node.front + .get_or_insert_with(|| Box::new(Node::new())); + self.build(node.front.as_mut().unwrap(), &front); + } + + if !back.is_empty() { + node.back + .get_or_insert_with(|| Box::new(Node::new())); + self.build(node.back.as_mut().unwrap(), &back); + } + } + + fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + let all_polys = self.all_polygons(node); + + let mut coplanar_polygons = Vec::new(); + let mut intersection_edges = Vec::new(); + + for poly in &all_polys { + let vcount = poly.vertices.len(); + if vcount < 2 { + continue; // degenerate polygon => skip + } + + // Use iterator to compute vertex types + let types: Vec<_> = poly + .vertices + .iter() + .map(|vertex| slicing_plane.orient_point(&vertex.pos)) + .collect(); + + let polygon_type = types.iter().fold(0, |acc, &vertex_type| acc | vertex_type); + + match polygon_type { + COPLANAR => { + coplanar_polygons.push(poly.clone()); + }, + FRONT | BACK => { + // Entirely on one side => no intersection + }, + SPANNING => { + // The polygon crosses the plane + let crossing_points: Vec<_> = (0..vcount) + .filter_map(|i| { + let j = (i + 1) % vcount; + let ti = types[i]; + let tj = types[j]; + let vi = &poly.vertices[i]; + let vj = &poly.vertices[j]; + + if (ti | tj) == SPANNING { + let denom = slicing_plane.normal().dot(&(vj.pos - vi.pos)); + if denom.abs() > EPSILON { + let intersection = (slicing_plane.offset() + - slicing_plane.normal().dot(&vi.pos.coords)) + / denom; + Some(vi.interpolate(vj, intersection)) + } else { + None + } + } else { + None + } + }) + .collect(); + + // Convert crossing points to intersection edges + intersection_edges.extend( + crossing_points + .chunks_exact(2) + .map(|chunk| [chunk[0].clone(), chunk[1].clone()]), + ); + }, + _ => { + // Shouldn't happen in typical classification + }, + } + } + + (coplanar_polygons, intersection_edges) + } +} \ No newline at end of file diff --git a/src/mesh/bsp/traits.rs b/src/mesh/bsp/traits.rs new file mode 100644 index 0000000..cd43793 --- /dev/null +++ b/src/mesh/bsp/traits.rs @@ -0,0 +1,82 @@ +//! Traits defining BSP tree operations for dependency inversion + +use crate::float_types::Real; +use crate::mesh::bsp::node::Node; +use crate::mesh::plane::Plane; +use crate::mesh::polygon::Polygon; +use crate::mesh::vertex::Vertex; + +/// Core BSP operations trait - implements algorithms on BSP nodes +pub trait BspOps { + /// Invert all polygons in the BSP tree + fn invert(&self, node: &mut Node); + + /// Recursively remove all polygons that are inside this BSP tree + fn clip_polygons(&self, node: &Node, polygons: &[Polygon]) -> Vec>; + + /// Remove all polygons in this BSP tree that are inside the other BSP tree + fn clip_to(&self, node: &mut Node, other: &Node); + + /// Build a BSP tree from the given polygons + fn build(&self, node: &mut Node, polygons: &[Polygon]); + + /// Return all polygons in this BSP tree + fn all_polygons(&self, node: &Node) -> Vec>; + + /// Slices this BSP node with the given plane + fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>); +} + +/// Trait for picking optimal splitting planes +pub trait SplittingPlaneStrategy { + /// Pick the best splitting plane from a set of polygons + fn pick_best_splitting_plane(&self, polygons: &[Polygon]) -> Plane; +} + +/// Default splitting plane strategy using balanced heuristic +pub struct BalancedSplittingStrategy { + pub span_weight: Real, + pub balance_weight: Real, +} + +impl Default for BalancedSplittingStrategy { + fn default() -> Self { + Self { + span_weight: 8.0, + balance_weight: 1.0, + } + } +} + +impl SplittingPlaneStrategy for BalancedSplittingStrategy { + fn pick_best_splitting_plane(&self, polygons: &[Polygon]) -> Plane { + let mut best_plane = polygons[0].plane.clone(); + let mut best_score = Real::MAX; + + // Take a sample of polygons as candidate planes + let sample_size = polygons.len().min(20); + + polygons.iter().take(sample_size).for_each(|p| { + let plane = &p.plane; + let (num_front, num_back, num_spanning) = polygons + .iter() + .map(|poly| match plane.classify_polygon(poly) { + crate::mesh::plane::COPLANAR => (0, 0, 0), + crate::mesh::plane::FRONT => (1, 0, 0), + crate::mesh::plane::BACK => (0, 1, 0), + crate::mesh::plane::SPANNING | _ => (0, 0, 1), + }) + .fold((0, 0, 0), |acc, x| (acc.0 + x.0, acc.1 + x.1, acc.2 + x.2)); + + let score = self.span_weight * num_spanning as Real + + self.balance_weight * ((num_front - num_back) as Real).abs(); + + if score < best_score { + best_score = score; + best_plane = plane.clone(); + } + }); + + best_plane + } +} \ No newline at end of file diff --git a/src/mesh/bsp_parallel.rs b/src/mesh/bsp_parallel.rs deleted file mode 100644 index b1d5220..0000000 --- a/src/mesh/bsp_parallel.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! Parallel versions of [BSP](https://en.wikipedia.org/wiki/Binary_space_partitioning) operations - -use crate::mesh::bsp::Node; -use std::fmt::Debug; - -#[cfg(feature = "parallel")] -use crate::mesh::plane::{BACK, COPLANAR, FRONT, Plane, SPANNING}; - -#[cfg(feature = "parallel")] -use rayon::prelude::*; - -#[cfg(feature = "parallel")] -use crate::mesh::Polygon; - -#[cfg(feature = "parallel")] -use crate::mesh::Vertex; - -#[cfg(feature = "parallel")] -use crate::float_types::EPSILON; - -impl Node { - /// Invert all polygons in the BSP tree using iterative approach to avoid stack overflow - #[cfg(feature = "parallel")] - pub fn invert(&mut self) { - // Use iterative approach with a stack to avoid recursive stack overflow - let mut stack = vec![self]; - - while let Some(node) = stack.pop() { - // Flip all polygons and plane in this node - node.polygons.par_iter_mut().for_each(|p| p.flip()); - if let Some(ref mut plane) = node.plane { - plane.flip(); - } - - // Swap front and back children - std::mem::swap(&mut node.front, &mut node.back); - - // Add children to stack for processing - if let Some(ref mut front) = node.front { - stack.push(front.as_mut()); - } - if let Some(ref mut back) = node.back { - stack.push(back.as_mut()); - } - } - } - - /// Parallel version of clip Polygons - #[cfg(feature = "parallel")] - pub fn clip_polygons(&self, polygons: &[Polygon]) -> Vec> { - // If this node has no plane, just return the original set - if self.plane.is_none() { - return polygons.to_vec(); - } - let plane = self.plane.as_ref().unwrap(); - - // Split each polygon in parallel; gather results - let (coplanar_front, coplanar_back, mut front, mut back) = polygons - .par_iter() - .map(|poly| plane.split_polygon(poly)) // <-- just pass poly - .reduce( - || (Vec::new(), Vec::new(), Vec::new(), Vec::new()), - |mut acc, x| { - acc.0.extend(x.0); - acc.1.extend(x.1); - acc.2.extend(x.2); - acc.3.extend(x.3); - acc - }, - ); - - // Decide where to send the coplanar polygons - for cp in coplanar_front { - if plane.orient_plane(&cp.plane) == FRONT { - front.push(cp); - } else { - back.push(cp); - } - } - for cp in coplanar_back { - if plane.orient_plane(&cp.plane) == FRONT { - front.push(cp); - } else { - back.push(cp); - } - } - - // Process front and back using parallel iterators to avoid recursive join - let mut result = if let Some(ref f) = self.front { - f.clip_polygons(&front) - } else { - front - }; - - if let Some(ref b) = self.back { - result.extend(b.clip_polygons(&back)); - } - // If there's no back node, we simply don't extend (effectively discarding back polygons) - - result - } - - /// Parallel version of `clip_to` using iterative approach to avoid stack overflow - #[cfg(feature = "parallel")] - pub fn clip_to(&mut self, bsp: &Node) { - // Use iterative approach with a stack to avoid recursive stack overflow - let mut stack = vec![self]; - - while let Some(node) = stack.pop() { - // Clip polygons at this node - node.polygons = bsp.clip_polygons(&node.polygons); - - // Add children to stack for processing - if let Some(ref mut front) = node.front { - stack.push(front.as_mut()); - } - if let Some(ref mut back) = node.back { - stack.push(back.as_mut()); - } - } - } - - /// Parallel version of `build`. - #[cfg(feature = "parallel")] - pub fn build(&mut self, polygons: &[Polygon]) { - if polygons.is_empty() { - return; - } - - // Choose splitting plane if not already set - if self.plane.is_none() { - self.plane = Some(self.pick_best_splitting_plane(polygons)); - } - let plane = self.plane.as_ref().unwrap(); - - // Split polygons in parallel - let (mut coplanar_front, mut coplanar_back, front, back) = - polygons.par_iter().map(|p| plane.split_polygon(p)).reduce( - || (Vec::new(), Vec::new(), Vec::new(), Vec::new()), - |mut acc, x| { - acc.0.extend(x.0); - acc.1.extend(x.1); - acc.2.extend(x.2); - acc.3.extend(x.3); - acc - }, - ); - - // Append coplanar fronts/backs to self.polygons - self.polygons.append(&mut coplanar_front); - self.polygons.append(&mut coplanar_back); - - // Build children sequentially to avoid stack overflow from recursive join - // The polygon splitting above already uses parallel iterators for the heavy work - if !front.is_empty() { - let mut front_node = self.front.take().unwrap_or_else(|| Box::new(Node::new())); - front_node.build(&front); - self.front = Some(front_node); - } - - if !back.is_empty() { - let mut back_node = self.back.take().unwrap_or_else(|| Box::new(Node::new())); - back_node.build(&back); - self.back = Some(back_node); - } - } - - // Parallel slice - #[cfg(feature = "parallel")] - pub fn slice(&self, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { - // Collect all polygons (this can be expensive, but let's do it). - let all_polys = self.all_polygons(); - - // Process polygons in parallel - let (coplanar_polygons, intersection_edges) = all_polys - .par_iter() - .map(|poly| { - let vcount = poly.vertices.len(); - if vcount < 2 { - // Degenerate => skip - return (Vec::new(), Vec::new()); - } - let mut polygon_type = 0; - let mut types = Vec::with_capacity(vcount); - - for vertex in &poly.vertices { - let vertex_type = slicing_plane.orient_point(&vertex.pos); - polygon_type |= vertex_type; - types.push(vertex_type); - } - - match polygon_type { - COPLANAR => { - // Entire polygon in plane - (vec![poly.clone()], Vec::new()) - }, - FRONT | BACK => { - // Entirely on one side => no intersection - (Vec::new(), Vec::new()) - }, - SPANNING => { - // The polygon crosses the plane => gather intersection edges - let mut crossing_points = Vec::new(); - for i in 0..vcount { - let j = (i + 1) % vcount; - let ti = types[i]; - let tj = types[j]; - let vi = &poly.vertices[i]; - let vj = &poly.vertices[j]; - - if (ti | tj) == SPANNING { - // The param intersection at which plane intersects the edge [vi -> vj]. - // Avoid dividing by zero: - let denom = slicing_plane.normal().dot(&(vj.pos - vi.pos)); - if denom.abs() > EPSILON { - let intersection = (slicing_plane.offset() - - slicing_plane.normal().dot(&vi.pos.coords)) - / denom; - // Interpolate: - let intersect_vert = vi.interpolate(vj, intersection); - crossing_points.push(intersect_vert); - } - } - } - - // Pair up intersection points => edges - let mut edges = Vec::new(); - for chunk in crossing_points.chunks_exact(2) { - edges.push([chunk[0].clone(), chunk[1].clone()]); - } - (Vec::new(), edges) - }, - _ => (Vec::new(), Vec::new()), - } - }) - .reduce( - || (Vec::new(), Vec::new()), - |mut acc, x| { - acc.0.extend(x.0); - acc.1.extend(x.1); - acc - }, - ); - - (coplanar_polygons, intersection_edges) - } -} diff --git a/src/mesh/mod.rs b/src/mesh/mod.rs index d657de3..92f87e6 100644 --- a/src/mesh/mod.rs +++ b/src/mesh/mod.rs @@ -25,7 +25,6 @@ use std::{cmp::PartialEq, fmt::Debug, num::NonZeroU32, sync::OnceLock}; use rayon::{iter::IntoParallelRefIterator, prelude::*}; pub mod bsp; -pub mod bsp_parallel; #[cfg(feature = "chull")] pub mod convex_hull; diff --git a/src/sketch/shapes.rs b/src/sketch/shapes.rs index 0203642..23cc5d7 100644 --- a/src/sketch/shapes.rs +++ b/src/sketch/shapes.rs @@ -21,7 +21,8 @@ impl Sketch { /// /// # Example /// ``` - /// let sq2 = Sketch::rectangle(2.0, 3.0, None); + /// use csgrs::sketch::Sketch; + /// let sq2: Sketch<()> = Sketch::rectangle(2.0, 3.0, None); /// ``` pub fn rectangle(width: Real, length: Real, metadata: Option) -> Self { // In geo, a Polygon is basically (outer: LineString, Vec for holes). From a91324a8a1681e6b958c50f6aea65b040f50082d Mon Sep 17 00:00:00 2001 From: ryancinsight Date: Thu, 10 Jul 2025 13:41:27 -0400 Subject: [PATCH 2/3] Refactor BSP module with dependency inversion and iterators +fmt/clippy - Implement dependency inversion pattern using BspOps trait - Create separate SerialBspOps and ParallelBspOps implementations - Replace manual loops with iterator patterns throughout - Maintain backward compatibility with existing Node API - Add SplittingPlaneStrategy trait for algorithm flexibility - Organize code into clean submodule structure: - traits.rs: Core trait definitions - node.rs: Node data structure only - serial.rs: Serial BSP operations - parallel.rs: Parallel BSP operations (with rayon) - mod.rs: Public API and backward compatibility - Fix doctest import issues and create missing directories - All tests passing (98 lib tests + 28 doc tests) This refactoring separates algorithms from data structures, enables easy testing of different BSP strategies, and follows modern Rust patterns while maintaining full API compatibility. --- src/io/stl.rs | 1 - src/lib.rs | 1 - src/main.rs | 44 +-- src/mesh/bsp/mod.rs | 10 +- src/mesh/bsp/node.rs | 2 +- src/mesh/bsp/parallel.rs | 25 +- src/mesh/bsp/serial.rs | 31 +- src/mesh/bsp/traits.rs | 10 +- src/mesh/metaballs.rs | 1 + src/nurbs/mod.rs | 2 +- src/sketch/extrudes.rs | 656 +++++++++++++++++++-------------------- src/sketch/hershey.rs | 1 - src/tests.rs | 2 +- 13 files changed, 399 insertions(+), 387 deletions(-) diff --git a/src/io/stl.rs b/src/io/stl.rs index 9459fb7..807ba5d 100644 --- a/src/io/stl.rs +++ b/src/io/stl.rs @@ -406,7 +406,6 @@ impl Sketch { } } - // // (C) Encode into a binary STL buffer // let mut cursor = Cursor::new(Vec::new()); diff --git a/src/lib.rs b/src/lib.rs index e528624..e0d9c83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ //! //! ![Example CSG output][Example CSG output] #![cfg_attr(doc, doc = doc_image_embed::embed_image!("Example CSG output", "docs/csg.png"))] -//! //! # Features //! #### Default //! - **f64**: use f64 as Real diff --git a/src/main.rs b/src/main.rs index 12e8d4b..8985a67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -349,11 +349,11 @@ fn main() { ); } - //let poor_geometry_shape = moved_cube.difference(&sphere); + // let poor_geometry_shape = moved_cube.difference(&sphere); //#[cfg(feature = "earclip-io")] - //let retriangulated_shape = poor_geometry_shape.triangulate_earclip(); + // let retriangulated_shape = poor_geometry_shape.triangulate_earclip(); //#[cfg(all(feature = "earclip-io", feature = "stl-io"))] - //let _ = fs::write("stl/retriangulated.stl", retriangulated_shape.to_stl_binary("retriangulated").unwrap()); + // let _ = fs::write("stl/retriangulated.stl", retriangulated_shape.to_stl_binary("retriangulated").unwrap()); let sphere_test = Mesh::sphere(1.0, 16, 8, None); let cube_test = Mesh::cube(1.0, None); @@ -724,8 +724,8 @@ fn main() { let _ = fs::write("stl/octahedron.stl", oct.to_stl_ascii("octahedron")); } - //let dodec = CSG::dodecahedron(15.0, None); - //let _ = fs::write("stl/dodecahedron.stl", dodec.to_stl_ascii("")); + // let dodec = CSG::dodecahedron(15.0, None); + // let _ = fs::write("stl/dodecahedron.stl", dodec.to_stl_ascii("")); #[cfg(feature = "stl-io")] { @@ -1041,19 +1041,17 @@ fn main() { ); } - /* - let helical = CSG::helical_involute_gear( - 2.0, // module - 20, // z - 20.0, // pressure angle - 0.05, 0.02, 14, - 25.0, // face-width - 15.0, // helix angle β [deg] - 40, // axial slices (resolution of the twist) - None, - ); - let _ = fs::write("stl/helical.stl", helical.to_stl_ascii("helical")); - */ + // let helical = CSG::helical_involute_gear( + // 2.0, // module + // 20, // z + // 20.0, // pressure angle + // 0.05, 0.02, 14, + // 25.0, // face-width + // 15.0, // helix angle β [deg] + // 40, // axial slices (resolution of the twist) + // None, + // ); + // let _ = fs::write("stl/helical.stl", helical.to_stl_ascii("helical")); // Bézier curve demo #[cfg(feature = "stl-io")] @@ -1081,8 +1079,10 @@ fn main() { let bspline_ctrl = &[[0.0, 0.0], [1.0, 2.5], [3.0, 3.0], [5.0, 0.0], [6.0, -1.5]]; let bspline_2d = Sketch::bspline( bspline_ctrl, - /* degree p = */ 3, - /* seg/span */ 32, + // degree p = + 3, + // seg/span + 32, None, ); let _ = fs::write("stl/bspline_2d.stl", bspline_2d.to_stl_ascii("bspline_2d")); @@ -1092,8 +1092,8 @@ fn main() { println!("{:#?}", bezier_3d.to_bevy_mesh()); // a quick thickening just like the Bézier - //let bspline_3d = bspline_2d.extrude(0.25); - //let _ = fs::write( + // let bspline_3d = bspline_2d.extrude(0.25); + // let _ = fs::write( // "stl/bspline_extruded.stl", // bspline_3d.to_stl_ascii("bspline_extruded"), //); diff --git a/src/mesh/bsp/mod.rs b/src/mesh/bsp/mod.rs index d439951..cff3bb6 100644 --- a/src/mesh/bsp/mod.rs +++ b/src/mesh/bsp/mod.rs @@ -1,5 +1,5 @@ //! Binary Space Partitioning (BSP) tree implementation -//! +//! //! This module provides BSP tree operations with dependency inversion, //! allowing for different algorithm implementations (serial/parallel). @@ -14,7 +14,7 @@ pub mod parallel; // Re-export core types for backward compatibility pub use node::Node; -pub use traits::{BspOps, SplittingPlaneStrategy, BalancedSplittingStrategy}; +pub use traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; #[cfg(not(feature = "parallel"))] pub use serial::SerialBspOps; @@ -23,10 +23,10 @@ pub use serial::SerialBspOps; pub use parallel::ParallelBspOps; // Backward compatibility implementations on Node -use std::fmt::Debug; use crate::mesh::plane::Plane; use crate::mesh::polygon::Polygon; use crate::mesh::vertex::Vertex; +use std::fmt::Debug; impl Node { /// Creates a new BSP node from polygons @@ -89,7 +89,7 @@ impl Node { let ops = SerialBspOps::new(); #[cfg(feature = "parallel")] let ops = ParallelBspOps::new(); - + ops.all_polygons(self) } @@ -118,4 +118,4 @@ impl Node { let ops = ParallelBspOps::new(); ops.slice(self, slicing_plane) } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/node.rs b/src/mesh/bsp/node.rs index bd73308..79ba179 100644 --- a/src/mesh/bsp/node.rs +++ b/src/mesh/bsp/node.rs @@ -38,4 +38,4 @@ impl Node { polygons: Vec::new(), } } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/parallel.rs b/src/mesh/bsp/parallel.rs index fd1c5b9..a6ca5bc 100644 --- a/src/mesh/bsp/parallel.rs +++ b/src/mesh/bsp/parallel.rs @@ -6,14 +6,17 @@ use rayon::prelude::*; use crate::float_types::EPSILON; use crate::mesh::bsp::node::Node; use crate::mesh::bsp::traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; -use crate::mesh::plane::{Plane, BACK, COPLANAR, FRONT, SPANNING}; +use crate::mesh::plane::{BACK, COPLANAR, FRONT, Plane, SPANNING}; use crate::mesh::polygon::Polygon; use crate::mesh::vertex::Vertex; use std::fmt::Debug; /// Parallel implementation of BSP operations #[cfg(feature = "parallel")] -pub struct ParallelBspOps = BalancedSplittingStrategy, S: Clone = ()> { +pub struct ParallelBspOps< + SP: SplittingPlaneStrategy = BalancedSplittingStrategy, + S: Clone = (), +> { splitting_strategy: SP, _phantom: std::marker::PhantomData, } @@ -39,7 +42,9 @@ impl, S: Clone> ParallelBspOps { } #[cfg(feature = "parallel")] -impl + Sync, S: Clone + Send + Sync + Debug> BspOps for ParallelBspOps { +impl + Sync, S: Clone + Send + Sync + Debug> BspOps + for ParallelBspOps +{ fn invert(&self, node: &mut Node) { // Use iterative approach with a stack to avoid stack overflow let mut stack = vec![node]; @@ -179,7 +184,7 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp while let Some(current) = stack.pop() { result.extend_from_slice(¤t.polygons); - + // Use iterator to add child nodes more efficiently stack.extend( [¤t.front, ¤t.back] @@ -190,7 +195,11 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp result } - fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + fn slice( + &self, + node: &Node, + slicing_plane: &Plane, + ) -> (Vec>, Vec<[Vertex; 2]>) { // Collect all polygons let all_polys = self.all_polygons(node); @@ -203,7 +212,7 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp // Degenerate => skip return (Vec::new(), Vec::new()); } - + let types: Vec<_> = poly .vertices .iter() @@ -254,7 +263,7 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp .chunks_exact(2) .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) .collect(); - + (Vec::new(), edges) }, _ => (Vec::new(), Vec::new()), @@ -271,4 +280,4 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp (coplanar_polygons, intersection_edges) } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/serial.rs b/src/mesh/bsp/serial.rs index 04a79ec..11c2437 100644 --- a/src/mesh/bsp/serial.rs +++ b/src/mesh/bsp/serial.rs @@ -3,13 +3,16 @@ use crate::float_types::EPSILON; use crate::mesh::bsp::node::Node; use crate::mesh::bsp::traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; -use crate::mesh::plane::{Plane, BACK, COPLANAR, FRONT, SPANNING}; +use crate::mesh::plane::{BACK, COPLANAR, FRONT, Plane, SPANNING}; use crate::mesh::polygon::Polygon; use crate::mesh::vertex::Vertex; use std::fmt::Debug; /// Serial implementation of BSP operations -pub struct SerialBspOps = BalancedSplittingStrategy, S: Clone = ()> { +pub struct SerialBspOps< + SP: SplittingPlaneStrategy = BalancedSplittingStrategy, + S: Clone = (), +> { splitting_strategy: SP, _phantom: std::marker::PhantomData, } @@ -32,9 +35,9 @@ impl, S: Clone> SerialBspOps { } } - - -impl, S: Clone + Send + Sync + Debug> BspOps for SerialBspOps { +impl, S: Clone + Send + Sync + Debug> BspOps + for SerialBspOps +{ fn invert(&self, node: &mut Node) { // Use iterative approach with a stack let mut stack = vec![node]; @@ -108,11 +111,11 @@ impl, S: Clone + Send + Sync + Debug> BspOps fo fn clip_to(&self, node: &mut Node, bsp: &Node) { node.polygons = self.clip_polygons(bsp, &node.polygons); - + if let Some(ref mut front) = node.front { self.clip_to(front, bsp); } - + if let Some(ref mut back) = node.back { self.clip_to(back, bsp); } @@ -164,19 +167,21 @@ impl, S: Clone + Send + Sync + Debug> BspOps fo // Build child nodes using lazy initialization pattern if !front.is_empty() { - node.front - .get_or_insert_with(|| Box::new(Node::new())); + node.front.get_or_insert_with(|| Box::new(Node::new())); self.build(node.front.as_mut().unwrap(), &front); } if !back.is_empty() { - node.back - .get_or_insert_with(|| Box::new(Node::new())); + node.back.get_or_insert_with(|| Box::new(Node::new())); self.build(node.back.as_mut().unwrap(), &back); } } - fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + fn slice( + &self, + node: &Node, + slicing_plane: &Plane, + ) -> (Vec>, Vec<[Vertex; 2]>) { let all_polys = self.all_polygons(node); let mut coplanar_polygons = Vec::new(); @@ -245,4 +250,4 @@ impl, S: Clone + Send + Sync + Debug> BspOps fo (coplanar_polygons, intersection_edges) } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/traits.rs b/src/mesh/bsp/traits.rs index cd43793..4ca4191 100644 --- a/src/mesh/bsp/traits.rs +++ b/src/mesh/bsp/traits.rs @@ -24,7 +24,11 @@ pub trait BspOps { fn all_polygons(&self, node: &Node) -> Vec>; /// Slices this BSP node with the given plane - fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>); + fn slice( + &self, + node: &Node, + slicing_plane: &Plane, + ) -> (Vec>, Vec<[Vertex; 2]>); } /// Trait for picking optimal splitting planes @@ -55,7 +59,7 @@ impl SplittingPlaneStrategy for BalancedSplittingStrategy { // Take a sample of polygons as candidate planes let sample_size = polygons.len().min(20); - + polygons.iter().take(sample_size).for_each(|p| { let plane = &p.plane; let (num_front, num_back, num_spanning) = polygons @@ -79,4 +83,4 @@ impl SplittingPlaneStrategy for BalancedSplittingStrategy { best_plane } -} \ No newline at end of file +} diff --git a/src/mesh/metaballs.rs b/src/mesh/metaballs.rs index 1636dd4..ef7c6bc 100644 --- a/src/mesh/metaballs.rs +++ b/src/mesh/metaballs.rs @@ -126,6 +126,7 @@ impl Mesh { } impl fast_surface_nets::ndshape::Shape<3> for GridShape { type Coord = u32; + #[inline] fn as_array(&self) -> [Self::Coord; 3] { [self.nx, self.ny, self.nz] diff --git a/src/nurbs/mod.rs b/src/nurbs/mod.rs index cd79ddd..4942a7d 100644 --- a/src/nurbs/mod.rs +++ b/src/nurbs/mod.rs @@ -1 +1 @@ -//pub mod nurbs; +// pub mod nurbs; diff --git a/src/sketch/extrudes.rs b/src/sketch/extrudes.rs index 0dc41cc..c30bb29 100644 --- a/src/sketch/extrudes.rs +++ b/src/sketch/extrudes.rs @@ -306,179 +306,177 @@ impl Sketch { Ok(Mesh::from_polygons(&polygons, bottom.metadata.clone())) } - /* - /// Perform a linear extrusion along some axis, with optional twist, center, slices, scale, etc. - /// - /// # Parameters - /// - `direction`: Direction vector for the extrusion. - /// - `twist`: Total twist in degrees around the extrusion axis from bottom to top. - /// - `segments`: Number of intermediate subdivisions. - /// - `scale`: A uniform scale factor to apply at the top slice (bottom is scale=1.0). - /// - /// # Assumptions - /// - This CSG is assumed to represent one or more 2D polygons lying in or near the XY plane. - /// - The resulting shape is extruded *initially* along +Z, then finally rotated if `v != [0,0,1]`. - /// - /// # Returns - /// A new 3D CSG. - /// - /// # Example - /// ``` - /// let shape_2d = CSG::square(2.0, None); // a 2D square in XY - /// let extruded = shape_2d.linear_extrude( - /// direction = Vector3::new(0.0, 0.0, 10.0), - /// twist = 360.0, - /// segments = 32, - /// scale = 1.2, - /// ); - /// ``` - pub fn linear_extrude( - shape: &CCShape, - direction: Vector3, - twist_degs: Real, - segments: usize, - scale_top: Real, - metadata: Option, - ) -> CSG { - let mut polygons_3d = Vec::new(); - if segments < 1 { - return CSG::new(); - } - let height = direction.norm(); - if height < EPSILON { - // no real extrusion - return CSG::new(); - } - - // Step 1) Build a series of “transforms” from bottom=0..top=height, subdivided into `segments`. - // For each i in [0..=segments], compute fraction f and: - // - scale in XY => s_i - // - twist about Z => rot_i - // - translate in Z => z_i - // - // We'll store each “slice” in 3D form as a Vec>>, - // i.e. one 3D polyline for each boundary or hole in the shape. - let mut slices: Vec>>> = Vec::with_capacity(segments + 1); - // The axis to rotate around is the unit of `direction`. We'll do final alignment after constructing them along +Z. - let axis_dir = direction.normalize(); - - for i in 0..=segments { - let f = i as Real / segments as Real; - let s_i = 1.0 + (scale_top - 1.0) * f; // lerp(1, scale_top, f) - let twist_rad = twist_degs.to_radians() * f; - let z_i = height * f; - - // Build transform T = Tz * Rz * Sxy - // - scale in XY - // - twist around Z - // - translate in Z - let mat_scale = Matrix4::new_nonuniform_scaling(&Vector3::new(s_i, s_i, 1.0)); - let mat_rot = Rotation3::from_axis_angle(&Vector3::z_axis(), twist_rad).to_homogeneous(); - let mat_trans = Translation3::new(0.0, 0.0, z_i).to_homogeneous(); - let slice_mat = mat_trans * mat_rot * mat_scale; - - let slice_3d = project_shape_3d(shape, &slice_mat); - slices.push(slice_3d); - } - - // Step 2) “Stitch” consecutive slices to form side polygons. - // For each pair of slices[i], slices[i+1], for each boundary polyline j, - // connect edges. We assume each polyline has the same vertex_count in both slices. - // (If the shape is closed, we do wrap edges [n..0].) - // Then we optionally build bottom & top caps if the polylines are closed. - - // a) bottom + top caps, similar to extrude_vector approach - // For slices[0], build a “bottom” by triangulating in XY, flipping normal. - // For slices[segments], build a “top” by normal up. - // - // But we only do it if each boundary is closed. - // We must group CCW with matching holes. This is the same logic as `extrude_vector`. - - // We'll do a small helper that triangulates shape in 2D, then lifts that triangulation to slice_3d. - // You can re‐use the logic from `extrude_vector`. - - // Build the “bottom” from slices[0] if polylines are all or partially closed - polygons_3d.extend( - build_caps_from_slice(shape, &slices[0], true, metadata.clone()) - ); - // Build the “top” from slices[segments] - polygons_3d.extend( - build_caps_from_slice(shape, &slices[segments], false, metadata.clone()) - ); - - // b) side walls - for i in 0..segments { - let bottom_slice = &slices[i]; - let top_slice = &slices[i + 1]; - - // We know bottom_slice has shape.ccw_plines.len() + shape.cw_plines.len() polylines - // in the same order. Each polyline has the same vertex_count as in top_slice. - // So we can do a direct 1:1 match: bottom_slice[j] <-> top_slice[j]. - for (pline_idx, bot3d) in bottom_slice.iter().enumerate() { - let top3d = &top_slice[pline_idx]; - if bot3d.len() < 2 { - continue; - } - // is it closed? We can check shape’s corresponding polyline - let is_closed = if pline_idx < shape.ccw_plines.len() { - shape.ccw_plines[pline_idx].polyline.is_closed() - } else { - shape.cw_plines[pline_idx - shape.ccw_plines.len()].polyline.is_closed() - }; - let n = bot3d.len(); - let edge_count = if is_closed { n } else { n - 1 }; - - for k in 0..edge_count { - let k_next = (k + 1) % n; - let b_i = bot3d[k]; - let b_j = bot3d[k_next]; - let t_i = top3d[k]; - let t_j = top3d[k_next]; - - let poly_side = Polygon::new( - vec![ - Vertex::new(b_i, Vector3::zeros()), - Vertex::new(b_j, Vector3::zeros()), - Vertex::new(t_j, Vector3::zeros()), - Vertex::new(t_i, Vector3::zeros()), - ], - metadata.clone(), - ); - polygons_3d.push(poly_side); - } - } - } - - // Step 3) If direction is not along +Z, rotate final mesh so +Z aligns with your direction - // (This is optional or can be done up front. Typical OpenSCAD style is to do everything - // along +Z, then rotate the final.) - if (axis_dir - Vector3::z()).norm() > EPSILON { - // rotate from +Z to axis_dir - let rot_axis = Vector3::z().cross(&axis_dir); - let sin_theta = rot_axis.norm(); - if sin_theta > EPSILON { - let cos_theta = Vector3::z().dot(&axis_dir); - let angle = cos_theta.acos(); - let rot = Rotation3::from_axis_angle(&Unit::new_normalize(rot_axis), angle); - let mat = rot.to_homogeneous(); - // transform the polygons - let mut final_polys = Vec::with_capacity(polygons_3d.len()); - for mut poly in polygons_3d { - for v in &mut poly.vertices { - let pos4 = mat * nalgebra::Vector4::new(v.pos.x, v.pos.y, v.pos.z, 1.0); - v.pos = Point3::new(pos4.x / pos4.w, pos4.y / pos4.w, pos4.z / pos4.w); - } - poly.set_new_normal(); - final_polys.push(poly); - } - return CSG::from_polygons(&final_polys); - } - } - - // otherwise, just return as is - CSG::from_polygons(&polygons_3d) - } - */ + // Perform a linear extrusion along some axis, with optional twist, center, slices, scale, etc. + // + // # Parameters + // - `direction`: Direction vector for the extrusion. + // - `twist`: Total twist in degrees around the extrusion axis from bottom to top. + // - `segments`: Number of intermediate subdivisions. + // - `scale`: A uniform scale factor to apply at the top slice (bottom is scale=1.0). + // + // # Assumptions + // - This CSG is assumed to represent one or more 2D polygons lying in or near the XY plane. + // - The resulting shape is extruded *initially* along +Z, then finally rotated if `v != [0,0,1]`. + // + // # Returns + // A new 3D CSG. + // + // # Example + // ``` + // let shape_2d = CSG::square(2.0, None); // a 2D square in XY + // let extruded = shape_2d.linear_extrude( + // direction = Vector3::new(0.0, 0.0, 10.0), + // twist = 360.0, + // segments = 32, + // scale = 1.2, + // ); + // ``` + // pub fn linear_extrude( + // shape: &CCShape, + // direction: Vector3, + // twist_degs: Real, + // segments: usize, + // scale_top: Real, + // metadata: Option, + // ) -> CSG { + // let mut polygons_3d = Vec::new(); + // if segments < 1 { + // return CSG::new(); + // } + // let height = direction.norm(); + // if height < EPSILON { + // no real extrusion + // return CSG::new(); + // } + // + // Step 1) Build a series of “transforms” from bottom=0..top=height, subdivided into `segments`. + // For each i in [0..=segments], compute fraction f and: + // - scale in XY => s_i + // - twist about Z => rot_i + // - translate in Z => z_i + // + // We'll store each “slice” in 3D form as a Vec>>, + // i.e. one 3D polyline for each boundary or hole in the shape. + // let mut slices: Vec>>> = Vec::with_capacity(segments + 1); + // The axis to rotate around is the unit of `direction`. We'll do final alignment after constructing them along +Z. + // let axis_dir = direction.normalize(); + // + // for i in 0..=segments { + // let f = i as Real / segments as Real; + // let s_i = 1.0 + (scale_top - 1.0) * f; // lerp(1, scale_top, f) + // let twist_rad = twist_degs.to_radians() * f; + // let z_i = height * f; + // + // Build transform T = Tz * Rz * Sxy + // - scale in XY + // - twist around Z + // - translate in Z + // let mat_scale = Matrix4::new_nonuniform_scaling(&Vector3::new(s_i, s_i, 1.0)); + // let mat_rot = Rotation3::from_axis_angle(&Vector3::z_axis(), twist_rad).to_homogeneous(); + // let mat_trans = Translation3::new(0.0, 0.0, z_i).to_homogeneous(); + // let slice_mat = mat_trans * mat_rot * mat_scale; + // + // let slice_3d = project_shape_3d(shape, &slice_mat); + // slices.push(slice_3d); + // } + // + // Step 2) “Stitch” consecutive slices to form side polygons. + // For each pair of slices[i], slices[i+1], for each boundary polyline j, + // connect edges. We assume each polyline has the same vertex_count in both slices. + // (If the shape is closed, we do wrap edges [n..0].) + // Then we optionally build bottom & top caps if the polylines are closed. + // + // a) bottom + top caps, similar to extrude_vector approach + // For slices[0], build a “bottom” by triangulating in XY, flipping normal. + // For slices[segments], build a “top” by normal up. + // + // But we only do it if each boundary is closed. + // We must group CCW with matching holes. This is the same logic as `extrude_vector`. + // + // We'll do a small helper that triangulates shape in 2D, then lifts that triangulation to slice_3d. + // You can re‐use the logic from `extrude_vector`. + // + // Build the “bottom” from slices[0] if polylines are all or partially closed + // polygons_3d.extend( + // build_caps_from_slice(shape, &slices[0], true, metadata.clone()) + // ); + // Build the “top” from slices[segments] + // polygons_3d.extend( + // build_caps_from_slice(shape, &slices[segments], false, metadata.clone()) + // ); + // + // b) side walls + // for i in 0..segments { + // let bottom_slice = &slices[i]; + // let top_slice = &slices[i + 1]; + // + // We know bottom_slice has shape.ccw_plines.len() + shape.cw_plines.len() polylines + // in the same order. Each polyline has the same vertex_count as in top_slice. + // So we can do a direct 1:1 match: bottom_slice[j] <-> top_slice[j]. + // for (pline_idx, bot3d) in bottom_slice.iter().enumerate() { + // let top3d = &top_slice[pline_idx]; + // if bot3d.len() < 2 { + // continue; + // } + // is it closed? We can check shape’s corresponding polyline + // let is_closed = if pline_idx < shape.ccw_plines.len() { + // shape.ccw_plines[pline_idx].polyline.is_closed() + // } else { + // shape.cw_plines[pline_idx - shape.ccw_plines.len()].polyline.is_closed() + // }; + // let n = bot3d.len(); + // let edge_count = if is_closed { n } else { n - 1 }; + // + // for k in 0..edge_count { + // let k_next = (k + 1) % n; + // let b_i = bot3d[k]; + // let b_j = bot3d[k_next]; + // let t_i = top3d[k]; + // let t_j = top3d[k_next]; + // + // let poly_side = Polygon::new( + // vec![ + // Vertex::new(b_i, Vector3::zeros()), + // Vertex::new(b_j, Vector3::zeros()), + // Vertex::new(t_j, Vector3::zeros()), + // Vertex::new(t_i, Vector3::zeros()), + // ], + // metadata.clone(), + // ); + // polygons_3d.push(poly_side); + // } + // } + // } + // + // Step 3) If direction is not along +Z, rotate final mesh so +Z aligns with your direction + // (This is optional or can be done up front. Typical OpenSCAD style is to do everything + // along +Z, then rotate the final.) + // if (axis_dir - Vector3::z()).norm() > EPSILON { + // rotate from +Z to axis_dir + // let rot_axis = Vector3::z().cross(&axis_dir); + // let sin_theta = rot_axis.norm(); + // if sin_theta > EPSILON { + // let cos_theta = Vector3::z().dot(&axis_dir); + // let angle = cos_theta.acos(); + // let rot = Rotation3::from_axis_angle(&Unit::new_normalize(rot_axis), angle); + // let mat = rot.to_homogeneous(); + // transform the polygons + // let mut final_polys = Vec::with_capacity(polygons_3d.len()); + // for mut poly in polygons_3d { + // for v in &mut poly.vertices { + // let pos4 = mat * nalgebra::Vector4::new(v.pos.x, v.pos.y, v.pos.z, 1.0); + // v.pos = Point3::new(pos4.x / pos4.w, pos4.y / pos4.w, pos4.z / pos4.w); + // } + // poly.set_new_normal(); + // final_polys.push(poly); + // } + // return CSG::from_polygons(&final_polys); + // } + // } + // + // otherwise, just return as is + // CSG::from_polygons(&polygons_3d) + // } /// **Mathematical Foundation: Surface of Revolution Generation** /// @@ -826,163 +824,161 @@ impl Sketch { // // # Returns // A new 3D `CSG` that is the swept volume. - /* - pub fn sweep(shape_2d: &Polygon, path_2d: &Polygon) -> CSG { - // Gather the path’s vertices in XY - if path_2d.vertices.len() < 2 { - // Degenerate path => no sweep - return CSG::new(); - } - let path_is_closed = !path_2d.open; // If false => open path, if true => closed path - - // Extract path points (x,y,0) from path_2d - let mut path_points = Vec::with_capacity(path_2d.vertices.len()); - for v in &path_2d.vertices { - // We only take X & Y; Z is typically 0 for a 2D path - path_points.push(Point3::new(v.pos.x, v.pos.y, 0.0)); - } - - // Convert the shape_2d into a list of its vertices in local coords (usually in XY). - // We assume shape_2d is a single polygon (can also handle multiple if needed). - let shape_is_closed = !shape_2d.open && shape_2d.vertices.len() >= 3; - let shape_count = shape_2d.vertices.len(); - - // For each path vertex, compute the orientation that aligns +Z to the path tangent. - // Then transform the shape’s 2D vertices into 3D “slice[i]”. - let n_path = path_points.len(); - let mut slices: Vec>> = Vec::with_capacity(n_path); - - for i in 0..n_path { - // The path tangent is p[i+1] - p[i] (or wrap if path is closed) - // If open and i == n_path-1 => we’ll copy the tangent from the last segment - let next_i = if i == n_path - 1 { - if path_is_closed { 0 } else { i - 1 } // if closed, wrap, else reuse the previous - } else { - i + 1 - }; - - let mut dir = path_points[next_i] - path_points[i]; - if dir.norm_squared() < EPSILON { - // Degenerate segment => fallback to the previous direction or just use +Z - dir = Vector3::z(); - } else { - dir.normalize_mut(); - } - - // Build a rotation that maps +Z to `dir`. - // We'll rotate the z-axis (0,0,1) onto `dir`. - let z = Vector3::z(); - let dot = z.dot(&dir); - // If dir is basically the same as z, no rotation needed - if (dot - 1.0).abs() < EPSILON { - return Matrix4::identity(); - } - // If dir is basically opposite z - if (dot + 1.0).abs() < EPSILON { - // 180 deg around X or Y axis - let rot180 = Rotation3::from_axis_angle(&Unit::new_normalize(Vector3::x()), PI); - return rot180.to_homogeneous(); - } - // Otherwise, general axis = z × dir - let axis = z.cross(&dir).normalize(); - let angle = z.dot(&dir).acos(); - let initial_rot = Rotation3::from_axis_angle(&Unit::new_unchecked(axis), angle); - let rot = initial_rot.to_homogeneous() - - // Build a translation that puts shape origin at path_points[i] - let trans = Translation3::from(path_points[i].coords); - - // Combined transform = T * R - let mat = trans.to_homogeneous() * rot; - - // Apply that transform to all shape_2d vertices => slice[i] - let mut slice_i = Vec::with_capacity(shape_count); - for sv in &shape_2d.vertices { - let local_pt = sv.pos; // (x, y, z=0) - let p4 = local_pt.to_homogeneous(); - let p4_trans = mat * p4; - slice_i.push(Point3::from_homogeneous(p4_trans).unwrap()); - } - slices.push(slice_i); - } - - // Build polygons for the new 3D swept solid. - // - (A) “Cap” polygons at start & end if path is open. - // - (B) “Side wall” quads between slice[i] and slice[i+1]. - // - // We’ll gather them all into a Vec>, then make a CSG. - - let mut all_polygons = Vec::new(); - - // Caps if path is open - // We replicate the shape_2d as polygons at slice[0] and slice[n_path-1]. - // We flip the first one so its normal faces outward. The last we keep as is. - if !path_is_closed { - // “Bottom” cap = slice[0], but we flip its winding so outward normal is “down” the path - if shape_is_closed { - let bottom_poly = polygon_from_slice( - &slices[0], - true, // flip - shape_2d.metadata.clone(), - ); - all_polygons.push(bottom_poly); - } - // “Top” cap = slice[n_path-1] (no flip) - if shape_is_closed { - let top_poly = polygon_from_slice( - &slices[n_path - 1], - false, // no flip - shape_2d.metadata.clone(), - ); - all_polygons.push(top_poly); - } - } - - // Side walls: For i in [0..n_path-1], or [0..n_path] if closed - let end_index = if path_is_closed { n_path } else { n_path - 1 }; - - for i in 0..end_index { - let i_next = (i + 1) % n_path; // wraps if closed - let slice_i = &slices[i]; - let slice_next = &slices[i_next]; - - // For each edge in the shape, connect vertices k..k+1 - // shape_2d may be open or closed. If open, we do shape_count-1 edges; if closed, shape_count edges. - let edge_count = if shape_is_closed { - shape_count // because last edge wraps - } else { - shape_count - 1 - }; - - for k in 0..edge_count { - let k_next = (k + 1) % shape_count; - - let v_i_k = slice_i[k]; - let v_i_knext = slice_i[k_next]; - let v_next_k = slice_next[k]; - let v_next_knext = slice_next[k_next]; - - // Build a quad polygon in CCW order for outward normal - // or you might choose a different ordering. Typically: - // [v_i_k, v_i_knext, v_next_knext, v_next_k] - // forms an outward-facing side wall if the shape_2d was originally CCW in XY. - let side_poly = Polygon::new( - vec![ - Vertex::new(v_i_k, Vector3::zeros()), - Vertex::new(v_i_knext, Vector3::zeros()), - Vertex::new(v_next_knext, Vector3::zeros()), - Vertex::new(v_next_k, Vector3::zeros()), - ], - shape_2d.metadata.clone(), - ); - all_polygons.push(side_poly); - } - } - - // Combine into a final CSG - CSG::from_polygons(&all_polygons) - } - */ + // pub fn sweep(shape_2d: &Polygon, path_2d: &Polygon) -> CSG { + // Gather the path’s vertices in XY + // if path_2d.vertices.len() < 2 { + // Degenerate path => no sweep + // return CSG::new(); + // } + // let path_is_closed = !path_2d.open; // If false => open path, if true => closed path + // + // Extract path points (x,y,0) from path_2d + // let mut path_points = Vec::with_capacity(path_2d.vertices.len()); + // for v in &path_2d.vertices { + // We only take X & Y; Z is typically 0 for a 2D path + // path_points.push(Point3::new(v.pos.x, v.pos.y, 0.0)); + // } + // + // Convert the shape_2d into a list of its vertices in local coords (usually in XY). + // We assume shape_2d is a single polygon (can also handle multiple if needed). + // let shape_is_closed = !shape_2d.open && shape_2d.vertices.len() >= 3; + // let shape_count = shape_2d.vertices.len(); + // + // For each path vertex, compute the orientation that aligns +Z to the path tangent. + // Then transform the shape’s 2D vertices into 3D “slice[i]”. + // let n_path = path_points.len(); + // let mut slices: Vec>> = Vec::with_capacity(n_path); + // + // for i in 0..n_path { + // The path tangent is p[i+1] - p[i] (or wrap if path is closed) + // If open and i == n_path-1 => we’ll copy the tangent from the last segment + // let next_i = if i == n_path - 1 { + // if path_is_closed { 0 } else { i - 1 } // if closed, wrap, else reuse the previous + // } else { + // i + 1 + // }; + // + // let mut dir = path_points[next_i] - path_points[i]; + // if dir.norm_squared() < EPSILON { + // Degenerate segment => fallback to the previous direction or just use +Z + // dir = Vector3::z(); + // } else { + // dir.normalize_mut(); + // } + // + // Build a rotation that maps +Z to `dir`. + // We'll rotate the z-axis (0,0,1) onto `dir`. + // let z = Vector3::z(); + // let dot = z.dot(&dir); + // If dir is basically the same as z, no rotation needed + // if (dot - 1.0).abs() < EPSILON { + // return Matrix4::identity(); + // } + // If dir is basically opposite z + // if (dot + 1.0).abs() < EPSILON { + // 180 deg around X or Y axis + // let rot180 = Rotation3::from_axis_angle(&Unit::new_normalize(Vector3::x()), PI); + // return rot180.to_homogeneous(); + // } + // Otherwise, general axis = z × dir + // let axis = z.cross(&dir).normalize(); + // let angle = z.dot(&dir).acos(); + // let initial_rot = Rotation3::from_axis_angle(&Unit::new_unchecked(axis), angle); + // let rot = initial_rot.to_homogeneous() + // + // Build a translation that puts shape origin at path_points[i] + // let trans = Translation3::from(path_points[i].coords); + // + // Combined transform = T * R + // let mat = trans.to_homogeneous() * rot; + // + // Apply that transform to all shape_2d vertices => slice[i] + // let mut slice_i = Vec::with_capacity(shape_count); + // for sv in &shape_2d.vertices { + // let local_pt = sv.pos; // (x, y, z=0) + // let p4 = local_pt.to_homogeneous(); + // let p4_trans = mat * p4; + // slice_i.push(Point3::from_homogeneous(p4_trans).unwrap()); + // } + // slices.push(slice_i); + // } + // + // Build polygons for the new 3D swept solid. + // - (A) “Cap” polygons at start & end if path is open. + // - (B) “Side wall” quads between slice[i] and slice[i+1]. + // + // We’ll gather them all into a Vec>, then make a CSG. + // + // let mut all_polygons = Vec::new(); + // + // Caps if path is open + // We replicate the shape_2d as polygons at slice[0] and slice[n_path-1]. + // We flip the first one so its normal faces outward. The last we keep as is. + // if !path_is_closed { + // “Bottom” cap = slice[0], but we flip its winding so outward normal is “down” the path + // if shape_is_closed { + // let bottom_poly = polygon_from_slice( + // &slices[0], + // true, // flip + // shape_2d.metadata.clone(), + // ); + // all_polygons.push(bottom_poly); + // } + // “Top” cap = slice[n_path-1] (no flip) + // if shape_is_closed { + // let top_poly = polygon_from_slice( + // &slices[n_path - 1], + // false, // no flip + // shape_2d.metadata.clone(), + // ); + // all_polygons.push(top_poly); + // } + // } + // + // Side walls: For i in [0..n_path-1], or [0..n_path] if closed + // let end_index = if path_is_closed { n_path } else { n_path - 1 }; + // + // for i in 0..end_index { + // let i_next = (i + 1) % n_path; // wraps if closed + // let slice_i = &slices[i]; + // let slice_next = &slices[i_next]; + // + // For each edge in the shape, connect vertices k..k+1 + // shape_2d may be open or closed. If open, we do shape_count-1 edges; if closed, shape_count edges. + // let edge_count = if shape_is_closed { + // shape_count // because last edge wraps + // } else { + // shape_count - 1 + // }; + // + // for k in 0..edge_count { + // let k_next = (k + 1) % shape_count; + // + // let v_i_k = slice_i[k]; + // let v_i_knext = slice_i[k_next]; + // let v_next_k = slice_next[k]; + // let v_next_knext = slice_next[k_next]; + // + // Build a quad polygon in CCW order for outward normal + // or you might choose a different ordering. Typically: + // [v_i_k, v_i_knext, v_next_knext, v_next_k] + // forms an outward-facing side wall if the shape_2d was originally CCW in XY. + // let side_poly = Polygon::new( + // vec![ + // Vertex::new(v_i_k, Vector3::zeros()), + // Vertex::new(v_i_knext, Vector3::zeros()), + // Vertex::new(v_next_knext, Vector3::zeros()), + // Vertex::new(v_next_k, Vector3::zeros()), + // ], + // shape_2d.metadata.clone(), + // ); + // all_polygons.push(side_poly); + // } + // } + // + // Combine into a final CSG + // CSG::from_polygons(&all_polygons) + // } } /// Helper to build a single Polygon from a “slice” of 3D points. diff --git a/src/sketch/hershey.rs b/src/sketch/hershey.rs index 564ccfc..452bbfc 100644 --- a/src/sketch/hershey.rs +++ b/src/sketch/hershey.rs @@ -21,7 +21,6 @@ impl Sketch { /// /// # Returns /// A new `Sketch` where each glyph stroke is a `Geometry::LineString` in `geometry`. - /// pub fn from_hershey( text: &str, font: &Font, diff --git a/src/tests.rs b/src/tests.rs index 36a6947..ae43e76 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1401,7 +1401,7 @@ fn test_same_number_of_vertices() { // - 3 side polygons (one for each edge of the triangle) assert_eq!( csg.polygons.len(), - 1 /*bottom*/ + 1 /*top*/ + 3 /*sides*/ + 1 /*bottom*/ + 1 /*top*/ + 3 // sides ); } From f1c79eee12e39c3ae3def6b1205ff820492e326b Mon Sep 17 00:00:00 2001 From: ryancinsight Date: Thu, 10 Jul 2025 13:41:27 -0400 Subject: [PATCH 3/3] Refactor BSP module with dependency inversion and iterators +fmt/clippy - Implement dependency inversion pattern using BspOps trait - Create separate SerialBspOps and ParallelBspOps implementations - Replace manual loops with iterator patterns throughout - Maintain backward compatibility with existing Node API - Add SplittingPlaneStrategy trait for algorithm flexibility - Organize code into clean submodule structure: - traits.rs: Core trait definitions - node.rs: Node data structure only - serial.rs: Serial BSP operations - parallel.rs: Parallel BSP operations (with rayon) - mod.rs: Public API and backward compatibility - Fix doctest import issues and create missing directories - All tests passing (98 lib tests + 28 doc tests) This refactoring separates algorithms from data structures, enables easy testing of different BSP strategies, and follows modern Rust patterns while maintaining full API compatibility. --- src/io/stl.rs | 1 - src/lib.rs | 1 - src/main.rs | 52 ++-- src/mesh/bsp/mod.rs | 10 +- src/mesh/bsp/node.rs | 2 +- src/mesh/bsp/parallel.rs | 25 +- src/mesh/bsp/serial.rs | 39 ++- src/mesh/bsp/traits.rs | 10 +- src/mesh/metaballs.rs | 1 + src/nurbs/mod.rs | 2 +- src/sketch/extrudes.rs | 656 +++++++++++++++++++-------------------- src/sketch/hershey.rs | 1 - src/tests.rs | 2 +- 13 files changed, 410 insertions(+), 392 deletions(-) diff --git a/src/io/stl.rs b/src/io/stl.rs index 9459fb7..807ba5d 100644 --- a/src/io/stl.rs +++ b/src/io/stl.rs @@ -406,7 +406,6 @@ impl Sketch { } } - // // (C) Encode into a binary STL buffer // let mut cursor = Cursor::new(Vec::new()); diff --git a/src/lib.rs b/src/lib.rs index e528624..e0d9c83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ //! //! ![Example CSG output][Example CSG output] #![cfg_attr(doc, doc = doc_image_embed::embed_image!("Example CSG output", "docs/csg.png"))] -//! //! # Features //! #### Default //! - **f64**: use f64 as Real diff --git a/src/main.rs b/src/main.rs index 12e8d4b..fceb367 100644 --- a/src/main.rs +++ b/src/main.rs @@ -252,7 +252,7 @@ fn main() { let ray_origin = Point3::new(0.0, 0.0, -5.0); let ray_dir = Vector3::new(0.0, 0.0, 1.0); // pointing along +Z let hits = cube.ray_intersections(&ray_origin, &ray_dir); - println!("Ray hits on the cube: {:?}", hits); + println!("Ray hits on the cube: {hits:?}"); } // 12) Polyhedron example (simple tetrahedron): @@ -297,9 +297,9 @@ fn main() { // 14) Mass properties (just printing them) let (mass, com, principal_frame) = cube.mass_properties(1.0); - println!("Cube mass = {}", mass); - println!("Cube center of mass = {:?}", com); - println!("Cube principal inertia local frame = {:?}", principal_frame); + println!("Cube mass = {mass}"); + println!("Cube center of mass = {com:?}"); + println!("Cube principal inertia local frame = {principal_frame:?}"); // 1) Create a cube from (-1,-1,-1) to (+1,+1,+1) // (By default, CSG::cube(None) is from -1..+1 if the "radius" is [1,1,1].) @@ -349,11 +349,11 @@ fn main() { ); } - //let poor_geometry_shape = moved_cube.difference(&sphere); + // let poor_geometry_shape = moved_cube.difference(&sphere); //#[cfg(feature = "earclip-io")] - //let retriangulated_shape = poor_geometry_shape.triangulate_earclip(); + // let retriangulated_shape = poor_geometry_shape.triangulate_earclip(); //#[cfg(all(feature = "earclip-io", feature = "stl-io"))] - //let _ = fs::write("stl/retriangulated.stl", retriangulated_shape.to_stl_binary("retriangulated").unwrap()); + // let _ = fs::write("stl/retriangulated.stl", retriangulated_shape.to_stl_binary("retriangulated").unwrap()); let sphere_test = Mesh::sphere(1.0, 16, 8, None); let cube_test = Mesh::cube(1.0, None); @@ -724,8 +724,8 @@ fn main() { let _ = fs::write("stl/octahedron.stl", oct.to_stl_ascii("octahedron")); } - //let dodec = CSG::dodecahedron(15.0, None); - //let _ = fs::write("stl/dodecahedron.stl", dodec.to_stl_ascii("")); + // let dodec = CSG::dodecahedron(15.0, None); + // let _ = fs::write("stl/dodecahedron.stl", dodec.to_stl_ascii("")); #[cfg(feature = "stl-io")] { @@ -1041,19 +1041,17 @@ fn main() { ); } - /* - let helical = CSG::helical_involute_gear( - 2.0, // module - 20, // z - 20.0, // pressure angle - 0.05, 0.02, 14, - 25.0, // face-width - 15.0, // helix angle β [deg] - 40, // axial slices (resolution of the twist) - None, - ); - let _ = fs::write("stl/helical.stl", helical.to_stl_ascii("helical")); - */ + // let helical = CSG::helical_involute_gear( + // 2.0, // module + // 20, // z + // 20.0, // pressure angle + // 0.05, 0.02, 14, + // 25.0, // face-width + // 15.0, // helix angle β [deg] + // 40, // axial slices (resolution of the twist) + // None, + // ); + // let _ = fs::write("stl/helical.stl", helical.to_stl_ascii("helical")); // Bézier curve demo #[cfg(feature = "stl-io")] @@ -1081,8 +1079,10 @@ fn main() { let bspline_ctrl = &[[0.0, 0.0], [1.0, 2.5], [3.0, 3.0], [5.0, 0.0], [6.0, -1.5]]; let bspline_2d = Sketch::bspline( bspline_ctrl, - /* degree p = */ 3, - /* seg/span */ 32, + // degree p = + 3, + // seg/span + 32, None, ); let _ = fs::write("stl/bspline_2d.stl", bspline_2d.to_stl_ascii("bspline_2d")); @@ -1092,8 +1092,8 @@ fn main() { println!("{:#?}", bezier_3d.to_bevy_mesh()); // a quick thickening just like the Bézier - //let bspline_3d = bspline_2d.extrude(0.25); - //let _ = fs::write( + // let bspline_3d = bspline_2d.extrude(0.25); + // let _ = fs::write( // "stl/bspline_extruded.stl", // bspline_3d.to_stl_ascii("bspline_extruded"), //); diff --git a/src/mesh/bsp/mod.rs b/src/mesh/bsp/mod.rs index d439951..cff3bb6 100644 --- a/src/mesh/bsp/mod.rs +++ b/src/mesh/bsp/mod.rs @@ -1,5 +1,5 @@ //! Binary Space Partitioning (BSP) tree implementation -//! +//! //! This module provides BSP tree operations with dependency inversion, //! allowing for different algorithm implementations (serial/parallel). @@ -14,7 +14,7 @@ pub mod parallel; // Re-export core types for backward compatibility pub use node::Node; -pub use traits::{BspOps, SplittingPlaneStrategy, BalancedSplittingStrategy}; +pub use traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; #[cfg(not(feature = "parallel"))] pub use serial::SerialBspOps; @@ -23,10 +23,10 @@ pub use serial::SerialBspOps; pub use parallel::ParallelBspOps; // Backward compatibility implementations on Node -use std::fmt::Debug; use crate::mesh::plane::Plane; use crate::mesh::polygon::Polygon; use crate::mesh::vertex::Vertex; +use std::fmt::Debug; impl Node { /// Creates a new BSP node from polygons @@ -89,7 +89,7 @@ impl Node { let ops = SerialBspOps::new(); #[cfg(feature = "parallel")] let ops = ParallelBspOps::new(); - + ops.all_polygons(self) } @@ -118,4 +118,4 @@ impl Node { let ops = ParallelBspOps::new(); ops.slice(self, slicing_plane) } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/node.rs b/src/mesh/bsp/node.rs index bd73308..79ba179 100644 --- a/src/mesh/bsp/node.rs +++ b/src/mesh/bsp/node.rs @@ -38,4 +38,4 @@ impl Node { polygons: Vec::new(), } } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/parallel.rs b/src/mesh/bsp/parallel.rs index fd1c5b9..a6ca5bc 100644 --- a/src/mesh/bsp/parallel.rs +++ b/src/mesh/bsp/parallel.rs @@ -6,14 +6,17 @@ use rayon::prelude::*; use crate::float_types::EPSILON; use crate::mesh::bsp::node::Node; use crate::mesh::bsp::traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; -use crate::mesh::plane::{Plane, BACK, COPLANAR, FRONT, SPANNING}; +use crate::mesh::plane::{BACK, COPLANAR, FRONT, Plane, SPANNING}; use crate::mesh::polygon::Polygon; use crate::mesh::vertex::Vertex; use std::fmt::Debug; /// Parallel implementation of BSP operations #[cfg(feature = "parallel")] -pub struct ParallelBspOps = BalancedSplittingStrategy, S: Clone = ()> { +pub struct ParallelBspOps< + SP: SplittingPlaneStrategy = BalancedSplittingStrategy, + S: Clone = (), +> { splitting_strategy: SP, _phantom: std::marker::PhantomData, } @@ -39,7 +42,9 @@ impl, S: Clone> ParallelBspOps { } #[cfg(feature = "parallel")] -impl + Sync, S: Clone + Send + Sync + Debug> BspOps for ParallelBspOps { +impl + Sync, S: Clone + Send + Sync + Debug> BspOps + for ParallelBspOps +{ fn invert(&self, node: &mut Node) { // Use iterative approach with a stack to avoid stack overflow let mut stack = vec![node]; @@ -179,7 +184,7 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp while let Some(current) = stack.pop() { result.extend_from_slice(¤t.polygons); - + // Use iterator to add child nodes more efficiently stack.extend( [¤t.front, ¤t.back] @@ -190,7 +195,11 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp result } - fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + fn slice( + &self, + node: &Node, + slicing_plane: &Plane, + ) -> (Vec>, Vec<[Vertex; 2]>) { // Collect all polygons let all_polys = self.all_polygons(node); @@ -203,7 +212,7 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp // Degenerate => skip return (Vec::new(), Vec::new()); } - + let types: Vec<_> = poly .vertices .iter() @@ -254,7 +263,7 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp .chunks_exact(2) .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) .collect(); - + (Vec::new(), edges) }, _ => (Vec::new(), Vec::new()), @@ -271,4 +280,4 @@ impl + Sync, S: Clone + Send + Sync + Debug> BspOp (coplanar_polygons, intersection_edges) } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/serial.rs b/src/mesh/bsp/serial.rs index 04a79ec..e6b5ee5 100644 --- a/src/mesh/bsp/serial.rs +++ b/src/mesh/bsp/serial.rs @@ -3,17 +3,26 @@ use crate::float_types::EPSILON; use crate::mesh::bsp::node::Node; use crate::mesh::bsp::traits::{BalancedSplittingStrategy, BspOps, SplittingPlaneStrategy}; -use crate::mesh::plane::{Plane, BACK, COPLANAR, FRONT, SPANNING}; +use crate::mesh::plane::{BACK, COPLANAR, FRONT, Plane, SPANNING}; use crate::mesh::polygon::Polygon; use crate::mesh::vertex::Vertex; use std::fmt::Debug; /// Serial implementation of BSP operations -pub struct SerialBspOps = BalancedSplittingStrategy, S: Clone = ()> { +pub struct SerialBspOps< + SP: SplittingPlaneStrategy = BalancedSplittingStrategy, + S: Clone = (), +> { splitting_strategy: SP, _phantom: std::marker::PhantomData, } +impl Default for SerialBspOps { + fn default() -> Self { + Self::new() + } +} + impl SerialBspOps { pub fn new() -> Self { Self { @@ -24,7 +33,7 @@ impl SerialBspOps { } impl, S: Clone> SerialBspOps { - pub fn with_strategy(strategy: SP) -> Self { + pub const fn with_strategy(strategy: SP) -> Self { Self { splitting_strategy: strategy, _phantom: std::marker::PhantomData, @@ -32,9 +41,9 @@ impl, S: Clone> SerialBspOps { } } - - -impl, S: Clone + Send + Sync + Debug> BspOps for SerialBspOps { +impl, S: Clone + Send + Sync + Debug> BspOps + for SerialBspOps +{ fn invert(&self, node: &mut Node) { // Use iterative approach with a stack let mut stack = vec![node]; @@ -108,11 +117,11 @@ impl, S: Clone + Send + Sync + Debug> BspOps fo fn clip_to(&self, node: &mut Node, bsp: &Node) { node.polygons = self.clip_polygons(bsp, &node.polygons); - + if let Some(ref mut front) = node.front { self.clip_to(front, bsp); } - + if let Some(ref mut back) = node.back { self.clip_to(back, bsp); } @@ -164,19 +173,21 @@ impl, S: Clone + Send + Sync + Debug> BspOps fo // Build child nodes using lazy initialization pattern if !front.is_empty() { - node.front - .get_or_insert_with(|| Box::new(Node::new())); + node.front.get_or_insert_with(|| Box::new(Node::new())); self.build(node.front.as_mut().unwrap(), &front); } if !back.is_empty() { - node.back - .get_or_insert_with(|| Box::new(Node::new())); + node.back.get_or_insert_with(|| Box::new(Node::new())); self.build(node.back.as_mut().unwrap(), &back); } } - fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>) { + fn slice( + &self, + node: &Node, + slicing_plane: &Plane, + ) -> (Vec>, Vec<[Vertex; 2]>) { let all_polys = self.all_polygons(node); let mut coplanar_polygons = Vec::new(); @@ -245,4 +256,4 @@ impl, S: Clone + Send + Sync + Debug> BspOps fo (coplanar_polygons, intersection_edges) } -} \ No newline at end of file +} diff --git a/src/mesh/bsp/traits.rs b/src/mesh/bsp/traits.rs index cd43793..4ca4191 100644 --- a/src/mesh/bsp/traits.rs +++ b/src/mesh/bsp/traits.rs @@ -24,7 +24,11 @@ pub trait BspOps { fn all_polygons(&self, node: &Node) -> Vec>; /// Slices this BSP node with the given plane - fn slice(&self, node: &Node, slicing_plane: &Plane) -> (Vec>, Vec<[Vertex; 2]>); + fn slice( + &self, + node: &Node, + slicing_plane: &Plane, + ) -> (Vec>, Vec<[Vertex; 2]>); } /// Trait for picking optimal splitting planes @@ -55,7 +59,7 @@ impl SplittingPlaneStrategy for BalancedSplittingStrategy { // Take a sample of polygons as candidate planes let sample_size = polygons.len().min(20); - + polygons.iter().take(sample_size).for_each(|p| { let plane = &p.plane; let (num_front, num_back, num_spanning) = polygons @@ -79,4 +83,4 @@ impl SplittingPlaneStrategy for BalancedSplittingStrategy { best_plane } -} \ No newline at end of file +} diff --git a/src/mesh/metaballs.rs b/src/mesh/metaballs.rs index 1636dd4..ef7c6bc 100644 --- a/src/mesh/metaballs.rs +++ b/src/mesh/metaballs.rs @@ -126,6 +126,7 @@ impl Mesh { } impl fast_surface_nets::ndshape::Shape<3> for GridShape { type Coord = u32; + #[inline] fn as_array(&self) -> [Self::Coord; 3] { [self.nx, self.ny, self.nz] diff --git a/src/nurbs/mod.rs b/src/nurbs/mod.rs index cd79ddd..4942a7d 100644 --- a/src/nurbs/mod.rs +++ b/src/nurbs/mod.rs @@ -1 +1 @@ -//pub mod nurbs; +// pub mod nurbs; diff --git a/src/sketch/extrudes.rs b/src/sketch/extrudes.rs index 0dc41cc..c30bb29 100644 --- a/src/sketch/extrudes.rs +++ b/src/sketch/extrudes.rs @@ -306,179 +306,177 @@ impl Sketch { Ok(Mesh::from_polygons(&polygons, bottom.metadata.clone())) } - /* - /// Perform a linear extrusion along some axis, with optional twist, center, slices, scale, etc. - /// - /// # Parameters - /// - `direction`: Direction vector for the extrusion. - /// - `twist`: Total twist in degrees around the extrusion axis from bottom to top. - /// - `segments`: Number of intermediate subdivisions. - /// - `scale`: A uniform scale factor to apply at the top slice (bottom is scale=1.0). - /// - /// # Assumptions - /// - This CSG is assumed to represent one or more 2D polygons lying in or near the XY plane. - /// - The resulting shape is extruded *initially* along +Z, then finally rotated if `v != [0,0,1]`. - /// - /// # Returns - /// A new 3D CSG. - /// - /// # Example - /// ``` - /// let shape_2d = CSG::square(2.0, None); // a 2D square in XY - /// let extruded = shape_2d.linear_extrude( - /// direction = Vector3::new(0.0, 0.0, 10.0), - /// twist = 360.0, - /// segments = 32, - /// scale = 1.2, - /// ); - /// ``` - pub fn linear_extrude( - shape: &CCShape, - direction: Vector3, - twist_degs: Real, - segments: usize, - scale_top: Real, - metadata: Option, - ) -> CSG { - let mut polygons_3d = Vec::new(); - if segments < 1 { - return CSG::new(); - } - let height = direction.norm(); - if height < EPSILON { - // no real extrusion - return CSG::new(); - } - - // Step 1) Build a series of “transforms” from bottom=0..top=height, subdivided into `segments`. - // For each i in [0..=segments], compute fraction f and: - // - scale in XY => s_i - // - twist about Z => rot_i - // - translate in Z => z_i - // - // We'll store each “slice” in 3D form as a Vec>>, - // i.e. one 3D polyline for each boundary or hole in the shape. - let mut slices: Vec>>> = Vec::with_capacity(segments + 1); - // The axis to rotate around is the unit of `direction`. We'll do final alignment after constructing them along +Z. - let axis_dir = direction.normalize(); - - for i in 0..=segments { - let f = i as Real / segments as Real; - let s_i = 1.0 + (scale_top - 1.0) * f; // lerp(1, scale_top, f) - let twist_rad = twist_degs.to_radians() * f; - let z_i = height * f; - - // Build transform T = Tz * Rz * Sxy - // - scale in XY - // - twist around Z - // - translate in Z - let mat_scale = Matrix4::new_nonuniform_scaling(&Vector3::new(s_i, s_i, 1.0)); - let mat_rot = Rotation3::from_axis_angle(&Vector3::z_axis(), twist_rad).to_homogeneous(); - let mat_trans = Translation3::new(0.0, 0.0, z_i).to_homogeneous(); - let slice_mat = mat_trans * mat_rot * mat_scale; - - let slice_3d = project_shape_3d(shape, &slice_mat); - slices.push(slice_3d); - } - - // Step 2) “Stitch” consecutive slices to form side polygons. - // For each pair of slices[i], slices[i+1], for each boundary polyline j, - // connect edges. We assume each polyline has the same vertex_count in both slices. - // (If the shape is closed, we do wrap edges [n..0].) - // Then we optionally build bottom & top caps if the polylines are closed. - - // a) bottom + top caps, similar to extrude_vector approach - // For slices[0], build a “bottom” by triangulating in XY, flipping normal. - // For slices[segments], build a “top” by normal up. - // - // But we only do it if each boundary is closed. - // We must group CCW with matching holes. This is the same logic as `extrude_vector`. - - // We'll do a small helper that triangulates shape in 2D, then lifts that triangulation to slice_3d. - // You can re‐use the logic from `extrude_vector`. - - // Build the “bottom” from slices[0] if polylines are all or partially closed - polygons_3d.extend( - build_caps_from_slice(shape, &slices[0], true, metadata.clone()) - ); - // Build the “top” from slices[segments] - polygons_3d.extend( - build_caps_from_slice(shape, &slices[segments], false, metadata.clone()) - ); - - // b) side walls - for i in 0..segments { - let bottom_slice = &slices[i]; - let top_slice = &slices[i + 1]; - - // We know bottom_slice has shape.ccw_plines.len() + shape.cw_plines.len() polylines - // in the same order. Each polyline has the same vertex_count as in top_slice. - // So we can do a direct 1:1 match: bottom_slice[j] <-> top_slice[j]. - for (pline_idx, bot3d) in bottom_slice.iter().enumerate() { - let top3d = &top_slice[pline_idx]; - if bot3d.len() < 2 { - continue; - } - // is it closed? We can check shape’s corresponding polyline - let is_closed = if pline_idx < shape.ccw_plines.len() { - shape.ccw_plines[pline_idx].polyline.is_closed() - } else { - shape.cw_plines[pline_idx - shape.ccw_plines.len()].polyline.is_closed() - }; - let n = bot3d.len(); - let edge_count = if is_closed { n } else { n - 1 }; - - for k in 0..edge_count { - let k_next = (k + 1) % n; - let b_i = bot3d[k]; - let b_j = bot3d[k_next]; - let t_i = top3d[k]; - let t_j = top3d[k_next]; - - let poly_side = Polygon::new( - vec![ - Vertex::new(b_i, Vector3::zeros()), - Vertex::new(b_j, Vector3::zeros()), - Vertex::new(t_j, Vector3::zeros()), - Vertex::new(t_i, Vector3::zeros()), - ], - metadata.clone(), - ); - polygons_3d.push(poly_side); - } - } - } - - // Step 3) If direction is not along +Z, rotate final mesh so +Z aligns with your direction - // (This is optional or can be done up front. Typical OpenSCAD style is to do everything - // along +Z, then rotate the final.) - if (axis_dir - Vector3::z()).norm() > EPSILON { - // rotate from +Z to axis_dir - let rot_axis = Vector3::z().cross(&axis_dir); - let sin_theta = rot_axis.norm(); - if sin_theta > EPSILON { - let cos_theta = Vector3::z().dot(&axis_dir); - let angle = cos_theta.acos(); - let rot = Rotation3::from_axis_angle(&Unit::new_normalize(rot_axis), angle); - let mat = rot.to_homogeneous(); - // transform the polygons - let mut final_polys = Vec::with_capacity(polygons_3d.len()); - for mut poly in polygons_3d { - for v in &mut poly.vertices { - let pos4 = mat * nalgebra::Vector4::new(v.pos.x, v.pos.y, v.pos.z, 1.0); - v.pos = Point3::new(pos4.x / pos4.w, pos4.y / pos4.w, pos4.z / pos4.w); - } - poly.set_new_normal(); - final_polys.push(poly); - } - return CSG::from_polygons(&final_polys); - } - } - - // otherwise, just return as is - CSG::from_polygons(&polygons_3d) - } - */ + // Perform a linear extrusion along some axis, with optional twist, center, slices, scale, etc. + // + // # Parameters + // - `direction`: Direction vector for the extrusion. + // - `twist`: Total twist in degrees around the extrusion axis from bottom to top. + // - `segments`: Number of intermediate subdivisions. + // - `scale`: A uniform scale factor to apply at the top slice (bottom is scale=1.0). + // + // # Assumptions + // - This CSG is assumed to represent one or more 2D polygons lying in or near the XY plane. + // - The resulting shape is extruded *initially* along +Z, then finally rotated if `v != [0,0,1]`. + // + // # Returns + // A new 3D CSG. + // + // # Example + // ``` + // let shape_2d = CSG::square(2.0, None); // a 2D square in XY + // let extruded = shape_2d.linear_extrude( + // direction = Vector3::new(0.0, 0.0, 10.0), + // twist = 360.0, + // segments = 32, + // scale = 1.2, + // ); + // ``` + // pub fn linear_extrude( + // shape: &CCShape, + // direction: Vector3, + // twist_degs: Real, + // segments: usize, + // scale_top: Real, + // metadata: Option, + // ) -> CSG { + // let mut polygons_3d = Vec::new(); + // if segments < 1 { + // return CSG::new(); + // } + // let height = direction.norm(); + // if height < EPSILON { + // no real extrusion + // return CSG::new(); + // } + // + // Step 1) Build a series of “transforms” from bottom=0..top=height, subdivided into `segments`. + // For each i in [0..=segments], compute fraction f and: + // - scale in XY => s_i + // - twist about Z => rot_i + // - translate in Z => z_i + // + // We'll store each “slice” in 3D form as a Vec>>, + // i.e. one 3D polyline for each boundary or hole in the shape. + // let mut slices: Vec>>> = Vec::with_capacity(segments + 1); + // The axis to rotate around is the unit of `direction`. We'll do final alignment after constructing them along +Z. + // let axis_dir = direction.normalize(); + // + // for i in 0..=segments { + // let f = i as Real / segments as Real; + // let s_i = 1.0 + (scale_top - 1.0) * f; // lerp(1, scale_top, f) + // let twist_rad = twist_degs.to_radians() * f; + // let z_i = height * f; + // + // Build transform T = Tz * Rz * Sxy + // - scale in XY + // - twist around Z + // - translate in Z + // let mat_scale = Matrix4::new_nonuniform_scaling(&Vector3::new(s_i, s_i, 1.0)); + // let mat_rot = Rotation3::from_axis_angle(&Vector3::z_axis(), twist_rad).to_homogeneous(); + // let mat_trans = Translation3::new(0.0, 0.0, z_i).to_homogeneous(); + // let slice_mat = mat_trans * mat_rot * mat_scale; + // + // let slice_3d = project_shape_3d(shape, &slice_mat); + // slices.push(slice_3d); + // } + // + // Step 2) “Stitch” consecutive slices to form side polygons. + // For each pair of slices[i], slices[i+1], for each boundary polyline j, + // connect edges. We assume each polyline has the same vertex_count in both slices. + // (If the shape is closed, we do wrap edges [n..0].) + // Then we optionally build bottom & top caps if the polylines are closed. + // + // a) bottom + top caps, similar to extrude_vector approach + // For slices[0], build a “bottom” by triangulating in XY, flipping normal. + // For slices[segments], build a “top” by normal up. + // + // But we only do it if each boundary is closed. + // We must group CCW with matching holes. This is the same logic as `extrude_vector`. + // + // We'll do a small helper that triangulates shape in 2D, then lifts that triangulation to slice_3d. + // You can re‐use the logic from `extrude_vector`. + // + // Build the “bottom” from slices[0] if polylines are all or partially closed + // polygons_3d.extend( + // build_caps_from_slice(shape, &slices[0], true, metadata.clone()) + // ); + // Build the “top” from slices[segments] + // polygons_3d.extend( + // build_caps_from_slice(shape, &slices[segments], false, metadata.clone()) + // ); + // + // b) side walls + // for i in 0..segments { + // let bottom_slice = &slices[i]; + // let top_slice = &slices[i + 1]; + // + // We know bottom_slice has shape.ccw_plines.len() + shape.cw_plines.len() polylines + // in the same order. Each polyline has the same vertex_count as in top_slice. + // So we can do a direct 1:1 match: bottom_slice[j] <-> top_slice[j]. + // for (pline_idx, bot3d) in bottom_slice.iter().enumerate() { + // let top3d = &top_slice[pline_idx]; + // if bot3d.len() < 2 { + // continue; + // } + // is it closed? We can check shape’s corresponding polyline + // let is_closed = if pline_idx < shape.ccw_plines.len() { + // shape.ccw_plines[pline_idx].polyline.is_closed() + // } else { + // shape.cw_plines[pline_idx - shape.ccw_plines.len()].polyline.is_closed() + // }; + // let n = bot3d.len(); + // let edge_count = if is_closed { n } else { n - 1 }; + // + // for k in 0..edge_count { + // let k_next = (k + 1) % n; + // let b_i = bot3d[k]; + // let b_j = bot3d[k_next]; + // let t_i = top3d[k]; + // let t_j = top3d[k_next]; + // + // let poly_side = Polygon::new( + // vec![ + // Vertex::new(b_i, Vector3::zeros()), + // Vertex::new(b_j, Vector3::zeros()), + // Vertex::new(t_j, Vector3::zeros()), + // Vertex::new(t_i, Vector3::zeros()), + // ], + // metadata.clone(), + // ); + // polygons_3d.push(poly_side); + // } + // } + // } + // + // Step 3) If direction is not along +Z, rotate final mesh so +Z aligns with your direction + // (This is optional or can be done up front. Typical OpenSCAD style is to do everything + // along +Z, then rotate the final.) + // if (axis_dir - Vector3::z()).norm() > EPSILON { + // rotate from +Z to axis_dir + // let rot_axis = Vector3::z().cross(&axis_dir); + // let sin_theta = rot_axis.norm(); + // if sin_theta > EPSILON { + // let cos_theta = Vector3::z().dot(&axis_dir); + // let angle = cos_theta.acos(); + // let rot = Rotation3::from_axis_angle(&Unit::new_normalize(rot_axis), angle); + // let mat = rot.to_homogeneous(); + // transform the polygons + // let mut final_polys = Vec::with_capacity(polygons_3d.len()); + // for mut poly in polygons_3d { + // for v in &mut poly.vertices { + // let pos4 = mat * nalgebra::Vector4::new(v.pos.x, v.pos.y, v.pos.z, 1.0); + // v.pos = Point3::new(pos4.x / pos4.w, pos4.y / pos4.w, pos4.z / pos4.w); + // } + // poly.set_new_normal(); + // final_polys.push(poly); + // } + // return CSG::from_polygons(&final_polys); + // } + // } + // + // otherwise, just return as is + // CSG::from_polygons(&polygons_3d) + // } /// **Mathematical Foundation: Surface of Revolution Generation** /// @@ -826,163 +824,161 @@ impl Sketch { // // # Returns // A new 3D `CSG` that is the swept volume. - /* - pub fn sweep(shape_2d: &Polygon, path_2d: &Polygon) -> CSG { - // Gather the path’s vertices in XY - if path_2d.vertices.len() < 2 { - // Degenerate path => no sweep - return CSG::new(); - } - let path_is_closed = !path_2d.open; // If false => open path, if true => closed path - - // Extract path points (x,y,0) from path_2d - let mut path_points = Vec::with_capacity(path_2d.vertices.len()); - for v in &path_2d.vertices { - // We only take X & Y; Z is typically 0 for a 2D path - path_points.push(Point3::new(v.pos.x, v.pos.y, 0.0)); - } - - // Convert the shape_2d into a list of its vertices in local coords (usually in XY). - // We assume shape_2d is a single polygon (can also handle multiple if needed). - let shape_is_closed = !shape_2d.open && shape_2d.vertices.len() >= 3; - let shape_count = shape_2d.vertices.len(); - - // For each path vertex, compute the orientation that aligns +Z to the path tangent. - // Then transform the shape’s 2D vertices into 3D “slice[i]”. - let n_path = path_points.len(); - let mut slices: Vec>> = Vec::with_capacity(n_path); - - for i in 0..n_path { - // The path tangent is p[i+1] - p[i] (or wrap if path is closed) - // If open and i == n_path-1 => we’ll copy the tangent from the last segment - let next_i = if i == n_path - 1 { - if path_is_closed { 0 } else { i - 1 } // if closed, wrap, else reuse the previous - } else { - i + 1 - }; - - let mut dir = path_points[next_i] - path_points[i]; - if dir.norm_squared() < EPSILON { - // Degenerate segment => fallback to the previous direction or just use +Z - dir = Vector3::z(); - } else { - dir.normalize_mut(); - } - - // Build a rotation that maps +Z to `dir`. - // We'll rotate the z-axis (0,0,1) onto `dir`. - let z = Vector3::z(); - let dot = z.dot(&dir); - // If dir is basically the same as z, no rotation needed - if (dot - 1.0).abs() < EPSILON { - return Matrix4::identity(); - } - // If dir is basically opposite z - if (dot + 1.0).abs() < EPSILON { - // 180 deg around X or Y axis - let rot180 = Rotation3::from_axis_angle(&Unit::new_normalize(Vector3::x()), PI); - return rot180.to_homogeneous(); - } - // Otherwise, general axis = z × dir - let axis = z.cross(&dir).normalize(); - let angle = z.dot(&dir).acos(); - let initial_rot = Rotation3::from_axis_angle(&Unit::new_unchecked(axis), angle); - let rot = initial_rot.to_homogeneous() - - // Build a translation that puts shape origin at path_points[i] - let trans = Translation3::from(path_points[i].coords); - - // Combined transform = T * R - let mat = trans.to_homogeneous() * rot; - - // Apply that transform to all shape_2d vertices => slice[i] - let mut slice_i = Vec::with_capacity(shape_count); - for sv in &shape_2d.vertices { - let local_pt = sv.pos; // (x, y, z=0) - let p4 = local_pt.to_homogeneous(); - let p4_trans = mat * p4; - slice_i.push(Point3::from_homogeneous(p4_trans).unwrap()); - } - slices.push(slice_i); - } - - // Build polygons for the new 3D swept solid. - // - (A) “Cap” polygons at start & end if path is open. - // - (B) “Side wall” quads between slice[i] and slice[i+1]. - // - // We’ll gather them all into a Vec>, then make a CSG. - - let mut all_polygons = Vec::new(); - - // Caps if path is open - // We replicate the shape_2d as polygons at slice[0] and slice[n_path-1]. - // We flip the first one so its normal faces outward. The last we keep as is. - if !path_is_closed { - // “Bottom” cap = slice[0], but we flip its winding so outward normal is “down” the path - if shape_is_closed { - let bottom_poly = polygon_from_slice( - &slices[0], - true, // flip - shape_2d.metadata.clone(), - ); - all_polygons.push(bottom_poly); - } - // “Top” cap = slice[n_path-1] (no flip) - if shape_is_closed { - let top_poly = polygon_from_slice( - &slices[n_path - 1], - false, // no flip - shape_2d.metadata.clone(), - ); - all_polygons.push(top_poly); - } - } - - // Side walls: For i in [0..n_path-1], or [0..n_path] if closed - let end_index = if path_is_closed { n_path } else { n_path - 1 }; - - for i in 0..end_index { - let i_next = (i + 1) % n_path; // wraps if closed - let slice_i = &slices[i]; - let slice_next = &slices[i_next]; - - // For each edge in the shape, connect vertices k..k+1 - // shape_2d may be open or closed. If open, we do shape_count-1 edges; if closed, shape_count edges. - let edge_count = if shape_is_closed { - shape_count // because last edge wraps - } else { - shape_count - 1 - }; - - for k in 0..edge_count { - let k_next = (k + 1) % shape_count; - - let v_i_k = slice_i[k]; - let v_i_knext = slice_i[k_next]; - let v_next_k = slice_next[k]; - let v_next_knext = slice_next[k_next]; - - // Build a quad polygon in CCW order for outward normal - // or you might choose a different ordering. Typically: - // [v_i_k, v_i_knext, v_next_knext, v_next_k] - // forms an outward-facing side wall if the shape_2d was originally CCW in XY. - let side_poly = Polygon::new( - vec![ - Vertex::new(v_i_k, Vector3::zeros()), - Vertex::new(v_i_knext, Vector3::zeros()), - Vertex::new(v_next_knext, Vector3::zeros()), - Vertex::new(v_next_k, Vector3::zeros()), - ], - shape_2d.metadata.clone(), - ); - all_polygons.push(side_poly); - } - } - - // Combine into a final CSG - CSG::from_polygons(&all_polygons) - } - */ + // pub fn sweep(shape_2d: &Polygon, path_2d: &Polygon) -> CSG { + // Gather the path’s vertices in XY + // if path_2d.vertices.len() < 2 { + // Degenerate path => no sweep + // return CSG::new(); + // } + // let path_is_closed = !path_2d.open; // If false => open path, if true => closed path + // + // Extract path points (x,y,0) from path_2d + // let mut path_points = Vec::with_capacity(path_2d.vertices.len()); + // for v in &path_2d.vertices { + // We only take X & Y; Z is typically 0 for a 2D path + // path_points.push(Point3::new(v.pos.x, v.pos.y, 0.0)); + // } + // + // Convert the shape_2d into a list of its vertices in local coords (usually in XY). + // We assume shape_2d is a single polygon (can also handle multiple if needed). + // let shape_is_closed = !shape_2d.open && shape_2d.vertices.len() >= 3; + // let shape_count = shape_2d.vertices.len(); + // + // For each path vertex, compute the orientation that aligns +Z to the path tangent. + // Then transform the shape’s 2D vertices into 3D “slice[i]”. + // let n_path = path_points.len(); + // let mut slices: Vec>> = Vec::with_capacity(n_path); + // + // for i in 0..n_path { + // The path tangent is p[i+1] - p[i] (or wrap if path is closed) + // If open and i == n_path-1 => we’ll copy the tangent from the last segment + // let next_i = if i == n_path - 1 { + // if path_is_closed { 0 } else { i - 1 } // if closed, wrap, else reuse the previous + // } else { + // i + 1 + // }; + // + // let mut dir = path_points[next_i] - path_points[i]; + // if dir.norm_squared() < EPSILON { + // Degenerate segment => fallback to the previous direction or just use +Z + // dir = Vector3::z(); + // } else { + // dir.normalize_mut(); + // } + // + // Build a rotation that maps +Z to `dir`. + // We'll rotate the z-axis (0,0,1) onto `dir`. + // let z = Vector3::z(); + // let dot = z.dot(&dir); + // If dir is basically the same as z, no rotation needed + // if (dot - 1.0).abs() < EPSILON { + // return Matrix4::identity(); + // } + // If dir is basically opposite z + // if (dot + 1.0).abs() < EPSILON { + // 180 deg around X or Y axis + // let rot180 = Rotation3::from_axis_angle(&Unit::new_normalize(Vector3::x()), PI); + // return rot180.to_homogeneous(); + // } + // Otherwise, general axis = z × dir + // let axis = z.cross(&dir).normalize(); + // let angle = z.dot(&dir).acos(); + // let initial_rot = Rotation3::from_axis_angle(&Unit::new_unchecked(axis), angle); + // let rot = initial_rot.to_homogeneous() + // + // Build a translation that puts shape origin at path_points[i] + // let trans = Translation3::from(path_points[i].coords); + // + // Combined transform = T * R + // let mat = trans.to_homogeneous() * rot; + // + // Apply that transform to all shape_2d vertices => slice[i] + // let mut slice_i = Vec::with_capacity(shape_count); + // for sv in &shape_2d.vertices { + // let local_pt = sv.pos; // (x, y, z=0) + // let p4 = local_pt.to_homogeneous(); + // let p4_trans = mat * p4; + // slice_i.push(Point3::from_homogeneous(p4_trans).unwrap()); + // } + // slices.push(slice_i); + // } + // + // Build polygons for the new 3D swept solid. + // - (A) “Cap” polygons at start & end if path is open. + // - (B) “Side wall” quads between slice[i] and slice[i+1]. + // + // We’ll gather them all into a Vec>, then make a CSG. + // + // let mut all_polygons = Vec::new(); + // + // Caps if path is open + // We replicate the shape_2d as polygons at slice[0] and slice[n_path-1]. + // We flip the first one so its normal faces outward. The last we keep as is. + // if !path_is_closed { + // “Bottom” cap = slice[0], but we flip its winding so outward normal is “down” the path + // if shape_is_closed { + // let bottom_poly = polygon_from_slice( + // &slices[0], + // true, // flip + // shape_2d.metadata.clone(), + // ); + // all_polygons.push(bottom_poly); + // } + // “Top” cap = slice[n_path-1] (no flip) + // if shape_is_closed { + // let top_poly = polygon_from_slice( + // &slices[n_path - 1], + // false, // no flip + // shape_2d.metadata.clone(), + // ); + // all_polygons.push(top_poly); + // } + // } + // + // Side walls: For i in [0..n_path-1], or [0..n_path] if closed + // let end_index = if path_is_closed { n_path } else { n_path - 1 }; + // + // for i in 0..end_index { + // let i_next = (i + 1) % n_path; // wraps if closed + // let slice_i = &slices[i]; + // let slice_next = &slices[i_next]; + // + // For each edge in the shape, connect vertices k..k+1 + // shape_2d may be open or closed. If open, we do shape_count-1 edges; if closed, shape_count edges. + // let edge_count = if shape_is_closed { + // shape_count // because last edge wraps + // } else { + // shape_count - 1 + // }; + // + // for k in 0..edge_count { + // let k_next = (k + 1) % shape_count; + // + // let v_i_k = slice_i[k]; + // let v_i_knext = slice_i[k_next]; + // let v_next_k = slice_next[k]; + // let v_next_knext = slice_next[k_next]; + // + // Build a quad polygon in CCW order for outward normal + // or you might choose a different ordering. Typically: + // [v_i_k, v_i_knext, v_next_knext, v_next_k] + // forms an outward-facing side wall if the shape_2d was originally CCW in XY. + // let side_poly = Polygon::new( + // vec![ + // Vertex::new(v_i_k, Vector3::zeros()), + // Vertex::new(v_i_knext, Vector3::zeros()), + // Vertex::new(v_next_knext, Vector3::zeros()), + // Vertex::new(v_next_k, Vector3::zeros()), + // ], + // shape_2d.metadata.clone(), + // ); + // all_polygons.push(side_poly); + // } + // } + // + // Combine into a final CSG + // CSG::from_polygons(&all_polygons) + // } } /// Helper to build a single Polygon from a “slice” of 3D points. diff --git a/src/sketch/hershey.rs b/src/sketch/hershey.rs index 564ccfc..452bbfc 100644 --- a/src/sketch/hershey.rs +++ b/src/sketch/hershey.rs @@ -21,7 +21,6 @@ impl Sketch { /// /// # Returns /// A new `Sketch` where each glyph stroke is a `Geometry::LineString` in `geometry`. - /// pub fn from_hershey( text: &str, font: &Font, diff --git a/src/tests.rs b/src/tests.rs index 36a6947..ae43e76 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1401,7 +1401,7 @@ fn test_same_number_of_vertices() { // - 3 side polygons (one for each edge of the triangle) assert_eq!( csg.polygons.len(), - 1 /*bottom*/ + 1 /*top*/ + 3 /*sides*/ + 1 /*bottom*/ + 1 /*top*/ + 3 // sides ); }