diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 447ae0b17..7b14b6990 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -881,7 +881,7 @@ impl OpenCADStudio { SplineCommand::control_vertices() } else { SplineCommand::new() - }; + }.with_document(&self.tabs[i].scene.document); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/modules/draw/draw/spline.rs b/src/modules/draw/draw/spline.rs index 8ace3cb2d..3aff06f25 100644 --- a/src/modules/draw/draw/spline.rs +++ b/src/modules/draw/draw/spline.rs @@ -5,7 +5,8 @@ use crate::t; use acadrust::types::Vector3; -use acadrust::{EntityType, Spline}; +use acadrust::{CadDocument, EntityType, Handle, Spline}; +use std::collections::HashMap; use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; @@ -26,6 +27,14 @@ pub struct SplineCommand { pts: Vec, control_vertices: bool, choosing_method: bool, + choosing_knots: bool, + choosing_tangent: bool, + knot_parameterization: i32, + begin_tangent: Vector3, + end_tangent: Vector3, + choosing_objects: bool, + convertible: HashMap, + selected_objects: Vec, } impl SplineCommand { @@ -34,6 +43,14 @@ impl SplineCommand { pts: Vec::new(), control_vertices: false, choosing_method: false, + choosing_knots: false, + choosing_tangent: false, + knot_parameterization: 0, + begin_tangent: Vector3::ZERO, + end_tangent: Vector3::ZERO, + choosing_objects: false, + convertible: HashMap::new(), + selected_objects: Vec::new(), } } @@ -44,18 +61,68 @@ impl SplineCommand { } } + pub fn with_document(mut self, document: &CadDocument) -> Self { + self.convertible = document.entities().filter_map(|entity| { + spline_from_polyline(entity).map(|spline| (entity.common().handle, spline)) + }).collect(); + self + } + fn build(&self, closed: bool) -> Option { if self.pts.len() < 2 { return None; } - Some(EntityType::Spline(make_spline( + let mut spline = make_spline( &self.pts, closed, self.control_vertices, - ))) + ); + spline.knot_parameterization = self.knot_parameterization; + spline.begin_tangent = self.begin_tangent; + spline.end_tangent = self.end_tangent; + Some(EntityType::Spline(spline)) } } +fn control_spline(points: &[[f64; 3]], degree: usize, closed: bool) -> Option { + let curve = cadkernel::space::NurbsCurve3::from_control_polygon(degree, points, closed)?; + let mut spline = Spline { + degree: curve.degree() as i32, + control_points: curve.control_points().iter().map(|p| Vector3::new(p[0], p[1], p[2])).collect(), + knots: curve.knots().to_vec(), + weights: curve.weights().to_vec(), + ..Default::default() + }; + spline.flags.closed = closed; + spline.flags.periodic = closed; + spline.flags.planar = crate::entities::curve::spline_is_planar(&spline); + Some(spline) +} + +fn spline_from_polyline(entity: &EntityType) -> Option { + let (degree, points, closed, normal) = match entity { + EntityType::Polyline2D(poly) if poly.flags.is_spline_fit() => { + let degree = match poly.smooth_surface as i16 { 5 => 2, 6 => 3, _ => return None }; + let plane = crate::entities::curve::ocs_plane(poly.normal, poly.elevation); + let controls: Vec<_> = poly.vertices.iter().filter(|vertex| vertex.flags.bits() & 16 != 0) + .map(|vertex| plane.point_at([vertex.location.x, vertex.location.y])).collect(); + (degree, controls, poly.is_closed(), poly.normal) + } + EntityType::Polyline3D(poly) if poly.flags.spline_fit => { + let degree = match poly.smooth_type as i16 { 5 => 2, 6 => 3, _ => return None }; + let controls: Vec<_> = poly.vertices.iter().filter(|vertex| vertex.flags & 16 != 0) + .map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]).collect(); + (degree, controls, poly.is_closed(), poly.normal) + } + _ => return None, + }; + let mut spline = control_spline(&points, degree, closed)?; + spline.normal = normal; + spline.common = entity.common().clone(); + spline.common.handle = Handle::NULL; + Some(spline) +} + /// Store the chosen construction method directly in the persistent spline. fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline { let mut points: Vec = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect(); @@ -93,10 +160,14 @@ fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline { } }; spline.flags.closed = closed; + // Closed fit curves use the kernel's periodic interpolation rather than + // appending a closing straight segment to an open interpolant. + spline.flags.periodic = closed && !control_vertices; spline } /// Preview the exact representation that `build()` commits. +#[cfg(test)] fn sample_curve(pts: &[DVec3], closed: bool, control_vertices: bool) -> Vec<[f32; 3]> { if pts.len() < 2 { return pts @@ -123,8 +194,13 @@ impl CadCommand for SplineCommand { } fn prompt(&self) -> String { + if self.choosing_objects { return t!("Select spline-fit polylines:").to_string(); } if self.choosing_method { "SPLINE Choose creation method [Fit/Control vertices]:".into() + } else if self.choosing_knots { + "SPLINE Enter knot parameterization [Chord/Square root/Uniform]:".into() + } else if self.choosing_tangent { + "SPLINE Specify tangent direction:".into() } else if self.pts.is_empty() && self.control_vertices { t!("SPLINE Specify first control point:").into_owned() } else if self.pts.is_empty() { @@ -137,16 +213,25 @@ impl CadCommand for SplineCommand { fn options(&self) -> Vec { use crate::command::CmdOption; + if self.choosing_objects { return Vec::new(); } if self.choosing_method { return vec![ CmdOption::new("Fit", "FIT"), CmdOption::new("Control vertices", "CV"), ]; } - if self.pts.is_empty() { - return vec![CmdOption::new(t!("Method").as_ref(), "M")]; + if self.choosing_tangent { return vec![]; } + if self.choosing_knots { + return vec![CmdOption::new("Chord", "CH"), CmdOption::new("Square root", "S"), CmdOption::new("Uniform", "U")]; } - let mut opts = vec![CmdOption::new(t!("Close").as_ref(), "C")]; + if self.pts.is_empty() { + let mut options = vec![CmdOption::new(t!("Method").as_ref(), "M"), CmdOption::new("Object", "O")]; + if !self.control_vertices { options.push(CmdOption::new("Knots", "K")); } + return options; + } + let mut opts = vec![]; + if self.pts.len() >= 3 { opts.push(CmdOption::new(t!("Close").as_ref(), "C")); } + if !self.control_vertices { opts.push(CmdOption::new("Tangency", "T")); } // Undo only makes sense once a control point exists. opts.push(CmdOption::new(t!("Undo").as_ref(), "U")); opts.push(CmdOption::enter(t!("Done").as_ref())); @@ -154,7 +239,22 @@ impl CadCommand for SplineCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { - if self.choosing_method || !pt.is_finite() { + if self.choosing_objects || self.choosing_method || self.choosing_knots || !pt.is_finite() { + return CmdResult::NeedPoint; + } + if self.choosing_tangent { + let Some(anchor) = self.pts.last() else { return CmdResult::NeedPoint; }; + let Some(dir) = (pt - *anchor).try_normalize() else { return CmdResult::NeedPoint; }; + let tangent = Vector3::new(dir.x, dir.y, dir.z); + if self.pts.len() == 1 { self.begin_tangent = tangent; } + else { self.end_tangent = tangent; } + self.choosing_tangent = false; + if self.pts.len() > 1 { + return self.build(false).map_or(CmdResult::NeedPoint, CmdResult::CommitAndExit); + } + return CmdResult::NeedPoint; + } + if self.pts.last().is_some_and(|last| last.distance_squared(pt) < 1e-20) { return CmdResult::NeedPoint; } self.pts.push(pt); @@ -162,10 +262,20 @@ impl CadCommand for SplineCommand { } fn on_enter(&mut self) -> CmdResult { - if self.choosing_method { + if self.choosing_objects { + let replacements = self.selected_objects.iter().filter_map(|handle| { + self.convertible.get(handle).map(|spline| (*handle, vec![EntityType::Spline(spline.clone())])) + }).collect::>(); + return if replacements.is_empty() { CmdResult::Cancel } + else { CmdResult::ReplaceMany(replacements, Vec::new()) }; + } + + if self.choosing_method || self.choosing_knots { self.choosing_method = false; + self.choosing_knots = false; return CmdResult::NeedPoint; } + if self.choosing_tangent { return CmdResult::NeedPoint; } match self.build(false) { Some(e) => CmdResult::CommitAndExit(e), None => CmdResult::Cancel, @@ -173,13 +283,28 @@ impl CadCommand for SplineCommand { } fn enter_accepts_default_start(&self) -> bool { - self.pts.is_empty() && !self.choosing_method + self.pts.is_empty() && !self.choosing_method && !self.choosing_objects + } + + fn is_selection_gathering(&self) -> bool { self.choosing_objects } + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + self.selected_objects = handles.into_iter().filter(|handle| self.convertible.contains_key(handle)).collect(); + self.on_enter() } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } + fn on_undo_step(&mut self) -> Option { + if self.pts.is_empty() { return None; } + self.pts.pop(); + self.end_tangent = Vector3::ZERO; + self.choosing_tangent = false; + if self.pts.is_empty() { self.begin_tangent = Vector3::ZERO; } + Some(CmdResult::NeedPoint) + } + fn wants_text_input(&self) -> bool { true } @@ -189,7 +314,30 @@ impl CadCommand for SplineCommand { } fn on_text_input(&mut self, text: &str) -> Option { + if self.choosing_tangent { return None; } + if self.choosing_knots { + self.knot_parameterization = match text.trim().to_ascii_uppercase().as_str() { + "CH" | "CHORD" => 0, + "S" | "SQUARE" | "SQUAREROOT" => 1, + "U" | "UNIFORM" => 2, + _ => return None, + }; + self.choosing_knots = false; + return Some(CmdResult::NeedPoint); + } match text.trim().to_uppercase().as_str() { + "K" | "KNOTS" if self.pts.is_empty() && !self.control_vertices => { + self.choosing_knots = true; + Some(CmdResult::NeedPoint) + } + "T" | "TANGENCY" if !self.pts.is_empty() && !self.control_vertices => { + self.choosing_tangent = true; + Some(CmdResult::NeedPoint) + } + "O" | "OBJECT" if self.pts.is_empty() => { + self.choosing_objects = true; + Some(CmdResult::NeedPoint) + } "M" | "METHOD" if self.pts.is_empty() => { self.choosing_method = true; Some(CmdResult::NeedPoint) @@ -204,28 +352,32 @@ impl CadCommand for SplineCommand { self.choosing_method = false; Some(CmdResult::NeedPoint) } - "C" | "CLOSE" => match self.build(true) { + "C" | "CLOSE" if self.pts.len() >= 3 => match self.build(true) { Some(e) => Some(CmdResult::CommitAndExit(e)), None => Some(CmdResult::NeedPoint), }, "U" | "UNDO" => { - self.pts.pop(); - Some(CmdResult::NeedPoint) + self.on_undo_step().or(Some(CmdResult::NeedPoint)) } _ => None, } } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - if self.pts.is_empty() { + if self.pts.is_empty() || self.choosing_method || self.choosing_knots || self.choosing_tangent { return None; } // Preview the committed construction method. - let mut ctrl = self.pts.clone(); - ctrl.push(pt); + self.pts.push(pt); + let entity = self.build(false); + self.pts.pop(); + let Some(EntityType::Spline(spline)) = entity else { return None; }; + let points = crate::entities::curve::spline_curve(&spline) + .map(|curve| crate::entities::curve::curve_points(&curve)) + .unwrap_or_else(|| crate::entities::spline::measurement_polyline(&spline)); Some(WireModel::solid( "rubber_band".into(), - sample_curve(&ctrl, false, self.control_vertices), + points.into_iter().map(|p| [p[0] as f32, p[1] as f32, p[2] as f32]).collect(), WireModel::CYAN, false, ))