feat(grip): value-prompt actions Lengthen/Radius/Arc Length
Lengthen / Radius / Arc Length need a number after the pick. Stash `grip_pending` on the picked action, push prompt to command line, route next `CommandSubmit` parse into `apply_grip_menu_value`. Escape cancels. - `Grippable::grip_menu_value_prompt` returns label when action wants value; `apply_grip_menu_value` runs the edit. Dispatched via `EntityTypeOps`. - Line: Lengthen extends either endpoint along line direction. - Arc: Radius sets radius; Arc Length redrives end angle from `value / radius`; Lengthen extends start / end angle the same way.
This commit is contained in:
parent
86ca1b7033
commit
559cd76369
5 changed files with 243 additions and 5 deletions
|
|
@ -43,6 +43,18 @@ pub struct GripPopup {
|
||||||
pub items: Vec<crate::scene::object::GripMenuItem>,
|
pub items: Vec<crate::scene::object::GripMenuItem>,
|
||||||
pub selected: usize,
|
pub selected: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pending follow-up value for grip-menu actions that need a number
|
||||||
|
/// (Lengthen / Radius / Arc Length / Rotate Text). The next number
|
||||||
|
/// typed in the command line is parsed and routed into
|
||||||
|
/// `apply_grip_menu_value` for `(handle, grip_id, action)`.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GripPendingValue {
|
||||||
|
pub handle: acadrust::Handle,
|
||||||
|
pub grip_id: usize,
|
||||||
|
pub action: crate::scene::object::GripMenuAction,
|
||||||
|
pub label: &'static str,
|
||||||
|
}
|
||||||
use crate::snap::Snapper;
|
use crate::snap::Snapper;
|
||||||
use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar};
|
use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar};
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
|
|
@ -122,6 +134,7 @@ pub(super) struct OpenCADStudio {
|
||||||
/// Open multi-functional grip popup. Persists across mouse moves
|
/// Open multi-functional grip popup. Persists across mouse moves
|
||||||
/// until dismissed (click outside, ESC, cursor leaves the grip).
|
/// until dismissed (click outside, ESC, cursor leaves the grip).
|
||||||
grip_popup: Option<GripPopup>,
|
grip_popup: Option<GripPopup>,
|
||||||
|
grip_pending: Option<GripPendingValue>,
|
||||||
/// Show the UCS icon in the bottom-left corner of model space (UCSICON).
|
/// Show the UCS icon in the bottom-left corner of model space (UCSICON).
|
||||||
show_ucs_icon: bool,
|
show_ucs_icon: bool,
|
||||||
/// Whether the ViewCube 3D gizmo is visible in model space (NAVVCUBE).
|
/// Whether the ViewCube 3D gizmo is visible in model space (NAVVCUBE).
|
||||||
|
|
@ -806,6 +819,7 @@ impl OpenCADStudio {
|
||||||
dyn_user_reshaped: false,
|
dyn_user_reshaped: false,
|
||||||
grip_hover: None,
|
grip_hover: None,
|
||||||
grip_popup: None,
|
grip_popup: None,
|
||||||
|
grip_pending: None,
|
||||||
show_ucs_icon: true,
|
show_ucs_icon: true,
|
||||||
show_viewcube: true,
|
show_viewcube: true,
|
||||||
show_properties: true,
|
show_properties: true,
|
||||||
|
|
|
||||||
|
|
@ -1019,6 +1019,39 @@ impl OpenCADStudio {
|
||||||
// dropdown so the dispatched command's new prompt is
|
// dropdown so the dispatched command's new prompt is
|
||||||
// immediately visible on the overlay.
|
// immediately visible on the overlay.
|
||||||
self.command_line.close_history();
|
self.command_line.close_history();
|
||||||
|
// Grip-menu value prompt — consume the typed number and
|
||||||
|
// route it through `apply_grip_menu_value`.
|
||||||
|
if let Some(pending) = self.grip_pending.take() {
|
||||||
|
let raw = self.command_line.input.trim().to_string();
|
||||||
|
self.command_line.input.clear();
|
||||||
|
let Ok(v) = raw.parse::<f64>() else {
|
||||||
|
self.command_line.push_error(&format!(
|
||||||
|
"{}: expected a number, got \"{raw}\"",
|
||||||
|
pending.label
|
||||||
|
));
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let i = self.active_tab;
|
||||||
|
use crate::entities::traits::EntityTypeOps;
|
||||||
|
self.push_undo_snapshot(i, pending.label);
|
||||||
|
if let Some(entity) = self
|
||||||
|
.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity_mut(pending.handle)
|
||||||
|
{
|
||||||
|
entity.apply_grip_menu_value(
|
||||||
|
pending.grip_id,
|
||||||
|
pending.action,
|
||||||
|
v,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_selected_grips();
|
||||||
|
self.refresh_properties();
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
// Interactive VPORTS: the entry after a bare `VPORTS` is the
|
// Interactive VPORTS: the entry after a bare `VPORTS` is the
|
||||||
// tiled configuration. Empty input defaults to SINGLE.
|
// tiled configuration. Empty input defaults to SINGLE.
|
||||||
if self.awaiting_vports {
|
if self.awaiting_vports {
|
||||||
|
|
@ -1203,6 +1236,10 @@ impl OpenCADStudio {
|
||||||
self.grip_hover = None;
|
self.grip_hover = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
if self.grip_pending.take().is_some() {
|
||||||
|
self.command_line.input.clear();
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
// Cancel layout rename / context menus first, then fall through.
|
// Cancel layout rename / context menus first, then fall through.
|
||||||
let i_e = self.active_tab;
|
let i_e = self.active_tab;
|
||||||
{
|
{
|
||||||
|
|
@ -3143,10 +3180,28 @@ impl OpenCADStudio {
|
||||||
use crate::entities::traits::EntityTypeOps;
|
use crate::entities::traits::EntityTypeOps;
|
||||||
use crate::scene::object::GripMenuAction;
|
use crate::scene::object::GripMenuAction;
|
||||||
if matches!(item.action, GripMenuAction::Stretch) {
|
if matches!(item.action, GripMenuAction::Stretch) {
|
||||||
// Default action: nothing — user can click the grip
|
|
||||||
// to start the normal drag.
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
// Actions that need a follow-up number stash a pending
|
||||||
|
// state + prompt; the next typed value drives
|
||||||
|
// `apply_grip_menu_value`.
|
||||||
|
let prompt = self
|
||||||
|
.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(popup.handle)
|
||||||
|
.and_then(|e| e.grip_menu_value_prompt(popup.grip_id, item.action));
|
||||||
|
if let Some(label) = prompt {
|
||||||
|
self.grip_pending = Some(super::GripPendingValue {
|
||||||
|
handle: popup.handle,
|
||||||
|
grip_id: popup.grip_id,
|
||||||
|
action: item.action,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
self.command_line.push_info(&format!("{label}:"));
|
||||||
|
return self.focus_cmd_input();
|
||||||
|
}
|
||||||
|
// One-shot action — apply immediately.
|
||||||
self.push_undo_snapshot(i, item.label);
|
self.push_undo_snapshot(i, item.label);
|
||||||
if let Some(entity) = self
|
if let Some(entity) = self
|
||||||
.tabs[i]
|
.tabs[i]
|
||||||
|
|
|
||||||
|
|
@ -253,8 +253,55 @@ impl crate::entities::traits::Grippable for Arc {
|
||||||
_grip_id: usize,
|
_grip_id: usize,
|
||||||
_action: crate::scene::object::GripMenuAction,
|
_action: crate::scene::object::GripMenuAction,
|
||||||
) {
|
) {
|
||||||
// Radius / Arc Length / Lengthen all need a follow-up prompt
|
// Radius / Arc Length / Lengthen all need a follow-up prompt;
|
||||||
// (numeric distance / length / radius) — Phase 3.
|
// the actual edit happens in `apply_grip_menu_value`.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
match action {
|
||||||
|
A::Radius => Some("New radius"),
|
||||||
|
A::ArcLength => Some("New arc length"),
|
||||||
|
A::Lengthen => Some("Distance"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
match action {
|
||||||
|
A::Radius if value > 0.0 => self.radius = value,
|
||||||
|
A::ArcLength if value > 0.0 && self.radius > 1e-9 => {
|
||||||
|
// Hold start_angle, derive new end_angle from arc length
|
||||||
|
// = r * Δθ.
|
||||||
|
let new_span = value / self.radius;
|
||||||
|
self.end_angle = self.start_angle + new_span;
|
||||||
|
}
|
||||||
|
A::Lengthen => {
|
||||||
|
// Extend either end by `value` arc-length units along
|
||||||
|
// the arc. Positive `value` lengthens; negative
|
||||||
|
// shortens. Grip 1 = start endpoint, grip 2 = end endpoint.
|
||||||
|
if self.radius < 1e-9 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dtheta = value / self.radius;
|
||||||
|
match grip_id {
|
||||||
|
1 => self.start_angle -= dtheta,
|
||||||
|
2 => self.end_angle += dtheta,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,55 @@ impl crate::entities::traits::Grippable for Line {
|
||||||
_grip_id: usize,
|
_grip_id: usize,
|
||||||
_action: crate::scene::object::GripMenuAction,
|
_action: crate::scene::object::GripMenuAction,
|
||||||
) {
|
) {
|
||||||
// Lengthen requires a follow-up distance prompt — Phase 3.
|
// Lengthen needs a follow-up distance — handled by
|
||||||
|
// `apply_grip_menu_value`.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
match action {
|
||||||
|
A::Lengthen => Some("Distance"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
if !matches!(action, A::Lengthen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dx = self.end.x - self.start.x;
|
||||||
|
let dy = self.end.y - self.start.y;
|
||||||
|
let dz = self.end.z - self.start.z;
|
||||||
|
let len = (dx * dx + dy * dy + dz * dz).sqrt();
|
||||||
|
if len < 1e-12 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (ux, uy, uz) = (dx / len, dy / len, dz / len);
|
||||||
|
match grip_id {
|
||||||
|
0 => {
|
||||||
|
// Move start endpoint backward along the line by `value`
|
||||||
|
// (positive = lengthen; negative = shorten).
|
||||||
|
self.start.x -= ux * value;
|
||||||
|
self.start.y -= uy * value;
|
||||||
|
self.start.z -= uz * value;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
self.end.x += ux * value;
|
||||||
|
self.end.y += uy * value;
|
||||||
|
self.end.z += uz * value;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,28 @@ pub trait Grippable {
|
||||||
/// Entities override this to mutate themselves when the user picks
|
/// Entities override this to mutate themselves when the user picks
|
||||||
/// Add Vertex / Remove Vertex / Convert / Reverse / …
|
/// Add Vertex / Remove Vertex / Convert / Reverse / …
|
||||||
fn apply_grip_menu(&mut self, _grip_id: usize, _action: GripMenuAction) {}
|
fn apply_grip_menu(&mut self, _grip_id: usize, _action: GripMenuAction) {}
|
||||||
|
/// Same as `apply_grip_menu` but for actions that need a numeric
|
||||||
|
/// follow-up (Lengthen / Radius / Arc Length / Rotate Text / …).
|
||||||
|
/// The app prompts the user for a value on the command line and
|
||||||
|
/// calls this with the parsed `f64`.
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
_action: GripMenuAction,
|
||||||
|
_value: f64,
|
||||||
|
) {}
|
||||||
|
/// If the popup action this entity defines for `grip_id` needs a
|
||||||
|
/// numeric prompt, return the prompt string and the action so the
|
||||||
|
/// app can stash a pending-value state and route the next typed
|
||||||
|
/// number into `apply_grip_menu_value`. `None` for one-shot
|
||||||
|
/// actions handled by `apply_grip_menu`.
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
_action: GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PropertyEditable {
|
pub trait PropertyEditable {
|
||||||
|
|
@ -78,6 +100,17 @@ pub trait EntityTypeOps {
|
||||||
fn apply_geom_prop(&mut self, field: &str, value: &str);
|
fn apply_geom_prop(&mut self, field: &str, value: &str);
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
||||||
fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction);
|
fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction);
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: GripMenuAction,
|
||||||
|
) -> Option<&'static str>;
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
);
|
||||||
fn apply_transform(&mut self, t: &EntityTransform);
|
fn apply_transform(&mut self, t: &EntityTransform);
|
||||||
fn mass_props(&self) -> Option<MassProps>;
|
fn mass_props(&self) -> Option<MassProps>;
|
||||||
fn text_content(&self) -> Option<String>;
|
fn text_content(&self) -> Option<String>;
|
||||||
|
|
@ -287,6 +320,47 @@ impl EntityTypeOps for EntityType {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
dispatch!(self,
|
||||||
|
|e| Grippable::grip_menu_value_prompt(e, grip_id, action),
|
||||||
|
[
|
||||||
|
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||||
|
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||||
|
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||||
|
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||||
|
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||||
|
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||||
|
Underlay, Shape, Ole2Frame,
|
||||||
|
],
|
||||||
|
_ => None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
) {
|
||||||
|
dispatch!(self,
|
||||||
|
|e| Grippable::apply_grip_menu_value(e, grip_id, action, value),
|
||||||
|
[
|
||||||
|
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||||
|
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||||
|
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||||
|
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||||
|
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||||
|
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||||
|
Underlay, Shape, Ole2Frame,
|
||||||
|
],
|
||||||
|
_ => {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||||
dispatch!(self,
|
dispatch!(self,
|
||||||
|e| Transformable::apply_transform(e, t),
|
|e| Transformable::apply_transform(e, t),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue