refactor(snap): take an entity's snap points from its curve

Each entity built its own, and what they offered had drifted apart from
what the geometry actually has:

- An arc offered no quadrants at all. It now offers the ones its sweep
  covers, and no more.
- A spline offered its control points as endpoints. For a control-point
  spline those are not on the curve, so the cursor landed in empty space
  beside the geometry. It now offers its two real ends and its midpoint,
  with the fit points — which are on the curve — still on offer.
- An elliptical arc had no midpoint and no ends.

A new SnapHint::Endpoint carries an end that cannot go in key_vertices.
Consecutive entries there are taken to be joined by a straight segment
and their midpoint offered, which is right for a line or a polyline and
wrong for anything curved: an arc's two ends would also produce a snap
out on the chord. Splines lose their key_vertices for the same reason —
the chord between two fit points of a curvy spline does not run along
it.

A polyline's midpoints and arc-segment centres stay out of snap_pts
deliberately. The engine already derives a chain's midpoints from its
vertices, and a wire carrying a centre is treated as round elsewhere,
which a polyline with one bulge in it is not.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5 committed 2026-08-09 00:43:44 +03:00
1 parent 57661c5754
commit b36e85e46e
6 files changed
+66 -51

No files matched your search

+7 -8
View File
@@ -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![],
+5 -12
View File
@@ -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,
+9 -25
View File
@@ -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![],
}
}
+36 -6
View File
@@ -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::<Vec<_>>(),
)
}
};
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![],
+8
View File
@@ -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.
+1
View File
@@ -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);