From f57a62deb7c8ee9c7f23fa6fec29dc93f2452caf Mon Sep 17 00:00:00 2001 From: Irakli Kourtis Date: Fri, 16 Oct 2026 03:00:56 -0400 Subject: [PATCH] Intersection edge nodes advance along the curve at fine display qualities (#1615) Thanks @jyviko. --- crates/engine/src/boolean_regression_tests.rs | 24 + crates/kernel/src/tess/ssi.rs | 93 +- .../front_triangle_hull_triangle.json | 836 ++++++++++++++++++ .../loft_join_bracket_hull_triangle.json | 305 +++++++ 4 files changed, 1243 insertions(+), 15 deletions(-) create mode 100644 examples/regressions/front_triangle_hull_triangle.json create mode 100644 examples/regressions/loft_join_bracket_hull_triangle.json diff --git a/crates/engine/src/boolean_regression_tests.rs b/crates/engine/src/boolean_regression_tests.rs index 9041841f..5f89fc15 100644 --- a/crates/engine/src/boolean_regression_tests.rs +++ b/crates/engine/src/boolean_regression_tests.rs @@ -288,3 +288,27 @@ fn cut_of_a_whole_face_beside_round_corners_after_a_chamfer() { let top = mesh.bounds().max.z; assert!((top - 15.0).abs() < 1e-6, "top at {top}"); } + +/// The display tessellation of loft bodies joined into a bike frame plans at every display +/// quality and every refinement step. The edge nodes on an intersection of two lofts once +/// stepped back along the curve (the edge's fit rang where truck's parameter changes speed, and +/// took a leader point off the curve past a vertex), so a face's boundary crossed itself and +/// the body fell back to the reference mesher, which drew a sheet across the down tube. +#[test] +fn joined_loft_frames_plan_their_display_at_every_quality() { + use solidcraft_doc::display_quality::{DisplayQuality, MAX_REFINE}; + for (script, name) in [ + (include_str!("../../../examples/regressions/loft_join_bracket_hull_triangle.json"), "DT2"), + (include_str!("../../../examples/regressions/front_triangle_hull_triangle.json"), "FrontTriangle"), + ] { + let s = replay(script); + let state = s.model.state(); + let body = &state.body(name).unwrap().body; + for q in DisplayQuality::ALL { + for k in 0..=MAX_REFINE { + let plan = body.display_plan_angular(q.refined_chord(body.size(), k), q.refined_angle(k)); + assert!(plan.is_ok(), "{name} at {} + {k}: {:?}", q.id(), plan.err()); + } + } + } +} diff --git a/crates/kernel/src/tess/ssi.rs b/crates/kernel/src/tess/ssi.rs index c69a36c5..ca32e948 100644 --- a/crates/kernel/src/tess/ssi.rs +++ b/crates/kernel/src/tess/ssi.rs @@ -5,8 +5,13 @@ //! each surface in turn from no starting guess: a nearest-point search per surface per point. //! Here the intersection is solved exactly at a set of leader parameters, and a cubic B-spline //! is fitted through those points together with both surfaces' parameters there (a smooth -//! leader with its curves on both surfaces). Each point is then refined from the B-spline by -//! Newton's method on the four unknowns (u1, v1, u2, v2): S1(u1, v1) = S2(u2, v2), held on the +//! leader with its curves on both surfaces). The B-spline runs over the chord length of the +//! solved points, never over the curve's own parameter: truck's parameter can change speed +//! abruptly (a polyline leader with short and long spans), and a cubic interpolated over it +//! rings there, stepping back along the curve (Lee, "Choosing nodes in parametric curve +//! interpolation", CAD 21, 1989). The curve's parameter reaches the chord length through the +//! piecewise-linear map between the seeds, which is monotone. Each point is then refined from +//! the B-spline by Newton's method on the four unknowns (u1, v1, u2, v2): S1(u1, v1) = S2(u2, v2), held on the //! plane through the B-spline point normal to the B-spline (Barnhill & Kersey, "A marching //! method for parametric surface/surface intersection", CAGD 7, 1990; Patrikalakis & Maekawa, //! "Shape Interrogation for CAD/CAM", 2002, ch. 5). Every point is independent once seeded. Where the surfaces are close to @@ -34,9 +39,11 @@ const MIN_SIN: f64 = 0.05; pub(crate) struct Intersection { surfaces: [EvalSurface; 2], domains: [Domain; 2], - /// The smooth leader over the curve's parameter: the intersection point (x, y, z) and both - /// surfaces' parameters (u1, v1, u2, v2), periodic directions unwrapped along it. + /// The smooth leader over the chord length of the seeds: the intersection point (x, y, z) + /// and both surfaces' parameters (u1, v1, u2, v2), periodic directions unwrapped along it. leader: CurveN<7>, + /// The curve's parameter at each seed and the chord length there, both increasing. + seeds: Vec<(f64, f64)>, /// Convergence tolerance (mm). eps: f64, } @@ -59,8 +66,14 @@ impl Intersection { // The leader lies within the curve's tolerance of both surfaces. let accept = (ic.tolerance() * 100.0).max(1e-6); let scale = (0..=SEEDS).map(|k| leader(t0 + (t1 - t0) * k as f64 / SEEDS as f64).0.len()).fold(1.0, f64::max); - let mut it = Intersection { surfaces, domains, leader: CurveN { degree: 1, knots: Vec::new(), ctrl: Vec::new() }, eps: scale * 1e-10 }; - let (mut params, mut points) = (Vec::with_capacity(SEEDS + 1), Vec::with_capacity(SEEDS + 1)); + let mut it = Intersection { + surfaces, + domains, + leader: CurveN { degree: 1, knots: Vec::new(), ctrl: Vec::new() }, + seeds: Vec::new(), + eps: scale * 1e-10, + }; + let (mut params, mut points): (Vec, Vec<[f64; 7]>) = (Vec::with_capacity(SEEDS + 1), Vec::with_capacity(SEEDS + 1)); let mut prev: Option<[f64; 4]> = None; for k in 0..=SEEDS { let t = t0 + (t1 - t0) * k as f64 / SEEDS as f64; @@ -77,25 +90,60 @@ impl Intersection { q[2 * i] = uv[0]; q[2 * i + 1] = uv[1]; } - // Exactly on the intersection where Newton converges (near-tangent seeds keep the - // leader point and its projections). - let (p, q) = d.normalized().and_then(|dir| it.newton(p, dir, q)).unwrap_or((p, q)); - params.push(t); + // The fit takes only points exactly on the intersection: a leader point Newton + // cannot place (near-tangent, or the solution leaves a surface's domain where a + // polyline leader overshoots a vertex) is off the curve and would bend the fit back + // along it. The ends stay either way, so the fit spans the whole curve. + let solved = d.normalized().and_then(|dir| it.newton(p, dir, q)); + prev = Some(solved.map_or(q, |(_, q)| q)); + let (p, q) = match solved { + Some(x) => x, + None if k == 0 || k == SEEDS => (p, q), + None => continue, + }; + // A seed that lands on the previous one adds no chord and stays out of the fit + // (whose parameters must increase). + let s = match it.seeds.last().zip(points.last()) { + Some((&(_, s), &[x, y, z, ..])) => { + let chord = p.dist(Vec3::new(x, y, z)); + if !(chord > it.eps) { + continue; + } + s + chord + } + None => 0.0, + }; + it.seeds.push((t, s)); + params.push(s); points.push([p.x, p.y, p.z, q[0], q[1], q[2], q[3]]); - prev = Some(q); } - it.leader = CurveN::interpolate(¶ms, &points, 3).ok()?; + let degree = points.len().checked_sub(1)?.min(3); + it.leader = CurveN::interpolate(¶ms, &points, degree).ok()?; Some(it) } /// The point at curve parameter `t` and both surfaces' parameters there, or `None` /// (near-tangent, or Newton failed). pub(crate) fn eval(&self, t: f64) -> Option<(Vec3, [f64; 4])> { - let (x, dx) = self.leader.eval(t)?; + let (x, dx) = self.leader.eval(self.chord_at(t)?)?; let dir = Vec3::new(dx[0], dx[1], dx[2]).normalized()?; self.newton(Vec3::new(x[0], x[1], x[2]), dir, [x[3], x[4], x[5], x[6]]) } + /// The chord length at curve parameter `t`: linear between the seeds, clamped to the ends. + fn chord_at(&self, t: f64) -> Option { + let (&(ta, sa), &(tb, sb)) = (self.seeds.first()?, self.seeds.last()?); + if !(t > ta) { + return Some(sa); + } + if !(t < tb) { + return Some(sb); + } + let k = self.seeds.partition_point(|&(tk, _)| tk <= t); + let (Some(&(t0, s0)), Some(&(t1, s1))) = (<[_]>::get(&self.seeds, k.checked_sub(1)?), <[_]>::get(&self.seeds, k)) else { return None }; + Some(s0 + (s1 - s0) * (t - t0) / (t1 - t0)) + } + /// The two surfaces, in the order of the parameters [`Intersection::eval`] gives. pub(crate) fn surfaces(&self) -> &[EvalSurface; 2] { &self.surfaces @@ -186,7 +234,7 @@ mod tests { /// Points on the intersection edges of a boolean (a cross hole: two cylinders) lie on both /// surfaces, on truck's curve (the parameterisation follows our smooth leader, so the same - /// parameter may sit at a slightly different place along it), and advance along the curve. + /// parameter may sit at a slightly different place along it), and advance along it. #[test] fn intersection_points_lie_on_both_surfaces_and_on_truck_s_curve() { let Ok(pin) = crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 6.0) else { panic!("pin") }; @@ -199,6 +247,21 @@ mod tests { let (t0, t1) = curve.range_tuple(); let dense: Vec = (0..=1000).map(|k| from_p3(curve.subs(t0 + (t1 - t0) * f64::from(k) / 1000.0))).collect(); let near = |p: Vec3| dense.windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::MAX, f64::min); + // Arc length along truck's curve to the point nearest `p`. + let along = |p: Vec3| { + let (mut run, mut best) = (0.0, (f64::MAX, 0.0)); + for w in dense.windows(2) { + let (a, b) = (w[0], w[1]); + let len = a.dist(b); + let f = if len > 0.0 { ((p - a).dot(b - a) / (len * len)).clamp(0.0, 1.0) } else { 0.0 }; + let d = p.dist(a + (b - a) * f); + if d < best.0 { + best = (d, run + f * len); + } + run += len; + } + best.1 + }; let mut last: Option = None; for k in 0..=50 { let t = t0 + (t1 - t0) * f64::from(k) / 50.0; @@ -208,7 +271,7 @@ mod tests { } assert!(near(p) < 1e-4, "t {t}: {p:?} is {} from truck's curve", near(p)); if let Some(q) = last { - assert!(p.dist(q) > 0.0, "t {t}: the points advance"); + assert!(along(p) > along(q), "t {t}: the points advance along the curve"); } last = Some(p); checked += 1; diff --git a/examples/regressions/front_triangle_hull_triangle.json b/examples/regressions/front_triangle_hull_triangle.json new file mode 100644 index 00000000..83bff9a3 --- /dev/null +++ b/examples/regressions/front_triangle_hull_triangle.json @@ -0,0 +1,836 @@ +{ + "description": "Bike front triangle (lofted tubes joined to a bottom-bracket cylinder): the display mesh drew a flat triangle inside the down tube bend at Fine/Ultra.", + "commands": [ + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 12, + 0.0, + 18 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.947, + 0.0, + -0.322 + ] + }, + "name": "S13" + } + }, + { + "command": "sketch.ellipse", + "params": { + "center": [ + 0, + 0 + ], + "major": [ + 19.0, + 0 + ], + "minor_radius": 18.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + -13.0, + 0.0, + 91.5 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.947, + 0.0, + -0.322 + ] + }, + "name": "S14" + } + }, + { + "command": "sketch.ellipse", + "params": { + "center": [ + 0, + 0 + ], + "major": [ + 27.0, + 0 + ], + "minor_radius": 24.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + -38, + 0.0, + 165 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.959, + 0.0, + -0.282 + ] + }, + "name": "S15" + } + }, + { + "command": "sketch.ellipse", + "params": { + "center": [ + 0, + 0 + ], + "major": [ + 24.0, + 0 + ], + "minor_radius": 22.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + -76.646, + 0.0, + 320.0 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.97, + 0.0, + -0.242 + ] + }, + "name": "S16" + } + }, + { + "command": "sketch.circle.center", + "params": { + "center": [ + 0, + 0 + ], + "radius": 20.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + -110.577, + 0.0, + 456.089 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.97, + 0.0, + -0.242 + ] + }, + "name": "S17" + } + }, + { + "command": "sketch.circle.center", + "params": { + "center": [ + 0, + 0 + ], + "radius": 19.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "solid.loft", + "params": { + "sections": [ + { + "sketch": "S13" + }, + { + "sketch": "S14" + }, + { + "sketch": "S15" + }, + { + "sketch": "S16" + }, + { + "sketch": "S17" + } + ], + "operation": "new", + "name": "SeatTube", + "body_name": "SeatTube" + } + }, + { + "command": "solid.cylinder", + "params": { + "diameter": 46, + "height": 73.0, + "base": [ + 0, + -36.5, + 0 + ], + "axis": "Y", + "operation": "new", + "name": "BBShell", + "body_name": "BBShell" + } + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 459.471, + 0.0, + 607.143 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + 0.349, + 0.0, + -0.937 + ] + }, + "name": "S10" + } + }, + { + "command": "sketch.ellipse", + "params": { + "center": [ + 0, + 0 + ], + "major": [ + 22.0, + 0 + ], + "minor_radius": 20.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 181.44, + 0.0, + 503.572 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + 0.349, + 0.0, + -0.937 + ] + }, + "name": "S11" + } + }, + { + "command": "sketch.ellipse", + "params": { + "center": [ + 0, + 0 + ], + "major": [ + 27.0, + 0 + ], + "minor_radius": 23.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + -96.592, + 0.0, + 400.0 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + 0.349, + 0.0, + -0.937 + ] + }, + "name": "S12" + } + }, + { + "command": "sketch.ellipse", + "params": { + "center": [ + 0, + 0 + ], + "major": [ + 17.0, + 0 + ], + "minor_radius": 16.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "solid.loft", + "params": { + "sections": [ + { + "sketch": "S10" + }, + { + "sketch": "S11" + }, + { + "sketch": "S12" + } + ], + "operation": "new", + "name": "TopTube", + "body_name": "TopTube" + } + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 501.231, + 0.0, + 519.592 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.903, + 0.0, + -0.431 + ] + }, + "name": "S1" + } + }, + { + "command": "sketch.circle.center", + "params": { + "center": [ + 0, + 0 + ], + "radius": 32.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 473.678, + 0.0, + 577.358 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.903, + 0.0, + -0.431 + ] + }, + "name": "S2" + } + }, + { + "command": "sketch.circle.center", + "params": { + "center": [ + 0, + 0 + ], + "radius": 28.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 447.417, + 0.0, + 632.416 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.903, + 0.0, + -0.431 + ] + }, + "name": "S3" + } + }, + { + "command": "sketch.circle.center", + "params": { + "center": [ + 0, + 0 + ], + "radius": 26.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "solid.loft", + "params": { + "sections": [ + { + "sketch": "S1" + }, + { + "sketch": "S2" + }, + { + "sketch": "S3" + } + ], + "operation": "new", + "name": "HeadTube", + "body_name": "HeadTube" + } + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 8, + 0.0, + 3 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.178, + 0.0, + 0.984 + ] + }, + "name": "S4" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -19.0, + -18.0 + ], + "p1": [ + 19.0, + 18.0 + ], + "radius": 10.799999999999999 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 80, + 0.0, + 16 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.211, + 0.0, + 0.977 + ] + }, + "name": "S5" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -30.0, + -26.0 + ], + "p1": [ + 30.0, + 26.0 + ], + "radius": 15.6 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 175, + 0.0, + 40 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.596, + 0.0, + 0.803 + ] + }, + "name": "S6" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -38.0, + -33.0 + ], + "p1": [ + 38.0, + 33.0 + ], + "radius": 19.8 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 314.248, + 0.0, + 271.657 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.857, + 0.0, + 0.515 + ] + }, + "name": "S7" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -37.0, + -33.0 + ], + "p1": [ + 37.0, + 33.0 + ], + "radius": 19.8 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 422.553, + 0.0, + 451.835 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.857, + 0.0, + 0.515 + ] + }, + "name": "S8" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -33.0, + -30.0 + ], + "p1": [ + 33.0, + 30.0 + ], + "radius": 18.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 484.441, + 0.0, + 554.793 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.857, + 0.0, + 0.515 + ] + }, + "name": "S9" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -23.0, + -22.0 + ], + "p1": [ + 23.0, + 22.0 + ], + "radius": 13.2 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "solid.loft", + "params": { + "sections": [ + { + "sketch": "S4" + }, + { + "sketch": "S5" + }, + { + "sketch": "S6" + }, + { + "sketch": "S7" + }, + { + "sketch": "S8" + }, + { + "sketch": "S9" + } + ], + "operation": "new", + "name": "DownTube", + "body_name": "DownTube" + } + }, + { + "command": "solid.combine", + "params": { + "target": "SeatTube", + "tools": [ + "BBShell" + ], + "operation": "join" + } + }, + { + "command": "solid.combine", + "params": { + "target": "SeatTube", + "tools": [ + "TopTube" + ], + "operation": "join" + } + }, + { + "command": "solid.combine", + "params": { + "target": "SeatTube", + "tools": [ + "HeadTube" + ], + "operation": "join" + } + }, + { + "command": "solid.combine", + "params": { + "target": "SeatTube", + "tools": [ + "DownTube" + ], + "operation": "join" + } + }, + { + "command": "body.rename", + "params": { + "body": "SeatTube", + "name": "FrontTriangle" + } + } + ] +} \ No newline at end of file diff --git a/examples/regressions/loft_join_bracket_hull_triangle.json b/examples/regressions/loft_join_bracket_hull_triangle.json new file mode 100644 index 00000000..640672c4 --- /dev/null +++ b/examples/regressions/loft_join_bracket_hull_triangle.json @@ -0,0 +1,305 @@ +{ + "description": "A lofted down tube (rounded-rectangle sections, widest 82 mm) joined to a 46 mm \u00d7 73 mm bottom-bracket cylinder: the display mesh of the tube's side face gained a flat triangle across the inside of its bend.", + "commands": [ + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + -4, + 0.0, + 2 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.164, + 0.0, + 0.986 + ] + }, + "name": "S4b" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -19.0, + -18.0 + ], + "p1": [ + 19.0, + 18.0 + ], + "radius": 10.799999999999999 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 80, + 0.0, + 16 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.205, + 0.0, + 0.979 + ] + }, + "name": "S5b" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -41.0, + -33.0 + ], + "p1": [ + 41.0, + 33.0 + ], + "radius": 19.8 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 175, + 0.0, + 40 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.596, + 0.0, + 0.803 + ] + }, + "name": "S6b" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -41.0, + -35.0 + ], + "p1": [ + 41.0, + 35.0 + ], + "radius": 21.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 314.248, + 0.0, + 271.657 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.857, + 0.0, + 0.515 + ] + }, + "name": "S7b" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -39.0, + -35.0 + ], + "p1": [ + 39.0, + 35.0 + ], + "radius": 21.0 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 422.553, + 0.0, + 451.835 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.857, + 0.0, + 0.515 + ] + }, + "name": "S8b" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -35.0, + -32.0 + ], + "p1": [ + 35.0, + 32.0 + ], + "radius": 19.2 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "sketch.create", + "params": { + "plane": { + "origin": [ + 484.441, + 0.0, + 554.793 + ], + "x_dir": [ + 0.0, + 1.0, + 0.0 + ], + "y_dir": [ + -0.857, + 0.0, + 0.515 + ] + }, + "name": "S9b" + } + }, + { + "command": "sketch.rectangle.rounded", + "params": { + "p0": [ + -23.0, + -22.0 + ], + "p1": [ + 23.0, + 22.0 + ], + "radius": 13.2 + } + }, + { + "command": "sketch.finish", + "params": {} + }, + { + "command": "solid.loft", + "params": { + "sections": [ + { + "sketch": "S4b" + }, + { + "sketch": "S5b" + }, + { + "sketch": "S6b" + }, + { + "sketch": "S7b" + }, + { + "sketch": "S8b" + }, + { + "sketch": "S9b" + } + ], + "operation": "new", + "name": "DT2", + "body_name": "DT2" + } + }, + { + "command": "solid.cylinder", + "params": { + "diameter": 46, + "height": 73.0, + "base": [ + 0, + -36.5, + 0 + ], + "axis": "Y", + "operation": "new", + "name": "BBShell", + "body_name": "BBShell" + } + }, + { + "command": "solid.combine", + "params": { + "target": "DT2", + "tools": [ + "BBShell" + ], + "operation": "join" + } + } + ] +} \ No newline at end of file