diff --git a/Cargo.lock b/Cargo.lock index 2924cbbff..bbc4f1b5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -945,7 +945,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#33aa850ac2a27fdb2b305a67df27cb0eae23acc2" +source = "git+https://github.com/HakanSeven12/cadkernel.git#e76334b7697a3c39292c8a6494d9b837bba78a6e" dependencies = [ "cavalier_contours", ] diff --git a/src/modules/draw/modify/spline_ops.rs b/src/modules/draw/modify/spline_ops.rs index 295a4e1ec..e13ef55e9 100644 --- a/src/modules/draw/modify/spline_ops.rs +++ b/src/modules/draw/modify/spline_ops.rs @@ -10,26 +10,64 @@ // fine until it is measured. use acadrust::entities::Spline; -use acadrust::kernel::geom2d::NurbsCurve; +use acadrust::kernel::geom2d::{NurbsCurve, Parameterization}; use acadrust::types::Vector3; use acadrust::Handle; +/// The drawing plane a spline sits on. +/// +/// Read from whichever point list the spline actually stores: a fit-point +/// spline carries no control points, and defaulting its elevation to zero +/// would drop every such spline to the ground plane on the first edit. +fn elevation(spl: &Spline) -> f64 { + spl.control_points + .first() + .or_else(|| spl.fit_points.first()) + .map(|v| v.z) + .unwrap_or(0.0) +} + // ── Conversion ───────────────────────────────────────────────────────────── /// Convert an acadrust `Spline` to a kernel `NurbsCurve`, weights included. /// -/// Returns `None` when there are too few control points for the degree, which -/// is the one case that has no curve to evaluate. A knot vector of the wrong -/// length is repaired by the kernel rather than rejected. +/// A SPLINE is stored one of two ways and both turn up: a control polygon the +/// curve approaches, or fit points it must pass through with optional end +/// tangents. Control points win when they describe a curve; otherwise the fit +/// points are interpolated into one, so a fit-point spline is as editable as +/// any other instead of failing every command that touches it. +/// +/// `None` only when neither form has enough points to describe a curve. pub fn spline_to_nurbs(spl: &Spline) -> Option { + let degree = (spl.degree.max(1)) as usize; let control_points: Vec<[f64; 2]> = spl .control_points .iter() .map(|p| [p.x, p.y]) .collect(); - let degree = (spl.degree.max(1)) as usize; let weights = (!spl.weights.is_empty()).then(|| spl.weights.clone()); - NurbsCurve::new(degree, control_points, spl.knots.clone(), weights) + + if let Some(curve) = + NurbsCurve::new(degree, control_points, spl.knots.clone(), weights) + { + return Some(curve); + } + + // No usable control polygon, so this is a fit-point spline. + let fit: Vec<[f64; 2]> = spl.fit_points.iter().map(|p| [p.x, p.y]).collect(); + let tangent = |v: &acadrust::types::Vector3| { + (v.x * v.x + v.y * v.y > 1e-18).then_some([v.x, v.y]) + }; + NurbsCurve::interpolate( + &fit, + tangent(&spl.begin_tangent), + tangent(&spl.end_tangent), + match spl.knot_parameterization { + 2 => Parameterization::Uniform, + 1 => Parameterization::Centripetal, + _ => Parameterization::Chord, + }, + ) } /// Rebuild an acadrust `Spline` from a kernel curve. @@ -37,7 +75,7 @@ pub fn spline_to_nurbs(spl: &Spline) -> Option { /// The `template` supplies entity common data and the Z elevation; splines /// here are planar, so the kernel's 2D points are lifted back onto it. pub fn nurbs_to_spline(curve: &NurbsCurve, template: &Spline) -> Spline { - let z = template.control_points.first().map(|v| v.z).unwrap_or(0.0); + let z = elevation(template); let mut spl = template.clone(); spl.common.handle = Handle::NULL; spl.degree = curve.degree() as i32; @@ -52,9 +90,12 @@ pub fn nurbs_to_spline(curve: &NurbsCurve, template: &Spline) -> Spline { } else { Vec::new() }; - // Fit points describe the original interpolation, which a split no longer - // satisfies. + // Fit points and their tangents describe the original interpolation, which + // a piece of it no longer satisfies. What comes back is a control-point + // spline. spl.fit_points.clear(); + spl.begin_tangent = Vector3::new(0.0, 0.0, 0.0); + spl.end_tangent = Vector3::new(0.0, 0.0, 0.0); spl } @@ -126,11 +167,7 @@ pub fn spline_pts_wire(spl: &Spline) -> Vec<[f32; 3]> { let Some(curve) = spline_to_nurbs(spl) else { return vec![]; }; - let elev = spl - .control_points - .first() - .map(|v| v.z as f32) - .unwrap_or(0.0); + let elev = elevation(spl) as f32; curve .tessellate(16) .iter() diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index b34062b71..d43214ae6 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -985,8 +985,13 @@ fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec { return None; } // Cut away everything before the interval, then everything after - // it, leaving [t_lo, t_hi]. - let (_, after_low) = spline_cut(spl, t_lo)?; + // it, leaving [t_lo, t_hi]. Either cut is a no-op when the + // interval already reaches that end of the curve — dropping the + // piece there would delete the half the user meant to keep. + let after_low = match spline_cut(spl, t_lo) { + Some((_, right)) => right, + None => spl.clone(), + }; let kept = match spline_cut(&after_low, t_hi) { Some((middle, _)) => middle, // The interval reaches the far end, so nothing is left to cut.