Align with snap points: from/to as {snap: vertex|edge_mid|face_center|circle_center, at}
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
3ee03ac597
commit
0daeb0c9fc
2 files changed
+61
-1
No files matched your search
@@ -34,7 +34,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
|||||||
CommandSpec::new("solid.align", "Align", align)
|
CommandSpec::new("solid.align", "Align", align)
|
||||||
.at("SOLID", "MODIFY")
|
.at("SOLID", "MODIFY")
|
||||||
.icon("move")
|
.icon("move")
|
||||||
.params("bodies: [names]; from: [x,y,z] | from_face: [x,y,z]; to: [x,y,z] | to_face: [x,y,z] (faces also turn to meet); flip?: bool"),
|
.params("bodies: [names]; from: [x,y,z] | {snap: vertex|edge_mid|face_center|circle_center, at: [x,y,z]} | from_face: [x,y,z]; to: (the same) | to_face: [x,y,z] (faces also turn to meet); flip?: bool"),
|
||||||
CommandSpec::new("solid.remove", "Remove", remove).at("SOLID", "MODIFY").icon("delete").params("bodies: [names] (removed from here on in the timeline)"),
|
CommandSpec::new("solid.remove", "Remove", remove).at("SOLID", "MODIFY").icon("delete").params("bodies: [names] (removed from here on in the timeline)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -202,6 +202,12 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let n = super::face::face_normal(s, fp).ok_or_else(|| bad(cmd, format!("`{fk}` must be on a planar face")))?;
|
let n = super::face::face_normal(s, fp).ok_or_else(|| bad(cmd, format!("`{fk}` must be on a planar face")))?;
|
||||||
return Ok((fp, Some(n)));
|
return Ok((fp, Some(n)));
|
||||||
}
|
}
|
||||||
|
// A snap point: {snap: vertex|edge_mid|face_center|circle_center, at: [x,y,z]}.
|
||||||
|
if let Some(o) = p.get(pk).filter(|v| v.is_object()) {
|
||||||
|
let at = o.get("at").and_then(vec3).ok_or_else(|| bad(cmd, format!("`{pk}.at` must be [x, y, z]")))?;
|
||||||
|
let kind = o.get("snap").and_then(Value::as_str).unwrap_or("vertex");
|
||||||
|
return snap_point(s, kind, at).map(|q| (q, None)).ok_or_else(|| bad(cmd, format!("no {kind} to snap to near `{pk}`")));
|
||||||
|
}
|
||||||
p.get(pk).and_then(vec3).map(|v| (v, None)).ok_or_else(|| bad(cmd, format!("give `{pk}` or `{fk}` as [x, y, z]")))
|
p.get(pk).and_then(vec3).map(|v| (v, None)).ok_or_else(|| bad(cmd, format!("give `{pk}` or `{fk}` as [x, y, z]")))
|
||||||
};
|
};
|
||||||
let (from, from_normal) = pick(s, "from", "from_face")?;
|
let (from, from_normal) = pick(s, "from", "from_face")?;
|
||||||
@@ -230,3 +236,38 @@ fn remove(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "features_more_tests.rs"]
|
#[path = "features_more_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
/// A snap point near `at` on the model: the nearest vertex, edge midpoint, planar face's centre
|
||||||
|
/// or circular edge's centre (the end of a cylindrical face).
|
||||||
|
pub(super) fn snap_point(s: &Session, kind: &str, at: Vec3) -> Option<Vec3> {
|
||||||
|
let st = s.world_state();
|
||||||
|
let near = |pts: Vec<Vec3>| pts.into_iter().min_by(|a, b| a.dist(at).total_cmp(&b.dist(at)));
|
||||||
|
match kind {
|
||||||
|
"vertex" => near(
|
||||||
|
st.bodies
|
||||||
|
.iter()
|
||||||
|
.flat_map(|b| b.mesh().edges.iter().flat_map(|e| [e.first().copied(), e.last().copied()]).flatten().collect::<Vec<_>>())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
"edge_mid" => {
|
||||||
|
let d = |e: &solvecraft_doc::kernel::EdgeInfo| e.points.windows(2).map(|w| at.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min);
|
||||||
|
st.bodies
|
||||||
|
.iter()
|
||||||
|
.filter_map(|b| b.body.edges((b.body.size() * 2e-3).max(1e-3)).ok())
|
||||||
|
.flatten()
|
||||||
|
.min_by(|a, b| d(a).total_cmp(&d(b)))
|
||||||
|
.map(|e| e.mid)
|
||||||
|
}
|
||||||
|
"face_center" => st.bodies.iter().find_map(|b| {
|
||||||
|
let i = solvecraft_doc::appearance::face_index_at(b, at)?;
|
||||||
|
b.body.faces((b.body.size() * 2e-3).max(1e-3)).ok()?.into_iter().find(|f| f.index == i).map(|f| f.centroid)
|
||||||
|
}),
|
||||||
|
"circle_center" => st.bodies.iter().find_map(|b| {
|
||||||
|
let c = solvecraft_doc::kernel::cylinder_face_at(&b.body, at)?;
|
||||||
|
let along = (at - c.axis_point).dot(c.axis);
|
||||||
|
let end = if (along - c.start).abs() <= (c.end - along).abs() { c.start } else { c.end };
|
||||||
|
Some(c.axis_point + c.axis * end)
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1789,3 +1789,22 @@ fn combine_keep_tools_and_new_component() {
|
|||||||
assert_eq!(s.doc.body_component("A", a.feature), comp);
|
assert_eq!(s.doc.body_component("A", a.feature), comp);
|
||||||
assert!(rel(solvecraft_kernel::measure(&a.body).unwrap().volume, 4000.0) < 1e-9);
|
assert!(rel(solvecraft_kernel::measure(&a.body).unwrap().volume, 4000.0) < 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Align with snap points: a box's vertex onto another's face centre, an edge midpoint onto a
|
||||||
|
/// hole's circle centre.
|
||||||
|
#[test]
|
||||||
|
fn align_with_snap_points() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "solid.box", json!({"length": 40, "width": 40, "height": 10, "body_name": "Base"}));
|
||||||
|
run(&mut s, "solid.box", json!({"corner": [100, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Cube"}));
|
||||||
|
// Cube's corner (100, 0, 0) onto Base's top face centre (20, 20, 10).
|
||||||
|
run(
|
||||||
|
&mut s,
|
||||||
|
"solid.align",
|
||||||
|
json!({"bodies": ["Cube"], "from": {"snap": "vertex", "at": [100.4, 0.3, 0.2]}, "to": {"snap": "face_center", "at": [5, 5, 10]}}),
|
||||||
|
);
|
||||||
|
let m = run(&mut s, "inspect.measure", json!({"bodies": ["Cube"]}));
|
||||||
|
let min = |i: usize| m["bodies"][0]["bbox"]["min"][i].as_f64().unwrap_or(f64::NAN);
|
||||||
|
assert!((min(0) - 20.0).abs() < 1e-6 && (min(1) - 20.0).abs() < 1e-6 && (min(2) - 10.0).abs() < 1e-6, "{m}");
|
||||||
|
assert!(s.execute("solid.align", &json!({"bodies": ["Cube"], "from": {"snap": "nope", "at": [0, 0, 0]}, "to": [0, 0, 0]})).is_err());
|
||||||
|
}
|
||||||
Reference in new issue
Block a user