From 4f22d998e7a187b5d621375ce1c2e14a38a53c72 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:08:52 +0300 Subject: [PATCH] Complete independent CHAMFEREDGE workflow --- Cargo.lock | 4 +- Cargo.toml | 5 +- src/app/command_driver.rs | 22 ++ src/app/commands/dim.rs | 91 ++++- src/app/model_ops.rs | 171 ++++++++- src/app/update/dynamic.rs | 8 + src/app/update/viewport.rs | 8 + src/command.rs | 8 + src/modules/model/chamferedge_cmd.rs | 536 +++++++++++++++++++++++++++ src/modules/model/mod.rs | 3 +- src/scene/entity.rs | 4 +- src/scene/model/solid_history.rs | 138 ++++++- src/scene/modify.rs | 126 ++++++- 13 files changed, 1098 insertions(+), 26 deletions(-) create mode 100644 src/modules/model/chamferedge_cmd.rs diff --git a/Cargo.lock b/Cargo.lock index 326bb7474..7226c8c8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.5.4" -source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=5b56571a190e7a17c8f12d36390d2938b0fb72f7#5b56571a190e7a17c8f12d36390d2938b0fb72f7" +source = "git+https://github.com/ramox81/cadcodec.git?rev=5c0c01dcad4543dde2ee8bcd0e08131dfafbf2dc#5c0c01dcad4543dde2ee8bcd0e08131dfafbf2dc" dependencies = [ "ahash 0.8.12", "anyhow", @@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4486076cb003a5909e3383900c596f92ef599192#4486076cb003a5909e3383900c596f92ef599192" +source = "git+https://github.com/ramox81/cadkernel.git?rev=daca8b7392cedd036b85a80499643a6690833011#daca8b7392cedd036b85a80499643a6690833011" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 21a3ba4a8..2769b0a14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,10 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } [patch."https://github.com/HakanSeven12/cadcodec.git"] -acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7" } +acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "5c0c01dcad4543dde2ee8bcd0e08131dfafbf2dc" } + +[patch."https://github.com/HakanSeven12/cadkernel.git"] +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "daca8b7392cedd036b85a80499643a6690833011" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 0b74e15fd..a0a8bf5af 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -733,6 +733,7 @@ impl OpenCADStudio { &result, CmdResult::Relaunch(..) | CmdResult::Dispatch(..) + | CmdResult::SolidChamferEdges { .. } | CmdResult::SolidSubtract { .. } ); let task = self.apply_cmd_result_inner(result); @@ -4115,6 +4116,27 @@ impl OpenCADStudio { return task; } + CmdResult::SolidChamferEdges { + handle, + edges, + base_face, + distance1, + distance2, + } => { + let task = self.solid_chamfer_edges( + handle, + &edges, + base_face, + distance1, + distance2, + ); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + return task; + } + CmdResult::SolidSubtract { bases, cutters, diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index b5c2a0eaf..4d86a78d6 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -17,6 +17,49 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option { } } +fn selected_chamfer_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 { + return None; + }; + matches!( + scene.document.get_entity(*handle), + Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_)) + ) + .then_some(*handle) +} + +fn chamfer_edge_sources( + app: &mut OpenCADStudio, + tab: usize, +) -> Vec<(acadrust::Handle, cadkernel::brep::Body)> { + let handles = app.tabs[tab] + .scene + .document + .entities() + .filter_map(|entity| { + matches!( + entity, + acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_) + ) + .then_some(entity.common().handle) + }) + .collect::>(); + app.tabs[tab].scene.restore_solid_models(&handles); + handles + .into_iter() + .filter_map(|handle| { + app.tabs[tab] + .scene + .solid_models + .get(&handle) + .cloned() + .map(|body| (handle, body)) + }) + .collect() +} + impl OpenCADStudio { pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option> { match cmd { @@ -1107,16 +1150,52 @@ impl OpenCADStudio { } } - "SOLIDCHAMFER" => { - use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i)); + "CHAMFEREDGE" | "SOLIDCHAMFER" => { + use crate::modules::model::chamferedge_cmd::ChamferEdgeCommand; + let target = selected_chamfer_body(self, i); + let header = &self.tabs[i].scene.document.header; + let distances = (header.chamfer_distance_a, header.chamfer_distance_b); + let bodies = chamfer_edge_sources(self, i); + let command = ChamferEdgeCommand::new( + target, + bodies, + crate::scene::WireModel::SELECTED, + distances, + ); + let distances = command.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 command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i)); + "CHAMFER" if selected_chamfer_body(self, i).is_some() => { + use crate::modules::model::chamferedge_cmd::ChamferEdgeCommand; + let target = selected_chamfer_body(self, i); + let header = &self.tabs[i].scene.document.header; + let distances = (header.chamfer_distance_a, header.chamfer_distance_b); + let bodies = chamfer_edge_sources(self, i); + let command = ChamferEdgeCommand::new( + target, + bodies, + crate::scene::WireModel::SELECTED, + distances, + ); + let distances = command.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 a262c0ee3..478594762 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; +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, @@ -646,6 +669,152 @@ impl super::OpenCADStudio { Task::none() } + fn replace_edge_body_with_chamfer( + &mut self, + handle: Handle, + source: &Body, + result: Body, + edges: &[EdgeKey], + base_face: FaceKey, + distance1: f64, + distance2: 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 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, distance1, distance2), + ) { + self.command_line + .push_error(crate::t!("The chamfer history could not be recorded.").as_ref()); + return false; + } + 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_chamfer_edges( + &mut self, + handle: Handle, + edges: &[EdgeKey], + base_face: FaceKey, + distance1: f64, + distance2: f64, + ) -> 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(_) | 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 edge body geometry could not be restored.").as_ref()); + return Task::none(); + }; + let result = match cadkernel::brep::chamfer_edges( + &body, + edges, + base_face, + distance1, + distance2, + ) { + Ok(result) => result, + Err(error) => { + self.command_line + .push_error(&format!("CHAMFEREDGE: {error}")); + return Task::none(); + } + }; + if self.replace_edge_body_with_chamfer( + handle, + &body, + result, + edges, + base_face, + distance1, + distance2, + ) { + self.tabs[i].scene.document.header.chamfer_distance_a = distance1; + self.tabs[i].scene.document.header.chamfer_distance_b = distance2; + self.command_line + .push_output(crate::t!("CHAMFEREDGE: solid updated.").as_ref()); + } + Task::none() + } + /// Intersect compatible selected solids, Regions, and coplanar Surfaces. pub(super) fn solid_intersect(&mut self) -> Task { let i = self.active_tab; diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index feaf6b016..1e1888055 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -740,6 +740,14 @@ impl OpenCADStudio { .as_mut() .map(|c| c.on_preview_wires(cur)) .unwrap_or_default(); + let preview_hidden = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.preview_hidden_handles().to_vec()) + .unwrap_or_default(); + self.tabs[i] + .scene + .set_command_preview_hidden(&preview_hidden); self.tabs[i].scene.set_preview_wires(previews); } } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index c39916fe4..12c339a01 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2468,6 +2468,14 @@ impl OpenCADStudio { previews.push(guide); } } + let preview_hidden = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.preview_hidden_handles().to_vec()) + .unwrap_or_default(); + self.tabs[i] + .scene + .set_command_preview_hidden(&preview_hidden); self.tabs[i].scene.set_preview_wires(previews); } else { self.tabs[i].snap_result = None; diff --git a/src/command.rs b/src/command.rs index 25ccfb027..d3cc7f4d7 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1554,6 +1554,14 @@ pub enum CmdResult { value: f64, fillet: bool, }, + /// Chamfer one or more resolved B-rep edges on a common base face. + SolidChamferEdges { + handle: Handle, + edges: Vec, + base_face: cadkernel::brep::FaceKey, + distance1: f64, + distance2: f64, + }, SolidSubtract { bases: Vec, cutters: Vec, diff --git a/src/modules/model/chamferedge_cmd.rs b/src/modules/model/chamferedge_cmd.rs new file mode 100644 index 000000000..d781fa331 --- /dev/null +++ b/src/modules/model/chamferedge_cmd.rs @@ -0,0 +1,536 @@ +use acadrust::Handle; +use cadkernel::brep::{Body, EdgeKey, FaceKey}; +use glam::DVec3; + +use crate::command::{ + CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec, +}; +use crate::scene::model::wire_model::WireModel; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ChamferStep { + Selecting, + PickingLoop, + LoopConfirm, + PreviewConfirm, + Distance1, + Distance2, + Expression, +} + +pub struct ChamferEdgeCommand { + target: Option, + bodies: Vec<(Handle, Body)>, + handle: Option, + selected_edges: Vec, + base_face: Option, + selection_batches: Vec<(Vec, DVec3)>, + step: ChamferStep, + value_return: ChamferStep, + expression_return: ChamferStep, + loop_candidates: Vec<(FaceKey, Vec)>, + loop_index: usize, + last_pick: Option, + distance1: f64, + distance2: f64, + preview_color: [f32; 4], + preview_wires: Vec, + preview_hidden: Vec, +} + +impl ChamferEdgeCommand { + pub fn new( + target: Option, + bodies: Vec<(Handle, Body)>, + preview_color: [f32; 4], + distances: (f64, f64), + ) -> Self { + Self { + target, + bodies, + handle: None, + selected_edges: Vec::new(), + base_face: None, + selection_batches: Vec::new(), + step: ChamferStep::Selecting, + value_return: ChamferStep::Selecting, + expression_return: ChamferStep::Distance1, + loop_candidates: Vec::new(), + loop_index: 0, + last_pick: None, + distance1: positive_default(distances.0), + distance2: positive_default(distances.1), + preview_color, + preview_wires: Vec::new(), + preview_hidden: Vec::new(), + } + } + + pub fn distances(&self) -> (f64, f64) { + (self.distance1, self.distance2) + } + + fn body(&self, handle: Handle) -> Option<&Body> { + self.bodies + .iter() + .find_map(|(candidate, body)| (*candidate == handle).then_some(body)) + } + + fn active_body(&self) -> Option<(Handle, &Body)> { + let handle = self.handle?; + self.body(handle).map(|body| (handle, body)) + } + + fn pick_allowed(&self, handle: Handle) -> bool { + !handle.is_null() + && self.target.is_none_or(|target| target == handle) + && self.handle.is_none_or(|selected| selected == handle) + && self.body(handle).is_some() + } + + fn add_batch(&mut self, edges: Vec, anchor: DVec3) -> bool { + let batch = edges + .into_iter() + .filter(|edge| !self.selected_edges.contains(edge)) + .collect::>(); + if batch.is_empty() { + return false; + } + self.selected_edges.extend(batch.iter().copied()); + self.selection_batches.push((batch, anchor)); + true + } + + fn current_loop(&self) -> Option<&(FaceKey, Vec)> { + self.loop_candidates.get(self.loop_index) + } + + fn preview_edges(&self) -> Vec { + let mut edges = self.selected_edges.clone(); + if self.step == ChamferStep::LoopConfirm { + if let Some((_, loop_edges)) = self.current_loop() { + for edge in loop_edges { + if !edges.contains(edge) { + edges.push(*edge); + } + } + } + } + edges + } + + fn preview_for_values( + &self, + distance1: f64, + distance2: f64, + ) -> Option<(Handle, Vec)> { + let edges = self.preview_edges(); + if edges.is_empty() { + return None; + } + let (handle, body) = self.active_body()?; + let base_face = self.base_face.or_else(|| self.current_loop().map(|item| item.0))?; + let result = + cadkernel::brep::chamfer_edges(body, &edges, base_face, distance1, distance2).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!("{}-CHAMFEREDGE-PREVIEW", handle.value()); + } + (!wires.is_empty()).then_some((handle, wires)) + } + + fn rebuild_preview(&mut self) { + self.preview_wires.clear(); + self.preview_hidden.clear(); + if let Some((handle, wires)) = self.preview_for_values(self.distance1, self.distance2) { + self.preview_wires = wires; + self.preview_hidden.push(handle); + } + } + + fn begin_distances(&mut self, return_to: ChamferStep) -> CmdResult { + self.value_return = return_to; + self.step = ChamferStep::Distance1; + 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 { + ChamferStep::Distance1 => { + self.distance1 = value; + self.step = ChamferStep::Distance2; + } + ChamferStep::Distance2 => { + self.distance2 = value; + self.step = self.value_return; + } + _ => return None, + } + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + + fn accept_loop(&mut self) -> CmdResult { + if let (Some((face, edges)), Some(anchor)) = (self.current_loop().cloned(), self.last_pick) { + if self.base_face.is_none() { + self.base_face = Some(face); + } + self.add_batch(edges, anchor); + } + self.loop_candidates.clear(); + self.loop_index = 0; + self.step = ChamferStep::Selecting; + self.rebuild_preview(); + CmdResult::NeedPoint + } + + fn finish(&self) -> CmdResult { + let (Some(handle), Some(base_face)) = (self.handle, self.base_face) else { + return CmdResult::Cancel; + }; + if self.selected_edges.is_empty() { + return CmdResult::Cancel; + } + CmdResult::SolidChamferEdges { + handle, + edges: self.selected_edges.clone(), + base_face, + distance1: self.distance1, + distance2: self.distance2, + } + } +} + +impl CadCommand for ChamferEdgeCommand { + fn name(&self) -> &'static str { + "CHAMFEREDGE" + } + + fn prompt(&self) -> String { + match self.step { + ChamferStep::Selecting => crate::t!("Select an edge or [Loop/Distance]:").into_owned(), + ChamferStep::PickingLoop => { + crate::t!("Select edge of loop or [Edge/Distance]:").into_owned() + } + ChamferStep::LoopConfirm => { + crate::t!("Enter an option [Accept/Next] :").into_owned() + } + ChamferStep::PreviewConfirm => { + crate::t!("Press Enter to accept the chamfer or [Distance]:").into_owned() + } + ChamferStep::Distance1 => { + crate::tf!("Specify Distance1 or [Expression] <{:.4}>:", self.distance1) + .into_owned() + } + ChamferStep::Distance2 => { + crate::tf!("Specify Distance2 or [Expression] <{:.4}>:", self.distance2) + .into_owned() + } + ChamferStep::Expression => crate::t!("Enter expression:").into_owned(), + } + } + + fn options(&self) -> Vec { + match self.step { + ChamferStep::Selecting => vec![ + CmdOption::new("Loop", "L"), + CmdOption::new("Distance", "D"), + ], + ChamferStep::PickingLoop => vec![ + CmdOption::new("Edge", "E"), + CmdOption::new("Distance", "D"), + ], + ChamferStep::LoopConfirm => vec![ + CmdOption::new("Accept", "A"), + CmdOption::new("Next", "N"), + ], + ChamferStep::PreviewConfirm => vec![CmdOption::new("Distance", "D")], + ChamferStep::Distance1 | ChamferStep::Distance2 => { + vec![CmdOption::new("Expression", "E")] + } + ChamferStep::Expression => Vec::new(), + } + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, ChamferStep::Selecting | ChamferStep::PickingLoop) + } + + fn entity_pick_includes_fills(&self) -> bool { + true + } + + fn entity_pick_uses_surface_point(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if !self.pick_allowed(handle) { + return CmdResult::NeedPoint; + } + let Some(seed) = self + .body(handle) + .and_then(|body| crate::scene::model::solid_model::nearest_edge(body, point.to_array())) + else { + return CmdResult::NeedPoint; + }; + self.handle = Some(handle); + self.last_pick = Some(point); + + if self.step == ChamferStep::PickingLoop { + let mut candidates = edge_loops(self.body(handle).expect("pick body exists"), seed); + if let Some(base_face) = self.base_face { + candidates.retain(|(face, _)| *face == base_face); + } + self.loop_candidates = candidates; + self.loop_index = 0; + self.step = if self.loop_candidates.is_empty() { + ChamferStep::Selecting + } else { + ChamferStep::LoopConfirm + }; + self.rebuild_preview(); + return CmdResult::NeedPoint; + } + + 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(); + CmdResult::NeedPoint + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + if !matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2) { + return CmdResult::NeedPoint; + } + let Some(anchor) = self.last_pick else { + return CmdResult::NeedPoint; + }; + self.accept_distance(point.distance(anchor)) + .unwrap_or(CmdResult::NeedPoint) + } + + fn on_text_input(&mut self, text: &str) -> Option { + let keyword = text.trim().to_ascii_uppercase(); + match self.step { + ChamferStep::Selecting => match keyword.as_str() { + "L" | "LOOP" => { + self.step = ChamferStep::PickingLoop; + Some(CmdResult::NeedPoint) + } + "D" | "DISTANCE" => Some(self.begin_distances(ChamferStep::Selecting)), + _ => None, + }, + ChamferStep::PickingLoop => match keyword.as_str() { + "E" | "EDGE" => { + self.step = ChamferStep::Selecting; + Some(CmdResult::NeedPoint) + } + "D" | "DISTANCE" => Some(self.begin_distances(ChamferStep::PickingLoop)), + _ => None, + }, + ChamferStep::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, + }, + ChamferStep::PreviewConfirm => match keyword.as_str() { + "D" | "DISTANCE" => Some(self.begin_distances(ChamferStep::PreviewConfirm)), + _ => None, + }, + ChamferStep::Distance1 | ChamferStep::Distance2 => { + if matches!(keyword.as_str(), "E" | "EXPRESSION") { + self.expression_return = self.step; + self.step = ChamferStep::Expression; + Some(CmdResult::NeedPoint) + } else { + self.accept_distance(keyword.parse::().ok()?) + } + } + ChamferStep::Expression => { + let value = crate::app::expr_eval::eval_number(text)?; + self.step = self.expression_return; + self.accept_distance(value) + } + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + ChamferStep::Selecting if !self.selected_edges.is_empty() => { + self.step = ChamferStep::PreviewConfirm; + self.rebuild_preview(); + CmdResult::NeedPoint + } + ChamferStep::LoopConfirm => self.accept_loop(), + ChamferStep::PreviewConfirm => self.finish(), + ChamferStep::Distance1 => self + .accept_distance(self.distance1) + .unwrap_or(CmdResult::NeedPoint), + ChamferStep::Distance2 => self + .accept_distance(self.distance2) + .unwrap_or(CmdResult::NeedPoint), + ChamferStep::Expression => CmdResult::NeedPoint, + _ => CmdResult::Cancel, + } + } + + fn on_undo_step(&mut self) -> Option { + if self.step == ChamferStep::LoopConfirm { + self.loop_candidates.clear(); + self.loop_index = 0; + self.step = ChamferStep::Selecting; + self.rebuild_preview(); + return Some(CmdResult::NeedPoint); + } + let (batch, _) = self.selection_batches.pop()?; + self.selected_edges.retain(|edge| !batch.contains(edge)); + if self.selected_edges.is_empty() { + self.handle = None; + self.base_face = None; + } + self.last_pick = self.selection_batches.last().map(|(_, anchor)| *anchor); + self.step = ChamferStep::Selecting; + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + + fn wants_text_input(&self) -> bool { + matches!( + self.step, + ChamferStep::Distance1 | ChamferStep::Distance2 | ChamferStep::Expression + ) + } + + fn dyn_commit_as_text(&self) -> bool { + matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2) + } + + fn dyn_spec(&self) -> Option { + let pick = self.last_pick?; + matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2).then(|| DynSpec { + anchor: DynAnchor::Point(pick), + fields: vec![DynFieldSpec::new(DynRole::Distance)], + guide: DynGuide::Radius, + ref_point: None, + }) + } + + fn dyn_live_value(&self, cursor: DVec3) -> Option { + matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2) + .then(|| self.last_pick.map(|pick| cursor.distance(pick))) + .flatten() + } + + fn on_preview_wires(&mut self, cursor: DVec3) -> Vec { + if matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2) { + if let Some(value) = self.last_pick.map(|pick| cursor.distance(pick)) { + if value > 0.0 && value.is_finite() { + let values = if self.step == ChamferStep::Distance2 { + (self.distance1, value) + } else { + (value, self.distance2) + }; + if let Some((handle, wires)) = self.preview_for_values(values.0, values.1) { + self.preview_wires = wires; + self.preview_hidden = vec![handle]; + } else { + self.preview_wires.clear(); + self.preview_hidden.clear(); + } + } + } + } + self.preview_wires.clone() + } + + fn preview_hidden_handles(&self) -> &[Handle] { + &self.preview_hidden + } +} + +fn edge_loops(body: &Body, seed: EdgeKey) -> Vec<(FaceKey, Vec)> { + let Some(edge) = body.edges.get(seed) else { + return Vec::new(); + }; + let mut candidates = Vec::new(); + for coedge_key in &edge.coedges { + let Some(coedge) = body.coedges.get(*coedge_key) else { + continue; + }; + let Some(edge_loop) = body.loops.get(coedge.owner) else { + continue; + }; + let edges = edge_loop + .coedges + .iter() + .filter_map(|key| body.coedges.get(*key).map(|coedge| coedge.edge)) + .collect::>(); + 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 + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["CHAMFEREDGE"] +}); diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index 18ee79a87..89c3d7e82 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -1,6 +1,7 @@ // Solid creation and kernel modelling tools. pub mod boolean_cmd; +pub mod chamferedge_cmd; pub mod cylinder_cmd; pub mod edge_cmd; pub mod polysolid_cmd; @@ -89,7 +90,7 @@ impl CadModule for ModelModule { title: "Edges", tools: vec![ RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)), - RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)), + RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)), ], }, ] diff --git a/src/scene/entity.rs b/src/scene/entity.rs index a1dbbf3af..ca4609047 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -596,8 +596,8 @@ impl Scene { .filter_map(|&handle| { let from_history = self .document - .solid_history_operation(handle) - .and_then(|operation| cadkernel::acis::rebuild_body(operation).ok()); + .solid_history_operations(handle) + .and_then(|operations| cadkernel::acis::rebuild_history(&operations).ok()); let body = from_history.or_else(|| match self.document.get_entity(handle) { Some(EntityType::Solid3D(solid)) => { crate::scene::convert::solid3d_tess::kernel_body(solid) diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index c52d28732..920c8171c 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -1,12 +1,13 @@ use acadrust::entities::{EmbeddedEntity, Solid3D}; use acadrust::objects::{ - DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone, - SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, + DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryChamfer, + SolidHistoryCone, SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, + SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus, }; use acadrust::EntityType; -use cadkernel::brep::Body; +use cadkernel::brep::{Body, Surface as KernelSurface}; use crate::command::EntityTransform; use crate::entities::traits::EntityTypeOps; @@ -32,6 +33,8 @@ pub const GRIP_SWEEP_PATH_FIRST: usize = 30_000; 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_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; @@ -197,7 +200,7 @@ pub fn has_specialized_primitive_properties( handle: acadrust::Handle, ) -> bool { matches!( - document.solid_history_operation(handle), + primitive_property_operation(document, handle).as_ref(), Some( SolidHistoryOperation::Box(_) | SolidHistoryOperation::Wedge(_) @@ -214,6 +217,18 @@ pub fn has_specialized_primitive_properties( ) } +/// Returns the primitive that owns the public Geometry rows. Later edge +/// operations refine it without replacing its editable type and dimensions. +pub fn primitive_property_operation( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> Option { + document + .solid_history_operations(handle) + .and_then(|operations| operations.into_iter().next()) + .or_else(|| document.solid_history_operation(handle).cloned()) +} + /// Solid history results whose public Properties palette is fully described by /// the common entity rows plus [`primitive_properties`]. A generic B-rep has /// no stable primitive dimensions to expose, but it still uses the compact @@ -1438,10 +1453,10 @@ pub fn primitive_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, ) -> Vec { - let Some(operation) = document.solid_history_operation(handle) else { + let Some(operation) = primitive_property_operation(document, handle) else { return brep_properties(document, handle); }; - match operation { + match &operation { SolidHistoryOperation::Box(value) => { rectangular_properties(document, handle, value, "Box") } @@ -3086,6 +3101,96 @@ fn grip( } } +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 KernelSurface::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, @@ -3093,6 +3198,9 @@ pub fn primitive_grips( let Some(operation) = document.solid_history_operation(handle) else { return Vec::new(); }; + 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) { @@ -3713,3 +3821,21 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation { ..SolidHistoryBrep::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 f84605425..552be4332 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -802,11 +802,39 @@ impl Scene { true } + pub fn append_solid_history( + &mut self, + handle: Handle, + operation: acadrust::objects::SolidHistoryOperation, + ) -> bool { + let previous = self + .document + .solid_history_graph(handle) + .map(|graph| graph.nodes) + .unwrap_or_default(); + self.record_solid_history_before(handle); + let Some(graph) = self.document.append_solid_history(handle, operation) else { + return false; + }; + for node in graph.nodes { + if !previous.contains(&node) { + self.record_undo_object_before(node, None); + } + } + self.sync_solid_reference_point(handle); + true + } + pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) { let reference = self .document - .solid_history_operation(handle) - .and_then(crate::scene::model::solid_history::reference_point); + .solid_history_operations(handle) + .and_then(|operations| { + operations + .iter() + .rev() + .find_map(crate::scene::model::solid_history::reference_point) + }); let Some(reference) = reference else { return; }; @@ -851,7 +879,34 @@ impl Scene { ) -> Option { use acadrust::objects::SolidHistoryOperation; - match (self.document.get_entity(handle)?, operation) { + let entity = self.document.get_entity(handle)?; + if matches!(entity, EntityType::Solid3D(_) | EntityType::Surface(_)) { + let mut operations = self.document.solid_history_operations(handle)?; + if operations.len() > 1 + || matches!(operation, SolidHistoryOperation::Chamfer(_)) + { + let replacement_id = operation.base().map(|base| { + if base.eval.node_id > 0 { + base.eval.node_id + } else { + base.step_id + } + })?; + let target = operations.iter_mut().find(|candidate| { + candidate.base().is_some_and(|base| { + let node_id = if base.eval.node_id > 0 { + base.eval.node_id + } else { + base.step_id + }; + node_id == replacement_id + }) + })?; + *target = operation.clone(); + return cadkernel::acis::rebuild_history(&operations).ok(); + } + } + match (entity, operation) { (EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => { cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok() } @@ -868,6 +923,17 @@ impl Scene { ) -> Option> { use acadrust::objects::SolidHistoryOperation; + if self + .document + .solid_history_operations(handle) + .is_some_and(|operations| operations.len() > 1) + { + return matches!( + self.document.get_entity(handle), + Some(EntityType::Solid3D(_) | EntityType::Surface(_)) + ) + .then_some(None); + } match (self.document.get_entity(handle)?, operation) { (EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => { crate::scene::model::solid_history::extrusion_surface_data(value).map(Some) @@ -909,7 +975,7 @@ impl Scene { self.record_solid_history_before(handle); if self .document - .update_solid_history(handle, operation) + .update_solid_history_step(handle, operation) .is_none() { return false; @@ -985,7 +1051,7 @@ impl Scene { } if self .document - .update_solid_history(handle, operation) + .update_solid_history_step(handle, operation) .is_none() { return false; @@ -1023,7 +1089,7 @@ impl Scene { }; if self .document - .update_solid_history(handle, operation) + .update_solid_history_step(handle, operation) .is_none() { return false; @@ -1122,6 +1188,47 @@ impl Scene { let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { return false; }; + 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, @@ -1138,7 +1245,12 @@ impl Scene { field: &str, value: &str, ) -> bool { - let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { + let Some(mut operation) = + crate::scene::model::solid_history::primitive_property_operation( + &self.document, + handle, + ) + else { return false; }; if !crate::scene::model::solid_history::apply_primitive_property(