forked from 1b-agent/opencadstudio-upstream
Complete SUBTRACT behavior
This commit is contained in:
1 parent
5857ffa2bd
commit
04a9a1b0a0
10 files changed
+562
-69
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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"] }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
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));
|
||||
}
|
||||
|
||||
+369
-43
@@ -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> {
|
||||
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<Handle>,
|
||||
@@ -43,6 +56,25 @@ struct PreparedUnion {
|
||||
common: EntityCommon,
|
||||
}
|
||||
|
||||
struct SubtractGroup {
|
||||
kind: UnionEntityKind,
|
||||
plane: Option<cadkernel::space::Plane>,
|
||||
bases: Vec<Handle>,
|
||||
cutters: Vec<Handle>,
|
||||
}
|
||||
|
||||
type PreparedSolidDisplay = (
|
||||
crate::scene::model::mesh_model::MeshLodSet,
|
||||
Vec<Wire>,
|
||||
[f64; 3],
|
||||
);
|
||||
|
||||
struct PreparedSubtract {
|
||||
bases: Vec<Handle>,
|
||||
cutters: Vec<Handle>,
|
||||
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<cadkernel::space::Plane> {
|
||||
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<cadkernel::space::Plane>,
|
||||
bases: Vec<Body>,
|
||||
cutters: Vec<Body>,
|
||||
) -> Result<Body, cadkernel::brep::Snag> {
|
||||
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::<Vec<_>>();
|
||||
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<EntityType> {
|
||||
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<Message> {
|
||||
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::<Handle, Body>::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::<SubtractGroup>::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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
let cutter_bodies = group
|
||||
.cutters
|
||||
.iter()
|
||||
.map(|handle| operand_bodies[handle].clone())
|
||||
.collect::<Vec<_>>();
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -1557,6 +1557,7 @@ pub enum CmdResult {
|
||||
SolidSubtract {
|
||||
bases: Vec<Handle>,
|
||||
cutters: Vec<Handle>,
|
||||
convert_meshes: bool,
|
||||
},
|
||||
/// INSERT landed on a block that has AttributeDefinitions.
|
||||
/// The host should look up the attdefs for `block_name` from the document
|
||||
|
||||
@@ -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<cadkernel::brep::Body> {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Option<Vec<_>>>()?;
|
||||
(indices.len() >= 3).then_some(indices)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
cadkernel::brep::make::faceted_solid(&vertices, &faces)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn face_triangle_indices(
|
||||
vertices: &[[f64; 3]],
|
||||
faces: &[Vec<usize>],
|
||||
|
||||
@@ -25,16 +25,20 @@ impl BoolOp {
|
||||
enum SubtractStep {
|
||||
Bases,
|
||||
Cutters,
|
||||
ConvertMeshes,
|
||||
}
|
||||
|
||||
pub struct SubtractCommand {
|
||||
step: SubtractStep,
|
||||
bases: Vec<Handle>,
|
||||
cutters: Vec<Handle>,
|
||||
selected: Vec<Handle>,
|
||||
bases_have_mesh: bool,
|
||||
selected_has_mesh: bool,
|
||||
}
|
||||
|
||||
impl SubtractCommand {
|
||||
pub fn new(bases: Vec<Handle>) -> Self {
|
||||
pub fn new(bases: Vec<Handle>, 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] <Yes>:"
|
||||
)
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
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<CmdResult> {
|
||||
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<SelectionEntity>) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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<glam::DVec3> {
|
||||
@@ -1441,7 +1439,21 @@ pub fn primitive_properties(
|
||||
handle: acadrust::Handle,
|
||||
) -> Vec<PropSection> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user