forked from 1b-agent/opencadstudio-upstream
fix(snap): arc segments of a polyline offer their centre and quadrants
Center and Quadrant found nothing on a polyline's bulge segments: the lightweight and heavy polylines carried no snap points for them, and a polyline split into several wires kept its points on one piece whose box the others' never reached. Each arc segment now carries its centre and quadrants, hovering the segment offers its centre, and a wire's box covers its snap points. Fixes #1640 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29392de267
commit
fae64886ee
6 files changed
+150
-28
No files matched your search
+38
-3
@@ -484,9 +484,9 @@ pub fn snap_from(curve: &PlanarCurve) -> CurveSnap {
|
||||
// there and let their midpoints be derived; everything else names its own
|
||||
// ends and middle explicitly.
|
||||
//
|
||||
// The same distinction is why a polyline's arc-segment centres are not
|
||||
// emitted: a wire carrying a centre is treated as round elsewhere, which
|
||||
// a polyline with one bulge in it is not.
|
||||
// The same distinction is why a polyline's arc-segment centres carry
|
||||
// their own hint: a wire carrying a `Center` is treated as round
|
||||
// elsewhere, which a polyline with one bulge in it is not.
|
||||
let chain = matches!(curve.curve, Curve::Line(_) | Curve::Polyline(_));
|
||||
let mut out = CurveSnap::default();
|
||||
let mut push = |world: [f64; 3], hint: SnapHint| {
|
||||
@@ -499,6 +499,7 @@ pub fn snap_from(curve: &PlanarCurve) -> CurveSnap {
|
||||
SnapKind::Endpoint => push(world, SnapHint::Endpoint),
|
||||
SnapKind::Midpoint if !chain => push(world, SnapHint::Midpoint),
|
||||
SnapKind::Centre if !chain => push(world, SnapHint::Center),
|
||||
SnapKind::Centre => push(world, SnapHint::ArcCenter),
|
||||
SnapKind::Quadrant => push(world, SnapHint::Quadrant),
|
||||
_ => {}
|
||||
}
|
||||
@@ -506,6 +507,20 @@ pub fn snap_from(curve: &PlanarCurve) -> CurveSnap {
|
||||
out
|
||||
}
|
||||
|
||||
/// The centres and quadrants of a polyline's arc segments, for a polyline
|
||||
/// render that names its vertices itself.
|
||||
pub fn arc_segment_snaps(entity: &EntityType) -> Vec<(glam::DVec3, SnapHint)> {
|
||||
entity_curve(entity)
|
||||
.map(|curve| {
|
||||
snap_from(&curve)
|
||||
.snap_pts
|
||||
.into_iter()
|
||||
.filter(|(_, hint)| matches!(hint, SnapHint::ArcCenter | SnapHint::Quadrant))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The magnitude the geometry's own coordinates sit at, for scaling a
|
||||
/// relative tolerance. Floored at one so geometry near the origin does not
|
||||
/// end up with a tolerance of nothing.
|
||||
@@ -806,6 +821,26 @@ mod tests {
|
||||
assert!(snap.snap_pts.is_empty(), "{:?}", snap.snap_pts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_polyline_arc_segment_offers_its_centre_and_quadrants() {
|
||||
let mut polyline = LwPolylineEnt::default();
|
||||
polyline.normal = v3(0.0, 0.0, 1.0);
|
||||
let mut bend = LwVertex::from_coords(0.0, 0.0);
|
||||
bend.bulge = 0.9999999999999999;
|
||||
polyline.vertices = vec![LwVertex::from_coords(-20.0, 0.0), bend, LwVertex::from_coords(20.0, 0.0)];
|
||||
let snaps = arc_segment_snaps(&EntityType::LwPolyline(polyline));
|
||||
let at = |hint: SnapHint, x: f64, y: f64| {
|
||||
snaps.iter().any(|(p, h)| {
|
||||
std::mem::discriminant(h) == std::mem::discriminant(&hint)
|
||||
&& (p.x - x).abs() < 1e-9
|
||||
&& (p.y - y).abs() < 1e-9
|
||||
})
|
||||
};
|
||||
assert!(at(SnapHint::ArcCenter, 10.0, 0.0), "{snaps:?}");
|
||||
assert!(at(SnapHint::Quadrant, 10.0, -10.0), "{snaps:?}");
|
||||
assert!(!at(SnapHint::Quadrant, 10.0, 10.0), "{snaps:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_curve_has_no_ends_to_offer() {
|
||||
let mut circle = CircleEnt::default();
|
||||
|
||||
@@ -1226,7 +1226,13 @@ fn apply_transform(pline: &mut LwPolyline, t: &EntityTransform) {
|
||||
|
||||
impl RenderConvertible for LwPolyline {
|
||||
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
||||
Some(to_render(self, document.header.fill_mode))
|
||||
let mut render = to_render(self, document.header.fill_mode);
|
||||
// Arc segments offer their centres and quadrants.
|
||||
if self.vertices.iter().any(|v| v.bulge != 0.0) {
|
||||
let entity = codec::EntityType::LwPolyline(self.clone());
|
||||
render.snap_pts.extend(crate::entities::curve::arc_segment_snaps(&entity));
|
||||
}
|
||||
Some(render)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -493,7 +493,13 @@ fn centerline_metadata_2d(
|
||||
|
||||
impl RenderConvertible for Polyline2D {
|
||||
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
||||
Some(tessellate_polyline2d(self, document.header.fill_mode))
|
||||
let mut render = tessellate_polyline2d(self, document.header.fill_mode);
|
||||
// Arc segments offer their centres and quadrants.
|
||||
if self.vertices.iter().any(|v| v.bulge != 0.0) {
|
||||
let entity = codec::EntityType::Polyline2D(self.clone());
|
||||
render.snap_pts.extend(crate::entities::curve::arc_segment_snaps(&entity));
|
||||
}
|
||||
Some(render)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1766,6 +1766,23 @@ pub(crate) fn set_wire_aabb(w: &mut WireModel, entity_box: [f32; 4]) {
|
||||
for (i, &p) in w.pick_tris.iter().enumerate() {
|
||||
extend(p, w.pick_tris_low.get(i).copied().unwrap_or([0.0; 3]));
|
||||
}
|
||||
// The entity's snap points ride one wire, which may be only one piece of
|
||||
// it (a polyline drawn per segment): its box must reach them, or a snap
|
||||
// on another piece — an arc segment's quadrant — is never tried.
|
||||
let split = |x: f64| {
|
||||
let high = x as f32;
|
||||
[high, (x - high as f64) as f32]
|
||||
};
|
||||
let points = w
|
||||
.snap_pts
|
||||
.iter()
|
||||
.map(|(p, _)| [p.x, p.y])
|
||||
.chain(w.key_vertices.iter().map(|p| [p[0], p[1]]))
|
||||
.collect::<Vec<_>>();
|
||||
for [x, y] in points {
|
||||
let ([xh, xl], [yh, yl]) = (split(x), split(y));
|
||||
extend([xh, yh, 0.0], [xl, yl, 0.0]);
|
||||
}
|
||||
|
||||
w.aabb = out;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
pub enum SnapHint {
|
||||
/// Geometric center of a circle, arc, or ellipse.
|
||||
Center,
|
||||
/// Centre of one arc segment of a chain (a polyline). Snaps like
|
||||
/// `Center`, but does not make the wire count as round.
|
||||
ArcCenter,
|
||||
/// Area centroid of a closed polyline.
|
||||
GeometricCenter,
|
||||
/// Point entity location.
|
||||
|
||||
+78
-23
@@ -1442,7 +1442,9 @@ impl Snapper {
|
||||
// 3D hints run on the separate 3D master + mode set; everything
|
||||
// else stays on the 2D master + mode set.
|
||||
let (snap_type, on) = match hint {
|
||||
SnapHint::Center => (SnapType::Center, self.is_on(SnapType::Center)),
|
||||
SnapHint::Center | SnapHint::ArcCenter => {
|
||||
(SnapType::Center, self.is_on(SnapType::Center))
|
||||
}
|
||||
SnapHint::GeometricCenter => (
|
||||
SnapType::GeometricCenter,
|
||||
self.is_on(SnapType::GeometricCenter),
|
||||
@@ -2475,12 +2477,35 @@ impl Snapper {
|
||||
// so it can update the candidate state directly. (#152)
|
||||
// 2D master applies (this direct-evaluation pass bypasses `try_pt`).
|
||||
if self.snap_enabled && self.is_on(SnapType::Center) {
|
||||
let mut offer = |wire: &WireModel, curve_d2: f32| {
|
||||
// A polyline drawn piece by piece keeps its arc centres on one of
|
||||
// its wires: the one with the same name that carries them.
|
||||
let carries = |w: &WireModel| {
|
||||
w.snap_pts.iter().any(|(_, hint)| matches!(hint, SnapHint::ArcCenter))
|
||||
};
|
||||
let arc_centre = |wire: &WireModel, on: (DVec3, DVec3)| -> Option<DVec3> {
|
||||
if carries(wire) {
|
||||
return arc_segment_center(wire, on);
|
||||
}
|
||||
let carrier = wires
|
||||
.iter()
|
||||
.find(|w| !wire.name.is_empty() && w.name == wire.name && carries(w))?;
|
||||
arc_segment_center(carrier, on)
|
||||
};
|
||||
let has_centre = |wire: &WireModel| {
|
||||
wire.snap_pts.iter().any(|(_, hint)| matches!(hint, SnapHint::Center))
|
||||
|| carries(wire)
|
||||
|| (!wire.name.is_empty()
|
||||
&& wires.iter().any(|w| w.name == wire.name && carries(w)))
|
||||
};
|
||||
// `on`: the piece of the wire under the cursor, which picks out
|
||||
// the arc segment of a polyline whose centre is offered.
|
||||
let mut offer = |wire: &WireModel, curve_d2: f32, on: Option<(DVec3, DVec3)>| {
|
||||
let Some(center) = wire
|
||||
.snap_pts
|
||||
.iter()
|
||||
.find(|(_, hint)| matches!(hint, SnapHint::Center))
|
||||
.map(|&(c, _)| c)
|
||||
.or_else(|| arc_centre(wire, on?))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -2517,16 +2542,12 @@ impl Snapper {
|
||||
}
|
||||
};
|
||||
if let Some(segments) = &local_segments {
|
||||
let mut distances: HashMap<u32, f32> = HashMap::default();
|
||||
let mut distances: HashMap<u32, (f32, (DVec3, DVec3))> = HashMap::default();
|
||||
for segment in segments {
|
||||
let Some(wire) = wires.source_wire(segment.wire) else {
|
||||
continue;
|
||||
};
|
||||
if !wire
|
||||
.snap_pts
|
||||
.iter()
|
||||
.any(|(_, hint)| matches!(hint, SnapHint::Center))
|
||||
{
|
||||
if !has_centre(wire) {
|
||||
continue;
|
||||
}
|
||||
let nearest = nearest_on_segment(cursor_world, segment.a, segment.b);
|
||||
@@ -2534,31 +2555,38 @@ impl Snapper {
|
||||
world_to_screen(nearest, view_rot, eye, bounds),
|
||||
cursor_screen,
|
||||
);
|
||||
let piece = (segment.a, segment.b);
|
||||
distances
|
||||
.entry(segment.wire)
|
||||
.and_modify(|best| *best = best.min(distance))
|
||||
.or_insert(distance);
|
||||
.and_modify(|best| {
|
||||
if distance < best.0 {
|
||||
*best = (distance, piece);
|
||||
}
|
||||
})
|
||||
.or_insert((distance, piece));
|
||||
}
|
||||
for (wire_idx, distance) in distances {
|
||||
for (wire_idx, (distance, piece)) in distances {
|
||||
if let Some(wire) = wires.source_wire(wire_idx) {
|
||||
offer(wire, distance);
|
||||
offer(wire, distance, Some(piece));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for wire in in_range_wires.iter() {
|
||||
let mut curve_d2 = f32::INFINITY;
|
||||
let mut piece = None;
|
||||
for index in 0..wire.points.len().saturating_sub(1) {
|
||||
let nearest = nearest_on_segment(
|
||||
cursor_world,
|
||||
wp_f64(wire, index),
|
||||
wp_f64(wire, index + 1),
|
||||
);
|
||||
curve_d2 = curve_d2.min(dist2(
|
||||
let (a, b) = (wp_f64(wire, index), wp_f64(wire, index + 1));
|
||||
let nearest = nearest_on_segment(cursor_world, a, b);
|
||||
let distance = dist2(
|
||||
world_to_screen(nearest, view_rot, eye, bounds),
|
||||
cursor_screen,
|
||||
));
|
||||
);
|
||||
if distance < curve_d2 {
|
||||
curve_d2 = distance;
|
||||
piece = Some((a, b));
|
||||
}
|
||||
}
|
||||
offer(wire, curve_d2);
|
||||
offer(wire, curve_d2, piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2589,6 +2617,29 @@ impl Snapper {
|
||||
}
|
||||
}
|
||||
|
||||
/// The centre of the polyline arc segment a drawn piece `(a, b)` of `wire`
|
||||
/// belongs to: the one its two ends are equally far from. A piece joining two
|
||||
/// of the chain's own vertices is a straight segment, which has none.
|
||||
fn arc_segment_center(wire: &WireModel, (a, b): (DVec3, DVec3)) -> Option<DVec3> {
|
||||
let vertex = |p: DVec3| {
|
||||
wire.key_vertices
|
||||
.iter()
|
||||
.any(|v| DVec3::from_array(*v).distance_squared(p) <= 1e-18 * p.length_squared().max(1.0))
|
||||
};
|
||||
if vertex(a) && vertex(b) {
|
||||
return None;
|
||||
}
|
||||
wire.snap_pts
|
||||
.iter()
|
||||
.filter(|(_, hint)| matches!(hint, SnapHint::ArcCenter))
|
||||
.map(|&(c, _)| (c, (c.distance(a) - c.distance(b)).abs()))
|
||||
// ponytail: a thousandth of the radius absorbs the drawn points' f32
|
||||
// rounding far from the origin; two arcs that close would both fit.
|
||||
.filter(|(c, gap)| *gap <= 1e-3 * c.distance(a).max(1e-9))
|
||||
.min_by(|x, y| x.1.total_cmp(&y.1))
|
||||
.map(|(c, _)| c)
|
||||
}
|
||||
|
||||
// ── Object-snap priority ───────────────────────────────────────────────────
|
||||
|
||||
/// Selection priority for an object snap — lower wins. Discrete snaps that
|
||||
@@ -2769,10 +2820,14 @@ fn tracking_dirs_at<W: WireSource + ?Sized>(
|
||||
|
||||
edges.len() >= 6 && perps.len() >= 6
|
||||
};
|
||||
// A chain (polyline) keeps its straight segments even when an arc
|
||||
// segment gives it quadrants.
|
||||
let is_round = |wire: &WireModel| {
|
||||
wire.snap_pts
|
||||
.iter()
|
||||
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant | SnapHint::Center))
|
||||
wire.key_vertices.is_empty()
|
||||
&& wire
|
||||
.snap_pts
|
||||
.iter()
|
||||
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant | SnapHint::Center))
|
||||
};
|
||||
if let Some(segments) = indexed_segments(wires) {
|
||||
for segment in segments {
|
||||
|
||||
Reference in new issue
Block a user