Give the Move dialog the canvas value box every other dialog already has (#739)
The Move dialog gets the same canvas value box as the other dialogs: X for translating moves, the angle for Rotate, placed clear of the triad.
This commit is contained in:
1 parent
4112189551
commit
6f4a9d96fb
6 files changed
+137
-14
No files matched your search
@@ -134,13 +134,30 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj
|
||||
app.dialog = Some(d);
|
||||
return;
|
||||
}
|
||||
// Move: the triad at the pivot; dragging it sets the distances and angles.
|
||||
// Move: the triad at the pivot; dragging it sets the distances and angles. Its value box
|
||||
// sits beside it and reads the move's main value, so a drag shows its number and typing
|
||||
// one sets it. The triad is the manipulator here, so the box comes without the drag arrow
|
||||
// every other dialog's value gets (`dir` is None below).
|
||||
if matches!(d.kind, Kind::Move { .. }) {
|
||||
if let Some(t) = crate::dialogs_move::triad(app, &d)
|
||||
&& let Some(g) = crate::gizmo::show(ui, painter, proj, egui::Id::new("sc_move_triad"), &t, snap_step(app.cam.half_height()))
|
||||
{
|
||||
crate::dialogs_move::drag(app, &mut d, g);
|
||||
}
|
||||
// Read the triad again after the drag, so the box follows it in the same frame. Move
|
||||
// types with no triad (Rotate, Point to Point, Point to Position) put the box on the
|
||||
// picked bodies instead.
|
||||
let triad = crate::dialogs_move::triad(app, &d);
|
||||
let at = triad.map(|t| t.center).or_else(|| anchor(app, &d).map(|(base, _)| base));
|
||||
let focus = std::mem::take(&mut d.focus);
|
||||
if let Some(bs) = at.and_then(|base| proj.to_screen(base))
|
||||
&& let Some((label, _, value)) = d.primary()
|
||||
{
|
||||
// Clear of the triad: its arrows reach LEN_PX and its rings RING of that, so a box
|
||||
// beside the centre would cover the handles the user is reaching for.
|
||||
let off = if triad.is_some() { vec2(crate::gizmo::LEN_PX as f32 + 16.0, -12.0) } else { vec2(16.0, -30.0) };
|
||||
value_box(ui, label, value, bs + off, focus);
|
||||
}
|
||||
app.dialog = Some(d);
|
||||
return;
|
||||
}
|
||||
@@ -311,8 +328,21 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
|
||||
box_at = ts + vec2(14.0, -12.0);
|
||||
}
|
||||
}
|
||||
value_box(ui, label, value, box_at, focus);
|
||||
if *value != before
|
||||
&& let Kind::Extrude { all, .. } = &mut d.kind
|
||||
{
|
||||
*all = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// The command's main value in the viewport, next to the geometry: the keyboard goes here while
|
||||
/// the window is open, so a number typed straight after a pick sets the value. `at` is its
|
||||
/// top-left corner.
|
||||
fn value_box(ui: &mut egui::Ui, label: &str, value: &mut String, at: Pos2, focus: bool) {
|
||||
let t = Tokens::get();
|
||||
let id = egui::Id::new("sc_canvas_value");
|
||||
crate::frame::area(egui::Id::new("sc_canvas_area")).fixed_pos(box_at).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
|
||||
crate::frame::area(egui::Id::new("sc_canvas_area")).fixed_pos(at).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
|
||||
egui::Frame::popup(ui.style()).inner_margin(egui::Margin::symmetric(6, 3)).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(label).color(t.text_dim).size(11.5));
|
||||
@@ -328,11 +358,6 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
|
||||
});
|
||||
});
|
||||
});
|
||||
if *value != before
|
||||
&& let Kind::Extrude { all, .. } = &mut d.kind
|
||||
{
|
||||
*all = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// A draggable arrow from `base` along `n` showing `l` (at least a short stub); dragging writes
|
||||
|
||||
@@ -679,7 +679,13 @@ impl Dialog {
|
||||
Kind::Shell { thickness } => ("Thickness", ValueKind::Length, thickness),
|
||||
Kind::Draft { angle } => ("Angle", ValueKind::Angle, angle),
|
||||
Kind::Hole { diameter, .. } => ("Diameter", ValueKind::Length, diameter),
|
||||
Kind::Move { x, .. } => ("X", ValueKind::Length, x),
|
||||
// A move's main value follows its type: a distance along X for the ones that
|
||||
// translate, the angle for Rotate. Point to Point is two picks with nothing to type.
|
||||
Kind::Move { mode, x, angle, .. } => match *mode {
|
||||
crate::dialogs_move::ROTATE => ("Angle", ValueKind::Angle, angle),
|
||||
crate::dialogs_move::POINT_TO_POINT => return None,
|
||||
_ => ("X", ValueKind::Length, x),
|
||||
},
|
||||
Kind::PatternRect { spacing, .. } => ("Spacing", ValueKind::Length, spacing),
|
||||
Kind::PatternCirc { angle, .. } => ("Angle", ValueKind::Angle, angle),
|
||||
Kind::Section { offset, .. } => ("Distance", ValueKind::Length, offset),
|
||||
|
||||
@@ -13,11 +13,11 @@ use crate::gizmo::{self, Change, Drag};
|
||||
use crate::selection::{AXES, BODIES, EDGES, FACES, SelInput, VERTICES};
|
||||
|
||||
pub const TYPES: [&str; 5] = ["Free Move", "Translate", "Rotate", "Point to Point", "Point to Position"];
|
||||
const FREE: usize = 0;
|
||||
const TRANSLATE: usize = 1;
|
||||
const ROTATE: usize = 2;
|
||||
const POINT_TO_POINT: usize = 3;
|
||||
const POINT_TO_POSITION: usize = 4;
|
||||
pub const FREE: usize = 0;
|
||||
pub const TRANSLATE: usize = 1;
|
||||
pub const ROTATE: usize = 2;
|
||||
pub const POINT_TO_POINT: usize = 3;
|
||||
pub const POINT_TO_POSITION: usize = 4;
|
||||
const POINTS: u16 = VERTICES | FACES | EDGES;
|
||||
|
||||
/// A fresh Move dialog of a type (all values zero).
|
||||
@@ -318,3 +318,7 @@ pub fn for_feature(d: &mut Dialog, bodies: &[String], translate: &[String; 3], a
|
||||
d.extra.insert("copy".into(), json!(true));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "dialogs_move_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,71 @@
|
||||
//! The Move window's main value: the one the canvas box shows and the keyboard types into.
|
||||
//! It follows the move type, because each type uses a different value.
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
use crate::Services;
|
||||
use crate::dialogs::Dialog;
|
||||
|
||||
fn move_dialog(mode: usize) -> Dialog {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20})).unwrap();
|
||||
let mut app = SolveApp::new(s, Services::default());
|
||||
app.start("solid.move");
|
||||
let mut d = app.dialog.clone().unwrap();
|
||||
d.kind = kind(mode);
|
||||
d.inputs = inputs(mode);
|
||||
if let Some(inp) = d.inputs.first_mut() {
|
||||
inp.items = vec![Sel::Body { name: "Body1".into() }];
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_main_value_follows_the_move_type() {
|
||||
for mode in [FREE, TRANSLATE, POINT_TO_POSITION] {
|
||||
let mut d = move_dialog(mode);
|
||||
let got = d.primary().map(|p| (p.0, p.1));
|
||||
assert_eq!(got, Some(("X", ValueKind::Length)), "{} types a distance", TYPES[mode]);
|
||||
}
|
||||
|
||||
let mut d = move_dialog(ROTATE);
|
||||
let got = d.primary().map(|p| (p.0, p.1));
|
||||
assert_eq!(got, Some(("Angle", ValueKind::Angle)), "Rotate types an angle, not an X distance");
|
||||
|
||||
let mut d = move_dialog(POINT_TO_POINT);
|
||||
assert!(d.primary().is_none(), "Point to Point is two picks with nothing to type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_main_value_writes_through_to_the_command() {
|
||||
let mut d = move_dialog(ROTATE);
|
||||
if let Some((_, _, value)) = d.primary() {
|
||||
*value = "90 deg".into();
|
||||
}
|
||||
let Kind::Move { angle, .. } = &d.kind else { panic!("not a move") };
|
||||
assert_eq!(angle, "90 deg", "the box edits the angle itself, not a copy of it");
|
||||
|
||||
let mut d = move_dialog(TRANSLATE);
|
||||
if let Some((_, _, value)) = d.primary() {
|
||||
*value = "30 mm".into();
|
||||
}
|
||||
let Kind::Move { x, y, z, .. } = &d.kind else { panic!("not a move") };
|
||||
assert_eq!((x.as_str(), y.as_str(), z.as_str()), ("30 mm", "0 mm", "0 mm"), "the box edits X alone");
|
||||
}
|
||||
|
||||
/// Free Move and Translate carry the triad. The other three have no manipulator yet, so their
|
||||
/// value box is the only way to drive them: it must still have somewhere to sit.
|
||||
#[test]
|
||||
fn every_move_type_has_somewhere_to_put_its_value_box() {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20})).unwrap();
|
||||
let app = SolveApp::new(s, Services::default());
|
||||
for mode in [FREE, TRANSLATE, ROTATE, POINT_TO_POINT, POINT_TO_POSITION] {
|
||||
let d = move_dialog(mode);
|
||||
let has_triad = triad(&app, &d).is_some();
|
||||
assert_eq!(has_triad, matches!(mode, FREE | TRANSLATE), "{} triad", TYPES[mode]);
|
||||
let at = triad(&app, &d).map(|t| t.center).or_else(|| crate::canvas::anchor(&app, &d).map(|(base, _)| base));
|
||||
assert!(at.is_some(), "{} has nowhere to put its value box", TYPES[mode]);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ struct State {
|
||||
|
||||
const DIRS: [Vec3; 3] = [Vec3::X, Vec3::Y, Vec3::Z];
|
||||
/// Arrow length on screen (px).
|
||||
const LEN_PX: f64 = 110.0;
|
||||
pub const LEN_PX: f64 = 110.0;
|
||||
|
||||
thread_local! {
|
||||
/// The last triad drawn: centre and arrow length in mm (for scenarios and tests).
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{"note": "The Move window's value box on the canvas. Picking a body gives it the keyboard, so a typed number sets the move without hunting for a field first. Which value the box reads follows the move type, covered by dialogs_move_tests."},
|
||||
{"call": "engine.execute", "params": {"command": "solid.box", "params": {"length": 20, "width": 20, "height": 20}}},
|
||||
{"call": "ui.view", "params": {"view": "iso"}},
|
||||
|
||||
{"note": "Free Move: pick the body, type 30, press Enter. Nothing is clicked in the window at all."},
|
||||
{"start": "solid.move"},
|
||||
{"click": {"world": [10, 10, 20]}},
|
||||
{"expect": {"dialog": "Move", "bodies": 1}},
|
||||
{"text": "30"},
|
||||
{"expect": {"dialog": "x: \"30\""}},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"dialog": null, "errors": 0, "bodies": 1}},
|
||||
{"expect": {"bbox": {"body": "Body1", "min": [30, 0, 0], "max": [50, 20, 20]}}},
|
||||
|
||||
{"shot": "move_canvas_value"}
|
||||
]
|
||||
Reference in new issue
Block a user