diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index a59efadb8..d4fb22f16 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -2544,16 +2544,37 @@ impl OpenCADStudio { .filter(|handle| { matches!( scene.document.get_entity(*handle), - Some(codec::EntityType::Solid3D(_) | codec::EntityType::Surface(_)) + Some( + codec::EntityType::Solid3D(_) + | codec::EntityType::Surface(_) + | codec::EntityType::Mesh(_) + | codec::EntityType::PolygonMesh(_) + | codec::EntityType::PolyfaceMesh(_) + ) ) }) .collect::>(); scene.restore_solid_models(&targets); - targets.retain(|handle| scene.solid_models.contains_key(handle)); + let mesh_extent = |handle: &codec::Handle| { + let entity = scene.document.get_entity(*handle)?; + let (vertices, _) = crate::entities::mesh::mesh_polygons(entity)?; + vertices.iter().fold(None, |bounds, point| { + let (mut min, mut max) = bounds.unwrap_or((*point, *point)); + for axis in 0..3 { + min[axis] = f64::min(min[axis], point[axis]); + max[axis] = f64::max(max[axis], point[axis]); + } + Some((min, max)) + }) + }; + targets.retain(|handle| { + scene.solid_models.contains_key(handle) || mesh_extent(handle).is_some() + }); let bounds = targets .iter() - .filter_map(|handle| { - crate::scene::model::solid_model::extent(&scene.solid_models[handle]) + .filter_map(|handle| match scene.solid_models.get(handle) { + Some(body) => crate::scene::model::solid_model::extent(body), + None => mesh_extent(handle), }) .fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| { Some(match bounds { diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 757d0e5c2..c7a44f5ba 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -1741,6 +1741,80 @@ impl super::OpenCADStudio { self.solid_slice_with_tool(requested, ModelSliceTool::Surface(cutter), keep_point) } + /// SLICE on meshes that do not close a volume: the kept side of each face + /// stays (or, with no side chosen, both sides become separate meshes), + /// open along the cut. One undo step; `None` when nothing was changed. + fn slice_open_meshes( + &mut self, + handles: &[Handle], + tool: &ModelSliceTool, + keep_point: Option, + ) -> Option> { + let i = self.active_tab; + let keep_positive = match keep_point { + Some(point) => match tool.side(point.to_array()) { + Some(distance) if distance.abs() > 1.0e-9 => Some(distance > 0.0), + _ => { + self.command_line.push_error( + crate::t!("SLICE: choose a point away from the cutting plane.").as_ref(), + ); + return None; + } + }, + None => None, + }; + let side = |point: [f64; 3]| tool.side(point).unwrap_or(0.0); + let mut pieces = Vec::with_capacity(handles.len()); + for handle in handles { + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else { + continue; + }; + let Some((vertices, faces)) = crate::entities::mesh::mesh_polygons(entity) else { + continue; + }; + let common = entity.common(); + let clip = |positive| { + crate::entities::mesh::clipped_mesh(&vertices, &faces, side, positive, common) + }; + let (kept, other) = match keep_positive { + Some(positive) => (clip(positive), None), + None => (clip(false), Some(clip(true))), + }; + // Both sides must have faces, or the plane missed this mesh. + let crosses = vertices.iter().any(|&point| side(point) > 0.0) + && vertices.iter().any(|&point| side(point) < 0.0); + let (Some(kept), true) = (kept, crosses) else { + self.command_line.push_error( + crate::t!("SLICE: the cutting plane does not cross every selected object; no object was changed.") + .as_ref(), + ); + return None; + }; + pieces.push((*handle, kept, other.flatten())); + } + self.push_undo_snapshot(i, "SLICE"); + let mut results = Vec::with_capacity(pieces.len() * 2); + for (handle, mut kept, other) in pieces { + kept.common.handle = handle; + self.tabs[i].scene.update_entity(EntityType::Mesh(kept)); + results.push(handle); + if let Some(mut other) = other { + other.common.handle = Handle::NULL; + results.push(self.tabs[i].scene.add_entity(EntityType::Mesh(other))); + } + } + self.tabs[i].scene.deselect_all(); + for handle in &results { + self.tabs[i].scene.select_entity(*handle, false); + } + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_output( + crate::t!("SLICE: created %{count} result object(s).", count = results.len()).as_ref(), + ); + Some(results) + } + fn solid_slice_with_tool( &mut self, requested: &[Handle], @@ -1755,14 +1829,50 @@ impl super::OpenCADStudio { .filter(|handle| { matches!( self.tabs[i].scene.document.get_entity(*handle), - Some(EntityType::Solid3D(_) | EntityType::Surface(_)) + Some( + EntityType::Solid3D(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) + ) ) }) .collect::>(); handles.sort_unstable_by_key(|handle| handle.value()); handles.dedup(); + // A mesh that closes a volume is sliced as a solid and capped; an open + // one is clipped face by face and stays a mesh. + let mut bodies = rustc_hash::FxHashMap::default(); + let mut open_meshes = Vec::new(); + for handle in &handles { + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else { + continue; + }; + if crate::entities::mesh::mesh_polygons(entity).is_none() { + continue; + } + match crate::entities::mesh::closed_mesh_body(entity) { + Some(body) => { + bodies.insert(*handle, body); + } + None => open_meshes.push(*handle), + } + } + handles.retain(|handle| !open_meshes.contains(handle)); self.tabs[i].scene.restore_solid_models(&handles); - handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); + for handle in &handles { + if let Some(body) = self.tabs[i].scene.solid_models.get(handle) { + bodies.entry(*handle).or_insert_with(|| body.clone()); + } + } + handles.retain(|handle| bodies.contains_key(handle)); + if !open_meshes.is_empty() { + let sliced = self.slice_open_meshes(&open_meshes, &tool, keep_point); + if handles.is_empty() || sliced.is_none() { + return Task::none(); + } + } if handles.is_empty() { self.command_line .push_error(crate::t!("SLICE: select at least one solid or surface.").as_ref()); @@ -1771,10 +1881,7 @@ impl super::OpenCADStudio { let keep_positive = if let Some(point) = keep_point { let tolerance = kernel::brep::operation_tolerance( - &handles - .iter() - .map(|handle| &self.tabs[i].scene.solid_models[handle]) - .collect::>(), + &handles.iter().map(|handle| &bodies[handle]).collect::>(), ); let Some(distance) = tool.side(point.to_array()) else { self.command_line @@ -1794,8 +1901,7 @@ impl super::OpenCADStudio { let mut prepared = Vec::with_capacity(handles.len()); for handle in &handles { - let body = &self.tabs[i].scene.solid_models[handle]; - let result = match tool.split(body) { + let result = match tool.split(&bodies[handle]) { Ok(Some(result)) => result, Ok(None) => { self.command_line.push_error( diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index ae173b81a..d134643b2 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -1054,15 +1054,26 @@ fn display_mesh(mesh: &Mesh) -> RefinedMesh { /// Open, non-manifold, non-planar, and degenerate meshes return `None` so a /// modelling command can leave the source untouched. pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option { + if let codec::EntityType::PolygonMesh(mesh) = entity { + if !mesh.is_closed_m() || !mesh.is_closed_n() { + return None; + } + } + let (vertices, faces) = mesh_polygons(entity)?; + kernel::brep::make::faceted_solid(&vertices, &faces) +} + +/// The vertices and face loops (vertex indices) of a MESH, polygon mesh or +/// polyface mesh, as drawn without smoothing. +pub(crate) fn mesh_polygons( + entity: &codec::EntityType, +) -> Option<(Vec<[f64; 3]>, Vec>)> { match entity { codec::EntityType::Mesh(mesh) => { let base = base_refined_mesh(mesh); - kernel::brep::make::faceted_solid(&base.vertices, &base.faces) + Some((base.vertices, base.faces)) } codec::EntityType::PolygonMesh(mesh) => { - if !mesh.is_closed_m() || !mesh.is_closed_n() { - return None; - } let m = mesh.m_vertex_count.max(0) as usize; let n = mesh.n_vertex_count.max(0) as usize; let vertices = mesh @@ -1074,9 +1085,11 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option Option { let vertices = mesh @@ -1105,12 +1118,62 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option= 3).then_some(indices) }) .collect::>(); - kernel::brep::make::faceted_solid(&vertices, &faces) + Some((vertices, faces)) } _ => None, } } +/// The part of a mesh on one side of a cut: every face loop clipped to where +/// `side` is positive (`keep_positive`) or negative, rebuilt as a MESH with +/// `common`. The mesh stays open where the cut crossed it. `None` when no face +/// reaches that side. +pub(crate) fn clipped_mesh( + vertices: &[[f64; 3]], + faces: &[Vec], + side: impl Fn([f64; 3]) -> f64, + keep_positive: bool, + common: &codec::entities::EntityCommon, +) -> Option { + let sign = if keep_positive { 1.0 } else { -1.0 }; + let distances: Vec = vertices.iter().map(|&point| sign * side(point)).collect(); + let mut mesh = Mesh::new(); + mesh.common = common.clone(); + let mut welded: std::collections::HashMap<[u64; 3], usize> = Default::default(); + let mut index_of = |mesh: &mut Mesh, point: [f64; 3]| { + *welded.entry(point.map(f64::to_bits)).or_insert_with(|| { + mesh.add_vertex(codec::types::Vector3::new(point[0], point[1], point[2])) + }) + }; + for face in faces { + if face.iter().any(|&index| index >= vertices.len()) { + continue; + } + let mut loop_points = Vec::with_capacity(face.len() + 2); + for (k, &a) in face.iter().enumerate() { + let b = face[(k + 1) % face.len()]; + let (da, db) = (distances[a], distances[b]); + if da >= 0.0 { + loop_points.push(vertices[a]); + } + if (da > 0.0 && db < 0.0) || (da < 0.0 && db > 0.0) { + let t = da / (da - db); + let (pa, pb) = (vertices[a], vertices[b]); + loop_points.push(std::array::from_fn(|axis| pa[axis] + (pb[axis] - pa[axis]) * t)); + } + } + if loop_points.len() < 3 { + continue; + } + let indices = loop_points + .into_iter() + .map(|point| index_of(&mut mesh, point)) + .collect::>(); + mesh.add_face(codec::entities::MeshFace::new(indices)); + } + (!mesh.faces.is_empty()).then_some(mesh) +} + fn face_triangle_indices(vertices: &[[f64; 3]], faces: &[Vec]) -> (Vec, Vec) { let mut indices = Vec::new(); let mut triangle_faces = Vec::new(); diff --git a/src/modules/model/slice_cmd.rs b/src/modules/model/slice_cmd.rs index 4b354252f..6c5b111de 100644 --- a/src/modules/model/slice_cmd.rs +++ b/src/modules/model/slice_cmd.rs @@ -335,8 +335,15 @@ impl CadCommand for SliceCommand { self.selected = entities .into_iter() .filter_map(|item| { - matches!(item.entity, EntityType::Solid3D(_) | EntityType::Surface(_)) - .then_some(item.handle) + matches!( + item.entity, + EntityType::Solid3D(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) + ) + .then_some(item.handle) }) .collect(); }