refactor(spline): evaluate splines with cadkernel, weights and all
The spline path ran on truck's BSplineCurve<Point3>, which is not rational. Weights were dropped on the way in and cleared on every split, so a NURBS circle — which is how most circles arrive once they have been through a modeller — was evaluated as a parabola and stayed one after a break. cadkernel's NurbsCurve carries the weights and applies them in homogeneous coordinates, and `spline_cut` splits by raising the cut to full knot multiplicity, so both halves are exact and both stay rational. `spline_ops` keeps the knot-domain call shapes the commands were written against — `spline_range` replaces reaching for `range_tuple`, `spline_cut` replaces `cut` plus the rebuild — so TRIM, BREAK, LENGTHEN and OFFSET changed only where they named those. Knot vectors for a rebuilt control polygon come from the kernel too, so the clamped uniform vector is defined once rather than assumed in two places. truck is now absent from the whole spline path. It remains for 3D solids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
95e61f3e8d
commit
0f8f4864ba
6 changed files with 154 additions and 183 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -945,7 +945,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git#feed535a0f72f9479d618a4999dba9bafcea250f"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git#33aa850ac2a27fdb2b305a67df27cb0eae23acc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@
|
||||||
//
|
//
|
||||||
// BREAK @ (at-sign as second point) → Break at a single point (splits without gap).
|
// BREAK @ (at-sign as second point) → Break at a single point (splits without gap).
|
||||||
|
|
||||||
use crate::modules::draw::modify::spline_ops::{
|
use crate::modules::draw::modify::spline_ops::{spline_cut, spline_nearest_t, spline_range};
|
||||||
bspline_to_spline, spline_nearest_t, spline_to_bspline,
|
|
||||||
};
|
|
||||||
use acadrust::entities::{
|
use acadrust::entities::{
|
||||||
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt,
|
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 acadrust::{EntityType, Handle};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
use truck_modeling::base::{BoundedCurve, Cut};
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
|
|
@ -307,37 +304,35 @@ fn break_spline(spl: &SplineEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||||
None => return vec![EntityType::Spline(spl.clone())],
|
None => return vec![EntityType::Spline(spl.clone())],
|
||||||
};
|
};
|
||||||
|
|
||||||
let bs = match spline_to_bspline(spl) {
|
let Some((t0, t_end)) = spline_range(spl) else {
|
||||||
Some(b) => b,
|
return vec![EntityType::Spline(spl.clone())];
|
||||||
None => return vec![EntityType::Spline(spl.clone())],
|
|
||||||
};
|
};
|
||||||
let (t0, t_end) = bs.range_tuple();
|
|
||||||
|
|
||||||
let (ta, tb) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
|
let (ta, tb) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
|
||||||
|
|
||||||
// Single-point break (ta ≈ tb): split into two segments at that point.
|
// Single-point break (ta ≈ tb): split into two segments at that point.
|
||||||
if (tb - ta).abs() < 1e-9 {
|
if (tb - ta).abs() < 1e-9 {
|
||||||
let mut piece = bs.clone();
|
return match spline_cut(spl, ta) {
|
||||||
let right = piece.cut(ta);
|
Some((left, right)) => {
|
||||||
return vec![
|
vec![EntityType::Spline(left), EntityType::Spline(right)]
|
||||||
EntityType::Spline(bspline_to_spline(&piece, spl)),
|
}
|
||||||
EntityType::Spline(bspline_to_spline(&right, spl)),
|
None => vec![EntityType::Spline(spl.clone())],
|
||||||
];
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two-point break: keep [t0..ta] and [tb..t_end], discard middle.
|
// Two-point break: keep [t0..ta] and [tb..t_end], discard middle.
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
// Left piece [t0, ta]
|
// Left piece [t0, ta]
|
||||||
if ta - t0 > 1e-9 {
|
if ta - t0 > 1e-9 {
|
||||||
let mut left = bs.clone();
|
if let Some((left, _)) = spline_cut(spl, ta) {
|
||||||
let _right = left.cut(ta); // left = [t0, ta]
|
result.push(EntityType::Spline(left));
|
||||||
result.push(EntityType::Spline(bspline_to_spline(&left, spl)));
|
}
|
||||||
}
|
}
|
||||||
// Right piece [tb, t_end]
|
// Right piece [tb, t_end]
|
||||||
if t_end - tb > 1e-9 {
|
if t_end - tb > 1e-9 {
|
||||||
let mut full = bs.clone();
|
if let Some((_, right)) = spline_cut(spl, tb) {
|
||||||
let right = full.cut(tb); // right = [tb, t_end]
|
result.push(EntityType::Spline(right));
|
||||||
result.push(EntityType::Spline(bspline_to_spline(&right, spl)));
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//
|
//
|
||||||
// The entity is modified at whichever end is closest to the pick point.
|
// 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::{
|
use acadrust::entities::{
|
||||||
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt,
|
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 acadrust::{EntityType, Handle};
|
||||||
use glam::{DVec3, Vec3};
|
use glam::{DVec3, Vec3};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
use truck_modeling::base::{BoundedCurve, Cut};
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
|
||||||
|
|
@ -411,23 +410,22 @@ fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||||
let bs = spline_to_bspline(spl)?;
|
let curve = spline_to_nurbs(spl)?;
|
||||||
let (t0, t1) = bs.range_tuple();
|
let (t0, t1) = curve.domain();
|
||||||
if (t1 - t0).abs() < 1e-12 {
|
if (t1 - t0).abs() < 1e-12 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approximate arc length via 64-point numerical integration.
|
// Approximate arc length via 64-point numerical integration.
|
||||||
use truck_modeling::base::ParametricCurve;
|
|
||||||
let arc_len = {
|
let arc_len = {
|
||||||
let n = 64usize;
|
let n = 64usize;
|
||||||
let mut len = 0.0f64;
|
let mut len = 0.0f64;
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
let ta = t0 + (t1 - t0) * (i as f64 / n as f64);
|
let ta = t0 + (t1 - t0) * (i as f64 / n as f64);
|
||||||
let tb = t0 + (t1 - t0) * ((i + 1) as f64 / n as f64);
|
let tb = t0 + (t1 - t0) * ((i + 1) as f64 / n as f64);
|
||||||
let pa = bs.subs(ta);
|
let pa = curve.point_at_knot(ta);
|
||||||
let pb = bs.subs(tb);
|
let pb = curve.point_at_knot(tb);
|
||||||
len += (pb.x - pa.x).hypot(pb.y - pa.y);
|
len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
|
||||||
}
|
}
|
||||||
len
|
len
|
||||||
};
|
};
|
||||||
|
|
@ -441,10 +439,10 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which end (start or end) is closer to pick_pt.
|
// Determine which end (start or end) is closer to pick_pt.
|
||||||
let p_start = bs.subs(t0);
|
let p_start = curve.point_at_knot(t0);
|
||||||
let p_end = bs.subs(t1);
|
let p_end = curve.point_at_knot(t1);
|
||||||
let dist_start = (p_start.x - pick_pt.x as f64).hypot(p_start.y - pick_pt.y as f64);
|
let dist_start = (p_start[0] - pick_pt.x as f64).hypot(p_start[1] - pick_pt.y as f64);
|
||||||
let dist_end = (p_end.x - pick_pt.x as f64).hypot(p_end.y - pick_pt.y as f64);
|
let dist_end = (p_end[0] - pick_pt.x as f64).hypot(p_end[1] - pick_pt.y as f64);
|
||||||
let extend_end = dist_end <= dist_start;
|
let extend_end = dist_end <= dist_start;
|
||||||
|
|
||||||
// Find the parameter `t_new` such that the arc length from the fixed end to t_new = new_len.
|
// Find the parameter `t_new` such that the arc length from the fixed end to t_new = new_len.
|
||||||
|
|
@ -467,9 +465,9 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
let ta = lo + (hi - lo) * (i as f64 / n as f64);
|
let ta = lo + (hi - lo) * (i as f64 / n as f64);
|
||||||
let tb = lo + (hi - lo) * ((i + 1) as f64 / n as f64);
|
let tb = lo + (hi - lo) * ((i + 1) as f64 / n as f64);
|
||||||
let pa = bs.subs(ta);
|
let pa = curve.point_at_knot(ta);
|
||||||
let pb = bs.subs(tb);
|
let pb = curve.point_at_knot(tb);
|
||||||
len += (pb.x - pa.x).hypot(pb.y - pa.y);
|
len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
|
||||||
}
|
}
|
||||||
len
|
len
|
||||||
};
|
};
|
||||||
|
|
@ -503,17 +501,10 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
|
||||||
let t_new = (lo_t + hi_t) * 0.5;
|
let t_new = (lo_t + hi_t) * 0.5;
|
||||||
let _ = fixed_t;
|
let _ = fixed_t;
|
||||||
|
|
||||||
// Split the spline at t_new.
|
// Split the spline at t_new and keep the side the pick asked for.
|
||||||
let mut piece = bs.clone();
|
let cut = t_new.clamp(t0 + 1e-10, t1 - 1e-10);
|
||||||
if extend_end {
|
let (left, right) = spline_cut(spl, cut)?;
|
||||||
// Keep [t0, t_new]: cut at t_new
|
Some(EntityType::Spline(if extend_end { left } else { right }))
|
||||||
let _right = piece.cut(t_new.clamp(t0 + 1e-10, t1 * 2.0));
|
|
||||||
Some(EntityType::Spline(bspline_to_spline(&piece, spl)))
|
|
||||||
} else {
|
|
||||||
// Keep [t_new, t1]: cut at t_new, return the right portion
|
|
||||||
let right = piece.cut(t_new.clamp(t0 * 0.5, t1 - 1e-10));
|
|
||||||
Some(EntityType::Spline(bspline_to_spline(&right, spl)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use acadrust::entities::{
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
// Polyline offsetting, and the angle normalisation that goes with it, come
|
// Polyline offsetting, and the angle normalisation that goes with it, come
|
||||||
// from the kernel; only the entity conversion stays here.
|
// from the kernel; only the entity conversion stays here.
|
||||||
|
use acadrust::kernel::geom2d::nurbs::clamped_uniform_knots;
|
||||||
use acadrust::kernel::geom2d::{
|
use acadrust::kernel::geom2d::{
|
||||||
normalize_angle as norm_rad, offset_polyline, Polyline as KernelPolyline,
|
normalize_angle as norm_rad, offset_polyline, Polyline as KernelPolyline,
|
||||||
PolylineVertex as KernelVertex,
|
PolylineVertex as KernelVertex,
|
||||||
|
|
@ -297,11 +298,10 @@ fn offset_spline(spl: &SplineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType
|
||||||
let degree = spl.degree.max(1) as usize;
|
let degree = spl.degree.max(1) as usize;
|
||||||
let new_ctrl: Vec<acadrust::types::Vector3> = offset_pts;
|
let new_ctrl: Vec<acadrust::types::Vector3> = offset_pts;
|
||||||
let n_ctrl = new_ctrl.len();
|
let n_ctrl = new_ctrl.len();
|
||||||
let kv = truck_modeling::KnotVec::uniform_knot(degree, n_ctrl - 1);
|
|
||||||
let mut new_spl = spl.clone();
|
let mut new_spl = spl.clone();
|
||||||
new_spl.common.handle = Handle::NULL;
|
new_spl.common.handle = Handle::NULL;
|
||||||
new_spl.control_points = new_ctrl;
|
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.fit_points.clear();
|
||||||
new_spl.weights.clear();
|
new_spl.weights.clear();
|
||||||
Some(EntityType::Spline(new_spl))
|
Some(EntityType::Spline(new_spl))
|
||||||
|
|
|
||||||
|
|
@ -1,127 +1,114 @@
|
||||||
// Shared B-spline utilities for modify commands (TRIM, BREAK, OFFSET, LENGTHEN).
|
// Shared B-spline utilities for modify commands (TRIM, BREAK, OFFSET, LENGTHEN).
|
||||||
//
|
//
|
||||||
// Provides:
|
// The evaluation lives in cadkernel; this file converts a drawing's SPLINE
|
||||||
// - spline_to_bspline : acadrust Spline → truck BSplineCurve<Point3>
|
// entity to and from it and keeps the knot-domain call shapes the commands
|
||||||
// - bspline_to_spline : truck BSplineCurve<Point3> → acadrust Spline
|
// were written against.
|
||||||
// - 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
|
// Weights are carried through now. The previous path evaluated splines with a
|
||||||
// - spline_pts_wire : world-space wire points for WireModel preview
|
// 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::entities::Spline;
|
||||||
|
use acadrust::kernel::geom2d::NurbsCurve;
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use truck_modeling::base::{BoundedCurve, ParametricCurve};
|
|
||||||
use truck_modeling::{BSplineCurve, KnotVec, Point3};
|
|
||||||
|
|
||||||
// ── Conversion ─────────────────────────────────────────────────────────────
|
// ── Conversion ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Convert an acadrust `Spline` to a truck `BSplineCurve<Point3>`.
|
/// Convert an acadrust `Spline` to a kernel `NurbsCurve`, weights included.
|
||||||
/// Returns `None` if the spline has too few control points or inconsistent data.
|
///
|
||||||
pub fn spline_to_bspline(spl: &Spline) -> Option<BSplineCurve<Point3>> {
|
/// Returns `None` when there are too few control points for the degree, which
|
||||||
let ctrl_pts: Vec<Point3> = spl
|
/// 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<NurbsCurve> {
|
||||||
|
let control_points: Vec<[f64; 2]> = spl
|
||||||
.control_points
|
.control_points
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| Point3::new(p.x, p.y, p.z))
|
.map(|p| [p.x, p.y])
|
||||||
.collect();
|
.collect();
|
||||||
if ctrl_pts.len() < 2 {
|
let degree = (spl.degree.max(1)) as usize;
|
||||||
return None;
|
let weights = (!spl.weights.is_empty()).then(|| spl.weights.clone());
|
||||||
}
|
NurbsCurve::new(degree, control_points, spl.knots.clone(), weights)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuild an acadrust `Spline` from a trimmed `BSplineCurve<Point3>`.
|
/// Rebuild an acadrust `Spline` from a kernel curve.
|
||||||
/// The `template` spline provides the entity common data, degree, and Z-elevation.
|
///
|
||||||
pub fn bspline_to_spline(bs: &BSplineCurve<Point3>, template: &Spline) -> Spline {
|
/// 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 = template.control_points.first().map(|v| v.z).unwrap_or(0.0);
|
||||||
let mut spl = template.clone();
|
let mut spl = template.clone();
|
||||||
spl.common.handle = Handle::NULL;
|
spl.common.handle = Handle::NULL;
|
||||||
spl.degree = bs.degree() as i32;
|
spl.degree = curve.degree() as i32;
|
||||||
spl.knots = bs.knot_vec().iter().copied().collect();
|
spl.knots = curve.knots().to_vec();
|
||||||
spl.control_points = bs
|
spl.control_points = curve
|
||||||
.control_points()
|
.control_points()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| Vector3::new(p.x, p.y, z))
|
.map(|p| Vector3::new(p[0], p[1], z))
|
||||||
.collect();
|
.collect();
|
||||||
spl.weights.clear(); // not rational after split
|
spl.weights = if curve.is_rational() {
|
||||||
spl.fit_points.clear(); // fit points are no longer valid
|
curve.weights().to_vec()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
// Fit points describe the original interpolation, which a split no longer
|
||||||
|
// satisfies.
|
||||||
|
spl.fit_points.clear();
|
||||||
spl
|
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 ───────────────────────────────────────────────────────────────
|
// ── Sampling ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Sample the spline's DXF-XY projection at `n+1` evenly-spaced parameter
|
/// Sample the spline's XY projection at `n+1` evenly spaced knot values.
|
||||||
/// values. Returns `(t_params, xy_points)` both of length `n+1`.
|
/// Returns `(knot_params, xy_points)`, both of length `n+1`.
|
||||||
pub fn spline_sample_xy(spl: &Spline, n: usize) -> (Vec<f64>, Vec<[f64; 2]>) {
|
pub fn spline_sample_xy(spl: &Spline, n: usize) -> (Vec<f64>, Vec<[f64; 2]>) {
|
||||||
let bs = match spline_to_bspline(spl) {
|
let Some(curve) = spline_to_nurbs(spl) else {
|
||||||
Some(b) => b,
|
return (vec![], vec![]);
|
||||||
None => return (vec![], vec![]),
|
|
||||||
};
|
};
|
||||||
let (t0, t1) = bs.range_tuple();
|
let (t0, t1) = curve.domain();
|
||||||
let ts: Vec<f64> = (0..=n)
|
let ts: Vec<f64> = (0..=n)
|
||||||
.map(|i| t0 + (t1 - t0) * (i as f64 / n as f64))
|
.map(|i| t0 + (t1 - t0) * (i as f64 / n as f64))
|
||||||
.collect();
|
.collect();
|
||||||
let pts: Vec<[f64; 2]> = ts
|
let pts: Vec<[f64; 2]> = ts.iter().map(|t| curve.point_at_knot(*t)).collect();
|
||||||
.iter()
|
|
||||||
.map(|&t| {
|
|
||||||
let p = bs.subs(t);
|
|
||||||
[p.x, p.y]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
(ts, pts)
|
(ts, pts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the spline parameter `t` (in the knot-vector range [t0, t1]) that is
|
/// The knot parameter closest to the XY point `(x, y)`.
|
||||||
/// closest to the DXF-XY point `(x, y)`.
|
|
||||||
/// Uses 64-sample coarse search followed by bisection.
|
|
||||||
pub fn spline_nearest_t(spl: &Spline, x: f64, y: f64) -> Option<f64> {
|
pub fn spline_nearest_t(spl: &Spline, x: f64, y: f64) -> Option<f64> {
|
||||||
let bs = spline_to_bspline(spl)?;
|
let curve = spline_to_nurbs(spl)?;
|
||||||
let (t0, t1) = bs.range_tuple();
|
let (t0, t1) = curve.domain();
|
||||||
let n = 64usize;
|
let normalised = curve.parameter_at([x, y]);
|
||||||
let step = (t1 - t0) / n as f64;
|
Some(t0 + normalised * (t1 - t0))
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the normalised parameter in [0, 1] given an actual B-spline
|
/// Normalise an actual knot parameter into `0..=1` over `[t0, t1]`.
|
||||||
/// parameter `t_actual` and the range `[t0, t1]`.
|
|
||||||
pub fn t_to_rel(t_actual: f64, t0: f64, t1: f64) -> f64 {
|
pub fn t_to_rel(t_actual: f64, t0: f64, t1: f64) -> f64 {
|
||||||
if (t1 - t0).abs() < 1e-12 {
|
if (t1 - t0).abs() < 1e-12 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|
@ -131,19 +118,22 @@ pub fn t_to_rel(t_actual: f64, t0: f64, t1: f64) -> f64 {
|
||||||
|
|
||||||
// ── Wire preview ───────────────────────────────────────────────────────────
|
// ── Wire preview ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// World-space wire points for a Spline (samples 64 segments).
|
/// World-space wire points for a Spline.
|
||||||
/// Drawing plane is world XY: DXF (x, y) → world (x, y), z = elevation.
|
///
|
||||||
|
/// 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]> {
|
pub fn spline_pts_wire(spl: &Spline) -> Vec<[f32; 3]> {
|
||||||
let (_, pts) = spline_sample_xy(spl, 64);
|
let Some(curve) = spline_to_nurbs(spl) else {
|
||||||
if pts.is_empty() {
|
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
};
|
||||||
let elev = spl
|
let elev = spl
|
||||||
.control_points
|
.control_points
|
||||||
.first()
|
.first()
|
||||||
.map(|v| v.z as f32)
|
.map(|v| v.z as f32)
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
pts.iter()
|
curve
|
||||||
|
.tessellate(16)
|
||||||
|
.iter()
|
||||||
.map(|p| [p[0] as f32, p[1] as f32, elev])
|
.map(|p| [p[0] as f32, p[1] as f32, elev])
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@ use acadrust::entities::{
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
use glam::DVec3;
|
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::command::{CadCommand, CmdResult};
|
||||||
use crate::modules::draw::modify::spline_ops::{
|
use crate::modules::draw::modify::spline_ops::{
|
||||||
bspline_to_spline, spline_nearest_t, spline_pts_wire, spline_sample_xy, spline_to_bspline,
|
spline_cut, spline_nearest_t, spline_pts_wire, spline_range, spline_sample_xy,
|
||||||
t_to_rel,
|
spline_to_nurbs, t_to_rel,
|
||||||
};
|
};
|
||||||
use crate::modules::IconKind;
|
use crate::modules::IconKind;
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
@ -943,11 +943,9 @@ fn extend_ellipse(orig: &EllipseEnt, t_click: f64, geos: &[Geo]) -> Option<Entit
|
||||||
/// Find normalised t-params ∈ [0,1] where a Spline intersects boundary geos.
|
/// Find normalised t-params ∈ [0,1] where a Spline intersects boundary geos.
|
||||||
/// Uses sampled polyline segments for intersection detection.
|
/// Uses sampled polyline segments for intersection detection.
|
||||||
fn spline_seg_ts(spl: &SplineEnt, target: Handle, geos: &[Geo]) -> Vec<f64> {
|
fn spline_seg_ts(spl: &SplineEnt, target: Handle, geos: &[Geo]) -> Vec<f64> {
|
||||||
let bs = match spline_to_bspline(spl) {
|
let Some((t0, t1)) = spline_range(spl) else {
|
||||||
Some(b) => b,
|
return vec![];
|
||||||
None => return vec![],
|
|
||||||
};
|
};
|
||||||
let (t0, t1) = bs.range_tuple();
|
|
||||||
let range = t1 - t0;
|
let range = t1 - t0;
|
||||||
if range < 1e-12 {
|
if range < 1e-12 {
|
||||||
return vec![];
|
return vec![];
|
||||||
|
|
@ -974,11 +972,9 @@ fn spline_seg_ts(spl: &SplineEnt, target: Handle, geos: &[Geo]) -> Vec<f64> {
|
||||||
|
|
||||||
/// Trim a Spline entity. Returns surviving spline pieces (one or two).
|
/// Trim a Spline entity. Returns surviving spline pieces (one or two).
|
||||||
fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
||||||
let bs = match spline_to_bspline(spl) {
|
let Some((t0, t1)) = spline_range(spl) else {
|
||||||
Some(b) => b,
|
return vec![];
|
||||||
None => return vec![],
|
|
||||||
};
|
};
|
||||||
let (t0, t1) = bs.range_tuple();
|
|
||||||
|
|
||||||
trim_intervals(ts, t_click)
|
trim_intervals(ts, t_click)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -988,11 +984,15 @@ fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
||||||
if t_hi - t_lo < 1e-9 {
|
if t_hi - t_lo < 1e-9 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut piece = bs.clone();
|
// Cut away everything before the interval, then everything after
|
||||||
let right = piece.cut(t_lo); // piece = [t0..t_lo] (discarded), right = [t_lo..t1]
|
// it, leaving [t_lo, t_hi].
|
||||||
let mut right = right;
|
let (_, after_low) = spline_cut(spl, t_lo)?;
|
||||||
let _tail = right.cut(t_hi); // right = [t_lo..t_hi], _tail discarded
|
let kept = match spline_cut(&after_low, t_hi) {
|
||||||
Some(EntityType::Spline(bspline_to_spline(&right, spl)))
|
Some((middle, _)) => middle,
|
||||||
|
// The interval reaches the far end, so nothing is left to cut.
|
||||||
|
None => after_low,
|
||||||
|
};
|
||||||
|
Some(EntityType::Spline(kept))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -1003,22 +1003,22 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityTy
|
||||||
// the current start (t<0 virtual) or end (t>1 virtual).
|
// the current start (t<0 virtual) or end (t>1 virtual).
|
||||||
// For splines we simply find whether the start (t=0) or end (t=1) is closer
|
// 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.
|
// to the click, then walk along that tangent direction to the nearest boundary.
|
||||||
let bs = spline_to_bspline(spl)?;
|
let curve = spline_to_nurbs(spl)?;
|
||||||
let (t0, t1) = bs.range_tuple();
|
let (t0, t1) = curve.domain();
|
||||||
let extend_end = t_click >= 0.5;
|
let extend_end = t_click >= 0.5;
|
||||||
|
|
||||||
// Tangent at the endpoint (numerical, Δ = 1e-4 of range)
|
// Tangent at the endpoint (numerical, Δ = 1e-4 of range)
|
||||||
let delta = (t1 - t0) * 1e-4;
|
let delta = (t1 - t0) * 1e-4;
|
||||||
let (ep_t, tang_dir) = if extend_end {
|
let (ep_t, tang_dir) = if extend_end {
|
||||||
let p0 = bs.subs(t1 - delta);
|
let p0 = curve.point_at_knot(t1 - delta);
|
||||||
let p1 = bs.subs(t1);
|
let p1 = curve.point_at_knot(t1);
|
||||||
(t1, [p1.x - p0.x, p1.y - p0.y])
|
(t1, [p1[0] - p0[0], p1[1] - p0[1]])
|
||||||
} else {
|
} else {
|
||||||
let p0 = bs.subs(t0);
|
let p0 = curve.point_at_knot(t0);
|
||||||
let p1 = bs.subs(t0 + delta);
|
let p1 = curve.point_at_knot(t0 + delta);
|
||||||
(t0, [p0.x - p1.x, p0.y - p1.y]) // reverse for "before start"
|
(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 (dx, dy) = (tang_dir[0], tang_dir[1]);
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
if len < 1e-12 {
|
if len < 1e-12 {
|
||||||
|
|
@ -1027,14 +1027,14 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityTy
|
||||||
let (dx, dy) = (dx / len, dy / len);
|
let (dx, dy) = (dx / len, dy / len);
|
||||||
|
|
||||||
// Shoot a ray from the endpoint along the tangent and find nearest boundary.
|
// Shoot a ray from the endpoint along the tangent and find nearest boundary.
|
||||||
let ray_end_x = ep.x + dx * TRIM_EXTENT;
|
let ray_end_x = ep[0] + dx * TRIM_EXTENT;
|
||||||
let ray_end_y = ep.y + dy * TRIM_EXTENT;
|
let ray_end_y = ep[1] + dy * TRIM_EXTENT;
|
||||||
let seg_ts = line_seg_ts(ep.x, ep.y, ray_end_x, ray_end_y, spl.common.handle, geos);
|
let seg_ts = line_seg_ts(ep[0], ep[1], ray_end_x, ray_end_y, spl.common.handle, geos);
|
||||||
|
|
||||||
let best_t = seg_ts.into_iter().filter(|&t| t > 1e-6).reduce(f64::min)?;
|
let best_t = seg_ts.into_iter().filter(|&t| t > 1e-6).reduce(f64::min)?;
|
||||||
|
|
||||||
let hit_x = ep.x + best_t * (ray_end_x - ep.x) * TRIM_EXTENT;
|
let hit_x = ep[0] + best_t * (ray_end_x - ep[0]) * TRIM_EXTENT;
|
||||||
let hit_y = ep.y + best_t * (ray_end_y - ep.y) * 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.
|
// 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);
|
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<EntityTy
|
||||||
// Rebuild knots (uniform) for the extended control polygon.
|
// Rebuild knots (uniform) for the extended control polygon.
|
||||||
let degree = new_spl.degree as usize;
|
let degree = new_spl.degree as usize;
|
||||||
let n = new_spl.control_points.len();
|
let n = new_spl.control_points.len();
|
||||||
let kv = truck_modeling::KnotVec::uniform_knot(degree, n - 1);
|
new_spl.knots = clamped_uniform_knots(degree, n);
|
||||||
new_spl.knots = kv.iter().copied().collect();
|
|
||||||
Some(EntityType::Spline(new_spl))
|
Some(EntityType::Spline(new_spl))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2264,8 +2263,7 @@ fn pick_trim_at(
|
||||||
}
|
}
|
||||||
let t_click = spline_nearest_t(s, px, py)
|
let t_click = spline_nearest_t(s, px, py)
|
||||||
.and_then(|t_actual| {
|
.and_then(|t_actual| {
|
||||||
let bs = spline_to_bspline(s)?;
|
let (t0, t1) = spline_range(s)?;
|
||||||
let (t0, t1) = bs.range_tuple();
|
|
||||||
Some(t_to_rel(t_actual, t0, t1))
|
Some(t_to_rel(t_actual, t0, t1))
|
||||||
})
|
})
|
||||||
.unwrap_or(0.5);
|
.unwrap_or(0.5);
|
||||||
|
|
@ -2334,8 +2332,7 @@ fn pick_extend_at(
|
||||||
Some(EntityType::Spline(s)) => {
|
Some(EntityType::Spline(s)) => {
|
||||||
let t_click = spline_nearest_t(s, px, py)
|
let t_click = spline_nearest_t(s, px, py)
|
||||||
.and_then(|t_actual| {
|
.and_then(|t_actual| {
|
||||||
let bs = spline_to_bspline(s)?;
|
let (t0, t1) = spline_range(s)?;
|
||||||
let (t0, t1) = bs.range_tuple();
|
|
||||||
Some(t_to_rel(t_actual, t0, t1))
|
Some(t_to_rel(t_actual, t0, t1))
|
||||||
})
|
})
|
||||||
.unwrap_or(0.5);
|
.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)
|
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
|
||||||
.and_then(|t_actual| {
|
.and_then(|t_actual| {
|
||||||
let bs = spline_to_bspline(s)?;
|
let (t0, t1) = spline_range(s)?;
|
||||||
let (t0, t1) = bs.range_tuple();
|
|
||||||
Some(t_to_rel(t_actual, t0, t1))
|
Some(t_to_rel(t_actual, t0, t1))
|
||||||
})
|
})
|
||||||
.unwrap_or(0.5);
|
.unwrap_or(0.5);
|
||||||
|
|
@ -3918,8 +3914,7 @@ impl CadCommand for ExtendCommand {
|
||||||
Some(EntityType::Spline(s)) => {
|
Some(EntityType::Spline(s)) => {
|
||||||
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
|
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
|
||||||
.and_then(|t_actual| {
|
.and_then(|t_actual| {
|
||||||
let bs = spline_to_bspline(s)?;
|
let (t0, t1) = spline_range(s)?;
|
||||||
let (t0, t1) = bs.range_tuple();
|
|
||||||
Some(t_to_rel(t_actual, t0, t1))
|
Some(t_to_rel(t_actual, t0, t1))
|
||||||
})
|
})
|
||||||
.unwrap_or(0.5);
|
.unwrap_or(0.5);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue