fix(math/mod): Move code pertaining to circles to new file, circle.rs

This commit is contained in:
Mikolaj Wielgus 2025-08-29 13:36:38 +02:00
parent a9cb7f8848
commit 4a057d3499
8 changed files with 149 additions and 141 deletions

View File

@ -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",

View File

@ -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")

View File

@ -78,7 +78,7 @@ impl<R: AccessRules> Layout<R> {
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,

View File

@ -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)]

131
src/math/circle.rs Normal file
View File

@ -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<Point> {
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<Point> {
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
}

View File

@ -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<Point> {
pub fn intersect_line_segments(line1: &Line, line2: &Line) -> Option<Point> {
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<Point> {
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. },

View File

@ -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<Point> {
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<Point> {
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 {

View File

@ -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,