Match LENGTHEN command behavior

This commit is contained in:
ramox81 committed 2026-09-11 10:23:43 +03:00
1 parent 7d0c059b30
commit d10b50778c
1 file changed
+190 -172
+190 -172
View File
@@ -1,6 +1,6 @@
// LENGTHEN command — extend or trim a Line or Arc by a specified delta or total.
//
// Options (entered as text after the entity pick):
// Choose an option, enter its value, then pick objects repeatedly:
// 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%)
@@ -9,7 +9,7 @@
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,
Ellipse as EllipseEnt, LwPolyline, Spline as SplineEnt,
};
use cadkernel::geom2d::{
Curve, Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc,
@@ -25,120 +25,191 @@ const TAU: f64 = std::f64::consts::TAU;
pub struct LengthenCommand {
state: LenState,
picked: Option<EntityType>,
measurement: Option<f64>,
edits: usize,
}
#[derive(Clone, Copy)]
enum ValueMode { Delta, Total, Percent, DeltaAngle, TotalAngle, Dynamic }
struct LengthenDefaults { mode: ValueMode, delta: f64, total: f64, percent: f64, delta_angle: f64, total_angle: f64 }
static LENGTHEN_DEFAULTS: std::sync::Mutex<LengthenDefaults> = std::sync::Mutex::new(
LengthenDefaults { mode: ValueMode::Total, delta: 0.0, total: 1.0, percent: 100.0, delta_angle: 0.0, total_angle: 180.0 / std::f64::consts::PI }
);
enum LenState {
PickEntity,
PickOption { handle: Handle, pick_pt: Vec3 },
ChooseMode,
Value(ValueMode),
Apply(LenMode),
DynamicPick,
DynamicPoint { handle: Handle, entity: EntityType, pick: DVec3 },
}
impl LengthenCommand {
pub fn new() -> Self {
Self {
state: LenState::PickEntity,
}
Self { state: LenState::ChooseMode, picked: None, measurement: None, edits: 0 }
}
}
impl CadCommand for LengthenCommand {
fn name(&self) -> &'static str {
"LENGTHEN"
}
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()
LenState::ChooseMode => {
let prompt = t!("LENGTHEN Select an object to measure or [Delta/Percent/Total/Dynamic]:");
self.measurement.map_or_else(|| prompt.to_string(), |length| format!("Length: {length:.4} {prompt}"))
}
LenState::Value(ValueMode::Delta) => format!("LENGTHEN Enter delta length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().delta),
LenState::Value(ValueMode::Total) => format!("LENGTHEN Enter total length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().total),
LenState::Value(ValueMode::Percent) => format!("LENGTHEN Enter percentage length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().percent),
LenState::Value(ValueMode::DeltaAngle) => format!("LENGTHEN Enter delta angle <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().delta_angle),
LenState::Value(ValueMode::TotalAngle) => format!("LENGTHEN Enter total angle <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().total_angle),
LenState::DynamicPoint { .. } => "LENGTHEN Specify new end point:".into(),
LenState::Apply(_) | LenState::DynamicPick | LenState::Value(ValueMode::Dynamic) => t!("LENGTHEN Select an object to change or [Undo]:").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;
fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption;
match self.state {
LenState::ChooseMode => vec![CmdOption::new("Delta", "DE"), CmdOption::new("Percent", "P"), CmdOption::new("Total", "TO"), CmdOption::new("Dynamic", "DY")],
LenState::Value(ValueMode::Delta | ValueMode::Total) => vec![CmdOption::new("Angle", "A")],
LenState::Apply(_) | LenState::DynamicPick => vec![CmdOption::new("Undo", "U")],
_ => Vec::new(),
}
self.state = LenState::PickOption {
handle,
pick_pt: pt,
};
CmdResult::NeedPoint
}
fn wants_text_input(&self) -> bool {
matches!(self.state, LenState::PickOption { .. })
fn needs_entity_pick(&self) -> bool { matches!(self.state, LenState::ChooseMode | LenState::Apply(_) | LenState::DynamicPick) }
fn inject_before_entity_pick(&self) -> bool { true }
fn inject_picked_entity(&mut self, entity: EntityType) { self.picked = Some(entity); }
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
if handle.is_null() { return CmdResult::NeedPoint; }
let Some(entity) = self.picked.take() else { return CmdResult::NeedPoint; };
match &self.state {
LenState::ChooseMode => {
self.measurement = crate::entities::curve::entity_curve(&entity)
.map(|curve| curve.curve.length()).filter(|length| length.is_finite());
CmdResult::NeedPoint
}
LenState::Apply(mode) => match lengthen_entity_precise(&entity, pt, mode) {
Some(replacement) => CmdResult::ReplaceManyContinue(vec![(handle, vec![replacement])]),
None => CmdResult::NeedPoint,
},
LenState::DynamicPick if matches!(entity, EntityType::Line(_) | EntityType::Arc(_)) => {
self.state = LenState::DynamicPoint { handle, entity, pick: pt };
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
}
}
fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) { self.edits += 1; }
fn wants_text_input(&self) -> bool { true }
fn dyn_commit_as_text(&self) -> bool { matches!(self.state, LenState::Value(_)) }
fn dyn_auto_sign_angle(&self) -> bool { false }
fn dyn_field(&self) -> crate::command::DynField {
if matches!(self.state, LenState::PickOption { .. }) {
crate::command::DynField::Scalar
} else {
crate::command::DynField::Point
}
if matches!(self.state, LenState::Value(ValueMode::DeltaAngle | ValueMode::TotalAngle)) { crate::command::DynField::Angle }
else if matches!(self.state, LenState::Value(_)) { 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),
})
if matches!(self.state, LenState::DynamicPoint { .. }) { return None; }
let upper = text.trim().to_uppercase();
if upper.is_empty() { return Some(self.on_enter()); }
if matches!(self.state, LenState::Apply(_) | LenState::DynamicPick) {
return Some(if matches!(upper.as_str(), "U" | "UNDO") && self.edits > 0 {
self.edits -= 1;
CmdResult::UndoDocument
} else { CmdResult::NeedPoint });
}
if matches!(self.state, LenState::ChooseMode) {
let mut parts = upper.split_whitespace();
let mode = match parts.next().unwrap_or("") {
"D" | "DE" | "DELTA" => ValueMode::Delta,
"T" | "TO" | "TOTAL" => ValueMode::Total,
"P" | "PERCENT" => ValueMode::Percent,
"DY" | "DYNAMIC" => ValueMode::Dynamic,
_ => return Some(CmdResult::NeedPoint),
};
LENGTHEN_DEFAULTS.lock().unwrap().mode = mode;
self.state = if matches!(mode, ValueMode::Dynamic) { LenState::DynamicPick } else { LenState::Value(mode) };
if let Some(value) = parts.next() { return self.on_text_input(value); }
return Some(CmdResult::NeedPoint);
}
let LenState::Value(mode) = self.state else { return Some(CmdResult::NeedPoint); };
if matches!(upper.as_str(), "A" | "ANGLE") {
self.state = match mode {
ValueMode::Delta => LenState::Value(ValueMode::DeltaAngle),
ValueMode::Total => LenState::Value(ValueMode::TotalAngle),
_ => return Some(CmdResult::NeedPoint),
};
return Some(CmdResult::NeedPoint);
}
let Some(value) = upper.replace(',', ".").parse::<f64>().ok().filter(|v| v.is_finite()) else {
return Some(CmdResult::NeedPoint);
};
if !matches!(mode, ValueMode::Delta | ValueMode::DeltaAngle) && value <= 0.0 { return Some(CmdResult::NeedPoint); }
if matches!(mode, ValueMode::TotalAngle) && value >= 360.0 { return Some(CmdResult::NeedPoint); }
{
let mut defaults = LENGTHEN_DEFAULTS.lock().unwrap();
match mode { ValueMode::Delta => defaults.delta = value, ValueMode::Total => defaults.total = value, ValueMode::Percent => defaults.percent = value,
ValueMode::DeltaAngle => defaults.delta_angle = value, ValueMode::TotalAngle => defaults.total_angle = value, ValueMode::Dynamic => {} }
}
self.state = LenState::Apply(match mode {
ValueMode::Delta => LenMode::Delta(value),
ValueMode::Total => LenMode::Total(value),
ValueMode::Percent => LenMode::Percent(value),
ValueMode::DeltaAngle => LenMode::DeltaAngle(value.to_radians()),
ValueMode::TotalAngle => LenMode::TotalAngle(value.to_radians()),
ValueMode::Dynamic => return Some(CmdResult::NeedPoint),
});
Some(CmdResult::NeedPoint)
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let LenState::DynamicPoint { handle, entity, pick } = &self.state {
if let Some(replacement) = lengthen_entity_precise(entity, *pick, &LenMode::Dynamic(pt)) {
let handle = *handle;
self.state = LenState::DynamicPick;
return CmdResult::ReplaceManyContinue(vec![(handle, vec![replacement])]);
}
}
CmdResult::NeedPoint
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<crate::scene::model::wire_model::WireModel> {
let LenState::DynamicPoint { entity, pick, .. } = &self.state else { return None; };
let replacement = lengthen_entity_precise(entity, *pick, &LenMode::Dynamic(pt))?;
let curve = crate::entities::curve::entity_curve(&replacement)?;
let points = crate::entities::curve::curve_points(&curve).into_iter()
.map(|p| [p[0] as f32, p[1] as f32, p[2] as f32]).collect();
Some(crate::scene::model::wire_model::WireModel::solid(
"lengthen_dynamic_preview".into(), points,
crate::scene::model::wire_model::WireModel::CYAN, false,
))
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
match self.state {
LenState::ChooseMode => {
let mode = LENGTHEN_DEFAULTS.lock().unwrap().mode;
self.state = if matches!(mode, ValueMode::Dynamic) { LenState::DynamicPick } else { LenState::Value(mode) };
CmdResult::NeedPoint
}
LenState::Value(mode) => {
let value = {
let defaults = LENGTHEN_DEFAULTS.lock().unwrap();
match mode { ValueMode::Delta => defaults.delta, ValueMode::Total => defaults.total, ValueMode::Percent => defaults.percent,
ValueMode::DeltaAngle => defaults.delta_angle, ValueMode::TotalAngle => defaults.total_angle, ValueMode::Dynamic => 0.0 }
};
self.on_text_input(&value.to_string()).unwrap_or(CmdResult::NeedPoint)
}
_ => CmdResult::Cancel,
}
}
}
// ── Mode enum (also used in CmdResult) ────────────────────────────────────
#[derive(Clone)]
@@ -146,6 +217,9 @@ pub enum LenMode {
Delta(f64),
Total(f64),
Percent(f64),
DeltaAngle(f64),
TotalAngle(f64),
Dynamic(DVec3),
}
// ── Geometry ───────────────────────────────────────────────────────────────
@@ -153,96 +227,48 @@ pub enum LenMode {
/// 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> {
lengthen_entity_precise(entity, pick_pt.as_dvec3(), mode)
}
fn lengthen_entity_precise(entity: &EntityType, pick: DVec3, mode: &LenMode) -> Option<EntityType> {
use cadkernel::space::lengthen::{LengthChange, lengthen_line, lengthen_arc};
let change = match mode {
LenMode::Delta(value) => LengthChange::Delta(*value),
LenMode::Total(value) => LengthChange::Total(*value),
LenMode::Percent(value) => LengthChange::Percent(*value),
LenMode::DeltaAngle(value) => LengthChange::DeltaAngle(*value),
LenMode::TotalAngle(value) => LengthChange::TotalAngle(*value),
LenMode::Dynamic(point) => LengthChange::Dynamic(point.to_array()),
};
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::Line(line) => {
let [start, end] = lengthen_line(
[line.start.x, line.start.y, line.start.z],
[line.end.x, line.end.y, line.end.z], pick.to_array(), change)?;
let mut result = line.clone();
result.common.handle = Handle::NULL;
result.start = Vector3::new(start[0], start[1], start[2]);
result.end = Vector3::new(end[0], end[1], end[2]);
Some(EntityType::Line(result))
}
EntityType::Arc(arc) => {
let (start, end) = lengthen_arc(&crate::entities::curve::arc_curve(arc), pick.to_array(), change)?;
let mut result = arc.clone();
result.common.handle = Handle::NULL;
result.start_angle = start;
result.end_angle = end;
Some(EntityType::Arc(result))
}
EntityType::Ellipse(e) => lengthen_ellipse(e, pick.as_vec3(), mode),
EntityType::Spline(s) => lengthen_spline(s, pick.as_vec3(), mode),
EntityType::LwPolyline(p) => {
let p = crate::entities::curve::lwpolyline_world_xy(p)?;
lengthen_lwpoly(&p, pick_pt, mode)
lengthen_lwpoly(&p, pick.as_vec3(), 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> {
// 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 || new_arc_len >= std::f64::consts::TAU * arc.radius - 1.0e-9 {
return None;
}
let new_span = new_arc_len / arc.radius;
// Which end (start or end angle) is closer to pick?
let curve = crate::entities::curve::arc_curve(arc);
let start_pt = Vec3::from_array(curve.point_at(0.0).map(|value| value as f32));
let end_pt = Vec3::from_array(curve.point_at(1.0).map(|value| value as f32));
let dist_start = (pick_pt - start_pt).length();
let dist_end = (pick_pt - end_pt).length();
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;
}
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 {
@@ -318,15 +344,7 @@ fn apply_mode(current: f64, mode: &LenMode) -> Option<f64> {
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(TAU);
if span < 1e-6 {
TAU
} else {
span
_ => None,
}
}