diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 55f27a0fe..e0d7eff9d 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -2,7 +2,7 @@ use acadrust::entities::Spline; use glam::Vec3; use truck_modeling::{ base::{BoundedCurve, ParametricCurve}, - builder, BSplineCurve, Curve, Edge, KnotVec, Point3, + builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire, }; use crate::command::EntityTransform; @@ -34,12 +34,7 @@ fn to_truck(spl: &Spline) -> TruckEntity { let (t0, t1) = bspline.range_tuple(); let p_start = bspline.subs(t0); let p_end = bspline.subs(t1); - let v_start = builder::vertex(p_start); - let v_end = builder::vertex(p_end); - let edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline)); - // Expose fit_points (preferred) or control_points as key_vertices so that - // Endpoint/Midpoint snap can reach spline construction points. let snap_source = if !spl.fit_points.is_empty() { &spl.fit_points } else { @@ -50,8 +45,32 @@ fn to_truck(spl: &Spline) -> TruckEntity { .map(|p| [p.x as f32, p.y as f32, p.z as f32]) .collect(); + let is_closed = spl.flags.closed || spl.flags.periodic; + let gap = { + let dx = (p_end.x - p_start.x) as f32; + let dy = (p_end.y - p_start.y) as f32; + let dz = (p_end.z - p_start.z) as f32; + (dx * dx + dy * dy + dz * dz).sqrt() + }; + + let object = if is_closed && gap > 1e-6 { + // The B-spline doesn't self-close — add an explicit closing segment. + let v_start = builder::vertex(p_start); + let v_end = builder::vertex(p_end); + let v_close = builder::vertex(p_start); + let main_edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline)); + let close_edge = builder::line(&v_end, &v_close); + let wire: Wire = [main_edge, close_edge].into_iter().collect(); + TruckObject::Contour(wire) + } else { + let v_start = builder::vertex(p_start); + let v_end = builder::vertex(p_end); + let edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline)); + TruckObject::Curve(edge) + }; + TruckEntity { - object: TruckObject::Curve(edge), + object, snap_pts: vec![], tangent_geoms: vec![], key_vertices, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1b8cd9344..c458d58ce 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -37,6 +37,10 @@ pub use wire_model::WireModel; use crate::command::EntityTransform; use acadrust::entities::{BoundaryEdge, BoundaryPath, Hatch as DxfHatch, PolylineEdge, Solid as DxfSolid}; +use truck_modeling::{ + base::{BoundedCurve, ParametricCurve}, + BSplineCurve as TruckBSpline, KnotVec, Point3, +}; use acadrust::entities::{Block, BlockEnd, Insert as DxfInsert}; use acadrust::objects::ObjectType; use acadrust::types::Vector2; @@ -1668,12 +1672,35 @@ impl Scene { } } BoundaryEdge::Spline(spline) => { - for cp in &spline.control_points { - boundary.push([cp.x as f32, cp.y as f32]); - } - if boundary.len() > 1 { - if let Some(&first) = boundary.first() { - boundary.push(first); + // Evaluate the B-spline curve into smooth boundary points. + // Fall back to control-point polyline for degenerate inputs. + let degree = spline.degree as usize; + let cps: Vec = spline.control_points + .iter() + .map(|p| Point3::new(p.x, p.y, 0.0)) + .collect(); + let knot_vec = if !spline.knots.is_empty() { + KnotVec::from(spline.knots.clone()) + } else if cps.len() >= 2 { + KnotVec::uniform_knot(degree, cps.len() - 1) + } else { + KnotVec::from(vec![]) + }; + let ok = cps.len() >= 2 + && degree >= 1 + && knot_vec.len() == cps.len() + degree + 1; + if ok { + let bspl = TruckBSpline::new(knot_vec, cps); + let (t0, t1) = bspl.range_tuple(); + let segs = 16u32; + for i in 0..=segs { + let t = t0 + (t1 - t0) * (i as f64 / segs as f64); + let p = bspl.subs(t); + boundary.push([p.x as f32, p.y as f32]); + } + } else { + for cp in &spline.control_points { + boundary.push([cp.x as f32, cp.y as f32]); } } }