fix(snap): improve OTRACK and extension snapping
This commit is contained in:
parent
2a852f4479
commit
02e0960138
2 changed files with 185 additions and 8 deletions
|
|
@ -253,17 +253,44 @@ impl OpenCADStudio {
|
||||||
eye: glam::DVec3,
|
eye: glam::DVec3,
|
||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
) -> Option<crate::snap::OtrackHit> {
|
) -> 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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let required_crossing_ray = match snap {
|
let required_crossing_ray = match snap {
|
||||||
Some(extension) => Some((extension.extension_origin?, extension.extension_dir?)),
|
Some(extension)
|
||||||
None => None,
|
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 step = (self.polar_mode && drafting).then_some(self.polar_increment_deg);
|
||||||
let (_, (ucs_x, ucs_y, _)) = self.drafting_grid_basis(tab);
|
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,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -274,6 +301,28 @@ impl OpenCADStudio {
|
||||||
ucs_x.as_dvec3(),
|
ucs_x.as_dvec3(),
|
||||||
ucs_y.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)
|
Some(hit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1217,7 +1266,14 @@ impl OpenCADStudio {
|
||||||
// Keep the entity's original geometry available for self-OSNAP,
|
// Keep the entity's original geometry available for self-OSNAP,
|
||||||
// Extension and OTRACK while the live grip preview is being deformed.
|
// Extension and OTRACK while the live grip preview is being deformed.
|
||||||
self.grip_snap_wires = snap.clone();
|
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
|
self.grip_text_verts = snap
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|w| w.text_verts.iter().copied())
|
.flat_map(|w| w.text_verts.iter().copied())
|
||||||
|
|
@ -1756,6 +1812,49 @@ impl OpenCADStudio {
|
||||||
self.axis_lock_dir = None;
|
self.axis_lock_dir = None;
|
||||||
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.snapper.update_otrack_dwell(
|
||||||
self.tabs[i].snap_result,
|
self.tabs[i].snap_result,
|
||||||
&snap_candidates,
|
&snap_candidates,
|
||||||
|
|
@ -3055,7 +3154,7 @@ impl OpenCADStudio {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.needs_entity_pick())
|
.map(|c| c.needs_entity_pick())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let snap_hit = if needs_entity_click {
|
let mut snap_hit = if needs_entity_click {
|
||||||
None
|
None
|
||||||
} else if needs_tan {
|
} else if needs_tan {
|
||||||
self.snapper.snap_tangent_only(
|
self.snapper.snap_tangent_only(
|
||||||
|
|
@ -3097,6 +3196,43 @@ impl OpenCADStudio {
|
||||||
construction_ray,
|
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.
|
// Snap runs in model space; the result is already model.
|
||||||
let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw);
|
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
|
// When no UCS is active clamp to world XY; with a UCS the point is
|
||||||
|
|
|
||||||
43
src/snap.rs
43
src/snap.rs
|
|
@ -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
|
/// 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.
|
/// 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
|
/// 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 self.is_on(SnapType::Intersection) {
|
||||||
if let Some((origin, through)) = construction_ray {
|
if let Some((origin, through)) = construction_ray {
|
||||||
try_ray_intersections(origin, through);
|
try_ray_intersections(origin, through);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue