Complete FILLETEDGE workflow and history

This commit is contained in:
ramox81 committed 2026-09-08 12:36:49 +03:00
1 parent febedad916
commit 8bb6703c70
16 files changed
+901 -71

No files matched your search

Generated
+2 -2
View File
@@ -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",
+4 -1
View File
@@ -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]
+11
View File
@@ -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] <Accept>:
.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__
+11
View File
@@ -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] <Kabul>:
.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__
+3 -2
View File
@@ -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();
+60 -5
View File
@@ -17,6 +17,33 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
}
}
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::<Vec<_>>();
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<Task<Message>> {
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));
}
+102 -11
View File
@@ -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::<Vec<_>>();
let Some(history_edges) = edges
.iter()
.map(|edge| {
ordered
.iter()
.position(|candidate| candidate == edge)
.and_then(|ordinal| i32::try_from(ordinal).ok())
})
.collect::<Option<Vec<_>>>()
else {
self.command_line
.push_error(crate::t!("A selected edge no longer belongs to the 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<Message> {
@@ -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());
}
+8
View File
@@ -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);
}
}
+8
View File
@@ -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;
+2 -2
View File
@@ -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<cadkernel::brep::EdgeKey>,
value: f64,
fillet: bool,
},
+11
View File
@@ -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] <Accept>:" => 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] <Object>:" => Some(("model", "specify-start-point-or-object-height-width-justify-object")),
"Specify height <{:.4}>:" => Some(("model", "specify-height")),
+542 -41
View File
@@ -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<Handle>,
picked: Option<(Handle, DVec3)>,
bodies: Vec<(Handle, Body)>,
handle: Option<Handle>,
selected_edges: Vec<EdgeKey>,
selection_batches: Vec<(Vec<EdgeKey>, DVec3)>,
selection_mode: EdgeSelectionMode,
step: EdgeStep,
radius_return: EdgeStep,
loop_candidates: Vec<Vec<EdgeKey>>,
loop_index: usize,
last_pick: Option<DVec3>,
default_value: f64,
preview_color: [f32; 4],
preview_wires: Vec<WireModel>,
preview_hidden: Vec<Handle>,
}
impl SolidEdgeCommand {
pub fn new(operation: EdgeOperation, target: Option<Handle>) -> Self {
pub fn new(
operation: EdgeOperation,
target: Option<Handle>,
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<EdgeKey>, anchor: DVec3) -> bool {
let batch = edges
.into_iter()
.filter(|edge| !self.selected_edges.contains(edge))
.collect::<Vec<_>>();
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<EdgeKey> {
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<WireModel>)> {
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<CmdResult> {
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] <Accept>:").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<CmdOption> {
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<CmdResult> {
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::<f64>().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::<f64>().ok()?;
self.accept_radius(value)
}
EdgeStep::Expression => {
let value = keyword.parse::<f64>().ok()?;
self.accept_radius(value)
}
EdgeStep::ChamferValue => None,
}
let value = text.trim().parse::<f64>().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<CmdResult> {
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<DynSpec> {
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<f64> {
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<WireModel> {
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<Vec<EdgeKey>> {
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::<Vec<_>>();
if !edges.is_empty() && !candidates.contains(&edges) {
candidates.push(edges);
}
}
candidates
}
fn edge_chain(body: &Body, seed: EdgeKey) -> Vec<EdgeKey> {
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<DVec3> {
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"]
});
+1 -1
View File
@@ -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)),
],
},
+2 -2
View File
@@ -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)
+71 -2
View File
@@ -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<GripDef> {
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::<Vec<_>>();
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<i32>, 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()
})
}
+63 -2
View File
@@ -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,