forked from 1b-agent/opencadstudio-upstream
- 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>
936 lines
32 KiB
Rust
936 lines
32 KiB
Rust
// 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
|
||
}
|
||
}
|