Joints mate face to face; Drive Joints takes the keyboard; Appearance on the engine's list
Joint dialog: picked origins mate as in Fusion, B's z opposite A's (two top faces put B upside down on A, not inside it; a pin goes into its hole); the dialog's Flip turns B the other way (sent as the command's flip, which turns B over from coinciding frames). Edit Joint shows the same. Found by solvecraft-sketchux's scenarios. Drive Joints: the first value box takes the keyboard with its text selected when the dialog opens, so typing 90 and Enter drives the joint (found by solvecraft-sketchux). Appearance: "In This Design" from appearance.list (in_design) when the engine has it; faces are sent as face targets once the engine knows them. Tests: face snaps mate face to face and Flip undoes it; typing into Drive Joints drives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
b2d611bafe
commit
4c3a5d9853
3 files changed
+130
-19
No files matched your search
@@ -131,8 +131,8 @@ fn apply_params(targets: &[Sel], color: [u8; 3], opacity: f32, look: Option<&str
|
||||
}
|
||||
if !faces.is_empty() {
|
||||
p["faces"] = json!(faces);
|
||||
// Until faces have their own looks: their bodies.
|
||||
if bodies.is_empty() {
|
||||
// An engine without face looks: their bodies.
|
||||
if bodies.is_empty() && solvecraft_engine::find_command("appearance.list").is_none() {
|
||||
let mut owners: Vec<String> =
|
||||
targets.iter().filter_map(|x| if let Sel::Face { body, .. } = x { Some(body.clone()) } else { None }).collect();
|
||||
owners.dedup();
|
||||
@@ -164,8 +164,18 @@ fn drop_target(app: &SolveApp, faces: bool) -> Vec<Sel> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The looks bodies have in this design (colour and how many bodies wear it).
|
||||
fn in_design(app: &SolveApp) -> Vec<([u8; 3], usize)> {
|
||||
/// The looks in this design (colour and how many items wear it): the engine's list when it has
|
||||
/// one, else the body colours.
|
||||
fn in_design(app: &mut SolveApp) -> Vec<([u8; 3], usize)> {
|
||||
if let Ok(v) = app.session.execute("appearance.list", &json!({}))
|
||||
&& let Some(list) = v.get("in_design").and_then(Value::as_array)
|
||||
{
|
||||
let count = |x: &Value, k: &str| x.get(k).and_then(Value::as_array).map_or(0, Vec::len);
|
||||
return list
|
||||
.iter()
|
||||
.filter_map(|x| Some((parse_color(x.get("color")?)?, count(x, "bodies") + count(x, "faces") + count(x, "components"))))
|
||||
.collect();
|
||||
}
|
||||
let mut out: Vec<([u8; 3], usize)> = Vec::new();
|
||||
for c in app.session.doc.appearances.values() {
|
||||
match out.iter_mut().find(|(x, _)| x == c) {
|
||||
@@ -270,7 +280,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel
|
||||
let items: Vec<(usize, &([u8; 3], usize))> = used.iter().enumerate().collect();
|
||||
swatch_rows(ui, &items, |ui, (i, (c, n))| {
|
||||
let look = Look { name: hex(*c), category: "In This Design".into(), color: *c, opacity: 1.0 };
|
||||
if swatch(ui, egui::Id::new(("ap_used", *i)), *c, 1.0, k.color == *c, &format!("{} · {n} bodies", hex(*c)), look).clicked() {
|
||||
if swatch(ui, egui::Id::new(("ap_used", *i)), *c, 1.0, k.color == *c, &format!("{} · {n} item(s)", hex(*c)), look).clicked() {
|
||||
k.color = *c;
|
||||
k.look = None;
|
||||
}
|
||||
@@ -382,7 +392,7 @@ mod tests {
|
||||
a.run(&id, p).unwrap();
|
||||
}
|
||||
assert_eq!(a.session.doc.appearances.get("Body2"), Some(&[196, 40, 36]));
|
||||
assert_eq!(in_design(&a), vec![([196, 40, 36], 2)]);
|
||||
assert_eq!(in_design(&mut a), vec![([196, 40, 36], 2)]);
|
||||
}
|
||||
|
||||
/// A swatch dropped on a face in face mode targets the face; otherwise its body.
|
||||
|
||||
@@ -30,13 +30,35 @@ const MOVES: [&str; 6] = ["JointAssembleCmdNew", "JointAsBuiltCmd", "FusionMoveJ
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Asm {
|
||||
NewComponent { name: String, activate: bool },
|
||||
NewComponent {
|
||||
name: String,
|
||||
activate: bool,
|
||||
},
|
||||
Joint(Box<JointForm>),
|
||||
JointOrigin { name: String, hover: Hover },
|
||||
JointOrigin {
|
||||
name: String,
|
||||
hover: Hover,
|
||||
},
|
||||
RigidGroup,
|
||||
Drive { joint: usize, values: Vec<String> },
|
||||
MotionLink { a: usize, b: usize, ia: usize, ib: usize, ratio: String, offset: String },
|
||||
Interference { result: Option<Value>, overlaps: Vec<Mesh>, of: Vec<Sel> },
|
||||
/// `focus`: the first value takes the keyboard (its text selected) on the next frame.
|
||||
Drive {
|
||||
joint: usize,
|
||||
values: Vec<String>,
|
||||
focus: bool,
|
||||
},
|
||||
MotionLink {
|
||||
a: usize,
|
||||
b: usize,
|
||||
ia: usize,
|
||||
ib: usize,
|
||||
ratio: String,
|
||||
offset: String,
|
||||
},
|
||||
Interference {
|
||||
result: Option<Value>,
|
||||
overlaps: Vec<Mesh>,
|
||||
of: Vec<Sel>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The snap under the cursor, remembered for the selection it was found for.
|
||||
@@ -49,6 +71,7 @@ pub struct JointForm {
|
||||
/// Editing this joint (its origins stay; type, alignment and limits change).
|
||||
pub editing: Option<u64>,
|
||||
pub kind: usize,
|
||||
/// Flip B relative to the mate (faces apart instead of together).
|
||||
pub flip: bool,
|
||||
pub angle: String,
|
||||
pub offset: [String; 3],
|
||||
@@ -130,7 +153,7 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
"RigidGroupCmd" => (Asm::RigidGroup, vec![SelInput::new("Components", BODIES, true)]),
|
||||
"FusionMoveJointsCommand" => {
|
||||
let joint = movable(s).first().copied().unwrap_or(0);
|
||||
(Asm::Drive { joint, values: current_values(s, joint) }, vec![])
|
||||
(Asm::Drive { joint, values: current_values(s, joint), focus: true }, vec![])
|
||||
}
|
||||
"FusionMotionRelationshipCommand" => {
|
||||
let b = usize::from(movable(s).len() > 1);
|
||||
@@ -173,7 +196,7 @@ pub fn edit_joint(app: &SolveApp, id: u64) -> Option<Dialog> {
|
||||
let mut f = JointForm::new(false);
|
||||
f.editing = Some(id);
|
||||
f.kind = TYPES.iter().position(|t| JointKind::parse(t) == Some(j.kind)).unwrap_or(0);
|
||||
f.flip = j.flip;
|
||||
f.flip = !j.flip;
|
||||
f.angle = num(j.angle.to_degrees(), "deg");
|
||||
f.offset[2] = num(j.offset, "mm");
|
||||
f.fit_limits();
|
||||
@@ -189,7 +212,7 @@ pub fn edit_joint(app: &SolveApp, id: u64) -> Option<Dialog> {
|
||||
/// The Drive Joints dialog on a joint (by id).
|
||||
pub fn drive_joint(app: &SolveApp, id: u64) -> Option<Dialog> {
|
||||
let joint = app.session.doc.assembly.joints.iter().position(|j| j.id == id)?;
|
||||
Some(Dialog::new(Kind::Assembly(Asm::Drive { joint, values: current_values(&app.session, joint) }), vec![]))
|
||||
Some(Dialog::new(Kind::Assembly(Asm::Drive { joint, values: current_values(&app.session, joint), focus: true }), vec![]))
|
||||
}
|
||||
|
||||
// ---- snaps ----
|
||||
@@ -418,7 +441,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
|
||||
ui.end_row();
|
||||
}
|
||||
Asm::RigidGroup => {}
|
||||
Asm::Drive { joint, values } => {
|
||||
Asm::Drive { joint, values, focus } => {
|
||||
let s = &app.session;
|
||||
let list = movable(s);
|
||||
if list.is_empty() {
|
||||
@@ -445,7 +468,23 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
|
||||
let unit_kind = if angle { ValueKind::Angle } else { ValueKind::Length };
|
||||
let name = j.kind.dofs().get(i).copied().unwrap_or("value");
|
||||
row_label(ui, &capital(name));
|
||||
enter |= field(ui, v);
|
||||
if i == 0 {
|
||||
// Typing replaces the value at once, as in the other dialogs' value boxes.
|
||||
let id = egui::Id::new("sc_drive_value");
|
||||
let r = ui.add(egui::TextEdit::singleline(v).id(id));
|
||||
crate::params_dialog::complete(ui, &r, v);
|
||||
if std::mem::take(focus) {
|
||||
r.request_focus();
|
||||
let n = v.chars().count();
|
||||
let mut st = egui::text_edit::TextEditState::load(ui.ctx(), id).unwrap_or_default();
|
||||
st.cursor
|
||||
.set_char_range(Some(egui::text_selection::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
|
||||
st.store(ui.ctx(), id);
|
||||
}
|
||||
enter |= r.lost_focus() && ui.input(|x| x.key_pressed(egui::Key::Enter));
|
||||
} else {
|
||||
enter |= field(ui, v);
|
||||
}
|
||||
ui.end_row();
|
||||
// A slider over the limits (or a turn, or ±100 mm).
|
||||
let (lo, hi) = match j.limits.get(i).copied().flatten() {
|
||||
@@ -618,7 +657,7 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
|
||||
let limits: Vec<Value> =
|
||||
f.limits.iter().take(jk.dofs().len()).map(|(on, lo, hi)| if *on { json!([lo, hi]) } else { Value::Null }).collect();
|
||||
if let Some(id) = f.editing {
|
||||
let mut out = cmd("joint.edit", json!({"joint": id, "type": kind, "flip": f.flip, "offset": f.offset[2], "angle": f.angle}));
|
||||
let mut out = cmd("joint.edit", json!({"joint": id, "type": kind, "flip": !f.flip, "offset": f.offset[2], "angle": f.angle}));
|
||||
for (i, l) in limits.iter().enumerate() {
|
||||
let p = match l.as_array() {
|
||||
Some(a) => json!({"joint": id, "index": i, "min": a.first(), "max": a.get(1)}),
|
||||
@@ -645,7 +684,10 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
|
||||
}
|
||||
let (x, y) = (eval(&f.offset[0], ValueKind::Length)?, eval(&f.offset[1], ValueKind::Length)?);
|
||||
let a_param = if x.abs() > 1e-12 || y.abs() > 1e-12 { shifted(&a, x, y) } else { a.param };
|
||||
let p = json!({"type": kind, "a": a_param, "b": b.param, "flip": f.flip, "offset": f.offset[2], "angle": f.angle, "limits": limits});
|
||||
// Picked origins mate as in Fusion: B's z opposite A's (faces meet face to face, a pin
|
||||
// goes into its hole); the dialog's Flip turns B the other way. The command's `flip`
|
||||
// turns B over from frames that coincide.
|
||||
let p = json!({"type": kind, "a": a_param, "b": b.param, "flip": !f.flip, "offset": f.offset[2], "angle": f.angle, "limits": limits});
|
||||
cmd("JointAssembleCmdNew", p)
|
||||
}
|
||||
Asm::JointOrigin { name, .. } => {
|
||||
@@ -662,7 +704,7 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
|
||||
}
|
||||
cmd("RigidGroupCmd", json!({ "occurrences": occs }))
|
||||
}
|
||||
Asm::Drive { joint, values } => {
|
||||
Asm::Drive { joint, values, .. } => {
|
||||
let j = s.doc.assembly.joints.get(*joint).ok_or("no joint that moves yet")?;
|
||||
cmd("FusionMoveJointsCommand", json!({"joint": j.id, "values": values}))
|
||||
}
|
||||
|
||||
@@ -203,3 +203,62 @@ fn ok_click_survives_a_busy_preview() {
|
||||
assert_eq!(app.session.doc.assembly.joints.len(), 1, "the click applied the joint");
|
||||
assert!(app.dialog.is_none());
|
||||
}
|
||||
|
||||
/// Two top faces mate face to face as in Fusion: B ends up upside down on A, not inside it;
|
||||
/// Flip turns it back over (into A).
|
||||
#[test]
|
||||
fn face_snaps_mate_face_to_face() {
|
||||
let mut app = two_boxes();
|
||||
app.start("JointAssembleCmdNew");
|
||||
let mut d = app.dialog.clone().unwrap();
|
||||
d.pick(&app.session, face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap());
|
||||
d.pick(&app.session, face_sel(&app.session, Vec3::new(40.0, 10.0, 10.0)).unwrap());
|
||||
let c = apply_commands(&app, &d).unwrap();
|
||||
assert_eq!(c[0].1["flip"], true, "the mate turns B over");
|
||||
for (id, p) in c {
|
||||
app.run(&id, p).unwrap();
|
||||
}
|
||||
let st = app.session.world_state();
|
||||
let b = st.body("Body2").unwrap().mesh().bounds();
|
||||
assert!((b.min.z - 10.0).abs() < 1e-6 && (b.max.z - 20.0).abs() < 1e-6, "{b:?}");
|
||||
// Edit Joint shows the mate unflipped; Flip puts B back into A's volume.
|
||||
let id = app.session.doc.assembly.joints[0].id;
|
||||
let mut e = edit_joint(&app, id).unwrap();
|
||||
if let Kind::Assembly(Asm::Joint(f)) = &mut e.kind {
|
||||
assert!(!f.flip);
|
||||
f.flip = true;
|
||||
}
|
||||
for (id, p) in apply_commands(&app, &e).unwrap() {
|
||||
app.run(&id, p).unwrap();
|
||||
}
|
||||
let b = app.session.world_state().body("Body2").unwrap().mesh().bounds();
|
||||
assert!((b.max.z - 10.0).abs() < 1e-6, "{b:?}");
|
||||
}
|
||||
|
||||
/// Drive Joints takes the keyboard: typing a value and Enter drives the joint.
|
||||
#[test]
|
||||
fn drive_value_takes_the_keyboard() {
|
||||
let mut app = two_boxes();
|
||||
let (a, b) = (occ(&app, "A"), occ(&app, "B"));
|
||||
app.run(
|
||||
"JointAssembleCmdNew",
|
||||
json!({"type": "revolute", "a": {"occurrence": a, "face": [10, 10, 10]}, "b": {"occurrence": b, "face": [40, 10, 0]}}),
|
||||
)
|
||||
.unwrap();
|
||||
app.start("FusionMoveJointsCommand");
|
||||
let ctx = egui::Context::default();
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
|
||||
let frame = |app: &mut SolveApp, events: Vec<egui::Event>| {
|
||||
let input = egui::RawInput { screen_rect: Some(screen), events, ..Default::default() };
|
||||
ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx()))
|
||||
};
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![egui::Event::Text("90".into())]);
|
||||
let key = |pressed| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed, repeat: false, modifiers: Default::default() };
|
||||
frame(&mut app, vec![key(true), key(false)]);
|
||||
frame(&mut app, vec![]);
|
||||
let j = &app.session.doc.assembly.joints[0];
|
||||
assert!((j.values[0] - 90f64.to_radians()).abs() < 1e-9, "{:?}", j.values);
|
||||
assert!(app.dialog.is_none());
|
||||
}
|
||||
Reference in new issue
Block a user