forked from 1b-agent/opencadstudio-upstream
Typed values were quantized to the f32 grid before reaching the entity (e.g. a length of 2000.8 committed as 2000.8020), worsening with magnitude. The interaction point layer was all glam::Vec3 (f32). Two-layer fix: 1. Coordinate backbone -> f64. last_point, last_cursor_world, dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord, the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance paths, command-line coordinate entry and the coordinate readout all compute in f64. Screen/GPU/alignment stays f32 (snap engine, render geometry, projection, UCS-icon/viewcube drawing) with casts only at those boundaries. 2. Per-command committed storage -> DVec3. on_point already received a DVec3, but many command impls narrowed it back to f32 (let pt = pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN, INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud, wipeout, attdef, table, tolerance, array center, mview, plot_window, plus the QDIM_PLACE handler and qdim_collect_points. Preview, hit-test and rubber-band paths keep f32 at the boundary. Hatch manual boundaries are left for a follow-up: HatchModel.boundary is structurally Arc<Vec<[f32;2]>> and needs a model-level widen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
521 lines
16 KiB
Rust
521 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::{bspline_to_spline, spline_to_bspline};
|
|
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 truck_modeling::base::{BoundedCurve, Cut};
|
|
|
|
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 => "LENGTHEN Select object:".into(),
|
|
LenState::PickOption { .. } => {
|
|
"LENGTHEN Enter option [DE <delta> / TO <total> / P <pct>]:".into()
|
|
}
|
|
}
|
|
}
|
|
|
|
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 bs = spline_to_bspline(spl)?;
|
|
let (t0, t1) = bs.range_tuple();
|
|
if (t1 - t0).abs() < 1e-12 {
|
|
return None;
|
|
}
|
|
|
|
// Approximate arc length via 64-point numerical integration.
|
|
use truck_modeling::base::ParametricCurve;
|
|
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 = bs.subs(ta);
|
|
let pb = bs.subs(tb);
|
|
len += (pb.x - pa.x).hypot(pb.y - pa.y);
|
|
}
|
|
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 = bs.subs(t0);
|
|
let p_end = bs.subs(t1);
|
|
let dist_start = (p_start.x - pick_pt.x as f64).hypot(p_start.y - pick_pt.y as f64);
|
|
let dist_end = (p_end.x - pick_pt.x as f64).hypot(p_end.y - 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 = bs.subs(ta);
|
|
let pb = bs.subs(tb);
|
|
len += (pb.x - pa.x).hypot(pb.y - pa.y);
|
|
}
|
|
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.
|
|
let mut piece = bs.clone();
|
|
if extend_end {
|
|
// Keep [t0, t_new]: cut at t_new
|
|
let _right = piece.cut(t_new.clamp(t0 + 1e-10, t1 * 2.0));
|
|
Some(EntityType::Spline(bspline_to_spline(&piece, spl)))
|
|
} else {
|
|
// Keep [t_new, t1]: cut at t_new, return the right portion
|
|
let right = piece.cut(t_new.clamp(t0 * 0.5, t1 - 1e-10));
|
|
Some(EntityType::Spline(bspline_to_spline(&right, spl)))
|
|
}
|
|
}
|
|
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["LENGTHEN"] }); // LengthenCommand
|