forked from 1b-agent/opencadstudio-upstream
Merge pull request #1853 from gianlucafiore/fix/extension-guide-visual-anchor
fix(osnap): keep Extension guides anchored when moving the cursor
This commit is contained in:
3 files changed
+239
-85
No files matched your search
+56
-18
@@ -31,6 +31,56 @@ const UCS_ICON_PICK_PX: f32 = 10.0;
|
||||
/// Pixel half-width for clicking an icon arm (line segment).
|
||||
const UCS_ICON_ARM_PX: f32 = 6.0;
|
||||
|
||||
/// Snapper coordinates are local to the active pane, while the overlay
|
||||
/// consumes canvas coordinates. Apply the same translation to the visible
|
||||
/// snap glyph and both Extension guide origins.
|
||||
fn snap_pixels_to_canvas(hit: &mut crate::snap::SnapResult, pane: iced::Rectangle) {
|
||||
let translate = |point: &mut Point| {
|
||||
point.x += pane.x;
|
||||
point.y += pane.y;
|
||||
};
|
||||
translate(&mut hit.screen);
|
||||
if let Some(base) = hit.extension_base.as_mut() {
|
||||
translate(base);
|
||||
}
|
||||
if let Some(base) = hit.extension_base2.as_mut() {
|
||||
translate(base);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod extension_canvas_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn translates_extension_anchors_with_the_snap_marker() {
|
||||
let mut hit = crate::snap::SnapResult {
|
||||
world: glam::DVec3::ZERO,
|
||||
screen: Point::new(150.0, 75.0),
|
||||
snap_type: crate::snap::SnapType::Extension,
|
||||
tangent_obj: None,
|
||||
extension_base: Some(Point::new(100.0, 150.0)),
|
||||
extension_base2: Some(Point::new(75.0, 200.0)),
|
||||
extension_origin: None,
|
||||
extension_dir: None,
|
||||
viewport: None,
|
||||
source: None,
|
||||
secondary_source: None,
|
||||
model_point: None,
|
||||
};
|
||||
let pane = iced::Rectangle {
|
||||
x: 32.0,
|
||||
y: 56.0,
|
||||
width: 600.0,
|
||||
height: 400.0,
|
||||
};
|
||||
snap_pixels_to_canvas(&mut hit, pane);
|
||||
assert_eq!(hit.screen, Point::new(182.0, 131.0));
|
||||
assert_eq!(hit.extension_base, Some(Point::new(132.0, 206.0)));
|
||||
assert_eq!(hit.extension_base2, Some(Point::new(107.0, 256.0)));
|
||||
}
|
||||
}
|
||||
|
||||
fn pt_pt_d2(a: Point, b: Point) -> f32 {
|
||||
(a.x - b.x).powi(2) + (a.y - b.y).powi(2)
|
||||
}
|
||||
@@ -1855,8 +1905,7 @@ impl OpenCADStudio {
|
||||
snap_hit.map(|s| s.world).unwrap_or(raw)
|
||||
};
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
s.screen.x += tile_b.x;
|
||||
s.screen.y += tile_b.y;
|
||||
snap_pixels_to_canvas(s, tile_b);
|
||||
}
|
||||
|
||||
if axis_lock.is_none()
|
||||
@@ -2900,11 +2949,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// Snap glyph is positioned in canvas space; shift the
|
||||
// tile-local snap screen point back to the full canvas.
|
||||
// Move Extension's acquired endpoints along with the snap glyph.
|
||||
// Previously only s.screen moved to canvas pixels, leaving the
|
||||
// dashed Extension line offset from the actual endpoint.
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
s.screen.x += tile_b.x;
|
||||
s.screen.y += tile_b.y;
|
||||
snap_pixels_to_canvas(s, tile_b);
|
||||
}
|
||||
|
||||
// Give the command the current UCS + Ctrl state before it
|
||||
@@ -3438,18 +3487,7 @@ impl OpenCADStudio {
|
||||
let world = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||
self.tabs[i].snap_result = snap_hit;
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
// Snap marker and its extension anchors are pane-local; lift them to
|
||||
// absolute canvas px.
|
||||
s.screen.x += tile_b.x;
|
||||
s.screen.y += tile_b.y;
|
||||
if let Some(base) = s.extension_base.as_mut() {
|
||||
base.x += tile_b.x;
|
||||
base.y += tile_b.y;
|
||||
}
|
||||
if let Some(base) = s.extension_base2.as_mut() {
|
||||
base.x += tile_b.x;
|
||||
base.y += tile_b.y;
|
||||
}
|
||||
snap_pixels_to_canvas(s, tile_b);
|
||||
}
|
||||
|
||||
use codec::types::Vector3;
|
||||
|
||||
+62
-8
@@ -914,6 +914,60 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
ob.y + (1.0 - ndc.y) * 0.5 * ob.height,
|
||||
)
|
||||
};
|
||||
// Project a *ray* using a screen-space baseline long enough to
|
||||
// survive f32 pixel rounding. Projecting base + one world unit can
|
||||
// produce a subpixel delta at low zoom: normalizing that delta in
|
||||
// the overlay magnifies the rounding error into a visibly wandering
|
||||
// guide, even when the acquired world-space ray is fixed.
|
||||
let ost_project_ray = |origin: glam::DVec3,
|
||||
dir: glam::DVec3,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
ob: iced::Rectangle|
|
||||
-> Option<(iced::Point, iced::Point)> {
|
||||
let b = ost_project(origin, view_rot, eye, ob);
|
||||
if !b.x.is_finite() || !b.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let unit = dir.try_normalize()?;
|
||||
let mut distance = 1.0_f64;
|
||||
for _ in 0..24 {
|
||||
let tip = ost_project(origin + unit * distance, view_rot, eye, ob);
|
||||
let length = (tip.x - b.x).hypot(tip.y - b.y);
|
||||
if length.is_finite() && length >= 64.0 {
|
||||
return Some((b, tip));
|
||||
}
|
||||
distance *= 2.0;
|
||||
}
|
||||
None
|
||||
};
|
||||
// Draw the Extension start at the actual acquired *world* point,
|
||||
// projected through the same camera/rect as the entity and guides.
|
||||
// The snap engine's screen-space base is only pane-local and can
|
||||
// differ from the final canvas coordinates used by the overlay.
|
||||
// Keep that older value as a fallback for snaps without origin.
|
||||
let snap_ext_base = tab
|
||||
.snap_result
|
||||
.filter(|hit| hit.snap_type == crate::snap::SnapType::Extension)
|
||||
.and_then(|hit| hit.extension_origin)
|
||||
.and_then(|origin| {
|
||||
let (rot, eye, rect) = otrack_proj?;
|
||||
let pixel = ost_project(origin, rot, eye, rect);
|
||||
(pixel.x.is_finite() && pixel.y.is_finite()).then_some(pixel)
|
||||
})
|
||||
.or(snap_ext_base);
|
||||
// OSNAP Extension is also active with F11 / OTRACK turned off.
|
||||
// Give its marker the actual acquired geometric direction instead
|
||||
// of reconstructing a new ray from the moving snap cursor.
|
||||
let snap_ext_dir: Option<iced::Point> = tab
|
||||
.snap_result
|
||||
.filter(|hit| hit.snap_type == crate::snap::SnapType::Extension)
|
||||
.and_then(|hit| hit.extension_origin.zip(hit.extension_dir))
|
||||
.and_then(|(origin, dir)| {
|
||||
let (rot, eye, rect) = otrack_proj?;
|
||||
let (base, tip) = ost_project_ray(origin, dir, rot, eye, rect)?;
|
||||
Some(iced::Point::new(tip.x - base.x, tip.y - base.y))
|
||||
});
|
||||
let ost_points: Vec<crate::ui::overlay::OstTrackPoint> =
|
||||
if let Some((view_rot, eye, ob)) = otrack_proj {
|
||||
self.snapper
|
||||
@@ -935,17 +989,16 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
// both are drawn — one guide alone hides what the point is (#1313).
|
||||
let otrack_lines: Vec<(iced::Point, iced::Point)> = match otrack_proj {
|
||||
Some((view_rot, eye, ob)) => {
|
||||
let a = ost_project(tab.last_cursor_world, view_rot, eye, ob);
|
||||
self.otrack_active
|
||||
.into_iter()
|
||||
.chain(self.otrack_cross)
|
||||
.filter_map(|(base, _dir)| {
|
||||
let b = ost_project(base, view_rot, eye, ob);
|
||||
(b.x.is_finite()
|
||||
&& b.y.is_finite()
|
||||
&& a.x.is_finite()
|
||||
&& a.y.is_finite())
|
||||
.then_some((b, a))
|
||||
.filter_map(|(base, dir)| {
|
||||
// Draw the acquired geometrical ray, not the line
|
||||
// from its origin to the moving cursor. The latter
|
||||
// rotates slightly as the pointer moves away from
|
||||
// the exact ray, making the Extension guide appear
|
||||
// to slide across its acquired endpoint.
|
||||
ost_project_ray(base, dir, view_rot, eye, ob)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1227,6 +1280,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
snap_info,
|
||||
snap_ext_base,
|
||||
snap_ext_base2,
|
||||
snap_ext_dir,
|
||||
grips,
|
||||
control_polygon,
|
||||
grip_clip,
|
||||
|
||||
Reference in new issue
Block a user