Merge pull request #1193 from ramox81/codex/complete-lengthen-options

This commit is contained in:
Hakan Seven committed 2026-09-11 12:42:06 +03:00
commit 9e74df4c67
2 files changed
+325 -173

No files matched your search

+49 -1
View File
@@ -4,10 +4,11 @@
// 1. Select objects (Enter to finish selection)
// 2. First source point → first destination point
// 3. Second source point → second destination point (Enter to skip = translate only)
// 4. Enter = apply (scale = optional: Y/N prompt after 2nd pair)
// 4. Continue for a third pair, or press Enter to choose two-pair scaling
//
// With 1 pair: pure translation (src1 → dst1)
// With 2 pairs: translate + rotate (+ optional uniform scale to fit)
// With 3 pairs: rigid 3D placement without scaling
use acadrust::Handle;
use glam::DVec3;
@@ -303,3 +304,50 @@ impl AlignCommand {
}
inventory::submit!(crate::command::CommandRegistration { names: &["ALIGN"] }); // AlignCommand
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn three_pairs_apply_a_rigid_spatial_frame() {
let handle = Handle::new(9);
let mut command = AlignCommand::with_selection(vec![handle]);
let source = [DVec3::ZERO, DVec3::X, DVec3::Y];
let origin = DVec3::new(5.0, 6.0, 7.0);
let target = [origin, origin + DVec3::Y, origin + DVec3::Z];
assert!(matches!(command.on_point(DVec3::splat(f64::NAN)), CmdResult::NeedPoint));
for point in [source[0], target[0], source[1], target[1]] {
assert!(matches!(command.on_point(point), CmdResult::NeedPoint));
}
assert!(matches!(command.on_point(DVec3::X * 2.0), CmdResult::NeedPoint));
assert!(matches!(command.state, AlignState::Src3));
assert!(matches!(command.on_point(source[2]), CmdResult::NeedPoint));
let CmdResult::TransformSelected(handles, EntityTransform::Affine(transform)) =
command.on_point(target[2])
else {
panic!("third destination must complete alignment");
};
assert_eq!(handles, vec![handle]);
for (from, expected) in source.into_iter().zip(target) {
let actual = transform.apply(acadrust::types::Vector3::new(from.x, from.y, from.z));
let actual = DVec3::new(actual.x, actual.y, actual.z);
assert!(actual.abs_diff_eq(expected, 1.0e-12));
}
}
#[test]
fn one_pair_still_translates_without_requesting_a_frame() {
let handle = Handle::new(3);
let mut command = AlignCommand::with_selection(vec![handle]);
assert!(matches!(command.on_point(DVec3::new(1.0, 2.0, 3.0)), CmdResult::NeedPoint));
assert!(matches!(command.on_point(DVec3::new(4.0, 6.0, 8.0)), CmdResult::NeedPoint));
assert!(matches!(
command.on_enter(),
CmdResult::TransformSelected(handles, EntityTransform::Translate(delta))
if handles == vec![handle] && delta == DVec3::new(3.0, 4.0, 5.0)
));
}
}
+276 -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 }
);
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,
}
}
@@ -436,3 +454,89 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["LENGTHEN"] }); // LengthenCommand
#[cfg(test)]
mod tests {
use super::*;
fn spatial_line() -> EntityType {
EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 4.0),
))
}
#[test]
fn repeated_delta_edits_offer_one_undo_per_replacement() {
let handle = Handle::new(11);
let mut command = LengthenCommand::new();
assert!(matches!(command.on_text_input("DE 2"), Some(CmdResult::NeedPoint)));
for _ in 0..2 {
command.inject_picked_entity(spatial_line());
let CmdResult::ReplaceManyContinue(replacements) =
command.on_entity_pick(handle, DVec3::new(0.0, 0.0, 4.0))
else {
panic!("delta edit must replace the picked line");
};
let EntityType::Line(line) = &replacements[0].1[0] else {
panic!("replacement must stay a line");
};
assert_eq!(line.end, Vector3::new(0.0, 0.0, 6.0));
command.on_entity_replaced(handle, &[]);
}
assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument)));
assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument)));
assert!(matches!(command.on_text_input("U"), Some(CmdResult::NeedPoint)));
}
#[test]
fn dynamic_line_endpoint_is_projected_onto_its_spatial_support() {
let handle = Handle::new(12);
let mut command = LengthenCommand::new();
assert!(matches!(command.on_text_input("DY"), Some(CmdResult::NeedPoint)));
command.inject_picked_entity(spatial_line());
assert!(matches!(
command.on_entity_pick(handle, DVec3::new(0.0, 0.0, 4.0)),
CmdResult::NeedPoint
));
let CmdResult::ReplaceManyContinue(replacements) =
command.on_point(DVec3::new(2.0, -3.0, 7.0))
else {
panic!("dynamic point must replace the line");
};
let EntityType::Line(line) = &replacements[0].1[0] else {
panic!("replacement must stay a line");
};
assert_eq!(line.start, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(line.end, Vector3::new(0.0, 0.0, 7.0));
assert!(command.needs_entity_pick());
}
#[test]
fn circular_arc_accepts_total_angle_in_radians() {
let arc = acadrust::entities::Arc::from_center_radius_angles(
Vector3::ZERO,
2.0,
0.0,
std::f64::consts::FRAC_PI_2,
);
let EntityType::Arc(result) = lengthen_entity_precise(
&EntityType::Arc(arc),
DVec3::new(0.0, 2.0, 0.0),
&LenMode::TotalAngle(std::f64::consts::PI),
)
.expect("valid angular total")
else {
panic!("replacement must stay an arc");
};
assert!((result.end_angle - result.start_angle - std::f64::consts::PI).abs() < 1.0e-12);
}
#[test]
fn default_total_angle_is_a_half_turn_in_degrees() {
assert_eq!(LENGTHEN_DEFAULTS.lock().unwrap().total_angle, 180.0);
}
}