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 <noreply@anthropic.com>
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

+19 -8
View File
@@ -13,7 +13,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SOLID", "CREATE")
.icon("extrude")
.key("E")
.params("distance: expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [[curve ids]] | [{point:[x,y]}]; direction?: positive|negative|symmetric; distance2?; start_offset?; operation?: new|join|cut|intersect; targets?: [body]; name?; body_name?"),
.params("distance: expr (or through_all: true); taper?: angle expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [[curve ids]] | [{point:[x,y]}]; direction?: positive|negative|symmetric; distance2?; start_offset?; operation?: new|join|cut|intersect; targets?: [body]; name?; body_name?"),
CommandSpec::new("Revolve", "Revolve", revolve)
.at("SOLID", "CREATE")
.icon("revolve")
@@ -143,8 +143,13 @@ fn add_feature(s: &mut Session, p: &Value, kind: FeatureKind) -> Result<Value> {
fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "Extrude";
let sketch = feature_sketch(s, p, cmd)?;
let distance = req_expr(cmd, p, "distance")?;
let through_all = bool_(p, "through_all").unwrap_or(false) || str_(p, "extent").is_some_and(|e| e.eq_ignore_ascii_case("through_all"));
let distance = if through_all { expr(p, "distance").unwrap_or_else(|| "0".into()) } else { req_expr(cmd, p, "distance")? };
check_expr(s, &distance, Kind::Length, cmd, "distance")?;
let taper = expr(p, "taper");
if let Some(t) = &taper {
check_expr(s, t, Kind::Angle, cmd, "taper")?;
}
let direction = match str_(p, "direction").map(str::to_ascii_lowercase).as_deref() {
None | Some("positive") | Some("one_side") => Direction::Positive,
Some("negative") | Some("reverse") | Some("flip") => Direction::Negative,
@@ -162,7 +167,7 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
let kind = FeatureKind::Extrude {
sketch,
profiles: profiles(p, cmd)?,
extent: Extent { distance, direction, distance2, start_offset },
extent: Extent { distance, direction, distance2, start_offset, through_all, taper },
operation: operation(p, cmd)?,
targets: string_list(p, "targets"),
};
@@ -172,11 +177,17 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "Revolve";
let sketch = feature_sketch(s, p, cmd)?;
let axis = match str_(p, "axis") {
Some(a @ ("X" | "Y" | "Z")) => AxisRef::World { axis: a.into() },
Some(a @ ("x" | "y")) => AxisRef::SketchAxis { axis: a.into() },
Some(id) => AxisRef::SketchLine { curve: id.into() },
None => return Err(bad(cmd, "`axis` must be a sketch line id, x, y (sketch axes) or X, Y, Z")),
let axis = if let Some(o) = p.get("axis").filter(|v| v.is_object()) {
let origin = o.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
let dir = o.get("dir").and_then(vec3).ok_or_else(|| bad(cmd, "axis needs `dir`"))?;
AxisRef::Line { origin, dir }
} else {
match str_(p, "axis") {
Some(a @ ("X" | "Y" | "Z")) => AxisRef::World { axis: a.into() },
Some(a @ ("x" | "y")) => AxisRef::SketchAxis { axis: a.into() },
Some(id) => AxisRef::SketchLine { curve: id.into() },
None => return Err(bad(cmd, "`axis` must be a sketch line id, x, y (sketch axes), X, Y, Z or {origin, dir}")),
}
};
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
+115 -15
View File
@@ -59,7 +59,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SKETCH", "CREATE")
.icon("arc_center")
.enabled(in_sketch)
.params("center, start, end (counter-clockwise from start)"),
.params("center, start, end (counter-clockwise from start) | center, start, sweep: signed degrees"),
CommandSpec::new("ShapePolygonInscribed", "Inscribed Polygon", polygon_inscribed)
.at("SKETCH", "CREATE")
.icon("polygon")
@@ -586,8 +586,12 @@ fn arc_three(s: &mut Session, p: &Value) -> Result<Value> {
// Counter-clockwise from start to end must pass the through point; otherwise swap.
let ang = |q: Vec2| (q - o).angle();
let ccw_span = |from: f64, to: f64| (to - from).rem_euclid(std::f64::consts::TAU);
let (s0, e0) = if ccw_span(ang(a.pos()), ang(t)) < ccw_span(ang(a.pos()), ang(b.pos())) { (a, b) } else { (b, a) };
let ccw = ccw_span(ang(a.pos()), ang(t)) < ccw_span(ang(a.pos()), ang(b.pos()));
let (s0, e0) = if ccw { (a, b) } else { (b, a) };
let ci = sk.add_arc(o, s0.pos(), e0.pos(), [None, s0.idx(), e0.idx()], id.as_deref())?;
if let Some(c) = sk.curves.get_mut(ci) {
c.reversed = !ccw;
}
Ok((ids_of(sk, &[ci]), Vec::new()))
})?;
Ok(result(out, info))
@@ -596,9 +600,36 @@ fn arc_three(s: &mut Session, p: &Value) -> Result<Value> {
fn arc_center(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "ArcCenterTwoPoint";
let id = str_(p, "id").map(str::to_string);
let sweep = num(p, "sweep");
let (out, info) = edit(s, p, cmd, false, |sk, _| {
let c = req_parg(sk, p, "center", cmd)?;
let a = req_parg(sk, p, "start", cmd)?;
if let Some(sw) = sweep {
// Start point and signed sweep (degrees); clockwise arcs keep `<id>.start` at the start.
if !(sw.abs() > 1e-9 && sw.abs() < 360.0) {
return Err(bad(cmd, "`sweep` must be between -360 and 360 degrees (not 0)"));
}
let end = c.pos() + Vec2::from_angle((a.pos() - c.pos()).angle() + sw.to_radians()) * a.pos().dist(c.pos());
let cid = match &id {
Some(i) => i.clone(),
None => sk.fresh("a"),
};
let ai = match a.idx() {
Some(i) => i,
None => sk.add_point(a.pos(), Some(&format!("{cid}.start")))?,
};
let bi = sk.add_point(end, Some(&format!("{cid}.end")))?;
let ci = if sw > 0.0 {
sk.add_arc(c.pos(), a.pos(), end, [c.idx(), Some(ai), Some(bi)], Some(&cid))?
} else {
let ci = sk.add_arc(c.pos(), end, a.pos(), [c.idx(), Some(bi), Some(ai)], Some(&cid))?;
if let Some(cu) = sk.curves.get_mut(ci) {
cu.reversed = true;
}
ci
};
return Ok((ids_of(sk, &[ci]), Vec::new()));
}
let b = req_parg(sk, p, "end", cmd)?;
let ci = sk.add_arc(c.pos(), a.pos(), b.pos(), [c.idx(), a.idx(), b.idx()], id.as_deref())?;
Ok((ids_of(sk, &[ci]), Vec::new()))
@@ -814,13 +845,43 @@ fn c_midpoint(s: &mut Session, p: &Value) -> Result<Value> {
}
fn c_symmetry(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "ConstraintSymmetry";
constrain(s, p, cmd, |sk| {
Ok(ConstraintKind::Symmetric {
p: point_ref(sk, p.get("a"), cmd, "a")?,
q: point_ref(sk, p.get("b"), cmd, "b")?,
l: curve_ref(sk, p.get("line"), cmd, "line")?,
})
})
let (ids, info) = edit(s, p, cmd, true, |sk, _| {
let l = curve_ref(sk, p.get("line"), cmd, "line")?;
let ra = p.get("a").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`a` must be a point or curve"))?;
let rb = p.get("b").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`b` must be a point or curve"))?;
let mut out = Vec::new();
match (sk.resolve_point(ra), sk.resolve_point(rb)) {
(Some(a), Some(b)) => out.push(add_c(sk, ConstraintKind::Symmetric { p: a, q: b, l })?),
_ => {
// Two curves: circles/arcs mirror their centres and keep equal radii; lines mirror
// their end points.
let ca = sk.curve_index(ra).ok_or_else(|| bad(cmd, format!("unknown entity `{ra}`")))?;
let cb = sk.curve_index(rb).ok_or_else(|| bad(cmd, format!("unknown entity `{rb}`")))?;
match (sk.curves.get(ca).map(|c| c.kind.clone()), sk.curves.get(cb).map(|c| c.kind.clone())) {
(Some(CurveKind::Line { a: a0, b: a1 }), Some(CurveKind::Line { a: b0, b: b1 })) => {
out.push(add_c(sk, ConstraintKind::Symmetric { p: a0, q: b0, l })?);
out.push(add_c(sk, ConstraintKind::Symmetric { p: a1, q: b1, l })?);
}
(Some(_), Some(_)) => {
let pa = round_center(sk, ca).ok_or_else(|| bad(cmd, "symmetry needs two lines or two circles/arcs"))?;
let pb = round_center(sk, cb).ok_or_else(|| bad(cmd, "symmetry needs two lines or two circles/arcs"))?;
out.push(add_c(sk, ConstraintKind::Symmetric { p: pa, q: pb, l })?);
out.push(add_c(sk, ConstraintKind::Equal { a: ca, b: cb })?);
}
_ => return Err(bad(cmd, "symmetry needs two points or two curves")),
}
}
}
Ok(out)
})?;
Ok(json!({"constraints": ids, "sketch": info}))
}
fn round_center(sk: &Sketch, c: usize) -> Option<usize> {
match sk.curves.get(c)?.kind {
CurveKind::Circle { c, .. } | CurveKind::Arc { c, .. } => Some(c),
CurveKind::Line { .. } => None,
}
}
fn c_fix(s: &mut Session, p: &Value) -> Result<Value> {
@@ -877,7 +938,14 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "`entities` must list one or two sketch entities"));
}
let ty = str_(p, "type").unwrap_or("auto").to_ascii_lowercase();
let ty = match (ty.as_str(), str_(p, "orientation").map(str::to_ascii_lowercase).as_deref()) {
("distance" | "auto", Some("horizontal")) => "horizontal".to_string(),
("distance" | "auto", Some("vertical")) => "vertical".to_string(),
("offset", _) => "distance".to_string(),
_ => ty,
};
let value = expr(p, "value");
let text_at = p.get("text_at").and_then(vec2);
let ((param, kind_name, current), info) = edit(s, p, cmd, true, |sk, doc| {
let ent = |r: &str| -> Result<(Option<usize>, Option<usize>)> {
if let Some(c) = sk.curve_index(r) {
@@ -937,13 +1005,18 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
let d = da.normalized().map(|d| d.cross(b0 - a0).abs()).unwrap_or(0.0);
(ConstraintKind::PointLineDistance { p: bq, l: a, value: d }, d)
} else {
let mut ang = da.cross(db).atan2(da.dot(db));
let (mut ka, mut kb) = (a, b);
if ang < 0.0 {
ang = -ang;
std::mem::swap(&mut ka, &mut kb);
match text_at {
Some(t) => angle_by_sector(a, b, (a0, a1), (b0, b1), t).ok_or_else(|| bad(cmd, "cannot place that angle"))?,
None => {
let mut ang = da.cross(db).atan2(da.dot(db));
let (mut ka, mut kb) = (a, b);
if ang < 0.0 {
ang = -ang;
std::mem::swap(&mut ka, &mut kb);
}
(ConstraintKind::Angle { a: ka, b: kb, value: ang, flip: false }, ang)
}
}
(ConstraintKind::Angle { a: ka, b: kb, value: ang }, ang)
}
}
_ => return Err(bad(cmd, "cannot dimension that combination of entities")),
@@ -967,6 +1040,33 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"param": param, "type": kind_name, "expression": v, "measured": current, "sketch": info}))
}
/// The angle dimension between two lines whose sector contains `t` (the text position): returns
/// the constraint and the current value of that sector angle.
fn angle_by_sector(a: usize, b: usize, (a0, a1): (Vec2, Vec2), (b0, b1): (Vec2, Vec2), t: Vec2) -> Option<(ConstraintKind, f64)> {
let (da, db) = ((a1 - a0).normalized()?, (b1 - b0).normalized()?);
let den = da.cross(db);
if den.abs() < 1e-12 {
return None;
}
let s = (b0 - a0).cross(db) / den;
let x = a0 + da * s;
let ta = (t - x).angle();
// The four rays from the intersection, with their line and direction sign.
let mut rays: Vec<(f64, usize, bool)> = vec![(da.angle(), a, true), ((-da).angle(), a, false), (db.angle(), b, true), ((-db).angle(), b, false)];
rays.sort_by(|p, q| p.0.total_cmp(&q.0));
let tau = std::f64::consts::TAU;
for k in 0..4 {
let (s0, l0, p0) = rays[k];
let (s1, l1, p1) = rays[(k + 1) % 4];
let span = (s1 - s0).rem_euclid(tau);
let off = (ta - s0).rem_euclid(tau);
if off <= span && l0 != l1 {
return Some((ConstraintKind::Angle { a: l0, b: l1, value: span, flip: p0 != p1 }, span));
}
}
None
}
fn round6(x: f64) -> f64 {
(x * 1e6).round() / 1e6
}