Hole To Object (drill to the face the axis meets there, or a point's depth; kept by persistent face name, copies in patterns move it too) and holes at every point of a sketch (standalone points and circle centres) when only the sketch is given
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
8e36d5d2fe
commit
214dcf7524
5 files changed
+93
-11
No files matched your search
@@ -285,6 +285,9 @@ pub enum FeatureKind {
|
|||||||
/// A cosmetic thread in the holes (e.g. "M6x1").
|
/// A cosmetic thread in the holes (e.g. "M6x1").
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
thread: Option<String>,
|
thread: Option<String>,
|
||||||
|
/// To Object: drill down to the face (or point) here instead of `depth`.
|
||||||
|
#[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
|
/// 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".
|
/// annotation). `designation` is an ISO metric size such as "M8" or "M8x1".
|
||||||
@@ -1013,7 +1016,8 @@ impl FeatureKind {
|
|||||||
| FeatureKind::Draft { faces, .. }
|
| FeatureKind::Draft { faces, .. }
|
||||||
| FeatureKind::OffsetFace { faces, .. }
|
| FeatureKind::OffsetFace { faces, .. }
|
||||||
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
|
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
|
||||||
FeatureKind::Hole { position, points: None, .. } => vec![position],
|
FeatureKind::Hole { position, points: None, to, .. } => std::iter::once(position).chain(to.iter_mut()).collect(),
|
||||||
|
FeatureKind::Hole { to: Some(t), .. } => vec![t],
|
||||||
FeatureKind::Extrude { extent, .. } => extent.to.iter_mut().chain(extent.from.iter_mut()).collect(),
|
FeatureKind::Extrude { extent, .. } => extent.to.iter_mut().chain(extent.from.iter_mut()).collect(),
|
||||||
FeatureKind::Revolve { to: Some(p), .. } => vec![p],
|
FeatureKind::Revolve { to: Some(p), .. } => vec![p],
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
|
|||||||
+29
-4
@@ -1143,6 +1143,28 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
|||||||
Ok((spots.into_iter().map(|p| hole_on_surface(st, p, dir)).collect(), dir))
|
Ok((spots.into_iter().map(|p| hole_on_surface(st, p, dir)).collect(), dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A hole's depth: down to the To Object (where the hole's axis meets the face there, else the
|
||||||
|
/// point's depth along the axis), else as given.
|
||||||
|
fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth: &Option<String>) -> Result<Option<String>> {
|
||||||
|
let Some(t) = to else { return Ok(depth.clone()) };
|
||||||
|
let mut d = (*t - *at).dot(dir);
|
||||||
|
// The face at the To point, met along the axis.
|
||||||
|
for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) {
|
||||||
|
let Some(fi) = crate::appearance::face_index_at(b, *t) else { continue };
|
||||||
|
let m = b.mesh();
|
||||||
|
if let Some((h, tri)) = m.raycast(*at + dir * 1e-6, dir)
|
||||||
|
&& m.tri_face.get(tri).is_some_and(|x| *x as usize == fi)
|
||||||
|
{
|
||||||
|
d = h + 1e-6;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !(d > 1e-6) {
|
||||||
|
return Err(DocError::Invalid("the hole's To object is not ahead of the hole".into()));
|
||||||
|
}
|
||||||
|
Ok(Some(format!("{d} mm")))
|
||||||
|
}
|
||||||
|
|
||||||
/// A hole's spots as given (not yet moved onto the surface) and its direction.
|
/// A hole's spots as given (not yet moved onto the surface) and its direction.
|
||||||
fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||||
let FeatureKind::Hole { position, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
|
let FeatureKind::Hole { position, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
|
||||||
@@ -1481,14 +1503,15 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
|||||||
FeatureKind::Torus { center, major, minor, .. } => {
|
FeatureKind::Torus { center, major, minor, .. } => {
|
||||||
Ok(vec![kernel::torus(*center, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?])
|
Ok(vec![kernel::torus(*center, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?])
|
||||||
}
|
}
|
||||||
FeatureKind::Hole { diameter, depth, hole, .. } => {
|
FeatureKind::Hole { diameter, depth, hole, to, .. } => {
|
||||||
let (spots, dir) = hole_spots(st, f)?;
|
let (spots, dir) = hole_spots(st, f)?;
|
||||||
if spots.is_empty() || spots.len() > 10_000 {
|
if spots.is_empty() || spots.len() > 10_000 {
|
||||||
return Err(DocError::Invalid("a hole needs 1…10000 points".into()));
|
return Err(DocError::Invalid("a hole needs 1…10000 points".into()));
|
||||||
}
|
}
|
||||||
let mut tools = Vec::new();
|
let mut tools = Vec::new();
|
||||||
for at in spots {
|
for at in spots {
|
||||||
tools.extend(hole_tools(vals, st, &at, dir, diameter, depth, hole)?);
|
let depth = hole_depth_to(st, &at, dir, to, depth)?;
|
||||||
|
tools.extend(hole_tools(vals, st, &at, dir, diameter, &depth, hole)?);
|
||||||
}
|
}
|
||||||
Ok(tools)
|
Ok(tools)
|
||||||
}
|
}
|
||||||
@@ -1863,7 +1886,7 @@ fn replay(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut
|
|||||||
}
|
}
|
||||||
let (op, targets) = feature_op(src);
|
let (op, targets) = feature_op(src);
|
||||||
let mut copies = Vec::new();
|
let mut copies = Vec::new();
|
||||||
if let FeatureKind::Hole { diameter, depth, hole, .. } = &src.kind {
|
if let FeatureKind::Hole { diameter, depth, hole, to, .. } = &src.kind {
|
||||||
// A hole is drilled again at each copy of its spot (found on the surface there), so
|
// A hole is drilled again at each copy of its spot (found on the surface there), so
|
||||||
// a blind hole's depth counts from the face at the copy, not from the bottom of the
|
// a blind hole's depth counts from the face at the copy, not from the bottom of the
|
||||||
// original hole under the original spot.
|
// original hole under the original spot.
|
||||||
@@ -1872,7 +1895,9 @@ fn replay(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut
|
|||||||
let d = crate::apply_vector(m, dir).normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
let d = crate::apply_vector(m, dir).normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||||
for p in &spots {
|
for p in &spots {
|
||||||
let at = hole_on_surface(st, crate::apply_point(m, *p), d);
|
let at = hole_on_surface(st, crate::apply_point(m, *p), d);
|
||||||
copies.extend(hole_tools(vals, st, &at, d, diameter, depth, hole)?);
|
let to = to.map(|t| crate::apply_point(m, t));
|
||||||
|
let depth = hole_depth_to(st, &at, d, &to, depth)?;
|
||||||
|
copies.extend(hole_tools(vals, st, &at, d, diameter, &depth, hole)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
|||||||
.at("SOLID", "CREATE")
|
.at("SOLID", "CREATE")
|
||||||
.icon("hole")
|
.icon("hole")
|
||||||
.key("H")
|
.key("H")
|
||||||
.params("position: [x,y,z] on a face | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch); direction?: [x,y,z] (default: into the face); diameter | thread (a tapped hole: the tap drill, major − pitch) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\" (cosmetic)"),
|
.params("position: [x,y,z] on a face | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch) | sketch alone (every sketch point: standalone points and circle centres); direction?: [x,y,z] (default: into the face); diameter | thread (a tapped hole: the tap drill, major − pitch) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\" (cosmetic)"),
|
||||||
CommandSpec::new("solid.pipe", "Pipe", pipe)
|
CommandSpec::new("solid.pipe", "Pipe", pipe)
|
||||||
.at("SOLID", "CREATE")
|
.at("SOLID", "CREATE")
|
||||||
.icon("pipe")
|
.icon("pipe")
|
||||||
@@ -885,10 +885,12 @@ fn clearance_diameter(size: &str, fit: &str) -> Option<f64> {
|
|||||||
fn hole(s: &mut Session, p: &Value) -> Result<Value> {
|
fn hole(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let cmd = "solid.hole";
|
let cmd = "solid.hole";
|
||||||
// At sketch points: the holes go perpendicular to the sketch and follow its points.
|
// At sketch points: the holes go perpendicular to the sketch and follow its points.
|
||||||
let points = match (p.get("sketch"), p.get("points")) {
|
let points = match (p.get("sketch"), p.get("points"), p.get("position")) {
|
||||||
(Some(_), Some(_)) => {
|
(Some(_), Some(_), _) | (Some(_), None, None) => {
|
||||||
let sketch = feature_sketch(s, p, cmd)?;
|
let sketch = feature_sketch(s, p, cmd)?;
|
||||||
let ids = string_list(p, "points");
|
// No points named: every sketch point (standalone points and circle centres, not
|
||||||
|
// line or arc ends, not the origin).
|
||||||
|
let ids = if p.get("points").is_some() { string_list(p, "points") } else { sketch_hole_points(s, sketch) };
|
||||||
if ids.is_empty() || ids.len() > 10_000 {
|
if ids.is_empty() || ids.len() > 10_000 {
|
||||||
return Err(bad(cmd, "`points` must list 1…10000 sketch point ids"));
|
return Err(bad(cmd, "`points` must list 1…10000 sketch point ids"));
|
||||||
}
|
}
|
||||||
@@ -976,7 +978,28 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
},
|
},
|
||||||
other => return Err(bad(cmd, format!("unknown hole type `{other}`"))),
|
other => return Err(bad(cmd, format!("unknown hole type `{other}`"))),
|
||||||
};
|
};
|
||||||
add_feature(s, p, FeatureKind::Hole { position, direction, diameter, depth, hole: kind, points: points.map(|x| x.0), thread })
|
let to = match p.get("to") {
|
||||||
|
Some(v) => Some(vec3(v).ok_or_else(|| bad(cmd, "`to` must be a point [x, y, z] on the face to drill to"))?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
add_feature(s, p, FeatureKind::Hole { position, direction, diameter, depth, hole: kind, points: points.map(|x| x.0), thread, to })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The points of a sketch a hole drills at when only the sketch is given.
|
||||||
|
fn sketch_hole_points(s: &Session, sketch: u64) -> Vec<String> {
|
||||||
|
let st = s.model.state();
|
||||||
|
let Some(ss) = st.sketch(sketch) else { return Vec::new() };
|
||||||
|
let mut ends: Vec<usize> = Vec::new();
|
||||||
|
for c in &ss.sketch.curves {
|
||||||
|
match &c.kind {
|
||||||
|
solvecraft_sketch::CurveKind::Circle { .. } => {}
|
||||||
|
solvecraft_sketch::CurveKind::Arc { a, b, .. } | solvecraft_sketch::CurveKind::Line { a, b } => ends.extend([*a, *b]),
|
||||||
|
solvecraft_sketch::CurveKind::Ellipse { m, .. } => ends.push(*m),
|
||||||
|
solvecraft_sketch::CurveKind::Spline { pts, .. } => ends.extend(pts.iter().copied()),
|
||||||
|
solvecraft_sketch::CurveKind::Conic { a, b, apex, .. } => ends.extend([*a, *b, *apex]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss.sketch.points.iter().enumerate().filter(|(i, p)| p.id != "origin" && !ends.contains(i)).map(|(_, p)| p.id.clone()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn face_points(p: &Value, cmd: &str) -> Result<Vec<Vec3>> {
|
fn face_points(p: &Value, cmd: &str) -> Result<Vec<Vec3>> {
|
||||||
|
|||||||
@@ -1742,3 +1742,33 @@ fn shell_directions_and_tangent_chain() {
|
|||||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
|
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
|
||||||
assert!(s.execute("solid.shell", &json!({"faces": [[20, 15, 20]], "thickness": 2, "direction": "sideways"})).is_err());
|
assert!(s.execute("solid.shell", &json!({"faces": [[20, 15, 20]], "thickness": 2, "direction": "sideways"})).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hole To Object (a face it meets, or a point) and holes at every point of a sketch.
|
||||||
|
#[test]
|
||||||
|
fn hole_to_object_and_at_sketch_points() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
|
||||||
|
// A pocket from below: its ceiling is at z = 10.
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Pocket"}));
|
||||||
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [10, 10], "p1": [30, 20]}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
run(&mut s, "solid.extrude", json!({"sketch": "Pocket", "distance": 10, "operation": "cut"}));
|
||||||
|
let v0 = volume(&mut s);
|
||||||
|
run(&mut s, "solid.hole", json!({"position": [20, 15, 20], "diameter": 4, "to": [25, 15, 10]}));
|
||||||
|
assert!(rel(v0 - volume(&mut s), PI * 4.0 * 10.0) < 1e-3, "{}", v0 - volume(&mut s));
|
||||||
|
run(&mut s, "edit.undo", json!({}));
|
||||||
|
run(&mut s, "solid.hole", json!({"position": [5, 5, 20], "diameter": 4, "to": [5, 5, 12]}));
|
||||||
|
assert!(rel(v0 - volume(&mut s), PI * 4.0 * 8.0) < 1e-3, "{}", v0 - volume(&mut s));
|
||||||
|
// Every point of a sketch: three points (the rectangle's corners are not hole points).
|
||||||
|
let mut t = Session::default();
|
||||||
|
run(&mut t, "solid.box", json!({"length": 40, "width": 30, "height": 10}));
|
||||||
|
run(&mut t, "sketch.create", json!({"plane": {"face": [20, 15, 10]}, "name": "Spots"}));
|
||||||
|
for x in [10, 20, 30] {
|
||||||
|
run(&mut t, "sketch.point", json!({"point": [x, 15]}));
|
||||||
|
}
|
||||||
|
run(&mut t, "sketch.rectangle.two_point", json!({"p0": [2, 2], "p1": [6, 6]}));
|
||||||
|
run(&mut t, "sketch.finish", json!({}));
|
||||||
|
let w0 = volume(&mut t);
|
||||||
|
let r = run(&mut t, "solid.hole", json!({"sketch": "Spots", "diameter": 3}));
|
||||||
|
assert!(rel(w0 - volume(&mut t), 3.0 * PI * 2.25 * 10.0) < 1e-3, "{r} {}", w0 - volume(&mut t));
|
||||||
|
}
|
||||||
@@ -2470,7 +2470,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
|||||||
d.extra.insert("pull".into(), pt3(*pull));
|
d.extra.insert("pull".into(), pt3(*pull));
|
||||||
d
|
d
|
||||||
}
|
}
|
||||||
FeatureKind::Hole { position, direction, diameter, depth, hole, points, thread } => {
|
FeatureKind::Hole { position, direction, diameter, depth, hole, points, thread, .. } => {
|
||||||
let mut d = start("solid.hole")?;
|
let mut d = start("solid.hole")?;
|
||||||
let mut k = hole_defaults();
|
let mut k = hole_defaults();
|
||||||
if let Kind::Hole { diameter: dia, depth: dep, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = &mut k {
|
if let Kind::Hole { diameter: dia, depth: dep, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = &mut k {
|
||||||
|
|||||||
Reference in new issue
Block a user