Sketch: a control spline can carry its own knots and weights

A sketch spline over control points was always the clamped uniform B-spline
over them, so a free-form edge whose knots are not evenly spaced (or whose
weights are not 1) could only come into a sketch as one curve per Bezier
span. A control spline now may save `nurbs`: its clamped knots and its
weights. Its points, polyline, exact pieces (one a knot span; conics for a
rational quadratic), the solver's distance and curvature, links, parallel
re-projection and inspect.sketch all read them; a loaded design's are
checked. One pending format file (spline-knots).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 23:06:33 -04:00
committed by jyviko
parent 20d5d8b135
commit b16e8a5423
24 changed files with 434 additions and 78 deletions
+2 -1
View File
@@ -676,6 +676,7 @@ pub fn read(s: &str) -> Result<Document> {
sketch.validate_wires().map_err(|e| DocError::Invalid(format!("damaged design: {e}")))?;
sketch.validate_patterns().map_err(|e| DocError::Invalid(format!("damaged design: {e}")))?;
sketch.validate_periodic_splines().map_err(|e| DocError::Invalid(format!("damaged design: {e}")))?;
sketch.validate_spline_knots().map_err(|e| DocError::Invalid(format!("damaged design: {e}")))?;
}
if let crate::FeatureKind::BodyTransfer { from, to, .. } = &feature.kind
&& [from, to].into_iter().any(|id| !component_ids.contains(id))
@@ -1811,7 +1812,7 @@ mod tests {
let first = pts[0];
let neighbor = pts[1];
let closing = pts[3];
sketch.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic: true }, None).unwrap();
sketch.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic: true, nurbs: None }, None).unwrap();
let mut document = Document::new("periodic file validation");
document.add_feature(crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
assert_eq!(read(&document.to_json()).unwrap(), document);
@@ -0,0 +1,6 @@
//! A sketch's control spline may save `nurbs`: its own `knots` (clamped, the point count plus
//! the degree plus one) and `weights` (one per point; none: all 1). A projected free-form edge
//! comes in as that one exact curve, where it came in as one spline per Bézier span. An older
//! design has none: its splines are the clamped uniform B-splines they were, and its projected
//! edges are read again as they rebuild. The last release refuses a design with this change
//! (the newer format number).
+6 -5
View File
@@ -587,8 +587,8 @@ fn link_geom(s: &Seg2) -> Option<LinkGeom> {
LinkGeom::Arc { c: center, a, b }
}
Seg2::Conic { a, apex, b, w } => LinkGeom::Conic { a, apex, b, rho: w / (1. + w) },
Seg2::Cubic { p0, p1, p2, p3 } => LinkGeom::Spline { pts: vec![p0, p1, p2, p3], control: true, degree: 3, periodic: false },
Seg2::Bezier { n, p } => LinkGeom::Spline { pts: p.get(..=usize::from(n))?.to_vec(), control: true, degree: n, periodic: false },
Seg2::Cubic { p0, p1, p2, p3 } => LinkGeom::Spline { pts: vec![p0, p1, p2, p3], control: true, degree: 3, periodic: false, nurbs: None },
Seg2::Bezier { n, p } => LinkGeom::Spline { pts: p.get(..=usize::from(n))?.to_vec(), control: true, degree: n, periodic: false, nurbs: None },
})
}
@@ -1088,9 +1088,10 @@ fn project_sketch_curve(sk: &Sketch, from: &Plane, ci: usize, to: &Plane) -> Vec
(Some(c), Some(m)) => vec![LinkGeom::Ellipse { c: map(c), major: map(m), minor: *r }],
_ => Vec::new(),
},
Some(CurveKind::Spline { pts, control, degree, periodic }) if parallel => {
Some(CurveKind::Spline { pts, control, degree, periodic, nurbs }) if parallel => {
let p: Option<Vec<Vec2>> = pts.iter().map(|q| sk.point(*q).map(map)).collect();
p.map(|pts| vec![LinkGeom::Spline { pts, control: *control, degree: *degree, periodic: *periodic }]).unwrap_or_default()
p.map(|pts| vec![LinkGeom::Spline { pts, control: *control, degree: *degree, periodic: *periodic, nurbs: nurbs.clone() }])
.unwrap_or_default()
}
Some(CurveKind::Conic { a, b, apex, rho }) if parallel => match (sk.point(*a), sk.point(*apex), sk.point(*b)) {
(Some(a), Some(x), Some(b)) => vec![LinkGeom::Conic { a: map(a), apex: map(x), b: map(b), rho: *rho }],
@@ -1206,7 +1207,7 @@ fn as_spline(pts: &[Vec2], tol: f64) -> Option<LinkGeom> {
<= tol
});
if ok {
return Some(LinkGeom::Spline { pts: fit, control: false, degree: 3, periodic: false });
return Some(LinkGeom::Spline { pts: fit, control: false, degree: 3, periodic: false, nurbs: None });
}
}
None
+3 -1
View File
@@ -247,7 +247,9 @@ fn link_polyline(g: &solidcraft_sketch::LinkGeom) -> Vec<Vec2> {
LinkGeom::Ellipse { c, major, minor } => {
(0..=96).map(|i| solidcraft_sketch::ellipse_point(c, major, minor, i as f64 / 96.0 * std::f64::consts::TAU)).collect()
}
LinkGeom::Spline { ref pts, control, degree, periodic } => solidcraft_sketch::spline_polyline_mode(pts, control, degree, periodic),
LinkGeom::Spline { ref pts, control, degree, periodic, ref nurbs } => {
solidcraft_sketch::spline_polyline_mode(pts, control, degree, periodic, nurbs.as_ref())
}
LinkGeom::Point(_) => Vec::new(),
}
}
+7 -1
View File
@@ -251,7 +251,13 @@ mod tests {
vec![
line(0., 1.),
line(1., 2.),
LinkGeom::Spline { pts: vec![Vec2::new(1., 0.), Vec2::new(2., 1.), Vec2::new(3., 1.)], control: false, degree: 2, periodic: false },
LinkGeom::Spline {
pts: vec![Vec2::new(1., 0.), Vec2::new(2., 1.), Vec2::new(3., 1.)],
control: false,
degree: 2,
periodic: false,
nurbs: None,
},
],
vec![line(0., 1.), LinkGeom::Arc { c: Vec2::new(1., 1.), a: Vec2::new(1., 0.), b: Vec2::new(2., 1.) }],
vec![line(0., 1.), line(1.00001, 2.)],
+11 -1
View File
@@ -186,7 +186,17 @@ fn sketch_json(s: &Session, world: &solidcraft_doc::ModelState, id: u64) -> Opti
CurveKind::Circle { c: cc, r } => json!({"id": c.id, "type": "circle", "center": pid(cc), "center_at": sk.point(cc), "radius": r, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
CurveKind::Arc { c: cc, a, b } => json!({"id": c.id, "type": "arc", "center": pid(cc), "start": pid(a), "end": pid(b), "center_at": sk.point(cc), "start_at": sk.point(a), "end_at": sk.point(b), "radius": sk.radius(i), "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
CurveKind::Ellipse { c: cc, m, r } => json!({"id": c.id, "type": "ellipse", "center": pid(cc), "major": pid(m), "center_at": sk.point(cc), "major_at": sk.point(m), "minor_radius": r, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
CurveKind::Spline { ref pts, control, degree, periodic } => json!({"id": c.id, "type": "spline", "points": pts.iter().map(|q| pid(*q)).collect::<Vec<_>>(), "points_at": pts.iter().map(|q| sk.point(*q)).collect::<Vec<_>>(), "control": control, "degree": degree, "periodic": periodic, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
CurveKind::Spline { ref pts, control, degree, periodic, ref nurbs } => {
let mut v = json!({"id": c.id, "type": "spline", "points": pts.iter().map(|q| pid(*q)).collect::<Vec<_>>(), "points_at": pts.iter().map(|q| sk.point(*q)).collect::<Vec<_>>(), "control": control, "degree": degree, "periodic": periodic, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det});
// A control spline with its own knots (a projected free-form edge) says them.
if let (Some(n), Some(o)) = (nurbs, v.as_object_mut()) {
o.insert("knots".into(), json!(n.knots));
if !n.weights.is_empty() {
o.insert("weights".into(), json!(n.weights));
}
}
v
}
CurveKind::Conic { a, b, apex, rho } => json!({"id": c.id, "type": "conic", "start": pid(a), "end": pid(b), "apex": pid(apex), "start_at": sk.point(a), "end_at": sk.point(b), "apex_at": sk.point(apex), "rho": rho, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
};
if let (Some(l), Some(o)) = (&c.link, v.as_object_mut()) {
+5 -5
View File
@@ -505,7 +505,7 @@ fn blend(s: &mut Session, p: &Value) -> Result<Value> {
let (sp, cons) = if order <= 1 {
let c1 = sk.add_point(qa + ta * (l / 3.0), None)?;
let c2 = sk.add_point(qb + tb * (l / 3.0), None)?;
let sp = sk.add_curve(CurveKind::Spline { pts: vec![pa, c1, c2, pb], control: true, degree: 3, periodic: false }, None)?;
let sp = sk.add_curve(CurveKind::Spline { pts: vec![pa, c1, c2, pb], control: true, degree: 3, periodic: false, nurbs: None }, None)?;
let cons = vec![add_c(sk, ConstraintKind::Tangent { a: ca, b: sp })?, add_c(sk, ConstraintKind::Tangent { a: cb, b: sp })?];
(sp, cons)
} else {
@@ -527,7 +527,7 @@ fn blend(s: &mut Session, p: &Value) -> Result<Value> {
});
}
let degree = u8::try_from(cps.len().saturating_sub(1)).map_err(|_| bad(cmd, "the blend curve needs too many control points"))?;
let sp = sk.add_curve(CurveKind::Spline { pts, control: true, degree, periodic: false }, None)?;
let sp = sk.add_curve(CurveKind::Spline { pts, control: true, degree, periodic: false, nurbs: None }, None)?;
(sp, Vec::new())
};
solve_new_only(sk, first_new);
@@ -655,7 +655,7 @@ fn spline(s: &mut Session, p: &Value, cmd: &str, control: bool, degree: u8) -> R
{
pts.push(*pts.first().ok_or_else(|| bad(cmd, "missing first point"))?);
}
let c = sk.add_curve(CurveKind::Spline { pts, control, degree, periodic }, None)?;
let c = sk.add_curve(CurveKind::Spline { pts, control, degree, periodic, nurbs: None }, None)?;
mark_construction(sk, &[c], p);
Ok((ids_of(sk, &[c]), Vec::new()))
})?;
@@ -1344,13 +1344,13 @@ mod blend_tests {
let mut broken = solidcraft_sketch::Sketch::new();
let p0 = broken.add_point(Vec2::new(1.0, 1.0), None).unwrap();
let p1 = broken.add_point(Vec2::ZERO, None).unwrap();
let l = broken.add_curve(CurveKind::Spline { pts: vec![p1, p0], control: true, degree: 1, periodic: false }, None).unwrap();
let l = broken.add_curve(CurveKind::Spline { pts: vec![p1, p0], control: true, degree: 1, periodic: false, nurbs: None }, None).unwrap();
broken.points[p0].pos = Vec2::new(f64::NAN, 4.0);
assert!(super::joint_derivatives(&broken, l, p1, 2, true, "a", "sketch.blend_curve").is_err(), "non-finite geometry");
let mut flat = solidcraft_sketch::Sketch::new();
let q0 = flat.add_point(Vec2::new(3.0, 3.0), None).unwrap();
let q1 = flat.add_point(Vec2::new(3.0, 3.0), None).unwrap();
let d = flat.add_curve(CurveKind::Spline { pts: vec![q0, q1], control: true, degree: 1, periodic: false }, None).unwrap();
let d = flat.add_curve(CurveKind::Spline { pts: vec![q0, q1], control: true, degree: 1, periodic: false, nurbs: None }, None).unwrap();
let e = super::joint_derivatives(&flat, d, q0, 2, true, "a", "sketch.blend_curve").unwrap_err().to_string();
assert!(e.contains("no tangent"), "{e}");
assert!(solidcraft_geom::hermite_bezier(Vec2::ZERO, Vec2::X, &[Vec2::Y; 5], &[Vec2::Y; 5]).is_none(), "no order above g4");
+2 -2
View File
@@ -156,7 +156,7 @@ fn add_pieces(sk: &mut Sketch, segs: &[Seg2], start: Option<usize>, end: Option<
Seg2::Line { .. } => CurveKind::Line { a: prev, b: to },
Seg2::Cubic { p1, p2, .. } => {
let (c1, c2) = (sk.add_point(p1, None)?, sk.add_point(p2, None)?);
CurveKind::Spline { pts: vec![prev, c1, c2, to], control: true, degree: 3, periodic: false }
CurveKind::Spline { pts: vec![prev, c1, c2, to], control: true, degree: 3, periodic: false, nurbs: None }
}
Seg2::Conic { apex, w, .. } => {
let x = sk.add_point(apex, None)?;
@@ -169,7 +169,7 @@ fn add_pieces(sk: &mut Sketch, segs: &[Seg2], start: Option<usize>, end: Option<
pts.push(sk.add_point(*q, None)?);
}
pts.push(to);
CurveKind::Spline { pts, control: true, degree: n, periodic: false }
CurveKind::Spline { pts, control: true, degree: n, periodic: false, nurbs: None }
}
Seg2::Arc { .. } => return Err(bad("trim", "unexpected arc piece")),
};
+1 -1
View File
@@ -101,7 +101,7 @@ pub(super) fn add_geometry(sk: &mut Sketch, geom: &[Geom2], f: &dyn Fn(Vec2) ->
idx.push(i);
}
}
(idx.len() >= 2).then_some(CurveKind::Spline { pts: idx, control: *control, degree: *degree, periodic: false })
(idx.len() >= 2).then_some(CurveKind::Spline { pts: idx, control: *control, degree: *degree, periodic: false, nurbs: None })
}
Geom2::Conic { a, apex, b, rho } => {
let (a, x, b) = (pt(sk, *a)?, pt(sk, *apex)?, pt(sk, *b)?);
+1 -1
View File
@@ -1764,7 +1764,7 @@ fn offset_side(sk: &mut Sketch, cmd: &str, order: &[(usize, bool)], sources: &[(
idx.push(pb);
idx.dedup();
if idx.len() >= 2 {
made.push(sk.add_curve(CurveKind::Spline { pts: idx, control: false, degree: 3, periodic: false }, None)?);
made.push(sk.add_curve(CurveKind::Spline { pts: idx, control: false, degree: 3, periodic: false, nurbs: None }, None)?);
}
}
Piece::Line(..) => {
+12 -2
View File
@@ -190,7 +190,12 @@ pub fn fit_beziers(p: &[Vec2]) -> Vec<[Vec2; 4]> {
pub fn fit_beziers_mode(p: &[Vec2], periodic: bool) -> crate::model::Result<Vec<[Vec2; 4]>> {
if periodic { crate::periodic_beziers(p) } else { Ok(fit_beziers(p)) }
}
pub fn spline_point_mode(p: &[Vec2], control: bool, degree: u8, periodic: bool, t: f64) -> Vec2 {
/// Point of a spline at `t` in [0, 1]: through its points, over them as a clamped uniform
/// B-spline, or with its own knots and weights (`nurbs`).
pub fn spline_point_mode(p: &[Vec2], control: bool, degree: u8, periodic: bool, nurbs: Option<&crate::Nurbs>, t: f64) -> Vec2 {
if let Some(nurbs) = nurbs {
return nurbs.point(p, degree, t).unwrap_or_else(|| p.first().copied().unwrap_or_default());
}
if !periodic {
return spline_point(p, control, degree, t);
}
@@ -211,7 +216,11 @@ pub fn spline_point_mode(p: &[Vec2], control: bool, degree: u8, periodic: bool,
let t = (remaining / lengths[i]).clamp(0.0, 1.0);
a * (1.0 - t).powi(3) + b * (3.0 * t * (1.0 - t).powi(2)) + c * (3.0 * t * t * (1.0 - t)) + d * t.powi(3)
}
pub fn spline_polyline_mode(p: &[Vec2], control: bool, degree: u8, periodic: bool) -> Vec<Vec2> {
/// Polyline of a spline, as [`spline_point_mode`] reads it.
pub fn spline_polyline_mode(p: &[Vec2], control: bool, degree: u8, periodic: bool, nurbs: Option<&crate::Nurbs>) -> Vec<Vec2> {
if let Some(nurbs) = nurbs {
return nurbs.polyline(p, degree);
}
if !periodic {
return spline_polyline(p, control, degree);
}
@@ -307,6 +316,7 @@ pub fn spline_polyline(p: &[Vec2], control: bool, degree: u8) -> Vec<Vec2> {
}
/// Unit tangent of a spline at its start (`at_end == false`) or end, pointing along the curve.
/// A control spline's runs along its end leg whatever its knots and (positive) weights.
pub fn spline_end_tangent(p: &[Vec2], control: bool, at_end: bool) -> Option<Vec2> {
let n = p.len();
if n < 2 {
+6 -5
View File
@@ -439,7 +439,7 @@ impl Sketch {
}
let output = self.curve_index(&record.output).ok_or_else(|| bad("offset output vanished"))?;
match &self.curves[output].kind {
CurveKind::Spline { pts, control: false, degree: 3, periodic }
CurveKind::Spline { pts, control: false, degree: 3, periodic, .. }
if *periodic == (record.certificate == 2)
&& (record.certificate != 2 || record.closed)
&& pts.len() == record.pieces * record.divisions + 1
@@ -484,7 +484,7 @@ impl Sketch {
p.fixed = true;
}
}
let output = candidate.add_curve(CurveKind::Spline { pts: indices, control: false, degree: 3, periodic: closed }, None)?;
let output = candidate.add_curve(CurveKind::Spline { pts: indices, control: false, degree: 3, periodic: closed, nurbs: None }, None)?;
candidate.freeform_offsets.push(FreeformOffset {
source: source_id,
output: candidate.curves[output].id.clone(),
@@ -551,7 +551,7 @@ impl Sketch {
return Err(bad("offset source topology changed"));
}
let old_indices = match &candidate.curves[output].kind {
CurveKind::Spline { pts, control: false, degree: 3, periodic }
CurveKind::Spline { pts, control: false, degree: 3, periodic, .. }
if *periodic == (record.certificate == 2)
&& (record.certificate != 2 || record.closed)
&& pts.len() == record.pieces * record.divisions + 1 =>
@@ -583,7 +583,8 @@ impl Sketch {
p.pos = pos;
p.fixed = true;
}
candidate.curves[output].kind = CurveKind::Spline { pts: indices, control: false, degree: 3, periodic: record.certificate == 2 };
candidate.curves[output].kind =
CurveKind::Spline { pts: indices, control: false, degree: 3, periodic: record.certificate == 2, nurbs: None };
candidate.freeform_offsets[record_index].divisions = divisions;
candidate.freeform_offsets[record_index].pieces = pieces;
}
@@ -632,7 +633,7 @@ mod tests {
.into_iter()
.map(|(x, y)| sk.add_point(Vec2::new(x, y), None).unwrap())
.collect();
let source = sk.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic: true }, None).unwrap();
let source = sk.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic: true, nurbs: None }, None).unwrap();
let original = sk.clone();
for (forward, distance) in [(true, 2.0), (false, -2.0)] {
let mut sk = original.clone();
+5 -4
View File
@@ -1080,7 +1080,7 @@ impl Sketch {
pts.push(candidate.add_point(*p, None)?);
}
pts.push(end);
CurveKind::Spline { pts, control: false, degree: 3, periodic: false }
CurveKind::Spline { pts, control: false, degree: 3, periodic: false, nurbs: None }
}
Out::Arc { center, sweep, .. } => {
let c = candidate.add_point(*center, None)?;
@@ -1111,7 +1111,7 @@ mod tests {
fn spline(coords: &[(f64, f64)], periodic: bool) -> (Sketch, usize) {
let mut sk = Sketch::new();
let pts = coords.iter().map(|(x, y)| sk.add_point(Vec2::new(*x, *y), None).unwrap()).collect();
let source = sk.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic }, None).unwrap();
let source = sk.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic, nurbs: None }, None).unwrap();
(sk, source)
}
@@ -1210,8 +1210,9 @@ mod tests {
for (k, b) in beziers.iter().enumerate() {
let mid = [1, 2].map(|i| sk.add_point(Vec2::new(b[i].0, b[i].1), None).unwrap());
let end = if k + 1 == beziers.len() { first } else { sk.add_point(Vec2::new(b[3].0, b[3].1), None).unwrap() };
let c =
sk.add_curve(CurveKind::Spline { pts: vec![start, mid[0], mid[1], end], control: true, degree: 3, periodic: false }, None).unwrap();
let c = sk
.add_curve(CurveKind::Spline { pts: vec![start, mid[0], mid[1], end], control: true, degree: 3, periodic: false, nurbs: None }, None)
.unwrap();
order.push((c, true));
start = end;
}
+2
View File
@@ -27,6 +27,7 @@ pub mod linalg;
mod link;
mod loci;
mod model;
mod nurbs;
mod offset;
mod offset_topology;
mod pattern;
@@ -55,6 +56,7 @@ pub use model::{
Constraint, ConstraintKind, Curve, CurveKind, MAX_CONSTRAINTS, MAX_COORD, MAX_CURVES, MAX_POINTS, PointExprs, SPoint, Sketch, SketchError,
SketchView, TwoCircles,
};
pub use nurbs::Nurbs;
pub use offset::OffsetChain;
pub use offset_topology::{OffsetCreated, OffsetRole, OffsetRoleFlags, OffsetRoleKind, OffsetTopology};
pub use pattern::{PatternAlignment, PatternInstance, SketchPattern, SketchPatternKind};
+53 -21
View File
@@ -238,12 +238,14 @@ pub enum LinkGeom {
major: Vec2,
minor: f64,
},
/// Spline through (`control == false`) or over (`control`) the points.
/// Spline through (`control == false`) or over (`control`) the points; a control spline
/// with its own knots and weights where it has them ([`crate::Nurbs`]).
Spline {
pts: Vec<Vec2>,
control: bool,
degree: u8,
periodic: bool,
nurbs: Option<crate::Nurbs>,
},
}
@@ -257,8 +259,9 @@ impl LinkGeom {
LinkGeom::Arc { c, a, b } => ok(c) && ok(a) && ok(b) && c.dist(*a) > 1e-9 && a.dist(*b) > MERGE_TOL,
LinkGeom::Conic { a, apex, b, rho } => ok(a) && ok(apex) && ok(b) && a.dist(*b) > MERGE_TOL && *rho > 1e-6 && *rho < 1.0 - 1e-6,
LinkGeom::Ellipse { c, major, minor } => ok(c) && ok(major) && c.dist(*major) > 1e-9 && radius_in_range(*minor),
LinkGeom::Spline { pts, control, degree, periodic } => {
LinkGeom::Spline { pts, control, degree, periodic, nurbs } => {
(!*periodic || (!*control && *degree == 3 && crate::periodic_beziers(pts).is_ok()))
&& nurbs.as_ref().is_none_or(|k| *control && !*periodic && k.check(pts.len(), *degree).is_ok())
&& (2..=crate::curves::MAX_SPLINE_POINTS).contains(&pts.len())
&& pts.iter().all(ok)
&& (1..=solidcraft_geom::MAX_BEZIER_DEGREE as u8).contains(degree)
@@ -275,7 +278,7 @@ struct Layout {
curves: Vec<LCurve>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
enum LCurve {
Line(usize, usize),
Circle(usize, f64),
@@ -283,8 +286,8 @@ enum LCurve {
Conic(usize, usize, usize, f64),
Ellipse(usize, usize, f64),
/// Start, end, first interior point and interior count (interior points are consecutive
/// and never shared), control flag, degree.
Spline(usize, usize, usize, usize, bool, u8, bool),
/// and never shared), control flag, degree, periodic, own knots and weights.
Spline(usize, usize, usize, usize, bool, u8, bool, Option<crate::Nurbs>),
}
fn layout(geom: &[LinkGeom]) -> Result<Layout> {
@@ -339,14 +342,14 @@ fn layout(geom: &[LinkGeom]) -> Result<Layout> {
l.curves.push(LCurve::Ellipse(c, m, minor));
}
}
LinkGeom::Spline { pts, control, degree, periodic } => {
LinkGeom::Spline { pts, control, degree, periodic, nurbs } => {
let (Some(first), Some(last)) = (pts.first(), pts.last()) else { continue };
let s0 = pt(&mut l.pts, *first);
let first_in = l.pts.len();
let inner = pts.get(1..pts.len() - 1).unwrap_or_default();
l.pts.extend(inner.iter().copied());
let s1 = pt(&mut l.pts, *last);
l.curves.push(LCurve::Spline(s0, s1, first_in, inner.len(), control, degree, periodic));
l.curves.push(LCurve::Spline(s0, s1, first_in, inner.len(), control, degree, periodic, nurbs));
}
}
}
@@ -402,6 +405,16 @@ fn same_shape(a: &LCurve, b: &LCurve) -> bool {
)
}
/// Whether two splines' own knots and weights ([`crate::Nurbs`]) are the same within `close`.
fn same_nurbs(a: Option<&crate::Nurbs>, b: Option<&crate::Nurbs>, close: impl Fn(f64, f64) -> bool) -> bool {
let all = |x: &[f64], y: &[f64]| x.len() == y.len() && x.iter().zip(y).all(|(p, q)| close(*p, *q));
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => all(&a.knots, &b.knots) && all(&a.weights, &b.weights),
_ => false,
}
}
/// Do two curve sets describe the same geometry, regardless of the order the curves come in or
/// the direction a loop was walked? Lines match with either end first; circles by centre and
/// radius; arcs by centre and their (start, end) pair; conics by all their points and rho.
@@ -422,7 +435,9 @@ fn same_geometry(a: &[(LCurve, Vec<Vec2>)], b: &[(LCurve, Vec<Vec2>)]) -> bool {
(LCurve::Arc(..), LCurve::Arc(..)) => all(),
(LCurve::Conic(.., ra), LCurve::Conic(.., rb)) => all() && close(*ra, *rb),
(LCurve::Ellipse(.., ra), LCurve::Ellipse(.., rb)) => all() && close(*ra, *rb),
(LCurve::Spline(.., ca, da, pa), LCurve::Spline(.., cb, db, pb)) => ca == cb && da == db && pa == pb && all(),
(LCurve::Spline(.., ca, da, pa, ka), LCurve::Spline(.., cb, db, pb, kb)) => {
ca == cb && da == db && pa == pb && same_nurbs(ka.as_ref(), kb.as_ref(), close) && all()
}
_ => false,
}
};
@@ -666,10 +681,10 @@ impl Sketch {
let at = |i: usize| self.points.get(i).map(|p| p.pos);
let near = |i: usize, q: Option<Vec2>| lay.pts.get(i).zip(q).is_some_and(|(p, q)| p.dist(q) <= MERGE_TOL);
let curve_present = |c: &LCurve| {
self.curves.iter().filter(|k| k.link.is_some()).any(|k| match (*c, &k.kind) {
(LCurve::Line(a, b), CurveKind::Line { a: p, b: q }) => (near(a, at(*p)) && near(b, at(*q))) || (near(a, at(*q)) && near(b, at(*p))),
(LCurve::Circle(c, r), CurveKind::Circle { c: p, r: s }) => near(c, at(*p)) && (r - s).abs() <= MERGE_TOL,
(LCurve::Arc(c, a, b), CurveKind::Arc { c: p, a: q, b: e }) => near(c, at(*p)) && near(a, at(*q)) && near(b, at(*e)),
self.curves.iter().filter(|k| k.link.is_some()).any(|k| match (c, &k.kind) {
(&LCurve::Line(a, b), CurveKind::Line { a: p, b: q }) => (near(a, at(*p)) && near(b, at(*q))) || (near(a, at(*q)) && near(b, at(*p))),
(&LCurve::Circle(c, r), CurveKind::Circle { c: p, r: s }) => near(c, at(*p)) && (r - s).abs() <= MERGE_TOL,
(&LCurve::Arc(c, a, b), CurveKind::Arc { c: p, a: q, b: e }) => near(c, at(*p)) && near(a, at(*q)) && near(b, at(*e)),
_ => false,
})
};
@@ -716,13 +731,13 @@ impl Sketch {
LCurve::Arc(c, a, b) => (CurveKind::Arc { c: point(self, c)?, a: point(self, a)?, b: point(self, b)? }, "a"),
LCurve::Conic(a, b, x, rho) => (CurveKind::Conic { a: point(self, a)?, b: point(self, b)?, apex: point(self, x)?, rho }, "k"),
LCurve::Ellipse(c, m, r) => (CurveKind::Ellipse { c: point(self, c)?, m: point(self, m)?, r }, "e"),
LCurve::Spline(a, b, f, n, control, degree, periodic) => {
LCurve::Spline(a, b, f, n, control, degree, periodic, ref nurbs) => {
let mut pts = vec![point(self, a)?];
for i in f..f + n {
pts.push(point(self, i)?);
}
pts.push(point(self, b)?);
(CurveKind::Spline { pts, control, degree, periodic }, "s")
(CurveKind::Spline { pts, control, degree, periodic, nurbs: nurbs.clone() }, "s")
}
};
let cid = self.fresh(prefix)?;
@@ -775,7 +790,7 @@ impl Sketch {
Some(CurveKind::Arc { c, a, b }) => Some(LCurve::Arc(local(*c)?, local(*a)?, local(*b)?)),
Some(CurveKind::Conic { a, b, apex, rho }) => Some(LCurve::Conic(local(*a)?, local(*b)?, local(*apex)?, *rho)),
Some(CurveKind::Ellipse { c, m, r }) => Some(LCurve::Ellipse(local(*c)?, local(*m)?, *r)),
Some(CurveKind::Spline { pts, control, degree, periodic }) => {
Some(CurveKind::Spline { pts, control, degree, periodic, nurbs }) => {
let (a, b) = (local(*pts.first()?)?, local(*pts.last()?)?);
let inner: Vec<usize> =
pts.get(1..pts.len().saturating_sub(1)).unwrap_or_default().iter().map(|q| local(*q)).collect::<Option<_>>()?;
@@ -784,7 +799,7 @@ impl Sketch {
if inner.iter().enumerate().any(|(k, x)| *x != f + k) {
return None;
}
Some(LCurve::Spline(a, b, f, inner.len(), *control, *degree, *periodic))
Some(LCurve::Spline(a, b, f, inner.len(), *control, *degree, *periodic, nurbs.clone()))
}
_ => None,
})
@@ -840,7 +855,7 @@ impl Sketch {
LCurve::Arc(c, a, b) => LCurve::Arc(renum(c), renum(a), renum(b)),
LCurve::Conic(a, b, x, r) => LCurve::Conic(renum(a), renum(b), renum(x), r),
LCurve::Ellipse(c, m, r) => LCurve::Ellipse(renum(c), renum(m), r),
LCurve::Spline(a, b, f, n, ct, d, periodic) => LCurve::Spline(renum(a), renum(b), renum(f), n, ct, d, periodic),
LCurve::Spline(a, b, f, n, ct, d, periodic, ref k) => LCurve::Spline(renum(a), renum(b), renum(f), n, ct, d, periodic, k.clone()),
})
.collect();
let same = match &cur {
@@ -855,8 +870,15 @@ impl Sketch {
(LCurve::Arc(a0, a1, a2), LCurve::Arc(b0, b1, b2)) => a0 == b0 && a1 == b1 && a2 == b2,
(LCurve::Conic(a0, a1, a2, _), LCurve::Conic(b0, b1, b2, _)) => a0 == b0 && a1 == b1 && a2 == b2,
(LCurve::Ellipse(a0, a1, _), LCurve::Ellipse(b0, b1, _)) => a0 == b0 && a1 == b1,
(LCurve::Spline(a0, a1, a2, a3, a4, a5, a6), LCurve::Spline(b0, b1, b2, b3, b4, b5, b6)) => {
a0 == b0 && a1 == b1 && (a2 == b2 || *a3 == 0) && a3 == b3 && a4 == b4 && a5 == b5 && a6 == b6
(LCurve::Spline(a0, a1, a2, a3, a4, a5, a6, a7), LCurve::Spline(b0, b1, b2, b3, b4, b5, b6, b7)) => {
a0 == b0
&& a1 == b1
&& (a2 == b2 || *a3 == 0)
&& a3 == b3
&& a4 == b4
&& a5 == b5
&& a6 == b6
&& a7.is_some() == b7.is_some()
}
_ => false,
}
@@ -873,12 +895,14 @@ impl Sketch {
let pos = |pi: usize| self.points.get(pi).map(|p| p.pos);
let old: Option<Vec<(LCurve, Vec<Vec2>)>> = cur
.iter()
.map(|c| curve_points(c).into_iter().map(|li| points.get(li).and_then(|pi| pos(*pi))).collect::<Option<Vec<_>>>().map(|ps| (*c, ps)))
.map(|c| {
curve_points(c).into_iter().map(|li| points.get(li).and_then(|pi| pos(*pi))).collect::<Option<Vec<_>>>().map(|ps| (c.clone(), ps))
})
.collect();
let fresh: Option<Vec<(LCurve, Vec<Vec2>)>> = lay
.curves
.iter()
.map(|c| curve_points(c).into_iter().map(|li| lay.pts.get(li).copied()).collect::<Option<Vec<_>>>().map(|ps| (*c, ps)))
.map(|c| curve_points(c).into_iter().map(|li| lay.pts.get(li).copied()).collect::<Option<Vec<_>>>().map(|ps| (c.clone(), ps)))
.collect();
let lone_old: Vec<Vec2> = points.iter().filter_map(|pi| pos(*pi)).collect();
if let (Some(old), Some(fresh)) = (old, fresh)
@@ -1028,6 +1052,11 @@ impl Sketch {
{
*r = *value;
}
if let (Some(Curve { kind: CurveKind::Spline { nurbs, .. }, .. }), Some(LCurve::Spline(.., value))) =
(self.curves.get_mut(*ci), lay.curves.get(li))
{
nurbs.clone_from(value);
}
}
return Ok(false);
}
@@ -1056,6 +1085,9 @@ impl Sketch {
if let (LCurve::Ellipse(.., r), Some(Curve { kind: CurveKind::Ellipse { r: slot, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
*slot = *r;
}
if let (LCurve::Spline(.., k), Some(Curve { kind: CurveKind::Spline { nurbs, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
nurbs.clone_from(k);
}
}
return Ok(false);
}
+21 -6
View File
@@ -104,6 +104,10 @@ pub enum CurveKind {
/// Explicit cyclic C2 fit interpolation; absent retains natural endpoints.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
periodic: bool,
/// A control spline's own knots and weights (a projected free-form edge as the exact
/// curve it is); none: the clamped uniform B-spline over its points, all weights 1.
#[serde(default, skip_serializing_if = "Option::is_none")]
nurbs: Option<crate::Nurbs>,
},
/// Conic from `a` to `b` shaped by the `apex` point and `rho` in (0, 1) (0.5 parabola,
/// less elliptical, more hyperbolic).
@@ -1463,8 +1467,9 @@ impl Sketch {
}
/// Exact boundary segments of a free-form curve: ellipses as four conics, conics, fit
/// splines and control splines up to `MAX_BEZIER_DEGREE` as Béziers. None for other curves
/// (and for higher-degree splines, which stay polylines).
/// splines and control splines up to `MAX_BEZIER_DEGREE` as Béziers (one a knot span for a
/// spline with its own knots, conics for a rational quadratic one). None for other curves
/// (and for higher-degree or other rational splines, which stay polylines).
pub fn exact_segs(&self, ci: usize) -> Option<Vec<Seg2>> {
let cu = self.curves.get(ci)?;
let pt = |i: &usize| self.point(*i);
@@ -1491,7 +1496,11 @@ impl Sketch {
crate::curves::fit_beziers_mode(&p, *periodic).ok()?.into_iter().map(|[p0, p1, p2, p3]| Seg2::Cubic { p0, p1, p2, p3 }).collect(),
)
}
CurveKind::Spline { pts, control: true, degree, .. } if (*degree as usize) <= solidcraft_geom::MAX_BEZIER_DEGREE => {
CurveKind::Spline { pts, control: true, degree, nurbs: Some(nurbs), .. } => {
let p: Vec<Vec2> = pts.iter().map(pt).collect::<Option<_>>()?;
nurbs.segs(&p, *degree)
}
CurveKind::Spline { pts, control: true, degree, nurbs: None, .. } if (*degree as usize) <= solidcraft_geom::MAX_BEZIER_DEGREE => {
let p: Vec<Vec2> = pts.iter().map(pt).collect::<Option<_>>()?;
let k = (*degree as usize).min(p.len().saturating_sub(1)).max(1);
Some(crate::curves::bspline_beziers(&p, k).iter().filter_map(|c| solidcraft_geom::bezier(c)).collect())
@@ -1521,9 +1530,9 @@ impl Sketch {
(Some(c), Some(m)) => crate::curves::ellipse_polyline(c, m, *r),
_ => Vec::new(),
},
CurveKind::Spline { pts, control, degree, periodic } => {
CurveKind::Spline { pts, control, degree, periodic, nurbs } => {
let p: Option<Vec<Vec2>> = pts.iter().map(pt).collect();
p.map(|p| crate::curves::spline_polyline_mode(&p, *control, *degree, *periodic)).unwrap_or_default()
p.map(|p| crate::curves::spline_polyline_mode(&p, *control, *degree, *periodic, nurbs.as_ref())).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),
@@ -1559,7 +1568,13 @@ impl Sketch {
}
"e"
}
CurveKind::Spline { pts, control, degree, periodic } => {
CurveKind::Spline { pts, control, degree, periodic, nurbs } => {
if let Some(nurbs) = nurbs {
if !*control || *periodic {
return Err(SketchError::Invalid("only an open control spline has its own knots".into()));
}
nurbs.check(pts.len(), *degree)?;
}
if pts.len() < 2 || pts.len() > crate::curves::MAX_SPLINE_POINTS || !(1..=solidcraft_geom::MAX_BEZIER_DEGREE as u8).contains(degree) {
return Err(SketchError::Invalid(format!(
"a spline needs 2 to 500 points and degree 1 to {}",
+200
View File
@@ -0,0 +1,200 @@
//! A control spline's own knots and weights ([`Nurbs`]): the exact form of a free-form edge a
//! sketch projects, whose knots need not be evenly spaced and whose weights need not be 1. A
//! control spline without them is the clamped uniform B-spline over its points.
use serde::{Deserialize, Serialize};
use solidcraft_geom::bspline::{self, HPoint};
use solidcraft_geom::{Seg2, Vec2, Vec3};
use crate::model::{Result, SketchError};
/// Most knots a spline may have (its points are capped at [`crate::MAX_SPLINE_POINTS`]).
const MAX_KNOTS: usize = crate::curves::MAX_SPLINE_POINTS + solidcraft_geom::MAX_BEZIER_DEGREE + 1;
/// Most a weight may exceed another by: past it the curve is all but its heaviest points.
const MAX_WEIGHT_RATIO: f64 = 1e12;
/// Polyline samples per knot span.
const SPAN_SAMPLES: usize = 24;
/// The knot vector and weights of a control spline of `degree` over `n` points: clamped (each
/// end knot `degree + 1` times, no interior knot more than `degree` times), `n + degree + 1`
/// non-decreasing knots, and one positive weight per point (none: all 1).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Nurbs {
pub knots: Vec<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub weights: Vec<f64>,
}
fn bad<T>(why: &str) -> Result<T> {
Err(SketchError::Invalid(format!("spline knots: {why}")))
}
impl Nurbs {
/// Whether these knots and weights fit a control spline of `degree` over `n` points.
pub fn check(&self, n: usize, degree: u8) -> Result<()> {
let d = usize::from(degree);
if !(1..=solidcraft_geom::MAX_BEZIER_DEGREE).contains(&d) || n <= d || n > crate::curves::MAX_SPLINE_POINTS {
return bad("a knotted spline needs more points than its degree, 1 to 9");
}
let k = &self.knots;
if k.len() != n + d + 1 || k.len() > MAX_KNOTS {
return bad("a spline needs its point count plus its degree plus one knots");
}
if k.iter().any(|x| !x.is_finite()) || k.windows(2).any(|w| w[1] < w[0]) {
return bad("knots must be finite and never decrease");
}
let (lo, hi) = (k[d], k[n]);
let span = hi - lo;
if !(span > 0.0 && span.is_finite()) || k[..=d].iter().any(|x| *x != lo) || k[n..].iter().any(|x| *x != hi) {
return bad("the knots must be clamped at both ends");
}
let mut run = 0usize;
for (i, x) in k.iter().enumerate().take(n).skip(d + 1) {
run = if k.get(i - 1) == Some(x) { run + 1 } else { 1 };
if *x == lo || *x == hi || run > d {
return bad("no knot inside the spline may repeat more than its degree");
}
}
if !self.weights.is_empty() {
if self.weights.len() != n || self.weights.iter().any(|w| !(w.is_finite() && *w > 0.0)) {
return bad("a rational spline needs one positive weight per point");
}
let (min, max) = self.weights.iter().fold((f64::INFINITY, 0.0f64), |(a, b), w| (a.min(*w), b.max(*w)));
if max / min > MAX_WEIGHT_RATIO {
return bad("the weights differ too much");
}
}
Ok(())
}
fn weight(&self, i: usize) -> f64 {
self.weights.get(i).copied().unwrap_or(1.0)
}
/// The control points `p` with their weights, homogeneous.
fn homogeneous(&self, p: &[Vec2]) -> Vec<HPoint> {
p.iter().enumerate().map(|(i, q)| HPoint::weighted(Vec3::new(q.x, q.y, 0.0), self.weight(i))).collect()
}
/// The knot domain `[lo, hi]` for `n` points of `degree`.
fn domain(&self, n: usize, degree: usize) -> Option<(f64, f64)> {
Some((*self.knots.get(degree)?, *self.knots.get(n)?))
}
/// The same curve run the other way: the knots mirrored, the weights reversed (the points
/// reverse with them).
pub fn reversed(&self) -> Nurbs {
let sum = self.knots.first().copied().unwrap_or(0.0) + self.knots.last().copied().unwrap_or(0.0);
Nurbs { knots: self.knots.iter().rev().map(|k| sum - k).collect(), weights: self.weights.iter().rev().copied().collect() }
}
/// The point of the spline over `p` (of `degree`) at `t` in [0, 1] across its knot domain.
/// `None` when the knots don't fit the points.
pub fn point(&self, p: &[Vec2], degree: u8, t: f64) -> Option<Vec2> {
self.check(p.len(), degree).ok()?;
let d = usize::from(degree);
let (lo, hi) = self.domain(p.len(), d)?;
self.eval(&self.homogeneous(p), d, lo + (hi - lo) * t.clamp(0.0, 1.0))
}
/// The point at knot parameter `u` of the spline of degree `d` over the homogeneous points
/// `h` (de Boor), the knots already checked.
fn eval(&self, h: &[HPoint], d: usize, u: f64) -> Option<Vec2> {
let k = &self.knots;
let n = h.len();
// The span holding u: the last knot ≤ u that starts a non-empty span.
let mut s = d;
while s + 1 < n && k.get(s + 1).is_some_and(|x| *x <= u) {
s += 1;
}
let mut c: Vec<HPoint> = h.get(s - d..=s)?.to_vec();
for r in 1..=d {
for j in (r..=d).rev() {
let i = s + j - d;
let (a, b) = (k.get(i).copied()?, k.get(i + d + 1 - r).copied()?);
let alpha = if b > a { (u - a) / (b - a) } else { 0.0 };
c[j] = c[j - 1] * (1.0 - alpha) + c[j] * alpha;
}
}
let q = c.get(d)?.project()?;
Some(Vec2::new(q.x, q.y))
}
/// The Bézier spans of the spline over `p`, homogeneous, in parameter order: every
/// interior knot inserted until it repeats `degree` times (Boehm). Exact up to rounding.
fn spans(&self, p: &[Vec2], degree: u8) -> Option<Vec<Vec<HPoint>>> {
self.check(p.len(), degree).ok()?;
let d = usize::from(degree);
let (mut k, mut c) = (self.knots.clone(), self.homogeneous(p));
let (lo, hi) = self.domain(p.len(), d)?;
let mut interior: Vec<f64> = self.knots.iter().copied().filter(|x| *x > lo && *x < hi).collect();
interior.dedup();
for u in interior {
while k.iter().filter(|x| **x == u).count() < d {
(k, c) = bspline::insert_knot(d, &k, &c, u).ok()?;
}
}
let mut spans = Vec::new();
for i in d..c.len() {
if k.get(i)? < k.get(i + 1)? {
spans.push(c.get(i - d..=i)?.to_vec());
}
}
Some(spans)
}
/// The spline over `p` as its exact [`Seg2`] pieces, one a knot span: Béziers, or conics
/// for a rational quadratic. `None` for other rational splines, which have no such pieces.
pub fn segs(&self, p: &[Vec2], degree: u8) -> Option<Vec<Seg2>> {
let spans = self.spans(p, degree)?;
let rational = self.weights.windows(2).any(|w| w[0] != w[1]);
let flat = |q: &HPoint| q.project().map(|v| Vec2::new(v.x, v.y));
spans
.iter()
.map(|span| {
let pts: Vec<Vec2> = span.iter().map(flat).collect::<Option<_>>()?;
match (rational, pts.as_slice()) {
(false, _) => solidcraft_geom::bezier(&pts),
(true, [a, apex, b]) => {
let w = span.get(1)?.w / (span.first()?.w * span.get(2)?.w).sqrt();
(w.is_finite() && w > 0.0).then_some(Seg2::Conic { a: *a, apex: *apex, b: *b, w })
}
_ => None,
}
})
.collect()
}
/// The spline over `p`, sampled evenly within each knot span.
pub fn polyline(&self, p: &[Vec2], degree: u8) -> Vec<Vec2> {
let d = usize::from(degree);
let Some((lo, hi)) = self.check(p.len(), degree).ok().and_then(|()| self.domain(p.len(), d)) else { return Vec::new() };
let mut breaks: Vec<f64> = self.knots.iter().copied().filter(|x| *x >= lo && *x <= hi).collect();
breaks.dedup();
let h = self.homogeneous(p);
let mut out = Vec::with_capacity(breaks.len() * SPAN_SAMPLES + 1);
for w in breaks.windows(2) {
for i in 0..SPAN_SAMPLES {
out.extend(self.eval(&h, d, w[0] + (w[1] - w[0]) * i as f64 / SPAN_SAMPLES as f64));
}
}
out.extend(self.eval(&h, d, hi));
out
}
}
impl crate::Sketch {
/// Every spline's own knots and weights fit it: on an open control spline, the right count,
/// clamped, with positive weights (a loaded design is checked before it is used).
pub fn validate_spline_knots(&self) -> Result<()> {
for curve in &self.curves {
if let crate::CurveKind::Spline { pts, control, degree, periodic, nurbs: Some(nurbs) } = &curve.kind {
if !*control || *periodic {
return bad("only an open control spline has its own knots");
}
nurbs.check(pts.len(), *degree)?;
}
}
Ok(())
}
}
+8 -6
View File
@@ -102,7 +102,7 @@ impl Sketch {
pub fn validate_periodic_splines(&self) -> Result<()> {
let mut work = 0usize;
for curve in &self.curves {
if let crate::CurveKind::Spline { pts, control, degree, periodic: true } = &curve.kind {
if let crate::CurveKind::Spline { pts, control, degree, periodic: true, .. } = &curve.kind {
work = work.saturating_add(pts.len());
if *control || *degree != 3 || work > 1_000_000 || pts.len() > MAX_SPLINE_POINTS {
return Err(bad());
@@ -136,7 +136,7 @@ mod tests {
(legacy[0][1] - legacy[0][0]).normalized().unwrap().dist((legacy.last().unwrap()[3] - legacy.last().unwrap()[2]).normalized().unwrap())
> 0.01
);
let global = |t| crate::spline_point_mode(&p, false, 3, true, t);
let global = |t| crate::spline_point_mode(&p, false, 3, true, None, t);
let h = 1e-5;
assert!(((global(h) - global(0.0)) / h - (global(1.0) - global(1.0 - h)) / h).len() < 0.002);
}
@@ -160,8 +160,9 @@ mod tests {
let c = sk.add_point(Vec2::new(-4.0, 0.0), None).unwrap();
let d = sk.add_point(Vec2::new(-3.0, -2.0), None).unwrap();
let last = sk.add_point(Vec2::ZERO, None).unwrap();
sk.add_curve(crate::CurveKind::Spline { pts: vec![first, a, b, seam], control: false, degree: 3, periodic: true }, None).unwrap();
sk.add_curve(crate::CurveKind::Spline { pts: vec![seam, c, d, last], control: false, degree: 3, periodic: true }, None).unwrap();
sk.add_curve(crate::CurveKind::Spline { pts: vec![first, a, b, seam], control: false, degree: 3, periodic: true, nurbs: None }, None)
.unwrap();
sk.add_curve(crate::CurveKind::Spline { pts: vec![seam, c, d, last], control: false, degree: 3, periodic: true, nurbs: None }, None).unwrap();
sk.points[first].pos = Vec2::new(0.2, 0.1);
assert!(crate::solve_with(&mut sk, &crate::Hold { points: vec![first], radii: vec![] }, None, crate::Analysis::Full, None).ok());
assert_eq!(sk.point(first), sk.point(seam));
@@ -186,7 +187,7 @@ mod tests {
let p = vec![Vec2::ZERO, Vec2::new(4.0, 0.0), Vec2::new(3.0, 2.0), Vec2::new(-1.0, 1.0), Vec2::ZERO];
let mut sk = Sketch::new();
let source = crate::LinkSource::SketchCurve { sketch: 99, curve: "source".into() };
let geometry = crate::LinkGeom::Spline { pts: p.clone(), control: false, degree: 3, periodic: true };
let geometry = crate::LinkGeom::Spline { pts: p.clone(), control: false, degree: 3, periodic: true, nurbs: None };
let id = sk.add_link(crate::LinkKind::Project, source, &[geometry]).unwrap();
let curve = sk.link_curves(&id)[0];
assert!(matches!(sk.curves[curve].kind, crate::CurveKind::Spline { periodic: true, .. }));
@@ -197,7 +198,8 @@ mod tests {
let mut singular = Sketch::new();
let points =
[Vec2::ZERO, Vec2::X, Vec2::ZERO, Vec2::new(0.0, 1.0), Vec2::ZERO].into_iter().map(|p| singular.add_point(p, None).unwrap()).collect();
let curve = singular.add_curve(crate::CurveKind::Spline { pts: points, control: false, degree: 3, periodic: true }, None).unwrap();
let curve =
singular.add_curve(crate::CurveKind::Spline { pts: points, control: false, degree: 3, periodic: true, nurbs: None }, None).unwrap();
let before = singular.clone();
assert!(singular.add_freeform_offset(curve, true, 0.1).is_err());
assert_eq!(singular, before);
+9 -7
View File
@@ -258,9 +258,9 @@ impl State<'_> {
};
crate::curves::ellipse_polyline(self.p(*ci), self.p(*m), r)
}
Some(CurveKind::Spline { pts, control, degree, periodic }) => {
Some(CurveKind::Spline { pts, control, degree, periodic, nurbs }) => {
let p = self.spline_points(pts, *periodic);
crate::curves::spline_polyline_mode(&p, *control, *degree, *periodic)
crate::curves::spline_polyline_mode(&p, *control, *degree, *periodic, nurbs.as_ref())
}
Some(CurveKind::Conic { a, b, apex, rho }) => crate::curves::conic_polyline(self.p(*a), self.p(*apex), self.p(*b), *rho),
_ => Vec::new(),
@@ -293,12 +293,14 @@ impl State<'_> {
let y = delta.cross(u) / (a * r);
return (x.hypot(y) - 1.0) * a.min(r);
}
Some(CurveKind::Spline { pts, control, degree, periodic }) => {
Some(CurveKind::Spline { pts, control, degree, periodic, nurbs }) => {
let p = self.spline_points(pts, *periodic);
if p.len() < 2 {
return q.dist(p.first().copied().unwrap_or_default());
}
if *control {
if let Some(nurbs) = nurbs {
nurbs.segs(&p, *degree).unwrap_or_default()
} else if *control {
let k = (*degree as usize).min(solidcraft_geom::MAX_BEZIER_DEGREE).min(p.len().saturating_sub(1)).max(1);
crate::curves::bspline_beziers(&p, k).iter().filter_map(|span| solidcraft_geom::bezier(span)).collect()
} else {
@@ -393,10 +395,10 @@ impl State<'_> {
let (cc, r) = self.round(c);
(cc - self.p(p)) / (r * r).max(1e-24)
}
CurveKind::Spline { pts, control, degree, periodic } => {
CurveKind::Spline { pts, control, degree, periodic, nurbs } => {
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_mode(&q, *control, *degree, *periodic, t), at_end)
crate::curves::end_curvature(&|t| crate::curves::spline_point_mode(&q, *control, *degree, *periodic, nurbs.as_ref(), t), at_end)
}
CurveKind::Conic { a, b, apex, rho } => {
let (pa, pb, px) = (self.p(*a), self.p(*b), self.p(*apex));
@@ -2938,7 +2940,7 @@ fn solve_inner(
}
}
for curve in &sk.curves {
if let CurveKind::Spline { pts, control, degree, periodic: true } = &curve.kind {
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;
+69 -5
View File
@@ -657,7 +657,7 @@ fn near_overlaps_that_are_not_one_path_keep_their_regions() {
.iter()
.map(|p| sk.add_point(*p, None).unwrap())
.collect();
sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: true, degree: 3, periodic: false }, None).unwrap();
sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: true, degree: 3, periodic: false, nurbs: None }, None).unwrap();
sk.add_line_pts(ids[3], ids[0], None).unwrap();
let ps = find_profiles(&sk);
assert_eq!(ps.len(), 1, "{:?}", ps.iter().map(|p| p.area).collect::<Vec<_>>());
@@ -953,7 +953,7 @@ fn exact_profiles_for_high_degree_and_cut_free_form_curves() {
.iter()
.map(|p| sk.add_point(*p, None).unwrap())
.collect();
sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: true, degree: 5, periodic: false }, None).unwrap();
sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: true, degree: 5, periodic: false, nurbs: None }, None).unwrap();
sk.add_line_pts(ids[5], ids[0], None).unwrap();
let pr = find_profiles(&sk);
assert_eq!(pr.len(), 1);
@@ -962,7 +962,7 @@ fn exact_profiles_for_high_degree_and_cut_free_form_curves() {
// A fit spline arch over a base line, cut by a vertical line at x = 10.
let mut sk = Sketch::default();
let ids: Vec<usize> = [v(0.0, 0.0), v(6.0, 9.0), v(14.0, 9.0), v(20.0, 0.0)].iter().map(|p| sk.add_point(*p, None).unwrap()).collect();
sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: false, degree: 3, periodic: false }, None).unwrap();
sk.add_curve(CurveKind::Spline { pts: ids.clone(), control: false, degree: 3, periodic: false, nurbs: None }, None).unwrap();
sk.add_line_pts(ids[3], ids[0], None).unwrap();
let (a, b) = (sk.add_point(v(10.0, -5.0), None).unwrap(), sk.add_point(v(10.0, 20.0), None).unwrap());
sk.add_line_pts(a, b, None).unwrap();
@@ -1033,7 +1033,7 @@ fn a_degree_nine_control_spline_stays_exact_and_closes_a_profile_exactly() {
let bump =
[v(0.0, 0.0), v(1.5, 4.0), v(3.0, 7.0), v(5.0, 9.5), v(7.5, 11.0), v(12.5, 11.0), v(15.0, 9.5), v(17.0, 7.0), v(18.5, 4.0), v(20.0, 0.0)];
let pts: Vec<usize> = bump.iter().map(|q| sk.add_point(*q, None).unwrap()).collect();
let s = sk.add_curve(CurveKind::Spline { pts: pts.clone(), control: true, degree: 9, periodic: false }, None).unwrap();
let s = sk.add_curve(CurveKind::Spline { pts: pts.clone(), control: true, degree: 9, periodic: false, nurbs: None }, None).unwrap();
let pieces = sk.exact_pieces(s);
assert_eq!(pieces.len(), 1, "{pieces:?}");
let Seg2::Bezier { n, p } = pieces[0] else { panic!("{:?}", pieces[0]) };
@@ -1068,7 +1068,7 @@ fn a_degree_nine_control_spline_stays_exact_and_closes_a_profile_exactly() {
// A degree above the cap is still refused.
let mut high: Vec<usize> = pts.clone();
high.push(sk.add_point(v(21.0, -2.0), None).unwrap());
assert!(sk.add_curve(CurveKind::Spline { pts: high, control: true, degree: 10, periodic: false }, None).is_err());
assert!(sk.add_curve(CurveKind::Spline { pts: high, control: true, degree: 10, periodic: false, nurbs: None }, None).is_err());
}
/// #755: a line drawn through a junction of a loop, but 2e-5 off it, still splits the loop. The
@@ -1295,3 +1295,67 @@ fn bounds_reach_the_extremes_of_circles_and_arcs() {
let (lo, hi) = sk.bounds().unwrap();
assert!(lo.dist(v(0.0, 0.0)) < 1e-9 && hi.dist(v(10.0, 10.0)) < 1e-9, "{lo:?} {hi:?}");
}
/// A control spline with its own knots and weights (a projected free-form edge): its exact pieces
/// are its knot spans, a rational quadratic's are conics, its points follow its knots, it reads
/// back from a saved design the same, and knots that don't fit it are refused.
#[test]
fn a_spline_with_its_own_knots_is_its_spans_exactly() {
let mut sk = Sketch::new();
// Two cubic Béziers end to end (the joint knot three times), the second twice as long in
// parameter: (0,0) (1,2) (2,2) (3,0) then (3,0) (4,-2) (5,-2) (6,0).
let ctrl = [v(0.0, 0.0), v(1.0, 2.0), v(2.0, 2.0), v(3.0, 0.0), v(4.0, -2.0), v(5.0, -2.0), v(6.0, 0.0)];
let nurbs = Nurbs { knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0, 3.0], weights: Vec::new() };
let pts: Vec<usize> = ctrl.iter().map(|q| sk.add_point(*q, None).unwrap()).collect();
let kind = CurveKind::Spline { pts: pts.clone(), control: true, degree: 3, periodic: false, nurbs: Some(nurbs.clone()) };
let s = sk.add_curve(kind.clone(), None).unwrap();
let pieces = sk.exact_pieces(s);
assert_eq!(pieces.len(), 2, "{pieces:?}");
let Seg2::Cubic { p0, p1, p2, p3 } = pieces[1] else { panic!("{:?}", pieces[1]) };
for (got, want) in [(p0, ctrl[3]), (p1, ctrl[4]), (p2, ctrl[5]), (p3, ctrl[6])] {
assert!(got.dist(want) < 1e-12, "{got:?} {want:?}");
}
// t runs over the knots: a third of the way is the joint, where the uniform spline over the
// same points would not be.
assert!(nurbs.point(&ctrl, 3, 1.0 / 3.0).unwrap().dist(ctrl[3]) < 1e-12);
assert!(crate::curves::spline_point(&ctrl, true, 3, 1.0 / 3.0).dist(ctrl[3]) > 1e-3);
let back = nurbs.reversed();
let rev: Vec<Vec2> = ctrl.iter().rev().copied().collect();
for k in 0..=12 {
let t = k as f64 / 12.0;
assert!(nurbs.point(&ctrl, 3, t).unwrap().dist(back.point(&rev, 3, 1.0 - t).unwrap()) < 1e-12, "t={t}");
}
let poly = sk.polyline(s);
assert!(poly.first().unwrap().dist(ctrl[0]) < 1e-12 && poly.last().unwrap().dist(ctrl[6]) < 1e-12);
// Saved and read back: the same curve.
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(serde_json::from_str::<CurveKind>(&json).unwrap(), kind);
// A quarter circle of radius 2 as a rational quadratic: one conic, its middle point on the circle.
let w = std::f64::consts::FRAC_1_SQRT_2;
let quarter = Nurbs { knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], weights: vec![1.0, w, 1.0] };
let arc = [v(2.0, 0.0), v(2.0, 2.0), v(0.0, 2.0)];
let [Seg2::Conic { w: cw, .. }] = quarter.segs(&arc, 2).unwrap()[..] else { panic!("conic") };
assert!((cw - w).abs() < 1e-12);
assert!((quarter.point(&arc, 2, 0.5).unwrap().len() - 2.0).abs() < 1e-12);
// Knots that don't fit: the wrong count, not clamped, decreasing, not finite, an interior knot
// past the degree, a weight that isn't positive; and knots on a fit spline.
let bad = [
Nurbs { knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], weights: Vec::new() },
Nurbs { knots: vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0, 3.0], weights: Vec::new() },
Nurbs { knots: vec![0.0, 0.0, 0.0, 0.0, 2.0, 1.0, 1.0, 3.0, 3.0, 3.0, 3.0], weights: Vec::new() },
Nurbs { knots: vec![0.0, 0.0, 0.0, 0.0, f64::NAN, 1.0, 1.0, 3.0, 3.0, 3.0, 3.0], weights: Vec::new() },
Nurbs { knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0, 3.0], weights: Vec::new() },
Nurbs { knots: nurbs.knots.clone(), weights: vec![1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0] },
];
for (i, k) in bad.iter().enumerate() {
assert!(k.check(ctrl.len(), 3).is_err(), "{i}");
assert!(
sk.add_curve(CurveKind::Spline { pts: pts.clone(), control: true, degree: 3, periodic: false, nurbs: Some(k.clone()) }, None).is_err(),
"{i}"
);
assert!(k.point(&ctrl, 3, 0.5).is_none() && k.segs(&ctrl, 3).is_none() && k.polyline(&ctrl, 3).is_empty(), "{i}");
}
let fourfold = Nurbs { knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0, 3.0], weights: Vec::new() };
assert!(fourfold.check(8, 3).is_err());
assert!(sk.add_curve(CurveKind::Spline { pts, control: false, degree: 3, periodic: false, nurbs: Some(nurbs) }, None).is_err());
}
+1 -1
View File
@@ -53,7 +53,7 @@ impl Outline {
self.line(to);
return;
}
self.out.push(LinkGeom::Spline { pts: vec![a, c1, c2, b], control: true, degree: 3, periodic: false });
self.out.push(LinkGeom::Spline { pts: vec![a, c1, c2, b], control: true, degree: 3, periodic: false, nurbs: None });
self.cur = to;
}
}
+1 -1
View File
@@ -378,7 +378,7 @@ fn outline_sketch(boundary: &[Seg2]) -> Result<Sketch> {
}
Seg2::Cubic { p0, p1, p2, p3 } => {
let pts = [p0, p1, p2, p3].iter().map(|p| sk.add_point(*p, None)).collect::<Result<Vec<_>>>()?;
sk.add_curve(CurveKind::Spline { pts, control: true, degree: 3, periodic: false }, None)?;
sk.add_curve(CurveKind::Spline { pts, control: true, degree: 3, periodic: false, nurbs: None }, None)?;
}
_ => return Err(bad("not supported yet: this kind of wall curve")),
}
+2 -2
View File
@@ -51,7 +51,7 @@ fn curve_centres_and_spline_interior_points_are_used_and_invalid_incidence_is_sa
let last = point(&mut sk, 21., 21.);
let mut spline = sk.curves[a].clone();
spline.id = "interior_spline".into();
spline.kind = CurveKind::Spline { pts: vec![first, interior, last], control: false, degree: 3, periodic: false };
spline.kind = CurveKind::Spline { pts: vec![first, interior, last], control: false, degree: 3, periodic: false, nurbs: None };
sk.curves.push(spline);
for p in [centre, interior] {
for c in [a, b] {
@@ -91,7 +91,7 @@ fn oversized_reference_input_is_refused_without_scanning_or_mutating_it() {
let mut sk = Sketch::new();
let a = sk.add_line(Vec2::new(0., 1.), Vec2::new(2., 1.), None, None, None).unwrap();
let before = sk.points.clone();
sk.curves[a].kind = CurveKind::Spline { pts: vec![usize::MAX; 1_000_001], control: false, degree: 3, periodic: false };
sk.curves[a].kind = CurveKind::Spline { pts: vec![usize::MAX; 1_000_001], control: false, degree: 3, periodic: false, nurbs: None };
assert!(sk.virtual_corners().is_empty());
assert_eq!(sk.points, before);
assert_eq!(sk.curves[a].kind.point_ids().len(), 1_000_001);
+1
View File
@@ -976,6 +976,7 @@ pub(crate) fn preview_curve(
control: t.cmd != "sketch.spline.fit_point",
degree: if t.cmd == "sketch.spline.control_point_5" { 5 } else { 3 },
periodic,
nurbs: None,
},
None,
)