From eca4c965699cc559159e44b80e79f274ab9b5173 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 7 Apr 2026 10:49:57 +0300 Subject: [PATCH] feat: OFFSET support for Ellipse entities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add offset_ellipse() that scales both semi-axes uniformly by the offset distance (inside vs outside determined by projecting side_pt onto the ellipse local frame). This is the standard CAD approximation; a true offset of an ellipse is a higher-order Lamé curve. Co-Authored-By: Claude Sonnet 4.6 --- src/modules/home/modify/offset.rs | 59 ++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/modules/home/modify/offset.rs b/src/modules/home/modify/offset.rs index 20ab894e9..3bdc0273b 100644 --- a/src/modules/home/modify/offset.rs +++ b/src/modules/home/modify/offset.rs @@ -12,7 +12,7 @@ use std::f64::consts::TAU; use acadrust::entities::LwVertex; -use acadrust::entities::{Arc as ArcEnt, Circle as CircleEnt, Line as LineEnt, LwPolyline}; +use acadrust::entities::{Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline}; use acadrust::{EntityType, Handle}; use glam::Vec3; @@ -255,6 +255,42 @@ fn offset_lwpolyline(p: &LwPolyline, dist: f64, side_pt: Vec3) -> Option Option { + let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); + if a < 1e-9 { return None; } + let b = a * e.minor_axis_ratio; + let nx = e.major_axis.x / a; + let ny = e.major_axis.y / a; + // Project side_pt onto ellipse local frame and test inside/outside. + let rx = side_pt.x as f64 - e.center.x; + let ry = side_pt.y as f64 - e.center.y; + let xl = rx * nx + ry * ny; + let yl = -rx * ny + ry * nx; + let inside = (xl / a).powi(2) + (yl / b).powi(2) < 1.0; + let sign = if inside { -1.0 } else { 1.0 }; + + let new_a = a + sign * dist; + let new_b = b + sign * dist; + if new_a <= 1e-9 || new_b <= 1e-9 { return None; } + + let mut new_e = e.clone(); + new_e.common.handle = Handle::NULL; + // Scale the major_axis vector proportionally. + let scale = new_a / a; + new_e.major_axis.x *= scale; + new_e.major_axis.y *= scale; + new_e.major_axis.z *= scale; + new_e.minor_axis_ratio = new_b / new_a; + Some(EntityType::Ellipse(new_e)) +} + // ── Dispatch ─────────────────────────────────────────────────────────────── fn compute_offset(entity: &EntityType, dist: f64, side_pt: Vec3) -> Option { @@ -263,6 +299,7 @@ fn compute_offset(entity: &EntityType, dist: f64, side_pt: Vec3) -> Option offset_circle(c, dist, side_pt), EntityType::Arc(a) => offset_arc(a, dist, side_pt), EntityType::LwPolyline(p) => offset_lwpolyline(p, dist, side_pt), + EntityType::Ellipse(e) => offset_ellipse(e, dist, side_pt), _ => None, } } @@ -312,6 +349,26 @@ fn entity_wire_pts(e: &EntityType) -> Vec<[f32; 3]> { .collect() } EntityType::LwPolyline(p) => lwpolyline_pts(p), + EntityType::Ellipse(e) => { + let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); + if a < 1e-9 { return vec![]; } + let b = a * e.minor_axis_ratio; + let nx = e.major_axis.x / a; + let ny = e.major_axis.y / a; + let t0 = e.start_parameter; + let mut t1 = e.end_parameter; + if t1 <= t0 { t1 += TAU; } + let span = t1 - t0; + let steps = ((span.abs() * 20.0).ceil() as usize).max(4); + (0..=steps).map(|i| { + let t = t0 + span * (i as f64 / steps as f64); + let lx = a * t.cos(); + let ly = b * t.sin(); + [(e.center.x + lx * nx - ly * ny) as f32, + (e.center.y + lx * ny + ly * nx) as f32, + e.center.z as f32] + }).collect() + } _ => vec![], } }