diff --git a/committed.toml b/committed.toml index 1f41ad8..09364c1 100644 --- a/committed.toml +++ b/committed.toml @@ -60,8 +60,10 @@ allowed_scopes = [ "layout/layout", "layout/poly", "layout/via", + "math/circle", "math/cyclic_search", "math/line", + "math/mod", "math/polygon_tangents", "math/bitangents", "math/tunnel", diff --git a/crates/specctra-core/src/math.rs b/crates/specctra-core/src/math.rs index 752c8d2..12fe1fd 100644 --- a/crates/specctra-core/src/math.rs +++ b/crates/specctra-core/src/math.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -use core::{fmt, ops::Sub}; +use core::fmt; use geo_types::geometry::Point; use serde::{Deserialize, Serialize}; @@ -18,50 +18,6 @@ pub struct PointWithRotation { pub rot: f64, } -impl fmt::Debug for Circle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Circle") - .field("x", &self.pos.0.x) - .field("y", &self.pos.0.y) - .field("r", &self.r) - .finish() - } -} - -impl Circle { - /// Calculate the point that lies on the circle at angle `phi`, - /// relative to coordinate axes. - /// - /// `phi` is the angle given in radians starting at `(r, 0)`. - pub fn position_at_angle(&self, phi: f64) -> Point { - geo_types::point! { - x: self.pos.0.x + self.r * phi.cos(), - y: self.pos.0.y + self.r * phi.sin() - } - } - - /// The (x,y) axis aligned bounding box for this circle. - #[cfg(feature = "rstar")] - pub fn bbox(&self, margin: f64) -> rstar::AABB<[f64; 2]> { - let r = self.r + margin; - rstar::AABB::from_corners( - [self.pos.0.x - r, self.pos.0.y - r], - [self.pos.0.x + r, self.pos.0.y + r], - ) - } -} - -impl Sub for Circle { - type Output = Self; - - fn sub(self, other: Self) -> Self { - Self { - pos: self.pos - other.pos, - r: self.r, - } - } -} - impl fmt::Debug for PointWithRotation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PointWithRotation") diff --git a/src/layout/collect_bands.rs b/src/layout/collect_bands.rs index 43ce7b4..1ed67d9 100644 --- a/src/layout/collect_bands.rs +++ b/src/layout/collect_bands.rs @@ -78,7 +78,7 @@ impl Layout { let loose_hline = orig_hline.orthogonal_through(&match shape { PrimitiveShape::Seg(seg) => { let seg_hline = LineInGeneralForm::from(seg.middle_line()); - match orig_hline.intersects(&seg_hline) { + match orig_hline.intersect(&seg_hline) { LineIntersection::Empty => return None, LineIntersection::Overlapping => shape.center(), LineIntersection::Point(pt) => pt, diff --git a/src/math/bitangents.rs b/src/math/bitangents.rs index c07140c..1fa0fb6 100644 --- a/src/math/bitangents.rs +++ b/src/math/bitangents.rs @@ -3,9 +3,10 @@ // SPDX-License-Identifier: MIT use geo::{geometry::Point, Line}; -use specctra_core::math::Circle; use thiserror::Error; +use crate::math::Circle; + use super::{seq_perp_dot_product, LineInGeneralForm, RotationSense}; #[derive(Error, Debug, Clone, Copy, PartialEq)] diff --git a/src/math/circle.rs b/src/math/circle.rs new file mode 100644 index 0000000..c5ee570 --- /dev/null +++ b/src/math/circle.rs @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2025 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::ops::Sub; + +use geo::{point, Distance, Euclidean, Line, Point}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub struct Circle { + pub pos: Point, + pub r: f64, +} + +impl Circle { + /// Calculate the point that lies on the circle at angle `phi`, + /// relative to coordinate axes. + /// + /// `phi` is the angle given in radians starting at `(r, 0)`. + pub fn position_at_angle(&self, phi: f64) -> Point { + point! { + x: self.pos.0.x + self.r * phi.cos(), + y: self.pos.0.y + self.r * phi.sin() + } + } + + /// The (x,y) axis aligned bounding box for this circle. + pub fn bbox(&self, margin: f64) -> rstar::AABB<[f64; 2]> { + let r = self.r + margin; + rstar::AABB::from_corners( + [self.pos.0.x - r, self.pos.0.y - r], + [self.pos.0.x + r, self.pos.0.y + r], + ) + } +} + +impl Sub for Circle { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Self { + pos: self.pos - other.pos, + r: self.r, + } + } +} + +/// Calculates the intersection of two circles, `circle1` and `circle2`. +/// +/// Returns a `Vec` holding zero, one, or two calculated intersection points, +/// depending on how many exist. +pub fn intersect_circles(circle1: &Circle, circle2: &Circle) -> Vec { + let delta = circle2.pos - circle1.pos; + let d = Euclidean::distance(&circle2.pos, &circle1.pos); + + if d > circle1.r + circle2.r { + // No intersection. + return vec![]; + } + + if d < (circle2.r - circle1.r).abs() { + // One contains the other. + return vec![]; + } + + // Distance from `circle1.pos` to the intersection of the diagonals. + let a = (circle1.r * circle1.r - circle2.r * circle2.r + d * d) / (2.0 * d); + + // Intersection of the diagonals. + let p = circle1.pos + delta * (a / d); + let h = (circle1.r * circle1.r - a * a).sqrt(); + + if h == 0.0 { + return [p].into(); + } + + let r = point! {x: -delta.x(), y: delta.y()} * (h / d); + + [p + r, p - r].into() +} + +/// Calculate the intersection between circle `circle` and line segment `segment`. +/// +/// Returns a `Vec` holding zero, one, or two calculated intersection points, +/// depending on how many exist. +pub fn intersect_circle_segment(circle: &Circle, segment: &Line) -> Vec { + let delta: Point = segment.delta().into(); + let from = segment.start_point(); + let to = segment.end_point(); + let epsilon = 1e-9; + let interval01 = 0.0..=1.0; + + let a = delta.dot(delta); + let b = + 2.0 * (delta.x() * (from.x() - circle.pos.x()) + delta.y() * (from.y() - circle.pos.y())); + let c = circle.pos.dot(circle.pos) + from.dot(from) + - 2.0 * circle.pos.dot(from) + - circle.r * circle.r; + let discriminant = b * b - 4.0 * a * c; + + if a.abs() < epsilon || discriminant < 0.0 { + return [].into(); + } + + if discriminant == 0.0 { + let u = -b / (2.0 * a); + + return if interval01.contains(&u) { + vec![from + (to - from) * -b / (2.0 * a)] + } else { + vec![] + }; + } + + let mut v = vec![]; + + let u1 = (-b + discriminant.sqrt()) / (2.0 * a); + + if interval01.contains(&u1) { + v.push(from + (to - from) * u1); + } + + let u2 = (-b - discriminant.sqrt()) / (2.0 * a); + + if interval01.contains(&u2) { + v.push(from + (to - from) * u2); + } + + v +} diff --git a/src/math/line.rs b/src/math/line.rs index 400bf10..a7f6253 100644 --- a/src/math/line.rs +++ b/src/math/line.rs @@ -54,7 +54,7 @@ impl LineInGeneralForm { } /// Calculate the intersection between two lines. - pub fn intersects(&self, b: &Self) -> LineIntersection { + pub fn intersect(&self, b: &Self) -> LineIntersection { const ALMOST_ZERO: f64 = f64::EPSILON * 16.0; let (mut a, mut b) = (*self, *b); let _ = (a.make_normal_unit(), b.make_normal_unit()); @@ -105,11 +105,11 @@ impl LineInGeneralForm { } /// Returns `Some(p)` when `p` lies in the intersection of the given lines. -pub fn intersect_lines(line1: &Line, line2: &Line) -> Option { +pub fn intersect_line_segments(line1: &Line, line2: &Line) -> Option { let nline1 = LineInGeneralForm::from(*line1); let nline2 = LineInGeneralForm::from(*line2); - match nline1.intersects(&nline2) { + match nline1.intersect(&nline2) { LineIntersection::Empty | LineIntersection::Overlapping => None, LineIntersection::Point(pt) => { let parv1 = geo::point! { @@ -142,7 +142,7 @@ pub fn intersect_line_and_ray(line1: &Line, ray2: &Line) -> Option { let nline1 = LineInGeneralForm::from(*line1); let nray2 = LineInGeneralForm::from(*ray2); - match nline1.intersects(&nray2) { + match nline1.intersect(&nray2) { LineIntersection::Empty | LineIntersection::Overlapping => None, LineIntersection::Point(pt) => { let parv1 = geo::point! { @@ -194,7 +194,7 @@ mod tests { #[test] fn intersect_line_and_line00() { assert_eq!( - intersect_lines( + intersect_line_segments( &Line { start: geo::coord! { x: -1., y: -1. }, end: geo::coord! { x: 1., y: 1. }, @@ -207,7 +207,7 @@ mod tests { Some(geo::point! { x: 0., y: 0. }) ); assert_eq!( - intersect_lines( + intersect_line_segments( &Line { start: geo::coord! { x: -1., y: -1. }, end: geo::coord! { x: 1., y: 1. }, diff --git a/src/math/mod.rs b/src/math/mod.rs index 00fe1ee..da65778 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -2,9 +2,7 @@ // // SPDX-License-Identifier: MIT -use geo::algorithm::line_measures::{Distance, Euclidean}; -use geo::{point, Line, Point}; -pub use specctra_core::math::{Circle, PointWithRotation}; +use geo::Point; mod cyclic_search; pub use cyclic_search::*; @@ -21,6 +19,9 @@ pub use bitangents::*; mod tunnel; pub use tunnel::*; +mod circle; +pub use circle::*; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RotationSense { Counterclockwise, @@ -49,90 +50,6 @@ impl RotationSense { } } -/// Calculates the intersection of two circles, `circle1` and `circle2`. -/// -/// Returns a `Vec` holding zero, one, or two calculated intersection points, -/// depending on how many exist. -pub fn intersect_circles(circle1: &Circle, circle2: &Circle) -> Vec { - let delta = circle2.pos - circle1.pos; - let d = Euclidean::distance(&circle2.pos, &circle1.pos); - - if d > circle1.r + circle2.r { - // No intersection. - return vec![]; - } - - if d < (circle2.r - circle1.r).abs() { - // One contains the other. - return vec![]; - } - - // Distance from `circle1.pos` to the intersection of the diagonals. - let a = (circle1.r * circle1.r - circle2.r * circle2.r + d * d) / (2.0 * d); - - // Intersection of the diagonals. - let p = circle1.pos + delta * (a / d); - let h = (circle1.r * circle1.r - a * a).sqrt(); - - if h == 0.0 { - return [p].into(); - } - - let r = point! {x: -delta.x(), y: delta.y()} * (h / d); - - [p + r, p - r].into() -} - -/// Calculate the intersection between circle `circle` and line segment `segment`. -/// -/// Returns a `Vec` holding zero, one, or two calculated intersection points, -/// depending on how many exist. -pub fn intersect_circle_segment(circle: &Circle, segment: &Line) -> Vec { - let delta: Point = segment.delta().into(); - let from = segment.start_point(); - let to = segment.end_point(); - let epsilon = 1e-9; - let interval01 = 0.0..=1.0; - - let a = delta.dot(delta); - let b = - 2.0 * (delta.x() * (from.x() - circle.pos.x()) + delta.y() * (from.y() - circle.pos.y())); - let c = circle.pos.dot(circle.pos) + from.dot(from) - - 2.0 * circle.pos.dot(from) - - circle.r * circle.r; - let discriminant = b * b - 4.0 * a * c; - - if a.abs() < epsilon || discriminant < 0.0 { - return [].into(); - } - - if discriminant == 0.0 { - let u = -b / (2.0 * a); - - return if interval01.contains(&u) { - vec![from + (to - from) * -b / (2.0 * a)] - } else { - vec![] - }; - } - - let mut v = vec![]; - - let u1 = (-b + discriminant.sqrt()) / (2.0 * a); - - if interval01.contains(&u1) { - v.push(from + (to - from) * u1); - } - - let u2 = (-b - discriminant.sqrt()) / (2.0 * a); - - if interval01.contains(&u2) { - v.push(from + (to - from) * u2); - } - - v -} - /// Returns `true` the point `p` is between the supporting lines of vectors /// `from` and `to`. pub fn between_vectors(p: Point, from: Point, to: Point) -> bool { diff --git a/src/specctra/design.rs b/src/specctra/design.rs index 175f86e..ffbbfeb 100644 --- a/src/specctra/design.rs +++ b/src/specctra/design.rs @@ -9,6 +9,7 @@ use std::collections::{btree_map::Entry as BTreeMapEntry, BTreeMap}; use geo::{Point, Rotate}; +use specctra_core::math::PointWithRotation; use crate::{ board::{edit::BoardEdit, AccessMesadata, Board}, @@ -21,7 +22,7 @@ use crate::{ }, geometry::{primitive::PrimitiveShape, GetLayer, GetWidth}, layout::{poly::SolidPolyWeight, Layout}, - math::{Circle, PointWithRotation}, + math::Circle, specctra::{ mesadata::SpecctraMesadata, read::ListTokenizer,