Merge remote-tracking branch 'origin/main' into codex/shell-parity

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/ocs_doc_api/Cargo.toml
#	src/modules/model/mod.rs
This commit is contained in:
ramox81 committed 2026-09-08 19:49:35 +03:00
commit 7913f8e2da
68 files changed
+5496 -548

No files matched your search

+15 -6
View File
@@ -7,7 +7,7 @@ use cadkernel::geom2d::{self, Curve as KernelCurve};
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
use crate::scene::model::wire_model::{TangentGeom, WireModel};
use glam::DVec3;
const TAU: f64 = std::f64::consts::TAU;
@@ -71,9 +71,9 @@ fn arc_preview(
end_angle: f64,
plane: WorkingPlane,
) -> Option<WireModel> {
let center = plane.to_local(center);
let center_local = plane.to_local(center);
let arc = geom2d::bounded_arc(
[center.x, center.y],
[center_local.x, center_local.y],
radius,
start_angle,
end_angle,
@@ -81,14 +81,23 @@ fn arc_preview(
let points = KernelCurve::Arc(arc)
.tessellate_angle(TAU / 64.0)
.into_iter()
.map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array())
.map(|point| plane.to_world(DVec3::new(point[0], point[1], center_local.z)).to_array())
.collect();
Some(WireModel::solid_f64(
let mut wire = WireModel::solid_f64(
"rubber_band".into(),
points,
WireModel::CYAN,
false,
))
);
wire.tangent_geoms.push(TangentGeom::Arc {
center: [center.x, center.y, center.z],
axis_x: plane.x.to_array(),
axis_y: plane.y.to_array(),
radius: arc.radius,
start_angle: arc.start_angle,
end_angle: arc.end_angle,
});
Some(wire)
}
fn make_arc(
+12 -5
View File
@@ -11,7 +11,7 @@ use crate::t;
use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlane};
use crate::modules::draw::defaults;
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
use crate::scene::model::wire_model::{TangentGeom, WireModel};
use glam::DVec3;
const TAU: f64 = std::f64::consts::TAU;
@@ -56,16 +56,23 @@ pub const ICON: IconKind = ICON_CR;
// ── Shared geometry ────────────────────────────────────────────────────────
fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel {
let center = plane.to_local(center);
let center_local = plane.to_local(center);
let points = KernelCurve::Circle(KernelCircle {
centre: [center.x, center.y],
centre: [center_local.x, center_local.y],
radius,
})
.tessellate_angle(TAU / 64.0)
.into_iter()
.map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array())
.map(|point| plane.to_world(DVec3::new(point[0], point[1], center_local.z)).to_array())
.collect();
WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false)
let mut wire = WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false);
wire.tangent_geoms.push(TangentGeom::PlanarCircle {
center: [center.x, center.y, center.z],
axis_x: plane.x.to_array(),
axis_y: plane.y.to_array(),
radius,
});
wire
}
fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType {
+11 -2
View File
@@ -11,7 +11,7 @@ use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
use crate::scene::model::wire_model::{TangentGeom, WireModel};
use glam::DVec3;
fn parse_num(text: &str) -> Option<f64> {
@@ -75,7 +75,16 @@ fn ellipse_wire(
[p.x, p.y, p.z]
})
.collect();
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
let mut wire = WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false);
wire.tangent_geoms.push(TangentGeom::PlanarEllipse {
center: [center.x, center.y, center.z],
major_axis: [major.x, major.y, major.z],
normal: plane.z.to_array(),
minor_axis_ratio: ratio,
start_param: t_start,
end_param: t_e,
});
wire
}
/// Convert a world point to the parametric angle on the ellipse.
+62 -5
View File
@@ -17,7 +17,7 @@ use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::scene::model::wire_model::{TangentGeom, WireModel};
// ── Ribbon definition ──────────────────────────────────────────────────────
@@ -443,6 +443,7 @@ impl CadCommand for PlineCommand {
let pt = self.plane.to_local(pt_world).as_vec3();
let mut pts: Vec<[f32; 3]> = Vec::new();
let mut tangent_geom = None;
match self.mode {
SegMode::Line => {
pts.push([last.x, last.y, last.z]);
@@ -456,8 +457,44 @@ impl CadCommand for PlineCommand {
.map(|t| t.as_dvec2())
.unwrap_or(DVec2::new(1.0, 0.0));
let bulge = compute_bulge(a, tangent, b);
let arc_pts = arc_sample_points(last, bulge, pt, 16);
pts.extend_from_slice(&arc_pts);
if bulge.abs() >= 1e-9 && (b - a).length_squared() >= 1e-12 {
if let Some(arc) =
crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], bulge)
{
for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) {
pts.push([s[0] as f32, s[1] as f32, last.z]);
}
let (sa, ea) = if arc.sweep >= 0.0 {
(arc.start_angle, arc.end_angle)
} else {
(arc.end_angle, arc.start_angle)
};
let world_center = self
.plane
.to_world(DVec3::new(arc.center[0], arc.center[1], last.z as f64));
if arc.radius > 0.0
&& arc.radius.is_finite()
&& arc.radius <= 1e6
&& sa.is_finite()
&& ea.is_finite()
{
tangent_geom = Some(TangentGeom::Arc {
center: world_center.to_array(),
axis_x: self.plane.x.to_array(),
axis_y: self.plane.y.to_array(),
radius: arc.radius,
start_angle: sa,
end_angle: ea,
});
}
} else {
let arc_pts = arc_sample_points(last, bulge, pt, 64);
pts.extend_from_slice(&arc_pts);
}
} else {
pts.push([last.x, last.y, last.z]);
pts.push([pt.x, pt.y, pt.z]);
}
}
}
@@ -465,12 +502,16 @@ impl CadCommand for PlineCommand {
.into_iter()
.map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array())
.collect();
Some(WireModel::solid(
let mut wire = WireModel::solid(
"rubber_band".into(),
world_points,
WireModel::CYAN,
false,
))
);
if let Some(tg) = tangent_geom {
wire.tangent_geoms.push(tg);
}
Some(wire)
}
}
@@ -548,4 +589,20 @@ mod tests {
CmdResult::FinalizeLiveEntity(h) if h == handle
));
}
#[test]
fn test_draw_line_then_arc() {
let mut cmd = PlineCommand::new();
cmd.on_point(DVec3::new(0.0, 0.0, 0.0));
let res = cmd.on_point(DVec3::new(10.0, 0.0, 0.0));
assert!(matches!(res, CmdResult::CommitLiveEntity(_)));
cmd.set_live_handle(Handle::new(1));
cmd.on_text_input("A");
let wire = cmd.on_mouse_move(DVec3::new(10.0, 5.0, 0.0)).expect("arc wire");
assert_eq!(wire.tangent_geoms.len(), 1);
assert!(matches!(wire.tangent_geoms[0], TangentGeom::Arc { .. }));
assert!(wire.points.len() >= 16);
let res2 = cmd.on_point(DVec3::new(10.0, 5.0, 0.0));
assert!(matches!(res2, CmdResult::UpdateLiveEntity { .. }));
}
}
+781 -43
View File
@@ -1,42 +1,311 @@
use std::sync::atomic::{AtomicU64, Ordering};
use acadrust::Handle;
use cadkernel::brep::{Body, EdgeKey, FaceKey};
use glam::DVec3;
use crate::command::{
CadCommand, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec,
CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec,
};
use crate::scene::model::wire_model::WireModel;
#[derive(Clone, Copy)]
static FILLET_RADIUS_BITS: AtomicU64 = AtomicU64::new(1.0f64.to_bits());
fn fillet_radius() -> f64 {
f64::from_bits(FILLET_RADIUS_BITS.load(Ordering::Relaxed))
}
fn set_fillet_radius(value: f64) {
FILLET_RADIUS_BITS.store(value.to_bits(), Ordering::Relaxed);
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum EdgeOperation {
Fillet,
Chamfer,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EdgeSelectionMode {
Edge,
Chain,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EdgeStep {
Selecting,
PickingLoop,
LoopConfirm,
PreviewConfirm,
Radius,
Expression,
ChamferDistance1,
ChamferDistance2,
}
pub struct SolidEdgeCommand {
operation: EdgeOperation,
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,
other_value: f64,
preview_color: [f32; 4],
preview_wires: Vec<WireModel>,
preview_hidden: Vec<Handle>,
}
impl SolidEdgeCommand {
pub fn new(operation: EdgeOperation, target: Option<Handle>) -> Self {
pub fn new(
operation: EdgeOperation,
target: Option<Handle>,
bodies: Vec<(Handle, Body)>,
preview_color: [f32; 4],
) -> Self {
Self {
operation,
target,
picked: None,
default_value: 1.0,
bodies,
handle: None,
selected_edges: Vec::new(),
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 {
let Some((handle, pick)) = self.picked else {
pub fn new_chamfer(
target: Option<Handle>,
bodies: Vec<(Handle, Body)>,
preview_color: [f32; 4],
distances: (f64, f64),
) -> Self {
let mut command = Self::new(EdgeOperation::Chamfer, target, bodies, preview_color);
command.default_value = positive_default(distances.0);
command.other_value = positive_default(distances.1);
command
}
pub fn chamfer_distances(&self) -> (f64, f64) {
(self.default_value, self.other_value)
}
fn body(&self, handle: Handle) -> Option<&Body> {
self.bodies
.iter()
.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;
};
if self.selected_edges.is_empty() {
return CmdResult::Cancel;
}
CmdResult::SolidEdgeBlend {
handle,
pick,
value,
fillet: matches!(self.operation, EdgeOperation::Fillet),
edges: self.selected_edges.clone(),
base_face: self.base_face,
value: self.default_value,
other_value: self.other_value,
fillet: self.operation == EdgeOperation::Fillet,
}
}
}
@@ -44,27 +313,114 @@ impl SolidEdgeCommand {
impl CadCommand for SolidEdgeCommand {
fn name(&self) -> &'static str {
match self.operation {
EdgeOperation::Fillet => "SOLIDFILLET",
EdgeOperation::Chamfer => "SOLIDCHAMFER",
EdgeOperation::Fillet => "FILLETEDGE",
EdgeOperation::Chamfer => "CHAMFEREDGE",
}
}
fn prompt(&self) -> String {
if self.picked.is_none() {
return crate::t!("Select a solid edge:").into_owned();
match (self.operation, self.step, self.selection_mode) {
(EdgeOperation::Chamfer, EdgeStep::Selecting, _) => {
crate::t!("Select 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 {
self.picked.is_none()
matches!(self.step, EdgeStep::Selecting | EdgeStep::PickingLoop)
}
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 {
if handle.is_null() || self.target.is_some_and(|target| target != handle) {
if !self.pick_allowed(handle) {
return CmdResult::NeedPoint;
}
self.picked = Some((handle, point));
let Some(seed) = self
.body(handle)
.and_then(|body| crate::scene::model::solid_model::nearest_edge(body, point.to_array()))
else {
return CmdResult::NeedPoint;
};
self.handle = Some(handle);
self.last_pick = Some(point);
if self.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
}
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;
};
let value = point.distance(pick);
if value > 0.0 && value.is_finite() {
self.finish(value)
} else {
CmdResult::NeedPoint
let value = point.distance(anchor);
if value <= 0.0 || !value.is_finite() {
return 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> {
if self.picked.is_none() {
return None;
let keyword = text.trim().to_ascii_uppercase();
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 {
if self.picked.is_some() {
self.finish(self.default_value)
} else {
CmdResult::Cancel
match (self.operation, self.step) {
(EdgeOperation::Chamfer, EdgeStep::Selecting) if !self.selected_edges.is_empty() => {
self.step = EdgeStep::PreviewConfirm;
self.rebuild_preview();
CmdResult::NeedPoint
}
(EdgeOperation::Chamfer, EdgeStep::LoopConfirm) => self.accept_loop(),
(EdgeOperation::Chamfer, EdgeStep::PreviewConfirm) => self.finish(),
(EdgeOperation::Chamfer, EdgeStep::ChamferDistance1) => self
.accept_distance(self.default_value)
.unwrap_or(CmdResult::NeedPoint),
(EdgeOperation::Chamfer, EdgeStep::ChamferDistance2) => self
.accept_distance(self.other_value)
.unwrap_or(CmdResult::NeedPoint),
(EdgeOperation::Chamfer, EdgeStep::Expression) => CmdResult::NeedPoint,
(EdgeOperation::Chamfer, _) => CmdResult::Cancel,
(_, EdgeStep::Selecting) if !self.selected_edges.is_empty() => {
self.step = EdgeStep::PreviewConfirm;
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 {
self.picked.is_some()
matches!(
self.step,
EdgeStep::Radius
| EdgeStep::Expression
| EdgeStep::ChamferDistance1
| EdgeStep::ChamferDistance2
)
}
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> {
let (_, pick) = self.picked?;
Some(DynSpec {
let pick = self.last_pick?;
matches!(
self.step,
EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2
)
.then(|| DynSpec {
anchor: DynAnchor::Point(pick),
fields: vec![DynFieldSpec::new(match self.operation {
EdgeOperation::Fillet => DynRole::Radius,
@@ -137,10 +722,163 @@ impl CadCommand for SolidEdgeCommand {
}
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
self.picked.map(|(_, pick)| cursor.distance(pick))
matches!(
self.step,
EdgeStep::Radius | EdgeStep::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 {
names: &["SOLIDFILLET", "SOLIDCHAMFER"]
names: &["FILLETEDGE", "SOLIDFILLET", "CHAMFEREDGE", "SOLIDCHAMFER"]
});
+3 -2
View File
@@ -6,6 +6,7 @@ pub mod edge_cmd;
pub mod polysolid_cmd;
pub mod primitive_cmd;
pub mod shell_cmd;
pub mod slice_cmd;
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
@@ -89,8 +90,8 @@ impl CadModule for ModelModule {
RibbonGroup {
title: "Edges",
tools: vec![
RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)),
RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)),
RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", FILLET_ICON)),
RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)),
RibbonItem::LargeTool(tool("SHELL", "Shell", PRESSPULL_ICON)),
],
},
+510
View File
@@ -0,0 +1,510 @@
use acadrust::{EntityType, Handle};
use cadkernel::space::Plane;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, SelectionEntity, WorkingPlane};
use crate::scene::model::wire_model::WireModel;
#[derive(Clone, Copy)]
enum PlaneKind {
Xy,
Yz,
Zx,
View,
}
enum Step {
Targets,
FirstPoint,
ThreeFirst,
ThreeSecond(DVec3),
ThirdPoint(DVec3, DVec3),
AxisFirst,
AxisSecond(DVec3),
PlanePoint(PlaneKind),
PickObject,
PickSurface,
Side(Plane),
SurfaceSide,
}
/// Interactive front-end for plane-based SLICE.
pub struct SliceCommand {
step: Step,
targets: Vec<Handle>,
selected: Vec<Handle>,
working: WorkingPlane,
view_normal: DVec3,
preview_center: DVec3,
preview_radius: f64,
picked: Option<EntityType>,
surface_tool: Option<cadkernel::brep::Body>,
notice: Option<&'static str>,
}
impl SliceCommand {
pub fn new(
targets: Vec<Handle>,
view_normal: DVec3,
preview_center: DVec3,
preview_radius: f64,
) -> Self {
let step = if targets.is_empty() { Step::Targets } else { Step::FirstPoint };
Self {
step,
targets,
selected: Vec::new(),
working: WorkingPlane::default(),
view_normal: view_normal.normalize_or(DVec3::Z),
preview_center,
preview_radius: preview_radius.max(1.0),
picked: None,
surface_tool: None,
notice: None,
}
}
fn plane(origin: DVec3, x: DVec3, normal: DVec3) -> Option<Plane> {
Plane::orthonormal(origin.to_array(), x.to_array(), normal.to_array())
}
fn enter_side(&mut self, plane: Plane) -> CmdResult {
self.notice = None;
self.step = Step::Side(plane);
CmdResult::NeedPoint
}
fn finish(&mut self, keep_point: Option<DVec3>) -> CmdResult {
let Step::Side(plane) = self.step else {
return CmdResult::NeedPoint;
};
CmdResult::SliceEntities {
targets: std::mem::take(&mut self.targets),
plane,
keep_point,
}
}
fn picked_plane(&self) -> Option<Plane> {
let entity = self.picked.as_ref()?;
if !matches!(
entity,
EntityType::Arc(_)
| EntityType::Circle(_)
| EntityType::Ellipse(_)
| EntityType::Spline(_)
| EntityType::LwPolyline(_)
| EntityType::Polyline(_)
) {
return None;
}
crate::scene::model::presspull_model::profile_geometry(entity)
.map(|(plane, _, _)| plane)
}
fn finish_surface(&mut self, keep_point: Option<DVec3>) -> CmdResult {
let Some(cutter) = self.surface_tool.take() else {
return CmdResult::NeedPoint;
};
CmdResult::SliceSurfaceEntities {
targets: std::mem::take(&mut self.targets),
cutter: Box::new(cutter),
keep_point,
}
}
fn plane_at(&self, point: DVec3, kind: PlaneKind) -> Option<Plane> {
let (x, normal) = match kind {
PlaneKind::Xy => (self.working.x, self.working.z),
PlaneKind::Yz => (self.working.y, self.working.x),
PlaneKind::Zx => (self.working.z, self.working.y),
PlaneKind::View => {
let candidate = if self.working.x.dot(self.view_normal).abs() < 0.98 {
self.working.x
} else {
self.working.y
};
(candidate, self.view_normal)
}
};
Self::plane(point, x, normal)
}
}
impl CadCommand for SliceCommand {
fn name(&self) -> &'static str {
"SLICE"
}
fn prompt(&self) -> String {
if let Some(notice) = self.notice {
return crate::t!(notice).into_owned();
}
match self.step {
Step::Targets => crate::t!(
"SLICE Select solids or surfaces to slice, then press Enter:"
)
.into_owned(),
Step::FirstPoint => crate::t!(
"SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:"
)
.into_owned(),
Step::ThreeFirst => {
crate::t!("SLICE Specify first point on slicing plane:").into_owned()
}
Step::ThreeSecond(_) => {
crate::t!("SLICE Specify second point on slicing plane:").into_owned()
}
Step::ThirdPoint(_, _) => {
crate::t!("SLICE Specify third point on slicing plane:").into_owned()
}
Step::AxisFirst => {
crate::t!("SLICE Specify a point on the slicing plane:").into_owned()
}
Step::AxisSecond(_) => {
crate::t!("SLICE Specify a point on the plane's normal axis:").into_owned()
}
Step::PlanePoint(PlaneKind::View) => {
crate::t!("SLICE Specify a point on the current view plane:").into_owned()
}
Step::PlanePoint(_) => {
crate::t!("SLICE Specify a point on the current UCS plane:").into_owned()
}
Step::PickObject => crate::t!("SLICE Select a planar curve object:").into_owned(),
Step::PickSurface => crate::t!("SLICE Select a cutting surface:").into_owned(),
Step::Side(_) => crate::t!(
"SLICE Specify a point on the desired side or [Both] <Both>:"
)
.into_owned(),
Step::SurfaceSide => crate::t!(
"SLICE Select a sliced object to keep or [Both] <Both>:"
)
.into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::FirstPoint => vec![
CmdOption::new(crate::t!("planar Object").as_ref(), "O"),
CmdOption::new(crate::t!("Surface").as_ref(), "S"),
CmdOption::new(crate::t!("Zaxis").as_ref(), "Z"),
CmdOption::new(crate::t!("View").as_ref(), "V"),
CmdOption::new("XY", "XY"),
CmdOption::new("YZ", "YZ"),
CmdOption::new("ZX", "ZX"),
CmdOption::new(crate::t!("3points").as_ref(), "3"),
],
Step::Side(_) | Step::SurfaceSide => {
vec![CmdOption::new(crate::t!("Both").as_ref(), "B")]
}
_ => Vec::new(),
}
}
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.working = plane;
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
self.notice = None;
match self.step {
Step::FirstPoint => {
self.step = Step::ThreeSecond(point);
CmdResult::NeedPoint
}
Step::ThreeFirst => {
self.step = Step::ThreeSecond(point);
CmdResult::NeedPoint
}
Step::ThreeSecond(first) => {
self.step = Step::ThirdPoint(first, point);
CmdResult::NeedPoint
}
Step::ThirdPoint(first, second) => {
let x = second - first;
let normal = x.cross(point - first);
Self::plane(first, x, normal)
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane))
}
Step::AxisFirst => {
self.step = Step::AxisSecond(point);
CmdResult::NeedPoint
}
Step::AxisSecond(origin) => {
let normal = point - origin;
let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 {
self.working.x
} else {
self.working.y
};
Self::plane(origin, x, normal)
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane))
}
Step::PlanePoint(kind) => self
.plane_at(point, kind)
.map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane)),
Step::Side(_) => self.finish(Some(point)),
Step::SurfaceSide => self.finish_surface(Some(point)),
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::Targets if !self.selected.is_empty() => {
self.targets = std::mem::take(&mut self.selected);
self.step = Step::FirstPoint;
CmdResult::DeselectAndContinue
}
Step::FirstPoint => {
self.step = Step::ThreeFirst;
CmdResult::NeedPoint
}
Step::Side(_) => self.finish(None),
Step::SurfaceSide => self.finish_surface(None),
_ => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
Some(match self.step {
Step::FirstPoint => match keyword.as_str() {
"O" | "OBJECT" => {
self.step = Step::PickObject;
CmdResult::NeedPoint
}
"S" | "SURFACE" => {
self.step = Step::PickSurface;
CmdResult::NeedPoint
}
"Z" | "ZAXIS" => {
self.step = Step::AxisFirst;
CmdResult::NeedPoint
}
"V" | "VIEW" => {
self.step = Step::PlanePoint(PlaneKind::View);
CmdResult::NeedPoint
}
"XY" => {
self.step = Step::PlanePoint(PlaneKind::Xy);
CmdResult::NeedPoint
}
"YZ" => {
self.step = Step::PlanePoint(PlaneKind::Yz);
CmdResult::NeedPoint
}
"ZX" | "XZ" => {
self.step = Step::PlanePoint(PlaneKind::Zx);
CmdResult::NeedPoint
}
"3" | "3P" | "3POINTS" => {
self.step = Step::ThreeFirst;
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
},
Step::Side(_) if matches!(keyword.as_str(), "" | "B" | "BOTH") => self.finish(None),
Step::SurfaceSide if matches!(keyword.as_str(), "" | "B" | "BOTH") => {
self.finish_surface(None)
}
_ => CmdResult::NeedPoint,
})
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::FirstPoint | Step::Side(_) | Step::SurfaceSide)
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, Step::Targets)
}
fn selection_forces_add(&self) -> bool {
true
}
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
self.selected = entities
.into_iter()
.filter_map(|item| {
matches!(item.entity, EntityType::Solid3D(_) | EntityType::Surface(_))
.then_some(item.handle)
})
.collect();
}
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
CmdResult::NeedPoint
}
fn needs_entity_pick(&self) -> bool {
matches!(
self.step,
Step::PickObject | Step::PickSurface | Step::SurfaceSide
)
}
fn entity_pick_highlights_hover(&self) -> bool {
self.needs_entity_pick()
}
fn inject_before_entity_pick(&self) -> bool {
matches!(self.step, Step::PickObject | Step::PickSurface)
}
fn entity_pick_uses_surface_point(&self) -> bool {
matches!(self.step, Step::SurfaceSide)
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, _handle: Handle, _point: DVec3) -> CmdResult {
if matches!(self.step, Step::SurfaceSide) {
return self.finish_surface(Some(_point));
}
let surface_only = matches!(self.step, Step::PickSurface);
if surface_only {
let body = self.picked.as_ref().and_then(|entity| match entity {
EntityType::Surface(surface) => {
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
}
_ => None,
});
if let Some(body) = body {
if let Some(plane) = planar_body_plane(&body) {
return self.enter_side(plane);
}
if body.face_keys().count() == 1 {
self.surface_tool = Some(body);
self.notice = None;
self.step = Step::SurfaceSide;
return CmdResult::NeedPoint;
}
}
self.notice = Some(
"SLICE The selected surface does not define one supported cutting sheet. Select another surface:",
);
return CmdResult::NeedPoint;
}
match self.picked_plane() {
Some(plane) => self.enter_side(plane),
None => {
self.notice = Some(
"SLICE The selected object does not define a plane. Select a planar curve object:",
);
CmdResult::NeedPoint
}
}
}
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
let mut wires = Vec::new();
let plane = match self.step {
Step::ThirdPoint(first, second) => {
wires.push(WireModel::solid_f64(
"slice_triangle".into(),
vec![
first.to_array(),
second.to_array(),
point.to_array(),
first.to_array(),
],
WireModel::CYAN,
false,
));
Self::plane(first, second - first, (second - first).cross(point - first))
}
Step::AxisSecond(origin) => {
wires.push(WireModel::solid_f64(
"slice_normal".into(),
vec![origin.to_array(), point.to_array()],
WireModel::CYAN,
false,
));
let normal = point - origin;
let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 {
self.working.x
} else {
self.working.y
};
Self::plane(origin, x, normal)
}
Step::PlanePoint(kind) => self.plane_at(point, kind),
Step::Side(plane) => Some(plane),
_ => None,
};
if let Some(plane) = plane {
wires.extend(plane_grid(
plane,
self.preview_center,
self.preview_radius,
));
}
wires
}
}
fn planar_body_plane(body: &cadkernel::brep::Body) -> Option<Plane> {
let mut faces = body.face_keys();
let first = cadkernel::brep::planar_face_profile(body, faces.next()?)?.plane;
let normal = DVec3::from_array(first.normal()?);
let tolerance = cadkernel::brep::operation_tolerance(&[body]);
faces
.all(|face| {
let Some(profile) = cadkernel::brep::planar_face_profile(body, face) else {
return false;
};
profile.plane.normal().is_some_and(|other| {
normal.dot(DVec3::from_array(other)).abs() >= 1.0 - 1e-9
&& first
.distance_to(profile.plane.origin)
.is_some_and(|gap| gap.abs() <= tolerance * 4.0)
})
})
.then_some(first)
}
fn plane_grid(plane: Plane, centre: DVec3, radius: f64) -> Vec<WireModel> {
let normal = DVec3::from_array(plane.normal().unwrap_or([0.0, 0.0, 1.0]));
let projected = centre
- normal
* plane
.distance_to(centre.to_array())
.unwrap_or_default();
let x = DVec3::from_array(plane.x_axis);
let y = DVec3::from_array(plane.y_axis);
let color = [0.2, 0.55, 1.0, 1.0];
(-5..=5)
.flat_map(|index| {
let offset = radius * index as f64 / 5.0;
[
WireModel::solid_f64(
format!("slice_grid_x_{index}"),
vec![
(projected + x * offset - y * radius).to_array(),
(projected + x * offset + y * radius).to_array(),
],
color,
false,
),
WireModel::solid_f64(
format!("slice_grid_y_{index}"),
vec![
(projected + y * offset - x * radius).to_array(),
(projected + y * offset + x * radius).to_array(),
],
color,
false,
),
]
})
.collect()
}
inventory::submit!(crate::command::CommandRegistration {
names: &["SLICE", "SL"]
});