Dynamic Input: the CIRCLE radius prompt shows one distance field, not X/Y (fixes #1135) (#1137)

This commit is contained in:
Marco Arnulfo
2026-10-19 02:29:48 -04:00
committed by GitHub
parent 9e3f584a68
commit 9aea9b16c4
6 changed files with 100 additions and 18 deletions
+6 -3
View File
@@ -755,9 +755,12 @@ impl Interactive for CircleM {
let last_r = s.doc().map(|d| d.header.f64("CIRCLERAD", 0.0)).unwrap_or(0.0);
match (self.mode, n) {
(0, 0) => Prompt::new("Specify center point for circle", Accept::POINT).kw(&["3P", "2P", "Ttr (tan tan radius)"]),
(0, _) if self.diameter => Prompt::new("Specify diameter of circle", Accept::POINT_OR_NUMBER).base_opt(self.pts.first().copied()),
(0, _) if self.diameter => {
Prompt::new("Specify diameter of circle", Accept::POINT_OR_NUMBER).base_opt(self.pts.first().copied()).distance()
}
(0, _) => {
let p = Prompt::new("Specify radius of circle", Accept::POINT_OR_NUMBER).kw(&["Diameter"]).base_opt(self.pts.first().copied());
let p =
Prompt::new("Specify radius of circle", Accept::POINT_OR_NUMBER).kw(&["Diameter"]).base_opt(self.pts.first().copied()).distance();
if last_r > 0.0 { p.default(format!("{last_r:.4}")) } else { p }
}
(1, k) => Prompt::new(
@@ -890,7 +893,7 @@ impl Interactive for PolygonM {
(Some(_), false, Some(_), None) => Prompt::new("Enter an option", Accept::TEXT)
.kw(&["Inscribed in circle", "Circumscribed about circle"])
.default(if s.last_used.polygon_circumscribed { "C" } else { "I" }),
(Some(_), false, Some(c), Some(_)) => Prompt::new("Specify radius of circle", Accept::POINT_OR_NUMBER).base(c),
(Some(_), false, Some(c), Some(_)) => Prompt::new("Specify radius of circle", Accept::POINT_OR_NUMBER).base(c).distance(),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
+1 -1
View File
@@ -578,7 +578,7 @@ impl Interactive for TanCircleM {
1 => Prompt::new("Specify point on object for second tangent of circle", Accept::POINT),
_ => {
let r = s.doc().map(|d| d.header.f64("CIRCLERAD", 0.0)).unwrap_or(0.0);
let p = Prompt::new("Specify radius of circle", Accept::POINT_OR_NUMBER).base_opt(self.picks.last().map(|x| x.1));
let p = Prompt::new("Specify radius of circle", Accept::POINT_OR_NUMBER).base_opt(self.picks.last().map(|x| x.1)).distance();
if r > 0.0 { p.default(format!("{r:.4}")) } else { p }
}
}
+3 -3
View File
@@ -90,19 +90,19 @@ impl PlineAsk {
PlineAsk::Angle | PlineAsk::CenterAngle(_) | PlineAsk::RadiusAngle(_) => Prompt::new("Specify included angle", Accept::NUMBER),
PlineAsk::AngleEnd(_) => end(&["CEnter", "Radius"]),
PlineAsk::AngleCenter(_) | PlineAsk::Center => Prompt::new("Specify center point of arc", Accept::POINT).base(st),
PlineAsk::AngleRadius(_) | PlineAsk::Radius => Prompt::new("Specify radius of arc", Accept::POINT_OR_NUMBER).base(st),
PlineAsk::AngleRadius(_) | PlineAsk::Radius => Prompt::new("Specify radius of arc", Accept::POINT_OR_NUMBER).base(st).distance(),
PlineAsk::Chord(a, _) => {
let d = shown_direction(s, chord_default(tangent, a));
Prompt::new("Specify direction of chord for arc", Accept::POINT_OR_NUMBER).base(st).default(d)
}
PlineAsk::CenterEnd(_) => end(&["Angle", "Length"]),
PlineAsk::CenterLength(_) => Prompt::new("Specify length of chord", Accept::POINT_OR_NUMBER).base(st),
PlineAsk::CenterLength(_) => Prompt::new("Specify length of chord", Accept::POINT_OR_NUMBER).base(st).distance(),
PlineAsk::Direction => Prompt::new("Specify the tangent direction for the start point of arc", Accept::POINT_OR_NUMBER).base(st),
PlineAsk::DirectionEnd(_) => Prompt::new("Specify endpoint of the arc", Accept::POINT).base(st),
PlineAsk::RadiusEnd(_) => end(&["Angle"]),
PlineAsk::Second => Prompt::new("Specify second point on arc", Accept::POINT).base(st),
PlineAsk::SecondEnd(_) => Prompt::new("Specify end point of arc", Accept::POINT).base(st),
PlineAsk::Length => Prompt::new("Specify length of line", Accept::POINT_OR_NUMBER).base(st),
PlineAsk::Length => Prompt::new("Specify length of line", Accept::POINT_OR_NUMBER).base(st).distance(),
}
}
+10
View File
@@ -57,6 +57,11 @@ pub struct Prompt {
/// prompt (the routine writes all of it).
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub raw: bool,
/// The prompt asks for a distance from `base` (a radius, a diameter, a length), and a point
/// answers with its distance from it: Dynamic Input shows one distance field instead of the
/// pointer boxes.
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub distance: bool,
}
/// Parse `@distance<angle` (relative polar), returning `(distance, angle in radians)`.
@@ -109,6 +114,11 @@ impl Prompt {
self.corner = true;
self
}
/// Asks for a distance from the base (the `distance` field).
pub fn distance(mut self) -> Self {
self.distance = true;
self
}
/// Accept deferred tangent/perpendicular snaps (the `deferred` field).
pub fn deferred(mut self) -> Self {
self.deferred = true;
+13 -7
View File
@@ -1854,9 +1854,11 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
if app.session.settings.dynmode
&& let Some(hp) = hover_pos
{
// A stretched grip shows its dimensions instead of the pointer boxes.
if !crate::grip_dyn::draw(app, &painter) {
dynamic_input(app, &painter, hp);
// A stretched grip shows its dimensions instead of the pointer boxes; a prompt asking
// for a distance shows its one field and keeps the prompt and its options.
let fields = crate::grip_dyn::draw(app, &painter);
if !fields || app.session.current_prompt().is_some_and(|p| p.distance) {
dynamic_input(app, &painter, hp, fields);
}
}
}
@@ -2022,8 +2024,9 @@ fn draw_crosshair(app: &CadApp, p: &egui::Painter, rect: Rect, at: Pos2, pickbox
/// The Dynamic Input tooltip beside the cursor, styled like the command line. At a point prompt
/// it shows the pair of value boxes, a view of the command-line text (see [`crate::dyninput`]);
/// otherwise the prompt with what is being typed, or the live distance and angle.
fn dynamic_input(app: &mut CadApp, p: &egui::Painter, hover: Pos2) {
/// otherwise the prompt with what is being typed, or the live distance and angle. With `fields`
/// (a distance prompt's field is on show, [`crate::grip_dyn`]) only the prompt and its options.
fn dynamic_input(app: &mut CadApp, p: &egui::Painter, hover: Pos2, fields: bool) {
app.cmd.option_rows.clear();
// While the options menu is open the tooltip stays where it opened, so the menu can be reached.
let at = match app.cmd.options {
@@ -2037,7 +2040,8 @@ fn dynamic_input(app: &mut CadApp, p: &egui::Painter, hover: Pos2) {
// DYNPROMPT = 0: no prompt beside the cursor (the value boxes stay).
let show_prompt = app.session.settings.dynprompt;
let options = show_prompt && !prompt.keywords.is_empty();
if let Some(f) = crate::cmdline::dyn_frame(app)
if !fields
&& let Some(f) = crate::cmdline::dyn_frame(app)
&& let Some(e) = crate::dyninput::parse(&app.cmd.buffer, &f)
&& let Some(c) = app.canvas.cursor
{
@@ -2050,7 +2054,9 @@ fn dynamic_input(app: &mut CadApp, p: &egui::Painter, hover: Pos2) {
return;
}
let mut text = crate::i18n::t(&prompt.message).to_string();
if !app.cmd.buffer.is_empty() {
if fields {
// The field shows the distance and what is typed.
} else if !app.cmd.buffer.is_empty() {
text = format!("{}: {}", crate::i18n::t(&prompt.message), app.cmd.buffer);
} else if let (Some(base), Some(c)) = (prompt.base, app.canvas.cursor) {
text = format!(
+67 -4
View File
@@ -5,6 +5,10 @@
//!
//! The command-line text is the active field's text, so Backspace and Escape behave as usual;
//! text that isn't a value (a keyword such as `C` for Copy) goes to the command as typed.
//!
//! A prompt that asks for a distance from its base ([`cadcraft_engine::Prompt::distance`]: a
//! circle's radius, a polyline arc's) shows the same way, as one length field from the base, and
//! Enter sends the typed value to the command as a number.
use cadcraft_doc::Handle;
use cadcraft_engine::Input;
@@ -20,22 +24,31 @@ use crate::theme::Tokens;
pub struct GripDyn {
/// The grip the fields belong to; another grip starts over.
grip: Option<(Handle, usize)>,
/// The base of the distance prompt the field belongs to; another one starts over.
base: Option<Vec2>,
/// The active field.
pub active: usize,
/// Locked fields: (field, value as typed).
pub locked: Vec<(usize, String)>,
}
/// The base of the prompt on show when it asks for a distance from it (a circle's radius, a
/// polyline arc's): its one field is that distance.
fn distance_base(app: &CadApp) -> Option<Vec2> {
crate::cmdline::dyn_frame(app).and_then(|_| app.session.current_prompt()).filter(|p| p.distance).and_then(|p| p.base)
}
/// The dimensions on show, after bringing the fields' state up to date with the prompt (empty when
/// no grip is stretched or the value boxes aren't live).
/// no grip is stretched, no distance is asked for, or the value boxes aren't live).
pub fn dims(app: &mut CadApp) -> Vec<GripDim> {
let grip = crate::cmdline::dyn_frame(app).and_then(|_| app.session.current_prompt()).and_then(|p| p.grip);
let base = distance_base(app);
let st = &mut app.canvas.grip_dyn;
if st.grip != grip {
*st = GripDyn { grip, ..GripDyn::default() };
if st.grip != grip || st.base != base {
*st = GripDyn { grip, base, ..GripDyn::default() };
}
if grip.is_none() {
return Vec::new();
return base.map(|b| vec![GripDim { kind: GripDimKind::Length, anchor: b, original: 0.0 }]).unwrap_or_default();
}
let dims = grip_dims(&app.session);
if app.canvas.grip_dyn.active >= dims.len() {
@@ -97,6 +110,17 @@ pub fn submit(app: &mut CadApp) -> bool {
if !typed.is_empty() && dims.get(app.canvas.grip_dyn.active).and_then(|d| d.parse(&app.session, typed)).is_none() {
return false;
}
// A distance prompt takes the value as typed (or as Tab locked it), so the command reads the
// number itself: a negative arc radius, a radius of zero refused.
if distance_base(app).is_some() {
if typed.is_empty()
&& let Some((_, l)) = app.canvas.grip_dyn.locked.first()
{
app.cmd.buffer = l.clone();
}
app.canvas.grip_dyn = GripDyn::default();
return false;
}
let vals = values(app, &dims);
if vals.is_empty() {
return false;
@@ -278,6 +302,45 @@ mod tests {
assert_eq!(lines, 2, "Copy mode stretched a copy");
}
/// CIRCLE's radius prompt shows one distance field from the centre, not the pointer boxes
/// (X/Y, or distance and angle), and a typed value is the radius (#1135).
#[test]
fn circle_radius_shows_one_distance_field_and_typed_value_is_the_radius() {
for cartesian in [true, false] {
let mut app = CadApp::new(cadcraft_engine::Session::new(), crate::Services::default());
app.session.settings.dynpi_cartesian = cartesian;
// The UCS icon paints "X" and "Y" too.
app.ui.show_ucs_icon = false;
let ctx = egui::Context::default();
crate::canvas::tests::painted_text(&mut app, &ctx, pos2(700.0, 450.0));
let at = app.canvas.xf.unwrap().to_screen(Vec2::ZERO) + vec2(130.0, -70.0);
let labels = |text: &str| text.lines().filter(|l| matches!(*l, "X" | "Y" | "<")).map(str::to_owned).collect::<Vec<_>>();
app.session.cmdline("circle").unwrap();
crate::canvas::tests::painted_text(&mut app, &ctx, at);
let text = crate::canvas::tests::painted_text(&mut app, &ctx, at);
assert_eq!(labels(&text), ["X", "Y"], "the centre is a point:\n{text}");
app.session.input(Input::Point(Vec2::ZERO)).unwrap();
crate::canvas::tests::painted_text(&mut app, &ctx, at);
let text = crate::canvas::tests::painted_text(&mut app, &ctx, at);
assert!(labels(&text).is_empty(), "pointer boxes at the radius prompt (DYNPIFORMAT {cartesian}):\n{text}");
assert_eq!(dims(&mut app).len(), 1, "one distance field");
let r = cadcraft_engine::units::format_distance(app.canvas.cursor.unwrap().len(), 2, 4);
assert!(text.contains(&r), "no radius {r} in:\n{text}");
assert!(text.contains("Specify radius of circle"), "{text}");
run_frame(&mut app, &ctx, vec![Event::Text("25".into())]);
run_frame(&mut app, &ctx, key(egui::Key::Enter));
assert!(app.session.running.is_none());
let radius = app.session.doc().unwrap().model.iter().find_map(|e| match &e.kind {
EntityKind::Circle(ci) => Some(ci.radius),
_ => None,
});
assert_eq!(radius, Some(25.0));
}
}
/// The grip's length fields read the drawing's length units (LUNITS/LUPREC) (#841).
#[test]
fn length_fields_follow_the_drawing_units() {