forked from 1b-agent/opencadstudio-upstream
Merge pull request #1070 from ramox81/codex/filletedge-parity
# Conflicts: # Cargo.lock
This commit is contained in:
commit
7ec07f3222
21 files changed
+1544
-138
No files matched your search
Generated
+2
-2
@@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=5b56571a190e7a17c8f12d36390d2938b0fb72f7#5b56571a190e7a17c8f12d36390d2938b0fb72f7"
|
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=36152bc31e2df0136335e15b54e51c3c19349c63#36152bc31e2df0136335e15b54e51c3c19349c63"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=f041b46431ac1e6fb38ae61d76bd7c2814b26164#f041b46431ac1e6fb38ae61d76bd7c2814b26164"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=37bdf263d85261982f6014b89262af2bf6eff144#37bdf263d85261982f6014b89262af2bf6eff144"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+2
-6
@@ -31,8 +31,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive", "string"] }
|
clap = { version = "4", features = ["derive", "string"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "36152bc31e2df0136335e15b54e51c3c19349c63", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "f041b46431ac1e6fb38ae61d76bd7c2814b26164", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "37bdf263d85261982f6014b89262af2bf6eff144", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
@@ -73,10 +73,6 @@ objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString"
|
|||||||
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||||
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
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" }
|
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
getrandom = "0.3"
|
getrandom = "0.3"
|
||||||
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }
|
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ bincode = "1.3"
|
|||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b56571a190e7a17c8f12d36390d2938b0fb72f7", features = ["serde"], optional = true }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "36152bc31e2df0136335e15b54e51c3c19349c63", features = ["serde"], optional = true }
|
||||||
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "f041b46431ac1e6fb38ae61d76bd7c2814b26164", features = ["acis", "offset"], optional = true }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "37bdf263d85261982f6014b89262af2bf6eff144", features = ["acis", "offset"], optional = true }
|
||||||
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -3220,6 +3220,17 @@ model =
|
|||||||
.cylinder-specify-axis-endpoint = CYLINDER Specify axis endpoint:
|
.cylinder-specify-axis-endpoint = CYLINDER Specify axis endpoint:
|
||||||
.elliptical = Elliptical
|
.elliptical = Elliptical
|
||||||
.select-a-solid-edge = Select a solid edge:
|
.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-fillet-radius = Specify fillet radius <__ocs_fmt_0__>:
|
||||||
.specify-chamfer-distance = Specify chamfer distance <__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__
|
.polysolid-height-width-justification = POLYSOLID Height = __ocs_fmt_0__, Width = __ocs_fmt_1__, Justification = __ocs_fmt_2__
|
||||||
|
|||||||
@@ -3179,6 +3179,17 @@ model =
|
|||||||
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
|
.cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }:
|
||||||
.elliptical = Eliptik
|
.elliptical = Eliptik
|
||||||
.select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }:
|
.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-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>:
|
||||||
.specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__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__
|
.polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__
|
||||||
|
|||||||
@@ -733,6 +733,7 @@ impl OpenCADStudio {
|
|||||||
&result,
|
&result,
|
||||||
CmdResult::Relaunch(..)
|
CmdResult::Relaunch(..)
|
||||||
| CmdResult::Dispatch(..)
|
| CmdResult::Dispatch(..)
|
||||||
|
| CmdResult::SolidEdgeBlend { .. }
|
||||||
| CmdResult::SolidSubtract { .. }
|
| CmdResult::SolidSubtract { .. }
|
||||||
| CmdResult::SliceEntities { .. }
|
| CmdResult::SliceEntities { .. }
|
||||||
| CmdResult::SliceSurfaceEntities { .. }
|
| CmdResult::SliceSurfaceEntities { .. }
|
||||||
@@ -4105,11 +4106,20 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
CmdResult::SolidEdgeBlend {
|
CmdResult::SolidEdgeBlend {
|
||||||
handle,
|
handle,
|
||||||
pick,
|
edges,
|
||||||
|
base_face,
|
||||||
value,
|
value,
|
||||||
|
other_value,
|
||||||
fillet,
|
fillet,
|
||||||
} => {
|
} => {
|
||||||
let task = self.solid_edge_blend(handle, pick, 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].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
|||||||
+92
-12
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
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 scene = &app.tabs.get(tab)?.scene;
|
||||||
let selected = scene.selected_handles_in_order();
|
let selected = scene.selected_handles_in_order();
|
||||||
let [handle] = selected.as_slice() else {
|
let [handle] = selected.as_slice() else {
|
||||||
@@ -9,7 +9,7 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
|||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
scene.document.get_entity(*handle),
|
scene.document.get_entity(*handle),
|
||||||
Some(acadrust::EntityType::Solid3D(_))
|
Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_))
|
||||||
) {
|
) {
|
||||||
Some(*handle)
|
Some(*handle)
|
||||||
} else {
|
} else {
|
||||||
@@ -17,6 +17,36 @@ 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(_) | 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 {
|
impl OpenCADStudio {
|
||||||
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
match cmd {
|
match cmd {
|
||||||
@@ -960,16 +990,30 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
"SOLIDFILLET" => {
|
"FILLETEDGE" | "SOLIDFILLET" => {
|
||||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||||
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
|
let target = selected_edge_body(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.command_line.push_info(&command.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
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};
|
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||||
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
|
let target = selected_edge_body(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.command_line.push_info(&command.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
}
|
}
|
||||||
@@ -1107,16 +1151,52 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"SOLIDCHAMFER" => {
|
"CHAMFEREDGE" | "SOLIDCHAMFER" => {
|
||||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
use crate::modules::model::edge_cmd::SolidEdgeCommand;
|
||||||
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
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_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.command_line.push_info(&command.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
}
|
}
|
||||||
|
|
||||||
"CHAMFER" if selected_solid(self, i).is_some() => {
|
"CHAMFER" if selected_edge_body(self, i).is_some() => {
|
||||||
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
use crate::modules::model::edge_cmd::SolidEdgeCommand;
|
||||||
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
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_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.command_line.push_info(&command.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,9 +335,9 @@ impl OpenCADStudio {
|
|||||||
"PERSP" => return Some(Task::done(Message::SetProjection(false))),
|
"PERSP" => return Some(Task::done(Message::SetProjection(false))),
|
||||||
"LAYERS" => return Some(Task::done(Message::ToggleLayers)),
|
"LAYERS" => return Some(Task::done(Message::ToggleLayers)),
|
||||||
|
|
||||||
// SCRIPT <path> — run a command script: each non-blank, non-comment
|
// SCRIPT <path> — run a command script: each non-comment
|
||||||
// line is fed through the same command path the `--script` startup
|
// line is fed through the same command path the `--script` startup
|
||||||
// flag uses, so the behaviour matches headless automation exactly.
|
// flag uses. Blank rows submit Enter to the active command.
|
||||||
"SCRIPT" | "SCR" => {
|
"SCRIPT" | "SCR" => {
|
||||||
use crate::command::ValuePromptCommand;
|
use crate::command::ValuePromptCommand;
|
||||||
let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:");
|
let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:");
|
||||||
@@ -352,10 +352,8 @@ impl OpenCADStudio {
|
|||||||
let cmds: Vec<Task<Message>> = text
|
let cmds: Vec<Task<Message>> = text
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|l| {
|
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
|
||||||
!l.is_empty() && !l.starts_with('#') && !l.starts_with(';')
|
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
|
||||||
})
|
|
||||||
.map(|l| Task::done(Message::Command(l.to_string())))
|
|
||||||
.collect();
|
.collect();
|
||||||
self.command_line.push_output(crate::tf!(
|
self.command_line.push_output(crate::tf!(
|
||||||
"SCRIPT: running {} command(s) from {p}.",
|
"SCRIPT: running {} command(s) from {p}.",
|
||||||
|
|||||||
+5
-1
@@ -2117,6 +2117,10 @@ pub enum Message {
|
|||||||
CommandInput(String),
|
CommandInput(String),
|
||||||
CommandSubmit,
|
CommandSubmit,
|
||||||
Command(String),
|
Command(String),
|
||||||
|
/// Execute one complete line read from a command script. Unlike UI/ribbon
|
||||||
|
/// dispatch, this accepts an interactive verb and all of its arguments on
|
||||||
|
/// the same line (`BOX 0,0,0 10,10,0 10`).
|
||||||
|
ScriptLine(String),
|
||||||
/// Append one typed character to the command-line input from the
|
/// Append one typed character to the command-line input from the
|
||||||
/// global key-press subscription. Used when the text-input widget
|
/// global key-press subscription. Used when the text-input widget
|
||||||
/// itself isn't focused (focus parked on viewport / button / etc.)
|
/// itself isn't focused (focus parked on viewport / button / etc.)
|
||||||
@@ -3832,7 +3836,7 @@ impl OpenCADStudio {
|
|||||||
Task::batch(
|
Task::batch(
|
||||||
cfg.script_lines
|
cfg.script_lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|line| Task::done(Message::Command(line))),
|
.map(|line| Task::done(Message::ScriptLine(line))),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
s.queue_startup_prompts();
|
s.queue_startup_prompts();
|
||||||
|
|||||||
+231
-45
@@ -7,7 +7,7 @@ use acadrust::{
|
|||||||
objects::SolidHistoryOperation,
|
objects::SolidHistoryOperation,
|
||||||
EntityType, Handle,
|
EntityType, Handle,
|
||||||
};
|
};
|
||||||
use cadkernel::brep::Body;
|
use cadkernel::brep::{Body, EdgeKey, FaceKey};
|
||||||
use iced::Task;
|
use iced::Task;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -273,6 +273,29 @@ fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option<Entit
|
|||||||
Some(source)
|
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 {
|
enum IntersectBodyOutcome {
|
||||||
Area(Body),
|
Area(Body),
|
||||||
Touching,
|
Touching,
|
||||||
@@ -580,44 +603,169 @@ impl super::OpenCADStudio {
|
|||||||
groups
|
groups
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool {
|
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 i = self.active_tab;
|
||||||
let Some(document) =
|
let Some(display) = self.tabs[i]
|
||||||
crate::scene::convert::acis_export::solid_to_sat(&result)
|
.scene
|
||||||
else {
|
.prepare_solid_model_display(handle, &result)
|
||||||
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)
|
.filter(|display| display.0.complete)
|
||||||
else {
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
self.push_undo_snapshot(i, label);
|
let ordered = source.edge_keys().collect::<Vec<_>>();
|
||||||
self.tabs[i].scene.delete_solid_history(handle);
|
let Some(history_edges) = edges
|
||||||
if !self.tabs[i]
|
.iter()
|
||||||
.scene
|
.map(|edge| {
|
||||||
.update_entity(EntityType::Solid3D(entity))
|
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 body.").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
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
let history = solid_history::brep_op(&result);
|
if !self.tabs[i].scene.append_solid_history(
|
||||||
self.tabs[i].scene.create_solid_history(handle, history);
|
handle,
|
||||||
self.tabs[i].scene.register_prepared_solid_model(handle, result, display);
|
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 _ = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 _ = 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.deselect_all();
|
||||||
self.tabs[i].scene.select_entity(handle, false);
|
self.tabs[i].scene.select_entity(handle, false);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
@@ -628,20 +776,21 @@ impl super::OpenCADStudio {
|
|||||||
pub(super) fn solid_edge_blend(
|
pub(super) fn solid_edge_blend(
|
||||||
&mut self,
|
&mut self,
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
pick: glam::DVec3,
|
edges: &[EdgeKey],
|
||||||
|
base_face: Option<FaceKey>,
|
||||||
value: f64,
|
value: f64,
|
||||||
|
other_value: f64,
|
||||||
fillet: bool,
|
fillet: bool,
|
||||||
) -> Task<Message> {
|
) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if self.reject_locked_edit(i, handle) {
|
if self.reject_locked_edit(i, handle) {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if !matches!(
|
if !matches!(self.tabs[i].scene.document.get_entity(handle), Some(
|
||||||
self.tabs[i].scene.document.get_entity(handle),
|
EntityType::Solid3D(_) | EntityType::Surface(_)
|
||||||
Some(EntityType::Solid3D(_))
|
)) {
|
||||||
) {
|
|
||||||
self.command_line
|
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();
|
return Task::none();
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.restore_solid_models(&[handle]);
|
self.tabs[i].scene.restore_solid_models(&[handle]);
|
||||||
@@ -650,15 +799,35 @@ impl super::OpenCADStudio {
|
|||||||
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
|
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
|
||||||
return Task::none();
|
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 {
|
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 {
|
} else {
|
||||||
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 {
|
let Some(result) = result else {
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
@@ -667,8 +836,25 @@ impl super::OpenCADStudio {
|
|||||||
);
|
);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let label = if fillet { "SOLIDFILLET" } else { "SOLIDCHAMFER" };
|
let label = if fillet { "FILLETEDGE" } else { "CHAMFEREDGE" };
|
||||||
if self.replace_solid_body(handle, result, label) {
|
let updated = if fillet {
|
||||||
|
self.replace_solid_body_with_fillet(handle, &body, result, edges, value)
|
||||||
|
} else {
|
||||||
|
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
|
self.command_line
|
||||||
.push_output(crate::tf!("{label}: solid updated.").as_ref());
|
.push_output(crate::tf!("{label}: solid updated.").as_ref());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -740,6 +740,14 @@ impl OpenCADStudio {
|
|||||||
.as_mut()
|
.as_mut()
|
||||||
.map(|c| c.on_preview_wires(cur))
|
.map(|c| c.on_preview_wires(cur))
|
||||||
.unwrap_or_default();
|
.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);
|
self.tabs[i].scene.set_preview_wires(previews);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1969,6 +1969,14 @@ impl OpenCADStudio {
|
|||||||
self.dispatch_command(&cmd)
|
self.dispatch_command(&cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::ScriptLine(line) => {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
self.feed_command(crate::command::StepInput::Enter)
|
||||||
|
} else {
|
||||||
|
self.run_command_line(&line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Message::ToggleLayers => {
|
Message::ToggleLayers => {
|
||||||
if self.active_modal == Some(super::ModalKind::Layers) {
|
if self.active_modal == Some(super::ModalKind::Layers) {
|
||||||
self.ribbon.deactivate_tool_if("LAYERS");
|
self.ribbon.deactivate_tool_if("LAYERS");
|
||||||
|
|||||||
@@ -2468,6 +2468,14 @@ impl OpenCADStudio {
|
|||||||
previews.push(guide);
|
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);
|
self.tabs[i].scene.set_preview_wires(previews);
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|||||||
+4
-2
@@ -1547,11 +1547,13 @@ pub enum CmdResult {
|
|||||||
options: LoftOptions,
|
options: LoftOptions,
|
||||||
color: [f32; 4],
|
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 {
|
SolidEdgeBlend {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
pick: DVec3,
|
edges: Vec<cadkernel::brep::EdgeKey>,
|
||||||
|
base_face: Option<cadkernel::brep::FaceKey>,
|
||||||
value: f64,
|
value: f64,
|
||||||
|
other_value: f64,
|
||||||
fillet: bool,
|
fillet: bool,
|
||||||
},
|
},
|
||||||
SolidSubtract {
|
SolidSubtract {
|
||||||
|
|||||||
@@ -3904,8 +3904,19 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"CYLINDER Specify axis endpoint:" => Some(("model", "cylinder-specify-axis-endpoint")),
|
"CYLINDER Specify axis endpoint:" => Some(("model", "cylinder-specify-axis-endpoint")),
|
||||||
"Elliptical" => Some(("model", "elliptical")),
|
"Elliptical" => Some(("model", "elliptical")),
|
||||||
"Select a solid edge:" => Some(("model", "select-a-solid-edge")),
|
"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 fillet radius <{:.3}>:" => Some(("model", "specify-fillet-radius")),
|
||||||
"Specify chamfer distance <{:.3}>:" => Some(("model", "specify-chamfer-distance")),
|
"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")),
|
"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 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")),
|
"Specify height <{:.4}>:" => Some(("model", "specify-height")),
|
||||||
|
|||||||
+3
-1
@@ -134,7 +134,9 @@ fn main() -> iced::Result {
|
|||||||
Ok(text) => text
|
Ok(text) => text
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
// Blank script rows are significant: they submit Enter to
|
||||||
|
// the active command (for example, accepting a preview).
|
||||||
|
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect(),
|
.collect(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
+781
-43
@@ -1,42 +1,311 @@
|
|||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
|
use cadkernel::brep::{Body, EdgeKey, FaceKey};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{
|
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 {
|
pub enum EdgeOperation {
|
||||||
Fillet,
|
Fillet,
|
||||||
Chamfer,
|
Chamfer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum EdgeSelectionMode {
|
||||||
|
Edge,
|
||||||
|
Chain,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum EdgeStep {
|
||||||
|
Selecting,
|
||||||
|
PickingLoop,
|
||||||
|
LoopConfirm,
|
||||||
|
PreviewConfirm,
|
||||||
|
Radius,
|
||||||
|
Expression,
|
||||||
|
ChamferDistance1,
|
||||||
|
ChamferDistance2,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SolidEdgeCommand {
|
pub struct SolidEdgeCommand {
|
||||||
operation: EdgeOperation,
|
operation: EdgeOperation,
|
||||||
target: Option<Handle>,
|
target: Option<Handle>,
|
||||||
picked: Option<(Handle, DVec3)>,
|
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,
|
||||||
|
value_return: EdgeStep,
|
||||||
|
expression_return: EdgeStep,
|
||||||
|
loop_candidates: Vec<(FaceKey, Vec<EdgeKey>)>,
|
||||||
|
loop_index: usize,
|
||||||
|
last_pick: Option<DVec3>,
|
||||||
default_value: f64,
|
default_value: f64,
|
||||||
|
other_value: f64,
|
||||||
|
preview_color: [f32; 4],
|
||||||
|
preview_wires: Vec<WireModel>,
|
||||||
|
preview_hidden: Vec<Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SolidEdgeCommand {
|
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 {
|
Self {
|
||||||
operation,
|
operation,
|
||||||
target,
|
target,
|
||||||
picked: None,
|
bodies,
|
||||||
default_value: 1.0,
|
handle: None,
|
||||||
|
selected_edges: Vec::new(),
|
||||||
|
base_face: None,
|
||||||
|
selection_batches: Vec::new(),
|
||||||
|
selection_mode: EdgeSelectionMode::Edge,
|
||||||
|
step: EdgeStep::Selecting,
|
||||||
|
value_return: EdgeStep::Selecting,
|
||||||
|
expression_return: EdgeStep::Radius,
|
||||||
|
loop_candidates: Vec::new(),
|
||||||
|
loop_index: 0,
|
||||||
|
last_pick: None,
|
||||||
|
default_value: if operation == EdgeOperation::Fillet {
|
||||||
|
fillet_radius()
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
},
|
||||||
|
other_value: 1.0,
|
||||||
|
preview_color,
|
||||||
|
preview_wires: Vec::new(),
|
||||||
|
preview_hidden: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&self, value: f64) -> CmdResult {
|
pub fn new_chamfer(
|
||||||
let Some((handle, pick)) = self.picked else {
|
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()
|
||||||
|
.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(|(_, 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> {
|
||||||
|
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 {
|
||||||
|
matches!(
|
||||||
|
self.step,
|
||||||
|
EdgeStep::Selecting
|
||||||
|
| EdgeStep::PickingLoop
|
||||||
|
| EdgeStep::LoopConfirm
|
||||||
|
| EdgeStep::PreviewConfirm
|
||||||
|
| EdgeStep::Radius
|
||||||
|
| EdgeStep::Expression
|
||||||
|
| EdgeStep::ChamferDistance1
|
||||||
|
| EdgeStep::ChamferDistance2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_for_values(
|
||||||
|
&self,
|
||||||
|
value: f64,
|
||||||
|
other_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 = 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!("{}-{}-PREVIEW", handle.value(), self.name());
|
||||||
|
}
|
||||||
|
(!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.default_value, self.other_value)
|
||||||
|
{
|
||||||
|
self.preview_wires = wires;
|
||||||
|
self.preview_hidden.push(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_radius(&mut self, return_to: EdgeStep) -> CmdResult {
|
||||||
|
self.value_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.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_loop(&mut self) -> CmdResult {
|
||||||
|
if let (Some(edges), Some(anchor)) = (
|
||||||
|
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();
|
||||||
|
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;
|
return CmdResult::Cancel;
|
||||||
};
|
};
|
||||||
|
if self.selected_edges.is_empty() {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
}
|
||||||
CmdResult::SolidEdgeBlend {
|
CmdResult::SolidEdgeBlend {
|
||||||
handle,
|
handle,
|
||||||
pick,
|
edges: self.selected_edges.clone(),
|
||||||
value,
|
base_face: self.base_face,
|
||||||
fillet: matches!(self.operation, EdgeOperation::Fillet),
|
value: self.default_value,
|
||||||
|
other_value: self.other_value,
|
||||||
|
fillet: self.operation == EdgeOperation::Fillet,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,27 +313,114 @@ impl SolidEdgeCommand {
|
|||||||
impl CadCommand for SolidEdgeCommand {
|
impl CadCommand for SolidEdgeCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
match self.operation {
|
match self.operation {
|
||||||
EdgeOperation::Fillet => "SOLIDFILLET",
|
EdgeOperation::Fillet => "FILLETEDGE",
|
||||||
EdgeOperation::Chamfer => "SOLIDCHAMFER",
|
EdgeOperation::Chamfer => "CHAMFEREDGE",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if self.picked.is_none() {
|
match (self.operation, self.step, self.selection_mode) {
|
||||||
return crate::t!("Select a solid edge:").into_owned();
|
(EdgeOperation::Chamfer, EdgeStep::Selecting, _) => {
|
||||||
|
crate::t!("Select an edge or [Loop/Distance]:").into_owned()
|
||||||
|
}
|
||||||
|
(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()
|
||||||
|
}
|
||||||
|
(_, 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.value_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::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 => {
|
(
|
||||||
crate::tf!("Specify chamfer distance <{:.3}>:", self.default_value).into_owned()
|
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"),
|
||||||
|
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::Fillet, EdgeStep::Radius, _) => {
|
||||||
|
vec![CmdOption::new("Expression", "E")]
|
||||||
|
}
|
||||||
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
self.picked.is_none()
|
matches!(self.step, EdgeStep::Selecting | EdgeStep::PickingLoop)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entity_pick_includes_fills(&self) -> bool {
|
fn entity_pick_includes_fills(&self) -> bool {
|
||||||
@@ -80,52 +436,281 @@ impl CadCommand for SolidEdgeCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
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;
|
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.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
|
||||||
|
} else {
|
||||||
|
EdgeStep::LoopConfirm
|
||||||
|
};
|
||||||
|
self.rebuild_preview();
|
||||||
|
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
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
let Some((_, pick)) = self.picked else {
|
if !matches!(
|
||||||
|
self.step,
|
||||||
|
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
|
||||||
|
) {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
let Some(anchor) = self.last_pick else {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
};
|
};
|
||||||
let value = point.distance(pick);
|
let value = point.distance(anchor);
|
||||||
if value > 0.0 && value.is_finite() {
|
if value <= 0.0 || !value.is_finite() {
|
||||||
self.finish(value)
|
return CmdResult::NeedPoint;
|
||||||
} else {
|
}
|
||||||
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> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if self.picked.is_none() {
|
let keyword = text.trim().to_ascii_uppercase();
|
||||||
return None;
|
if self.operation == EdgeOperation::Chamfer {
|
||||||
|
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 {
|
||||||
|
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 = crate::app::expr_eval::eval_number(text)?;
|
||||||
|
self.accept_radius(value)
|
||||||
|
}
|
||||||
|
EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => None,
|
||||||
}
|
}
|
||||||
let value = text.trim().parse::<f64>().ok()?;
|
|
||||||
(value > 0.0 && value.is_finite()).then(|| self.finish(value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
if self.picked.is_some() {
|
match (self.operation, self.step) {
|
||||||
self.finish(self.default_value)
|
(EdgeOperation::Chamfer, EdgeStep::Selecting) if !self.selected_edges.is_empty() => {
|
||||||
} else {
|
self.step = EdgeStep::PreviewConfirm;
|
||||||
CmdResult::Cancel
|
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;
|
||||||
|
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.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.base_face = 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 {
|
fn wants_text_input(&self) -> bool {
|
||||||
self.picked.is_some()
|
matches!(
|
||||||
|
self.step,
|
||||||
|
EdgeStep::Radius
|
||||||
|
| EdgeStep::Expression
|
||||||
|
| EdgeStep::ChamferDistance1
|
||||||
|
| EdgeStep::ChamferDistance2
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_commit_as_text(&self) -> bool {
|
fn dyn_commit_as_text(&self) -> bool {
|
||||||
self.picked.is_some()
|
matches!(
|
||||||
|
self.step,
|
||||||
|
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_spec(&self) -> Option<DynSpec> {
|
fn dyn_spec(&self) -> Option<DynSpec> {
|
||||||
let (_, pick) = self.picked?;
|
let pick = self.last_pick?;
|
||||||
Some(DynSpec {
|
matches!(
|
||||||
|
self.step,
|
||||||
|
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
|
||||||
|
)
|
||||||
|
.then(|| DynSpec {
|
||||||
anchor: DynAnchor::Point(pick),
|
anchor: DynAnchor::Point(pick),
|
||||||
fields: vec![DynFieldSpec::new(match self.operation {
|
fields: vec![DynFieldSpec::new(match self.operation {
|
||||||
EdgeOperation::Fillet => DynRole::Radius,
|
EdgeOperation::Fillet => DynRole::Radius,
|
||||||
@@ -137,10 +722,163 @@ impl CadCommand for SolidEdgeCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||||
self.picked.map(|(_, pick)| cursor.distance(pick))
|
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 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() {
|
||||||
|
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);
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
names: &["SOLIDFILLET", "SOLIDCHAMFER"]
|
names: &["FILLETEDGE", "SOLIDFILLET", "CHAMFEREDGE", "SOLIDCHAMFER"]
|
||||||
});
|
});
|
||||||
@@ -89,8 +89,8 @@ impl CadModule for ModelModule {
|
|||||||
RibbonGroup {
|
RibbonGroup {
|
||||||
title: "Edges",
|
title: "Edges",
|
||||||
tools: vec![
|
tools: vec![
|
||||||
RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)),
|
RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", 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| {
|
.filter_map(|&handle| {
|
||||||
let from_history = self
|
let from_history = self
|
||||||
.document
|
.document
|
||||||
.solid_history_operation(handle)
|
.solid_history_operations(handle)
|
||||||
.and_then(|operation| cadkernel::acis::rebuild_body(operation).ok());
|
.and_then(|operations| cadkernel::acis::rebuild_history(&operations).ok());
|
||||||
let body = from_history.or_else(|| match self.document.get_entity(handle) {
|
let body = from_history.or_else(|| match self.document.get_entity(handle) {
|
||||||
Some(EntityType::Solid3D(solid)) => {
|
Some(EntityType::Solid3D(solid)) => {
|
||||||
crate::scene::convert::solid3d_tess::kernel_body(solid)
|
crate::scene::convert::solid3d_tess::kernel_body(solid)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use acadrust::entities::{EmbeddedEntity, Solid3D};
|
use acadrust::entities::{EmbeddedEntity, Solid3D};
|
||||||
use acadrust::objects::{
|
use acadrust::objects::{
|
||||||
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
|
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryChamfer,
|
||||||
SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation,
|
SolidHistoryCone, SolidHistoryCylinder, SolidHistoryFillet, SolidHistoryLoft,
|
||||||
|
SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation,
|
||||||
SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep,
|
SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep,
|
||||||
SolidHistoryTorus,
|
SolidHistoryTorus,
|
||||||
};
|
};
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
use cadkernel::brep::Body;
|
use cadkernel::brep::{Body, Surface};
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::traits::EntityTypeOps;
|
use crate::entities::traits::EntityTypeOps;
|
||||||
@@ -32,6 +33,9 @@ pub const GRIP_SWEEP_PATH_FIRST: usize = 30_000;
|
|||||||
pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000;
|
pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000;
|
||||||
pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000;
|
pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000;
|
||||||
pub const GRIP_REVOLVE_AXIS: usize = 10_013;
|
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_CORNER_FIRST: usize = 10_100;
|
||||||
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
|
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
|
||||||
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
||||||
@@ -197,7 +201,7 @@ pub fn has_specialized_primitive_properties(
|
|||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
document.solid_history_operation(handle),
|
primitive_property_operation(document, handle).as_ref(),
|
||||||
Some(
|
Some(
|
||||||
SolidHistoryOperation::Box(_)
|
SolidHistoryOperation::Box(_)
|
||||||
| SolidHistoryOperation::Wedge(_)
|
| SolidHistoryOperation::Wedge(_)
|
||||||
@@ -214,6 +218,19 @@ pub fn has_specialized_primitive_properties(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the primitive that owns the public Geometry rows. Later operations
|
||||||
|
/// such as edge fillets and chamfers refine that primitive but do not replace
|
||||||
|
/// its editable type, position, or 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
|
/// Solid history results whose public Properties palette is fully described by
|
||||||
/// the common entity rows plus [`primitive_properties`]. A generic B-rep has
|
/// the common entity rows plus [`primitive_properties`]. A generic B-rep has
|
||||||
/// no stable primitive dimensions to expose, but it still uses the compact
|
/// no stable primitive dimensions to expose, but it still uses the compact
|
||||||
@@ -1438,10 +1455,10 @@ pub fn primitive_properties(
|
|||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
) -> Vec<PropSection> {
|
) -> 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);
|
return brep_properties(document, handle);
|
||||||
};
|
};
|
||||||
match operation {
|
match &operation {
|
||||||
SolidHistoryOperation::Box(value) => {
|
SolidHistoryOperation::Box(value) => {
|
||||||
rectangular_properties(document, handle, value, "Box")
|
rectangular_properties(document, handle, value, "Box")
|
||||||
}
|
}
|
||||||
@@ -3086,6 +3103,149 @@ 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 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(
|
pub fn primitive_grips(
|
||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
@@ -3093,6 +3253,21 @@ pub fn primitive_grips(
|
|||||||
let Some(operation) = document.solid_history_operation(handle) else {
|
let Some(operation) = document.solid_history_operation(handle) else {
|
||||||
return Vec::new();
|
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();
|
||||||
|
}
|
||||||
|
if let SolidHistoryOperation::Chamfer(value) = operation {
|
||||||
|
return chamfer_distance_grips(document, handle, value);
|
||||||
|
}
|
||||||
let mut grips = Vec::new();
|
let mut grips = Vec::new();
|
||||||
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
|
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
|
||||||
if let Some(world) = world_point(transform, point) {
|
if let Some(world) = world_point(transform, point) {
|
||||||
@@ -3713,3 +3888,32 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
|||||||
..SolidHistoryBrep::default()
|
..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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
+135
-6
@@ -802,11 +802,39 @@ impl Scene {
|
|||||||
true
|
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) {
|
pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) {
|
||||||
let reference = self
|
let reference = self
|
||||||
.document
|
.document
|
||||||
.solid_history_operation(handle)
|
.solid_history_operations(handle)
|
||||||
.and_then(crate::scene::model::solid_history::reference_point);
|
.and_then(|operations| {
|
||||||
|
operations
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find_map(crate::scene::model::solid_history::reference_point)
|
||||||
|
});
|
||||||
let Some(reference) = reference else {
|
let Some(reference) = reference else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -855,8 +883,31 @@ impl Scene {
|
|||||||
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
|
(EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => {
|
||||||
cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok()
|
cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok()
|
||||||
}
|
}
|
||||||
(EntityType::Surface(_), SolidHistoryOperation::Loft(_))
|
(EntityType::Surface(_), SolidHistoryOperation::Loft(_)) => {
|
||||||
| (EntityType::Solid3D(_), _) => cadkernel::acis::rebuild_body(operation).ok(),
|
cadkernel::acis::rebuild_body(operation).ok()
|
||||||
|
}
|
||||||
|
(EntityType::Solid3D(_), _) => {
|
||||||
|
let mut operations = self.document.solid_history_operations(handle)?;
|
||||||
|
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();
|
||||||
|
cadkernel::acis::rebuild_history(&operations).ok()
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -909,7 +960,7 @@ impl Scene {
|
|||||||
self.record_solid_history_before(handle);
|
self.record_solid_history_before(handle);
|
||||||
if self
|
if self
|
||||||
.document
|
.document
|
||||||
.update_solid_history(handle, operation)
|
.update_solid_history_step(handle, operation)
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -1122,6 +1173,79 @@ impl Scene {
|
|||||||
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
|
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
|
||||||
return false;
|
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 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(
|
if !crate::scene::model::solid_history::apply_primitive_grip(
|
||||||
&mut operation,
|
&mut operation,
|
||||||
grip_id,
|
grip_id,
|
||||||
@@ -1138,7 +1262,12 @@ impl Scene {
|
|||||||
field: &str,
|
field: &str,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> bool {
|
) -> 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;
|
return false;
|
||||||
};
|
};
|
||||||
if !crate::scene::model::solid_history::apply_primitive_property(
|
if !crate::scene::model::solid_history::apply_primitive_property(
|
||||||
|
|||||||
Reference in new issue
Block a user