mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Squash of PR #502 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8e07de0f4e
commit
b4b62e4cf0
6 files changed
+359
-10
No files matched your search
@@ -2470,6 +2470,10 @@ impl ChamferCut {
|
||||
/// arc or line is added and both objects are left as they were.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn fillet_corner(s: &mut Session, h1: Handle, p1: Vec2, h2: Handle, p2: Vec2, r: f64, cut: Option<ChamferCut>, trim: bool) -> Result<Option<Handle>> {
|
||||
let dist = cut.map(|c| move |u1: Vec2, u2: Vec2| c.distances(u1, u2));
|
||||
if let Some(made) = super::modify2::fillet_pline(s, h1, p1, h2, p2, r, dist.as_ref().map(|f| f as super::modify2::ChamferDistances), trim)? {
|
||||
return Ok(made);
|
||||
}
|
||||
let d = s.doc()?;
|
||||
let k1 = d.entity(h1).map(|e| e.kind.clone());
|
||||
let k2 = d.entity(h2).map(|e| e.kind.clone());
|
||||
|
||||
@@ -15,7 +15,9 @@ use super::*;
|
||||
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
mod curve_edits;
|
||||
mod fillet_pline;
|
||||
pub(crate) use curve_edits::join_curves;
|
||||
pub(crate) use fillet_pline::{ChamferDistances, fillet_pline};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
//! FILLET and CHAMFER with polyline segments (#466): two straight segments of one polyline
|
||||
//! (adjacent, or with one segment between them, which the corner replaces), or a straight end
|
||||
//! segment of an open polyline with a line, which joins the polyline.
|
||||
|
||||
use cadcraft_doc::{EntityKind, Handle, LwPolyline};
|
||||
use cadcraft_geom::{Line, PolyVertex, Polyline, Segment, Vec2, arc_to_bulge, bulge_to_arc, line_line_infinite};
|
||||
|
||||
use super::{curves, other, push_entity, set_kind};
|
||||
use crate::cmd::helpers::{arc, line};
|
||||
use crate::{Result, Session};
|
||||
|
||||
/// CHAMFER's distances back from a corner along the first and second picked objects, given the
|
||||
/// unit directions from the corner towards their kept sides.
|
||||
pub(crate) type ChamferDistances<'a> = &'a dyn Fn(Vec2, Vec2) -> Result<(f64, f64)>;
|
||||
|
||||
/// A cut corner: the new end on each side and the bulge from the first to the second (0 for a
|
||||
/// chamfer or a sharp corner).
|
||||
struct Corner {
|
||||
t1: Vec2,
|
||||
t2: Vec2,
|
||||
bulge: f64,
|
||||
}
|
||||
|
||||
/// The corner at `x` between runs leaving it along `u1` (first pick) and `u2` (second pick),
|
||||
/// whose kept ends are `room1` and `room2` away.
|
||||
fn corner(x: Vec2, (u1, room1): (Vec2, f64), (u2, room2): (Vec2, f64), r: f64, chamfer: Option<ChamferDistances>) -> Result<Corner> {
|
||||
if u1 == Vec2::ZERO || u2 == Vec2::ZERO {
|
||||
return Err(other("The segments meet at a kept end; nothing to cut."));
|
||||
}
|
||||
let (d1, d2, bulge) = if let Some(f) = chamfer {
|
||||
let (d1, d2) = f(u1, u2)?;
|
||||
(d1, d2, 0.0)
|
||||
} else if r <= 1e-12 {
|
||||
(0.0, 0.0, 0.0)
|
||||
} else {
|
||||
let half = u1.dot(u2).clamp(-1.0, 1.0).acos() / 2.0;
|
||||
if half < 1e-9 || (std::f64::consts::FRAC_PI_2 - half).abs() < 1e-9 {
|
||||
return Err(other("The segments are parallel or in line."));
|
||||
}
|
||||
let d = r / half.tan();
|
||||
// Travelling in along -u1 and out along u2.
|
||||
let turn = (-u1).cross(u2).atan2((-u1).dot(u2));
|
||||
(d, d, arc_to_bulge(turn))
|
||||
};
|
||||
if !(d1.is_finite() && d2.is_finite()) || d1 > room1 + 1e-9 || d2 > room2 + 1e-9 {
|
||||
return Err(other(if chamfer.is_some() { "The chamfer distances are too large." } else { "Radius is too large." }));
|
||||
}
|
||||
Ok(Corner { t1: x + u1 * d1, t2: x + u2 * d2, bulge })
|
||||
}
|
||||
|
||||
fn push_point(vs: &mut Vec<PolyVertex>, v: PolyVertex) {
|
||||
match vs.last_mut() {
|
||||
Some(l) if l.p.near(v.p, 1e-12) => l.bulge = v.bulge,
|
||||
_ => vs.push(v),
|
||||
}
|
||||
}
|
||||
|
||||
/// The index of the polyline segment nearest `p`.
|
||||
fn picked_segment(segs: &[Segment], p: Vec2) -> Option<usize> {
|
||||
(0..segs.len()).min_by(|a, b| {
|
||||
let d = |i: &usize| segs.get(*i).map_or(f64::INFINITY, |s| s.closest(p).dist(p));
|
||||
d(a).total_cmp(&d(b))
|
||||
})
|
||||
}
|
||||
|
||||
/// With TRIMMODE 0: the arc (or chamfer line) on its own, both objects left as they are.
|
||||
fn add_loose(s: &mut Session, near: Handle, c: &Corner) -> Result<Option<Handle>> {
|
||||
let e = curves::entity(s, near)?;
|
||||
let space = s.doc()?.space_of(near).unwrap_or_else(|| s.space());
|
||||
let k = match bulge_to_arc(c.t1, c.t2, c.bulge) {
|
||||
Some((a, _)) => arc(&a),
|
||||
None if !c.t1.near(c.t2, 1e-12) => line(c.t1, c.t2),
|
||||
None => return Ok(None),
|
||||
};
|
||||
Ok(Some(push_entity(s, &space, e.common.clone(), k)?))
|
||||
}
|
||||
|
||||
/// Two segments of one polyline.
|
||||
fn same_polyline(
|
||||
s: &mut Session,
|
||||
h: Handle,
|
||||
pl: &LwPolyline,
|
||||
p1: Vec2,
|
||||
p2: Vec2,
|
||||
r: f64,
|
||||
chamfer: Option<ChamferDistances>,
|
||||
trim: bool,
|
||||
) -> Result<Option<Handle>> {
|
||||
let segs = Polyline { vertices: pl.vertices.clone(), closed: pl.closed }.segments();
|
||||
let n = pl.vertices.len();
|
||||
let ns = segs.len();
|
||||
let (Some(a), Some(b)) = (picked_segment(&segs, p1), picked_segment(&segs, p2)) else { return Err(other("Select two segments.")) };
|
||||
if a == b {
|
||||
return Err(other("Select two different segments of the polyline."));
|
||||
}
|
||||
let next = |k: usize| if pl.closed { Some((k + 1) % ns) } else { (k + 1 < ns).then_some(k + 1) };
|
||||
// (first segment, second segment, segments between, picked in that order?)
|
||||
let (i, j, gap, in_order) = if next(a) == Some(b) {
|
||||
(a, b, 0, true)
|
||||
} else if next(b) == Some(a) {
|
||||
(b, a, 0, false)
|
||||
} else if next(a).and_then(next) == Some(b) {
|
||||
(a, b, 1, true)
|
||||
} else if next(b).and_then(next) == Some(a) {
|
||||
(b, a, 1, false)
|
||||
} else {
|
||||
return Err(other("The segments must be adjacent or separated by one segment."));
|
||||
};
|
||||
let (Some(Segment::Line(si)), Some(Segment::Line(sj))) = (segs.get(i), segs.get(j)) else {
|
||||
return Err(other("Fillet and chamfer work on straight polyline segments."));
|
||||
};
|
||||
let x =
|
||||
if gap == 0 { si.b } else { line_line_infinite(si.a, si.b, sj.a, sj.b).map(|r| r.0).ok_or_else(|| other("The segments are parallel."))? };
|
||||
let side = |keep: Vec2| ((keep - x).normalized(), keep.dist(x));
|
||||
// CHAMFER's first distance goes on the first picked segment.
|
||||
let c = if in_order {
|
||||
corner(x, side(si.a), side(sj.b), r, chamfer)?
|
||||
} else {
|
||||
let c = corner(x, side(sj.b), side(si.a), r, chamfer)?;
|
||||
Corner { t1: c.t2, t2: c.t1, bulge: -c.bulge }
|
||||
};
|
||||
if !trim {
|
||||
return add_loose(s, h, &c);
|
||||
}
|
||||
// Rotate a closed polyline so the first segment starts at vertex 0 (nothing wraps).
|
||||
let mut vs = pl.vertices.clone();
|
||||
if pl.closed {
|
||||
vs.rotate_left(i % n.max(1));
|
||||
}
|
||||
let (i, j) = if pl.closed { (0, (j + ns - i) % ns) } else { (i, j) };
|
||||
let tmpl = vs.get(i + 1).copied().unwrap_or_default();
|
||||
let mut out: Vec<PolyVertex> = vs.get(..=i).map(<[PolyVertex]>::to_vec).unwrap_or_default();
|
||||
push_point(&mut out, PolyVertex { p: c.t1, bulge: c.bulge, ..tmpl });
|
||||
push_point(&mut out, PolyVertex { p: c.t2, bulge: 0.0, ..tmpl });
|
||||
for v in vs.iter().skip(j + 1) {
|
||||
push_point(&mut out, *v);
|
||||
}
|
||||
set_kind(s, h, EntityKind::LwPolyline(LwPolyline { vertices: out, ..pl.clone() }))?;
|
||||
Ok(Some(h))
|
||||
}
|
||||
|
||||
/// An open polyline's straight end segment with a line: the corner is cut and the line joins
|
||||
/// the polyline.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn polyline_and_line(
|
||||
s: &mut Session,
|
||||
(hp, pl, pp): (Handle, &LwPolyline, Vec2),
|
||||
(hl, ln, lp): (Handle, Line, Vec2),
|
||||
polyline_first: bool,
|
||||
r: f64,
|
||||
chamfer: Option<ChamferDistances>,
|
||||
trim: bool,
|
||||
) -> Result<Option<Handle>> {
|
||||
let segs = Polyline { vertices: pl.vertices.clone(), closed: pl.closed }.segments();
|
||||
let k = picked_segment(&segs, pp).ok_or_else(|| other("Select a polyline segment."))?;
|
||||
let last = segs.len().checked_sub(1).ok_or_else(|| other("Select a polyline segment."))?;
|
||||
if pl.closed || (k != 0 && k != last) {
|
||||
return Err(other("A line fillets or chamfers with an end segment of an open polyline."));
|
||||
}
|
||||
let at_end = match segs.get(k) {
|
||||
// A single segment: the end nearer the corner is cut.
|
||||
Some(sg) if k == 0 && k == last => {
|
||||
line_line_infinite(sg.start(), sg.end(), ln.a, ln.b).is_none_or(|(x, _, _)| x.dist(sg.end()) <= x.dist(sg.start()))
|
||||
}
|
||||
_ => k == last,
|
||||
};
|
||||
// Work at the polyline's end; turn it round if the first segment was picked.
|
||||
let mut vs = if at_end { pl.vertices.clone() } else { curves::reverse_vertices(&pl.vertices, false) };
|
||||
let n = vs.len();
|
||||
let (Some(inner), Some(end)) = (vs.get(n.wrapping_sub(2)).copied(), vs.get(n.wrapping_sub(1)).copied()) else {
|
||||
return Err(other("Select a polyline segment."));
|
||||
};
|
||||
if inner.bulge.abs() > 1e-12 {
|
||||
return Err(other("Fillet and chamfer work on straight polyline segments."));
|
||||
}
|
||||
let x = line_line_infinite(inner.p, end.p, ln.a, ln.b).map(|r| r.0).ok_or_else(|| other("Lines are parallel."))?;
|
||||
let keep_line = if (ln.a - x).dot(lp - x) >= (ln.b - x).dot(lp - x) { ln.a } else { ln.b };
|
||||
let side = |keep: Vec2| ((keep - x).normalized(), keep.dist(x));
|
||||
let c = if polyline_first {
|
||||
corner(x, side(inner.p), side(keep_line), r, chamfer)?
|
||||
} else {
|
||||
let c = corner(x, side(keep_line), side(inner.p), r, chamfer)?;
|
||||
Corner { t1: c.t2, t2: c.t1, bulge: -c.bulge }
|
||||
};
|
||||
if !trim {
|
||||
return add_loose(s, hp, &c);
|
||||
}
|
||||
vs.truncate(n - 1);
|
||||
push_point(&mut vs, PolyVertex { p: c.t1, bulge: c.bulge, ..end });
|
||||
push_point(&mut vs, PolyVertex { p: c.t2, bulge: 0.0, ..end });
|
||||
push_point(&mut vs, PolyVertex { p: keep_line, bulge: 0.0, ..end });
|
||||
if !at_end {
|
||||
vs = curves::reverse_vertices(&vs, false);
|
||||
}
|
||||
set_kind(s, hp, EntityKind::LwPolyline(LwPolyline { vertices: vs, ..pl.clone() }))?;
|
||||
s.doc_mut()?.remove_entity(hl);
|
||||
Ok(Some(hp))
|
||||
}
|
||||
|
||||
/// FILLET (`chamfer` None, radius `r`) or CHAMFER where a polyline is picked: `None` when
|
||||
/// neither object is a polyline. Returns the polyline that now holds the corner (or, with
|
||||
/// `trim` false, the separate arc or line).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn fillet_pline(
|
||||
s: &mut Session,
|
||||
h1: Handle,
|
||||
p1: Vec2,
|
||||
h2: Handle,
|
||||
p2: Vec2,
|
||||
r: f64,
|
||||
chamfer: Option<ChamferDistances>,
|
||||
trim: bool,
|
||||
) -> Result<Option<Option<Handle>>> {
|
||||
let (e1, e2) = (curves::entity(s, h1)?, curves::entity(s, h2)?);
|
||||
if !matches!(e1.kind, EntityKind::LwPolyline(_)) && !matches!(e2.kind, EntityKind::LwPolyline(_)) {
|
||||
return Ok(None);
|
||||
}
|
||||
if curves::is_locked(s, h1) || curves::is_locked(s, h2) {
|
||||
return Err(other("The object is on a locked layer."));
|
||||
}
|
||||
let as_line = |k: &EntityKind| match k {
|
||||
EntityKind::Line(l) => Some(Line::new(l.a.xy(), l.b.xy())),
|
||||
_ => None,
|
||||
};
|
||||
let made = match (&e1.kind, &e2.kind) {
|
||||
(EntityKind::LwPolyline(pl), _) if h1 == h2 => same_polyline(s, h1, pl, p1, p2, r, chamfer, trim)?,
|
||||
(EntityKind::LwPolyline(pl), k) if as_line(k).is_some() => {
|
||||
let ln = as_line(k).ok_or_else(|| other("Select a line."))?;
|
||||
polyline_and_line(s, (h1, pl, p1), (h2, ln, p2), true, r, chamfer, trim)?
|
||||
}
|
||||
(k, EntityKind::LwPolyline(pl)) if as_line(k).is_some() => {
|
||||
let ln = as_line(k).ok_or_else(|| other("Select a line."))?;
|
||||
polyline_and_line(s, (h2, pl, p2), (h1, ln, p1), false, r, chamfer, trim)?
|
||||
}
|
||||
(EntityKind::LwPolyline(_), _) | (_, EntityKind::LwPolyline(_)) => {
|
||||
return Err(other("A polyline fillets or chamfers with a line, or with another segment of itself."));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(made))
|
||||
}
|
||||
@@ -460,3 +460,106 @@ fn fillet_failures_leave_the_drawing_alone() {
|
||||
assert!(err.to_string().contains("No fillet"), "{err}");
|
||||
assert_eq!(spline(&s, &sp), before);
|
||||
}
|
||||
|
||||
// ---------------- FILLET / CHAMFER with polyline segments ----------------
|
||||
|
||||
fn pline(s: &mut Session, pts: &[[f64; 2]], closed: bool) -> String {
|
||||
h(&s.execute("pline", &json!({ "vertices": pts, "closed": closed })).unwrap())
|
||||
}
|
||||
|
||||
/// Vertices as (x, y, bulge), rounded.
|
||||
fn verts(s: &Session, h: &str) -> Vec<(f64, f64, f64)> {
|
||||
let r = |v: f64| (v * 1e9).round() / 1e9;
|
||||
match kind(s, h) {
|
||||
EntityKind::LwPolyline(p) => p.vertices.iter().map(|v| (r(v.p.x), r(v.p.y), r(v.bulge))).collect(),
|
||||
k => panic!("not a polyline: {k:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Bulge of a quarter turn, counter-clockwise.
|
||||
fn quarter() -> f64 {
|
||||
((PI / 8.0).tan() * 1e9).round() / 1e9
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_and_chamfer_two_segments_of_one_polyline() {
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]], false);
|
||||
s.execute("fillet", &json!({ "h1": p, "p1": [5, 0], "h2": p, "p2": [10, 5], "radius": 2 })).unwrap();
|
||||
assert_eq!(verts(&s, &p), vec![(0.0, 0.0, 0.0), (8.0, 0.0, quarter()), (10.0, 2.0, 0.0), (10.0, 10.0, 0.0)]);
|
||||
assert_eq!(count(&s), 1);
|
||||
// CHAMFER: the first distance goes on the first segment picked.
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]], false);
|
||||
s.execute("chamfer", &json!({ "h1": p, "p1": [10, 5], "h2": p, "p2": [5, 0], "d1": 1, "d2": 2 })).unwrap();
|
||||
assert_eq!(verts(&s, &p), vec![(0.0, 0.0, 0.0), (8.0, 0.0, 0.0), (10.0, 1.0, 0.0), (10.0, 10.0, 0.0)]);
|
||||
// One segment between them: the corner replaces it (radius 0: a sharp corner).
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 0.0], [10.0, 0.0], [11.0, 1.0], [11.0, 10.0]], false);
|
||||
s.execute("fillet", &json!({ "h1": p, "p1": [5, 0], "h2": p, "p2": [11, 5], "radius": 0 })).unwrap();
|
||||
assert_eq!(verts(&s, &p), vec![(0.0, 0.0, 0.0), (11.0, 0.0, 0.0), (11.0, 10.0, 0.0)]);
|
||||
// A closed polyline's corner across its start vertex.
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]], true);
|
||||
s.execute("fillet", &json!({ "h1": p, "p1": [5, 0], "h2": p, "p2": [0, 5], "radius": 1 })).unwrap();
|
||||
let v = verts(&s, &p);
|
||||
assert_eq!(v.len(), 5, "{v:?}");
|
||||
assert!(v.contains(&(0.0, 1.0, quarter())) && v.contains(&(1.0, 0.0, 0.0)), "{v:?}");
|
||||
assert!(!v.iter().any(|(x, y, _)| *x == 0.0 && *y == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_and_chamfer_a_line_into_a_polyline() {
|
||||
// The polyline's last segment with a line: the line joins the polyline.
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 5.0], [0.0, 0.0], [8.0, 0.0]], false);
|
||||
let l = h(&s.execute("line", &json!({ "points": [[10, 2], [10, 10]] })).unwrap());
|
||||
s.execute("fillet", &json!({ "h1": p, "p1": [4, 0], "h2": l, "p2": [10, 8], "radius": 1 })).unwrap();
|
||||
assert_eq!(verts(&s, &p), vec![(0.0, 5.0, 0.0), (0.0, 0.0, 0.0), (9.0, 0.0, quarter()), (10.0, 1.0, 0.0), (10.0, 10.0, 0.0)]);
|
||||
assert_eq!(count(&s), 1);
|
||||
// Its first segment, the line picked first, CHAMFER distances in pick order.
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[8.0, 0.0], [0.0, 0.0], [0.0, 5.0]], false);
|
||||
let l = h(&s.execute("line", &json!({ "points": [[10, 2], [10, 10]] })).unwrap());
|
||||
s.execute("chamfer", &json!({ "h1": l, "p1": [10, 8], "h2": p, "p2": [4, 0], "d1": 1, "d2": 2 })).unwrap();
|
||||
assert_eq!(verts(&s, &p), vec![(10.0, 10.0, 0.0), (10.0, 1.0, 0.0), (8.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 5.0, 0.0)]);
|
||||
assert_eq!(count(&s), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polyline_fillets_at_the_command_line_and_with_no_trim() {
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]], false);
|
||||
s.script("FILLET R 2 5,0 10,5\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
assert_eq!(verts(&s, &p).len(), 4, "{:?}", s.log);
|
||||
// No trim: the polyline stays, the arc is drawn on its own.
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]], false);
|
||||
let before = verts(&s, &p);
|
||||
s.script("FILLET T N R 2 5,0 10,5\n").unwrap();
|
||||
assert_eq!(verts(&s, &p), before);
|
||||
let (c, r, _, _) = last_arc(&s);
|
||||
assert!(c.near(Vec2::new(8.0, 2.0), 1e-9) && (r - 2.0).abs() < 1e-9, "{c:?} {r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polyline_fillet_refusals_leave_it_alone() {
|
||||
let mut s = Session::new();
|
||||
let p = pline(&mut s, &[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 20.0]], false);
|
||||
let before = verts(&s, &p);
|
||||
let c = h(&s.execute("circle", &json!({ "center": [30, 0], "radius": 1 })).unwrap());
|
||||
let l = h(&s.execute("line", &json!({ "points": [[20, -5], [20, 5]] })).unwrap());
|
||||
for q in [
|
||||
// Segments too far apart, the same segment, a circle, a middle segment with a line, too big.
|
||||
json!({ "h1": p, "p1": [5, 0], "h2": p, "p2": [0, 15], "radius": 1 }),
|
||||
json!({ "h1": p, "p1": [5, 0], "h2": p, "p2": [6, 0], "radius": 1 }),
|
||||
json!({ "h1": p, "p1": [5, 0], "h2": c, "p2": [31, 0], "radius": 1 }),
|
||||
json!({ "h1": p, "p1": [10, 5], "h2": l, "p2": [20, 4], "radius": 1 }),
|
||||
json!({ "h1": p, "p1": [5, 0], "h2": p, "p2": [10, 5], "radius": 100 }),
|
||||
] {
|
||||
assert!(s.execute("fillet", &q).is_err(), "{q}");
|
||||
assert_eq!(verts(&s, &p), before);
|
||||
}
|
||||
assert_eq!(count(&s), 3);
|
||||
}
|
||||
@@ -33,7 +33,6 @@ do them first.
|
||||
| F3 | **[alpha blocker]** (block editor and REFEDIT; dynamic-block authoring is beta) No block editor (BEDIT) or dynamic blocks (parameters, actions, visibility, lookup); dynamic blocks from other files lose their behaviour | issue #185 offers help | High: standard libraries are dynamic | 50–75 |
|
||||
| F4 | Autosave leftovers: no autosave in the web build (browser storage); autosave files are DXF, not DWG; no AUDITCTL `.adt` log and no separate RECOVER mode in the file readers (SAVETIME, Drawing Recovery, AUDIT and RECOVER landed with #406) | `engine/src/autosave.rs`, `engine/src/audit.rs` | Low-medium (web users) | 3–5 |
|
||||
| F5 | Arrays are not associative: no ARRAYEDIT; ARRAY/ARRAYRECT/ARRAYPOLAR edit the array in an option loop, then create plain copies (ASsociative, Levels, polar ROWs and grip editing print "not available yet") | `cmd/array.rs` | Medium-high | 8–12 |
|
||||
| F6 | FILLET/CHAMFER of a polyline segment with another object, or of two segments of one polyline (FILLET, TRIM, EXTEND, JOIN and LENGTHEN on ellipses and splines done, #466; FILLET Trim/Multiple and CHAMFER Angle/mEthod/Polyline done, #248) | [geometry-parity](geometry-parity.md#trim-extend-fillet-chamfer-break-join-lengthen--65) | Medium | 3–5 |
|
||||
| F7 | Dimensions: no DIMSTYLE manager dialog; 43 of ~80 DIMSTYLE variables (DIMTOFL, DIMATFIT, DIMTMOVE, DIMLTYPE, DIMLWD, DIMFXL, DIMTOLJ, DIMALTU …); no smart DIM; no DIMJOGGED, DIMJOGLINE, DIMBREAK, oblique DIMEDIT, inspection; QDIM continuous only (SETVAR DIM* now become style overrides, #129) | `doc/src/tables.rs:223-266`, `annotate.rs:64,364` | High for detailers | 25–35 |
|
||||
| F8 | TOLERANCE leftovers (feature control frames landed, #420: model, our own symbol drawing, DXF, command-line prompts, explode, grips): no Geometric Tolerance dialog, no LEADER/QLEADER tolerance annotation, no in-place editing of a frame's contents | `cmd/tolerance.rs` | Low-medium (mechanical) | 2–4 |
|
||||
| F9 | MTEXT editor is a dialog box, not in place; no columns, bullets, numbering, tabs, indents; no fields (FIELD, UPDATEFIELD); no spell check (SPELL) | `ui-egui/dialogs.rs:188-240` | Medium-high | 30–45 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Geometry parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (FILLET, EXTEND, JOIN and LENGTHEN on elliptical arcs and splines; a closed spline trims to one piece, #466) · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (FILLET, EXTEND, JOIN and LENGTHEN on elliptical arcs and splines; FILLET/CHAMFER with polyline segments; a closed spline trims to one piece, #466) · **Target:** Autodesk AutoCAD 2027
|
||||
|
||||
CAD users trust numbers: a trimmed endpoint, an offset, an intersection or an area is either right
|
||||
to the last displayed digit or the tool is unusable for production drawings. This document
|
||||
@@ -76,14 +76,14 @@ constraints 10%.
|
||||
| Polyline with arcs | exact, with self-intersection trimming, OFFSETGAPTYPE (extend, fillet, chamfer) | segments offset and joined at neighbours' intersections; **arc bulges not recomputed** after endpoints move (comment at L413), **no loop cleanup**, collapsed segments not removed; OFFSETGAPTYPE only echoed |
|
||||
| Options: Through, Erase, Layer, Multiple | yes | see [gaps.md](gaps.md) |
|
||||
|
||||
## Trim, extend, fillet, chamfer, break, join, lengthen (≈ 65%)
|
||||
## Trim, extend, fillet, chamfer, break, join, lengthen (≈ 70%)
|
||||
|
||||
| Command | AutoCAD supports | CADCraft supports |
|
||||
|---|---|---|
|
||||
| TRIM | every curve type, quick and standard modes | line, arc, circle, polyline; ellipse and spline via sampled cutting (`modify.rs:485-656`, `modify2.rs` `trim_other`); a closed spline leaves one open spline |
|
||||
| EXTEND | every open curve | line, arc, polyline, elliptical arc, open spline (runs on along its end tangent, `modify2/curve_edits.rs`) |
|
||||
| FILLET | lines, arcs, circles, polylines, ellipses, splines, xlines, rays; Trim/No trim | lines, arcs, circles (TTR circle), polyline mode, Trim/No trim, Multiple; elliptical arcs and splines with any of these (tangent circle from sampled offsets refined by Newton's method, `geom/src/fillet.rs`; arcs trimmed or extended round the ellipse, splines split at the touch point); no polyline segment with another object |
|
||||
| CHAMFER | lines, polylines, xlines, rays | lines, Distance/Angle, whole polylines (Polyline option); refuses arcs, ellipses and splines with a message, as AutoCAD; no polyline segment with another object |
|
||||
| FILLET | lines, arcs, circles, polylines, ellipses, splines, xlines, rays; Trim/No trim | lines, arcs, circles (TTR circle), polyline mode, Trim/No trim, Multiple; elliptical arcs and splines with any of these (tangent circle from sampled offsets refined by Newton's method, `geom/src/fillet.rs`; arcs trimmed or extended round the ellipse, splines split at the touch point); two straight segments of one polyline (adjacent, or with one between that the corner replaces), a polyline's straight end segment with a line (the line joins it); not polyline arc segments, xlines or rays |
|
||||
| CHAMFER | lines, polylines, xlines, rays | lines, Distance/Angle, whole polylines (Polyline option), polyline segments as for FILLET (`modify2/fillet_pline.rs`); refuses arcs, ellipses and splines with a message, as AutoCAD; not xlines or rays |
|
||||
| BREAK | every curve | line, arc, circle, polyline |
|
||||
| JOIN | lines, arcs, polylines, ellipses, splines, helixes | line, arc, polyline into a polyline; elliptical arcs on one ellipse into a larger arc or the full ellipse (gaps close); splines with lines, arcs, polylines and elliptical arcs into one exact NURBS (degree elevation, rational arcs; `geom/src/nurbs.rs`); no helixes |
|
||||
| LENGTHEN | lines, arcs, polylines, elliptical arcs, splines | line, arc, polyline; elliptical arc (arc length along the ellipse); open spline (cut along the curve, or run on along its end tangent) |
|
||||
@@ -138,11 +138,10 @@ exist for parsing, not for numerics.
|
||||
spline/ellipse offset as fitted splines within a tolerance. 15–25 h.
|
||||
3. A tolerance context relative to the drawing's extents, and a known-answer test suite at
|
||||
origin and at 1e6–1e7 coordinates. 8–12 h.
|
||||
4. FILLET/CHAMFER between a polyline segment and another object, or two segments of one polyline. 3–5 h.
|
||||
5. Exact areas and MASSPROP for bulge polylines, regions (2D) and hatch loops. 6–10 h.
|
||||
6. Hatch gap tolerance and curved boundaries. 8–12 h.
|
||||
7. Solver: DOF reporting, G2 Smooth, ellipse/spline geometry, constraint inference. 20–30 h.
|
||||
8. Spline fit options (end tangents, knot parameterisation, fit tolerance, periodic). 6–10 h.
|
||||
4. Exact areas and MASSPROP for bulge polylines, regions (2D) and hatch loops. 6–10 h.
|
||||
5. Hatch gap tolerance and curved boundaries. 8–12 h.
|
||||
6. Solver: DOF reporting, G2 Smooth, ellipse/spline geometry, constraint inference. 20–30 h.
|
||||
7. Spline fit options (end tangents, knot parameterisation, fit tolerance, periodic). 6–10 h.
|
||||
|
||||
Total ≈ 90–140 h; items 1–6 parallelise across two or three agents.
|
||||
|
||||
@@ -153,3 +152,4 @@ Total ≈ 90–140 h; items 1–6 parallelise across two or three agents.
|
||||
| 2026-10-10 | major | First geometry-precision audit: tolerances, curves, intersections, offsets, modify commands, measurement, hatch, solver |
|
||||
| 2026-10-11 | minor | EXTEND, JOIN and LENGTHEN on elliptical arcs and splines; closed splines trim to one piece; trim/extend section 45% → 55% (#466) |
|
||||
| 2026-10-11 | minor | FILLET with elliptical arcs and splines; trim/extend section 55% → 65% (#466) |
|
||||
| 2026-10-11 | minor | FILLET/CHAMFER with polyline segments (same polyline, end segment with a line); section 65% → 70% (#466) |
|
||||
Reference in new issue
Block a user