forked from 1b-agent/opencadstudio-upstream
Merge pull request #839 from gianlucafiore/fix/extension-intersection
fix(snap): improve OTRACK and extension snapping
This commit is contained in:
2 files changed
+185
-8
No files matched your search
+143
-7
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user