Files
OneCAD-Draft/src/app/model_ops.rs
T

2620 lines
100 KiB
Rust

// Kernel B-rep solid modelling and exact ACIS persistence.
use codec::{
entities::{AcisData, EntityCommon, Region, Solid3D, Surface, SurfaceData, SurfaceKind, Wire},
objects::{SolidHistoryBoolean, SolidHistoryOperation},
EntityType, Handle,
};
use iced::Task;
use kernel::brep::{Body, EdgeKey, FaceKey};
use std::collections::{HashMap, HashSet};
use super::Message;
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<Self> {
Some(match entity {
EntityType::Solid3D(_) => Self::Solid,
EntityType::Region(_) => Self::Region,
EntityType::Surface(_) => Self::Surface,
_ => return None,
})
}
}
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>,
}
struct PreparedUnion {
kind: UnionEntityKind,
handles: Vec<Handle>,
body: Body,
common: EntityCommon,
}
struct SubtractGroup {
kind: UnionEntityKind,
plane: Option<kernel::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)>,
}
struct PreparedSlice {
retained: Handle,
entity: EntityType,
body: Body,
display: PreparedSolidDisplay,
extra: Option<(EntityType, Body)>,
}
enum ModelSliceTool {
Plane(kernel::space::Plane),
Surface(Body),
}
impl ModelSliceTool {
fn side(&self, point: [f64; 3]) -> Option<f64> {
match self {
Self::Plane(plane) => plane.distance_to(point),
Self::Surface(surface) => kernel::brep::surface_side(surface, point),
}
}
fn split(&self, body: &Body) -> Result<Option<kernel::brep::PlaneSlice>, kernel::brep::Snag> {
match self {
Self::Plane(plane) => kernel::brep::slice_by_plane(body, *plane),
Self::Surface(surface) => kernel::brep::slice_by_surface(body, surface),
}
}
}
struct IntersectGroup {
kind: UnionEntityKind,
plane: Option<kernel::space::Plane>,
handles: Vec<Handle>,
}
enum PreparedIntersectOutcome {
Replace {
retained: Handle,
entity: EntityType,
body: Body,
display: PreparedSolidDisplay,
},
Consume,
}
struct PreparedIntersect {
handles: Vec<Handle>,
outcome: PreparedIntersectOutcome,
}
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<Body>) -> Result<Body, kernel::brep::Snag> {
if kind == UnionEntityKind::Solid {
let mut operands = bodies.into_iter();
let mut result = operands.next().ok_or(kernel::brep::Snag::CutRefused)?;
for operand in operands {
result = solid_model::boolean_result(Bool::Union, &result, &operand)?;
}
return Ok(result);
}
let references = bodies.iter().collect::<Vec<_>>();
let tolerance = kernel::brep::operation_tolerance(&references);
kernel::brep::union_planar_regions(&bodies, tolerance)
}
fn planar_body_plane(body: &Body) -> Option<kernel::space::Plane> {
let mut faces = body.face_keys();
let first = kernel::brep::planar_face_profile(body, faces.next()?)?.plane;
let first_normal = glam::DVec3::from_array(first.normal()?);
let tolerance = kernel::brep::operation_tolerance(&[body]);
faces
.all(|face| {
let Some(profile) = kernel::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: kernel::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 = kernel::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<kernel::space::Plane>,
bases: Vec<Body>,
cutters: Vec<Body>,
) -> Result<Body, kernel::brep::Snag> {
if kind != UnionEntityKind::Solid && plane.is_none() {
return Err(kernel::brep::Snag::NoClosedForm);
}
if kind != UnionEntityKind::Solid && plane.is_some() {
let references = bases.iter().chain(&cutters).collect::<Vec<_>>();
let tolerance = kernel::brep::operation_tolerance(&references);
return kernel::brep::subtract_planar_regions(&bases, &cutters, tolerance);
}
let mut bases = bases.into_iter();
let mut result = bases.next().ok_or(kernel::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)
}
inventory::submit!(crate::command::CommandRegistration {
names: &["SECTIONCULL"]
});
fn entity_with_boolean_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)
}
fn entity_with_history_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);
match &mut source {
EntityType::Solid3D(entity) => {
entity.wires = wires;
entity.silhouettes.clear();
entity.set_sat_document(&document);
}
EntityType::Surface(entity) => {
entity.wires = wires;
entity.silhouettes.clear();
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)
}
enum IntersectBodyOutcome {
Area(Body),
Touching,
Disjoint,
}
fn intersect_bodies(
kind: UnionEntityKind,
bodies: Vec<Body>,
) -> Result<IntersectBodyOutcome, kernel::brep::Snag> {
if kind == UnionEntityKind::Solid {
let mut operands = bodies.into_iter();
let mut result = operands.next().ok_or(kernel::brep::Snag::CutRefused)?;
for operand in operands {
result = solid_model::boolean_result(Bool::Intersect, &result, &operand)?;
if result.faces.is_empty() {
return Ok(IntersectBodyOutcome::Disjoint);
}
}
return Ok(IntersectBodyOutcome::Area(result));
}
let references = bodies.iter().collect::<Vec<_>>();
let tolerance = kernel::brep::operation_tolerance(&references);
Ok(
match kernel::brep::intersect_planar_regions(&bodies, tolerance)? {
kernel::brep::PlanarIntersection::Area(body) => IntersectBodyOutcome::Area(body),
kernel::brep::PlanarIntersection::Touching => IntersectBodyOutcome::Touching,
kernel::brep::PlanarIntersection::Disjoint => IntersectBodyOutcome::Disjoint,
},
)
}
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;
};
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 {
self.command_line
.push_error(crate::t!("The solid could not be encoded as ACIS.").as_ref());
return Handle::NULL;
};
inner.set_sat_document(&document);
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(_));
self.tabs[i].scene.create_solid_history(handle, history);
if !self.tabs[i].scene.register_solid_model(handle, solid)
|| (require_complete
&& self.tabs[i]
.scene
.meshes
.get(&handle)
.is_none_or(|mesh| !mesh.complete))
{
// A command result is not successful until it has renderable
// geometry. Roll the new entity back so DELOBJ cannot consume a
// visible source in exchange for an invisible result.
self.tabs[i].scene.rollback_new_entities(&[handle]);
return Handle::NULL;
}
handle
}
/// 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)
}
pub(super) fn add_surface_model_with_history(
&mut self,
entity: EntityType,
surface: Body,
history: SolidHistoryOperation,
) -> Handle {
let i = self.active_tab;
let handle = self.add_surface_model_inner(entity, surface, true);
if handle.is_null() {
return handle;
}
if !self.tabs[i].scene.create_solid_history(handle, history) {
self.tabs[i].scene.rollback_new_entities(&[handle]);
return Handle::NULL;
}
handle
}
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;
};
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 {
self.command_line
.push_error(crate::t!("The surface could not be encoded as ACIS.").as_ref());
return Handle::NULL;
};
inner.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string());
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|
matches!(entity, EntityType::Surface(value) if value.kind == codec::entities::SurfaceKind::Lofted));
if !self.tabs[i].scene.register_solid_model(handle, surface)
|| (require_complete
&& self.tabs[i]
.scene
.meshes
.get(&handle)
.is_none_or(|mesh| !mesh.complete))
{
self.tabs[i].scene.rollback_new_entities(&[handle]);
return Handle::NULL;
}
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<Handle> {
let i = self.active_tab;
let mut handles: Vec<Handle> = self.tabs[i]
.scene
.selected_handles_in_order()
.into_iter()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.filter(|handle| {
matches!(
self.tabs[i].scene.document.get_entity(*handle),
Some(EntityType::Solid3D(_))
)
})
.collect();
self.tabs[i].scene.restore_solid_models(&handles);
handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle));
handles
}
fn selected_union_groups(&self) -> Vec<UnionGroup> {
let scene = &self.tabs[self.active_tab].scene;
let mut groups: Vec<UnionGroup> = 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)
}
pub(super) fn intersect_ready(&self) -> bool {
self.union_ready()
}
fn selected_intersect_handles(&self) -> Vec<Handle> {
let scene = &self.tabs[self.active_tab].scene;
scene
.selected_handles_in_order()
.into_iter()
.filter(|handle| !scene.is_layer_locked(*handle))
.filter(|handle| {
scene
.document
.get_entity(*handle)
.and_then(UnionEntityKind::from_entity)
.is_some()
})
.collect()
}
fn intersect_groups(
&self,
handles: &[Handle],
bodies: &HashMap<Handle, Body>,
) -> Vec<IntersectGroup> {
let scene = &self.tabs[self.active_tab].scene;
let mut groups = Vec::<IntersectGroup>::new();
for handle in handles {
let Some(kind) = scene
.document
.get_entity(*handle)
.and_then(UnionEntityKind::from_entity)
else {
continue;
};
let plane = (kind != UnionEntityKind::Solid)
.then(|| planar_body_plane(&bodies[handle]))
.flatten();
if let Some(group) = groups.iter_mut().find(|group| {
group.kind == kind
&& if kind == UnionEntityKind::Solid {
true
} else {
match (group.plane, plane) {
(Some(group_plane), Some(_)) => {
coplanar_bodies(group_plane, &bodies[handle])
}
(None, None) => true,
_ => false,
}
}
}) {
group.handles.push(*handle);
} else {
groups.push(IntersectGroup {
kind,
plane,
handles: vec![*handle],
});
}
}
groups.retain(|group| group.handles.len() >= 2);
groups
}
fn replace_solid_body_with_fillet(
&mut self,
handle: Handle,
source: &Body,
result: Body,
edges: &[EdgeKey],
radius: f64,
) -> bool {
let i = self.active_tab;
let Some(display) = self.tabs[i]
.scene
.prepare_solid_model_display(handle, &result)
.filter(|display| display.0.complete)
else {
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original edge body was retained.").as_ref());
return false;
};
let ordered = source.edge_keys().collect::<Vec<_>>();
let Some(history_edges) = edges
.iter()
.map(|edge| {
ordered
.iter()
.position(|candidate| candidate == edge)
.and_then(|ordinal| i32::try_from(ordinal).ok())
})
.collect::<Option<Vec<_>>>()
else {
self.command_line
.push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref());
return false;
};
self.push_undo_snapshot(i, "FILLETEDGE");
// A solid without history stays without one, as in the reference.
let has_history = self.tabs[i]
.scene
.document
.solid_history_graph(handle)
.is_some();
if has_history
&& !self.tabs[i]
.scene
.append_solid_history(handle, solid_history::fillet_op(history_edges, radius))
{
self.command_line
.push_error(crate::t!("The fillet history could not be recorded.").as_ref());
return false;
}
if has_history {
let _ = self.tabs[i].scene.apply_solid_history_choice(
handle,
solid_history::PROP_HISTORY,
"Record",
);
}
let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
return false;
};
let Some(entity) = entity_with_history_body(entity, &result) else {
return false;
};
if !self.tabs[i].scene.update_entity(entity) {
self.command_line
.push_error(crate::t!("The edge body could not be updated.").as_ref());
return false;
}
self.tabs[i]
.scene
.register_prepared_solid_model(handle, result, display);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].dirty = true;
self.refresh_properties();
true
}
fn replace_edge_body_with_chamfer(
&mut self,
handle: Handle,
source: &Body,
result: Body,
edges: &[EdgeKey],
base_face: FaceKey,
base_distance: f64,
other_distance: f64,
) -> bool {
let i = self.active_tab;
let Some(display) = self.tabs[i]
.scene
.prepare_solid_model_display(handle, &result)
.filter(|display| display.0.complete)
else {
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original edge body was retained.").as_ref());
return false;
};
let ordered_edges = source.edge_keys().collect::<Vec<_>>();
let Some(history_edges) = edges
.iter()
.map(|edge| {
ordered_edges
.iter()
.position(|candidate| candidate == edge)
.and_then(|ordinal| i32::try_from(ordinal).ok())
})
.collect::<Option<Vec<_>>>()
else {
self.command_line
.push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref());
return false;
};
let Some(history_face) = source
.face_keys()
.position(|candidate| candidate == base_face)
.and_then(|ordinal| i32::try_from(ordinal).ok())
else {
self.command_line.push_error(
crate::t!("The selected base face no longer belongs to the body.").as_ref(),
);
return false;
};
self.push_undo_snapshot(i, "CHAMFEREDGE");
let has_history = self.tabs[i]
.scene
.document
.solid_history_graph(handle)
.is_some();
if has_history
&& !self.tabs[i].scene.append_solid_history(
handle,
solid_history::chamfer_op(
history_edges,
history_face,
base_distance,
other_distance,
),
)
{
self.command_line
.push_error(crate::t!("The chamfer history could not be recorded.").as_ref());
return false;
}
if has_history {
let _ = self.tabs[i].scene.apply_solid_history_choice(
handle,
solid_history::PROP_HISTORY,
"Record",
);
}
let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
return false;
};
let Some(entity) = entity_with_history_body(entity, &result) else {
return false;
};
if !self.tabs[i].scene.update_entity(entity) {
self.command_line
.push_error(crate::t!("The edge body could not be updated.").as_ref());
return false;
}
self.tabs[i]
.scene
.register_prepared_solid_model(handle, result, display);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].dirty = true;
self.refresh_properties();
true
}
pub(super) fn solid_edge_blend(
&mut self,
handle: Handle,
edges: &[EdgeKey],
base_face: Option<FaceKey>,
value: f64,
other_value: f64,
fillet: bool,
) -> Task<Message> {
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return Task::none();
}
if !matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(EntityType::Solid3D(_) | EntityType::Surface(_))
) {
self.command_line
.push_error(crate::t!("Select a 3D solid or surface edge.").as_ref());
return Task::none();
}
self.tabs[i].scene.restore_solid_models(&[handle]);
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
self.command_line
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
return Task::none();
};
let result = if fillet {
match kernel::brep::fillet_edges(&body, edges, value) {
Ok(result) => Some(result),
Err(error) => {
self.command_line
.push_error(&format!("FILLETEDGE: {error}"));
return Task::none();
}
}
} else {
let Some(base_face) = base_face else {
self.command_line
.push_error(crate::t!("CHAMFEREDGE: select a base face edge.").as_ref());
return Task::none();
};
match kernel::brep::chamfer_edges(&body, edges, base_face, value, other_value) {
Ok(result) => Some(result),
Err(error) => {
self.command_line
.push_error(&format!("CHAMFEREDGE: {error}"));
return Task::none();
}
}
};
let Some(result) = result else {
self.command_line.push_error(
crate::t!("Edge operation failed. Use a convex planar solid and a smaller value.")
.as_ref(),
);
return Task::none();
};
let label = if fillet { "FILLETEDGE" } else { "CHAMFEREDGE" };
let updated = if fillet {
self.replace_solid_body_with_fillet(handle, &body, result, edges, value)
} else {
self.replace_edge_body_with_chamfer(
handle,
&body,
result,
edges,
base_face.expect("checked chamfer face"),
value,
other_value,
)
};
if updated {
if !fillet {
self.tabs[i].scene.document.header.chamfer_distance_a = value;
self.tabs[i].scene.document.header.chamfer_distance_b = other_value;
}
self.command_line
.push_output(crate::tf!("{label}: solid updated.").as_ref());
}
Task::none()
}
pub(super) fn solid_shell(
&mut self,
handle: Handle,
actions: &[crate::command::ShellFaceAction],
distance: f64,
) -> Task<Message> {
use crate::command::ShellFaceAction;
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return Task::none();
}
if !matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(EntityType::Solid3D(_))
) {
self.command_line
.push_error(crate::t!("Select a 3D solid.").as_ref());
return Task::none();
}
if !distance.is_finite() || distance.abs() <= f64::EPSILON {
self.command_line
.push_error(crate::t!("The shell offset distance must be nonzero.").as_ref());
return Task::none();
}
self.tabs[i].scene.restore_solid_models(&[handle]);
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
self.command_line
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
return Task::none();
};
let all_faces = body.face_keys().collect::<Vec<_>>();
let mut removed = HashSet::new();
for action in actions {
match *action {
ShellFaceAction::Remove(point) => {
let Some(face) = kernel::brep::shell_face_at_point(&body, point.to_array())
else {
self.command_line.push_error(
crate::t!("A selected face could not be resolved.").as_ref(),
);
return Task::none();
};
removed.insert(face);
}
ShellFaceAction::Add(point) => {
let Some(face) = kernel::brep::shell_face_at_point(&body, point.to_array())
else {
self.command_line.push_error(
crate::t!("A selected face could not be resolved.").as_ref(),
);
return Task::none();
};
removed.remove(&face);
}
ShellFaceAction::RemoveAll => removed.extend(all_faces.iter().copied()),
ShellFaceAction::AddAll => removed.clear(),
}
}
let removed = removed.into_iter().collect::<Vec<_>>();
let result = match kernel::brep::shell(&body, &removed, distance) {
Ok(result) => result,
Err(kernel::brep::ShellError::InvalidDistance) => {
self.command_line
.push_error(crate::t!("The shell offset distance must be nonzero.").as_ref());
return Task::none();
}
Err(kernel::brep::ShellError::UnsupportedSolid) => {
self.command_line.push_error(
crate::t!("SHELL supports rectangular boxes, circular cylinders, and spheres.")
.as_ref(),
);
return Task::none();
}
Err(kernel::brep::ShellError::UnknownFace) => {
self.command_line.push_error(
crate::t!("A selected face does not belong to the solid.").as_ref(),
);
return Task::none();
}
Err(kernel::brep::ShellError::NoMaterial) => {
self.command_line.push_error(
crate::t!("The offset removes all material. Use a smaller distance or add a face back.")
.as_ref(),
);
return Task::none();
}
Err(kernel::brep::ShellError::Kernel(_)) => {
self.command_line.push_error(
crate::t!(
"The shell could not be constructed from the selected solid and distance."
)
.as_ref(),
);
return Task::none();
}
};
let Some(source) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
return Task::none();
};
let Some(entity) = entity_with_boolean_body(source, &result) else {
self.command_line
.push_error(crate::t!("The result could not be encoded as ACIS.").as_ref());
return Task::none();
};
let Some(display) = self.tabs[i]
.scene
.prepare_solid_model_display(handle, &result)
.filter(|display| display.0.complete)
else {
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original solid was retained.").as_ref());
return Task::none();
};
self.push_undo_snapshot(i, "SHELL");
self.tabs[i].scene.delete_solid_history(handle);
if !self.tabs[i].scene.update_entity(entity) {
self.command_line
.push_error(crate::t!("The solid could not be updated.").as_ref());
return Task::none();
}
self.tabs[i]
.scene
.create_solid_history(handle, solid_history::brep_op(&result));
self.tabs[i]
.scene
.register_prepared_solid_model(handle, result, display);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(crate::t!("SHELL: solid updated.").as_ref());
Task::none()
}
/// Intersect compatible selected solids, Regions, and coplanar Surfaces.
/// Before a boolean replaces `retained` with `entity`: with SOLIDHIST on,
/// join the histories of the solids it combined (`steps`, in the order
/// the result applied them) into `retained`'s and hand that history to
/// `entity`; otherwise drop it. True when the history was kept.
fn join_boolean_history(
&mut self,
retained: Handle,
entity: &mut EntityType,
steps: &[(Handle, u8)],
bodies: &HashMap<Handle, Body>,
) -> bool {
let scene = &mut self.tabs[self.active_tab].scene;
let joined = scene.document.header.record_solid_history
&& scene.join_boolean_histories(retained, retained, steps, bodies);
if !joined {
scene.delete_solid_history(retained);
return false;
}
if let (EntityType::Solid3D(entity), Some(EntityType::Solid3D(current))) =
(entity, scene.document.get_entity(retained))
{
entity.history_handle = current.history_handle;
}
true
}
pub(super) fn solid_intersect(&mut self) -> Task<Message> {
let i = self.active_tab;
let mut handles = self.selected_intersect_handles();
self.tabs[i].scene.restore_solid_models(&handles);
handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle));
let bodies = handles
.iter()
.filter_map(|handle| {
self.tabs[i]
.scene
.solid_models
.get(handle)
.cloned()
.map(|body| (*handle, body))
})
.collect::<HashMap<_, _>>();
let groups = self.intersect_groups(&handles, &bodies);
if groups.is_empty() {
self.command_line.push_error(
crate::t!("INTERSECT: select at least two solids, Regions, or coplanar Surfaces of a compatible type.")
.as_ref(),
);
return Task::none();
}
let mut prepared = Vec::new();
let mut retained_without_change = 0usize;
for group in groups {
let operands = group
.handles
.iter()
.map(|handle| bodies[handle].clone())
.collect::<Vec<_>>();
let outcome = match intersect_bodies(group.kind, operands) {
Ok(outcome) => outcome,
Err(kernel::brep::Snag::NoClosedForm) => {
self.command_line.push_error(
crate::t!("INTERSECT: the selected objects do not have a supported coplanar surface intersection.")
.as_ref(),
);
return Task::none();
}
Err(kernel::brep::Snag::Coincident) => {
self.command_line.push_error(
crate::t!("INTERSECT: the selected coincident geometry is ambiguous.")
.as_ref(),
);
return Task::none();
}
Err(kernel::brep::Snag::CutRefused) => {
self.command_line.push_error(
crate::t!(
"INTERSECT: the selected topology could not be intersected safely."
)
.as_ref(),
);
return Task::none();
}
};
match outcome {
IntersectBodyOutcome::Area(body) => {
let retained = group.handles[0];
let Some(source) = self.tabs[i].scene.document.get_entity(retained).cloned()
else {
return Task::none();
};
let Some(entity) = entity_with_boolean_body(source, &body) else {
self.command_line.push_error(
crate::t!("The INTERSECT result could not be encoded as ACIS.")
.as_ref(),
);
return Task::none();
};
let Some(display) = self.tabs[i]
.scene
.prepare_solid_model_display(retained, &body)
.filter(|display| display.0.complete)
else {
self.command_line.push_error(
crate::t!("The INTERSECT result could not be displayed completely. The original objects were retained.")
.as_ref(),
);
return Task::none();
};
prepared.push(PreparedIntersect {
handles: group.handles,
outcome: PreparedIntersectOutcome::Replace {
retained,
entity,
body,
display,
},
});
}
IntersectBodyOutcome::Touching
if group.kind == UnionEntityKind::Region
|| group.kind == UnionEntityKind::Surface =>
{
prepared.push(PreparedIntersect {
handles: group.handles,
outcome: PreparedIntersectOutcome::Consume,
});
}
IntersectBodyOutcome::Disjoint if group.kind == UnionEntityKind::Region => {
prepared.push(PreparedIntersect {
handles: group.handles,
outcome: PreparedIntersectOutcome::Consume,
});
}
IntersectBodyOutcome::Touching | IntersectBodyOutcome::Disjoint => {
retained_without_change += group.handles.len();
}
}
}
if prepared.is_empty() {
self.command_line.push_output(
crate::t!(
"INTERSECT: no positive-area common result; %{count} original object(s) retained.",
count = retained_without_change
)
.as_ref(),
);
return Task::none();
}
let record_history = self.tabs[i].scene.document.header.record_solid_history;
self.push_undo_snapshot(i, "INTERSECT");
let mut results = Vec::new();
let mut consumed = Vec::new();
for group in prepared {
match group.outcome {
PreparedIntersectOutcome::Replace {
retained,
mut entity,
body,
display,
} => {
let steps = group
.handles
.iter()
.filter(|handle| **handle != retained)
.map(|handle| (*handle, SolidHistoryBoolean::INTERSECT))
.collect::<Vec<_>>();
let joined = self.join_boolean_history(retained, &mut entity, &steps, &bodies);
if !self.tabs[i].scene.update_entity(entity) {
self.command_line.push_error(
crate::t!("INTERSECT: the retained object could not be updated.")
.as_ref(),
);
return Task::none();
}
if record_history
&& !joined
&& matches!(
self.tabs[i].scene.document.get_entity(retained),
Some(EntityType::Solid3D(_))
)
{
let history = solid_history::brep_op(&body);
self.tabs[i].scene.create_solid_history(retained, history);
}
self.tabs[i]
.scene
.register_prepared_solid_model(retained, body, display);
results.push(retained);
consumed.extend(
group
.handles
.into_iter()
.filter(|handle| *handle != retained),
);
}
PreparedIntersectOutcome::Consume => consumed.extend(group.handles),
}
}
self.tabs[i].scene.erase_entities(&consumed);
self.tabs[i].scene.deselect_all();
for handle in &results {
self.tabs[i].scene.select_entity(*handle, false);
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::t!(
"INTERSECT: created %{count} result object(s).",
count = results.len()
)
.as_ref(),
);
Task::none()
}
/// Run a boolean over the selected solids in selection order.
pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task<Message> {
match op {
BoolOp::Union => return self.union_selected_entities(),
BoolOp::Intersect => return self.solid_intersect(),
BoolOp::Subtract => {}
}
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.len() < 2 {
self.command_line
.push_error(crate::t!("Boolean: select at least two solids.").as_ref());
return Task::none();
}
let kind = match op {
BoolOp::Union => Bool::Union,
BoolOp::Subtract => Bool::Subtract,
BoolOp::Intersect => Bool::Intersect,
};
let mut operands = handles
.iter()
.filter_map(|handle| self.tabs[i].scene.solid_models.get(handle).cloned());
let mut result = operands.next().expect("at least two restored solids");
for operand in operands {
let Some(combined) = solid_model::boolean(kind, &result, &operand) else {
self.command_line.push_error(
crate::t!("Boolean failed while combining the selected solids.").as_ref(),
);
return Task::none();
};
result = combined;
}
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();
}
self.push_undo_snapshot(i, "BOOLEAN");
self.tabs[i].scene.erase_entities(&handles);
let mut s3d = Solid3D::new();
s3d.wires = solid_model::edge_wires(&result);
let history = solid_history::brep_op(&result);
let handle = self.add_solid_model(EntityType::Solid3D(s3d), 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()
}
fn union_selected_entities(&mut self) -> Task<Message> {
let i = self.active_tab;
let mut groups = self.selected_union_groups();
let handles = groups
.iter()
.flat_map(|group| group.handles.iter().copied())
.collect::<Vec<_>>();
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::<Vec<_>>();
let result = match union_bodies(group.kind, bodies) {
Ok(result) => result,
Err(kernel::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(kernel::brep::Snag::Coincident) => {
self.command_line.push_error(
crate::t!("UNION: the selected coincident geometry is ambiguous.").as_ref(),
);
return Task::none();
}
Err(kernel::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::<Vec<_>>();
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;
let handle = self.add_solid_model_preserving_style(
EntityType::Solid3D(entity),
group.body,
history,
);
// The new solid's history becomes the joined histories
// of the solids it unites.
let record = self.tabs[i].scene.document.header.record_solid_history;
if !handle.is_null() && record {
let bodies = group
.handles
.iter()
.filter_map(|operand| {
let body = self.tabs[i].scene.solid_models.get(operand)?;
Some((*operand, body.clone()))
})
.collect::<HashMap<_, _>>();
let steps = group
.handles
.iter()
.skip(1)
.map(|operand| (*operand, SolidHistoryBoolean::UNION))
.collect::<Vec<_>>();
let scene = &mut self.tabs[i].scene;
if !scene.join_boolean_histories(handle, group.handles[0], &steps, &bodies)
{
if let Some(body) = scene.solid_models.get(&handle).cloned() {
scene.create_solid_history(handle, solid_history::brep_op(&body));
}
}
}
handle
}
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(Box::new(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::t!(
"UNION: created %{count} result object(s).",
count = created.len()
)
.as_ref(),
);
Task::none()
}
pub(super) fn solid_subtract(
&mut self,
bases: &[Handle],
cutters: &[Handle],
convert_meshes: bool,
) -> Task<Message> {
let i = self.active_tab;
let valid_operand = |handle: &Handle| {
!self.tabs[i].scene.is_layer_locked(*handle)
&& 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_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);
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 and one cutter Solid, Region, or Surface."
)
.as_ref(),
);
return Task::none();
}
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);
}
}
}
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();
}
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(kernel::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(kernel::brep::Snag::Coincident) => {
self.command_line.push_error(
crate::t!("SUBTRACT: the selected coincident geometry is ambiguous.")
.as_ref(),
);
return Task::none();
}
Err(kernel::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_boolean_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");
let mut retained = Vec::new();
let mut consumed = Vec::new();
for group in prepared {
if let Some((handle, mut entity, body, display)) = group.result {
let steps = group
.bases
.iter()
.skip(1)
.map(|base| (*base, SolidHistoryBoolean::UNION))
.chain(
group
.cutters
.iter()
.map(|cutter| (*cutter, SolidHistoryBoolean::SUBTRACT)),
)
.collect::<Vec<_>>();
let joined =
self.join_boolean_history(handle, &mut entity, &steps, &operand_bodies);
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
&& !joined
&& 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();
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::t!(
"SUBTRACT: created %{count} result object(s).",
count = retained.len()
)
.as_ref(),
);
Task::none()
}
/// Split every selected solid or surface by an arbitrary plane. All
/// geometry, persistence records, and displays are prepared before the
/// first source entity is changed.
pub(super) fn solid_slice(
&mut self,
requested: &[Handle],
plane: kernel::space::Plane,
keep_point: Option<glam::DVec3>,
) -> Task<Message> {
self.solid_slice_with_tool(requested, ModelSliceTool::Plane(plane), keep_point)
}
pub(super) fn solid_slice_surface(
&mut self,
requested: &[Handle],
cutter: Body,
keep_point: Option<glam::DVec3>,
) -> Task<Message> {
self.solid_slice_with_tool(requested, ModelSliceTool::Surface(cutter), keep_point)
}
/// SLICE on meshes that do not close a volume: the kept side of each face
/// stays (or, with no side chosen, both sides become separate meshes),
/// open along the cut. One undo step; `None` when nothing was changed.
fn slice_open_meshes(
&mut self,
handles: &[Handle],
tool: &ModelSliceTool,
keep_point: Option<glam::DVec3>,
) -> Option<Vec<Handle>> {
let i = self.active_tab;
let keep_positive = match keep_point {
Some(point) => match tool.side(point.to_array()) {
Some(distance) if distance.abs() > 1.0e-9 => Some(distance > 0.0),
_ => {
self.command_line.push_error(
crate::t!("SLICE: choose a point away from the cutting plane.").as_ref(),
);
return None;
}
},
None => None,
};
let side = |point: [f64; 3]| tool.side(point).unwrap_or(0.0);
let mut pieces = Vec::with_capacity(handles.len());
for handle in handles {
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else {
continue;
};
let Some((vertices, faces)) = crate::entities::mesh::mesh_polygons(entity) else {
continue;
};
let common = entity.common();
let clip = |positive| {
crate::entities::mesh::clipped_mesh(&vertices, &faces, side, positive, common)
};
let (kept, other) = match keep_positive {
Some(positive) => (clip(positive), None),
None => (clip(false), Some(clip(true))),
};
// Both sides must have faces, or the plane missed this mesh.
let crosses = vertices.iter().any(|&point| side(point) > 0.0)
&& vertices.iter().any(|&point| side(point) < 0.0);
let (Some(kept), true) = (kept, crosses) else {
self.command_line.push_error(
crate::t!("SLICE: the cutting plane does not cross every selected object; no object was changed.")
.as_ref(),
);
return None;
};
pieces.push((*handle, kept, other.flatten()));
}
self.push_undo_snapshot(i, "SLICE");
let mut results = Vec::with_capacity(pieces.len() * 2);
for (handle, mut kept, other) in pieces {
kept.common.handle = handle;
self.tabs[i].scene.update_entity(EntityType::Mesh(kept));
results.push(handle);
if let Some(mut other) = other {
other.common.handle = Handle::NULL;
results.push(self.tabs[i].scene.add_entity(EntityType::Mesh(other)));
}
}
self.tabs[i].scene.deselect_all();
for handle in &results {
self.tabs[i].scene.select_entity(*handle, false);
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::t!(
"SLICE: created %{count} result object(s).",
count = results.len()
)
.as_ref(),
);
Some(results)
}
fn solid_slice_with_tool(
&mut self,
requested: &[Handle],
tool: ModelSliceTool,
keep_point: Option<glam::DVec3>,
) -> Task<Message> {
let i = self.active_tab;
let mut handles = requested
.iter()
.copied()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.filter(|handle| {
matches!(
self.tabs[i].scene.document.get_entity(*handle),
Some(
EntityType::Solid3D(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_)
)
)
})
.collect::<Vec<_>>();
handles.sort_unstable_by_key(|handle| handle.value());
handles.dedup();
// A mesh that closes a volume is sliced as a solid and capped; an open
// one is clipped face by face and stays a mesh.
let mut bodies = rustc_hash::FxHashMap::default();
let mut open_meshes = Vec::new();
for handle in &handles {
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else {
continue;
};
if crate::entities::mesh::mesh_polygons(entity).is_none() {
continue;
}
match crate::entities::mesh::closed_mesh_body(entity) {
Some(body) => {
bodies.insert(*handle, body);
}
None => open_meshes.push(*handle),
}
}
handles.retain(|handle| !open_meshes.contains(handle));
self.tabs[i].scene.restore_solid_models(&handles);
for handle in &handles {
if let Some(body) = self.tabs[i].scene.solid_models.get(handle) {
bodies.entry(*handle).or_insert_with(|| body.clone());
}
}
handles.retain(|handle| bodies.contains_key(handle));
if !open_meshes.is_empty() {
let sliced = self.slice_open_meshes(&open_meshes, &tool, keep_point);
if handles.is_empty() || sliced.is_none() {
return Task::none();
}
}
if handles.is_empty() {
self.command_line
.push_error(crate::t!("SLICE: select at least one solid or surface.").as_ref());
return Task::none();
}
let keep_positive = if let Some(point) = keep_point {
let tolerance = kernel::brep::operation_tolerance(
&handles
.iter()
.map(|handle| &bodies[handle])
.collect::<Vec<_>>(),
);
let Some(distance) = tool.side(point.to_array()) else {
self.command_line
.push_error(crate::t!("SLICE: the cutting plane is invalid.").as_ref());
return Task::none();
};
if distance.abs() <= tolerance {
self.command_line.push_error(
crate::t!("SLICE: choose a point away from the cutting plane.").as_ref(),
);
return Task::none();
}
Some(distance > 0.0)
} else {
None
};
let mut prepared = Vec::with_capacity(handles.len());
for handle in &handles {
let result = match tool.split(&bodies[handle]) {
Ok(Some(result)) => result,
Ok(None) => {
self.command_line.push_error(
crate::t!("SLICE: the cutting plane does not cross every selected object; no object was changed.")
.as_ref(),
);
return Task::none();
}
Err(kernel::brep::Snag::NoClosedForm) => {
self.command_line.push_error(
crate::t!("SLICE: the selected geometry has an unsupported surface intersection; no object was changed.")
.as_ref(),
);
return Task::none();
}
Err(kernel::brep::Snag::Coincident) => {
self.command_line.push_error(
crate::t!("SLICE: the cutting plane is coincident with source geometry; no object was changed.")
.as_ref(),
);
return Task::none();
}
Err(kernel::brep::Snag::CutRefused) => {
self.command_line.push_error(
crate::t!("SLICE: the selected topology could not be split safely; no object was changed.")
.as_ref(),
);
return Task::none();
}
};
let (body, extra_body) = match keep_positive {
Some(true) => (result.positive, None),
Some(false) => (result.negative, None),
None => (result.negative, Some(result.positive)),
};
let Some(source) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
return Task::none();
};
let Some(entity) = entity_with_boolean_body(source.clone(), &body) else {
self.command_line.push_error(
crate::t!("SLICE: a result could not be encoded; no object was changed.")
.as_ref(),
);
return Task::none();
};
let Some(display) = self.tabs[i]
.scene
.prepare_solid_model_display(*handle, &body)
.filter(|display| display.0.complete)
else {
self.command_line.push_error(
crate::t!(
"SLICE: a result could not be displayed completely; no object was changed."
)
.as_ref(),
);
return Task::none();
};
let extra = match extra_body {
Some(extra_body) => {
let mut extra_entity = match entity_with_boolean_body(source, &extra_body) {
Some(entity) => entity,
None => {
self.command_line.push_error(
crate::t!(
"SLICE: a result could not be encoded; no object was changed."
)
.as_ref(),
);
return Task::none();
}
};
extra_entity.common_mut().handle = Handle::NULL;
Some((extra_entity, extra_body))
}
None => None,
};
prepared.push(PreparedSlice {
retained: *handle,
entity,
body,
display,
extra,
});
}
self.push_undo_snapshot(i, "SLICE");
let mut created = Vec::new();
for item in &prepared {
let Some((entity, body)) = &item.extra else {
continue;
};
let handle = match entity {
EntityType::Solid3D(_) => self.add_solid_model_preserving_style(
entity.clone(),
body.clone(),
solid_history::brep_op(body),
),
EntityType::Surface(_) => {
self.add_surface_model_preserving_style(entity.clone(), body.clone())
}
_ => Handle::NULL,
};
if handle.is_null() {
self.tabs[i].scene.rollback_new_entities(&created);
self.discard_last_undo_entry(i);
self.command_line.push_error(
crate::t!(
"SLICE: a result could not be committed; all source objects were retained."
)
.as_ref(),
);
return Task::none();
}
created.push(handle);
}
let record_history = self.tabs[i].scene.document.header.record_solid_history;
let mut retained = Vec::with_capacity(prepared.len());
for item in prepared {
self.tabs[i].scene.delete_solid_history(item.retained);
if !self.tabs[i].scene.update_entity(item.entity) {
self.command_line.push_error(
crate::t!("SLICE: a retained result could not be updated.").as_ref(),
);
return Task::none();
}
if record_history
&& matches!(
self.tabs[i].scene.document.get_entity(item.retained),
Some(EntityType::Solid3D(_))
)
{
self.tabs[i]
.scene
.create_solid_history(item.retained, solid_history::brep_op(&item.body));
}
self.tabs[i].scene.register_prepared_solid_model(
item.retained,
item.body,
item.display,
);
retained.push(item.retained);
}
retained.extend(created);
self.tabs[i].scene.deselect_all();
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::t!(
"SLICE: created %{count} result object(s).",
count = retained.len()
)
.as_ref(),
);
Task::none()
}
pub(super) fn slice_selected(
&mut self,
plane: kernel::space::Plane,
keep_point: Option<glam::DVec3>,
) -> Task<Message> {
let handles = self.tabs[self.active_tab].scene.selected_handles_in_order();
self.solid_slice(&handles, plane, keep_point)
}
/// SECTIONCULL — keep only what lies inside the selected section object
/// (or the live section): bodies are cut to it, open meshes clipped, and
/// everything wholly outside it erased. One undo step.
pub(super) fn section_cull(&mut self) -> Task<Message> {
let i = self.active_tab;
let Some(section) = self.tabs[i].scene.cull_section() else {
self.command_line.push_error(
crate::t!("SECTIONCULL: select a section object or turn on a live section.")
.as_ref(),
);
return Task::none();
};
let Some(cull) = self.tabs[i].scene.section_cull(section) else {
return Task::none();
};
let mut cut = Vec::with_capacity(cull.cut.len());
for (handle, body) in cull.cut {
let scene = &self.tabs[i].scene;
let entity = scene
.document
.get_entity(handle)
.cloned()
.and_then(|source| entity_with_boolean_body(source, &body));
let display = scene
.prepare_solid_model_display(handle, &body)
.filter(|display| display.0.complete);
let (Some(entity), Some(display)) = (entity, display) else {
self.command_line.push_error(
crate::t!(
"SECTIONCULL: a cut object could not be prepared; nothing was changed."
)
.as_ref(),
);
return Task::none();
};
cut.push((handle, entity, body, display));
}
let changed = cut.len() + cull.clipped.len() + cull.outside.len();
if changed == 0 {
self.command_line
.push_output(crate::t!("SECTIONCULL: nothing lies outside the section.").as_ref());
return Task::none();
}
self.push_undo_snapshot(i, "SECTIONCULL");
let scene = &mut self.tabs[i].scene;
for (handle, entity, body, display) in cut {
scene.delete_solid_history(handle);
scene.update_entity(entity);
scene.register_prepared_solid_model(handle, body, display);
}
for mesh in cull.clipped {
scene.update_entity(mesh);
}
scene.erase_entities(&cull.outside);
scene.deselect_all();
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::t!(
"SECTIONCULL: erased %{erased} object(s) and cut %{cut} across the section.",
erased = cull.outside.len(),
cut = changed - cull.outside.len()
)
.as_ref(),
);
Task::none()
}
/// INTERFERE — create a solid from the overlap of the two selected solids,
/// leaving the originals in place (a non-destructive boolean intersect).
pub(super) fn solid_interfere(&mut self) -> Task<Message> {
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.len() != 2 {
self.command_line.push_error(
crate::t!("INTERFERE: select exactly two solids created this session.").as_ref(),
);
return Task::none();
}
let a = self.tabs[i].scene.solid_models[&handles[0]].clone();
let b = self.tabs[i].scene.solid_models[&handles[1]].clone();
match solid_model::boolean(Bool::Intersect, &a, &b) {
Some(result) => {
self.push_undo_snapshot(i, "INTERFERE");
// Keep both originals; add the interference solid.
let mut s3d = Solid3D::new();
s3d.wires = solid_model::edge_wires(&result);
let history = solid_history::brep_op(&result);
self.add_solid_model(EntityType::Solid3D(s3d), result, history);
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::t!("INTERFERE: created an interference solid from the overlap.")
.as_ref(),
);
}
None => self
.command_line
.push_output(crate::t!("INTERFERE: the selected solids do not overlap.").as_ref()),
}
Task::none()
}
/// 3DROTATE — rotate the one selected solid about the X/Y/Z axis (0/1/2)
/// through its centre by `angle_deg` degrees. Rotation preserves the solid's
/// orientation, so it reuses the cached B-rep directly.
pub(super) fn solid_rotate3d(&mut self, axis: usize, angle_deg: f64) -> Task<Message> {
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.len() != 1 {
self.command_line.push_error(
crate::t!("3DROTATE: select exactly one solid created this session.").as_ref(),
);
return Task::none();
}
let solid = self.tabs[i].scene.solid_models[&handles[0]].clone();
let Some(middle) = solid_model::centre(&solid) else {
self.command_line.push_error(
crate::t!("3DROTATE: could not determine the solid's extent.").as_ref(),
);
return Task::none();
};
let Some(rotated) = solid_model::turned(&solid, axis, angle_deg.to_radians(), middle)
else {
self.command_line
.push_error(crate::t!("3DROTATE: could not turn the solid.").as_ref());
return Task::none();
};
self.push_undo_snapshot(i, "3DROTATE");
self.tabs[i].scene.erase_entities(&handles);
let mut s3d = Solid3D::new();
s3d.wires = solid_model::edge_wires(&rotated);
let history = solid_history::brep_op(&rotated);
let handle = self.add_solid_model(EntityType::Solid3D(s3d), rotated, 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();
self.command_line.push_output(
crate::tf!(
"3DROTATE: rotated {angle_deg}° about the {} axis.",
["X", "Y", "Z"][axis]
)
.as_ref(),
);
Task::none()
}
/// 3DMIRROR — add a mirrored copy of the one selected solid across the plane
/// perpendicular to the X/Y/Z axis (0/1/2) through its centre, keeping the
/// original. A reflection loses handedness, so the kernel reverses every
/// loop on the way through; without that the copy lights black.
pub(super) fn solid_mirror3d(&mut self, axis: usize) -> Task<Message> {
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.len() != 1 {
self.command_line.push_error(
crate::t!("3DMIRROR: select exactly one solid created this session.").as_ref(),
);
return Task::none();
}
let solid = self.tabs[i].scene.solid_models[&handles[0]].clone();
let Some(middle) = solid_model::centre(&solid) else {
self.command_line.push_error(
crate::t!("3DMIRROR: could not determine the solid's extent.").as_ref(),
);
return Task::none();
};
let Some(reflected) = solid_model::mirrored(&solid, axis, middle) else {
self.command_line
.push_error(crate::t!("3DMIRROR: could not mirror the solid.").as_ref());
return Task::none();
};
self.push_undo_snapshot(i, "3DMIRROR");
let mut s3d = Solid3D::new();
s3d.wires = solid_model::edge_wires(&reflected);
let history = solid_history::brep_op(&reflected);
let h = self.add_solid_model(EntityType::Solid3D(s3d), reflected, history);
self.tabs[i].scene.deselect_all();
if !h.is_null() {
self.tabs[i].scene.select_entity(h, false);
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::tf!(
"3DMIRROR: added a mirror across the {} plane.",
["X", "Y", "Z"][axis]
)
.as_ref(),
);
Task::none()
}
/// 3DALIGN — move/rotate the one selected solid so its three source points
/// land on the three destination points. The frame-to-frame transform is
/// computed in glam (`M = D · S⁻¹`); both frames are right-handed, so the
/// result is a pure rotation and translation and the kernel accepts it.
pub(super) fn solid_align3d(
&mut self,
src: [[f64; 3]; 3],
dst: [[f64; 3]; 3],
) -> Task<Message> {
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.len() != 1 {
self.command_line.push_error(
crate::t!("3DALIGN: select exactly one solid created this session.").as_ref(),
);
return Task::none();
}
// Build a right-handed frame (origin + orthonormal axes) from 3 points.
let frame = |p: [[f64; 3]; 3]| -> Option<glam::DMat4> {
let p1 = glam::DVec3::from_array(p[0]);
let p2 = glam::DVec3::from_array(p[1]);
let p3 = glam::DVec3::from_array(p[2]);
let x = (p2 - p1).normalize_or_zero();
let z = (p2 - p1).cross(p3 - p1).normalize_or_zero();
if x.length_squared() < 1e-12 || z.length_squared() < 1e-12 {
return None; // coincident or collinear points
}
let y = z.cross(x);
Some(glam::DMat4::from_cols(
x.extend(0.0),
y.extend(0.0),
z.extend(0.0),
p1.extend(1.0),
))
};
let (Some(s), Some(d)) = (frame(src), frame(dst)) else {
self.command_line.push_error(
crate::t!("3DALIGN: each point triple must be non-coincident and non-collinear.")
.as_ref(),
);
return Task::none();
};
let solid = self.tabs[i].scene.solid_models[&handles[0]].clone();
let Some(aligned) = solid_model::by_matrix(&solid, (d * s.inverse()).to_cols_array())
else {
self.command_line
.push_error(crate::t!("3DALIGN: could not align the solid.").as_ref());
return Task::none();
};
self.push_undo_snapshot(i, "3DALIGN");
self.tabs[i].scene.erase_entities(&handles);
let mut s3d = Solid3D::new();
s3d.wires = solid_model::edge_wires(&aligned);
let history = solid_history::brep_op(&aligned);
let h = self.add_solid_model(EntityType::Solid3D(s3d), aligned, history);
self.tabs[i].scene.deselect_all();
if !h.is_null() {
self.tabs[i].scene.select_entity(h, false);
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::t!("3DALIGN: aligned the solid to the destination points.").as_ref(),
);
Task::none()
}
/// SECTION — draw the cross-section outline where an axis-aligned plane
/// (X/Y/Z = `axis` at `value`) cuts the one selected solid, as Line
/// entities.
pub(super) fn solid_section(&mut self, axis: usize, value: f64) -> Task<Message> {
use codec::types::Vector3;
use codec::Line;
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.len() != 1 {
self.command_line.push_error(
crate::t!("SECTION: select exactly one solid created this session.").as_ref(),
);
return Task::none();
}
let solid = self.tabs[i].scene.solid_models[&handles[0]].clone();
let Some((min, max)) = solid_model::extent(&solid) else {
self.command_line
.push_error(crate::t!("SECTION: could not determine the solid's extent.").as_ref());
return Task::none();
};
// The plane has to actually reach the solid to cut it.
if value < min[axis] || value > max[axis] {
self.command_line
.push_output(crate::t!("SECTION: the plane does not cross the solid.").as_ref());
return Task::none();
}
let segs = solid_model::section(&solid, axis, value);
if segs.is_empty() {
self.command_line
.push_output(crate::t!("SECTION: the plane does not cross the solid.").as_ref());
return Task::none();
}
self.push_undo_snapshot(i, "SECTION");
for (p1, p2) in &segs {
let line = Line::from_points(
Vector3::new(p1[0], p1[1], p1[2]),
Vector3::new(p2[0], p2[1], p2[2]),
);
self.tabs[i].scene.add_entity(EntityType::Line(line));
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::tf!(
"SECTION: created {} section line(s) at {}={value}.",
segs.len(),
["X", "Y", "Z"][axis]
)
.as_ref(),
);
Task::none()
}
/// PYRAMID — create an `n`-sided pyramid: a regular polygon base of the
/// given circumradius with its apex at `height`.
///
/// A real B-rep rather than a bag of faces, so it joins the boolean tools
/// and the exact-geometry save path like every other primitive.
pub(super) fn solid_pyramid(
&mut self,
radius: f64,
height: f64,
sides: usize,
) -> Task<Message> {
use crate::modules::insert::solid3d_cmds::empty_solid3d;
let i = self.active_tab;
let n = sides.max(3);
let Some(solid) = solid_model::pyramid_solid([0.0; 3], radius, height, n) else {
self.command_line
.push_error(crate::t!("PYRAMID: radius and height must be positive.").as_ref());
return Task::none();
};
self.push_undo_snapshot(i, "PYRAMID");
let mut entity = empty_solid3d();
if let EntityType::Solid3D(inner) = &mut entity {
inner.wires = solid_model::edge_wires(&solid);
}
let history = solid_history::pyramid_op(
glam::DMat4::IDENTITY.to_cols_array(),
radius,
0.0,
height,
n,
true,
);
let handle = self.add_solid_model(entity, solid, 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();
self.command_line.push_output(
crate::tf!(
"PYRAMID: created a {n}-sided pyramid (radius {radius}, height {height})."
)
.as_ref(),
);
}
Task::none()
}
/// SPLINEFIT — replace the selected polyline with a cubic spline that passes
/// through its vertices. Control points come from the Catmull-Rom → cubic
/// Bézier formula (the curve provably interpolates each vertex), with a
/// clamped piecewise-Bézier knot vector the spline renderer reads directly.
pub(super) fn fit_spline(&mut self) -> Task<Message> {
use codec::entities::Spline;
use codec::types::Vector3;
let i = self.active_tab;
let found: Option<(Handle, Vec<[f64; 3]>)> = self.tabs[i]
.scene
.selected_entities()
.iter()
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
.find_map(|(h, e)| {
let EntityType::LwPolyline(_) = e else {
return None;
};
let curve = crate::entities::curve::entity_curve(e)?;
let kernel::geom2d::Curve::Polyline(polyline) = &curve.curve else {
return None;
};
Some((
*h,
polyline
.vertices
.iter()
.map(|vertex| curve.plane.point_at(vertex.position))
.collect(),
))
});
let Some((handle, fit)) = found else {
self.command_line.push_error(
crate::t!("SPLINEFIT: select a polyline to fit a spline through.").as_ref(),
);
return Task::none();
};
if fit.len() < 3 {
self.command_line
.push_error(crate::t!("SPLINEFIT: need at least 3 points.").as_ref());
return Task::none();
}
let n = fit.len();
let m = n - 1; // Bézier segments
let p = |k: usize| glam::DVec3::new(fit[k][0], fit[k][1], fit[k][2]);
// Catmull-Rom → cubic Bézier control points: [P0, b1,b2,P1, b1,b2,P2, …].
let mut ctrl: Vec<Vector3> = Vec::with_capacity(3 * m + 1);
ctrl.push(Vector3::new(fit[0][0], fit[0][1], fit[0][2]));
for seg in 0..m {
let p0 = p(seg);
let p1 = p(seg + 1);
let prev = if seg > 0 { p(seg - 1) } else { p0 };
let next = if seg + 2 <= m { p(seg + 2) } else { p1 };
let b1 = p0 + (p1 - prev) / 6.0;
let b2 = p1 - (next - p0) / 6.0;
ctrl.push(Vector3::new(b1.x, b1.y, b1.z));
ctrl.push(Vector3::new(b2.x, b2.y, b2.z));
ctrl.push(Vector3::new(p1.x, p1.y, p1.z));
}
// Clamped piecewise-Bézier knots (degree 3): len == ctrl.len()+degree+1.
let mut knots: Vec<f64> = vec![0.0; 4];
for s in 1..m {
knots.extend_from_slice(&[s as f64, s as f64, s as f64]);
}
knots.extend_from_slice(&[m as f64; 4]);
let mut spl = Spline::new();
spl.degree = 3;
spl.control_points = ctrl;
spl.knots = knots;
spl.fit_points = fit.iter().map(|q| Vector3::new(q[0], q[1], q[2])).collect();
// flags.rational defaults to false (non-rational) — exactly what we want.
self.push_undo_snapshot(i, "SPLINEFIT");
self.tabs[i].scene.erase_entities(&[handle]);
self.tabs[i].scene.add_entity(EntityType::Spline(spl));
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(crate::tf!("SPLINEFIT: fit a spline through {n} points.").as_ref());
Task::none()
}
/// FLATSHOT — project the selected solid's edges onto the XY plane (Z=0) as
/// Line entities, giving a flattened 2D shot of the model. Reuses the cached
/// solid's edge wires (the same source SECTION uses).
pub(super) fn solid_flatshot(&mut self) -> Task<Message> {
use codec::types::Vector3;
use codec::Line;
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.is_empty() {
self.command_line
.push_error(crate::t!("FLATSHOT: select a solid created this session.").as_ref());
return Task::none();
}
self.push_undo_snapshot(i, "FLATSHOT");
let mut n = 0usize;
for h in &handles {
let solid = self.tabs[i].scene.solid_models[h].clone();
for w in solid_model::edge_wires(&solid) {
for seg in w.points.windows(2) {
let line = Line::from_points(
Vector3::new(seg[0].x, seg[0].y, 0.0),
Vector3::new(seg[1].x, seg[1].y, 0.0),
);
self.tabs[i].scene.add_entity(EntityType::Line(line));
n += 1;
}
}
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(crate::tf!("FLATSHOT: created {n} projected edge(s) at Z=0.").as_ref());
Task::none()
}
/// CONVTOSURFACE — convert the selected solid(s) into Surface entities,
/// carrying the solid's edge wires (reuses the cached B-rep's edge wires).
pub(super) fn solid_convtosurface(&mut self) -> Task<Message> {
use codec::entities::{Surface, SurfaceKind, Wire as AWire};
use codec::types::Vector3;
let i = self.active_tab;
let handles = self.selected_solid_handles();
if handles.is_empty() {
self.command_line.push_error(
crate::t!("CONVTOSURFACE: select a solid created this session.").as_ref(),
);
return Task::none();
}
let mut surfaces: Vec<Surface> = Vec::new();
for h in &handles {
let solid = self.tabs[i].scene.solid_models[h].clone();
let awires: Vec<AWire> = solid_model::edge_wires(&solid)
.into_iter()
.map(|w| {
let mut aw = AWire::new();
aw.points = w
.points
.iter()
.map(|p| Vector3::new(p.x, p.y, p.z))
.collect();
aw
})
.collect();
let mut surf = Surface::new(SurfaceKind::Generic);
surf.wires = awires;
surf.common.layer = self.tabs[i].active_layer.clone();
surfaces.push(surf);
}
self.push_undo_snapshot(i, "CONVTOSURFACE");
if self.delete_objects != 0 {
self.tabs[i].scene.erase_entities(&handles);
}
let n = surfaces.len();
for surf in surfaces {
self.tabs[i]
.scene
.add_entity(EntityType::Surface(Box::new(surf)));
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(
crate::tf!("CONVTOSURFACE: converted {n} solid(s) to surface(s).").as_ref(),
);
Task::none()
}
}