diff --git a/Cargo.lock b/Cargo.lock index 7d4975fe2..287dcca72 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/HakanSeven12/cadkernel.git?rev=6a5e6fc2822f6af6b2611cd605b44437b35158f0#6a5e6fc2822f6af6b2611cd605b44437b35158f0" +source = "git+https://github.com/ramox81/cadkernel.git?rev=328b55a9a45bc2118526a49156bcb94e46f10444#328b55a9a45bc2118526a49156bcb94e46f10444" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 5ccd655d6..ffc5091e2 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/HakanSeven12/cadkernel.git", rev = "6a5e6fc2822f6af6b2611cd605b44437b35158f0", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "328b55a9a45bc2118526a49156bcb94e46f10444", 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 973bd0459..0b7944852 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/HakanSeven12/cadkernel.git", rev = "6a5e6fc2822f6af6b2611cd605b44437b35158f0", features = ["acis", "offset"], optional = true } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "328b55a9a45bc2118526a49156bcb94e46f10444", 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 5090140ef..71a281e8b 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -972,32 +972,46 @@ impl OpenCADStudio { } // ── Solid booleans ───────────────────────────────────────────── - "UNION" | "INTERSECT" => { + "UNION" => { use crate::modules::model::boolean_cmd::BoolOp; - if let Some(op) = BoolOp::from_id(cmd) { - 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 { - use crate::modules::draw::select::SelectObjectsCommand; - let selection = SelectObjectsCommand::new(cmd); - self.command_line.push_info(&selection.prompt()); - self.tabs[i].active_cmd = Some(Box::new(selection)); - } else { - return Some(self.solid_boolean(op)); - } + if self.union_ready() { + return Some(self.solid_boolean(BoolOp::Union)); + } + use crate::modules::draw::select::SelectObjectsCommand; + let selection = SelectObjectsCommand::plain("UNION", "UNIONAPPLY"); + self.command_line.push_info(&selection.prompt()); + self.tabs[i].active_cmd = Some(Box::new(selection)); + } + + "UNIONAPPLY" => { + use crate::modules::model::boolean_cmd::BoolOp; + return Some(self.solid_boolean(BoolOp::Union)); + } + + "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 { + use crate::modules::draw::select::SelectObjectsCommand; + let selection = SelectObjectsCommand::new(cmd); + self.command_line.push_info(&selection.prompt()); + self.tabs[i].active_cmd = Some(Box::new(selection)); + } else { + return Some(self.solid_boolean(BoolOp::Intersect)); } } @@ -1069,9 +1083,9 @@ impl OpenCADStudio { // REGION — convert selected closed boundaries (closed polylines / // circles) into Region entities (one wire loop each). "REGION" | "REG" => { - use acadrust::entities::{Region, Wire}; + use acadrust::entities::Region; use acadrust::types::Vector3; - let mut loops: Vec> = Vec::new(); + let mut regions = Vec::new(); for (_, e) in self.tabs[i].scene.selected_entities().iter() { let supported = matches!( e, @@ -1079,32 +1093,39 @@ impl OpenCADStudio { if pl.is_closed && pl.vertices.len() >= 3 ) || matches!(e, acadrust::EntityType::Circle(_)); if supported { - let Some(curve) = crate::entities::curve::entity_curve(e) else { + let Some((plane, loops, true)) = + crate::scene::model::presspull_model::profile_geometry(e) + else { continue; }; - loops.push( - crate::entities::curve::curve_points(&curve) - .into_iter() - .map(|point| Vector3::new(point[0], point[1], point[2])) - .collect(), + let Some(body) = cadkernel::brep::planar_region(plane, &loops) else { + continue; + }; + let mut region = Region::new(); + region.point_of_reference = Vector3::new( + plane.origin[0], + plane.origin[1], + plane.origin[2], ); + region.common.layer = self.tabs[i].active_layer.clone(); + regions.push((region, body)); } } - if loops.is_empty() { + if regions.is_empty() { self.command_line .push_error(crate::t!("REGION: select closed polylines or circles.").as_ref()); } else { self.push_undo_snapshot(i, "REGION"); - let count = loops.len(); - for pts in loops { - let mut w = Wire::new(); - let first = pts.first().copied().unwrap_or(Vector3::new(0.0, 0.0, 0.0)); - w.points = pts; - let mut r = Region::new(); - r.point_of_reference = first; - r.wires = vec![w]; - r.common.layer = self.tabs[i].active_layer.clone(); - self.tabs[i].scene.add_entity(acadrust::EntityType::Region(r)); + let count = regions.len(); + let mut created = Vec::with_capacity(count); + for (region, body) in regions { + let handle = self.add_region_model(region, body); + if handle.is_null() { + self.tabs[i].scene.rollback_new_entities(&created); + self.discard_last_undo_entry(i); + return Some(iced::Task::none()); + } + created.push(handle); } self.tabs[i].dirty = true; self.command_line diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 38c287d23..a98e7a6df 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::Solid3D, objects::SolidHistoryOperation, EntityType, Handle, + entities::{EntityCommon, Region, Solid3D, Surface, SurfaceKind}, + objects::SolidHistoryOperation, + EntityType, Handle, }; use cadkernel::brep::Body; use iced::Task; @@ -11,19 +13,112 @@ use crate::modules::model::boolean_cmd::BoolOp; use crate::scene::model::solid_history; use crate::scene::model::solid_model::{self, Bool}; +#[derive(Clone, Copy, PartialEq, Eq)] +enum UnionEntityKind { + Solid, + Region, + Surface, +} + +impl UnionEntityKind { + fn from_entity(entity: &EntityType) -> Option { + Some(match entity { + EntityType::Solid3D(_) => Self::Solid, + EntityType::Region(_) => Self::Region, + EntityType::Surface(_) => Self::Surface, + _ => return None, + }) + } +} + +struct UnionGroup { + kind: UnionEntityKind, + handles: Vec, +} + +struct PreparedUnion { + kind: UnionEntityKind, + handles: Vec, + body: Body, + common: EntityCommon, +} + +fn inherited_common(source: &EntityCommon) -> EntityCommon { + let mut result = EntityCommon::new(); + result.layer = source.layer.clone(); + result.color = source.color; + result.line_weight = source.line_weight; + result.linetype = source.linetype.clone(); + result.linetype_handle = source.linetype_handle; + result.linetype_scale = source.linetype_scale; + result.transparency = source.transparency; + result.color_name = source.color_name.clone(); + result.invisible = source.invisible; + result.color_book_handle = source.color_book_handle; + result.full_visual_style_handle = source.full_visual_style_handle; + result.face_visual_style_handle = source.face_visual_style_handle; + result.edge_visual_style_handle = source.edge_visual_style_handle; + result.material_flags = source.material_flags; + result.material_handle = source.material_handle; + result.shadow_flags = source.shadow_flags; + result.plotstyle_flags = source.plotstyle_flags; + result.plotstyle_handle = source.plotstyle_handle; + result +} + +fn union_bodies( + kind: UnionEntityKind, + bodies: Vec, +) -> 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::Union, &result, &operand)?; + } + return Ok(result); + } + let references = bodies.iter().collect::>(); + let tolerance = cadkernel::brep::operation_tolerance(&references); + cadkernel::brep::union_planar_regions(&bodies, tolerance) +} + impl super::OpenCADStudio { /// Add a solid and register its persistent B-rep. pub(super) fn add_solid_model( + &mut self, + entity: EntityType, + solid: Body, + history: SolidHistoryOperation, + ) -> Handle { + self.add_solid_model_inner(entity, solid, history, true) + } + + fn add_solid_model_preserving_style( + &mut self, + entity: EntityType, + solid: Body, + history: SolidHistoryOperation, + ) -> Handle { + self.add_solid_model_inner(entity, solid, history, false) + } + + fn add_solid_model_inner( &mut self, mut entity: EntityType, solid: Body, history: SolidHistoryOperation, + apply_creation_style: bool, ) -> Handle { let i = self.active_tab; let EntityType::Solid3D(inner) = &mut entity else { return Handle::NULL; }; - inner.common.plotstyle_flags = 2; + if apply_creation_style { + inner.common.plotstyle_flags = 2; + } inner.wires = solid_model::edge_wires(&solid); let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&solid) else { @@ -32,7 +127,12 @@ impl super::OpenCADStudio { return Handle::NULL; }; inner.set_sat_document(&document); - let Some(handle) = self.commit_entity_handle(entity) else { + let handle = if apply_creation_style { + self.commit_entity_handle(entity) + } else { + self.commit_entity_handle_preserve_style(entity) + }; + let Some(handle) = handle else { return Handle::NULL; }; let require_complete = matches!(history, SolidHistoryOperation::Loft(_)); @@ -51,15 +151,34 @@ impl super::OpenCADStudio { /// Add an open sheet body as a persistent Surface entity and register its /// exact B-rep for shaded and wireframe display. pub(super) fn add_surface_model( + &mut self, + entity: EntityType, + surface: Body, + ) -> Handle { + self.add_surface_model_inner(entity, surface, true) + } + + fn add_surface_model_preserving_style( + &mut self, + entity: EntityType, + surface: Body, + ) -> Handle { + self.add_surface_model_inner(entity, surface, false) + } + + fn add_surface_model_inner( &mut self, mut entity: EntityType, surface: Body, + apply_creation_style: bool, ) -> Handle { let i = self.active_tab; let EntityType::Surface(inner) = &mut entity else { return Handle::NULL; }; - inner.common.plotstyle_flags = 2; + if apply_creation_style { + inner.common.plotstyle_flags = 2; + } inner.wires = solid_model::edge_wires(&surface); let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&surface) else { @@ -68,7 +187,12 @@ impl super::OpenCADStudio { return Handle::NULL; }; inner.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string()); - let Some(handle) = self.commit_entity_handle(entity) else { + let handle = if apply_creation_style { + self.commit_entity_handle(entity) + } else { + self.commit_entity_handle_preserve_style(entity) + }; + let Some(handle) = handle else { return Handle::NULL; }; let require_complete = self.tabs[i].scene.document.get_entity(handle).is_some_and(|entity| @@ -81,6 +205,44 @@ impl super::OpenCADStudio { handle } + pub(super) fn add_region_model(&mut self, region: Region, body: Body) -> Handle { + self.add_region_model_inner(region, body, false) + } + + fn add_region_model_preserving_style(&mut self, region: Region, body: Body) -> Handle { + self.add_region_model_inner(region, body, true) + } + + fn add_region_model_inner( + &mut self, + mut region: Region, + body: Body, + preserve_style: bool, + ) -> Handle { + let i = self.active_tab; + region.wires = solid_model::edge_wires(&body); + let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { + self.command_line + .push_error(crate::t!("The region could not be encoded as ACIS.").as_ref()); + return Handle::NULL; + }; + region.set_sat_document(&document); + let entity = EntityType::Region(region); + let handle = if preserve_style { + self.commit_entity_handle_preserve_style(entity) + } else { + self.commit_entity_handle(entity) + }; + let Some(handle) = handle else { + return Handle::NULL; + }; + if !self.tabs[i].scene.register_solid_model(handle, body) { + self.tabs[i].scene.rollback_new_entities(&[handle]); + return Handle::NULL; + } + handle + } + fn selected_solid_handles(&mut self) -> Vec { let i = self.active_tab; let mut handles: Vec = self.tabs[i] @@ -100,6 +262,38 @@ impl super::OpenCADStudio { handles } + fn selected_union_groups(&self) -> Vec { + let scene = &self.tabs[self.active_tab].scene; + let mut groups: Vec = Vec::new(); + for handle in scene.selected_handles_in_order() { + if scene.is_layer_locked(handle) { + continue; + } + let Some(kind) = scene + .document + .get_entity(handle) + .and_then(UnionEntityKind::from_entity) + else { + continue; + }; + if let Some(group) = groups.iter_mut().find(|group| group.kind == kind) { + group.handles.push(handle); + } else { + groups.push(UnionGroup { + kind, + handles: vec![handle], + }); + } + } + groups + } + + pub(super) fn union_ready(&self) -> bool { + self.selected_union_groups() + .iter() + .any(|group| group.handles.len() >= 2) + } + fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool { let i = self.active_tab; let Some(document) = @@ -197,6 +391,9 @@ impl super::OpenCADStudio { /// 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(); + } let i = self.active_tab; let handles = self.selected_solid_handles(); if handles.len() < 2 { @@ -243,6 +440,131 @@ impl super::OpenCADStudio { Task::none() } + fn union_selected_entities(&mut self) -> Task { + let i = self.active_tab; + let mut groups = self.selected_union_groups(); + let handles = groups + .iter() + .flat_map(|group| group.handles.iter().copied()) + .collect::>(); + self.tabs[i].scene.restore_solid_models(&handles); + for group in &mut groups { + group + .handles + .retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); + } + groups.retain(|group| group.handles.len() >= 2); + if groups.is_empty() { + self.command_line.push_error( + crate::t!("UNION: select at least two solids, Regions, or planar Surfaces of the same type.") + .as_ref(), + ); + return Task::none(); + } + + let mut prepared = Vec::with_capacity(groups.len()); + for group in groups { + let bodies = group + .handles + .iter() + .filter_map(|handle| self.tabs[i].scene.solid_models.get(handle).cloned()) + .collect::>(); + let result = match union_bodies(group.kind, bodies) { + Ok(result) => result, + Err(cadkernel::brep::Snag::NoClosedForm) => { + self.command_line.push_error( + crate::t!("UNION: 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!("UNION: the selected coincident geometry is ambiguous.").as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::CutRefused) => { + self.command_line.push_error( + crate::t!("UNION: the selected topology could not be closed safely.").as_ref(), + ); + return Task::none(); + } + }; + if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() { + self.command_line.push_error( + crate::t!("The UNION result could not be encoded as ACIS.").as_ref(), + ); + return Task::none(); + } + let Some(source) = group + .handles + .first() + .and_then(|handle| self.tabs[i].scene.document.get_entity(*handle)) + else { + return Task::none(); + }; + prepared.push(PreparedUnion { + kind: group.kind, + handles: group.handles, + body: result, + common: inherited_common(source.common()), + }); + } + + let consumed = prepared + .iter() + .flat_map(|group| group.handles.iter().copied()) + .collect::>(); + self.push_undo_snapshot(i, "UNION"); + let mut created = Vec::with_capacity(prepared.len()); + for group in prepared { + let handle = match group.kind { + UnionEntityKind::Solid => { + let history = solid_history::brep_op(&group.body); + let mut entity = Solid3D::new(); + entity.common = group.common; + self.add_solid_model_preserving_style( + EntityType::Solid3D(entity), + group.body, + history, + ) + } + UnionEntityKind::Region => { + let mut entity = Region::new(); + entity.common = group.common; + self.add_region_model_preserving_style(entity, group.body) + } + UnionEntityKind::Surface => { + let mut entity = Surface::new(SurfaceKind::Generic); + entity.common = group.common; + self.add_surface_model_preserving_style( + EntityType::Surface(entity), + group.body, + ) + } + }; + if handle.is_null() { + self.tabs[i].scene.rollback_new_entities(&created); + self.discard_last_undo_entry(i); + return Task::none(); + } + created.push(handle); + } + + self.tabs[i].scene.erase_entities(&consumed); + self.tabs[i].scene.deselect_all(); + for handle in &created { + self.tabs[i].scene.select_entity(*handle, false); + } + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_output( + crate::tf!("UNION: created %{count} result object(s).", count = created.len()).as_ref(), + ); + Task::none() + } + pub(super) fn solid_subtract( &mut self, bases: &[Handle], diff --git a/src/app/properties.rs b/src/app/properties.rs index 55e291820..ad055f125 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -440,15 +440,15 @@ impl OpenCADStudio { let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane); let entity = &display_entity; let group_names = self.tabs[i].scene.group_names_for_entity(handle); - let specialized_primitive = - crate::scene::model::solid_history::has_specialized_primitive_properties( + let compact_solid = + crate::scene::model::solid_history::has_compact_solid_properties( &self.tabs[i].scene.document, handle, ); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); - if specialized_primitive { - retain_specialized_primitive_sections(&mut sections); + if compact_solid { + retain_compact_solid_sections(&mut sections); } sections.extend( crate::scene::model::solid_history::primitive_properties( @@ -636,7 +636,7 @@ impl OpenCADStudio { } } - if !specialized_primitive && matches!( + if !compact_solid && matches!( entity, acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Region(_) @@ -710,7 +710,7 @@ impl OpenCADStudio { } } - if !specialized_primitive { + if !compact_solid { sections.extend(crate::entities::object_data::sections( &self.tabs[i].scene.document, &self.tabs[i].scene.object_data_cache, @@ -2068,8 +2068,8 @@ impl OpenCADStudio { }); } } - if specialized_primitive { - retain_specialized_primitive_sections(&mut sections); + if compact_solid { + retain_compact_solid_sections(&mut sections); } let title = match entity { acadrust::EntityType::Insert(ins) => { @@ -2166,7 +2166,7 @@ impl OpenCADStudio { .collect(); let mut sections = aggregate_sections(&local_refs, &text_style_names); if local_refs.iter().all(|(handle, _)| { - crate::scene::model::solid_history::has_specialized_primitive_properties( + crate::scene::model::solid_history::has_compact_solid_properties( &self.tabs[i].scene.document, *handle, ) @@ -2591,7 +2591,7 @@ impl OpenCADStudio { &mut self, entity: acadrust::EntityType, ) -> Option { - self.commit_entity_handle_with_policies(entity, false, false) + self.commit_entity_handle_with_policies(entity, false, false, false) } pub(super) fn commit_entity_handle_with_dimension_policy( @@ -2603,6 +2603,7 @@ impl OpenCADStudio { entity, preserve_dimension_layer_and_style, false, + false, ) } @@ -2610,7 +2611,14 @@ impl OpenCADStudio { &mut self, entity: acadrust::EntityType, ) -> Option { - self.commit_entity_handle_with_policies(entity, false, true) + self.commit_entity_handle_with_policies(entity, false, true, false) + } + + pub(super) fn commit_entity_handle_preserve_style( + &mut self, + entity: acadrust::EntityType, + ) -> Option { + self.commit_entity_handle_with_policies(entity, false, true, true) } fn commit_entity_handle_with_policies( @@ -2618,6 +2626,7 @@ impl OpenCADStudio { mut entity: acadrust::EntityType, preserve_dimension_layer_and_style: bool, preserve_entity_layer: bool, + preserve_entity_style: bool, ) -> Option { let i = self.active_tab; let tracks_dimension_chain = matches!( @@ -2735,25 +2744,32 @@ impl OpenCADStudio { } } - crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color); - crate::scene::view::dispatch::apply_common_prop( - &mut entity, - "linetype", - &self.ribbon.active_linetype.clone(), - ); - crate::scene::view::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight); - // CELTSCALE (header.current_entity_linetype_scale): new entities - // pick up the document's saved per-entity linetype scale. The user - // can override per entity later via the properties panel. - let celtscale = self.tabs[i].scene.document.header.current_entity_linetype_scale; - if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 { - entity.common_mut().linetype_scale = celtscale; + if !preserve_entity_style { + crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color); + crate::scene::view::dispatch::apply_common_prop( + &mut entity, + "linetype", + &self.ribbon.active_linetype.clone(), + ); + crate::scene::view::dispatch::apply_line_weight( + &mut entity, + self.ribbon.active_lineweight, + ); + // CELTSCALE (header.current_entity_linetype_scale): new entities + // pick up the document's saved per-entity linetype scale. The user + // can override per entity later via the properties panel. + let celtscale = self.tabs[i].scene.document.header.current_entity_linetype_scale; + if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 { + entity.common_mut().linetype_scale = celtscale; + } } - crate::scene::creation_style::apply_current_creation_styles( - &self.tabs[i].scene.document, - &mut entity, - ); + if !preserve_entity_style { + crate::scene::creation_style::apply_current_creation_styles( + &self.tabs[i].scene.document, + &mut entity, + ); + } if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) = (inherited_dimension, &mut entity) { @@ -3061,7 +3077,7 @@ fn aggregate_solid_history_sections( merged } -fn retain_specialized_primitive_sections( +fn retain_compact_solid_sections( sections: &mut Vec, ) { sections.iter_mut().for_each(|section| { diff --git a/src/modules/draw/select.rs b/src/modules/draw/select.rs index 9ff9efe08..8116fbc07 100644 --- a/src/modules/draw/select.rs +++ b/src/modules/draw/select.rs @@ -17,6 +17,7 @@ use crate::modules::draw::fence::FencePick; use crate::scene::model::wire_model::WireModel; pub struct SelectObjectsCommand { + prompt_cmd: String, pending_cmd: String, /// Selection accumulated so far (kept in sync with the scene selection by /// each `on_selection_complete` call). Applied on Enter when `commit_on_enter`. @@ -24,6 +25,8 @@ pub struct SelectObjectsCommand { /// When true, gathering continues until Enter / right-click commits the set. /// When false, the first completed selection action fires immediately. commit_on_enter: bool, + /// Whether the generic selection methods are shown as command buttons. + show_options: bool, /// A Fence / WPolygon / CPolygon path being picked point by point. While /// one is under way the host must send clicks here as points rather than /// treating them as picks, which is what `is_selection_gathering` reports. @@ -37,9 +40,25 @@ impl SelectObjectsCommand { /// Standard selection set: accumulate picks, apply on Enter / right-click. pub fn new(pending_cmd: &str) -> Self { Self { + prompt_cmd: pending_cmd.to_string(), pending_cmd: pending_cmd.to_string(), handles: Vec::new(), commit_on_enter: true, + show_options: true, + pick: None, + pick_crossing: true, + } + } + + /// Plain gather prompt with no generic selection-method buttons. The + /// visible command name may differ from the private apply command. + pub fn plain(prompt_cmd: &str, pending_cmd: &str) -> Self { + Self { + prompt_cmd: prompt_cmd.to_string(), + pending_cmd: pending_cmd.to_string(), + handles: Vec::new(), + commit_on_enter: true, + show_options: false, pick: None, pick_crossing: true, } @@ -49,9 +68,11 @@ impl SelectObjectsCommand { /// immediately, with no Enter (used by commands that act on one object). pub fn instant(pending_cmd: &str) -> Self { Self { + prompt_cmd: pending_cmd.to_string(), pending_cmd: pending_cmd.to_string(), handles: Vec::new(), commit_on_enter: false, + show_options: false, pick: None, pick_crossing: true, } @@ -82,12 +103,12 @@ impl CadCommand for SelectObjectsCommand { if self.commit_on_enter && !self.handles.is_empty() { t!( "%{cmd} Select objects (%{count} selected, Enter to apply):", - cmd = self.pending_cmd, + cmd = self.prompt_cmd, count = self.handles.len() ) .into_owned() } else { - t!("%{cmd} Select objects:", cmd = self.pending_cmd).into_owned() + t!("%{cmd} Select objects:", cmd = self.prompt_cmd).into_owned() } } @@ -108,7 +129,7 @@ impl CadCommand for SelectObjectsCommand { // they work typed as well — these buttons only surface them, which is what // the on-screen keyboard-less case needs. fn options(&self) -> Vec { - if self.commit_on_enter { + if self.commit_on_enter && self.show_options { vec![ CmdOption::new(t!("Window").as_ref(), "W"), CmdOption::new(t!("Crossing").as_ref(), "C"), diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index ebfa32df8..3e3376bfa 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -163,6 +163,10 @@ 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) +} + 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 1fe0317b4..89faaa20e 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -605,6 +605,9 @@ impl Scene { 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) + } _ => None, })?; Some((handle, body)) diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 37293ac70..3130cb3bc 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -214,6 +214,21 @@ pub fn has_specialized_primitive_properties( ) } +/// Solid history results whose public Properties palette is fully described by +/// the common entity rows plus [`primitive_properties`]. A generic B-rep has +/// no stable primitive dimensions to expose, but it still uses the compact +/// Solid History palette rather than the internal ACIS/cache diagnostics. +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(_)) + ) +} + pub fn reference_point(operation: &SolidHistoryOperation) -> Option { match operation { SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => world_point( @@ -975,6 +990,41 @@ fn cone_properties( ] } +fn brep_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> Vec { + let (record_history, object_show_history, show_history_mode) = + history_flags(document, handle).unwrap_or((false, false, 1)); + let (show_history, show_history_editable) = + displayed_history_state(object_show_history, show_history_mode); + vec![PropSection { + title: t!("Solid History").into_owned(), + props: vec![ + Property { + label: t!("History").into_owned(), + field: PROP_HISTORY, + value: PropValue::Choice { + selected: if record_history { "Record" } else { "None" }.to_string(), + options: vec!["None".to_string(), "Record".to_string()], + }, + }, + Property { + label: t!("Show History").into_owned(), + field: PROP_SHOW_HISTORY, + value: if show_history_editable { + PropValue::Choice { + selected: if show_history { "Yes" } else { "No" }.to_string(), + options: vec!["No".to_string(), "Yes".to_string()], + } + } else { + PropValue::ReadOnly(if show_history { "Yes" } else { "No" }.to_string()) + }, + }, + ], + }] +} + fn cylinder_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -1411,6 +1461,7 @@ pub fn primitive_properties( extrusion_properties(document, handle, value) } SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value), + SolidHistoryOperation::Brep(_) => brep_properties(document, handle), _ => Vec::new(), } } diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index 24969c146..d6e8c6d9a 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -365,13 +365,18 @@ pub enum Bool { /// missing, and passing that on unchanged is the point: a half-done boolean /// looks finished. pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option { + boolean_result(op, a, b).ok() +} + +/// Combine two solids while retaining the kernel's exact refusal reason. +pub fn boolean_result(op: Bool, a: &Body, b: &Body) -> Result { let how = match op { Bool::Union => brep::Operation::Union, Bool::Subtract => brep::Operation::Difference, Bool::Intersect => brep::Operation::Intersection, }; let tolerance = brep::operation_tolerance(&[a, b]); - brep::combine(a.clone(), b.clone(), how, tolerance).ok() + brep::combine(a.clone(), b.clone(), how, tolerance) } // ── Tessellation ────────────────────────────────────────────────────────────