Variable-radius fillets through middle points: radii at positions along the edge, straight or smooth between
kernel::fillet_variable_points(body, edge, start, [(position, radius)...], smooth): the radius law is a B-spline of the edge parameter (straight pieces, or C1 cubic pieces with Catmull-Rom slopes), and since every section's arc is affine in its radius the blend is exactly a rational B-spline (degree 2 across, the law's degree along) with B-spline contact curves on the faces. The largest radius along the edge decides whether the blend fits. Fillet type variable takes mid: [[position, radius expr], ...] and smooth; middle radii are parameters of the feature (Mid radius) and follow user parameters. Tests: straight pieces against (1 - pi/4) L (a^2 + ab + b^2) / 3 per piece, smooth against the integrated law, either end as start, refusals; engine test with a parameter-driven middle radius. Refs #9 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
1 parent
a7fe0429f0
commit
be18bd6573
8 files changed
+192
-19
No files matched your search
@@ -773,8 +773,16 @@ pub enum FilletStyle {
|
||||
Constant,
|
||||
/// `radius` is the chord: the width across the blend.
|
||||
Chord,
|
||||
/// `radius` at the end of each edge nearest `start`, `radius2` at the other, linear between.
|
||||
Variable { radius2: String, start: Vec3 },
|
||||
/// `radius` at the end of each edge nearest `start`, `radius2` at the other; `mid` radii at
|
||||
/// positions between (0 to 1 from the start end); straight between them, or `smooth`.
|
||||
Variable {
|
||||
radius2: String,
|
||||
start: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
mid: Vec<(f64, String)>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
smooth: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl FilletStyle {
|
||||
@@ -1148,8 +1156,9 @@ impl FeatureKind {
|
||||
}
|
||||
FeatureKind::Fillet { radius, style, .. } => {
|
||||
v.push(radius);
|
||||
if let FilletStyle::Variable { radius2, .. } = style {
|
||||
if let FilletStyle::Variable { radius2, mid, .. } = style {
|
||||
v.push(radius2);
|
||||
v.extend(mid.iter().map(|(_, r)| r.as_str()));
|
||||
}
|
||||
}
|
||||
FeatureKind::Chamfer { distance, distance2, angle, .. } => {
|
||||
|
||||
@@ -2620,11 +2620,15 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
};
|
||||
let nb = match &f.kind {
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Chord, .. } => kernel::fillet_chord(&mb.body, &edges, r)?,
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Variable { radius2, start }, .. } => {
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Variable { radius2, start, mid, smooth }, .. } => {
|
||||
let r2 = val(vals, radius2, Kind::Length)?;
|
||||
let mut pts = vec![(0.0, r), (1.0, r2)];
|
||||
for (t, e) in mid {
|
||||
pts.push((*t, val(vals, e, Kind::Length)?));
|
||||
}
|
||||
let mut b = mb.body.clone();
|
||||
for p in &edges {
|
||||
b = kernel::fillet_variable(&b, *p, *start, r, r2)?;
|
||||
b = kernel::fillet_variable_points(&b, *p, *start, &pts, *smooth)?;
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
@@ -75,9 +75,10 @@ impl FeatureKind {
|
||||
FeatureKind::Fillet { radius, style, .. } => match style {
|
||||
crate::FilletStyle::Constant => v.push(("Radius", radius, L)),
|
||||
crate::FilletStyle::Chord => v.push(("Chord length", radius, L)),
|
||||
crate::FilletStyle::Variable { radius2, .. } => {
|
||||
crate::FilletStyle::Variable { radius2, mid, .. } => {
|
||||
v.push(("Start radius", radius, L));
|
||||
v.push(("End radius", radius2, L));
|
||||
v.extend(mid.iter_mut().map(|(_, r)| ("Mid radius", r, L)));
|
||||
}
|
||||
},
|
||||
FeatureKind::Chamfer { distance, distance2, angle, .. } => {
|
||||
|
||||
@@ -31,7 +31,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("fillet")
|
||||
.key("F")
|
||||
.params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other); radius2?; start?: [x,y,z]"),
|
||||
.params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other); radius2?; start?: [x,y,z]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii)"),
|
||||
CommandSpec::new("solid.chamfer", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}]; distance: expr; distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?"),
|
||||
CommandSpec::new("solid.combine", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool; new_component?: bool (the result goes into a new component; component_name?)"),
|
||||
CommandSpec::new("solid.pattern.rectangular", "Rectangular Pattern", pattern_rect)
|
||||
@@ -604,7 +604,24 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
check_expr(s, &radius2, Kind::Length, cmd, "radius2")?;
|
||||
let start =
|
||||
p.get("start").and_then(vec3).ok_or_else(|| bad(cmd, "a variable fillet needs `start`: [x,y,z] near the end that takes `radius`"))?;
|
||||
solvecraft_doc::FilletStyle::Variable { radius2, start }
|
||||
let mut mid = Vec::new();
|
||||
for m in p.get("mid").and_then(Value::as_array).into_iter().flatten().take(100) {
|
||||
let (Some(t), Some(r)) = (m.get(0).and_then(Value::as_f64), m.get(1)) else {
|
||||
return Err(bad(cmd, "`mid` entries are [position, radius]"));
|
||||
};
|
||||
if !(t.is_finite() && t > 0.0 && t < 1.0) {
|
||||
return Err(bad(cmd, "`mid` positions lie between 0 and 1 (from the start end)"));
|
||||
}
|
||||
let r = match r {
|
||||
Value::String(x) => x.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
_ => return Err(bad(cmd, "a `mid` radius is a number or an expression")),
|
||||
};
|
||||
check_expr(s, &r, Kind::Length, cmd, "mid radius")?;
|
||||
mid.push((t, r));
|
||||
}
|
||||
let smooth = p.get("smooth").and_then(Value::as_bool).unwrap_or(false);
|
||||
solvecraft_doc::FilletStyle::Variable { radius2, start, mid, smooth }
|
||||
}
|
||||
Some(o) => return Err(bad(cmd, format!("unknown fillet type `{o}` (constant, chord or variable)"))),
|
||||
};
|
||||
|
||||
@@ -1069,6 +1069,29 @@ fn chord_and_variable_fillets() {
|
||||
let removed = (1.0 - PI / 4.0) * 20.0 * (4.0 + 10.0 + 25.0) / 3.0;
|
||||
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v}");
|
||||
assert!(s.execute("solid.fillet", &json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable"})).is_err());
|
||||
// A radius through a middle point (a user parameter), straight between: 2 → 4 → 2.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"}));
|
||||
run(&mut s, "parameters.add", json!({"name": "waist", "expression": "4 mm"}));
|
||||
run(
|
||||
&mut s,
|
||||
"solid.fillet",
|
||||
json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 2, "start": [10, 0, 30], "mid": [[0.5, "waist"]]}),
|
||||
);
|
||||
let vol = |s: &Session| solvecraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume;
|
||||
let removed = 2.0 * (1.0 - PI / 4.0) * 10.0 * (4.0 + 8.0 + 16.0) / 3.0;
|
||||
assert!(rel(vol(&s), 6000.0 - removed) < 1e-4, "{}", vol(&s));
|
||||
// The middle radius follows its parameter.
|
||||
run(&mut s, "parameters.change", json!({"name": "waist", "expression": "3 mm"}));
|
||||
let removed = 2.0 * (1.0 - PI / 4.0) * 10.0 * (4.0 + 6.0 + 9.0) / 3.0;
|
||||
assert!(rel(vol(&s), 6000.0 - removed) < 1e-4, "{}", vol(&s));
|
||||
assert!(
|
||||
s.execute(
|
||||
"solid.fillet",
|
||||
&json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 2, "start": [10, 0, 30], "mid": [[1.5, 3]]})
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
/// Components export as a STEP assembly: products, occurrences, placements.
|
||||
|
||||
@@ -56,11 +56,78 @@ fn edge_frame(faces: &[&mt::Face], edge: &mt::Edge) -> Result<EdgeFrame> {
|
||||
|
||||
/// Fillet one straight edge with radius `r0` at its end nearest `start` and `r1` at the other.
|
||||
pub fn fillet_variable(body: &Body, edge_at: Vec3, start: Vec3, r0: f64, r1: f64) -> Result<Body> {
|
||||
fillet_variable_points(body, edge_at, start, &[(0.0, r0), (1.0, r1)], false)
|
||||
}
|
||||
|
||||
/// The radius along the edge as a B-spline of the edge parameter (0 at the edge's front):
|
||||
/// degree, knots and control radii.
|
||||
struct RadiusLaw {
|
||||
deg: usize,
|
||||
knots: Vec<f64>,
|
||||
radii: Vec<f64>,
|
||||
}
|
||||
|
||||
impl RadiusLaw {
|
||||
/// Through (position, radius) points sorted by position from 0 to 1: straight between them,
|
||||
/// or `smooth` (cubic pieces with Catmull-Rom slopes, C¹ through every point).
|
||||
fn through(pts: &[(f64, f64)], smooth: bool) -> RadiusLaw {
|
||||
if !smooth || pts.len() < 3 {
|
||||
let mut knots = vec![0.0];
|
||||
knots.extend(pts.iter().map(|p| p.0));
|
||||
knots.push(1.0);
|
||||
return RadiusLaw { deg: 1, knots, radii: pts.iter().map(|p| p.1).collect() };
|
||||
}
|
||||
let n = pts.len();
|
||||
let at = |i: usize| pts.get(i).copied().unwrap_or((0.0, 0.0));
|
||||
let slope = |i: usize| {
|
||||
let (a, b) = (at(i.saturating_sub(1)), at((i + 1).min(n - 1)));
|
||||
if b.0 > a.0 { (b.1 - a.1) / (b.0 - a.0) } else { 0.0 }
|
||||
};
|
||||
let mut knots = vec![0.0; 4];
|
||||
let mut radii = vec![at(0).1];
|
||||
for i in 0..n - 1 {
|
||||
let ((t0, r0), (t1, r1)) = (at(i), at(i + 1));
|
||||
let h = t1 - t0;
|
||||
radii.extend([r0 + slope(i) * h / 3.0, r1 - slope(i + 1) * h / 3.0, r1]);
|
||||
knots.extend(if i + 2 == n { vec![1.0; 4] } else { vec![t1; 3] });
|
||||
}
|
||||
RadiusLaw { deg: 3, knots, radii }
|
||||
}
|
||||
|
||||
/// Greville abscissae: where each control radius sits along the edge.
|
||||
fn greville(&self) -> Vec<f64> {
|
||||
(0..self.radii.len()).map(|j| self.knots.iter().skip(j + 1).take(self.deg).sum::<f64>() / self.deg as f64).collect()
|
||||
}
|
||||
|
||||
/// The radius at `t` (sampled for checks).
|
||||
fn eval(&self, t: f64) -> f64 {
|
||||
let kv = mt::KnotVec::from(self.knots.clone());
|
||||
let b = kv.try_bspline_basis_functions(self.deg, t.clamp(0.0, 1.0)).unwrap_or_default();
|
||||
b.iter().zip(&self.radii).map(|(x, r)| x * r).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fillet one straight edge with a radius that varies along it: (position, radius) points with
|
||||
/// positions 0 (the end nearest `start`) to 1 (the other end) and any between, the radius
|
||||
/// straight between them or `smooth`.
|
||||
pub fn fillet_variable_points(body: &Body, edge_at: Vec3, start: Vec3, points: &[(f64, f64)], smooth: bool) -> Result<Body> {
|
||||
body.require_brep("fillet")?;
|
||||
for r in [r0, r1] {
|
||||
if !(r.is_finite() && r > 1e-6 && r < 1e6) {
|
||||
if points.len() < 2 || points.len() > 100 {
|
||||
return Err(KernelError::Invalid("a variable radius needs 2…100 points".into()));
|
||||
}
|
||||
for (t, r) in points {
|
||||
if !(r.is_finite() && *r > 1e-6 && *r < 1e6) {
|
||||
return Err(KernelError::Invalid("fillet radii must be positive".into()));
|
||||
}
|
||||
if !(t.is_finite() && (0.0..=1.0).contains(t)) {
|
||||
return Err(KernelError::Invalid("radius positions run from 0 to 1 along the edge".into()));
|
||||
}
|
||||
}
|
||||
let mut pts = points.to_vec();
|
||||
pts.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
pts.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-9);
|
||||
if pts.first().map(|p| p.0) != Some(0.0) || pts.last().map(|p| p.0) != Some(1.0) || pts.len() < 2 {
|
||||
return Err(KernelError::Invalid("give the radius at both ends of the edge".into()));
|
||||
}
|
||||
let edge = crate::surfaces::edge_near(body, edge_at).ok_or_else(|| KernelError::Invalid(format!("no edge at {edge_at:?}")))?;
|
||||
let size = body.size();
|
||||
@@ -78,8 +145,17 @@ pub fn fillet_variable(body: &Body, edge_at: Vec3, start: Vec3, r0: f64, r1: f64
|
||||
let faces: Vec<&mt::Face> = shell.face_iter().collect();
|
||||
let fr = edge_frame(&faces, &edge)?;
|
||||
let (v0, v1) = (edge.absolute_front().clone(), edge.absolute_back().clone());
|
||||
// r0 at the end nearest `start` (the edge keeps its own direction).
|
||||
let (ra, rb) = if vtx(&v1).dist(start) < vtx(&v0).dist(start) { (r1, r0) } else { (r0, r1) };
|
||||
// Positions from the end nearest `start` (the edge keeps its own direction).
|
||||
if vtx(&v1).dist(start) < vtx(&v0).dist(start) {
|
||||
pts = pts.iter().rev().map(|(t, r)| (1.0 - t, *r)).collect();
|
||||
}
|
||||
let law = RadiusLaw::through(&pts, smooth);
|
||||
let (ra, rb) = (law.eval(0.0), law.eval(1.0));
|
||||
// The largest radius along the edge decides whether the blend fits.
|
||||
let rmax = (0..=200).map(|k| law.eval(k as f64 / 200.0)).fold(0.0, f64::max);
|
||||
if (0..=200).any(|k| law.eval(k as f64 / 200.0) <= 1e-6) {
|
||||
return Err(KernelError::Invalid("the radius must stay positive along the edge".into()));
|
||||
}
|
||||
let (p0, p1) = (vtx(&v0), vtx(&v1));
|
||||
let d = (p1 - p0).normalized().ok_or_else(|| KernelError::Failed("zero-length edge".into()))?;
|
||||
let tol = (size * 1e-7).max(1e-9);
|
||||
@@ -114,7 +190,7 @@ pub fn fillet_variable(body: &Body, edge_at: Vec3, start: Vec3, r0: f64, r1: f64
|
||||
}
|
||||
let other = if e.front() == v { vtx(e.back()) } else { vtx(e.front()) };
|
||||
let along = (other - p).dot(t);
|
||||
if (other - p - t * along).len() > tol * 100.0 + 1e-7 || along <= back(r) + tol {
|
||||
if (other - p - t * along).len() > tol * 100.0 + 1e-7 || along <= back(r.max(rmax)) + tol {
|
||||
return Err(unsupported("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
}
|
||||
@@ -133,8 +209,18 @@ pub fn fillet_variable(body: &Body, edge_at: Vec3, start: Vec3, r0: f64, r1: f64
|
||||
};
|
||||
let c0 = builder::circle_arc(&a0, &b0, p3(arc_mid(p0, s0, ra)));
|
||||
let c1 = builder::circle_arc(&a1, &b1, p3(arc_mid(p1, s1, rb)));
|
||||
let la = builder::line(&a0, &a1);
|
||||
let lb = builder::line(&b0, &b1);
|
||||
// Contact curves: the edge moved along each face by the setback of the radius there.
|
||||
let g = law.greville();
|
||||
let contact = |t: Vec3, a: &mt::Vertex, b: &mt::Vertex| -> Result<mt::Edge> {
|
||||
if law.deg == 1 && law.radii.len() == 2 {
|
||||
return Ok(builder::line(a, b));
|
||||
}
|
||||
let ctrl: Vec<mt::Point3> = g.iter().zip(&law.radii).map(|(gj, rj)| p3(p0 + (p1 - p0) * *gj + t * back(*rj))).collect();
|
||||
let c = mt::BSplineCurve::try_new(mt::KnotVec::from(law.knots.clone()), ctrl).map_err(|e| KernelError::Failed(format!("fillet: {e}")))?;
|
||||
mt::Edge::try_new(a, b, mt::Curve::BSplineCurve(c)).map_err(|e| KernelError::Failed(format!("fillet: {e}")))
|
||||
};
|
||||
let la = contact(fr.t1, &a0, &a1)?;
|
||||
let lb = contact(fr.t2, &b0, &b1)?;
|
||||
let mut subst: HashMap<mt::EdgeID, mt::Edge> = HashMap::new();
|
||||
for (end, a, b) in [(end0, &a0, &b0), (end1, &a1, &b1)] {
|
||||
for (e, nv) in [(&end.e1, a), (&end.e2, b)] {
|
||||
@@ -166,8 +252,11 @@ pub fn fillet_variable(body: &Body, edge_at: Vec3, start: Vec3, r0: f64, r1: f64
|
||||
// weight sin(φ/2)) at each end, joined linearly.
|
||||
let w = (fr.phi / 2.0).sin();
|
||||
let h = |p: Vec3, wt: f64| mt::Vector4::new(p.x * wt, p.y * wt, p.z * wt, wt);
|
||||
let ctrl = vec![vec![h(pa0, 1.0), h(pa1, 1.0)], vec![h(p0, w), h(p1, w)], vec![h(pb0, 1.0), h(pb1, 1.0)]];
|
||||
let bsp = mt::BSplineSurface::new((mt::KnotVec::bezier_knot(2), mt::KnotVec::bezier_knot(1)), ctrl);
|
||||
let row = |t: Vec3, wt: f64, off: bool| -> Vec<mt::Vector4> {
|
||||
g.iter().zip(&law.radii).map(|(gj, rj)| h(p0 + (p1 - p0) * *gj + if off { t * back(*rj) } else { Vec3::ZERO }, wt)).collect()
|
||||
};
|
||||
let ctrl = vec![row(fr.t1, 1.0, true), row(fr.t1, w, false), row(fr.t2, 1.0, true)];
|
||||
let bsp = mt::BSplineSurface::new((mt::KnotVec::bezier_knot(2), mt::KnotVec::from(law.knots.clone())), ctrl);
|
||||
let mut surf = mt::Surface::NurbsSurface(mt::NurbsSurface::new(bsp));
|
||||
// Outward: away from the arc centre on a convex edge, toward it on a concave one.
|
||||
let (c, mid) = (p0 + fr.t1 * s0 + fr.n1 * (ra * fr.side), arc_mid(p0, s0, ra));
|
||||
@@ -260,6 +349,36 @@ mod tests {
|
||||
assert!(fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 2.0, 50.0).is_err());
|
||||
}
|
||||
|
||||
/// A radius through a middle point: straight pieces (exact: each piece removes
|
||||
/// (1 − π/4) L (a² + ab + b²) / 3), and smooth (the removed volume integrates the law).
|
||||
#[test]
|
||||
fn variable_radius_through_a_middle_point() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
|
||||
let (at, start) = (Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0));
|
||||
let k = 1.0 - std::f64::consts::FRAC_PI_4;
|
||||
let pts = [(0.0, 2.0), (0.5, 4.0), (1.0, 2.0)];
|
||||
let f = fillet_variable_points(&b, at, start, &pts, false).unwrap();
|
||||
let removed = 2.0 * k * 10.0 * (4.0 + 8.0 + 16.0) / 3.0;
|
||||
let v = measure(&f).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v} vs {}", 6000.0 - removed);
|
||||
assert_eq!(f.unmeshed_faces(), 0);
|
||||
let s = fillet_variable_points(&b, at, start, &pts, true).unwrap();
|
||||
let law = RadiusLaw::through(&pts, true);
|
||||
let n = 20000;
|
||||
let integral: f64 = (0..n).map(|i| law.eval((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64 * 20.0;
|
||||
assert!((law.eval(0.5) - 4.0).abs() < 1e-12 && (law.eval(0.0) - 2.0).abs() < 1e-12);
|
||||
let v = measure(&s).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - k * integral) < 1e-4, "{v} vs {}", 6000.0 - k * integral);
|
||||
// Unsorted, from the other end: the same.
|
||||
let r = fillet_variable_points(&b, at, Vec3::new(10.0, 20.0, 30.0), &[(1.0, 2.0), (0.0, 2.0), (0.5, 4.0)], true).unwrap();
|
||||
assert!(rel(measure(&r).unwrap().volume, v) < 1e-9);
|
||||
// A middle radius too big for the faces, missing ends, bad positions: refused.
|
||||
assert!(fillet_variable_points(&b, at, start, &[(0.0, 2.0), (0.5, 40.0), (1.0, 2.0)], false).is_err());
|
||||
assert!(fillet_variable_points(&b, at, start, &[(0.0, 2.0), (0.5, 4.0)], false).is_err());
|
||||
assert!(fillet_variable_points(&b, at, start, &[(0.0, 2.0), (f64::NAN, 4.0), (1.0, 2.0)], false).is_err());
|
||||
assert!(fillet_variable_points(&b, at, start, &[(0.0, 2.0), (1.0, -1.0)], false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chord_length_on_a_right_angle() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
|
||||
|
||||
@@ -50,7 +50,7 @@ mod topo;
|
||||
|
||||
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{fillet_chord, fillet_variable};
|
||||
pub use blend_variable::{fillet_chord, fillet_variable, fillet_variable_points};
|
||||
pub use body::{Body, EdgeInfo, FaceInfo, FacePaint, Paint};
|
||||
pub use build::{PathSeg, box_solid, cylinder, extrude, extrude_tapered, loft, revolve, sphere, sweep, torus};
|
||||
pub use delete_face::delete_faces;
|
||||
|
||||
@@ -2790,7 +2790,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
FilletStyle::Chord => {
|
||||
d.extra.insert("type".into(), json!("chord"));
|
||||
}
|
||||
FilletStyle::Variable { radius2, start } => {
|
||||
FilletStyle::Variable { radius2, start, .. } => {
|
||||
d.extra.insert("type".into(), json!("variable"));
|
||||
d.extra.insert("radius2".into(), json!(radius2));
|
||||
// The start was the first edge's last point: ends swapped.
|
||||
|
||||
Reference in new issue
Block a user