diff --git a/Cargo.lock b/Cargo.lock index 0ee3226e9..2924cbbff 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#feed535a0f72f9479d618a4999dba9bafcea250f" +source = "git+https://github.com/HakanSeven12/cadkernel.git#33aa850ac2a27fdb2b305a67df27cb0eae23acc2" dependencies = [ "cavalier_contours", ] diff --git a/src/modules/draw/modify/break_cmd.rs b/src/modules/draw/modify/break_cmd.rs index 779de52f7..a236baf89 100644 --- a/src/modules/draw/modify/break_cmd.rs +++ b/src/modules/draw/modify/break_cmd.rs @@ -7,9 +7,7 @@ // // BREAK @ (at-sign as second point) → Break at a single point (splits without gap). -use crate::modules::draw::modify::spline_ops::{ - bspline_to_spline, spline_nearest_t, spline_to_bspline, -}; +use crate::modules::draw::modify::spline_ops::{spline_cut, spline_nearest_t, spline_range}; use acadrust::entities::{ Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt, }; @@ -17,7 +15,6 @@ use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::DVec3; use crate::t; -use truck_modeling::base::{BoundedCurve, Cut}; use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; @@ -307,37 +304,35 @@ fn break_spline(spl: &SplineEnt, p1: DVec3, p2: DVec3) -> Vec { None => return vec![EntityType::Spline(spl.clone())], }; - let bs = match spline_to_bspline(spl) { - Some(b) => b, - None => return vec![EntityType::Spline(spl.clone())], + let Some((t0, t_end)) = spline_range(spl) else { + return vec![EntityType::Spline(spl.clone())]; }; - let (t0, t_end) = bs.range_tuple(); let (ta, tb) = if t1 <= t2 { (t1, t2) } else { (t2, t1) }; // Single-point break (ta ≈ tb): split into two segments at that point. if (tb - ta).abs() < 1e-9 { - let mut piece = bs.clone(); - let right = piece.cut(ta); - return vec![ - EntityType::Spline(bspline_to_spline(&piece, spl)), - EntityType::Spline(bspline_to_spline(&right, spl)), - ]; + return match spline_cut(spl, ta) { + Some((left, right)) => { + vec![EntityType::Spline(left), EntityType::Spline(right)] + } + None => vec![EntityType::Spline(spl.clone())], + }; } // Two-point break: keep [t0..ta] and [tb..t_end], discard middle. let mut result = vec![]; // Left piece [t0, ta] if ta - t0 > 1e-9 { - let mut left = bs.clone(); - let _right = left.cut(ta); // left = [t0, ta] - result.push(EntityType::Spline(bspline_to_spline(&left, spl))); + if let Some((left, _)) = spline_cut(spl, ta) { + result.push(EntityType::Spline(left)); + } } // Right piece [tb, t_end] if t_end - tb > 1e-9 { - let mut full = bs.clone(); - let right = full.cut(tb); // right = [tb, t_end] - result.push(EntityType::Spline(bspline_to_spline(&right, spl))); + if let Some((_, right)) = spline_cut(spl, tb) { + result.push(EntityType::Spline(right)); + } } result } diff --git a/src/modules/draw/modify/lengthen.rs b/src/modules/draw/modify/lengthen.rs index 42abf57dd..3044a2dd2 100644 --- a/src/modules/draw/modify/lengthen.rs +++ b/src/modules/draw/modify/lengthen.rs @@ -7,7 +7,7 @@ // // The entity is modified at whichever end is closest to the pick point. -use crate::modules::draw::modify::spline_ops::{bspline_to_spline, spline_to_bspline}; +use crate::modules::draw::modify::spline_ops::{spline_cut, spline_to_nurbs}; use acadrust::entities::{ Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt, }; @@ -15,7 +15,6 @@ use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::{DVec3, Vec3}; use crate::t; -use truck_modeling::base::{BoundedCurve, Cut}; use crate::command::{CadCommand, CmdResult}; @@ -411,23 +410,22 @@ fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option Option { - let bs = spline_to_bspline(spl)?; - let (t0, t1) = bs.range_tuple(); + let curve = spline_to_nurbs(spl)?; + let (t0, t1) = curve.domain(); if (t1 - t0).abs() < 1e-12 { return None; } // Approximate arc length via 64-point numerical integration. - use truck_modeling::base::ParametricCurve; let arc_len = { let n = 64usize; let mut len = 0.0f64; for i in 0..n { let ta = t0 + (t1 - t0) * (i as f64 / n as f64); let tb = t0 + (t1 - t0) * ((i + 1) as f64 / n as f64); - let pa = bs.subs(ta); - let pb = bs.subs(tb); - len += (pb.x - pa.x).hypot(pb.y - pa.y); + let pa = curve.point_at_knot(ta); + let pb = curve.point_at_knot(tb); + len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]); } len }; @@ -441,10 +439,10 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option Option Option Option = offset_pts; let n_ctrl = new_ctrl.len(); - let kv = truck_modeling::KnotVec::uniform_knot(degree, n_ctrl - 1); let mut new_spl = spl.clone(); new_spl.common.handle = Handle::NULL; new_spl.control_points = new_ctrl; - new_spl.knots = kv.iter().copied().collect(); + new_spl.knots = clamped_uniform_knots(degree, n_ctrl); new_spl.fit_points.clear(); new_spl.weights.clear(); Some(EntityType::Spline(new_spl)) diff --git a/src/modules/draw/modify/spline_ops.rs b/src/modules/draw/modify/spline_ops.rs index bf1e79a23..295a4e1ec 100644 --- a/src/modules/draw/modify/spline_ops.rs +++ b/src/modules/draw/modify/spline_ops.rs @@ -1,127 +1,114 @@ // Shared B-spline utilities for modify commands (TRIM, BREAK, OFFSET, LENGTHEN). // -// Provides: -// - spline_to_bspline : acadrust Spline → truck BSplineCurve -// - bspline_to_spline : truck BSplineCurve → acadrust Spline -// - spline_sample_xy : sample the DXF-XY projection at N+1 points (with t params) -// - spline_nearest_t : find the spline parameter t closest to a DXF-XY click -// - spline_pts_wire : world-space wire points for WireModel preview +// The evaluation lives in cadkernel; this file converts a drawing's SPLINE +// entity to and from it and keeps the knot-domain call shapes the commands +// were written against. +// +// Weights are carried through now. The previous path evaluated splines with a +// non-rational B-spline and cleared the weights on every split, which turns a +// piece of a NURBS circle into a piece of a parabola — a change that looks +// fine until it is measured. use acadrust::entities::Spline; +use acadrust::kernel::geom2d::NurbsCurve; use acadrust::types::Vector3; use acadrust::Handle; -use truck_modeling::base::{BoundedCurve, ParametricCurve}; -use truck_modeling::{BSplineCurve, KnotVec, Point3}; // ── Conversion ───────────────────────────────────────────────────────────── -/// Convert an acadrust `Spline` to a truck `BSplineCurve`. -/// Returns `None` if the spline has too few control points or inconsistent data. -pub fn spline_to_bspline(spl: &Spline) -> Option> { - let ctrl_pts: Vec = spl +/// 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. +pub fn spline_to_nurbs(spl: &Spline) -> Option { + let control_points: Vec<[f64; 2]> = spl .control_points .iter() - .map(|p| Point3::new(p.x, p.y, p.z)) + .map(|p| [p.x, p.y]) .collect(); - if ctrl_pts.len() < 2 { - return None; - } - let degree = spl.degree as usize; - let expected_knots = ctrl_pts.len() + degree + 1; - let knot_vec = if spl.knots.len() == expected_knots { - KnotVec::from(spl.knots.clone()) - } else { - KnotVec::uniform_knot(degree, ctrl_pts.len() - 1) - }; - Some(BSplineCurve::new(knot_vec, ctrl_pts)) + 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) } -/// Rebuild an acadrust `Spline` from a trimmed `BSplineCurve`. -/// The `template` spline provides the entity common data, degree, and Z-elevation. -pub fn bspline_to_spline(bs: &BSplineCurve, template: &Spline) -> Spline { +/// Rebuild an acadrust `Spline` from a kernel curve. +/// +/// 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 mut spl = template.clone(); spl.common.handle = Handle::NULL; - spl.degree = bs.degree() as i32; - spl.knots = bs.knot_vec().iter().copied().collect(); - spl.control_points = bs + spl.degree = curve.degree() as i32; + spl.knots = curve.knots().to_vec(); + spl.control_points = curve .control_points() .iter() - .map(|p| Vector3::new(p.x, p.y, z)) + .map(|p| Vector3::new(p[0], p[1], z)) .collect(); - spl.weights.clear(); // not rational after split - spl.fit_points.clear(); // fit points are no longer valid + spl.weights = if curve.is_rational() { + curve.weights().to_vec() + } else { + Vec::new() + }; + // Fit points describe the original interpolation, which a split no longer + // satisfies. + spl.fit_points.clear(); spl } +// ── Domain and splitting ─────────────────────────────────────────────────── + +/// The knot values the spline is defined over, `(start, end)`. +pub fn spline_range(spl: &Spline) -> Option<(f64, f64)> { + Some(spline_to_nurbs(spl)?.domain()) +} + +/// Split a spline at knot value `t`, returning `(left, right)`. +/// +/// Both halves are exact — the kernel raises the cut to full knot +/// multiplicity rather than refitting — and a rational spline stays rational. +/// `None` when the cut falls at or outside an end. +pub fn spline_cut(spl: &Spline, t: f64) -> Option<(Spline, Spline)> { + let curve = spline_to_nurbs(spl)?; + let (start, end) = curve.domain(); + if (end - start).abs() < 1e-12 { + return None; + } + let normalised = (t - start) / (end - start); + let (left, right) = curve.split_at(normalised)?; + Some(( + nurbs_to_spline(&left, spl), + nurbs_to_spline(&right, spl), + )) +} + // ── Sampling ─────────────────────────────────────────────────────────────── -/// Sample the spline's DXF-XY projection at `n+1` evenly-spaced parameter -/// values. Returns `(t_params, xy_points)` both of length `n+1`. +/// Sample the spline's XY projection at `n+1` evenly spaced knot values. +/// Returns `(knot_params, xy_points)`, both of length `n+1`. pub fn spline_sample_xy(spl: &Spline, n: usize) -> (Vec, Vec<[f64; 2]>) { - let bs = match spline_to_bspline(spl) { - Some(b) => b, - None => return (vec![], vec![]), + let Some(curve) = spline_to_nurbs(spl) else { + return (vec![], vec![]); }; - let (t0, t1) = bs.range_tuple(); + let (t0, t1) = curve.domain(); let ts: Vec = (0..=n) .map(|i| t0 + (t1 - t0) * (i as f64 / n as f64)) .collect(); - let pts: Vec<[f64; 2]> = ts - .iter() - .map(|&t| { - let p = bs.subs(t); - [p.x, p.y] - }) - .collect(); + let pts: Vec<[f64; 2]> = ts.iter().map(|t| curve.point_at_knot(*t)).collect(); (ts, pts) } -/// Find the spline parameter `t` (in the knot-vector range [t0, t1]) that is -/// closest to the DXF-XY point `(x, y)`. -/// Uses 64-sample coarse search followed by bisection. +/// The knot parameter closest to the XY point `(x, y)`. pub fn spline_nearest_t(spl: &Spline, x: f64, y: f64) -> Option { - let bs = spline_to_bspline(spl)?; - let (t0, t1) = bs.range_tuple(); - let n = 64usize; - let step = (t1 - t0) / n as f64; - - // Coarse search - let mut best_t = t0; - let mut best_d = f64::MAX; - for i in 0..=n { - let t = t0 + i as f64 * step; - let p = bs.subs(t); - let d = (p.x - x).powi(2) + (p.y - y).powi(2); - if d < best_d { - best_d = d; - best_t = t; - } - } - - // Bisection refinement in [best_t - step, best_t + step] - let lo = (best_t - step).max(t0); - let hi = (best_t + step).min(t1); - let mut a = lo; - let mut b = hi; - for _ in 0..32 { - let m1 = a + (b - a) / 3.0; - let m2 = b - (b - a) / 3.0; - let p1 = bs.subs(m1); - let p2 = bs.subs(m2); - let d1 = (p1.x - x).powi(2) + (p1.y - y).powi(2); - let d2 = (p2.x - x).powi(2) + (p2.y - y).powi(2); - if d1 < d2 { - b = m2; - } else { - a = m1; - } - } - Some((a + b) * 0.5) + let curve = spline_to_nurbs(spl)?; + let (t0, t1) = curve.domain(); + let normalised = curve.parameter_at([x, y]); + Some(t0 + normalised * (t1 - t0)) } -/// Returns the normalised parameter in [0, 1] given an actual B-spline -/// parameter `t_actual` and the range `[t0, t1]`. +/// Normalise an actual knot parameter into `0..=1` over `[t0, t1]`. pub fn t_to_rel(t_actual: f64, t0: f64, t1: f64) -> f64 { if (t1 - t0).abs() < 1e-12 { return 0.0; @@ -131,19 +118,22 @@ pub fn t_to_rel(t_actual: f64, t0: f64, t1: f64) -> f64 { // ── Wire preview ─────────────────────────────────────────────────────────── -/// World-space wire points for a Spline (samples 64 segments). -/// Drawing plane is world XY: DXF (x, y) → world (x, y), z = elevation. +/// World-space wire points for a Spline. +/// +/// Sampled per knot span rather than uniformly, so a curve whose knots bunch +/// up is cut finely where its shape actually changes. pub fn spline_pts_wire(spl: &Spline) -> Vec<[f32; 3]> { - let (_, pts) = spline_sample_xy(spl, 64); - if pts.is_empty() { + 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); - pts.iter() + curve + .tessellate(16) + .iter() .map(|p| [p[0] as f32, p[1] as f32, elev]) .collect() } diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 3354decc5..b34062b71 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -28,12 +28,12 @@ use acadrust::entities::{ use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::DVec3; -use truck_modeling::base::{BoundedCurve, Cut, ParametricCurve}; +use acadrust::kernel::geom2d::nurbs::clamped_uniform_knots; use crate::command::{CadCommand, CmdResult}; use crate::modules::draw::modify::spline_ops::{ - bspline_to_spline, spline_nearest_t, spline_pts_wire, spline_sample_xy, spline_to_bspline, - t_to_rel, + spline_cut, spline_nearest_t, spline_pts_wire, spline_range, spline_sample_xy, + spline_to_nurbs, t_to_rel, }; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; @@ -943,11 +943,9 @@ fn extend_ellipse(orig: &EllipseEnt, t_click: f64, geos: &[Geo]) -> Option Vec { - let bs = match spline_to_bspline(spl) { - Some(b) => b, - None => return vec![], + let Some((t0, t1)) = spline_range(spl) else { + return vec![]; }; - let (t0, t1) = bs.range_tuple(); let range = t1 - t0; if range < 1e-12 { return vec![]; @@ -974,11 +972,9 @@ fn spline_seg_ts(spl: &SplineEnt, target: Handle, geos: &[Geo]) -> Vec { /// Trim a Spline entity. Returns surviving spline pieces (one or two). fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec { - let bs = match spline_to_bspline(spl) { - Some(b) => b, - None => return vec![], + let Some((t0, t1)) = spline_range(spl) else { + return vec![]; }; - let (t0, t1) = bs.range_tuple(); trim_intervals(ts, t_click) .into_iter() @@ -988,11 +984,15 @@ fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec { if t_hi - t_lo < 1e-9 { return None; } - let mut piece = bs.clone(); - let right = piece.cut(t_lo); // piece = [t0..t_lo] (discarded), right = [t_lo..t1] - let mut right = right; - let _tail = right.cut(t_hi); // right = [t_lo..t_hi], _tail discarded - Some(EntityType::Spline(bspline_to_spline(&right, spl))) + // Cut away everything before the interval, then everything after + // it, leaving [t_lo, t_hi]. + let (_, after_low) = spline_cut(spl, t_lo)?; + let kept = match spline_cut(&after_low, t_hi) { + Some((middle, _)) => middle, + // The interval reaches the far end, so nothing is left to cut. + None => after_low, + }; + Some(EntityType::Spline(kept)) }) .collect() } @@ -1003,22 +1003,22 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option1 virtual). // For splines we simply find whether the start (t=0) or end (t=1) is closer // to the click, then walk along that tangent direction to the nearest boundary. - let bs = spline_to_bspline(spl)?; - let (t0, t1) = bs.range_tuple(); + let curve = spline_to_nurbs(spl)?; + let (t0, t1) = curve.domain(); let extend_end = t_click >= 0.5; // Tangent at the endpoint (numerical, Δ = 1e-4 of range) let delta = (t1 - t0) * 1e-4; let (ep_t, tang_dir) = if extend_end { - let p0 = bs.subs(t1 - delta); - let p1 = bs.subs(t1); - (t1, [p1.x - p0.x, p1.y - p0.y]) + let p0 = curve.point_at_knot(t1 - delta); + let p1 = curve.point_at_knot(t1); + (t1, [p1[0] - p0[0], p1[1] - p0[1]]) } else { - let p0 = bs.subs(t0); - let p1 = bs.subs(t0 + delta); - (t0, [p0.x - p1.x, p0.y - p1.y]) // reverse for "before start" + let p0 = curve.point_at_knot(t0); + let p1 = curve.point_at_knot(t0 + delta); + (t0, [p0[0] - p1[0], p0[1] - p1[1]]) // reverse for "before start" }; - let ep = bs.subs(ep_t); + let ep = curve.point_at_knot(ep_t); let (dx, dy) = (tang_dir[0], tang_dir[1]); let len = (dx * dx + dy * dy).sqrt(); if len < 1e-12 { @@ -1027,14 +1027,14 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option 1e-6).reduce(f64::min)?; - let hit_x = ep.x + best_t * (ray_end_x - ep.x) * TRIM_EXTENT; - let hit_y = ep.y + best_t * (ray_end_y - ep.y) * TRIM_EXTENT; + let hit_x = ep[0] + best_t * (ray_end_x - ep[0]) * TRIM_EXTENT; + let hit_y = ep[1] + best_t * (ray_end_y - ep[1]) * TRIM_EXTENT; // Add a new control point at the hit location by appending/prepending. let z = spl.control_points.first().map(|v| v.z).unwrap_or(0.0); @@ -1053,8 +1053,7 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option { let t_click = spline_nearest_t(s, px, py) .and_then(|t_actual| { - let bs = spline_to_bspline(s)?; - let (t0, t1) = bs.range_tuple(); + let (t0, t1) = spline_range(s)?; Some(t_to_rel(t_actual, t0, t1)) }) .unwrap_or(0.5); @@ -3423,8 +3420,7 @@ impl CadCommand for TrimCommand { } let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64) .and_then(|t_actual| { - let bs = spline_to_bspline(s)?; - let (t0, t1) = bs.range_tuple(); + let (t0, t1) = spline_range(s)?; Some(t_to_rel(t_actual, t0, t1)) }) .unwrap_or(0.5); @@ -3918,8 +3914,7 @@ impl CadCommand for ExtendCommand { Some(EntityType::Spline(s)) => { let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64) .and_then(|t_actual| { - let bs = spline_to_bspline(s)?; - let (t0, t1) = bs.range_tuple(); + let (t0, t1) = spline_range(s)?; Some(t_to_rel(t_actual, t0, t1)) }) .unwrap_or(0.5);