diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 2e08abcef..b254f8c14 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -10,7 +10,7 @@ use crate::entities::common::{ use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; -use crate::scene::model::wire_model::{SnapHint, TangentGeom}; +use crate::scene::model::wire_model::TangentGeom; const TAU: f64 = std::f64::consts::TAU; @@ -42,11 +42,10 @@ fn to_truck(arc: &Arc) -> TruckEntity { ) }; - let cv = glam::DVec3::new(cwx, cwy, cwz); - // Arc-length centre — one well-defined midpoint snap. Circles and - // ellipses (closed curves) deliberately don't emit this; see #34. - let mid_pt_3 = arc_pt(mid_a); - let mv = glam::DVec3::new(mid_pt_3.x, mid_pt_3.y, mid_pt_3.z); + // Centre, ends, arc-length midpoint and the quadrants the sweep actually + // covers, all from the entity's own curve. Circles and ellipses (closed + // curves) deliberately emit no midpoint; see #34. + let snap = crate::entities::curve::snap_from(&crate::entities::curve::arc_curve(arc)); let tangent = TangentGeom::Circle { center: [cwx as f32, cwy as f32, cwz as f32], radius: r as f32, @@ -81,7 +80,7 @@ fn to_truck(arc: &Arc) -> TruckEntity { return TruckEntity { pick_tris: extrusion_wall_tris(&base, [t * nx, t * ny, t * nz]), object: TruckObject::Lines(pts), - snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)], + snap_pts: snap.snap_pts.clone(), tangent_geoms: vec![tangent], key_vertices: vec![], fill_tris: vec![], @@ -97,7 +96,7 @@ fn to_truck(arc: &Arc) -> TruckEntity { TruckEntity { pick_tris: Vec::new(), object: TruckObject::Curve(edge), - snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)], + snap_pts: snap.snap_pts.clone(), tangent_geoms: vec![tangent], key_vertices: vec![], fill_tris: vec![], diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 3c1a93f70..983aa473b 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -8,7 +8,7 @@ use crate::entities::common::{ use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; -use crate::scene::model::wire_model::{SnapHint, TangentGeom}; +use crate::scene::model::wire_model::TangentGeom; fn to_truck(circle: &Circle) -> TruckEntity { let cx = circle.center.x; @@ -20,18 +20,11 @@ fn to_truck(circle: &Circle) -> TruckEntity { let (ax, ay) = crate::scene::view::transform::ocs_axes(normal); let (cwx, cwy, cwz) = crate::scene::view::transform::ocs_point_to_wcs((cx, cy, cz), normal); - let cv = glam::DVec3::new(cwx, cwy, cwz); let rf = r as f32; - let q = |d: (f64, f64, f64)| { - glam::DVec3::new(cwx + r * d.0, cwy + r * d.1, cwz + r * d.2) - }; - let snap_pts = vec![ - (cv, SnapHint::Center), - (q(ax), SnapHint::Quadrant), - (q(ay), SnapHint::Quadrant), - (q((-ax.0, -ax.1, -ax.2)), SnapHint::Quadrant), - (q((-ay.0, -ay.1, -ay.2)), SnapHint::Quadrant), - ]; + // Centre and quadrants come from the entity's own curve, so the same + // definition answers here, in the tessellation and in a trim. + let snap = crate::entities::curve::snap_from(&crate::entities::curve::circle_curve(circle)); + let snap_pts = snap.snap_pts; let tangent = TangentGeom::Circle { center: [cwx as f32, cwy as f32, cwz as f32], radius: rf, diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index cde496da1..3552433ba 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -9,7 +9,6 @@ use crate::entities::common::{ use crate::entities::traits::TruckConvertible; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; -use crate::scene::model::wire_model::SnapHint; const TAU: f64 = std::f64::consts::TAU; @@ -43,8 +42,12 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { // Minor axis direction: WCS_normal × u (both unit vectors, always perpendicular). let wcs_normal = glam::Vec3::new(nx as f32, ny as f32, nz as f32); let v_axis = wcs_normal.cross(u); - let center_v3 = glam::DVec3::new(cwx, cwy, cwz); let is_closed = (t1 - t0 - TAU).abs() < 1e-6; + // Centre, axis ends and — for an open arc — its own two ends and middle, + // all read off the entity's curve rather than rebuilt from these locals. + let snap = crate::entities::curve::ellipse_curve(ell) + .map(|curve| crate::entities::curve::snap_from(&curve)) + .unwrap_or_default(); if is_closed { let n = 16usize; @@ -81,21 +84,7 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { let edge_upper = Edge::new(&v_pos, &v_neg, Curve::BSplineCurve(spl_u)); let edge_lower = Edge::new(&v_neg, &v_pos, Curve::BSplineCurve(spl_l)); let wire: Wire = [edge_upper, edge_lower].into_iter().collect(); - // Quadrant points at ±major and ±minor axis endpoints in WCS. - let q = |lx: f64, lz: f64| { - glam::DVec3::new( - cwx + lx * u.x as f64 + lz * v_axis.x as f64, - cwy + lx * u.y as f64 + lz * v_axis.y as f64, - cwz + lx * u.z as f64 + lz * v_axis.z as f64, - ) - }; - let snap_pts = vec![ - (center_v3, SnapHint::Center), - (q(r_major, 0.0), SnapHint::Quadrant), - (q(-r_major, 0.0), SnapHint::Quadrant), - (q(0.0, r_minor), SnapHint::Quadrant), - (q(0.0, -r_minor), SnapHint::Quadrant), - ]; + let snap_pts = snap.snap_pts.clone(); TruckEntity { pick_tris: Vec::new(), object: TruckObject::Contour(wire), @@ -123,18 +112,13 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { let v_start = builder::vertex(*ctrl_pts.first().unwrap()); let v_end = builder::vertex(*ctrl_pts.last().unwrap()); let edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline)); - let pt_start = ctrl_pts.first().unwrap(); - let pt_end = ctrl_pts.last().unwrap(); - let key_vertices: Vec<[f64; 3]> = vec![ - [pt_start.x, pt_start.y, pt_start.z], - [pt_end.x, pt_end.y, pt_end.z], - ]; + TruckEntity { pick_tris: Vec::new(), object: TruckObject::Curve(edge), - snap_pts: vec![(center_v3, SnapHint::Center)], + snap_pts: snap.snap_pts.clone(), tangent_geoms: vec![], - key_vertices, + key_vertices: vec![], fill_tris: vec![], } } diff --git a/src/entities/spline.rs b/src/entities/spline.rs index e644bc91f..a814e3b8f 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -87,12 +87,42 @@ fn to_truck(spl: &Spline) -> TruckEntity { ) }; - let snap_source = if !spl.fit_points.is_empty() { - &spl.fit_points - } else { - &spl.control_points + // Snap sources from the spline's own curve. A spline is not a chain of + // straight segments, so nothing goes in `key_vertices`: consecutive + // entries there are joined and their midpoints offered, and the chord + // between two fit points of a curvy spline does not run along it. + // + // Fit points are on the curve and stay on offer, as ends in their own + // right. Control points are not on the curve and are dropped — snapping + // to one put the cursor in empty space beside the geometry. + let curve_snap = crate::entities::curve::spline_curve(spl) + .map(|curve| crate::entities::curve::snap_from(&curve)); + let (snap_pts, key_vertices) = match curve_snap { + Some(snap) => { + let mut points = snap.snap_pts; + points.extend(spl.fit_points.iter().map(|p| { + ( + glam::DVec3::new(p.x, p.y, p.z), + crate::scene::model::wire_model::SnapHint::Endpoint, + ) + })); + (points, Vec::new()) + } + // A spline through points in space is not a planar curve, so the + // kernel has nothing to say about it. Its stored points remain the + // only thing to offer. + None => { + let source = if spl.fit_points.is_empty() { + &spl.control_points + } else { + &spl.fit_points + }; + ( + Vec::new(), + source.iter().map(|p| [p.x, p.y, p.z]).collect::>(), + ) + } }; - let key_vertices: Vec<[f64; 3]> = snap_source.iter().map(|p| [p.x, p.y, p.z]).collect(); let is_closed = spl.flags.closed || spl.flags.periodic; let gap = { @@ -120,7 +150,7 @@ fn to_truck(spl: &Spline) -> TruckEntity { TruckEntity { pick_tris: Vec::new(), object, - snap_pts: vec![], + snap_pts, tangent_geoms: vec![], key_vertices, fill_tris: vec![], diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 50e9467c0..3ee671e94 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -14,6 +14,14 @@ pub enum SnapHint { /// arc-length centre, a spline's `t = 0.5`). Lines / polylines do /// not use this — their midpoints are derived from `key_vertices`. Midpoint, + /// An end of a curve that is not a chain of straight segments — an arc, + /// an elliptical arc, a spline. + /// + /// Such a curve cannot put its ends in `key_vertices`: consecutive + /// entries there are taken to be joined by a straight segment, so an + /// arc's two ends would also produce a midpoint snap out on the chord, + /// which is not on the geometry. + Endpoint, } /// Geometric primitive used by the tangent-snap engine. diff --git a/src/snap.rs b/src/snap.rs index add3ee424..ffdee88b0 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -1026,6 +1026,7 @@ impl Snapper { SnapHint::Quadrant => SnapType::Quadrant, SnapHint::Insertion => SnapType::Insertion, SnapHint::Midpoint => SnapType::Midpoint, + SnapHint::Endpoint => SnapType::Endpoint, }; if self.is_on(snap_type) { try_pt(world, snap_type);