diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index b98581034..313cba46a 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -2353,6 +2353,7 @@ properties = .chord-x = Chord X .chord-y = Chord Y .chord-z = Chord Z + .chord = Chord .circumference = Circumference .class = Class .class-id = Class ID @@ -2379,6 +2380,10 @@ properties = .control-point-x = Control Point X .control-point-y = Control Point Y .control-point-z = Control Point Z + .control-point-x-sentence-case = Control point X + .control-point-y-sentence-case = Control point Y + .control-point-z-sentence-case = Control point Z + .control-vertices = Control Vertices .coordination-model = Coordination Model .creased-edges = Creased Edges .croppings = Croppings @@ -2386,6 +2391,9 @@ properties = .current-color-scheme = Current Color Scheme .current-vertex-title-case = Current Vertex .current-vertex-sentence-case = Current vertex + .current-control-point = Current Control point + .current-fit-point = Current Fit point + .cv-frame = CV frame .cutting-line = Cutting Line .dwg-payload = DWG Payload .dwg-version = DWG Version @@ -2445,6 +2453,9 @@ properties = .end-tangent-x = End Tangent X .end-tangent-y = End Tangent Y .end-tangent-z = End Tangent Z + .end-tangent-vector-x = End tangent vector X + .end-tangent-vector-y = End tangent vector Y + .end-tangent-vector-z = End tangent vector Z .end-x = End X .end-y = End Y .end-z = End Z @@ -2482,6 +2493,10 @@ properties = .fit-point-y = Fit Point Y .fit-point-z = Fit Point Z .fit-tolerance = Fit Tolerance + .fit-point-x-sentence-case = Fit point X + .fit-point-y-sentence-case = Fit point Y + .fit-point-z-sentence-case = Fit point Z + .fit-tolerance-sentence-case = Fit tolerance .fit-smooth-title-case = Fit/Smooth .fit-smooth-sentence-case = Fit/smooth .fixed-elevation-range = Fixed Elevation Range @@ -2527,6 +2542,7 @@ properties = .jog-z = Jog Z .justify = Justify .knot-parameterization = Knot Parameterization + .knot-parameterization-sentence-case = Knot parameterization .lamp-preset = Lamp Preset .landing-gap = Landing Gap .layout-print-configuration = Layout Print Configuration @@ -2619,6 +2635,8 @@ properties = .number-of-grips = Number of Grips .number-of-vertices = Number of Vertices .number-of-turns = Number of turns + .number-of-control-points = Number of control points + .number-of-fit-points = Number of fit points .ole-frame = OLE Frame .object-references = Object References .obliquing = Obliquing @@ -2738,6 +2756,13 @@ properties = .start-tangent-x = Start Tangent X .start-tangent-y = Start Tangent Y .start-tangent-z = Start Tangent Z + .start-tangent-vector-x = Start tangent vector X + .start-tangent-vector-y = Start tangent vector Y + .start-tangent-vector-z = Start tangent vector Z + .hide = Hide + .periodic = Periodic + .square-root = Square Root + .uniform = Uniform .start-x = Start X .start-y = Start Y .start-z = Start Z diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 50d439a44..b8d18e9c2 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -2293,6 +2293,7 @@ properties = .chord-x = Kiriş X .chord-y = Kiriş Y .chord-z = Kiriş Z + .chord = Kiriş .circumference = Çevre .class = Sınıf .class-id = Sınıf Kimliği @@ -2319,6 +2320,10 @@ properties = .control-point-x = Kontrol Noktası X .control-point-y = Kontrol Noktası Y .control-point-z = Kontrol Noktası Z + .control-point-x-sentence-case = Kontrol noktası X + .control-point-y-sentence-case = Kontrol noktası Y + .control-point-z-sentence-case = Kontrol noktası Z + .control-vertices = Kontrol Köşeleri .coordination-model = Koordinasyon Modeli .creased-edges = Keskin Kenarlar .croppings = Kırpmalar @@ -2326,6 +2331,9 @@ properties = .current-color-scheme = Geçerli Renk Düzeni .current-vertex-title-case = Geçerli Köşe .current-vertex-sentence-case = Geçerli köşe + .current-control-point = Geçerli kontrol noktası + .current-fit-point = Geçerli uydurma noktası + .cv-frame = Kontrol çerçevesi .cutting-line = Kesme Çizgisi .dwg-payload = DWG Verisi .dwg-version = DWG Sürümü @@ -2385,6 +2393,9 @@ properties = .end-tangent-x = Bitiş Teğeti X .end-tangent-y = Bitiş Teğeti Y .end-tangent-z = Bitiş Teğeti Z + .end-tangent-vector-x = Bitiş teğet vektörü X + .end-tangent-vector-y = Bitiş teğet vektörü Y + .end-tangent-vector-z = Bitiş teğet vektörü Z .end-x = Bitiş X .end-y = Bitiş Y .end-z = Bitiş Z @@ -2422,6 +2433,10 @@ properties = .fit-point-y = Uydurma Noktası Y .fit-point-z = Uydurma Noktası Z .fit-tolerance = Uydurma Toleransı + .fit-point-x-sentence-case = Uydurma noktası X + .fit-point-y-sentence-case = Uydurma noktası Y + .fit-point-z-sentence-case = Uydurma noktası Z + .fit-tolerance-sentence-case = Uydurma toleransı .fit-smooth-title-case = Uydur/Yumuşat .fit-smooth-sentence-case = Uydur/yumuşat .fixed-elevation-range = Sabit Kot Aralığı @@ -2467,6 +2482,7 @@ properties = .jog-z = Kırılma Z .justify = Hizala .knot-parameterization = Düğüm Parametrelemesi + .knot-parameterization-sentence-case = Düğüm parametrelemesi .lamp-preset = Lamba Ön Ayarı .landing-gap = Yatay Bağlantı Boşluğu .layout-print-configuration = Yerleşim Yazdırma Yapılandırması @@ -2559,6 +2575,8 @@ properties = .number-of-grips = Tutamaç Sayısı .number-of-vertices = Köşe Sayısı .number-of-turns = Sarım sayısı + .number-of-control-points = Kontrol noktası sayısı + .number-of-fit-points = Uydurma noktası sayısı .ole-frame = OLE Çerçevesi .object-references = Nesne Referansları .obliquing = Eğiklik @@ -2678,6 +2696,13 @@ properties = .start-tangent-x = Başlangıç Teğeti X .start-tangent-y = Başlangıç Teğeti Y .start-tangent-z = Başlangıç Teğeti Z + .start-tangent-vector-x = Başlangıç teğet vektörü X + .start-tangent-vector-y = Başlangıç teğet vektörü Y + .start-tangent-vector-z = Başlangıç teğet vektörü Z + .hide = Gizle + .periodic = Periyodik + .square-root = Karekök + .uniform = Eşit Aralıklı .start-x = Başlangıç X .start-y = Başlangıç Y .start-z = Başlangıç Z diff --git a/src/app/properties.rs b/src/app/properties.rs index 21a5fbaea..450585bc5 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -121,6 +121,11 @@ impl OpenCADStudio { acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()), acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()), acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()), + acadrust::EntityType::Spline(spline) => Some(if spline.fit_points.is_empty() { + spline.control_points.len() + } else { + spline.fit_points.len() + }), _ => None, }); vertex_count.map_or(prop_vertex, |count| prop_vertex.min(count.saturating_sub(1))) diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 026b72031..6f717cbb5 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -2099,6 +2099,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.invalidate_property_targets(i, &handles); self.tabs[i].dirty = true; + if field == "spline_method" { + self.tabs[i].properties.prop_vertex = 0; + self.tabs[i].properties.prop_vertex_indicator_active = false; + } self.refresh_properties(); } Task::none() @@ -2121,6 +2125,35 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } else { crate::app::expr_eval::eval_to_string(&raw_val) }; + if matches!(field, "current_fit_point" | "current_control_point") { + let count = handles + .iter() + .filter_map(|handle| { + let entity = self.tabs[i].scene.document.get_entity(*handle)?; + match (field, entity) { + ( + "current_fit_point", + acadrust::EntityType::Spline(spline), + ) => Some(spline.fit_points.len()), + ( + "current_control_point", + acadrust::EntityType::Spline(spline), + ) => Some(spline.control_points.len()), + _ => None, + } + }) + .min() + .unwrap_or(0); + if let Ok(requested) = val.trim().parse::() { + if count > 0 { + let next = requested.clamp(1, count) - 1; + self.tabs[i].properties.prop_vertex = next; + self.tabs[i].properties.prop_vertex_indicator_active = true; + } + } + self.refresh_properties(); + return Task::none(); + } self.push_undo_snapshot(i, "CHPROP"); if field == "block" { // Name row on a block reference: an existing name diff --git a/src/entities/spline.rs b/src/entities/spline.rs index da20e90d0..b5692e424 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -8,7 +8,7 @@ use crate::entities::common::{ }; use crate::entities::traits::RenderConvertible; use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; fn to_render(spl: &Spline) -> RenderEntity { let n = spl.control_points.len(); @@ -410,6 +410,158 @@ fn control_vertices(spline: &Spline) -> Vec { .unwrap_or_default() } +fn choice_prop( + label: &str, + field: &'static str, + selected: &str, + options: &[&str], +) -> Property { + Property { + label: label.into(), + field, + value: PropValue::Choice { + selected: selected.to_string(), + options: options.iter().map(|option| option.to_string()).collect(), + }, + } +} + +fn index_prop(label: &str, field: &'static str, index: usize, count: usize) -> Property { + Property { + label: label.into(), + field, + value: PropValue::EditText(if count == 0 { + "0".to_string() + } else { + (index + 1).to_string() + }), + } +} + +fn convert_to_control_method(spline: &mut Spline) -> bool { + if !spline.control_points.is_empty() && spline.fit_points.is_empty() { + return true; + } + let elevation = spline.fit_points.first().map(|point| point.z).unwrap_or(0.0); + let Some(curve) = crate::modules::draw::modify::spline_ops::spline_control_curve(spline) else { + return false; + }; + 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], elevation)) + .collect(); + spline.weights = if curve.is_rational() { + curve.weights().to_vec() + } else { + Vec::new() + }; + spline.flags.rational = curve.is_rational(); + spline.fit_points.clear(); + spline.begin_tangent = acadrust::types::Vector3::ZERO; + spline.end_tangent = acadrust::types::Vector3::ZERO; + true +} + +fn convert_to_fit_method(spline: &mut Spline) -> bool { + if !spline.fit_points.is_empty() { + return true; + } + let degree = spline.degree.max(1) as usize; + let controls: Vec<_> = spline + .control_points + .iter() + .map(|point| [point.x, point.y, point.z]) + .collect(); + let weights = (spline.weights.len() == controls.len()).then(|| spline.weights.clone()); + let Some(curve) = NurbsCurve3::new(degree, controls, spline.knots.clone(), weights) else { + return false; + }; + let (start, end) = curve.domain(); + let mut parameters: Vec = curve.knots()[degree..=curve.control_points().len()] + .iter() + .copied() + .filter(|parameter| *parameter >= start && *parameter <= end) + .collect(); + parameters.dedup_by(|left, right| (*left - *right).abs() < 1e-12); + let sample_count = if degree == 3 { + curve.control_points().len().saturating_sub(2).max(2) + } else { + curve.control_points().len().max(4) + }; + for step in 0..sample_count { + let fraction = step as f64 / (sample_count - 1) as f64; + parameters.push(start + (end - start) * fraction); + } + parameters.sort_by(f64::total_cmp); + parameters.dedup_by(|left, right| (*left - *right).abs() < 1e-12); + + spline.fit_points = parameters + .iter() + .map(|parameter| { + let point = curve.point_at_knot(*parameter); + acadrust::types::Vector3::new(point[0], point[1], point[2]) + }) + .collect(); + let begin = curve.tangent_at_knot(start); + let end_tangent = curve.tangent_at_knot(end); + spline.begin_tangent = acadrust::types::Vector3::new(begin[0], begin[1], begin[2]); + spline.end_tangent = + acadrust::types::Vector3::new(end_tangent[0], end_tangent[1], end_tangent[2]); + spline.degree = 3; + spline.knots.clear(); + spline.control_points.clear(); + spline.weights.clear(); + spline.flags.rational = false; + spline.knot_parameterization = 0; + true +} + +fn is_planar(spline: &Spline) -> bool { + let points = if spline.fit_points.is_empty() { + &spline.control_points + } else { + &spline.fit_points + }; + if points.len() <= 3 { + return true; + } + let origin = points[0]; + let Some(axis) = points.iter().skip(1).find_map(|point| { + let vector = [point.x - origin.x, point.y - origin.y, point.z - origin.z]; + let length2 = vector.iter().map(|value| value * value).sum::(); + (length2 > 1e-18).then_some(vector) + }) else { + return true; + }; + let Some(normal) = points.iter().skip(1).find_map(|point| { + let vector = [point.x - origin.x, point.y - origin.y, point.z - origin.z]; + let cross = [ + axis[1] * vector[2] - axis[2] * vector[1], + axis[2] * vector[0] - axis[0] * vector[2], + axis[0] * vector[1] - axis[1] * vector[0], + ]; + let length2 = cross.iter().map(|value| value * value).sum::(); + (length2 > 1e-18).then_some(cross) + }) else { + return true; + }; + let normal_length = normal.iter().map(|value| value * value).sum::().sqrt(); + points.iter().all(|point| { + let offset = [point.x - origin.x, point.y - origin.y, point.z - origin.z]; + let distance = normal + .iter() + .zip(offset) + .map(|(component, value)| component * value) + .sum::() + .abs() + / normal_length; + distance <= 1e-8 + }) +} + fn grips(spline: &Spline) -> Vec { let (source, show_control_vertices) = if spline.cv_frame_visible { (control_vertices(spline), true) @@ -430,166 +582,267 @@ fn grips(spline: &Spline) -> Vec { } }) .collect(); - if !spline.fit_points.is_empty() { - if let Some(first) = source.first() { - grips.push(dropdown_grip( - SPLINE_MODE_GRIP_ID, - glam::DVec3::new(first.x, first.y, first.z), - )); - } + if let Some(first) = source.first() { + grips.push(dropdown_grip( + SPLINE_MODE_GRIP_ID, + glam::DVec3::new(first.x, first.y, first.z), + )); } grips } fn properties(spline: &Spline) -> Vec { - let show = if spline.cv_frame_visible || spline.fit_points.is_empty() { - "Control Vertices" - } else { - "Fit" - }; - let cp0 = spline.control_points.first(); - let fp0 = spline.fit_points.first(); - let w0 = spline.weights.first().copied(); - - // Polyline-approximation length over the effective defining points. - let pts = if spline.fit_points.is_empty() { - &spline.control_points - } else { - &spline.fit_points - }; - let mut length = 0.0; - for w in pts.windows(2) { - let dx = w[1].x - w[0].x; - let dy = w[1].y - w[0].y; - let dz = w[1].z - w[0].z; - length += (dx * dx + dy * dy + dz * dz).sqrt(); - } - + let fit_method = !spline.fit_points.is_empty(); + let method = if fit_method { "Fit" } else { "Control Vertices" }; let closed = spline.flags.closed || spline.flags.periodic; let yes_no = |b: bool| if b { "Yes" } else { "No" }; - - // Knot parameterization method (R2013+ DWG); older splines report 0. let knot_param = match spline.knot_parameterization { 0 => "Chord", 1 => "Square Root", 2 => "Uniform", + 15 => "Custom", _ => "Custom", }; - - // Closed splines enclose an area; approximate it with the shoelace - // formula over the defining points projected to the XY plane, matching - // the polyline approximation already used for Length. - let area = if closed && pts.len() >= 3 { - let mut acc = 0.0; - for w in pts.windows(2) { - acc += w[0].x * w[1].y - w[1].x * w[0].y; - } - if let (Some(first), Some(last)) = (pts.first(), pts.last()) { - acc += last.x * first.y - first.x * last.y; - } - format!("{:.4}", acc.abs() * 0.5) + let current = crate::scene::view::dispatch::prop_current_vertex(); + let mut data_points = if fit_method { + let count = spline.fit_points.len(); + let index = current.min(count.saturating_sub(1)); + let point = spline.fit_points.get(index); + vec![ + ro( + t!("Number of fit points").as_ref(), + "fit_pt_count", + count.to_string(), + ), + index_prop( + t!("Current Fit point").as_ref(), + "current_fit_point", + index, + count, + ), + edit( + t!("Fit point X").as_ref(), + "fit_pt_x", + point.map(|value| value.x).unwrap_or(0.0), + ), + edit( + t!("Fit point Y").as_ref(), + "fit_pt_y", + point.map(|value| value.y).unwrap_or(0.0), + ), + edit( + t!("Fit point Z").as_ref(), + "fit_pt_z", + point.map(|value| value.z).unwrap_or(0.0), + ), + choice_prop( + t!("Knot parameterization").as_ref(), + "knot_param", + knot_param, + &["Chord", "Square Root", "Uniform", "Custom"], + ), + ] } else { - String::new() + let count = spline.control_points.len(); + let index = current.min(count.saturating_sub(1)); + let point = spline.control_points.get(index); + let weight = spline.weights.get(index).copied().unwrap_or(1.0); + vec![ + ro( + t!("Number of control points").as_ref(), + "ctrl_pt_count", + count.to_string(), + ), + index_prop( + t!("Current Control point").as_ref(), + "current_control_point", + index, + count, + ), + edit( + t!("Control point X").as_ref(), + "ctrl_pt_x", + point.map(|value| value.x).unwrap_or(0.0), + ), + edit( + t!("Control point Y").as_ref(), + "ctrl_pt_y", + point.map(|value| value.y).unwrap_or(0.0), + ), + edit( + t!("Control point Z").as_ref(), + "ctrl_pt_z", + point.map(|value| value.z).unwrap_or(0.0), + ), + edit(t!("Weight").as_ref(), "weight", weight), + ] }; + data_points.push(choice_prop( + t!("CV frame").as_ref(), + "cv_frame", + if spline.cv_frame_visible { "Show" } else { "Hide" }, + &["Hide", "Show"], + )); + + let mut misc = vec![ + choice_prop( + t!("Method").as_ref(), + "spline_method", + method, + &["Fit", "Control Vertices"], + ), + ro(t!("Degree").as_ref(), "degree", spline.degree.to_string()), + choice_prop( + t!("Closed").as_ref(), + "closed", + yes_no(closed), + &["No", "Yes"], + ), + ro( + t!("Periodic").as_ref(), + "periodic", + yes_no(spline.flags.periodic), + ), + ro( + t!("Planar").as_ref(), + "planar", + yes_no(is_planar(spline)), + ), + ]; + if fit_method { + misc.extend([ + edit( + t!("Start tangent vector X").as_ref(), + "start_tan_x", + spline.begin_tangent.x, + ), + edit( + t!("Start tangent vector Y").as_ref(), + "start_tan_y", + spline.begin_tangent.y, + ), + edit( + t!("Start tangent vector Z").as_ref(), + "start_tan_z", + spline.begin_tangent.z, + ), + edit( + t!("End tangent vector X").as_ref(), + "end_tan_x", + spline.end_tangent.x, + ), + edit( + t!("End tangent vector Y").as_ref(), + "end_tan_y", + spline.end_tangent.y, + ), + edit( + t!("End tangent vector Z").as_ref(), + "end_tan_z", + spline.end_tangent.z, + ), + edit( + t!("Fit tolerance").as_ref(), + "fit_tolerance", + spline.fit_tolerance, + ), + ]); + } vec![ PropSection { title: t!("Data Points").into_owned(), - props: vec![ - ro(t!("Show").as_ref(), "show", show), - ro(t!("Degree").as_ref(), "degree", spline.degree.to_string()), - ro( - t!("Control Point Count").as_ref(), - "ctrl_pt_count", - spline.control_points.len().to_string(), - ), - ro(t!("Control Point").as_ref(), "ctrl_pt_index", "0"), - edit( - t!("Control Point X").as_ref(), - "ctrl_pt_x", - cp0.map(|p| p.x).unwrap_or(0.0), - ), - edit( - t!("Control Point Y").as_ref(), - "ctrl_pt_y", - cp0.map(|p| p.y).unwrap_or(0.0), - ), - edit( - t!("Control Point Z").as_ref(), - "ctrl_pt_z", - cp0.map(|p| p.z).unwrap_or(0.0), - ), - edit(t!("Weight").as_ref(), "weight", w0.unwrap_or(1.0)), - ro(t!("Knot Parameterization").as_ref(), "knot_param", knot_param), - ro( - t!("Fit Point Count").as_ref(), - "fit_pt_count", - spline.fit_points.len().to_string(), - ), - ro(t!("Fit Point").as_ref(), "fit_pt_index", "0"), - edit(t!("Fit Point X").as_ref(), "fit_pt_x", fp0.map(|p| p.x).unwrap_or(0.0)), - edit(t!("Fit Point Y").as_ref(), "fit_pt_y", fp0.map(|p| p.y).unwrap_or(0.0)), - edit(t!("Fit Point Z").as_ref(), "fit_pt_z", fp0.map(|p| p.z).unwrap_or(0.0)), - edit(t!("Fit Tolerance").as_ref(), "fit_tolerance", spline.fit_tolerance), - edit(t!("Start Tangent X").as_ref(), "start_tan_x", spline.begin_tangent.x), - edit(t!("Start Tangent Y").as_ref(), "start_tan_y", spline.begin_tangent.y), - edit(t!("Start Tangent Z").as_ref(), "start_tan_z", spline.begin_tangent.z), - edit(t!("End Tangent X").as_ref(), "end_tan_x", spline.end_tangent.x), - edit(t!("End Tangent Y").as_ref(), "end_tan_y", spline.end_tangent.y), - edit(t!("End Tangent Z").as_ref(), "end_tan_z", spline.end_tangent.z), - ], + props: data_points, }, PropSection { title: t!("Misc").into_owned(), - props: vec![ - ro(t!("Closed").as_ref(), "closed", yes_no(closed)), - ro(t!("Planar").as_ref(), "planar", yes_no(spline.flags.planar)), - ro(t!("Length").as_ref(), "length", format!("{length:.4}")), - ro(t!("Area").as_ref(), "area", area), - ], + props: misc, }, ] } fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { + match field { + "spline_method" => { + if value == "Fit" { + convert_to_fit_method(spline); + } else if value == "Control Vertices" { + convert_to_control_method(spline); + } + return; + } + "knot_param" => { + spline.knot_parameterization = match value { + "Chord" => 0, + "Square Root" => 1, + "Uniform" => 2, + "Custom" => 15, + _ => return, + }; + return; + } + "cv_frame" => { + spline.cv_frame_visible = value == "Show"; + return; + } + "closed" => { + spline.flags.closed = value == "Yes"; + if !spline.flags.closed { + spline.flags.periodic = false; + } + return; + } + _ => {} + } let Some(v) = parse_f64(value) else { return }; + let control_index = crate::scene::view::dispatch::prop_current_vertex() + .min(spline.control_points.len().saturating_sub(1)); + let fit_index = crate::scene::view::dispatch::prop_current_vertex() + .min(spline.fit_points.len().saturating_sub(1)); match field { "ctrl_pt_x" => { - if let Some(cp) = spline.control_points.first_mut() { + if let Some(cp) = spline.control_points.get_mut(control_index) { cp.x = v; } } "ctrl_pt_y" => { - if let Some(cp) = spline.control_points.first_mut() { + if let Some(cp) = spline.control_points.get_mut(control_index) { cp.y = v; } } "ctrl_pt_z" => { - if let Some(cp) = spline.control_points.first_mut() { + if let Some(cp) = spline.control_points.get_mut(control_index) { cp.z = v; } } "weight" => { - if let Some(w) = spline.weights.first_mut() { - *w = v; + if v > 0.0 && !spline.control_points.is_empty() { + if spline.weights.len() != spline.control_points.len() { + spline.weights = vec![1.0; spline.control_points.len()]; + } + spline.weights[control_index] = v; + spline.flags.rational = spline + .weights + .iter() + .any(|weight| (*weight - spline.weights[0]).abs() > 1e-12); } } "fit_pt_x" => { - if let Some(fp) = spline.fit_points.first_mut() { + if let Some(fp) = spline.fit_points.get_mut(fit_index) { fp.x = v; } } "fit_pt_y" => { - if let Some(fp) = spline.fit_points.first_mut() { + if let Some(fp) = spline.fit_points.get_mut(fit_index) { fp.y = v; } } "fit_pt_z" => { - if let Some(fp) = spline.fit_points.first_mut() { + if let Some(fp) = spline.fit_points.get_mut(fit_index) { fp.z = v; } } - "fit_tolerance" => spline.fit_tolerance = v, + "fit_tolerance" if v >= 0.0 => spline.fit_tolerance = v, "start_tan_x" => spline.begin_tangent.x = v, "start_tan_y" => spline.begin_tangent.y = v, "start_tan_z" => spline.begin_tangent.z = v, @@ -598,6 +851,7 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { "end_tan_z" => spline.end_tangent.z = v, _ => {} } + spline.flags.planar = is_planar(spline); } fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) { @@ -609,22 +863,9 @@ fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) { // control-point storage so the dragged vertex remains authoritative when // the drawing is saved. if spline.cv_frame_visible && spline.control_points.is_empty() { - let elevation = spline.fit_points.first().map(|p| p.z).unwrap_or(0.0); - let Some(curve) = crate::modules::draw::modify::spline_ops::spline_control_curve(spline) - else { + if !convert_to_control_method(spline) { return; - }; - spline.degree = curve.degree() as i32; - spline.knots = curve.knots().to_vec(); - spline.control_points = curve - .control_points() - .iter() - .map(|p| acadrust::types::Vector3::new(p[0], p[1], elevation)) - .collect(); - spline.weights = curve.is_rational().then(|| curve.weights().to_vec()).unwrap_or_default(); - spline.fit_points.clear(); - spline.begin_tangent = acadrust::types::Vector3::ZERO; - spline.end_tangent = acadrust::types::Vector3::ZERO; + } } let target = if spline.cv_frame_visible || spline.fit_points.is_empty() { spline.control_points.get_mut(grip_id) @@ -674,7 +915,7 @@ impl crate::entities::traits::Grippable for Spline { fn grip_menu(&self, grip_id: usize) -> Vec { use crate::scene::model::object::{GripMenuAction, GripMenuItem}; if grip_id == SPLINE_MODE_GRIP_ID { - return if self.cv_frame_visible { + return if self.cv_frame_visible || self.fit_points.is_empty() { vec![ GripMenuItem { label: "Fit", @@ -724,9 +965,11 @@ impl crate::entities::traits::Grippable for Spline { ) { use crate::scene::model::object::GripMenuAction as A; match action { - A::ShowFit if !self.fit_points.is_empty() => { - self.cv_frame_visible = false; - return; + A::ShowFit => { + if !self.fit_points.is_empty() || convert_to_fit_method(self) { + self.cv_frame_visible = false; + return; + } } A::ShowControlVertices if control_vertices(self).len() >= 2 => { self.cv_frame_visible = true; diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 18322e33b..fd5bf0ffc 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -577,6 +577,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Character Spacing" => Some(("properties", "character-spacing")), "Check your internet or proxy connection, then retry. Manually added repositories remain available." => Some(("plugins", "check-your-internet-or-proxy-connection-then-r")), "Choose New to capture the current layer settings." => Some(("layers", "choose-new-to-capture-the-current-layer-settin")), + "Chord" => Some(("properties", "chord")), "Chord X" => Some(("properties", "chord-x")), "Chord Y" => Some(("properties", "chord-y")), "Chord Z" => Some(("properties", "chord-z")), @@ -645,6 +646,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Control Point X" => Some(("properties", "control-point-x")), "Control Point Y" => Some(("properties", "control-point-y")), "Control Point Z" => Some(("properties", "control-point-z")), + "Control Vertices" => Some(("properties", "control-vertices")), + "Control point X" => Some(("properties", "control-point-x-sentence-case")), + "Control point Y" => Some(("properties", "control-point-y-sentence-case")), + "Control point Z" => Some(("properties", "control-point-z-sentence-case")), "Coordinates" => Some(("common", "coordinates")), "Coordination Model" => Some(("properties", "coordination-model")), "Copied path: {}" => Some(("common", "copied-path")), @@ -666,6 +671,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Current Color Scheme" => Some(("properties", "current-color-scheme")), "Current MLeader style: {}" => Some(("layers", "current-mleader-style")), "Current Vertex" => Some(("properties", "current-vertex-title-case")), + "Current Control point" => Some(("properties", "current-control-point")), + "Current Fit point" => Some(("properties", "current-fit-point")), + "CV frame" => Some(("properties", "cv-frame")), "Current annotation scale: {name}" => Some(("view", "current-annotation-scale-name")), "Current dim style set to '{}'." => Some(("common", "current-dim-style-set-to")), "Current layer" => Some(("layers", "current-layer")), @@ -961,6 +969,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "End Tangent X" => Some(("properties", "end-tangent-x")), "End Tangent Y" => Some(("properties", "end-tangent-y")), "End Tangent Z" => Some(("properties", "end-tangent-z")), + "End tangent vector X" => Some(("properties", "end-tangent-vector-x")), + "End tangent vector Y" => Some(("properties", "end-tangent-vector-y")), + "End tangent vector Z" => Some(("properties", "end-tangent-vector-z")), "End X" => Some(("properties", "end-x")), "End Y" => Some(("properties", "end-y")), "End Z" => Some(("properties", "end-z")), @@ -1066,6 +1077,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Fit Point Y" => Some(("properties", "fit-point-y")), "Fit Point Z" => Some(("properties", "fit-point-z")), "Fit Tolerance" => Some(("properties", "fit-tolerance")), + "Fit point X" => Some(("properties", "fit-point-x-sentence-case")), + "Fit point Y" => Some(("properties", "fit-point-y-sentence-case")), + "Fit point Z" => Some(("properties", "fit-point-z-sentence-case")), + "Fit tolerance" => Some(("properties", "fit-tolerance-sentence-case")), "Fit to paper" => Some(("plot", "fit-to-paper")), "Fit/Smooth" => Some(("properties", "fit-smooth-title-case")), "Fit/smooth" => Some(("properties", "fit-smooth-sentence-case")), @@ -1258,6 +1273,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Kilometers" => Some(("common", "kilometers")), "Kind" => Some(("model", "kind")), "Knot Parameterization" => Some(("properties", "knot-parameterization")), + "Knot parameterization" => Some(("properties", "knot-parameterization-sentence-case")), "LANDXMLIMPORT path to the .xml file:" => Some(("view", "landxmlimport-path-to-the-xml-file")), "LANDXMLIMPORT: cannot read \"{path}\": {e}" => Some(("view", "landxmlimport-cannot-read-path-e")), "LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view." => Some(("view", "landxmlimport-imported-survey-point-s-use-zoom")), @@ -1701,6 +1717,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Number of Grips" => Some(("properties", "number-of-grips")), "Number of Vertices" => Some(("properties", "number-of-vertices")), "Number of turns" => Some(("properties", "number-of-turns")), + "Number of control points" => Some(("properties", "number-of-control-points")), + "Number of fit points" => Some(("properties", "number-of-fit-points")), "OBJECTSCALE: added the active scale to {n} object(s)." => Some(("view", "objectscale-added-the-active-scale-to-n-object")), "OBJECTSCALE: select objects first." => Some(("view", "objectscale-select-objects-first")), "OBJECTSCALE: the active annotation scale is unavailable." => Some(("view", "objectscale-the-active-annotation-scale-is-una")), @@ -1905,6 +1923,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Pinned" => Some(("common", "pinned")), "Pitch And Family" => Some(("properties", "pitch-and-family")), "Place text manually (DIMUPT)" => Some(("common", "place-text-manually-dimupt")), + "Periodic" => Some(("properties", "periodic")), "Planar" => Some(("properties", "planar")), "Platform" => Some(("ui", "platform")), "Plot" => Some(("common", "plot")), @@ -2324,6 +2343,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Shortcut '{key}' not found." => Some(("view", "shortcut-key-not-found")), "Shortcut '{key}' removed." => Some(("view", "shortcut-key-removed")), "Shortcut set: {key} → {cmd_str}" => Some(("view", "shortcut-set-key-cmd-str")), + "Hide" => Some(("properties", "hide")), "Show" => Some(("properties", "show")), "Show Annotation Objects" => Some(("ui", "show-annotation-objects")), "Show Clipping" => Some(("properties", "show-clipping")), @@ -2369,6 +2389,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Start Tangent X" => Some(("properties", "start-tangent-x")), "Start Tangent Y" => Some(("properties", "start-tangent-y")), "Start Tangent Z" => Some(("properties", "start-tangent-z")), + "Start tangent vector X" => Some(("properties", "start-tangent-vector-x")), + "Start tangent vector Y" => Some(("properties", "start-tangent-vector-y")), + "Start tangent vector Z" => Some(("properties", "start-tangent-vector-z")), + "Square Root" => Some(("properties", "square-root")), "Start X" => Some(("properties", "start-x")), "Start Y" => Some(("properties", "start-y")), "Start Z" => Some(("properties", "start-z")), @@ -2769,6 +2793,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Usage: {prefix} NEW " => Some(("layers", "usage-prefix-new-name")), "Usage: {prefix} OBLIQUE