diff --git a/src/mesh/flatten_slice.rs b/src/mesh/flatten_slice.rs index 52b903a..0e8119f 100644 --- a/src/mesh/flatten_slice.rs +++ b/src/mesh/flatten_slice.rs @@ -69,6 +69,8 @@ impl Mesh { geometry: new_gc, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: Sketch::::prepare_origin_vec_and_quat(self.origin), } } @@ -154,6 +156,8 @@ impl Mesh { geometry: new_gc, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: Sketch::::prepare_origin_vec_and_quat(self.origin), } } } diff --git a/src/mesh/mod.rs b/src/mesh/mod.rs index 9db7e2b..5b4c410 100644 --- a/src/mesh/mod.rs +++ b/src/mesh/mod.rs @@ -57,6 +57,9 @@ pub struct Mesh { /// Metadata pub metadata: Option, + + /// Origin + pub origin: Vertex, } impl Mesh { @@ -80,6 +83,7 @@ impl Mesh { polygons: polys, bounding_box: std::sync::OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, } } } @@ -450,6 +454,7 @@ impl CSG for Mesh { polygons: Vec::new(), bounding_box: OnceLock::new(), metadata: None, + origin: Default::default(), } } @@ -492,6 +497,7 @@ impl CSG for Mesh { polygons: final_polys, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, } } @@ -546,6 +552,7 @@ impl CSG for Mesh { polygons: final_polys, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, } } @@ -578,6 +585,7 @@ impl CSG for Mesh { polygons: a.all_polygons(), bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, } } @@ -791,6 +799,7 @@ impl From> for Mesh { polygons: final_polygons, bounding_box: OnceLock::new(), metadata: None, + origin: sketch.origin, } } } diff --git a/src/mesh/vertex.rs b/src/mesh/vertex.rs index 22eb07e..1332808 100644 --- a/src/mesh/vertex.rs +++ b/src/mesh/vertex.rs @@ -11,6 +11,13 @@ pub struct Vertex { pub normal: Vector3, } +/// Default to a position of (0, 0, 0) facing +Z +impl Default for Vertex { + fn default() -> Self { + Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0)) + } +} + impl Vertex { /// Create a new [`Vertex`]. /// diff --git a/src/sketch/extrudes.rs b/src/sketch/extrudes.rs index 304a74b..894beb1 100644 --- a/src/sketch/extrudes.rs +++ b/src/sketch/extrudes.rs @@ -1,11 +1,12 @@ //! Functions to extrude, revolve, loft, and otherwise transform 2D `Sketch`s into 3D `Mesh`s +use crate::apply_origin_transform; use crate::errors::ValidationError; use crate::float_types::{Real, tolerance}; use crate::mesh::Mesh; use crate::mesh::polygon::Polygon; use crate::mesh::vertex::Vertex; -use crate::sketch::Sketch; +use crate::sketch::{OriginTransformVecQuat, Sketch}; use crate::traits::CSG; use geo::{Area, CoordsIter, LineString, Polygon as GeoPolygon}; use nalgebra::{Point3, Vector3}; @@ -13,6 +14,7 @@ use std::fmt::Debug; use std::sync::OnceLock; impl Sketch { + #[inline] /// Linearly extrude this (2D) shape in the +Z direction by `height`. /// /// This is just a convenience wrapper around extrude_vector using Vector3::new(0.0, 0.0, height) @@ -89,17 +91,25 @@ impl Sketch { return Mesh::new(); } + // !TODO! Find a way to avoid re-allocating so much // Collect 3-D polygons generated from every `geo` geometry in the sketch let mut out: Vec> = Vec::new(); for geom in &self.geometry { - Self::extrude_geometry(geom, direction, &self.metadata, &mut out); + Self::extrude_geometry( + geom, + direction, + self.origin_transform, + &self.metadata, + &mut out, + ); } Mesh { polygons: out, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, } } @@ -107,6 +117,7 @@ impl Sketch { fn extrude_geometry( geom: &geo::Geometry, direction: Vector3, + origin_transform: OriginTransformVecQuat, metadata: &Option, out_polygons: &mut Vec>, ) { @@ -127,9 +138,18 @@ impl Sketch { // bottom for tri in &tris { - let v0 = Vertex::new(tri[2], -Vector3::z()); - let v1 = Vertex::new(tri[1], -Vector3::z()); - let v2 = Vertex::new(tri[0], -Vector3::z()); + let v0 = apply_origin_transform!( + Vertex::new(tri[2], -Vector3::z()), + origin_transform + ); + let v1 = apply_origin_transform!( + Vertex::new(tri[1], -Vector3::z()), + origin_transform + ); + let v2 = apply_origin_transform!( + Vertex::new(tri[0], -Vector3::z()), + origin_transform + ); out_polygons.push(Polygon::new(vec![v0, v1, v2], metadata.clone())); } // top @@ -137,9 +157,18 @@ impl Sketch { let p0 = tri[0] + direction; let p1 = tri[1] + direction; let p2 = tri[2] + direction; - let v0 = Vertex::new(p0, Vector3::z()); - let v1 = Vertex::new(p1, Vector3::z()); - let v2 = Vertex::new(p2, Vector3::z()); + let v0 = apply_origin_transform!( + Vertex::new(p0, Vector3::z()), + origin_transform + ); + let v1 = apply_origin_transform!( + Vertex::new(p1, Vector3::z()), + origin_transform + ); + let v2 = apply_origin_transform!( + Vertex::new(p2, Vector3::z()), + origin_transform + ); out_polygons.push(Polygon::new(vec![v0, v1, v2], metadata.clone())); } @@ -156,10 +185,22 @@ impl Sketch { let t_j = b_j + direction; out_polygons.push(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()), + apply_origin_transform!( + Vertex::new(b_i, Vector3::zeros()), + origin_transform + ), + apply_origin_transform!( + Vertex::new(b_j, Vector3::zeros()), + origin_transform + ), + apply_origin_transform!( + Vertex::new(t_j, Vector3::zeros()), + origin_transform + ), + apply_origin_transform!( + Vertex::new(t_i, Vector3::zeros()), + origin_transform + ), ], metadata.clone(), )); @@ -171,6 +212,7 @@ impl Sketch { Self::extrude_geometry( &geo::Geometry::Polygon(poly.clone()), direction, + origin_transform, metadata, out_polygons, ); @@ -178,7 +220,13 @@ impl Sketch { }, geo::Geometry::GeometryCollection(gc) => { for sub in &gc.0 { - Self::extrude_geometry(sub, direction, metadata, out_polygons); + Self::extrude_geometry( + sub, + direction, + origin_transform, + metadata, + out_polygons, + ); } }, geo::Geometry::LineString(ls) => { @@ -195,10 +243,22 @@ impl Sketch { let normal = (b_j - b_i).cross(&(t_i - b_i)).normalize(); out_polygons.push(Polygon::new( vec![ - Vertex::new(b_i, normal), - Vertex::new(b_j, normal), - Vertex::new(t_j, normal), - Vertex::new(t_i, normal), + apply_origin_transform!( + Vertex::new(b_i, normal), + origin_transform + ), + apply_origin_transform!( + Vertex::new(b_j, normal), + origin_transform + ), + apply_origin_transform!( + Vertex::new(t_j, normal), + origin_transform + ), + apply_origin_transform!( + Vertex::new(t_i, normal), + origin_transform + ), ], metadata.clone(), )); @@ -215,10 +275,10 @@ impl Sketch { let normal = (b1 - b0).cross(&(t0 - b0)).normalize(); out_polygons.push(Polygon::new( vec![ - Vertex::new(b0, normal), - Vertex::new(b1, normal), - Vertex::new(t1, normal), - Vertex::new(t0, normal), + apply_origin_transform!(Vertex::new(b0, normal), origin_transform), + apply_origin_transform!(Vertex::new(b1, normal), origin_transform), + apply_origin_transform!(Vertex::new(t1, normal), origin_transform), + apply_origin_transform!(Vertex::new(t0, normal), origin_transform), ], metadata.clone(), )); @@ -230,6 +290,7 @@ impl Sketch { Self::extrude_geometry( &geo::Geometry::Polygon(poly2d), direction, + origin_transform, metadata, out_polygons, ); @@ -241,6 +302,7 @@ impl Sketch { Self::extrude_geometry( &geo::Geometry::Polygon(poly2d), direction, + origin_transform, metadata, out_polygons, ); @@ -810,6 +872,7 @@ impl Sketch { polygons: new_polygons, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, }) } diff --git a/src/sketch/hershey.rs b/src/sketch/hershey.rs index 564ccfc..3b51df3 100644 --- a/src/sketch/hershey.rs +++ b/src/sketch/hershey.rs @@ -68,6 +68,8 @@ impl Sketch { geometry: geo_coll, bounding_box: OnceLock::new(), metadata, + origin: Default::default(), + origin_transform: Sketch::::prepare_origin_vec_and_quat(Default::default()), } } } diff --git a/src/sketch/mod.rs b/src/sketch/mod.rs index 4788ae4..e3abd17 100644 --- a/src/sketch/mod.rs +++ b/src/sketch/mod.rs @@ -3,14 +3,16 @@ use crate::float_types::Real; use crate::float_types::parry3d::bounding_volume::Aabb; use crate::mesh::Mesh; +use crate::mesh::vertex::Vertex; use crate::traits::CSG; +use geo::Line; use geo::algorithm::winding_order::Winding; use geo::{ AffineOps, AffineTransform, BooleanOps as GeoBooleanOps, BoundingRect, Coord, CoordsIter, Geometry, GeometryCollection, LineString, MultiPolygon, Orient, Polygon as GeoPolygon, Rect, orient::Direction, }; -use nalgebra::{Matrix4, Point3, partial_max, partial_min}; +use nalgebra::{Matrix4, Point3, UnitQuaternion, Vector3, partial_max, partial_min}; use std::fmt::Debug; use std::sync::OnceLock; @@ -32,6 +34,84 @@ pub mod offset; #[cfg(feature = "truetype-text")] pub mod truetype; +/// Position transform, rotation quaternion +pub(crate) type OriginTransformVecQuat = (Vector3, UnitQuaternion); + +#[macro_export] +macro_rules! apply_origin_transform { + ($in_vertex:expr, $origin_transform:expr) => {{ + let (pos_transform, rotation_quats) = $origin_transform; + let quat = rotation_quats; + + let mut out_vertex = Vertex::default(); + + out_vertex.pos.coords = quat * $in_vertex.pos.coords; + out_vertex.normal = quat * $in_vertex.normal; + + out_vertex.pos.coords += pos_transform; + + out_vertex + }}; +} + +macro_rules! apply_origin_transform_graphicpoint { + ($in_point:expr, $origin_transform:expr) => {{ + let (pos_transform, rotation_quats) = $origin_transform; + let quat = rotation_quats; + + let mut out_point = Point3::new(0.0, 0.0, 0.0); + + out_point.coords = quat * $in_point.coords; + + out_point.coords += pos_transform; + + out_point + }}; +} + +#[derive(Debug, Clone)] +/// String of line points for rendering in 3D space +pub struct GraphicLineString { + pub points: Vec<[f32; 3]>, +} + +impl GraphicLineString { + /// Get the total number of individual lines that can be made with the + /// line string + pub fn line_count(&self) -> usize { + let point_count = self.points.len(); + + if point_count == 0 { + return 0; + } + + let line_count = point_count - 1; + + line_count + } +} + +#[derive(Debug, Clone)] +/// Collection of independent graphic line strings +pub struct GraphicLineStrings { + pub line_strings: Vec, +} + +impl GraphicLineStrings { + /// Get the total number of individual lines that can be made with all of + /// the line strings. Iterates through each line string stored so can get + /// expensive + pub fn total_line_count(&self) -> usize { + let mut total_line_count = 0; + + for line_string in &self.line_strings { + total_line_count += line_string.line_count(); + } + + total_line_count + } +} + #[derive(Clone, Debug)] pub struct Sketch { /// 2D points, lines, polylines, polygons, and multipolygons @@ -42,6 +122,13 @@ pub struct Sketch { /// Metadata pub metadata: Option, + + /// Origin of the sketch in 3D space. + /// Not public due to needing to update the origin transform each time + /// the origin is set. + pub(crate) origin: Vertex, + + pub(crate) origin_transform: OriginTransformVecQuat, } impl Sketch { @@ -209,8 +296,172 @@ impl Sketch { geometry: GeometryCollection(oriented_geoms), bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } + + #[inline] + /// Set the origin of the sketch. + pub fn set_origin(&mut self, origin: Vertex) { + self.origin_transform = Self::prepare_origin_vec_and_quat(origin); + self.origin = origin; + } + + #[inline] + /// Specifies the origin during creation of a Sketch. + pub fn origin(mut self, origin: Vertex) -> Self { + self.set_origin(origin); + + self + } + + /// Prepares the vector and quaternion needed to move all the points of + /// a mesh generated by this sketch to the origin + pub(crate) fn prepare_origin_vec_and_quat(origin: Vertex) -> OriginTransformVecQuat { + let default_origin = Vertex::default(); + + let pos_transform = origin.pos - default_origin.pos; + + let rotation_quat = + match UnitQuaternion::rotation_between(&default_origin.normal, &origin.normal) { + Some(quat) => quat, + None => { + // Assumes the default normal is +Z. We'll do +Y + let inter_origin = Vector3::new(0.0, 1.0, 0.0); + + let quat1 = UnitQuaternion::rotation_between( + &default_origin.normal, + &inter_origin, + ) + .unwrap(); + let quat2 = + UnitQuaternion::rotation_between(&inter_origin, &origin.normal) + .unwrap(); + + quat2 * quat1 + }, + }; + + (pos_transform, rotation_quat) + } + + /// Create graphic line strings from the edges of the sketch + pub fn build_graphic_line_strings(&self) -> GraphicLineStrings { + let mut graphic_line_strings = Vec::new(); + for geometry in &self.geometry { + match geometry { + Geometry::Polygon(polygon) => { + graphic_line_strings + .push(self.line_string_to_graphic_line_string(polygon.exterior())); + }, + Geometry::Line(line) => { + let start_point = + Point3::new(line.start.x as f32, line.start.y as f32, 0.0); + let end_point = Point3::new(line.end.x as f32, line.end.y as f32, 0.0); + let graphic_line_string = GraphicLineString { + points: vec![ + start_point.coords.cast::().into(), + end_point.coords.cast::().into(), + ], + }; + + graphic_line_strings.push(graphic_line_string) + }, + Geometry::LineString(line_string) => { + graphic_line_strings + .push(self.line_string_to_graphic_line_string(&line_string)); + }, + Geometry::MultiLineString(line_strings) => { + for line_string in line_strings.into_iter() { + graphic_line_strings + .push(self.line_string_to_graphic_line_string(line_string)); + } + }, + Geometry::MultiPolygon(polygons) => { + for polygon in polygons.into_iter() { + graphic_line_strings + .push(self.line_string_to_graphic_line_string(polygon.exterior())); + } + }, + Geometry::Rect(rect) => { + let line_string = Self::lines_to_line_string(&rect.to_lines()); + + graphic_line_strings + .push(self.line_string_to_graphic_line_string(&line_string)); + }, + Geometry::Triangle(triangle) => { + let line_string = Self::lines_to_line_string(&triangle.to_lines()); + + graphic_line_strings + .push(self.line_string_to_graphic_line_string(&line_string)); + }, + Geometry::GeometryCollection(geometry_collection) => { + let child_sketch = Self { + geometry: geometry_collection.clone(), + bounding_box: self.bounding_box.clone(), + metadata: self.metadata.clone(), + origin: self.origin.clone(), + origin_transform: self.origin_transform.clone(), + }; + + let line_strings = child_sketch.build_graphic_line_strings(); + + graphic_line_strings.extend_from_slice(&line_strings.line_strings); + }, + // Don't render points + _ => {}, + } + } + + GraphicLineStrings { + line_strings: graphic_line_strings, + } + } + + fn lines_to_line_string(lines: &[Line]) -> LineString { + let mut coord_vec = Vec::with_capacity(lines.len() + 1); + let mut lines_iter = lines.into_iter(); + + let first_line = match lines_iter.next() { + Some(line) => line, + None => { + return LineString::empty(); + }, + }; + + coord_vec.push(first_line.start); + coord_vec.push(first_line.end); + + for line in lines_iter { + coord_vec.push(line.end); + } + + LineString::new(coord_vec) + } + + fn line_string_to_graphic_line_string( + &self, + line_string: &LineString, + ) -> GraphicLineString { + let points_vec = line_string + .coords() + .map(|coord| { + let point = Point3::new(coord.x, coord.y, 0.0); + + let point_transformed = + apply_origin_transform_graphicpoint!(point, self.origin_transform); + + let graphics_point: [f32; 3] = point_transformed.coords.cast::().into(); + + graphics_point + }) + .collect(); + + let graphic_line_string = GraphicLineString { points: points_vec }; + + graphic_line_string + } } impl CSG for Sketch { @@ -220,6 +471,8 @@ impl CSG for Sketch { geometry: GeometryCollection::default(), bounding_box: OnceLock::new(), metadata: None, + origin: Default::default(), + origin_transform: Self::prepare_origin_vec_and_quat(Vertex::default()), } } @@ -271,6 +524,8 @@ impl CSG for Sketch { geometry: final_gc, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } @@ -312,6 +567,8 @@ impl CSG for Sketch { geometry: final_gc, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } @@ -359,6 +616,8 @@ impl CSG for Sketch { geometry: final_gc, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } @@ -406,6 +665,8 @@ impl CSG for Sketch { geometry: final_gc, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } @@ -538,6 +799,8 @@ impl CSG for Sketch { geometry: GeometryCollection(oriented_geoms), bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } } @@ -594,6 +857,8 @@ impl From> for Sketch { geometry: new_gc, bounding_box: OnceLock::new(), metadata: None, + origin: mesh.origin, + origin_transform: Self::prepare_origin_vec_and_quat(mesh.origin), } } } diff --git a/src/sketch/offset.rs b/src/sketch/offset.rs index a649025..15de164 100644 --- a/src/sketch/offset.rs +++ b/src/sketch/offset.rs @@ -215,6 +215,8 @@ impl Sketch { geometry: new_collection, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } @@ -298,6 +300,8 @@ impl Sketch { geometry: new_collection, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } @@ -336,6 +340,8 @@ impl Sketch { geometry: new_collection, bounding_box: OnceLock::new(), metadata: self.metadata.clone(), + origin: self.origin, + origin_transform: self.origin_transform, } } } diff --git a/src/sketch/shapes.rs b/src/sketch/shapes.rs index c590fb7..2014ca2 100644 --- a/src/sketch/shapes.rs +++ b/src/sketch/shapes.rs @@ -605,6 +605,8 @@ impl Sketch { geometry: shape.geometry, bounding_box: OnceLock::new(), metadata, + origin: Default::default(), + origin_transform: Sketch::::prepare_origin_vec_and_quat(Default::default()), } }