M1: hole feature (simple, drilled, counterbore, countersink), cone classification and apex counting
Oracle: 19/29 parts match Fusion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42e0362f5b
commit
431b87c2b9
12 files changed
+396
-41
No files matched your search
@@ -216,6 +216,18 @@ pub enum FeatureKind {
|
||||
features: Vec<String>,
|
||||
plane: PlaneRef,
|
||||
},
|
||||
/// A drilled hole (simple, counterbore or countersink) at a point, into the material.
|
||||
Hole {
|
||||
position: Vec3,
|
||||
/// Drilling direction (into the material).
|
||||
direction: Vec3,
|
||||
diameter: String,
|
||||
/// Depth to the shoulder; `None` = through all.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
depth: Option<String>,
|
||||
#[serde(default)]
|
||||
hole: HoleKind,
|
||||
},
|
||||
/// A construction plane (sketch placement, split tool).
|
||||
ConstructionPlane {
|
||||
plane: PlaneRef,
|
||||
@@ -235,6 +247,26 @@ pub enum FeatureKind {
|
||||
},
|
||||
}
|
||||
|
||||
/// Hole shapes.
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum HoleKind {
|
||||
/// Plain hole; blind holes get a drill point when `tip_angle` is set.
|
||||
#[default]
|
||||
Simple,
|
||||
Drilled {
|
||||
tip_angle: String,
|
||||
},
|
||||
Counterbore {
|
||||
cb_diameter: String,
|
||||
cb_depth: String,
|
||||
},
|
||||
Countersink {
|
||||
cs_diameter: String,
|
||||
cs_angle: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Pattern layouts.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
@@ -298,6 +330,7 @@ impl FeatureKind {
|
||||
FeatureKind::Pattern { .. } => "CircularPatternFeature",
|
||||
FeatureKind::Mirror { .. } => "MirrorFeature",
|
||||
FeatureKind::ConstructionPlane { .. } => "ConstructionPlane",
|
||||
FeatureKind::Hole { .. } => "HoleFeature",
|
||||
FeatureKind::Split { .. } => "SplitBodyFeature",
|
||||
FeatureKind::Move { .. } => "MoveFeature",
|
||||
}
|
||||
@@ -319,6 +352,7 @@ impl FeatureKind {
|
||||
FeatureKind::Pattern { .. } => "CircularPattern",
|
||||
FeatureKind::Mirror { .. } => "Mirror",
|
||||
FeatureKind::ConstructionPlane { .. } => "Plane",
|
||||
FeatureKind::Hole { .. } => "Hole",
|
||||
FeatureKind::Split { .. } => "Split",
|
||||
FeatureKind::Move { .. } => "Move",
|
||||
}
|
||||
@@ -359,6 +393,16 @@ impl FeatureKind {
|
||||
PatternKind::Circular { count, angle, .. } => v.extend([count.as_str(), angle]),
|
||||
},
|
||||
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
|
||||
FeatureKind::Hole { diameter, depth, hole, .. } => {
|
||||
v.push(diameter);
|
||||
v.extend(depth.iter().map(String::as_str));
|
||||
match hole {
|
||||
HoleKind::Simple => {}
|
||||
HoleKind::Drilled { tip_angle } => v.push(tip_angle),
|
||||
HoleKind::Counterbore { cb_diameter, cb_depth } => v.extend([cb_diameter.as_str(), cb_depth]),
|
||||
HoleKind::Countersink { cs_diameter, cs_angle } => v.extend([cs_diameter.as_str(), cs_angle]),
|
||||
}
|
||||
}
|
||||
FeatureKind::Move { translate, angle, .. } => {
|
||||
v.extend(translate.iter().map(String::as_str));
|
||||
if let Some(a) = angle {
|
||||
|
||||
+76
-1
@@ -10,7 +10,7 @@ use solvecraft_kernel::{self as kernel, Body, BoolOp};
|
||||
use solvecraft_sketch::{CurveKind, Profile, SolveReport, find_profiles, solve};
|
||||
|
||||
use crate::expr::{self, Kind, Value};
|
||||
use crate::{AxisRef, DocError, Document, Feature, FeatureKind, Operation, ProfileSel, Result};
|
||||
use crate::{AxisRef, DocError, Document, Feature, FeatureKind, HoleKind, Operation, ProfileSel, Result};
|
||||
|
||||
/// A body in the evaluated model.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -604,6 +604,76 @@ fn feature_tools(vals: &BTreeMap<String, Value>, f: &Feature, st: &ModelState) -
|
||||
FeatureKind::Torus { center, major, minor, .. } => {
|
||||
Ok(vec![kernel::torus(*center, val(vals, major, Kind::Length)?, val(vals, minor, Kind::Length)?)?])
|
||||
}
|
||||
FeatureKind::Hole { position, direction, diameter, depth, hole } => {
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let r = val(vals, diameter, Kind::Length)? / 2.0;
|
||||
if !(r > 1e-6) {
|
||||
return Err(DocError::Invalid("hole diameter must be positive".into()));
|
||||
}
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let top = (size * 0.01).max(0.1);
|
||||
let bottom = match depth {
|
||||
Some(d) => -val(vals, d, Kind::Length)?,
|
||||
None => -(size * 2.0 + 1.0),
|
||||
};
|
||||
if bottom >= 0.0 {
|
||||
return Err(DocError::Invalid("hole depth must be positive".into()));
|
||||
}
|
||||
// Tools are cylinders and cones made by (tapered) extrudes along the axis, cut one
|
||||
// after another, widest first.
|
||||
let plane = Plane::from_normal(*position, -dir).ok_or_else(|| DocError::Invalid("hole axis".into()))?;
|
||||
// Each tool's circle seam at its own angle, so seams don't line up between tools.
|
||||
let seam = std::cell::Cell::new(0.0);
|
||||
let disc = |rad: f64| {
|
||||
seam.set(seam.get() + 0.613);
|
||||
Region2 { outer: solvecraft_geom::Loop2::circle_from(Vec2::ZERO, rad, seam.get()), holes: vec![] }
|
||||
};
|
||||
let cyl = |rad: f64, lo: f64, hi: f64| -> Result<Body> {
|
||||
kernel::extrude(&plane, &[disc(rad)], lo, hi)?.pop().ok_or_else(|| DocError::Invalid("hole tool".into()))
|
||||
};
|
||||
let mut tools = Vec::new();
|
||||
match hole {
|
||||
HoleKind::Simple => tools.push(cyl(r, bottom, top)?),
|
||||
HoleKind::Drilled { tip_angle } => {
|
||||
let half = val(vals, tip_angle, Kind::Angle)? / 2.0;
|
||||
if depth.is_some() && half > 1e-3 && half < std::f64::consts::FRAC_PI_2 {
|
||||
// One revolved tool (cylinder with a drill point).
|
||||
let tip = r / half.tan();
|
||||
let up = -dir;
|
||||
let side = up.any_perp();
|
||||
let rp = Plane::new(*position, side, up).ok_or_else(|| DocError::Invalid("hole axis".into()))?;
|
||||
let pts = [Vec2::new(0.0, top), Vec2::new(r, top), Vec2::new(r, bottom), Vec2::new(0.0, bottom - tip)];
|
||||
let region = Region2 { outer: solvecraft_geom::Loop2::polygon(&pts), holes: vec![] };
|
||||
tools.extend(kernel::revolve(&rp, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU)?);
|
||||
} else {
|
||||
tools.push(cyl(r, bottom, top)?);
|
||||
}
|
||||
}
|
||||
HoleKind::Counterbore { cb_diameter, cb_depth } => {
|
||||
let (rc, dc) = (val(vals, cb_diameter, Kind::Length)? / 2.0, val(vals, cb_depth, Kind::Length)?);
|
||||
if !(rc > r && dc > 0.0 && -dc > bottom) {
|
||||
return Err(DocError::Invalid("the counterbore must be wider than the hole and shallower than it".into()));
|
||||
}
|
||||
// Wide and shallow first: the narrow hole then crosses the pocket floor.
|
||||
tools.push(cyl(rc, -dc, top)?);
|
||||
tools.push(cyl(r, bottom, top)?);
|
||||
}
|
||||
HoleKind::Countersink { cs_diameter, cs_angle } => {
|
||||
let (rc, half) = (val(vals, cs_diameter, Kind::Length)? / 2.0, val(vals, cs_angle, Kind::Angle)? / 2.0);
|
||||
if !(rc > r && half > 1e-3 && half < std::f64::consts::FRAC_PI_2) {
|
||||
return Err(DocError::Invalid("the countersink must be wider than the hole".into()));
|
||||
}
|
||||
// Cone from above the face to just inside the hole radius, then the hole.
|
||||
let r_top = rc + top * half.tan();
|
||||
let r_end = r * 0.9;
|
||||
let len = (r_top - r_end) / half.tan();
|
||||
let top_plane = plane.offset(top);
|
||||
tools.push(kernel::extrude_tapered(&top_plane, &disc(r_top), len, -1.0, -half)?);
|
||||
tools.push(cyl(r, bottom, top)?);
|
||||
}
|
||||
}
|
||||
Ok(tools)
|
||||
}
|
||||
_ => Err(DocError::Invalid(format!("{} cannot be patterned or mirrored yet", f.name))),
|
||||
}
|
||||
}
|
||||
@@ -616,6 +686,7 @@ fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
|
||||
| FeatureKind::Cylinder { operation, .. }
|
||||
| FeatureKind::Sphere { operation, .. }
|
||||
| FeatureKind::Torus { operation, .. } => (*operation, Vec::new()),
|
||||
FeatureKind::Hole { .. } => (Operation::Cut, Vec::new()),
|
||||
_ => (Operation::NewBody, Vec::new()),
|
||||
}
|
||||
}
|
||||
@@ -755,6 +826,10 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let tools = feature_tools(vals, f, st)?;
|
||||
apply_op(st, f, tools, *operation, targets)
|
||||
}
|
||||
FeatureKind::Hole { .. } => {
|
||||
let tools = feature_tools(vals, f, st)?;
|
||||
apply_op(st, f, tools, Operation::Cut, &[])
|
||||
}
|
||||
FeatureKind::ConstructionPlane { plane } => {
|
||||
doc.resolve_plane(vals, plane, 0)?;
|
||||
Ok(())
|
||||
|
||||
Reference in new issue
Block a user