Merge pull request #1792 from gianlucafiore/fix/otrack-live-distance-click-precision

Improve precise picking, OSNAP/OTRACK feedback and selection cursor
This commit is contained in:
gianlucafiore authored and GitHub committed 2026-10-14 11:33:07 -03:00
commit 8e7612c0c9
7 files changed
+138 -9

No files matched your search

+10
View File
@@ -32,6 +32,7 @@ impl DraftingSettingsState {
ortho_on: app.ortho_mode,
polar_increment_deg: app.polar_increment_deg,
osnap_on: app.snapper.snap_enabled,
osnap_aperture_input: format!("{}", app.snapper.osnap_radius_px),
otrack_on: app.snapper.otrack_enabled,
snap_modes: app.snapper.enabled.clone(),
osnap3d_on: app.snapper.snap3d_enabled,
@@ -107,6 +108,13 @@ impl OpenCADStudio {
self.polar_mode = state.polar_on;
self.ortho_mode = state.ortho_on;
self.polar_increment_deg = state.polar_increment_deg;
let aperture = state.osnap_aperture_input.trim().parse::<f32>().ok()
.filter(|v| v.is_finite() && (4.0..=40.0).contains(v));
let Some(aperture) = aperture else {
self.command_line.push_error("OSNAP capture radius must be between 4 and 40 pixels.");
return false;
};
self.snapper.osnap_radius_px = aperture;
self.snapper.snap_enabled = state.osnap_on;
self.snapper.otrack_enabled = state.otrack_on;
self.snapper.enabled = state.snap_modes.clone();
@@ -145,6 +153,7 @@ mod tests {
ortho_on: false,
polar_increment_deg: 15.0,
osnap_on: true,
osnap_aperture_input: "15".to_string(),
otrack_on: false,
snap_modes: rustc_hash::FxHashSet::default(),
osnap3d_on: false,
@@ -186,6 +195,7 @@ mod tests {
ortho_on: false,
polar_increment_deg: 15.0,
osnap_on: true,
osnap_aperture_input: "15".to_string(),
otrack_on: false,
snap_modes: rustc_hash::FxHashSet::default(),
osnap3d_on: false,
+3
View File
@@ -3189,6 +3189,8 @@ pub enum Message {
/// never fall through and rotate a different camera.
ViewportClick(Option<codec::Handle>),
ViewportMove(Point),
/// Position recorded by the pointer event itself, not the last hover.
ViewportLeftPressAt(Point),
ViewportLeftPress,
ViewportLeftRelease,
ViewportRightPress,
@@ -3490,6 +3492,7 @@ pub enum Message {
DraftingSettingsTogglePolar,
DraftingSettingsToggleOrtho,
DraftingSettingsToggleOsnap,
DraftingSettingsOsnapApertureChanged(String),
DraftingSettingsToggleOtrack,
DraftingSettingsToggleSnapMode(crate::snap::SnapType),
DraftingSettingsToggleSnapMode3d(crate::snap::SnapType),
+19 -1
View File
@@ -50,7 +50,7 @@ fn is_modal_blocked_key_msg(msg: &Message) -> bool {
fn perf_message_label(msg: &Message) -> &'static str {
match msg {
Message::ViewportLeftPress | Message::PanePress(_) => "pointer-down",
Message::ViewportLeftPressAt(_) | Message::ViewportLeftPress | Message::PanePress(_) => "pointer-down",
Message::ViewportLeftRelease | Message::PaneRelease(_) => "pointer-up",
Message::ViewportMove(_) | Message::PaneMove(_, _) => "pointer-move",
Message::CommandFinalize => "command-finalize",
@@ -4404,6 +4404,7 @@ impl OpenCADStudio {
// A multi-finger gesture owns the pointer while it runs.
Message::ViewportMove(_)
| Message::ViewportLeftPressAt(_)
| Message::ViewportLeftPress
| Message::ViewportLeftRelease
| Message::PaneMove(..)
@@ -4488,6 +4489,17 @@ impl OpenCADStudio {
self.update(Message::ViewportScroll(d))
}
Message::ViewportLeftPressAt(position) => {
// Use the pointer location captured by the actual button
// event. The last hover can lag behind fast cursor motion,
// particularly while expensive OSNAP/OTRACK previews run.
// Refresh the snap and world point before accepting the pick.
let moved = self.on_viewport_move(position);
self.graphic_attributes.open_menu = None;
let sweep = self.sync_active_field_if_any();
Task::batch(vec![moved, sweep, self.on_viewport_left_press()])
}
Message::ViewportLeftPress => {
self.graphic_attributes.open_menu = None;
let sweep = self.sync_active_field_if_any();
@@ -6339,6 +6351,12 @@ impl OpenCADStudio {
}
Task::none()
}
Message::DraftingSettingsOsnapApertureChanged(value) => {
if let Some(state) = &mut self.drafting_settings_state {
state.osnap_aperture_input = value;
}
Task::none()
}
Message::DraftingSettingsToggleOsnap => {
if let Some(state) = &mut self.drafting_settings_state {
state.osnap_on = !state.osnap_on;
+14 -2
View File
@@ -2663,7 +2663,15 @@ impl OpenCADStudio {
.map(|snap| snap.world)
.unwrap_or(cursor_world);
axis_lock_apply(point, base, dir)
} else if let Some(h) = otrack_hit {
} else if let Some(h) = otrack_hit.filter(|_| {
// A real object snap must win over tracking alignment.
// Otherwise the endpoint glyph can appear at the
// corner while the preview/accepted point follows the
// tracking line instead.
!self.tabs[i].snap_result.is_some_and(|s| {
s.snap_type != crate::snap::SnapType::Grid
})
}) {
h.aligned
} else {
// Snap runs in model space (viewport camera or the
@@ -4498,7 +4506,11 @@ impl OpenCADStudio {
} else {
None
};
if let Some(h) = otrack {
if let Some(h) = otrack.filter(|_| {
// Match the hover path: never override a genuine OSNAP
// endpoint/intersection with a tracking projection.
!snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid)
}) {
pt = h.aligned;
if clamp_world_xy {
pt.z = 0.0;
+81 -5
View File
@@ -15,6 +15,46 @@ use iced::widget::{
use iced::window;
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
use iced_aw::ContextMenu;
/// Invisible event surface for accurate point picking. MouseArea emits a
/// positionless press message; capturing the cursor in the actual button
/// event avoids using the previous hover location when clicking quickly.
#[derive(Debug, Clone, Copy)]
struct ViewportPickSurface;
impl iced::widget::canvas::Program<Message> for ViewportPickSurface {
type State = ();
fn update(
&self,
_state: &mut Self::State,
event: &iced::Event,
bounds: iced::Rectangle,
cursor: iced::mouse::Cursor,
) -> Option<iced::widget::canvas::Action<Message>> {
if let iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)) =
event
{
return cursor.position_in(bounds).map(|position| {
iced::widget::canvas::Action::publish(Message::ViewportLeftPressAt(position))
.and_capture()
});
}
None
}
fn draw(
&self,
_state: &Self::State,
_renderer: &iced::Renderer,
_theme: &iced::Theme,
_bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry> {
Vec::new()
}
}
use std::sync::Arc;
mod controls;
@@ -876,10 +916,16 @@ bg={bg_ms:.1}ms n={view_count}"
.hover_highlight
.map(|h| tab.scene.is_layer_locked(h))
.unwrap_or(false);
let point_cursor = tab
.active_cmd
.as_ref()
.is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering());
// Keep the hollow pickbox at rest. Use the standard LINE/RECTANG
// dot crosshair only after the first window/crossing corner.
// No OSNAP or selection behavior changes.
let selecting_window = tab.active_cmd.is_none()
&& tab.scene.selection.borrow().gesture.box_anchor.is_some();
let point_cursor = selecting_window
|| tab
.active_cmd
.as_ref()
.is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering());
let constraint_cursor_badge = if is_paper {
None
} else {
@@ -986,9 +1032,15 @@ bg={bg_ms:.1}ms n={view_count}"
mark("viewport_mouse");
let viewport_mouse = mouse_area(container(
iced::widget::Space::new().width(Fill).height(Fill),
iced::widget::canvas(ViewportPickSurface)
.width(Fill)
.height(Fill),
))
.on_move(Message::ViewportMove)
// Left mouse presses are captured by the canvas with their exact
// event-time position. MouseArea::on_press has no coordinate, so
// queued cursor moves could otherwise make quick clicks use a stale
// snap position. Touch presses still use this fallback.
.on_press(Message::ViewportLeftPress)
.on_release(Message::ViewportLeftRelease)
.on_right_press(Message::ViewportRightPress)
@@ -1036,6 +1088,20 @@ bg={bg_ms:.1}ms n={view_count}"
{
let w = tab.last_cursor_world;
let base = tab.dyn_anchor.or(self.last_point);
// A single distance field on an acquired tracking ray is measured
// from the acquired OSNAP/OTRACK point, not from the command's
// previous point (or from WCS zero before the first pick).
// Keep typed fields and ordinary polar distance inputs unchanged.
let tracking_distance = if tab.dyn_fields.len() == 1
&& tab.dyn_fields[0].component == DynComponent::Distance
{
self.active_distance_ray(self.active_tab).and_then(|(origin, direction)| {
let axis = direction.try_normalize()?;
Some((w - origin).dot(axis).abs())
})
} else {
None
};
let label_screen = tab
.active_cmd
.as_ref()
@@ -1185,6 +1251,16 @@ bg={bg_ms:.1}ms n={view_count}"
format!("{lv:.4}")
}
}
// The displayed live distance must use the same
// acquired tracking origin as the guide and typed
// distance placement, not the last picked point.
(None, _) if f.component == DynComponent::Distance
&& tracking_distance.is_some() =>
{
crate::entities::common::format_length(
tracking_distance.unwrap() * f.role.value_scale() as f64,
)
}
_ => dyn_component_value(
f,
w,
+1 -1
View File
@@ -14,7 +14,7 @@ use kernel::geom2d::Curve;
use crate::command::{DimensionAssociationSource, TangentObject};
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
use crate::scene::pick::interaction_index::WireSource;
const DEFAULT_OSNAP_RADIUS_PX: f32 = 15.0;
const DEFAULT_OSNAP_RADIUS_PX: f32 = 25.0;
// ── Snap type ─────────────────────────────────────────────────────────────
+10
View File
@@ -44,6 +44,7 @@ pub struct DraftingSettingsState {
pub polar_increment_deg: f32,
// Object Snap
pub osnap_on: bool,
pub osnap_aperture_input: String,
pub otrack_on: bool,
pub snap_modes: rustc_hash::FxHashSet<SnapType>,
// 3D Object Snap
@@ -78,6 +79,7 @@ impl DraftingSettingsState {
|| self.ortho_on != saved.ortho_on
|| (self.polar_increment_deg - saved.polar_increment_deg).abs() > 0.001
|| self.osnap_on != saved.osnap_on
|| self.osnap_aperture_input != saved.osnap_aperture_input
|| self.otrack_on != saved.otrack_on
|| self.snap_modes != saved.snap_modes
|| self.osnap3d_on != saved.osnap3d_on
@@ -508,6 +510,14 @@ pub fn view_window<'a>(
column![
row![osnap_on_toggle, Space::new().width(24), otrack_on_toggle].align_y(iced::Center),
row![
text("OSNAP capture radius (pixels)").size(11),
text_input("25", &state.osnap_aperture_input)
.on_input(Message::DraftingSettingsOsnapApertureChanged)
.width(70),
]
.spacing(10)
.align_y(iced::Center),
action_buttons,
Space::new().height(4),
snap_modes_group,