forked from 1b-agent/opencadstudio-upstream
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:
1 parent
95e61f3e8d
commit
0f8f4864ba
6 files changed
+154
-183
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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<EntityType> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<E
|
||||
}
|
||||
|
||||
fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||
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<Ent
|
||||
}
|
||||
|
||||
// Determine which end (start or end) is closer to pick_pt.
|
||||
let p_start = bs.subs(t0);
|
||||
let p_end = bs.subs(t1);
|
||||
let dist_start = (p_start.x - pick_pt.x as f64).hypot(p_start.y - 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 p_start = curve.point_at_knot(t0);
|
||||
let p_end = curve.point_at_knot(t1);
|
||||
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[0] - pick_pt.x as f64).hypot(p_end[1] - pick_pt.y as f64);
|
||||
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.
|
||||
@@ -467,9 +465,9 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
|
||||
for i in 0..n {
|
||||
let ta = lo + (hi - lo) * (i as f64 / n as f64);
|
||||
let tb = lo + (hi - lo) * ((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
|
||||
};
|
||||
@@ -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 _ = fixed_t;
|
||||
|
||||
// Split the spline at t_new.
|
||||
let mut piece = bs.clone();
|
||||
if extend_end {
|
||||
// Keep [t0, t_new]: cut at t_new
|
||||
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)))
|
||||
}
|
||||
// Split the spline at t_new and keep the side the pick asked for.
|
||||
let cut = t_new.clamp(t0 + 1e-10, t1 - 1e-10);
|
||||
let (left, right) = spline_cut(spl, cut)?;
|
||||
Some(EntityType::Spline(if extend_end { left } else { right }))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use acadrust::entities::{
|
||||
use acadrust::{EntityType, Handle};
|
||||
// Polyline offsetting, and the angle normalisation that goes with it, come
|
||||
// from the kernel; only the entity conversion stays here.
|
||||
use acadrust::kernel::geom2d::nurbs::clamped_uniform_knots;
|
||||
use acadrust::kernel::geom2d::{
|
||||
normalize_angle as norm_rad, offset_polyline, Polyline as KernelPolyline,
|
||||
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 new_ctrl: Vec<acadrust::types::Vector3> = 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))
|
||||
|
||||
@@ -1,127 +1,114 @@
|
||||
// Shared B-spline utilities for modify commands (TRIM, BREAK, OFFSET, LENGTHEN).
|
||||
//
|
||||
// Provides:
|
||||
// - spline_to_bspline : acadrust Spline → truck BSplineCurve<Point3>
|
||||
// - bspline_to_spline : truck BSplineCurve<Point3> → 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<Point3>`.
|
||||
/// Returns `None` if the spline has too few control points or inconsistent data.
|
||||
pub fn spline_to_bspline(spl: &Spline) -> Option<BSplineCurve<Point3>> {
|
||||
let ctrl_pts: Vec<Point3> = 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<NurbsCurve> {
|
||||
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<Point3>`.
|
||||
/// The `template` spline provides the entity common data, degree, and Z-elevation.
|
||||
pub fn bspline_to_spline(bs: &BSplineCurve<Point3>, 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<f64>, 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<f64> = (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<f64> {
|
||||
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()
|
||||
}
|
||||
@@ -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<Entit
|
||||
/// Find normalised t-params ∈ [0,1] where a Spline intersects boundary geos.
|
||||
/// Uses sampled polyline segments for intersection detection.
|
||||
fn spline_seg_ts(spl: &SplineEnt, target: Handle, geos: &[Geo]) -> Vec<f64> {
|
||||
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<f64> {
|
||||
|
||||
/// Trim a Spline entity. Returns surviving spline pieces (one or two).
|
||||
fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
||||
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<EntityType> {
|
||||
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]) -> Option<EntityTy
|
||||
// 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
|
||||
// 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<EntityTy
|
||||
let (dx, dy) = (dx / len, dy / len);
|
||||
|
||||
// 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_y = ep.y + dy * TRIM_EXTENT;
|
||||
let seg_ts = line_seg_ts(ep.x, ep.y, ray_end_x, ray_end_y, spl.common.handle, geos);
|
||||
let ray_end_x = ep[0] + dx * TRIM_EXTENT;
|
||||
let ray_end_y = ep[1] + dy * TRIM_EXTENT;
|
||||
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 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<EntityTy
|
||||
// Rebuild knots (uniform) for the extended control polygon.
|
||||
let degree = new_spl.degree as usize;
|
||||
let n = new_spl.control_points.len();
|
||||
let kv = truck_modeling::KnotVec::uniform_knot(degree, n - 1);
|
||||
new_spl.knots = kv.iter().copied().collect();
|
||||
new_spl.knots = clamped_uniform_knots(degree, n);
|
||||
Some(EntityType::Spline(new_spl))
|
||||
}
|
||||
|
||||
@@ -2264,8 +2263,7 @@ fn pick_trim_at(
|
||||
}
|
||||
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);
|
||||
@@ -2334,8 +2332,7 @@ fn pick_extend_at(
|
||||
Some(EntityType::Spline(s)) => {
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user