Files
OneCAD-Draft/src/modules/home/modify/fillet.rs
T
Hakan SevenandClaude Sonnet 4.6 a88542a69a Fix ribbon active state, polar snap, FILLET pick mode, and arc preview plane
- Deactivate ribbon tool highlight when command ends (issue #1)
- Reset last_point on new command dispatch to fix polar snap offset (issue #2)
- FILLET/CHAMFER: disable text input during entity pick so clicks reach viewport (issue #3)
- Dropdown icon buttons now use RibbonToolClick so they highlight blue when active
- FILLET/CHAMFER: AutoCAD-style sub-step UX — typing R/D + Enter prompts for value
- Fix FILLET arc preview plane: arc was rendering in XZ instead of XY
- Bump version to 0.1.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 00:31:28 +03:00

936 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fillet / Chamfer — ribbon definitions + full command implementations.
//
// FILLET (F):
// Pick two lines. Command finds their intersection, computes a tangent arc
// of radius R, trims both lines to the tangent points, and inserts the arc.
// R=0 just extends/trims both lines to the exact intersection (sharp corner).
//
// CHAMFER (CHA):
// Pick two lines. Command finds their intersection, backs off dist1 along
// line 1 and dist2 along line 2, trims, and inserts a straight chamfer line.
use acadrust::entities::{Arc as ArcEnt, Line as LineEnt};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::Vec3;
use crate::command::{CadCommand, CmdResult};
use crate::modules::home::defaults;
use crate::modules::IconKind;
use crate::scene::wire_model::WireModel;
// ── Dropdown constants ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "fillet_chamfer";
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/fillet.svg"));
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
(
"FILLET",
"Fillet",
IconKind::Svg(include_bytes!("../../../../assets/icons/fillet.svg")),
),
(
"CHAMFER",
"Chamfer",
IconKind::Svg(include_bytes!("../../../../assets/icons/chamfer.svg")),
),
];
// ══════════════════════════════════════════════════════════════════════════
// Geometry
// ══════════════════════════════════════════════════════════════════════════
/// Intersect two infinite lines. Returns (t on L1, u on L2).
fn ll(
ax: f64,
ay: f64,
dx: f64,
dy: f64,
cx: f64,
cy: f64,
ex: f64,
ey: f64,
) -> Option<(f64, f64)> {
let det = dx * ey - dy * ex;
if det.abs() < 1e-10 {
return None;
}
let t = ((cx - ax) * ey - (cy - ay) * ex) / det;
let u = ((cx - ax) * dy - (cy - ay) * dx) / det;
Some((t, u))
}
/// Extract coords and unit direction for a Line entity.
fn line_geom(l: &LineEnt) -> ([f64; 2], [f64; 2], [f64; 2], f64) {
let p1 = [l.start.x, l.start.y];
let p2 = [l.end.x, l.end.y];
let dx = p2[0] - p1[0];
let dy = p2[1] - p1[1];
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
(p1, p2, [dx / len, dy / len], len)
}
/// Project click onto line, returning t ∈ ℝ.
fn project_click(click: [f64; 2], p1: [f64; 2], unit: [f64; 2]) -> f64 {
(click[0] - p1[0]) * unit[0] + (click[1] - p1[1]) * unit[1]
}
// ── Fillet ─────────────────────────────────────────────────────────────────
/// Compute fillet: trim l1/l2 and insert a tangent arc of `radius`.
/// Returns (trimmed_l1, trimmed_l2, fillet_arc).
fn compute_fillet(
l1: &LineEnt,
click1: [f64; 2],
l2: &LineEnt,
click2: [f64; 2],
radius: f64,
) -> Option<(EntityType, EntityType, Option<EntityType>)> {
let (p1, _p2, u1, _len1) = line_geom(l1);
let (p3, _p4, u2, _len2) = line_geom(l2);
// Intersection of infinite lines
let (t_p, u_p) = ll(p1[0], p1[1], u1[0], u1[1], p3[0], p3[1], u2[0], u2[1])?;
// Intersection point
let px = p1[0] + t_p * u1[0];
let py = p1[1] + t_p * u1[1];
// Direction from P toward each click (the "keep" side)
let s1 = project_click(click1, [px, py], u1); // positive = along u1
let s2 = project_click(click2, [px, py], u2);
let dir1 = if s1 >= 0.0 {
[u1[0], u1[1]]
} else {
[-u1[0], -u1[1]]
};
let dir2 = if s2 >= 0.0 {
[u2[0], u2[1]]
} else {
[-u2[0], -u2[1]]
};
// Angle between the two keep-directions
let cos_a = (dir1[0] * dir2[0] + dir1[1] * dir2[1]).clamp(-1.0, 1.0);
let angle = cos_a.acos();
// Lines are parallel / anti-parallel
if angle < 1e-6 || (angle - std::f64::consts::PI).abs() < 1e-6 {
return None;
}
let half = angle / 2.0;
let z = l1.start.z;
if radius < 1e-9 {
// r = 0: just extend/trim both lines to the intersection
let (new_l1, new_l2) = trim_to_point(l1, t_p, p1, u1, l2, u_p, p3, u2)?;
return Some((EntityType::Line(new_l1), EntityType::Line(new_l2), None));
}
// Distance from P to tangent points
let d = radius / half.tan();
// Tangent points
let t1 = [px + d * dir1[0], py + d * dir1[1]];
let t2 = [px + d * dir2[0], py + d * dir2[1]];
// Arc center: along bisector of dir1+dir2, distance = r / sin(half)
let bx = dir1[0] + dir2[0];
let by = dir1[1] + dir2[1];
let blen = (bx * bx + by * by).sqrt();
if blen < 1e-10 {
return None;
}
let arc_dist = radius / half.sin();
let arc_cx = px + arc_dist * bx / blen;
let arc_cy = py + arc_dist * by / blen;
// Arc angles in degrees
let a_start_deg = (t1[1] - arc_cy).atan2(t1[0] - arc_cx).to_degrees();
let a_end_deg = (t2[1] - arc_cy).atan2(t2[0] - arc_cx).to_degrees();
// Pick CCW direction that fills the concave corner
let cross = dir1[0] * dir2[1] - dir1[1] * dir2[0];
let (start_deg, end_deg) = if cross <= 0.0 {
(a_start_deg, a_end_deg)
} else {
(a_end_deg, a_start_deg)
};
// Trim l1 to T1 and l2 to T2
let new_l1 = trim_to_xy(l1, t_p, t1, dir1, p1, u1)?;
let new_l2 = trim_to_xy(l2, u_p, t2, dir2, p3, u2)?;
// Build arc entity
let mut arc = ArcEnt::new();
arc.common = l1.common.clone();
arc.common.handle = Handle::NULL;
arc.center = Vector3::new(arc_cx, arc_cy, z);
arc.radius = radius;
arc.start_angle = start_deg;
arc.end_angle = end_deg;
Some((
EntityType::Line(new_l1),
EntityType::Line(new_l2),
Some(EntityType::Arc(arc)),
))
}
/// Trim a line's parameter to an intersection t on the same side as dir (keep side).
fn trim_to_xy(
orig: &LineEnt,
t_isect: f64,
tangent: [f64; 2],
dir: [f64; 2],
p1: [f64; 2],
unit: [f64; 2],
) -> Option<LineEnt> {
let z = orig.start.z;
let mut l = orig.clone();
l.common.handle = Handle::NULL;
// t_tangent: parameter of the tangent point along the line from start
let t_tan = (tangent[0] - p1[0]) * unit[0] + (tangent[1] - p1[1]) * unit[1];
// dir is positive along unit → we keep the portion BEYOND t_tan in that direction
// dir positive: keep from t_tan to +∞ (i.e. set start to tangent point)
// dir negative: keep from -∞ to t_tan (i.e. set end to tangent point)
let len = {
let dx = orig.end.x - orig.start.x;
let dy = orig.end.y - orig.start.y;
(dx * dx + dy * dy).sqrt().max(1e-12)
};
let dot = dir[0] * unit[0] + dir[1] * unit[1]; // +1 or -1
if dot > 0.0 {
// keep from tangent to end → move start to tangent point
l.start = Vector3::new(tangent[0], tangent[1], z);
} else {
// keep from start to tangent → move end to tangent point
l.end = Vector3::new(tangent[0], tangent[1], z);
}
let _ = (t_isect, len, t_tan); // used implicitly via `dot`
Some(l)
}
/// Trim both lines exactly to their intersection point (r=0 case).
fn trim_to_point(
l1: &LineEnt,
t_p: f64,
p1: [f64; 2],
u1: [f64; 2],
l2: &LineEnt,
u_p: f64,
_p3: [f64; 2],
_u2: [f64; 2],
) -> Option<(LineEnt, LineEnt)> {
let px = p1[0] + t_p * u1[0];
let py = p1[1] + t_p * u1[1];
let z1 = l1.start.z;
let z2 = l2.start.z;
// For l1: if t_p is past the midpoint, keep start…P; else keep P…end
// We use the same "which end is P closer to" logic
let mut ll1 = l1.clone();
ll1.common.handle = Handle::NULL;
let mut ll2 = l2.clone();
ll2.common.handle = Handle::NULL;
if t_p >= 0.0 {
ll1.end = Vector3::new(px, py, z1);
} else {
ll1.start = Vector3::new(px, py, z1);
}
if u_p >= 0.0 {
ll2.end = Vector3::new(px, py, z2);
} else {
ll2.start = Vector3::new(px, py, z2);
}
Some((ll1, ll2))
}
// ── Point-generation helpers ──────────────────────────────────────────────
fn line_pts(l: &LineEnt) -> Vec<[f32; 3]> {
vec![
[l.start.x as f32, l.start.y as f32, l.start.z as f32],
[l.end.x as f32, l.end.y as f32, l.end.z as f32],
]
}
fn arc_pts(cx: f64, cy: f64, r: f64, a0_deg: f64, a1_deg: f64, z: f64) -> Vec<[f32; 3]> {
use std::f64::consts::TAU;
let fn_norm = |a: f64| -> f64 { ((a % TAU) + TAU) % TAU };
let a0 = a0_deg.to_radians();
let a1 = a1_deg.to_radians();
let span = {
let s = fn_norm(a1) - fn_norm(a0);
if s <= 0.0 {
s + TAU
} else {
s
}
};
let steps = (span.abs() * 20.0).ceil().max(4.0) as usize;
(0..=steps)
.map(|i| {
let ang = fn_norm(a0) + span * (i as f64 / steps as f64);
[
(cx + r * ang.cos()) as f32,
(cy + r * ang.sin()) as f32,
z as f32,
]
})
.collect()
}
fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {
match e {
EntityType::Line(l) => line_pts(l),
EntityType::Arc(a) => arc_pts(
a.center.x,
a.center.y,
a.radius,
a.start_angle,
a.end_angle,
a.center.z,
),
_ => vec![],
}
}
// ── Chamfer ────────────────────────────────────────────────────────────────
/// Compute chamfer: trim l1 by dist1 from intersection, l2 by dist2, add chamfer line.
fn compute_chamfer(
l1: &LineEnt,
click1: [f64; 2],
dist1: f64,
l2: &LineEnt,
click2: [f64; 2],
dist2: f64,
) -> Option<(EntityType, EntityType, EntityType)> {
let (p1, _, u1, _) = line_geom(l1);
let (p3, _, u2, _) = line_geom(l2);
let (t_p, u_p) = ll(p1[0], p1[1], u1[0], u1[1], p3[0], p3[1], u2[0], u2[1])?;
let px = p1[0] + t_p * u1[0];
let py = p1[1] + t_p * u1[1];
let z = l1.start.z;
let s1 = project_click(click1, [px, py], u1);
let s2 = project_click(click2, [px, py], u2);
let dir1 = if s1 >= 0.0 {
[u1[0], u1[1]]
} else {
[-u1[0], -u1[1]]
};
let dir2 = if s2 >= 0.0 {
[u2[0], u2[1]]
} else {
[-u2[0], -u2[1]]
};
// Chamfer points: back off dist from P along keep-direction
let c1 = [px + dist1 * dir1[0], py + dist1 * dir1[1]];
let c2 = [px + dist2 * dir2[0], py + dist2 * dir2[1]];
// Trim l1 to c1 and l2 to c2
let new_l1 = trim_to_xy(l1, t_p, c1, dir1, p1, u1)?;
let new_l2 = trim_to_xy(l2, u_p, c2, dir2, p3, u2)?;
// Chamfer line
let mut cline = l1.clone();
cline.common.handle = Handle::NULL;
cline.start = Vector3::new(c1[0], c1[1], z);
cline.end = Vector3::new(c2[0], c2[1], z);
Some((
EntityType::Line(new_l1),
EntityType::Line(new_l2),
EntityType::Line(cline),
))
}
// ══════════════════════════════════════════════════════════════════════════
// FilletCommand
// ══════════════════════════════════════════════════════════════════════════
enum FilletStep {
First,
WaitingForRadius,
Second {
h1: Handle,
l1: LineEnt,
click1: [f64; 2],
},
}
pub struct FilletCommand {
radius: f64,
step: FilletStep,
all_entities: Vec<EntityType>,
}
impl FilletCommand {
pub fn new(radius: f32, all_entities: Vec<EntityType>) -> Self {
Self {
radius: radius as f64,
step: FilletStep::First,
all_entities,
}
}
}
impl CadCommand for FilletCommand {
fn name(&self) -> &'static str {
"FILLET"
}
fn prompt(&self) -> String {
match &self.step {
FilletStep::First => format!(
"FILLET Select first line [R={:.4} | type R to change]:",
self.radius
),
FilletStep::WaitingForRadius => format!(
"FILLET Enter fillet radius <{:.4}>:",
self.radius
),
FilletStep::Second { .. } => {
format!("FILLET Select second line [R={:.4}]:", self.radius)
}
}
}
fn wants_text_input(&self) -> bool {
matches!(self.step, FilletStep::WaitingForRadius)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match self.step {
FilletStep::WaitingForRadius => {
let t = text.trim();
if t.is_empty() {
// Keep current radius, return to First
self.step = FilletStep::First;
return Some(CmdResult::NeedPoint);
}
if let Ok(v) = t.replace(',', ".").parse::<f64>() {
if v >= 0.0 {
self.radius = v;
defaults::set_fillet_radius(v as f32);
}
self.step = FilletStep::First;
return Some(CmdResult::NeedPoint);
}
// Invalid — stay and re-prompt
Some(CmdResult::NeedPoint)
}
FilletStep::First | FilletStep::Second { .. } => {
let t = text.trim();
let upper = t.to_uppercase();
// "R" alone → enter sub-step to collect radius
if upper == "R" {
self.step = FilletStep::WaitingForRadius;
return Some(CmdResult::NeedPoint);
}
// "R 5.0" inline shorthand
if upper.starts_with('R') {
let body = t[1..].trim();
if let Ok(v) = body.replace(',', ".").parse::<f64>() {
if v >= 0.0 {
self.radius = v;
defaults::set_fillet_radius(v as f32);
}
return Some(CmdResult::NeedPoint);
}
// "R" + invalid body → enter sub-step
self.step = FilletStep::WaitingForRadius;
return Some(CmdResult::NeedPoint);
}
None
}
}
}
fn needs_entity_pick(&self) -> bool {
!matches!(self.step, FilletStep::WaitingForRadius)
}
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let click = [pt.x as f64, pt.y as f64];
match &self.step {
FilletStep::WaitingForRadius => return CmdResult::NeedPoint,
FilletStep::First => {
// Must be a line
let l1 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
} else {
None
}
});
if let Some(l) = l1 {
self.step = FilletStep::Second {
h1: handle,
l1: l,
click1: click,
};
CmdResult::NeedPoint
} else {
CmdResult::NeedPoint // not a line — ignore
}
}
FilletStep::Second { h1, l1, click1 } => {
let h1 = *h1;
let l1 = l1.clone();
let click1 = *click1;
if handle == h1 {
return CmdResult::NeedPoint;
}
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
} else {
None
}
});
if let Some(l2) = l2 {
match compute_fillet(&l1, click1, &l2, click, self.radius) {
Some((new_l1, new_l2, maybe_arc)) => {
let mut additions = vec![];
if let Some(arc) = maybe_arc {
additions.push(arc);
}
CmdResult::ReplaceMany(
vec![(h1, vec![new_l1]), (handle, vec![new_l2])],
additions,
)
}
None => {
// Could not compute fillet (parallel lines, etc.)
CmdResult::NeedPoint
}
}
} else {
CmdResult::NeedPoint
}
}
}
}
fn on_hover_entity(&mut self, handle: Handle, pt: Vec3) -> Vec<WireModel> {
if handle.is_null() {
return vec![];
}
let click = [pt.x as f64, pt.y as f64];
match &self.step {
FilletStep::WaitingForRadius => vec![],
FilletStep::First => {
// Highlight hovered line in cyan
let pts = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(line_pts(l))
} else {
None
}
});
if let Some(pts) = pts {
vec![WireModel::solid(
"fillet_hover".into(),
pts,
WireModel::CYAN,
false,
)]
} else {
vec![]
}
}
FilletStep::Second { l1, click1, .. } => {
let l1 = l1.clone();
let click1 = *click1;
if handle.is_null() {
return vec![];
}
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
} else {
None
}
});
if let Some(l2) = l2 {
if let Some((new_l1, new_l2, maybe_arc)) =
compute_fillet(&l1, click1, &l2, click, self.radius)
{
let mut out = vec![
WireModel::solid(
"fillet_l1".into(),
entity_pts(&new_l1),
WireModel::CYAN,
false,
),
WireModel::solid(
"fillet_l2".into(),
entity_pts(&new_l2),
WireModel::CYAN,
false,
),
];
if let Some(arc) = maybe_arc {
out.push(WireModel::solid(
"fillet_arc".into(),
entity_pts(&arc),
WireModel::CYAN,
false,
));
}
return out;
}
}
vec![]
}
}
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
// ══════════════════════════════════════════════════════════════════════════
// ChamferCommand
// ══════════════════════════════════════════════════════════════════════════
enum ChamferStep {
First,
WaitingForDist1,
WaitingForDist2,
Second {
h1: Handle,
l1: LineEnt,
click1: [f64; 2],
},
}
pub struct ChamferCommand {
dist1: f64,
dist2: f64,
step: ChamferStep,
all_entities: Vec<EntityType>,
}
impl ChamferCommand {
pub fn new(dist: f32, all_entities: Vec<EntityType>) -> Self {
Self {
dist1: dist as f64,
dist2: defaults::get_chamfer_dist2() as f64,
step: ChamferStep::First,
all_entities,
}
}
}
impl CadCommand for ChamferCommand {
fn name(&self) -> &'static str {
"CHAMFER"
}
fn prompt(&self) -> String {
match &self.step {
ChamferStep::First => format!(
"CHAMFER Select first line [D1={:.4} D2={:.4} | type D to change]:",
self.dist1, self.dist2
),
ChamferStep::WaitingForDist1 => format!(
"CHAMFER Enter first chamfer distance <{:.4}>:",
self.dist1
),
ChamferStep::WaitingForDist2 => format!(
"CHAMFER Enter second chamfer distance <{:.4}>:",
self.dist2
),
ChamferStep::Second { .. } => format!(
"CHAMFER Select second line [D1={:.4} D2={:.4}]:",
self.dist1, self.dist2
),
}
}
fn wants_text_input(&self) -> bool {
matches!(
self.step,
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2
)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match self.step {
ChamferStep::WaitingForDist1 => {
let t = text.trim();
if t.is_empty() {
// Keep current dist1, move on to dist2
self.step = ChamferStep::WaitingForDist2;
return Some(CmdResult::NeedPoint);
}
if let Ok(v) = t.replace(',', ".").parse::<f64>() {
self.dist1 = v.max(0.0);
defaults::set_chamfer_dist1(self.dist1 as f32);
self.step = ChamferStep::WaitingForDist2;
return Some(CmdResult::NeedPoint);
}
// Invalid — stay and re-prompt
Some(CmdResult::NeedPoint)
}
ChamferStep::WaitingForDist2 => {
let t = text.trim();
if t.is_empty() {
// Keep current dist2, return to First
self.step = ChamferStep::First;
return Some(CmdResult::NeedPoint);
}
if let Ok(v) = t.replace(',', ".").parse::<f64>() {
self.dist2 = v.max(0.0);
defaults::set_chamfer_dist2(self.dist2 as f32);
self.step = ChamferStep::First;
return Some(CmdResult::NeedPoint);
}
// Invalid — stay and re-prompt
Some(CmdResult::NeedPoint)
}
ChamferStep::First | ChamferStep::Second { .. } => {
let t = text.trim();
let upper = t.to_uppercase();
// "D" alone → enter sub-step to collect distances
if upper == "D" {
self.step = ChamferStep::WaitingForDist1;
return Some(CmdResult::NeedPoint);
}
// "D 5.0" or "D 5.0 3.0" inline shorthand
if upper.starts_with('D') {
let body = t[1..].trim();
let parts: Vec<f64> = body
.split_whitespace()
.filter_map(|s| s.replace(',', ".").parse::<f64>().ok())
.collect();
if !parts.is_empty() {
if let Some(&v) = parts.first() {
self.dist1 = v.max(0.0);
defaults::set_chamfer_dist1(self.dist1 as f32);
}
if let Some(&v) = parts.get(1) {
self.dist2 = v.max(0.0);
defaults::set_chamfer_dist2(self.dist2 as f32);
} else {
self.dist2 = self.dist1;
defaults::set_chamfer_dist2(self.dist2 as f32);
}
return Some(CmdResult::NeedPoint);
}
// "D" + invalid body → enter sub-step
self.step = ChamferStep::WaitingForDist1;
return Some(CmdResult::NeedPoint);
}
None
}
}
}
fn needs_entity_pick(&self) -> bool {
!matches!(
self.step,
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2
)
}
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let click = [pt.x as f64, pt.y as f64];
match &self.step {
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => {
return CmdResult::NeedPoint;
}
ChamferStep::First => {
let l1 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
} else {
None
}
});
if let Some(l) = l1 {
self.step = ChamferStep::Second {
h1: handle,
l1: l,
click1: click,
};
CmdResult::NeedPoint
} else {
CmdResult::NeedPoint
}
}
ChamferStep::Second { h1, l1, click1 } => {
let h1 = *h1;
let l1 = l1.clone();
let click1 = *click1;
if handle == h1 {
return CmdResult::NeedPoint;
}
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
} else {
None
}
});
if let Some(l2) = l2 {
match compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2) {
Some((new_l1, new_l2, chamfer_line)) => CmdResult::ReplaceMany(
vec![(h1, vec![new_l1]), (handle, vec![new_l2])],
vec![chamfer_line],
),
None => CmdResult::NeedPoint,
}
} else {
CmdResult::NeedPoint
}
}
}
}
fn on_hover_entity(&mut self, handle: Handle, pt: Vec3) -> Vec<WireModel> {
if handle.is_null() {
return vec![];
}
let click = [pt.x as f64, pt.y as f64];
match &self.step {
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => return vec![],
ChamferStep::First => {
let pts = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(line_pts(l))
} else {
None
}
});
if let Some(pts) = pts {
vec![WireModel::solid(
"chamfer_hover".into(),
pts,
WireModel::CYAN,
false,
)]
} else {
vec![]
}
}
ChamferStep::Second { l1, click1, .. } => {
let l1 = l1.clone();
let click1 = *click1;
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
} else {
None
}
});
if let Some(l2) = l2 {
if let Some((new_l1, new_l2, cline)) =
compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2)
{
return vec![
WireModel::solid(
"chamfer_l1".into(),
entity_pts(&new_l1),
WireModel::CYAN,
false,
),
WireModel::solid(
"chamfer_l2".into(),
entity_pts(&new_l2),
WireModel::CYAN,
false,
),
WireModel::solid(
"chamfer_line".into(),
entity_pts(&cline),
WireModel::CYAN,
false,
),
];
}
}
vec![]
}
}
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}