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:
Hakan Seven committed 2026-05-29 14:58:35 +03:00
1 parent 86ca1b7033
commit 559cd76369
5 files changed
+243 -5

No files matched your search

+49 -2
View File
@@ -253,8 +253,55 @@ impl crate::entities::traits::Grippable for Arc {
_grip_id: usize,
_action: crate::scene::object::GripMenuAction,
) {
// Radius / Arc Length / Lengthen all need a follow-up prompt
// (numeric distance / length / radius) — Phase 3.
// Radius / Arc Length / Lengthen all need a follow-up prompt;
// 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,
_ => {}
}
}
_ => {}
}
}
}
+49 -1
View File
@@ -182,7 +182,55 @@ impl crate::entities::traits::Grippable for Line {
_grip_id: usize,
_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;
}
_ => {}
}
}
}
+74
View File
@@ -35,6 +35,28 @@ pub trait Grippable {
/// Entities override this to mutate themselves when the user picks
/// Add Vertex / Remove Vertex / Convert / Reverse / …
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 {
@@ -78,6 +100,17 @@ pub trait EntityTypeOps {
fn apply_geom_prop(&mut self, field: &str, value: &str);
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
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 mass_props(&self) -> Option<MassProps>;
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) {
dispatch!(self,
|e| Transformable::apply_transform(e, t),