forked from 1b-agent/opencadstudio-upstream
feat(model): SLICE cuts meshes
SLICE now takes MESH, polygon mesh and polyface mesh objects. A mesh that closes a volume is sliced as a solid, so each side is capped and becomes a 3D solid. An open mesh, such as a pipe run imported without end caps, is clipped face by face and each side stays a mesh, open along the cut. Keeping one side or both works as for solids, in one undo step. Refs #1398 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e55bcd3bf
commit
5e6290ce30
4 files changed
+220
-23
No files matched your search
@@ -2544,16 +2544,37 @@ impl OpenCADStudio {
|
|||||||
.filter(|handle| {
|
.filter(|handle| {
|
||||||
matches!(
|
matches!(
|
||||||
scene.document.get_entity(*handle),
|
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::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
scene.restore_solid_models(&targets);
|
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
|
let bounds = targets
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|handle| {
|
.filter_map(|handle| match scene.solid_models.get(handle) {
|
||||||
crate::scene::model::solid_model::extent(&scene.solid_models[handle])
|
Some(body) => crate::scene::model::solid_model::extent(body),
|
||||||
|
None => mesh_extent(handle),
|
||||||
})
|
})
|
||||||
.fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| {
|
.fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| {
|
||||||
Some(match bounds {
|
Some(match bounds {
|
||||||
|
|||||||
+114
-8
@@ -1741,6 +1741,80 @@ impl super::OpenCADStudio {
|
|||||||
self.solid_slice_with_tool(requested, ModelSliceTool::Surface(cutter), keep_point)
|
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<glam::DVec3>,
|
||||||
|
) -> Option<Vec<Handle>> {
|
||||||
|
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(
|
fn solid_slice_with_tool(
|
||||||
&mut self,
|
&mut self,
|
||||||
requested: &[Handle],
|
requested: &[Handle],
|
||||||
@@ -1755,14 +1829,50 @@ impl super::OpenCADStudio {
|
|||||||
.filter(|handle| {
|
.filter(|handle| {
|
||||||
matches!(
|
matches!(
|
||||||
self.tabs[i].scene.document.get_entity(*handle),
|
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::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
handles.sort_unstable_by_key(|handle| handle.value());
|
handles.sort_unstable_by_key(|handle| handle.value());
|
||||||
handles.dedup();
|
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);
|
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() {
|
if handles.is_empty() {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("SLICE: select at least one solid or surface.").as_ref());
|
.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 keep_positive = if let Some(point) = keep_point {
|
||||||
let tolerance = kernel::brep::operation_tolerance(
|
let tolerance = kernel::brep::operation_tolerance(
|
||||||
&handles
|
&handles.iter().map(|handle| &bodies[handle]).collect::<Vec<_>>(),
|
||||||
.iter()
|
|
||||||
.map(|handle| &self.tabs[i].scene.solid_models[handle])
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
);
|
);
|
||||||
let Some(distance) = tool.side(point.to_array()) else {
|
let Some(distance) = tool.side(point.to_array()) else {
|
||||||
self.command_line
|
self.command_line
|
||||||
@@ -1794,8 +1901,7 @@ impl super::OpenCADStudio {
|
|||||||
|
|
||||||
let mut prepared = Vec::with_capacity(handles.len());
|
let mut prepared = Vec::with_capacity(handles.len());
|
||||||
for handle in &handles {
|
for handle in &handles {
|
||||||
let body = &self.tabs[i].scene.solid_models[handle];
|
let result = match tool.split(&bodies[handle]) {
|
||||||
let result = match tool.split(body) {
|
|
||||||
Ok(Some(result)) => result,
|
Ok(Some(result)) => result,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
|
|||||||
+72
-9
@@ -1054,15 +1054,26 @@ fn display_mesh(mesh: &Mesh) -> RefinedMesh {
|
|||||||
/// Open, non-manifold, non-planar, and degenerate meshes return `None` so a
|
/// Open, non-manifold, non-planar, and degenerate meshes return `None` so a
|
||||||
/// modelling command can leave the source untouched.
|
/// modelling command can leave the source untouched.
|
||||||
pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option<kernel::brep::Body> {
|
pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option<kernel::brep::Body> {
|
||||||
|
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<Vec<usize>>)> {
|
||||||
match entity {
|
match entity {
|
||||||
codec::EntityType::Mesh(mesh) => {
|
codec::EntityType::Mesh(mesh) => {
|
||||||
let base = base_refined_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) => {
|
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 m = mesh.m_vertex_count.max(0) as usize;
|
||||||
let n = mesh.n_vertex_count.max(0) as usize;
|
let n = mesh.n_vertex_count.max(0) as usize;
|
||||||
let vertices = mesh
|
let vertices = mesh
|
||||||
@@ -1074,9 +1085,11 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option<kernel::bre
|
|||||||
if m < 2 || n < 2 || vertices.len() != m.saturating_mul(n) {
|
if m < 2 || n < 2 || vertices.len() != m.saturating_mul(n) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut faces = Vec::with_capacity(m.saturating_mul(n));
|
let rows = if mesh.is_closed_m() { m } else { m - 1 };
|
||||||
for row in 0..m {
|
let columns = if mesh.is_closed_n() { n } else { n - 1 };
|
||||||
for column in 0..n {
|
let mut faces = Vec::with_capacity(rows.saturating_mul(columns));
|
||||||
|
for row in 0..rows {
|
||||||
|
for column in 0..columns {
|
||||||
faces.push(vec![
|
faces.push(vec![
|
||||||
row * n + column,
|
row * n + column,
|
||||||
((row + 1) % m) * n + column,
|
((row + 1) % m) * n + column,
|
||||||
@@ -1085,7 +1098,7 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option<kernel::bre
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kernel::brep::make::faceted_solid(&vertices, &faces)
|
Some((vertices, faces))
|
||||||
}
|
}
|
||||||
codec::EntityType::PolyfaceMesh(mesh) => {
|
codec::EntityType::PolyfaceMesh(mesh) => {
|
||||||
let vertices = mesh
|
let vertices = mesh
|
||||||
@@ -1105,12 +1118,62 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option<kernel::bre
|
|||||||
(indices.len() >= 3).then_some(indices)
|
(indices.len() >= 3).then_some(indices)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
kernel::brep::make::faceted_solid(&vertices, &faces)
|
Some((vertices, faces))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => 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<usize>],
|
||||||
|
side: impl Fn([f64; 3]) -> f64,
|
||||||
|
keep_positive: bool,
|
||||||
|
common: &codec::entities::EntityCommon,
|
||||||
|
) -> Option<Mesh> {
|
||||||
|
let sign = if keep_positive { 1.0 } else { -1.0 };
|
||||||
|
let distances: Vec<f64> = 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::<Vec<_>>();
|
||||||
|
mesh.add_face(codec::entities::MeshFace::new(indices));
|
||||||
|
}
|
||||||
|
(!mesh.faces.is_empty()).then_some(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
fn face_triangle_indices(vertices: &[[f64; 3]], faces: &[Vec<usize>]) -> (Vec<u32>, Vec<usize>) {
|
fn face_triangle_indices(vertices: &[[f64; 3]], faces: &[Vec<usize>]) -> (Vec<u32>, Vec<usize>) {
|
||||||
let mut indices = Vec::new();
|
let mut indices = Vec::new();
|
||||||
let mut triangle_faces = Vec::new();
|
let mut triangle_faces = Vec::new();
|
||||||
|
|||||||
@@ -335,8 +335,15 @@ impl CadCommand for SliceCommand {
|
|||||||
self.selected = entities
|
self.selected = entities
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|item| {
|
.filter_map(|item| {
|
||||||
matches!(item.entity, EntityType::Solid3D(_) | EntityType::Surface(_))
|
matches!(
|
||||||
.then_some(item.handle)
|
item.entity,
|
||||||
|
EntityType::Solid3D(_)
|
||||||
|
| EntityType::Surface(_)
|
||||||
|
| EntityType::Mesh(_)
|
||||||
|
| EntityType::PolygonMesh(_)
|
||||||
|
| EntityType::PolyfaceMesh(_)
|
||||||
|
)
|
||||||
|
.then_some(item.handle)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user