parts: a standard part's sketches are fully defined (#6376)

A standard part (a nut, washer, bolt, screw, pin or bearing) drew its sketches without
constraints, so each was left with free degrees: 3 for a hex, 8 to 12 for a revolved outline.
A check of a design that holds parts listed every one of them as not fully defined. The part
builder now fixes each sketch's points and curves before finishing it: the outline is the
part's size, and a resize builds it again the same way.


Claude-Session: https://claude.ai/code/session_01XhgPofrmFDeLvMhar6Wsqw

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NOWN-DEV
2026-10-25 01:55:34 +00:00
committed by GitHub
parent 75fb613e25
commit f4d937d8a2
+41
View File
@@ -242,6 +242,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Result<Value> {
fn hex(s: &mut Session, af: f64, z0: f64, z1: f64, op: &str) -> Result<()> {
run(s, "sketch.create", json!({"plane": "XY", "offset": format!("{z0} mm")}))?;
run(s, "sketch.polygon.circumscribed", json!({"center": [0, 0], "radius": af / 2.0, "sides": 6}))?;
fix_all(s)?;
run(s, "sketch.finish", json!({}))?;
let (dist, dir) = if z1 >= z0 { (z1 - z0, "positive") } else { (z0 - z1, "negative") };
run(s, "solid.extrude", json!({"distance": format!("{dist} mm"), "direction": dir, "operation": op}))?;
@@ -262,11 +263,27 @@ fn revolve(s: &mut Session, pts: &[(f64, f64)], op: &str) -> Result<()> {
run(s, "sketch.create", json!({"plane": "XZ"}))?;
let points: Vec<[f64; 2]> = pts.iter().map(|(r, z)| [*r, -*z]).collect();
run(s, "sketch.line", json!({"points": points, "closed": true}))?;
fix_all(s)?;
run(s, "sketch.finish", json!({}))?;
run(s, "solid.revolve", json!({"axis": "y", "operation": op}))?;
Ok(())
}
/// Fix every point and curve of the sketch being drawn: a standard part's outline is its size, so
/// its sketches are fully defined and a check of the design finds nothing left free in them.
fn fix_all(s: &mut Session) -> Result<()> {
let sketch = s.active_sketch.and_then(|id| s.doc.features.iter().find(|f| f.id == id)).and_then(|f| match &f.kind {
solidcraft_doc::FeatureKind::Sketch { sketch, .. } => Some(sketch),
_ => None,
});
let entities: Vec<String> =
sketch.map(|sk| sk.points.iter().map(|p| p.id.clone()).chain(sk.curves.iter().map(|c| c.id.clone())).collect()).unwrap_or_default();
if !entities.is_empty() {
run(s, "sketch.constraint.fix", json!({"entities": entities, "fixed": true}))?;
}
Ok(())
}
fn thread(s: &mut Session, d: f64, pitch: f64, at: Vec3) {
let _ = run(s, "solid.thread", json!({"face": at, "designation": format!("M{}x{}", d, pitch)}));
let _ = d;
@@ -631,6 +648,30 @@ mod tests {
assert!(s.model.state().threads.iter().any(|t| t.designation == "M8" && (t.pitch - 1.25).abs() < 1e-9));
}
#[test]
fn every_sketch_of_a_standard_part_is_fully_defined() {
for (family, size, resized) in [
("socket_head_cap_screw", "M6", "M8"),
("hex_bolt", "M6", "M8"),
("hex_nut", "M6", "M8"),
("washer", "M6", "M8"),
("set_screw", "M6", "M8"),
("dowel_pin", "6", "8"),
("bearing", "608", "6200"),
] {
let mut s = Session::default();
let part = run(&mut s, "parts.insert", json!({"family": family, "size": size}));
for step in ["inserted", "resized"] {
let st = s.model.state();
let free: Vec<_> =
st.sketches.iter().filter(|k| k.report.dof != 0 || !k.report.fully_constrained()).map(|k| (&k.name, k.report.dof)).collect();
assert!(!st.sketches.is_empty() && free.is_empty(), "{family} {step}: {free:?}");
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{family} {step}");
run(&mut s, "parts.resize", json!({"component": part["component"], "size": resized}));
}
}
}
#[test]
fn resizing_placed_parts_uses_local_geometry_and_millimetres_in_every_design_unit() {
for (family, initial, resized, prefix) in [