diff --git a/Cargo.lock b/Cargo.lock index 22ba3b22d..42fe7a48b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -908,7 +908,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ef843c6b151344d0fe03080ec80bcd975560a937#ef843c6b151344d0fe03080ec80bcd975560a937" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1d81d7f9470b843feee06fb9136997d4a69ac553#1d81d7f9470b843feee06fb9136997d4a69ac553" dependencies = [ "acadrust", "cavalier_contours", @@ -919,7 +919,7 @@ dependencies = [ [[package]] name = "cadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ef843c6b151344d0fe03080ec80bcd975560a937#ef843c6b151344d0fe03080ec80bcd975560a937" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1d81d7f9470b843feee06fb9136997d4a69ac553#1d81d7f9470b843feee06fb9136997d4a69ac553" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 871ae54a7..6e8db013f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,8 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ef843c6b151344d0fe03080ec80bcd975560a937", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ef843c6b151344d0fe03080ec80bcd975560a937" } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1d81d7f9470b843feee06fb9136997d4a69ac553", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1d81d7f9470b843feee06fb9136997d4a69ac553" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/app/properties.rs b/src/app/properties.rs index 1006a1c40..7096e7b03 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -292,15 +292,15 @@ impl OpenCADStudio { let mut plot_table_options = vec!["None".to_string()]; plot_table_options.extend(crate::io::plot_style::available_ctb_names()); let visual_style = match tab.visual_style.as_str() { - "Wireframe 2D" => t!("2D Wireframe").into_owned(), - "Wireframe 3D" => t!("3D Wireframe").into_owned(), + "Wireframe 2D" => "2D Wireframe".to_string(), + "Wireframe 3D" => "3D Wireframe".to_string(), value => value.to_string(), }; let visual_style_options = crate::modules::view::visual_style::VISUAL_STYLES .iter() .map(|style| match style.label { - "Wireframe 2D" => t!("2D Wireframe").into_owned(), - "Wireframe 3D" => t!("3D Wireframe").into_owned(), + "Wireframe 2D" => "2D Wireframe".to_string(), + "Wireframe 3D" => "3D Wireframe".to_string(), value => value.to_string(), }) .collect(); diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 22eb2fc5f..7e253e373 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -1,7 +1,6 @@ use acadrust::entities::{LwPolyline, LwVertex}; use cadkernel::geom2d::{ - circle_circle_points, fillet_between_rays, signed_area, Curve, Polyline, PolylineVertex, - RectangleFrame, Tolerance, Vec2, + signed_area, Curve, Polyline, PolylineVertex, RectangleFrame, Tolerance, Vec2, }; use crate::t; @@ -1163,407 +1162,56 @@ fn apply_grip(pline: &mut LwPolyline, grip_id: usize, apply: GripApply) { } } -fn nearest_line_circle_intersection( - line_point: Vec2, - line_direction: Vec2, - circle_center: Vec2, - circle_radius: f64, - near: Vec2, -) -> Option { - let aa = line_direction.dot(line_direction); - if aa <= Tolerance::default().linear().powi(2) { - return None; +fn kernel_polyline(polyline: &LwPolyline) -> Polyline { + Polyline { + vertices: polyline + .vertices + .iter() + .map(|vertex| PolylineVertex { + position: [vertex.location.x, vertex.location.y], + bulge: vertex.bulge, + }) + .collect(), + closed: polyline.is_closed, } - let relative = line_point - circle_center; - let bb = 2.0 * relative.dot(line_direction); - let cc = relative.dot(relative) - circle_radius * circle_radius; - let discriminant = bb * bb - 4.0 * aa * cc; - if discriminant < -Tolerance::default().linear() { - return None; - } - let root = discriminant.max(0.0).sqrt(); - let first = line_point + line_direction * ((-bb - root) / (2.0 * aa)); - let second = line_point + line_direction * ((-bb + root) / (2.0 * aa)); - Some(if first.distance(near) <= second.distance(near) { - first - } else { - second - }) } -fn nearest_circle_circle_intersection( - first_center: Vec2, - first_radius: f64, - second_center: Vec2, - second_radius: f64, - near: Vec2, -) -> Option { - circle_circle_points( - first_center.into(), - first_radius, - second_center.into(), - second_radius, - ) - .into_iter() - .map(Vec2::from) - .min_by(|a, b| a.distance(near).total_cmp(&b.distance(near))) -} - -fn bulge_for_circle_arc(center: Vec2, start: Vec2, end: Vec2, direction: f64) -> Option { - let start_angle = (start.y - center.y).atan2(start.x - center.x); - let end_angle = (end.y - center.y).atan2(end.x - center.x); - let sweep = if direction >= 0.0 { - (end_angle - start_angle).rem_euclid(TAU) - } else { - -(start_angle - end_angle).rem_euclid(TAU) - }; - let bulge = (sweep * 0.25).tan(); - bulge.is_finite().then_some(bulge.clamp(-1.0e6, 1.0e6)) -} - -fn move_segment_parallel(pline: &mut LwPolyline, seg: usize, offset: f64) { - let n = pline.vertices.len(); - let count = if pline.is_closed { - n - } else { - n.saturating_sub(1) - }; - if seg >= count || !offset.is_finite() { - return; - } - let i0 = seg; - let i1 = (seg + 1) % n; - if pline.vertices[i0].bulge.abs() >= 1.0e-9 { - return; - } - let p0 = Vec2::new(pline.vertices[i0].location.x, pline.vertices[i0].location.y); - let p1 = Vec2::new(pline.vertices[i1].location.x, pline.vertices[i1].location.y); - let selected = p1 - p0; - let length = selected.length(); - if length <= Tolerance::default().linear() { - return; - } - let normal = Vec2::new(-selected.y / length, selected.x / length); - let q0 = p0 + normal * offset; - - let intersect = |a: Vec2, direction: Vec2| -> Option { - let cross = direction.x * selected.y - direction.y * selected.x; - if cross.abs() < 1.0e-9 * direction.length().max(1.0) * length.max(1.0) { - return None; +fn edit_polyline_geometry( + polyline: &mut LwPolyline, + edit: impl FnOnce(&mut Polyline) -> bool, +) { + let mut geometry = kernel_polyline(polyline); + if edit(&mut geometry) { + for (target, source) in polyline.vertices.iter_mut().zip(geometry.vertices) { + target.location = acadrust::types::Vector2::new(source.position[0], source.position[1]); + target.bulge = source.bulge; } - let delta = q0 - a; - let t = (delta.x * selected.y - delta.y * selected.x) / cross; - Some(a + direction * t) - }; - let mut previous_arc_bulge = None; - let new0 = if pline.is_closed || seg > 0 { - let previous = (i0 + n - 1) % n; - let a = Vec2::new( - pline.vertices[previous].location.x, - pline.vertices[previous].location.y, - ); - if pline.vertices[previous].bulge.abs() >= 1.0e-9 { - let Some(neighbor) = crate::entities::common::BulgeArc::from_bulge( - a.into(), - p0.into(), - pline.vertices[previous].bulge, - ) else { - return; - }; - let center = Vec2::from(neighbor.center); - let Some(point) = - nearest_line_circle_intersection(q0, selected, center, neighbor.radius, p0) - else { - return; - }; - let Some(bulge) = bulge_for_circle_arc(center, a, point, neighbor.sweep.signum()) - else { - return; - }; - previous_arc_bulge = Some((previous, bulge)); - point - } else { - let Some(point) = intersect(a, p0 - a) else { - return; - }; - point - } - } else { - q0 - }; - let mut next_arc_bulge = None; - let new1 = if pline.is_closed || seg + 1 < count { - let next = (i1 + 1) % n; - let d = Vec2::new( - pline.vertices[next].location.x, - pline.vertices[next].location.y, - ); - if pline.vertices[i1].bulge.abs() >= 1.0e-9 { - let Some(neighbor) = crate::entities::common::BulgeArc::from_bulge( - p1.into(), - d.into(), - pline.vertices[i1].bulge, - ) else { - return; - }; - let center = Vec2::from(neighbor.center); - let Some(point) = - nearest_line_circle_intersection(q0, selected, center, neighbor.radius, p1) - else { - return; - }; - let Some(bulge) = bulge_for_circle_arc(center, point, d, neighbor.sweep.signum()) - else { - return; - }; - next_arc_bulge = Some((i1, bulge)); - point - } else { - let Some(point) = intersect(p1, d - p1) else { - return; - }; - point - } - } else { - p1 + normal * offset - }; - pline.vertices[i0].location = acadrust::types::Vector2::new(new0.x, new0.y); - pline.vertices[i1].location = acadrust::types::Vector2::new(new1.x, new1.y); - if let Some((previous, bulge)) = previous_arc_bulge { - pline.vertices[previous].bulge = bulge; - } - if let Some((next, bulge)) = next_arc_bulge { - pline.vertices[next].bulge = bulge; } } -/// Resize an arc concentrically while preserving its neighboring geometry. -/// This is used exclusively by the Radius grip-menu action. -fn resize_arc_concentrically(pline: &mut LwPolyline, seg: usize, offset: f64) { - let n = pline.vertices.len(); - let count = if pline.is_closed { n } else { n.saturating_sub(1) }; - if seg >= count || !offset.is_finite() { - return; - } - let i0 = seg; - let i1 = (seg + 1) % n; - let p0 = Vec2::new(pline.vertices[i0].location.x, pline.vertices[i0].location.y); - let p1 = Vec2::new(pline.vertices[i1].location.x, pline.vertices[i1].location.y); - let bulge = pline.vertices[i0].bulge; - let Some(arc) = crate::entities::common::BulgeArc::from_bulge(p0.into(), p1.into(), bulge) - else { - return; - }; - let center = Vec2::from(arc.center); - let radius = arc.radius + offset; - if radius <= Tolerance::default().linear() { - return; - } - - let linear_sq = Tolerance::default().linear().powi(2); - let mut sync_previous = None; - let mut previous_arc_bulge = None; - let new0 = if pline.is_closed || seg > 0 { - let previous = (i0 + n - 1) % n; - let a = Vec2::new( - pline.vertices[previous].location.x, - pline.vertices[previous].location.y, - ); - if pline.vertices[previous].bulge.abs() >= 1.0e-9 { - let Some(neighbor) = crate::entities::common::BulgeArc::from_bulge( - a.into(), p0.into(), pline.vertices[previous].bulge, - ) else { - return; - }; - let neighbor_center = Vec2::from(neighbor.center); - let Some(point) = nearest_circle_circle_intersection( - center, radius, neighbor_center, neighbor.radius, p0, - ) else { - return; - }; - let Some(new_bulge) = bulge_for_circle_arc( - neighbor_center, a, point, neighbor.sweep.signum(), - ) else { - return; - }; - previous_arc_bulge = Some((previous, new_bulge)); - point - } else if (p0 - a).length_squared() <= linear_sq { - sync_previous = Some(previous); - center + (p0 - center) * (radius / arc.radius) - } else { - let Some(point) = nearest_line_circle_intersection( - a, p0 - a, center, radius, p0, - ) else { - return; - }; - point - } - } else { - center + (p0 - center) * (radius / arc.radius) - }; - let mut sync_next = None; - let mut next_arc_bulge = None; - let new1 = if pline.is_closed || seg + 1 < count { - let next = (i1 + 1) % n; - let d = Vec2::new( - pline.vertices[next].location.x, - pline.vertices[next].location.y, - ); - if pline.vertices[i1].bulge.abs() >= 1.0e-9 { - let Some(neighbor) = crate::entities::common::BulgeArc::from_bulge( - p1.into(), d.into(), pline.vertices[i1].bulge, - ) else { - return; - }; - let neighbor_center = Vec2::from(neighbor.center); - let Some(point) = nearest_circle_circle_intersection( - center, radius, neighbor_center, neighbor.radius, p1, - ) else { - return; - }; - let Some(new_bulge) = bulge_for_circle_arc( - neighbor_center, point, d, neighbor.sweep.signum(), - ) else { - return; - }; - next_arc_bulge = Some((i1, new_bulge)); - point - } else if (d - p1).length_squared() <= linear_sq { - sync_next = Some(next); - center + (p1 - center) * (radius / arc.radius) - } else { - let Some(point) = nearest_line_circle_intersection( - p1, d - p1, center, radius, p1, - ) else { - return; - }; - point - } - } else { - center + (p1 - center) * (radius / arc.radius) - }; - - let Some(new_bulge) = bulge_for_circle_arc(center, new0, new1, arc.sweep.signum()) else { - return; - }; - pline.vertices[i0].location = acadrust::types::Vector2::new(new0.x, new0.y); - pline.vertices[i1].location = acadrust::types::Vector2::new(new1.x, new1.y); - if let Some(previous) = sync_previous { - pline.vertices[previous].location = acadrust::types::Vector2::new(new0.x, new0.y); - } - if let Some(next) = sync_next { - pline.vertices[next].location = acadrust::types::Vector2::new(new1.x, new1.y); - } - if let Some((previous, bulge)) = previous_arc_bulge { - pline.vertices[previous].bulge = bulge; - } - if let Some((next, bulge)) = next_arc_bulge { - pline.vertices[next].bulge = bulge; - } - pline.vertices[i0].bulge = new_bulge; +fn move_segment_parallel(polyline: &mut LwPolyline, segment: usize, offset: f64) { + edit_polyline_geometry(polyline, |geometry| { + cadkernel::geom2d::move_polyline_segment_parallel(geometry, segment, offset) + }); } -/// Return the corner and the two rays retained by the straight segments on -/// either side of an internal arc. Open endpoint arcs have no fillet frame. -fn arc_fillet_frame(pline: &LwPolyline, seg: usize) -> Option<(Vec2, Vec2, Vec2)> { - let n = pline.vertices.len(); - let count = if pline.is_closed { n } else { n.checked_sub(1)? }; - if seg >= count || (!pline.is_closed && (seg == 0 || seg + 1 == count)) { - return None; - } - let i1 = (seg + 1) % n; - let previous = (seg + n - 1) % n; - let next = (i1 + 1) % n; - if pline.vertices[previous].bulge.abs() >= 1.0e-9 - || pline.vertices[i1].bulge.abs() >= 1.0e-9 - { - return None; - } - let as_vec = |p: acadrust::types::Vector2| Vec2::new(p.x, p.y); - let outer0 = as_vec(pline.vertices[previous].location); - let tangent0 = as_vec(pline.vertices[seg].location); - let tangent1 = as_vec(pline.vertices[i1].location); - let outer1 = as_vec(pline.vertices[next].location); - let d0 = (tangent0 - outer0).normalize()?; - let d1 = (outer1 - tangent1).normalize()?; - let arc = crate::entities::common::BulgeArc::from_bulge( - tangent0.into(), - tangent1.into(), - pline.vertices[seg].bulge, - )?; - let center = Vec2::from(arc.center); - let tangent_tolerance = (arc.radius * 1.0e-6).max(Tolerance::default().linear()); - if (tangent0 - center).dot(d0).abs() > tangent_tolerance - || (tangent1 - center).dot(d1).abs() > tangent_tolerance - { - // Merely being between two lines does not make an arc a fillet. An - // ordinary polyline arc must keep its centre while its radius changes. - return None; - } - let cross = d0.cross(d1); - if cross.abs() <= 1.0e-9 { - return None; - } - let apex = outer0 + d0 * ((tangent1 - outer0).cross(d1) / cross); - Some(( - apex, - (outer0 - apex).normalize()?, - (outer1 - apex).normalize()?, - )) +fn resize_arc_concentrically(polyline: &mut LwPolyline, segment: usize, offset: f64) { + edit_polyline_geometry(polyline, |geometry| { + geometry + .segment_arc(segment) + .is_some_and(|arc| cadkernel::geom2d::resize_polyline_arc( + geometry, + segment, + arc.radius + offset, + )) + }); } -fn resize_arc_segment_radius(pline: &mut LwPolyline, seg: usize, radius: f64) { - if radius <= Tolerance::default().linear() { - return; - } - let n = pline.vertices.len(); - let count = if pline.is_closed { n } else { n.saturating_sub(1) }; - if seg >= count { - return; - } - let i1 = (seg + 1) % n; - let (Some(vertex0), Some(vertex1)) = (pline.vertices.get(seg), pline.vertices.get(i1)) else { - return; - }; - let p0 = Vec2::new(vertex0.location.x, vertex0.location.y); - let p1 = Vec2::new(vertex1.location.x, vertex1.location.y); - let original_bulge = vertex0.bulge; - let Some(original) = crate::entities::common::BulgeArc::from_bulge( - p0.into(), p1.into(), original_bulge, - ) else { - return; - }; - - if let Some((apex, keep0, keep1)) = arc_fillet_frame(pline, seg) { - let Some(fillet) = fillet_between_rays(apex.into(), keep0.into(), keep1.into(), radius) - else { - return; - }; - let new0 = Vec2::from(fillet.tangent1); - let new1 = Vec2::from(fillet.tangent2); - let center = Vec2::from(fillet.centre); - let (Some(r0), Some(r1)) = ((new0 - center).normalize(), (new1 - center).normalize()) - else { - return; - }; - let sweep = r0.dot(r1).clamp(-1.0, 1.0).acos(); - let bulge = original_bulge.signum() * (sweep * 0.25).tan(); - if !bulge.is_finite() { - return; - } - pline.vertices[seg].location = acadrust::types::Vector2::new(new0.x, new0.y); - pline.vertices[i1].location = acadrust::types::Vector2::new(new1.x, new1.y); - pline.vertices[seg].bulge = bulge; - return; - } - - // Ordinary arcs keep their centre. Connected endpoints slide along the - // infinite lines of their adjacent straight segments, preserving those - // lines' angles; only free open endpoints move radially. - resize_arc_concentrically(pline, seg, radius - original.radius); +fn resize_arc_segment_radius(polyline: &mut LwPolyline, segment: usize, radius: f64) { + edit_polyline_geometry(polyline, |geometry| { + cadkernel::geom2d::resize_polyline_arc(geometry, segment, radius) + }); } - fn apply_transform(pline: &mut LwPolyline, t: &EntityTransform) { crate::scene::view::transform::apply_standard_entity_transform(pline, t, |entity, p1, p2| { for v in &mut entity.vertices { @@ -1721,63 +1369,24 @@ impl crate::entities::traits::Grippable for LwPolyline { if action == A::Radius { let n = self.vertices.len(); let seg = grip_id.checked_sub(n)?; - let count = if self.is_closed { n } else { n.saturating_sub(1) }; - if seg >= count { - return None; - } let plane = crate::entities::curve::lwpolyline_curve(self)?.plane; let point = Vec2::from(plane.project(point.to_array())?); - let p0 = self.vertices[seg].location; - let p1 = self.vertices[(seg + 1) % n].location; - let arc = crate::entities::common::BulgeArc::from_bulge( - [p0.x, p0.y], [p1.x, p1.y], self.vertices[seg].bulge, - )?; - if let Some((apex, _, _)) = arc_fillet_frame(self, seg) { - let midpoint = Vec2::from(arc.sample(0.5)); - let ray = midpoint - apex; - let scale = (point - apex).dot(ray) / ray.dot(ray); - let radius = arc.radius * scale; - return (radius > Tolerance::default().linear()).then_some(radius); - } - let radius = point.distance(Vec2::from(arc.center)); - return (radius > Tolerance::default().linear()).then_some(radius); + return cadkernel::geom2d::polyline_arc_radius_from_point( + &kernel_polyline(self), + seg, + point.into(), + ); } if action == A::MoveParallel { let n = self.vertices.len(); let seg = grip_id.checked_sub(n)?; - let count = if self.is_closed { - n - } else { - n.saturating_sub(1) - }; - if seg >= count || self.vertices[seg].bulge.abs() >= 1.0e-9 { - return None; - } let plane = crate::entities::curve::lwpolyline_curve(self)?.plane; let point = Vec2::from(plane.project(point.to_array())?); - let p0 = Vec2::new(self.vertices[seg].location.x, self.vertices[seg].location.y); - let p1 = Vec2::new( - self.vertices[(seg + 1) % n].location.x, - self.vertices[(seg + 1) % n].location.y, + return cadkernel::geom2d::polyline_segment_parallel_offset( + &kernel_polyline(self), + seg, + point.into(), ); - let bulge = self.vertices[seg].bulge; - if bulge.abs() >= 1.0e-9 { - let arc = crate::entities::common::BulgeArc::from_bulge( - p0.into(), - p1.into(), - bulge, - )?; - let midpoint = Vec2::from(arc.sample(0.5)); - let radial = (midpoint - Vec2::from(arc.center)).normalize()?; - return Some((point - midpoint).dot(radial)); - } - let direction = p1 - p0; - let length = direction.length(); - if length <= Tolerance::default().linear() { - return None; - } - let normal = Vec2::new(-direction.y / length, direction.x / length); - return Some((point - p0).dot(normal)); } let (frame, plane) = rectangle_frame(self)?; let seg = grip_id.checked_sub(4)?; diff --git a/src/io/step.rs b/src/io/step.rs index 11afee36e..007085489 100644 --- a/src/io/step.rs +++ b/src/io/step.rs @@ -57,7 +57,7 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option { let nx = ab[1] * ac[2] - ab[2] * ac[1]; let ny = ab[2] * ac[0] - ab[0] * ac[2]; let nz = ab[0] * ac[1] - ab[1] * ac[0]; - let len = (nx * nx + ny * ny + nz * nz).sqrt(); + let len = nx.hypot(ny).hypot(nz); let n = if len == 0.0 && i0 < normals.len() { normals[i0] } else { diff --git a/src/io/stl.rs b/src/io/stl.rs index 89aeac137..a2831b3fa 100644 --- a/src/io/stl.rs +++ b/src/io/stl.rs @@ -48,7 +48,7 @@ pub fn build_stl(meshes: &[&MeshModel]) -> Option> { let nx = ab[1] * ac[2] - ab[2] * ac[1]; let ny = ab[2] * ac[0] - ab[0] * ac[2]; let nz = ab[0] * ac[1] - ab[1] * ac[0]; - let len = (nx * nx + ny * ny + nz * nz).sqrt(); + let len = nx.hypot(ny).hypot(nz); let normal = if len == 0.0 && i0 < mesh.normals.len() { mesh.normals[i0] } else { @@ -159,6 +159,16 @@ mod tests { assert_eq!(first_facet_normal(&stl), [0.0, 0.0, 1.0]); } + #[test] + fn a_facet_whose_squared_cross_product_underflows_keeps_its_own_normal() { + let mesh = triangle( + vec![[0.0, 0.0, 0.0], [1.0e-12, 0.0, 0.0], [0.0, 1.0e-12, 0.0]], + vec![[0.0, 1.0, 0.0]; 3], + ); + let stl = build_stl(&[&mesh]).expect("stl"); + assert_eq!(first_facet_normal(&stl), [0.0, 0.0, 1.0]); + } + /// A facet with no area has no normal of its own, so it keeps the one /// the mesh gives it rather than a zero vector. #[test] diff --git a/src/modules/draw/draw/helix.rs b/src/modules/draw/draw/helix.rs index 224d6f215..18547a70d 100644 --- a/src/modules/draw/draw/helix.rs +++ b/src/modules/draw/draw/helix.rs @@ -521,4 +521,22 @@ mod tests { assert_eq!(helix.spline.control_points.len(), 2); assert_eq!(helix.spline.control_points[0], helix.spline.control_points[1]); } + + #[test] + fn final_point_uses_its_distance_from_the_center_as_height() { + let mut command = HelixCommand::new(); + command.step = Step::Final; + command.center = DVec3::ZERO; + command.base_radius = 2.0; + command.top_radius = 2.0; + command.turns = 2.0; + + let CmdResult::CommitAndExit(EntityType::Helix(helix)) = + command.on_point(DVec3::new(3.0, 4.0, 0.0)) + else { + panic!("expected helix"); + }; + + assert_eq!(helix.turn_height * helix.turns, 5.0); + } }