M1: oracle recipe coverage (arcs, refs-based constraints/dimensions, extents), taper and through-all extrudes, curve splitting in profiles, region merging, angle dimensions by sector, smoother tangency solve, merged topology for revolves

Oracle: 13/29 parts match Fusion.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 18:19:28 +00:00
1 parent 0f3e7701a7
commit b85ccd62cf
17 files changed
+1067 -164

No files matched your search

+11
View File
@@ -101,6 +101,12 @@ pub struct Extent {
/// Start offset from the sketch plane (expression).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_offset: Option<String>,
/// Through all bodies in the direction(s) instead of `distance`.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub through_all: bool,
/// Taper angle (expression; positive grows the profile).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub taper: Option<String>,
}
/// Revolve axis.
@@ -113,6 +119,8 @@ pub enum AxisRef {
SketchAxis { axis: String },
/// World X / Y / Z axis through the origin.
World { axis: String },
/// Any line in the sketch plane, in world coordinates.
Line { origin: Vec3, dir: Vec3 },
}
/// Feature definitions.
@@ -260,6 +268,9 @@ impl FeatureKind {
if let Some(d) = &extent.start_offset {
v.push(d);
}
if let Some(d) = &extent.taper {
v.push(d);
}
}
FeatureKind::Revolve { angle, .. } => v.push(angle),
FeatureKind::Fillet { radius, .. } => v.push(radius),
+47 -3
View File
@@ -328,6 +328,31 @@ fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation
Ok(())
}
/// Distance that reaches through every target body from the sketch plane (either side).
fn through_all_distance(st: &ModelState, plane: &Plane, targets: &[String]) -> Result<f64> {
let mut far: f64 = 0.0;
let mut any = false;
for b in st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)) {
let bb = b.mesh().bounds();
if bb.is_empty() {
continue;
}
any = true;
for i in 0..8 {
let p = Vec3::new(
if i & 1 == 0 { bb.min.x } else { bb.max.x },
if i & 2 == 0 { bb.min.y } else { bb.max.y },
if i & 4 == 0 { bb.min.z } else { bb.max.z },
);
far = far.max(plane.height(p).abs());
}
}
if !any {
return Err(DocError::Invalid("through all needs a body to cut".into()));
}
Ok(far * 1.02 + 1.0)
}
/// Sample points inside a region (sketch coordinates).
fn region_samples(r: &Region2) -> Vec<Vec2> {
let mut out = vec![r.interior_point()];
@@ -428,6 +453,14 @@ fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
}
Ok((pl.to_local(Vec3::ZERO), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
}
AxisRef::Line { origin, dir } => {
let pl = &ss.plane;
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(*origin).abs() > 1e-6 {
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
}
Ok((pl.to_local(*origin), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
}
}
}
@@ -448,8 +481,19 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Extrude { sketch, profiles, extent, operation, targets } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
let regions = select_profiles(&ss, profiles)?;
let d = val(vals, &extent.distance, Kind::Length)?;
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let d = if extent.through_all { through_all_distance(st, &ss.plane, targets)? } else { val(vals, &extent.distance, Kind::Length)? };
if let Some(tp) = &extent.taper {
let taper = val(vals, tp, Kind::Angle)?;
if extent.distance2.is_some() || extent.start_offset.is_some() || extent.direction == crate::Direction::Symmetric {
return Err(DocError::Invalid("not supported yet: taper with two-sided, symmetric or offset extents".into()));
}
let sign = if extent.direction == crate::Direction::Negative { -1.0 } else { 1.0 };
let (len, sign) = if d < 0.0 { (-d, -sign) } else { (d, sign) };
let tools =
regions.iter().map(|r| kernel::extrude_tapered(&ss.plane, r, len, sign, taper)).collect::<std::result::Result<Vec<_>, _>>()?;
return apply_op(st, f, tools, *operation, targets);
}
let off = match &extent.start_offset {
Some(e) => val(vals, e, Kind::Length)?,
None => 0.0,
@@ -471,7 +515,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Revolve { sketch, profiles, axis, angle, operation, targets } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?.clone();
let regions = select_profiles(&ss, profiles)?;
let regions = solvecraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let (o, d) = revolve_axis(&ss, axis)?;
let ang = val(vals, angle, Kind::Angle)?;
let tools = kernel::revolve(&ss.plane, &regions, o, d, ang)?;
+16 -2
View File
@@ -31,7 +31,14 @@ fn plate_doc() -> (Document, u64) {
FeatureKind::Extrude {
sketch: s,
profiles: ProfileSel::All,
extent: Extent { distance: "20".into(), direction: Direction::Positive, distance2: None, start_offset: None },
extent: Extent {
distance: "20".into(),
direction: Direction::Positive,
distance2: None,
start_offset: None,
through_all: false,
taper: None,
},
operation: Operation::NewBody,
targets: vec![],
},
@@ -82,7 +89,14 @@ fn box_fillet_cut_timeline() {
FeatureKind::Extrude {
sketch: s2,
profiles: ProfileSel::Points { points: vec![Vec2::new(20.0, 15.0)] },
extent: Extent { distance: "20".into(), direction: Direction::Positive, distance2: None, start_offset: None },
extent: Extent {
distance: "20".into(),
direction: Direction::Positive,
distance2: None,
start_offset: None,
through_all: false,
taper: None,
},
operation: Operation::Cut,
targets: vec![],
},