Modelled threads (Thread with modeled: true): the ISO 68-1 profile cut along a helix, M3 to M20, on shafts and in bores; running out past a free end of the face, stopping square at a set length. The groove is a watertight screw-sweep mesh (kernel::sweep_helix_mesh) cut by kernel::faceted_boolean (BSP on the faceted body, tolerance ladder checked by A−B + A∩B = A); the polygon rebuild welds vertices across grid cells, splits pinched loops, finds T-junctions through a grid, and takes the group's plane for a loop a hair off flat
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
1f619e49cc
commit
141a5fea8a
11 files changed
+421
-34
No files matched your search
@@ -289,8 +289,9 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
to: Option<Vec3>,
|
||||
},
|
||||
/// A cosmetic thread on a cylindrical face (no geometry change; drawn and exported as an
|
||||
/// annotation). `designation` is an ISO metric size such as "M8" or "M8x1".
|
||||
/// A thread on a cylindrical face. Cosmetic by default (no geometry change; drawn and
|
||||
/// exported as an annotation); `modeled` cuts the real helical ISO profile into the face.
|
||||
/// `designation` is an ISO metric size such as "M8" or "M8x1".
|
||||
Thread {
|
||||
face: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -298,6 +299,8 @@ pub enum FeatureKind {
|
||||
/// Thread length from the face's end nearest `face`; `None` = full length.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
length: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
modeled: bool,
|
||||
},
|
||||
/// Ruled loft through profiles of several sketches, in order.
|
||||
Loft {
|
||||
@@ -1033,6 +1036,7 @@ impl FeatureKind {
|
||||
FeatureKind::Hole { to: Some(t), .. } => vec![t],
|
||||
FeatureKind::Extrude { extent, .. } => extent.to.iter_mut().chain(extent.from.iter_mut()).collect(),
|
||||
FeatureKind::Revolve { to: Some(p), .. } => vec![p],
|
||||
FeatureKind::Thread { face, .. } => vec![face],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
+60
-2
@@ -186,6 +186,49 @@ fn thread_on(st: &ModelState, feature: u64, p: Vec3, designation: Option<&str>,
|
||||
})
|
||||
}
|
||||
|
||||
/// The helical space between the teeth of a modelled ISO metric thread (ISO 68-1 basic
|
||||
/// profile, 60° flanks): cut from the shaft (external) or the hole wall (internal), running from
|
||||
/// `start` to `end` along the axis. `radius` is the face's own radius; `open` says which ends
|
||||
/// of the threaded length are free ends of the face.
|
||||
fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result<solvecraft_geom::Mesh> {
|
||||
let (p, d) = (t.pitch, t.major_diameter);
|
||||
let h = p * 3f64.sqrt() / 2.0;
|
||||
let slope = (std::f64::consts::PI / 6.0).tan();
|
||||
let len = t.end - t.start;
|
||||
if !(p > 1e-3 && len > 1e-6) {
|
||||
return Err(DocError::Invalid("thread too short".into()));
|
||||
}
|
||||
if len / p > 190.0 {
|
||||
return Err(DocError::Invalid(format!("a modelled thread is limited to 190 turns ({:.0} mm at this pitch); use a cosmetic one", 190.0 * p)));
|
||||
}
|
||||
// (radius, half width of the groove) at its two ends: a trapezoid in the axial plane.
|
||||
let (r0, w0, r1, w1) = if t.internal {
|
||||
// From inside the hole wall out to the major diameter, where the bolt's crest sits
|
||||
// (the root flat is P/8 wide).
|
||||
let r_in = radius.min(d / 2.0 - 5.0 * h / 8.0) - 0.1 * p;
|
||||
(r_in, (p / 8.0 + 2.0 * (d / 2.0 - r_in) * slope) / 2.0, d / 2.0, p / 16.0)
|
||||
} else {
|
||||
// From the root (5H/8 under the major diameter, flat P/4 wide) out past the surface.
|
||||
let r_root = d / 2.0 - 5.0 * h / 8.0;
|
||||
let r_out = radius.max(d / 2.0) + 0.05 * p;
|
||||
(r_root, p / 8.0, r_out, (p / 4.0 + 2.0 * (r_out - r_root) * slope) / 2.0)
|
||||
};
|
||||
if w0.max(w1) * 2.0 >= p * 0.98 {
|
||||
return Err(DocError::Invalid("the face is too far from the thread's diameter".into()));
|
||||
}
|
||||
let k = t.axis;
|
||||
let u = k.cross(if k.x.abs() < 0.9 { Vec3::new(1.0, 0.0, 0.0) } else { Vec3::new(0.0, 1.0, 0.0) });
|
||||
let u = u.normalized().ok_or_else(|| DocError::Invalid("thread axis".into()))?;
|
||||
// Past a free end of the face the groove runs out into the air; elsewhere it stops square
|
||||
// at the threaded length's end.
|
||||
let (s0, s1) = (if open.0 { t.start - p } else { t.start }, if open.1 { t.end + p } else { t.end });
|
||||
let base = t.axis_point + k * s0;
|
||||
let plane = Plane { origin: base, x: u, y: k };
|
||||
let section = [Vec2::new(r0, -w0), Vec2::new(r1, -w1), Vec2::new(r1, w1), Vec2::new(r0, w0)];
|
||||
// Sections 7.5° apart: the chords sag 0.2% of the radius.
|
||||
Ok(kernel::sweep_helix_mesh(&plane, §ion, base, k, p, (s1 - s0) / p, 48)?)
|
||||
}
|
||||
|
||||
/// Model after some prefix of the timeline.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ModelState {
|
||||
@@ -2272,13 +2315,28 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Thread { face, designation, length } => {
|
||||
FeatureKind::Thread { face, designation, length, modeled } => {
|
||||
let l = match length {
|
||||
Some(e) => Some(val(vals, e, Kind::Length)?),
|
||||
None => None,
|
||||
};
|
||||
let t = thread_on(st, f.id, *face, designation.as_deref(), l)?;
|
||||
st.threads.push(t);
|
||||
if !*modeled {
|
||||
st.threads.push(t);
|
||||
return Ok(());
|
||||
}
|
||||
let i = st.bodies.iter().position(|b| b.name == t.body).ok_or_else(|| DocError::Invalid("threaded body".into()))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let cyl = kernel::cylinder_face_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("no cylindrical face there to thread".into()))?;
|
||||
let tol = t.pitch * 1e-3;
|
||||
let groove = thread_groove(&t, cyl.radius, ((t.start - cyl.start).abs() < tol, (t.end - cyl.end).abs() < tol))?;
|
||||
// The B-rep boolean can't take helical walls in reasonable time: the body is cut
|
||||
// faceted (curved faces within a two-hundredth of the pitch).
|
||||
let nb = kernel::faceted_boolean(&mb.body, &groove, BoolOp::Cut, t.pitch * 0.005)?
|
||||
.ok_or_else(|| DocError::Invalid("the thread removed everything".into()))?;
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, nb, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Shell { faces, thickness, body, direction, tangent_chain } => {
|
||||
|
||||
Reference in new issue
Block a user