forked from 1b-agent/opencadstudio-upstream
Pick spline control vertices with the configured screen aperture
This commit is contained in:
1 parent
12c661d118
commit
42439c2e50
4 files changed
+74
-10
No files matched your search
@@ -312,6 +312,33 @@ impl OpenCADStudio {
|
||||
true
|
||||
}
|
||||
|
||||
/// Refresh screen-space feature picking immediately before point dispatch.
|
||||
pub(super) fn refresh_command_point_pick_context(&mut self, i: usize) {
|
||||
if self.tabs[i].active_cmd.as_ref()
|
||||
.is_some_and(|command| command.wants_point_pick_context())
|
||||
{
|
||||
let scene = &self.tabs[i].scene;
|
||||
let size = scene.selection.borrow().vp_size;
|
||||
let edit = scene.viewport_edit_frame(size);
|
||||
let tile = edit.as_ref().map(|(_, rect)| *rect)
|
||||
.unwrap_or_else(|| scene.active_model_tile_bounds(size.0, size.1));
|
||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: tile.width, height: tile.height };
|
||||
let context = if bounds.width > 0.0 && bounds.height > 0.0 {
|
||||
let (view, eye) = if let Some((camera, _)) = edit {
|
||||
(camera.view_proj_rte(bounds), camera.eye())
|
||||
} else {
|
||||
let camera = scene.camera.borrow();
|
||||
(camera.view_proj_rte(bounds), camera.eye())
|
||||
};
|
||||
Some(crate::command::PointPickContext { view, eye, bounds,
|
||||
aperture_px: crate::ui::overlay::pick_box_aperture_px(self.pick_box) })
|
||||
} else { None };
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
command.set_point_pick_context(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive the active command's step machine with one [`StepInput`], then
|
||||
/// apply the result. This is the single entry point every input source
|
||||
/// (command line, headless, dynamic input, plugin API, viewport) funnels
|
||||
@@ -414,6 +441,7 @@ impl OpenCADStudio {
|
||||
command.inject_selection_entities(entities);
|
||||
}
|
||||
}
|
||||
if matches!(&input, StepInput::Point(_)) { self.refresh_command_point_pick_context(i); }
|
||||
let ctrl = self.ctrl_down;
|
||||
let shift = self.shift_down;
|
||||
let result: Option<CmdResult> = {
|
||||
|
||||
@@ -3887,6 +3887,7 @@ impl OpenCADStudio {
|
||||
} else if self.tabs[i].active_cmd.as_ref()
|
||||
.is_some_and(|command| command.entity_pick_accepts_points())
|
||||
{
|
||||
self.refresh_command_point_pick_context(i);
|
||||
self.tabs[i].active_cmd.as_mut().map(|command| command.on_point(pick_wcs))
|
||||
} else if self.tabs[i]
|
||||
.active_cmd
|
||||
@@ -3963,6 +3964,7 @@ impl OpenCADStudio {
|
||||
// (LINE tangent to two circles, which needs both). When
|
||||
// it does, sync last_point to the command's resolved
|
||||
// anchor since it replaced the picked coordinate.
|
||||
self.refresh_command_point_pick_context(i);
|
||||
let handled = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_mut()
|
||||
|
||||
@@ -1939,6 +1939,15 @@ impl InputKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Current screen projection and configured aperture for point-feature picking.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PointPickContext {
|
||||
pub view: glam::Mat4,
|
||||
pub eye: DVec3,
|
||||
pub bounds: iced::Rectangle,
|
||||
pub aperture_px: f32,
|
||||
}
|
||||
|
||||
pub trait CadCommand: Send {
|
||||
/// Keep the layer already carried by entities committed by this command
|
||||
/// instead of replacing it with the current drawing layer.
|
||||
@@ -2006,6 +2015,12 @@ pub trait CadCommand: Send {
|
||||
/// Called when the user left-clicks in the viewport (point pick).
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult;
|
||||
|
||||
/// Opt in only while a point input selects an existing point feature.
|
||||
fn wants_point_pick_context(&self) -> bool { false }
|
||||
|
||||
/// Refreshed for each point input so zoom and viewport changes are reflected.
|
||||
fn set_point_pick_context(&mut self, _context: Option<PointPickContext>) {}
|
||||
|
||||
/// Called when the user presses Enter (finalize / next option).
|
||||
fn on_enter(&mut self) -> CmdResult;
|
||||
|
||||
|
||||
@@ -47,11 +47,31 @@ pub struct SplineditCommand {
|
||||
spline: Option<acadrust::entities::Spline>,
|
||||
pending: Option<acadrust::entities::Spline>,
|
||||
history: Vec<(acadrust::Handle, acadrust::entities::Spline)>,
|
||||
pick_context: Option<crate::command::PointPickContext>,
|
||||
}
|
||||
|
||||
impl SplineditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new() }
|
||||
Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None }
|
||||
}
|
||||
|
||||
fn picked_vertex(&self, point: DVec3) -> Option<usize> {
|
||||
let context = self.pick_context?;
|
||||
let project = |point: DVec3| {
|
||||
let clip = context.view * (point - context.eye).as_vec3().extend(1.0);
|
||||
if !clip.is_finite() || clip.w <= 0.0 { return None; }
|
||||
let screen = crate::scene::pick::hit_test::world_to_screen(
|
||||
point, context.view, context.eye, context.bounds);
|
||||
(screen.x.is_finite() && screen.y.is_finite()
|
||||
&& screen.x >= 0.0 && screen.x <= context.bounds.width
|
||||
&& screen.y >= 0.0 && screen.y <= context.bounds.height).then_some(screen)
|
||||
};
|
||||
let cursor = project(point)?;
|
||||
self.spline.as_ref()?.control_points.iter().enumerate().filter_map(|(index, vertex)| {
|
||||
let screen = project(DVec3::new(vertex.x, vertex.y, vertex.z))?;
|
||||
let distance = (screen.x - cursor.x).hypot(screen.y - cursor.y);
|
||||
(distance.is_finite() && distance <= context.aperture_px).then_some((index, distance))
|
||||
}).min_by(|a, b| a.1.total_cmp(&b.1)).map(|(index, _)| index)
|
||||
}
|
||||
|
||||
fn replace(&mut self, spline: acadrust::entities::Spline) -> CmdResult {
|
||||
@@ -194,25 +214,24 @@ impl CadCommand for SplineditCommand {
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
fn wants_point_pick_context(&self) -> bool {
|
||||
matches!(self.step, Step::Delete | Step::SelectVertex { .. })
|
||||
}
|
||||
fn set_point_pick_context(&mut self, context: Option<crate::command::PointPickContext>) {
|
||||
self.pick_context = context;
|
||||
}
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if !point.is_finite() { return CmdResult::NeedPoint; }
|
||||
match self.step {
|
||||
Step::SelectVertex { weight, refine } => {
|
||||
if let Some(index) = self.spline.as_ref().and_then(|spline| spline.control_points.iter().enumerate()
|
||||
.min_by(|(_, a), (_, b)| {
|
||||
let distance = |p: &Vector3| point.distance_squared(DVec3::new(p.x, p.y, p.z));
|
||||
distance(a).total_cmp(&distance(b))
|
||||
}).map(|(index, _)| index)) {
|
||||
if let Some(index) = self.picked_vertex(point) {
|
||||
self.step = if weight { Step::Weight { index } } else { Step::Move { index, refine } };
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Delete => {
|
||||
let Some(source) = self.spline.as_ref() else { return CmdResult::NeedPoint; };
|
||||
let Some(index) = source.control_points.iter().enumerate().min_by(|(_, a), (_, b)| {
|
||||
let distance = |p: &Vector3| point.distance_squared(DVec3::new(p.x, p.y, p.z));
|
||||
distance(a).total_cmp(&distance(b))
|
||||
}).map(|(index, _)| index) else { return CmdResult::NeedPoint; };
|
||||
let Some(index) = self.picked_vertex(point) else { return CmdResult::NeedPoint; };
|
||||
let controls = source.control_points.iter().map(|point| [point.x, point.y, point.z]).collect();
|
||||
let weights = if source.weights.is_empty() { vec![1.0; source.control_points.len()] }
|
||||
else { source.weights.clone() };
|
||||
|
||||
Reference in new issue
Block a user