forked from 1b-agent/opencadstudio-upstream
Complete independent CHAMFEREDGE workflow
This commit is contained in:
1 parent
febedad916
commit
4f22d998e7
13 files changed
+1098
-26
No files matched your search
Generated
+2
-2
@@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.5.4"
|
||||
source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=5b56571a190e7a17c8f12d36390d2938b0fb72f7#5b56571a190e7a17c8f12d36390d2938b0fb72f7"
|
||||
source = "git+https://github.com/ramox81/cadcodec.git?rev=5c0c01dcad4543dde2ee8bcd0e08131dfafbf2dc#5c0c01dcad4543dde2ee8bcd0e08131dfafbf2dc"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
@@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4486076cb003a5909e3383900c596f92ef599192#4486076cb003a5909e3383900c596f92ef599192"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=daca8b7392cedd036b85a80499643a6690833011#daca8b7392cedd036b85a80499643a6690833011"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+4
-1
@@ -74,7 +74,10 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa
|
||||
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||
|
||||
[patch."https://github.com/HakanSeven12/cadcodec.git"]
|
||||
acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7" }
|
||||
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "5c0c01dcad4543dde2ee8bcd0e08131dfafbf2dc" }
|
||||
|
||||
[patch."https://github.com/HakanSeven12/cadkernel.git"]
|
||||
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "daca8b7392cedd036b85a80499643a6690833011" }
|
||||
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
||||
@@ -733,6 +733,7 @@ impl OpenCADStudio {
|
||||
&result,
|
||||
CmdResult::Relaunch(..)
|
||||
| CmdResult::Dispatch(..)
|
||||
| CmdResult::SolidChamferEdges { .. }
|
||||
| CmdResult::SolidSubtract { .. }
|
||||
);
|
||||
let task = self.apply_cmd_result_inner(result);
|
||||
@@ -4115,6 +4116,27 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
|
||||
CmdResult::SolidChamferEdges {
|
||||
handle,
|
||||
edges,
|
||||
base_face,
|
||||
distance1,
|
||||
distance2,
|
||||
} => {
|
||||
let task = self.solid_chamfer_edges(
|
||||
handle,
|
||||
&edges,
|
||||
base_face,
|
||||
distance1,
|
||||
distance2,
|
||||
);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
return task;
|
||||
}
|
||||
|
||||
CmdResult::SolidSubtract {
|
||||
bases,
|
||||
cutters,
|
||||
|
||||
+85
-6
@@ -17,6 +17,49 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_chamfer_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 {
|
||||
return None;
|
||||
};
|
||||
matches!(
|
||||
scene.document.get_entity(*handle),
|
||||
Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_))
|
||||
)
|
||||
.then_some(*handle)
|
||||
}
|
||||
|
||||
fn chamfer_edge_sources(
|
||||
app: &mut OpenCADStudio,
|
||||
tab: usize,
|
||||
) -> Vec<(acadrust::Handle, cadkernel::brep::Body)> {
|
||||
let handles = app.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|entity| {
|
||||
matches!(
|
||||
entity,
|
||||
acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_)
|
||||
)
|
||||
.then_some(entity.common().handle)
|
||||
})
|
||||
.collect::<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 {
|
||||
@@ -1107,16 +1150,52 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"SOLIDCHAMFER" => {
|
||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
||||
"CHAMFEREDGE" | "SOLIDCHAMFER" => {
|
||||
use crate::modules::model::chamferedge_cmd::ChamferEdgeCommand;
|
||||
let target = selected_chamfer_body(self, i);
|
||||
let header = &self.tabs[i].scene.document.header;
|
||||
let distances = (header.chamfer_distance_a, header.chamfer_distance_b);
|
||||
let bodies = chamfer_edge_sources(self, i);
|
||||
let command = ChamferEdgeCommand::new(
|
||||
target,
|
||||
bodies,
|
||||
crate::scene::WireModel::SELECTED,
|
||||
distances,
|
||||
);
|
||||
let distances = command.distances();
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"Distance1 = {:.4}, Distance2 = {:.4}",
|
||||
distances.0,
|
||||
distances.1
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"CHAMFER" if selected_solid(self, i).is_some() => {
|
||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
||||
"CHAMFER" if selected_chamfer_body(self, i).is_some() => {
|
||||
use crate::modules::model::chamferedge_cmd::ChamferEdgeCommand;
|
||||
let target = selected_chamfer_body(self, i);
|
||||
let header = &self.tabs[i].scene.document.header;
|
||||
let distances = (header.chamfer_distance_a, header.chamfer_distance_b);
|
||||
let bodies = chamfer_edge_sources(self, i);
|
||||
let command = ChamferEdgeCommand::new(
|
||||
target,
|
||||
bodies,
|
||||
crate::scene::WireModel::SELECTED,
|
||||
distances,
|
||||
);
|
||||
let distances = command.distances();
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"Distance1 = {:.4}, Distance2 = {:.4}",
|
||||
distances.0,
|
||||
distances.1
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
+170
-1
@@ -7,7 +7,7 @@ use acadrust::{
|
||||
objects::SolidHistoryOperation,
|
||||
EntityType, Handle,
|
||||
};
|
||||
use cadkernel::brep::Body;
|
||||
use cadkernel::brep::{Body, EdgeKey, FaceKey};
|
||||
use iced::Task;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -244,6 +244,29 @@ fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option<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,
|
||||
@@ -646,6 +669,152 @@ impl super::OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn replace_edge_body_with_chamfer(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
source: &Body,
|
||||
result: Body,
|
||||
edges: &[EdgeKey],
|
||||
base_face: FaceKey,
|
||||
distance1: f64,
|
||||
distance2: f64,
|
||||
) -> bool {
|
||||
let i = self.active_tab;
|
||||
let Some(display) = self.tabs[i]
|
||||
.scene
|
||||
.prepare_solid_model_display(handle, &result)
|
||||
.filter(|display| display.0.complete)
|
||||
else {
|
||||
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original edge body was retained.").as_ref());
|
||||
return false;
|
||||
};
|
||||
let ordered_edges = source.edge_keys().collect::<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 edge body history could not be created.").as_ref());
|
||||
return false;
|
||||
}
|
||||
if !self.tabs[i].scene.append_solid_history(
|
||||
handle,
|
||||
solid_history::chamfer_op(history_edges, history_face, distance1, distance2),
|
||||
) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The chamfer history could not be recorded.").as_ref());
|
||||
return false;
|
||||
}
|
||||
let _ = self.tabs[i].scene.apply_solid_history_choice(
|
||||
handle,
|
||||
solid_history::PROP_HISTORY,
|
||||
"Record",
|
||||
);
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let Some(entity) = entity_with_history_body(entity, &result) else {
|
||||
return false;
|
||||
};
|
||||
if !self.tabs[i].scene.update_entity(entity) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The edge body could not be updated.").as_ref());
|
||||
return false;
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.register_prepared_solid_model(handle, result, display);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entity(handle, false);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn solid_chamfer_edges(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
edges: &[EdgeKey],
|
||||
base_face: FaceKey,
|
||||
distance1: f64,
|
||||
distance2: f64,
|
||||
) -> Task<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(_) | EntityType::Surface(_))
|
||||
) {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Select a 3D solid or surface edge.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].scene.restore_solid_models(&[handle]);
|
||||
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("The edge body geometry could not be restored.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
let result = match cadkernel::brep::chamfer_edges(
|
||||
&body,
|
||||
edges,
|
||||
base_face,
|
||||
distance1,
|
||||
distance2,
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
self.command_line
|
||||
.push_error(&format!("CHAMFEREDGE: {error}"));
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
if self.replace_edge_body_with_chamfer(
|
||||
handle,
|
||||
&body,
|
||||
result,
|
||||
edges,
|
||||
base_face,
|
||||
distance1,
|
||||
distance2,
|
||||
) {
|
||||
self.tabs[i].scene.document.header.chamfer_distance_a = distance1;
|
||||
self.tabs[i].scene.document.header.chamfer_distance_b = distance2;
|
||||
self.command_line
|
||||
.push_output(crate::t!("CHAMFEREDGE: solid updated.").as_ref());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Intersect compatible selected solids, Regions, and coplanar Surfaces.
|
||||
pub(super) fn solid_intersect(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1554,6 +1554,14 @@ pub enum CmdResult {
|
||||
value: f64,
|
||||
fillet: bool,
|
||||
},
|
||||
/// Chamfer one or more resolved B-rep edges on a common base face.
|
||||
SolidChamferEdges {
|
||||
handle: Handle,
|
||||
edges: Vec<cadkernel::brep::EdgeKey>,
|
||||
base_face: cadkernel::brep::FaceKey,
|
||||
distance1: f64,
|
||||
distance2: f64,
|
||||
},
|
||||
SolidSubtract {
|
||||
bases: Vec<Handle>,
|
||||
cutters: Vec<Handle>,
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
use acadrust::Handle;
|
||||
use cadkernel::brep::{Body, EdgeKey, FaceKey};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{
|
||||
CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec,
|
||||
};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum ChamferStep {
|
||||
Selecting,
|
||||
PickingLoop,
|
||||
LoopConfirm,
|
||||
PreviewConfirm,
|
||||
Distance1,
|
||||
Distance2,
|
||||
Expression,
|
||||
}
|
||||
|
||||
pub struct ChamferEdgeCommand {
|
||||
target: Option<Handle>,
|
||||
bodies: Vec<(Handle, Body)>,
|
||||
handle: Option<Handle>,
|
||||
selected_edges: Vec<EdgeKey>,
|
||||
base_face: Option<FaceKey>,
|
||||
selection_batches: Vec<(Vec<EdgeKey>, DVec3)>,
|
||||
step: ChamferStep,
|
||||
value_return: ChamferStep,
|
||||
expression_return: ChamferStep,
|
||||
loop_candidates: Vec<(FaceKey, Vec<EdgeKey>)>,
|
||||
loop_index: usize,
|
||||
last_pick: Option<DVec3>,
|
||||
distance1: f64,
|
||||
distance2: f64,
|
||||
preview_color: [f32; 4],
|
||||
preview_wires: Vec<WireModel>,
|
||||
preview_hidden: Vec<Handle>,
|
||||
}
|
||||
|
||||
impl ChamferEdgeCommand {
|
||||
pub fn new(
|
||||
target: Option<Handle>,
|
||||
bodies: Vec<(Handle, Body)>,
|
||||
preview_color: [f32; 4],
|
||||
distances: (f64, f64),
|
||||
) -> Self {
|
||||
Self {
|
||||
target,
|
||||
bodies,
|
||||
handle: None,
|
||||
selected_edges: Vec::new(),
|
||||
base_face: None,
|
||||
selection_batches: Vec::new(),
|
||||
step: ChamferStep::Selecting,
|
||||
value_return: ChamferStep::Selecting,
|
||||
expression_return: ChamferStep::Distance1,
|
||||
loop_candidates: Vec::new(),
|
||||
loop_index: 0,
|
||||
last_pick: None,
|
||||
distance1: positive_default(distances.0),
|
||||
distance2: positive_default(distances.1),
|
||||
preview_color,
|
||||
preview_wires: Vec::new(),
|
||||
preview_hidden: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn distances(&self) -> (f64, f64) {
|
||||
(self.distance1, self.distance2)
|
||||
}
|
||||
|
||||
fn body(&self, handle: Handle) -> Option<&Body> {
|
||||
self.bodies
|
||||
.iter()
|
||||
.find_map(|(candidate, body)| (*candidate == handle).then_some(body))
|
||||
}
|
||||
|
||||
fn active_body(&self) -> Option<(Handle, &Body)> {
|
||||
let handle = self.handle?;
|
||||
self.body(handle).map(|body| (handle, body))
|
||||
}
|
||||
|
||||
fn pick_allowed(&self, handle: Handle) -> bool {
|
||||
!handle.is_null()
|
||||
&& self.target.is_none_or(|target| target == handle)
|
||||
&& self.handle.is_none_or(|selected| selected == handle)
|
||||
&& self.body(handle).is_some()
|
||||
}
|
||||
|
||||
fn add_batch(&mut self, edges: Vec<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<&(FaceKey, Vec<EdgeKey>)> {
|
||||
self.loop_candidates.get(self.loop_index)
|
||||
}
|
||||
|
||||
fn preview_edges(&self) -> Vec<EdgeKey> {
|
||||
let mut edges = self.selected_edges.clone();
|
||||
if self.step == ChamferStep::LoopConfirm {
|
||||
if let Some((_, loop_edges)) = self.current_loop() {
|
||||
for edge in loop_edges {
|
||||
if !edges.contains(edge) {
|
||||
edges.push(*edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
fn preview_for_values(
|
||||
&self,
|
||||
distance1: f64,
|
||||
distance2: f64,
|
||||
) -> Option<(Handle, Vec<WireModel>)> {
|
||||
let edges = self.preview_edges();
|
||||
if edges.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (handle, body) = self.active_body()?;
|
||||
let base_face = self.base_face.or_else(|| self.current_loop().map(|item| item.0))?;
|
||||
let result =
|
||||
cadkernel::brep::chamfer_edges(body, &edges, base_face, distance1, distance2).ok()?;
|
||||
let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle);
|
||||
for wire in &mut wires {
|
||||
wire.color = self.preview_color;
|
||||
wire.name = format!("{}-CHAMFEREDGE-PREVIEW", handle.value());
|
||||
}
|
||||
(!wires.is_empty()).then_some((handle, wires))
|
||||
}
|
||||
|
||||
fn rebuild_preview(&mut self) {
|
||||
self.preview_wires.clear();
|
||||
self.preview_hidden.clear();
|
||||
if let Some((handle, wires)) = self.preview_for_values(self.distance1, self.distance2) {
|
||||
self.preview_wires = wires;
|
||||
self.preview_hidden.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_distances(&mut self, return_to: ChamferStep) -> CmdResult {
|
||||
self.value_return = return_to;
|
||||
self.step = ChamferStep::Distance1;
|
||||
self.rebuild_preview();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn accept_distance(&mut self, value: f64) -> Option<CmdResult> {
|
||||
if value <= 0.0 || !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
match self.step {
|
||||
ChamferStep::Distance1 => {
|
||||
self.distance1 = value;
|
||||
self.step = ChamferStep::Distance2;
|
||||
}
|
||||
ChamferStep::Distance2 => {
|
||||
self.distance2 = value;
|
||||
self.step = self.value_return;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
self.rebuild_preview();
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn accept_loop(&mut self) -> CmdResult {
|
||||
if let (Some((face, edges)), Some(anchor)) = (self.current_loop().cloned(), self.last_pick) {
|
||||
if self.base_face.is_none() {
|
||||
self.base_face = Some(face);
|
||||
}
|
||||
self.add_batch(edges, anchor);
|
||||
}
|
||||
self.loop_candidates.clear();
|
||||
self.loop_index = 0;
|
||||
self.step = ChamferStep::Selecting;
|
||||
self.rebuild_preview();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn finish(&self) -> CmdResult {
|
||||
let (Some(handle), Some(base_face)) = (self.handle, self.base_face) else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
if self.selected_edges.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
CmdResult::SolidChamferEdges {
|
||||
handle,
|
||||
edges: self.selected_edges.clone(),
|
||||
base_face,
|
||||
distance1: self.distance1,
|
||||
distance2: self.distance2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ChamferEdgeCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"CHAMFEREDGE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
ChamferStep::Selecting => crate::t!("Select an edge or [Loop/Distance]:").into_owned(),
|
||||
ChamferStep::PickingLoop => {
|
||||
crate::t!("Select edge of loop or [Edge/Distance]:").into_owned()
|
||||
}
|
||||
ChamferStep::LoopConfirm => {
|
||||
crate::t!("Enter an option [Accept/Next] <Accept>:").into_owned()
|
||||
}
|
||||
ChamferStep::PreviewConfirm => {
|
||||
crate::t!("Press Enter to accept the chamfer or [Distance]:").into_owned()
|
||||
}
|
||||
ChamferStep::Distance1 => {
|
||||
crate::tf!("Specify Distance1 or [Expression] <{:.4}>:", self.distance1)
|
||||
.into_owned()
|
||||
}
|
||||
ChamferStep::Distance2 => {
|
||||
crate::tf!("Specify Distance2 or [Expression] <{:.4}>:", self.distance2)
|
||||
.into_owned()
|
||||
}
|
||||
ChamferStep::Expression => crate::t!("Enter expression:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
ChamferStep::Selecting => vec![
|
||||
CmdOption::new("Loop", "L"),
|
||||
CmdOption::new("Distance", "D"),
|
||||
],
|
||||
ChamferStep::PickingLoop => vec![
|
||||
CmdOption::new("Edge", "E"),
|
||||
CmdOption::new("Distance", "D"),
|
||||
],
|
||||
ChamferStep::LoopConfirm => vec![
|
||||
CmdOption::new("Accept", "A"),
|
||||
CmdOption::new("Next", "N"),
|
||||
],
|
||||
ChamferStep::PreviewConfirm => vec![CmdOption::new("Distance", "D")],
|
||||
ChamferStep::Distance1 | ChamferStep::Distance2 => {
|
||||
vec![CmdOption::new("Expression", "E")]
|
||||
}
|
||||
ChamferStep::Expression => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, ChamferStep::Selecting | ChamferStep::PickingLoop)
|
||||
}
|
||||
|
||||
fn entity_pick_includes_fills(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||
if !self.pick_allowed(handle) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let Some(seed) = self
|
||||
.body(handle)
|
||||
.and_then(|body| crate::scene::model::solid_model::nearest_edge(body, point.to_array()))
|
||||
else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.handle = Some(handle);
|
||||
self.last_pick = Some(point);
|
||||
|
||||
if self.step == ChamferStep::PickingLoop {
|
||||
let mut candidates = edge_loops(self.body(handle).expect("pick body exists"), seed);
|
||||
if let Some(base_face) = self.base_face {
|
||||
candidates.retain(|(face, _)| *face == base_face);
|
||||
}
|
||||
self.loop_candidates = candidates;
|
||||
self.loop_index = 0;
|
||||
self.step = if self.loop_candidates.is_empty() {
|
||||
ChamferStep::Selecting
|
||||
} else {
|
||||
ChamferStep::LoopConfirm
|
||||
};
|
||||
self.rebuild_preview();
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
if self.base_face.is_none() {
|
||||
self.base_face = self
|
||||
.body(handle)
|
||||
.and_then(|body| nearest_edge_face(body, seed, point.to_array()));
|
||||
}
|
||||
let Some(base_face) = self.base_face else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if !edge_belongs_to_face(self.body(handle).expect("pick body exists"), seed, base_face) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.add_batch(vec![seed], point);
|
||||
self.rebuild_preview();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if !matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let Some(anchor) = self.last_pick else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.accept_distance(point.distance(anchor))
|
||||
.unwrap_or(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim().to_ascii_uppercase();
|
||||
match self.step {
|
||||
ChamferStep::Selecting => match keyword.as_str() {
|
||||
"L" | "LOOP" => {
|
||||
self.step = ChamferStep::PickingLoop;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"D" | "DISTANCE" => Some(self.begin_distances(ChamferStep::Selecting)),
|
||||
_ => None,
|
||||
},
|
||||
ChamferStep::PickingLoop => match keyword.as_str() {
|
||||
"E" | "EDGE" => {
|
||||
self.step = ChamferStep::Selecting;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"D" | "DISTANCE" => Some(self.begin_distances(ChamferStep::PickingLoop)),
|
||||
_ => None,
|
||||
},
|
||||
ChamferStep::LoopConfirm => match keyword.as_str() {
|
||||
"A" | "ACCEPT" => Some(self.accept_loop()),
|
||||
"N" | "NEXT" if !self.loop_candidates.is_empty() => {
|
||||
self.loop_index = (self.loop_index + 1) % self.loop_candidates.len();
|
||||
self.rebuild_preview();
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
ChamferStep::PreviewConfirm => match keyword.as_str() {
|
||||
"D" | "DISTANCE" => Some(self.begin_distances(ChamferStep::PreviewConfirm)),
|
||||
_ => None,
|
||||
},
|
||||
ChamferStep::Distance1 | ChamferStep::Distance2 => {
|
||||
if matches!(keyword.as_str(), "E" | "EXPRESSION") {
|
||||
self.expression_return = self.step;
|
||||
self.step = ChamferStep::Expression;
|
||||
Some(CmdResult::NeedPoint)
|
||||
} else {
|
||||
self.accept_distance(keyword.parse::<f64>().ok()?)
|
||||
}
|
||||
}
|
||||
ChamferStep::Expression => {
|
||||
let value = crate::app::expr_eval::eval_number(text)?;
|
||||
self.step = self.expression_return;
|
||||
self.accept_distance(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
ChamferStep::Selecting if !self.selected_edges.is_empty() => {
|
||||
self.step = ChamferStep::PreviewConfirm;
|
||||
self.rebuild_preview();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ChamferStep::LoopConfirm => self.accept_loop(),
|
||||
ChamferStep::PreviewConfirm => self.finish(),
|
||||
ChamferStep::Distance1 => self
|
||||
.accept_distance(self.distance1)
|
||||
.unwrap_or(CmdResult::NeedPoint),
|
||||
ChamferStep::Distance2 => self
|
||||
.accept_distance(self.distance2)
|
||||
.unwrap_or(CmdResult::NeedPoint),
|
||||
ChamferStep::Expression => CmdResult::NeedPoint,
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if self.step == ChamferStep::LoopConfirm {
|
||||
self.loop_candidates.clear();
|
||||
self.loop_index = 0;
|
||||
self.step = ChamferStep::Selecting;
|
||||
self.rebuild_preview();
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
let (batch, _) = self.selection_batches.pop()?;
|
||||
self.selected_edges.retain(|edge| !batch.contains(edge));
|
||||
if self.selected_edges.is_empty() {
|
||||
self.handle = None;
|
||||
self.base_face = None;
|
||||
}
|
||||
self.last_pick = self.selection_batches.last().map(|(_, anchor)| *anchor);
|
||||
self.step = ChamferStep::Selecting;
|
||||
self.rebuild_preview();
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
ChamferStep::Distance1 | ChamferStep::Distance2 | ChamferStep::Expression
|
||||
)
|
||||
}
|
||||
|
||||
fn dyn_commit_as_text(&self) -> bool {
|
||||
matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2)
|
||||
}
|
||||
|
||||
fn dyn_spec(&self) -> Option<DynSpec> {
|
||||
let pick = self.last_pick?;
|
||||
matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2).then(|| DynSpec {
|
||||
anchor: DynAnchor::Point(pick),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Distance)],
|
||||
guide: DynGuide::Radius,
|
||||
ref_point: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2)
|
||||
.then(|| self.last_pick.map(|pick| cursor.distance(pick)))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, cursor: DVec3) -> Vec<WireModel> {
|
||||
if matches!(self.step, ChamferStep::Distance1 | ChamferStep::Distance2) {
|
||||
if let Some(value) = self.last_pick.map(|pick| cursor.distance(pick)) {
|
||||
if value > 0.0 && value.is_finite() {
|
||||
let values = if self.step == ChamferStep::Distance2 {
|
||||
(self.distance1, value)
|
||||
} else {
|
||||
(value, self.distance2)
|
||||
};
|
||||
if let Some((handle, wires)) = self.preview_for_values(values.0, values.1) {
|
||||
self.preview_wires = wires;
|
||||
self.preview_hidden = vec![handle];
|
||||
} else {
|
||||
self.preview_wires.clear();
|
||||
self.preview_hidden.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.preview_wires.clone()
|
||||
}
|
||||
|
||||
fn preview_hidden_handles(&self) -> &[Handle] {
|
||||
&self.preview_hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn edge_loops(body: &Body, seed: EdgeKey) -> Vec<(FaceKey, Vec<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<_>>();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["CHAMFEREDGE"]
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// Solid creation and kernel modelling tools.
|
||||
|
||||
pub mod boolean_cmd;
|
||||
pub mod chamferedge_cmd;
|
||||
pub mod cylinder_cmd;
|
||||
pub mod edge_cmd;
|
||||
pub mod polysolid_cmd;
|
||||
@@ -89,7 +90,7 @@ impl CadModule for ModelModule {
|
||||
title: "Edges",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)),
|
||||
RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)),
|
||||
RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use acadrust::entities::{EmbeddedEntity, Solid3D};
|
||||
use acadrust::objects::{
|
||||
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
|
||||
SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation,
|
||||
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryChamfer,
|
||||
SolidHistoryCone, SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters,
|
||||
SolidHistoryNodeBase, SolidHistoryOperation,
|
||||
SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep,
|
||||
SolidHistoryTorus,
|
||||
};
|
||||
use acadrust::EntityType;
|
||||
use cadkernel::brep::Body;
|
||||
use cadkernel::brep::{Body, Surface as KernelSurface};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
@@ -32,6 +33,8 @@ pub const GRIP_SWEEP_PATH_FIRST: usize = 30_000;
|
||||
pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000;
|
||||
pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000;
|
||||
pub const GRIP_REVOLVE_AXIS: usize = 10_013;
|
||||
pub const GRIP_CHAMFER_DISTANCE1: usize = 10_015;
|
||||
pub const GRIP_CHAMFER_DISTANCE2: usize = 10_016;
|
||||
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
|
||||
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
|
||||
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
||||
@@ -197,7 +200,7 @@ pub fn has_specialized_primitive_properties(
|
||||
handle: acadrust::Handle,
|
||||
) -> bool {
|
||||
matches!(
|
||||
document.solid_history_operation(handle),
|
||||
primitive_property_operation(document, handle).as_ref(),
|
||||
Some(
|
||||
SolidHistoryOperation::Box(_)
|
||||
| SolidHistoryOperation::Wedge(_)
|
||||
@@ -214,6 +217,18 @@ pub fn has_specialized_primitive_properties(
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the primitive that owns the public Geometry rows. Later edge
|
||||
/// operations refine it without replacing its editable type and dimensions.
|
||||
pub fn primitive_property_operation(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
) -> Option<SolidHistoryOperation> {
|
||||
document
|
||||
.solid_history_operations(handle)
|
||||
.and_then(|operations| operations.into_iter().next())
|
||||
.or_else(|| document.solid_history_operation(handle).cloned())
|
||||
}
|
||||
|
||||
/// Solid history results whose public Properties palette is fully described by
|
||||
/// the common entity rows plus [`primitive_properties`]. A generic B-rep has
|
||||
/// no stable primitive dimensions to expose, but it still uses the compact
|
||||
@@ -1438,10 +1453,10 @@ pub fn primitive_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
) -> Vec<PropSection> {
|
||||
let Some(operation) = document.solid_history_operation(handle) else {
|
||||
let Some(operation) = primitive_property_operation(document, handle) else {
|
||||
return brep_properties(document, handle);
|
||||
};
|
||||
match operation {
|
||||
match &operation {
|
||||
SolidHistoryOperation::Box(value) => {
|
||||
rectangular_properties(document, handle, value, "Box")
|
||||
}
|
||||
@@ -3086,6 +3101,96 @@ fn grip(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chamfer_distance_grips(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
value: &SolidHistoryChamfer,
|
||||
) -> Vec<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 KernelSurface::Plane(plane) = source.surfaces.get(face.surface)? else {
|
||||
return None;
|
||||
};
|
||||
let normal = glam::DVec3::from_array(plane.normal()?).try_normalize()?;
|
||||
Some(if face.forward { normal } else { -normal })
|
||||
};
|
||||
let Some(base_normal) = outward(base_face) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(other_normal) = outward(other_face) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let dot = base_normal.dot(other_normal);
|
||||
let Some(base_inward) = (-(other_normal - base_normal * dot)).try_normalize() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(other_inward) = (-(base_normal - other_normal * dot)).try_normalize() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(start) = source.vertices.get(edge.start) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(end) = source.vertices.get(edge.end) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let midpoint =
|
||||
(glam::DVec3::from_array(start.point) + glam::DVec3::from_array(end.point)) * 0.5;
|
||||
[
|
||||
(GRIP_CHAMFER_DISTANCE1, value.base_distance, base_inward),
|
||||
(GRIP_CHAMFER_DISTANCE2, value.other_distance, other_inward),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(_, distance, _)| distance.is_finite() && *distance > 0.0)
|
||||
.map(|(id, distance, axis)| {
|
||||
grip(
|
||||
id,
|
||||
midpoint + axis * distance,
|
||||
GripShape::Square,
|
||||
Some(axis),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn primitive_grips(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -3093,6 +3198,9 @@ pub fn primitive_grips(
|
||||
let Some(operation) = document.solid_history_operation(handle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if let SolidHistoryOperation::Chamfer(value) = operation {
|
||||
return chamfer_distance_grips(document, handle, value);
|
||||
}
|
||||
let mut grips = Vec::new();
|
||||
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
|
||||
if let Some(world) = world_point(transform, point) {
|
||||
@@ -3713,3 +3821,21 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
||||
..SolidHistoryBrep::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn chamfer_op(
|
||||
edges: Vec<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()
|
||||
})
|
||||
}
|
||||
+119
-7
@@ -802,11 +802,39 @@ impl Scene {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn append_solid_history(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
operation: acadrust::objects::SolidHistoryOperation,
|
||||
) -> bool {
|
||||
let previous = self
|
||||
.document
|
||||
.solid_history_graph(handle)
|
||||
.map(|graph| graph.nodes)
|
||||
.unwrap_or_default();
|
||||
self.record_solid_history_before(handle);
|
||||
let Some(graph) = self.document.append_solid_history(handle, operation) else {
|
||||
return false;
|
||||
};
|
||||
for node in graph.nodes {
|
||||
if !previous.contains(&node) {
|
||||
self.record_undo_object_before(node, None);
|
||||
}
|
||||
}
|
||||
self.sync_solid_reference_point(handle);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) {
|
||||
let reference = self
|
||||
.document
|
||||
.solid_history_operation(handle)
|
||||
.and_then(crate::scene::model::solid_history::reference_point);
|
||||
.solid_history_operations(handle)
|
||||
.and_then(|operations| {
|
||||
operations
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(crate::scene::model::solid_history::reference_point)
|
||||
});
|
||||
let Some(reference) = reference else {
|
||||
return;
|
||||
};
|
||||
@@ -851,7 +879,34 @@ impl Scene {
|
||||
) -> Option<cadkernel::brep::Body> {
|
||||
use acadrust::objects::SolidHistoryOperation;
|
||||
|
||||
match (self.document.get_entity(handle)?, operation) {
|
||||
let entity = self.document.get_entity(handle)?;
|
||||
if matches!(entity, EntityType::Solid3D(_) | EntityType::Surface(_)) {
|
||||
let mut operations = self.document.solid_history_operations(handle)?;
|
||||
if operations.len() > 1
|
||||
|| matches!(operation, SolidHistoryOperation::Chamfer(_))
|
||||
{
|
||||
let replacement_id = operation.base().map(|base| {
|
||||
if base.eval.node_id > 0 {
|
||||
base.eval.node_id
|
||||
} else {
|
||||
base.step_id
|
||||
}
|
||||
})?;
|
||||
let target = operations.iter_mut().find(|candidate| {
|
||||
candidate.base().is_some_and(|base| {
|
||||
let node_id = if base.eval.node_id > 0 {
|
||||
base.eval.node_id
|
||||
} else {
|
||||
base.step_id
|
||||
};
|
||||
node_id == replacement_id
|
||||
})
|
||||
})?;
|
||||
*target = operation.clone();
|
||||
return cadkernel::acis::rebuild_history(&operations).ok();
|
||||
}
|
||||
}
|
||||
match (entity, operation) {
|
||||
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
|
||||
cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok()
|
||||
}
|
||||
@@ -868,6 +923,17 @@ impl Scene {
|
||||
) -> Option<Option<acadrust::entities::SurfaceData>> {
|
||||
use acadrust::objects::SolidHistoryOperation;
|
||||
|
||||
if self
|
||||
.document
|
||||
.solid_history_operations(handle)
|
||||
.is_some_and(|operations| operations.len() > 1)
|
||||
{
|
||||
return matches!(
|
||||
self.document.get_entity(handle),
|
||||
Some(EntityType::Solid3D(_) | EntityType::Surface(_))
|
||||
)
|
||||
.then_some(None);
|
||||
}
|
||||
match (self.document.get_entity(handle)?, operation) {
|
||||
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
|
||||
crate::scene::model::solid_history::extrusion_surface_data(value).map(Some)
|
||||
@@ -909,7 +975,7 @@ impl Scene {
|
||||
self.record_solid_history_before(handle);
|
||||
if self
|
||||
.document
|
||||
.update_solid_history(handle, operation)
|
||||
.update_solid_history_step(handle, operation)
|
||||
.is_none()
|
||||
{
|
||||
return false;
|
||||
@@ -985,7 +1051,7 @@ impl Scene {
|
||||
}
|
||||
if self
|
||||
.document
|
||||
.update_solid_history(handle, operation)
|
||||
.update_solid_history_step(handle, operation)
|
||||
.is_none()
|
||||
{
|
||||
return false;
|
||||
@@ -1023,7 +1089,7 @@ impl Scene {
|
||||
};
|
||||
if self
|
||||
.document
|
||||
.update_solid_history(handle, operation)
|
||||
.update_solid_history_step(handle, operation)
|
||||
.is_none()
|
||||
{
|
||||
return false;
|
||||
@@ -1122,6 +1188,47 @@ impl Scene {
|
||||
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
|
||||
return false;
|
||||
};
|
||||
if matches!(
|
||||
grip_id,
|
||||
crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
|
||||
| crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE2
|
||||
) {
|
||||
let definitions = {
|
||||
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &operation else {
|
||||
return false;
|
||||
};
|
||||
crate::scene::model::solid_history::chamfer_distance_grips(
|
||||
&self.document,
|
||||
handle,
|
||||
value,
|
||||
)
|
||||
};
|
||||
let Some(definition) = definitions.into_iter().find(|grip| grip.id == grip_id) else {
|
||||
return false;
|
||||
};
|
||||
let Some(axis) = definition.axis else {
|
||||
return false;
|
||||
};
|
||||
let change = match apply {
|
||||
GripApply::Absolute(world) => (world - definition.world).dot(axis),
|
||||
GripApply::Translate(delta) => delta.dot(axis),
|
||||
};
|
||||
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else {
|
||||
return false;
|
||||
};
|
||||
let distance = if grip_id
|
||||
== crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
|
||||
{
|
||||
&mut value.base_distance
|
||||
} else {
|
||||
&mut value.other_distance
|
||||
};
|
||||
*distance += change;
|
||||
if !distance.is_finite() || *distance <= 1.0e-6 {
|
||||
return false;
|
||||
}
|
||||
return self.preview_solid_history(handle, operation);
|
||||
}
|
||||
if !crate::scene::model::solid_history::apply_primitive_grip(
|
||||
&mut operation,
|
||||
grip_id,
|
||||
@@ -1138,7 +1245,12 @@ impl Scene {
|
||||
field: &str,
|
||||
value: &str,
|
||||
) -> bool {
|
||||
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
|
||||
let Some(mut operation) =
|
||||
crate::scene::model::solid_history::primitive_property_operation(
|
||||
&self.document,
|
||||
handle,
|
||||
)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !crate::scene::model::solid_history::apply_primitive_property(
|
||||
|
||||
Reference in new issue
Block a user