feat: EXTEND and LENGTHEN now support LwPolyline

Both commands detect which terminal segment/vertex is nearest to the
pick point, compute the new endpoint along the segment direction, and
move only that terminal vertex — preserving all other vertices and bulge
values intact.  ROADMAP updated: EXTEND and LENGTHEN marked .

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-07 15:05:44 +03:00
commit 5a682a365a
3 changed files with 192 additions and 10 deletions

View file

@ -116,9 +116,9 @@ Underlay (PDF/DWF/DGN)
| ARRAY Path | ✅ | — |
| BREAK (BR) | ✅ | — |
| TRIM (TR) | ✅ | — |
| EXTEND (EX) | 🔧 | LwPolyline |
| EXTEND (EX) | ✅ | — |
| OFFSET (O) | ✅ | — |
| LENGTHEN (LEN) | 🔧 | LwPolyline |
| LENGTHEN (LEN) | ✅ | — |
| FILLET (F) | ✅ | — |
| CHAMFER (CHA) | ✅ | — |
| JOIN (J) | ✅ | — |
@ -302,7 +302,7 @@ Underlay (PDF/DWF/DGN)
## Öncelik Sırası (Bir Sonraki Adımlar)
### Yüksek Öncelik
1. **EXTEND / LENGTHEN → LwPolyline desteği**
1. **Solid3D tessellation** tamamlama (ACIS → truck pipeline)
### Orta Öncelik
5. **Solid3D tessellation** tamamlama (ACIS → truck pipeline)

View file

@ -7,7 +7,7 @@
//
// The entity is modified at whichever end is closest to the pick point.
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, Spline as SplineEnt};
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt};
use truck_modeling::base::{BoundedCurve, Cut, ParametricCurve};
use crate::modules::home::modify::spline_ops::{bspline_to_spline, spline_nearest_t, spline_to_bspline};
use acadrust::types::Vector3;
@ -98,10 +98,11 @@ pub enum LenMode {
/// `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::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,
}
}
@ -266,6 +267,56 @@ fn xz_to_v3(v: Vec3, z: f64) -> Vector3 {
Vector3::new(v.x as f64, v.y as f64, z)
}
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.z as f64; // Y-up: world Z = DXF Y
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();

View file

@ -10,7 +10,7 @@
use std::f64::consts::TAU;
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, Ray as RayEnt, Spline as SplineEnt, XLine as XLineEnt};
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Ray as RayEnt, Spline as SplineEnt, XLine as XLineEnt};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::Vec3;
@ -976,7 +976,116 @@ fn trim_arc(orig: &ArcEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
.collect()
}
// ── Extend helper ─────────────────────────────────────────────────────────
// ── Extend helpers ────────────────────────────────────────────────────────
/// Extend the first or last segment of an LwPolyline to the nearest boundary.
/// Click point (DXF XY) determines which end to extend.
fn extend_lwpoly(poly: &LwPolyline, click_x: f64, click_y: f64, geos: &[Geo]) -> Option<EntityType> {
let n = poly.vertices.len();
if n < 2 { return None; }
let first = &poly.vertices[0];
let second = &poly.vertices[1];
let last = &poly.vertices[n - 1];
let prev = &poly.vertices[n - 2];
let d_first = (first.location.x - click_x).hypot(first.location.y - click_y);
let d_last = (last.location.x - click_x).hypot(last.location.y - click_y);
let extend_end = d_last <= d_first;
// Extract the terminal segment as a virtual line.
let (ax, ay, bx, by) = if extend_end {
(prev.location.x, prev.location.y, last.location.x, last.location.y)
} else {
(second.location.x, second.location.y, first.location.x, first.location.y)
};
let (dx, dy) = (bx - ax, by - ay);
let len2 = dx * dx + dy * dy;
if len2 < 1e-12 { return None; }
// t_click on the segment: 0 = ax/ay, 1 = bx/by. We're extending beyond t=1.
let target = poly.common.handle;
let mut best_t = f64::INFINITY;
for geo in geos {
match geo {
Geo::Line { handle, p1, p2 } => {
if *handle == target { continue; }
let (ex, ey) = (p2[0] - p1[0], p2[1] - p1[1]);
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
if (-1e-9..=1.0 + 1e-9).contains(&u) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Arc { handle, cx, cy, r, a0, a1 } => {
if *handle == target { continue; }
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
let ix = ax + t * dx;
let iy = ay + t * dy;
if in_arc((iy - cy).atan2(ix - cx), *a0, *a1) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Circle { handle, cx, cy, r } => {
if *handle == target { continue; }
for t in lc(ax, ay, dx, dy, *cx, *cy, *r) {
if t > 1.0 + 1e-6 && t < best_t { best_t = t; }
}
}
Geo::Ray { handle, bx: rbx, by: rby, dx: rdx, dy: rdy } => {
if *handle == target { continue; }
if let Some((t, u)) = ll(ax, ay, dx, dy, *rbx, *rby, *rdx, *rdy) {
if u >= -1e-9 && t > 1.0 + 1e-6 && t < best_t { best_t = t; }
}
}
Geo::InfLine { handle, bx: ibx, by: iby, dx: idx, dy: idy } => {
if *handle == target { continue; }
if let Some((t, _)) = ll(ax, ay, dx, dy, *ibx, *iby, *idx, *idy) {
if t > 1.0 + 1e-6 && t < best_t { best_t = t; }
}
}
Geo::Ellipse { handle, cx, cy, a, b, nx, ny, t0, t1 } => {
if *handle == target { continue; }
for (t, t_ell) in le(ax, ay, dx, dy, *cx, *cy, *a, *b, *nx, *ny) {
if in_arc(t_ell, *t0, *t1) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
Geo::Spline { handle, segs } => {
if *handle == target { continue; }
for (p1, p2) in segs {
let ex = p2[0] - p1[0]; let ey = p2[1] - p1[1];
if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) {
if (-1e-9..=1.0+1e-9).contains(&u) && t > 1.0 + 1e-6 && t < best_t {
best_t = t;
}
}
}
}
}
}
if !best_t.is_finite() { return None; }
let new_x = ax + best_t * dx;
let new_y = ay + best_t * dy;
let mut new_poly = poly.clone();
new_poly.common.handle = Handle::NULL;
if extend_end {
let last_v = new_poly.vertices.last_mut()?;
last_v.location.x = new_x;
last_v.location.y = new_y;
} else {
let first_v = new_poly.vertices.first_mut()?;
first_v.location.x = new_x;
first_v.location.y = new_y;
}
Some(EntityType::LwPolyline(new_poly))
}
/// Extend a Line to the nearest boundary on the extended side.
/// t_click < 0.5 → extend start (look for t < 0); t_click ≥ 0.5 → extend end (t > 1).
@ -1268,6 +1377,19 @@ fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {
ellipse_pts(e.center.x, e.center.y, a, b, nx, ny, t0, t1, e.center.z)
}
EntityType::Spline(s) => spline_pts_wire(s),
EntityType::LwPolyline(p) => {
let elev = p.elevation as f32;
let n = p.vertices.len();
let seg_count = if p.is_closed { n } else { n.saturating_sub(1) };
let mut pts = Vec::with_capacity(seg_count * 2);
for i in 0..seg_count {
let v0 = &p.vertices[i];
let v1 = &p.vertices[(i + 1) % n];
pts.push([v0.location.x as f32, elev, v0.location.y as f32]);
pts.push([v1.location.x as f32, elev, v1.location.y as f32]);
}
pts
}
// For preview, show a 20-unit section of semi-infinite results
EntityType::Ray(r) => {
let bx = r.base_point.x;
@ -1724,6 +1846,9 @@ impl CadCommand for ExtendCommand {
let _ = span;
extend_ellipse(e, t_click, &self.geos)
}
Some(EntityType::LwPolyline(p)) => {
extend_lwpoly(p, pt.x as f64, pt.y as f64, &self.geos)
}
Some(EntityType::Spline(s)) => {
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
.and_then(|t_actual| {
@ -1757,6 +1882,7 @@ impl CadCommand for ExtendCommand {
EntityType::Line(l) => l.common.handle = new_handle,
EntityType::Ellipse(e) => e.common.handle = new_handle,
EntityType::Spline(s) => s.common.handle = new_handle,
EntityType::LwPolyline(p) => p.common.handle = new_handle,
_ => {}
}
if let Some(pos) = self
@ -1810,6 +1936,11 @@ impl CadCommand for ExtendCommand {
}
}
}
Some(EntityType::LwPolyline(p)) => {
if let Some(ext) = extend_lwpoly(p, pt.x as f64, pt.y as f64, &self.geos) {
return vec![WireModel::solid("extend_prev".into(), entity_pts(&ext), WireModel::CYAN, false)];
}
}
Some(EntityType::Spline(s)) => {
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
.and_then(|t_actual| {