Thanks @jijinbei.
This commit is contained in:
1 parent
487aa64e10
commit
2acc344aa4
14 files changed
+661
-7
No files matched your search
@@ -0,0 +1,216 @@
|
||||
//! Loose ends of a sketch's profile curves, for the sketch to show where a profile can fail.
|
||||
//!
|
||||
//! Profiles are read from the geometry: ends that lie together close a loop whether or not a
|
||||
//! constraint ties them (see `profiles`). Two kinds of end are worth showing:
|
||||
//! - an **open end** touches no other profile curve, so no profile runs through it;
|
||||
//! - an **untied end** touches another profile curve only by position: the loop closes now, but
|
||||
//! nothing keeps it closed when a dimension changes or a point is dragged.
|
||||
//!
|
||||
//! An end is tied when it is shared with another curve's end, joined to one by coincident
|
||||
//! constraints, or held on a curve (point on curve, midpoint).
|
||||
|
||||
use solidcraft_geom::{Seg2, Vec2};
|
||||
|
||||
use crate::model::{ConstraintKind, CurveKind, Sketch};
|
||||
use crate::union_find::{find, union};
|
||||
|
||||
/// How close an end has to be to another curve to touch it: the distance at which profile
|
||||
/// finding joins and splits curves.
|
||||
pub const END_TOL: f64 = 1e-5;
|
||||
|
||||
/// Ends looked at, at most: a larger sketch reports nothing rather than taking long.
|
||||
const MAX_ENDS: usize = 100_000;
|
||||
|
||||
/// What an untied end touches.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EndTouch {
|
||||
/// Another curve's end (a point index).
|
||||
Point(usize),
|
||||
/// Another curve, between its ends (a curve index).
|
||||
Curve(usize),
|
||||
}
|
||||
|
||||
/// An end that touches another curve without being tied to it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct UntiedEnd {
|
||||
/// The end's point index.
|
||||
pub point: usize,
|
||||
/// The curve it is an end of.
|
||||
pub curve: usize,
|
||||
pub touches: EndTouch,
|
||||
}
|
||||
|
||||
/// The open and untied ends of a sketch's drawn profile curves (point indices).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct EndReport {
|
||||
pub open: Vec<usize>,
|
||||
pub untied: Vec<UntiedEnd>,
|
||||
}
|
||||
|
||||
impl EndReport {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.open.is_empty() && self.untied.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Curves profiles are made of (the ones `find_profiles` reads).
|
||||
fn profile_curve(sk: &Sketch, ci: usize) -> bool {
|
||||
sk.curves.get(ci).is_some_and(|c| !(c.construction || c.centerline || c.infinite))
|
||||
}
|
||||
|
||||
/// A box around a segment (conservative for curved ones).
|
||||
fn seg_box(s: &Seg2) -> (Vec2, Vec2) {
|
||||
let hull = |pts: &[Vec2]| {
|
||||
pts.iter().fold((Vec2::new(f64::INFINITY, f64::INFINITY), Vec2::new(f64::NEG_INFINITY, f64::NEG_INFINITY)), |(lo, hi), p| {
|
||||
(Vec2::new(lo.x.min(p.x), lo.y.min(p.y)), Vec2::new(hi.x.max(p.x), hi.y.max(p.y)))
|
||||
})
|
||||
};
|
||||
match *s {
|
||||
Seg2::Line { a, b } => hull(&[a, b]),
|
||||
Seg2::Arc { center, radius, .. } => {
|
||||
let r = Vec2::new(radius.abs(), radius.abs());
|
||||
(center - r, center + r)
|
||||
}
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => hull(&[p0, p1, p2, p3]),
|
||||
Seg2::Conic { a, apex, b, .. } => hull(&[a, apex, b]),
|
||||
Seg2::Bezier { n, p } => hull(p.get(..=usize::from(n)).unwrap_or(p.as_slice())),
|
||||
}
|
||||
}
|
||||
|
||||
/// The open and untied ends of the drawn profile curves (lines, arcs and open free-form curves
|
||||
/// that are not construction, projected or text). Positions are the sketch's current ones.
|
||||
pub fn loose_ends(sk: &Sketch) -> EndReport {
|
||||
let n = sk.points.len();
|
||||
// Coincident constraints join points into one place.
|
||||
let mut group: Vec<usize> = (0..n).collect();
|
||||
for c in &sk.constraints {
|
||||
if let ConstraintKind::Coincident { p, q } = c.kind {
|
||||
union(&mut group, p, q);
|
||||
}
|
||||
}
|
||||
// Per place: how many profile curve ends meet there, and whether a point there is held
|
||||
// on a profile curve.
|
||||
let mut ends_at = vec![0usize; n];
|
||||
let mut held = vec![false; n];
|
||||
let mut ends: Vec<(usize, usize)> = Vec::new();
|
||||
for (ci, c) in sk.curves.iter().enumerate() {
|
||||
if !profile_curve(sk, ci) {
|
||||
continue;
|
||||
}
|
||||
let Some((a, b)) = c.kind.ends() else { continue };
|
||||
for p in [a, b] {
|
||||
let g = find(&mut group, p);
|
||||
if let Some(k) = ends_at.get_mut(g) {
|
||||
*k += 1;
|
||||
}
|
||||
}
|
||||
// Lines shorter than profile finding keeps are left out of profiles too.
|
||||
let degenerate = matches!(c.kind, CurveKind::Line { .. }) && sk.point(a).zip(sk.point(b)).is_some_and(|(pa, pb)| pa.dist(pb) <= END_TOL);
|
||||
if c.link.is_none() && !degenerate {
|
||||
ends.push((a, ci));
|
||||
if b != a {
|
||||
ends.push((b, ci));
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in &sk.constraints {
|
||||
let on = match c.kind {
|
||||
ConstraintKind::PointOnCurve { p, c } | ConstraintKind::Midpoint { p, l: c } if profile_curve(sk, c) => Some(p),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(p) = on {
|
||||
let g = find(&mut group, p);
|
||||
if let Some(h) = held.get_mut(g) {
|
||||
*h = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ends.len() > MAX_ENDS {
|
||||
return EndReport::default();
|
||||
}
|
||||
// Ends tied to nothing: the ones to look at by position.
|
||||
let mut free: Vec<(usize, usize, Vec2)> = Vec::new();
|
||||
for &(p, ci) in &ends {
|
||||
let g = find(&mut group, p);
|
||||
if ends_at.get(g).copied().unwrap_or(0) >= 2 || held.get(g).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
if let Some(at) = sk.point(p).filter(|v| v.x.is_finite() && v.y.is_finite()) {
|
||||
free.push((p, ci, at));
|
||||
}
|
||||
}
|
||||
if free.is_empty() {
|
||||
return EndReport::default();
|
||||
}
|
||||
free.sort_by(|a, b| a.2.x.total_cmp(&b.2.x));
|
||||
// What each free end touches: another curve's end first, else another curve's span.
|
||||
let mut touch: Vec<Option<EndTouch>> = vec![None; free.len()];
|
||||
for (ci, c) in sk.curves.iter().enumerate() {
|
||||
if !profile_curve(sk, ci) {
|
||||
continue;
|
||||
}
|
||||
let curve_ends = c.kind.ends();
|
||||
for seg in sk.segs(ci) {
|
||||
let (lo, hi) = seg_box(&seg);
|
||||
let first = free.partition_point(|f| f.2.x < lo.x - END_TOL);
|
||||
for (k, &(p, own, at)) in free.iter().enumerate().skip(first) {
|
||||
if at.x > hi.x + END_TOL {
|
||||
break;
|
||||
}
|
||||
if at.y < lo.y - END_TOL || at.y > hi.y + END_TOL || matches!(touch.get(k), Some(Some(EndTouch::Point(_)))) {
|
||||
continue;
|
||||
}
|
||||
// Another end of a curve (its own other end too: a loop drawn as one curve).
|
||||
let at_end = curve_ends.and_then(|(a, b)| [a, b].into_iter().find(|&q| q != p && sk.point(q).is_some_and(|v| v.dist(at) <= END_TOL)));
|
||||
let found = match at_end {
|
||||
Some(q) => Some(EndTouch::Point(q)),
|
||||
None if ci != own && seg.point_at(seg.closest_param(at)).dist(at) <= END_TOL => Some(EndTouch::Curve(ci)),
|
||||
None => None,
|
||||
};
|
||||
if let (Some(found), Some(slot)) = (found, touch.get_mut(k))
|
||||
&& (slot.is_none() || matches!(found, EndTouch::Point(_)))
|
||||
{
|
||||
*slot = Some(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Free ends that touch each other are one place, however many meet there: each is tied to
|
||||
// the place's first end, which is tied to what it touches outside the place (if anything).
|
||||
let index: std::collections::HashMap<usize, usize> = free.iter().enumerate().map(|(k, f)| (f.0, k)).collect();
|
||||
let mut place: Vec<usize> = (0..free.len()).collect();
|
||||
for (k, t) in touch.iter().enumerate() {
|
||||
if let Some(EndTouch::Point(q)) = t
|
||||
&& let Some(&j) = index.get(q)
|
||||
{
|
||||
union(&mut place, k, j);
|
||||
}
|
||||
}
|
||||
let mut first: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
|
||||
for (k, f) in free.iter().enumerate() {
|
||||
let root = find(&mut place, k);
|
||||
let e = first.entry(root).or_insert(f.0);
|
||||
*e = (*e).min(f.0);
|
||||
}
|
||||
let mut report = EndReport::default();
|
||||
for (k, &(p, curve, _)) in free.iter().enumerate() {
|
||||
let Some(t) = touch.get(k).copied().flatten() else {
|
||||
report.open.push(p);
|
||||
continue;
|
||||
};
|
||||
let root = find(&mut place, k);
|
||||
let anchor = first.get(&root).copied().unwrap_or(p);
|
||||
let touches = match t {
|
||||
_ if anchor != p => EndTouch::Point(anchor),
|
||||
// The place's first end: tied to what it touches outside the place.
|
||||
EndTouch::Point(q) if index.contains_key(&q) => continue,
|
||||
t => t,
|
||||
};
|
||||
report.untied.push(UntiedEnd { point: p, curve, touches });
|
||||
}
|
||||
report.open.sort_unstable();
|
||||
report.open.dedup();
|
||||
report.untied.sort_by_key(|u| u.point);
|
||||
report.untied.dedup_by_key(|u| u.point);
|
||||
report
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Open and untied ends of profile curves.
|
||||
|
||||
use solidcraft_geom::Vec2;
|
||||
|
||||
use crate::{ConstraintKind, EndTouch, Sketch, find_profiles, loose_ends};
|
||||
|
||||
fn v(x: f64, y: f64) -> Vec2 {
|
||||
Vec2::new(x, y)
|
||||
}
|
||||
|
||||
/// A closed polyline whose corners are each one shared point.
|
||||
fn shared_square(sk: &mut Sketch) -> Vec<usize> {
|
||||
let pts: Vec<usize> = [v(0.0, 0.0), v(10.0, 0.0), v(10.0, 10.0), v(0.0, 10.0)].iter().map(|p| sk.add_point(*p, None).unwrap()).collect();
|
||||
(0..4).map(|i| sk.add_line_pts(pts[i], pts[(i + 1) % 4], None).unwrap()).collect()
|
||||
}
|
||||
|
||||
/// A square drawn as four separate lines: every corner is two points at one place.
|
||||
fn loose_square(sk: &mut Sketch) -> Vec<usize> {
|
||||
let c = [v(0.0, 0.0), v(10.0, 0.0), v(10.0, 10.0), v(0.0, 10.0)];
|
||||
(0..4).map(|i| sk.add_line(c[i], c[(i + 1) % 4], None, None, None).unwrap()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_corners_are_tied() {
|
||||
let mut sk = Sketch::new();
|
||||
shared_square(&mut sk);
|
||||
assert!(loose_ends(&sk).is_empty(), "{:?}", loose_ends(&sk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corners_together_by_position_only_are_untied() {
|
||||
let mut sk = Sketch::new();
|
||||
loose_square(&mut sk);
|
||||
// The square closes (profiles are read from positions)...
|
||||
assert_eq!(find_profiles(&sk).len(), 1);
|
||||
// ...but nothing keeps it closed: four corners, each reported once.
|
||||
let r = loose_ends(&sk);
|
||||
assert!(r.open.is_empty(), "{r:?}");
|
||||
assert_eq!(r.untied.len(), 4, "{r:?}");
|
||||
assert!(r.untied.iter().all(|u| matches!(u.touches, EndTouch::Point(_))), "{r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_constraints_tie_corners() {
|
||||
let mut sk = Sketch::new();
|
||||
let lines = loose_square(&mut sk);
|
||||
for i in 0..4 {
|
||||
let end = sk.curves[lines[i]].kind.ends().unwrap().1;
|
||||
let start = sk.curves[lines[(i + 1) % 4]].kind.ends().unwrap().0;
|
||||
sk.add_constraint(ConstraintKind::Coincident { p: end, q: start }, None).unwrap();
|
||||
}
|
||||
assert!(loose_ends(&sk).is_empty(), "{:?}", loose_ends(&sk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gap_is_two_open_ends() {
|
||||
let mut sk = Sketch::new();
|
||||
let a = sk.add_point(v(0.0, 0.0), None).unwrap();
|
||||
let b = sk.add_point(v(10.0, 0.0), None).unwrap();
|
||||
let c = sk.add_point(v(10.0, 10.0), None).unwrap();
|
||||
sk.add_line_pts(a, b, None).unwrap();
|
||||
sk.add_line_pts(b, c, None).unwrap();
|
||||
// The closing line stops 0.01 mm short of the start.
|
||||
let d = sk.add_point(v(0.0, 0.01), None).unwrap();
|
||||
sk.add_line_pts(c, d, None).unwrap();
|
||||
assert!(find_profiles(&sk).is_empty());
|
||||
let r = loose_ends(&sk);
|
||||
assert_eq!(r.open, vec![a, d], "{r:?}");
|
||||
assert!(r.untied.is_empty(), "{r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_end_on_another_curve_is_untied_until_held() {
|
||||
let mut sk = Sketch::new();
|
||||
shared_square(&mut sk);
|
||||
// A divider from the bottom side's middle to the top side's middle, by position only.
|
||||
let l = sk.add_line(v(5.0, 0.0), v(5.0, 10.0), None, None, None).unwrap();
|
||||
assert_eq!(find_profiles(&sk).len(), 2);
|
||||
let r = loose_ends(&sk);
|
||||
assert_eq!(r.untied.len(), 2, "{r:?}");
|
||||
assert!(r.untied.iter().all(|u| u.curve == l && matches!(u.touches, EndTouch::Curve(_))), "{r:?}");
|
||||
// Held on the sides, they are tied.
|
||||
for u in r.untied {
|
||||
if let EndTouch::Curve(c) = u.touches {
|
||||
sk.add_constraint(ConstraintKind::PointOnCurve { p: u.point, c }, None).unwrap();
|
||||
}
|
||||
}
|
||||
assert!(loose_ends(&sk).is_empty(), "{:?}", loose_ends(&sk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_lines_and_circles_have_no_loose_ends() {
|
||||
let mut sk = Sketch::new();
|
||||
let l = sk.add_line(v(0.0, 0.0), v(10.0, 0.0), None, None, None).unwrap();
|
||||
sk.curves[l].construction = true;
|
||||
sk.add_circle(v(20.0, 0.0), 3.0, None, None).unwrap();
|
||||
assert!(loose_ends(&sk).is_empty(), "{:?}", loose_ends(&sk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lone_line_has_two_open_ends() {
|
||||
let mut sk = Sketch::new();
|
||||
let l = sk.add_line(v(0.0, 0.0), v(10.0, 0.0), None, None, None).unwrap();
|
||||
let (a, b) = sk.curves[l].kind.ends().unwrap();
|
||||
assert_eq!(loose_ends(&sk).open, vec![a.min(b), a.max(b)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_ends_meeting_by_position_are_one_place_tied_at_once() {
|
||||
let mut sk = Sketch::new();
|
||||
for to in [v(0.0, 0.0), v(20.0, 0.0), v(10.0, 10.0)] {
|
||||
sk.add_line(to, v(10.0, 0.0), None, None, None).unwrap();
|
||||
}
|
||||
let r = loose_ends(&sk);
|
||||
// Two of the three need tying, both to the third.
|
||||
assert_eq!(r.untied.len(), 2, "{r:?}");
|
||||
let anchor = match r.untied[0].touches {
|
||||
EndTouch::Point(q) => q,
|
||||
EndTouch::Curve(_) => usize::MAX,
|
||||
};
|
||||
assert!(r.untied.iter().all(|u| u.touches == EndTouch::Point(anchor) && u.point != anchor), "{r:?}");
|
||||
for u in &r.untied {
|
||||
sk.add_constraint(ConstraintKind::Coincident { p: u.point, q: anchor }, None).unwrap();
|
||||
}
|
||||
assert!(loose_ends(&sk).untied.is_empty(), "{:?}", loose_ends(&sk));
|
||||
}
|
||||
@@ -20,6 +20,7 @@ mod construct;
|
||||
mod curves;
|
||||
mod decompose;
|
||||
mod dims;
|
||||
mod ends;
|
||||
mod fonts;
|
||||
mod freeform_offset;
|
||||
pub mod linalg;
|
||||
@@ -44,6 +45,7 @@ pub use curves::{
|
||||
};
|
||||
pub use decompose::Budget;
|
||||
pub use dims::{DimFrame, DimLayout, chain_centres, default_text, dim_frame, dim_layout, dim_layout_reaching, dim_reach, encode_text};
|
||||
pub use ends::{END_TOL, EndReport, EndTouch, UntiedEnd, loose_ends};
|
||||
pub use fonts::{DEFAULT_FONT_FAMILY, FontData, families_in, find_style_in, font_data, font_data_styled, installed_families, set_font_file_check};
|
||||
pub use freeform_offset::{FreeformOffset, OFFSET_TOLERANCE};
|
||||
pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES, SKETCH_AXIS_HALF, TextExprs, sketch_axis_geom};
|
||||
@@ -74,6 +76,8 @@ mod construct_tests;
|
||||
#[cfg(test)]
|
||||
mod continuation_tests;
|
||||
#[cfg(test)]
|
||||
mod ends_tests;
|
||||
#[cfg(test)]
|
||||
mod font_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
Reference in new issue
Block a user