Merge pull request #903 from ramox81/codex/fix-3dpoly-behavior

Fix 3D polyline behavior and properties
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-08-28 19:46:27 +03:00
commit aa6652f7c8
7 files changed
+361 -62

No files matched your search

+1 -1
View File
@@ -2831,7 +2831,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
@@ -4927,6 +4927,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,
};
+344 -48
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, Vec3};
// ── Polyline (old-style 3D heavy polyline) ────────────────────────────────────
@@ -783,20 +785,191 @@ 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| Vec3::from(pair[0]).distance(Vec3::from(pair[1])))
.sum::<f64>();
if pl.is_closed() && points.len() >= 2 {
length += Vec3::from(points[0])
.distance(Vec3::from(points[points.len() - 1]));
}
length
}
fn polyline3d_spline_samples(
controls: &[[f64; 3]],
degree: usize,
closed: bool,
) -> Option<Vec<[f64; 3]>> {
if !controls.iter().flatten().all(|value| value.is_finite()) {
return None;
}
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)?
};
let mut samples = curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
if closed
&& samples.len() >= 2
&& Vec3::from(samples[0]).distance(Vec3::from(samples[samples.len() - 1])) <= 1.0e-9
{
samples.pop();
}
Some(samples)
}
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 =
(vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL)) | POLY3D_VERTEX;
}
pl.vertices = controls;
pl.flags.spline_fit = false;
return true;
}
if pl.smooth_type == SST::Bezier {
pl.smooth_type = SST::CubicBSpline;
}
let degree = match pl.smooth_type {
SST::QuadraticBSpline => 2,
SST::CubicBSpline => 3,
SST::Bezier => unreachable!(),
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 = (vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL))
| 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]
};
// 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();
// Spline samples draw the wire; frame vertices remain snap points.
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();
let ctrl: Vec<[f64; 3]> = ctrl_pts.iter().map(|v| to_pt(v)).collect();
let ctrl: Vec<[f64; 3]> = if ctrl_pts.is_empty() {
polyline3d_controls(pl).iter().map(to_pt).collect()
} else {
ctrl_pts.iter().map(|v| to_pt(v)).collect()
};
(wire, ctrl)
} else {
let pts: Vec<[f64; 3]> = pl.vertices.iter().map(to_pt).collect();
@@ -826,8 +999,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 +1013,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,12 +1027,19 @@ 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);
}
}
}
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
vec![
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
let mut items = vec![
GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
@@ -866,40 +1048,75 @@ impl Grippable for Polyline3D {
label: "Add Vertex",
action: GripMenuAction::AddVertex,
},
GripMenuItem {
];
let minimum = match self.smooth_type {
SST::None if self.is_closed() => 3,
SST::None => 2,
SST::QuadraticBSpline => 3,
SST::CubicBSpline | SST::Bezier => 4,
};
if polyline3d_control_vertex_count(self) > minimum {
items.push(GripMenuItem {
label: "Remove Vertex",
action: GripMenuAction::RemoveVertex,
},
]
});
}
items
}
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();
if n >= 2 {
let previous = &controls[n - 2].position;
let last = &controls[n - 1].position;
let next = Vec3::new(previous.x, previous.y, previous.z).lerp(
Vec3::new(last.x, last.y, last.z),
2.0,
);
new_v.position.x = next.x;
new_v.position.y = next.y;
new_v.position.z = next.z;
}
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 midpoint = Vec3::new(v0.position.x, v0.position.y, v0.position.z).lerp(
Vec3::new(v1.position.x, v1.position.y, v1.position.z),
0.5,
);
let mut new_v = v0.clone();
new_v.position.x = midpoint.x;
new_v.position.y = midpoint.y;
new_v.position.z = midpoint.z;
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 if self.is_closed() => 3,
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,40 +1126,79 @@ 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",
};
let mut smooth_options = vec!["None".to_string()];
if n >= 3 {
smooth_options.push("Quadratic".to_string());
}
if n >= 4 {
smooth_options.push("Cubic".to_string());
}
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: smooth_options,
},
},
Property {
label: t!("Closed").into_owned(),
field: "pl3_closed",
value: PropValue::BoolToggle {
field: "pl3_closed",
value: self.is_closed(),
value: if n >= 3 {
PropValue::BoolToggle {
field: "pl3_closed",
value: self.is_closed(),
}
} else {
PropValue::ReadOnly("No".to_string())
},
},
ro(t!("Fit/Smooth").as_ref(), "pl3_smooth", fit_smooth),
],
},
]
@@ -956,25 +1212,65 @@ impl PropertyEditable for Polyline3D {
} else {
value == "true"
};
if closed && polyline3d_control_vertex_count(self) < 3 {
return;
}
if closed {
self.close();
} 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).filter(|value| value.is_finite()),
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).filter(|value| value.is_finite()),
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).filter(|value| value.is_finite()),
raw_index.and_then(|index| self.vertices.get_mut(index)),
) {
vert.position.z = v;
let _ = rebuild_polyline3d_fit(self);
}
}
_ => {}
+6 -13
View File
@@ -1,11 +1,4 @@
// 3D polyline tool — interactive command.
//
// Command: 3DPOLY — pick a sequence of points whose Z ordinates may differ and
// commit them as a single 3D polyline of straight segments. Unlike PLINE (a
// planar lightweight polyline with bulge arcs), every vertex keeps its own Z,
// so the result is a true non-planar path. Close (C) adds a closing segment
// back to the first point; Undo (U) drops the last picked vertex. Enter / Esc
// finishes the open polyline.
// 3DPOLY creates open or closed non-planar paths.
use acadrust::entities::Polyline3D;
use acadrust::types::Vector3;
@@ -67,7 +60,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 +91,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
@@ -1771,6 +1771,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,