Files
OneCAD/crates/sketch/src/shape.rs
T

168 lines
5.9 KiB
Rust

//! Analytic curve shapes for geometry operations (profiles, trim, extend, fillet…).
use std::f64::consts::TAU;
use solvecraft_geom::{Seg2, Vec2};
use crate::model::{CurveKind, Sketch};
/// A curve of the sketch as an analytic shape for splitting.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Shape {
Line {
a: Vec2,
b: Vec2,
},
/// Circle (full when `sweep` is 2π) from `start` angle sweeping counter-clockwise.
Round {
c: Vec2,
r: f64,
start: f64,
sweep: f64,
},
}
impl Shape {
/// Curve parameter of `p` if it lies on the curve's interior (line t in (0,1); round:
/// angle offset from start in (0, sweep)).
pub fn param_on(&self, p: Vec2, tol: f64) -> Option<f64> {
match *self {
Shape::Line { a, b } => {
let d = b - a;
let l2 = d.len2();
if l2 < 1e-24 {
return None;
}
let t = (p - a).dot(d) / l2;
let q = a + d * t;
(q.dist(p) <= tol && t * l2.sqrt() > tol && (1.0 - t) * l2.sqrt() > tol).then_some(t)
}
Shape::Round { c, r, start, sweep } => {
if (p.dist(c) - r).abs() > tol {
return None;
}
let off = ((p - c).angle() - start).rem_euclid(TAU);
let ang_tol = tol / r.max(1e-12);
let full = sweep >= TAU - 1e-12;
if full {
return Some(off);
}
(off > ang_tol && off < sweep - ang_tol).then_some(off)
}
}
}
pub fn point(&self, t: f64) -> Vec2 {
match *self {
Shape::Line { a, b } => a.lerp(b, t),
Shape::Round { c, r, start, .. } => c + Vec2::from_angle(start + t) * r,
}
}
}
/// Intersection points of two shapes (as circles/infinite lines; callers filter by range).
pub fn intersections(x: &Shape, y: &Shape) -> Vec<Vec2> {
match (*x, *y) {
(Shape::Line { a, b }, Shape::Line { a: c, b: d }) => {
let (r, s) = (b - a, d - c);
let den = r.cross(s);
if den.abs() < 1e-12 * r.len() * s.len() {
return Vec::new();
}
let t = (c - a).cross(s) / den;
vec![a + r * t]
}
(Shape::Line { a, b }, Shape::Round { c, r, .. }) | (Shape::Round { c, r, .. }, Shape::Line { a, b }) => {
let d = b - a;
let f = a - c;
let (qa, qb, qc) = (d.len2(), 2.0 * f.dot(d), f.len2() - r * r);
let disc = qb * qb - 4.0 * qa * qc;
if qa < 1e-24 || disc < 0.0 {
return Vec::new();
}
let sq = disc.sqrt();
vec![a + d * ((-qb - sq) / (2.0 * qa)), a + d * ((-qb + sq) / (2.0 * qa))]
}
(Shape::Round { c: c1, r: r1, .. }, Shape::Round { c: c2, r: r2, .. }) => {
let dv = c2 - c1;
let d = dv.len();
if d < 1e-12 || d > r1 + r2 || d < (r1 - r2).abs() {
return Vec::new();
}
let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
let h = (r1 * r1 - a * a).max(0.0).sqrt();
let m = c1 + dv * (a / d);
let n = dv.perp() / d;
vec![m + n * h, m - n * h]
}
}
}
impl Shape {
/// Unbounded curve parameter of `p` (line: t along a→b; round: angle offset from start in
/// [0, 2π)).
pub fn param(&self, p: Vec2) -> f64 {
match *self {
Shape::Line { a, b } => {
let d = b - a;
let l2 = d.len2();
if l2 < 1e-24 { 0.0 } else { (p - a).dot(d) / l2 }
}
Shape::Round { c, start, .. } => ((p - c).angle() - start).rem_euclid(TAU),
}
}
/// Parameter range of the bounded curve.
pub fn range(&self) -> (f64, f64) {
match *self {
Shape::Line { .. } => (0.0, 1.0),
Shape::Round { sweep, .. } => (0.0, sweep),
}
}
pub fn is_full(&self) -> bool {
matches!(*self, Shape::Round { sweep, .. } if sweep >= TAU - 1e-12)
}
/// Unit tangent at parameter `t` in the direction of increasing `t`.
pub fn tangent(&self, t: f64) -> Vec2 {
match *self {
Shape::Line { a, b } => (b - a).normalized().unwrap_or(Vec2::X),
Shape::Round { start, .. } => Vec2::from_angle(start + t).perp(),
}
}
/// Closest point on the unbounded curve.
pub fn project(&self, p: Vec2) -> Vec2 {
match *self {
Shape::Line { a, b } => a.lerp(b, self.param(p)),
Shape::Round { c, r, .. } => c + (p - c).normalized().unwrap_or(Vec2::X) * r,
}
}
/// Distance from `p` to the bounded curve.
pub fn dist(&self, p: Vec2) -> f64 {
let (t0, t1) = self.range();
let t = self.param(p);
match *self {
Shape::Line { .. } => p.dist(self.point(t.clamp(t0, t1))),
Shape::Round { c, r, sweep, .. } => {
if sweep >= TAU - 1e-12 || t <= sweep {
(p.dist(c) - r).abs()
} else {
p.dist(self.point(0.0)).min(p.dist(self.point(sweep)))
}
}
}
}
}
impl Sketch {
/// The analytic shape of a curve (arcs counter-clockwise from their stored start).
pub fn shape(&self, ci: usize) -> Option<Shape> {
match self.curves.get(ci)?.kind {
CurveKind::Line { a, b } => Some(Shape::Line { a: self.point(a)?, b: self.point(b)? }),
CurveKind::Circle { c, r } => Some(Shape::Round { c: self.point(c)?, r, start: 0.0, sweep: TAU }),
CurveKind::Arc { .. } => match self.segs(ci).into_iter().next()? {
Seg2::Arc { center, radius, start, sweep } => Some(Shape::Round { c: center, r: radius, start, sweep }),
_ => None,
},
_ => None,
}
}
}