forked from 1b-agent/opencadstudio-upstream
648 lines
23 KiB
Rust
648 lines
23 KiB
Rust
// LENGTHEN command — extend or trim a Line or Arc by a specified delta or total.
|
|
//
|
|
// 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%)
|
|
//
|
|
// 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 crate::t;
|
|
use codec::entities::Spline as SplineEnt;
|
|
use codec::types::Vector3;
|
|
use codec::{EntityType, Handle};
|
|
use glam::{DVec3, Vec3};
|
|
use kernel::geom2d::Curve;
|
|
|
|
use crate::command::{CadCommand, CmdResult};
|
|
|
|
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 {
|
|
ChooseMode,
|
|
Value(ValueMode),
|
|
Apply(LenMode),
|
|
DynamicPick,
|
|
DynamicPoint {
|
|
handle: Handle,
|
|
entity: EntityType,
|
|
pick: DVec3,
|
|
},
|
|
}
|
|
|
|
impl LengthenCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
state: LenState::ChooseMode,
|
|
picked: None,
|
|
measurement: None,
|
|
edits: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CadCommand for LengthenCommand {
|
|
fn name(&self) -> &'static str {
|
|
"LENGTHEN"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
match &self.state {
|
|
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 <{}>:",
|
|
crate::entities::common::format_angle(
|
|
LENGTHEN_DEFAULTS.lock().unwrap().delta_angle.to_radians(),
|
|
)
|
|
),
|
|
LenState::Value(ValueMode::TotalAngle) => format!(
|
|
"LENGTHEN Enter total angle <{}>:",
|
|
crate::entities::common::format_angle(
|
|
LENGTHEN_DEFAULTS.lock().unwrap().total_angle.to_radians(),
|
|
)
|
|
),
|
|
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 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(),
|
|
}
|
|
}
|
|
|
|
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(_) | EntityType::Ellipse(_)
|
|
) =>
|
|
{
|
|
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::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> {
|
|
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 angle_mode = matches!(mode, ValueMode::DeltaAngle | ValueMode::TotalAngle);
|
|
let parsed = if angle_mode {
|
|
crate::entities::common::parse_typed_angle(&upper).map(f64::to_degrees)
|
|
} else {
|
|
upper.replace(',', ".").parse::<f64>().ok()
|
|
};
|
|
let Some(value) = parsed.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 {
|
|
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 {
|
|
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,
|
|
}
|
|
};
|
|
let text = if matches!(mode, ValueMode::DeltaAngle | ValueMode::TotalAngle) {
|
|
format!("{value}d")
|
|
} else {
|
|
value.to_string()
|
|
};
|
|
self.on_text_input(&text).unwrap_or(CmdResult::NeedPoint)
|
|
}
|
|
_ => CmdResult::Cancel,
|
|
}
|
|
}
|
|
}
|
|
// ── Mode enum (also used in CmdResult) ────────────────────────────────────
|
|
|
|
#[derive(Clone)]
|
|
pub enum LenMode {
|
|
Delta(f64),
|
|
Total(f64),
|
|
Percent(f64),
|
|
DeltaAngle(f64),
|
|
TotalAngle(f64),
|
|
Dynamic(DVec3),
|
|
}
|
|
|
|
// ── 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> {
|
|
lengthen_entity_precise(entity, pick_pt.as_dvec3(), mode)
|
|
}
|
|
|
|
fn lengthen_entity_precise(entity: &EntityType, pick: DVec3, mode: &LenMode) -> Option<EntityType> {
|
|
use kernel::space::lengthen::{lengthen_arc, lengthen_line, LengthChange};
|
|
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(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(ellipse) => {
|
|
let curve = crate::entities::curve::ellipse_curve(ellipse)?;
|
|
let (start, end) =
|
|
kernel::space::lengthen::lengthen_ellipse(&curve, pick.to_array(), change)?;
|
|
let mut result = ellipse.clone();
|
|
result.start_parameter = start;
|
|
result.end_parameter = end;
|
|
Some(EntityType::Ellipse(result))
|
|
}
|
|
EntityType::Spline(s) => lengthen_spline(s, pick.as_vec3(), mode),
|
|
EntityType::LwPolyline(polyline) => {
|
|
let curve = crate::entities::curve::lwpolyline_curve(polyline)?;
|
|
let vertices =
|
|
kernel::space::lengthen::lengthen_polyline(&curve, pick.to_array(), change)?;
|
|
let mut result = polyline.clone();
|
|
result.vertices = vertices
|
|
.into_iter()
|
|
.map(|vertex| {
|
|
let mut value = polyline.vertices[vertex.source].clone();
|
|
value.location.x = vertex.position[0];
|
|
value.location.y = vertex.position[1];
|
|
value.bulge = vertex.bulge;
|
|
let width_delta = value.end_width - value.start_width;
|
|
value.end_width = value.start_width + width_delta * vertex.end_fraction;
|
|
value.start_width += width_delta * vertex.start_fraction;
|
|
value
|
|
})
|
|
.collect();
|
|
if result.vertices.iter().any(|v| {
|
|
!v.start_width.is_finite()
|
|
|| !v.end_width.is_finite()
|
|
|| v.start_width < 0.0
|
|
|| v.end_width < 0.0
|
|
}) {
|
|
return None;
|
|
}
|
|
Some(EntityType::LwPolyline(result))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
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),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
|
let nurbs = spline_to_nurbs(spl)?;
|
|
let (t0, t1) = nurbs.domain();
|
|
if (t1 - t0).abs() < 1e-12 {
|
|
return None;
|
|
}
|
|
let curve = Curve::Nurbs(nurbs.clone());
|
|
let arc_len = curve.length();
|
|
if arc_len < 1e-10 {
|
|
return None;
|
|
}
|
|
let new_len = apply_mode(arc_len, mode)?;
|
|
if new_len < 1e-10 || new_len >= arc_len {
|
|
// A spline is shortened by splitting it, so there is nothing to keep
|
|
// if the new length is the whole of it or more. Extending would mean
|
|
// continuing the curve past its own control polygon, which is a
|
|
// different operation from cutting one.
|
|
return None;
|
|
}
|
|
|
|
let p_start = nurbs.point_at_knot(t0);
|
|
let p_end = nurbs.point_at_knot(t1);
|
|
let (px, py) = (pick_pt.x as f64, pick_pt.y as f64);
|
|
let extend_end =
|
|
(p_end[0] - px).hypot(p_end[1] - py) <= (p_start[0] - px).hypot(p_start[1] - py);
|
|
|
|
// Where to cut, by distance along the curve rather than by a bisection
|
|
// over repeated chord sums. Keeping the head means cutting `new_len` from
|
|
// the start; keeping the tail means cutting what is left over.
|
|
let along = if extend_end {
|
|
new_len
|
|
} else {
|
|
arc_len - new_len
|
|
};
|
|
let at = curve.parameter_at_distance(along);
|
|
let cut = (t0 + at * (t1 - t0)).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
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn spatial_line() -> EntityType {
|
|
EntityType::Line(codec::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 = codec::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);
|
|
}
|
|
|
|
#[test]
|
|
fn polyline_lengthening_preserves_plane_and_interpolates_widths() {
|
|
let mut polyline = codec::entities::LwPolyline::new();
|
|
polyline.normal = Vector3::new(0.0, 1.0, 0.0);
|
|
polyline.elevation = 5.0;
|
|
let mut start = codec::entities::LwVertex::from_coords(0.0, 0.0);
|
|
start.start_width = 2.0;
|
|
start.end_width = 4.0;
|
|
polyline.vertices = vec![start, codec::entities::LwVertex::from_coords(10.0, 0.0)];
|
|
let entity = EntityType::LwPolyline(polyline.clone());
|
|
let pick = DVec3::from_array(
|
|
crate::entities::curve::lwpolyline_curve(&polyline)
|
|
.unwrap()
|
|
.point_at(1.0),
|
|
);
|
|
|
|
let EntityType::LwPolyline(result) =
|
|
lengthen_entity_precise(&entity, pick, &LenMode::Total(15.0)).unwrap()
|
|
else {
|
|
panic!("expected polyline")
|
|
};
|
|
assert_eq!(result.normal, polyline.normal);
|
|
assert_eq!(result.elevation, polyline.elevation);
|
|
assert!((result.vertices.last().unwrap().location.x - 15.0).abs() < 1e-12);
|
|
assert!((result.vertices[0].start_width - 2.0).abs() < 1e-12);
|
|
assert!((result.vertices[0].end_width - 5.0).abs() < 1e-12);
|
|
}
|
|
}
|