diff --git a/Cargo.lock b/Cargo.lock index 287dcca72..795add4b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,7 +882,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/ramox81/cadkernel.git?rev=328b55a9a45bc2118526a49156bcb94e46f10444#328b55a9a45bc2118526a49156bcb94e46f10444" +source = "git+https://github.com/ramox81/cadkernel.git?rev=2162f5cce085929966047b8c24c7e0da83c26910#2162f5cce085929966047b8c24c7e0da83c26910" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index ffc5091e2..4599c5f7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"] } -cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "328b55a9a45bc2118526a49156bcb94e46f10444", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "2162f5cce085929966047b8c24c7e0da83c26910", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_doc_api/Cargo.toml b/crates/ocs_doc_api/Cargo.toml index 0b7944852..2ac2a9851 100644 --- a/crates/ocs_doc_api/Cargo.toml +++ b/crates/ocs_doc_api/Cargo.toml @@ -15,7 +15,7 @@ thiserror = "1" # Host adapters convert ObjectId to acadrust handles; kernel handles stay local. acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"], optional = true } -cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "328b55a9a45bc2118526a49156bcb94e46f10444", features = ["acis", "offset"], optional = true } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "2162f5cce085929966047b8c24c7e0da83c26910", features = ["acis", "offset"], optional = true } ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] } [build-dependencies] diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index e2dd3c6e5..0b74e15fd 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4115,8 +4115,12 @@ impl OpenCADStudio { return task; } - CmdResult::SolidSubtract { bases, cutters } => { - let task = self.solid_subtract(&bases, &cutters); + CmdResult::SolidSubtract { + bases, + cutters, + convert_meshes, + } => { + let task = self.solid_subtract(&bases, &cutters, convert_meshes); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 71a281e8b..ad42b31a9 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1017,22 +1017,40 @@ impl OpenCADStudio { "SUBTRACT" => { use crate::modules::model::boolean_cmd::SubtractCommand; - let bases = { + let (bases, bases_have_mesh) = { let scene = &self.tabs[i].scene; - scene + let bases = scene .selected_handles_in_order() .into_iter() .filter(|handle| !scene.is_layer_locked(*handle)) .filter(|handle| { matches!( scene.document.get_entity(*handle), - Some(acadrust::EntityType::Solid3D(_)) + Some( + acadrust::EntityType::Solid3D(_) + | acadrust::EntityType::Region(_) + | acadrust::EntityType::Surface(_) + | acadrust::EntityType::Mesh(_) + | acadrust::EntityType::PolygonMesh(_) + | acadrust::EntityType::PolyfaceMesh(_) + ) ) }) - .collect() + .collect::>(); + let bases_have_mesh = bases.iter().any(|handle| { + matches!( + scene.document.get_entity(*handle), + Some( + acadrust::EntityType::Mesh(_) + | acadrust::EntityType::PolygonMesh(_) + | acadrust::EntityType::PolyfaceMesh(_) + ) + ) + }); + (bases, bases_have_mesh) }; self.tabs[i].scene.deselect_all(); - let subtract = SubtractCommand::new(bases); + let subtract = SubtractCommand::new(bases, bases_have_mesh); self.command_line.push_info(&subtract.prompt()); self.tabs[i].active_cmd = Some(Box::new(subtract)); } diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index a98e7a6df..60ec88977 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -1,7 +1,9 @@ // Kernel B-rep solid modelling and exact ACIS persistence. use acadrust::{ - entities::{EntityCommon, Region, Solid3D, Surface, SurfaceKind}, + entities::{ + AcisData, EntityCommon, Region, Solid3D, Surface, SurfaceData, SurfaceKind, Wire, + }, objects::SolidHistoryOperation, EntityType, Handle, }; @@ -31,6 +33,17 @@ impl UnionEntityKind { } } +fn subtract_entity_kind(entity: &EntityType, convert_meshes: bool) -> Option { + UnionEntityKind::from_entity(entity).or_else(|| { + (convert_meshes + && matches!( + entity, + EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) + )) + .then_some(UnionEntityKind::Solid) + }) +} + struct UnionGroup { kind: UnionEntityKind, handles: Vec, @@ -43,6 +56,25 @@ struct PreparedUnion { common: EntityCommon, } +struct SubtractGroup { + kind: UnionEntityKind, + plane: Option, + bases: Vec, + cutters: Vec, +} + +type PreparedSolidDisplay = ( + crate::scene::model::mesh_model::MeshLodSet, + Vec, + [f64; 3], +); + +struct PreparedSubtract { + bases: Vec, + cutters: Vec, + result: Option<(Handle, EntityType, Body, PreparedSolidDisplay)>, +} + fn inherited_common(source: &EntityCommon) -> EntityCommon { let mut result = EntityCommon::new(); result.layer = source.layer.clone(); @@ -85,6 +117,111 @@ fn union_bodies( cadkernel::brep::union_planar_regions(&bodies, tolerance) } +fn planar_body_plane(body: &Body) -> Option { + let mut faces = body.face_keys(); + let first = cadkernel::brep::planar_face_profile(body, faces.next()?)?.plane; + let first_normal = glam::DVec3::from_array(first.normal()?); + let tolerance = cadkernel::brep::operation_tolerance(&[body]); + faces + .all(|face| { + let Some(profile) = cadkernel::brep::planar_face_profile(body, face) else { + return false; + }; + let Some(normal) = profile.plane.normal() else { + return false; + }; + first_normal.dot(glam::DVec3::from_array(normal)).abs() >= 1.0 - 1e-9 + && first + .distance_to(profile.plane.origin) + .is_some_and(|distance| distance.abs() <= tolerance * 4.0) + }) + .then_some(first) +} + +fn coplanar_bodies(plane: cadkernel::space::Plane, body: &Body) -> bool { + let Some(other) = planar_body_plane(body) else { + return false; + }; + let Some(one_normal) = plane.normal().map(glam::DVec3::from_array) else { + return false; + }; + let Some(other_normal) = other.normal().map(glam::DVec3::from_array) else { + return false; + }; + let tolerance = cadkernel::brep::operation_tolerance(&[body]); + one_normal.dot(other_normal).abs() >= 1.0 - 1e-9 + && plane + .distance_to(other.origin) + .is_some_and(|distance| distance.abs() <= tolerance * 4.0) +} + +fn subtract_bodies( + kind: UnionEntityKind, + plane: Option, + bases: Vec, + cutters: Vec, +) -> Result { + if kind != UnionEntityKind::Solid && plane.is_none() { + return Err(cadkernel::brep::Snag::NoClosedForm); + } + if kind != UnionEntityKind::Solid && plane.is_some() { + let references = bases.iter().chain(&cutters).collect::>(); + let tolerance = cadkernel::brep::operation_tolerance(&references); + return cadkernel::brep::subtract_planar_regions(&bases, &cutters, tolerance); + } + + let mut bases = bases.into_iter(); + let mut result = bases + .next() + .ok_or(cadkernel::brep::Snag::CutRefused)?; + for base in bases { + result = solid_model::boolean_result(Bool::Union, &result, &base)?; + } + for cutter in cutters { + result = solid_model::boolean_result(Bool::Subtract, &result, &cutter)?; + } + Ok(result) +} + +fn entity_with_subtract_body(mut source: EntityType, body: &Body) -> Option { + let document = crate::scene::convert::acis_export::solid_to_sat(body)?; + let wires = solid_model::edge_wires(body); + if matches!( + &source, + EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) + ) { + let mut solid = Solid3D::new(); + solid.common = source.common().clone(); + source = EntityType::Solid3D(solid); + } + match &mut source { + EntityType::Solid3D(entity) => { + entity.wires = wires; + entity.silhouettes.clear(); + entity.history_handle = None; + entity.set_sat_document(&document); + } + EntityType::Region(entity) => { + entity.wires = wires; + entity.silhouettes.clear(); + entity.history_handle = None; + entity.set_sat_document(&document); + } + EntityType::Surface(entity) => { + entity.wires = wires; + entity.silhouettes.clear(); + entity.history_handle = None; + if entity.kind != SurfaceKind::Plane { + entity.kind = SurfaceKind::Generic; + entity.surface_data = SurfaceData::Generic; + } + entity.acis_data = AcisData::from_sat(&document.to_sat_string()); + } + _ => return None, + } + Some(source) +} + impl super::OpenCADStudio { /// Add a solid and register its persistent B-rep. pub(super) fn add_solid_model( @@ -569,73 +706,262 @@ impl super::OpenCADStudio { &mut self, bases: &[Handle], cutters: &[Handle], + convert_meshes: bool, ) -> Task { let i = self.active_tab; - let valid_solid = |handle: &Handle| { + let valid_operand = |handle: &Handle| { !self.tabs[i].scene.is_layer_locked(*handle) - && matches!( - self.tabs[i].scene.document.get_entity(*handle), - Some(EntityType::Solid3D(_)) - ) + && self.tabs[i] + .scene + .document + .get_entity(*handle) + .and_then(|entity| subtract_entity_kind(entity, convert_meshes)) + .is_some() }; - let mut base_handles: Vec<_> = bases.iter().copied().filter(valid_solid).collect(); - let mut cutter_handles: Vec<_> = cutters.iter().copied().filter(valid_solid).collect(); + let mut base_handles: Vec<_> = bases.iter().copied().filter(valid_operand).collect(); + let mut cutter_handles: Vec<_> = cutters.iter().copied().filter(valid_operand).collect(); cutter_handles.retain(|handle| !base_handles.contains(handle)); let mut operands = base_handles.clone(); operands.extend(cutter_handles.iter().copied()); self.tabs[i].scene.restore_solid_models(&operands); - base_handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); - cutter_handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); + let mut operand_bodies = std::collections::HashMap::::new(); + for handle in &operands { + if let Some(body) = self.tabs[i].scene.solid_models.get(handle).cloned() { + operand_bodies.insert(*handle, body); + continue; + } + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else { + continue; + }; + if convert_meshes + && matches!( + entity, + EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) + ) + { + let Some(body) = crate::entities::mesh::closed_mesh_body(entity) else { + self.command_line.push_error( + crate::t!("SUBTRACT: a selected Mesh is not a supported closed single-shell mesh; no object was changed.") + .as_ref(), + ); + return Task::none(); + }; + operand_bodies.insert(*handle, body); + } + } + base_handles.retain(|handle| operand_bodies.contains_key(handle)); + cutter_handles.retain(|handle| operand_bodies.contains_key(handle)); if base_handles.is_empty() || cutter_handles.is_empty() { self.command_line.push_error( - crate::t!("SUBTRACT: select at least one base solid and one cutter solid.") + crate::t!("SUBTRACT: select at least one base and one cutter Solid, Region, or Surface.") .as_ref(), ); return Task::none(); } - let mut result = self.tabs[i].scene.solid_models[&base_handles[0]].clone(); - for handle in &base_handles[1..] { - let operand = &self.tabs[i].scene.solid_models[handle]; - let Some(combined) = solid_model::boolean(Bool::Union, &result, operand) else { - self.command_line.push_error( - crate::t!("SUBTRACT failed while combining the base solids.").as_ref(), - ); - return Task::none(); - }; - result = combined; + let kind_for = |handle: Handle| { + self.tabs[i] + .scene + .document + .get_entity(handle) + .and_then(|entity| subtract_entity_kind(entity, convert_meshes)) + }; + let mut groups = Vec::::new(); + for kind in [ + UnionEntityKind::Solid, + UnionEntityKind::Region, + UnionEntityKind::Surface, + ] { + let kind_bases = base_handles + .iter() + .copied() + .filter(|handle| kind_for(*handle) == Some(kind)) + .collect::>(); + if kind_bases.is_empty() { + continue; + } + if kind == UnionEntityKind::Solid { + let kind_cutters = cutter_handles + .iter() + .copied() + .filter(|handle| kind_for(*handle) == Some(kind)) + .collect::>(); + if !kind_cutters.is_empty() { + groups.push(SubtractGroup { + kind, + plane: None, + bases: kind_bases, + cutters: kind_cutters, + }); + } + continue; + } + + for handle in kind_bases { + let body = &operand_bodies[&handle]; + let plane = planar_body_plane(body); + if let Some(group) = groups.iter_mut().find(|group| { + group.kind == kind + && match (group.plane, plane) { + (Some(group_plane), Some(_)) => coplanar_bodies(group_plane, body), + (None, None) => true, + _ => false, + } + }) { + group.bases.push(handle); + } else { + groups.push(SubtractGroup { + kind, + plane, + bases: vec![handle], + cutters: Vec::new(), + }); + } + } + for handle in cutter_handles + .iter() + .copied() + .filter(|handle| kind_for(*handle) == Some(kind)) + { + let body = &operand_bodies[&handle]; + let plane = planar_body_plane(body); + if let Some(group) = groups.iter_mut().find(|group| { + group.kind == kind + && match (group.plane, plane) { + (Some(group_plane), Some(_)) => coplanar_bodies(group_plane, body), + (None, None) => true, + _ => false, + } + }) { + group.cutters.push(handle); + } + } } - for handle in &cutter_handles { - let operand = &self.tabs[i].scene.solid_models[handle]; - let Some(difference) = solid_model::boolean(Bool::Subtract, &result, operand) else { - self.command_line.push_error( - crate::t!("SUBTRACT failed while removing the selected solids.").as_ref(), - ); - return Task::none(); - }; - result = difference; - } - if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() { - self.command_line - .push_error(crate::t!("The boolean result could not be encoded as ACIS.").as_ref()); + groups.retain(|group| !group.bases.is_empty() && !group.cutters.is_empty()); + if groups.is_empty() { + self.command_line.push_error( + crate::t!("SUBTRACT: no compatible base and cutter types or planes were selected.") + .as_ref(), + ); return Task::none(); } - operands = base_handles; - operands.extend(cutter_handles); + let mut prepared = Vec::with_capacity(groups.len()); + for group in groups { + let base_bodies = group + .bases + .iter() + .map(|handle| operand_bodies[handle].clone()) + .collect::>(); + let cutter_bodies = group + .cutters + .iter() + .map(|handle| operand_bodies[handle].clone()) + .collect::>(); + let result = match subtract_bodies(group.kind, group.plane, base_bodies, cutter_bodies) { + Ok(result) => result, + Err(cadkernel::brep::Snag::NoClosedForm) => { + self.command_line.push_error( + crate::t!("SUBTRACT: the selected geometry includes an unsupported surface intersection.") + .as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::Coincident) => { + self.command_line.push_error( + crate::t!("SUBTRACT: the selected coincident geometry is ambiguous.") + .as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::CutRefused) => { + self.command_line.push_error( + crate::t!("SUBTRACT: the selected topology could not be cut safely.") + .as_ref(), + ); + return Task::none(); + } + }; + let staged_result = if result.faces.is_empty() { + None + } else { + let retained = group.bases[0]; + let Some(source) = self.tabs[i].scene.document.get_entity(retained).cloned() else { + return Task::none(); + }; + let Some(entity) = entity_with_subtract_body(source, &result) else { + self.command_line.push_error( + crate::t!("The SUBTRACT result could not be encoded as ACIS.").as_ref(), + ); + return Task::none(); + }; + let Some(display) = self.tabs[i] + .scene + .prepare_solid_model_display(retained, &result) + .filter(|display| display.0.complete) + else { + self.command_line.push_error( + crate::t!("The SUBTRACT result could not be displayed completely. The original objects were retained.") + .as_ref(), + ); + return Task::none(); + }; + Some((retained, entity, result, display)) + }; + prepared.push(PreparedSubtract { + bases: group.bases, + cutters: group.cutters, + result: staged_result, + }); + } + + let record_history = self.tabs[i].scene.document.header.record_solid_history; self.push_undo_snapshot(i, "SUBTRACT"); - self.tabs[i].scene.erase_entities(&operands); - let mut entity = Solid3D::new(); - entity.wires = solid_model::edge_wires(&result); - let history = solid_history::brep_op(&result); - let handle = self.add_solid_model(EntityType::Solid3D(entity), result, history); + let mut retained = Vec::new(); + let mut consumed = Vec::new(); + for group in prepared { + if let Some((handle, entity, body, display)) = group.result { + self.tabs[i].scene.delete_solid_history(handle); + if !self.tabs[i].scene.update_entity(entity) { + self.command_line.push_error( + crate::t!("SUBTRACT: the retained base object could not be updated.").as_ref(), + ); + return Task::none(); + } + if record_history + && matches!( + self.tabs[i].scene.document.get_entity(handle), + Some(EntityType::Solid3D(_)) + ) + { + let history = solid_history::brep_op(&body); + self.tabs[i].scene.create_solid_history(handle, history); + } + self.tabs[i] + .scene + .register_prepared_solid_model(handle, body, display); + retained.push(handle); + consumed.extend(group.bases.into_iter().skip(1)); + } else { + consumed.extend(group.bases); + } + consumed.extend(group.cutters); + } + self.tabs[i].scene.erase_entities(&consumed); self.tabs[i].scene.deselect_all(); - if !handle.is_null() { - self.tabs[i].scene.select_entity(handle, false); + for handle in &retained { + self.tabs[i].scene.select_entity(*handle, false); } self.tabs[i].dirty = true; self.refresh_properties(); + self.command_line.push_output( + crate::tf!( + "SUBTRACT: created %{count} result object(s).", + count = retained.len() + ) + .as_ref(), + ); Task::none() } diff --git a/src/command.rs b/src/command.rs index 12222ac40..25ccfb027 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1557,6 +1557,7 @@ pub enum CmdResult { SolidSubtract { bases: Vec, cutters: Vec, + convert_meshes: bool, }, /// INSERT landed on a block that has AttributeDefinitions. /// The host should look up the attdefs for `block_name` from the document diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index af3775653..e11ebed08 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -991,6 +991,67 @@ fn display_mesh(mesh: &Mesh) -> RefinedMesh { refined } +/// Convert a closed indexed mesh entity to an exact planar-faced B-rep. +/// 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: &acadrust::EntityType) -> Option { + match entity { + acadrust::EntityType::Mesh(mesh) => { + let base = base_refined_mesh(mesh); + cadkernel::brep::make::faceted_solid(&base.vertices, &base.faces) + } + acadrust::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 + .vertices + .iter() + .take(m.saturating_mul(n)) + .map(|vertex| v3(&vertex.location)) + .collect::>(); + if m < 2 || n < 2 || vertices.len() != m.saturating_mul(n) { + return None; + } + let mut faces = Vec::with_capacity(m.saturating_mul(n)); + for row in 0..m { + for column in 0..n { + faces.push(vec![ + row * n + column, + ((row + 1) % m) * n + column, + ((row + 1) % m) * n + (column + 1) % n, + row * n + (column + 1) % n, + ]); + } + } + cadkernel::brep::make::faceted_solid(&vertices, &faces) + } + acadrust::EntityType::PolyfaceMesh(mesh) => { + let vertices = mesh + .vertices + .iter() + .map(|vertex| v3(&vertex.location)) + .collect::>(); + let faces = mesh + .faces + .iter() + .filter_map(|face| { + let indices = [face.index1, face.index2, face.index3, face.index4] + .into_iter() + .filter(|index| *index != 0) + .map(|index| (index.unsigned_abs() as usize).checked_sub(1)) + .collect::>>()?; + (indices.len() >= 3).then_some(indices) + }) + .collect::>(); + cadkernel::brep::make::faceted_solid(&vertices, &faces) + } + _ => None, + } +} + fn face_triangle_indices( vertices: &[[f64; 3]], faces: &[Vec], diff --git a/src/modules/model/boolean_cmd.rs b/src/modules/model/boolean_cmd.rs index ff607b815..d24567b3c 100644 --- a/src/modules/model/boolean_cmd.rs +++ b/src/modules/model/boolean_cmd.rs @@ -25,16 +25,20 @@ impl BoolOp { enum SubtractStep { Bases, Cutters, + ConvertMeshes, } pub struct SubtractCommand { step: SubtractStep, bases: Vec, + cutters: Vec, selected: Vec, + bases_have_mesh: bool, + selected_has_mesh: bool, } impl SubtractCommand { - pub fn new(bases: Vec) -> Self { + pub fn new(bases: Vec, bases_have_mesh: bool) -> Self { let step = if bases.is_empty() { SubtractStep::Bases } else { @@ -43,7 +47,18 @@ impl SubtractCommand { Self { step, bases, + cutters: Vec::new(), selected: Vec::new(), + bases_have_mesh, + selected_has_mesh: false, + } + } + + fn finish(&mut self, convert_meshes: bool) -> CmdResult { + CmdResult::SolidSubtract { + bases: std::mem::take(&mut self.bases), + cutters: std::mem::take(&mut self.cutters), + convert_meshes, } } } @@ -55,11 +70,26 @@ impl CadCommand for SubtractCommand { fn prompt(&self) -> String { match self.step { - SubtractStep::Bases => crate::t!("SUBTRACT Select base solids, then press Enter:") + SubtractStep::Bases => crate::t!("SUBTRACT Select base Solids, Regions, Surfaces, or Meshes, then press Enter:") .into_owned(), SubtractStep::Cutters => { - crate::t!("SUBTRACT Select solids to subtract, then press Enter:").into_owned() + crate::t!("SUBTRACT Select Solids, Regions, Surfaces, or Meshes to subtract, then press Enter:").into_owned() } + SubtractStep::ConvertMeshes => crate::t!( + "SUBTRACT Convert selected closed Mesh objects to solids? [Yes/No] :" + ) + .into_owned(), + } + } + + fn options(&self) -> Vec { + if matches!(self.step, SubtractStep::ConvertMeshes) { + vec![ + crate::command::CmdOption::new(crate::t!("Yes").as_ref(), "Y"), + crate::command::CmdOption::new(crate::t!("No").as_ref(), "N"), + ] + } else { + Vec::new() } } @@ -68,31 +98,72 @@ impl CadCommand for SubtractCommand { } fn on_enter(&mut self) -> CmdResult { + if matches!(self.step, SubtractStep::ConvertMeshes) { + return self.finish(true); + } if self.selected.is_empty() { return CmdResult::NeedPoint; } match self.step { SubtractStep::Bases => { self.bases = std::mem::take(&mut self.selected); + self.bases_have_mesh = self.selected_has_mesh; + self.selected_has_mesh = false; self.step = SubtractStep::Cutters; CmdResult::DeselectAndContinue } - SubtractStep::Cutters => CmdResult::SolidSubtract { - bases: std::mem::take(&mut self.bases), - cutters: std::mem::take(&mut self.selected), - }, + SubtractStep::Cutters => { + self.cutters = std::mem::take(&mut self.selected); + if self.bases_have_mesh || self.selected_has_mesh { + self.step = SubtractStep::ConvertMeshes; + CmdResult::NeedPoint + } else { + self.finish(false) + } + } + SubtractStep::ConvertMeshes => unreachable!(), } } + fn wants_text_input(&self) -> bool { + matches!(self.step, SubtractStep::ConvertMeshes) + } + + fn on_text_input(&mut self, text: &str) -> Option { + if !matches!(self.step, SubtractStep::ConvertMeshes) { + return None; + } + Some(match text.trim().to_ascii_lowercase().as_str() { + "" | "y" | "yes" => self.finish(true), + "n" | "no" => self.finish(false), + _ => CmdResult::NeedPoint, + }) + } + fn is_selection_gathering(&self) -> bool { - true + !matches!(self.step, SubtractStep::ConvertMeshes) } fn inject_selection_entities(&mut self, entities: Vec) { + self.selected_has_mesh = entities.iter().any(|entity| { + matches!( + entity.entity, + EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) + ) + }); self.selected = entities .into_iter() .filter_map(|entity| { - matches!(entity.entity, EntityType::Solid3D(_)).then_some(entity.handle) + matches!( + entity.entity, + EntityType::Solid3D(_) + | EntityType::Region(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) + ) + .then_some(entity.handle) }) .collect(); } diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 3130cb3bc..e292f0ee6 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -222,11 +222,9 @@ pub fn has_compact_solid_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, ) -> bool { - has_specialized_primitive_properties(document, handle) - || matches!( - document.solid_history_operation(handle), - Some(SolidHistoryOperation::Brep(_)) - ) + document + .get_entity(handle) + .is_some_and(|entity| matches!(entity, acadrust::EntityType::Solid3D(_))) } pub fn reference_point(operation: &SolidHistoryOperation) -> Option { @@ -1441,7 +1439,21 @@ pub fn primitive_properties( handle: acadrust::Handle, ) -> Vec { let Some(operation) = document.solid_history_operation(handle) else { - return Vec::new(); + return vec![PropSection { + title: t!("Solid History").into_owned(), + props: vec![ + Property { + label: t!("History").into_owned(), + field: PROP_HISTORY, + value: PropValue::ReadOnly("None".to_string()), + }, + Property { + label: t!("Show History").into_owned(), + field: PROP_SHOW_HISTORY, + value: PropValue::ReadOnly("No".to_string()), + }, + ], + }]; }; match operation { SolidHistoryOperation::Box(value) => { @@ -1462,7 +1474,7 @@ pub fn primitive_properties( } SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value), SolidHistoryOperation::Brep(_) => brep_properties(document, handle), - _ => Vec::new(), + _ => brep_properties(document, handle), } }