Merge pull request #1194 from ramox81/codex/complete-spline-fit-options

This commit is contained in:
Hakan Seven committed 2026-09-11 12:42:56 +03:00
commit 0d45d2f04c
2 files changed
+169 -17

No files matched your search

+1 -1
View File
@@ -881,7 +881,7 @@ impl OpenCADStudio {
SplineCommand::control_vertices() SplineCommand::control_vertices()
} else { } else {
SplineCommand::new() SplineCommand::new()
}; }.with_document(&self.tabs[i].scene.document);
self.command_line.push_info(&new_cmd.prompt()); self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.tabs[i].active_cmd = Some(Box::new(new_cmd));
} }
+168 -16
View File
@@ -5,7 +5,8 @@
use crate::t; use crate::t;
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::{EntityType, Spline}; use acadrust::{CadDocument, EntityType, Handle, Spline};
use std::collections::HashMap;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
@@ -26,6 +27,14 @@ pub struct SplineCommand {
pts: Vec<DVec3>, pts: Vec<DVec3>,
control_vertices: bool, control_vertices: bool,
choosing_method: bool, choosing_method: bool,
choosing_knots: bool,
choosing_tangent: bool,
knot_parameterization: i32,
begin_tangent: Vector3,
end_tangent: Vector3,
choosing_objects: bool,
convertible: HashMap<Handle, Spline>,
selected_objects: Vec<Handle>,
} }
impl SplineCommand { impl SplineCommand {
@@ -34,6 +43,14 @@ impl SplineCommand {
pts: Vec::new(), pts: Vec::new(),
control_vertices: false, control_vertices: false,
choosing_method: false, choosing_method: false,
choosing_knots: false,
choosing_tangent: false,
knot_parameterization: 0,
begin_tangent: Vector3::ZERO,
end_tangent: Vector3::ZERO,
choosing_objects: false,
convertible: HashMap::new(),
selected_objects: Vec::new(),
} }
} }
@@ -44,18 +61,68 @@ impl SplineCommand {
} }
} }
pub fn with_document(mut self, document: &CadDocument) -> Self {
self.convertible = document.entities().filter_map(|entity| {
spline_from_polyline(entity).map(|spline| (entity.common().handle, spline))
}).collect();
self
}
fn build(&self, closed: bool) -> Option<EntityType> { fn build(&self, closed: bool) -> Option<EntityType> {
if self.pts.len() < 2 { if self.pts.len() < 2 {
return None; return None;
} }
Some(EntityType::Spline(make_spline( let mut spline = make_spline(
&self.pts, &self.pts,
closed, closed,
self.control_vertices, self.control_vertices,
))) );
spline.knot_parameterization = self.knot_parameterization;
spline.begin_tangent = self.begin_tangent;
spline.end_tangent = self.end_tangent;
Some(EntityType::Spline(spline))
} }
} }
fn control_spline(points: &[[f64; 3]], degree: usize, closed: bool) -> Option<Spline> {
let curve = cadkernel::space::NurbsCurve3::from_control_polygon(degree, points, closed)?;
let mut spline = Spline {
degree: curve.degree() as i32,
control_points: curve.control_points().iter().map(|p| Vector3::new(p[0], p[1], p[2])).collect(),
knots: curve.knots().to_vec(),
weights: curve.weights().to_vec(),
..Default::default()
};
spline.flags.closed = closed;
spline.flags.periodic = closed;
spline.flags.planar = crate::entities::curve::spline_is_planar(&spline);
Some(spline)
}
fn spline_from_polyline(entity: &EntityType) -> Option<Spline> {
let (degree, points, closed, normal) = match entity {
EntityType::Polyline2D(poly) if poly.flags.is_spline_fit() => {
let degree = match poly.smooth_surface as i16 { 5 => 2, 6 => 3, _ => return None };
let plane = crate::entities::curve::ocs_plane(poly.normal, poly.elevation);
let controls: Vec<_> = poly.vertices.iter().filter(|vertex| vertex.flags.bits() & 16 != 0)
.map(|vertex| plane.point_at([vertex.location.x, vertex.location.y])).collect();
(degree, controls, poly.is_closed(), poly.normal)
}
EntityType::Polyline3D(poly) if poly.flags.spline_fit => {
let degree = match poly.smooth_type as i16 { 5 => 2, 6 => 3, _ => return None };
let controls: Vec<_> = poly.vertices.iter().filter(|vertex| vertex.flags & 16 != 0)
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z]).collect();
(degree, controls, poly.is_closed(), poly.normal)
}
_ => return None,
};
let mut spline = control_spline(&points, degree, closed)?;
spline.normal = normal;
spline.common = entity.common().clone();
spline.common.handle = Handle::NULL;
Some(spline)
}
/// Store the chosen construction method directly in the persistent spline. /// Store the chosen construction method directly in the persistent spline.
fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline { fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline {
let mut points: Vec<Vector3> = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect(); let mut points: Vec<Vector3> = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect();
@@ -93,10 +160,14 @@ fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline {
} }
}; };
spline.flags.closed = closed; spline.flags.closed = closed;
// Closed fit curves use the kernel's periodic interpolation rather than
// appending a closing straight segment to an open interpolant.
spline.flags.periodic = closed && !control_vertices;
spline spline
} }
/// Preview the exact representation that `build()` commits. /// Preview the exact representation that `build()` commits.
#[cfg(test)]
fn sample_curve(pts: &[DVec3], closed: bool, control_vertices: bool) -> Vec<[f32; 3]> { fn sample_curve(pts: &[DVec3], closed: bool, control_vertices: bool) -> Vec<[f32; 3]> {
if pts.len() < 2 { if pts.len() < 2 {
return pts return pts
@@ -123,8 +194,13 @@ impl CadCommand for SplineCommand {
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
if self.choosing_objects { return t!("Select spline-fit polylines:").to_string(); }
if self.choosing_method { if self.choosing_method {
"SPLINE Choose creation method [Fit/Control vertices]:".into() "SPLINE Choose creation method [Fit/Control vertices]:".into()
} else if self.choosing_knots {
"SPLINE Enter knot parameterization [Chord/Square root/Uniform]:".into()
} else if self.choosing_tangent {
"SPLINE Specify tangent direction:".into()
} else if self.pts.is_empty() && self.control_vertices { } else if self.pts.is_empty() && self.control_vertices {
t!("SPLINE Specify first control point:").into_owned() t!("SPLINE Specify first control point:").into_owned()
} else if self.pts.is_empty() { } else if self.pts.is_empty() {
@@ -137,16 +213,25 @@ impl CadCommand for SplineCommand {
fn options(&self) -> Vec<crate::command::CmdOption> { fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption; use crate::command::CmdOption;
if self.choosing_objects { return Vec::new(); }
if self.choosing_method { if self.choosing_method {
return vec![ return vec![
CmdOption::new("Fit", "FIT"), CmdOption::new("Fit", "FIT"),
CmdOption::new("Control vertices", "CV"), CmdOption::new("Control vertices", "CV"),
]; ];
} }
if self.pts.is_empty() { if self.choosing_tangent { return vec![]; }
return vec![CmdOption::new(t!("Method").as_ref(), "M")]; if self.choosing_knots {
return vec![CmdOption::new("Chord", "CH"), CmdOption::new("Square root", "S"), CmdOption::new("Uniform", "U")];
} }
let mut opts = vec![CmdOption::new(t!("Close").as_ref(), "C")]; if self.pts.is_empty() {
let mut options = vec![CmdOption::new(t!("Method").as_ref(), "M"), CmdOption::new("Object", "O")];
if !self.control_vertices { options.push(CmdOption::new("Knots", "K")); }
return options;
}
let mut opts = vec![];
if self.pts.len() >= 3 { opts.push(CmdOption::new(t!("Close").as_ref(), "C")); }
if !self.control_vertices { opts.push(CmdOption::new("Tangency", "T")); }
// Undo only makes sense once a control point exists. // Undo only makes sense once a control point exists.
opts.push(CmdOption::new(t!("Undo").as_ref(), "U")); opts.push(CmdOption::new(t!("Undo").as_ref(), "U"));
opts.push(CmdOption::enter(t!("Done").as_ref())); opts.push(CmdOption::enter(t!("Done").as_ref()));
@@ -154,7 +239,22 @@ impl CadCommand for SplineCommand {
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
if self.choosing_method || !pt.is_finite() { if self.choosing_objects || self.choosing_method || self.choosing_knots || !pt.is_finite() {
return CmdResult::NeedPoint;
}
if self.choosing_tangent {
let Some(anchor) = self.pts.last() else { return CmdResult::NeedPoint; };
let Some(dir) = (pt - *anchor).try_normalize() else { return CmdResult::NeedPoint; };
let tangent = Vector3::new(dir.x, dir.y, dir.z);
if self.pts.len() == 1 { self.begin_tangent = tangent; }
else { self.end_tangent = tangent; }
self.choosing_tangent = false;
if self.pts.len() > 1 {
return self.build(false).map_or(CmdResult::NeedPoint, CmdResult::CommitAndExit);
}
return CmdResult::NeedPoint;
}
if self.pts.last().is_some_and(|last| last.distance_squared(pt) < 1e-20) {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
self.pts.push(pt); self.pts.push(pt);
@@ -162,10 +262,20 @@ impl CadCommand for SplineCommand {
} }
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
if self.choosing_method { if self.choosing_objects {
let replacements = self.selected_objects.iter().filter_map(|handle| {
self.convertible.get(handle).map(|spline| (*handle, vec![EntityType::Spline(spline.clone())]))
}).collect::<Vec<_>>();
return if replacements.is_empty() { CmdResult::Cancel }
else { CmdResult::ReplaceMany(replacements, Vec::new()) };
}
if self.choosing_method || self.choosing_knots {
self.choosing_method = false; self.choosing_method = false;
self.choosing_knots = false;
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
if self.choosing_tangent { return CmdResult::NeedPoint; }
match self.build(false) { match self.build(false) {
Some(e) => CmdResult::CommitAndExit(e), Some(e) => CmdResult::CommitAndExit(e),
None => CmdResult::Cancel, None => CmdResult::Cancel,
@@ -173,13 +283,28 @@ impl CadCommand for SplineCommand {
} }
fn enter_accepts_default_start(&self) -> bool { fn enter_accepts_default_start(&self) -> bool {
self.pts.is_empty() && !self.choosing_method self.pts.is_empty() && !self.choosing_method && !self.choosing_objects
}
fn is_selection_gathering(&self) -> bool { self.choosing_objects }
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
self.selected_objects = handles.into_iter().filter(|handle| self.convertible.contains_key(handle)).collect();
self.on_enter()
} }
fn on_escape(&mut self) -> CmdResult { fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_undo_step(&mut self) -> Option<CmdResult> {
if self.pts.is_empty() { return None; }
self.pts.pop();
self.end_tangent = Vector3::ZERO;
self.choosing_tangent = false;
if self.pts.is_empty() { self.begin_tangent = Vector3::ZERO; }
Some(CmdResult::NeedPoint)
}
fn wants_text_input(&self) -> bool { fn wants_text_input(&self) -> bool {
true true
} }
@@ -189,7 +314,30 @@ impl CadCommand for SplineCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.choosing_tangent { return None; }
if self.choosing_knots {
self.knot_parameterization = match text.trim().to_ascii_uppercase().as_str() {
"CH" | "CHORD" => 0,
"S" | "SQUARE" | "SQUAREROOT" => 1,
"U" | "UNIFORM" => 2,
_ => return None,
};
self.choosing_knots = false;
return Some(CmdResult::NeedPoint);
}
match text.trim().to_uppercase().as_str() { match text.trim().to_uppercase().as_str() {
"K" | "KNOTS" if self.pts.is_empty() && !self.control_vertices => {
self.choosing_knots = true;
Some(CmdResult::NeedPoint)
}
"T" | "TANGENCY" if !self.pts.is_empty() && !self.control_vertices => {
self.choosing_tangent = true;
Some(CmdResult::NeedPoint)
}
"O" | "OBJECT" if self.pts.is_empty() => {
self.choosing_objects = true;
Some(CmdResult::NeedPoint)
}
"M" | "METHOD" if self.pts.is_empty() => { "M" | "METHOD" if self.pts.is_empty() => {
self.choosing_method = true; self.choosing_method = true;
Some(CmdResult::NeedPoint) Some(CmdResult::NeedPoint)
@@ -204,28 +352,32 @@ impl CadCommand for SplineCommand {
self.choosing_method = false; self.choosing_method = false;
Some(CmdResult::NeedPoint) Some(CmdResult::NeedPoint)
} }
"C" | "CLOSE" => match self.build(true) { "C" | "CLOSE" if self.pts.len() >= 3 => match self.build(true) {
Some(e) => Some(CmdResult::CommitAndExit(e)), Some(e) => Some(CmdResult::CommitAndExit(e)),
None => Some(CmdResult::NeedPoint), None => Some(CmdResult::NeedPoint),
}, },
"U" | "UNDO" => { "U" | "UNDO" => {
self.pts.pop(); self.on_undo_step().or(Some(CmdResult::NeedPoint))
Some(CmdResult::NeedPoint)
} }
_ => None, _ => None,
} }
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if self.pts.is_empty() { if self.pts.is_empty() || self.choosing_method || self.choosing_knots || self.choosing_tangent {
return None; return None;
} }
// Preview the committed construction method. // Preview the committed construction method.
let mut ctrl = self.pts.clone(); self.pts.push(pt);
ctrl.push(pt); let entity = self.build(false);
self.pts.pop();
let Some(EntityType::Spline(spline)) = entity else { return None; };
let points = crate::entities::curve::spline_curve(&spline)
.map(|curve| crate::entities::curve::curve_points(&curve))
.unwrap_or_else(|| crate::entities::spline::measurement_polyline(&spline));
Some(WireModel::solid( Some(WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
sample_curve(&ctrl, false, self.control_vertices), points.into_iter().map(|p| [p[0] as f32, p[1] as f32, p[2] as f32]).collect(),
WireModel::CYAN, WireModel::CYAN,
false, false,
)) ))