diff --git a/Cargo.lock b/Cargo.lock index fb8d159ea..32a1413b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=895f8b7#895f8b7b4d53845608689ead46c1a122e352b79b" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=467c3fd#467c3fd02f419ab5af4030691699b34b25d2f51a" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index ddb09ba19..ec1734bb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "895f8b7", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "467c3fd", 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/src/app/model_ops.rs b/src/app/model_ops.rs index 2c67604a7..a279e5e79 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -1,5 +1,5 @@ // 3D solid modelling support on the App: committing Model-tab primitives, -// and the Design-group boolean operations over the scene's session-cached +// and the Design-group boolean operations over the scene's cached // B-reps. // // The bodies come from the geometry kernel, so every operation here is on @@ -36,18 +36,30 @@ impl super::OpenCADStudio { handle } - /// Run a boolean (`union` / `subtract` / `intersect`) on exactly two - /// selected solids whose B-reps are in the session cache. - pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task { + fn selected_solid_handles(&mut self) -> Vec { let i = self.active_tab; - // Selected entities that have a cached B-rep. - let handles: Vec = self.tabs[i] + let mut handles: Vec = self.tabs[i] .scene .selected_handles_in_order() .into_iter() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .filter(|handle| { + matches!( + self.tabs[i].scene.document.get_entity(*handle), + Some(EntityType::Solid3D(_)) + ) + }) .collect(); + self.tabs[i].scene.restore_solid_models(&handles); + handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); + handles + } + + /// Run a boolean (`union` / `subtract` / `intersect`) on exactly two + /// selected solids whose B-reps can be restored by the kernel. + pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task { + let i = self.active_tab; + let handles = self.selected_solid_handles(); if handles.len() != 2 { self.command_line .push_error(crate::t!("Boolean: select exactly two solids created this session.").as_ref()); @@ -91,14 +103,7 @@ impl super::OpenCADStudio { /// the same boolean path the Design-group tools use. pub(super) fn solid_slice(&mut self, axis: usize, value: f64, keep_low: bool) -> Task { let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.len() != 1 { self.command_line .push_error(crate::t!("SLICE: select exactly one solid created this session.").as_ref()); @@ -167,14 +172,7 @@ impl super::OpenCADStudio { /// leaving the originals in place (a non-destructive boolean intersect). pub(super) fn solid_interfere(&mut self) -> Task { let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.len() != 2 { self.command_line .push_error(crate::t!("INTERFERE: select exactly two solids created this session.").as_ref()); @@ -207,14 +205,7 @@ impl super::OpenCADStudio { /// orientation, so it reuses the cached B-rep directly. pub(super) fn solid_rotate3d(&mut self, axis: usize, angle_deg: f64) -> Task { let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.len() != 1 { self.command_line .push_error(crate::t!("3DROTATE: select exactly one solid created this session.").as_ref()); @@ -333,14 +324,7 @@ impl super::OpenCADStudio { /// loop on the way through; without that the copy lights black. pub(super) fn solid_mirror3d(&mut self, axis: usize) -> Task { let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.len() != 1 { self.command_line .push_error(crate::t!("3DMIRROR: select exactly one solid created this session.").as_ref()); @@ -385,14 +369,7 @@ impl super::OpenCADStudio { dst: [[f64; 3]; 3], ) -> Task { let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.len() != 1 { self.command_line .push_error(crate::t!("3DALIGN: select exactly one solid created this session.").as_ref()); @@ -453,14 +430,7 @@ impl super::OpenCADStudio { use acadrust::Line; let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.len() != 1 { self.command_line .push_error(crate::t!("SECTION: select exactly one solid created this session.").as_ref()); @@ -636,14 +606,7 @@ impl super::OpenCADStudio { use acadrust::types::Vector3; use acadrust::Line; let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.is_empty() { self.command_line .push_error(crate::t!("FLATSHOT: select a solid created this session.").as_ref()); @@ -677,14 +640,7 @@ impl super::OpenCADStudio { use acadrust::entities::{Surface, SurfaceKind, Wire as AWire}; use acadrust::types::Vector3; let i = self.active_tab; - let handles: Vec = self.tabs[i] - .scene - .selected - .iter() - .copied() - .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) - .filter(|h| self.tabs[i].scene.solid_models.contains_key(h)) - .collect(); + let handles = self.selected_solid_handles(); if handles.is_empty() { self.command_line .push_error(crate::t!("CONVTOSURFACE: select a solid created this session.").as_ref()); diff --git a/src/modules/model/boolean_cmd.rs b/src/modules/model/boolean_cmd.rs index 94d926f18..44a29a08d 100644 --- a/src/modules/model/boolean_cmd.rs +++ b/src/modules/model/boolean_cmd.rs @@ -1,6 +1,6 @@ // Boolean operations on 3D solids (Design group). The actual CSG runs in // `App::solid_boolean` (src/app/model_ops.rs) using the kernel on the -// session-cached B-reps; this module just names the operations. +// cached B-reps; this module just names the operations. /// Which boolean a Design-group tool performs on the two selected solids. #[derive(Clone, Copy, PartialEq, Eq, Debug)] diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 57792eddf..76fdad524 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -1,5 +1,6 @@ use acadrust::entities::acis::{SabReader, SatBody, SatDocument}; use acadrust::entities::{Body, Region, Solid3D}; +use cadkernel::brep::{self, Body as KernelBody}; use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; @@ -154,6 +155,33 @@ fn parse_acis( }) } +pub fn kernel_body(solid: &Solid3D) -> Option { + let sat = parse_acis( + || solid.parse_sat(), + solid.acis_data.is_binary, + &solid.acis_data.sab_data, + )?; + let (mut bodies, loss) = cadkernel::acis::lift(&sat); + if !loss.is_empty() || bodies.len() != 1 { + return None; + } + let body = bodies.pop()?; + let source = body.provenance.source()?; + let Some((matrix, translation, scale)) = body_transform(&sat, source.index() as usize).ok()? + else { + return Some(body); + }; + brep::transform( + &body, + &brep::Placement { + x_axis: [scale * matrix[0], scale * matrix[1], scale * matrix[2]], + y_axis: [scale * matrix[3], scale * matrix[4], scale * matrix[5]], + z_axis: [scale * matrix[6], scale * matrix[7], scale * matrix[8]], + origin: translation, + }, + ) +} + fn remap_acis_material_bindings( set: &mut MeshLodSet, acis: &acadrust::entities::AcisData, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 9c638f1a0..fb5e60f65 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -586,11 +586,28 @@ impl Scene { self.images.insert(handle, model); } self.refresh_meshes_for_handles(&[handle]); - if let Some(operation) = self.document.solid_history_operation(handle).cloned() { - if let Ok(body) = cadkernel::acis::rebuild_body(&operation) { - self.solid_models.insert(handle, body); - } - } + self.restore_solid_models(&[handle]); + } + + pub fn restore_solid_models(&mut self, handles: &[Handle]) { + let bodies: Vec<(Handle, cadkernel::brep::Body)> = handles + .iter() + .filter(|handle| !self.solid_models.contains_key(handle)) + .filter_map(|&handle| { + let from_history = self + .document + .solid_history_operation(handle) + .and_then(|operation| cadkernel::acis::rebuild_body(operation).ok()); + let body = from_history.or_else(|| match self.document.get_entity(handle) { + Some(EntityType::Solid3D(solid)) => { + crate::scene::convert::solid3d_tess::kernel_body(solid) + } + _ => None, + })?; + Some((handle, body)) + }) + .collect(); + self.solid_models.extend(bodies); } /// Re-tessellate only the named ACIS entities. The former edit path diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e141c8141..7f63fbe93 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1645,11 +1645,7 @@ pub struct Scene { /// owning block emits a transformed instance so a block placed at an /// INSERT scale renders at the right size. (#123) pub block_meshes: HashMap, - /// Live B-reps for solids created this session by the Model tab, keyed by - /// entity handle. Backs the Design-group boolean tools (a solid must be - /// here to be combined) and the exact-geometry save path, which writes - /// each one back out as ACIS rather than as facets. Not persisted — - /// rebuilt only by creating or combining primitives in-session. + /// Kernel B-reps used by solid operations and exact-geometry saves. pub solid_models: HashMap, /// GPU render data for raster images (RasterImage entities), keyed by handle. pub images: HashMap,