Fix 3D polyline behavior and properties

This commit is contained in:
ramox81 committed 2026-08-28 11:21:03 +03:00
1 parent cc430cdbe9
commit 3ca959a34b
7 files changed
+305 -43

No files matched your search

+1 -1
View File
@@ -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
+3
View File
@@ -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) {
+3
View File
@@ -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())
}
+1
View File
@@ -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,
};
+289 -37
View File
@@ -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<usize> {
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<usize> = 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<acadrust::entities::Vertex3DPolyline> {
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::<f64>();
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<Vec<[f64; 3]>> {
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<GripDef> {
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<PropSection> {
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);
}
}
_ => {}
+5 -5
View File
@@ -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<CmdResult> {
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)
+3
View File
@@ -1701,6 +1701,9 @@ fn coord_suffix(label: &str) -> Option<(&str, usize)> {
/// Length of the coordinate group starting at `idx`: consecutive text rows
/// labelled "<Base> X", "<Base> Y" and optionally "<Base> 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,