forked from 1b-agent/opencadstudio-upstream
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:
7 files changed
+138
-9
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user