M1: oracle recipe coverage (arcs, refs-based constraints/dimensions, extents), taper and through-all extrudes, curve splitting in profiles, region merging, angle dimensions by sector, smoother tangency solve, merged topology for revolves
Oracle: 13/29 parts match Fusion. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0f3e7701a7
commit
b85ccd62cf
17 files changed
+1067
-164
No files matched your search
@@ -14,7 +14,7 @@ mod profiles;
|
||||
mod solver;
|
||||
|
||||
pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError};
|
||||
pub use profiles::{Profile, find_profiles};
|
||||
pub use profiles::{Profile, find_profiles, merge_regions};
|
||||
pub use solver::{SolveReport, SolveStatus, solve};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -49,6 +49,10 @@ pub struct Curve {
|
||||
pub kind: CurveKind,
|
||||
#[serde(default)]
|
||||
pub construction: bool,
|
||||
/// An arc drawn clockwise: stored counter-clockwise, but `<id>.start` / `<id>.end` keep the
|
||||
/// as-drawn meaning.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub reversed: bool,
|
||||
}
|
||||
|
||||
/// Geometric constraints and dimensions. Indices refer to `Sketch::points` (`p`, `q`) or
|
||||
@@ -147,11 +151,15 @@ pub enum ConstraintKind {
|
||||
c: usize,
|
||||
value: f64,
|
||||
},
|
||||
/// Angle from line `a` to line `b`, radians, counter-clockwise.
|
||||
/// Angle from line `a` to line `b`, radians, counter-clockwise. With `flip` the angle is
|
||||
/// measured to the reversed direction of one line (the constraint holds `value + 180°`), which
|
||||
/// lets a dimension pick any of the four angles between two lines.
|
||||
Angle {
|
||||
a: usize,
|
||||
b: usize,
|
||||
value: f64,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
flip: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -316,7 +324,8 @@ impl Sketch {
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh(&mut self, prefix: &str) -> String {
|
||||
/// A new unused id with the given prefix (`l` → `l3`).
|
||||
pub fn fresh(&mut self, prefix: &str) -> String {
|
||||
let n = self.counters.entry(prefix.to_string()).or_insert(0);
|
||||
loop {
|
||||
*n += 1;
|
||||
@@ -386,7 +395,7 @@ impl Sketch {
|
||||
return Err(SketchError::Invalid("line needs two distinct points".into()));
|
||||
}
|
||||
let id = self.curve_id(id, "l")?;
|
||||
self.curves.push(Curve { id, kind: CurveKind::Line { a, b }, construction: false });
|
||||
self.curves.push(Curve { id, kind: CurveKind::Line { a, b }, construction: false, reversed: false });
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
@@ -415,7 +424,7 @@ impl Sketch {
|
||||
Some(i) if i < self.points.len() => i,
|
||||
_ => self.own_point(&id, "center", center)?,
|
||||
};
|
||||
self.curves.push(Curve { id, kind: CurveKind::Circle { c, r }, construction: false });
|
||||
self.curves.push(Curve { id, kind: CurveKind::Circle { c, r }, construction: false, reversed: false });
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
@@ -441,7 +450,7 @@ impl Sketch {
|
||||
Some(i) if i < n && i != a => i,
|
||||
_ => self.own_point(&id, "end", p1)?,
|
||||
};
|
||||
self.curves.push(Curve { id, kind: CurveKind::Arc { c, a, b }, construction: false });
|
||||
self.curves.push(Curve { id, kind: CurveKind::Arc { c, a, b }, construction: false, reversed: false });
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
@@ -656,6 +665,11 @@ impl Sketch {
|
||||
}
|
||||
let (cid, role) = r.rsplit_once('.')?;
|
||||
let c = self.curves.get(self.curve_index(cid)?)?;
|
||||
let role = match (role, c.reversed) {
|
||||
("start", true) => "end",
|
||||
("end", true) => "start",
|
||||
(r, _) => r,
|
||||
};
|
||||
match (&c.kind, role) {
|
||||
(CurveKind::Line { a, .. }, "start") | (CurveKind::Arc { a, .. }, "start") => Some(*a),
|
||||
(CurveKind::Line { b, .. }, "end") | (CurveKind::Arc { b, .. }, "end") => Some(*b),
|
||||
|
||||
+373
-48
@@ -41,10 +41,30 @@ impl Profile {
|
||||
}
|
||||
|
||||
struct Edge {
|
||||
curve: usize,
|
||||
seg: Seg2,
|
||||
u: usize,
|
||||
v: usize,
|
||||
id: String,
|
||||
}
|
||||
|
||||
/// Parameter of `p` at an end of the shape (lines: 0 or 1), for intersections at end points.
|
||||
fn end_param(sh: &Shape, p: Vec2, tol: f64) -> Option<f64> {
|
||||
match *sh {
|
||||
Shape::Line { a, b } => {
|
||||
if p.dist(a) <= tol {
|
||||
Some(0.0)
|
||||
} else if p.dist(b) <= tol {
|
||||
Some(1.0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Shape::Round { c, r, start, sweep } => {
|
||||
let s0 = c + Vec2::from_angle(start) * r;
|
||||
let s1 = c + Vec2::from_angle(start + sweep) * r;
|
||||
(p.dist(s0) <= tol || p.dist(s1) <= tol).then_some(0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find(p: &mut [usize], mut i: usize) -> usize {
|
||||
@@ -57,64 +77,258 @@ fn find(p: &mut [usize], mut i: usize) -> usize {
|
||||
i
|
||||
}
|
||||
|
||||
/// Find all closed profiles of a sketch.
|
||||
pub fn find_profiles(sk: &Sketch) -> Vec<Profile> {
|
||||
// Node ids: point indices merged by position.
|
||||
let n = sk.points.len();
|
||||
let mut parent: Vec<usize> = (0..n).collect();
|
||||
let mut order: Vec<usize> = (0..n).collect();
|
||||
order.sort_by(|a, b| {
|
||||
let (pa, pb) = (sk.points.get(*a).map(|p| p.pos.x).unwrap_or(0.0), sk.points.get(*b).map(|p| p.pos.x).unwrap_or(0.0));
|
||||
pa.total_cmp(&pb)
|
||||
});
|
||||
for (k, &i) in order.iter().enumerate() {
|
||||
let Some(pi) = sk.points.get(i).map(|p| p.pos) else { continue };
|
||||
for &j in order.iter().skip(k + 1) {
|
||||
let Some(pj) = sk.points.get(j).map(|p| p.pos) else { continue };
|
||||
if pj.x - pi.x > MERGE_TOL {
|
||||
break;
|
||||
}
|
||||
if pi.dist(pj) <= MERGE_TOL {
|
||||
let (ri, rj) = (find(&mut parent, i), find(&mut parent, j));
|
||||
if ri != rj
|
||||
&& let Some(s) = parent.get_mut(rj)
|
||||
{
|
||||
*s = ri;
|
||||
/// A curve of the sketch as an analytic shape for splitting.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Shape {
|
||||
Line {
|
||||
a: Vec2,
|
||||
b: Vec2,
|
||||
},
|
||||
/// Circle (full when `sweep` is 2π) from `start` angle sweeping counter-clockwise.
|
||||
Round {
|
||||
c: Vec2,
|
||||
r: f64,
|
||||
start: f64,
|
||||
sweep: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
/// Curve parameter of `p` if it lies on the curve's interior (line t in (0,1); round:
|
||||
/// angle offset from start in (0, sweep)).
|
||||
fn param_on(&self, p: Vec2, tol: f64) -> Option<f64> {
|
||||
match *self {
|
||||
Shape::Line { a, b } => {
|
||||
let d = b - a;
|
||||
let l2 = d.len2();
|
||||
if l2 < 1e-24 {
|
||||
return None;
|
||||
}
|
||||
let t = (p - a).dot(d) / l2;
|
||||
let q = a + d * t;
|
||||
(q.dist(p) <= tol && t * l2.sqrt() > tol && (1.0 - t) * l2.sqrt() > tol).then_some(t)
|
||||
}
|
||||
Shape::Round { c, r, start, sweep } => {
|
||||
if (p.dist(c) - r).abs() > tol {
|
||||
return None;
|
||||
}
|
||||
let off = ((p - c).angle() - start).rem_euclid(TAU);
|
||||
let ang_tol = tol / r.max(1e-12);
|
||||
let full = sweep >= TAU - 1e-12;
|
||||
if full {
|
||||
return Some(off);
|
||||
}
|
||||
(off > ang_tol && off < sweep - ang_tol).then_some(off)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn point(&self, t: f64) -> Vec2 {
|
||||
match *self {
|
||||
Shape::Line { a, b } => a.lerp(b, t),
|
||||
Shape::Round { c, r, start, .. } => c + Vec2::from_angle(start + t) * r,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut loops: Vec<(Loop2, Vec<String>, usize)> = Vec::new(); // (ccw loop, curve ids, component)
|
||||
let mut edges: Vec<Edge> = Vec::new();
|
||||
/// Intersection points of two shapes (as circles/infinite lines; callers filter by range).
|
||||
fn intersections(x: &Shape, y: &Shape) -> Vec<Vec2> {
|
||||
match (*x, *y) {
|
||||
(Shape::Line { a, b }, Shape::Line { a: c, b: d }) => {
|
||||
let (r, s) = (b - a, d - c);
|
||||
let den = r.cross(s);
|
||||
if den.abs() < 1e-12 * r.len() * s.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let t = (c - a).cross(s) / den;
|
||||
vec![a + r * t]
|
||||
}
|
||||
(Shape::Line { a, b }, Shape::Round { c, r, .. }) | (Shape::Round { c, r, .. }, Shape::Line { a, b }) => {
|
||||
let d = b - a;
|
||||
let f = a - c;
|
||||
let (qa, qb, qc) = (d.len2(), 2.0 * f.dot(d), f.len2() - r * r);
|
||||
let disc = qb * qb - 4.0 * qa * qc;
|
||||
if qa < 1e-24 || disc < 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let sq = disc.sqrt();
|
||||
vec![a + d * ((-qb - sq) / (2.0 * qa)), a + d * ((-qb + sq) / (2.0 * qa))]
|
||||
}
|
||||
(Shape::Round { c: c1, r: r1, .. }, Shape::Round { c: c2, r: r2, .. }) => {
|
||||
let dv = c2 - c1;
|
||||
let d = dv.len();
|
||||
if d < 1e-12 || d > r1 + r2 || d < (r1 - r2).abs() {
|
||||
return Vec::new();
|
||||
}
|
||||
let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
|
||||
let h = (r1 * r1 - a * a).max(0.0).sqrt();
|
||||
let m = c1 + dv * (a / d);
|
||||
let n = dv.perp() / d;
|
||||
vec![m + n * h, m - n * h]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Signed curvature at the start of a segment (left turns positive).
|
||||
fn curvature(s: &Seg2) -> f64 {
|
||||
match *s {
|
||||
Seg2::Line { .. } => 0.0,
|
||||
Seg2::Arc { radius, sweep, .. } => sweep.signum() / radius.max(1e-12),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all closed profiles of a sketch. Curves are split where other curves end on them or
|
||||
/// cross them, like a drawing would be read.
|
||||
pub fn find_profiles(sk: &Sketch) -> Vec<Profile> {
|
||||
let tol = MERGE_TOL * 10.0;
|
||||
// Analytic shapes of the profile curves.
|
||||
let mut shapes: Vec<(usize, Shape)> = Vec::new();
|
||||
for (ci, c) in sk.curves.iter().enumerate() {
|
||||
if c.construction {
|
||||
continue;
|
||||
}
|
||||
match c.kind {
|
||||
CurveKind::Circle { .. } => {
|
||||
let segs = sk.segs(ci);
|
||||
if !segs.is_empty() {
|
||||
loops.push((Loop2 { segs }, vec![c.id.clone()], usize::MAX - ci));
|
||||
CurveKind::Line { a, b } => {
|
||||
if let (Some(a), Some(b)) = (sk.point(a), sk.point(b))
|
||||
&& a.dist(b) > MERGE_TOL
|
||||
{
|
||||
shapes.push((ci, Shape::Line { a, b }));
|
||||
}
|
||||
}
|
||||
CurveKind::Line { a, b } | CurveKind::Arc { a, b, .. } => {
|
||||
let (u, v) = (find(&mut parent, a), find(&mut parent, b));
|
||||
if u == v {
|
||||
continue;
|
||||
CurveKind::Circle { c, r } => {
|
||||
if let Some(c) = sk.point(c) {
|
||||
shapes.push((ci, Shape::Round { c, r, start: 0.0, sweep: TAU }));
|
||||
}
|
||||
if let Some(seg) = sk.segs(ci).into_iter().next()
|
||||
&& seg.length() > MERGE_TOL
|
||||
{
|
||||
edges.push(Edge { curve: ci, seg, u, v });
|
||||
}
|
||||
CurveKind::Arc { .. } => {
|
||||
if let Some(Seg2::Arc { center, radius, start, sweep }) = sk.segs(ci).into_iter().next() {
|
||||
shapes.push((ci, Shape::Round { c: center, r: radius, start, sweep }));
|
||||
}
|
||||
}
|
||||
}
|
||||
if edges.len() > MAX_EDGES {
|
||||
if shapes.len() > MAX_EDGES {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Split parameters per shape: end points of other curves on it, and crossings.
|
||||
let mut ends: Vec<Vec2> = Vec::new();
|
||||
for (_, sh) in &shapes {
|
||||
match *sh {
|
||||
Shape::Line { a, b } => ends.extend([a, b]),
|
||||
Shape::Round { sweep, .. } if sweep < TAU - 1e-12 => ends.extend([sh.point(0.0), sh.point(sweep)]),
|
||||
Shape::Round { .. } => {}
|
||||
}
|
||||
}
|
||||
let mut cuts: Vec<Vec<f64>> = vec![Vec::new(); shapes.len()];
|
||||
for (i, (_, sh)) in shapes.iter().enumerate() {
|
||||
for p in &ends {
|
||||
if let Some(t) = sh.param_on(*p, tol)
|
||||
&& let Some(c) = cuts.get_mut(i)
|
||||
{
|
||||
c.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
if shapes.len() <= 2000 {
|
||||
for i in 0..shapes.len() {
|
||||
for j in (i + 1)..shapes.len() {
|
||||
let (Some((_, x)), Some((_, y))) = (shapes.get(i), shapes.get(j)) else { continue };
|
||||
let mut pts = intersections(x, y);
|
||||
// A tangent touch gives two nearly equal roots: one point.
|
||||
if let [p, q] = pts[..]
|
||||
&& p.dist(q) < tol * 100.0
|
||||
{
|
||||
pts = vec![(p + q) * 0.5];
|
||||
}
|
||||
for p in pts {
|
||||
// Crossings at (or next to) curve ends are already cuts.
|
||||
if ends.iter().any(|e| e.dist(p) < tol * 100.0) {
|
||||
continue;
|
||||
}
|
||||
let on_x = x.param_on(p, tol).or_else(|| end_param(x, p, tol));
|
||||
let on_y = y.param_on(p, tol).or_else(|| end_param(y, p, tol));
|
||||
if let (Some(tx), Some(ty)) = (on_x, on_y) {
|
||||
if x.param_on(p, tol).is_some()
|
||||
&& let Some(c) = cuts.get_mut(i)
|
||||
{
|
||||
c.push(tx);
|
||||
}
|
||||
if y.param_on(p, tol).is_some()
|
||||
&& let Some(c) = cuts.get_mut(j)
|
||||
{
|
||||
c.push(ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pieces → edges between merged nodes.
|
||||
let mut nodes: Vec<Vec2> = Vec::new();
|
||||
let mut node = |p: Vec2| -> usize {
|
||||
if let Some(i) = nodes.iter().position(|q| q.dist(p) <= tol) {
|
||||
return i;
|
||||
}
|
||||
nodes.push(p);
|
||||
nodes.len() - 1
|
||||
};
|
||||
let mut loops: Vec<(Loop2, Vec<String>, usize)> = Vec::new(); // (ccw loop, curve ids, component)
|
||||
let mut edges: Vec<Edge> = Vec::new();
|
||||
for (k, (ci, sh)) in shapes.iter().enumerate() {
|
||||
let mut ts = cuts.get(k).cloned().unwrap_or_default();
|
||||
ts.sort_by(f64::total_cmp);
|
||||
// Cuts closer than a few tolerances are one cut (tangent touches give near-double roots).
|
||||
let scale = match *sh {
|
||||
Shape::Line { a, b } => a.dist(b),
|
||||
Shape::Round { r, .. } => r,
|
||||
};
|
||||
let close = (tol * 100.0 / scale.max(1e-12)).max(1e-9);
|
||||
ts.dedup_by(|a, b| (*a - *b).abs() < close);
|
||||
if let (Shape::Round { sweep, .. }, Some(&first), Some(&last)) = (*sh, ts.first(), ts.last())
|
||||
&& sweep >= TAU - 1e-12
|
||||
&& ts.len() > 1
|
||||
&& (first + TAU - last).abs() < close
|
||||
{
|
||||
ts.pop();
|
||||
}
|
||||
let id = sk.curves.get(*ci).map(|c| c.id.clone()).unwrap_or_default();
|
||||
match *sh {
|
||||
Shape::Line { a, b } => {
|
||||
let mut ps = vec![a];
|
||||
ps.extend(ts.iter().map(|t| sh.point(*t)));
|
||||
ps.push(b);
|
||||
for w in ps.windows(2) {
|
||||
let (p, q) = (w[0], w[1]);
|
||||
if p.dist(q) > MERGE_TOL {
|
||||
let (u, v) = (node(p), node(q));
|
||||
edges.push(Edge { seg: Seg2::Line { a: p, b: q }, u, v, id: id.clone() });
|
||||
}
|
||||
}
|
||||
}
|
||||
Shape::Round { c, r, start, sweep } => {
|
||||
let full = sweep >= TAU - 1e-12;
|
||||
if full && ts.is_empty() {
|
||||
loops.push((Loop2::circle(c, r), vec![id.clone()], usize::MAX - *ci));
|
||||
continue;
|
||||
}
|
||||
let mut bounds: Vec<f64> =
|
||||
if full { ts.clone() } else { std::iter::once(0.0).chain(ts.iter().copied()).chain(std::iter::once(sweep)).collect() };
|
||||
if full {
|
||||
let first = bounds.first().copied().unwrap_or(0.0);
|
||||
bounds.push(first + TAU);
|
||||
}
|
||||
for w in bounds.windows(2) {
|
||||
let (t0, t1) = (w[0], w[1]);
|
||||
if (t1 - t0) * r <= MERGE_TOL {
|
||||
continue;
|
||||
}
|
||||
let seg = Seg2::Arc { center: c, radius: r, start: start + t0, sweep: t1 - t0 };
|
||||
let (u, v) = (node(seg.start()), node(seg.end()));
|
||||
edges.push(Edge { seg, u, v, id: id.clone() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let n = nodes.len();
|
||||
// Prune dangling edges.
|
||||
let mut alive = vec![true; edges.len()];
|
||||
loop {
|
||||
@@ -190,27 +404,34 @@ pub fn find_profiles(sk: &Sketch) -> Vec<Profile> {
|
||||
break;
|
||||
};
|
||||
segs.push(seg);
|
||||
if let Some(e) = edges.get(h / 2)
|
||||
&& let Some(c) = sk.curves.get(e.curve)
|
||||
{
|
||||
ids.push(c.id.clone());
|
||||
if let Some(e) = edges.get(h / 2) {
|
||||
ids.push(e.id.clone());
|
||||
}
|
||||
let back = (-seg.end_tangent()).angle();
|
||||
let twin = h ^ 1;
|
||||
let mut best: Option<(f64, usize)> = None;
|
||||
// First clockwise turn from the way back. Directions are compared a short way
|
||||
// along each segment (curvature decides between equal tangents, e.g. a line tangent
|
||||
// to an arc, or an arc leaving back along the incoming line).
|
||||
let kt = half(twin).map(|(ts, _, _)| curvature(&ts)).unwrap_or(0.0);
|
||||
let mut best: Option<(f64, f64, usize)> = None;
|
||||
for &o in outgoing.get(to).map(Vec::as_slice).unwrap_or(&[]) {
|
||||
if o == twin {
|
||||
continue;
|
||||
}
|
||||
let Some((os, _, _)) = half(o) else { continue };
|
||||
let mut d = (back - os.start_tangent().angle()) % TAU;
|
||||
let mut raw = (back - os.start_tangent().angle()).rem_euclid(TAU);
|
||||
if raw > TAU - 1e-9 {
|
||||
raw = 0.0;
|
||||
}
|
||||
let mut d = raw + (kt - curvature(&os)) * 1e-7;
|
||||
if d <= 1e-12 {
|
||||
d += TAU;
|
||||
}
|
||||
if best.is_none_or(|(bd, _)| d < bd) {
|
||||
best = Some((d, o));
|
||||
if best.is_none_or(|(bd, _, _)| d < bd) {
|
||||
best = Some((d, 0.0, o));
|
||||
}
|
||||
}
|
||||
let best = best.map(|(d, _, o)| (d, o));
|
||||
let Some((_, next)) = best else {
|
||||
ok = false;
|
||||
break;
|
||||
@@ -271,3 +492,107 @@ pub fn find_profiles(sk: &Sketch) -> Vec<Profile> {
|
||||
out.sort_by(|a, b| first_curve(a).cmp(&first_curve(b)).then(b.area.total_cmp(&a.area)));
|
||||
out
|
||||
}
|
||||
|
||||
fn same_seg(a: &Seg2, b: &Seg2, tol: f64) -> bool {
|
||||
match (*a, *b) {
|
||||
(Seg2::Line { a: p, b: q }, Seg2::Line { a: r, b: s }) => p.dist(r) <= tol && q.dist(s) <= tol,
|
||||
(Seg2::Arc { center: c1, radius: r1, .. }, Seg2::Arc { center: c2, radius: r2, .. }) => {
|
||||
c1.dist(c2) <= tol
|
||||
&& (r1 - r2).abs() <= tol
|
||||
&& a.start().dist(b.start()) <= tol
|
||||
&& a.end().dist(b.end()) <= tol
|
||||
&& a.mid().dist(b.mid()) <= tol
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Union of regions that touch along shared boundary segments (one extrude of several adjacent
|
||||
/// profiles makes one body). Disjoint regions stay separate.
|
||||
pub fn merge_regions(regions: &[Region2]) -> Vec<Region2> {
|
||||
let tol = MERGE_TOL * 10.0;
|
||||
// Every boundary segment oriented with the region on its left.
|
||||
let mut segs: Vec<Seg2> = Vec::new();
|
||||
for r in regions {
|
||||
segs.extend(r.outer.ccw().segs);
|
||||
for h in &r.holes {
|
||||
segs.extend(h.ccw().reversed().segs);
|
||||
}
|
||||
}
|
||||
if segs.len() > MAX_EDGES {
|
||||
return regions.to_vec();
|
||||
}
|
||||
// Drop pairs that run against each other (shared boundaries).
|
||||
let mut alive = vec![true; segs.len()];
|
||||
let mut shared = false;
|
||||
for i in 0..segs.len() {
|
||||
if !alive.get(i).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
for j in (i + 1)..segs.len() {
|
||||
if alive.get(j).copied().unwrap_or(false)
|
||||
&& let (Some(a), Some(b)) = (segs.get(i), segs.get(j))
|
||||
&& same_seg(a, &b.reversed(), tol)
|
||||
{
|
||||
alive[i] = false;
|
||||
alive[j] = false;
|
||||
shared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !shared {
|
||||
return regions.to_vec();
|
||||
}
|
||||
let rest: Vec<Seg2> = segs.iter().zip(&alive).filter(|(_, a)| **a).map(|(s, _)| *s).collect();
|
||||
// Chain into loops, taking the sharpest left turn where several continue.
|
||||
let mut used = vec![false; rest.len()];
|
||||
let mut loops: Vec<Loop2> = Vec::new();
|
||||
for start in 0..rest.len() {
|
||||
if used.get(start).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let mut lp = Vec::new();
|
||||
let mut cur = start;
|
||||
for _ in 0..=rest.len() {
|
||||
if let Some(u) = used.get_mut(cur) {
|
||||
*u = true;
|
||||
}
|
||||
let Some(seg) = rest.get(cur).copied() else { break };
|
||||
lp.push(seg);
|
||||
let end = seg.end();
|
||||
if let Some(first) = rest.get(start)
|
||||
&& end.dist(first.start()) <= tol
|
||||
&& lp.len() > 1
|
||||
{
|
||||
break;
|
||||
}
|
||||
let back = (-seg.end_tangent()).angle();
|
||||
let next = (0..rest.len())
|
||||
.filter(|k| !used.get(*k).copied().unwrap_or(true) && rest.get(*k).is_some_and(|s| s.start().dist(end) <= tol))
|
||||
.min_by(|x, y| {
|
||||
let d = |k: usize| rest.get(k).map(|s| (back - s.start_tangent().angle()).rem_euclid(TAU)).unwrap_or(TAU);
|
||||
d(*x).total_cmp(&d(*y))
|
||||
});
|
||||
match next {
|
||||
Some(k) => cur = k,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
if !lp.is_empty() && lp.first().zip(lp.last()).is_some_and(|(f, l)| l.end().dist(f.start()) <= tol) {
|
||||
loops.push(Loop2 { segs: lp });
|
||||
}
|
||||
}
|
||||
let outers: Vec<&Loop2> = loops.iter().filter(|l| l.signed_area() > 0.0).collect();
|
||||
let holes: Vec<&Loop2> = loops.iter().filter(|l| l.signed_area() < 0.0).collect();
|
||||
let mut out: Vec<Region2> = outers.iter().map(|o| Region2 { outer: (*o).clone(), holes: Vec::new() }).collect();
|
||||
for h in holes {
|
||||
let probe = h.segs.first().map(Seg2::mid).unwrap_or_default();
|
||||
// Innermost containing outer loop.
|
||||
let best = out.iter_mut().filter(|r| r.outer.contains(probe)).min_by(|a, b| a.outer.signed_area().total_cmp(&b.outer.signed_area()));
|
||||
if let Some(r) = best {
|
||||
r.holes.push(h.clone());
|
||||
}
|
||||
}
|
||||
if out.is_empty() { regions.to_vec() } else { out }
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use solvecraft_geom::Vec2;
|
||||
use crate::linalg::{Mat, determined_vars, solve as lin_solve};
|
||||
use crate::model::{ConstraintKind, CurveKind, Sketch};
|
||||
|
||||
const MAX_ITERS: usize = 200;
|
||||
const MAX_ITERS: usize = 500;
|
||||
/// Converged when every residual is below this (mm).
|
||||
const TOL: f64 = 1e-9;
|
||||
/// A solution is accepted when every residual is below this (mm).
|
||||
@@ -254,21 +254,24 @@ fn residuals(s: &State, k: &ConstraintKind, out: &mut Vec<f64>) {
|
||||
}
|
||||
Radius { c, value } => out.push(s.round(c).1 - value),
|
||||
Diameter { c, value } => out.push(2.0 * s.round(c).1 - value),
|
||||
Angle { a, b, value } => {
|
||||
Angle { a, b, value, flip } => {
|
||||
let (a0, a1) = s.line(a);
|
||||
let (b0, b1) = s.line(b);
|
||||
let (da, db) = (a1 - a0, b1 - b0);
|
||||
let ang = da.cross(db).atan2(da.dot(db));
|
||||
let scale = 0.5 * (da.len() + db.len());
|
||||
out.push(wrap_angle(ang - value) * scale.max(1e-6));
|
||||
let target = if flip { value + std::f64::consts::PI } else { value };
|
||||
out.push(wrap_angle(ang - target) * scale.max(1e-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One residual block: implicit arc radius equality or a user constraint.
|
||||
/// One residual block: implicit arc radius equality, a user constraint, or a tangency at a known
|
||||
/// touch point (better conditioned than the distance form when an end point is on the curve).
|
||||
enum Block<'a> {
|
||||
Arc { c: usize, a: usize, b: usize },
|
||||
User(usize, &'a ConstraintKind),
|
||||
TangentAt(usize, &'a ConstraintKind, usize),
|
||||
}
|
||||
|
||||
fn block_residuals(s: &State, b: &Block, out: &mut Vec<f64>) {
|
||||
@@ -278,9 +281,45 @@ fn block_residuals(s: &State, b: &Block, out: &mut Vec<f64>) {
|
||||
out.push(cc.dist(s.p(*b)) - cc.dist(s.p(*a)));
|
||||
}
|
||||
Block::User(_, k) => residuals(s, k, out),
|
||||
Block::TangentAt(_, k, p) => {
|
||||
let ConstraintKind::Tangent { a, b } = **k else { return };
|
||||
let pt = s.p(*p);
|
||||
if s.is_line(a) || s.is_line(b) {
|
||||
let (l, c) = if s.is_line(a) { (a, b) } else { (b, a) };
|
||||
let (p0, p1) = s.line(l);
|
||||
let (cc, _) = s.round(c);
|
||||
let d = p1 - p0;
|
||||
// The radius to the touch point is perpendicular to the line.
|
||||
out.push((pt - cc).dot(d) / d.len().max(1e-12));
|
||||
} else {
|
||||
let (c1, _) = s.round(a);
|
||||
let (c2, _) = s.round(b);
|
||||
let (u, v) = (pt - c1, pt - c2);
|
||||
// Both centres on one line through the touch point.
|
||||
out.push(u.cross(v) / (u.len() + v.len()).max(1e-12));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A point where a tangency must happen: an end point shared by the two curves, or a line end
|
||||
/// point constrained onto the round curve.
|
||||
fn touch_point(sk: &Sketch, k: &ConstraintKind) -> Option<usize> {
|
||||
let ConstraintKind::Tangent { a, b } = *k else { return None };
|
||||
let ends = |c: usize| -> Vec<usize> {
|
||||
match sk.curves.get(c).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { a, b }) | Some(CurveKind::Arc { a, b, .. }) => vec![*a, *b],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
};
|
||||
let (ea, eb) = (ends(a), ends(b));
|
||||
if let Some(p) = ea.iter().find(|p| eb.contains(p)) {
|
||||
return Some(*p);
|
||||
}
|
||||
let on = |p: usize, c: usize| sk.constraints.iter().any(|x| x.kind == ConstraintKind::PointOnCurve { p, c });
|
||||
ea.iter().find(|p| on(**p, b)).or_else(|| eb.iter().find(|p| on(**p, a))).copied()
|
||||
}
|
||||
|
||||
/// Variables a block depends on.
|
||||
fn block_vars(sk: &Sketch, lay: &Layout, b: &Block) -> Vec<usize> {
|
||||
let mut v = Vec::new();
|
||||
@@ -345,6 +384,13 @@ fn block_vars(sk: &Sketch, lay: &Layout, b: &Block) -> Vec<usize> {
|
||||
}
|
||||
Fix { .. } => {}
|
||||
},
|
||||
Block::TangentAt(_, k, p) => {
|
||||
pt(&mut v, *p);
|
||||
if let ConstraintKind::Tangent { a, b } = **k {
|
||||
curve(&mut v, a);
|
||||
curve(&mut v, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
v.sort_unstable();
|
||||
v.dedup();
|
||||
@@ -517,7 +563,10 @@ pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||||
}
|
||||
}
|
||||
for (i, c) in sk.constraints.iter().enumerate() {
|
||||
let bl = Block::User(i, &c.kind);
|
||||
let bl = match touch_point(sk, &c.kind) {
|
||||
Some(p) => Block::TangentAt(i, &c.kind, p),
|
||||
None => Block::User(i, &c.kind),
|
||||
};
|
||||
let vars = block_vars(sk, &lay, &bl);
|
||||
blocks.push((bl, vars));
|
||||
}
|
||||
@@ -569,6 +618,7 @@ pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||||
let b2 = match b {
|
||||
Block::Arc { c, a, b } => Block::Arc { c: *c, a: *a, b: *b },
|
||||
Block::User(i, k) => Block::User(*i, k),
|
||||
Block::TangentAt(i, k, p) => Block::TangentAt(*i, k, *p),
|
||||
};
|
||||
(b2, v.clone())
|
||||
})
|
||||
@@ -583,7 +633,7 @@ pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||||
let mut failing: Vec<String> = Vec::new();
|
||||
for (row, v) in r.iter().enumerate() {
|
||||
if !(v.abs() < ACCEPT)
|
||||
&& let Some((Block::User(ci, _), _)) = owner.get(row).and_then(|bi| all_blocks.get(*bi))
|
||||
&& let Some((Block::User(ci, _) | Block::TangentAt(ci, _, _), _)) = owner.get(row).and_then(|bi| all_blocks.get(*bi))
|
||||
&& let Some(c) = sk.constraints.get(*ci)
|
||||
&& !failing.contains(&c.id)
|
||||
{
|
||||
|
||||
@@ -121,7 +121,7 @@ fn line_tangent_to_circle_and_angles() {
|
||||
let (a, _) = line_pts(&sk, l);
|
||||
assert!((a.y.abs() - 10.0).abs() < 1e-7, "{a:?}");
|
||||
let m = sk.add_line(v(0.0, 0.0), v(10.0, 3.0), Some(0), None, None).unwrap();
|
||||
sk.add_constraint(Angle { a: l, b: m, value: PI / 6.0 }, None).unwrap();
|
||||
sk.add_constraint(Angle { a: l, b: m, value: PI / 6.0, flip: false }, None).unwrap();
|
||||
sk.add_constraint(Length { l: m, value: 8.0 }, None).unwrap();
|
||||
assert!(solve(&mut sk).ok());
|
||||
let (p, q) = line_pts(&sk, m);
|
||||
@@ -250,3 +250,54 @@ fn hostile_values_rejected() {
|
||||
assert!(sk.add_constraint(ConstraintKind::Radius { c, value: f64::NAN }, None).is_err());
|
||||
assert!(sk.add_constraint(ConstraintKind::Radius { c, value: -2.0 }, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiles_split_at_crossings_and_touch_points() {
|
||||
// A line across a circle: two half discs.
|
||||
let mut sk = Sketch::new();
|
||||
sk.add_circle(v(0.0, 0.0), 10.0, None, None).unwrap();
|
||||
sk.add_line(v(-15.0, 0.0), v(15.0, 0.0), None, None, None).unwrap();
|
||||
let ps = find_profiles(&sk);
|
||||
assert_eq!(ps.len(), 2, "{ps:?}");
|
||||
for p in &ps {
|
||||
assert!((p.area - PI * 50.0).abs() < 1e-6, "{}", p.area);
|
||||
}
|
||||
// Belt: two circles joined by two tangent lines whose ends sit on the circles.
|
||||
let mut b = Sketch::new();
|
||||
let c1 = b.add_circle(v(0.0, 0.0), 20.0, Some(0), None).unwrap();
|
||||
let c2 = b.add_circle(v(50.0, 0.0), 10.0, None, None).unwrap();
|
||||
let l1 = b.add_line(v(3.0, 19.0), v(47.0, 13.0), None, None, None).unwrap();
|
||||
let l2 = b.add_line(v(1.0, -22.0), v(44.0, -8.0), None, None, None).unwrap();
|
||||
use ConstraintKind::*;
|
||||
for (l, end, c) in [(l1, "start", c1), (l1, "end", c2), (l2, "start", c1), (l2, "end", c2)] {
|
||||
let p = b.resolve_point(&format!("{}.{end}", b.curves[l].id)).unwrap();
|
||||
b.add_constraint(PointOnCurve { p, c }, None).unwrap();
|
||||
}
|
||||
for (l, c) in [(l1, c1), (l1, c2), (l2, c1), (l2, c2)] {
|
||||
b.add_constraint(Tangent { a: l, b: c }, None).unwrap();
|
||||
}
|
||||
b.add_constraint(Radius { c: c1, value: 20.0 }, None).unwrap();
|
||||
b.add_constraint(Radius { c: c2, value: 10.0 }, None).unwrap();
|
||||
let r = solve(&mut b);
|
||||
assert!(r.ok(), "{r:?}");
|
||||
let ps = find_profiles(&b);
|
||||
assert_eq!(ps.len(), 3, "two discs and the band between them: {:?}", ps.iter().map(|p| p.area).collect::<Vec<_>>());
|
||||
let total: f64 = ps.iter().map(|p| p.area).sum();
|
||||
assert!(total > PI * 500.0, "{total}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merging_adjacent_regions() {
|
||||
let a =
|
||||
solvecraft_geom::Region2 { outer: solvecraft_geom::Loop2::polygon(&[v(0.0, 0.0), v(10.0, 0.0), v(10.0, 10.0), v(0.0, 10.0)]), holes: vec![] };
|
||||
let b = solvecraft_geom::Region2 {
|
||||
outer: solvecraft_geom::Loop2::polygon(&[v(10.0, 0.0), v(20.0, 0.0), v(20.0, 10.0), v(10.0, 10.0)]),
|
||||
holes: vec![],
|
||||
};
|
||||
let c = solvecraft_geom::Region2 { outer: solvecraft_geom::Loop2::polygon(&[v(50.0, 0.0), v(60.0, 0.0), v(60.0, 10.0)]), holes: vec![] };
|
||||
let m = merge_regions(&[a, b, c]);
|
||||
assert_eq!(m.len(), 2);
|
||||
let mut areas: Vec<f64> = m.iter().map(|r| r.area()).collect();
|
||||
areas.sort_by(f64::total_cmp);
|
||||
assert!((areas[0] - 50.0).abs() < 1e-9 && (areas[1] - 200.0).abs() < 1e-9, "{areas:?}");
|
||||
}
|
||||
Reference in new issue
Block a user