Sketch drag: move only what the constraints need (#2060)
Thanks @jijinbei.
This commit is contained in:
1 parent
b8d7b42808
commit
932efbf341
4 files changed
+97
-12
No files matched your search
@@ -0,0 +1,61 @@
|
||||
//! A solve with geometry held moves the rest as little as it can.
|
||||
|
||||
use solidcraft_geom::Vec2;
|
||||
|
||||
use crate::{Analysis, ConstraintKind, Hold, Sketch, solve_with};
|
||||
|
||||
fn v(x: f64, y: f64) -> Vec2 {
|
||||
Vec2::new(x, y)
|
||||
}
|
||||
|
||||
/// A rectangle with a divider whose ends are held on the bottom and top sides (point on curve).
|
||||
/// Dragging the top-left corner tilts the top side: the divider's top end slides along with it,
|
||||
/// the far corner stays.
|
||||
#[test]
|
||||
fn a_point_held_on_a_line_slides_instead_of_swinging_the_line() {
|
||||
let mut sk = Sketch::new();
|
||||
let c: Vec<usize> = [v(0.0, 0.0), v(40.0, 0.0), v(40.0, 30.0), v(0.0, 30.0)].iter().map(|p| sk.add_point(*p, None).unwrap()).collect();
|
||||
let sides: Vec<usize> = (0..4).map(|i| sk.add_line_pts(c[i], c[(i + 1) % 4], None).unwrap()).collect();
|
||||
let bottom = sk.add_point(v(20.0, 0.0), None).unwrap();
|
||||
let top = sk.add_point(v(20.0, 30.0), None).unwrap();
|
||||
sk.add_line_pts(bottom, top, None).unwrap();
|
||||
sk.add_constraint(ConstraintKind::PointOnCurve { p: bottom, c: sides[0] }, None).unwrap();
|
||||
sk.add_constraint(ConstraintKind::PointOnCurve { p: top, c: sides[2] }, None).unwrap();
|
||||
let before: Vec<Vec2> = sk.points.iter().map(|p| p.pos).collect();
|
||||
sk.points[c[3]].pos = v(1.0, 31.0);
|
||||
let rep = solve_with(&mut sk, &Hold { points: vec![c[3]], radii: vec![] }, Some(&before), Analysis::StatusOnly, None);
|
||||
assert!(rep.ok(), "{rep:?}");
|
||||
let moved = |i: usize| sk.points[i].pos.dist(before[i]);
|
||||
// The top side through (1, 31) and (40, 30) passes x = 20 at y ≈ 30.51: the divider's end
|
||||
// needs about half a millimetre. The far corner needs nothing.
|
||||
assert!(moved(top) < 0.6, "divider end moved {}", moved(top));
|
||||
assert!(moved(c[2]) < 0.3, "far corner moved {}", moved(c[2]));
|
||||
assert!(moved(c[1]) < 1e-6 && moved(bottom) < 1e-6, "the bottom has no reason to move");
|
||||
}
|
||||
|
||||
/// The same rectangle, dragging the top-right corner down: the top side turns about its other
|
||||
/// corner, which stays, and the divider's end slides down with it.
|
||||
#[test]
|
||||
fn the_far_corner_stays_when_the_side_a_point_is_held_on_turns() {
|
||||
let mut sk = Sketch::new();
|
||||
let c: Vec<usize> = [v(0.0, 0.0), v(40.0, 0.0), v(40.0, 30.0), v(0.0, 30.0)].iter().map(|p| sk.add_point(*p, None).unwrap()).collect();
|
||||
let sides: Vec<usize> = (0..4).map(|i| sk.add_line_pts(c[i], c[(i + 1) % 4], None).unwrap()).collect();
|
||||
let bottom = sk.add_point(v(20.0, 0.0), None).unwrap();
|
||||
let top = sk.add_point(v(20.0, 30.0), None).unwrap();
|
||||
sk.add_line_pts(bottom, top, None).unwrap();
|
||||
sk.add_constraint(ConstraintKind::PointOnCurve { p: bottom, c: sides[0] }, None).unwrap();
|
||||
sk.add_constraint(ConstraintKind::PointOnCurve { p: top, c: sides[2] }, None).unwrap();
|
||||
// Down in steps, as a live drag goes: each solve starts from the last.
|
||||
for k in 1..=10 {
|
||||
let before: Vec<Vec2> = sk.points.iter().map(|p| p.pos).collect();
|
||||
sk.points[c[2]].pos = v(40.0, 30.0 - k as f64);
|
||||
let rep = solve_with(&mut sk, &Hold { points: vec![c[2]], radii: vec![] }, Some(&before), Analysis::StatusOnly, None);
|
||||
assert!(rep.ok(), "{rep:?}");
|
||||
}
|
||||
let corner = sk.points[c[3]].pos;
|
||||
assert!(corner.dist(v(0.0, 30.0)) < 0.1, "the far corner went to {corner:?}");
|
||||
// On the turned side, near where it was.
|
||||
let (end, a, b) = (sk.points[top].pos, corner, sk.points[c[2]].pos);
|
||||
let off_side = ((b - a).cross(end - a) / a.dist(b)).abs();
|
||||
assert!(off_side < 1e-6 && (end.x - 20.0).abs() < 1.0, "the divider's end is at {end:?}");
|
||||
}
|
||||
@@ -80,6 +80,8 @@ mod ends_tests;
|
||||
#[cfg(test)]
|
||||
mod font_tests;
|
||||
#[cfg(test)]
|
||||
mod least_motion_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod thin_tests;
|
||||
|
||||
@@ -123,7 +123,7 @@ pub(super) fn dependent_blocks(
|
||||
/// dimension (it constrains nothing) or an unknown index.
|
||||
pub fn constraint_residual(sk: &Sketch, ci: usize) -> Option<f64> {
|
||||
let c = sk.constraints.get(ci).filter(|c| !c.driven)?;
|
||||
let fixed = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||||
let fixed = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0, slide: Vec::new() };
|
||||
let on_curve = points_on_curves(sk);
|
||||
let b = match super::touch_point(sk, &c.kind, &on_curve) {
|
||||
Some(p) => Block::TangentAt(ci, &c.kind, p),
|
||||
|
||||
+33
-11
@@ -36,6 +36,9 @@ const MAX_COMPONENT_VARS: usize = 4_000;
|
||||
const TOL: f64 = 1e-9;
|
||||
/// A solution is accepted when every residual is below this (mm).
|
||||
const ACCEPT: f64 = 1e-6;
|
||||
/// How much less it costs a solve to move a point held on a curve than any other point: such a
|
||||
/// point is placed relative to the curve, so it follows the curve rather than the curve it.
|
||||
const SLIDE_WEIGHT: f64 = 0.01;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -107,6 +110,9 @@ struct Layout {
|
||||
n: usize,
|
||||
/// The point coordinates' variables: `0..points`.
|
||||
points: usize,
|
||||
/// Per variable: a coordinate of a point held on a curve (point on curve, midpoint), which
|
||||
/// a solve slides along it before moving the curve (see [`SLIDE_WEIGHT`]).
|
||||
slide: Vec<bool>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
@@ -145,7 +151,7 @@ pub(crate) fn held_points(sk: &Sketch, hold: &Hold) -> Vec<bool> {
|
||||
|
||||
fn layout(sk: &Sketch, hold: &Hold) -> Layout {
|
||||
let mut n = 0;
|
||||
let pvar = held_points(sk, hold)
|
||||
let pvar: Vec<Option<usize>> = held_points(sk, hold)
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if *f {
|
||||
@@ -169,7 +175,19 @@ fn layout(sk: &Sketch, hold: &Hold) -> Layout {
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
Layout { pvar, rvar, n, points }
|
||||
let mut slide = vec![false; n];
|
||||
for c in &sk.constraints {
|
||||
if let ConstraintKind::PointOnCurve { p, .. } | ConstraintKind::Midpoint { p, .. } = c.kind
|
||||
&& let Some(Some(v)) = pvar.get(p)
|
||||
{
|
||||
for k in [*v, *v + 1] {
|
||||
if let Some(s) = slide.get_mut(k) {
|
||||
*s = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout { pvar, rvar, n, points, slide }
|
||||
}
|
||||
|
||||
/// Read-only view of the sketch with variable values substituted.
|
||||
@@ -618,7 +636,7 @@ fn measure(s: &State, k: &ConstraintKind) -> Option<f64> {
|
||||
/// What a dimension measures on the sketch as it stands (mm, radians for angles); `None` for a
|
||||
/// constraint that is not a dimension.
|
||||
pub fn measure_dimension(sk: &Sketch, k: &ConstraintKind) -> Option<f64> {
|
||||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0, slide: Vec::new() };
|
||||
measure(&State { sk, lay: &lay, x: &[] }, k).filter(|v| v.is_finite())
|
||||
}
|
||||
|
||||
@@ -954,13 +972,17 @@ fn lm_capped(
|
||||
}
|
||||
}
|
||||
let mut improved = false;
|
||||
// Damping floor: variables the residuals barely see must not take huge steps.
|
||||
let floor = (0..n).map(|i| a.get(i, i)).fold(0.0_f64, f64::max) * 1e-4;
|
||||
// Damping by distance moved, not by JᵀJ's diagonal (Marquardt): of the steps that fit the
|
||||
// residuals equally well it takes the shortest, so what the constraints leave free moves
|
||||
// as little as it can. (Scaled by the diagonal, a variable the residuals see weakly is
|
||||
// cheap to move: a point held on a line made the line's far end swing round instead of
|
||||
// the point sliding along it.) A point held on a curve is cheaper still, so it slides
|
||||
// rather than the curve's ends moving.
|
||||
let weight: Vec<f64> = vars.iter().map(|&v| if lay.slide.get(v).copied().unwrap_or(false) { SLIDE_WEIGHT } else { 1.0 }).collect();
|
||||
for _ in 0..30 {
|
||||
let mut m = a.clone();
|
||||
for i in 0..n {
|
||||
let d = a.get(i, i);
|
||||
m.add(i, i, lambda * (d.max(floor) + 1e-9) + 1e-12);
|
||||
for (i, w) in weight.iter().enumerate() {
|
||||
m.add(i, i, lambda * w + 1e-12);
|
||||
}
|
||||
let rhs: Vec<f64> = g.iter().map(|v| -v).collect();
|
||||
let Some(step) = lin_solve(m, rhs) else {
|
||||
@@ -2201,7 +2223,7 @@ fn solve_inner(
|
||||
None => true,
|
||||
};
|
||||
// Driven dimensions measure the solved geometry.
|
||||
let fixed_lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||||
let fixed_lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0, slide: Vec::new() };
|
||||
let measured: Vec<Option<f64>> =
|
||||
sk.constraints.iter().map(|c| if c.driven { measure(&State { sk, lay: &fixed_lay, x: &[] }, &c.kind) } else { None }).collect();
|
||||
for (c, m) in sk.constraints.iter_mut().zip(measured) {
|
||||
@@ -2265,7 +2287,7 @@ mod continuation_unit_tests {
|
||||
let a = sk.add_point(Vec2::new(0.0, 0.0), None).unwrap();
|
||||
let b = sk.add_point(Vec2::new(40.0, 0.0), None).unwrap();
|
||||
let c = sk.add_point(Vec2::new(20.0, 1e-6), None).unwrap();
|
||||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0, slide: Vec::new() };
|
||||
let turn = Orient::Turn([a, b, c]);
|
||||
let state =
|
||||
|sk: &Sketch| (orientation(&State { sk, lay: &lay, x: &[] }, turn), reference_orientation(&State { sk, lay: &lay, x: &[] }, turn));
|
||||
@@ -2285,7 +2307,7 @@ mod continuation_unit_tests {
|
||||
let q = sk.add_point(Vec2::new(10.0, 5.0), None).unwrap();
|
||||
let r = sk.add_point(Vec2::new(0.0, 10.0), None).unwrap();
|
||||
let l = sk.add_line(Vec2::new(-5.0, -5.0), Vec2::new(5.0, -5.0), None, None, None).unwrap();
|
||||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0 };
|
||||
let lay = Layout { pvar: vec![None; sk.points.len()], rvar: vec![None; sk.curves.len()], n: 0, points: 0, slide: Vec::new() };
|
||||
let sign = |sk: &Sketch, o: Orient| orientation(&State { sk, lay: &lay, x: &[] }, o);
|
||||
let checks = [Orient::SideX(p, q, 10.0), Orient::SideY(p, q, 5.0), Orient::OfLine(q, l, 10.0), Orient::Turn([p, q, r])];
|
||||
let before: Vec<_> = checks.iter().map(|o| sign(&sk, *o)).collect();
|
||||
|
||||
Reference in new issue
Block a user