diff --git a/src/app/update/command.rs b/src/app/update/command.rs index a01824f79..15ea1e6bd 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1723,15 +1723,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .get_entity(popup.handle) .is_some_and(|entity| match (item.action, entity) { (GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => { - !spline.cv_frame_visible - && crate::entities::spline::uses_fit_method(spline) + crate::entities::spline::shows_fit_points(spline) } ( GripMenuAction::ShowControlVertices, acadrust::EntityType::Spline(spline), ) => { - spline.cv_frame_visible - || !crate::entities::spline::uses_fit_method(spline) + crate::entities::spline::shows_control_vertices(spline) } _ => false, }); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index d1ecc1648..b77f6bdd9 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -591,7 +591,8 @@ bg={bg_ms:.1}ms n={view_count}" }; let control_polygon = tab.selected_handle.and_then(|handle| { let spline = match tab.scene.document.get_entity(handle) { - Some(acadrust::EntityType::Spline(spline)) if spline.cv_frame_visible => spline, + Some(acadrust::EntityType::Spline(spline)) + if crate::entities::spline::shows_control_vertices(spline) => spline, _ => return None, }; if tab diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 5a8813561..481d57543 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -18,7 +18,13 @@ pub(crate) fn uses_fit_method(spline: &Spline) -> bool { } pub(crate) fn shows_fit_points(spline: &Spline) -> bool { - uses_fit_method(spline) && (!spline.cv_frame_visible || spline.flags.periodic) + !spline.fit_points.is_empty() && (!spline.cv_frame_visible || spline.flags.periodic) +} + +pub(crate) fn shows_control_vertices(spline: &Spline) -> bool { + !shows_fit_points(spline) + && !(uses_fit_method(spline) && spline.flags.periodic) + && control_vertices(spline).len() >= 2 } pub(crate) fn fit_nurbs3(spline: &Spline) -> Option { @@ -105,14 +111,24 @@ pub(crate) fn spline_knot_points(spline: &Spline) -> Vec { } pub(crate) fn replace_with_nurbs(spline: &mut Spline, curve: &NurbsCurve3) { + let mut fit_points = spline.fit_points.clone(); spline.degree = curve.degree() as i32; spline.knots = curve.knots().to_vec(); spline.control_points = curve.control_points().iter() .map(|point| acadrust::types::Vector3::new(point[0], point[1], point[2])).collect(); spline.weights = curve.weights().to_vec(); - spline.fit_points.clear(); + if !fit_points.is_empty() { + if let (Some(first), Some(point)) = (fit_points.first_mut(), spline.control_points.first()) { + *first = *point; + } + if let (Some(last), Some(point)) = (fit_points.last_mut(), spline.control_points.last()) { + *last = *point; + } + } + spline.fit_points = fit_points; spline.begin_tangent = acadrust::types::Vector3::ZERO; spline.end_tangent = acadrust::types::Vector3::ZERO; + spline.cv_frame_visible = true; spline.flags.rational = spline.weights.windows(2) .any(|pair| (pair[0] - pair[1]).abs() > 1e-12); spline.flags.closed = false; @@ -615,7 +631,15 @@ fn index_prop(label: &str, field: &'static str, index: usize, count: usize) -> P fn convert_to_control_method(spline: &mut Spline) -> bool { if !uses_fit_method(spline) { - return true; + let valid = spline.control_points.len() >= 2; + if valid { + spline.fit_points.clear(); + spline.begin_tangent = acadrust::types::Vector3::ZERO; + spline.end_tangent = acadrust::types::Vector3::ZERO; + spline.dwg_flags1 &= !1; + spline.dxf_flags &= !(32 | 1024); + } + return valid; } let Some(planar) = crate::entities::curve::spline_curve(spline) else { return false; @@ -647,18 +671,74 @@ fn convert_to_control_method(spline: &mut Spline) -> bool { } fn convert_to_fit_method(spline: &mut Spline) -> bool { - uses_fit_method(spline) + if uses_fit_method(spline) { + return true; + } + if spline.fit_points.len() < 2 { + return false; + } + spline.control_points.clear(); + spline.knots.clear(); + spline.weights.clear(); + spline.flags.rational = false; + spline.dwg_flags1 |= 1; + spline.dxf_flags |= 32 | 1024; + true +} + +fn derived_fit_points(spline: &Spline) -> Vec { + if spline.flags.closed || spline.flags.periodic { + return Vec::new(); + } + let Some(curve) = nurbs3(spline) else { + return Vec::new(); + }; + let (start, end) = curve.domain(); + if !(start.is_finite() && end.is_finite() && end > start) { + return Vec::new(); + } + let tolerance = (end - start).abs().max(1.0) * 1.0e-12; + let mut parameters = vec![start]; + for knot in curve.knots().iter().copied() { + if knot > start + tolerance + && knot < end - tolerance + && parameters + .last() + .is_none_or(|previous| (knot - *previous).abs() > tolerance) + { + parameters.push(knot); + } + } + parameters.push(end); + parameters + .into_iter() + .map(|parameter| { + let point = curve.point_at_knot(parameter); + acadrust::types::Vector3::new(point[0], point[1], point[2]) + }) + .collect() +} + +fn can_show_fit_points(spline: &Spline) -> bool { + spline.fit_points.len() >= 2 || derived_fit_points(spline).len() >= 2 +} + +fn prepare_fit_point_view(spline: &mut Spline) -> bool { + if spline.fit_points.len() < 2 { + spline.fit_points = derived_fit_points(spline); + } + spline.fit_points.len() >= 2 } fn grips(spline: &Spline) -> Vec { - let fit_method = uses_fit_method(spline); let derived_control = control_vertices(spline); - let show_control_vertices = (spline.cv_frame_visible || !fit_method) - && !(fit_method && spline.flags.periodic) + let show_fit = shows_fit_points(spline); + let show_control_vertices = !show_fit + && !(uses_fit_method(spline) && spline.flags.periodic) && derived_control.len() >= 2; let source = if show_control_vertices { derived_control - } else if fit_method { + } else if show_fit { spline.fit_points.clone() } else { spline.control_points.clone() @@ -885,7 +965,7 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { match field { "spline_method" => { if value == "Fit" { - if convert_to_fit_method(spline) { + if prepare_fit_point_view(spline) { spline.cv_frame_visible = false; } } else if value == "Control Vertices" { @@ -896,6 +976,12 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { return; } "knot_param" => { + if shows_fit_points(spline) + && !uses_fit_method(spline) + && !convert_to_fit_method(spline) + { + return; + } spline.knot_parameterization = match value { "Chord" => 0, "Square Root" => 1, @@ -930,9 +1016,30 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { { return; } - if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight") - && uses_fit_method(spline) - && !convert_to_control_method(spline) + if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight") { + if !shows_fit_points(spline) && !spline.fit_points.is_empty() { + if !convert_to_control_method(spline) { + return; + } + } else if uses_fit_method(spline) && !convert_to_control_method(spline) { + return; + } + } + if shows_fit_points(spline) + && !uses_fit_method(spline) + && matches!( + field, + "fit_pt_x" + | "fit_pt_y" + | "fit_pt_z" + | "start_tan_x" + | "start_tan_y" + | "start_tan_z" + | "end_tan_x" + | "end_tan_y" + | "end_tan_z" + ) + && !convert_to_fit_method(spline) { return; } @@ -1015,16 +1122,23 @@ fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) { return; } // First CV edit makes the displayed kernel controls authoritative. - let fit_method = uses_fit_method(spline); - if spline.cv_frame_visible && fit_method && !spline.flags.periodic { + let editing_fit = shows_fit_points(spline); + if editing_fit && !uses_fit_method(spline) { + if !convert_to_fit_method(spline) { + return; + } + } else if !editing_fit + && (uses_fit_method(spline) || !spline.fit_points.is_empty()) + && !spline.flags.periodic + { if !convert_to_control_method(spline) { return; } } - let target = if (spline.cv_frame_visible && !spline.flags.periodic) || !fit_method { - spline.control_points.get_mut(grip_id) - } else { + let target = if editing_fit { spline.fit_points.get_mut(grip_id) + } else { + spline.control_points.get_mut(grip_id) }; if let Some(cp) = target { match apply { @@ -1088,9 +1202,10 @@ impl crate::entities::traits::Grippable for Spline { let fit_method = uses_fit_method(self); let can_show_control = control_vertices(self).len() >= 2 && !(fit_method && self.flags.periodic); + let can_show_fit = can_show_fit_points(self); return if !shows_fit_points(self) { vec![ - (can_show_control && fit_method).then_some(GripMenuItem { + can_show_fit.then_some(GripMenuItem { label: "Fit", action: GripMenuAction::ShowFit, }), @@ -1131,7 +1246,7 @@ impl crate::entities::traits::Grippable for Spline { use crate::scene::model::object::GripMenuAction as A; match action { A::ShowFit => { - if convert_to_fit_method(self) { + if prepare_fit_point_view(self) { self.cv_frame_visible = false; return; } diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 642b26925..fef63b70e 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -2961,8 +2961,8 @@ fn solve_scope( } } (EntityType::Spline(spline), EntityGeom::Spline { curve, control_z }) => { - let mut changed = - spline.fit_points.len() > 0 || spline.control_points.len() != curve.poles.len(); + let mut changed = crate::entities::spline::uses_fit_method(spline) + || spline.control_points.len() != curve.poles.len(); let control_points: Vec<_> = curve .poles .iter() @@ -2983,7 +2983,18 @@ fn solve_scope( updated.knots = curve.knots.clone(); updated.control_points = control_points; updated.weights = curve.weights.iter().map(|id| store.get(*id)).collect(); - updated.fit_points.clear(); + if !updated.fit_points.is_empty() { + if let (Some(first), Some(point)) = + (updated.fit_points.first_mut(), updated.control_points.first()) + { + *first = *point; + } + if let (Some(last), Some(point)) = + (updated.fit_points.last_mut(), updated.control_points.last()) + { + *last = *point; + } + } updated.begin_tangent = Vector3::ZERO; updated.end_tangent = Vector3::ZERO; updated.flags.rational = updated