Files
OneCAD/crates/ui-egui/src/workspace.rs
T
f7967b1ff6 Joints: gears, rack and pinion, and screws (#1524)
Motion links can carry a coupling (gear pair, rack and pinion, screw) whose ratio follows radii, picked round faces or pitch. Design format 23.

Co-authored-by: SychPL <SychPL@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 18:47:38 -04:00

252 lines
9.8 KiB
Rust

//! Explicit toolbar layouts: Sketch CREATE, the SOLID tab's ASSEMBLE panel, SHEET METAL and
//! PLASTIC. A command is registered on one tab and panel;
//! these panels list the commands they show in order (the first few get big buttons), so Extrude
//! or Joint appear on several tabs. Commands registered on such a panel but not listed follow at
//! the end.
use solidcraft_engine::CommandSpec;
use solidcraft_engine::doc::FeatureKind;
const ASSEMBLE: &[&str] = &[
"component.create",
"joint.create",
"joint.as_built",
"component.insert_design",
"joint.origin",
"joint.rigid_group",
"joint.drive",
"joint.motion_link",
"joint.gear",
"joint.rack_pinion",
"joint.screw",
"motion.study",
"explode.create",
"contact.enable_sets",
"contact.enable_all",
"contact.disable_all",
"contact.create",
"inspect.interference",
];
const SHEET_CREATE: &[&str] = &[
"sketch.create",
"sheet.flange",
"sheet.flat_pattern",
"sheet.hem",
"sheet.fold",
"sheet.convert",
"component.create",
"solid.extrude",
"solid.hole",
"solid.thread",
"solid.pattern.rectangular",
"solid.pattern.circular",
"solid.pattern.path",
"solid.mirror",
"joint.origin",
];
const SHEET_MODIFY: &[&str] = &[
"sheet.manage_rules",
"sheet.unfold",
"sheet.refold",
"solid.fillet",
"solid.chamfer",
"solid.move",
"solid.align",
"timeline.delete",
"solid.remove",
"material.assign",
"parameters.change",
];
const PLASTIC_SETUP: &[&str] = &["plastic.manage_rules", "plastic.assign_rule"];
const PLASTIC_CREATE: &[&str] = &[
"sketch.create",
"plastic.boss",
"plastic.snap_fit",
"plastic.lip",
"plastic.rest",
"component.create",
"solid.extrude",
"solid.revolve",
"solid.sweep",
"solid.loft",
"solid.rib",
"solid.web",
"solid.emboss",
"solid.hole",
"solid.thread",
"solid.box",
"solid.cylinder",
"solid.sphere",
"solid.torus",
"solid.coil",
"solid.pipe",
"solid.pattern.rectangular",
"solid.pattern.circular",
"solid.pattern.path",
"solid.mirror",
"joint.origin",
];
/// Panels of a tab, when this module lays the tab out.
pub fn panels(tab: &str) -> Option<&'static [&'static str]> {
match tab {
"SOLID" => Some(&["CREATE", "MODIFY", "ASSEMBLE", "CONFIGURE", "CONSTRUCT", "INSPECT", "INSERT", "SELECT"]),
"PLASTIC" => Some(&["SETUP", "CREATE", "MODIFY", "ASSEMBLE", "CONSTRUCT", "INSPECT", "INSERT", "SELECT"]),
_ => None,
}
}
/// The commands a panel shows, in order, and how many get a big button; `None` leaves the panel
/// to the registry.
pub fn layout<'a>(tab: &str, panel: &str, specs: &[&'a CommandSpec]) -> Option<(Vec<&'a CommandSpec>, usize)> {
let (ids, promoted, own_tab): (&[&str], usize, &str) = match (tab, panel) {
("SKETCH", "CREATE") => (
&[
"sketch.line",
"sketch.rectangle.two_point",
"sketch.circle.center",
"sketch.arc.three_point",
"sketch.slot.center_to_center",
"sketch.polygon.inscribed",
"sketch.spline.fit_point",
"sketch.point",
],
8,
tab,
),
("SOLID", "CONFIGURE") => (&["parameters.change", "config.table"], 2, tab),
("SOLID" | "SHEET METAL" | "PLASTIC", "ASSEMBLE") => (ASSEMBLE, 3, tab),
("SHEET METAL", "CREATE") => (SHEET_CREATE, 3, tab),
("SHEET METAL", "MODIFY") => (SHEET_MODIFY, 2, tab),
("PLASTIC", "SETUP") => (PLASTIC_SETUP, 2, tab),
("PLASTIC", "CREATE") => (PLASTIC_CREATE, 5, tab),
// The solid tab's panels.
("PLASTIC", "MODIFY") => (&[], 4, "SOLID"),
("SHEET METAL" | "PLASTIC", "CONSTRUCT" | "INSPECT" | "INSERT") => (&[], 2, "SOLID"),
_ => return None,
};
let mut out: Vec<&CommandSpec> = ids.iter().filter_map(|id| specs.iter().find(|c| c.id == *id).copied()).collect();
for c in specs.iter().filter(|c| (c.tab == tab || c.tab == own_tab) && c.panel == panel) {
if !out.iter().any(|o| o.id == c.id) {
out.push(*c);
}
}
Some((out, promoted))
}
/// Timeline and browser icon of the sheet metal and plastic features (`None`: not one of them).
pub fn feature_icon(k: &FeatureKind) -> Option<&'static str> {
Some(match k {
FeatureKind::SheetBase { .. } | FeatureKind::SheetContour { .. } | FeatureKind::SheetFlange { .. } => "flange",
FeatureKind::SheetHem { .. } => "hem",
FeatureKind::SheetFold { .. } => "fold",
FeatureKind::SheetUnfold { refold: false, .. } => "unfold",
FeatureKind::SheetUnfold { .. } => "refold",
FeatureKind::SheetConvert { .. } => "convert_sheet",
FeatureKind::Boss { .. } => "boss",
FeatureKind::Lip { .. } => "lip",
FeatureKind::SnapFit { .. } => "snap",
FeatureKind::Rest { .. } => "rest",
FeatureKind::Rib { .. } => "rib",
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn ids(tab: &str, panel: &str) -> Vec<&'static str> {
let specs = solidcraft_engine::command_specs();
layout(tab, panel, &specs).map(|(v, _)| v.iter().map(|c| c.id).collect()).unwrap_or_default()
}
#[test]
fn configure_header_clicks_open_parameters_and_configurations_separately() {
use serde_json::json;
let mut h = crate::scenario::Harness::new();
let commands = ids("SOLID", "CONFIGURE");
assert_eq!(&commands[..2], &["parameters.change", "config.table"]);
assert!(commands.contains(&"config.configure"), "Configure remains available in the dropdown");
h.step(&json!({"widget":"Parameters"})).unwrap();
assert!(h.app.params.is_some(), "the ordinary Parameters button opens the editor");
assert!(h.app.dialog.is_none());
assert!(h.widget("User Parameters").is_some());
assert!(h.widget("Model Parameters").is_some());
// Exercise the editor's Add button, not a script-only parameter command.
let parameters = h.app.params.as_mut().unwrap();
parameters.new_name = "width".into();
parameters.new_expr = "25 mm".into();
h.frames(3);
h.step(&json!({"widget":"Add"})).unwrap();
let values = h.app.session.execute("parameters.list", &json!({})).unwrap();
assert!(values["parameters"].as_array().unwrap().iter().any(|p| p["name"] == "width" && p["value"] == 25.0));
h.step(&json!({"click":{"handle":"params:close"}})).unwrap();
assert!(h.app.params.is_none());
h.step(&json!({"widget":"Configurations"})).unwrap();
assert!(h.app.params.is_none());
assert!(matches!(
h.app.dialog.as_ref().map(|d| &d.kind),
Some(crate::dialogs::Kind::Motion(crate::dialogs_motion::Mo::Configurations { .. }))
));
assert!(h.widget("Add Column").is_some());
assert!(h.widget("Add Row").is_some());
}
/// Fusion's layout: joints on every tab's ASSEMBLE panel, sheet metal and plastic tabs
/// reach the shared solid commands, the plastic rules sit on SETUP.
#[test]
fn tabs_share_commands_in_fusion_order() {
for tab in ["SOLID", "SHEET METAL", "PLASTIC"] {
let a = ids(tab, "ASSEMBLE");
assert_eq!(a.get(1).copied(), Some("joint.create"), "{tab}: {a:?}");
assert!(a.contains(&"joint.drive") && a.contains(&"component.create"));
}
let c = ids("SHEET METAL", "CREATE");
assert_eq!(c.get(1).copied(), Some("sheet.flange"));
assert!(c.contains(&"solid.extrude") && c.contains(&"sheet.flat_pattern"));
assert!(ids("SHEET METAL", "MODIFY").contains(&"sheet.unfold"));
assert!(ids("PLASTIC", "SETUP").contains(&"plastic.manage_rules"));
let p = ids("PLASTIC", "CREATE");
assert!(p.contains(&"plastic.boss") && p.contains(&"solid.rib") && p.contains(&"plastic.lip"));
assert!(ids("PLASTIC", "MODIFY").contains(&"solid.shell"));
assert!(panels("PLASTIC").is_some_and(|p| p.first() == Some(&"SETUP")));
}
/// #1240: a saved part reaches an assembly from the ASSEMBLE panel (and the File menu) as a
/// linked component, and a STEP file picked there is inserted as Insert STEP would.
#[test]
fn insert_design_is_on_the_assemble_panel_and_inserts_a_saved_part() {
use serde_json::json;
for tab in ["SOLID", "SHEET METAL", "PLASTIC"] {
assert!(ids(tab, "ASSEMBLE").contains(&"component.insert_design"), "{tab}");
}
assert!(!ids("SOLID", "ASSEMBLE")[..3].contains(&"component.insert_design"), "Component, Joint and As-built Joint keep the big buttons");
let mut h = crate::scenario::Harness::new();
let part = h.dir.join("part1.solidcraft");
let mut s = solidcraft_engine::Session::default();
s.execute("solid.box", &json!({"length": 10, "width": 20, "height": 30})).unwrap();
s.execute("file.save", &json!({"path": part})).unwrap();
s.execute("file.export", &json!({"path": h.dir.join("part2.step")})).unwrap();
h.step(&json!({"choose_file": "part1.solidcraft"})).unwrap();
h.step(&json!({"start": "component.insert_design"})).unwrap();
h.frames(3);
let links = h.app.session.execute("component.links", &json!({})).unwrap();
assert_eq!(links["links"][0]["name"], "part1", "{links}");
assert_eq!(h.app.session.world_state().bodies.len(), 1);
h.step(&json!({"choose_file": "part2.step"})).unwrap();
h.step(&json!({"start": "component.insert_design"})).unwrap();
for _ in 0..600 {
if h.app.session.world_state().bodies.len() == 2 {
break;
}
h.frames(1);
}
assert_eq!(h.app.session.world_state().bodies.len(), 2, "the STEP file's body is inserted");
}
}