diff --git a/Cargo.lock b/Cargo.lock index b85f7a77b..c45a8f9d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/ramox81/cadkernel.git?rev=f9cb9dbcbbc25e25a139accd4fbf1cf73ca1f536#f9cb9dbcbbc25e25a139accd4fbf1cf73ca1f536" +source = "git+https://github.com/ramox81/cadkernel.git?rev=2318df5ee52b22a65e38cd4c34ab27ba8bdcf5c4#2318df5ee52b22a65e38cd4c34ab27ba8bdcf5c4" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 3fb177f53..1d8f7725b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0 acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "af1f31287d4730f228cbb27d6884078295627ca7" } [patch."https://github.com/HakanSeven12/cadkernel.git"] -cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "f9cb9dbcbbc25e25a139accd4fbf1cf73ca1f536" } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "2318df5ee52b22a65e38cd4c34ab27ba8bdcf5c4" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b0db25725..128dad867 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4105,10 +4105,19 @@ impl OpenCADStudio { CmdResult::SolidEdgeBlend { handle, edges, + base_face, value, + other_value, fillet, } => { - let task = self.solid_edge_blend(handle, &edges, value, fillet); + 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(); diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index c083f6f60..8adf3b441 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -1,6 +1,6 @@ use super::*; -fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option { +fn selected_edge_body(app: &OpenCADStudio, tab: usize) -> Option { let scene = &app.tabs.get(tab)?.scene; let selected = scene.selected_handles_in_order(); let [handle] = selected.as_slice() else { @@ -9,7 +9,7 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option { if matches!( scene.document.get_entity(*handle), - Some(acadrust::EntityType::Solid3D(_)) + Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_)) ) { Some(*handle) } else { @@ -26,7 +26,10 @@ fn solid_edge_sources( .document .entities() .filter_map(|entity| { - matches!(entity, acadrust::EntityType::Solid3D(_)) + matches!( + entity, + acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_) + ) .then_some(entity.common().handle) }) .collect::>(); @@ -989,7 +992,7 @@ impl OpenCADStudio { "FILLETEDGE" | "SOLIDFILLET" => { use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let target = selected_solid(self, i); + let target = selected_edge_body(self, i); let bodies = solid_edge_sources(self, i); let command = SolidEdgeCommand::new( EdgeOperation::Fillet, @@ -1001,9 +1004,9 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(command)); } - "FILLET" if selected_solid(self, i).is_some() => { + "FILLET" if selected_edge_body(self, i).is_some() => { use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let target = selected_solid(self, i); + let target = selected_edge_body(self, i); let bodies = solid_edge_sources(self, i); let command = SolidEdgeCommand::new( EdgeOperation::Fillet, @@ -1148,29 +1151,51 @@ impl OpenCADStudio { } } - "SOLIDCHAMFER" => { - use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let target = selected_solid(self, i); + "CHAMFEREDGE" | "SOLIDCHAMFER" => { + use crate::modules::model::edge_cmd::SolidEdgeCommand; + let target = selected_edge_body(self, i); + let header = &self.tabs[i].scene.document.header; + let distances = (header.chamfer_distance_a, header.chamfer_distance_b); let bodies = solid_edge_sources(self, i); - let command = SolidEdgeCommand::new( - EdgeOperation::Chamfer, + let command = SolidEdgeCommand::new_chamfer( target, bodies, crate::scene::WireModel::SELECTED, + distances, + ); + let distances = command.chamfer_distances(); + self.command_line.push_output( + crate::tf!( + "Distance1 = {:.4}, Distance2 = {:.4}", + distances.0, + distances.1 + ) + .as_ref(), ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } - "CHAMFER" if selected_solid(self, i).is_some() => { - use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let target = selected_solid(self, i); + "CHAMFER" if selected_edge_body(self, i).is_some() => { + use crate::modules::model::edge_cmd::SolidEdgeCommand; + let target = selected_edge_body(self, i); + let header = &self.tabs[i].scene.document.header; + let distances = (header.chamfer_distance_a, header.chamfer_distance_b); let bodies = solid_edge_sources(self, i); - let command = SolidEdgeCommand::new( - EdgeOperation::Chamfer, + let command = SolidEdgeCommand::new_chamfer( target, bodies, crate::scene::WireModel::SELECTED, + distances, + ); + let distances = command.chamfer_distances(); + self.command_line.push_output( + crate::tf!( + "Distance1 = {:.4}, Distance2 = {:.4}", + distances.0, + distances.1 + ) + .as_ref(), ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 16e87d6f1..17c39b765 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -7,7 +7,7 @@ use acadrust::{ objects::SolidHistoryOperation, EntityType, Handle, }; -use cadkernel::brep::{Body, EdgeKey}; +use cadkernel::brep::{Body, EdgeKey, FaceKey}; use iced::Task; use std::collections::HashMap; @@ -244,6 +244,29 @@ fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option Option { + 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, @@ -551,51 +574,6 @@ impl super::OpenCADStudio { groups } - fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool { - let i = self.active_tab; - let Some(document) = - crate::scene::convert::acis_export::solid_to_sat(&result) - else { - self.command_line - .push_error(crate::t!("The result could not be encoded as ACIS.").as_ref()); - return false; - }; - let Some(EntityType::Solid3D(mut entity)) = - self.tabs[i].scene.document.get_entity(handle).cloned() - else { - return false; - }; - entity.wires = solid_model::edge_wires(&result); - entity.silhouettes.clear(); - entity.history_handle = None; - entity.set_sat_document(&document); - - 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 false; - }; - self.push_undo_snapshot(i, label); - self.tabs[i].scene.delete_solid_history(handle); - if !self.tabs[i] - .scene - .update_entity(EntityType::Solid3D(entity)) - { - self.command_line - .push_error(crate::t!("The solid could not be updated.").as_ref()); - return false; - } - let history = solid_history::brep_op(&result); - self.tabs[i].scene.create_solid_history(handle, history); - 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_solid_body_with_fillet( &mut self, handle: Handle, @@ -605,17 +583,12 @@ impl super::OpenCADStudio { radius: f64, ) -> bool { let i = self.active_tab; - let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&result) else { - self.command_line - .push_error(crate::t!("The result could not be encoded as ACIS.").as_ref()); - return false; - }; 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()); + 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::>(); @@ -630,7 +603,7 @@ impl super::OpenCADStudio { .collect::>>() else { self.command_line - .push_error(crate::t!("A selected edge no longer belongs to the solid.").as_ref()); + .push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref()); return false; }; @@ -641,7 +614,7 @@ impl super::OpenCADStudio { .create_solid_history(handle, solid_history::brep_op(source)) { self.command_line - .push_error(crate::t!("The solid history could not be created.").as_ref()); + .push_error(crate::t!("The edge body history could not be created.").as_ref()); return false; } if !self.tabs[i].scene.append_solid_history( @@ -652,20 +625,103 @@ impl super::OpenCADStudio { .push_error(crate::t!("The fillet history could not be recorded.").as_ref()); return false; } - let Some(EntityType::Solid3D(mut entity)) = - self.tabs[i].scene.document.get_entity(handle).cloned() - else { + let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else { return false; }; - entity.wires = solid_model::edge_wires(&result); - entity.silhouettes.clear(); - entity.set_sat_document(&document); - if !self.tabs[i] + 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 - .update_entity(EntityType::Solid3D(entity)) + .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::>(); + let Some(history_edges) = edges + .iter() + .map(|edge| { + ordered_edges + .iter() + .position(|candidate| candidate == edge) + .and_then(|ordinal| i32::try_from(ordinal).ok()) + }) + .collect::>>() + 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"); + if self.tabs[i].scene.document.solid_history_graph(handle).is_none() + && !self.tabs[i] + .scene + .create_solid_history(handle, solid_history::brep_op(source)) { self.command_line - .push_error(crate::t!("The solid could not be updated.").as_ref()); + .push_error(crate::t!("The edge body history could not be created.").as_ref()); + return false; + } + if !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; + } + 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] @@ -682,19 +738,20 @@ impl super::OpenCADStudio { &mut self, handle: Handle, edges: &[EdgeKey], + base_face: Option, value: f64, + other_value: f64, fillet: bool, ) -> Task { 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(_)) - ) { + 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 edge.").as_ref()); + .push_error(crate::t!("Select a 3D solid or surface edge.").as_ref()); return Task::none(); } self.tabs[i].scene.restore_solid_models(&[handle]); @@ -713,9 +770,25 @@ impl super::OpenCADStudio { } } } else { - edges - .first() - .and_then(|edge| cadkernel::brep::chamfer(&body, *edge, value)) + 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 cadkernel::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( @@ -724,13 +797,25 @@ impl super::OpenCADStudio { ); return Task::none(); }; - let label = if fillet { "FILLETEDGE" } else { "SOLIDCHAMFER" }; + 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_solid_body(handle, result, label) + 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()); } diff --git a/src/command.rs b/src/command.rs index f91b124b8..c68bfb9f2 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1551,7 +1551,9 @@ pub enum CmdResult { SolidEdgeBlend { handle: Handle, edges: Vec, + base_face: Option, value: f64, + other_value: f64, fillet: bool, }, SolidSubtract { diff --git a/src/modules/model/edge_cmd.rs b/src/modules/model/edge_cmd.rs index a88e2fc88..ae7cb1303 100644 --- a/src/modules/model/edge_cmd.rs +++ b/src/modules/model/edge_cmd.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use acadrust::Handle; -use cadkernel::brep::{Body, EdgeKey}; +use cadkernel::brep::{Body, EdgeKey, FaceKey}; use glam::DVec3; use crate::command::{ @@ -39,7 +39,8 @@ enum EdgeStep { PreviewConfirm, Radius, Expression, - ChamferValue, + ChamferDistance1, + ChamferDistance2, } pub struct SolidEdgeCommand { @@ -48,14 +49,17 @@ pub struct SolidEdgeCommand { bodies: Vec<(Handle, Body)>, handle: Option, selected_edges: Vec, + base_face: Option, selection_batches: Vec<(Vec, DVec3)>, selection_mode: EdgeSelectionMode, step: EdgeStep, - radius_return: EdgeStep, - loop_candidates: Vec>, + value_return: EdgeStep, + expression_return: EdgeStep, + loop_candidates: Vec<(FaceKey, Vec)>, loop_index: usize, last_pick: Option, default_value: f64, + other_value: f64, preview_color: [f32; 4], preview_wires: Vec, preview_hidden: Vec, @@ -74,10 +78,12 @@ impl SolidEdgeCommand { bodies, handle: None, selected_edges: Vec::new(), + base_face: None, selection_batches: Vec::new(), selection_mode: EdgeSelectionMode::Edge, step: EdgeStep::Selecting, - radius_return: EdgeStep::Selecting, + value_return: EdgeStep::Selecting, + expression_return: EdgeStep::Radius, loop_candidates: Vec::new(), loop_index: 0, last_pick: None, @@ -86,12 +92,29 @@ impl SolidEdgeCommand { } else { 1.0 }, + other_value: 1.0, preview_color, preview_wires: Vec::new(), preview_hidden: Vec::new(), } } + pub fn new_chamfer( + target: Option, + bodies: Vec<(Handle, Body)>, + preview_color: [f32; 4], + distances: (f64, f64), + ) -> Self { + let mut command = Self::new(EdgeOperation::Chamfer, target, bodies, preview_color); + command.default_value = positive_default(distances.0); + command.other_value = positive_default(distances.1); + command + } + + pub fn chamfer_distances(&self) -> (f64, f64) { + (self.default_value, self.other_value) + } + fn body(&self, handle: Handle) -> Option<&Body> { self.bodies .iter() @@ -126,7 +149,13 @@ impl SolidEdgeCommand { fn current_loop(&self) -> Option<&[EdgeKey]> { self.loop_candidates .get(self.loop_index) - .map(Vec::as_slice) + .map(|(_, edges)| edges.as_slice()) + } + + fn current_loop_face(&self) -> Option { + self.loop_candidates + .get(self.loop_index) + .map(|(face, _)| *face) } fn preview_edges(&self) -> Vec { @@ -144,17 +173,24 @@ impl SolidEdgeCommand { } fn preview_active(&self) -> bool { - self.operation == EdgeOperation::Fillet - && matches!( - self.step, - EdgeStep::LoopConfirm - | EdgeStep::PreviewConfirm - | EdgeStep::Radius - | EdgeStep::Expression - ) + matches!( + self.step, + EdgeStep::Selecting + | EdgeStep::PickingLoop + | EdgeStep::LoopConfirm + | EdgeStep::PreviewConfirm + | EdgeStep::Radius + | EdgeStep::Expression + | EdgeStep::ChamferDistance1 + | EdgeStep::ChamferDistance2 + ) } - fn preview_for_value(&self, value: f64) -> Option<(Handle, Vec)> { + fn preview_for_values( + &self, + value: f64, + other_value: f64, + ) -> Option<(Handle, Vec)> { if !self.preview_active() { return None; } @@ -163,11 +199,21 @@ impl SolidEdgeCommand { return None; } let (handle, body) = self.active_body()?; - let result = cadkernel::brep::fillet_edges(body, &edges, value).ok()?; + let result = match self.operation { + EdgeOperation::Fillet => cadkernel::brep::fillet_edges(body, &edges, value).ok()?, + EdgeOperation::Chamfer => cadkernel::brep::chamfer_edges( + body, + &edges, + self.base_face.or_else(|| self.current_loop_face())?, + value, + other_value, + ) + .ok()?, + }; let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle); for wire in &mut wires { wire.color = self.preview_color; - wire.name = format!("{}-FILLETEDGE-PREVIEW", handle.value()); + wire.name = format!("{}-{}-PREVIEW", handle.value(), self.name()); } (!wires.is_empty()).then_some((handle, wires)) } @@ -175,14 +221,16 @@ impl SolidEdgeCommand { fn rebuild_preview(&mut self) { self.preview_wires.clear(); self.preview_hidden.clear(); - if let Some((handle, wires)) = self.preview_for_value(self.default_value) { + if let Some((handle, wires)) = + self.preview_for_values(self.default_value, self.other_value) + { self.preview_wires = wires; self.preview_hidden.push(handle); } } fn begin_radius(&mut self, return_to: EdgeStep) -> CmdResult { - self.radius_return = return_to; + self.value_return = return_to; self.step = EdgeStep::Radius; self.rebuild_preview(); CmdResult::NeedPoint @@ -196,7 +244,33 @@ impl SolidEdgeCommand { if self.operation == EdgeOperation::Fillet { set_fillet_radius(value); } - self.step = self.radius_return; + self.step = self.value_return; + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + + fn begin_distances(&mut self, return_to: EdgeStep) -> CmdResult { + self.value_return = return_to; + self.step = EdgeStep::ChamferDistance1; + self.rebuild_preview(); + CmdResult::NeedPoint + } + + fn accept_distance(&mut self, value: f64) -> Option { + if value <= 0.0 || !value.is_finite() { + return None; + } + match self.step { + EdgeStep::ChamferDistance1 => { + self.default_value = value; + self.step = EdgeStep::ChamferDistance2; + } + EdgeStep::ChamferDistance2 => { + self.other_value = value; + self.step = self.value_return; + } + _ => return None, + } self.rebuild_preview(); Some(CmdResult::NeedPoint) } @@ -206,6 +280,9 @@ impl SolidEdgeCommand { self.current_loop().map(|edges| edges.to_vec()), self.last_pick, ) { + if self.operation == EdgeOperation::Chamfer && self.base_face.is_none() { + self.base_face = self.current_loop_face(); + } self.add_batch(edges, anchor); } self.loop_candidates.clear(); @@ -225,7 +302,9 @@ impl SolidEdgeCommand { CmdResult::SolidEdgeBlend { handle, edges: self.selected_edges.clone(), + base_face: self.base_face, value: self.default_value, + other_value: self.other_value, fillet: self.operation == EdgeOperation::Fillet, } } @@ -235,20 +314,31 @@ impl CadCommand for SolidEdgeCommand { fn name(&self) -> &'static str { match self.operation { EdgeOperation::Fillet => "FILLETEDGE", - EdgeOperation::Chamfer => "SOLIDCHAMFER", + EdgeOperation::Chamfer => "CHAMFEREDGE", } } fn prompt(&self) -> String { match (self.operation, self.step, self.selection_mode) { (EdgeOperation::Chamfer, EdgeStep::Selecting, _) => { - crate::t!("Select a solid edge:").into_owned() + crate::t!("Select an edge or [Loop/Distance]:").into_owned() } - (EdgeOperation::Chamfer, EdgeStep::ChamferValue, _) => crate::tf!( - "Specify chamfer distance <{:.3}>:", + (EdgeOperation::Chamfer, EdgeStep::PickingLoop, _) => { + crate::t!("Select edge of loop or [Edge/Distance]:").into_owned() + } + (EdgeOperation::Chamfer, EdgeStep::PreviewConfirm, _) => { + crate::t!("Press Enter to accept the chamfer or [Distance]:").into_owned() + } + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance1, _) => crate::tf!( + "Specify Distance1 or [Expression] <{:.4}>:", self.default_value ) .into_owned(), + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance2, _) => crate::tf!( + "Specify Distance2 or [Expression] <{:.4}>:", + self.other_value + ) + .into_owned(), (_, EdgeStep::Selecting, EdgeSelectionMode::Edge) => { crate::t!("Select an edge or [Chain/Loop/Radius]:").into_owned() } @@ -264,7 +354,7 @@ impl CadCommand for SolidEdgeCommand { (_, EdgeStep::PreviewConfirm, _) => { crate::t!("Press Enter to accept the fillet or [Radius]:").into_owned() } - (_, EdgeStep::Radius, _) if self.radius_return == EdgeStep::PreviewConfirm => crate::tf!( + (_, EdgeStep::Radius, _) if self.value_return == EdgeStep::PreviewConfirm => crate::tf!( "Specify Radius or [Expression] <{:.4}>:", self.default_value ) @@ -281,6 +371,26 @@ impl CadCommand for SolidEdgeCommand { fn options(&self) -> Vec { match (self.operation, self.step, self.selection_mode) { + (EdgeOperation::Chamfer, EdgeStep::Selecting, _) => vec![ + CmdOption::new("Loop", "L"), + CmdOption::new("Distance", "D"), + ], + (EdgeOperation::Chamfer, EdgeStep::PickingLoop, _) => vec![ + CmdOption::new("Edge", "E"), + CmdOption::new("Distance", "D"), + ], + (EdgeOperation::Chamfer, EdgeStep::LoopConfirm, _) => vec![ + CmdOption::new("Accept", "A"), + CmdOption::new("Next", "N"), + ], + (EdgeOperation::Chamfer, EdgeStep::PreviewConfirm, _) => { + vec![CmdOption::new("Distance", "D")] + } + ( + EdgeOperation::Chamfer, + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2, + _, + ) => vec![CmdOption::new("Expression", "E")], (EdgeOperation::Fillet, EdgeStep::Selecting, EdgeSelectionMode::Edge) => vec![ CmdOption::new("Chain", "C"), CmdOption::new("Loop", "L"), @@ -338,14 +448,14 @@ impl CadCommand for SolidEdgeCommand { self.handle = Some(handle); self.last_pick = Some(point); - if self.operation == EdgeOperation::Chamfer { - self.add_batch(vec![seed], point); - self.step = EdgeStep::ChamferValue; - return CmdResult::NeedPoint; - } - if self.step == EdgeStep::PickingLoop { self.loop_candidates = edge_loops(self.body(handle).expect("pick body exists"), seed); + if self.operation == EdgeOperation::Chamfer { + if let Some(base_face) = self.base_face { + self.loop_candidates + .retain(|(face, _)| *face == base_face); + } + } self.loop_index = 0; self.step = if self.loop_candidates.is_empty() { EdgeStep::Selecting @@ -356,16 +466,38 @@ impl CadCommand for SolidEdgeCommand { return CmdResult::NeedPoint; } + if self.operation == EdgeOperation::Chamfer { + if self.base_face.is_none() { + self.base_face = self.body(handle).and_then(|body| { + nearest_edge_face(body, seed, point.to_array()) + }); + } + let Some(base_face) = self.base_face else { + return CmdResult::NeedPoint; + }; + if !edge_belongs_to_face(self.body(handle).expect("pick body exists"), seed, base_face) + { + return CmdResult::NeedPoint; + } + self.add_batch(vec![seed], point); + self.rebuild_preview(); + return CmdResult::NeedPoint; + } + let edges = match self.selection_mode { EdgeSelectionMode::Edge => vec![seed], EdgeSelectionMode::Chain => edge_chain(self.body(handle).expect("pick body exists"), seed), }; self.add_batch(edges, point); + self.rebuild_preview(); CmdResult::NeedPoint } fn on_point(&mut self, point: DVec3) -> CmdResult { - if !matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue) { + if !matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) { return CmdResult::NeedPoint; } let Some(anchor) = self.last_pick else { @@ -375,26 +507,65 @@ impl CadCommand for SolidEdgeCommand { if value <= 0.0 || !value.is_finite() { return CmdResult::NeedPoint; } - if self.step == EdgeStep::ChamferValue { - self.default_value = value; - self.finish() - } else { - self.accept_radius(value).unwrap_or(CmdResult::NeedPoint) + match self.step { + EdgeStep::Radius => self.accept_radius(value).unwrap_or(CmdResult::NeedPoint), + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => { + self.accept_distance(value).unwrap_or(CmdResult::NeedPoint) + } + _ => CmdResult::NeedPoint, } } fn on_text_input(&mut self, text: &str) -> Option { let keyword = text.trim().to_ascii_uppercase(); if self.operation == EdgeOperation::Chamfer { - if self.step != EdgeStep::ChamferValue { - return None; - } - let value = keyword.parse::().ok()?; - if value <= 0.0 || !value.is_finite() { - return None; - } - self.default_value = value; - return Some(self.finish()); + return match self.step { + EdgeStep::Selecting => match keyword.as_str() { + "L" | "LOOP" => { + self.step = EdgeStep::PickingLoop; + Some(CmdResult::NeedPoint) + } + "D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::Selecting)), + _ => None, + }, + EdgeStep::PickingLoop => match keyword.as_str() { + "E" | "EDGE" => { + self.step = EdgeStep::Selecting; + Some(CmdResult::NeedPoint) + } + "D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::PickingLoop)), + _ => None, + }, + EdgeStep::LoopConfirm => match keyword.as_str() { + "A" | "ACCEPT" => Some(self.accept_loop()), + "N" | "NEXT" if !self.loop_candidates.is_empty() => { + self.loop_index = (self.loop_index + 1) % self.loop_candidates.len(); + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + _ => None, + }, + EdgeStep::PreviewConfirm => match keyword.as_str() { + "D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::PreviewConfirm)), + _ => None, + }, + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => { + if matches!(keyword.as_str(), "E" | "EXPRESSION") { + self.expression_return = self.step; + self.step = EdgeStep::Expression; + Some(CmdResult::NeedPoint) + } else { + let value = keyword.parse::().ok()?; + self.accept_distance(value) + } + } + EdgeStep::Expression => { + let value = crate::app::expr_eval::eval_number(text)?; + self.step = self.expression_return; + self.accept_distance(value) + } + EdgeStep::Radius => None, + }; } match self.step { @@ -450,16 +621,29 @@ impl CadCommand for SolidEdgeCommand { self.accept_radius(value) } EdgeStep::Expression => { - let value = keyword.parse::().ok()?; + let value = crate::app::expr_eval::eval_number(text)?; self.accept_radius(value) } - EdgeStep::ChamferValue => None, + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => None, } } fn on_enter(&mut self) -> CmdResult { match (self.operation, self.step) { - (EdgeOperation::Chamfer, EdgeStep::ChamferValue) => self.finish(), + (EdgeOperation::Chamfer, EdgeStep::Selecting) if !self.selected_edges.is_empty() => { + self.step = EdgeStep::PreviewConfirm; + self.rebuild_preview(); + CmdResult::NeedPoint + } + (EdgeOperation::Chamfer, EdgeStep::LoopConfirm) => self.accept_loop(), + (EdgeOperation::Chamfer, EdgeStep::PreviewConfirm) => self.finish(), + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance1) => self + .accept_distance(self.default_value) + .unwrap_or(CmdResult::NeedPoint), + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance2) => self + .accept_distance(self.other_value) + .unwrap_or(CmdResult::NeedPoint), + (EdgeOperation::Chamfer, EdgeStep::Expression) => CmdResult::NeedPoint, (EdgeOperation::Chamfer, _) => CmdResult::Cancel, (_, EdgeStep::Selecting) if !self.selected_edges.is_empty() => { self.step = EdgeStep::PreviewConfirm; @@ -477,9 +661,6 @@ impl CadCommand for SolidEdgeCommand { } fn on_undo_step(&mut self) -> Option { - if self.operation != EdgeOperation::Fillet { - return None; - } if self.step == EdgeStep::LoopConfirm { self.loop_candidates.clear(); self.loop_index = 0; @@ -495,6 +676,7 @@ impl CadCommand for SolidEdgeCommand { } if self.selected_edges.is_empty() { self.handle = None; + self.base_face = None; } self.last_pick = self .selection_batches @@ -508,17 +690,27 @@ impl CadCommand for SolidEdgeCommand { fn wants_text_input(&self) -> bool { matches!( self.step, - EdgeStep::Radius | EdgeStep::Expression | EdgeStep::ChamferValue + EdgeStep::Radius + | EdgeStep::Expression + | EdgeStep::ChamferDistance1 + | EdgeStep::ChamferDistance2 ) } fn dyn_commit_as_text(&self) -> bool { - matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue) + matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) } fn dyn_spec(&self) -> Option { let pick = self.last_pick?; - matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue).then(|| DynSpec { + matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) + .then(|| DynSpec { anchor: DynAnchor::Point(pick), fields: vec![DynFieldSpec::new(match self.operation { EdgeOperation::Fillet => DynRole::Radius, @@ -530,16 +722,26 @@ impl CadCommand for SolidEdgeCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { - matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue) + matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) .then(|| self.last_pick.map(|pick| cursor.distance(pick))) .flatten() } fn on_preview_wires(&mut self, cursor: DVec3) -> Vec { - if self.step == EdgeStep::Radius { + if matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) { if let Some(value) = self.last_pick.map(|pick| cursor.distance(pick)) { if value > 0.0 && value.is_finite() { - if let Some((handle, wires)) = self.preview_for_value(value) { + let values = match self.step { + EdgeStep::ChamferDistance2 => (self.default_value, value), + _ => (value, self.other_value), + }; + if let Some((handle, wires)) = self.preview_for_values(values.0, values.1) { self.preview_wires = wires; self.preview_hidden.clear(); self.preview_hidden.push(handle); @@ -558,7 +760,7 @@ impl CadCommand for SolidEdgeCommand { } } -fn edge_loops(body: &Body, seed: EdgeKey) -> Vec> { +fn edge_loops(body: &Body, seed: EdgeKey) -> Vec<(FaceKey, Vec)> { let Some(edge) = body.edges.get(seed) else { return Vec::new(); }; @@ -575,13 +777,48 @@ fn edge_loops(body: &Body, seed: EdgeKey) -> Vec> { .iter() .filter_map(|key| body.coedges.get(*key).map(|coedge| coedge.edge)) .collect::>(); - if !edges.is_empty() && !candidates.contains(&edges) { - candidates.push(edges); + let candidate = (edge_loop.owner, edges); + if !candidate.1.is_empty() && !candidates.contains(&candidate) { + candidates.push(candidate); } } candidates } +fn edge_belongs_to_face(body: &Body, edge: EdgeKey, face: FaceKey) -> bool { + body.edges.get(edge).is_some_and(|edge| { + edge.coedges.iter().any(|coedge| { + body.coedges + .get(*coedge) + .and_then(|coedge| body.loops.get(coedge.owner)) + .is_some_and(|edge_loop| edge_loop.owner == face) + }) + }) +} + +fn nearest_edge_face(body: &Body, edge: EdgeKey, point: [f64; 3]) -> Option { + let edge = body.edges.get(edge)?; + edge.coedges + .iter() + .filter_map(|coedge| { + let edge_loop = body.loops.get(body.coedges.get(*coedge)?.owner)?; + let face = body.faces.get(edge_loop.owner)?; + let surface = body.surfaces.get(face.surface)?; + let distance = surface.distance_to(point).abs(); + distance.is_finite().then_some((edge_loop.owner, distance)) + }) + .min_by(|first, second| first.1.total_cmp(&second.1)) + .map(|(face, _)| face) +} + +fn positive_default(value: f64) -> f64 { + if value.is_finite() && value > 0.0 { + value + } else { + 1.0 + } +} + fn edge_chain(body: &Body, seed: EdgeKey) -> Vec { let mut chain = vec![seed]; let mut next = 0; @@ -643,5 +880,5 @@ fn edge_tangent_from_vertex( } inventory::submit!(crate::command::CommandRegistration { - names: &["FILLETEDGE", "SOLIDFILLET", "SOLIDCHAMFER"] + names: &["FILLETEDGE", "SOLIDFILLET", "CHAMFEREDGE", "SOLIDCHAMFER"] }); diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index 72699a0b7..d33b28208 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -89,7 +89,7 @@ impl CadModule for ModelModule { title: "Edges", tools: vec![ RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", FILLET_ICON)), - RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)), + RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)), ], }, ] diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 0e86cd324..650b90b66 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -1,7 +1,8 @@ use acadrust::entities::{EmbeddedEntity, Solid3D}; use acadrust::objects::{ - DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone, - SolidHistoryCylinder, SolidHistoryFillet, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, + DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryChamfer, + SolidHistoryCone, SolidHistoryCylinder, SolidHistoryFillet, SolidHistoryLoft, + SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus, }; @@ -33,6 +34,8 @@ pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000; pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000; pub const GRIP_REVOLVE_AXIS: usize = 10_013; pub const GRIP_FILLET_RADIUS: usize = 10_014; +pub const GRIP_CHAMFER_DISTANCE1: usize = 10_015; +pub const GRIP_CHAMFER_DISTANCE2: usize = 10_016; pub const GRIP_BOX_CORNER_FIRST: usize = 10_100; pub const GRIP_BOX_FACE_X_MIN: usize = 10_110; pub const GRIP_BOX_FACE_X_MAX: usize = 10_111; @@ -3132,6 +3135,104 @@ pub fn fillet_radius_grip(body: &Body, radius: f64) -> Option { None } +pub fn chamfer_distance_grips( + document: &acadrust::CadDocument, + handle: acadrust::Handle, + value: &SolidHistoryChamfer, +) -> Vec { + let Some(operations) = document.solid_history_operations(handle) else { + return Vec::new(); + }; + let Some(chamfer_index) = operations + .iter() + .rposition(|operation| matches!(operation, SolidHistoryOperation::Chamfer(_))) + else { + return Vec::new(); + }; + let Ok(source) = cadkernel::acis::rebuild_history(&operations[..chamfer_index]) else { + return Vec::new(); + }; + let edges = source.edge_keys().collect::>(); + let faces = source.face_keys().collect::>(); + let Some(edge) = value + .edges + .first() + .and_then(|ordinal| usize::try_from(*ordinal).ok()) + .and_then(|ordinal| edges.get(ordinal).copied()) + .and_then(|edge| source.edges.get(edge)) + else { + return Vec::new(); + }; + let Some(base_face) = usize::try_from(value.base_face) + .ok() + .and_then(|ordinal| faces.get(ordinal).copied()) + else { + return Vec::new(); + }; + let adjacent = edge + .coedges + .iter() + .filter_map(|coedge| source.coedges.get(*coedge)) + .filter_map(|coedge| source.loops.get(coedge.owner)) + .map(|edge_loop| edge_loop.owner) + .collect::>(); + let Some(other_face) = adjacent.iter().copied().find(|face| *face != base_face) else { + return Vec::new(); + }; + let outward = |face_key| { + let face = source.faces.get(face_key)?; + let Surface::Plane(plane) = source.surfaces.get(face.surface)? else { + return None; + }; + let normal = glam::DVec3::from_array(plane.normal()?).try_normalize()?; + Some(if face.forward { normal } else { -normal }) + }; + let Some(base_normal) = outward(base_face) else { + return Vec::new(); + }; + let Some(other_normal) = outward(other_face) else { + return Vec::new(); + }; + let dot = base_normal.dot(other_normal); + let Some(base_inward) = (-(other_normal - base_normal * dot)).try_normalize() else { + return Vec::new(); + }; + let Some(other_inward) = (-(base_normal - other_normal * dot)).try_normalize() else { + return Vec::new(); + }; + let Some(start) = source.vertices.get(edge.start) else { + return Vec::new(); + }; + let Some(end) = source.vertices.get(edge.end) else { + return Vec::new(); + }; + let midpoint = + (glam::DVec3::from_array(start.point) + glam::DVec3::from_array(end.point)) * 0.5; + [ + ( + GRIP_CHAMFER_DISTANCE1, + value.base_distance, + base_inward, + ), + ( + GRIP_CHAMFER_DISTANCE2, + value.other_distance, + other_inward, + ), + ] + .into_iter() + .filter(|(_, distance, _)| distance.is_finite() && *distance > 0.0) + .map(|(id, distance, axis)| { + grip( + id, + midpoint + axis * distance, + GripShape::Square, + Some(axis), + ) + }) + .collect() +} + pub fn primitive_grips( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -3151,6 +3252,9 @@ pub fn primitive_grips( .into_iter() .collect(); } + if let SolidHistoryOperation::Chamfer(value) = operation { + return chamfer_distance_grips(document, handle, value); + } let mut grips = Vec::new(); let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| { if let Some(world) = world_point(transform, point) { @@ -3782,3 +3886,21 @@ pub fn fillet_op(edges: Vec, radius: f64) -> SolidHistoryOperation { ..SolidHistoryFillet::default() }) } + +pub fn chamfer_op( + edges: Vec, + base_face: i32, + base_distance: f64, + other_distance: f64, +) -> SolidHistoryOperation { + SolidHistoryOperation::Chamfer(SolidHistoryChamfer { + base: base(glam::DMat4::IDENTITY.to_cols_array()), + operation_major: 1, + method: 0, + base_distance, + other_distance, + edges, + base_face, + ..SolidHistoryChamfer::default() + }) +} diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 47dc462ca..da829a765 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1183,6 +1183,47 @@ impl Scene { value.radii.push(radius); return self.preview_solid_history(handle, operation); } + if matches!( + grip_id, + crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1 + | crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE2 + ) { + let definitions = { + let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &operation else { + return false; + }; + crate::scene::model::solid_history::chamfer_distance_grips( + &self.document, + handle, + value, + ) + }; + let Some(definition) = definitions.into_iter().find(|grip| grip.id == grip_id) else { + return false; + }; + let Some(axis) = definition.axis else { + return false; + }; + let change = match apply { + GripApply::Absolute(world) => (world - definition.world).dot(axis), + GripApply::Translate(delta) => delta.dot(axis), + }; + let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else { + return false; + }; + let distance = if grip_id + == crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1 + { + &mut value.base_distance + } else { + &mut value.other_distance + }; + *distance += change; + if !distance.is_finite() || *distance <= 1.0e-6 { + return false; + } + return self.preview_solid_history(handle, operation); + } if !crate::scene::model::solid_history::apply_primitive_grip( &mut operation, grip_id,