From d8790ef433c7a3d751b4cf4e58cc4fba84dae580 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 4 Sep 2026 19:34:38 +0300 Subject: [PATCH] Use exact kernel curves for PRESSPULL area containment --- src/app/presspull_ops.rs | 2 +- src/scene/model/presspull_model.rs | 28 ++++++++++--------- tests/presspull_curved_boundary.rs | 43 ++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 tests/presspull_curved_boundary.rs diff --git a/src/app/presspull_ops.rs b/src/app/presspull_ops.rs index e9e729946..a2fc64a79 100644 --- a/src/app/presspull_ops.rs +++ b/src/app/presspull_ops.rs @@ -237,7 +237,7 @@ impl super::OpenCADStudio { if let Some(command) = self.tabs[i].active_cmd.as_mut() { command.on_presspull_applied(true); } - self.command_line.push_output(crate::tf!("PRESSPULL: %{count} object(s) updated.", count = handles.len()).as_ref()); + self.command_line.push_output(crate::tf!("PRESSPULL: {count} object(s) updated.", count = handles.len()).as_ref()); self.refresh_properties(); self.refresh_presspull_prompt(); } diff --git a/src/scene/model/presspull_model.rs b/src/scene/model/presspull_model.rs index 6db93beeb..151b15e95 100644 --- a/src/scene/model/presspull_model.rs +++ b/src/scene/model/presspull_model.rs @@ -141,24 +141,20 @@ fn boundary_sources(scene: &Scene, plane: WorkingPlane) -> FxHashMap Vec { - ring.iter().copied().zip(ring.iter().copied().cycle().skip(1)) - .take(ring.len()).map(|(start, end)| Curve::Line(Line { start, end })).collect() -} - fn selected_rings(sources: &FxHashMap, point: [f64; 2]) -> Option>> { let rings = boundary_faces(sources, BOUNDARY_TOLERANCE); + let loops = boundary_loops(sources, &rings)?; let tolerance = Tolerance::new(BOUNDARY_TOLERANCE); let outer = rings.iter().enumerate() - .filter(|(_, ring)| contains(&ring_curves(ring), point, tolerance)) + .filter(|(index, _)| contains(&loops[*index], point, tolerance)) .min_by(|(_, left), (_, right)| signed_area(left).abs().total_cmp(&signed_area(right).abs()))? .0; let depths = ring_nesting_depths(&rings); - let boundary = ring_curves(&rings[outer]); + let boundary = &loops[outer]; let mut result = vec![rings[outer].clone()]; for (index, ring) in rings.iter().enumerate() { if index != outer && depths.get(index) == depths.get(outer).map(|depth| depth + 1).as_ref() - && ring.first().is_some_and(|point| contains(&boundary, *point, tolerance)) + && ring.first().is_some_and(|point| contains(boundary, *point, tolerance)) { result.push(ring.clone()); } @@ -166,17 +162,23 @@ fn selected_rings(sources: &FxHashMap, point: [f64; 2]) Some(result) } -fn boundary_region( - sources: &FxHashMap, rings: &[Vec<[f64; 2]>], plane: WorkingPlane, -) -> Option { +fn boundary_loops( + sources: &FxHashMap, rings: &[Vec<[f64; 2]>], +) -> Option>> { let exterior = (0..rings.len()).map(|index| index == 0).collect::>(); let paths = exact_hatch_paths(rings, &exterior, sources, BOUNDARY_TOLERANCE); if paths.len() != rings.len() { return None; } - let loops = paths.iter().map(|path| { + paths.iter().map(|path| { path.edges.iter().map(crate::entities::hatch::edge_curve).collect::>>() - }).collect::>>()?; + }).collect() +} + +fn boundary_region( + sources: &FxHashMap, rings: &[Vec<[f64; 2]>], plane: WorkingPlane, +) -> Option { + let loops = boundary_loops(sources, rings)?; let kernel_plane = Plane::from_axes(plane.origin.to_array(), plane.x.to_array(), plane.y.to_array()); let sheet = brep::planar_region(kernel_plane, &loops)?; let document = crate::scene::convert::acis_export::solid_to_sat(&sheet)?; diff --git a/tests/presspull_curved_boundary.rs b/tests/presspull_curved_boundary.rs new file mode 100644 index 000000000..7bf2d9d15 --- /dev/null +++ b/tests/presspull_curved_boundary.rs @@ -0,0 +1,43 @@ +use acadrust::{entities::Circle, types::Vector3, EntityType}; +use glam::DVec3; +use OpenCADStudio::command::WorkingPlane; +use OpenCADStudio::scene::{model::presspull_model, Scene}; + +#[test] +fn area_picks_follow_exact_curves_and_keep_holes() { + let mut scene = Scene::new(); + for radius in [1000.0, 500.0] { + scene + .document + .add_entity(EntityType::Circle(Circle::from_center_radius( + Vector3::new(0.0, 0.0, 0.0), + radius, + ))) + .unwrap(); + } + // Halfway between display vertices, these points lie beyond the chords. + let angle = std::f64::consts::PI / 48.0; + for (radius, expected_loops) in [(999.0, 2), (499.0, 1)] { + let point = DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0); + let target = + presspull_model::resolve_target(&scene, None, point, WorkingPlane::default(), false) + .expect("point inside the exact circular boundary"); + let presspull_model::PresspullTargetKind::Profile { entity, .. } = target.kind else { + panic!("expected a bounded profile"); + }; + let (_, loops, closed) = presspull_model::profile_geometry(&entity).unwrap(); + assert!(closed); + assert_eq!(loops.len(), expected_loops); + assert!(loops + .iter() + .flatten() + .all(|curve| !matches!(curve, cadkernel::geom2d::Curve::Line(_)))); + let body = presspull_model::extrusion_body(&entity, [0.0, 0.0, 10.0]).unwrap(); + assert!(body.validate().is_empty()); + } + let outside = DVec3::new(1001.0 * angle.cos(), 1001.0 * angle.sin(), 0.0); + assert!( + presspull_model::resolve_target(&scene, None, outside, WorkingPlane::default(), false,) + .is_err() + ); +}