diff --git a/Cargo.lock b/Cargo.lock index 60d13ff9e..5c1a5eafa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,7 +946,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#53e15eccfb5c476a04c49cbea2809c5c4e13b691" +source = "git+https://github.com/HakanSeven12/cadkernel.git#0e4b23845de75242e00f016feed9fc2341b4a85a" [[package]] name = "calloop" diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index 8c0a655f7..b756bcadf 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -14,7 +14,7 @@ // // Unsupported entity types are skipped silently. -use std::f64::consts::TAU; +use super::geom::normalize_angle as norm_angle; use acadrust::entities::EntityCommon; use acadrust::entities::{ @@ -260,10 +260,6 @@ fn bulge_to_arc( Some(EntityType::Arc(arc)) } -fn norm_angle(a: f64) -> f64 { - ((a % TAU) + TAU) % TAU -} - fn explode_mline(ml: &MLine) -> Vec { let n = ml.vertices.len(); if n < 2 { diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index 36b1e9ae6..d1f01ade5 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -10,6 +10,10 @@ // Finds intersection, backs off dist1 along line 1 and dist2 along line 2. use acadrust::entities::{Arc as ArcEnt, Line as LineEnt, LwPolyline}; + +// Shared plane geometry, from cadkernel via the local adapters. +use super::geom; +use super::geom::{arc_points as arc_pts, line_line as ll, normalize_angle as norm_angle}; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::DVec3; @@ -46,26 +50,6 @@ pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[ // Geometry // ══════════════════════════════════════════════════════════════════════════ -/// Intersect two infinite lines. Returns (t on L1, u on L2). -fn ll( - ax: f64, - ay: f64, - dx: f64, - dy: f64, - cx: f64, - cy: f64, - ex: f64, - ey: f64, -) -> Option<(f64, f64)> { - let det = dx * ey - dy * ex; - if det.abs() < 1e-10 { - return None; - } - let t = ((cx - ax) * ey - (cy - ay) * ex) / det; - let u = ((cx - ax) * dy - (cy - ay) * dx) / det; - Some((t, u)) -} - /// Extract coords and unit direction for a Line entity. fn line_geom(l: &LineEnt) -> ([f64; 2], [f64; 2], [f64; 2], f64) { let p1 = [l.start.x, l.start.y]; @@ -229,32 +213,10 @@ fn trim_to_xy( // ── Point-generation helpers ────────────────────────────────────────────── fn line_pts(l: &LineEnt) -> Vec<[f32; 3]> { - vec![ - [l.start.x as f32, l.start.y as f32, l.start.z as f32], - [l.end.x as f32, l.end.y as f32, l.end.z as f32], - ] -} - -fn arc_pts(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, z: f64) -> Vec<[f32; 3]> { - let span = { - let s = norm_angle(a1) - norm_angle(a0); - if s <= 0.0 { - s + TAU - } else { - s - } - }; - let steps = (span.abs() * 20.0).ceil().max(4.0) as usize; - (0..=steps) - .map(|i| { - let ang = norm_angle(a0) + span * (i as f64 / steps as f64); - [ - (cx + r * ang.cos()) as f32, - (cy + r * ang.sin()) as f32, - z as f32, - ] - }) - .collect() + geom::line_points( + [l.start.x, l.start.y, l.start.z], + [l.end.x, l.end.y, l.end.z], + ) } fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> { @@ -286,11 +248,6 @@ fn arc_geom(a: &ArcEnt) -> ([f64; 2], f64, f64, f64, f64) { ) } -/// Normalize angle to [0, 2π). -fn norm_angle(a: f64) -> f64 { - ((a % TAU) + TAU) % TAU -} - /// Return the CCW angular span from `start` to `end`. fn arc_span(start: f64, end: f64) -> f64 { let s = (end - start).rem_euclid(TAU); diff --git a/src/modules/draw/modify/geom.rs b/src/modules/draw/modify/geom.rs new file mode 100644 index 000000000..ca8eacb87 --- /dev/null +++ b/src/modules/draw/modify/geom.rs @@ -0,0 +1,128 @@ +//! Adapters between the editing commands and `cadkernel`'s plane geometry. +//! +//! The kernel takes points as `[f64; 2]` and hands tessellation back as +//! `[f64; 3]`. The commands here grew up passing loose `x, y` scalars and +//! feeding [`WireModel::points`](crate::scene::model::wire_model::WireModel), +//! which is `[f32; 3]`. These wrappers absorb that difference in one place so +//! the maths itself lives once, in the kernel. +//! +//! Narrowing to `f32` happens here rather than in the kernel deliberately: +//! preview wires leave `points_low` empty, so they accept the loss, while the +//! resident render path splits the `f64` into a high/low pair instead. Only a +//! caller knows which of the two it is. + +use acadrust::kernel::geom2d::{self, Ellipse}; + +/// Re-exported unchanged: these already speak in plain `f64`, so there is no +/// call-shape difference for this module to absorb. +pub use acadrust::kernel::geom2d::{angle_within_arc, arc_parameter, lerp, normalize_angle}; + +/// Preview geometry keeps the density the commands have always used. +const SEGMENTS_PER_RADIAN: f64 = geom2d::DEFAULT_SEGMENTS_PER_RADIAN; + +fn narrow(points: Vec<[f64; 3]>) -> Vec<[f32; 3]> { + points + .into_iter() + .map(|p| [p[0] as f32, p[1] as f32, p[2] as f32]) + .collect() +} + +/// Where two infinite lines cross, as `(t, u)`; `None` when parallel. +/// +/// Both parameters are unbounded, so EXTEND can use a crossing that lies past +/// the end of either line. +#[allow(clippy::too_many_arguments)] +pub fn line_line( + px: f64, + py: f64, + dx: f64, + dy: f64, + qx: f64, + qy: f64, + ex: f64, + ey: f64, +) -> Option<(f64, f64)> { + geom2d::line_line([px, py], [dx, dy], [qx, qy], [ex, ey]) +} + +/// Line parameters where `p + t·d` meets a circle: none, one when tangent, or +/// two ordered by increasing `t`. +pub fn line_circle(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64) -> Vec { + geom2d::line_circle([px, py], [dx, dy], [cx, cy], r) +} + +/// Angles **on the first circle** where two circles meet. +pub fn circle_circle_angles(cx1: f64, cy1: f64, r1: f64, cx2: f64, cy2: f64, r2: f64) -> Vec { + geom2d::circle_circle_angles([cx1, cy1], r1, [cx2, cy2], r2) +} + +/// `(s_on_line, t_on_ellipse)` pairs where a line meets an ellipse. +/// +/// `nx, ny` is the unit major axis. +#[allow(clippy::too_many_arguments)] +pub fn line_ellipse( + px: f64, + py: f64, + dx: f64, + dy: f64, + cx: f64, + cy: f64, + a: f64, + b: f64, + nx: f64, + ny: f64, +) -> Vec<(f64, f64)> { + geom2d::line_ellipse( + [px, py], + [dx, dy], + &Ellipse { + centre: [cx, cy], + major_radius: a, + minor_radius: b, + major_axis: [nx, ny], + }, + ) +} + +/// The two endpoints of a straight segment, as render vertices. +pub fn line_points(start: [f64; 3], end: [f64; 3]) -> Vec<[f32; 3]> { + narrow(vec![start, end]) +} + +/// A circular arc sampled counter-clockwise, as render vertices. +pub fn arc_points(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, z: f64) -> Vec<[f32; 3]> { + narrow(geom2d::arc([cx, cy], r, a0, a1, z, SEGMENTS_PER_RADIAN)) +} + +/// An elliptical arc sampled between two of the ellipse's own parameters, as +/// render vertices. +/// +/// Components come back as `[x, y, z]`. The hand-rolled version this replaces +/// wrote them as `[x, z, y]`, which flattened ELLIPSE trim previews onto +/// `y = 0` and displaced them along Z. +#[allow(clippy::too_many_arguments)] +pub fn ellipse_points( + cx: f64, + cy: f64, + a: f64, + b: f64, + nx: f64, + ny: f64, + t0: f64, + t1: f64, + z: f64, +) -> Vec<[f32; 3]> { + let ellipse = Ellipse { + centre: [cx, cy], + major_radius: a, + minor_radius: b, + major_axis: [nx, ny], + }; + narrow(geom2d::ellipse_arc( + &ellipse, + t0, + t1, + z, + SEGMENTS_PER_RADIAN, + )) +} diff --git a/src/modules/draw/modify/mod.rs b/src/modules/draw/modify/mod.rs index 04302a18b..d4c146e4d 100644 --- a/src/modules/draw/modify/mod.rs +++ b/src/modules/draw/modify/mod.rs @@ -8,6 +8,7 @@ pub mod delete; mod entity_index; pub mod explode; pub mod fillet; +pub mod geom; pub mod join; pub mod lengthen; pub mod mirror; diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index fb60b2ff9..631dac151 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -10,6 +10,15 @@ use std::f64::consts::TAU; +// The plane geometry these commands run on lives in cadkernel; `geom` adapts +// its call shapes to the loose scalars and f32 render vertices used here. +use super::geom; +use super::geom::{ + angle_within_arc as in_arc, arc_parameter as arc_t, arc_points as arc_pts, + circle_circle_angles as cc_angles, ellipse_points as ellipse_pts, lerp as lerp2, + line_circle as lc, line_ellipse as le, line_line as ll, normalize_angle as norm, +}; + use crate::modules::draw::fence::{crossing_box_preview, FencePick}; use acadrust::entities::{ @@ -54,163 +63,6 @@ pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[ // Geometry helpers // ══════════════════════════════════════════════════════════════════════════ -/// Normalize angle to [0, 2π). -fn norm(a: f64) -> f64 { - ((a % TAU) + TAU) % TAU -} - -/// Is angle `a` within the arc from `s` to `e` (CCW, radians)? -fn in_arc(a: f64, s: f64, e: f64) -> bool { - let (a, s, e) = (norm(a), norm(s), norm(e)); - if (e - s).abs() < 1e-9 || (e - s - TAU).abs() < 1e-9 { - return true; - } - if s <= e { - a >= s - 1e-9 && a <= e + 1e-9 - } else { - a >= s - 1e-9 || a <= e + 1e-9 - } -} - -/// Parametric t ∈ [0,1] on arc (a0→a1 CCW) for angle `a`. -fn arc_t(a: f64, a0: f64, a1: f64) -> f64 { - let span = { - let s = norm(a1) - norm(a0); - if s <= 0.0 { - s + TAU - } else { - s - } - }; - let da = { - let d = norm(a) - norm(a0); - if d < 0.0 { - d + TAU - } else { - d - } - }; - (da / span).clamp(0.0, 1.0) -} - -/// Intersect infinite lines (p+t·d) and (q+u·e). Returns (t, u). -fn ll( - px: f64, - py: f64, - dx: f64, - dy: f64, - qx: f64, - qy: f64, - ex: f64, - ey: f64, -) -> Option<(f64, f64)> { - let det = dx * ey - dy * ex; - if det.abs() < 1e-10 { - return None; - } - let t = ((qx - px) * ey - (qy - py) * ex) / det; - let u = ((qx - px) * dy - (qy - py) * dx) / det; - Some((t, u)) -} - -/// Intersect infinite line (p+t·d) with circle (cx,cy,r). Returns t values. -fn lc(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64) -> Vec { - let fx = px - cx; - let fy = py - cy; - let a = dx * dx + dy * dy; - let b = 2.0 * (fx * dx + fy * dy); - let c = fx * fx + fy * fy - r * r; - let disc = b * b - 4.0 * a * c; - if disc < 0.0 { - return vec![]; - } - let sq = disc.sqrt(); - if disc < 1e-14 { - vec![(-b) / (2.0 * a)] - } else { - vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)] - } -} - -/// Circle-circle intersection: angles on circle 1 where they meet. -fn cc_angles(cx1: f64, cy1: f64, r1: f64, cx2: f64, cy2: f64, r2: f64) -> Vec { - let d = ((cx2 - cx1).powi(2) + (cy2 - cy1).powi(2)).sqrt(); - if d < 1e-9 || d > r1 + r2 + 1e-9 || d < (r1 - r2).abs() - 1e-9 { - return vec![]; - } - let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d); - let h2 = r1 * r1 - a * a; - if h2 < 0.0 { - return vec![]; - } - let h = h2.sqrt(); - let mx = cx1 + a * (cx2 - cx1) / d; - let my = cy1 + a * (cy2 - cy1) / d; - let px = h * (cy2 - cy1) / d; - let py = -h * (cx2 - cx1) / d; - let a1 = ((my + py) - cy1).atan2((mx + px) - cx1); - let a2 = ((my - py) - cy1).atan2((mx - px) - cx1); - if h < 1e-9 { - vec![a1] - } else { - vec![a1, a2] - } -} - -/// Line (px+s·d) vs ellipse (cx,cy,a,b,nx,ny). Returns (s_on_line, t_on_ellipse) pairs. -/// nx,ny: unit major-axis; perp = (-ny, nx). Parametric ellipse: P(t) = center + a·cos(t)·n + b·sin(t)·v. -fn le( - px: f64, - py: f64, - dpx: f64, - dpy: f64, - cx: f64, - cy: f64, - a: f64, - b: f64, - nx: f64, - ny: f64, -) -> Vec<(f64, f64)> { - // Transform line origin to ellipse local frame - let rx = px - cx; - let ry = py - cy; - // Project onto major/minor axes - let xl0 = rx * nx + ry * ny; - let yl0 = -rx * ny + ry * nx; - let dxl = dpx * nx + dpy * ny; - let dyl = -dpx * ny + dpy * nx; - // Scale by 1/a, 1/b → circle equation - let xa = xl0 / a; - let xda = dxl / a; - let yb = yl0 / b; - let ydb = dyl / b; - let big_a = xda * xda + ydb * ydb; - if big_a < 1e-20 { - return vec![]; - } - let big_b = 2.0 * (xa * xda + yb * ydb); - let big_c = xa * xa + yb * yb - 1.0; - let disc = big_b * big_b - 4.0 * big_a * big_c; - if disc < 0.0 { - return vec![]; - } - let sq = disc.sqrt(); - let s_vals: Vec = if disc < 1e-14 { - vec![(-big_b) / (2.0 * big_a)] - } else { - vec![(-big_b - sq) / (2.0 * big_a), (-big_b + sq) / (2.0 * big_a)] - }; - s_vals - .into_iter() - .map(|s| { - let xl = xl0 + s * dxl; - let yl = yl0 + s * dyl; - let t = yl.atan2(xl); // ≡ atan2(yl/b, xl/a) but faster since sign is preserved - (s, t) - }) - .collect() -} - // ── Boundary geometry ───────────────────────────────────────────────────── /// Virtual extent used to represent infinite ends of Ray / XLine. @@ -1051,34 +903,6 @@ fn extend_ellipse(orig: &EllipseEnt, t_click: f64, geos: &[Geo]) -> Option Vec<[f32; 3]> { - let span = t1 - t0; - let steps = (span.abs() * 20.0).ceil().max(4.0) as usize; - (0..=steps) - .map(|i| { - let t = t0 + span * (i as f64 / steps as f64); - let lx = a * t.cos(); - let ly = b * t.sin(); - [ - (cx + lx * nx - ly * ny) as f32, - z as f32, - (cy + lx * ny + ly * nx) as f32, - ] - }) - .collect() -} - // ── Spline trim / extend ────────────────────────────────────────────────── /// Find normalised t-params ∈ [0,1] where a Spline intersects boundary geos. @@ -1221,10 +1045,6 @@ fn trim_intervals(ts: &[f64], t_click: f64) -> Vec<(f64, f64)> { .collect() } -fn lerp2(p1: [f64; 2], p2: [f64; 2], t: f64) -> [f64; 2] { - [p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])] -} - /// Trim a Line entity. Returns the surviving line segments. fn trim_line(orig: &LineEnt, ts: &[f64], t_click: f64) -> Vec { let p1 = [orig.start.x, orig.start.y]; @@ -2004,32 +1824,10 @@ fn trim_xline(orig: &XLineEnt, ts: &[f64], t_click: f64) -> Vec { const DIM_RED: [f32; 4] = [1.0, 0.3, 0.3, 0.6]; fn line_pts(l: &LineEnt) -> Vec<[f32; 3]> { - vec![ - [l.start.x as f32, l.start.y as f32, l.start.z as f32], - [l.end.x as f32, l.end.y as f32, l.end.z as f32], - ] -} - -fn arc_pts(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, z: f64) -> Vec<[f32; 3]> { - let span = { - let s = norm(a1) - norm(a0); - if s <= 0.0 { - s + TAU - } else { - s - } - }; - let steps = (span.abs() * 20.0).ceil().max(4.0) as usize; - (0..=steps) - .map(|i| { - let ang = norm(a0) + span * (i as f64 / steps as f64); - [ - (cx + r * ang.cos()) as f32, - (cy + r * ang.sin()) as f32, - z as f32, - ] - }) - .collect() + geom::line_points( + [l.start.x, l.start.y, l.start.z], + [l.end.x, l.end.y, l.end.z], + ) } fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {