Sketch free-form curves: Ellipse (centre, major axis end, minor radius), Fit Point and Control Point (degree 3 and 5) Splines, Conic Curve (rho); solved like other curves (points on them, tangency at shared ends, drag fit points), profiled as polylines; mirror/pattern/move/scale copy them; Tangent Arc continues splines and conics
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
956492c993
commit
8d7a0cf3d0
14 files changed
+708
-100
No files matched your search
@@ -0,0 +1,214 @@
|
||||
//! Free-form sketch curves: ellipses, splines (through fit points, or a clamped B-spline over
|
||||
//! control points) and conics (rational quadratic Béziers). Evaluation works on positions so
|
||||
//! the solver can use it with trial values.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use solvecraft_geom::Vec2;
|
||||
|
||||
/// Polyline samples per spline span / conic / ellipse quarter.
|
||||
const SPAN_SAMPLES: usize = 24;
|
||||
/// Most points a spline may have.
|
||||
pub const MAX_SPLINE_POINTS: usize = 500;
|
||||
|
||||
/// Point on an ellipse with centre `c`, major-axis end `m` and minor radius `r` at angle `t`.
|
||||
pub fn ellipse_point(c: Vec2, m: Vec2, r: f64, t: f64) -> Vec2 {
|
||||
let u = m - c;
|
||||
let a = u.len().max(1e-12);
|
||||
let v = u.perp() / a * r;
|
||||
c + u * t.cos() + v * t.sin()
|
||||
}
|
||||
|
||||
pub fn ellipse_polyline(c: Vec2, m: Vec2, r: f64) -> Vec<Vec2> {
|
||||
let n = SPAN_SAMPLES * 4;
|
||||
(0..=n).map(|i| ellipse_point(c, m, r, TAU * i as f64 / n as f64)).collect()
|
||||
}
|
||||
|
||||
/// Rational quadratic Bézier from `a` to `b` with control `apex` and shape `rho` in (0, 1)
|
||||
/// (0.5 = parabola, below = ellipse, above = hyperbola).
|
||||
pub fn conic_point(a: Vec2, apex: Vec2, b: Vec2, rho: f64, t: f64) -> Vec2 {
|
||||
let w = rho / (1.0 - rho).max(1e-9);
|
||||
let (b0, b1, b2) = ((1.0 - t) * (1.0 - t), 2.0 * t * (1.0 - t) * w, t * t);
|
||||
let den = b0 + b1 + b2;
|
||||
(a * b0 + apex * b1 + b * b2) / den.max(1e-12)
|
||||
}
|
||||
|
||||
pub fn conic_polyline(a: Vec2, apex: Vec2, b: Vec2, rho: f64) -> Vec<Vec2> {
|
||||
let n = SPAN_SAMPLES * 2;
|
||||
(0..=n).map(|i| conic_point(a, apex, b, rho, i as f64 / n as f64)).collect()
|
||||
}
|
||||
|
||||
/// Natural cubic interpolation through `p` (chord-length parameters): per span the cubic
|
||||
/// Hermite data (start, end, start tangent, end tangent) in span-parameter units.
|
||||
fn fit_spans(p: &[Vec2]) -> Vec<(Vec2, Vec2, Vec2, Vec2)> {
|
||||
let n = p.len();
|
||||
if n < 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
let h: Vec<f64> = p.windows(2).map(|w| w[0].dist(w[1]).max(1e-9)).collect();
|
||||
if n == 2 {
|
||||
let d = p[1] - p[0];
|
||||
return vec![(p[0], p[1], d, d)];
|
||||
}
|
||||
// Second derivatives M (natural ends: M0 = Mn = 0) by the tridiagonal (Thomas) solve.
|
||||
let m = n - 2;
|
||||
let mut diag = vec![0.0; m];
|
||||
let mut up = vec![0.0; m];
|
||||
let mut low = vec![0.0; m];
|
||||
let mut rhs = vec![Vec2::ZERO; m];
|
||||
for i in 0..m {
|
||||
let (h0, h1) = (h[i], h[i + 1]);
|
||||
diag[i] = 2.0 * (h0 + h1);
|
||||
low[i] = h0;
|
||||
up[i] = h1;
|
||||
rhs[i] = ((p[i + 2] - p[i + 1]) / h1 - (p[i + 1] - p[i]) / h0) * 6.0;
|
||||
}
|
||||
for i in 1..m {
|
||||
let w = low[i] / diag[i - 1];
|
||||
diag[i] -= w * up[i - 1];
|
||||
rhs[i] = rhs[i] - rhs[i - 1] * w;
|
||||
}
|
||||
let mut sol = vec![Vec2::ZERO; m];
|
||||
for i in (0..m).rev() {
|
||||
let next = if i + 1 < m { sol[i + 1] * up[i] } else { Vec2::ZERO };
|
||||
sol[i] = (rhs[i] - next) / diag[i];
|
||||
}
|
||||
let mm = |i: usize| if i == 0 || i == n - 1 { Vec2::ZERO } else { sol[i - 1] };
|
||||
(0..n - 1)
|
||||
.map(|i| {
|
||||
let hi = h[i];
|
||||
let slope = (p[i + 1] - p[i]) / hi;
|
||||
// First derivatives (per unit chord parameter) at both ends of the span.
|
||||
let d0 = slope - (mm(i) * 2.0 + mm(i + 1)) * (hi / 6.0);
|
||||
let d1 = slope + (mm(i) + mm(i + 1) * 2.0) * (hi / 6.0);
|
||||
(p[i], p[i + 1], d0 * hi, d1 * hi)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hermite(p0: Vec2, p1: Vec2, t0: Vec2, t1: Vec2, s: f64) -> Vec2 {
|
||||
let (s2, s3) = (s * s, s * s * s);
|
||||
p0 * (2.0 * s3 - 3.0 * s2 + 1.0) + t0 * (s3 - 2.0 * s2 + s) + p1 * (-2.0 * s3 + 3.0 * s2) + t1 * (s3 - s2)
|
||||
}
|
||||
|
||||
/// Clamped uniform B-spline of `degree` over control points `p`, sampled.
|
||||
fn bspline_polyline(p: &[Vec2], degree: usize) -> Vec<Vec2> {
|
||||
let n = p.len();
|
||||
let k = degree.min(n.saturating_sub(1)).max(1);
|
||||
// Knots: k+1 zeros, uniform interior, k+1 ones.
|
||||
let spans = n - k;
|
||||
let mut knots = vec![0.0; k + 1];
|
||||
for i in 1..spans {
|
||||
knots.push(i as f64 / spans as f64);
|
||||
}
|
||||
knots.extend(std::iter::repeat_n(1.0, k + 1));
|
||||
let total = SPAN_SAMPLES * spans;
|
||||
(0..=total)
|
||||
.map(|i| {
|
||||
let t = i as f64 / total as f64;
|
||||
// De Boor.
|
||||
let mut s = k;
|
||||
while s + 1 < n && knots.get(s + 1).is_some_and(|x| *x <= t) {
|
||||
s += 1;
|
||||
}
|
||||
let mut d: Vec<Vec2> = (0..=k).map(|j| p.get(j + s - k).copied().unwrap_or_default()).collect();
|
||||
for r in 1..=k {
|
||||
for j in (r..=k).rev() {
|
||||
let i0 = j + s - k;
|
||||
let (lo, hi) = (knots.get(i0).copied().unwrap_or(0.0), knots.get(i0 + k + 1 - r).copied().unwrap_or(1.0));
|
||||
let a = if hi - lo > 1e-15 { (t - lo) / (hi - lo) } else { 0.0 };
|
||||
d[j] = d[j - 1] * (1.0 - a) + d[j] * a;
|
||||
}
|
||||
}
|
||||
d[k]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Polyline of a spline through (`control == false`) or over (`control`) the points.
|
||||
pub fn spline_polyline(p: &[Vec2], control: bool, degree: u8) -> Vec<Vec2> {
|
||||
if p.len() < 2 {
|
||||
return p.to_vec();
|
||||
}
|
||||
if control {
|
||||
return bspline_polyline(p, degree.clamp(1, 7) as usize);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for (p0, p1, t0, t1) in fit_spans(p) {
|
||||
for i in 0..SPAN_SAMPLES {
|
||||
out.push(hermite(p0, p1, t0, t1, i as f64 / SPAN_SAMPLES as f64));
|
||||
}
|
||||
}
|
||||
if let Some(l) = p.last() {
|
||||
out.push(*l);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Unit tangent of a spline at its start (`at_end == false`) or end, pointing along the curve.
|
||||
pub fn spline_end_tangent(p: &[Vec2], control: bool, at_end: bool) -> Option<Vec2> {
|
||||
let n = p.len();
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
if control {
|
||||
return if at_end { (p[n - 1] - p[n - 2]).normalized() } else { (p[1] - p[0]).normalized() };
|
||||
}
|
||||
let spans = fit_spans(p);
|
||||
if at_end { spans.last().and_then(|s| s.3.normalized()) } else { spans.first().and_then(|s| s.2.normalized()) }
|
||||
}
|
||||
|
||||
/// Distance from `q` to a polyline.
|
||||
pub fn polyline_dist(poly: &[Vec2], q: Vec2) -> f64 {
|
||||
let mut best = f64::INFINITY;
|
||||
for w in poly.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
let d = b - a;
|
||||
let l2 = d.len2();
|
||||
let t = if l2 > 0.0 { ((q - a).dot(d) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
best = best.min(q.dist(a + d * t));
|
||||
}
|
||||
if poly.len() == 1 {
|
||||
best = poly[0].dist(q);
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fit_spline_passes_through_its_points_and_is_smooth() {
|
||||
let p = [Vec2::new(0.0, 0.0), Vec2::new(10.0, 5.0), Vec2::new(20.0, 0.0), Vec2::new(30.0, 8.0)];
|
||||
let poly = spline_polyline(&p, false, 3);
|
||||
for q in p {
|
||||
assert!(poly.iter().any(|x| x.dist(q) < 1e-9));
|
||||
}
|
||||
// A straight row of points stays straight.
|
||||
let line = [Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0), Vec2::new(3.0, 3.0)];
|
||||
assert!(spline_polyline(&line, false, 3).iter().all(|q| (q.x - q.y).abs() < 1e-9));
|
||||
let t = spline_end_tangent(&line, false, false).unwrap();
|
||||
assert!((t.x - t.y).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_spline_is_clamped_and_conic_hits_its_ends() {
|
||||
let p = [Vec2::new(0.0, 0.0), Vec2::new(0.0, 10.0), Vec2::new(10.0, 10.0), Vec2::new(10.0, 0.0)];
|
||||
let poly = spline_polyline(&p, true, 3);
|
||||
assert!(poly.first().unwrap().dist(p[0]) < 1e-12 && poly.last().unwrap().dist(p[3]) < 1e-12);
|
||||
// Cubic Bézier midpoint.
|
||||
let mid = poly[poly.len() / 2];
|
||||
assert!(mid.dist(Vec2::new(5.0, 7.5)) < 1e-9, "{mid:?}");
|
||||
let c = conic_polyline(
|
||||
Vec2::new(10.0, 0.0),
|
||||
Vec2::new(10.0, 10.0),
|
||||
Vec2::new(0.0, 10.0),
|
||||
std::f64::consts::FRAC_1_SQRT_2 / (1.0 + std::f64::consts::FRAC_1_SQRT_2),
|
||||
);
|
||||
// w = cos(45°): a quarter circle of radius 10 about the origin.
|
||||
assert!(c.iter().all(|q| (q.len() - 10.0).abs() < 1e-9));
|
||||
let e = ellipse_polyline(Vec2::ZERO, Vec2::new(5.0, 0.0), 2.0);
|
||||
assert!(e.iter().all(|q| ((q.x / 5.0).powi(2) + (q.y / 2.0).powi(2) - 1.0).abs() < 1e-9));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod curves;
|
||||
mod linalg;
|
||||
mod link;
|
||||
mod model;
|
||||
@@ -15,6 +16,7 @@ mod profiles;
|
||||
mod shape;
|
||||
mod solver;
|
||||
|
||||
pub use curves::{MAX_SPLINE_POINTS, conic_point, ellipse_point, spline_end_tangent, spline_polyline};
|
||||
pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES};
|
||||
pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError};
|
||||
pub use profiles::{Profile, find_drawn_profiles, find_profiles, merge_regions};
|
||||
|
||||
@@ -274,7 +274,7 @@ impl Sketch {
|
||||
Some(CurveKind::Line { a, b }) => Some(LCurve::Line(local(*a)?, local(*b)?)),
|
||||
Some(CurveKind::Circle { c, r }) => Some(LCurve::Circle(local(*c)?, *r)),
|
||||
Some(CurveKind::Arc { c, a, b }) => Some(LCurve::Arc(local(*c)?, local(*a)?, local(*b)?)),
|
||||
None => None,
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// The new layout numbers points in first-use order: curves first, then lone points,
|
||||
|
||||
+163
-41
@@ -43,6 +43,82 @@ pub enum CurveKind {
|
||||
/// Counter-clockwise arc around `c` from `a` to `b` (radius = |a − c|; the solver keeps
|
||||
/// |b − c| equal to it).
|
||||
Arc { c: usize, a: usize, b: usize },
|
||||
/// Ellipse: centre `c`, end of the major axis `m`, minor radius `r`.
|
||||
Ellipse { c: usize, m: usize, r: f64 },
|
||||
/// Spline through its points (fit points) or, with `control`, a clamped B-spline of
|
||||
/// `degree` over them (control points).
|
||||
Spline {
|
||||
pts: Vec<usize>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
control: bool,
|
||||
#[serde(default = "three")]
|
||||
degree: u8,
|
||||
},
|
||||
/// Conic from `a` to `b` shaped by the `apex` point and `rho` in (0, 1) (0.5 parabola,
|
||||
/// less elliptical, more hyperbolic).
|
||||
Conic { a: usize, b: usize, apex: usize, rho: f64 },
|
||||
}
|
||||
|
||||
fn three() -> u8 {
|
||||
3
|
||||
}
|
||||
|
||||
impl CurveKind {
|
||||
/// Every point the curve uses.
|
||||
pub fn point_ids(&self) -> Vec<usize> {
|
||||
match self {
|
||||
CurveKind::Line { a, b } => vec![*a, *b],
|
||||
CurveKind::Circle { c, .. } => vec![*c],
|
||||
CurveKind::Arc { c, a, b } => vec![*c, *a, *b],
|
||||
CurveKind::Ellipse { c, m, .. } => vec![*c, *m],
|
||||
CurveKind::Spline { pts, .. } => pts.clone(),
|
||||
CurveKind::Conic { a, b, apex, .. } => vec![*a, *b, *apex],
|
||||
}
|
||||
}
|
||||
pub fn uses(&self, p: usize) -> bool {
|
||||
self.point_ids().contains(&p)
|
||||
}
|
||||
/// Renumber the points the curve uses.
|
||||
pub fn map_points(&mut self, f: &dyn Fn(usize) -> usize) {
|
||||
match self {
|
||||
CurveKind::Line { a, b } => {
|
||||
*a = f(*a);
|
||||
*b = f(*b);
|
||||
}
|
||||
CurveKind::Circle { c, .. } => *c = f(*c),
|
||||
CurveKind::Arc { c, a, b } => {
|
||||
*c = f(*c);
|
||||
*a = f(*a);
|
||||
*b = f(*b);
|
||||
}
|
||||
CurveKind::Ellipse { c, m, .. } => {
|
||||
*c = f(*c);
|
||||
*m = f(*m);
|
||||
}
|
||||
CurveKind::Spline { pts, .. } => {
|
||||
for q in pts.iter_mut() {
|
||||
*q = f(*q);
|
||||
}
|
||||
}
|
||||
CurveKind::Conic { a, b, apex, .. } => {
|
||||
*a = f(*a);
|
||||
*b = f(*b);
|
||||
*apex = f(*apex);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Start and end points of an open curve.
|
||||
pub fn ends(&self) -> Option<(usize, usize)> {
|
||||
match self {
|
||||
CurveKind::Line { a, b } | CurveKind::Arc { a, b, .. } | CurveKind::Conic { a, b, .. } => Some((*a, *b)),
|
||||
CurveKind::Spline { pts, .. } => Some((*pts.first()?, *pts.last()?)),
|
||||
CurveKind::Circle { .. } | CurveKind::Ellipse { .. } => None,
|
||||
}
|
||||
}
|
||||
/// Free-form curves (drawn and profiled as polylines).
|
||||
pub fn is_freeform(&self) -> bool {
|
||||
matches!(self, CurveKind::Ellipse { .. } | CurveKind::Spline { .. } | CurveKind::Conic { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -384,7 +460,7 @@ impl Sketch {
|
||||
Ok(self.points.len() - 1)
|
||||
}
|
||||
|
||||
fn curve_id(&mut self, id: Option<&str>, prefix: &str) -> Result<String> {
|
||||
pub(crate) fn curve_id(&mut self, id: Option<&str>, prefix: &str) -> Result<String> {
|
||||
match id {
|
||||
Some(i) if self.id_taken(i) => Err(SketchError::Duplicate(i.into())),
|
||||
Some("") => Err(SketchError::Invalid("empty id".into())),
|
||||
@@ -510,7 +586,8 @@ impl Sketch {
|
||||
need_round(b)?;
|
||||
}
|
||||
Tangent { a, b } => {
|
||||
if !(is_round(a) || is_round(b)) {
|
||||
let free = |i: usize| self.curves.get(i).is_some_and(|c| c.kind.is_freeform());
|
||||
if !(is_round(a) || is_round(b) || free(a) || free(b)) {
|
||||
return Err(SketchError::WrongKind(name(a), "tangent needs a circle or arc".into()));
|
||||
}
|
||||
}
|
||||
@@ -535,19 +612,23 @@ impl Sketch {
|
||||
match self.curves.get(ci)?.kind {
|
||||
CurveKind::Circle { r, .. } => Some(r),
|
||||
CurveKind::Arc { c, a, .. } => Some(self.point(c)?.dist(self.point(a)?)),
|
||||
CurveKind::Line { .. } => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn center(&self, ci: usize) -> Option<Vec2> {
|
||||
match self.curves.get(ci)?.kind {
|
||||
CurveKind::Circle { c, .. } | CurveKind::Arc { c, .. } => self.point(c),
|
||||
CurveKind::Line { .. } => None,
|
||||
CurveKind::Circle { c, .. } | CurveKind::Arc { c, .. } | CurveKind::Ellipse { c, .. } => self.point(c),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Boundary segments of a curve (a circle gives two half arcs).
|
||||
pub fn segs(&self, ci: usize) -> Vec<Seg2> {
|
||||
let Some(cu) = self.curves.get(ci) else { return Vec::new() };
|
||||
if cu.kind.is_freeform() {
|
||||
let poly = self.polyline(ci);
|
||||
return poly.windows(2).filter(|w| w[0].dist(w[1]) > 1e-12).map(|w| Seg2::Line { a: w[0], b: w[1] }).collect();
|
||||
}
|
||||
match cu.kind {
|
||||
CurveKind::Line { a, b } => match (self.point(a), self.point(b)) {
|
||||
(Some(a), Some(b)) => vec![Seg2::Line { a, b }],
|
||||
@@ -568,20 +649,83 @@ impl Sketch {
|
||||
}
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Polyline of a free-form curve (ellipse, spline, conic); other curves: their segments'
|
||||
/// polylines.
|
||||
pub fn polyline(&self, ci: usize) -> Vec<Vec2> {
|
||||
let Some(cu) = self.curves.get(ci) else { return Vec::new() };
|
||||
let pt = |i: &usize| self.point(*i);
|
||||
match &cu.kind {
|
||||
CurveKind::Ellipse { c, m, r } => match (pt(c), pt(m)) {
|
||||
(Some(c), Some(m)) => crate::curves::ellipse_polyline(c, m, *r),
|
||||
_ => Vec::new(),
|
||||
},
|
||||
CurveKind::Spline { pts, control, degree } => {
|
||||
let p: Option<Vec<Vec2>> = pts.iter().map(pt).collect();
|
||||
p.map(|p| crate::curves::spline_polyline(&p, *control, *degree)).unwrap_or_default()
|
||||
}
|
||||
CurveKind::Conic { a, b, apex, rho } => match (pt(a), pt(apex), pt(b)) {
|
||||
(Some(a), Some(x), Some(b)) => crate::curves::conic_polyline(a, x, b, *rho),
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => {
|
||||
let mut out: Vec<Vec2> = Vec::new();
|
||||
for s in self.segs(ci) {
|
||||
for q in s.polyline(1e-3) {
|
||||
if out.last().is_none_or(|l| l.dist(q) > 1e-12) {
|
||||
out.push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a curve of any kind over existing points.
|
||||
pub fn add_curve(&mut self, kind: CurveKind, id: Option<&str>) -> Result<usize> {
|
||||
let ids = kind.point_ids();
|
||||
if ids.iter().any(|p| *p >= self.points.len()) {
|
||||
return Err(SketchError::Invalid("curve point out of range".into()));
|
||||
}
|
||||
let prefix = match &kind {
|
||||
CurveKind::Line { .. } => "l",
|
||||
CurveKind::Circle { .. } => "c",
|
||||
CurveKind::Arc { .. } => "a",
|
||||
CurveKind::Ellipse { m, c, r } => {
|
||||
if m == c || !(r.is_finite() && *r > 1e-9 && *r < 1e9) {
|
||||
return Err(SketchError::Invalid("degenerate ellipse".into()));
|
||||
}
|
||||
"e"
|
||||
}
|
||||
CurveKind::Spline { pts, degree, .. } => {
|
||||
if pts.len() < 2 || pts.len() > crate::curves::MAX_SPLINE_POINTS || !(1..=7).contains(degree) {
|
||||
return Err(SketchError::Invalid("a spline needs 2 to 500 points and degree 1 to 7".into()));
|
||||
}
|
||||
"s"
|
||||
}
|
||||
CurveKind::Conic { rho, .. } => {
|
||||
if !(rho.is_finite() && *rho > 1e-6 && *rho < 1.0 - 1e-6) {
|
||||
return Err(SketchError::Invalid("rho must be between 0 and 1".into()));
|
||||
}
|
||||
"k"
|
||||
}
|
||||
};
|
||||
let id = self.curve_id(id, prefix)?;
|
||||
self.curves.push(Curve { id, kind, construction: false, reversed: false, link: None });
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
/// Remove curves (by index) and then any points no longer used by a curve (except the
|
||||
/// origin and free points), and constraints that referred to removed entities.
|
||||
pub fn remove_curves(&mut self, which: &[usize]) {
|
||||
let mut owned: Vec<usize> = Vec::new();
|
||||
for (i, c) in self.curves.iter().enumerate() {
|
||||
if which.contains(&i) {
|
||||
match c.kind {
|
||||
CurveKind::Line { a, b } => owned.extend([a, b]),
|
||||
CurveKind::Circle { c, .. } => owned.push(c),
|
||||
CurveKind::Arc { c, a, b } => owned.extend([c, a, b]),
|
||||
}
|
||||
owned.extend(c.kind.point_ids());
|
||||
}
|
||||
}
|
||||
self.drop_curves(which);
|
||||
@@ -593,11 +737,7 @@ impl Sketch {
|
||||
}
|
||||
|
||||
fn point_used_by_curve(&self, p: usize) -> bool {
|
||||
self.curves.iter().any(|c| match c.kind {
|
||||
CurveKind::Line { a, b } => a == p || b == p,
|
||||
CurveKind::Circle { c, .. } => c == p,
|
||||
CurveKind::Arc { c, a, b } => c == p || a == p || b == p,
|
||||
})
|
||||
self.curves.iter().any(|c| c.kind.uses(p))
|
||||
}
|
||||
|
||||
/// Remove curves only (points stay), remapping constraints.
|
||||
@@ -627,17 +767,8 @@ impl Sketch {
|
||||
return;
|
||||
}
|
||||
let gone = |p: usize| p != 0 && which.contains(&p);
|
||||
let dead_curves: Vec<usize> = self
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| match c.kind {
|
||||
CurveKind::Line { a, b } => gone(a) || gone(b),
|
||||
CurveKind::Circle { c, .. } => gone(c),
|
||||
CurveKind::Arc { c, a, b } => gone(c) || gone(a) || gone(b),
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
let dead_curves: Vec<usize> =
|
||||
self.curves.iter().enumerate().filter(|(_, c)| c.kind.point_ids().into_iter().any(gone)).map(|(i, _)| i).collect();
|
||||
self.drop_curves(&dead_curves);
|
||||
let mut pmap = vec![None; self.points.len()];
|
||||
let mut n = 0;
|
||||
@@ -649,18 +780,7 @@ impl Sketch {
|
||||
}
|
||||
let pm = |i: usize| pmap.get(i).copied().flatten();
|
||||
for c in &mut self.curves {
|
||||
match &mut c.kind {
|
||||
CurveKind::Line { a, b } => {
|
||||
*a = pm(*a).unwrap_or(0);
|
||||
*b = pm(*b).unwrap_or(0);
|
||||
}
|
||||
CurveKind::Circle { c, .. } => *c = pm(*c).unwrap_or(0),
|
||||
CurveKind::Arc { c, a, b } => {
|
||||
*c = pm(*c).unwrap_or(0);
|
||||
*a = pm(*a).unwrap_or(0);
|
||||
*b = pm(*b).unwrap_or(0);
|
||||
}
|
||||
}
|
||||
c.kind.map_points(&|i| pm(i).unwrap_or(0));
|
||||
}
|
||||
let cm = |i: usize| Some(i);
|
||||
self.constraints.retain_mut(|c| c.kind.remap(&pm, &cm));
|
||||
@@ -686,9 +806,11 @@ impl Sketch {
|
||||
(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),
|
||||
(CurveKind::Circle { c, .. }, "center") | (CurveKind::Arc { c, .. }, "center") => Some(*c),
|
||||
(CurveKind::Circle { c, .. }, "center") | (CurveKind::Arc { c, .. }, "center") | (CurveKind::Ellipse { c, .. }, "center") => Some(*c),
|
||||
(CurveKind::Ellipse { m, .. }, "major") => Some(*m),
|
||||
(CurveKind::Conic { apex, .. }, "apex") => Some(*apex),
|
||||
(k, "start") => k.ends().map(|e| e.0),
|
||||
(k, "end") => k.ends().map(|e| e.1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,14 @@ pub fn find_profiles(sk: &Sketch) -> Vec<Profile> {
|
||||
shapes.push((ci, Shape::Round { c: center, r: radius, start, sweep }));
|
||||
}
|
||||
}
|
||||
// Free-form curves take part as their polylines.
|
||||
_ => {
|
||||
for s in sk.segs(ci) {
|
||||
if let Seg2::Line { a, b } = s {
|
||||
shapes.push((ci, Shape::Line { a, b }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if shapes.len() > MAX_EDGES {
|
||||
break;
|
||||
|
||||
@@ -161,6 +161,7 @@ impl Sketch {
|
||||
Seg2::Arc { center, radius, start, sweep } => Some(Shape::Round { c: center, r: radius, start, sweep }),
|
||||
Seg2::Line { .. } => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
-31
@@ -86,7 +86,7 @@ fn layout(sk: &Sketch) -> Layout {
|
||||
.curves
|
||||
.iter()
|
||||
.map(|c| match c.kind {
|
||||
CurveKind::Circle { .. } if c.link.is_none() => {
|
||||
CurveKind::Circle { .. } | CurveKind::Ellipse { .. } if c.link.is_none() => {
|
||||
n += 1;
|
||||
Some(n - 1)
|
||||
}
|
||||
@@ -136,6 +136,53 @@ impl State<'_> {
|
||||
fn is_line(&self, c: usize) -> bool {
|
||||
matches!(self.sk.curves.get(c).map(|c| &c.kind), Some(CurveKind::Line { .. }))
|
||||
}
|
||||
fn is_freeform(&self, c: usize) -> bool {
|
||||
self.sk.curves.get(c).is_some_and(|c| c.kind.is_freeform())
|
||||
}
|
||||
/// Polyline of a free-form curve with trial values.
|
||||
fn poly(&self, c: usize) -> Vec<Vec2> {
|
||||
match self.sk.curves.get(c).map(|c| &c.kind) {
|
||||
Some(CurveKind::Ellipse { c: ci, m, r }) => {
|
||||
let r = match self.lay.rvar.get(c).copied().flatten() {
|
||||
Some(v) => self.x.get(v).copied().unwrap_or(*r),
|
||||
None => *r,
|
||||
};
|
||||
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::Conic { a, b, apex, rho }) => crate::curves::conic_polyline(self.p(*a), self.p(*apex), self.p(*b), *rho),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
/// Unit tangent of any curve at its point `p` (an end point for open free-form curves).
|
||||
fn tangent_at(&self, c: usize, p: usize) -> Option<Vec2> {
|
||||
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::Conic { a, b, apex, .. } => {
|
||||
if p == *a {
|
||||
(self.p(*apex) - self.p(*a)).normalized()
|
||||
} else if p == *b {
|
||||
(self.p(*b) - self.p(*apex)).normalized()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
CurveKind::Ellipse { .. } => {
|
||||
let poly = self.poly(c);
|
||||
let q = self.p(p);
|
||||
let k = (0..poly.len().saturating_sub(1)).min_by(|i, j| poly[*i].dist(q).total_cmp(&poly[*j].dist(q)))?;
|
||||
(*poly.get(k + 1)? - *poly.get(k)?).normalized()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_angle(a: f64) -> f64 {
|
||||
@@ -166,7 +213,9 @@ fn residuals(s: &State, k: &ConstraintKind, out: &mut Vec<f64>) {
|
||||
}
|
||||
PointOnCurve { p, c } => {
|
||||
let pp = s.p(p);
|
||||
if s.is_line(c) {
|
||||
if s.is_freeform(c) {
|
||||
out.push(crate::curves::polyline_dist(&s.poly(c), pp));
|
||||
} else if s.is_line(c) {
|
||||
let (a, b) = s.line(c);
|
||||
out.push(line_dist(pp, a, b));
|
||||
} else {
|
||||
@@ -200,7 +249,22 @@ fn residuals(s: &State, k: &ConstraintKind, out: &mut Vec<f64>) {
|
||||
}
|
||||
Tangent { a, b } => {
|
||||
let (la, lb) = (s.is_line(a), s.is_line(b));
|
||||
if la || lb {
|
||||
if s.is_freeform(a) || s.is_freeform(b) {
|
||||
// Without a shared end point: the curves touch (closest approach zero).
|
||||
let (f, o) = if s.is_freeform(a) { (a, b) } else { (b, a) };
|
||||
let poly = s.poly(f);
|
||||
let d = if s.is_line(o) {
|
||||
let (p0, p1) = s.line(o);
|
||||
poly.iter().map(|q| line_dist(*q, p0, p1).abs()).fold(f64::INFINITY, f64::min)
|
||||
} else if s.is_freeform(o) {
|
||||
let other = s.poly(o);
|
||||
poly.iter().map(|q| crate::curves::polyline_dist(&other, *q)).fold(f64::INFINITY, f64::min)
|
||||
} else {
|
||||
let (cc, r) = s.round(o);
|
||||
poly.iter().map(|q| (q.dist(cc) - r).abs()).fold(f64::INFINITY, f64::min)
|
||||
};
|
||||
out.push(if d.is_finite() { d } else { 0.0 });
|
||||
} else if la || lb {
|
||||
let (l, c) = if la { (a, b) } else { (b, a) };
|
||||
let (p0, p1) = s.line(l);
|
||||
let (cc, r) = s.round(c);
|
||||
@@ -284,7 +348,13 @@ fn block_residuals(s: &State, b: &Block, out: &mut Vec<f64>) {
|
||||
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) {
|
||||
if s.is_freeform(a) || s.is_freeform(b) {
|
||||
// Directions at the shared end parallel.
|
||||
match (s.tangent_at(a, *p), s.tangent_at(b, *p)) {
|
||||
(Some(u), Some(v)) => out.push(u.cross(v) * 10.0),
|
||||
_ => out.push(0.0),
|
||||
}
|
||||
} else 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);
|
||||
@@ -308,8 +378,8 @@ 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(),
|
||||
Some(k) => k.ends().map(|(a, b)| vec![a, b]).unwrap_or_default(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
};
|
||||
let (ea, eb) = (ends(a), ends(b));
|
||||
@@ -328,23 +398,15 @@ fn block_vars(sk: &Sketch, lay: &Layout, b: &Block) -> Vec<usize> {
|
||||
v.extend([x, x + 1]);
|
||||
}
|
||||
};
|
||||
let curve = |v: &mut Vec<usize>, c: usize| match sk.curves.get(c).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { a, b }) => {
|
||||
pt(v, *a);
|
||||
pt(v, *b);
|
||||
}
|
||||
Some(CurveKind::Circle { c: ci, .. }) => {
|
||||
pt(v, *ci);
|
||||
let curve = |v: &mut Vec<usize>, c: usize| {
|
||||
if let Some(k) = sk.curves.get(c).map(|c| &c.kind) {
|
||||
for q in k.point_ids() {
|
||||
pt(v, q);
|
||||
}
|
||||
if let Some(r) = lay.rvar.get(c).copied().flatten() {
|
||||
v.push(r);
|
||||
}
|
||||
}
|
||||
Some(CurveKind::Arc { c, a, b }) => {
|
||||
pt(v, *c);
|
||||
pt(v, *a);
|
||||
pt(v, *b);
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
use ConstraintKind::*;
|
||||
match b {
|
||||
@@ -546,7 +608,7 @@ pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||||
}
|
||||
}
|
||||
for (i, c) in sk.curves.iter().enumerate() {
|
||||
if let (CurveKind::Circle { r, .. }, Some(v)) = (&c.kind, lay.rvar.get(i).copied().flatten())
|
||||
if let (CurveKind::Circle { r, .. } | CurveKind::Ellipse { r, .. }, Some(v)) = (&c.kind, lay.rvar.get(i).copied().flatten())
|
||||
&& let Some(s) = x.get_mut(v)
|
||||
{
|
||||
*s = *r;
|
||||
@@ -654,7 +716,7 @@ pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||||
}
|
||||
}
|
||||
for (i, c) in sk.curves.iter_mut().enumerate() {
|
||||
if let (CurveKind::Circle { r, .. }, Some(v)) = (&mut c.kind, lay.rvar.get(i).copied().flatten()) {
|
||||
if let (CurveKind::Circle { r, .. } | CurveKind::Ellipse { r, .. }, Some(v)) = (&mut c.kind, lay.rvar.get(i).copied().flatten()) {
|
||||
*r = use_x.get(v).copied().unwrap_or(*r).abs().max(1e-9);
|
||||
}
|
||||
}
|
||||
@@ -664,16 +726,8 @@ pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||||
None => true,
|
||||
};
|
||||
let point_determined: Vec<bool> = (0..sk.points.len()).map(pdet).collect();
|
||||
let curve_determined = sk
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| match c.kind {
|
||||
CurveKind::Line { a, b } => pdet(a) && pdet(b),
|
||||
CurveKind::Circle { c, .. } => pdet(c) && vdet(lay.rvar.get(i).copied().flatten()),
|
||||
CurveKind::Arc { c, a, b } => pdet(c) && pdet(a) && pdet(b),
|
||||
})
|
||||
.collect();
|
||||
let curve_determined =
|
||||
sk.curves.iter().enumerate().map(|(i, c)| c.kind.point_ids().into_iter().all(pdet) && vdet(lay.rvar.get(i).copied().flatten())).collect();
|
||||
SolveReport {
|
||||
status,
|
||||
dof: lay.n.saturating_sub(rank),
|
||||
|
||||
Reference in new issue
Block a user