feat: Spline support for TRIM, EXTEND, OFFSET, BREAK, and LENGTHEN

Add shared spline_ops module with:
- spline_to_bspline / bspline_to_spline: acadrust ↔ truck BSplineCurve conversion
- spline_sample_xy: sample DXF-XY projection at N+1 points with parameters
- spline_nearest_t: nearest-parameter search via coarse+bisection
- spline_pts_wire: world-space wire points for WireModel preview

TRIM: Spline entities can now be used as cutting boundaries (Geo::Spline
sampled into polyline segments) and as entities being trimmed; uses
BSplineCurve::cut() to produce surviving pieces.

EXTEND: extend_spline() shoots a tangent ray from the nearest endpoint
and appends a control point at the first boundary hit.

OFFSET: offset_spline() offsets each sample point along the local
perpendicular normal and rebuilds a new spline from the offset control pts.

BREAK: break_spline() uses spline_nearest_t() + BSplineCurve::cut() to
split the spline at one or two click points.

LENGTHEN: lengthen_spline() uses numerical arc-length integration +
bisection to cut the spline at the parameter corresponding to the new
requested length.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-07 15:00:22 +03:00
commit 083eb5cbb5
7 changed files with 591 additions and 11 deletions

View file

@ -7,7 +7,9 @@
//
// BREAK @ (at-sign as second point) → Break at a single point (splits without gap).
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline};
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt};
use truck_modeling::base::{BoundedCurve, Cut};
use crate::modules::home::modify::spline_ops::{bspline_to_spline, spline_nearest_t, spline_to_bspline};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::Vec3;
@ -39,6 +41,7 @@ pub fn break_entity(entity: &EntityType, p1: Vec3, p2: Vec3) -> Option<Vec<Entit
EntityType::Circle(c) => Some(break_circle(c, p1, p2)),
EntityType::LwPolyline(p) => Some(break_lwpolyline(p, p1, p2)),
EntityType::Ellipse(e) => Some(break_ellipse(e, p1, p2)),
EntityType::Spline(s) => Some(break_spline(s, p1, p2)),
_ => None,
}
}
@ -271,6 +274,52 @@ fn vec3_to_v3(v: Vec3) -> Vector3 {
Vector3::new(v.x as f64, v.y as f64, v.z as f64)
}
fn break_spline(spl: &SplineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
// Find the two nearest parameters to p1 and p2 (DXF XY: world x, z).
let t1 = match spline_nearest_t(spl, p1.x as f64, p1.z as f64) {
Some(t) => t,
None => return vec![EntityType::Spline(spl.clone())],
};
let t2 = match spline_nearest_t(spl, p2.x as f64, p2.z as f64) {
Some(t) => t,
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 (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)),
];
}
// 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)));
}
// 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)));
}
result
}
// ── CadCommand (simplified — break logic via CmdResult::BreakEntity) ───────
/// Thin wrapper for commands.rs to register the break command using the