forked from 1b-agent/opencadstudio-upstream
feat(grips): compatible grip shortcut menu
A right-click while a grip is hot opens the grip menu: Enter places the grip, Stretch / Move / Rotate / Scale / Mirror switch how the gripped objects are edited (the latter four hand the selection to the matching command with the grip as its base point), Base Point re-bases the gesture at the next click, Copy leaves the originals in place and adds the moved shapes as new entities while keeping the grip hot, Undo snaps the shapes back to where the gesture started, Exit drops the edit. The grip commit path moves out of the left-release handler into `commit_active_grip_edit` so the menu, Enter and the placement click share it; Enter with a hot grip now places it instead of repeating the last command. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d753874fe4
commit
a5fd040745
4 files changed
+413
-96
No files matched your search
+168
-91
@@ -3436,102 +3436,21 @@ impl OpenCADStudio {
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
sel.clear_left_selection_gesture();
|
||||
}
|
||||
// Grip-menu Base Point: this click re-bases the gesture at the
|
||||
// cursor instead of placing anything.
|
||||
if self.tabs[i].grip_base_pending {
|
||||
self.tabs[i].grip_base_pending = false;
|
||||
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
||||
active.origin_world = active.last_world;
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
let moved = grip.last_world != grip.origin_world;
|
||||
if is_click && !moved {
|
||||
// Engaging click — stay hot, wait for the placement click.
|
||||
return Task::none();
|
||||
}
|
||||
if matches!(
|
||||
grip.mode,
|
||||
GripEditMode::Lengthen
|
||||
| GripEditMode::Radius
|
||||
| GripEditMode::ArcLength
|
||||
| GripEditMode::RectangleWidth
|
||||
| GripEditMode::RectangleHeight
|
||||
| GripEditMode::MoveParallel
|
||||
) {
|
||||
self.grip_pending = None;
|
||||
self.command_line.input.clear();
|
||||
}
|
||||
let added_vertex_focus = grip.targets.iter().find_map(|target| {
|
||||
let original = self
|
||||
.grip_originals
|
||||
.iter()
|
||||
.find(|(handle, _)| *handle == target.handle)
|
||||
.map(|(_, entity)| entity)?;
|
||||
let current = self.tabs[i].scene.document.get_entity(target.handle)?;
|
||||
is_added_polyline_vertex(original, current, target.grip_id)
|
||||
.then_some(target.grip_id)
|
||||
});
|
||||
// Keep originals available until every history shape has a valid
|
||||
// final display. A rejected rebuild cancels the entire gesture.
|
||||
let history_handles: Vec<_> = self
|
||||
.grip_preview_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.solid_history_operation(*handle)
|
||||
.is_some()
|
||||
})
|
||||
.collect();
|
||||
for handle in history_handles {
|
||||
if !self.tabs[i].scene.finalize_solid_history(handle) {
|
||||
self.cancel_active_grip_edit();
|
||||
self.command_line.push_error(crate::t!("The edited shape could not be displayed; the original geometry was restored.").as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_grip = None;
|
||||
// Commit the grip drag as one undoable group, then put every
|
||||
// edited entity back into the resident tessellation.
|
||||
let handles = std::mem::take(&mut self.grip_preview_handles);
|
||||
let originals = std::mem::take(&mut self.grip_originals);
|
||||
let history_originals = std::mem::take(&mut self.grip_history_originals);
|
||||
let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty);
|
||||
if !handles.is_empty() {
|
||||
if !originals.is_empty() {
|
||||
self.push_entity_group_history(
|
||||
i,
|
||||
"GRIP",
|
||||
originals
|
||||
.into_iter()
|
||||
.map(|(handle, entity)| (handle, std::sync::Arc::new(entity)))
|
||||
.collect(),
|
||||
history_originals
|
||||
.into_iter()
|
||||
.flat_map(|(_, objects)| objects)
|
||||
.collect(),
|
||||
dirty_before,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.grip_reference_wires.clear();
|
||||
self.grip_text_verts = Vec::new();
|
||||
self.grip_text_slide = false;
|
||||
for &handle in &handles {
|
||||
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||
}
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
let changes: Vec<_> = handles
|
||||
.into_iter()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities_after_parametric_solve(&changes);
|
||||
}
|
||||
// Placement confirmed — keep the just-added leader.
|
||||
self.grip_add_provisional = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
if let Some(vertex_id) = added_vertex_focus {
|
||||
self.tabs[i].properties.prop_vertex = vertex_id;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = true;
|
||||
crate::scene::view::dispatch::set_prop_current_vertex(vertex_id);
|
||||
}
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
return self.commit_active_grip_edit();
|
||||
}
|
||||
|
||||
// Map the release point into the active Model tile so the
|
||||
@@ -6204,6 +6123,164 @@ properties={:.1}ms picked={}",
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Commit the active grip edit at its current (already applied) position:
|
||||
/// one undoable group per gesture, every edited entity back into the
|
||||
/// resident tessellation. With the grip-menu Copy toggle on, the
|
||||
/// originals are put back and the moved shapes are added as new
|
||||
/// entities, and the grip stays hot for the next copy.
|
||||
pub(in crate::app) fn commit_active_grip_edit(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let Some(grip) = self.tabs[i].active_grip.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
if grip.last_world == grip.origin_world {
|
||||
// Nothing placed yet — the grip simply stays hot.
|
||||
return Task::none();
|
||||
}
|
||||
if self.tabs[i].grip_copy {
|
||||
return self.commit_active_grip_edit_as_copy(grip);
|
||||
}
|
||||
self.tabs[i].grip_base_pending = false;
|
||||
if matches!(
|
||||
grip.mode,
|
||||
GripEditMode::Lengthen
|
||||
| GripEditMode::Radius
|
||||
| GripEditMode::ArcLength
|
||||
| GripEditMode::RectangleWidth
|
||||
| GripEditMode::RectangleHeight
|
||||
| GripEditMode::MoveParallel
|
||||
) {
|
||||
self.grip_pending = None;
|
||||
self.command_line.input.clear();
|
||||
}
|
||||
let added_vertex_focus = grip.targets.iter().find_map(|target| {
|
||||
let original = self
|
||||
.grip_originals
|
||||
.iter()
|
||||
.find(|(handle, _)| *handle == target.handle)
|
||||
.map(|(_, entity)| entity)?;
|
||||
let current = self.tabs[i].scene.document.get_entity(target.handle)?;
|
||||
is_added_polyline_vertex(original, current, target.grip_id)
|
||||
.then_some(target.grip_id)
|
||||
});
|
||||
// Keep originals available until every history shape has a valid
|
||||
// final display. A rejected rebuild cancels the entire gesture.
|
||||
let history_handles: Vec<_> = self
|
||||
.grip_preview_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.solid_history_operation(*handle)
|
||||
.is_some()
|
||||
})
|
||||
.collect();
|
||||
for handle in history_handles {
|
||||
if !self.tabs[i].scene.finalize_solid_history(handle) {
|
||||
self.cancel_active_grip_edit();
|
||||
self.command_line.push_error(crate::t!("The edited shape could not be displayed; the original geometry was restored.").as_ref());
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_grip = None;
|
||||
// Commit the grip drag as one undoable group, then put every
|
||||
// edited entity back into the resident tessellation.
|
||||
let handles = std::mem::take(&mut self.grip_preview_handles);
|
||||
let originals = std::mem::take(&mut self.grip_originals);
|
||||
let history_originals = std::mem::take(&mut self.grip_history_originals);
|
||||
let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty);
|
||||
if !handles.is_empty() {
|
||||
if !originals.is_empty() {
|
||||
self.push_entity_group_history(
|
||||
i,
|
||||
"GRIP",
|
||||
originals
|
||||
.into_iter()
|
||||
.map(|(handle, entity)| (handle, std::sync::Arc::new(entity)))
|
||||
.collect(),
|
||||
history_originals
|
||||
.into_iter()
|
||||
.flat_map(|(_, objects)| objects)
|
||||
.collect(),
|
||||
dirty_before,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.grip_reference_wires.clear();
|
||||
self.grip_text_verts = Vec::new();
|
||||
self.grip_text_slide = false;
|
||||
for &handle in &handles {
|
||||
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||
}
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
let changes: Vec<_> = handles
|
||||
.into_iter()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities_after_parametric_solve(&changes);
|
||||
}
|
||||
// Placement confirmed — keep the just-added leader.
|
||||
self.grip_add_provisional = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
if let Some(vertex_id) = added_vertex_focus {
|
||||
self.tabs[i].properties.prop_vertex = vertex_id;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = true;
|
||||
crate::scene::view::dispatch::set_prop_current_vertex(vertex_id);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Grip-menu Copy: leave the originals where they were and add the
|
||||
/// edited shapes as new entities, then re-arm the same grip at its
|
||||
/// origin so the next placement makes another copy (AutoCAD grip Copy).
|
||||
fn commit_active_grip_edit_as_copy(&mut self, grip: GripEdit) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let handles = std::mem::take(&mut self.grip_preview_handles);
|
||||
let originals = std::mem::take(&mut self.grip_originals);
|
||||
let _ = std::mem::take(&mut self.grip_history_originals);
|
||||
self.grip_dirty_before = None;
|
||||
self.grip_add_provisional = None;
|
||||
self.grip_reference_wires.clear();
|
||||
self.grip_text_verts = Vec::new();
|
||||
self.grip_text_slide = false;
|
||||
for &handle in &handles {
|
||||
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||
}
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
if !originals.is_empty() {
|
||||
self.push_undo_snapshot(i, "GRIP");
|
||||
let mut restored = Vec::with_capacity(originals.len());
|
||||
for (handle, original) in &originals {
|
||||
let Some(moved) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
|
||||
continue;
|
||||
};
|
||||
if self.tabs[i].scene.update_entity(original.clone()) {
|
||||
restored.push((*handle, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
self.tabs[i].scene.add_entity_clone(moved);
|
||||
}
|
||||
self.tabs[i].scene.bump_entities_after_parametric_solve(&restored);
|
||||
self.finish_pending_history(i);
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.tabs[i].snap_result = None;
|
||||
// Re-arm: the grip sits at its origin again over the restored shape.
|
||||
let mut again = grip;
|
||||
let delta = again.last_world - again.origin_world;
|
||||
again.last_world = again.origin_world;
|
||||
for target in &mut again.targets {
|
||||
target.last_world -= delta;
|
||||
}
|
||||
self.tabs[i].active_grip = Some(again);
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user