Files
OneCAD-Draft/src/modules/draw/modify/lengthen.rs
T
Hakan SevenandClaude Opus 5 0f8f4864ba 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>
2026-08-08 16:55:07 +03:00

513 lines
16 KiB
Rust

// LENGTHEN command — extend or trim a Line or Arc by a specified delta or total.
//
// Options (entered as text after the entity pick):
// DE <value> — extend by delta (positive extends, negative trims)
// TO <value> — set total length (Line) or arc length (Arc)
// P <pct> — change by percentage (100 = no change, 150 = +50%)
//
// The entity is modified at whichever end is closest to the pick point.
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,
};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::{DVec3, Vec3};
use crate::t;
use crate::command::{CadCommand, CmdResult};
pub struct LengthenCommand {
state: LenState,
}
enum LenState {
PickEntity,
PickOption { handle: Handle, pick_pt: Vec3 },
}
impl LengthenCommand {
pub fn new() -> Self {
Self {
state: LenState::PickEntity,
}
}
}
impl CadCommand for LengthenCommand {
fn name(&self) -> &'static str {
"LENGTHEN"
}
fn prompt(&self) -> String {
match &self.state {
LenState::PickEntity => t!("LENGTHEN Select object:").into_owned(),
LenState::PickOption { .. } => {
t!("LENGTHEN Enter option [DE <delta> / TO <total> / P <pct>]:").into_owned()
}
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.state, LenState::PickEntity)
}
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { let pt = pt.as_vec3();
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.state = LenState::PickOption {
handle,
pick_pt: pt,
};
CmdResult::NeedPoint
}
fn wants_text_input(&self) -> bool {
matches!(self.state, LenState::PickOption { .. })
}
fn dyn_field(&self) -> crate::command::DynField {
if matches!(self.state, LenState::PickOption { .. }) {
crate::command::DynField::Scalar
} else {
crate::command::DynField::Point
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let (handle, pick_pt) = match &self.state {
LenState::PickOption { handle, pick_pt } => (*handle, *pick_pt),
_ => return None,
};
let pick_pt = pick_pt.as_dvec3();
let text = text.trim().to_uppercase();
if let Some(rest) = text.strip_prefix("DE ").or_else(|| text.strip_prefix("DE")) {
let delta: f64 = rest.trim().replace(',', ".").parse().ok()?;
Some(CmdResult::LengthenEntity {
handle,
pick_pt,
mode: LenMode::Delta(delta),
})
} else if let Some(rest) = text.strip_prefix("TO ").or_else(|| text.strip_prefix("TO")) {
let total: f64 = rest
.trim()
.replace(',', ".")
.parse()
.ok()
.filter(|&v: &f64| v > 0.0)?;
Some(CmdResult::LengthenEntity {
handle,
pick_pt,
mode: LenMode::Total(total),
})
} else if let Some(rest) = text.strip_prefix("P ").or_else(|| text.strip_prefix("P")) {
let pct: f64 = rest
.trim()
.replace(',', ".")
.parse()
.ok()
.filter(|&v: &f64| v > 0.0)?;
Some(CmdResult::LengthenEntity {
handle,
pick_pt,
mode: LenMode::Percent(pct),
})
} else {
// Try plain number as delta
let delta: f64 = text.replace(',', ".").parse().ok()?;
Some(CmdResult::LengthenEntity {
handle,
pick_pt,
mode: LenMode::Delta(delta),
})
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
// ── Mode enum (also used in CmdResult) ────────────────────────────────────
#[derive(Clone)]
pub enum LenMode {
Delta(f64),
Total(f64),
Percent(f64),
}
// ── Geometry ───────────────────────────────────────────────────────────────
/// Apply LENGTHEN to a Line, Arc, Ellipse, or Spline.
/// `pick_pt` determines which end to extend/trim (closest end is modified).
pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
match entity {
EntityType::Line(l) => lengthen_line(l, pick_pt, mode),
EntityType::Arc(a) => lengthen_arc(a, pick_pt, mode),
EntityType::Ellipse(e) => lengthen_ellipse(e, pick_pt, mode),
EntityType::Spline(s) => lengthen_spline(s, pick_pt, mode),
EntityType::LwPolyline(p) => lengthen_lwpoly(p, pick_pt, mode),
_ => None,
}
}
fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
// Keep the line's own coordinates on the f64 grid; only pick_pt (screen-only,
// nearest-end selection) widens for the distance compare.
let sx = line.start.x;
let sy = line.start.y;
let ex = line.end.x;
let ey = line.end.y;
let dx = ex - sx;
let dy = ey - sy;
let current_len = (dx * dx + dy * dy).sqrt();
if current_len < 1e-10 {
return None;
}
let new_len = apply_mode(current_len, mode)?;
if new_len < 1e-10 {
return None;
}
let ux = dx / current_len;
let uy = dy / current_len;
// Which end is closer to pick?
let px = pick_pt.x as f64;
let py = pick_pt.y as f64;
let dist_to_start = (px - sx).hypot(py - sy);
let dist_to_end = (px - ex).hypot(py - ey);
let mut result = line.clone();
result.common.handle = Handle::NULL;
if dist_to_end <= dist_to_start {
// Extend/trim the end
let new_x = sx + ux * new_len;
let new_y = sy + uy * new_len;
result.end = Vector3::new(new_x, new_y, line.end.z);
} else {
// Extend/trim the start (move start backward along dir)
let new_x = ex - ux * new_len;
let new_y = ey - uy * new_len;
result.start = Vector3::new(new_x, new_y, line.start.z);
}
Some(EntityType::Line(result))
}
fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
let cx = arc.center.x as f32;
let cy = arc.center.y as f32;
// Current arc span
let span = arc_span_rad(arc.start_angle, arc.end_angle);
let current_arc_len = arc.radius * span;
let new_arc_len = apply_mode(current_arc_len, mode)?;
if new_arc_len < 1e-10 {
return None;
}
let new_span = new_arc_len / arc.radius;
// Which end (start or end angle) is closer to pick?
let start_rad = arc.start_angle;
let end_rad = arc.end_angle;
let start_pt = Vec3::new(
cx + arc.radius as f32 * start_rad.cos() as f32,
pick_pt.y,
cy + arc.radius as f32 * start_rad.sin() as f32,
);
let end_pt = Vec3::new(
cx + arc.radius as f32 * end_rad.cos() as f32,
pick_pt.y,
cy + arc.radius as f32 * end_rad.sin() as f32,
);
let dist_start = (pick_pt - start_pt).length();
let dist_end = (pick_pt - end_pt).length();
let delta_span = new_span - span;
let mut result = arc.clone();
result.common.handle = Handle::NULL;
if dist_end <= dist_start {
// Extend end angle
result.end_angle = arc.start_angle + new_span;
} else {
// Extend start angle (move start backwards)
result.start_angle = arc.end_angle - new_span;
}
let _ = delta_span;
Some(EntityType::Arc(result))
}
fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
let a = (ell.major_axis.x.powi(2) + ell.major_axis.y.powi(2)).sqrt();
if a < 1e-9 {
return None;
}
let b = a * ell.minor_axis_ratio;
let nx = ell.major_axis.x / a;
let ny = ell.major_axis.y / a;
let t0 = ell.start_parameter;
let mut t1 = ell.end_parameter;
if t1 <= t0 {
t1 += std::f64::consts::TAU;
}
let span = t1 - t0;
// Approximate arc length via 128-point Gaussian quadrature estimate.
let arc_len_approx = |span: f64| -> f64 {
let n = 128usize;
let mut len = 0.0;
for i in 0..n {
let ti = t0 + span * (i as f64 / n as f64);
let tip = t0 + span * ((i + 1) as f64 / n as f64);
let xi = a * ti.cos() * nx - b * ti.sin() * ny + ell.center.x;
let yi = a * ti.cos() * ny + b * ti.sin() * nx + ell.center.y;
let xip = a * tip.cos() * nx - b * tip.sin() * ny + ell.center.x;
let yip = a * tip.cos() * ny + b * tip.sin() * nx + ell.center.y;
len += (xip - xi).hypot(yip - yi);
}
len
};
let current_len = arc_len_approx(span);
if current_len < 1e-10 {
return None;
}
let new_len = apply_mode(current_len, mode)?;
if new_len < 1e-10 {
return None;
}
// Find the new span via bisection so that arc_len_approx(new_span) ≈ new_len.
let max_span = std::f64::consts::TAU;
let mut lo = 0.0f64;
let mut hi = max_span;
for _ in 0..40 {
let mid = (lo + hi) * 0.5;
if arc_len_approx(mid) < new_len {
lo = mid;
} else {
hi = mid;
}
}
let new_span = (lo + hi) * 0.5;
// Determine which end is closer to pick_pt (use DXF XY plane).
let p_x = pick_pt.x as f64;
let p_y = pick_pt.y as f64;
let pt_start_x = ell.center.x + a * t0.cos() * nx - b * t0.sin() * ny;
let pt_start_y = ell.center.y + a * t0.cos() * ny + b * t0.sin() * nx;
let pt_end_x = ell.center.x + a * t1.cos() * nx - b * t1.sin() * ny;
let pt_end_y = ell.center.y + a * t1.cos() * ny + b * t1.sin() * nx;
let dist_start = (p_x - pt_start_x).hypot(p_y - pt_start_y);
let dist_end = (p_x - pt_end_x).hypot(p_y - pt_end_y);
let mut result = ell.clone();
result.common.handle = Handle::NULL;
if dist_end <= dist_start {
result.end_parameter = t0 + new_span;
} else {
result.start_parameter = t1 - new_span;
}
Some(EntityType::Ellipse(result))
}
fn apply_mode(current: f64, mode: &LenMode) -> Option<f64> {
match mode {
LenMode::Delta(d) => Some(current + d),
LenMode::Total(t) => Some(*t),
LenMode::Percent(p) => Some(current * p / 100.0),
}
}
fn arc_span_rad(start: f64, end: f64) -> f64 {
let span = (end - start).rem_euclid(std::f64::consts::TAU);
if span < 1e-6 {
std::f64::consts::TAU
} else {
span
}
}
fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
let n = poly.vertices.len();
if n < 2 {
return None;
}
// Determine which end is closer to the pick point (DXF XY: pick_pt.x, pick_pt.z).
let px = pick_pt.x as f64;
let py = pick_pt.y as f64;
let first = &poly.vertices[0];
let last = &poly.vertices[n - 1];
let d_first = (first.location.x - px).hypot(first.location.y - py);
let d_last = (last.location.x - px).hypot(last.location.y - py);
let at_end = d_last <= d_first;
// Terminal segment direction and current length.
let (sx, sy, ex, ey) = if at_end {
(
poly.vertices[n - 2].location.x,
poly.vertices[n - 2].location.y,
last.location.x,
last.location.y,
)
} else {
(
poly.vertices[1].location.x,
poly.vertices[1].location.y,
first.location.x,
first.location.y,
)
};
let dx = ex - sx;
let dy = ey - sy;
let current_len = (dx * dx + dy * dy).sqrt();
if current_len < 1e-10 {
return None;
}
let new_len = apply_mode(current_len, mode)?;
if new_len < 1e-10 {
return None;
}
let ux = dx / current_len;
let uy = dy / current_len;
let new_x = sx + ux * new_len;
let new_y = sy + uy * new_len;
let mut new_poly = poly.clone();
new_poly.common.handle = Handle::NULL;
if at_end {
let v = new_poly.vertices.last_mut()?;
v.location.x = new_x;
v.location.y = new_y;
} else {
let v = new_poly.vertices.first_mut()?;
v.location.x = new_x;
v.location.y = new_y;
}
Some(EntityType::LwPolyline(new_poly))
}
fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
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.
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 = curve.point_at_knot(ta);
let pb = curve.point_at_knot(tb);
len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
}
len
};
if arc_len < 1e-10 {
return None;
}
let new_len = apply_mode(arc_len, mode)?;
if new_len < 1e-10 {
return None;
}
// Determine which end (start or end) is closer to pick_pt.
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.
// Use bisection on cumulative arc length.
let fixed_t = if extend_end { t0 } else { t1 };
let delta_ratio = new_len / arc_len;
// Find t_new via bisection: cumulative_len(fixed_t..t_new) = new_len.
let cum_len = |t_end_param: f64| -> f64 {
let (lo, hi) = if extend_end {
(t0, t_end_param)
} else {
(t_end_param, t1)
};
if hi <= lo {
return 0.0;
}
let n = 32usize;
let mut len = 0.0f64;
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 = curve.point_at_knot(ta);
let pb = curve.point_at_knot(tb);
len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
}
len
};
let (mut lo_t, mut hi_t) = if extend_end {
(t0, t0 + delta_ratio * (t1 - t0) * 2.0)
} else {
(t1 - delta_ratio * (t1 - t0) * 2.0, t1)
};
// Clamp to valid range with some buffer for extension.
let buf = (t1 - t0) * 0.5;
lo_t = lo_t.max(t0 - buf);
hi_t = hi_t.min(t1 + buf);
for _ in 0..40 {
let mid = (lo_t + hi_t) * 0.5;
if cum_len(mid) < new_len {
if extend_end {
hi_t = mid;
} else {
lo_t = mid;
}
} else {
if extend_end {
lo_t = mid;
} else {
hi_t = mid;
}
}
}
let t_new = (lo_t + hi_t) * 0.5;
let _ = fixed_t;
// 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 }))
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["LENGTHEN"] }); // LengthenCommand