diff --git a/Cargo.lock b/Cargo.lock index 795add4b3..b1dbfbfc2 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=2162f5cce085929966047b8c24c7e0da83c26910#2162f5cce085929966047b8c24c7e0da83c26910" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=81ad6019564ba4b3785affdc99adbb6600b2d2ea#81ad6019564ba4b3785affdc99adbb6600b2d2ea" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 4599c5f7f..fac73d49e 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 = "2162f5cce085929966047b8c24c7e0da83c26910", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "81ad6019564ba4b3785affdc99adbb6600b2d2ea", 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 2ac2a9851..1f877f22d 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 = "2162f5cce085929966047b8c24c7e0da83c26910", features = ["acis", "offset"], optional = true } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "81ad6019564ba4b3785affdc99adbb6600b2d2ea", features = ["acis", "offset"], optional = true } ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] } [build-dependencies] diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index ad42b31a9..3a2b4a131 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -990,22 +990,7 @@ impl OpenCADStudio { "INTERSECT" => { use crate::modules::model::boolean_cmd::BoolOp; - let solid_count = { - let scene = &self.tabs[i].scene; - 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(_)) - ) - }) - .take(2) - .count() - }; - if solid_count < 2 { + if !self.intersect_ready() { use crate::modules::draw::select::SelectObjectsCommand; let selection = SelectObjectsCommand::new(cmd); self.command_line.push_info(&selection.prompt()); diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 60ec88977..a262c0ee3 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -9,6 +9,7 @@ use acadrust::{ }; use cadkernel::brep::Body; use iced::Task; +use std::collections::HashMap; use super::Message; use crate::modules::model::boolean_cmd::BoolOp; @@ -75,6 +76,27 @@ struct PreparedSubtract { result: Option<(Handle, EntityType, Body, PreparedSolidDisplay)>, } +struct IntersectGroup { + kind: UnionEntityKind, + plane: Option, + handles: Vec, +} + +enum PreparedIntersectOutcome { + Replace { + retained: Handle, + entity: EntityType, + body: Body, + display: PreparedSolidDisplay, + }, + Consume, +} + +struct PreparedIntersect { + handles: Vec, + outcome: PreparedIntersectOutcome, +} + fn inherited_common(source: &EntityCommon) -> EntityCommon { let mut result = EntityCommon::new(); result.layer = source.layer.clone(); @@ -183,7 +205,7 @@ fn subtract_bodies( Ok(result) } -fn entity_with_subtract_body(mut source: EntityType, body: &Body) -> Option { +fn entity_with_boolean_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!( @@ -222,6 +244,39 @@ fn entity_with_subtract_body(mut source: EntityType, body: &Body) -> Option, +) -> Result { + if kind == UnionEntityKind::Solid { + let mut operands = bodies.into_iter(); + let mut result = operands + .next() + .ok_or(cadkernel::brep::Snag::CutRefused)?; + for operand in operands { + result = solid_model::boolean_result(Bool::Intersect, &result, &operand)?; + if result.faces.is_empty() { + return Ok(IntersectBodyOutcome::Disjoint); + } + } + return Ok(IntersectBodyOutcome::Area(result)); + } + + let references = bodies.iter().collect::>(); + let tolerance = cadkernel::brep::operation_tolerance(&references); + Ok(match cadkernel::brep::intersect_planar_regions(&bodies, tolerance)? { + cadkernel::brep::PlanarIntersection::Area(body) => IntersectBodyOutcome::Area(body), + cadkernel::brep::PlanarIntersection::Touching => IntersectBodyOutcome::Touching, + cadkernel::brep::PlanarIntersection::Disjoint => IntersectBodyOutcome::Disjoint, + }) +} + impl super::OpenCADStudio { /// Add a solid and register its persistent B-rep. pub(super) fn add_solid_model( @@ -431,6 +486,71 @@ impl super::OpenCADStudio { .any(|group| group.handles.len() >= 2) } + pub(super) fn intersect_ready(&self) -> bool { + self.union_ready() + } + + fn selected_intersect_handles(&self) -> Vec { + let scene = &self.tabs[self.active_tab].scene; + scene + .selected_handles_in_order() + .into_iter() + .filter(|handle| !scene.is_layer_locked(*handle)) + .filter(|handle| { + scene + .document + .get_entity(*handle) + .and_then(UnionEntityKind::from_entity) + .is_some() + }) + .collect() + } + + fn intersect_groups( + &self, + handles: &[Handle], + bodies: &HashMap, + ) -> Vec { + let scene = &self.tabs[self.active_tab].scene; + let mut groups = Vec::::new(); + for handle in handles { + let Some(kind) = scene + .document + .get_entity(*handle) + .and_then(UnionEntityKind::from_entity) + else { + continue; + }; + let plane = (kind != UnionEntityKind::Solid) + .then(|| planar_body_plane(&bodies[handle])) + .flatten(); + if let Some(group) = groups.iter_mut().find(|group| { + group.kind == kind + && if kind == UnionEntityKind::Solid { + true + } else { + match (group.plane, plane) { + (Some(group_plane), Some(_)) => { + coplanar_bodies(group_plane, &bodies[handle]) + } + (None, None) => true, + _ => false, + } + } + }) { + group.handles.push(*handle); + } else { + groups.push(IntersectGroup { + kind, + plane, + handles: vec![*handle], + }); + } + } + groups.retain(|group| group.handles.len() >= 2); + groups + } + fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool { let i = self.active_tab; let Some(document) = @@ -526,10 +646,198 @@ impl super::OpenCADStudio { Task::none() } + /// Intersect compatible selected solids, Regions, and coplanar Surfaces. + pub(super) fn solid_intersect(&mut self) -> Task { + let i = self.active_tab; + let mut handles = self.selected_intersect_handles(); + self.tabs[i].scene.restore_solid_models(&handles); + handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); + let bodies = handles + .iter() + .filter_map(|handle| { + self.tabs[i] + .scene + .solid_models + .get(handle) + .cloned() + .map(|body| (*handle, body)) + }) + .collect::>(); + let groups = self.intersect_groups(&handles, &bodies); + if groups.is_empty() { + self.command_line.push_error( + crate::t!("INTERSECT: select at least two solids, Regions, or coplanar Surfaces of a compatible type.") + .as_ref(), + ); + return Task::none(); + } + + let mut prepared = Vec::new(); + let mut retained_without_change = 0usize; + for group in groups { + let operands = group + .handles + .iter() + .map(|handle| bodies[handle].clone()) + .collect::>(); + let outcome = match intersect_bodies(group.kind, operands) { + Ok(outcome) => outcome, + Err(cadkernel::brep::Snag::NoClosedForm) => { + self.command_line.push_error( + crate::t!("INTERSECT: the selected objects do not have a supported coplanar surface intersection.") + .as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::Coincident) => { + self.command_line.push_error( + crate::t!("INTERSECT: the selected coincident geometry is ambiguous.") + .as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::CutRefused) => { + self.command_line.push_error( + crate::t!("INTERSECT: the selected topology could not be intersected safely.") + .as_ref(), + ); + return Task::none(); + } + }; + + match outcome { + IntersectBodyOutcome::Area(body) => { + let retained = group.handles[0]; + let Some(source) = self.tabs[i].scene.document.get_entity(retained).cloned() + else { + return Task::none(); + }; + let Some(entity) = entity_with_boolean_body(source, &body) else { + self.command_line.push_error( + crate::t!("The INTERSECT result could not be encoded as ACIS.") + .as_ref(), + ); + return Task::none(); + }; + let Some(display) = self.tabs[i] + .scene + .prepare_solid_model_display(retained, &body) + .filter(|display| display.0.complete) + else { + self.command_line.push_error( + crate::t!("The INTERSECT result could not be displayed completely. The original objects were retained.") + .as_ref(), + ); + return Task::none(); + }; + prepared.push(PreparedIntersect { + handles: group.handles, + outcome: PreparedIntersectOutcome::Replace { + retained, + entity, + body, + display, + }, + }); + } + IntersectBodyOutcome::Touching + if group.kind == UnionEntityKind::Region + || group.kind == UnionEntityKind::Surface => + { + prepared.push(PreparedIntersect { + handles: group.handles, + outcome: PreparedIntersectOutcome::Consume, + }); + } + IntersectBodyOutcome::Disjoint if group.kind == UnionEntityKind::Region => { + prepared.push(PreparedIntersect { + handles: group.handles, + outcome: PreparedIntersectOutcome::Consume, + }); + } + IntersectBodyOutcome::Touching | IntersectBodyOutcome::Disjoint => { + retained_without_change += group.handles.len(); + } + } + } + + if prepared.is_empty() { + self.command_line.push_output( + crate::tf!( + "INTERSECT: no positive-area common result; %{count} original object(s) retained.", + count = retained_without_change + ) + .as_ref(), + ); + return Task::none(); + } + + let record_history = self.tabs[i].scene.document.header.record_solid_history; + self.push_undo_snapshot(i, "INTERSECT"); + let mut results = Vec::new(); + let mut consumed = Vec::new(); + for group in prepared { + match group.outcome { + PreparedIntersectOutcome::Replace { + retained, + entity, + body, + display, + } => { + self.tabs[i].scene.delete_solid_history(retained); + if !self.tabs[i].scene.update_entity(entity) { + self.command_line.push_error( + crate::t!("INTERSECT: the retained object could not be updated.") + .as_ref(), + ); + return Task::none(); + } + if record_history + && matches!( + self.tabs[i].scene.document.get_entity(retained), + Some(EntityType::Solid3D(_)) + ) + { + let history = solid_history::brep_op(&body); + self.tabs[i].scene.create_solid_history(retained, history); + } + self.tabs[i] + .scene + .register_prepared_solid_model(retained, body, display); + results.push(retained); + consumed.extend( + group + .handles + .into_iter() + .filter(|handle| *handle != retained), + ); + } + PreparedIntersectOutcome::Consume => consumed.extend(group.handles), + } + } + self.tabs[i].scene.erase_entities(&consumed); + 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::tf!( + "INTERSECT: created %{count} result object(s).", + count = results.len() + ) + .as_ref(), + ); + Task::none() + } + /// Run a boolean over the selected solids in selection order. pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task { - if op == BoolOp::Union { - return self.union_selected_entities(); + match op { + BoolOp::Union => return self.union_selected_entities(), + BoolOp::Intersect => return self.solid_intersect(), + BoolOp::Subtract => {} } let i = self.active_tab; let handles = self.selected_solid_handles(); @@ -890,7 +1198,7 @@ impl super::OpenCADStudio { 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 { + let Some(entity) = entity_with_boolean_body(source, &result) else { self.command_line.push_error( crate::t!("The SUBTRACT result could not be encoded as ACIS.").as_ref(), ); diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 3e3376bfa..5095dd680 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -159,14 +159,14 @@ pub fn kernel_body(solid: &Solid3D) -> Option { kernel_acis_body(&solid.acis_data) } -pub fn kernel_surface_body(surface: &Surface) -> Option { - kernel_acis_body(&surface.acis_data) -} - pub fn kernel_region_body(region: &Region) -> Option { kernel_acis_body(®ion.acis_data) } +pub fn kernel_surface_body(surface: &Surface) -> Option { + kernel_acis_body(&surface.acis_data) +} + fn kernel_acis_body(acis: &acadrust::entities::AcisData) -> Option { let sat = parse_acis( || acis.parse(), diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 89faaa20e..a1dbbf3af 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -602,12 +602,12 @@ impl Scene { Some(EntityType::Solid3D(solid)) => { crate::scene::convert::solid3d_tess::kernel_body(solid) } - Some(EntityType::Surface(surface)) => { - crate::scene::convert::solid3d_tess::kernel_surface_body(surface) - } Some(EntityType::Region(region)) => { crate::scene::convert::solid3d_tess::kernel_region_body(region) } + Some(EntityType::Surface(surface)) => { + crate::scene::convert::solid3d_tess::kernel_surface_body(surface) + } _ => None, })?; Some((handle, body))