Complete CHAMFEREDGE workflow and history

This commit is contained in:
ramox81 committed 2026-09-08 15:45:10 +03:00
1 parent ed8cddc556
commit 8822371e18
10 files changed
+678 -157

No files matched your search

Generated
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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]
+10 -1
View File
@@ -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();
+41 -16
View File
@@ -1,6 +1,6 @@
use super::*;
fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
fn selected_edge_body(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
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<acadrust::Handle> {
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::<Vec<_>>();
@@ -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));
+158 -73
View File
@@ -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<Entit
Some(source)
}
fn entity_with_history_body(mut source: EntityType, body: &Body) -> Option<EntityType> {
let document = crate::scene::convert::acis_export::solid_to_sat(body)?;
let wires = solid_model::edge_wires(body);
match &mut source {
EntityType::Solid3D(entity) => {
entity.wires = wires;
entity.silhouettes.clear();
entity.set_sat_document(&document);
}
EntityType::Surface(entity) => {
entity.wires = wires;
entity.silhouettes.clear();
if entity.kind != SurfaceKind::Plane {
entity.kind = SurfaceKind::Generic;
entity.surface_data = SurfaceData::Generic;
}
entity.acis_data = AcisData::from_sat(&document.to_sat_string());
}
_ => return None,
}
Some(source)
}
enum IntersectBodyOutcome {
Area(Body),
Touching,
@@ -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::<Vec<_>>();
@@ -630,7 +603,7 @@ impl super::OpenCADStudio {
.collect::<Option<Vec<_>>>()
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::<Vec<_>>();
let Some(history_edges) = edges
.iter()
.map(|edge| {
ordered_edges
.iter()
.position(|candidate| candidate == edge)
.and_then(|ordinal| i32::try_from(ordinal).ok())
})
.collect::<Option<Vec<_>>>()
else {
self.command_line
.push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref());
return false;
};
let Some(history_face) = source
.face_keys()
.position(|candidate| candidate == base_face)
.and_then(|ordinal| i32::try_from(ordinal).ok())
else {
self.command_line
.push_error(crate::t!("The selected base face no longer belongs to the body.").as_ref());
return false;
};
self.push_undo_snapshot(i, "CHAMFEREDGE");
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<FaceKey>,
value: f64,
other_value: f64,
fillet: bool,
) -> Task<Message> {
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return Task::none();
}
if !matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(EntityType::Solid3D(_))
) {
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());
}
+2
View File
@@ -1551,7 +1551,9 @@ pub enum CmdResult {
SolidEdgeBlend {
handle: Handle,
edges: Vec<cadkernel::brep::EdgeKey>,
base_face: Option<cadkernel::brep::FaceKey>,
value: f64,
other_value: f64,
fillet: bool,
},
SolidSubtract {
+299 -62
View File
@@ -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<Handle>,
selected_edges: Vec<EdgeKey>,
base_face: Option<FaceKey>,
selection_batches: Vec<(Vec<EdgeKey>, DVec3)>,
selection_mode: EdgeSelectionMode,
step: EdgeStep,
radius_return: EdgeStep,
loop_candidates: Vec<Vec<EdgeKey>>,
value_return: EdgeStep,
expression_return: EdgeStep,
loop_candidates: Vec<(FaceKey, Vec<EdgeKey>)>,
loop_index: usize,
last_pick: Option<DVec3>,
default_value: f64,
other_value: f64,
preview_color: [f32; 4],
preview_wires: Vec<WireModel>,
preview_hidden: Vec<Handle>,
@@ -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<Handle>,
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<FaceKey> {
self.loop_candidates
.get(self.loop_index)
.map(|(face, _)| *face)
}
fn preview_edges(&self) -> Vec<EdgeKey> {
@@ -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<WireModel>)> {
fn preview_for_values(
&self,
value: f64,
other_value: f64,
) -> Option<(Handle, Vec<WireModel>)> {
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<CmdResult> {
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<CmdOption> {
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<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
if self.operation == EdgeOperation::Chamfer {
if self.step != EdgeStep::ChamferValue {
return None;
}
let value = keyword.parse::<f64>().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::<f64>().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::<f64>().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<CmdResult> {
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<DynSpec> {
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<f64> {
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<WireModel> {
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<Vec<EdgeKey>> {
fn edge_loops(body: &Body, seed: EdgeKey) -> Vec<(FaceKey, Vec<EdgeKey>)> {
let Some(edge) = body.edges.get(seed) else {
return Vec::new();
};
@@ -575,13 +777,48 @@ fn edge_loops(body: &Body, seed: EdgeKey) -> Vec<Vec<EdgeKey>> {
.iter()
.filter_map(|key| body.coedges.get(*key).map(|coedge| coedge.edge))
.collect::<Vec<_>>();
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<FaceKey> {
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<EdgeKey> {
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"]
});
+1 -1
View File
@@ -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)),
],
},
]
+124 -2
View File
@@ -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<GripDef> {
None
}
pub fn chamfer_distance_grips(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistoryChamfer,
) -> Vec<GripDef> {
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::<Vec<_>>();
let faces = source.face_keys().collect::<Vec<_>>();
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::<Vec<_>>();
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<i32>, radius: f64) -> SolidHistoryOperation {
..SolidHistoryFillet::default()
})
}
pub fn chamfer_op(
edges: Vec<i32>,
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()
})
}
+41
View File
@@ -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,