The conic tool takes a typed Rho in its canvas value box and previews the curve it's about to draw, with the tangent legs as a faint guide.
567 lines
24 KiB
Rust
567 lines
24 KiB
Rust
//! Dragging sketch geometry without jumping to another solution.
|
|
//!
|
|
//! A constrained sketch usually has several solutions for one cursor position (a two-link arm's
|
|
//! elbow up or down, a fillet inside or outside its corner). Each held solve of a drag is seeded
|
|
//! on the positions from before it: the points the solver's construction places (from distances
|
|
//! and alignments to placed points) keep the orientation they had there however far the step.
|
|
//! The rest is left to the iteration, which from far away can land on any solution, so a long
|
|
//! drag still walks from where the geometry is to where the cursor puts it in short steps, each
|
|
//! solved from the previous step's result (continuation). Continuation is what keeps lengths the
|
|
//! construction does not read (an Equal to another line, a rigid shape hanging off a held point);
|
|
//! it also lets the arc sweep guard see every step, and lets a path that crosses where the
|
|
//! geometry cannot reach stop short at the edge of that stretch and go on past it. It steps only
|
|
//! when the move is long next to the dragged geometry, so a normal mouse frame is a single solve.
|
|
|
|
use std::f64::consts::{PI, TAU};
|
|
|
|
use solidcraft_geom::Vec2;
|
|
use solidcraft_sketch::{Analysis, Budget, ConstraintKind, CurveKind, Hold, Sketch, conic_rho_from_point};
|
|
|
|
use super::{Ent, Left, counted_solve};
|
|
|
|
/// Longest step, as a fraction of the smallest driving length or radius of the dragged group.
|
|
/// Each step is seeded on its pre-step positions, so what the seed constructs keeps its branch
|
|
/// whatever the step; the steps are for what it does not (an arc's sweep guard, paths across
|
|
/// unreachable stretches). The drag tests first fail at 1 (an arc end wraps through its start):
|
|
/// this keeps a factor of four. It holds where the seed placed what it could: once a held
|
|
/// solve's decomposition runs out of budget (see [`Budget::exhausted`]), the rest of the drag
|
|
/// takes `SHORT_STEP_FRACTION` steps, and continuation keeps the branch.
|
|
const STEP_FRACTION: f64 = 0.25;
|
|
/// The steps of a drag whose seed did not place everything it could (as before seeding steps).
|
|
const SHORT_STEP_FRACTION: f64 = 0.125;
|
|
/// Shortest step (mm), so a tiny feature cannot make a drag crawl.
|
|
const MIN_STEP: f64 = 1e-3;
|
|
/// Most steps one drag is split into.
|
|
const MAX_STEPS: usize = 64;
|
|
/// After a step fails, the way to it from the last solved state is halved until what is left is
|
|
/// shorter than a step over this; at most `MAX_STEPS` such solves per drag.
|
|
const HALVINGS: usize = 16;
|
|
/// An arc whose sweep gets this close (radians) to 0 or a full turn has run into its start.
|
|
const SWEEP_EPS: f64 = 1e-6;
|
|
|
|
/// What a drag moves, from where it is (`t = 0`) to where the cursor puts it (`t = 1`).
|
|
enum Path {
|
|
/// Points slide by `d` (a dragged point by its way to the cursor).
|
|
Slide { pts: Vec<(usize, Vec2)>, d: Vec2 },
|
|
/// A circle's radius or an ellipse's minor radius follows the cursor.
|
|
Radius { c: usize, from: f64, to: f64 },
|
|
/// An arc's ends move along their rays from the centre `o` to the cursor's radius.
|
|
ArcRadius { o: Vec2, ends: Vec<(usize, Vec2)>, from: f64, to: f64 },
|
|
/// A conic's `rho` follows the cursor, read off its position between the chord's midpoint
|
|
/// and the apex (see [`solidcraft_sketch::conic_rho_from_point`]). `a`, `b` and `apex` stay
|
|
/// put; only the curve's fullness changes, the way an ellipse's outline changes its minor
|
|
/// radius rather than sliding the ellipse.
|
|
ConicRho { c: usize, from: f64, to: f64 },
|
|
}
|
|
|
|
impl Path {
|
|
fn length(&self) -> f64 {
|
|
match self {
|
|
Path::Slide { d, .. } => d.len(),
|
|
Path::Radius { from, to, .. } | Path::ArcRadius { from, to, .. } | Path::ConicRho { from, to, .. } => (to - from).abs(),
|
|
}
|
|
}
|
|
|
|
/// The points the drag moves.
|
|
fn points(&self) -> impl Iterator<Item = usize> + '_ {
|
|
let pts = match self {
|
|
Path::Slide { pts, .. } | Path::ArcRadius { ends: pts, .. } => pts.as_slice(),
|
|
Path::Radius { .. } | Path::ConicRho { .. } => &[],
|
|
};
|
|
pts.iter().map(|(q, _)| *q)
|
|
}
|
|
|
|
/// Put the dragged geometry where it is at `t` along the drag.
|
|
fn apply(&self, sk: &mut Sketch, t: f64) {
|
|
let mut set = |q: usize, at: Vec2| {
|
|
if let Some(pt) = sk.points.get_mut(q) {
|
|
pt.pos = at;
|
|
}
|
|
};
|
|
match self {
|
|
Path::Slide { pts, d } => {
|
|
for &(q, at) in pts {
|
|
set(q, at + *d * t);
|
|
}
|
|
}
|
|
Path::ArcRadius { o, ends, from, to } => {
|
|
let r = from + (to - from) * t;
|
|
for &(q, u) in ends {
|
|
set(q, *o + u * r);
|
|
}
|
|
}
|
|
Path::Radius { c, from, to } => {
|
|
if let Some(CurveKind::Circle { r, .. } | CurveKind::Ellipse { r, .. }) = sk.curves.get_mut(*c).map(|x| &mut x.kind) {
|
|
*r = from + (to - from) * t;
|
|
}
|
|
}
|
|
Path::ConicRho { c, from, to } => {
|
|
if let Some(CurveKind::Conic { rho, .. }) = sk.curves.get_mut(*c).map(|x| &mut x.kind) {
|
|
*rho = from + (to - from) * t;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Drag: the grabbed entity follows the cursor exactly (a point goes to it; a line or spline
|
|
/// slides by the cursor's move; a circle or arc grabbed on its edge changes radius about its
|
|
/// centre; a conic grabbed on its curve reads its fullness, `rho`, off the cursor) and the rest
|
|
/// of the sketch moves as little as it can, staying on the
|
|
/// solution it started on. Fixed geometry stays. A step that cannot be solved with the entity
|
|
/// held (the path crosses where the geometry cannot reach) is approached by halving the way to
|
|
/// it, and the next steps are tried from the last solved state, the last of them being the whole
|
|
/// remaining move. When none past it solves, the first failed step stays placed but unsolved:
|
|
/// the edit's ordinary, non-strict solve that follows (seeded on the last solved state, see
|
|
/// [`Left::MovedFrom`]) then pulls it back (fully constrained geometry) or leaves the sketch
|
|
/// unsolved for the user to see. A step that would sweep an arc through its own start is undone
|
|
/// and the drag ends there. Returns how far it got and what it leaves for the edit's solve
|
|
/// ([`Left::Solved`]: the held solve of the last step it kept left the sketch solved).
|
|
///
|
|
/// `live`: one frame of an interactive drag, whose held solves share one frame's decomposition
|
|
/// budget ([`Budget::frame`]); otherwise each has a solve's own.
|
|
pub(super) fn drag_entity(sk: &mut Sketch, e: Ent, from: Vec2, to: Vec2, live: bool) -> (Dragged, Left) {
|
|
let Some((path, hold, n, short)) = prepare(sk, e, from, to) else {
|
|
return (Dragged { reached: 0.0, moving: Vec::new() }, Left::Unsolved);
|
|
};
|
|
#[cfg(test)]
|
|
let (n, short) = (n.min(step_cap::get()), short.min(step_cap::get()));
|
|
let moving = path.points().filter_map(|q| Some((q, sk.point(q)?))).collect();
|
|
let mut frame = live.then(frame_budget);
|
|
// Only success matters to these solves: the degrees of freedom of a sketch with the entity
|
|
// held are not the sketch's. Each is seeded on the last solved state (`last`), so what the
|
|
// constraints pin down keeps the orientation it had there. A failed or wrapping step is
|
|
// undone (a failed solve leaves the step placed). Also returns whether the decomposition ran
|
|
// out of budget.
|
|
let mut solves = Solves::default();
|
|
let mut step = |sk: &mut Sketch, last: &Geometry, t: f64, solves: &mut Solves| {
|
|
path.apply(sk, t);
|
|
let mut own = Budget::full();
|
|
let budget = frame.as_mut().unwrap_or(&mut own);
|
|
let spent = budget.spent();
|
|
let ok = counted_solve(sk, &hold, Some(&last.pos), Analysis::StatusOnly, Some(&mut *budget)).ok();
|
|
solves.count += 1;
|
|
solves.work += budget.spent() - spent;
|
|
solves.cut |= budget.exhausted();
|
|
let outcome = if !ok {
|
|
Step::Failed
|
|
} else if last.sweep_wrapped(sk) {
|
|
Step::Wrapped
|
|
} else {
|
|
return Step::Solved;
|
|
};
|
|
last.restore(sk);
|
|
outcome
|
|
};
|
|
let mut last = Geometry::default();
|
|
last.take(sk);
|
|
// How far along the last solved state is; the first step since it that failed.
|
|
let (mut good, mut failed) = (0.0, None);
|
|
let mut spare = MAX_STEPS;
|
|
// The steps: `k` of `dt` from `base`; once a seed ran out of budget, the rest of the way in
|
|
// short steps.
|
|
let (mut base, mut dt, mut k, mut t) = (0.0, 1.0 / n as f64, 0, 0.0);
|
|
let mut shortened = false;
|
|
'walk: while t < 1.0 {
|
|
if solves.cut && !shortened {
|
|
shortened = true;
|
|
let rest = 1.0 - t;
|
|
let m = steps(rest * short as f64, 1.0);
|
|
(base, dt, k) = (t, rest / m as f64, 0);
|
|
}
|
|
k += 1;
|
|
t = base + dt * k as f64;
|
|
// The last step ends exactly at the cursor.
|
|
if 1.0 - t < 0.5 * dt {
|
|
t = 1.0;
|
|
}
|
|
match step(sk, &last, t, &mut solves) {
|
|
Step::Solved => {
|
|
(good, failed) = (t, None);
|
|
if t < 1.0 {
|
|
last.take(sk);
|
|
}
|
|
continue;
|
|
}
|
|
Step::Wrapped => {
|
|
failed = None;
|
|
break 'walk;
|
|
}
|
|
Step::Failed => {}
|
|
}
|
|
if failed.is_none() {
|
|
// Halve the way to the failed step, so the state the next steps start from (and are
|
|
// seeded on) is as close to it as the geometry follows. The shortest move is tried
|
|
// first: geometry that cannot move at all (pinned) costs one solve, not every halving.
|
|
let fine = dt / HALVINGS as f64;
|
|
let mut miss = t;
|
|
let mut first = true;
|
|
while spare > 0 && miss - good > fine {
|
|
spare -= 1;
|
|
let mid = if std::mem::take(&mut first) { good + fine } else { 0.5 * (good + miss) };
|
|
match step(sk, &last, mid, &mut solves) {
|
|
Step::Solved => {
|
|
good = mid;
|
|
last.take(sk);
|
|
}
|
|
Step::Wrapped => break 'walk,
|
|
Step::Failed => miss = mid,
|
|
}
|
|
}
|
|
failed = Some(t);
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
last_drag::set(solves);
|
|
let left = match failed {
|
|
// The failed step is left placed for the edit's solve, seeded on the last solved state.
|
|
Some(t) => {
|
|
path.apply(sk, t);
|
|
Left::MovedFrom(last.pos)
|
|
}
|
|
None if good > 0.0 => Left::Solved,
|
|
None => Left::Unsolved,
|
|
};
|
|
(Dragged { reached: good, moving }, left)
|
|
}
|
|
|
|
/// The held solves of one drag: how many, the decomposition work they did, and whether one ran
|
|
/// out of budget.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
pub(super) struct Solves {
|
|
pub count: usize,
|
|
pub work: usize,
|
|
pub cut: bool,
|
|
}
|
|
|
|
/// One live frame's decomposition budget (smaller in tests that set one).
|
|
fn frame_budget() -> Budget {
|
|
#[cfg(test)]
|
|
if let Some(work) = last_drag::frame_work() {
|
|
return Budget::new(work);
|
|
}
|
|
Budget::frame()
|
|
}
|
|
|
|
/// What the last drag on this thread did, and a frame budget for tests to set.
|
|
#[cfg(test)]
|
|
pub(super) mod last_drag {
|
|
use std::cell::Cell;
|
|
|
|
use super::Solves;
|
|
|
|
thread_local! {
|
|
static LAST: Cell<Solves> = const { Cell::new(Solves { count: 0, work: 0, cut: false }) };
|
|
static FRAME: Cell<Option<usize>> = const { Cell::new(None) };
|
|
}
|
|
|
|
pub fn set(s: Solves) {
|
|
LAST.with(|c| c.set(s));
|
|
}
|
|
|
|
/// The held solves of the last drag.
|
|
pub fn get() -> Solves {
|
|
LAST.with(Cell::get)
|
|
}
|
|
|
|
pub fn frame_work() -> Option<usize> {
|
|
FRAME.with(Cell::get)
|
|
}
|
|
|
|
/// Restores the frame budget when dropped, also when the test panics.
|
|
struct Restore(Option<usize>);
|
|
|
|
impl Drop for Restore {
|
|
fn drop(&mut self) {
|
|
FRAME.with(|c| c.set(self.0));
|
|
}
|
|
}
|
|
|
|
/// Run `f` with live frames given `work` units of decomposition budget.
|
|
pub fn with_frame_work<R>(work: usize, f: impl FnOnce() -> R) -> R {
|
|
let _restore = Restore(FRAME.with(|c| c.replace(Some(work))));
|
|
f()
|
|
}
|
|
}
|
|
|
|
/// How one held solve of a drag went.
|
|
enum Step {
|
|
Solved,
|
|
Failed,
|
|
/// Solved, but it swept an arc through its own start.
|
|
Wrapped,
|
|
}
|
|
|
|
/// How far a drag got.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub(super) struct Dragged {
|
|
/// The part of the way from where it was grabbed to the cursor the geometry followed (0..1).
|
|
pub reached: f64,
|
|
/// The points the drag moves (not the fixed ones it holds), where they were before it.
|
|
pub moving: Vec<(usize, Vec2)>,
|
|
}
|
|
|
|
/// The geometry a step changes (point positions; curves, for radii and the arc ends a solve
|
|
/// may swap), kept to undo a step. Its buffers are reused from step to step.
|
|
#[derive(Default)]
|
|
struct Geometry {
|
|
pos: Vec<Vec2>,
|
|
curves: Vec<(CurveKind, bool)>,
|
|
}
|
|
|
|
impl Geometry {
|
|
fn take(&mut self, sk: &Sketch) {
|
|
self.pos.clear();
|
|
self.pos.extend(sk.points.iter().map(|p| p.pos));
|
|
self.curves.clear();
|
|
self.curves.extend(sk.curves.iter().map(|c| (c.kind.clone(), c.reversed)));
|
|
}
|
|
|
|
fn restore(&self, sk: &mut Sketch) {
|
|
for (p, at) in sk.points.iter_mut().zip(&self.pos) {
|
|
p.pos = *at;
|
|
}
|
|
for (c, (kind, reversed)) in sk.curves.iter_mut().zip(&self.curves) {
|
|
c.kind = kind.clone();
|
|
c.reversed = *reversed;
|
|
}
|
|
}
|
|
|
|
/// Did the step drive some arc's end through its start: the sweep jumped through 0 / a full
|
|
/// turn, or ran from clear of them onto one? An arc already that close may still move.
|
|
/// Measured on the arc's ends as they were before the step (a solve may swap them).
|
|
fn sweep_wrapped(&self, sk: &Sketch) -> bool {
|
|
let was = |q: usize| self.pos.get(q).copied();
|
|
let now = |q: usize| sk.point(q);
|
|
let clear = |s: f64| (SWEEP_EPS..=TAU - SWEEP_EPS).contains(&s);
|
|
self.curves.iter().any(|(kind, _)| {
|
|
let CurveKind::Arc { c, a, b } = *kind else { return false };
|
|
let (Some(s0), Some(s1)) = (sweep(was(c), was(a), was(b)), sweep(now(c), now(a), now(b))) else { return false };
|
|
(s1 - s0).abs() > PI || (clear(s0) && !clear(s1))
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The held solves dragging `e` from `from` to `to` takes (0 when it cannot move): the plan
|
|
/// `drag_entity` runs, for tests.
|
|
#[cfg(test)]
|
|
pub(super) fn drag_steps(sk: &Sketch, e: Ent, from: Vec2, to: Vec2) -> usize {
|
|
prepare(sk, e, from, to).map_or(0, |(_, _, n, _)| n)
|
|
}
|
|
|
|
/// A drag's path, what its solves hold and its number of steps (and of short steps, see
|
|
/// `SHORT_STEP_FRACTION`), or `None` if it cannot move.
|
|
fn prepare(sk: &Sketch, e: Ent, from: Vec2, to: Vec2) -> Option<(Path, Hold, usize, usize)> {
|
|
if !(from.is_finite() && to.is_finite()) {
|
|
return None;
|
|
}
|
|
let (path, hold) = plan(sk, e, from, to)?;
|
|
let n = step_count(sk, &hold, path.length(), STEP_FRACTION);
|
|
let short = step_count(sk, &hold, path.length(), SHORT_STEP_FRACTION);
|
|
Some((path, hold, n, short))
|
|
}
|
|
|
|
/// A cap on the steps `drag_entity` takes on this thread, for tests that measure what fewer
|
|
/// steps keep (the planned count, `drag_steps`, is not capped).
|
|
#[cfg(test)]
|
|
pub(super) mod step_cap {
|
|
use std::cell::Cell;
|
|
|
|
thread_local! {
|
|
static CAP: Cell<usize> = const { Cell::new(super::MAX_STEPS) };
|
|
}
|
|
|
|
pub fn get() -> usize {
|
|
CAP.with(Cell::get)
|
|
}
|
|
|
|
/// Restores the cap when dropped, also when the test panics.
|
|
struct Restore(usize);
|
|
|
|
impl Drop for Restore {
|
|
fn drop(&mut self) {
|
|
CAP.with(|c| c.set(self.0));
|
|
}
|
|
}
|
|
|
|
/// Run `f` with drags split into at most `cap` steps (at least one).
|
|
pub fn with<R>(cap: usize, f: impl FnOnce() -> R) -> R {
|
|
let _restore = Restore(CAP.with(|c| c.replace(cap.max(1))));
|
|
f()
|
|
}
|
|
}
|
|
|
|
/// Steps of at most `step` covering `length`: at least one, at most `MAX_STEPS`.
|
|
fn steps(length: f64, step: f64) -> usize {
|
|
let n = (length / step).ceil();
|
|
if n.is_finite() && n >= 1.0 { (n as usize).min(MAX_STEPS) } else { 1 }
|
|
}
|
|
|
|
/// The dragged geometry's path and what the solves hold, or `None` if it cannot move.
|
|
fn plan(sk: &Sketch, e: Ent, from: Vec2, to: Vec2) -> Option<(Path, Hold)> {
|
|
let mut hold = Hold::default();
|
|
let path = match e {
|
|
Ent::Point(q) => {
|
|
if sk.point_locked(q) {
|
|
return None;
|
|
}
|
|
hold.points.push(q);
|
|
let at = sk.point(q)?;
|
|
Path::Slide { pts: vec![(q, at)], d: to - at }
|
|
}
|
|
Ent::Curve(c) => {
|
|
if sk.curve_locked(c) {
|
|
return None;
|
|
}
|
|
let kind = sk.curves.get(c).map(|x| x.kind.clone())?;
|
|
let locked = sk.locked_points();
|
|
let free = |q: &usize| !locked.get(*q).copied().unwrap_or(true);
|
|
// A cursor on the centre leaves the radius as it is.
|
|
let radius_to = |o: Vec2, r: f64| Some(o.dist(to)).filter(|x| *x > 1e-9).unwrap_or(r);
|
|
match kind {
|
|
CurveKind::Circle { c: cc, r } => {
|
|
let to_r = sk.point(cc).map_or(r, |o| radius_to(o, r));
|
|
hold.points.push(cc);
|
|
hold.radii.push(c);
|
|
Path::Radius { c, from: r, to: to_r }
|
|
}
|
|
CurveKind::Arc { c: cc, a, b } => {
|
|
let o = sk.point(cc)?;
|
|
let r = sk.radius(c).unwrap_or(0.0);
|
|
hold.points.extend([cc, a, b]);
|
|
let ends = [a, b].into_iter().filter(free).filter_map(|q| Some((q, (sk.point(q)? - o).normalized()?))).collect();
|
|
Path::ArcRadius { o, ends, from: r, to: radius_to(o, r) }
|
|
}
|
|
CurveKind::Ellipse { c: cc, m, r } => {
|
|
// Grabbed on the outline: the minor radius follows (the major stays).
|
|
let (o, pm) = (sk.point(cc)?, sk.point(m)?);
|
|
let u = (pm - o).normalized().unwrap_or(Vec2::X);
|
|
let minor = u.cross(to - o).abs();
|
|
hold.points.extend([cc, m]);
|
|
hold.radii.push(c);
|
|
Path::Radius { c, from: r, to: if minor > 1e-9 { minor } else { r } }
|
|
}
|
|
CurveKind::Conic { a, b, apex, rho } => {
|
|
// Grabbed on the curve itself: its fullness (`rho`) follows the drag, the
|
|
// way an ellipse's outline changes its minor radius rather than sliding the
|
|
// ellipse. `a`, `b` and the apex (the tangent intersection, dragged
|
|
// separately as an ordinary point) stay where they are.
|
|
let (pa, px, pb) = (sk.point(a)?, sk.point(apex)?, sk.point(b)?);
|
|
hold.points.extend([a, b, apex]);
|
|
let to_rho = conic_rho_from_point(pa, px, pb, to);
|
|
Path::ConicRho { c, from: rho, to: to_rho }
|
|
}
|
|
k => {
|
|
// Lines and splines slide with the cursor.
|
|
let mut ids = k.point_ids();
|
|
ids.sort_unstable();
|
|
ids.dedup();
|
|
let pts = ids.iter().filter(|q| free(q)).filter_map(|&q| Some((q, sk.point(q)?))).collect();
|
|
hold.points.extend(ids);
|
|
Path::Slide { pts, d: to - from }
|
|
}
|
|
}
|
|
}
|
|
};
|
|
Some((path, hold))
|
|
}
|
|
|
|
/// Steps for a move of `length`: enough that none is longer than `fraction` of the smallest
|
|
/// size of the geometry the solve moves with what it holds (at least `MIN_STEP`), at most
|
|
/// `MAX_STEPS`. A move short next to every size in the sketch (a normal mouse frame) is one
|
|
/// step, found without grouping the sketch.
|
|
fn step_count(sk: &Sketch, hold: &Hold, length: f64, fraction: f64) -> usize {
|
|
let step = |size: f64| (size * fraction).max(MIN_STEP);
|
|
if length <= step(smallest_size(sk, hold, None)) {
|
|
return 1;
|
|
}
|
|
let (points, radii) = sk.coupled(&hold.points, &hold.radii);
|
|
steps(length, step(smallest_size(sk, hold, Some(Group { points: &points, radii: &radii }))))
|
|
}
|
|
|
|
/// What a solve moves together: per point and per curve radius.
|
|
#[derive(Clone, Copy)]
|
|
struct Group<'a> {
|
|
points: &'a [bool],
|
|
radii: &'a [bool],
|
|
}
|
|
|
|
impl Group<'_> {
|
|
/// Does an entity with these points and curves belong to the group?
|
|
fn has(&self, points: &[usize], curves: &[usize]) -> bool {
|
|
let any = |flags: &[bool], ids: &[usize]| ids.iter().any(|i| flags.get(*i).copied().unwrap_or(false));
|
|
any(self.points, points) || any(self.radii, curves)
|
|
}
|
|
}
|
|
|
|
/// The smallest size that can make a constrained shape take another solution (of the geometry
|
|
/// in `group`, or of the whole sketch), or infinity when there is none: every length the
|
|
/// constraints hold. That is driving length dimensions (offsets along an axis or from a line
|
|
/// too: the solver holds their size, either side), radii and the current lengths of lines held
|
|
/// equal to another (a link's length can come from an Equal as well as a dimension). A line that
|
|
/// only keeps a direction (a rectangle's side) holds no length: it may pass through zero and turn
|
|
/// over, so it does not shrink the steps. Nor does a distance between two held points (the
|
|
/// dragged line's own length): held geometry moves as one. A circle's radius counts when the
|
|
/// solve moves the radius itself (not merely its centre).
|
|
fn smallest_size(sk: &Sketch, hold: &Hold, group: Option<Group>) -> f64 {
|
|
// Point lists are built only to test a size smaller than the smallest so far against the
|
|
// group; without a group nothing is built.
|
|
let in_group = |points: &dyn Fn() -> Vec<usize>, curves: &[usize]| group.is_none_or(|g| g.has(&points(), curves));
|
|
// A radius is the circle's own variable, or (an arc's) the distance between its points.
|
|
let radius_in_group = |c: usize| match sk.curves.get(c).map(|x| &x.kind) {
|
|
Some(CurveKind::Circle { .. } | CurveKind::Ellipse { .. }) => in_group(&Vec::new, &[c]),
|
|
Some(k) => in_group(&|| k.point_ids(), &[]),
|
|
None => false,
|
|
};
|
|
let line_length = |l: usize| super::line_pts(sk, l).map(|(a, b)| a.dist(b));
|
|
let points = |k: &ConstraintKind| sk.constraint_points(k);
|
|
let mut min = f64::INFINITY;
|
|
let mut take = |size: f64, counts: &dyn Fn() -> bool| {
|
|
let size = size.abs();
|
|
if size.is_finite() && size > 1e-9 && size < min && counts() {
|
|
min = size;
|
|
}
|
|
};
|
|
for k in sk.constraints.iter().filter(|k| !k.driven).map(|k| &k.kind) {
|
|
match *k {
|
|
ConstraintKind::Equal { a, b } => {
|
|
if let Some(v) = line_length(a).into_iter().chain(line_length(b)).reduce(f64::min) {
|
|
take(v, &|| in_group(&|| points(k), &[]));
|
|
}
|
|
}
|
|
ConstraintKind::Radius { c, value } => take(value, &|| radius_in_group(c)),
|
|
ConstraintKind::Diameter { c, value } => take(value / 2.0, &|| radius_in_group(c)),
|
|
_ => {
|
|
if let Some(v) = driving_length(k) {
|
|
// Held points move as one: a length between them never changes.
|
|
take(v, &|| {
|
|
let pts = points(k);
|
|
!pts.iter().all(|p| hold.points.contains(p)) && in_group(&|| pts.clone(), &k.indices().1)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for c in 0..sk.curves.len() {
|
|
if let Some(r) = sk.radius(c) {
|
|
take(r, &|| radius_in_group(c));
|
|
}
|
|
}
|
|
min
|
|
}
|
|
|
|
/// The length a driving dimension sets (a linear diameter's radius), if it sets one.
|
|
fn driving_length(k: &ConstraintKind) -> Option<f64> {
|
|
if k.is_angle() {
|
|
return None;
|
|
}
|
|
let v = k.value()?;
|
|
Some(if matches!(k, ConstraintKind::LinearDiameter { .. }) { v / 2.0 } else { v })
|
|
}
|
|
|
|
/// Counter-clockwise sweep (radians, `[0, 2π)`) from `a` to `b` about `c`.
|
|
fn sweep(c: Option<Vec2>, a: Option<Vec2>, b: Option<Vec2>) -> Option<f64> {
|
|
let (o, pa, pb) = (c?, a?, b?);
|
|
Some(((pb - o).angle() - (pa - o).angle()).rem_euclid(TAU))
|
|
}
|