fix(offset): allow OFFSET of XLINE (and Ellipse/Spline) objects (#296)

OFFSET rejected XLINE targets: on_entity_pick only accepted
Line/Circle/Arc/LwPolyline, compute_offset had no XLine arm, and
entity_wire_pts returned no points for it (so hover/preview/through-distance
failed too).

Add offset_xline (parallel infinite line: same direction, base point shifted
perpendicular toward the pick side), a compute_offset arm, and an
entity_wire_pts case (a long segment standing in for the infinite line during
hit-test and preview; the committed XLine renders true). Also widen the pick
filter to every type compute_offset supports — this additionally reaches
Ellipse and Spline, whose offset functions already existed but weren't
selectable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-10 13:40:09 +03:00
commit 07eb9dcb9a

View file

@ -15,7 +15,7 @@ use crate::modules::draw::modify::spline_ops::{spline_pts_wire, spline_sample_xy
use acadrust::entities::LwVertex; use acadrust::entities::LwVertex;
use acadrust::entities::{ use acadrust::entities::{
Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline,
Spline as SplineEnt, Spline as SplineEnt, XLine as XLineEnt,
}; };
use acadrust::{EntityType, Handle}; use acadrust::{EntityType, Handle};
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
@ -86,6 +86,33 @@ fn offset_line(l: &LineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
Some(EntityType::Line(new_l)) Some(EntityType::Line(new_l))
} }
// ── XLine (infinite construction line) offset ────────────────────────────────
// A parallel infinite line: same direction, base point shifted perpendicular by
// `dist` toward the side the cursor is on. (#296)
fn offset_xline(x: &XLineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
let dx = x.direction.x;
let dy = x.direction.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-12 {
return None;
}
let nx = -dy / len; // left-perpendicular
let ny = dx / len;
let vx = side_pt.x as f64 - x.base_point.x;
let vy = side_pt.y as f64 - x.base_point.y;
let cross = dx * vy - dy * vx;
let sign = if cross >= 0.0 { 1.0 } else { -1.0 };
let mut new_x = x.clone();
new_x.common.handle = Handle::NULL;
new_x.base_point.x += sign * nx * dist;
new_x.base_point.y += sign * ny * dist;
Some(EntityType::XLine(new_x))
}
// ── Circle offset ────────────────────────────────────────────────────────── // ── Circle offset ──────────────────────────────────────────────────────────
fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> { fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
@ -424,6 +451,7 @@ fn compute_offset(entity: &EntityType, dist: f64, side_pt: Vec3) -> Option<Entit
EntityType::LwPolyline(p) => offset_lwpolyline(p, dist, side_pt), EntityType::LwPolyline(p) => offset_lwpolyline(p, dist, side_pt),
EntityType::Ellipse(e) => offset_ellipse(e, dist, side_pt), EntityType::Ellipse(e) => offset_ellipse(e, dist, side_pt),
EntityType::Spline(s) => offset_spline(s, dist, side_pt), EntityType::Spline(s) => offset_spline(s, dist, side_pt),
EntityType::XLine(x) => offset_xline(x, dist, side_pt),
_ => None, _ => None,
} }
} }
@ -540,6 +568,18 @@ fn entity_wire_pts(e: &EntityType) -> Vec<[f32; 3]> {
.collect() .collect()
} }
EntityType::Spline(s) => spline_pts_wire(s), EntityType::Spline(s) => spline_pts_wire(s),
EntityType::XLine(x) => {
// Infinite in both directions — represent it as a very long segment
// for hit-testing and the offset preview. Long enough to read as
// infinite at any working zoom; the committed XLine renders true.
const HL: f64 = 1.0e6;
let (bx, by, bz) = (x.base_point.x, x.base_point.y, x.base_point.z);
let (dx, dy, dz) = (x.direction.x, x.direction.y, x.direction.z);
vec![
[(bx - dx * HL) as f32, (by - dy * HL) as f32, (bz - dz * HL) as f32],
[(bx + dx * HL) as f32, (by + dy * HL) as f32, (bz + dz * HL) as f32],
]
}
_ => vec![], _ => vec![],
} }
} }
@ -690,11 +730,16 @@ impl CadCommand for OffsetCommand {
.find(|e| e.common().handle == handle) .find(|e| e.common().handle == handle)
.cloned(); .cloned();
// Accept every type compute_offset can offset — including XLine (#296),
// and Ellipse/Spline whose offset functions existed but weren't reachable.
match entity { match entity {
Some(e @ EntityType::Line(_)) Some(e @ EntityType::Line(_))
| Some(e @ EntityType::Circle(_)) | Some(e @ EntityType::Circle(_))
| Some(e @ EntityType::Arc(_)) | Some(e @ EntityType::Arc(_))
| Some(e @ EntityType::LwPolyline(_)) => { | Some(e @ EntityType::LwPolyline(_))
| Some(e @ EntityType::Ellipse(_))
| Some(e @ EntityType::Spline(_))
| Some(e @ EntityType::XLine(_)) => {
self.step = Step::PickSide { self.step = Step::PickSide {
handle, handle,
entity: e, entity: e,