forked from 1b-agent/opencadstudio-upstream
Merge pull request #1225 from gianlucafiore/fix/issue-1135-workflow-improvements
Improve command workflow for undo, move, stretch and fillet
This commit is contained in:
6 files changed
+197
-21
No files matched your search
@@ -4169,6 +4169,81 @@ impl OpenCADStudio {
|
|||||||
self.command_line.push_output(
|
self.command_line.push_output(
|
||||||
crate::t!("STRETCH: nothing crosses the window.").as_ref(),
|
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;
|
use crate::command::CadCommand;
|
||||||
|
|||||||
+24
-2
@@ -287,8 +287,30 @@ impl OpenCADStudio {
|
|||||||
"DYNINPUT" => Message::ToggleDynInput,
|
"DYNINPUT" => Message::ToggleDynInput,
|
||||||
"SELECTALL" => Message::SelectAllShortcut,
|
"SELECTALL" => Message::SelectAllShortcut,
|
||||||
"PASTECLIP" => Message::PasteShortcut,
|
"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()),
|
other => Message::Command(other.to_string()),
|
||||||
};
|
};
|
||||||
self.update(message)
|
|
||||||
|
self.update(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
-2
@@ -3098,15 +3098,32 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||||||
let previous = self.plot_dialog.clone();
|
let previous = self.plot_dialog.clone();
|
||||||
let previous_style = self.active_plot_style.clone();
|
let previous_style = self.active_plot_style.clone();
|
||||||
let previous_window = self.plot_window;
|
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();
|
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_options_prev = Some(previous);
|
||||||
self.print_all_plot_style_prev = Some(previous_style);
|
self.print_all_plot_style_prev = Some(previous_style);
|
||||||
self.print_all_plot_window_prev = Some(previous_window);
|
self.print_all_plot_window_prev = Some(previous_window);
|
||||||
self.print_all_plot_setup_prev = Some(previous_setup);
|
|
||||||
self.print_all_options = true;
|
self.print_all_options = true;
|
||||||
self.plot_dialog.paper_space = true;
|
self.plot_dialog.paper_space = true;
|
||||||
self.plot_dialog.area = "Layout".into();
|
self.plot_dialog.area = "Layout".into();
|
||||||
|
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -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_base = tab.snap_result.and_then(|s| s.extension_base);
|
||||||
let snap_ext_base2 = tab.snap_result.and_then(|s| s.extension_base2);
|
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<crate::ui::overlay::GripMarker> =
|
let grips: Vec<crate::ui::overlay::GripMarker> =
|
||||||
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;
|
let (vw, vh) = sel_ref.vp_size;
|
||||||
// Overlays project through the active tile's camera, so
|
// Overlays project through the active tile's camera, so
|
||||||
// they must use the active tile's screen rectangle (with
|
// they must use the active tile's screen rectangle (with
|
||||||
|
|||||||
@@ -1296,7 +1296,14 @@ fn compute_chamfer(
|
|||||||
enum FilletStep {
|
enum FilletStep {
|
||||||
First,
|
First,
|
||||||
Polyline,
|
Polyline,
|
||||||
|
|
||||||
|
// Radius may be typed directly or measured by picking two points.
|
||||||
WaitingForRadius,
|
WaitingForRadius,
|
||||||
|
|
||||||
|
RadiusSecondPoint {
|
||||||
|
first: DVec3,
|
||||||
|
},
|
||||||
|
|
||||||
Second {
|
Second {
|
||||||
h1: Handle,
|
h1: Handle,
|
||||||
e1: FilletEntity,
|
e1: FilletEntity,
|
||||||
@@ -1370,6 +1377,21 @@ impl FilletCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for FilletCommand {
|
impl CadCommand for FilletCommand {
|
||||||
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||||
|
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 {
|
fn name(&self) -> &'static str {
|
||||||
"FILLET"
|
"FILLET"
|
||||||
}
|
}
|
||||||
@@ -1387,7 +1409,15 @@ impl CadCommand for FilletCommand {
|
|||||||
)
|
)
|
||||||
.into_owned(),
|
.into_owned(),
|
||||||
FilletStep::WaitingForRadius => {
|
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 { .. } => {
|
FilletStep::Second { .. } => {
|
||||||
crate::tf!(
|
crate::tf!(
|
||||||
@@ -1410,7 +1440,9 @@ impl CadCommand for FilletCommand {
|
|||||||
FilletStep::Second { .. } => {
|
FilletStep::Second { .. } => {
|
||||||
vec![CmdOption::new("Radius", "R")]
|
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
|
None
|
||||||
}
|
}
|
||||||
FilletStep::Polyline => None,
|
FilletStep::Polyline
|
||||||
|
| FilletStep::RadiusSecondPoint { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
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 {
|
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
|
let click = [pt.x as f64, pt.y as f64]; // drawing plane is world XY
|
||||||
|
|
||||||
match &self.step {
|
match &self.step {
|
||||||
FilletStep::WaitingForRadius => return CmdResult::NeedPoint,
|
FilletStep::WaitingForRadius
|
||||||
|
| FilletStep::RadiusSecondPoint { .. } => {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
|
||||||
FilletStep::Polyline => {
|
FilletStep::Polyline => {
|
||||||
let poly = self
|
let poly = self
|
||||||
.entity_index
|
.entity_index
|
||||||
@@ -1586,7 +1627,8 @@ impl CadCommand for FilletCommand {
|
|||||||
let click = [pt.x as f64, pt.y as f64];
|
let click = [pt.x as f64, pt.y as f64];
|
||||||
|
|
||||||
match &self.step {
|
match &self.step {
|
||||||
FilletStep::WaitingForRadius => vec![],
|
FilletStep::WaitingForRadius
|
||||||
|
| FilletStep::RadiusSecondPoint { .. } => vec![],
|
||||||
FilletStep::Polyline => {
|
FilletStep::Polyline => {
|
||||||
let preview = self
|
let preview = self
|
||||||
.entity_index
|
.entity_index
|
||||||
@@ -1677,8 +1719,29 @@ impl CadCommand for FilletCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
CmdResult::NeedPoint
|
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]) {
|
fn on_entity_replaced(&mut self, _old: Handle, new_handles: &[Handle]) {
|
||||||
let mut handles = new_handles.iter().copied();
|
let mut handles = new_handles.iter().copied();
|
||||||
|
|||||||
@@ -109,12 +109,5 @@ impl CadCommand for MoveCommand {
|
|||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview_hidden_handles(&self) -> &[Handle] {
|
|
||||||
match self.step {
|
|
||||||
Step::Base => &[],
|
|
||||||
Step::Target(_) => &self.handles,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user