diff --git a/Cargo.toml b/Cargo.toml index e617441..ef4a6ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ contracts-try = "0.7" derive-getters.workspace = true enum_dispatch = "0.3" geo.workspace = true +log.workspace = true petgraph.workspace = true rstar.workspace = true serde.workspace = true diff --git a/src/board/mod.rs b/src/board/mod.rs index 3cb6bdc..f352ad4 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -62,13 +62,7 @@ impl<'a> ResolvedSelector<'a> { let (layer, loose) = match node { NodeIndex::Primitive(primitive) => ( primitive.primitive(board.layout().drawing()).layer(), - match primitive { - PrimitiveIndex::LooseDot(dot) => Some(dot.into()), - PrimitiveIndex::LoneLooseSeg(seg) => Some(seg.into()), - PrimitiveIndex::SeqLooseSeg(seg) => Some(seg.into()), - PrimitiveIndex::LooseBend(bend) => Some(bend.into()), - _ => None, - }, + primitive.try_into().ok(), ), NodeIndex::Compound(compound) => { match board.layout().drawing().compound_weight(compound) { diff --git a/src/drawing/loose.rs b/src/drawing/loose.rs index 0003891..4bb2235 100644 --- a/src/drawing/loose.rs +++ b/src/drawing/loose.rs @@ -57,6 +57,19 @@ impl From for PrimitiveIndex { } } +impl TryFrom for LooseIndex { + type Error = (); + fn try_from(primitive: PrimitiveIndex) -> Result { + match primitive { + PrimitiveIndex::LooseDot(dot) => Ok(dot.into()), + PrimitiveIndex::LoneLooseSeg(seg) => Ok(seg.into()), + PrimitiveIndex::SeqLooseSeg(seg) => Ok(seg.into()), + PrimitiveIndex::LooseBend(bend) => Ok(bend.into()), + _ => Err(()), + } + } +} + #[enum_dispatch(GetPrevNextLoose, GetDrawing, GetPetgraphIndex)] pub enum Loose<'a, CW: Copy, R: AccessRules> { Dot(LooseDot<'a, CW, R>), diff --git a/src/geometry/primitive.rs b/src/geometry/primitive.rs index 59082bb..bc37e1b 100644 --- a/src/geometry/primitive.rs +++ b/src/geometry/primitive.rs @@ -6,7 +6,7 @@ use std::f64::consts::TAU; use enum_dispatch::enum_dispatch; use geo::algorithm::line_measures::{Distance, Euclidean}; -use geo::{point, polygon, Contains, Intersects, Point, Polygon, Rotate}; +use geo::{point, polygon, Contains, Intersects, Line, Point, Polygon, Rotate}; use rstar::{RTreeObject, AABB}; use crate::{ @@ -148,6 +148,13 @@ impl SegShape { polygon![p1.0, p2.0, p3.0, p4.0] } + + pub fn middle_line(&self) -> Line { + Line { + start: self.from.into(), + end: self.to.into(), + } + } } impl MeasureLength for SegShape { diff --git a/src/layout/layout.rs b/src/layout/layout.rs index 2b5d58c..2b24e91 100644 --- a/src/layout/layout.rs +++ b/src/layout/layout.rs @@ -10,7 +10,7 @@ use rstar::AABB; use crate::{ drawing::{ - band::BandTermsegIndex, + band::{BandTermsegIndex, BandUid}, bend::{BendIndex, BendWeight, LooseBendWeight}, dot::{ DotIndex, DotWeight, FixedDotIndex, FixedDotWeight, GeneralDotWeight, LooseDotIndex, @@ -18,20 +18,28 @@ use crate::{ }, gear::GearIndex, graph::{GetMaybeNet, IsInLayer, MakePrimitive, PrimitiveIndex, PrimitiveWeight}, - primitive::MakePrimitiveShape, + loose::LooseIndex, + primitive::{GetWeight, MakePrimitiveShape, Primitive}, rules::AccessRules, seg::{ FixedSegIndex, FixedSegWeight, LoneLooseSegIndex, LoneLooseSegWeight, SegIndex, SegWeight, SeqLooseSegIndex, SeqLooseSegWeight, }, - Cane, Drawing, DrawingEdit, DrawingException, Infringement, + Cane, Collect, Drawing, DrawingEdit, DrawingException, Infringement, + }, + geometry::{ + compound::ManageCompounds, + edit::ApplyGeometryEdit, + primitive::{AccessPrimitiveShape, PrimitiveShape, SegShape}, + shape::{AccessShape, Shape}, + GenericNode, GetSetPos, }, - geometry::{edit::ApplyGeometryEdit, shape::Shape, GenericNode}, graph::{GenericIndex, GetPetgraphIndex}, layout::{ poly::{MakePolygon, Poly, PolyWeight}, via::{Via, ViaWeight}, }, + math::{LineIntersection, NormalLine}, }; /// Represents a weight for various compounds @@ -336,6 +344,119 @@ impl Layout { } } + /// Checks if a node is not a primitive part of a compound, and if yes, returns its center + pub fn center_of_compoundless_node(&self, node: NodeIndex) -> Option { + match node { + NodeIndex::Primitive(primitive) => { + if self + .drawing() + .geometry() + .compounds(GenericIndex::<()>::new(primitive.petgraph_index())) + .next() + .is_some() + { + return None; + } + match primitive.primitive(self.drawing()) { + Primitive::FixedDot(dot) => Some(dot.weight().pos()), + // Primitive::LooseDot(dot) => Some(dot.weight().pos()), + _ => None, + } + } + NodeIndex::Compound(_) => Some(self.node_shape(node).center()), + } + } + + /// Finds all bands on `layer` between `left` and `right` + /// (usually assuming `left` and `right` are neighbors in a Delaunay triangulation) + /// and returns them ordered from `left` to `right`. + pub fn bands_between_nodes( + &self, + layer: usize, + left: NodeIndex, + right: NodeIndex, + ) -> Vec { + assert_ne!(left, right); + let left_pos = self.node_shape(left).center(); + let right_pos = self.node_shape(right).center(); + let ltr_line = geo::Line { + start: left_pos.into(), + end: right_pos.into(), + }; + let fake_seg = SegShape { + from: left_pos.into(), + to: right_pos.into(), + width: f64::EPSILON * 16.0, + }; + let mut orig_hline = NormalLine::from(ltr_line); + orig_hline.make_normal_unit(); + let orig_hline = orig_hline; + let location_denom = orig_hline.segment_interval(<r_line); + let location_start = location_denom.start(); + let location_denom = location_denom.end() - location_denom.start(); + + let mut bands: Vec<_> = self + .drawing + .rtree() + .locate_in_envelope_intersecting(&{ + let aabb_init = AABB::from_corners( + [left_pos.x(), left_pos.y()], + [right_pos.x(), right_pos.y()], + ); + AABB::from_corners( + [aabb_init.lower()[0], aabb_init.lower()[1], layer as f64], + [aabb_init.upper()[0], aabb_init.upper()[1], layer as f64], + ) + }) + // TODO: handle non-loose entries (bends, segs) + .filter_map(|geom| match geom.data { + NodeIndex::Primitive(prim) => LooseIndex::try_from(prim).ok(), + NodeIndex::Compound(_) => None, + }) + .map(|loose| { + let prim: PrimitiveIndex = loose.into(); + let shape = prim.primitive(&self.drawing).shape(); + (loose, shape) + }) + .filter_map(|(loose, shape)| { + let band_uid = self.drawing.loose_band_uid(loose); + let loose_hline = orig_hline.orthogonal_through(&match shape { + PrimitiveShape::Seg(seg) => { + let seg_hline = NormalLine::from(seg.middle_line()); + match orig_hline.intersects(&seg_hline) { + LineIntersection::Empty => return None, + LineIntersection::Overlapping => shape.center(), + LineIntersection::Point(pt) => pt, + } + } + _ => { + if !fake_seg.intersects(&shape) { + return None; + } + shape.center() + } + }); + let location = (loose_hline.offset - location_start) / location_denom; + log::trace!( + "intersection ({:?}) with {:?} is at {:?}", + band_uid, + shape, + location + ); + (0.0..=1.0) + .contains(&location) + .then_some((location, band_uid)) + }) + .collect(); + bands.sort_by(|a, b| f64::total_cmp(&a.0, &b.0)); + + // TODO: handle "loops" of bands, or multiple primitives from the band crossing the segment + // both in the case of "edge" of a primitive/loose, and in case the band actually goes into a segment + // and then again out of it. + + bands.into_iter().map(|(_, band_uid)| band_uid).collect() + } + pub fn rules(&self) -> &R { self.drawing.rules() } diff --git a/src/math/mod.rs b/src/math/mod.rs index 3698058..118b0a1 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -9,6 +9,95 @@ pub use specctra_core::math::{Circle, PointWithRotation}; mod tangents; pub use tangents::*; +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineIntersection { + Empty, + Overlapping, + Point(Point), +} + +/// A line in normal form: `x0*y + y0*y + offset = 0` +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NormalLine { + pub x: f64, + pub y: f64, + pub offset: f64, +} + +impl From for NormalLine { + fn from(l: Line) -> Self { + // the normal vector is perpendicular to the line + let normal = geo::point! { + x: l.dy(), + y: -l.dx(), + }; + Self { + x: normal.0.x, + y: normal.0.y, + offset: -perp_dot_product(l.end.into(), l.start.into()), + } + } +} + +impl NormalLine { + pub fn evaluate_at(&self, pt: Point) -> f64 { + self.x * pt.x() + self.y * pt.y() + self.offset + } + + pub fn angle(&self) -> f64 { + self.y.atan2(self.x) + } + + pub fn make_normal_unit(&mut self) { + let normal_len = self.y.hypot(self.x); + if normal_len > (f64::EPSILON * 16.0) { + self.x /= normal_len; + self.y /= normal_len; + self.offset /= normal_len; + } + } + + pub fn intersects(&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()); + let apt = geo::point! { x: a.x, y: a.y }; + let bpt = geo::point! { x: b.x, y: b.y }; + let det = perp_dot_product(apt, bpt); + let rpx = -b.y * a.offset + a.y * b.offset; + let rpy = b.x * a.offset - a.x * b.offset; + + if det.abs() > ALMOST_ZERO { + LineIntersection::Point(geo::point! { x: rpx, y: rpy } / det) + } else if rpx.abs() <= ALMOST_ZERO && rpy.abs() <= ALMOST_ZERO { + LineIntersection::Overlapping + } else { + LineIntersection::Empty + } + } + + /// project the point `pt` onto this line, and generate a new line which is orthogonal + /// to `self`, and goes through `pt`. + #[inline] + pub fn orthogonal_through(&self, pt: &Point) -> Self { + Self { + // recover the original parallel vector + x: -self.y, + y: self.x, + offset: -self.x * pt.0.y + self.y * pt.0.x, + } + } + + pub fn segment_interval(&self, line: &Line) -> core::ops::RangeInclusive { + // recover the original parallel vector + let parv = geo::point! { + x: -self.y, + y: self.x, + }; + dot_product(parv, line.start.into())..=dot_product(parv, line.end.into()) + } +} + pub fn intersect_circles(circle1: &Circle, circle2: &Circle) -> Vec { let delta = circle2.pos - circle1.pos; let d = Euclidean::distance(&circle2.pos, &circle1.pos); diff --git a/src/math/tangents.rs b/src/math/tangents.rs index 16e8d37..ee4e0f5 100644 --- a/src/math/tangents.rs +++ b/src/math/tangents.rs @@ -6,20 +6,13 @@ use geo::{geometry::Point, Line}; use specctra_core::math::Circle; use thiserror::Error; -use super::seq_perp_dot_product; +use super::{seq_perp_dot_product, NormalLine}; #[derive(Error, Debug, Clone, Copy, PartialEq)] #[error("no tangents for {0:?} and {1:?}")] // TODO add real error message pub struct NoTangents(pub Circle, pub Circle); -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct CanonicalLine { - pub a: f64, - pub b: f64, - pub c: f64, -} - -fn _tangent(center: Point, r1: f64, r2: f64) -> Result { +fn _tangent(center: Point, r1: f64, r2: f64) -> Result { let epsilon = 1e-9; let dr = r2 - r1; let norm = center.x() * center.x() + center.y() * center.y(); @@ -31,15 +24,15 @@ fn _tangent(center: Point, r1: f64, r2: f64) -> Result { let sqrt_discriminant = f64::sqrt(f64::abs(discriminant)); - Ok(CanonicalLine { - a: (center.x() * dr + center.y() * sqrt_discriminant) / norm, - b: (center.y() * dr - center.x() * sqrt_discriminant) / norm, - c: r1, + Ok(NormalLine { + x: (center.x() * dr + center.y() * sqrt_discriminant) / norm, + y: (center.y() * dr - center.x() * sqrt_discriminant) / norm, + offset: r1, }) } -fn _tangents(circle1: Circle, circle2: Circle) -> Result<[CanonicalLine; 4], ()> { - let mut tgs: [CanonicalLine; 4] = [ +fn _tangents(circle1: Circle, circle2: Circle) -> Result<[NormalLine; 4], ()> { + let mut tgs: [NormalLine; 4] = [ _tangent((circle2 - circle1).pos, -circle1.r, -circle2.r)?, _tangent((circle2 - circle1).pos, -circle1.r, circle2.r)?, _tangent((circle2 - circle1).pos, circle1.r, -circle2.r)?, @@ -47,18 +40,18 @@ fn _tangents(circle1: Circle, circle2: Circle) -> Result<[CanonicalLine; 4], ()> ]; for tg in tgs.iter_mut() { - tg.c -= tg.a * circle1.pos.x() + tg.b * circle1.pos.y(); + tg.offset -= tg.x * circle1.pos.x() + tg.y * circle1.pos.y(); } Ok(tgs) } -fn cast_point_to_canonical_line(pt: Point, line: CanonicalLine) -> Point { +fn cast_point_to_canonical_line(pt: Point, line: NormalLine) -> Point { ( - (line.b * (line.b * pt.x() - line.a * pt.y()) - line.a * line.c) - / (line.a * line.a + line.b * line.b), - (line.a * (-line.b * pt.x() + line.a * pt.y()) - line.b * line.c) - / (line.a * line.a + line.b * line.b), + (line.y * (line.y * pt.x() - line.x * pt.y()) - line.x * line.offset) + / (line.x * line.x + line.y * line.y), + (line.x * (-line.y * pt.x() + line.x * pt.y()) - line.y * line.offset) + / (line.x * line.x + line.y * line.y), ) .into() }