fix(model): repair solid selection flows
This commit is contained in:
parent
06cd0ec6a9
commit
7794736271
6 changed files with 237 additions and 14 deletions
|
|
@ -683,8 +683,12 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let was_active = self.tabs[self.active_tab].active_cmd.is_some();
|
let was_active = self.tabs[self.active_tab].active_cmd.is_some();
|
||||||
let preserve_selection =
|
let preserve_selection = matches!(
|
||||||
matches!(result, CmdResult::Relaunch(..) | CmdResult::Dispatch(..));
|
&result,
|
||||||
|
CmdResult::Relaunch(..)
|
||||||
|
| CmdResult::Dispatch(..)
|
||||||
|
| CmdResult::SolidSubtract { .. }
|
||||||
|
);
|
||||||
let task = self.apply_cmd_result_inner(result);
|
let task = self.apply_cmd_result_inner(result);
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let preview_hidden = self.tabs[i]
|
let preview_hidden = self.tabs[i]
|
||||||
|
|
@ -3251,6 +3255,15 @@ impl OpenCADStudio {
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CmdResult::SolidSubtract { bases, cutters } => {
|
||||||
|
let task = self.solid_subtract(&bases, &cutters);
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
CmdResult::HatcheditApply {
|
CmdResult::HatcheditApply {
|
||||||
handle,
|
handle,
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
||||||
let selected = app.tabs[tab].scene.selected_handles_in_order();
|
let scene = &app.tabs.get(tab)?.scene;
|
||||||
(selected.len() == 1
|
let selected = scene.selected_handles_in_order();
|
||||||
&& matches!(
|
let [handle] = selected.as_slice() else {
|
||||||
app.tabs[tab].scene.document.get_entity(selected[0]),
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
scene.document.get_entity(*handle),
|
||||||
Some(acadrust::EntityType::Solid3D(_))
|
Some(acadrust::EntityType::Solid3D(_))
|
||||||
))
|
) {
|
||||||
.then_some(selected[0])
|
Some(*handle)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
|
|
||||||
|
|
@ -934,12 +934,56 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Solid booleans ─────────────────────────────────────────────
|
// ── Solid booleans ─────────────────────────────────────────────
|
||||||
"UNION" | "SUBTRACT" | "INTERSECT" => {
|
"UNION" | "INTERSECT" => {
|
||||||
use crate::modules::model::boolean_cmd::BoolOp;
|
use crate::modules::model::boolean_cmd::BoolOp;
|
||||||
if let Some(op) = BoolOp::from_id(cmd) {
|
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));
|
return Some(self.solid_boolean(op));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"SUBTRACT" => {
|
||||||
|
use crate::modules::model::boolean_cmd::SubtractCommand;
|
||||||
|
let bases = {
|
||||||
|
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(_))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
let subtract = SubtractCommand::new(bases);
|
||||||
|
self.command_line.push_info(&subtract.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(subtract));
|
||||||
|
}
|
||||||
|
|
||||||
// INTERFERE — non-destructive intersect: solid from the overlap.
|
// INTERFERE — non-destructive intersect: solid from the overlap.
|
||||||
"INTERFERE" => {
|
"INTERFERE" => {
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,80 @@ impl super::OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn solid_subtract(
|
||||||
|
&mut self,
|
||||||
|
bases: &[Handle],
|
||||||
|
cutters: &[Handle],
|
||||||
|
) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let valid_solid = |handle: &Handle| {
|
||||||
|
!self.tabs[i].scene.is_layer_locked(*handle)
|
||||||
|
&& matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(*handle),
|
||||||
|
Some(EntityType::Solid3D(_))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut base_handles: Vec<_> = bases.iter().copied().filter(valid_solid).collect();
|
||||||
|
let mut cutter_handles: Vec<_> = cutters.iter().copied().filter(valid_solid).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));
|
||||||
|
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.")
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
operands = base_handles;
|
||||||
|
operands.extend(cutter_handles);
|
||||||
|
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);
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
if !handle.is_null() {
|
||||||
|
self.tabs[i].scene.select_entity(handle, false);
|
||||||
|
}
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
/// Slice the one selected solid with an axis-aligned plane (axis 0/1/2 =
|
/// Slice the one selected solid with an axis-aligned plane (axis 0/1/2 =
|
||||||
/// X/Y/Z at `value`), keeping the lower side when `keep_low` is true. The
|
/// X/Y/Z at `value`), keeping the lower side when `keep_low` is true. The
|
||||||
/// kept half is the intersection of the solid with a half-space box, reusing
|
/// kept half is the intersection of the solid with a half-space box, reusing
|
||||||
|
|
|
||||||
|
|
@ -1465,6 +1465,10 @@ pub enum CmdResult {
|
||||||
value: f64,
|
value: f64,
|
||||||
fillet: bool,
|
fillet: bool,
|
||||||
},
|
},
|
||||||
|
SolidSubtract {
|
||||||
|
bases: Vec<Handle>,
|
||||||
|
cutters: Vec<Handle>,
|
||||||
|
},
|
||||||
/// INSERT landed on a block that has AttributeDefinitions.
|
/// INSERT landed on a block that has AttributeDefinitions.
|
||||||
/// The host should look up the attdefs for `block_name` from the document
|
/// The host should look up the attdefs for `block_name` from the document
|
||||||
/// and call `attreq_set_attdefs()` on the command, then loop on text input.
|
/// and call `attreq_set_attdefs()` on the command, then loop on text input.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Boolean operations on 3D solids. The actual CSG runs in
|
use acadrust::{EntityType, Handle};
|
||||||
// `App::solid_boolean` (src/app/model_ops.rs) using the kernel on the
|
use glam::DVec3;
|
||||||
// cached B-reps; this module just names the operations.
|
|
||||||
|
use crate::command::{CadCommand, CmdResult, SelectionEntity};
|
||||||
|
|
||||||
/// Which boolean operation to apply to selected solids.
|
/// Which boolean operation to apply to selected solids.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
|
@ -21,6 +22,86 @@ impl BoolOp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SubtractStep {
|
||||||
|
Bases,
|
||||||
|
Cutters,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SubtractCommand {
|
||||||
|
step: SubtractStep,
|
||||||
|
bases: Vec<Handle>,
|
||||||
|
selected: Vec<Handle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubtractCommand {
|
||||||
|
pub fn new(bases: Vec<Handle>) -> Self {
|
||||||
|
let step = if bases.is_empty() {
|
||||||
|
SubtractStep::Bases
|
||||||
|
} else {
|
||||||
|
SubtractStep::Cutters
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
step,
|
||||||
|
bases,
|
||||||
|
selected: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for SubtractCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SUBTRACT"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.step {
|
||||||
|
SubtractStep::Bases => crate::t!("SUBTRACT Select base solids, then press Enter:")
|
||||||
|
.into_owned(),
|
||||||
|
SubtractStep::Cutters => {
|
||||||
|
crate::t!("SUBTRACT Select solids to subtract, then press Enter:").into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
if self.selected.is_empty() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
match self.step {
|
||||||
|
SubtractStep::Bases => {
|
||||||
|
self.bases = std::mem::take(&mut self.selected);
|
||||||
|
self.step = SubtractStep::Cutters;
|
||||||
|
CmdResult::DeselectAndContinue
|
||||||
|
}
|
||||||
|
SubtractStep::Cutters => CmdResult::SolidSubtract {
|
||||||
|
bases: std::mem::take(&mut self.bases),
|
||||||
|
cutters: std::mem::take(&mut self.selected),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selection_gathering(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||||
|
self.selected = entities
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entity| {
|
||||||
|
matches!(entity.entity, EntityType::Solid3D(_)).then_some(entity.handle)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
inventory::submit!(crate::command::CommandRegistration {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
names: &["UNION", "SUBTRACT", "INTERSECT"]
|
names: &["UNION", "SUBTRACT", "INTERSECT"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue