diff --git a/Cargo.lock b/Cargo.lock index 72eb2896f..565f6a2f8 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#77d8ee9d3ee7f8bfafecbafb0ee3097c4b413ccd" +source = "git+https://github.com/HakanSeven12/cadkernel.git#e4c9fe626a60fd1500e8f2264e5a712e939c4027" dependencies = [ "cavalier_contours", ] diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 2e9223ef7..022fc3f9c 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -1,4 +1,9 @@ use acadrust::entities::{BoundaryEdge, Hatch}; +use acadrust::kernel::geom2d::{ + Arc as KernelArc, Curve as KernelCurve, Ellipse as KernelEllipse, + EllipseArc as KernelEllipseArc, Line as KernelLine, NurbsCurve as KernelNurbs, + Parameterization, Polyline as KernelPolyline, PolylineVertex as KernelVertex, +}; use glam::Vec3; use crate::t; @@ -9,54 +14,128 @@ use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Pr use crate::scene::convert::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry}; use crate::scene::model::wire_model::SnapHint; -/// Signed area of every boundary path, summed via the shoelace formula over -/// the OCS boundary vertices (bulge/curved edges approximated by their end -/// points). Returns the absolute total area. +/// The area the hatch's boundary paths enclose. +/// +/// Summed edge by edge through the kernel, which measures what each edge +/// actually encloses rather than what a polygon through some of its points +/// would. The version this replaced pushed an arc's *centre* into the ring +/// and a spline's control points — neither of which is on the boundary — so +/// the number it produced was not the area of anything. +/// +/// Outer paths and their holes both contribute; the sign of a loop says +/// which it is, so the magnitude of the sum is the region's own area. fn boundary_area(h: &Hatch) -> f64 { let mut area = 0.0; for path in &h.paths { - let mut ring: Vec<[f64; 2]> = Vec::new(); + let mut path_area = 0.0; + let mut ends: Vec<[f64; 2]> = Vec::new(); for edge in &path.edges { - match edge { - BoundaryEdge::Polyline(poly) => { - for v in &poly.vertices { - ring.push([v.x, v.y]); - } - } - BoundaryEdge::Line(l) => { - ring.push([l.start.x, l.start.y]); - ring.push([l.end.x, l.end.y]); - } - BoundaryEdge::CircularArc(a) => ring.push([a.center.x, a.center.y]), - BoundaryEdge::EllipticArc(e) => ring.push([e.center.x, e.center.y]), - BoundaryEdge::Spline(s) => { - let src = if !s.fit_points.is_empty() { - s.fit_points.iter().map(|p| [p.x, p.y]).collect::>() - } else { - s.control_points - .iter() - .map(|p| [p.x, p.y]) - .collect::>() - }; - ring.extend(src); - } - } + let Some(curve) = edge_curve(edge) else { + continue; + }; + path_area += curve.enclosed_area(); + ends.push(curve.point_at(0.0)); + ends.push(curve.point_at(1.0)); } - let n = ring.len(); - if n < 3 { - continue; + // Edges are stored as separate pieces, so the chain has to be closed + // by the chord from the last end back to the first — the same closing + // an open polyline gets. + if let (Some(first), Some(last)) = (ends.first(), ends.last()) { + path_area += 0.5 * (last[0] * first[1] - first[0] * last[1]); } - let mut acc = 0.0; - for i in 0..n { - let a = ring[i]; - let b = ring[(i + 1) % n]; - acc += a[0] * b[1] - b[0] * a[1]; - } - area += acc.abs() * 0.5; + area += path_area.abs(); } area } +/// A hatch boundary edge as a kernel curve, in the hatch's own OCS. +fn edge_curve(edge: &BoundaryEdge) -> Option { + Some(match edge { + BoundaryEdge::Line(l) => KernelCurve::Line(KernelLine { + start: [l.start.x, l.start.y], + end: [l.end.x, l.end.y], + }), + BoundaryEdge::CircularArc(a) => { + // A clockwise edge is the same arc walked the other way, so the + // stored angles swap rather than the sweep going negative. + let (start, end) = if a.counter_clockwise { + (a.start_angle, a.end_angle) + } else { + (a.end_angle, a.start_angle) + }; + KernelCurve::Arc(KernelArc { + centre: [a.center.x, a.center.y], + radius: a.radius, + start_angle: start, + end_angle: end, + }) + } + BoundaryEdge::EllipticArc(e) => { + let major = (e.major_axis_endpoint.x, e.major_axis_endpoint.y); + let radius = major.0.hypot(major.1); + if radius < 1e-12 { + return None; + } + let (start, end) = if e.counter_clockwise { + (e.start_angle, e.end_angle) + } else { + (e.end_angle, e.start_angle) + }; + KernelCurve::Ellipse(KernelEllipseArc { + ellipse: KernelEllipse { + centre: [e.center.x, e.center.y], + major_radius: radius, + minor_radius: radius * e.minor_axis_ratio, + major_axis: [major.0 / radius, major.1 / radius], + }, + start_parameter: start, + end_parameter: end, + }) + } + BoundaryEdge::Polyline(poly) => { + let vertices: Vec = poly + .vertices + .iter() + .map(|v| KernelVertex { + position: [v.x, v.y], + // A boundary polyline stores its bulge in the vertex's + // third component. + bulge: v.z, + }) + .collect(); + if vertices.len() < 2 { + return None; + } + KernelCurve::Polyline(KernelPolyline { + vertices, + closed: poly.is_closed, + }) + } + BoundaryEdge::Spline(s) => { + let control: Vec<[f64; 2]> = s.control_points.iter().map(|p| [p.x, p.y]).collect(); + // A boundary spline's control points carry their weight in the + // third component; for a polynomial one it is unset and the + // kernel's own default of all-ones applies. + let weights = s + .rational + .then(|| s.control_points.iter().map(|p| p.z).collect::>()); + let curve = KernelNurbs::new( + s.degree.max(1) as usize, + control, + s.knots.clone(), + weights, + ) + .or_else(|| { + // A fit-point boundary spline, interpolated the same way a + // SPLINE entity's is. + let fit: Vec<[f64; 2]> = s.fit_points.iter().map(|p| [p.x, p.y]).collect(); + KernelNurbs::interpolate(&fit, None, None, Parameterization::Chord) + })?; + KernelCurve::Nurbs(curve) + } + }) +} + /// Mean of every boundary edge point (OCS) — the centroid used to place the /// pattern-origin grip on the hatch body. `None` when there are no boundary /// points. diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index 4bb6c9216..e5abbafec 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -7,6 +7,7 @@ // the MeshModel vertices to match. use acadrust::entities::{Body, Region, Solid3D, Surface}; +use acadrust::kernel::space::polygon; use crate::t; use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, ro_prop as ro}; @@ -203,30 +204,25 @@ fn position_section(prefix: &str, p: &acadrust::types::Vector3) -> PropSection { /// holes subtract) and halves its magnitude — exact for a single planar /// loop, approximate for multi-loop or curved regions. Returns zeros when /// there is nothing to measure. +/// The area and perimeter of a region's boundary wires. +/// +/// Measured in space rather than in projection: a region need not lie in a +/// coordinate plane, and flattening it to XY first would report its shadow. fn region_area_perimeter(wires: &[acadrust::entities::Wire]) -> (f64, f64) { + let mut area_vector = [0.0f64; 3]; let mut perimeter = 0.0; - let (mut nx, mut ny, mut nz) = (0.0, 0.0, 0.0); for wire in wires { - let pts = &wire.points; - if pts.len() < 2 { - continue; - } - for seg in pts.windows(2) { - let dx = seg[1].x - seg[0].x; - let dy = seg[1].y - seg[0].y; - let dz = seg[1].z - seg[0].z; - perimeter += (dx * dx + dy * dy + dz * dz).sqrt(); - } - let n = pts.len(); - for i in 0..n { - let a = &pts[i]; - let b = &pts[(i + 1) % n]; - nx += (a.y - b.y) * (a.z + b.z); - ny += (a.z - b.z) * (a.x + b.x); - nz += (a.x - b.x) * (a.y + b.y); + let ring: Vec<[f64; 3]> = wire.points.iter().map(|p| [p.x, p.y, p.z]).collect(); + // Wires arrive as open chains that the region closes between them, so + // the length is the chain's and the area vectors are summed before + // being measured — one wire's contribution is not an area on its own. + perimeter += polygon::chain_length(&ring); + let piece = polygon::area_vector(&ring); + for axis in 0..3 { + area_vector[axis] += piece[axis]; } } - let area = 0.5 * (nx * nx + ny * ny + nz * nz).sqrt(); + let area = (area_vector[0].powi(2) + area_vector[1].powi(2) + area_vector[2].powi(2)).sqrt(); (area, perimeter) } diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 2c961c096..78ee1dc0a 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -1,9 +1,11 @@ +use acadrust::kernel::space::polygon; use acadrust::{EntityType, Handle}; use glam::DVec3; use crate::command::{ AreaPreviewRegion, AreaPreviewSource, CadCommand, CmdOption, CmdResult, SelectionEntity, }; +use crate::entities::curve::entity_curve; use crate::entities::traits::EntityTypeOps; use crate::scene::model::wire_model::WireModel; @@ -47,100 +49,50 @@ impl AreaCommand { CmdOption { label, keyword: keyword.to_string() } } + /// Area and perimeter of a ring of picked points. + /// + /// Measured in space rather than in projection — a picked sequence need + /// not lie in a coordinate plane, and flattening it to XY first would + /// report its shadow. fn point_measurement(points: &[DVec3], close_perimeter: bool) -> AreaMeasurement { if points.len() < 2 { return AreaMeasurement { area: 0.0, perimeter: Some(0.0) }; } - - let origin = points[0]; - let mut area_vector = DVec3::ZERO; - for index in 0..points.len() { - let a = points[index] - origin; - let b = points[(index + 1) % points.len()] - origin; - area_vector += a.cross(b); + let ring: Vec<[f64; 3]> = points.iter().map(|p| [p.x, p.y, p.z]).collect(); + let perimeter = if close_perimeter { + polygon::perimeter(&ring) + } else { + polygon::chain_length(&ring) + }; + AreaMeasurement { + area: polygon::area(&ring), + perimeter: Some(perimeter), } - - let mut perimeter = points - .windows(2) - .map(|pair| (pair[1] - pair[0]).length()) - .sum::(); - if close_perimeter { - perimeter += (points[0] - points[points.len() - 1]).length(); - } - - AreaMeasurement { area: area_vector.length() * 0.5, perimeter: Some(perimeter) } } - fn bulged_polyline_measurement( - points: &[(f64, f64)], - bulges: &[f64], - closed: bool, - ) -> AreaMeasurement { - let count = points.len(); - if count < 2 { - return AreaMeasurement { area: 0.0, perimeter: Some(0.0) }; - } - - let segment_count = if closed { count } else { count - 1 }; - let origin = points[0]; - let mut signed_area = 0.0; - let mut perimeter = 0.0; - for index in 0..segment_count { - let a = points[index]; - let b = points[(index + 1) % count]; - let local_a = (a.0 - origin.0, a.1 - origin.1); - let local_b = (b.0 - origin.0, b.1 - origin.1); - signed_area += 0.5 * (local_a.0 * local_b.1 - local_b.0 * local_a.1); - - let chord = (b.0 - a.0).hypot(b.1 - a.1); - let bulge = bulges.get(index).copied().unwrap_or(0.0); - if bulge.abs() < 1e-12 || chord <= 1e-12 { - perimeter += chord; - continue; - } - - let angle = 4.0 * bulge.atan(); - let sine = (angle * 0.5).sin().abs(); - if sine <= 1e-12 { - perimeter += chord; - continue; - } - let radius = chord / (2.0 * sine); - signed_area += 0.5 * radius * radius * (angle - angle.sin()); - perimeter += radius * angle.abs(); - } - - AreaMeasurement { area: signed_area.abs(), perimeter: Some(perimeter) } + /// Area and perimeter of an entity whose geometry is a planar curve. + /// + /// Both exact: the kernel measures the curve rather than a polygon + /// through some of its points, so a bulged polyline counts each arc's own + /// bite and length, and a spline is integrated rather than sampled. + fn curve_measurement(entity: &EntityType) -> Option { + let curve = entity_curve(entity)?; + let perimeter = curve.length(); + Some(AreaMeasurement { + area: curve.curve.enclosed_area().abs(), + perimeter: perimeter.is_finite().then_some(perimeter), + }) } fn entity_measurement(entity: &EntityType) -> Option { + // Anything the converter recognises is measured from its own curve. + if let Some(measurement) = Self::curve_measurement(entity) { + return Some(measurement); + } match entity { - EntityType::LwPolyline(polyline) => { - let points = polyline - .vertices - .iter() - .map(|vertex| (vertex.location.x, vertex.location.y)) - .collect::>(); - let bulges = polyline.vertices.iter().map(|vertex| vertex.bulge).collect::>(); - Some(Self::bulged_polyline_measurement( - &points, - &bulges, - polyline.is_closed, - )) - } - EntityType::Polyline2D(polyline) => { - let points = polyline - .vertices - .iter() - .map(|vertex| (vertex.location.x, vertex.location.y)) - .collect::>(); - let bulges = polyline.vertices.iter().map(|vertex| vertex.bulge).collect::>(); - Some(Self::bulged_polyline_measurement( - &points, - &bulges, - polyline.is_closed(), - )) - } + // What is left is the genuinely spatial: a 3D polyline, and a + // spline whose points wander off any plane. Both are measured as + // rings of points in space. EntityType::Polyline(polyline) => { let points = polyline .vertices