From 042a4792448686e30663a02605e90e3e3752d422 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 8 Aug 2026 17:08:17 +0300 Subject: [PATCH] fix(spline): edit fit-point splines, and stop deleting the kept half MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two faults, reported together. Fit-point splines could not be edited at all. A SPLINE is stored either as a control polygon the curve approaches or as fit points it must pass through with optional end tangents, and `spline_to_nurbs` only read the first. A fit-point spline has no control points, so every command that touched one — TRIM, BREAK, LENGTHEN, OFFSET — got nothing back and did nothing. Two fit points with start and end tangents, which is what a drawing stores for the simplest curve, was the clearest case. Fit points now interpolate into an exact curve through cadkernel, so they are as editable as any other spline. Trimming deleted the whole spline when the surviving piece reached the curve's own start. `trim_spline` cut away everything before the interval and everything after it, and a cut at either end is a no-op that reports nothing to do — which the end case handled and the start case propagated as failure, dropping the piece. That is why one side of the cutting line worked and the other emptied the drawing. Elevation is read from whichever point list the spline actually has. A fit-point spline has no control points, so it was falling to z = 0 on the first edit. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 2 +- src/modules/draw/modify/spline_ops.rs | 65 +++++++++++++++++++++------ src/modules/draw/modify/trim.rs | 9 +++- 3 files changed, 59 insertions(+), 17 deletions(-) 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.