diff --git a/Cargo.lock b/Cargo.lock index 326bb7474..e4aceb44e 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=1c7b30ff62aef5dd934c91bf8dd1cbdeb3d7155d#1c7b30ff62aef5dd934c91bf8dd1cbdeb3d7155d" 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=a1286c8fe2223973293c1010c7f8329e4ba0daf5#a1286c8fe2223973293c1010c7f8329e4ba0daf5" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 21a3ba4a8..82788538f 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 = "1c7b30ff62aef5dd934c91bf8dd1cbdeb3d7155d" } + +[patch."https://github.com/HakanSeven12/cadkernel.git"] +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "a1286c8fe2223973293c1010c7f8329e4ba0daf5" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 6c97312fe..2edae3fdb 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3187,6 +3187,17 @@ model = .cylinder-specify-axis-endpoint = CYLINDER Specify axis endpoint: .elliptical = Elliptical .select-a-solid-edge = Select a solid edge: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = Specify fillet radius <__ocs_fmt_0__>: .specify-chamfer-distance = Specify chamfer distance <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID Height = __ocs_fmt_0__, Width = __ocs_fmt_1__, Justification = __ocs_fmt_2__ diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 69705fee8..bbc60a6e3 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3146,6 +3146,17 @@ model = .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Eliptik .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Bir kenar seçin veya [Zincir/Döngü/Yarıçap]: + .filletedge-select-edge-chain-or-edge-radius = Bir kenar zinciri seçin veya [Kenar/Yarıçap]: + .filletedge-select-loop-edge-or-edge-chain-radius = Döngünün kenarını seçin veya [Kenar/Zincir/Yarıçap]: + .filletedge-enter-option-accept-next = Bir seçenek girin [Kabul/Sonraki] : + .filletedge-accept-or-radius = Fillet'i kabul etmek için Enter'a basın veya [Yarıçap]: + .filletedge-enter-radius-or-expression = Fillet yarıçapını girin veya [İfade] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Yarıçapı belirtin veya [İfade] <__ocs_fmt_0__>: + .filletedge-enter-expression = İfadeyi girin: + .filletedge-stale-edge = Seçilen kenar artık katıya ait değil. + .filletedge-history-create-failed = Katı geçmişi oluşturulamadı. + .filletedge-history-append-failed = Fillet geçmişi kaydedilemedi. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 0b74e15fd..b0db25725 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::SolidEdgeBlend { .. } | CmdResult::SolidSubtract { .. } ); let task = self.apply_cmd_result_inner(result); @@ -4103,11 +4104,11 @@ impl OpenCADStudio { CmdResult::SolidEdgeBlend { handle, - pick, + edges, value, fillet, } => { - let task = self.solid_edge_blend(handle, pick, value, fillet); + let task = self.solid_edge_blend(handle, &edges, 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 b5c2a0eaf..c083f6f60 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -17,6 +17,33 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option { } } +fn solid_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(_)) + .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 { @@ -960,16 +987,30 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "SOLIDFILLET" => { + "FILLETEDGE" | "SOLIDFILLET" => { use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i)); + let target = selected_solid(self, i); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new( + EdgeOperation::Fillet, + target, + bodies, + crate::scene::WireModel::SELECTED, + ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } "FILLET" if selected_solid(self, i).is_some() => { use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i)); + let target = selected_solid(self, i); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new( + EdgeOperation::Fillet, + target, + bodies, + crate::scene::WireModel::SELECTED, + ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } @@ -1109,14 +1150,28 @@ impl OpenCADStudio { "SOLIDCHAMFER" => { use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i)); + let target = selected_solid(self, i); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new( + EdgeOperation::Chamfer, + target, + bodies, + crate::scene::WireModel::SELECTED, + ); 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)); + let target = selected_solid(self, i); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new( + EdgeOperation::Chamfer, + target, + bodies, + crate::scene::WireModel::SELECTED, + ); 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..16e87d6f1 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}; use iced::Task; use std::collections::HashMap; @@ -596,10 +596,92 @@ impl super::OpenCADStudio { true } + 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(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()); + return false; + }; + let ordered = source.edge_keys().collect::>(); + let Some(history_edges) = edges + .iter() + .map(|edge| { + ordered + .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 solid.").as_ref()); + return false; + }; + + self.push_undo_snapshot(i, "FILLETEDGE"); + 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 history could not be created.").as_ref()); + return false; + } + if !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; + } + 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.set_sat_document(&document); + 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; + } + 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, - pick: glam::DVec3, + edges: &[EdgeKey], value: f64, fillet: bool, ) -> Task { @@ -621,15 +703,19 @@ impl super::OpenCADStudio { .push_error(crate::t!("The solid geometry could not be restored.").as_ref()); return Task::none(); }; - let Some(edge) = solid_model::nearest_edge(&body, pick.to_array()) else { - self.command_line - .push_error(crate::t!("Select a solid edge.").as_ref()); - return Task::none(); - }; let result = if fillet { - cadkernel::brep::fillet(&body, edge, value) + match cadkernel::brep::fillet_edges(&body, edges, value) { + Ok(result) => Some(result), + Err(error) => { + self.command_line + .push_error(&format!("FILLETEDGE: {error}")); + return Task::none(); + } + } } else { - cadkernel::brep::chamfer(&body, edge, value) + edges + .first() + .and_then(|edge| cadkernel::brep::chamfer(&body, *edge, value)) }; let Some(result) = result else { self.command_line.push_error( @@ -638,8 +724,13 @@ impl super::OpenCADStudio { ); return Task::none(); }; - let label = if fillet { "SOLIDFILLET" } else { "SOLIDCHAMFER" }; - if self.replace_solid_body(handle, result, label) { + let label = if fillet { "FILLETEDGE" } else { "SOLIDCHAMFER" }; + let updated = if fillet { + self.replace_solid_body_with_fillet(handle, &body, result, edges, value) + } else { + self.replace_solid_body(handle, result, label) + }; + if updated { self.command_line .push_output(crate::tf!("{label}: solid updated.").as_ref()); } 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..f91b124b8 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1547,10 +1547,10 @@ pub enum CmdResult { options: LoftOptions, color: [f32; 4], }, - /// Round or bevel the straight edge nearest `pick` on a solid. + /// Round or bevel one or more resolved B-rep edges on a solid. SolidEdgeBlend { handle: Handle, - pick: DVec3, + edges: Vec, value: f64, fillet: bool, }, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index eda54347e..72a2814df 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3871,8 +3871,19 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "CYLINDER Specify axis endpoint:" => Some(("model", "cylinder-specify-axis-endpoint")), "Elliptical" => Some(("model", "elliptical")), "Select a solid edge:" => Some(("model", "select-a-solid-edge")), + "Select an edge or [Chain/Loop/Radius]:" => Some(("model", "filletedge-select-edge-or-chain-loop-radius")), + "Select an edge chain or [Edge/Radius]:" => Some(("model", "filletedge-select-edge-chain-or-edge-radius")), + "Select edge of loop or [Edge/Chain/Radius]:" => Some(("model", "filletedge-select-loop-edge-or-edge-chain-radius")), + "Enter an option [Accept/Next] :" => Some(("model", "filletedge-enter-option-accept-next")), + "Press Enter to accept the fillet or [Radius]:" => Some(("model", "filletedge-accept-or-radius")), + "Enter fillet radius or [Expression] <{:.4}>:" => Some(("model", "filletedge-enter-radius-or-expression")), + "Specify Radius or [Expression] <{:.4}>:" => Some(("model", "filletedge-specify-radius-or-expression")), + "Enter expression:" => Some(("model", "filletedge-enter-expression")), "Specify fillet radius <{:.3}>:" => Some(("model", "specify-fillet-radius")), "Specify chamfer distance <{:.3}>:" => Some(("model", "specify-chamfer-distance")), + "A selected edge no longer belongs to the solid." => Some(("model", "filletedge-stale-edge")), + "The solid history could not be created." => Some(("model", "filletedge-history-create-failed")), + "The fillet history could not be recorded." => Some(("model", "filletedge-history-append-failed")), "POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}" => Some(("model", "polysolid-height-width-justification")), "Specify start point or [Object/Height/Width/Justify] :" => Some(("model", "specify-start-point-or-object-height-width-justify-object")), "Specify height <{:.4}>:" => Some(("model", "specify-height")), diff --git a/src/modules/model/edge_cmd.rs b/src/modules/model/edge_cmd.rs index 65ee2d315..a88e2fc88 100644 --- a/src/modules/model/edge_cmd.rs +++ b/src/modules/model/edge_cmd.rs @@ -1,42 +1,232 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + use acadrust::Handle; +use cadkernel::brep::{Body, EdgeKey}; use glam::DVec3; use crate::command::{ - CadCommand, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec, + CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec, }; +use crate::scene::model::wire_model::WireModel; -#[derive(Clone, Copy)] +static FILLET_RADIUS_BITS: AtomicU64 = AtomicU64::new(1.0f64.to_bits()); + +fn fillet_radius() -> f64 { + f64::from_bits(FILLET_RADIUS_BITS.load(Ordering::Relaxed)) +} + +fn set_fillet_radius(value: f64) { + FILLET_RADIUS_BITS.store(value.to_bits(), Ordering::Relaxed); +} + +#[derive(Clone, Copy, PartialEq, Eq)] pub enum EdgeOperation { Fillet, Chamfer, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum EdgeSelectionMode { + Edge, + Chain, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum EdgeStep { + Selecting, + PickingLoop, + LoopConfirm, + PreviewConfirm, + Radius, + Expression, + ChamferValue, +} + pub struct SolidEdgeCommand { operation: EdgeOperation, target: Option, - picked: Option<(Handle, DVec3)>, + bodies: Vec<(Handle, Body)>, + handle: Option, + selected_edges: Vec, + selection_batches: Vec<(Vec, DVec3)>, + selection_mode: EdgeSelectionMode, + step: EdgeStep, + radius_return: EdgeStep, + loop_candidates: Vec>, + loop_index: usize, + last_pick: Option, default_value: f64, + preview_color: [f32; 4], + preview_wires: Vec, + preview_hidden: Vec, } impl SolidEdgeCommand { - pub fn new(operation: EdgeOperation, target: Option) -> Self { + pub fn new( + operation: EdgeOperation, + target: Option, + bodies: Vec<(Handle, Body)>, + preview_color: [f32; 4], + ) -> Self { Self { operation, target, - picked: None, - default_value: 1.0, + bodies, + handle: None, + selected_edges: Vec::new(), + selection_batches: Vec::new(), + selection_mode: EdgeSelectionMode::Edge, + step: EdgeStep::Selecting, + radius_return: EdgeStep::Selecting, + loop_candidates: Vec::new(), + loop_index: 0, + last_pick: None, + default_value: if operation == EdgeOperation::Fillet { + fillet_radius() + } else { + 1.0 + }, + preview_color, + preview_wires: Vec::new(), + preview_hidden: Vec::new(), } } - fn finish(&self, value: f64) -> CmdResult { - let Some((handle, pick)) = self.picked else { + 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<&[EdgeKey]> { + self.loop_candidates + .get(self.loop_index) + .map(Vec::as_slice) + } + + fn preview_edges(&self) -> Vec { + let mut edges = self.selected_edges.clone(); + if self.step == EdgeStep::LoopConfirm { + if let Some(loop_edges) = self.current_loop() { + for edge in loop_edges { + if !edges.contains(edge) { + edges.push(*edge); + } + } + } + } + edges + } + + fn preview_active(&self) -> bool { + self.operation == EdgeOperation::Fillet + && matches!( + self.step, + EdgeStep::LoopConfirm + | EdgeStep::PreviewConfirm + | EdgeStep::Radius + | EdgeStep::Expression + ) + } + + fn preview_for_value(&self, value: f64) -> Option<(Handle, Vec)> { + if !self.preview_active() { + return None; + } + let edges = self.preview_edges(); + if edges.is_empty() { + return None; + } + let (handle, body) = self.active_body()?; + let result = cadkernel::brep::fillet_edges(body, &edges, 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()); + } + (!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_value(self.default_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.step = EdgeStep::Radius; + self.rebuild_preview(); + CmdResult::NeedPoint + } + + fn accept_radius(&mut self, value: f64) -> Option { + if value <= 0.0 || !value.is_finite() { + return None; + } + self.default_value = value; + if self.operation == EdgeOperation::Fillet { + set_fillet_radius(value); + } + self.step = self.radius_return; + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + + fn accept_loop(&mut self) -> CmdResult { + if let (Some(edges), Some(anchor)) = ( + self.current_loop().map(|edges| edges.to_vec()), + self.last_pick, + ) { + self.add_batch(edges, anchor); + } + self.loop_candidates.clear(); + self.loop_index = 0; + self.step = EdgeStep::Selecting; + self.rebuild_preview(); + CmdResult::NeedPoint + } + + fn finish(&self) -> CmdResult { + let Some(handle) = self.handle else { return CmdResult::Cancel; }; + if self.selected_edges.is_empty() { + return CmdResult::Cancel; + } CmdResult::SolidEdgeBlend { handle, - pick, - value, - fillet: matches!(self.operation, EdgeOperation::Fillet), + edges: self.selected_edges.clone(), + value: self.default_value, + fillet: self.operation == EdgeOperation::Fillet, } } } @@ -44,27 +234,83 @@ impl SolidEdgeCommand { impl CadCommand for SolidEdgeCommand { fn name(&self) -> &'static str { match self.operation { - EdgeOperation::Fillet => "SOLIDFILLET", + EdgeOperation::Fillet => "FILLETEDGE", EdgeOperation::Chamfer => "SOLIDCHAMFER", } } fn prompt(&self) -> String { - if self.picked.is_none() { - return crate::t!("Select a solid edge:").into_owned(); + match (self.operation, self.step, self.selection_mode) { + (EdgeOperation::Chamfer, EdgeStep::Selecting, _) => { + crate::t!("Select a solid edge:").into_owned() + } + (EdgeOperation::Chamfer, EdgeStep::ChamferValue, _) => crate::tf!( + "Specify chamfer distance <{:.3}>:", + self.default_value + ) + .into_owned(), + (_, EdgeStep::Selecting, EdgeSelectionMode::Edge) => { + crate::t!("Select an edge or [Chain/Loop/Radius]:").into_owned() + } + (_, EdgeStep::Selecting, EdgeSelectionMode::Chain) => { + crate::t!("Select an edge chain or [Edge/Radius]:").into_owned() + } + (_, EdgeStep::PickingLoop, _) => { + crate::t!("Select edge of loop or [Edge/Chain/Radius]:").into_owned() + } + (_, EdgeStep::LoopConfirm, _) => { + crate::t!("Enter an option [Accept/Next] :").into_owned() + } + (_, EdgeStep::PreviewConfirm, _) => { + crate::t!("Press Enter to accept the fillet or [Radius]:").into_owned() + } + (_, EdgeStep::Radius, _) if self.radius_return == EdgeStep::PreviewConfirm => crate::tf!( + "Specify Radius or [Expression] <{:.4}>:", + self.default_value + ) + .into_owned(), + (_, EdgeStep::Radius, _) => crate::tf!( + "Enter fillet radius or [Expression] <{:.4}>:", + self.default_value + ) + .into_owned(), + (_, EdgeStep::Expression, _) => crate::t!("Enter expression:").into_owned(), + _ => String::new(), } - match self.operation { - EdgeOperation::Fillet => { - crate::tf!("Specify fillet radius <{:.3}>:", self.default_value).into_owned() + } + + fn options(&self) -> Vec { + match (self.operation, self.step, self.selection_mode) { + (EdgeOperation::Fillet, EdgeStep::Selecting, EdgeSelectionMode::Edge) => vec![ + CmdOption::new("Chain", "C"), + CmdOption::new("Loop", "L"), + CmdOption::new("Radius", "R"), + ], + (EdgeOperation::Fillet, EdgeStep::Selecting, EdgeSelectionMode::Chain) => vec![ + CmdOption::new("Edge", "E"), + CmdOption::new("Radius", "R"), + ], + (EdgeOperation::Fillet, EdgeStep::PickingLoop, _) => vec![ + CmdOption::new("Edge", "E"), + CmdOption::new("Chain", "C"), + CmdOption::new("Radius", "R"), + ], + (EdgeOperation::Fillet, EdgeStep::LoopConfirm, _) => vec![ + CmdOption::new("Accept", "A"), + CmdOption::new("Next", "N"), + ], + (EdgeOperation::Fillet, EdgeStep::PreviewConfirm, _) => { + vec![CmdOption::new("Radius", "R")] } - EdgeOperation::Chamfer => { - crate::tf!("Specify chamfer distance <{:.3}>:", self.default_value).into_owned() + (EdgeOperation::Fillet, EdgeStep::Radius, _) => { + vec![CmdOption::new("Expression", "E")] } + _ => Vec::new(), } } fn needs_entity_pick(&self) -> bool { - self.picked.is_none() + matches!(self.step, EdgeStep::Selecting | EdgeStep::PickingLoop) } fn entity_pick_includes_fills(&self) -> bool { @@ -80,52 +326,199 @@ impl CadCommand for SolidEdgeCommand { } fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { - if handle.is_null() || self.target.is_some_and(|target| target != handle) { + if !self.pick_allowed(handle) { return CmdResult::NeedPoint; } - self.picked = Some((handle, point)); + 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.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); + self.loop_index = 0; + self.step = if self.loop_candidates.is_empty() { + EdgeStep::Selecting + } else { + EdgeStep::LoopConfirm + }; + 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); CmdResult::NeedPoint } fn on_point(&mut self, point: DVec3) -> CmdResult { - let Some((_, pick)) = self.picked else { + if !matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue) { + return CmdResult::NeedPoint; + } + let Some(anchor) = self.last_pick else { return CmdResult::NeedPoint; }; - let value = point.distance(pick); - if value > 0.0 && value.is_finite() { - self.finish(value) + let value = point.distance(anchor); + if value <= 0.0 || !value.is_finite() { + return CmdResult::NeedPoint; + } + if self.step == EdgeStep::ChamferValue { + self.default_value = value; + self.finish() } else { - CmdResult::NeedPoint + self.accept_radius(value).unwrap_or(CmdResult::NeedPoint) } } fn on_text_input(&mut self, text: &str) -> Option { - if self.picked.is_none() { - return None; + 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()); + } + + match self.step { + EdgeStep::Selecting => match keyword.as_str() { + "R" | "RADIUS" => Some(self.begin_radius(EdgeStep::Selecting)), + "C" | "CHAIN" => { + self.selection_mode = EdgeSelectionMode::Chain; + Some(CmdResult::NeedPoint) + } + "E" | "EDGE" if self.selection_mode == EdgeSelectionMode::Chain => { + self.selection_mode = EdgeSelectionMode::Edge; + Some(CmdResult::NeedPoint) + } + "L" | "LOOP" => { + self.step = EdgeStep::PickingLoop; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + EdgeStep::PickingLoop => match keyword.as_str() { + "E" | "EDGE" => { + self.selection_mode = EdgeSelectionMode::Edge; + self.step = EdgeStep::Selecting; + Some(CmdResult::NeedPoint) + } + "C" | "CHAIN" => { + self.selection_mode = EdgeSelectionMode::Chain; + self.step = EdgeStep::Selecting; + Some(CmdResult::NeedPoint) + } + "R" | "RADIUS" => Some(self.begin_radius(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() { + "R" | "RADIUS" => Some(self.begin_radius(EdgeStep::PreviewConfirm)), + _ => None, + }, + EdgeStep::Radius => { + if matches!(keyword.as_str(), "E" | "EXPRESSION") { + self.step = EdgeStep::Expression; + return Some(CmdResult::NeedPoint); + } + let value = keyword.parse::().ok()?; + self.accept_radius(value) + } + EdgeStep::Expression => { + let value = keyword.parse::().ok()?; + self.accept_radius(value) + } + EdgeStep::ChamferValue => None, } - let value = text.trim().parse::().ok()?; - (value > 0.0 && value.is_finite()).then(|| self.finish(value)) } fn on_enter(&mut self) -> CmdResult { - if self.picked.is_some() { - self.finish(self.default_value) - } else { - CmdResult::Cancel + match (self.operation, self.step) { + (EdgeOperation::Chamfer, EdgeStep::ChamferValue) => self.finish(), + (EdgeOperation::Chamfer, _) => CmdResult::Cancel, + (_, EdgeStep::Selecting) if !self.selected_edges.is_empty() => { + self.step = EdgeStep::PreviewConfirm; + self.rebuild_preview(); + CmdResult::NeedPoint + } + (_, EdgeStep::LoopConfirm) => self.accept_loop(), + (_, EdgeStep::PreviewConfirm) => self.finish(), + (_, EdgeStep::Radius) => self + .accept_radius(self.default_value) + .unwrap_or(CmdResult::NeedPoint), + (_, EdgeStep::Expression) => CmdResult::NeedPoint, + _ => CmdResult::Cancel, } } + 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; + self.step = EdgeStep::Selecting; + self.rebuild_preview(); + return Some(CmdResult::NeedPoint); + } + let (batch, _) = self.selection_batches.pop()?; + for edge in batch { + if let Some(index) = self.selected_edges.iter().position(|candidate| *candidate == edge) { + self.selected_edges.remove(index); + } + } + if self.selected_edges.is_empty() { + self.handle = None; + } + self.last_pick = self + .selection_batches + .last() + .map(|(_, anchor)| *anchor); + self.step = EdgeStep::Selecting; + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + fn wants_text_input(&self) -> bool { - self.picked.is_some() + matches!( + self.step, + EdgeStep::Radius | EdgeStep::Expression | EdgeStep::ChamferValue + ) } fn dyn_commit_as_text(&self) -> bool { - self.picked.is_some() + matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue) } fn dyn_spec(&self) -> Option { - let (_, pick) = self.picked?; - Some(DynSpec { + let pick = self.last_pick?; + matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue).then(|| DynSpec { anchor: DynAnchor::Point(pick), fields: vec![DynFieldSpec::new(match self.operation { EdgeOperation::Fillet => DynRole::Radius, @@ -137,10 +530,118 @@ impl CadCommand for SolidEdgeCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { - self.picked.map(|(_, pick)| cursor.distance(pick)) + matches!(self.step, EdgeStep::Radius | EdgeStep::ChamferValue) + .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 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) { + self.preview_wires = wires; + self.preview_hidden.clear(); + self.preview_hidden.push(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> { + 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::>(); + if !edges.is_empty() && !candidates.contains(&edges) { + candidates.push(edges); + } + } + candidates +} + +fn edge_chain(body: &Body, seed: EdgeKey) -> Vec { + let mut chain = vec![seed]; + let mut next = 0; + while next < chain.len() { + let current_key = chain[next]; + next += 1; + let Some(current) = body.edges.get(current_key) else { + continue; + }; + for vertex in [current.start, current.end] { + let Some(current_tangent) = edge_tangent_from_vertex(body, current_key, vertex) else { + continue; + }; + for candidate_key in body.edge_keys() { + if chain.contains(&candidate_key) { + continue; + } + let Some(candidate) = body.edges.get(candidate_key) else { + continue; + }; + if candidate.start != vertex && candidate.end != vertex { + continue; + } + let Some(candidate_tangent) = + edge_tangent_from_vertex(body, candidate_key, vertex) + else { + continue; + }; + if current_tangent.dot(candidate_tangent) < -0.999 { + chain.push(candidate_key); + } + } + } + } + chain +} + +fn edge_tangent_from_vertex( + body: &Body, + edge_key: EdgeKey, + vertex: cadkernel::brep::VertexKey, +) -> Option { + let edge = body.edges.get(edge_key)?; + let curve = body.curves.get(edge.curve)?; + let span = edge.end_parameter - edge.start_parameter; + if !span.is_finite() || span.abs() <= f64::EPSILON { + return None; + } + let step = span * 1.0e-6; + let (at, inside) = if edge.start == vertex { + (edge.start_parameter, edge.start_parameter + step) + } else if edge.end == vertex { + (edge.end_parameter, edge.end_parameter - step) + } else { + return None; + }; + let tangent = DVec3::from(curve.point_at(inside)) - DVec3::from(curve.point_at(at)); + (tangent.length_squared() > 0.0 && tangent.is_finite()).then(|| tangent.normalize()) +} + inventory::submit!(crate::command::CommandRegistration { - names: &["SOLIDFILLET", "SOLIDCHAMFER"] + names: &["FILLETEDGE", "SOLIDFILLET", "SOLIDCHAMFER"] }); diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index 18ee79a87..72699a0b7 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -88,7 +88,7 @@ impl CadModule for ModelModule { RibbonGroup { title: "Edges", tools: vec![ - RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)), + RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", FILLET_ICON)), RibbonItem::LargeTool(tool("SOLIDCHAMFER", "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..0e86cd324 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -1,12 +1,12 @@ use acadrust::entities::{EmbeddedEntity, Solid3D}; use acadrust::objects::{ DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone, - SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, + SolidHistoryCylinder, SolidHistoryFillet, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus, }; use acadrust::EntityType; -use cadkernel::brep::Body; +use cadkernel::brep::{Body, Surface}; use crate::command::EntityTransform; use crate::entities::traits::EntityTypeOps; @@ -32,6 +32,7 @@ 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_FILLET_RADIUS: usize = 10_014; 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; @@ -3086,6 +3087,51 @@ fn grip( } } +pub fn fillet_radius_grip(body: &Body, radius: f64) -> Option { + if !radius.is_finite() || radius <= 0.0 { + return None; + } + for (face_key, face) in body.faces.iter() { + let Some(Surface::Cylinder(cylinder)) = body.surfaces.get(face.surface) else { + continue; + }; + if (cylinder.radius.abs() - radius).abs() > radius.max(1.0) * 1.0e-7 { + continue; + } + let axis = glam::DVec3::from_array(cylinder.base.normal()?).try_normalize()?; + let origin = glam::DVec3::from_array(cylinder.base.origin); + let points = body + .face_coedges(face_key) + .into_iter() + .filter_map(|coedge| body.coedges.get(coedge)) + .filter_map(|coedge| body.edges.get(coedge.edge)) + .flat_map(|edge| [edge.start, edge.end]) + .filter_map(|vertex| body.vertices.get(vertex)) + .map(|vertex| glam::DVec3::from_array(vertex.point)) + .collect::>(); + let minimum = points + .iter() + .map(|point| (*point - origin).dot(axis)) + .reduce(f64::min)?; + let maximum = points + .iter() + .map(|point| (*point - origin).dot(axis)) + .reduce(f64::max)?; + let center = origin + axis * ((minimum + maximum) * 0.5); + let radial = points + .iter() + .map(|point| *point - center - axis * (*point - center).dot(axis)) + .find_map(|radial| radial.try_normalize())?; + return Some(grip( + GRIP_FILLET_RADIUS, + center + radial * radius, + GripShape::Square, + Some(radial), + )); + } + None +} + pub fn primitive_grips( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -3093,6 +3139,18 @@ pub fn primitive_grips( let Some(operation) = document.solid_history_operation(handle) else { return Vec::new(); }; + if let SolidHistoryOperation::Fillet(value) = operation { + let Some(radius) = value.radii.first().copied() else { + return Vec::new(); + }; + let Some(EntityType::Solid3D(solid)) = document.get_entity(handle) else { + return Vec::new(); + }; + return crate::scene::convert::solid3d_tess::kernel_body(solid) + .and_then(|body| fillet_radius_grip(&body, radius)) + .into_iter() + .collect(); + } 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 +3771,14 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation { ..SolidHistoryBrep::default() }) } + +pub fn fillet_op(edges: Vec, radius: f64) -> SolidHistoryOperation { + SolidHistoryOperation::Fillet(SolidHistoryFillet { + base: base(glam::DMat4::IDENTITY.to_cols_array()), + operation_major: 1, + method: 0, + edges, + radii: vec![radius], + ..SolidHistoryFillet::default() + }) +} diff --git a/src/scene/modify.rs b/src/scene/modify.rs index f84605425..47dc462ca 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -802,6 +802,29 @@ 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 @@ -855,8 +878,14 @@ impl Scene { (EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => { cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok() } - (EntityType::Surface(_), SolidHistoryOperation::Loft(_)) - | (EntityType::Solid3D(_), _) => cadkernel::acis::rebuild_body(operation).ok(), + (EntityType::Surface(_), SolidHistoryOperation::Loft(_)) => { + cadkernel::acis::rebuild_body(operation).ok() + } + (EntityType::Solid3D(_), _) => { + let mut operations = self.document.solid_history_operations(handle)?; + *operations.last_mut()? = operation.clone(); + cadkernel::acis::rebuild_history(&operations).ok() + } _ => None, } } @@ -1122,6 +1151,38 @@ impl Scene { let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { return false; }; + if grip_id == crate::scene::model::solid_history::GRIP_FILLET_RADIUS { + let acadrust::objects::SolidHistoryOperation::Fillet(value) = &mut operation else { + return false; + }; + let Some(radius) = value.radii.first().copied() else { + return false; + }; + self.restore_solid_models(&[handle]); + let Some(definition) = self + .solid_models + .get(&handle) + .and_then(|body| { + crate::scene::model::solid_history::fillet_radius_grip(body, radius) + }) + 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 radius = radius + change; + if !radius.is_finite() || radius <= 1.0e-6 { + return false; + } + value.radii.clear(); + value.radii.push(radius); + return self.preview_solid_history(handle, operation); + } if !crate::scene::model::solid_history::apply_primitive_grip( &mut operation, grip_id,