From d68f297d8dbe0b59ea370d20e68d33177f08676a Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Sat, 12 Sep 2026 21:41:34 -0300 Subject: [PATCH 1/5] fix(print-all): preserve applied plot options --- src/app/update/file.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index dffa0b90d..1bad3d3ce 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -3098,15 +3098,32 @@ pub(super) fn on_open_file(&mut self) -> Task { let previous = self.plot_dialog.clone(); let previous_style = self.active_plot_style.clone(); let previous_window = self.plot_window; - let previous_setup = self.plot_setup_template.clone(); + + // Once Print All has an explicit override, reopening Options must keep + // those settings instead of replacing them with the active layout's + // page setup. + let keep_print_all_override = self.print_all_settings_override; + + // Refresh runtime data such as printers, paper sizes, plot styles and + // named page setups. let task = self.on_plot_dialog_open(); + + if keep_print_all_override { + // Restore only the user-editable plot settings. Runtime lists populated + // above remain intact. + self.plot_dialog.copy_settings_from(&previous); + self.active_plot_style = previous_style.clone(); + self.plot_window = previous_window; + } + self.print_all_options_prev = Some(previous); self.print_all_plot_style_prev = Some(previous_style); self.print_all_plot_window_prev = Some(previous_window); - self.print_all_plot_setup_prev = Some(previous_setup); + self.print_all_options = true; self.plot_dialog.paper_space = true; self.plot_dialog.area = "Layout".into(); + task } From 17ecd9ff4d0a9d5f67cc75a96e47e625cf11053d Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Sat, 12 Sep 2026 21:41:34 -0300 Subject: [PATCH 2/5] fix(commands): handle Ctrl+Z inside active commands --- src/app/shortcuts.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/app/shortcuts.rs b/src/app/shortcuts.rs index f3116f38c..3682bbbe3 100644 --- a/src/app/shortcuts.rs +++ b/src/app/shortcuts.rs @@ -287,8 +287,30 @@ impl OpenCADStudio { "DYNINPUT" => Message::ToggleDynInput, "SELECTALL" => Message::SelectAllShortcut, "PASTECLIP" => Message::PasteShortcut, + + "UNDO" => { + let i = self.active_tab; + + // Multi-step drawing commands may consume Ctrl+Z themselves. + // For example PLINE/SPLINE should remove only the last entered + // point instead of cancelling the whole in-progress command. + let command_result = self.tabs[i] + .active_cmd + .as_mut() + .and_then(|cmd| cmd.on_undo_step()); + + if let Some(result) = command_result { + return self.apply_cmd_result(result); + } + + Message::Undo + } + + "REDO" => Message::Redo, + other => Message::Command(other.to_string()), - }; - self.update(message) + }; + + self.update(message) } } From 7ab311d86c6f9c54aa1a74ba5f9a5271d0f78511 Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Sat, 12 Sep 2026 21:41:34 -0300 Subject: [PATCH 3/5] fix(move): keep source geometry visible during preview --- src/modules/draw/modify/translate.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/modules/draw/modify/translate.rs b/src/modules/draw/modify/translate.rs index aad5250ea..061d2dc82 100644 --- a/src/modules/draw/modify/translate.rs +++ b/src/modules/draw/modify/translate.rs @@ -109,12 +109,5 @@ impl CadCommand for MoveCommand { false, )); out - } - - fn preview_hidden_handles(&self) -> &[Handle] { - match self.step { - Step::Base => &[], - Step::Target(_) => &self.handles, - } - } + } } From 8a45a84c436e3189ec6e512dfd2dbe88f94c911d Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Sat, 12 Sep 2026 21:41:34 -0300 Subject: [PATCH 4/5] fix(stretch): highlight affected vertices during selection --- src/app/command_driver.rs | 75 +++++++++++++++++++++++++++++++++++++++ src/app/view/mod.rs | 8 ++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 15c3ea5b7..d56df0dad 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4169,6 +4169,81 @@ impl OpenCADStudio { self.command_line.push_output( crate::t!("STRETCH: nothing crosses the window.").as_ref(), ); + } else { + // Visual feedback must only highlight entities that actually have a + // stretchable point inside one of the crossing windows. `handles` can + // contain the whole candidate/preselection set, while `windows` decides + // which vertices will really move. + let point_inside = |point: glam::DVec3| { + const EPS: f64 = 1.0e-9; + + windows.iter().any(|(win_min, win_max)| { + point.x >= win_min.x - EPS + && point.x <= win_max.x + EPS + && point.y >= win_min.y - EPS + && point.y <= win_max.y + EPS + && point.z >= win_min.z - EPS + && point.z <= win_max.z + EPS + }) + }; + + let mut visual_handles = Vec::new(); + + for &handle in &handles { + let wires = self.tabs[i].scene.wire_models_for(&[handle]); + + let affected = wires.iter().any(|wire| { + // LINE / LWPOLYLINE / other vertex-based entities. + let vertex_hit = wire + .key_vertices + .iter() + .any(|point| point_inside(glam::DVec3::from_array(*point))); + + if vertex_hit { + return true; + } + + // Whole-object STRETCH cases such as circles/arcs/inserts/points: + // use only their meaningful anchor snap, not quadrants or + // tessellated display points. + wire.snap_pts.iter().any(|(point, hint)| { + matches!( + hint, + crate::scene::model::wire_model::SnapHint::Center + | crate::scene::model::wire_model::SnapHint::Insertion + | crate::scene::model::wire_model::SnapHint::Node + ) && point_inside(*point) + }) + }); + + if affected { + visual_handles.push(handle); + } + } + + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.select_entities(&visual_handles); + self.refresh_selected_grips(); + // STRETCH temporary grips: keep only the actual vertices/endpoints + // that lie inside one of the accumulated crossing windows. + // + // Mid-segment grips are deliberately excluded: STRETCH moves vertices, + // not the midpoint affordances used by normal grip editing. + let mut stretch_grips = Vec::new(); + let mut stretch_grip_handles = Vec::new(); + + for (handle, grip) in std::mem::take(&mut self.tabs[i].selected_grip_handles) + .into_iter() + .zip(std::mem::take(&mut self.tabs[i].selected_grips)) + { + if !grip.is_midpoint && point_inside(grip.world) { + stretch_grip_handles.push(handle); + stretch_grips.push(grip); + } + } + + self.tabs[i].selected_grip_handles = stretch_grip_handles; + self.tabs[i].selected_grips = stretch_grips; } use crate::command::CadCommand; diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 4d9e4c785..7e937e0e0 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -489,8 +489,14 @@ bg={bg_ms:.1}ms n={view_count}" let snap_ext_base = tab.snap_result.and_then(|s| s.extension_base); let snap_ext_base2 = tab.snap_result.and_then(|s| s.extension_base2); + let show_grips = tab.active_cmd.is_none() + || tab + .active_cmd + .as_ref() + .is_some_and(|cmd| cmd.name() == "STRETCH"); + let grips: Vec = - if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() { + if show_grips && !tab.selected_grips.is_empty() { let (vw, vh) = sel_ref.vp_size; // Overlays project through the active tile's camera, so // they must use the active tile's screen rectangle (with From 5d77c2dd00d7757a95fd948e14ce6a3075b87145 Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Sat, 12 Sep 2026 21:41:34 -0300 Subject: [PATCH 5/5] feat(fillet): allow radius from two picked points --- src/modules/draw/modify/fillet.rs | 79 +++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index e23f3635a..ec19c9728 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1296,7 +1296,14 @@ fn compute_chamfer( enum FilletStep { First, Polyline, + + // Radius may be typed directly or measured by picking two points. WaitingForRadius, + + RadiusSecondPoint { + first: DVec3, + }, + Second { h1: Handle, e1: FilletEntity, @@ -1370,6 +1377,21 @@ impl FilletCommand { } impl CadCommand for FilletCommand { + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + let FilletStep::RadiusSecondPoint { first } = self.step else { + return None; + }; + + Some(WireModel::solid_f64( + "fillet_radius_preview".into(), + vec![ + [first.x, first.y, first.z], + [pt.x, pt.y, pt.z], + ], + WireModel::CYAN, + false, + )) + } fn name(&self) -> &'static str { "FILLET" } @@ -1387,7 +1409,15 @@ impl CadCommand for FilletCommand { ) .into_owned(), FilletStep::WaitingForRadius => { - crate::tf!("FILLET Enter fillet radius <{:.4}>:", self.radius).into_owned() + crate::tf!( + "FILLET Specify fillet radius or first point <{:.4}>:", + self.radius + ) + .into_owned() + } + + FilletStep::RadiusSecondPoint { .. } => { + crate::t!("FILLET Specify second point for radius:").into_owned() } FilletStep::Second { .. } => { crate::tf!( @@ -1410,7 +1440,9 @@ impl CadCommand for FilletCommand { FilletStep::Second { .. } => { vec![CmdOption::new("Radius", "R")] } - FilletStep::Polyline | FilletStep::WaitingForRadius => vec![], + FilletStep::Polyline + | FilletStep::WaitingForRadius + | FilletStep::RadiusSecondPoint { .. } => vec![], } } @@ -1475,12 +1507,17 @@ impl CadCommand for FilletCommand { } None } - FilletStep::Polyline => None, + FilletStep::Polyline + | FilletStep::RadiusSecondPoint { .. } => None, } } fn needs_entity_pick(&self) -> bool { - !matches!(self.step, FilletStep::WaitingForRadius) + !matches!( + self.step, + FilletStep::WaitingForRadius + | FilletStep::RadiusSecondPoint { .. } + ) } fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { @@ -1490,7 +1527,11 @@ impl CadCommand for FilletCommand { let click = [pt.x as f64, pt.y as f64]; // drawing plane is world XY match &self.step { - FilletStep::WaitingForRadius => return CmdResult::NeedPoint, + FilletStep::WaitingForRadius + | FilletStep::RadiusSecondPoint { .. } => { + return CmdResult::NeedPoint; + } + FilletStep::Polyline => { let poly = self .entity_index @@ -1586,7 +1627,8 @@ impl CadCommand for FilletCommand { let click = [pt.x as f64, pt.y as f64]; match &self.step { - FilletStep::WaitingForRadius => vec![], + FilletStep::WaitingForRadius + | FilletStep::RadiusSecondPoint { .. } => vec![], FilletStep::Polyline => { let preview = self .entity_index @@ -1677,8 +1719,29 @@ impl CadCommand for FilletCommand { } } - fn on_point(&mut self, _pt: DVec3) -> CmdResult { - CmdResult::NeedPoint + fn on_point(&mut self, pt: DVec3) -> CmdResult { + match self.step { + FilletStep::WaitingForRadius => { + // First point of the temporary radius measurement. + self.step = FilletStep::RadiusSecondPoint { first: pt }; + CmdResult::NeedPoint + } + + FilletStep::RadiusSecondPoint { first } => { + let radius = first.distance(pt); + + // Ignore an accidental zero-length measurement. + if radius > 1.0e-9 { + self.radius = radius; + defaults::set_fillet_radius(radius); + self.resume_after_radius(); + } + + CmdResult::NeedPoint + } + + _ => CmdResult::NeedPoint, + } } fn on_entity_replaced(&mut self, _old: Handle, new_handles: &[Handle]) { let mut handles = new_handles.iter().copied();