diff --git a/Cargo.lock b/Cargo.lock index 718fbb6c6..663716dbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,7 +935,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#9406868b1bedd57d121b11fc75658bf9eaa2c764" +source = "git+https://github.com/HakanSeven12/cadkernel.git#92ef66a4e124881252d10eef066c65bd129ebddc" dependencies = [ "cavalier_contours", ] diff --git a/src/modules/draw/inquiry/divide.rs b/src/modules/draw/inquiry/divide.rs index 5c9fabe90..6ce799c80 100644 --- a/src/modules/draw/inquiry/divide.rs +++ b/src/modules/draw/inquiry/divide.rs @@ -1,12 +1,12 @@ // DIVIDE command — place Point entities at N equal intervals along an entity. // MEASURE command — place Point entities at fixed-distance intervals along an entity. -use std::f64::consts::PI; - use acadrust::entities::Point as PointEnt; +use acadrust::kernel::space::PlanarCurve; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::DVec3; +use crate::entities::curve::entity_curve; use crate::t; use crate::command::{CadCommand, CmdResult}; @@ -164,16 +164,12 @@ pub fn divide_entity(entity: &EntityType, n: usize) -> Vec { if n < 2 { return vec![]; } - let total = entity_length(entity); - if total < 1e-10 { + let Some((curve, total)) = measurable(entity) else { return vec![]; - } + }; let step = total / n as f64; (1..n) - .filter_map(|k| { - let t = step * k as f64; - point_at_distance(entity, t).map(make_point) - }) + .map(|k| make_point(curve.point_at_distance(step * k as f64))) .collect() } @@ -182,135 +178,30 @@ pub fn measure_entity(entity: &EntityType, segment_length: f64) -> Vec EntityType { +fn make_point(pos: [f64; 3]) -> EntityType { let mut p = PointEnt::new(); - p.location = pos; + p.location = Vector3::new(pos[0], pos[1], pos[2]); EntityType::Point(p) } -fn entity_length(entity: &EntityType) -> f64 { - match entity { - EntityType::Line(l) => { - let dx = l.end.x - l.start.x; - let dy = l.end.y - l.start.y; - let dz = l.end.z - l.start.z; - (dx * dx + dy * dy + dz * dz).sqrt() - } - EntityType::Arc(a) => { - let span = arc_span_rad(a.start_angle, a.end_angle); - a.radius * span - } - EntityType::Circle(c) => 2.0 * PI * c.radius, - EntityType::LwPolyline(p) => { - let n = p.vertices.len(); - if n < 2 { - return 0.0; - } - let segs = if p.is_closed { n } else { n - 1 }; - (0..segs) - .map(|i| { - let v0 = &p.vertices[i]; - let v1 = &p.vertices[(i + 1) % n]; - let dx = v1.location.x - v0.location.x; - let dy = v1.location.y - v0.location.y; - (dx * dx + dy * dy).sqrt() - }) - .sum() - } - _ => 0.0, - } -} - -fn point_at_distance(entity: &EntityType, d: f64) -> Option { - match entity { - EntityType::Line(l) => { - let total = entity_length(entity); - if total < 1e-10 { - return None; - } - let t = (d / total).clamp(0.0, 1.0); - Some(Vector3::new( - l.start.x + t * (l.end.x - l.start.x), - l.start.y + t * (l.end.y - l.start.y), - l.start.z + t * (l.end.z - l.start.z), - )) - } - EntityType::Arc(a) => { - let span = arc_span_rad(a.start_angle, a.end_angle); - let t = d / a.radius; // arc_length = r * theta - if t > span { - return None; - } - let angle = a.start_angle + t; - Some(Vector3::new( - a.center.x + a.radius * angle.cos(), - a.center.y + a.radius * angle.sin(), - a.center.z, - )) - } - EntityType::Circle(c) => { - let circumference = 2.0 * PI * c.radius; - if circumference < 1e-10 { - return None; - } - let angle = 2.0 * PI * (d / circumference); - Some(Vector3::new( - c.center.x + c.radius * angle.cos(), - c.center.y + c.radius * angle.sin(), - c.center.z, - )) - } - EntityType::LwPolyline(p) => { - let n = p.vertices.len(); - if n < 2 { - return None; - } - let segs = if p.is_closed { n } else { n - 1 }; - let mut acc = 0.0f64; - for i in 0..segs { - let v0 = &p.vertices[i]; - let v1 = &p.vertices[(i + 1) % n]; - let dx = v1.location.x - v0.location.x; - let dy = v1.location.y - v0.location.y; - let seg_len = (dx * dx + dy * dy).sqrt(); - if acc + seg_len >= d - 1e-10 { - let t = (d - acc) / seg_len.max(1e-10); - return Some(Vector3::new( - v0.location.x + t * dx, - v0.location.y + t * dy, - p.elevation, - )); - } - acc += seg_len; - } - None - } - _ => None, - } -} - -fn arc_span_rad(start: f64, end: f64) -> f64 { - let span = (end - start).rem_euclid(2.0 * PI); - if span < 1e-6 { - 2.0 * PI - } else { - span - } +/// The entity's curve and its length, or `None` for anything that cannot be +/// walked along — a hatch, a block, an unbounded ray. +fn measurable(entity: &EntityType) -> Option<(PlanarCurve, f64)> { + let curve = entity_curve(entity)?; + let total = curve.curve.length(); + (total.is_finite() && total > 1e-10).then_some((curve, total)) } diff --git a/src/modules/draw/modify/array.rs b/src/modules/draw/modify/array.rs index 4960a2d12..d0d7a83b6 100644 --- a/src/modules/draw/modify/array.rs +++ b/src/modules/draw/modify/array.rs @@ -422,102 +422,13 @@ impl CadCommand for ArrayPolarCommand { // → Returns BatchCopy with Translate transforms derived from path samples. use acadrust::EntityType; +use crate::entities::curve::entity_curve; use std::f64::consts::PI as FPI; use std::f64::consts::TAU as FTAU; // ── Path geometry helpers ────────────────────────────────────────────────── -/// Tessellate an LwPolyline into dense WCS points, matching the renderer: -/// bulge arcs use the canonical `BulgeArc` (correct centre side + signed -/// sweep) and every point is mapped OCS→WCS via the polyline's normal. The -/// previous ad-hoc bulge math placed the arc centre on the wrong side of the -/// chord and ignored the extrusion, so arc-segment paths came out mirrored. -fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec { - use crate::entities::common::BulgeArc; - use crate::scene::view::transform::ocs_point_to_wcs; - let verts = &p.vertices; - if verts.is_empty() { - return vec![]; - } - let normal = (p.normal.x, p.normal.y, p.normal.z); - let elev = p.elevation; - let to_wcs = |x: f64, y: f64| -> DVec3 { - let (wx, wy, wz) = ocs_point_to_wcs((x, y, elev), normal); - DVec3::new(wx, wy, wz) - }; - - let n = verts.len(); - let segs = if p.is_closed { n } else { n.saturating_sub(1) }; - let mut out: Vec = vec![]; - - for i in 0..segs { - let v0 = &verts[i]; - let v1 = &verts[(i + 1) % n]; - let p0 = [v0.location.x, v0.location.y]; - let p1 = [v1.location.x, v1.location.y]; - - if out.is_empty() { - out.push(to_wcs(p0[0], p0[1])); - } - - let bulge = v0.bulge; - match (bulge.abs() >= 1e-9) - .then(|| BulgeArc::from_bulge(p0, p1, bulge)) - .flatten() - { - Some(arc) => { - let steps = ((arc.radius * arc.sweep.abs() / 2.0).ceil() as usize).clamp(4, 64); - for j in 1..=steps { - let s = arc.sample(j as f64 / steps as f64); - out.push(to_wcs(s[0], s[1])); - } - } - None => out.push(to_wcs(p1[0], p1[1])), - } - } - out -} - -/// Walk `pts` (ordered) and return `count` points at equal arc-length spacing. -fn subsample_equidistant(pts: &[DVec3], count: usize) -> Vec { - if count == 0 { - return vec![]; - } - if pts.len() < 2 { - return vec![pts.first().copied().unwrap_or(DVec3::ZERO); count]; - } - - let mut cum = vec![0.0f64; pts.len()]; - for i in 1..pts.len() { - cum[i] = cum[i - 1] + pts[i].distance(pts[i - 1]); - } - let total = *cum.last().unwrap(); - if total < 1e-9 { - return vec![pts[0]; count]; - } - - let mut out = Vec::with_capacity(count); - let mut seg = 0usize; - for i in 0..count { - let target = if count > 1 { - total * i as f64 / (count - 1) as f64 - } else { - 0.0 - }; - while seg + 2 < pts.len() && cum[seg + 1] < target - 1e-9 { - seg += 1; - } - let seg_len = cum[seg + 1] - cum[seg]; - let t = if seg_len > 1e-9 { - (target - cum[seg]) / seg_len - } else { - 0.0 - }; - out.push(pts[seg].lerp(pts[seg + 1], t.clamp(0.0, 1.0))); - } - out -} // ── State machine ────────────────────────────────────────────────────────── @@ -570,66 +481,38 @@ impl ArrayPathCommand { } /// Sample `count` evenly-spaced points along `entity`. + /// `count` points spaced evenly **by distance** along the path. + /// + /// By distance, not by parameter. The two agree on a line, a circle and a + /// circular arc, which is how a per-type version got away with confusing + /// them; on an ellipse or a spline they do not, and copies laid out at + /// even parameters bunch up wherever the parameter runs slow. The path's + /// own OCS is honoured too, so an arc carrying a mirrored normal is + /// walked along the side it is drawn on. fn sample_path(entity: &EntityType, count: usize) -> Vec { if count == 0 { return vec![]; } - match entity { - EntityType::Line(l) => { - let p0 = DVec3::new(l.start.x, l.start.y, 0.0); - let p1 = DVec3::new(l.end.x, l.end.y, 0.0); - let d = (count - 1).max(1) as f64; - (0..count).map(|i| p0.lerp(p1, i as f64 / d)).collect() - } - EntityType::Arc(a) => { - // Sample in WCS via the arc's OCS basis so flipped-normal arcs - // (normal = -Z) are traced on the same side the renderer draws, - // matching `entities::arc::to_truck`. Sampling the raw centre + - // angle ignores the extrusion and lands on the mirrored arc. - use crate::scene::view::transform::{ocs_axes, ocs_point_to_wcs}; - let normal = (a.normal.x, a.normal.y, a.normal.z); - let (ax, ay) = ocs_axes(normal); - let (cx, cy, cz) = - ocs_point_to_wcs((a.center.x, a.center.y, a.center.z), normal); - let r = a.radius; - let (sa, ea) = (a.start_angle, a.end_angle); - let end = if ea >= sa { ea } else { ea + std::f64::consts::TAU }; - let span = end - sa; - let d = (count - 1).max(1) as f64; - (0..count) - .map(|i| { - let ang = sa + span * (i as f64 / d); - let (c, s) = (ang.cos(), ang.sin()); - DVec3::new( - cx + r * c * ax.0 + r * s * ay.0, - cy + r * c * ax.1 + r * s * ay.1, - cz + r * c * ax.2 + r * s * ay.2, - ) - }) - .collect() - } - EntityType::Circle(c) => { - use crate::scene::view::transform::{ocs_axes, ocs_point_to_wcs}; - let normal = (c.normal.x, c.normal.y, c.normal.z); - let (ax, ay) = ocs_axes(normal); - let (cx, cy, cz) = - ocs_point_to_wcs((c.center.x, c.center.y, c.center.z), normal); - let r = c.radius; - (0..count) - .map(|i| { - let ang = i as f64 / count as f64 * std::f64::consts::TAU; - let (cs, sn) = (ang.cos(), ang.sin()); - DVec3::new( - cx + r * cs * ax.0 + r * sn * ay.0, - cy + r * cs * ax.1 + r * sn * ay.1, - cz + r * cs * ax.2 + r * sn * ay.2, - ) - }) - .collect() - } - EntityType::LwPolyline(p) => subsample_equidistant(&lw_dense_pts(p), count), - _ => vec![DVec3::ZERO; count], + let Some(curve) = entity_curve(entity) else { + return vec![DVec3::ZERO; count]; + }; + let total = curve.length(); + if !total.is_finite() || total <= 0.0 { + return vec![DVec3::ZERO; count]; } + // A closed path has no far end to stop short of, so the last copy + // must not land on top of the first. + let steps = if curve.is_closed() { + count as f64 + } else { + (count - 1).max(1) as f64 + }; + (0..count) + .map(|i| { + let p = curve.point_at_distance(total * (i as f64 / steps)); + DVec3::new(p[0], p[1], p[2]) + }) + .collect() } /// Continuous path-tangent angle (XY) at each sample, via central diff --git a/src/modules/draw/modify/lengthen.rs b/src/modules/draw/modify/lengthen.rs index 3044a2dd2..a8ece8057 100644 --- a/src/modules/draw/modify/lengthen.rs +++ b/src/modules/draw/modify/lengthen.rs @@ -11,6 +11,9 @@ use crate::modules::draw::modify::spline_ops::{spline_cut, spline_to_nurbs}; use acadrust::entities::{ Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt, }; +use acadrust::kernel::geom2d::{ + Curve, Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, +}; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::{DVec3, Vec3}; @@ -18,6 +21,8 @@ use crate::t; use crate::command::{CadCommand, CmdResult}; +const TAU: f64 = std::f64::consts::TAU; + pub struct LengthenCommand { state: LenState, } @@ -263,71 +268,64 @@ fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option 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 + // Measured by the kernel rather than by a hundred and twenty-eight + // chords: the chord sum reads short, so LENGTHEN's idea of "current" was + // already below the true length before a delta was applied to it. + let shape = KernelEllipse { + centre: [ell.center.x, ell.center.y], + major_radius: a, + minor_radius: b, + major_axis: [nx, ny], }; - - let current_len = arc_len_approx(span); + let arc = |from: f64, to: f64| { + Curve::Ellipse(KernelEllipseArc { + ellipse: shape, + start_parameter: from, + end_parameter: to, + }) + }; + let current_len = arc(t0, t1).length(); 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); + // Which end is closer to the pick, in the DXF XY plane. + let point_at = |t: f64| { + ( + ell.center.x + a * t.cos() * nx - b * t.sin() * ny, + ell.center.y + a * t.cos() * ny + b * t.sin() * nx, + ) + }; + let (p_x, p_y) = (pick_pt.x as f64, pick_pt.y as f64); + let (sx, sy) = point_at(t0); + let (ex, ey) = point_at(t1); + let extend_end = (p_x - ex).hypot(p_y - ey) <= (p_x - sx).hypot(p_y - sy); let mut result = ell.clone(); result.common.handle = Handle::NULL; - - if dist_end <= dist_start { - result.end_parameter = t0 + new_span; + if extend_end { + // Walk `new_len` forward from the fixed start. A whole turn is the + // most there is to walk, and the kernel clamps to it. + let whole = arc(t0, t0 + TAU); + result.end_parameter = t0 + whole.parameter_at_distance(new_len) * TAU; } else { - result.start_parameter = t1 - new_span; + // The same measured backwards from the fixed end: the last `new_len` + // of a whole turn ending at t1. + let whole = arc(t1 - TAU, t1); + let from_start = whole.length() - new_len; + result.start_parameter = t1 - TAU + whole.parameter_at_distance(from_start) * TAU; } Some(EntityType::Ellipse(result)) } + fn apply_mode(current: f64, mode: &LenMode) -> Option { match mode { LenMode::Delta(d) => Some(current + d), @@ -337,9 +335,9 @@ fn apply_mode(current: f64, mode: &LenMode) -> Option { } fn arc_span_rad(start: f64, end: f64) -> f64 { - let span = (end - start).rem_euclid(std::f64::consts::TAU); + let span = (end - start).rem_euclid(TAU); if span < 1e-6 { - std::f64::consts::TAU + TAU } else { span } @@ -410,103 +408,44 @@ fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option Option { - let curve = spline_to_nurbs(spl)?; - let (t0, t1) = curve.domain(); + let nurbs = spline_to_nurbs(spl)?; + let (t0, t1) = nurbs.domain(); if (t1 - t0).abs() < 1e-12 { return None; } - - // Approximate arc length via 64-point numerical integration. - 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 = curve.point_at_knot(ta); - let pb = curve.point_at_knot(tb); - len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]); - } - len - }; + let curve = Curve::Nurbs(nurbs.clone()); + let arc_len = curve.length(); if arc_len < 1e-10 { return None; } - let new_len = apply_mode(arc_len, mode)?; - if new_len < 1e-10 { + if new_len < 1e-10 || new_len >= arc_len { + // A spline is shortened by splitting it, so there is nothing to keep + // if the new length is the whole of it or more. Extending would mean + // continuing the curve past its own control polygon, which is a + // different operation from cutting one. return None; } - // Determine which end (start or end) is closer to pick_pt. - let p_start = curve.point_at_knot(t0); - let p_end = curve.point_at_knot(t1); - let dist_start = (p_start[0] - pick_pt.x as f64).hypot(p_start[1] - pick_pt.y as f64); - let dist_end = (p_end[0] - pick_pt.x as f64).hypot(p_end[1] - pick_pt.y as f64); - let extend_end = dist_end <= dist_start; + let p_start = nurbs.point_at_knot(t0); + let p_end = nurbs.point_at_knot(t1); + let (px, py) = (pick_pt.x as f64, pick_pt.y as f64); + let extend_end = (p_end[0] - px).hypot(p_end[1] - py) + <= (p_start[0] - px).hypot(p_start[1] - py); - // 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 = curve.point_at_knot(ta); - let pb = curve.point_at_knot(tb); - len += (pb[0] - pa[0]).hypot(pb[1] - pa[1]); - } - len - }; - - let (mut lo_t, mut hi_t) = if extend_end { - (t0, t0 + delta_ratio * (t1 - t0) * 2.0) + // Where to cut, by distance along the curve rather than by a bisection + // over repeated chord sums. Keeping the head means cutting `new_len` from + // the start; keeping the tail means cutting what is left over. + let along = if extend_end { + new_len } else { - (t1 - delta_ratio * (t1 - t0) * 2.0, t1) + arc_len - new_len }; - // 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 and keep the side the pick asked for. - let cut = t_new.clamp(t0 + 1e-10, t1 - 1e-10); + let at = curve.parameter_at_distance(along); + let cut = (t0 + at * (t1 - t0)).clamp(t0 + 1e-10, t1 - 1e-10); let (left, right) = spline_cut(spl, cut)?; Some(EntityType::Spline(if extend_end { left } else { right })) } - // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["LENGTHEN"] }); // LengthenCommand