From 3ca959a34b625233d2c2bf51085737a7564d80ff Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:21:03 +0300 Subject: [PATCH] Fix 3D polyline behavior and properties --- locales/en-US/opencadstudio.ftl | 2 +- src/app/properties.rs | 3 + src/app/update/mod.rs | 3 + src/app/view/mod.rs | 1 + src/entities/polyline.rs | 326 ++++++++++++++++++++++++++++---- src/modules/draw/draw/poly3d.rs | 10 +- src/ui/properties.rs | 3 + 7 files changed, 305 insertions(+), 43 deletions(-) diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 03332480c..ca26cd5b4 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -2829,7 +2829,7 @@ properties = .polyfacemesh = PolyfaceMesh .polygonmesh = PolygonMesh .polyline2d = Polyline2D - .polyline3d = Polyline3D + .polyline3d = 3D Polyline .predefined = Predefined .property-bag = Property Bag .proxy-entity = Proxy Entity diff --git a/src/app/properties.rs b/src/app/properties.rs index d884905a7..141701366 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -121,6 +121,9 @@ impl OpenCADStudio { .and_then(|entity| match entity { acadrust::EntityType::LwPolyline(polyline) => Some(polyline.vertices.len()), acadrust::EntityType::Polyline2D(polyline) => Some(polyline.vertices.len()), + acadrust::EntityType::Polyline3D(polyline) => Some( + crate::entities::polyline::polyline3d_control_vertex_count(polyline), + ), acadrust::EntityType::Leader(leader) => Some(leader.vertices.len()), acadrust::EntityType::Spline(spline) => { Some(if crate::entities::spline::shows_fit_points(spline) { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index cb501dc7d..84a89215d 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4901,6 +4901,9 @@ impl OpenCADStudio { match self.tabs[i].scene.document.get_entity(handles[0]) { Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(), Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(), + Some(acadrust::EntityType::Polyline3D(p)) => { + crate::entities::polyline::polyline3d_control_vertex_count(p) + } Some(acadrust::EntityType::Table(table)) => { table.row_count().saturating_mul(table.column_count()) } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index a9c104fa1..36d15d5f1 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -432,6 +432,7 @@ impl OpenCADStudio { let indexed = match tab.scene.document.get_entity(h) { Some(acadrust::EntityType::LwPolyline(_)) | Some(acadrust::EntityType::Polyline2D(_)) + | Some(acadrust::EntityType::Polyline3D(_)) | Some(acadrust::EntityType::Spline(_)) => true, _ => false, }; diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 54d98ab77..3a8faf0b9 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -3,12 +3,14 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{ - edit_prop as edit, parse_f64, ro_prop as ro, square_grip, stepper_prop as stepper, + edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip, + stepper_prop as stepper, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::TangentGeom; +use cadkernel::space::NurbsCurve3; // ── Polyline (old-style 3D heavy polyline) ──────────────────────────────────── @@ -783,6 +785,159 @@ impl Transformable for Polyline2D { // ── Polyline3D ──────────────────────────────────────────────────────────────── +const POLY3D_SPLINE_POINT: i32 = 8; +const POLY3D_SPLINE_CONTROL: i32 = 16; +const POLY3D_VERTEX: i32 = 32; + +fn polyline3d_control_indices(pl: &Polyline3D) -> Vec { + if pl + .vertices + .iter() + .any(|vertex| vertex.flags & POLY3D_SPLINE_CONTROL != 0) + { + return pl + .vertices + .iter() + .enumerate() + .filter_map(|(index, vertex)| { + (vertex.flags & POLY3D_SPLINE_CONTROL != 0).then_some(index) + }) + .collect(); + } + + let controls: Vec = pl + .vertices + .iter() + .enumerate() + .filter_map(|(index, vertex)| { + (vertex.flags & POLY3D_SPLINE_POINT == 0).then_some(index) + }) + .collect(); + if controls.is_empty() { + (0..pl.vertices.len()).collect() + } else { + controls + } +} + +pub(crate) fn polyline3d_control_vertex_count(pl: &Polyline3D) -> usize { + polyline3d_control_indices(pl).len() +} + +fn polyline3d_controls(pl: &Polyline3D) -> Vec { + polyline3d_control_indices(pl) + .into_iter() + .filter_map(|index| pl.vertices.get(index).cloned()) + .collect() +} + +fn polyline3d_wire_points(pl: &Polyline3D) -> Vec<[f64; 3]> { + let curve: Vec<[f64; 3]> = pl + .vertices + .iter() + .filter(|vertex| vertex.flags & POLY3D_SPLINE_POINT != 0) + .map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]) + .collect(); + if !curve.is_empty() { + curve + } else { + polyline3d_controls(pl) + .iter() + .map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]) + .collect() + } +} + +fn polyline3d_length(pl: &Polyline3D) -> f64 { + let points = polyline3d_wire_points(pl); + let mut length = points + .windows(2) + .map(|pair| { + let dx = pair[1][0] - pair[0][0]; + let dy = pair[1][1] - pair[0][1]; + let dz = pair[1][2] - pair[0][2]; + (dx * dx + dy * dy + dz * dz).sqrt() + }) + .sum::(); + if pl.is_closed() && points.len() >= 2 { + let first = points[0]; + let last = points[points.len() - 1]; + let dx = first[0] - last[0]; + let dy = first[1] - last[1]; + let dz = first[2] - last[2]; + length += (dx * dx + dy * dy + dz * dz).sqrt(); + } + length +} + +fn polyline3d_spline_samples( + controls: &[[f64; 3]], + degree: usize, + closed: bool, +) -> Option> { + let curve = if closed { + let mut periodic_controls = controls.to_vec(); + periodic_controls.extend(controls.iter().take(degree).copied()); + let knots = (0..periodic_controls.len() + degree + 1) + .map(|index| index as f64) + .collect(); + NurbsCurve3::new(degree, periodic_controls, knots, None)?.with_periodicity(true) + } else { + NurbsCurve3::new(degree, controls.to_vec(), Vec::new(), None)? + }; + Some(curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)) +} + +fn rebuild_polyline3d_fit(pl: &mut Polyline3D) -> bool { + use acadrust::entities::polyline3d::SmoothSurfaceType as SST; + + let mut controls = polyline3d_controls(pl); + if pl.smooth_type == SST::None { + for vertex in &mut controls { + vertex.flags = POLY3D_VERTEX; + } + pl.vertices = controls; + pl.flags.spline_fit = false; + return true; + } + + let degree = match pl.smooth_type { + SST::QuadraticBSpline => 2, + SST::CubicBSpline | SST::Bezier => 3, + SST::None => unreachable!(), + }; + if controls.len() <= degree { + return false; + } + let points: Vec<[f64; 3]> = controls + .iter() + .map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]) + .collect(); + let Some(samples) = polyline3d_spline_samples(&points, degree, pl.is_closed()) else { + return false; + }; + + for vertex in &mut controls { + vertex.flags = POLY3D_VERTEX | POLY3D_SPLINE_CONTROL; + } + let vertex_layer = controls + .first() + .map(|vertex| vertex.layer.clone()) + .unwrap_or_else(|| "0".to_string()); + let curve_vertices = samples.into_iter().map(|point| { + let mut vertex = acadrust::entities::Vertex3DPolyline::from_xyz( + point[0], point[1], point[2], + ); + vertex.layer = vertex_layer.clone(); + vertex.flags = POLY3D_VERTEX | POLY3D_SPLINE_POINT; + vertex + }); + controls.extend(curve_vertices); + pl.vertices = controls; + pl.flags.spline_fit = true; + true +} + fn tessellate_polyline3d(pl: &Polyline3D) -> RenderEntity { let to_pt = |v: &acadrust::entities::Vertex3DPolyline| -> [f64; 3] { [v.position.x, v.position.y, v.position.z] @@ -791,8 +946,16 @@ fn tessellate_polyline3d(pl: &Polyline3D) -> RenderEntity { // DXF vertex flags: 8 = spline-fit curve point, 16 = spline frame control point. // When spline-fit vertices are present use them for the wire and control points for snap; // otherwise treat all vertices uniformly. - let spline_curve: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 8 != 0).collect(); - let ctrl_pts: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 16 != 0).collect(); + let spline_curve: Vec<_> = pl + .vertices + .iter() + .filter(|v| v.flags & POLY3D_SPLINE_POINT != 0) + .collect(); + let ctrl_pts: Vec<_> = pl + .vertices + .iter() + .filter(|v| v.flags & POLY3D_SPLINE_CONTROL != 0) + .collect(); let (wire_pts, key_verts) = if !spline_curve.is_empty() { let wire: Vec<[f64; 3]> = spline_curve.iter().map(|v| to_pt(v)).collect(); @@ -826,8 +989,9 @@ impl RenderConvertible for Polyline3D { impl Grippable for Polyline3D { fn grips(&self) -> Vec { - self.vertices - .iter() + polyline3d_control_indices(self) + .into_iter() + .filter_map(|index| self.vertices.get(index)) .enumerate() .map(|(i, v)| { square_grip( @@ -839,7 +1003,8 @@ impl Grippable for Polyline3D { } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { - if let Some(v) = self.vertices.get_mut(grip_id) { + let raw_index = polyline3d_control_indices(self).get(grip_id).copied(); + if let Some(v) = raw_index.and_then(|index| self.vertices.get_mut(index)) { match apply { GripApply::Translate(d) => { v.position.x += d.x as f64; @@ -852,6 +1017,11 @@ impl Grippable for Polyline3D { v.position.z = p.z as f64; } } + if self.smooth_type + != acadrust::entities::polyline3d::SmoothSurfaceType::None + { + let _ = rebuild_polyline3d_fit(self); + } } } @@ -875,31 +1045,44 @@ impl Grippable for Polyline3D { fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) { use crate::scene::model::object::GripMenuAction as A; - let n = self.vertices.len(); + use acadrust::entities::polyline3d::SmoothSurfaceType as SST; + + let mut controls = polyline3d_controls(self); + let n = controls.len(); match action { A::AddVertex if grip_id < n => { if grip_id == n - 1 && !self.is_closed() { - let mut new_v = self.vertices[grip_id].clone(); + let mut new_v = controls[grip_id].clone(); new_v.handle = acadrust::Handle::NULL; - self.vertices.push(new_v); - return; + controls.push(new_v); + } else { + let i1 = (grip_id + 1) % n; + let v0 = &controls[grip_id]; + let v1 = &controls[i1]; + let mx = (v0.position.x + v1.position.x) * 0.5; + let my = (v0.position.y + v1.position.y) * 0.5; + let mz = (v0.position.z + v1.position.z) * 0.5; + let mut new_v = v0.clone(); + new_v.position.x = mx; + new_v.position.y = my; + new_v.position.z = mz; + new_v.handle = acadrust::Handle::NULL; + controls.insert(grip_id + 1, new_v); } - let i1 = (grip_id + 1) % n; - let v0 = &self.vertices[grip_id]; - let v1 = &self.vertices[i1]; - let mx = (v0.position.x + v1.position.x) * 0.5; - let my = (v0.position.y + v1.position.y) * 0.5; - let mz = (v0.position.z + v1.position.z) * 0.5; - let mut new_v = v0.clone(); - new_v.position.x = mx; - new_v.position.y = my; - new_v.position.z = mz; - new_v.handle = acadrust::Handle::NULL; - let insert_at = (grip_id + 1).min(self.vertices.len()); - self.vertices.insert(insert_at, new_v); + self.vertices = controls; + let _ = rebuild_polyline3d_fit(self); } - A::RemoveVertex if grip_id < n && n > 2 => { - self.vertices.remove(grip_id); + A::RemoveVertex if grip_id < n => { + let minimum = match self.smooth_type { + SST::None => 2, + SST::QuadraticBSpline => 3, + SST::CubicBSpline | SST::Bezier => 4, + }; + if n > minimum { + controls.remove(grip_id); + self.vertices = controls; + let _ = rebuild_polyline3d_fit(self); + } } _ => {} } @@ -909,31 +1092,64 @@ impl Grippable for Polyline3D { impl PropertyEditable for Polyline3D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { use acadrust::entities::polyline3d::SmoothSurfaceType as SST; - let n = self.vertices.len(); - let v0 = self.vertices.first(); - let vertex_x = v0.map(|v| v.position.x).unwrap_or_default(); - let vertex_y = v0.map(|v| v.position.y).unwrap_or_default(); - let vertex_z = v0.map(|v| v.position.z).unwrap_or_default(); + let control_indices = polyline3d_control_indices(self); + let n = control_indices.len(); + let vi = if n == 0 { + 0 + } else { + crate::scene::view::dispatch::prop_current_vertex().min(n - 1) + }; + let vertex = control_indices + .get(vi) + .and_then(|index| self.vertices.get(*index)); + let vertex_x = vertex.map(|v| v.position.x).unwrap_or_default(); + let vertex_y = vertex.map(|v| v.position.y).unwrap_or_default(); + let vertex_z = vertex.map(|v| v.position.z).unwrap_or_default(); + let vertex_label = if n == 0 { + "—".to_string() + } else { + format!("{} / {}", vi + 1, n) + }; let fit_smooth = match self.smooth_type { SST::None => "None", SST::QuadraticBSpline => "Quadratic", - SST::CubicBSpline => "Cubic", - SST::Bezier => "Bezier", + SST::CubicBSpline | SST::Bezier => "Cubic", }; vec![ PropSection { title: t!("Geometry").into_owned(), props: vec![ - ro(t!("Vertex").as_ref(), "pl3_vertex", if n > 0 { "1" } else { "" }), + stepper( + t!("Current Vertex").as_ref(), + "pl3_current_vertex", + vertex_label, + ), edit(t!("Vertex X").as_ref(), "pl3_vertex_x", vertex_x), edit(t!("Vertex Y").as_ref(), "pl3_vertex_y", vertex_y), edit(t!("Vertex Z").as_ref(), "pl3_vertex_z", vertex_z), + ro( + t!("Length").as_ref(), + "pl3_length", + format_length(polyline3d_length(self)), + ), ], }, PropSection { title: t!("Misc").into_owned(), props: vec![ + Property { + label: t!("Fit/Smooth").into_owned(), + field: "pl3_smooth", + value: PropValue::Choice { + selected: fit_smooth.to_string(), + options: vec![ + "None".to_string(), + "Quadratic".to_string(), + "Cubic".to_string(), + ], + }, + }, Property { label: t!("Closed").into_owned(), field: "pl3_closed", @@ -942,7 +1158,6 @@ impl PropertyEditable for Polyline3D { value: self.is_closed(), }, }, - ro(t!("Fit/Smooth").as_ref(), "pl3_smooth", fit_smooth), ], }, ] @@ -961,20 +1176,57 @@ impl PropertyEditable for Polyline3D { } else { self.open(); } + if self.smooth_type + != acadrust::entities::polyline3d::SmoothSurfaceType::None + { + let _ = rebuild_polyline3d_fit(self); + } + } + "pl3_smooth" => { + use acadrust::entities::polyline3d::SmoothSurfaceType as SST; + let next = match value { + "None" => SST::None, + "Quadratic" => SST::QuadraticBSpline, + "Cubic" => SST::CubicBSpline, + _ => return, + }; + let previous = self.smooth_type; + self.smooth_type = next; + if !rebuild_polyline3d_fit(self) { + self.smooth_type = previous; + } } "pl3_vertex_x" => { - if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + let vi = crate::scene::view::dispatch::prop_current_vertex(); + let raw_index = polyline3d_control_indices(self).get(vi).copied(); + if let (Some(v), Some(vert)) = ( + parse_f64(value), + raw_index.and_then(|index| self.vertices.get_mut(index)), + ) { vert.position.x = v; + let _ = rebuild_polyline3d_fit(self); } } "pl3_vertex_y" => { - if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + let vi = crate::scene::view::dispatch::prop_current_vertex(); + let raw_index = polyline3d_control_indices(self).get(vi).copied(); + if let (Some(v), Some(vert)) = ( + parse_f64(value), + raw_index.and_then(|index| self.vertices.get_mut(index)), + ) { vert.position.y = v; + let _ = rebuild_polyline3d_fit(self); } } "pl3_vertex_z" => { - if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) { + let vi = crate::scene::view::dispatch::prop_current_vertex(); + let raw_index = polyline3d_control_indices(self).get(vi).copied(); + if let (Some(v), Some(vert)) = ( + parse_f64(value), + raw_index.and_then(|index| self.vertices.get_mut(index)), + ) { vert.position.z = v; + let _ = rebuild_polyline3d_fit(self); } } _ => {} diff --git a/src/modules/draw/draw/poly3d.rs b/src/modules/draw/draw/poly3d.rs index 4891d31f5..923d5d45c 100644 --- a/src/modules/draw/draw/poly3d.rs +++ b/src/modules/draw/draw/poly3d.rs @@ -67,7 +67,7 @@ impl CadCommand for Poly3dCommand { fn prompt(&self) -> String { match self.points.len() { 0 => t!("3DPOLY Specify start point of polyline:").into_owned(), - 1 => t!("3DPOLY Specify next point [Undo]:").into_owned(), + 1 | 2 => t!("3DPOLY Specify next point [Undo]:").into_owned(), _ => t!("3DPOLY Specify next point [Close/Undo]:").into_owned(), } } @@ -98,10 +98,10 @@ impl CadCommand for Poly3dCommand { fn on_text_input(&mut self, text: &str) -> Option { match text.trim().to_uppercase().as_str() { - "C" | "CLOSE" => match self.build(true) { - Some(e) => Some(CmdResult::CommitAndExit(e)), - None => Some(CmdResult::NeedPoint), - }, + "C" | "CLOSE" if self.points.len() >= 3 => self + .build(true) + .map(CmdResult::CommitAndExit), + "C" | "CLOSE" => Some(CmdResult::NeedPoint), "U" | "UNDO" => { self.points.pop(); Some(CmdResult::NeedPoint) diff --git a/src/ui/properties.rs b/src/ui/properties.rs index b6d129710..b6d6d697a 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -1701,6 +1701,9 @@ fn coord_suffix(label: &str) -> Option<(&str, usize)> { /// Length of the coordinate group starting at `idx`: consecutive text rows /// labelled " X", " Y" and optionally " Z". 0/1 = no group. fn coord_group_len(props: &[crate::scene::model::object::Property], idx: usize) -> usize { + if props[idx].field == "pl3_vertex_x" { + return 0; + } let groupable = |p: &crate::scene::model::object::Property| { matches!( p.value,