feat: stabilize face attachments and closed spline workflows
This commit is contained in:
1 parent
c1d819f235
commit
a6bb8fc924
83 files changed
+6271
-385
No files matched your search
+108
-12
@@ -169,9 +169,9 @@ impl State<'_> {
|
||||
};
|
||||
crate::curves::ellipse_polyline(self.p(*ci), self.p(*m), r)
|
||||
}
|
||||
Some(CurveKind::Spline { pts, control, degree }) => {
|
||||
let p: Vec<Vec2> = pts.iter().map(|q| self.p(*q)).collect();
|
||||
crate::curves::spline_polyline(&p, *control, *degree)
|
||||
Some(CurveKind::Spline { pts, control, degree, periodic }) => {
|
||||
let p = self.spline_points(pts, *periodic);
|
||||
crate::curves::spline_polyline_mode(&p, *control, *degree, *periodic)
|
||||
}
|
||||
Some(CurveKind::Conic { a, b, apex, rho }) => crate::curves::conic_polyline(self.p(*a), self.p(*apex), self.p(*b), *rho),
|
||||
_ => Vec::new(),
|
||||
@@ -179,6 +179,16 @@ impl State<'_> {
|
||||
}
|
||||
/// Point-on-curve constraints use the exact shape. A polyline residual would move an
|
||||
/// exact crossing onto its chord, changing geometry merely by splitting it.
|
||||
fn spline_points(&self, pts: &[usize], periodic: bool) -> Vec<Vec2> {
|
||||
let mut points: Vec<Vec2> = pts.iter().map(|i| self.p(*i)).collect();
|
||||
if periodic
|
||||
&& let Some(first) = points.first().copied()
|
||||
&& let Some(last) = points.last_mut()
|
||||
{
|
||||
*last = first;
|
||||
}
|
||||
points
|
||||
}
|
||||
fn freeform_dist(&self, c: usize, q: Vec2) -> f64 {
|
||||
let segs: Vec<Seg2> = match self.sk.curves.get(c).map(|c| &c.kind) {
|
||||
Some(CurveKind::Ellipse { c: ci, m, .. }) => {
|
||||
@@ -194,8 +204,8 @@ impl State<'_> {
|
||||
let y = delta.cross(u) / (a * r);
|
||||
return (x.hypot(y) - 1.0) * a.min(r);
|
||||
}
|
||||
Some(CurveKind::Spline { pts, control, degree }) => {
|
||||
let p: Vec<Vec2> = pts.iter().map(|i| self.p(*i)).collect();
|
||||
Some(CurveKind::Spline { pts, control, degree, periodic }) => {
|
||||
let p = self.spline_points(pts, *periodic);
|
||||
if p.len() < 2 {
|
||||
return q.dist(p.first().copied().unwrap_or_default());
|
||||
}
|
||||
@@ -203,7 +213,11 @@ impl State<'_> {
|
||||
let k = (*degree as usize).min(solvecraft_geom::MAX_BEZIER_DEGREE).min(p.len().saturating_sub(1)).max(1);
|
||||
crate::curves::bspline_beziers(&p, k).iter().filter_map(|span| solvecraft_geom::bezier(span)).collect()
|
||||
} else {
|
||||
crate::curves::fit_beziers(&p).into_iter().map(|[p0, p1, p2, p3]| Seg2::Cubic { p0, p1, p2, p3 }).collect()
|
||||
crate::curves::fit_beziers_mode(&p, *periodic)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|[p0, p1, p2, p3]| Seg2::Cubic { p0, p1, p2, p3 })
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
Some(CurveKind::Conic { a, b, apex, rho }) => {
|
||||
@@ -269,10 +283,10 @@ impl State<'_> {
|
||||
let (cc, r) = self.round(c);
|
||||
(cc - self.p(p)) / (r * r).max(1e-24)
|
||||
}
|
||||
CurveKind::Spline { pts, control, degree } => {
|
||||
let q: Vec<Vec2> = pts.iter().map(|i| self.p(*i)).collect();
|
||||
CurveKind::Spline { pts, control, degree, periodic } => {
|
||||
let q = self.spline_points(pts, *periodic);
|
||||
let at_end = pts.last() == Some(&p) && pts.first() != Some(&p);
|
||||
crate::curves::end_curvature(&|t| crate::curves::spline_point(&q, *control, *degree, t), at_end)
|
||||
crate::curves::end_curvature(&|t| crate::curves::spline_point_mode(&q, *control, *degree, *periodic, t), at_end)
|
||||
}
|
||||
CurveKind::Conic { a, b, apex, rho } => {
|
||||
let (pa, pb, px) = (self.p(*a), self.p(*b), self.p(*apex));
|
||||
@@ -285,9 +299,9 @@ impl State<'_> {
|
||||
match self.sk.curves.get(c).map(|c| &c.kind)? {
|
||||
CurveKind::Line { a, b } => (self.p(*b) - self.p(*a)).normalized(),
|
||||
CurveKind::Circle { .. } | CurveKind::Arc { .. } => (self.p(p) - self.round(c).0).perp().normalized(),
|
||||
CurveKind::Spline { pts, control, .. } => {
|
||||
let q: Vec<Vec2> = pts.iter().map(|i| self.p(*i)).collect();
|
||||
crate::curves::spline_end_tangent(&q, *control, pts.last() == Some(&p) && pts.first() != Some(&p))
|
||||
CurveKind::Spline { pts, control, periodic, .. } => {
|
||||
let q = self.spline_points(pts, *periodic);
|
||||
crate::curves::spline_end_tangent_mode(&q, *control, *periodic, pts.last() == Some(&p) && pts.first() != Some(&p))
|
||||
}
|
||||
CurveKind::Conic { a, b, apex, .. } => {
|
||||
if p == *a {
|
||||
@@ -540,6 +554,7 @@ pub fn measure_dimension(sk: &Sketch, k: &ConstraintKind) -> Option<f64> {
|
||||
/// 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> {
|
||||
Closure { a: usize, b: usize },
|
||||
Arc { c: usize, a: usize, b: usize },
|
||||
User(usize, &'a ConstraintKind),
|
||||
TangentAt(usize, &'a ConstraintKind, usize),
|
||||
@@ -547,6 +562,7 @@ enum Block<'a> {
|
||||
|
||||
fn copy_block<'a>(b: &Block<'a>) -> Block<'a> {
|
||||
match b {
|
||||
Block::Closure { a, b } => Block::Closure { a: *a, b: *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),
|
||||
@@ -555,6 +571,10 @@ fn copy_block<'a>(b: &Block<'a>) -> Block<'a> {
|
||||
|
||||
fn block_residuals(s: &State, b: &Block, out: &mut Vec<f64>) {
|
||||
match b {
|
||||
Block::Closure { a, b } => {
|
||||
let d = s.p(*a) - s.p(*b);
|
||||
out.extend([d.x, d.y]);
|
||||
}
|
||||
Block::Arc { c, a, b } => {
|
||||
let cc = s.p(*c);
|
||||
out.push(cc.dist(s.p(*b)) - cc.dist(s.p(*a)));
|
||||
@@ -625,6 +645,10 @@ fn block_vars(sk: &Sketch, lay: &Layout, b: &Block) -> Vec<usize> {
|
||||
};
|
||||
use ConstraintKind::*;
|
||||
match b {
|
||||
Block::Closure { a, b } => {
|
||||
pt(&mut v, *a);
|
||||
pt(&mut v, *b);
|
||||
}
|
||||
Block::Arc { c, a, b } => {
|
||||
pt(&mut v, *c);
|
||||
pt(&mut v, *a);
|
||||
@@ -947,6 +971,14 @@ pub fn solve_holding(sk: &mut Sketch, hold: &Hold) -> SolveReport {
|
||||
|
||||
let mut blocks: Vec<(Block, Vec<usize>)> = Vec::new();
|
||||
for c in &sk.curves {
|
||||
if let CurveKind::Spline { pts, periodic: true, .. } = &c.kind
|
||||
&& let (Some(a), Some(b)) = (pts.first(), pts.last())
|
||||
&& a != b
|
||||
{
|
||||
let bl = Block::Closure { a: *a, b: *b };
|
||||
let vars = block_vars(sk, &lay, &bl);
|
||||
blocks.push((bl, vars));
|
||||
}
|
||||
if let CurveKind::Arc { c, a, b } = c.kind {
|
||||
let bl = Block::Arc { c, a, b };
|
||||
let vars = block_vars(sk, &lay, &bl);
|
||||
@@ -1067,6 +1099,63 @@ pub fn solve_holding(sk: &mut Sketch, hold: &Hold) -> SolveReport {
|
||||
let (r, _, owner) = system(sk, &lay, &consts, &mut x, false);
|
||||
check(&r, &owner, &consts, &mut failing);
|
||||
}
|
||||
// Weld the whole connected closure graph, preserving every immutable node.
|
||||
// Validate the actual prospective geometry before accepting a numerical solve.
|
||||
let has_periodic = sk.curves.iter().any(|c| matches!(c.kind, CurveKind::Spline { periodic: true, .. }));
|
||||
let periodic_positions = if has_periodic {
|
||||
let state = State { sk, lay: &lay, x: &x };
|
||||
let mut positions = (0..sk.points.len()).map(|i| state.p(i)).collect::<Vec<_>>();
|
||||
let mut parent = (0..positions.len()).collect::<Vec<_>>();
|
||||
for curve in &sk.curves {
|
||||
if let CurveKind::Spline { pts, periodic: true, .. } = &curve.kind
|
||||
&& let (Some(a), Some(b)) = (pts.first(), pts.last())
|
||||
&& *a < parent.len()
|
||||
&& *b < parent.len()
|
||||
{
|
||||
let (a, b) = (find(&mut parent, *a), find(&mut parent, *b));
|
||||
if let Some(slot) = parent.get_mut(b) {
|
||||
*slot = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut representatives = std::collections::BTreeMap::new();
|
||||
let mut valid = true;
|
||||
for i in 0..positions.len() {
|
||||
let root = find(&mut parent, i);
|
||||
if let Some(previous) = representatives.get(&root).copied() {
|
||||
let fixed = |p| lay.pvar.get(p).is_some_and(Option::is_none);
|
||||
if fixed(i) && fixed(previous) && positions[i] != positions[previous] {
|
||||
valid = false;
|
||||
}
|
||||
if fixed(i) && !fixed(previous) {
|
||||
representatives.insert(root, i);
|
||||
}
|
||||
} else {
|
||||
representatives.insert(root, i);
|
||||
}
|
||||
}
|
||||
for (i, position) in positions.iter_mut().enumerate() {
|
||||
if let Some(index) = representatives.get(&find(&mut parent, i)) {
|
||||
*position = state.p(*index);
|
||||
}
|
||||
}
|
||||
for curve in &sk.curves {
|
||||
if let CurveKind::Spline { pts, control, degree, periodic: true } = &curve.kind {
|
||||
let points = pts.iter().map(|i| positions.get(*i).copied()).collect::<Option<Vec<_>>>();
|
||||
if *control || *degree != 3 || points.as_ref().is_none_or(|p| crate::periodic::validate_periodic_points(p).is_err()) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
max_residual = max_residual.max(1.0);
|
||||
None
|
||||
} else {
|
||||
Some(positions)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let status = if max_residual < ACCEPT { SolveStatus::Solved } else { SolveStatus::Failed };
|
||||
let use_x = if status == SolveStatus::Solved { &x } else { &x0 };
|
||||
// Write back.
|
||||
@@ -1080,6 +1169,13 @@ pub fn solve_holding(sk: &mut Sketch, hold: &Hold) -> SolveReport {
|
||||
*r = use_x.get(v).copied().unwrap_or(*r).abs().max(1e-9);
|
||||
}
|
||||
}
|
||||
if status == SolveStatus::Solved
|
||||
&& let Some(positions) = periodic_positions
|
||||
{
|
||||
for (point, position) in sk.points.iter_mut().zip(positions) {
|
||||
point.pos = position;
|
||||
}
|
||||
}
|
||||
if status == SolveStatus::Failed {
|
||||
for (ci, a_before, b_before, reversed) in orientation_before.into_iter().rev() {
|
||||
if let Some(curve) = sk.curves.get_mut(ci)
|
||||
|
||||
Reference in new issue
Block a user