forked from 1b-agent/opencadstudio-upstream
fix: preserve perpendicular tracking after typed distance
This commit is contained in:
1 parent
6df70538ea
commit
99fff30c53
3 files changed
+111
-2
No files matched your search
@@ -977,7 +977,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
// A tracking ray carries its own base, so it needs no anchor: that is
|
// A tracking ray carries its own base, so it needs no anchor: that is
|
||||||
// how OTRACK and Extension accepted a bare distance for the *first*
|
// how OTRACK and Extension accepted a bare distance for the *first*
|
||||||
// point of a command, before any anchor exists.
|
// point of a command, before any anchor exists.
|
||||||
let pt = if let Some((base, dir)) = self.active_distance_ray(i) {
|
let distance_ray = self.active_distance_ray(i);
|
||||||
|
let pt = if let Some((base, dir)) = distance_ray {
|
||||||
base + dir * dist
|
base + dir * dist
|
||||||
} else {
|
} else {
|
||||||
let anchor = self.tabs[i]
|
let anchor = self.tabs[i]
|
||||||
@@ -1010,6 +1011,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
self.dyn_coord_absolute = false;
|
self.dyn_coord_absolute = false;
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
self.reset_tracking_after_point();
|
self.reset_tracking_after_point();
|
||||||
|
if let Some((_, dir)) = distance_ray {
|
||||||
|
self.snapper.remember_distance_reference(dir);
|
||||||
|
}
|
||||||
self.push_ucs_to_cmd(i);
|
self.push_ucs_to_cmd(i);
|
||||||
|
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt))?;
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt))?;
|
||||||
|
|||||||
@@ -679,6 +679,7 @@ impl OpenCADStudio {
|
|||||||
self.dyn_coord_absolute = false;
|
self.dyn_coord_absolute = false;
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
self.reset_tracking_after_point();
|
self.reset_tracking_after_point();
|
||||||
|
self.snapper.remember_distance_reference(dir);
|
||||||
self.push_ucs_to_cmd(i);
|
self.push_ucs_to_cmd(i);
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
||||||
let task = result.map(|r| self.apply_cmd_result(r))?;
|
let task = result.map(|r| self.apply_cmd_result(r))?;
|
||||||
|
|||||||
+105
-1
@@ -237,6 +237,11 @@ pub struct Snapper {
|
|||||||
/// consumed by Extension snap and extended-intersection logic. These rays
|
/// consumed by Extension snap and extended-intersection logic. These rays
|
||||||
/// belong only to OTRACK when Perpendicular OSNAP is enabled. (#695)
|
/// belong only to OTRACK when Perpendicular OSNAP is enabled. (#695)
|
||||||
pub tracking_perp_dirs: Vec<Vec<DVec3>>,
|
pub tracking_perp_dirs: Vec<Vec<DVec3>>,
|
||||||
|
/// Perpendicular direction carried from a point placed by typed distance
|
||||||
|
/// along an Extension/OTRACK ray. It survives only until the next point
|
||||||
|
/// commit, so the source ray can still drive a perpendicular construction
|
||||||
|
/// through the newly placed point.
|
||||||
|
post_distance_perp_dir: Option<DVec3>,
|
||||||
/// Last snap world position (for dwell detection).
|
/// Last snap world position (for dwell detection).
|
||||||
pub last_snap_world: Option<DVec3>,
|
pub last_snap_world: Option<DVec3>,
|
||||||
/// When the cursor first rested near `last_snap_world`.
|
/// When the cursor first rested near `last_snap_world`.
|
||||||
@@ -301,6 +306,7 @@ impl Default for Snapper {
|
|||||||
tracking_points: Vec::new(),
|
tracking_points: Vec::new(),
|
||||||
tracking_dirs: Vec::new(),
|
tracking_dirs: Vec::new(),
|
||||||
tracking_perp_dirs: Vec::new(),
|
tracking_perp_dirs: Vec::new(),
|
||||||
|
post_distance_perp_dir: None,
|
||||||
last_snap_world: None,
|
last_snap_world: None,
|
||||||
dwell_since: None,
|
dwell_since: None,
|
||||||
dwell_acquired: false,
|
dwell_acquired: false,
|
||||||
@@ -751,7 +757,18 @@ impl Snapper {
|
|||||||
ucs_x: DVec3,
|
ucs_x: DVec3,
|
||||||
ucs_y: DVec3,
|
ucs_y: DVec3,
|
||||||
) -> Option<OtrackHit> {
|
) -> Option<OtrackHit> {
|
||||||
if !self.otrack_enabled || self.tracking_points.is_empty() {
|
// A point created by typed distance along Extension/OTRACK carries
|
||||||
|
// that source direction for exactly one following point. Perpendicular
|
||||||
|
// OSNAP may use it even when F11 tracking itself is off.
|
||||||
|
let carried_perp = if self.is_on(SnapType::Perpendicular) {
|
||||||
|
self.post_distance_perp_dir.zip(last_point)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!self.otrack_enabled || self.tracking_points.is_empty())
|
||||||
|
&& carried_perp.is_none()
|
||||||
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,6 +878,18 @@ impl Snapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Typed distance along an Extension/OTRACK ray creates a new point
|
||||||
|
// that still knows the direction it came from. Offer exactly one
|
||||||
|
// perpendicular ray through that new point for the following pick.
|
||||||
|
if let Some((dir, origin)) = carried_perp {
|
||||||
|
rays.push(Ray {
|
||||||
|
origin,
|
||||||
|
dir,
|
||||||
|
group: self.tracking_points.len(),
|
||||||
|
kind: TrackingKind::Perpendicular,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// OTRACK rays come first; the auxiliary rays appended below (polar from
|
// OTRACK rays come first; the auxiliary rays appended below (polar from
|
||||||
// last_point, ortho axis from last_point) only participate in
|
// last_point, ortho axis from last_point) only participate in
|
||||||
// intersection locking, never in single-ray fallback.
|
// intersection locking, never in single-ray fallback.
|
||||||
@@ -1006,11 +1035,21 @@ impl Snapper {
|
|||||||
best.map(|(_, h)| h)
|
best.map(|(_, h)| h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Carry the direction of a reference ray used by typed-distance entry
|
||||||
|
/// into the next point, as its perpendicular construction direction.
|
||||||
|
pub fn remember_distance_reference(&mut self, dir: DVec3) {
|
||||||
|
let planar = DVec3::new(dir.x, dir.y, 0.0);
|
||||||
|
self.post_distance_perp_dir = planar
|
||||||
|
.try_normalize()
|
||||||
|
.map(|unit| DVec3::new(-unit.y, unit.x, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear all acquired tracking points (e.g. when command ends).
|
/// Clear all acquired tracking points (e.g. when command ends).
|
||||||
pub fn clear_tracking(&mut self) {
|
pub fn clear_tracking(&mut self) {
|
||||||
self.tracking_points.clear();
|
self.tracking_points.clear();
|
||||||
self.tracking_dirs.clear();
|
self.tracking_dirs.clear();
|
||||||
self.tracking_perp_dirs.clear();
|
self.tracking_perp_dirs.clear();
|
||||||
|
self.post_distance_perp_dir = None;
|
||||||
self.parallel_ref = None;
|
self.parallel_ref = None;
|
||||||
self.parallel_dwell = None;
|
self.parallel_dwell = None;
|
||||||
self.last_snap_world = None;
|
self.last_snap_world = None;
|
||||||
@@ -1152,6 +1191,7 @@ impl Snapper {
|
|||||||
tracking_points: Vec::new(),
|
tracking_points: Vec::new(),
|
||||||
tracking_dirs: Vec::new(),
|
tracking_dirs: Vec::new(),
|
||||||
tracking_perp_dirs: Vec::new(),
|
tracking_perp_dirs: Vec::new(),
|
||||||
|
post_distance_perp_dir: None,
|
||||||
last_snap_world: None,
|
last_snap_world: None,
|
||||||
dwell_since: None,
|
dwell_since: None,
|
||||||
dwell_acquired: false,
|
dwell_acquired: false,
|
||||||
@@ -4165,6 +4205,70 @@ mod ext_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_distance_reference_offers_perpendicular_for_next_point() {
|
||||||
|
let mut s = Snapper::default();
|
||||||
|
s.enabled.insert(SnapType::Perpendicular);
|
||||||
|
// This must not depend on F11: the carried construction belongs to
|
||||||
|
// Perpendicular OSNAP itself.
|
||||||
|
s.otrack_enabled = false;
|
||||||
|
s.osnap_radius_px = 10.0;
|
||||||
|
|
||||||
|
// The first point was placed along a horizontal Extension ray.
|
||||||
|
s.remember_distance_reference(DVec3::X);
|
||||||
|
let base = DVec3::new(2_000.0, 3_000.0, 0.0);
|
||||||
|
|
||||||
|
let view_rot = Mat4::from_scale(Vec3::splat(0.0001));
|
||||||
|
let eye = DVec3::ZERO;
|
||||||
|
let bounds = Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: 1000.0,
|
||||||
|
height: 1000.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pull almost vertically from the newly placed point. The preserved
|
||||||
|
// horizontal reference must produce a vertical perpendicular guide.
|
||||||
|
let cursor = base + DVec3::Y * 5_000.0 + DVec3::X * 0.05;
|
||||||
|
let hit = s
|
||||||
|
.otrack_snap(
|
||||||
|
cursor,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
None,
|
||||||
|
Some(base),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
DVec3::X,
|
||||||
|
DVec3::Y,
|
||||||
|
)
|
||||||
|
.expect("carried Extension direction should offer perpendicular");
|
||||||
|
|
||||||
|
assert_eq!(hit.kind, TrackingKind::Perpendicular);
|
||||||
|
assert!((hit.base - base).length() < 1e-12);
|
||||||
|
assert!((hit.aligned.x - base.x).abs() < 1e-12);
|
||||||
|
assert!((hit.aligned.y - (base.y + 5_000.0)).abs() < 1e-9);
|
||||||
|
|
||||||
|
// It is deliberately one-step state: the next accepted point clears it.
|
||||||
|
s.clear_tracking();
|
||||||
|
assert!(
|
||||||
|
s.otrack_snap(
|
||||||
|
cursor,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
None,
|
||||||
|
Some(base),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
DVec3::X,
|
||||||
|
DVec3::Y,
|
||||||
|
)
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tangent_points_are_perpendicular_to_the_radius() {
|
fn tangent_points_are_perpendicular_to_the_radius() {
|
||||||
let c = Vec3::new(0.0, 0.0, 0.0);
|
let c = Vec3::new(0.0, 0.0, 0.0);
|
||||||
|
|||||||
Reference in new issue
Block a user