fix(fillet/chamfer): polyline arc preview, edge highlight, polyline chamfer
Three polyline issues in the FILLET/CHAMFER commands: - The polyline preview wire ignored bulge, so a filleted corner previewed as a straight line instead of the arc. lwpoly_pts now tessellates bulge segments via BulgeArc (same as the entity render). - Hovering a polyline edge didn't highlight it: the first-pick hover only handled Line/Arc. Both commands now highlight the nearest polyline segment on hover. - CHAMFER didn't work on polylines at all (Line-only). Added a SecondPoly step + chamfer_lwpoly_corner: pick two adjacent segments of one polyline to cut the shared corner back by the two distances with a straight edge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d8c60c32d4
commit
ef29d19ae8
1 changed files with 160 additions and 29 deletions
|
|
@ -525,7 +525,7 @@ fn lwpoly_shorten_seg(
|
||||||
new_poly
|
new_poly
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wire points for a LwPolyline (straight segments only, for preview).
|
/// Wire points for a LwPolyline (honours bulge → arcs, for preview/highlight).
|
||||||
fn lwpoly_pts(poly: &LwPolyline) -> Vec<[f32; 3]> {
|
fn lwpoly_pts(poly: &LwPolyline) -> Vec<[f32; 3]> {
|
||||||
let elev = poly.elevation as f32;
|
let elev = poly.elevation as f32;
|
||||||
let n = poly.vertices.len();
|
let n = poly.vertices.len();
|
||||||
|
|
@ -538,12 +538,56 @@ fn lwpoly_pts(poly: &LwPolyline) -> Vec<[f32; 3]> {
|
||||||
for i in 0..seg_count {
|
for i in 0..seg_count {
|
||||||
let v0 = &poly.vertices[i];
|
let v0 = &poly.vertices[i];
|
||||||
let v1 = &poly.vertices[(i + 1) % n];
|
let v1 = &poly.vertices[(i + 1) % n];
|
||||||
pts.push([v0.location.x as f32, v0.location.y as f32, elev]);
|
lwpoly_seg_pts(
|
||||||
pts.push([v1.location.x as f32, v1.location.y as f32, elev]);
|
[v0.location.x, v0.location.y],
|
||||||
|
[v1.location.x, v1.location.y],
|
||||||
|
v0.bulge,
|
||||||
|
elev,
|
||||||
|
&mut pts,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
pts
|
pts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wire points highlighting the polyline segment nearest to `click`.
|
||||||
|
fn lwpoly_seg_hover_pts(poly: &LwPolyline, click: [f64; 2]) -> Vec<[f32; 3]> {
|
||||||
|
let n = poly.vertices.len();
|
||||||
|
if n < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let seg = lwpoly_nearest_seg(poly, click);
|
||||||
|
let v0 = &poly.vertices[seg];
|
||||||
|
let v1 = &poly.vertices[(seg + 1) % n];
|
||||||
|
let mut pts = Vec::new();
|
||||||
|
lwpoly_seg_pts(
|
||||||
|
[v0.location.x, v0.location.y],
|
||||||
|
[v1.location.x, v1.location.y],
|
||||||
|
v0.bulge,
|
||||||
|
poly.elevation as f32,
|
||||||
|
&mut pts,
|
||||||
|
);
|
||||||
|
pts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append the wire points of one polyline segment (straight, or a bulge arc).
|
||||||
|
fn lwpoly_seg_pts(p0: [f64; 2], p1: [f64; 2], bulge: f64, elev: f32, out: &mut Vec<[f32; 3]>) {
|
||||||
|
if let Some(arc) = (bulge.abs() >= 1e-9)
|
||||||
|
.then(|| crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge))
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
const STEPS: usize = 16;
|
||||||
|
for j in 0..STEPS {
|
||||||
|
let a = arc.sample(j as f64 / STEPS as f64);
|
||||||
|
let b = arc.sample((j + 1) as f64 / STEPS as f64);
|
||||||
|
out.push([a[0] as f32, a[1] as f32, elev]);
|
||||||
|
out.push([b[0] as f32, b[1] as f32, elev]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push([p0[0] as f32, p0[1] as f32, elev]);
|
||||||
|
out.push([p1[0] as f32, p1[1] as f32, elev]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Unified fillet entity type ─────────────────────────────────────────────
|
// ── Unified fillet entity type ─────────────────────────────────────────────
|
||||||
|
|
||||||
/// A pickable entity for FILLET: Line, Arc, or an LwPolyline segment.
|
/// A pickable entity for FILLET: Line, Arc, or an LwPolyline segment.
|
||||||
|
|
@ -1333,8 +1377,9 @@ impl CadCommand for FilletCommand {
|
||||||
.all_entities
|
.all_entities
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.common().handle == handle)
|
.find(|e| e.common().handle == handle)
|
||||||
.and_then(|e| {
|
.and_then(|e| match e {
|
||||||
FilletEntity::from_entity(e).map(|fe| entity_pts(&fe.to_entity_type()))
|
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
|
||||||
|
_ => FilletEntity::from_entity(e).map(|fe| entity_pts(&fe.to_entity_type())),
|
||||||
});
|
});
|
||||||
if let Some(pts) = pts {
|
if let Some(pts) = pts {
|
||||||
vec![WireModel::solid(
|
vec![WireModel::solid(
|
||||||
|
|
@ -1412,6 +1457,51 @@ impl CadCommand for FilletCommand {
|
||||||
// ChamferCommand
|
// ChamferCommand
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Chamfer a corner of a single LwPolyline: the two picks select two adjacent
|
||||||
|
/// segments; the shared corner vertex is cut back by dist1/dist2 and replaced
|
||||||
|
/// with a straight chamfer edge (bulge 0). Returns the rebuilt polyline.
|
||||||
|
fn chamfer_lwpoly_corner(
|
||||||
|
poly: &LwPolyline,
|
||||||
|
click1: [f64; 2],
|
||||||
|
dist1: f64,
|
||||||
|
click2: [f64; 2],
|
||||||
|
dist2: f64,
|
||||||
|
) -> Option<EntityType> {
|
||||||
|
let n = poly.vertices.len();
|
||||||
|
if n < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let s1 = lwpoly_nearest_seg(poly, click1);
|
||||||
|
let s2 = lwpoly_nearest_seg(poly, click2);
|
||||||
|
if s1 == s2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (low, high) = if s1 < s2 { (s1, s2) } else { (s2, s1) };
|
||||||
|
let consecutive =
|
||||||
|
high == low + 1 || (poly.is_closed && low == 0 && high == n.saturating_sub(1));
|
||||||
|
if !consecutive {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let corner_idx = high % n;
|
||||||
|
let l1 = lwpoly_seg_as_line(poly, low);
|
||||||
|
let l2 = lwpoly_seg_as_line(poly, high % n.max(1));
|
||||||
|
// Map each click + distance to whichever segment it was picked on.
|
||||||
|
let (c1, d1, c2, d2) = if s1 == low {
|
||||||
|
(click1, dist1, click2, dist2)
|
||||||
|
} else {
|
||||||
|
(click2, dist2, click1, dist1)
|
||||||
|
};
|
||||||
|
match compute_chamfer(&l1, c1, d1, &l2, c2, d2)? {
|
||||||
|
(EntityType::Line(tl1), EntityType::Line(tl2), _) => {
|
||||||
|
let t1 = [tl1.end.x, tl1.end.y];
|
||||||
|
let t2 = [tl2.start.x, tl2.start.y];
|
||||||
|
let new_poly = lwpoly_replace_corner(poly, corner_idx, t1, t2, 0.0);
|
||||||
|
Some(EntityType::LwPolyline(new_poly))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ChamferStep {
|
enum ChamferStep {
|
||||||
First,
|
First,
|
||||||
WaitingForDist1,
|
WaitingForDist1,
|
||||||
|
|
@ -1421,6 +1511,13 @@ enum ChamferStep {
|
||||||
l1: LineEnt,
|
l1: LineEnt,
|
||||||
click1: [f64; 2],
|
click1: [f64; 2],
|
||||||
},
|
},
|
||||||
|
/// First pick was an LwPolyline segment; waiting for the second segment
|
||||||
|
/// of the same polyline to chamfer the shared corner.
|
||||||
|
SecondPoly {
|
||||||
|
h1: Handle,
|
||||||
|
poly: LwPolyline,
|
||||||
|
click1: [f64; 2],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChamferCommand {
|
pub struct ChamferCommand {
|
||||||
|
|
@ -1463,6 +1560,10 @@ impl CadCommand for ChamferCommand {
|
||||||
"CHAMFER Select second line [D1={:.4} D2={:.4}]:",
|
"CHAMFER Select second line [D1={:.4} D2={:.4}]:",
|
||||||
self.dist1, self.dist2
|
self.dist1, self.dist2
|
||||||
),
|
),
|
||||||
|
ChamferStep::SecondPoly { .. } => format!(
|
||||||
|
"CHAMFER Select the adjacent polyline segment [D1={:.4} D2={:.4}]:",
|
||||||
|
self.dist1, self.dist2
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1507,7 +1608,9 @@ impl CadCommand for ChamferCommand {
|
||||||
// Invalid — stay and re-prompt
|
// Invalid — stay and re-prompt
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
ChamferStep::First | ChamferStep::Second { .. } => {
|
ChamferStep::First
|
||||||
|
| ChamferStep::Second { .. }
|
||||||
|
| ChamferStep::SecondPoly { .. } => {
|
||||||
let t = text.trim();
|
let t = text.trim();
|
||||||
let upper = t.to_uppercase();
|
let upper = t.to_uppercase();
|
||||||
// "D" alone → enter sub-step to collect distances
|
// "D" alone → enter sub-step to collect distances
|
||||||
|
|
@ -1563,26 +1666,40 @@ impl CadCommand for ChamferCommand {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
ChamferStep::First => {
|
ChamferStep::First => {
|
||||||
let l1 = self
|
match self
|
||||||
.all_entities
|
.all_entities
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.common().handle == handle)
|
.find(|e| e.common().handle == handle)
|
||||||
.and_then(|e| {
|
{
|
||||||
if let EntityType::Line(l) = e {
|
Some(EntityType::Line(l)) => {
|
||||||
Some(l.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(l) = l1 {
|
|
||||||
self.step = ChamferStep::Second {
|
self.step = ChamferStep::Second {
|
||||||
h1: handle,
|
h1: handle,
|
||||||
l1: l,
|
l1: l.clone(),
|
||||||
click1: click,
|
click1: click,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
Some(EntityType::LwPolyline(p)) => {
|
||||||
|
self.step = ChamferStep::SecondPoly {
|
||||||
|
h1: handle,
|
||||||
|
poly: p.clone(),
|
||||||
|
click1: click,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
} else {
|
}
|
||||||
CmdResult::NeedPoint
|
ChamferStep::SecondPoly { h1, poly, click1 } => {
|
||||||
|
let h1 = *h1;
|
||||||
|
let poly = poly.clone();
|
||||||
|
let click1 = *click1;
|
||||||
|
if handle != h1 {
|
||||||
|
// Chamfer corners only within the same polyline.
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
match chamfer_lwpoly_corner(&poly, click1, self.dist1, click, self.dist2) {
|
||||||
|
Some(new_poly) => CmdResult::ReplaceMany(vec![(h1, vec![new_poly])], vec![]),
|
||||||
|
None => CmdResult::NeedPoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChamferStep::Second { h1, l1, click1 } => {
|
ChamferStep::Second { h1, l1, click1 } => {
|
||||||
|
|
@ -1633,12 +1750,10 @@ impl CadCommand for ChamferCommand {
|
||||||
.all_entities
|
.all_entities
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.common().handle == handle)
|
.find(|e| e.common().handle == handle)
|
||||||
.and_then(|e| {
|
.and_then(|e| match e {
|
||||||
if let EntityType::Line(l) = e {
|
EntityType::Line(l) => Some(line_pts(l)),
|
||||||
Some(line_pts(l))
|
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
|
||||||
} else {
|
_ => None,
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if let Some(pts) = pts {
|
if let Some(pts) = pts {
|
||||||
vec![WireModel::solid(
|
vec![WireModel::solid(
|
||||||
|
|
@ -1693,6 +1808,22 @@ impl CadCommand for ChamferCommand {
|
||||||
}
|
}
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
ChamferStep::SecondPoly { h1, poly, click1 } => {
|
||||||
|
if handle != *h1 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
if let Some(new_poly) =
|
||||||
|
chamfer_lwpoly_corner(poly, *click1, self.dist1, click, self.dist2)
|
||||||
|
{
|
||||||
|
return vec![WireModel::solid(
|
||||||
|
"chamfer_poly".into(),
|
||||||
|
entity_pts(&new_poly),
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue