Merge pull request #839 from gianlucafiore/fix/extension-intersection

fix(snap): improve OTRACK and extension snapping
This commit is contained in:
gianlucafiore authored and GitHub committed 2026-08-20 18:51:44 -03:00
commit b1b5d31be5
2 files changed
+185 -8

No files matched your search

+143 -7
View File
@@ -253,17 +253,44 @@ impl OpenCADStudio {
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Option<crate::snap::OtrackHit> {
if snap.is_some_and(|hit| hit.snap_type != crate::snap::SnapType::Extension) {
let intersection = snap.filter(|hit| {
hit.snap_type == crate::snap::SnapType::Intersection
});
// Ordinary object snaps suppress OTRACK. Extension and Intersection are
// exceptions: both may lie on an active tracking vector.
if snap.is_some_and(|hit| {
!matches!(
hit.snap_type,
crate::snap::SnapType::Extension | crate::snap::SnapType::Intersection
)
}) {
return None;
}
let required_crossing_ray = match snap {
Some(extension) => Some((extension.extension_origin?, extension.extension_dir?)),
None => None,
Some(extension)
if extension.snap_type == crate::snap::SnapType::Extension =>
{
Some((
extension.extension_origin?,
extension.extension_dir?,
))
}
_ => None,
};
// When Intersection has already won OSNAP selection, test OTRACK at the
// exact intersection rather than at the free cursor position.
let track_cursor = intersection
.map(|hit| hit.world)
.unwrap_or(cursor);
let step = (self.polar_mode && drafting).then_some(self.polar_increment_deg);
let (_, (ucs_x, ucs_y, _)) = self.drafting_grid_basis(tab);
let hit = self.snapper.otrack_snap(
cursor,
let mut hit = self.snapper.otrack_snap(
track_cursor,
view_rot,
eye,
bounds,
@@ -274,6 +301,28 @@ impl OpenCADStudio {
ucs_x.as_dvec3(),
ucs_y.as_dvec3(),
)?;
if let Some(intersection) = intersection {
// Only keep the vector when it really passes through the highlighted
// intersection. This avoids showing an unrelated nearby tracking ray.
let ndc = view_rot.project_point3((hit.aligned - eye).as_vec3());
let aligned_screen = iced::Point::new(
(ndc.x + 1.0) * 0.5 * bounds.width,
(1.0 - ndc.y) * 0.5 * bounds.height,
);
let dx = aligned_screen.x - intersection.screen.x;
let dy = aligned_screen.y - intersection.screen.y;
if dx * dx + dy * dy > 4.0 {
return None;
}
// OTRACK is only providing the visual/reference vector here.
// Intersection remains the exact picked point.
hit.aligned = intersection.world;
}
Some(hit)
}
@@ -1217,7 +1266,14 @@ impl OpenCADStudio {
// Keep the entity's original geometry available for self-OSNAP,
// Extension and OTRACK while the live grip preview is being deformed.
self.grip_snap_wires = snap.clone();
// The user explicitly picked this grip, so treat its original position as an
// acquired tracking point immediately. This lets Extension/OTRACK use the
// original edge direction on the very first movement instead of requiring the
// cursor to return to the old grip position and dwell there first.
self.snapper.acquire_grip_tracking_point(
grip.origin_world,
&self.grip_snap_wires,
);
self.grip_text_verts = snap
.iter()
.flat_map(|w| w.text_verts.iter().copied())
@@ -1756,6 +1812,49 @@ impl OpenCADStudio {
self.axis_lock_dir = None;
None
};
// An acquired OTRACK ray can cross real drawing geometry even before the
// command has a first point. Probe the active tracking ray and let the normal
// snap engine evaluate that ray against nearby geometry as a construction ray.
//
// This makes OTRACK × entity crossings real Intersection snaps, so the normal
// dwell acquisition can subsequently turn the crossing into a tracking point.
if axis_lock.is_none() && !is_window_corner {
let tracking_probe = self.active_otrack_hit(
i,
cursor_world,
None,
self.last_point,
true,
view_rot,
eye,
bounds,
);
if let Some(track) = tracking_probe {
let (go, gr) = self.drafting_grid_basis(i);
let tracked_snap = self.snapper.snap(
snap_cursor,
p,
&snap_candidates,
view_rot,
eye,
bounds,
go,
gr,
Some((track.base, track.base + track.dir)),
);
// The second pass exists only to discover a crossing between the
// active OTRACK ray and real geometry. Keep the original snap result
// for every other kind of snap.
if tracked_snap
.is_some_and(|hit| hit.snap_type == crate::snap::SnapType::Intersection)
{
self.tabs[i].snap_result = tracked_snap;
}
}
}
self.snapper.update_otrack_dwell(
self.tabs[i].snap_result,
&snap_candidates,
@@ -3057,7 +3156,7 @@ impl OpenCADStudio {
.as_ref()
.map(|c| c.needs_entity_pick())
.unwrap_or(false);
let snap_hit = if needs_entity_click {
let mut snap_hit = if needs_entity_click {
None
} else if needs_tan {
self.snapper.snap_tangent_only(
@@ -3099,6 +3198,43 @@ impl OpenCADStudio {
construction_ray,
)
};
// Mirror the cursor-move OTRACK × geometry intersection pass when the
// point is actually clicked. The move path may already display the
// Intersection marker, but click handling recomputes snapping from scratch.
if !needs_entity_click && !needs_tan && !is_window_corner {
let tracking_probe = self.active_otrack_hit(
i,
raw,
None,
self.last_point,
true,
view_rot,
eye,
bounds,
);
if let Some(track) = tracking_probe {
let (go, gr) = self.drafting_grid_basis(i);
let tracked_snap = self.snapper.snap(
snap_cursor,
p,
&snap_candidates,
view_rot,
eye,
bounds,
go,
gr,
Some((track.base, track.base + track.dir)),
);
if tracked_snap
.is_some_and(|hit| hit.snap_type == crate::snap::SnapType::Intersection)
{
snap_hit = tracked_snap;
}
}
}
// Snap runs in model space; the result is already model.
let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw);
// When no UCS is active clamp to world XY; with a UCS the point is
+42 -1
View File
@@ -382,7 +382,24 @@ impl Snapper {
}
}
}
/// Immediately acquire an explicit grip origin as a tracking point.
///
/// Unlike normal cursor acquisition, a grip was deliberately selected by the
/// user, so it should not require OTRACK dwell before Extension/OTRACK can use it.
pub fn acquire_grip_tracking_point<W: WireSource + ?Sized>(
&mut self,
p: DVec3,
wires: &W,
) {
if !self.tracking_active() {
return;
}
// With OTRACK disabled, Extension acquisition must remain endpoint-only.
let endpoints_only = !self.otrack_enabled;
self.acquire_tracking_point(p, wires, endpoints_only);
}
/// Add `p` as a tracking point (capturing its corner edge directions) unless
/// it is already tracked; drops the oldest when the 4-point cap is reached.
/// Edge directions are scanned once here, at acquisition, so OTRACK can align
@@ -1255,7 +1272,31 @@ impl Snapper {
}
}
};
// Check engaged extension tracking rays against nearby real geometry.
// OTRACK can align the cursor to an acquired segment extension even when
// Extension OSNAP itself is not the current snap result. Allow Intersection
// to stop that active extension ray where it crosses drawing geometry.
if self.is_on(SnapType::Intersection) && self.otrack_enabled {
for (&origin, dirs) in self.tracking_points.iter().zip(&self.tracking_dirs) {
for &dir in dirs {
// Only test the extension ray while the cursor is actually
// engaged with it, so Intersection does not become Nearest.
if extension_snap(
cursor_world,
origin,
dir,
view_rot,
eye,
bounds,
self.osnap_radius_px,
)
.is_some()
{
try_ray_intersections(origin, origin + dir);
}
}
}
}
if self.is_on(SnapType::Intersection) {
if let Some((origin, through)) = construction_ray {
try_ray_intersections(origin, through);