1 parent
7ba2d5db47
commit
98ce6a4ee8
22 files changed
+804
-55
No files matched your search
@@ -565,10 +565,16 @@ impl Dialog {
|
||||
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Axis", AXES, false)],
|
||||
),
|
||||
"solid.loft" => Dialog::new(Kind::Loft { operation: usize::from(has_bodies) }, vec![SelInput::new("Profiles", PROFILES, true)]),
|
||||
"solid.sweep" => Dialog::new(
|
||||
Kind::Sweep { operation: usize::from(has_bodies), opts: crate::dialogs_sweep::SweepOpts::default() },
|
||||
vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Path", CURVES, true), crate::dialogs_sweep::direction_input()],
|
||||
),
|
||||
"solid.sweep" => Dialog::new(Kind::Sweep { operation: usize::from(has_bodies), opts: crate::dialogs_sweep::SweepOpts::default() }, {
|
||||
let [rail, rail2] = crate::dialogs_sweep::rail_inputs();
|
||||
vec![
|
||||
SelInput::new("Profile", PROFILES, true),
|
||||
SelInput::new("Path", CURVES, true),
|
||||
crate::dialogs_sweep::direction_input(),
|
||||
rail,
|
||||
rail2,
|
||||
]
|
||||
}),
|
||||
"solid.move" => Dialog::new(crate::dialogs_move::kind(0), crate::dialogs_move::inputs(0)),
|
||||
"solid.hole" => Dialog::new(hole_defaults(), vec![SelInput::new("Face", HOLE_FACES, true)]),
|
||||
"solid.box" => Dialog::new(
|
||||
@@ -3900,7 +3906,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
let path: Vec<String> = sels(d, 1).iter().filter_map(|x| if let Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect();
|
||||
let path_sketch = curves_sketch(app, &path, sketch.as_u64()).ok_or("the path curves must be in one sketch")?;
|
||||
let mut p = json!({"sketch": sketch, "profiles": idx, "path_sketch": path_sketch, "path": path, "operation": OPS.get(*operation).copied().unwrap_or("new")});
|
||||
crate::dialogs_sweep::params(app, opts, sels(d, 2).first(), &mut p)?;
|
||||
crate::dialogs_sweep::params(app, opts, sels(d, 2).first(), [sels(d, 3), sels(d, 4)], &mut p)?;
|
||||
("solid.sweep", p)
|
||||
}
|
||||
Kind::OffsetPlane { offset } => {
|
||||
@@ -5005,13 +5011,16 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
}
|
||||
d
|
||||
}
|
||||
FeatureKind::Sweep { sketch, profiles, path, operation, targets, orientation, twist, scale, .. } => {
|
||||
FeatureKind::Sweep { sketch, profiles, path, operation, targets, orientation, twist, scale, rails, rail_fit, .. } => {
|
||||
let mut d = start("solid.sweep")?;
|
||||
let (opts, direction) = crate::dialogs_sweep::SweepOpts::of(orientation, twist, scale, &feature_frame(s, f.component));
|
||||
let (opts, direction) = crate::dialogs_sweep::SweepOpts::of(orientation, twist, scale, *rail_fit, &feature_frame(s, f.component));
|
||||
d.kind = Kind::Sweep { operation: op_index(operation), opts };
|
||||
if let Some(inp) = d.inputs.get_mut(2) {
|
||||
inp.items = direction.into_iter().collect();
|
||||
}
|
||||
for (inp, items) in d.inputs.iter_mut().skip(3).zip(crate::dialogs_sweep::rail_sels(rails)) {
|
||||
inp.items = items;
|
||||
}
|
||||
let items = st.sketch(sketch.0).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
|
||||
if let Some(inp) = d.inputs.get_mut(0) {
|
||||
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect();
|
||||
|
||||
@@ -117,11 +117,16 @@ fn inputs(k: &Surf) -> Vec<SelInput> {
|
||||
match k {
|
||||
Surf::Extrude { .. } => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true)],
|
||||
Surf::Revolve { .. } => vec![SelInput::new("Profiles or curves", PROFILES | CURVES, true), SelInput::new("Axis", AXES, false)],
|
||||
Surf::Sweep(_) => vec![
|
||||
SelInput::new("Profiles or curves", PROFILES | CURVES, true),
|
||||
SelInput::new("Path", CURVES, true),
|
||||
crate::dialogs_sweep::direction_input(),
|
||||
],
|
||||
Surf::Sweep(_) => {
|
||||
let [rail, rail2] = crate::dialogs_sweep::rail_inputs();
|
||||
vec![
|
||||
SelInput::new("Profiles or curves", PROFILES | CURVES, true),
|
||||
SelInput::new("Path", CURVES, true),
|
||||
crate::dialogs_sweep::direction_input(),
|
||||
rail,
|
||||
rail2,
|
||||
]
|
||||
}
|
||||
Surf::Loft => vec![SelInput::new("Sections (one sketch each)", PROFILES | CURVES, true)],
|
||||
Surf::Patch { .. } => vec![SelInput::new("Boundary", PROFILES | EDGES, true), SelInput::new("Pass through", VERTICES | FACES, true)],
|
||||
Surf::Unstitch => vec![SelInput::new("Body, or faces to take out", BODIES | FACES, true)],
|
||||
@@ -371,7 +376,7 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
|
||||
p["path_sketch"] =
|
||||
json!(crate::dialogs::curves_sketch(app, &path, p["sketch"].as_u64()).ok_or("the path curves must be in one other sketch")?);
|
||||
p["path"] = json!(path);
|
||||
crate::dialogs_sweep::params(app, opts, items(inputs, 2).first(), &mut p)?;
|
||||
crate::dialogs_sweep::params(app, opts, items(inputs, 2).first(), [items(inputs, 3), items(inputs, 4)], &mut p)?;
|
||||
("surface.sweep", p)
|
||||
}
|
||||
Surf::Loft => {
|
||||
@@ -610,14 +615,17 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
|
||||
}
|
||||
return Some((Kind::Surface(k), ins, extra));
|
||||
}
|
||||
FeatureKind::SurfaceSweep { sketch, profiles, curves, path, orientation, twist, scale, .. } => {
|
||||
let (opts, direction) = crate::dialogs_sweep::SweepOpts::of(orientation, twist, scale, frame);
|
||||
FeatureKind::SurfaceSweep { sketch, profiles, curves, path, orientation, twist, scale, rails, rail_fit, .. } => {
|
||||
let (opts, direction) = crate::dialogs_sweep::SweepOpts::of(orientation, twist, scale, *rail_fit, frame);
|
||||
let [rail, rail2] = crate::dialogs_sweep::rail_sels(rails);
|
||||
(
|
||||
Surf::Sweep(opts),
|
||||
vec![
|
||||
sheet_sels(app, sketch.0, profiles.as_ref(), curves),
|
||||
path.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect(),
|
||||
direction.into_iter().collect(),
|
||||
rail,
|
||||
rail2,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
//! The options Sweep and Surface Sweep share (#771): how the profile turns along the path, a
|
||||
//! twist over the path's length and the profile's scale at the end.
|
||||
//! twist over the path's length, the profile's scale at the end, and guide rails with how they
|
||||
//! reshape the profile.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::{Expr, Mat, SweepOrientation};
|
||||
use solidcraft_engine::doc::{Expr, Mat, RailFit, SweepOrientation, SweepRail};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{combo, field, row_label};
|
||||
use crate::selection::{AXES, EDGES, SelInput};
|
||||
use crate::selection::{AXES, CURVES, EDGES, SelInput};
|
||||
|
||||
/// The orientation choices, in combo order: perpendicular, parallel, a direction.
|
||||
const ORIENTATIONS: [&str; 3] = ["Turn with Path", "Keep as Drawn", "Follow Direction"];
|
||||
@@ -18,6 +19,8 @@ const DIRECTION: usize = 2;
|
||||
const NO_TWIST: &str = "0 deg";
|
||||
/// An end scale field that means no scaling.
|
||||
const NO_SCALE: &str = "1";
|
||||
/// The rail fits, in combo order: scale, stretch.
|
||||
const RAIL_FITS: [&str; 2] = ["Scale", "Stretch"];
|
||||
|
||||
/// A sweep dialog's options.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -28,11 +31,13 @@ pub struct SweepOpts {
|
||||
pub scale: String,
|
||||
/// A stored direction (world), used while no direction is picked.
|
||||
pub direction: Option<Vec3>,
|
||||
/// An index into [`RAIL_FITS`].
|
||||
pub rail_fit: usize,
|
||||
}
|
||||
|
||||
impl Default for SweepOpts {
|
||||
fn default() -> Self {
|
||||
SweepOpts { orientation: 0, twist: NO_TWIST.into(), scale: NO_SCALE.into(), direction: None }
|
||||
SweepOpts { orientation: 0, twist: NO_TWIST.into(), scale: NO_SCALE.into(), direction: None, rail_fit: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +45,17 @@ impl SweepOpts {
|
||||
/// A feature's stored options as dialog values; `frame` maps the feature's component frame to
|
||||
/// the world (a stored direction is in the component's frame). A direction along an origin
|
||||
/// axis comes back as a pick of that axis.
|
||||
pub fn of(orientation: &SweepOrientation, twist: &Option<Expr>, scale: &Option<Expr>, frame: &Mat) -> (SweepOpts, Option<Sel>) {
|
||||
pub fn of(
|
||||
orientation: &SweepOrientation,
|
||||
twist: &Option<Expr>,
|
||||
scale: &Option<Expr>,
|
||||
rail_fit: RailFit,
|
||||
frame: &Mat,
|
||||
) -> (SweepOpts, Option<Sel>) {
|
||||
let mut o = SweepOpts {
|
||||
twist: twist.as_ref().map_or_else(|| NO_TWIST.into(), |t| t.to_string()),
|
||||
scale: scale.as_ref().map_or_else(|| NO_SCALE.into(), |k| k.to_string()),
|
||||
rail_fit: usize::from(rail_fit == RailFit::Stretch),
|
||||
..SweepOpts::default()
|
||||
};
|
||||
let mut pick = None;
|
||||
@@ -74,6 +86,18 @@ pub fn direction_input() -> SelInput {
|
||||
SelInput::new("Direction", AXES | EDGES, false)
|
||||
}
|
||||
|
||||
/// The inputs that pick a sweep's guide rails: the first rail's curves and the second's, in order
|
||||
/// along each rail; both optional.
|
||||
pub fn rail_inputs() -> [SelInput; 2] {
|
||||
[SelInput::new("Rail", CURVES, true), SelInput::new("Second Rail", CURVES, true)]
|
||||
}
|
||||
|
||||
/// A feature's stored rails as the rail inputs' selections.
|
||||
pub fn rail_sels(rails: &[SweepRail]) -> [Vec<Sel>; 2] {
|
||||
let sels = |i: usize| rails.get(i).map(|r| r.curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect()).unwrap_or_default();
|
||||
[sels(0), sels(1)]
|
||||
}
|
||||
|
||||
/// The option rows; true when Enter was pressed in a field.
|
||||
pub fn rows(ui: &mut egui::Ui, o: &mut SweepOpts, id: &str) -> bool {
|
||||
let mut enter = false;
|
||||
@@ -86,12 +110,34 @@ pub fn rows(ui: &mut egui::Ui, o: &mut SweepOpts, id: &str) -> bool {
|
||||
row_label(ui, "End Scale");
|
||||
enter |= field(ui, &mut o.scale);
|
||||
ui.end_row();
|
||||
row_label(ui, "Rail Fit");
|
||||
combo(ui, &format!("{id}_rail_fit"), &RAIL_FITS, &mut o.rail_fit);
|
||||
ui.end_row();
|
||||
enter
|
||||
}
|
||||
|
||||
/// The options as command parameters, added to `p`: the orientation, and the twist and end
|
||||
/// scale when they change anything. `picked` is the direction input's selection.
|
||||
pub fn params(app: &SolveApp, o: &SweepOpts, picked: Option<&Sel>, p: &mut Value) -> Result<(), String> {
|
||||
/// scale when they change anything; with rails picked, the rails (each in the sketch its curves
|
||||
/// are in) and the fit instead. `picked` is the direction input's selection, `rails` the rail
|
||||
/// inputs'.
|
||||
pub fn params(app: &SolveApp, o: &SweepOpts, picked: Option<&Sel>, rails: [&[Sel]; 2], p: &mut Value) -> Result<(), String> {
|
||||
let mut given = Vec::new();
|
||||
for sels in rails {
|
||||
let curves: Vec<String> = sels.iter().filter_map(|x| if let Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect();
|
||||
if curves.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let sketch = crate::dialogs::curves_sketch(app, &curves, None).ok_or("each rail's curves must be in one sketch")?;
|
||||
given.push(json!({"sketch": sketch, "curves": curves}));
|
||||
}
|
||||
if !given.is_empty() {
|
||||
// Rails set the profile's turn and size: the other options don't apply.
|
||||
p["rails"] = json!(given);
|
||||
if o.rail_fit == 1 {
|
||||
p["rail_fit"] = json!("stretch");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
match o.orientation {
|
||||
0 => {}
|
||||
1 => p["orientation"] = json!("parallel"),
|
||||
|
||||
@@ -99,3 +99,42 @@ fn editing_sweeps_keeps_their_options() {
|
||||
assert_eq!(after.param_names, before.param_names, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A rail picked in the dialog reaches the command with its sketch, the fit with it; editing the
|
||||
/// sweep brings both back and keeps them.
|
||||
#[test]
|
||||
fn a_rail_picked_in_the_dialog_is_kept_through_an_edit() {
|
||||
let mut h = Harness::new();
|
||||
let sketch = setup(&mut h);
|
||||
h.app.run("sketch.create", json!({"plane":"XZ","name":"R"})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points":[[5,0],[10,-40]],"ids":["out"]})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
let rail_sketch = h.app.session.doc.find_feature("R").unwrap().id;
|
||||
h.step(&json!({"call":"ui.start","params":{"command":"solid.sweep"}})).unwrap();
|
||||
h.frames(3);
|
||||
assert!(h.widget("Rail Fit").is_some());
|
||||
let d = h.app.dialog.as_mut().unwrap();
|
||||
d.inputs[0].items = vec![Sel::Profile { sketch, index: 0 }];
|
||||
d.inputs[1].items = vec![Sel::SketchCurve { id: "up".into() }];
|
||||
d.inputs[3].items = vec![Sel::SketchCurve { id: "out".into() }];
|
||||
let Kind::Sweep { opts, .. } = &mut d.kind else { panic!("{:?}", d.kind) };
|
||||
opts.rail_fit = 1;
|
||||
h.frames(2);
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
assert!(h.app.dialog.is_none(), "{:?}", h.app.session.log);
|
||||
let name = h.app.session.doc.features.last().unwrap().name.clone();
|
||||
let rails = |h: &Harness| match &h.app.session.doc.find_feature(&name).unwrap().kind {
|
||||
FeatureKind::Sweep { rails, rail_fit, .. } => (rails.clone(), *rail_fit),
|
||||
k => panic!("{k:?}"),
|
||||
};
|
||||
let (stored, fit) = rails(&h);
|
||||
assert_eq!(stored, vec![solidcraft_engine::doc::SweepRail { sketch: solidcraft_engine::doc::SketchId(rail_sketch), curves: vec!["out".into()] }]);
|
||||
assert_eq!(fit, solidcraft_engine::doc::RailFit::Stretch);
|
||||
h.step(&json!({"call":"ui.editFeature","params":{"feature":name}})).unwrap();
|
||||
h.frames(8);
|
||||
let d = h.app.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.inputs[3].items, vec![Sel::SketchCurve { id: "out".into() }]);
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
assert!(h.app.dialog.is_none(), "{:?}", h.app.session.log);
|
||||
assert_eq!(rails(&h), (stored, fit));
|
||||
}
|
||||
Reference in new issue
Block a user