Files
OneCAD-Draft/src/app/command_driver/solid3d.rs
T
Hakan Seven 5bb0efd93e Merge pull request #1679 from ramox81/codex/surface-associativity-sources
Associative extruded, revolved and lofted surfaces; spline and ellipse sources

# Conflicts:
#	src/app/command_driver/solid3d.rs
#	src/scene/dwg_native_constraints.rs
#	src/scene/model/mod.rs
#	src/scene/model/sweep_command_model.rs
#	src/scene/model/sweep_model.rs
#	src/scene/modify.rs
2026-10-12 00:14:34 +03:00

902 lines
35 KiB
Rust

use super::*;
impl OpenCADStudio {
pub(super) fn handle_extrude_entities(&mut self, result: CmdResult) -> Option<Task<Message>> {
let i = self.active_tab;
let CmdResult::ExtrudeEntities {
handles,
extent,
mode,
taper_angle,
color: _,
} = result
else {
unreachable!("router only routes the matching variant");
};
let delete_sources = self.delete_objects != 0;
if handles.is_empty()
|| handles
.iter()
.any(|handle| self.reject_locked_edit(i, *handle))
{
self.tabs[i].active_cmd = None;
return Some(Task::none());
}
use crate::command::{ExtrudeExtent, ExtrudeMode};
use crate::modules::insert::solid3d_cmds::{empty_extruded_surface, empty_solid3d};
use crate::scene::model::sweep_model;
let path = match extent {
ExtrudeExtent::Path(handle) => self.tabs[i]
.scene
.document
.get_entity(handle)
.cloned()
.map(|entity| (handle, entity)),
_ => None,
};
if matches!(extent, ExtrudeExtent::Path(_)) && path.is_none() {
self.command_line
.push_error(crate::t!("EXTRUDE: path entity not found.").as_ref());
self.tabs[i].active_cmd = None;
return Some(Task::none());
}
let pending = self.begin_undo(i, "EXTRUDE", handles.len(), true);
let mut created_handles = Vec::new();
let mut associative_sources = Vec::new();
let mut consumed = Vec::new();
let mut failed = 0usize;
for handle in &handles {
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
failed += 1;
continue;
};
let Some((profile, closed)) = sweep_model::extrusion_profile_of(&entity) else {
failed += 1;
continue;
};
let direction = match extent {
ExtrudeExtent::Height(height) => profile.plane.normal().map(|normal| {
glam::DVec3::new(normal[0] * height, normal[1] * height, normal[2] * height)
}),
ExtrudeExtent::Direction(direction) => Some(direction),
ExtrudeExtent::Path(_) => None,
};
let path_direction = path
.as_ref()
.and_then(|(_, path)| sweep_model::straight_path_direction(path))
.map(glam::DVec3::from_array);
// The creation mode controls closed profiles. Open curves
// always create sheet surfaces.
let creates_surface = matches!(mode, ExtrudeMode::Surface) || !closed;
let body = match (creates_surface, &path, direction) {
(false, Some((_, path)), _) => {
sweep_model::extruded_along_path(&entity, path, taper_angle)
}
(false, None, Some(direction)) => {
sweep_model::extruded_direction(&entity, direction.to_array(), taper_angle)
}
(true, None, Some(direction)) => {
sweep_model::extruded_surface(&entity, direction.to_array(), taper_angle)
}
(true, Some(_), _) => path_direction.and_then(|direction| {
sweep_model::extruded_surface(&entity, direction.to_array(), taper_angle)
}),
_ => None,
};
let Some(body) = body else {
failed += 1;
continue;
};
let created = if creates_surface {
let direction = direction.or(path_direction).unwrap_or(glam::DVec3::ZERO);
let history = path
.is_none()
.then(|| {
sweep_model::extrusion_history(
&entity,
None,
direction.to_array(),
taper_angle,
profile.plane.origin,
)
})
.flatten();
let mut surface = empty_extruded_surface(direction, taper_angle);
if let (
Some(codec::objects::SolidHistoryOperation::Extrusion(value)),
codec::EntityType::Surface(entity),
) = (&history, &mut surface)
{
if let Some(data) =
crate::scene::model::solid_history::extrusion_surface_data(value)
{
entity.surface_data = data;
}
}
let created = match history {
Some(history) => {
self.add_surface_model_with_history(surface, body, history)
}
None => self.add_surface_model(surface, body),
};
// An associative surface keeps reading its profile.
if sweep_model::surface_associativity() && path.is_none() && !created.is_null() {
use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources};
let (height, given) = match extent {
ExtrudeExtent::Height(height) => (height, None),
_ => (direction.length(), Some(direction)),
};
link_surface_sources(
&mut self.tabs[i].scene.document,
created,
&SurfaceSources::Extrude { profile: *handle, height, direction: given, taper: taper_angle },
);
associative_sources.push(*handle);
}
created
} else {
let direction = direction.unwrap_or(glam::DVec3::ZERO);
let history = path
.as_ref()
.and_then(|(_, path)| {
sweep_model::sweep_history(&entity, path, taper_angle, profile.plane.origin)
})
.or_else(|| {
sweep_model::extrusion_history(
&entity,
None,
direction.to_array(),
taper_angle,
profile.plane.origin,
)
})
.unwrap_or_else(|| crate::scene::model::solid_history::brep_op(&body));
self.add_solid_model(empty_solid3d(), body, history)
};
if created.is_null() {
failed += 1;
} else {
created_handles.push(created);
if delete_sources && !associative_sources.contains(handle) {
consumed.push(*handle);
}
}
}
if delete_sources && !created_handles.is_empty() {
consumed.sort_unstable_by_key(|handle| handle.value());
consumed.dedup();
self.tabs[i].scene.erase_entities(&consumed);
}
if !created_handles.is_empty() {
self.tabs[i].scene.deselect_all();
for handle in &created_handles {
self.tabs[i].scene.select_entity(*handle, true);
}
self.tabs[i].dirty = true;
self.command_line.push_output(
crate::t!(
"EXTRUDE: created %{created} object(s); %{failed} source(s) could not be extruded.",
created = created_handles.len(),
failed = failed
)
.as_ref(),
);
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
} else {
self.command_line.push_error(
crate::t!(
"EXTRUDE: no selected object could be extruded with the requested options."
)
.as_ref(),
);
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
self.refresh_properties();
None
}
pub(super) fn handle_thicken_entities(
&mut self,
handles: Vec<Handle>,
distance: f64,
) -> Option<Task<Message>> {
let i = self.active_tab;
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
if handles.is_empty() {
self.command_line
.push_output(crate::t!("No surfaces selected.").as_ref());
return Some(Task::none());
}
if !distance.is_finite() {
self.command_line
.push_error(crate::t!("Requires numeric distance or two points.").as_ref());
return Some(Task::none());
}
if distance.abs() <= f64::EPSILON {
return Some(Task::none());
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
let pending = self.begin_undo(i, "THICKEN", handles.len(), true);
let mut created = 0usize;
let mut failed = 0usize;
let mut self_intersections = 0usize;
for handle in handles {
let Some(codec::EntityType::Surface(surface)) =
self.tabs[i].scene.document.get_entity(handle)
else {
failed += 1;
continue;
};
let kernel_distance = if surface.kind == codec::entities::SurfaceKind::Revolved {
-distance
} else {
distance
};
let body = self.tabs[i]
.scene
.solid_models
.get(&handle)
.cloned()
.or_else(|| crate::scene::convert::solid3d_tess::kernel_surface_body(surface));
let Some(body) = body else {
failed += 1;
continue;
};
let solid = match kernel::brep::thicken(&body, kernel_distance) {
Ok(solid) => solid,
Err(kernel::brep::ThickenError::SelfIntersection) => {
failed += 1;
self_intersections += 1;
continue;
}
Err(_) => {
failed += 1;
continue;
}
};
let history = crate::scene::model::solid_history::brep_op(&solid);
if self
.add_solid_model(empty_solid3d(), solid, history)
.is_null()
{
failed += 1;
} else {
created += 1;
}
}
if created > 0 {
self.tabs[i].dirty = true;
}
if self_intersections > 0 {
self.command_line.push_error(&crate::tf!(
"{} surface(s) cannot be thickened because the offset intersects itself.",
self_intersections
));
}
if failed > 0 {
self.command_line.push_output(&crate::tf!(
"THICKEN: created {} solid(s); {} surface(s) could not be thickened exactly.",
created,
failed
));
}
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
self.refresh_properties();
None
}
pub(super) fn handle_presspull_pick(&mut self, result: CmdResult) {
let CmdResult::PresspullPick {
handle,
point,
offset,
multiple: _,
} = result
else {
unreachable!("router only routes the matching variant");
};
self.presspull_pick(handle, point, offset);
}
pub(super) fn handle_presspull_apply(
&mut self,
targets: Vec<crate::scene::model::presspull_model::PresspullTarget>,
distance: f64,
) {
self.presspull_apply(targets, distance);
}
pub(super) fn handle_revolve_entities(&mut self, result: CmdResult) -> Option<Task<Message>> {
let i = self.active_tab;
let CmdResult::RevolveEntities {
handles,
axis_start,
axis_end,
axis_object,
angle,
start_angle,
mode,
color: _,
} = result
else {
unreachable!("router only routes the matching variant");
};
if handles.is_empty() {
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 Some(Task::none());
}
let delete_sources = self.delete_objects != 0;
use crate::command::ExtrudeMode;
use crate::modules::insert::solid3d_cmds::{empty_revolved_surface, empty_solid3d};
use crate::scene::model::sweep_model;
let from = axis_start.to_array();
let to = axis_end.to_array();
let mut editable_handles = Vec::with_capacity(handles.len());
let mut failed = 0usize;
for handle in handles {
if self.reject_locked_edit(i, handle) {
failed += 1;
} else {
editable_handles.push(handle);
}
}
let pending = if editable_handles.is_empty() {
None
} else {
self.begin_undo(i, "REVOLVE", editable_handles.len(), true)
};
let mut created_handles = Vec::new();
let mut associative_sources = Vec::new();
let mut consumed = Vec::new();
for handle in &editable_handles {
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
failed += 1;
continue;
};
let Some((_, closed)) = sweep_model::extrusion_profile_of(&entity) else {
failed += 1;
continue;
};
let creates_surface = mode == ExtrudeMode::Surface || !closed;
let created = if creates_surface {
let Some(body) =
sweep_model::revolved_surface(&entity, from, to, angle, start_angle)
else {
failed += 1;
continue;
};
let created = self.add_surface_model(
empty_revolved_surface(
&entity,
axis_start,
axis_end,
angle,
start_angle,
),
body,
);
// An associative surface keeps reading its profile and axis.
if sweep_model::surface_associativity() && !created.is_null() {
use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources};
link_surface_sources(
&mut self.tabs[i].scene.document,
created,
&SurfaceSources::Revolve {
profile: *handle,
axis: axis_object,
start: axis_start,
end: axis_end,
angle,
start_angle,
},
);
associative_sources.push(*handle);
}
created
} else {
let result = sweep_model::revolve_history(&entity, from, to, angle, start_angle)
.and_then(|history| {
kernel::acis::rebuild_body(&history)
.ok()
.map(|solid| (solid, history))
})
.or_else(|| {
sweep_model::revolved(&entity, from, to, angle, start_angle).map(|solid| {
let history = crate::scene::model::solid_history::brep_op(&solid);
(solid, history)
})
});
let Some((solid, history)) = result else {
failed += 1;
continue;
};
self.add_solid_model(empty_solid3d(), solid, history)
};
if created.is_null() {
failed += 1;
} else {
created_handles.push(created);
if delete_sources && !associative_sources.contains(handle) {
consumed.push(*handle);
}
}
}
if delete_sources && !created_handles.is_empty() {
consumed.sort_unstable_by_key(|handle| handle.value());
consumed.dedup();
self.tabs[i].scene.erase_entities(&consumed);
}
if !created_handles.is_empty() {
self.tabs[i].scene.deselect_all();
for handle in &created_handles {
self.tabs[i].scene.select_entity(*handle, true);
}
self.tabs[i].dirty = true;
self.command_line.push_output(
crate::t!(
"REVOLVE: created %{created} object(s); %{failed} source(s) could not be revolved.",
created = created_handles.len(),
failed = failed
)
.as_ref(),
);
} else {
self.command_line.push_error(
crate::t!(
"REVOLVE: no selected object could be revolved with the requested options."
)
.as_ref(),
);
}
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
self.refresh_properties();
None
}
pub(super) fn handle_sweep_entities(&mut self, result: CmdResult) {
let i = self.active_tab;
let CmdResult::SweepEntities {
handles,
path_handle,
mode,
options,
color: _,
expressions,
} = result
else {
unreachable!("router only routes the matching variant");
};
use crate::command::ExtrudeMode;
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::sweep_model;
let delete_objects = self.delete_objects;
let delete_profiles = delete_objects != 0;
let path = self.tabs[i].scene.document.get_entity(path_handle).cloned();
let mut profiles = Vec::new();
let mut failed = 0usize;
for handle in handles {
if handle == path_handle || self.reject_locked_edit(i, handle) {
failed += 1;
continue;
}
if let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() {
profiles.push((handle, entity));
} else {
failed += 1;
}
}
let selection = profiles
.iter()
.map(|(_, entity)| entity.clone())
.collect::<Vec<_>>();
// Group 294: a base point the user picked.
let picked_base = options.base_point.is_some();
let several = profiles.len() > 1;
let picked = options;
let options = sweep_model::sweep_selection_options(&selection, options);
let pending = if profiles.is_empty() {
None
} else {
self.begin_undo(i, "SWEEP", profiles.len(), true)
};
let mut created_handles = Vec::new();
let mut consumed = Vec::new();
for (handle, profile) in profiles {
// A refused sweep reports the modeler error code, as the
// reference does.
let result = (|| -> Result<_, Option<u32>> {
let (path, options) = path.as_ref().zip(options).ok_or(None)?;
if sweep_model::spatial_profile(&profile).is_some() {
let record = sweep_model::spatial_sweep_record(&profile, path, options)?;
let body =
kernel::acis::rebuild_sweep_with_mode(&record, true).map_err(|_| None)?;
return Ok((body, record, true));
}
let mut record = sweep_model::sweep_record(&profile, path, options).ok_or(None)?;
record.flags_294_296[0] |= picked_base;
let (_, _, closed) = kernel::acis::sweep_profile_geometry(
record.sweep_entity.as_ref().ok_or(None)?,
record.sweep_entity_transform,
)
.map_err(|_| None)?;
let surface = mode == ExtrudeMode::Surface || !closed;
let body =
kernel::acis::rebuild_sweep_with_mode(&record, surface).map_err(|error| {
match error {
kernel::acis::HistoryRebuildError::Refused(why) => {
Some(sweep_model::sweep_refusal_code(why))
}
_ => None,
}
})?;
Ok((body, record, surface))
})();
let (body, record, surface) = match result {
Ok(value) => value,
Err(code) => {
if let Some(code) = code {
self.command_line
.push_output(crate::t!("Modeling Operation Error:").as_ref());
self.command_line.push_output(
crate::tf!("Error Code Number is {code}", code = code).as_ref(),
);
}
failed += 1;
continue;
}
};
// An associative surface keeps the profile and path it reads.
let associative = surface && sweep_model::surface_associativity();
let deletes_path =
!associative && crate::app::delobj_deletes_auxiliary(delete_objects, surface);
if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) {
failed += 1;
continue;
}
let created = if surface {
let created =
self.add_surface_model(sweep_model::swept_surface_entity(&record), body);
sweep_model::link_sweep_expressions(
&mut self.tabs[i].scene.document,
created,
&expressions,
);
if associative {
// A picked base point, or the one several profiles share.
let linked = crate::command::SweepOptions {
base_point: if picked_base || several {
options.and_then(|o| o.base_point)
} else {
None
},
..picked
};
sweep_model::link_sweep_sources(
&mut self.tabs[i].scene.document,
created,
handle,
path_handle,
&linked,
);
}
created
} else {
self.add_solid_model(
empty_solid3d(),
body,
codec::objects::SolidHistoryOperation::Sweep(record),
)
};
if created.is_null() {
failed += 1;
} else {
created_handles.push(created);
if delete_profiles && !associative {
consumed.push(handle);
}
if deletes_path {
consumed.push(path_handle);
}
}
}
if !created_handles.is_empty() {
consumed.sort_unstable_by_key(|handle| handle.value());
consumed.dedup();
self.tabs[i].scene.erase_entities(&consumed);
self.tabs[i].scene.deselect_all();
for handle in &created_handles {
self.tabs[i].scene.select_entity(*handle, true);
}
self.tabs[i].dirty = true;
}
if failed > 0 {
self.command_line.push_output(
crate::tf!(
"Unable to sweep {failed} selected objects.",
failed = failed
)
.as_ref(),
);
}
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
self.refresh_properties();
}
pub(super) fn handle_loft_entities(&mut self, result: CmdResult) -> Option<Task<Message>> {
let i = self.active_tab;
let CmdResult::LoftEntities {
sections,
guides,
path,
mode,
options,
color: _,
} = result
else {
unreachable!("router only routes the matching variant");
};
use crate::command::LoftSectionSelection;
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::loft_command_model;
let mut sources = sections
.iter()
.flat_map(|section| match section {
LoftSectionSelection::Entity(handle) => vec![*handle],
LoftSectionSelection::Join(handles) => handles.clone(),
LoftSectionSelection::Point(_) => Vec::new(),
})
.collect::<Vec<_>>();
sources.sort_unstable_by_key(|handle| handle.value());
sources.dedup();
let delete_objects = self.delete_objects;
let delete_sections = delete_objects != 0;
let section_locked = delete_sections
&& sources
.iter()
.any(|handle| self.tabs[i].scene.is_layer_locked(*handle));
let available = sources
.iter()
.chain(guides.iter())
.copied()
.chain(path)
.filter_map(|handle| {
self.tabs[i]
.scene
.document
.get_entity(handle)
.cloned()
.map(|entity| (handle, entity))
})
.collect::<Vec<_>>();
let result = if section_locked {
Err(
"LOFT: a source is on a locked layer; disable source deletion or unlock it."
.to_string(),
)
} else {
loft_command_model::record(&sections, &guides, path, &available, mode, options)
.and_then(|record| {
kernel::acis::rebuild_loft_with_options(&record).map(|body| (body, record))
})
};
match result {
Ok((body, record)) => {
let surface = record
.parameters
.as_ref()
.is_some_and(|settings| settings.surface);
let delete_auxiliary =
crate::app::delobj_deletes_auxiliary(delete_objects, surface);
if delete_auxiliary
&& guides
.iter()
.copied()
.chain(path)
.any(|handle| self.tabs[i].scene.is_layer_locked(handle))
{
self.command_line.push_error(
"LOFT: a source is on a locked layer; disable source deletion or unlock it.",
);
return Some(Task::none());
}
let dirty_before = self.tabs[i].dirty;
let pending = self.begin_undo(i, "LOFT", 1, true);
let mut associative = false;
let created = if surface {
let handle =
self.add_surface_model(loft_command_model::surface_entity(&record), body);
if !handle.is_null() {
self.tabs[i].scene.create_solid_history(
handle,
codec::objects::SolidHistoryOperation::Loft(record),
);
// An associative surface keeps reading its cross
// sections (each section a single object).
let single: Option<Vec<Handle>> = sections
.iter()
.map(|section| match section {
LoftSectionSelection::Entity(handle) => Some(*handle),
_ => None,
})
.collect();
if let Some(single) = single.filter(|_| {
crate::scene::model::sweep_model::surface_associativity()
&& guides.is_empty()
&& path.is_none()
}) {
crate::scene::model::surface_sources::link_surface_sources(
&mut self.tabs[i].scene.document,
handle,
&crate::scene::model::surface_sources::SurfaceSources::Loft { sections: single },
);
associative = true;
}
}
handle
} else {
self.add_solid_model(
empty_solid3d(),
body,
codec::objects::SolidHistoryOperation::Loft(record),
)
};
if created.is_null() {
// The creation helper already removed its provisional
// result. Discard its empty undo recording as well.
self.tabs[i].scene.take_undo_recording();
self.tabs[i].dirty = dirty_before;
self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref());
return Some(Task::none());
} else {
let mut consumed = if delete_sections && !associative {
sources.clone()
} else {
Vec::new()
};
if delete_auxiliary {
consumed.extend(guides.iter().copied());
consumed.extend(path);
}
consumed.sort_unstable_by_key(|handle| handle.value());
consumed.dedup();
self.tabs[i].scene.erase_entities(&consumed);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(created, true);
self.tabs[i].dirty = true;
let message = if surface {
crate::t!("LOFT: surface created.")
} else {
crate::t!("LOFT: solid created.")
};
self.command_line.push_output(message.as_ref());
}
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
}
Err(error) => {
self.command_line.push_error(&error);
return Some(Task::none());
}
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
self.refresh_properties();
None
}
pub(super) fn handle_solid_edge_blend(&mut self, result: CmdResult) -> Task<Message> {
let i = self.active_tab;
let CmdResult::SolidEdgeBlend {
handle,
edges,
base_face,
value,
other_value,
fillet,
} = result
else {
unreachable!("router only routes the matching variant");
};
let task = self.solid_edge_blend(handle, &edges, base_face, value, other_value, fillet);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
task
}
pub(super) fn handle_solid_shell(
&mut self,
handle: Handle,
actions: Vec<crate::command::ShellFaceAction>,
distance: f64,
) -> Task<Message> {
let i = self.active_tab;
let task = self.solid_shell(handle, &actions, distance);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
task
}
pub(super) fn handle_solid_subtract(
&mut self,
bases: Vec<Handle>,
cutters: Vec<Handle>,
convert_meshes: bool,
) -> Task<Message> {
let i = self.active_tab;
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();
self.restore_pre_cmd_tangent();
task
}
pub(super) fn handle_slice_entities(
&mut self,
targets: Vec<Handle>,
plane: kernel::space::Plane,
keep_point: Option<glam::DVec3>,
) -> Task<Message> {
let i = self.active_tab;
let task = self.solid_slice(&targets, plane, keep_point);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
task
}
pub(super) fn handle_slice_surface_entities(
&mut self,
targets: Vec<Handle>,
cutter: Box<kernel::brep::Body>,
keep_point: Option<glam::DVec3>,
) -> Task<Message> {
let i = self.active_tab;
let task = self.solid_slice_surface(&targets, *cutter, keep_point);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
task
}
}