feat: strengthen modeling, configuration, and assembly workflows
Preserve linked sketch identities through deterministic face projection and resizing. Fix origin-corner picking, persistent inline navigation, appearance editing, configuration styles, component properties, contact sweeps, sheet exports, analytic STEP offsets, and bounded rolling-ball fit accuracy. Validated with full cargo xtask ci, native workflow screenshots, repeated enclosure replays, and analytic geometry regressions.
This commit is contained in:
1 parent
22672585c9
commit
d71bdb78ec
54 files changed
+4179
-316
No files matched your search
+141
-31
@@ -300,6 +300,13 @@ fn same_geometry(a: &[(LCurve, Vec<Vec2>)], b: &[(LCurve, Vec<Vec2>)]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
// Unchanged ordered links are common; avoid a quadratic permutation scan.
|
||||
if a.iter().zip(b).all(|(x, y)| matches(x, y)) {
|
||||
return true;
|
||||
}
|
||||
if a.len() > 512 {
|
||||
return false;
|
||||
}
|
||||
let mut used = vec![false; b.len()];
|
||||
a.iter().all(|x| {
|
||||
let hit = b.iter().enumerate().find(|(j, y)| !used.get(*j).copied().unwrap_or(true) && matches(x, y)).map(|(j, _)| j);
|
||||
@@ -330,6 +337,48 @@ fn curve_points(c: &LCurve) -> Vec<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Incidence roles distinguish nearby arc centres, endpoints and hole centres during a
|
||||
/// resize. Spatial proximity alone can select a different kind of point in a dense rim.
|
||||
fn point_roles(curves: &[LCurve], point_count: usize) -> Vec<[usize; 10]> {
|
||||
let mut roles = vec![[0; 10]; point_count];
|
||||
let mut add = |point: usize, role: usize| {
|
||||
if let Some(count) = roles.get_mut(point).and_then(|r| r.get_mut(role)) {
|
||||
*count += 1;
|
||||
}
|
||||
};
|
||||
for curve in curves {
|
||||
match *curve {
|
||||
LCurve::Line(a, b) => {
|
||||
add(a, 0);
|
||||
add(b, 0);
|
||||
}
|
||||
LCurve::Circle(c, _) => add(c, 1),
|
||||
LCurve::Arc(c, a, b) => {
|
||||
add(c, 2);
|
||||
add(a, 3);
|
||||
add(b, 3);
|
||||
}
|
||||
LCurve::Conic(a, b, x, _) => {
|
||||
add(a, 4);
|
||||
add(b, 4);
|
||||
add(x, 5);
|
||||
}
|
||||
LCurve::Ellipse(c, m, _) => {
|
||||
add(c, 6);
|
||||
add(m, 7);
|
||||
}
|
||||
LCurve::Spline(a, b, first, count, ..) => {
|
||||
add(a, 8);
|
||||
add(b, 8);
|
||||
for i in first..first + count {
|
||||
add(i, 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
roles
|
||||
}
|
||||
|
||||
impl Sketch {
|
||||
pub fn link(&self, id: &str) -> Option<&Link> {
|
||||
self.links.iter().find(|l| l.id == id)
|
||||
@@ -539,26 +588,6 @@ impl Sketch {
|
||||
if let Some(l) = self.links.iter_mut().find(|l| l.id == id) {
|
||||
l.lost = false;
|
||||
}
|
||||
if same {
|
||||
for (k, pi) in points.iter().enumerate() {
|
||||
let p = order.get(k).and_then(|li| lay.pts.get(*li)).copied();
|
||||
if let (Some(p), Some(sp)) = (p, self.points.get_mut(*pi)) {
|
||||
sp.pos = p;
|
||||
}
|
||||
}
|
||||
for (ci, c) in curves.iter().zip(&new) {
|
||||
if let (LCurve::Circle(_, r), Some(Curve { kind: CurveKind::Circle { r: slot, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
|
||||
*slot = *r;
|
||||
}
|
||||
if let (LCurve::Conic(.., r), Some(Curve { kind: CurveKind::Conic { rho, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
|
||||
*rho = *r;
|
||||
}
|
||||
if let (LCurve::Ellipse(.., r), Some(Curve { kind: CurveKind::Ellipse { r: slot, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
|
||||
*slot = *r;
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
// The same geometry walked in another order or direction (e.g. a face loop re-read after
|
||||
// a rollback) is not a change: keep the entities, and the constraints on them.
|
||||
if let Some(cur) = &cur {
|
||||
@@ -588,6 +617,7 @@ impl Sketch {
|
||||
&& points.len() == lay.pts.len()
|
||||
&& cur.len() == lay.curves.len()
|
||||
&& points.len() <= 512
|
||||
&& cur.len() <= 512
|
||||
{
|
||||
let old: Vec<Vec2> = points.iter().filter_map(|i| self.point(*i)).collect();
|
||||
let normalized = |ps: &[Vec2]| -> Vec<Vec2> {
|
||||
@@ -596,9 +626,11 @@ impl Sketch {
|
||||
ps.iter().map(|p| Vec2::new((p.x - lo.x) / (hi.x - lo.x).max(1e-9), (p.y - lo.y) / (hi.y - lo.y).max(1e-9))).collect()
|
||||
};
|
||||
let (a, b) = (normalized(&old), normalized(&lay.pts));
|
||||
let (old_roles, fresh_roles) = (point_roles(cur, old.len()), point_roles(&lay.curves, lay.pts.len()));
|
||||
let mut mapping = Vec::new();
|
||||
for p in &a {
|
||||
let mut candidates: Vec<(usize, f64)> = b.iter().enumerate().map(|(i, q)| (i, p.dist(*q))).collect();
|
||||
for (old_i, p) in a.iter().enumerate() {
|
||||
let mut candidates: Vec<(usize, f64)> =
|
||||
b.iter().enumerate().filter(|(i, _)| old_roles.get(old_i) == fresh_roles.get(*i)).map(|(i, q)| (i, p.dist(*q))).collect();
|
||||
candidates.sort_by(|x, y| x.1.total_cmp(&y.1));
|
||||
if let Some((i, d)) = candidates.first()
|
||||
&& *d < 0.1
|
||||
@@ -613,22 +645,59 @@ impl Sketch {
|
||||
let mut used = Vec::new();
|
||||
let mut curve_map = Vec::new();
|
||||
if mapping.len() == points.len() {
|
||||
for c in cur {
|
||||
for (curve_i, c) in cur.iter().enumerate() {
|
||||
let mapped: Option<Vec<usize>> = curve_points(c).iter().map(|i| mapping.get(*i).copied()).collect();
|
||||
let found = lay
|
||||
let candidates: Vec<usize> = lay
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(i, fresh)| {
|
||||
if used.contains(i) || !same_shape(c, fresh) {
|
||||
return false;
|
||||
.filter_map(|(i, fresh)| {
|
||||
if !same_shape(c, fresh) {
|
||||
return None;
|
||||
}
|
||||
let fresh_pts = curve_points(fresh);
|
||||
mapped.as_ref().is_some_and(|p| {
|
||||
*p == fresh_pts || matches!(c, LCurve::Line(..)) && p.iter().rev().copied().collect::<Vec<_>>() == fresh_pts
|
||||
})
|
||||
mapped
|
||||
.as_ref()
|
||||
.is_some_and(|p| {
|
||||
*p == fresh_pts || matches!(c, LCurve::Line(..)) && p.iter().rev().copied().collect::<Vec<_>>() == fresh_pts
|
||||
})
|
||||
.then_some(i)
|
||||
})
|
||||
.map(|(i, _)| i);
|
||||
.collect();
|
||||
let found = if candidates.len() == 1 {
|
||||
candidates.first().copied()
|
||||
} else if let LCurve::Circle(center, _) = c {
|
||||
// Concentric circular boundaries share a point graph. Their radius
|
||||
// order identifies the inner/outer rings through an upstream resize.
|
||||
let mut previous: Vec<(usize, f64)> = cur
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, x)| match x {
|
||||
LCurve::Circle(p, r) if p == center => Some((i, *r)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let mut incoming: Vec<(usize, f64)> = candidates
|
||||
.iter()
|
||||
.filter_map(|i| match lay.curves.get(*i) {
|
||||
Some(LCurve::Circle(_, r)) => Some((*i, *r)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
previous.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
incoming.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let distinct = |xs: &[(usize, f64)]| {
|
||||
xs.windows(2).all(|w| matches!(w, [a, b] if b.1 - a.1 > MERGE_TOL * (1.0 + a.1.abs().max(b.1.abs()))))
|
||||
};
|
||||
if previous.len() == incoming.len() && distinct(&previous) && distinct(&incoming) {
|
||||
previous.iter().position(|(i, _)| *i == curve_i).and_then(|rank| incoming.get(rank).map(|(i, _)| *i))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let found = found.filter(|i| !used.contains(i));
|
||||
if let Some(i) = found {
|
||||
used.push(i);
|
||||
curve_map.push(i);
|
||||
@@ -649,10 +718,47 @@ impl Sketch {
|
||||
{
|
||||
*r = *value;
|
||||
}
|
||||
if let (Some(Curve { kind: CurveKind::Conic { rho, .. }, .. }), Some(LCurve::Conic(.., value))) =
|
||||
(self.curves.get_mut(*ci), lay.curves.get(li))
|
||||
{
|
||||
*rho = *value;
|
||||
}
|
||||
if let (Some(Curve { kind: CurveKind::Ellipse { r, .. }, .. }), Some(LCurve::Ellipse(.., value))) =
|
||||
(self.curves.get_mut(*ci), lay.curves.get(li))
|
||||
{
|
||||
*r = *value;
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
// Equal topology alone does not identify a face boundary: an outer rim and a
|
||||
// hole can have identical point graphs. Never move saved curve ids between them
|
||||
// when the geometric match is ambiguous; the document reports a lost link.
|
||||
if same && self.link(id).is_some_and(|l| matches!(l.source, LinkSource::Face { .. })) {
|
||||
self.set_link_lost(id, true);
|
||||
return Err(SketchError::Invalid("projected face boundary identity is ambiguous; last geometry retained".into()));
|
||||
}
|
||||
if same {
|
||||
for (k, pi) in points.iter().enumerate() {
|
||||
let p = order.get(k).and_then(|li| lay.pts.get(*li)).copied();
|
||||
if let (Some(p), Some(sp)) = (p, self.points.get_mut(*pi)) {
|
||||
sp.pos = p;
|
||||
}
|
||||
}
|
||||
for (ci, c) in curves.iter().zip(&new) {
|
||||
if let (LCurve::Circle(_, r), Some(Curve { kind: CurveKind::Circle { r: slot, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
|
||||
*slot = *r;
|
||||
}
|
||||
if let (LCurve::Conic(.., r), Some(Curve { kind: CurveKind::Conic { rho, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
|
||||
*rho = *r;
|
||||
}
|
||||
if let (LCurve::Ellipse(.., r), Some(Curve { kind: CurveKind::Ellipse { r: slot, .. }, .. })) = (c, self.curves.get_mut(*ci)) {
|
||||
*slot = *r;
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
// Rebuild: drop the old entities (constraints on them go too) and make new ones.
|
||||
self.drop_link_entities(id);
|
||||
self.build_link(id, &lay)?;
|
||||
@@ -724,3 +830,7 @@ impl Sketch {
|
||||
self.links.retain(|_| k.next().copied().unwrap_or(true));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "link_identity_tests.rs"]
|
||||
mod identity_tests;
|
||||
@@ -0,0 +1,158 @@
|
||||
use super::*;
|
||||
use crate::{ConstraintKind, solve};
|
||||
|
||||
fn square(size: f64) -> Vec<LinkGeom> {
|
||||
let pts = [Vec2::new(-size, -size), Vec2::new(size, -size), Vec2::new(size, size), Vec2::new(-size, size)];
|
||||
(0..4).map(|i| LinkGeom::Line(pts[i], pts[(i + 1) % 4])).collect()
|
||||
}
|
||||
|
||||
fn face() -> LinkSource {
|
||||
LinkSource::Face { body: "Case".into(), at: Vec3::ZERO }
|
||||
}
|
||||
|
||||
fn ring(outer_first: bool, outer: f64, inner: f64) -> Vec<LinkGeom> {
|
||||
let (first, second) = if outer_first { (outer, inner) } else { (inner, outer) };
|
||||
square(first).into_iter().chain(square(second)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_face_link_keeps_inner_ids_when_boundary_order_changes() {
|
||||
let mut sk = Sketch::new();
|
||||
// An older saved face may have enumerated its inner boundary first.
|
||||
let link = sk.add_link(LinkKind::Project, face(), &ring(false, 10.0, 8.0)).unwrap();
|
||||
let inner = sk.link_curves(&link)[0];
|
||||
let corner = match sk.curves[inner].kind {
|
||||
CurveKind::Line { a, .. } => a,
|
||||
_ => panic!(),
|
||||
};
|
||||
let free = sk.add_point(Vec2::new(-8.0, -8.0), None).unwrap();
|
||||
sk.add_constraint(ConstraintKind::Coincident { p: free, q: corner }, None).unwrap();
|
||||
let serialized = serde_json::to_string(&sk).unwrap();
|
||||
let mut sk: Sketch = serde_json::from_str(&serialized).unwrap();
|
||||
let before = sk.clone();
|
||||
assert!(!sk.update_link(&link, &ring(true, 10.0, 8.0)).unwrap());
|
||||
assert_eq!(sk, before, "canonical outer-first enumeration must not reinterpret saved ids");
|
||||
assert!(solve(&mut sk).ok());
|
||||
assert_eq!(sk.points[free].pos, Vec2::new(-8.0, -8.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resized_and_reordered_face_keeps_constraints_on_the_same_boundary() {
|
||||
let mut sk = Sketch::new();
|
||||
let link = sk.add_link(LinkKind::Project, face(), &ring(false, 10.0, 8.0)).unwrap();
|
||||
let points = sk.link_points(&link);
|
||||
let original: Vec<Vec2> = points.iter().map(|p| sk.points[*p].pos).collect();
|
||||
let ids: Vec<String> = sk.link_curves(&link).iter().map(|c| sk.curves[*c].id.clone()).collect();
|
||||
let free = sk.add_point(Vec2::new(-8.0, -8.0), None).unwrap();
|
||||
sk.add_constraint(ConstraintKind::Coincident { p: free, q: points[0] }, None).unwrap();
|
||||
let fresh: Vec<LinkGeom> = ring(true, 10.0, 8.0)
|
||||
.into_iter()
|
||||
.map(|g| match g {
|
||||
LinkGeom::Line(a, b) => LinkGeom::Line(Vec2::new(b.x * 2.0, b.y * 3.0), Vec2::new(a.x * 2.0, a.y * 3.0)),
|
||||
_ => panic!(),
|
||||
})
|
||||
.collect();
|
||||
assert!(!sk.update_link(&link, &fresh).unwrap());
|
||||
for (p, old) in points.iter().zip(original) {
|
||||
assert_eq!(sk.points[*p].pos, Vec2::new(old.x * 2.0, old.y * 3.0));
|
||||
}
|
||||
assert_eq!(ids, sk.link_curves(&link).iter().map(|c| sk.curves[*c].id.clone()).collect::<Vec<_>>());
|
||||
assert!(solve(&mut sk).ok());
|
||||
assert!(sk.points[free].pos.dist(Vec2::new(-16.0, -24.0)) < 1e-7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambiguous_face_refresh_preserves_last_geometry_and_marks_link_lost() {
|
||||
let mut sk = Sketch::new();
|
||||
let link = sk.add_link(LinkKind::Project, face(), &ring(true, 10.0, 9.9)).unwrap();
|
||||
let before = sk.clone();
|
||||
assert!(sk.update_link(&link, &ring(false, 10.0, 2.0)).is_err());
|
||||
assert_eq!(sk.points, before.points);
|
||||
assert_eq!(sk.curves, before.curves);
|
||||
assert!(sk.link(&link).unwrap().lost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_face_permutation_is_bounded_and_preserves_geometry() {
|
||||
let mut sk = Sketch::new();
|
||||
let geom: Vec<LinkGeom> = (1..=129).flat_map(|i| square(i as f64)).collect();
|
||||
let link = sk.add_link(LinkKind::Project, face(), &geom).unwrap();
|
||||
assert!(!sk.update_link(&link, &geom).unwrap(), "ordered unchanged geometry needs no permutation scan");
|
||||
let before = sk.clone();
|
||||
let reversed: Vec<LinkGeom> = (1..=129).rev().flat_map(|i| square(i as f64)).collect();
|
||||
assert!(sk.update_link(&link, &reversed).is_err());
|
||||
assert_eq!(sk.points, before.points);
|
||||
assert_eq!(sk.curves, before.curves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometric_refresh_updates_conic_and_ellipse_scalar_values() {
|
||||
let mut sk = Sketch::new();
|
||||
let geom = |rho, minor| {
|
||||
vec![
|
||||
LinkGeom::Conic { a: Vec2::new(1.0, 1.0), apex: Vec2::new(2.0, 3.0), b: Vec2::new(4.0, 1.0), rho },
|
||||
LinkGeom::Ellipse { c: Vec2::new(10.0, 10.0), major: Vec2::new(14.0, 10.0), minor },
|
||||
]
|
||||
};
|
||||
let link = sk.add_link(LinkKind::Project, LinkSource::Origin, &geom(0.4, 2.0)).unwrap();
|
||||
assert!(!sk.update_link(&link, &geom(0.6, 3.0)).unwrap());
|
||||
assert!(matches!(sk.curves[0].kind, CurveKind::Conic { rho, .. } if rho == 0.6));
|
||||
assert!(matches!(sk.curves[1].kind, CurveKind::Ellipse { r, .. } if r == 3.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_curves_sharing_few_points_do_not_enter_permutation_matching() {
|
||||
let mut sk = Sketch::new();
|
||||
let geom = vec![LinkGeom::Line(Vec2::new(1.0, 1.0), Vec2::new(2.0, 2.0)); 513];
|
||||
let link = sk.add_link(LinkKind::Project, face(), &geom).unwrap();
|
||||
assert_eq!(sk.link_points(&link).len(), 2);
|
||||
let before = sk.clone();
|
||||
let shifted = vec![LinkGeom::Line(Vec2::new(3.0, 3.0), Vec2::new(4.0, 4.0)); 513];
|
||||
assert!(sk.update_link(&link, &shifted).is_err());
|
||||
assert_eq!(sk.points, before.points);
|
||||
assert_eq!(sk.curves, before.curves);
|
||||
assert!(sk.link(&link).unwrap().lost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concentric_face_resize_keeps_inner_outer_circle_identity() {
|
||||
let mut sk = Sketch::new();
|
||||
let link = sk.add_link(LinkKind::Project, face(), &[LinkGeom::Circle(Vec2::ZERO, 8.0), LinkGeom::Circle(Vec2::ZERO, 10.0)]).unwrap();
|
||||
let ids: Vec<String> = sk.link_curves(&link).iter().map(|i| sk.curves[*i].id.clone()).collect();
|
||||
assert!(!sk.update_link(&link, &[LinkGeom::Circle(Vec2::ZERO, 20.0), LinkGeom::Circle(Vec2::ZERO, 16.0)]).unwrap());
|
||||
assert!(matches!(sk.curves[0].kind, CurveKind::Circle { r, .. } if r == 16.0));
|
||||
assert!(matches!(sk.curves[1].kind, CurveKind::Circle { r, .. } if r == 20.0));
|
||||
assert_eq!(ids, sk.link_curves(&link).iter().map(|i| sk.curves[*i].id.clone()).collect::<Vec<_>>());
|
||||
// Coincident circles do not have a unique boundary identity.
|
||||
let before = sk.clone();
|
||||
assert!(sk.update_link(&link, &[LinkGeom::Circle(Vec2::ZERO, 18.0), LinkGeom::Circle(Vec2::ZERO, 18.0)]).is_err());
|
||||
assert_eq!(sk.points, before.points);
|
||||
assert_eq!(sk.curves, before.curves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resized_dense_rim_matches_arc_centres_by_role_before_proximity() {
|
||||
let mut sk = Sketch::new();
|
||||
let geometry = |half_width: f64, x: f64| {
|
||||
let mut out = square(half_width);
|
||||
let c = Vec2::new(x, -16.5);
|
||||
let left = c - Vec2::X * 3.0;
|
||||
let right = c + Vec2::X * 3.0;
|
||||
out.push(LinkGeom::Arc { c, a: right, b: left });
|
||||
out.push(LinkGeom::Arc { c, a: left, b: right });
|
||||
out
|
||||
};
|
||||
let link = sk.add_link(LinkKind::Project, face(), &geometry(38.0, -26.5)).unwrap();
|
||||
let arcs: Vec<usize> = sk.link_curves(&link).into_iter().filter(|i| matches!(sk.curves[*i].kind, CurveKind::Arc { .. })).collect();
|
||||
let ids: Vec<String> = arcs.iter().map(|i| sk.curves[*i].id.clone()).collect();
|
||||
// The old centre is spatially closer in normalized space to a fresh endpoint.
|
||||
// Both semicircles must still keep the same shared centre and logical endpoints.
|
||||
assert!(!sk.update_link(&link, &geometry(43.0, -31.5)).unwrap());
|
||||
for i in &arcs {
|
||||
let CurveKind::Arc { c, a, b } = sk.curves[*i].kind else { panic!() };
|
||||
assert_eq!(sk.points[c].pos, Vec2::new(-31.5, -16.5));
|
||||
assert!((sk.points[a].pos.dist(sk.points[c].pos) - 3.0).abs() < 1e-9);
|
||||
assert!((sk.points[b].pos.dist(sk.points[c].pos) - 3.0).abs() < 1e-9);
|
||||
}
|
||||
assert_eq!(ids, arcs.iter().map(|i| sk.curves[*i].id.clone()).collect::<Vec<_>>());
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
//! gives the remaining degrees of freedom and which points are fully determined.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solvecraft_geom::Vec2;
|
||||
use solvecraft_geom::{Seg2, Vec2};
|
||||
|
||||
use crate::linalg::{Mat, determined_vars, solve as lin_solve};
|
||||
use crate::model::{ConstraintKind, CurveKind, Sketch};
|
||||
@@ -177,6 +177,51 @@ impl State<'_> {
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
/// Point-on-curve constraints use the exact shape. A polyline residual would move an
|
||||
/// exact crossing onto its chord, changing geometry merely by splitting it.
|
||||
fn freeform_dist(&self, c: usize, q: Vec2) -> f64 {
|
||||
let segs: Vec<Seg2> = match self.sk.curves.get(c).map(|c| &c.kind) {
|
||||
Some(CurveKind::Ellipse { c: ci, m, .. }) => {
|
||||
let center = self.p(*ci);
|
||||
let u = self.p(*m) - center;
|
||||
let a = u.len();
|
||||
let r = self.radius_of(c);
|
||||
if a <= 1e-12 || r <= 1e-12 {
|
||||
return q.dist(center);
|
||||
}
|
||||
let delta = q - center;
|
||||
let x = delta.dot(u) / (a * a);
|
||||
let y = delta.cross(u) / (a * r);
|
||||
return (x.hypot(y) - 1.0) * a.min(r);
|
||||
}
|
||||
Some(CurveKind::Spline { pts, control, degree }) => {
|
||||
let p: Vec<Vec2> = pts.iter().map(|i| self.p(*i)).collect();
|
||||
if p.len() < 2 {
|
||||
return q.dist(p.first().copied().unwrap_or_default());
|
||||
}
|
||||
if *control {
|
||||
let k = (*degree as usize).min(solvecraft_geom::MAX_BEZIER_DEGREE).min(p.len().saturating_sub(1)).max(1);
|
||||
crate::curves::bspline_beziers(&p, k).iter().filter_map(|span| solvecraft_geom::bezier(span)).collect()
|
||||
} else {
|
||||
crate::curves::fit_beziers(&p).into_iter().map(|[p0, p1, p2, p3]| Seg2::Cubic { p0, p1, p2, p3 }).collect()
|
||||
}
|
||||
}
|
||||
Some(CurveKind::Conic { a, b, apex, rho }) => {
|
||||
vec![Seg2::Conic { a: self.p(*a), b: self.p(*b), apex: self.p(*apex), w: rho / (1.0 - rho).max(1e-9) }]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
segs.iter()
|
||||
.map(|seg| {
|
||||
let t = seg.closest_param(q);
|
||||
let delta = q - seg.point_at(t);
|
||||
let d = delta.len();
|
||||
(d, if seg.derivative(t).cross(delta) >= 0.0 { d } else { -d })
|
||||
})
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0))
|
||||
.map(|(_, signed)| signed)
|
||||
.unwrap_or_else(|| crate::curves::polyline_signed_dist(&self.poly(c), q))
|
||||
}
|
||||
/// Radius of a circle or arc; an ellipse's minor radius.
|
||||
fn radius_of(&self, c: usize) -> f64 {
|
||||
match self.sk.curves.get(c).map(|x| &x.kind) {
|
||||
@@ -281,7 +326,7 @@ fn residuals(s: &State, k: &ConstraintKind, out: &mut Vec<f64>) {
|
||||
PointOnCurve { p, c } => {
|
||||
let pp = s.p(p);
|
||||
if s.is_freeform(c) {
|
||||
out.push(crate::curves::polyline_signed_dist(&s.poly(c), pp));
|
||||
out.push(s.freeform_dist(c, pp));
|
||||
} else if s.is_line(c) {
|
||||
let (a, b) = s.line(c);
|
||||
out.push(line_dist(pp, a, b));
|
||||
|
||||
Reference in new issue
Block a user