One tool window for sketch Fillet and the three sketch Chamfers (#789)
Sketch Fillet and the three sketch Chamfers share one tool window, with its own icons, labels and a scenario.
This commit is contained in:
1 parent
dbfb689f6c
commit
bdcfe3ff1d
13 files changed
+567
-70
No files matched your search
@@ -8,17 +8,30 @@
|
||||
//! right-click, then one more click for the line, with nothing on screen saying what was
|
||||
//! picked. Move joins them too, which had no toolbar placement, no tool and no dialog: its only
|
||||
//! route into the UI was the sketch context menu, and that went through the same
|
||||
//! empty-parameter fallback and reported an engine error. `sketch.fillet`, the three
|
||||
//! `sketch.chamfer.*` commands and the sketch patterns are the same gap and join this module
|
||||
//! later, each its own change.
|
||||
//! empty-parameter fallback and reported an engine error. `Mod::Corner` joins them here,
|
||||
//! serving `sketch.fillet` and the three `sketch.chamfer.*` commands together, the way
|
||||
//! `solid.fillet` and `solid.chamfer` already share one window. Before it, a click committed a
|
||||
//! size guessed at 20 percent of the shorter leg and then opened a bare parameter editor, so
|
||||
//! the design changed before any number was asked for. `sketch.chamfer.distance_angle` sent a
|
||||
//! hard-coded 45 degrees and `sketch.chamfer.two_distance` forced its second distance equal to
|
||||
//! the first, leaving both values unreachable from the UI. The sketch patterns are the same
|
||||
//! gap and join this module later, each its own change.
|
||||
//!
|
||||
//! The Chamfer Type choice is a row of icon buttons rather than a drop-down: the modes differ
|
||||
//! in shape, and a picture of each shape names it better than a line of text.
|
||||
|
||||
use egui::Pos2;
|
||||
use serde_json::{Map, Value, json};
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::expr::Kind as ValueKind;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{Kind, check, field, row_label};
|
||||
use crate::dialogs::{CHAMFER_TYPES, Kind, check, field, icon_choice, row_label};
|
||||
use crate::selection::{AXES, CURVES, POINTS, SelInput};
|
||||
use crate::viewport::{Hit, Proj};
|
||||
|
||||
/// The three chamfer types, drawn as the cut each one makes, in `CHAMFER_TYPES` order.
|
||||
const CHAMFER_TYPE_ICONS: [&str; 3] = ["sketch_chamfer_equal", "sketch_chamfer_two", "sketch_chamfer_angle"];
|
||||
|
||||
/// A sketch modify tool window's values.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -39,6 +52,13 @@ pub enum Mod {
|
||||
/// turn about the centre, and whether the originals stay behind. The command already
|
||||
/// carries the copy flag, so the Create Copy row needs no engine change.
|
||||
Move { x: String, y: String, angle: String, copy: bool },
|
||||
/// `sketch.fillet` and the three `sketch.chamfer.*` commands. `chamfer` chooses between a
|
||||
/// fillet and a chamfer and never changes while the window is open; `ctype` chooses which
|
||||
/// of the three chamfer commands runs (0 equal distance, 1 two distances, 2 distance and
|
||||
/// angle). `size` is the radius or the first distance, `distance2` the second distance and
|
||||
/// `angle` the angle from the first line. `flip` names the two lines in the other order, so
|
||||
/// the first distance lands on the other leg of the corner.
|
||||
Corner { chamfer: bool, ctype: usize, size: String, distance2: String, angle: String, flip: bool },
|
||||
}
|
||||
|
||||
impl Mod {
|
||||
@@ -48,6 +68,8 @@ impl Mod {
|
||||
Mod::Offset { .. } => "OFFSET",
|
||||
Mod::Mirror => "MIRROR",
|
||||
Mod::Move { .. } => "MOVE/COPY",
|
||||
Mod::Corner { chamfer: false, .. } => "FILLET",
|
||||
Mod::Corner { .. } => "CHAMFER",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +80,8 @@ impl Mod {
|
||||
Mod::Offset { distance, .. } => Some(("Distance", ValueKind::Length, distance)),
|
||||
Mod::Move { x, .. } => Some(("X Distance", ValueKind::Length, x)),
|
||||
Mod::Mirror => None,
|
||||
Mod::Corner { chamfer: false, size, .. } => Some(("Radius", ValueKind::Length, size)),
|
||||
Mod::Corner { size, .. } => Some(("Distance", ValueKind::Length, size)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,16 +93,32 @@ fn inputs(k: &Mod) -> Vec<SelInput> {
|
||||
// AXES is the mask that takes a straight sketch curve (`selection::SelInput::accepts`).
|
||||
Mod::Mirror => vec![SelInput::new("Entities", CURVES | POINTS, true), SelInput::new("Mirror line", AXES, false)],
|
||||
Mod::Move { .. } => vec![SelInput::new("Entities", CURVES | POINTS, true), SelInput::new("Center", POINTS, false)],
|
||||
// A fillet rounds any number of corners to one radius (`points`). Each chamfer command
|
||||
// cuts one corner, so its input holds one.
|
||||
Mod::Corner { chamfer: false, .. } => vec![SelInput::new("Corners", POINTS, true)],
|
||||
Mod::Corner { .. } => vec![SelInput::new("Corner", POINTS, false)],
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog for a sketch modify command.
|
||||
pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
let corner = |chamfer: bool, ctype: usize| Mod::Corner {
|
||||
chamfer,
|
||||
ctype,
|
||||
size: "2 mm".into(),
|
||||
distance2: "2 mm".into(),
|
||||
angle: "45 deg".into(),
|
||||
flip: false,
|
||||
};
|
||||
let k = match id {
|
||||
"sketch.scale" => Mod::Scale { factor: "2".into() },
|
||||
"sketch.offset" => Mod::Offset { distance: "2 mm".into(), chain: true, flip: false },
|
||||
"sketch.mirror" => Mod::Mirror,
|
||||
"sketch.move" => Mod::Move { x: "0 mm".into(), y: "0 mm".into(), angle: "0 deg".into(), copy: false },
|
||||
"sketch.fillet" => corner(false, 0),
|
||||
"sketch.chamfer.equal_distance" => corner(true, 0),
|
||||
"sketch.chamfer.two_distance" => corner(true, 1),
|
||||
"sketch.chamfer.distance_angle" => corner(true, 2),
|
||||
_ => return None,
|
||||
};
|
||||
let i = inputs(&k);
|
||||
@@ -121,10 +161,62 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Mod) -> bool {
|
||||
check(ui, copy);
|
||||
ui.end_row();
|
||||
}
|
||||
Mod::Corner { chamfer, ctype, size, distance2, angle, flip } => {
|
||||
if *chamfer {
|
||||
row_label(ui, "Chamfer Type");
|
||||
icon_choice(ui, &CHAMFER_TYPES, &CHAMFER_TYPE_ICONS, ctype);
|
||||
ui.end_row();
|
||||
}
|
||||
row_label(ui, size_label(*chamfer, *ctype));
|
||||
enter |= field(ui, size);
|
||||
ui.end_row();
|
||||
if *chamfer && *ctype == 1 {
|
||||
row_label(ui, "Distance 2");
|
||||
enter |= field(ui, distance2);
|
||||
ui.end_row();
|
||||
}
|
||||
if *chamfer && *ctype == 2 {
|
||||
row_label(ui, "Angle");
|
||||
enter |= field(ui, angle);
|
||||
ui.end_row();
|
||||
}
|
||||
// With two equal distances there is nothing to swap.
|
||||
if *chamfer && *ctype > 0 {
|
||||
row_label(ui, "Flip");
|
||||
check(ui, flip);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
enter
|
||||
}
|
||||
|
||||
/// What the first value means for this corner type.
|
||||
fn size_label(chamfer: bool, ctype: usize) -> &'static str {
|
||||
if !chamfer {
|
||||
"Radius"
|
||||
} else if ctype == 0 {
|
||||
"Distance"
|
||||
} else {
|
||||
"Distance 1"
|
||||
}
|
||||
}
|
||||
|
||||
/// The corner under the cursor while the Fillet or Chamfer window is open: the hover hit and
|
||||
/// the selection it stands for.
|
||||
///
|
||||
/// Corners are resolved through `sketch_tools::corner_point` rather than the ordinary
|
||||
/// nearest-point test. A rectangle drawn from the origin leaves its own endpoint on top of the
|
||||
/// fixed origin point, and the nearest-point test returns the origin, which ends no two curves.
|
||||
/// That resolution chooses among the endpoints that actually make a corner instead.
|
||||
pub(crate) fn corner_hit(app: &SolveApp, proj: &Proj, pos: Pos2) -> Option<(Hit, Sel)> {
|
||||
let Some(Kind::SketchModify(Mod::Corner { chamfer, .. })) = app.dialog.as_ref().map(|d| &d.kind) else { return None };
|
||||
let sketch = app.session.active_sketch?;
|
||||
let (at, id) = crate::sketch_tools::corner_point(app, proj, pos, !*chamfer)?;
|
||||
let sel = Sel::SketchPoint { id: format!("{sketch}:{id}") };
|
||||
Some((Hit::SketchPoint { sketch, id, at }, sel))
|
||||
}
|
||||
|
||||
/// What input `i` holds.
|
||||
fn items(inputs: &[SelInput], i: usize) -> &[Sel] {
|
||||
inputs.get(i).map(|x| x.items.as_slice()).unwrap_or(&[])
|
||||
@@ -195,6 +287,53 @@ fn signed_length(app: &SolveApp, text: &str, flip: bool) -> Result<f64, String>
|
||||
Ok(if flip { -v } else { v })
|
||||
}
|
||||
|
||||
/// Corner point ids picked in input `i`, within the active sketch. A `POINTS` pick is stored as
|
||||
/// "<sketch id>:<point id>"; a point selected without the window carries the bare id.
|
||||
fn corner_ids(app: &SolveApp, inputs: &[SelInput], i: usize) -> Vec<String> {
|
||||
let active = app.session.active_sketch;
|
||||
items(inputs, i)
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Sel::SketchPoint { id } => match id.split_once(':') {
|
||||
Some((sketch, point)) => (sketch.parse::<u64>().ok() == active).then(|| point.to_string()),
|
||||
None => Some(id.clone()),
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A corner size evaluated against the document's parameters, which every corner command needs
|
||||
/// to be strictly positive. A bare number means the design's units for a length and degrees for
|
||||
/// an angle, as every other typed sketch value does. `value` above is the Move window's reading
|
||||
/// of a field, where an empty row means zero and a negative distance is a direction; neither is
|
||||
/// a radius or a chamfer leg, so this one is its own.
|
||||
fn corner_size(app: &SolveApp, label: &str, text: &str, kind: ValueKind) -> Result<f64, String> {
|
||||
let doc = &app.session.doc;
|
||||
let v = doc.eval(&doc.with_design_unit(text.trim(), kind), kind).map_err(|e| format!("{label}: {e}"))?;
|
||||
if !v.is_finite() || v <= 1e-9 {
|
||||
return Err(format!("{label} must be a positive number"));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// The corner as a chamfer command takes it. Unflipped it is the point itself, and the command
|
||||
/// resolves the two lines in curve order. Flipped it names those two lines in the other order,
|
||||
/// so `distance` applies to the other leg.
|
||||
fn corner_ref(app: &SolveApp, point: &str, flip: bool) -> Result<Value, String> {
|
||||
if !flip {
|
||||
return Ok(json!({ "point": point }));
|
||||
}
|
||||
let gone = || "that corner is no longer in the sketch".to_string();
|
||||
let sketch = app.session.active_sketch.ok_or("open a sketch first")?;
|
||||
let state = app.session.world_state();
|
||||
let sk = &state.sketch(sketch).ok_or_else(gone)?.sketch;
|
||||
let index = sk.resolve_point(point).ok_or_else(gone)?;
|
||||
let (a, b) = crate::sketch_tools::corner_curves(sk, index, false).ok_or("a chamfer needs a corner of exactly two lines")?;
|
||||
let id = |c: usize| sk.curves.get(c).map(|x| x.id.clone()).ok_or_else(gone);
|
||||
Ok(json!({"a": id(b)?, "b": id(a)?}))
|
||||
}
|
||||
|
||||
/// The command a sketch modify dialog makes. `sketch.scale` takes a plain evaluated number for
|
||||
/// its factor, `sketch.offset` one for its distance and `sketch.move` its distances and angle,
|
||||
/// not expression strings: none has a model parameter to carry one, unlike `sketch.fillet`'s radius.
|
||||
@@ -254,6 +393,31 @@ pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map<String
|
||||
}
|
||||
("sketch.move", p)
|
||||
}
|
||||
Mod::Corner { chamfer: false, size, .. } => {
|
||||
let corners = corner_ids(app, inputs, 0);
|
||||
if corners.is_empty() {
|
||||
return Err("select the corners to round first".into());
|
||||
}
|
||||
// The radius is checked where it is evaluated, against the document's parameters.
|
||||
("sketch.fillet", json!({"points": corners, "radius": size.trim()}))
|
||||
}
|
||||
Mod::Corner { ctype, size, distance2, angle, flip, .. } => {
|
||||
let point = corner_ids(app, inputs, 0).into_iter().next().ok_or("select the corner to cut first")?;
|
||||
let mut p = corner_ref(app, &point, *flip && *ctype > 0)?;
|
||||
p["distance"] = json!(corner_size(app, size_label(true, *ctype), size, ValueKind::Length)?);
|
||||
match ctype {
|
||||
1 => {
|
||||
p["distance2"] = json!(corner_size(app, "Distance 2", distance2, ValueKind::Length)?);
|
||||
("sketch.chamfer.two_distance", p)
|
||||
}
|
||||
2 => {
|
||||
// The command takes degrees; an evaluated angle is in radians.
|
||||
p["angle"] = json!(corner_size(app, "Angle", angle, ValueKind::Angle)?.to_degrees());
|
||||
("sketch.chamfer.distance_angle", p)
|
||||
}
|
||||
_ => ("sketch.chamfer.equal_distance", p),
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Value::Object(m) = &mut params {
|
||||
for (key, value) in extra {
|
||||
|
||||
Reference in new issue
Block a user