From c27a3396893eb83c16eacb04099bb77621b14c31 Mon Sep 17 00:00:00 2001 From: Anton Lazarev Date: Thu, 5 Jan 2023 12:33:17 -0800 Subject: [PATCH] support arcs in sketches --- crates/fj-kernel/src/builder/curve.rs | 16 ++++ crates/fj-kernel/src/builder/edge.rs | 54 ++++++++++++++ crates/fj-kernel/src/geometry/path.rs | 10 ++- crates/fj-math/src/circle.rs | 69 ++++++++++++++++++ crates/fj-math/src/lib.rs | 2 +- crates/fj-operations/src/sketch.rs | 101 ++++++++++++++++++++------ crates/fj/src/shape_2d.rs | 51 ++++++++++--- models/test/src/lib.rs | 40 ++++++++-- 8 files changed, 303 insertions(+), 40 deletions(-) diff --git a/crates/fj-kernel/src/builder/curve.rs b/crates/fj-kernel/src/builder/curve.rs index 9eb520bd5a..b21f95f30d 100644 --- a/crates/fj-kernel/src/builder/curve.rs +++ b/crates/fj-kernel/src/builder/curve.rs @@ -13,6 +13,13 @@ pub trait CurveBuilder { /// Update partial curve to be a circle, from the provided radius fn update_as_circle_from_radius(&mut self, radius: impl Into); + /// Update partial curve to be a circle, from the provided radius + fn update_as_circle_from_center_and_radius( + &mut self, + center: impl Into>, + radius: impl Into, + ); + /// Update partial curve to be a line, from the provided points fn update_as_line_from_points(&mut self, points: [impl Into>; 2]); } @@ -36,6 +43,15 @@ impl CurveBuilder for PartialCurve { self.path = Some(SurfacePath::circle_from_radius(radius)); } + fn update_as_circle_from_center_and_radius( + &mut self, + center: impl Into>, + radius: impl Into, + ) { + self.path = + Some(SurfacePath::circle_from_center_and_radius(center, radius)); + } + fn update_as_line_from_points(&mut self, points: [impl Into>; 2]) { let (path, _) = SurfacePath::line_from_points(points); self.path = Some(path); diff --git a/crates/fj-kernel/src/builder/edge.rs b/crates/fj-kernel/src/builder/edge.rs index 4736d3e212..95163aee30 100644 --- a/crates/fj-kernel/src/builder/edge.rs +++ b/crates/fj-kernel/src/builder/edge.rs @@ -23,6 +23,10 @@ pub trait HalfEdgeBuilder { /// Update partial half-edge to be a circle, from the given radius fn update_as_circle_from_radius(&mut self, radius: impl Into); + /// Update partial half-edge to be an arc, spanning the given angle in + /// radians + fn update_as_arc(&mut self, angle_rad: impl Into); + /// Update partial half-edge to be a line segment, from the given points fn update_as_line_segment_from_points( &mut self, @@ -79,6 +83,56 @@ impl HalfEdgeBuilder for PartialHalfEdge { self.infer_global_form(); } + fn update_as_arc(&mut self, angle_rad: impl Into) { + let angle_rad = angle_rad.into(); + if angle_rad <= -Scalar::TAU || angle_rad >= Scalar::TAU { + panic!("arc angle must be in the range (-360, 360)"); + } + let points_surface = self.vertices.each_ref_ext().map(|vertex| { + vertex + .read() + .surface_form + .read() + .position + .expect("Can't infer arc without surface position") + }); + + let arc_circle_data = fj_math::ArcCircleData::from_endpoints_and_angle( + points_surface[0], + points_surface[1], + angle_rad, + ); + + let mut curve = self.curve(); + curve.write().update_as_circle_from_center_and_radius( + arc_circle_data.center, + arc_circle_data.radius, + ); + + let path = curve + .read() + .path + .expect("Expected path that was just created"); + + let [a_curve, b_curve] = if arc_circle_data.flipped_construction { + [arc_circle_data.end_angle, arc_circle_data.start_angle] + } else { + [arc_circle_data.start_angle, arc_circle_data.end_angle] + } + .map(|coord| Point::from([coord])); + + for (vertex, point_curve) in + self.vertices.each_mut_ext().zip_ext([a_curve, b_curve]) + { + let mut vertex = vertex.write(); + vertex.position = Some(point_curve); + vertex.surface_form.write().position = + Some(path.point_from_path_coords(point_curve)); + } + + self.infer_global_form(); + } + fn update_as_line_segment_from_points( &mut self, surface: impl Into>, diff --git a/crates/fj-kernel/src/geometry/path.rs b/crates/fj-kernel/src/geometry/path.rs index d1cc59980c..8641589fe6 100644 --- a/crates/fj-kernel/src/geometry/path.rs +++ b/crates/fj-kernel/src/geometry/path.rs @@ -37,9 +37,17 @@ pub enum SurfacePath { impl SurfacePath { /// Build a circle from the given radius pub fn circle_from_radius(radius: impl Into) -> Self { + Self::circle_from_center_and_radius(Point::origin(), radius) + } + + /// Build a circle from the given radius + pub fn circle_from_center_and_radius( + center: impl Into>, + radius: impl Into, + ) -> Self { let radius = radius.into(); - Self::Circle(Circle::from_center_and_radius(Point::origin(), radius)) + Self::Circle(Circle::from_center_and_radius(center.into(), radius)) } /// Construct a line from two points diff --git a/crates/fj-math/src/circle.rs b/crates/fj-math/src/circle.rs index 303e8fdd0a..743cd8aa03 100644 --- a/crates/fj-math/src/circle.rs +++ b/crates/fj-math/src/circle.rs @@ -167,6 +167,75 @@ impl approx::AbsDiffEq for Circle { } } +/// Calculated geometry that is useful when dealing with an arc +pub struct ArcCircleData { + /// Start point of the arc + pub start: Point<2>, + /// End point of the arc + pub end: Point<2>, + /// Center of the circle the arc is constructed on + pub center: Point<2>, + /// Radius of the circle the arc is constructed on + pub radius: Scalar, + /// Angle of `start` relative to `center`, in radians + /// + /// Guaranteed to be less than `end_angle`. + pub start_angle: Scalar, + /// Angle of `end` relative to `center`, in radians + /// + /// Guaranteed to be greater than `end_angle`. + pub end_angle: Scalar, + /// True if `start` and `end` were switched to ensure `end_angle` > `start_angle` + pub flipped_construction: bool, +} + +impl ArcCircleData { + /// Constructs an [`ArcCircleData`] from two endpoints and the associated angle. + pub fn from_endpoints_and_angle( + p0: impl Into>, + p1: impl Into>, + angle: Scalar, + ) -> Self { + use num_traits::Float; + + let (p0, p1) = (p0.into(), p1.into()); + + let flipped_construction = angle <= Scalar::ZERO; + let angle_rad = angle.abs(); + + let [p0, p1] = if flipped_construction { + [p1, p0] + } else { + [p0, p1] + }; + let [[x0, y0], [x1, y1]] = [p0, p1].map(|p| p.coords.components); + // https://math.stackexchange.com/questions/27535/how-to-find-center-of-an-arc-given-start-point-end-point-radius-and-arc-direc + // distance between endpoints + let d = ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt(); + // radius + let r = d / (2. * (angle_rad.into_f64() / 2.).sin()); + // distance from center to midpoint between endpoints + let h = (r.powi(2) - (d.powi(2) / 4.)).sqrt(); + // (u, v) is the unit normal in the direction of p1 - p0 + let u = (x1 - x0) / d; + let v = (y1 - y0) / d; + // (cx, cy) is the center of the circle + let cx = ((x0 + x1) / 2.) - h * v; + let cy = ((y0 + y1) / 2.) + h * u; + let start_angle = (y0 - cy).atan2(x0 - cx); + let end_angle = (y1 - cy).atan2(x1 - cx); + Self { + start: p0, + end: p1, + center: Point::from([cx, cy]), + radius: r, + start_angle, + end_angle, + flipped_construction, + } + } +} + #[cfg(test)] mod tests { use std::f64::consts::{FRAC_PI_2, PI}; diff --git a/crates/fj-math/src/lib.rs b/crates/fj-math/src/lib.rs index e0c160134d..ab22d4bc03 100644 --- a/crates/fj-math/src/lib.rs +++ b/crates/fj-math/src/lib.rs @@ -52,7 +52,7 @@ mod vector; pub use self::{ aabb::Aabb, - circle::Circle, + circle::{ArcCircleData, Circle}, coordinates::{Uv, Xyz, T}, line::Line, plane::Plane, diff --git a/crates/fj-operations/src/sketch.rs b/crates/fj-operations/src/sketch.rs index 4cd863d8ef..eec6df398e 100644 --- a/crates/fj-operations/src/sketch.rs +++ b/crates/fj-operations/src/sketch.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use fj_interop::{debug::DebugInfo, mesh::Color}; use fj_kernel::{ - builder::{FaceBuilder, HalfEdgeBuilder}, + builder::{CycleBuilder, HalfEdgeBuilder}, insert::Insert, objects::{Objects, Sketch}, partial::{ @@ -56,18 +56,55 @@ impl Shape for fj::Sketch { } } fj::Chain::PolyChain(poly_chain) => { - let points = poly_chain - .to_segments() - .into_iter() - .map(|fj::SketchSegment::LineTo { point }| point) - .map(Point::from); - - let mut face = PartialFace::default(); - face.exterior.write().surface = Partial::from(surface); - face.update_exterior_as_polygon_from_points(points); - face.color = Some(Color(self.color())); - - face + let segments = poly_chain.to_segments(); + assert!( + segments.len() > 0, + "Attempted to compute a Brep from an empty sketch" + ); + + let exterior = { + let mut cycle = PartialCycle::default(); + cycle.surface = Partial::from(surface); + let mut line_segments = vec![]; + let mut arcs = vec![]; + poly_chain.to_segments().into_iter().for_each( + |fj::SketchSegment { endpoint, route }| { + let endpoint = Point::from(endpoint); + match route { + fj::SketchSegmentRoute::Direct => { + line_segments.push( + cycle + .add_half_edge_from_point_to_start( + endpoint, + ), + ); + } + fj::SketchSegmentRoute::Arc { angle } => { + arcs.push(( + cycle + .add_half_edge_from_point_to_start( + endpoint, + ), + angle, + )); + } + } + }, + ); + line_segments.into_iter().for_each(|mut half_edge| { + half_edge.write().update_as_line_segment() + }); + arcs.into_iter().for_each(|(mut half_edge, angle)| { + half_edge.write().update_as_arc(angle.rad()) + }); + Partial::from_partial(cycle) + }; + + PartialFace { + exterior, + color: Some(Color(self.color())), + ..Default::default() + } } }; @@ -85,14 +122,36 @@ impl Shape for fj::Sketch { min: Point::from([-circle.radius(), -circle.radius(), 0.0]), max: Point::from([circle.radius(), circle.radius(), 0.0]), }, - fj::Chain::PolyChain(poly_chain) => Aabb::<3>::from_points( - poly_chain - .to_segments() - .into_iter() - .map(|fj::SketchSegment::LineTo { point }| point) - .map(Point::from) - .map(Point::to_xyz), - ), + fj::Chain::PolyChain(poly_chain) => { + let segments = poly_chain.to_segments(); + assert!( + segments.len() > 0, + "Attempted to compute a bounding box from an empty sketch" + ); + + let mut points = vec![]; + + let mut start_point = segments[segments.len() - 1].endpoint; + segments.iter().for_each(|segment| { + match segment.route { + fj::SketchSegmentRoute::Direct => (), + fj::SketchSegmentRoute::Arc { angle } => { + use std::f64::consts::PI; + let arc_circle_data = fj_math::ArcCircleData::from_endpoints_and_angle(start_point, segment.endpoint, fj_math::Scalar::from_f64(angle.rad())); + for circle_minmax_angle in [0., PI/2., PI, 3.*PI/2.] { + let mm_angle = fj_math::Scalar::from_f64(circle_minmax_angle); + if arc_circle_data.start_angle < mm_angle && mm_angle < arc_circle_data.end_angle { + points.push(arc_circle_data.center + [arc_circle_data.radius * circle_minmax_angle.cos(), arc_circle_data.radius * circle_minmax_angle.sin()]); + } + } + }, + } + points.push(Point::from(segment.endpoint)); + start_point = segment.endpoint; + }); + + Aabb::<3>::from_points(points.into_iter().map(Point::to_xyz)) + } } } } diff --git a/crates/fj/src/shape_2d.rs b/crates/fj/src/shape_2d.rs index b1b3b5649b..cade8b933f 100644 --- a/crates/fj/src/shape_2d.rs +++ b/crates/fj/src/shape_2d.rs @@ -1,4 +1,4 @@ -use crate::{abi::ffi_safe, Shape}; +use crate::{abi::ffi_safe, Angle, Shape}; /// A 2-dimensional shape #[derive(Clone, Debug, PartialEq)] @@ -101,7 +101,15 @@ pub struct Sketch { } impl Sketch { - /// Create a sketch from a bunch of points + /// Create a sketch made of sketch segments + pub fn from_segments(segments: Vec) -> Self { + Self { + chain: Chain::PolyChain(PolyChain::from_segments(segments)), + color: [255, 0, 0, 255], + } + } + + /// Create a sketch made of straight lines from a bunch of points pub fn from_points(points: Vec<[f64; 2]>) -> Self { Self { chain: Chain::PolyChain(PolyChain::from_points(points)), @@ -188,13 +196,23 @@ pub struct PolyChain { } impl PolyChain { + /// Construct an instance from a list of segments + pub fn from_segments(segments: Vec) -> Self { + Self { + segments: segments.into(), + } + } + /// Construct an instance from a list of points pub fn from_points(points: Vec<[f64; 2]>) -> Self { - let points = points + let segments = points .into_iter() - .map(|point| SketchSegment::LineTo { point }) + .map(|endpoint| SketchSegment { + endpoint, + route: SketchSegmentRoute::Direct, + }) .collect(); - Self { segments: points } + Self::from_segments(segments) } /// Return the points that define the polygonal chain @@ -209,10 +227,23 @@ impl PolyChain { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] -pub enum SketchSegment { - /// A line to a point - LineTo { - /// The destination point of the line - point: [f64; 2], +pub struct SketchSegment { + /// The destination point of the segment + pub endpoint: [f64; 2], + /// The path taken by the segment to get to the endpoint + pub route: SketchSegmentRoute, +} + +/// Possible paths that a [`SketchSegment`] can take to the next endpoint +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[repr(C)] +pub enum SketchSegmentRoute { + /// A straight line to the endpoint + Direct, + /// An arc to the endpoint with a given angle + Arc { + /// The angle of the arc + angle: Angle, }, } diff --git a/models/test/src/lib.rs b/models/test/src/lib.rs index 76f955cf66..842adc3c0d 100644 --- a/models/test/src/lib.rs +++ b/models/test/src/lib.rs @@ -4,8 +4,8 @@ use fj::{syntax::*, Angle}; #[fj::model] pub fn model() -> fj::Shape { - let a = star(4, 1., [0, 255, 0, 200]); - let b = star(5, -1., [255, 0, 0, 255]) + let a = star(4, 1., [0, 255, 0, 200], Some(-30.)); + let b = star(5, -1., [255, 0, 0, 255], None) .rotate([1., 1., 1.], Angle::from_deg(45.)) .translate([3., 3., 1.]); let c = spacer().translate([6., 6., 1.]); @@ -15,7 +15,12 @@ pub fn model() -> fj::Shape { group.into() } -fn star(num_points: u64, height: f64, color: [u8; 4]) -> fj::Shape { +fn star( + num_points: u64, + height: f64, + color: [u8; 4], + arm_angle: Option, +) -> fj::Shape { let r1 = 1.; let r2 = 2.; @@ -39,12 +44,33 @@ fn star(num_points: u64, height: f64, color: [u8; 4]) -> fj::Shape { let x = cos * radius; let y = sin * radius; - outer.push([x, y]); - inner.push([x / 2., y / 2.]); + if let Some(angle) = arm_angle { + outer.push(fj::SketchSegment { + endpoint: [x, y], + route: fj::SketchSegmentRoute::Arc { + angle: fj::Angle::from_deg(angle), + }, + }); + inner.push(fj::SketchSegment { + endpoint: [x / 2., y / 2.], + route: fj::SketchSegmentRoute::Arc { + angle: fj::Angle::from_deg(-angle), + }, + }); + } else { + outer.push(fj::SketchSegment { + endpoint: [x, y], + route: fj::SketchSegmentRoute::Direct, + }); + inner.push(fj::SketchSegment { + endpoint: [x / 2., y / 2.], + route: fj::SketchSegmentRoute::Direct, + }); + } } - let outer = fj::Sketch::from_points(outer).with_color(color); - let inner = fj::Sketch::from_points(inner); + let outer = fj::Sketch::from_segments(outer).with_color(color); + let inner = fj::Sketch::from_segments(inner); let footprint = fj::Difference2d::from_shapes([outer.into(), inner.into()]);