forked from 1b-agent/opencadstudio-upstream
Harden spline editing and source joins
This commit is contained in:
1 parent
e11796af29
commit
da34032ffc
5 files changed
+316
-22
No files matched your search
Generated
+2
-2
@@ -894,7 +894,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=2c189defcfd7dad9499746470abb7380fdef0e20#2c189defcfd7dad9499746470abb7380fdef0e20"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=979e668a2a81ef28147b694510b05f6e87bb96c5#979e668a2a81ef28147b694510b05f6e87bb96c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
@@ -905,7 +905,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel-constraints"
|
name = "cadkernel-constraints"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=2c189defcfd7dad9499746470abb7380fdef0e20#2c189defcfd7dad9499746470abb7380fdef0e20"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=979e668a2a81ef28147b694510b05f6e87bb96c5#979e668a2a81ef28147b694510b05f6e87bb96c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -31,8 +31,8 @@ rfd = "0.17"
|
|||||||
clap = { version = "4", features = ["derive", "string"] }
|
clap = { version = "4", features = ["derive", "string"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "b49cbbfc49ba267712045831a601d2f3d0ed386a", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "b49cbbfc49ba267712045831a601d2f3d0ed386a", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "2c189defcfd7dad9499746470abb7380fdef0e20", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "979e668a2a81ef28147b694510b05f6e87bb96c5", features = ["acis", "offset"] }
|
||||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "2c189defcfd7dad9499746470abb7380fdef0e20" }
|
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "979e668a2a81ef28147b694510b05f6e87bb96c5" }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -3111,12 +3111,11 @@ mod tests {
|
|||||||
for command in ["SETBYLAYER", "OVERKILL"] {
|
for command in ["SETBYLAYER", "OVERKILL"] {
|
||||||
let mut app = fresh_app();
|
let mut app = fresh_app();
|
||||||
let _ = app.run_command_line(command);
|
let _ = app.run_command_line(command);
|
||||||
assert_eq!(
|
assert!(
|
||||||
app.tabs[app.active_tab]
|
app.tabs[app.active_tab]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|active| active.name()),
|
.is_some_and(|active| active.is_selection_gathering()),
|
||||||
Some("SELECT"),
|
|
||||||
"{command} should gather an explicit selection"
|
"{command} should gather an explicit selection"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ impl JoinCommand {
|
|||||||
pub fn with_source(mut self, handle: Handle, entity: EntityType) -> Self {
|
pub fn with_source(mut self, handle: Handle, entity: EntityType) -> Self {
|
||||||
self.picked = Some(entity); self.on_entity_pick(handle, DVec3::ZERO); self
|
self.picked = Some(entity); self.on_entity_pick(handle, DVec3::ZERO); self
|
||||||
}
|
}
|
||||||
|
fn has_arc_source(&self) -> bool { matches!(&self.source, Some((_, EntityType::Arc(_)))) }
|
||||||
}
|
}
|
||||||
impl CadCommand for JoinCommand {
|
impl CadCommand for JoinCommand {
|
||||||
fn name(&self) -> &'static str { "JOIN" }
|
fn name(&self) -> &'static str { "JOIN" }
|
||||||
@@ -42,6 +43,8 @@ impl CadCommand for JoinCommand {
|
|||||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||||
if matches!(&self.source,Some((_,EntityType::Arc(_)))) { vec![crate::command::CmdOption::new("Close","L")] } else {Vec::new()}
|
if matches!(&self.source,Some((_,EntityType::Arc(_)))) { vec![crate::command::CmdOption::new("Close","L")] } else {Vec::new()}
|
||||||
}
|
}
|
||||||
|
fn wants_text_input(&self) -> bool { self.has_arc_source() }
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool { self.has_arc_source() }
|
||||||
fn on_text_input(&mut self,text:&str) -> Option<CmdResult> {
|
fn on_text_input(&mut self,text:&str) -> Option<CmdResult> {
|
||||||
if matches!(text.trim().to_ascii_uppercase().as_str(),"L"|"CLOSE") {
|
if matches!(text.trim().to_ascii_uppercase().as_str(),"L"|"CLOSE") {
|
||||||
if let Some((handle,EntityType::Arc(arc)))=&self.source {
|
if let Some((handle,EntityType::Arc(arc)))=&self.source {
|
||||||
@@ -122,7 +125,7 @@ pub fn join_to_source(source: &EntityType, candidates: &[(Handle, &EntityType)])
|
|||||||
spline.control_points=joined.control_points().iter().map(|p|Vector3::new(p[0],p[1],p[2])).collect();
|
spline.control_points=joined.control_points().iter().map(|p|Vector3::new(p[0],p[1],p[2])).collect();
|
||||||
spline.knots=joined.knots().to_vec();spline.weights=joined.weights().to_vec();
|
spline.knots=joined.knots().to_vec();spline.weights=joined.weights().to_vec();
|
||||||
spline.fit_points.clear();spline.flags.rational=joined.is_rational();
|
spline.fit_points.clear();spline.flags.rational=joined.is_rational();
|
||||||
spline.dwg_flags1 &= !1;spline.dxf_flags &= !32;
|
spline.dwg_flags1 &= !1;spline.dxf_flags &= !(32 | 1024);
|
||||||
spline.flags.planar=cadkernel::space::are_coplanar(joined.control_points(),&[]);
|
spline.flags.planar=cadkernel::space::are_coplanar(joined.control_points(),&[]);
|
||||||
spline.begin_tangent=Vector3::new(0.0,0.0,0.0);spline.end_tangent=Vector3::new(0.0,0.0,0.0);
|
spline.begin_tangent=Vector3::new(0.0,0.0,0.0);spline.end_tangent=Vector3::new(0.0,0.0,0.0);
|
||||||
Some(EntityType::Spline(spline))
|
Some(EntityType::Spline(spline))
|
||||||
@@ -511,6 +514,94 @@ mod join_tests {
|
|||||||
l.thickness
|
l.thickness
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_join_consumes_only_compatible_lines_and_keeps_direction() {
|
||||||
|
let source_handle = Handle::new(11);
|
||||||
|
let rejected_handle = Handle::new(12);
|
||||||
|
let joined_handle = Handle::new(13);
|
||||||
|
let mut source = line(5.0, 0.0, 1.0, 0.0, 2.0);
|
||||||
|
source.common_mut().handle = source_handle;
|
||||||
|
let rejected = line(1.0, 0.0, 1.0, 2.0, 0.0);
|
||||||
|
let joined = line(8.0, 0.0, 6.0, 0.0, 0.0);
|
||||||
|
let (result, consumed) = join_to_source(
|
||||||
|
&source,
|
||||||
|
&[(rejected_handle, &rejected), (joined_handle, &joined)],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(consumed, vec![joined_handle]);
|
||||||
|
let EntityType::Line(result) = result else {
|
||||||
|
panic!("expected a line");
|
||||||
|
};
|
||||||
|
assert_eq!(result.common.handle, source_handle);
|
||||||
|
assert_eq!(result.start, Vector3::new(8.0, 0.0, 0.0));
|
||||||
|
assert_eq!(result.end, Vector3::new(1.0, 0.0, 0.0));
|
||||||
|
assert_eq!(result.thickness, 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_join_elevates_splines_and_clears_fit_representation() {
|
||||||
|
let source_handle = Handle::new(21);
|
||||||
|
let candidate_handle = Handle::new(22);
|
||||||
|
let mut source = acadrust::entities::Spline::new();
|
||||||
|
source.common.handle = source_handle;
|
||||||
|
source.degree = 1;
|
||||||
|
source.control_points = vec![
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
Vector3::new(1.0, 0.0, 0.0),
|
||||||
|
];
|
||||||
|
source.knots = vec![0.0, 0.0, 1.0, 1.0];
|
||||||
|
source.fit_points = vec![Vector3::new(0.0, 0.0, 0.0)];
|
||||||
|
source.dwg_flags1 = 1;
|
||||||
|
source.dxf_flags = 32 | 1024;
|
||||||
|
let mut candidate = acadrust::entities::Spline::new();
|
||||||
|
candidate.degree = 3;
|
||||||
|
candidate.control_points = vec![
|
||||||
|
Vector3::new(1.0, 0.0, 0.0),
|
||||||
|
Vector3::new(1.3, 0.2, 0.0),
|
||||||
|
Vector3::new(1.7, 0.8, 0.0),
|
||||||
|
Vector3::new(2.0, 1.0, 0.0),
|
||||||
|
];
|
||||||
|
candidate.knots = vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
|
||||||
|
let candidate_entity = EntityType::Spline(candidate);
|
||||||
|
let (result, consumed) = join_to_source(
|
||||||
|
&EntityType::Spline(source),
|
||||||
|
&[(candidate_handle, &candidate_entity)],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(consumed, vec![candidate_handle]);
|
||||||
|
let EntityType::Spline(result) = result else {
|
||||||
|
panic!("expected a spline");
|
||||||
|
};
|
||||||
|
assert_eq!(result.common.handle, source_handle);
|
||||||
|
assert_eq!(result.degree, 3);
|
||||||
|
assert!(result.fit_points.is_empty());
|
||||||
|
assert_eq!(result.dwg_flags1 & 1, 0);
|
||||||
|
assert_eq!(result.dxf_flags & (32 | 1024), 0);
|
||||||
|
assert!(result.flags.planar);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arc_source_accepts_the_typed_close_option() {
|
||||||
|
let handle = Handle::new(31);
|
||||||
|
let mut arc = acadrust::entities::Arc::new();
|
||||||
|
arc.common.handle = handle;
|
||||||
|
arc.center = Vector3::new(2.0, 3.0, 4.0);
|
||||||
|
arc.radius = 5.0;
|
||||||
|
arc.thickness = 0.75;
|
||||||
|
let mut command = JoinCommand::new().with_source(handle, EntityType::Arc(arc));
|
||||||
|
assert!(command.input_kind().wants_text());
|
||||||
|
assert!(command.point_step_accepts_keywords());
|
||||||
|
let Some(CmdResult::ReplaceMany(mut replacements, _)) = command.on_text_input("Close") else {
|
||||||
|
panic!("expected the arc to close");
|
||||||
|
};
|
||||||
|
let Some(EntityType::Circle(circle)) = replacements[0].1.pop() else {
|
||||||
|
panic!("expected a circle");
|
||||||
|
};
|
||||||
|
assert_eq!(circle.center, Vector3::new(2.0, 3.0, 4.0));
|
||||||
|
assert_eq!(circle.radius, 5.0);
|
||||||
|
assert_eq!(circle.thickness, 0.75);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when every vertex lies on one straight line (within tolerance).
|
/// True when every vertex lies on one straight line (within tolerance).
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ pub struct SplineditCommand {
|
|||||||
join_candidates: Vec<crate::command::SelectionEntity>,
|
join_candidates: Vec<crate::command::SelectionEntity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_fit_method(spline: &mut acadrust::entities::Spline) {
|
||||||
|
spline.fit_points.clear();
|
||||||
|
spline.begin_tangent = Vector3::ZERO;
|
||||||
|
spline.end_tangent = Vector3::ZERO;
|
||||||
|
spline.dwg_flags1 &= !1;
|
||||||
|
spline.dxf_flags &= !(32 | 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weights_are_rational(weights: &[f64]) -> bool {
|
||||||
|
weights.first().is_some_and(|first| {
|
||||||
|
weights
|
||||||
|
.iter()
|
||||||
|
.any(|weight| (*weight - *first).abs() > 1e-12)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl SplineditCommand {
|
impl SplineditCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None, join_candidates: Vec::new() }
|
Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None, join_candidates: Vec::new() }
|
||||||
@@ -126,19 +142,16 @@ impl SplineditCommand {
|
|||||||
source.control_points.iter().map(|point| [point.x, point.y, point.z]).collect(),
|
source.control_points.iter().map(|point| [point.x, point.y, point.z]).collect(),
|
||||||
source.knots.clone(), weights)?
|
source.knots.clone(), weights)?
|
||||||
};
|
};
|
||||||
let curve = curve.with_periodicity(source.flags.closed || source.flags.periodic)
|
let curve = curve.with_periodicity(source.flags.closed || source.flags.periodic);
|
||||||
.elevated(degree - current)?
|
let elevation = degree.checked_sub(curve.degree())?;
|
||||||
|
let curve = if elevation == 0 { curve } else { curve.elevated(elevation)? }
|
||||||
.compact_knots(source.control_tolerance.max(1e-9))?;
|
.compact_knots(source.control_tolerance.max(1e-9))?;
|
||||||
let mut result = source.clone();
|
let mut result = source.clone();
|
||||||
result.degree = curve.degree() as i32;
|
result.degree = curve.degree() as i32;
|
||||||
result.control_points = curve.control_points().iter().map(|point| Vector3::new(point[0], point[1], point[2])).collect();
|
result.control_points = curve.control_points().iter().map(|point| Vector3::new(point[0], point[1], point[2])).collect();
|
||||||
result.knots = curve.knots().to_vec();
|
result.knots = curve.knots().to_vec();
|
||||||
result.weights = curve.weights().to_vec();
|
result.weights = curve.weights().to_vec();
|
||||||
result.fit_points.clear();
|
clear_fit_method(&mut result);
|
||||||
result.begin_tangent = Vector3::ZERO;
|
|
||||||
result.end_tangent = Vector3::ZERO;
|
|
||||||
result.dwg_flags1 &= !1;
|
|
||||||
result.dxf_flags &= !(32 | 1024);
|
|
||||||
result.flags.rational = curve.is_rational();
|
result.flags.rational = curve.is_rational();
|
||||||
result.flags.planar = crate::entities::curve::spline_is_planar(&result);
|
result.flags.planar = crate::entities::curve::spline_is_planar(&result);
|
||||||
return Some(result);
|
return Some(result);
|
||||||
@@ -158,6 +171,9 @@ impl SplineditCommand {
|
|||||||
let world = planar.plane.point_at(*point);
|
let world = planar.plane.point_at(*point);
|
||||||
Vector3::new(world[0], world[1], world[2])
|
Vector3::new(world[0], world[1], world[2])
|
||||||
}).collect();
|
}).collect();
|
||||||
|
clear_fit_method(&mut result);
|
||||||
|
result.flags.rational = curve.is_rational();
|
||||||
|
result.flags.planar = crate::entities::curve::spline_is_planar(&result);
|
||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +278,10 @@ impl CadCommand for SplineditCommand {
|
|||||||
},
|
},
|
||||||
Step::Elevate => {
|
Step::Elevate => {
|
||||||
let current_order = self.spline.as_ref()?.degree.max(1) as usize + 1;
|
let current_order = self.spline.as_ref()?.degree.max(1) as usize + 1;
|
||||||
let order = upper.parse::<usize>().ok().filter(|order| *order >= current_order && *order <= 26)?;
|
let Some(order) = upper.parse::<usize>().ok()
|
||||||
|
.filter(|order| *order >= current_order && *order <= 26) else {
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
};
|
||||||
if order == current_order {
|
if order == current_order {
|
||||||
self.step = Step::Refine;
|
self.step = Step::Refine;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
@@ -290,8 +309,8 @@ impl CadCommand for SplineditCommand {
|
|||||||
let mut spline = self.spline.clone()?;
|
let mut spline = self.spline.clone()?;
|
||||||
spline.weights.resize(count, 1.0);
|
spline.weights.resize(count, 1.0);
|
||||||
spline.weights[index] = weight;
|
spline.weights[index] = weight;
|
||||||
spline.flags.rational = true;
|
spline.flags.rational = weights_are_rational(&spline.weights);
|
||||||
spline.fit_points.clear();
|
clear_fit_method(&mut spline);
|
||||||
return Some(self.replace(spline));
|
return Some(self.replace(spline));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,7 +348,8 @@ impl CadCommand for SplineditCommand {
|
|||||||
spline.control_points = curve.control_points().iter().map(|point| Vector3::new(point[0], point[1], point[2])).collect();
|
spline.control_points = curve.control_points().iter().map(|point| Vector3::new(point[0], point[1], point[2])).collect();
|
||||||
spline.weights = curve.weights().to_vec();
|
spline.weights = curve.weights().to_vec();
|
||||||
spline.knots = curve.knots().to_vec();
|
spline.knots = curve.knots().to_vec();
|
||||||
spline.fit_points.clear();
|
clear_fit_method(&mut spline);
|
||||||
|
spline.flags.rational = curve.is_rational();
|
||||||
spline.flags.planar = crate::entities::curve::spline_is_planar(&spline);
|
spline.flags.planar = crate::entities::curve::spline_is_planar(&spline);
|
||||||
self.replace(spline)
|
self.replace(spline)
|
||||||
}
|
}
|
||||||
@@ -338,7 +358,8 @@ impl CadCommand for SplineditCommand {
|
|||||||
let Some(mut spline) = self.spline.clone() else { return CmdResult::NeedPoint; };
|
let Some(mut spline) = self.spline.clone() else { return CmdResult::NeedPoint; };
|
||||||
let Some(vertex) = spline.control_points.get_mut(index) else { return CmdResult::NeedPoint; };
|
let Some(vertex) = spline.control_points.get_mut(index) else { return CmdResult::NeedPoint; };
|
||||||
*vertex = Vector3::new(point.x, point.y, point.z);
|
*vertex = Vector3::new(point.x, point.y, point.z);
|
||||||
spline.fit_points.clear();
|
clear_fit_method(&mut spline);
|
||||||
|
spline.flags.planar = crate::entities::curve::spline_is_planar(&spline);
|
||||||
self.replace(spline)
|
self.replace(spline)
|
||||||
}
|
}
|
||||||
_ => CmdResult::NeedPoint,
|
_ => CmdResult::NeedPoint,
|
||||||
@@ -418,7 +439,7 @@ fn change_closure(source: &acadrust::entities::Spline, closed: bool) -> Option<a
|
|||||||
} else if closed {
|
} else if closed {
|
||||||
NurbsCurve3::from_weighted_control_polygon(degree, &controls, &weights, true)?
|
NurbsCurve3::from_weighted_control_polygon(degree, &controls, &weights, true)?
|
||||||
} else {
|
} else {
|
||||||
let curve = stored_curve()?;
|
let curve = stored_curve()?.with_periodicity(false);
|
||||||
curve.without_control_vertex(controls.len().checked_sub(1)?)?
|
curve.without_control_vertex(controls.len().checked_sub(1)?)?
|
||||||
};
|
};
|
||||||
let mut result = source.clone();
|
let mut result = source.clone();
|
||||||
@@ -429,7 +450,7 @@ fn change_closure(source: &acadrust::entities::Spline, closed: bool) -> Option<a
|
|||||||
result.flags.closed = closed;
|
result.flags.closed = closed;
|
||||||
result.flags.periodic = closed;
|
result.flags.periodic = closed;
|
||||||
if closed { result.dxf_flags |= 2048; } else { result.dxf_flags &= !2048; }
|
if closed { result.dxf_flags |= 2048; } else { result.dxf_flags &= !2048; }
|
||||||
result.flags.rational = if fit_method { false } else { source.flags.rational || curve.is_rational() };
|
result.flags.rational = !fit_method && curve.is_rational();
|
||||||
result.fit_points = if !closed && fit_method {
|
result.fit_points = if !closed && fit_method {
|
||||||
fit_points.iter().map(|point| Vector3::new(point[0], point[1], point[2])).collect()
|
fit_points.iter().map(|point| Vector3::new(point[0], point[1], point[2])).collect()
|
||||||
} else { Vec::new() };
|
} else { Vec::new() };
|
||||||
@@ -443,5 +464,188 @@ fn change_closure(source: &acadrust::entities::Spline, closed: bool) -> Option<a
|
|||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use acadrust::entities::Spline;
|
||||||
|
use acadrust::Handle;
|
||||||
|
use iced::Rectangle;
|
||||||
|
|
||||||
|
fn control_spline(points: &[[f64; 3]], degree: usize) -> Spline {
|
||||||
|
let mut spline = Spline::new();
|
||||||
|
spline.degree = degree as i32;
|
||||||
|
spline.control_points = points
|
||||||
|
.iter()
|
||||||
|
.map(|point| Vector3::new(point[0], point[1], point[2]))
|
||||||
|
.collect();
|
||||||
|
spline.knots = cadkernel::space::clamped_uniform_knots(degree, points.len());
|
||||||
|
spline
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_command(spline: Spline) -> SplineditCommand {
|
||||||
|
let mut command = SplineditCommand::new();
|
||||||
|
command.inject_picked_entity(EntityType::Spline(spline));
|
||||||
|
command.on_entity_pick(Handle::new(7), DVec3::ZERO);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replacement(result: CmdResult) -> Spline {
|
||||||
|
let CmdResult::ReplaceManyContinue(mut replacements) = result else {
|
||||||
|
panic!("expected a continuing replacement");
|
||||||
|
};
|
||||||
|
let Some(EntityType::Spline(spline)) = replacements[0].1.pop() else {
|
||||||
|
panic!("expected a spline replacement");
|
||||||
|
};
|
||||||
|
spline
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moving_a_control_vertex_clears_fit_state_and_recomputes_planarity() {
|
||||||
|
let mut spline = control_spline(
|
||||||
|
&[
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[2.0, 1.0, 0.0],
|
||||||
|
[3.0, 0.0, 0.0],
|
||||||
|
],
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
spline.fit_points = vec![Vector3::new(0.0, 0.0, 0.0)];
|
||||||
|
spline.begin_tangent = Vector3::new(1.0, 0.0, 0.0);
|
||||||
|
spline.end_tangent = Vector3::new(1.0, 0.0, 0.0);
|
||||||
|
spline.dwg_flags1 = 1;
|
||||||
|
spline.dxf_flags = 32 | 1024;
|
||||||
|
spline.flags.planar = true;
|
||||||
|
let mut command = selected_command(spline);
|
||||||
|
command.on_text_input("MOVE");
|
||||||
|
let moved = replacement(command.on_point(DVec3::new(0.0, 0.0, 1.0)));
|
||||||
|
assert!(moved.fit_points.is_empty());
|
||||||
|
assert_eq!(moved.begin_tangent, Vector3::ZERO);
|
||||||
|
assert_eq!(moved.end_tangent, Vector3::ZERO);
|
||||||
|
assert_eq!(moved.dwg_flags1 & 1, 0);
|
||||||
|
assert_eq!(moved.dxf_flags & (32 | 1024), 0);
|
||||||
|
assert!(!moved.flags.planar);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn equal_control_weights_are_not_marked_rational() {
|
||||||
|
let spline = control_spline(
|
||||||
|
&[
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[1.0, 1.0, 0.0],
|
||||||
|
[2.0, 1.0, 0.0],
|
||||||
|
[3.0, 0.0, 0.0],
|
||||||
|
],
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
let mut command = selected_command(spline);
|
||||||
|
command.on_text_input("REFINE");
|
||||||
|
command.on_text_input("WEIGHT");
|
||||||
|
let weighted = replacement(command.on_text_input("1").unwrap());
|
||||||
|
assert!(!weighted.flags.rational);
|
||||||
|
assert_eq!(weighted.weights, vec![1.0; 4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleting_the_only_weighted_vertex_clears_rational_and_fit_flags() {
|
||||||
|
let mut spline = control_spline(
|
||||||
|
&[
|
||||||
|
[-0.6, 0.0, 0.0],
|
||||||
|
[-0.2, 0.4, 0.0],
|
||||||
|
[0.2, 0.4, 0.0],
|
||||||
|
[0.6, 0.0, 0.0],
|
||||||
|
],
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
spline.weights = vec![1.0, 2.0, 1.0, 1.0];
|
||||||
|
spline.flags.rational = true;
|
||||||
|
spline.dwg_flags1 = 1;
|
||||||
|
spline.dxf_flags = 32 | 1024;
|
||||||
|
let mut command = selected_command(spline);
|
||||||
|
command.on_text_input("REFINE");
|
||||||
|
command.on_text_input("DELETE");
|
||||||
|
command.pick_context = Some(crate::command::PointPickContext {
|
||||||
|
view: glam::Mat4::IDENTITY,
|
||||||
|
eye: DVec3::ZERO,
|
||||||
|
bounds: Rectangle::new(iced::Point::ORIGIN, iced::Size::new(100.0, 100.0)),
|
||||||
|
aperture_px: 2.0,
|
||||||
|
});
|
||||||
|
let deleted = replacement(command.on_point(DVec3::new(-0.2, 0.4, 0.0)));
|
||||||
|
assert_eq!(deleted.control_points.len(), 3);
|
||||||
|
assert!(!deleted.flags.rational);
|
||||||
|
assert_eq!(deleted.dwg_flags1 & 1, 0);
|
||||||
|
assert_eq!(deleted.dxf_flags & (32 | 1024), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fit_only_degree_elevation_uses_the_interpolated_curve_degree() {
|
||||||
|
let mut spline = Spline::new();
|
||||||
|
spline.degree = 1;
|
||||||
|
spline.fit_points = vec![
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
Vector3::new(1.0, 2.0, 0.0),
|
||||||
|
Vector3::new(3.0, 0.0, 0.0),
|
||||||
|
];
|
||||||
|
let mut command = selected_command(spline);
|
||||||
|
command.on_text_input("REFINE");
|
||||||
|
command.on_text_input("ELEVATE");
|
||||||
|
assert!(matches!(command.on_text_input("invalid"), Some(CmdResult::NeedPoint)));
|
||||||
|
assert!(matches!(command.step, Step::Elevate));
|
||||||
|
let elevated = replacement(command.on_text_input("5").unwrap());
|
||||||
|
assert_eq!(elevated.degree, 4);
|
||||||
|
assert!(elevated.fit_points.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_closed_weighted_control_curve_can_be_opened() {
|
||||||
|
let curve = cadkernel::space::NurbsCurve3::from_weighted_control_polygon(
|
||||||
|
2,
|
||||||
|
&[
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[2.0, 0.0, 0.0],
|
||||||
|
[2.0, 2.0, 1.0],
|
||||||
|
[0.0, 2.0, 0.0],
|
||||||
|
],
|
||||||
|
&[1.0, 2.0, 0.75, 1.5],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut spline = control_spline(curve.control_points(), curve.degree());
|
||||||
|
spline.knots = curve.knots().to_vec();
|
||||||
|
spline.weights = curve.weights().to_vec();
|
||||||
|
spline.flags.closed = true;
|
||||||
|
spline.flags.periodic = true;
|
||||||
|
spline.flags.rational = true;
|
||||||
|
let opened = change_closure(&spline, false).expect("closed control curve opens");
|
||||||
|
assert!(!opened.flags.closed);
|
||||||
|
assert!(!opened.flags.periodic);
|
||||||
|
assert!(cadkernel::space::NurbsCurve3::new_strict(
|
||||||
|
opened.degree as usize,
|
||||||
|
opened
|
||||||
|
.control_points
|
||||||
|
.iter()
|
||||||
|
.map(|point| [point.x, point.y, point.z])
|
||||||
|
.collect(),
|
||||||
|
opened.knots.clone(),
|
||||||
|
opened.weights.clone(),
|
||||||
|
)
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_vertex_pick_uses_the_screen_aperture() {
|
||||||
|
let spline = control_spline(&[[-0.5, 0.0, 0.0], [0.5, 0.0, 0.0]], 1);
|
||||||
|
let mut command = selected_command(spline);
|
||||||
|
command.pick_context = Some(crate::command::PointPickContext {
|
||||||
|
view: glam::Mat4::IDENTITY,
|
||||||
|
eye: DVec3::ZERO,
|
||||||
|
bounds: Rectangle::new(iced::Point::ORIGIN, iced::Size::new(100.0, 100.0)),
|
||||||
|
aperture_px: 2.0,
|
||||||
|
});
|
||||||
|
assert_eq!(command.picked_vertex(DVec3::new(-0.49, 0.0, 0.0)), Some(0));
|
||||||
|
assert_eq!(command.picked_vertex(DVec3::new(0.0, 0.5, 0.0)), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["SPLINEDIT"] }); // SplineditCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["SPLINEDIT"] }); // SplineditCommand
|
||||||
Reference in new issue
Block a user