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 <[email protected]> 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() {
|
if !faces.is_empty() {
|
||||||
p["faces"] = json!(faces);
|
p["faces"] = json!(faces);
|
||||||
// Until faces have their own looks: their bodies.
|
// An engine without face looks: their bodies.
|
||||||
if bodies.is_empty() {
|
if bodies.is_empty() && solvecraft_engine::find_command("appearance.list").is_none() {
|
||||||
let mut owners: Vec<String> =
|
let mut owners: Vec<String> =
|
||||||
targets.iter().filter_map(|x| if let Sel::Face { body, .. } = x { Some(body.clone()) } else { None }).collect();
|
targets.iter().filter_map(|x| if let Sel::Face { body, .. } = x { Some(body.clone()) } else { None }).collect();
|
||||||
owners.dedup();
|
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).
|
/// The looks in this design (colour and how many items wear it): the engine's list when it has
|
||||||
fn in_design(app: &SolveApp) -> Vec<([u8; 3], usize)> {
|
/// 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();
|
let mut out: Vec<([u8; 3], usize)> = Vec::new();
|
||||||
for c in app.session.doc.appearances.values() {
|
for c in app.session.doc.appearances.values() {
|
||||||
match out.iter_mut().find(|(x, _)| x == c) {
|
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();
|
let items: Vec<(usize, &([u8; 3], usize))> = used.iter().enumerate().collect();
|
||||||
swatch_rows(ui, &items, |ui, (i, (c, n))| {
|
swatch_rows(ui, &items, |ui, (i, (c, n))| {
|
||||||
let look = Look { name: hex(*c), category: "In This Design".into(), color: *c, opacity: 1.0 };
|
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.color = *c;
|
||||||
k.look = None;
|
k.look = None;
|
||||||
}
|
}
|
||||||
@@ -382,7 +392,7 @@ mod tests {
|
|||||||
a.run(&id, p).unwrap();
|
a.run(&id, p).unwrap();
|
||||||
}
|
}
|
||||||
assert_eq!(a.session.doc.appearances.get("Body2"), Some(&[196, 40, 36]));
|
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.
|
/// 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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Asm {
|
pub enum Asm {
|
||||||
NewComponent { name: String, activate: bool },
|
NewComponent {
|
||||||
|
name: String,
|
||||||
|
activate: bool,
|
||||||
|
},
|
||||||
Joint(Box<JointForm>),
|
Joint(Box<JointForm>),
|
||||||
JointOrigin { name: String, hover: Hover },
|
JointOrigin {
|
||||||
|
name: String,
|
||||||
|
hover: Hover,
|
||||||
|
},
|
||||||
RigidGroup,
|
RigidGroup,
|
||||||
Drive { joint: usize, values: Vec<String> },
|
/// `focus`: the first value takes the keyboard (its text selected) on the next frame.
|
||||||
MotionLink { a: usize, b: usize, ia: usize, ib: usize, ratio: String, offset: String },
|
Drive {
|
||||||
Interference { result: Option<Value>, overlaps: Vec<Mesh>, of: Vec<Sel> },
|
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.
|
/// 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).
|
/// Editing this joint (its origins stay; type, alignment and limits change).
|
||||||
pub editing: Option<u64>,
|
pub editing: Option<u64>,
|
||||||
pub kind: usize,
|
pub kind: usize,
|
||||||
|
/// Flip B relative to the mate (faces apart instead of together).
|
||||||
pub flip: bool,
|
pub flip: bool,
|
||||||
pub angle: String,
|
pub angle: String,
|
||||||
pub offset: [String; 3],
|
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)]),
|
"RigidGroupCmd" => (Asm::RigidGroup, vec![SelInput::new("Components", BODIES, true)]),
|
||||||
"FusionMoveJointsCommand" => {
|
"FusionMoveJointsCommand" => {
|
||||||
let joint = movable(s).first().copied().unwrap_or(0);
|
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" => {
|
"FusionMotionRelationshipCommand" => {
|
||||||
let b = usize::from(movable(s).len() > 1);
|
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);
|
let mut f = JointForm::new(false);
|
||||||
f.editing = Some(id);
|
f.editing = Some(id);
|
||||||
f.kind = TYPES.iter().position(|t| JointKind::parse(t) == Some(j.kind)).unwrap_or(0);
|
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.angle = num(j.angle.to_degrees(), "deg");
|
||||||
f.offset[2] = num(j.offset, "mm");
|
f.offset[2] = num(j.offset, "mm");
|
||||||
f.fit_limits();
|
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).
|
/// The Drive Joints dialog on a joint (by id).
|
||||||
pub fn drive_joint(app: &SolveApp, id: u64) -> Option<Dialog> {
|
pub fn drive_joint(app: &SolveApp, id: u64) -> Option<Dialog> {
|
||||||
let joint = app.session.doc.assembly.joints.iter().position(|j| j.id == id)?;
|
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 ----
|
// ---- snaps ----
|
||||||
@@ -418,7 +441,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
|
|||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Asm::RigidGroup => {}
|
Asm::RigidGroup => {}
|
||||||
Asm::Drive { joint, values } => {
|
Asm::Drive { joint, values, focus } => {
|
||||||
let s = &app.session;
|
let s = &app.session;
|
||||||
let list = movable(s);
|
let list = movable(s);
|
||||||
if list.is_empty() {
|
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 unit_kind = if angle { ValueKind::Angle } else { ValueKind::Length };
|
||||||
let name = j.kind.dofs().get(i).copied().unwrap_or("value");
|
let name = j.kind.dofs().get(i).copied().unwrap_or("value");
|
||||||
row_label(ui, &capital(name));
|
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();
|
ui.end_row();
|
||||||
// A slider over the limits (or a turn, or ±100 mm).
|
// A slider over the limits (or a turn, or ±100 mm).
|
||||||
let (lo, hi) = match j.limits.get(i).copied().flatten() {
|
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> =
|
let limits: Vec<Value> =
|
||||||
f.limits.iter().take(jk.dofs().len()).map(|(on, lo, hi)| if *on { json!([lo, hi]) } else { Value::Null }).collect();
|
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 {
|
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() {
|
for (i, l) in limits.iter().enumerate() {
|
||||||
let p = match l.as_array() {
|
let p = match l.as_array() {
|
||||||
Some(a) => json!({"joint": id, "index": i, "min": a.first(), "max": a.get(1)}),
|
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 (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 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)
|
cmd("JointAssembleCmdNew", p)
|
||||||
}
|
}
|
||||||
Asm::JointOrigin { name, .. } => {
|
Asm::JointOrigin { name, .. } => {
|
||||||
@@ -662,7 +704,7 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
|
|||||||
}
|
}
|
||||||
cmd("RigidGroupCmd", json!({ "occurrences": occs }))
|
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")?;
|
let j = s.doc.assembly.joints.get(*joint).ok_or("no joint that moves yet")?;
|
||||||
cmd("FusionMoveJointsCommand", json!({"joint": j.id, "values": values}))
|
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_eq!(app.session.doc.assembly.joints.len(), 1, "the click applied the joint");
|
||||||
assert!(app.dialog.is_none());
|
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