diff --git a/Cargo.lock b/Cargo.lock index d59ee5617..bdc515374 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,7 +935,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#69a8bf2b747c8b0a4abe6a571859c4c3a4f610d2" +source = "git+https://github.com/HakanSeven12/cadkernel.git#5499242f1160018e066f78d4e462c9532e665a96" dependencies = [ "cavalier_contours", ] diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index dec9c4b73..b5c2703a5 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -12,6 +12,8 @@ // straightens every segment. use acadrust::entities::LwVertex; +use acadrust::kernel::geom2d::nurbs::clamped_uniform_knots; +use acadrust::kernel::geom2d::NurbsCurve; use acadrust::types::Vector2; use acadrust::{EntityType, Handle}; use glam::{DVec2, DVec3}; @@ -535,69 +537,61 @@ fn fit_curve(p: &mut acadrust::LwPolyline) -> bool { /// PEDIT Spline: replace the shape with a sampled uniform cubic B-spline of /// the vertex frame (8 samples per span; a closed frame wraps around). +/// Replace the polyline's vertices with a sampling of the cubic B-spline its +/// vertices control. +/// +/// Evaluated by the kernel rather than from a hand-written basis. The four +/// blending polynomials that were here are the uniform cubic ones spelled +/// out, with the open case clamped by repeating end points and then the two +/// ends pinned back afterwards to undo the drift that leaves — all of which +/// a clamped knot vector expresses directly and exactly. fn spline_smooth(p: &mut acadrust::LwPolyline) -> bool { - let n = p.vertices.len(); - if n < 3 { + const DEGREE: usize = 3; + const PER_SPAN: usize = 8; + + let control: Vec<[f64; 2]> = p.vertices.iter().map(|v| [v.location.x, v.location.y]).collect(); + if control.len() < 3 { return false; } - let pts: Vec = p.vertices.iter().map(vert_xy).collect(); - const S: usize = 8; - let bspline = |p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64| -> DVec2 { - let t2 = t * t; - let t3 = t2 * t; - (p0 * (-t3 + 3.0 * t2 - 3.0 * t + 1.0) - + p1 * (3.0 * t3 - 6.0 * t2 + 4.0) - + p2 * (-3.0 * t3 + 3.0 * t2 + 3.0 * t + 1.0) - + p3 * t3) - / 6.0 - }; - let mut out: Vec = Vec::new(); - if p.is_closed { - for i in 0..n { - let p0 = pts[(i + n - 1) % n]; - let p1 = pts[i]; - let p2 = pts[(i + 1) % n]; - let p3 = pts[(i + 2) % n]; - for s in 0..S { - out.push(bspline(p0, p1, p2, p3, s as f64 / S as f64)); - } - } + let curve = if p.is_closed { + // A periodic curve is written by wrapping the first `degree` control + // points onto the end, over a uniform knot vector — which is what + // makes the seam as smooth as everywhere else. + let mut wrapped = control.clone(); + wrapped.extend(control.iter().take(DEGREE).copied()); + let count = wrapped.len(); + let knots: Vec = (0..count + DEGREE + 1).map(|i| i as f64).collect(); + NurbsCurve::new(DEGREE, wrapped, knots, None) } else { - // Clamp the ends by repeating the end control points so the curve - // starts and finishes on them. - let at = |i: isize| -> DVec2 { pts[i.clamp(0, n as isize - 1) as usize] }; - for i in -1..(n as isize - 2) { - let (p0, p1, p2, p3) = (at(i), at(i + 1), at(i + 2), at(i + 3)); - let last_span = i == n as isize - 3; - let steps = if last_span { S + 1 } else { S }; - for s in 0..steps { - out.push(bspline(p0, p1, p2, p3, s as f64 / S as f64)); - } - } - // Pin the exact endpoints. - if let (Some(v), Some(q)) = (out.first_mut(), pts.first()) { - *v = *q; - } - if let (Some(v), Some(q)) = (out.last_mut(), pts.last()) { - *v = *q; - } - } - if out.len() < 2 { + // Clamped: the curve starts and finishes on its outer control points + // without them needing to be repeated. + let knots = clamped_uniform_knots(DEGREE, control.len()); + NurbsCurve::new(DEGREE, control.clone(), knots, None) + }; + let Some(curve) = curve else { + return false; + }; + + let steps = PER_SPAN * control.len(); + let sampled: Vec<[f64; 2]> = (0..=steps) + .map(|step| curve.point_at(step as f64 / steps as f64)) + .collect(); + if sampled.len() < 2 { return false; } - let w = p.constant_width; - p.vertices = out + + let width = p.constant_width; + p.vertices = sampled .into_iter() .map(|q| { - let mut v = LwVertex::new(Vector2::new(q.x, q.y)); - v.start_width = w; - v.end_width = w; + let mut v = LwVertex::new(Vector2::new(q[0], q[1])); + v.start_width = width; + v.end_width = width; v }) .collect(); true } - // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["PEDIT"] }); // PeditCommand diff --git a/src/modules/draw/modify/splinedit.rs b/src/modules/draw/modify/splinedit.rs index 75f1173ce..6a5852f73 100644 --- a/src/modules/draw/modify/splinedit.rs +++ b/src/modules/draw/modify/splinedit.rs @@ -9,9 +9,12 @@ // // Control-point dragging is already supported via the grip editing system. +use acadrust::types::Vector3; use acadrust::EntityType; use glam::DVec3; +use crate::modules::draw::modify::spline_ops::spline_to_nurbs; + use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; @@ -190,12 +193,40 @@ pub fn apply_spline_op(doc: &mut acadrust::CadDocument, handle: acadrust::Handle } } "__SPLINEDIT_REVERSE__" => { - spline.control_points.reverse(); spline.fit_points.reverse(); - spline.knots = acadrust::entities::Spline::generate_clamped_knots( - spline.degree as usize, - spline.control_points.len(), - ); + let begin = spline.begin_tangent; + spline.begin_tangent = spline.end_tangent; + spline.end_tangent = begin; + // The knots mirror within the domain rather than being + // regenerated. Rebuilding a clamped *uniform* vector here changed + // the shape of any spline whose knots were unevenly spaced — + // which is most of them once a modeller has been near one. + match spline_to_nurbs(spline).map(|curve| curve.reversed()) { + Some(reversed) => { + let elevation = spline + .control_points + .first() + .or_else(|| spline.fit_points.first()) + .map(|p| p.z) + .unwrap_or(0.0); + spline.degree = reversed.degree() as i32; + spline.knots = reversed.knots().to_vec(); + spline.control_points = reversed + .control_points() + .iter() + .map(|p| Vector3::new(p[0], p[1], elevation)) + .collect(); + spline.weights = if reversed.is_rational() { + reversed.weights().to_vec() + } else { + Vec::new() + }; + } + // Nothing the kernel can read — a spline with too few points + // to describe a curve. Reversing its stored points is still + // the honest answer. + None => spline.control_points.reverse(), + } } _ => {} }