ASSEMBLE panel (SOLID, SHEET METAL, PLASTIC): New Component, Joint, As-built Joint, Joint Origin, Rigid Group, Drive Joints, Motion Link, Interference. Joint origins are picked by hovering: faces snap to their centre (round faces to the centre of the nearer end), edges to their middle (round edges to their centre), vertices to themselves, shown as glyphs with their z axis. The Joint dialog has the motion type, angle, offsets X/Y/Z, flip, per-value limits and Animate (the preview plays the motion); joints and drives preview on the placed model (world states). Drive Joints moves live with a slider. Interference lists the overlaps and shows them red. Joints appear in the timeline and in a Browser Joints folder (edit, drive, suppress, delete). SHEET METAL tab in Fusion's layout: Flange (base, edge or contour, from the picks; height arrow on the canvas), Hem, Unfold, Refold, Convert to Sheet Metal, Sheet Metal Rules (rule manager), Create Flat Pattern (flat view with up/down bend lines, bend table, DXF export). PLASTIC tab with SETUP: Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Rib, Web and the shared solid commands on the tab. Every feature dialog has live preview, Edit Feature and pre-selection; icons drawn in code. Toolbar layout per tab in workspace.rs; tests for the dialog to command mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
529 lines
21 KiB
Rust
529 lines
21 KiB
Rust
//! Plastic dialogs: Boss, Lip / Groove, Snap Fit and Rest (placed by a pick on a face), Manage
|
||
//! Plastic Rules and Assign Plastic Rule. Values left empty take the body's plastic rule.
|
||
|
||
use serde_json::{Map, Value, json};
|
||
use solvecraft_engine::Sel;
|
||
use solvecraft_engine::doc::FeatureKind;
|
||
use solvecraft_engine::doc::expr::Kind as ValueKind;
|
||
use solvecraft_engine::doc::plastic::PlasticRule;
|
||
use solvecraft_engine::geom::Vec3;
|
||
|
||
use crate::SolveApp;
|
||
use crate::dialogs::{Kind, combo, face_sel, field, row_label};
|
||
use crate::selection::{BODIES, FACES, PLANAR_FACES, SelInput};
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub enum Pl {
|
||
Boss {
|
||
diameter: String,
|
||
height: String,
|
||
hole_diameter: String,
|
||
hole_depth: String,
|
||
draft: String,
|
||
fillet: String,
|
||
ribs: String,
|
||
rib_thickness: String,
|
||
rib_length: String,
|
||
rib_offset: String,
|
||
},
|
||
Lip {
|
||
groove: bool,
|
||
width: String,
|
||
height: String,
|
||
gap: String,
|
||
outside: bool,
|
||
},
|
||
SnapFit {
|
||
hook: usize,
|
||
length: String,
|
||
thickness: String,
|
||
width: String,
|
||
catch_depth: String,
|
||
catch_length: String,
|
||
},
|
||
Rest {
|
||
round: bool,
|
||
width: String,
|
||
length: String,
|
||
height: String,
|
||
draft: String,
|
||
thickness: String,
|
||
},
|
||
/// The rule manager: the rule shown (index into the rules, past the end for a new one).
|
||
Rules {
|
||
pick: usize,
|
||
name: String,
|
||
values: Vec<String>,
|
||
active: bool,
|
||
loaded: Option<usize>,
|
||
},
|
||
Assign {
|
||
rule: usize,
|
||
},
|
||
}
|
||
|
||
impl Pl {
|
||
pub fn title(&self) -> &'static str {
|
||
match self {
|
||
Pl::Boss { .. } => "BOSS",
|
||
Pl::Lip { groove: false, .. } => "LIP",
|
||
Pl::Lip { .. } => "GROOVE",
|
||
Pl::SnapFit { .. } => "SNAP FIT",
|
||
Pl::Rest { .. } => "REST",
|
||
Pl::Rules { .. } => "MANAGE PLASTIC RULES",
|
||
Pl::Assign { .. } => "ASSIGN PLASTIC RULE",
|
||
}
|
||
}
|
||
|
||
pub fn previews(&self) -> bool {
|
||
!matches!(self, Pl::Rules { .. } | Pl::Assign { .. })
|
||
}
|
||
|
||
pub fn primary(&mut self) -> Option<(&'static str, ValueKind, &mut String)> {
|
||
Some(match self {
|
||
Pl::Boss { height, .. } | Pl::Lip { height, .. } | Pl::Rest { height, .. } => ("Height", ValueKind::Length, height),
|
||
Pl::SnapFit { length, .. } => ("Length", ValueKind::Length, length),
|
||
_ => return None,
|
||
})
|
||
}
|
||
}
|
||
|
||
fn boss() -> Pl {
|
||
Pl::Boss {
|
||
diameter: "6 mm".into(),
|
||
height: "10 mm".into(),
|
||
hole_diameter: "3 mm".into(),
|
||
hole_depth: String::new(),
|
||
draft: String::new(),
|
||
fillet: String::new(),
|
||
ribs: "0".into(),
|
||
rib_thickness: "1 mm".into(),
|
||
rib_length: "3 mm".into(),
|
||
rib_offset: "1 mm".into(),
|
||
}
|
||
}
|
||
|
||
fn snap_fit() -> Pl {
|
||
Pl::SnapFit {
|
||
hook: 0,
|
||
length: "10 mm".into(),
|
||
thickness: "1.5 mm".into(),
|
||
width: "5 mm".into(),
|
||
catch_depth: "1 mm".into(),
|
||
catch_length: "2 mm".into(),
|
||
}
|
||
}
|
||
|
||
fn rest() -> Pl {
|
||
Pl::Rest { round: true, width: "10 mm".into(), length: "15 mm".into(), height: "2 mm".into(), draft: String::new(), thickness: String::new() }
|
||
}
|
||
|
||
fn lip(groove: bool) -> Pl {
|
||
Pl::Lip { groove, width: "1.5 mm".into(), height: "2 mm".into(), gap: String::new(), outside: false }
|
||
}
|
||
|
||
/// The dialog for a plastic command.
|
||
pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||
let (pl, inputs) = match id {
|
||
"FusionBossCommand" => (boss(), vec![SelInput::new("Position", PLANAR_FACES, false)]),
|
||
"FusionLipCommand" => (lip(false), vec![SelInput::new("Rim face", FACES, false)]),
|
||
"FusionSnapFitCommand" => (snap_fit(), vec![SelInput::new("Position", PLANAR_FACES, false)]),
|
||
"FusionRestCommand" => (rest(), vec![SelInput::new("Position", PLANAR_FACES, false)]),
|
||
"FusionManagePlasticRuleCommand" => {
|
||
let rules = app.session.doc.plastic_rules();
|
||
let pick = app.session.doc.plastic.active.as_ref().and_then(|a| rules.iter().position(|r| &r.name == a)).unwrap_or(0);
|
||
(Pl::Rules { pick, name: String::new(), values: Vec::new(), active: false, loaded: None }, vec![])
|
||
}
|
||
"FusionAssignPlasticRuleCommand" => (Pl::Assign { rule: 1 }, vec![SelInput::new("Bodies", BODIES, true)]),
|
||
_ => return None,
|
||
};
|
||
Some((Kind::Plastic(pl), inputs))
|
||
}
|
||
|
||
/// A value row whose empty value means "from the rule" (or another default).
|
||
fn opt_field(ui: &mut egui::Ui, label: &str, v: &mut String, hint: &str) -> bool {
|
||
row_label(ui, label);
|
||
let r = ui.add(egui::TextEdit::singleline(v).hint_text(hint));
|
||
crate::params_dialog::complete(ui, &r, v);
|
||
ui.end_row();
|
||
r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
|
||
}
|
||
|
||
fn value_row(ui: &mut egui::Ui, label: &str, v: &mut String) -> bool {
|
||
row_label(ui, label);
|
||
let e = field(ui, v);
|
||
ui.end_row();
|
||
e
|
||
}
|
||
|
||
/// Directions across a face (with normal `n`) a snap fit's catch can point: two axes and their
|
||
/// opposites, named after the world axis they run along.
|
||
pub fn hook_dirs(n: Vec3) -> Vec<(String, Vec3)> {
|
||
let u = [Vec3::X, Vec3::Y, Vec3::Z].into_iter().filter_map(|a| (a - n * a.dot(n)).normalized()).next().unwrap_or_else(|| n.any_perp());
|
||
let v = n.cross(u);
|
||
let name = |d: Vec3| -> String {
|
||
let (axis, c) = [("X", d.x), ("Y", d.y), ("Z", d.z)].into_iter().max_by(|a, b| a.1.abs().total_cmp(&b.1.abs())).unwrap_or(("X", 0.0));
|
||
format!("{}{axis}", if c < 0.0 { "−" } else { "+" })
|
||
};
|
||
[u, -u, v, -v].into_iter().map(|d| (name(d), d)).collect()
|
||
}
|
||
|
||
fn face_normal(app: &SolveApp, inputs: &[SelInput]) -> Option<Vec3> {
|
||
match inputs.first()?.items.first()? {
|
||
Sel::Face { body, index, .. } => crate::dialogs::planar_face(&app.session, body, *index).map(|(_, n)| n),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// The dialog's own rows (after the selection inputs); true when Enter was pressed in a value.
|
||
pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pl, inputs: &[SelInput]) -> bool {
|
||
let mut enter = false;
|
||
match k {
|
||
Pl::Boss { diameter, height, hole_diameter, hole_depth, draft, fillet, ribs, rib_thickness, rib_length, rib_offset } => {
|
||
enter |= value_row(ui, "Diameter", diameter);
|
||
enter |= value_row(ui, "Height", height);
|
||
enter |= opt_field(ui, "Hole Diameter", hole_diameter, "no hole");
|
||
enter |= opt_field(ui, "Hole Depth", hole_depth, "to the face");
|
||
enter |= opt_field(ui, "Draft Angle", draft, "from the rule");
|
||
enter |= opt_field(ui, "Root Fillet", fillet, "none");
|
||
enter |= value_row(ui, "Ribs", ribs);
|
||
if app.session.doc.eval(ribs, ValueKind::Unitless).is_ok_and(|n| n >= 1.0) {
|
||
enter |= value_row(ui, " Rib Thickness", rib_thickness);
|
||
enter |= value_row(ui, " Rib Length", rib_length);
|
||
enter |= value_row(ui, " Rib Offset", rib_offset);
|
||
}
|
||
}
|
||
Pl::Lip { groove, width, height, gap, outside } => {
|
||
row_label(ui, "Type");
|
||
let mut ty = usize::from(*groove);
|
||
combo(ui, "pl_lip", &["Lip", "Groove"], &mut ty);
|
||
*groove = ty == 1;
|
||
ui.end_row();
|
||
row_label(ui, "Rim Edge");
|
||
let mut side = usize::from(*outside);
|
||
combo(ui, "pl_side", &["Inside", "Outside"], &mut side);
|
||
*outside = side == 1;
|
||
ui.end_row();
|
||
enter |= value_row(ui, "Width", width);
|
||
enter |= value_row(ui, "Height", height);
|
||
if *groove {
|
||
enter |= opt_field(ui, "Clearance", gap, "from the rule");
|
||
}
|
||
}
|
||
Pl::SnapFit { hook, length, thickness, width, catch_depth, catch_length } => {
|
||
let dirs = face_normal(app, inputs).map(hook_dirs).unwrap_or_default();
|
||
if !dirs.is_empty() {
|
||
let names: Vec<&str> = dirs.iter().map(|(n, _)| n.as_str()).collect();
|
||
row_label(ui, "Catch Direction");
|
||
combo(ui, "pl_hook", &names, hook);
|
||
ui.end_row();
|
||
}
|
||
enter |= value_row(ui, "Length", length);
|
||
enter |= value_row(ui, "Thickness", thickness);
|
||
enter |= value_row(ui, "Width", width);
|
||
enter |= value_row(ui, "Catch Depth", catch_depth);
|
||
enter |= value_row(ui, "Catch Length", catch_length);
|
||
}
|
||
Pl::Rest { round, width, length, height, draft, thickness } => {
|
||
row_label(ui, "Shape");
|
||
let mut sh = usize::from(!*round);
|
||
combo(ui, "pl_rest", &["Round", "Rectangular"], &mut sh);
|
||
*round = sh == 0;
|
||
ui.end_row();
|
||
enter |= value_row(ui, if *round { "Diameter" } else { "Width" }, width);
|
||
if !*round {
|
||
enter |= value_row(ui, "Length", length);
|
||
}
|
||
enter |= value_row(ui, "Height", height);
|
||
enter |= opt_field(ui, "Draft Angle", draft, "from the rule");
|
||
enter |= opt_field(ui, "Wall Thickness", thickness, "solid");
|
||
}
|
||
Pl::Rules { pick, name, values, active, loaded } => {
|
||
let rules = app.session.doc.plastic_rules();
|
||
let mut names: Vec<String> = rules.iter().map(|r| r.name.clone()).collect();
|
||
names.push("New rule…".into());
|
||
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
|
||
row_label(ui, "Rule");
|
||
combo(ui, "pl_rules", &refs, pick);
|
||
ui.end_row();
|
||
if *loaded != Some(*pick) {
|
||
let base = rules.get(*pick).or(rules.first()).cloned();
|
||
if let Some(b) = base {
|
||
*values = std::iter::once(b.material.clone()).chain(b.fields().iter().map(|(_, e, _)| (*e).clone())).collect();
|
||
*active = app.session.doc.plastic.active.as_deref() == Some(b.name.as_str()) && *pick < rules.len();
|
||
}
|
||
if *pick >= rules.len() {
|
||
*name = format!("Plastic Rule{}", app.session.doc.plastic.rules.len() + 1);
|
||
}
|
||
*loaded = Some(*pick);
|
||
}
|
||
if *pick >= rules.len() {
|
||
row_label(ui, "Name");
|
||
ui.text_edit_singleline(name);
|
||
ui.end_row();
|
||
}
|
||
let keys = rule_keys();
|
||
for (i, v) in values.iter_mut().enumerate() {
|
||
let label = if i == 0 { "Material".to_string() } else { keys.get(i - 1).map(|k| label_of(k)).unwrap_or_default() };
|
||
row_label(ui, &label);
|
||
enter |= if i == 0 { ui.text_edit_singleline(v).lost_focus() && ui.input(|x| x.key_pressed(egui::Key::Enter)) } else { field(ui, v) };
|
||
ui.end_row();
|
||
}
|
||
row_label(ui, "Active rule");
|
||
ui.checkbox(active, "");
|
||
ui.end_row();
|
||
}
|
||
Pl::Assign { rule } => {
|
||
let mut names: Vec<String> = vec!["(no rule)".into()];
|
||
names.extend(app.session.doc.plastic_rules().into_iter().map(|r| r.name));
|
||
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
|
||
row_label(ui, "Rule");
|
||
combo(ui, "pl_assign", &refs, rule);
|
||
ui.end_row();
|
||
}
|
||
}
|
||
enter
|
||
}
|
||
|
||
/// The rule's value fields, in the order `PlasticRule::fields` gives them.
|
||
fn rule_keys() -> Vec<&'static str> {
|
||
PlasticRule::library().first().map(|r| r.fields().iter().map(|(k, _, _)| *k).collect()).unwrap_or_default()
|
||
}
|
||
|
||
fn label_of(key: &str) -> String {
|
||
key.split('_')
|
||
.map(|w| {
|
||
let mut c = w.chars();
|
||
c.next().map(|f| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default()
|
||
})
|
||
.collect::<Vec<_>>()
|
||
.join(" ")
|
||
}
|
||
|
||
fn pt(v: Vec3) -> Value {
|
||
json!([v.x, v.y, v.z])
|
||
}
|
||
|
||
/// Put non-empty optional values into the parameters.
|
||
fn put(p: &mut Value, list: &[(&str, &String)]) {
|
||
for (k, v) in list {
|
||
if !v.trim().is_empty() {
|
||
p[*k] = json!(v);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The commands the dialog's OK runs (`extra`: parameters kept from the feature being edited).
|
||
pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map<String, Value>) -> Result<Vec<(String, Value)>, String> {
|
||
let face = inputs
|
||
.first()
|
||
.and_then(|i| i.items.first())
|
||
.and_then(|x| if let Sel::Face { body, point, .. } = x { Some((body.clone(), *point)) } else { None });
|
||
let placed = |what: &str| -> Result<Value, String> {
|
||
let (body, at) = face.clone().ok_or_else(|| format!("select {what} first"))?;
|
||
Ok(json!({"position": pt(at), "body": body}))
|
||
};
|
||
let (cmd, mut p): (&str, Value) = match k {
|
||
Pl::Boss { diameter, height, hole_diameter, hole_depth, draft, fillet, ribs, rib_thickness, rib_length, rib_offset } => {
|
||
let mut p = placed("a point on a face")?;
|
||
p["diameter"] = json!(diameter);
|
||
p["height"] = json!(height);
|
||
put(&mut p, &[("hole_diameter", hole_diameter), ("hole_depth", hole_depth), ("draft", draft), ("fillet", fillet)]);
|
||
if app.session.doc.eval(ribs, ValueKind::Unitless).is_ok_and(|n| n >= 1.0) {
|
||
put(&mut p, &[("ribs", ribs), ("rib_thickness", rib_thickness), ("rib_length", rib_length), ("rib_offset", rib_offset)]);
|
||
}
|
||
("FusionBossCommand", p)
|
||
}
|
||
Pl::Lip { groove, width, height, gap, outside } => {
|
||
let (body, at) = face.clone().ok_or("select the rim face first")?;
|
||
let mut p = json!({"face": pt(at), "body": body, "width": width, "height": height,
|
||
"type": if *groove { "groove" } else { "lip" }, "side": if *outside { "outside" } else { "inside" }});
|
||
if *groove {
|
||
put(&mut p, &[("gap", gap)]);
|
||
}
|
||
("FusionLipCommand", p)
|
||
}
|
||
Pl::SnapFit { hook, length, thickness, width, catch_depth, catch_length } => {
|
||
let mut p = placed("a point on a face")?;
|
||
let n = extra
|
||
.get("direction")
|
||
.and_then(|v| Some(Vec3::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?, v.get(2)?.as_f64()?)))
|
||
.or(face_normal(app, inputs));
|
||
let dir = n.map(hook_dirs).and_then(|d| d.get(*hook).map(|x| x.1)).ok_or("pick a planar face")?;
|
||
p["hook"] = pt(dir);
|
||
for (key, v) in
|
||
[("length", length), ("thickness", thickness), ("width", width), ("catch_depth", catch_depth), ("catch_length", catch_length)]
|
||
{
|
||
p[key] = json!(v);
|
||
}
|
||
("FusionSnapFitCommand", p)
|
||
}
|
||
Pl::Rest { round, width, length, height, draft, thickness } => {
|
||
let mut p = placed("a point on a face")?;
|
||
p["width"] = json!(width);
|
||
p["height"] = json!(height);
|
||
if !*round {
|
||
p["length"] = json!(length);
|
||
}
|
||
put(&mut p, &[("draft", draft), ("thickness", thickness)]);
|
||
("FusionRestCommand", p)
|
||
}
|
||
Pl::Rules { pick, name, values, active, .. } => {
|
||
let rules = app.session.doc.plastic_rules();
|
||
let n = match rules.get(*pick) {
|
||
Some(r) => r.name.clone(),
|
||
None => name.trim().to_string(),
|
||
};
|
||
if n.is_empty() {
|
||
return Err("give the rule a name".into());
|
||
}
|
||
let mut p = json!({"name": n, "active": active});
|
||
if let Some(m) = values.first().filter(|m| !m.trim().is_empty()) {
|
||
p["material"] = json!(m);
|
||
}
|
||
for (key, v) in rule_keys().iter().zip(values.iter().skip(1)) {
|
||
if !v.trim().is_empty() {
|
||
p[*key] = json!(v);
|
||
}
|
||
}
|
||
("FusionManagePlasticRuleCommand", p)
|
||
}
|
||
Pl::Assign { rule } => {
|
||
let bodies: Vec<String> = inputs
|
||
.first()
|
||
.into_iter()
|
||
.flat_map(|i| &i.items)
|
||
.filter_map(|x| if let Sel::Body { name } = x { Some(name.clone()) } else { None })
|
||
.collect();
|
||
if bodies.is_empty() {
|
||
return Err("select bodies first".into());
|
||
}
|
||
let name = rule.checked_sub(1).and_then(|i| app.session.doc.plastic_rules().get(i).map(|r| r.name.clone())).unwrap_or_default();
|
||
("FusionAssignPlasticRuleCommand", json!({"bodies": bodies, "rule": name}))
|
||
}
|
||
};
|
||
if let Value::Object(m) = &mut p {
|
||
for (key, v) in extra {
|
||
m.entry(key.clone()).or_insert_with(|| v.clone());
|
||
}
|
||
}
|
||
Ok(vec![(cmd.to_string(), p)])
|
||
}
|
||
|
||
fn s(o: &Option<String>) -> String {
|
||
o.clone().unwrap_or_default()
|
||
}
|
||
|
||
/// The dialog editing a plastic feature, filled from it.
|
||
pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelInput>, Map<String, Value>)> {
|
||
let ss = &app.session;
|
||
let mut extra = Map::new();
|
||
let (pl, id, at): (Pl, &str, Vec3) = match kind {
|
||
FeatureKind::Boss {
|
||
position,
|
||
direction,
|
||
diameter,
|
||
height,
|
||
hole_diameter,
|
||
hole_depth,
|
||
draft,
|
||
fillet,
|
||
ribs,
|
||
rib_thickness,
|
||
rib_length,
|
||
rib_offset,
|
||
body,
|
||
} => {
|
||
extra.insert("direction".into(), pt(*direction));
|
||
if let Some(b) = body {
|
||
extra.insert("body".into(), json!(b));
|
||
}
|
||
let mut b = boss();
|
||
if let Pl::Boss {
|
||
diameter: d,
|
||
height: h,
|
||
hole_diameter: hd,
|
||
hole_depth: hp,
|
||
draft: dr,
|
||
fillet: f,
|
||
ribs: r,
|
||
rib_thickness: rt,
|
||
rib_length: rl,
|
||
rib_offset: ro,
|
||
} = &mut b
|
||
{
|
||
*d = diameter.clone();
|
||
*h = height.clone();
|
||
*hd = s(hole_diameter);
|
||
*hp = s(hole_depth);
|
||
*dr = s(draft);
|
||
*f = s(fillet);
|
||
*r = ribs.clone().unwrap_or_else(|| "0".into());
|
||
for (slot, v) in [(rt, rib_thickness), (rl, rib_length), (ro, rib_offset)] {
|
||
if let Some(v) = v {
|
||
*slot = v.clone();
|
||
}
|
||
}
|
||
}
|
||
(b, "FusionBossCommand", *position)
|
||
}
|
||
FeatureKind::Lip { face, width, height, groove, gap, outside, body } => {
|
||
if let Some(b) = body {
|
||
extra.insert("body".into(), json!(b));
|
||
}
|
||
(Pl::Lip { groove: *groove, width: width.clone(), height: height.clone(), gap: s(gap), outside: *outside }, "FusionLipCommand", *face)
|
||
}
|
||
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
|
||
extra.insert("direction".into(), pt(*direction));
|
||
if let Some(b) = body {
|
||
extra.insert("body".into(), json!(b));
|
||
}
|
||
let h = hook_dirs(*direction).iter().position(|(_, d)| d.dist(*hook) < 1e-6);
|
||
if h.is_none() {
|
||
extra.insert("hook".into(), pt(*hook));
|
||
}
|
||
let f = Pl::SnapFit {
|
||
hook: h.unwrap_or(0),
|
||
length: length.clone(),
|
||
thickness: thickness.clone(),
|
||
width: width.clone(),
|
||
catch_depth: catch_depth.clone(),
|
||
catch_length: catch_length.clone(),
|
||
};
|
||
(f, "FusionSnapFitCommand", *position)
|
||
}
|
||
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
|
||
extra.insert("direction".into(), pt(*direction));
|
||
if let Some(a) = along {
|
||
extra.insert("along".into(), pt(*a));
|
||
}
|
||
if let Some(b) = body {
|
||
extra.insert("body".into(), json!(b));
|
||
}
|
||
let r = Pl::Rest {
|
||
round: length.is_none(),
|
||
width: width.clone(),
|
||
length: length.clone().unwrap_or_else(|| "15 mm".into()),
|
||
height: height.clone(),
|
||
draft: s(draft),
|
||
thickness: s(thickness),
|
||
};
|
||
(r, "FusionRestCommand", *position)
|
||
}
|
||
_ => return None,
|
||
};
|
||
let (_, mut inputs) = start(app, id)?;
|
||
if let Some(i) = inputs.first_mut() {
|
||
i.items = face_sel(ss, at).into_iter().collect();
|
||
}
|
||
// A pick that no longer finds its face keeps the stored point.
|
||
if let Some(i) = inputs.first_mut().filter(|i| i.items.is_empty()) {
|
||
i.items = vec![Sel::Face { body: extra.get("body").and_then(Value::as_str).unwrap_or_default().to_string(), index: 0, point: at }];
|
||
}
|
||
Some((Kind::Plastic(pl), inputs, extra))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "dialogs_plastic_tests.rs"]
|
||
mod tests;
|