mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user