From 59473c36c8a277353012aa5b06969e5c2dc633a0 Mon Sep 17 00:00:00 2001 From: Ellen Emilia Anna Zscheile Date: Sat, 28 Jun 2025 11:32:38 +0200 Subject: [PATCH] refactor(layout/layout): Put bands_between_* methods into separate file collect_bands.rs --- committed.toml | 1 + src/layout/collect_bands.rs | 264 ++++++++++++++++++++++++++++++++++++ src/layout/layout.rs | 245 +-------------------------------- src/layout/mod.rs | 1 + 4 files changed, 270 insertions(+), 241 deletions(-) create mode 100644 src/layout/collect_bands.rs diff --git a/committed.toml b/committed.toml index 88e4710..f7df6e2 100644 --- a/committed.toml +++ b/committed.toml @@ -56,6 +56,7 @@ allowed_scopes = [ "interactor/interaction", "interactor/interactor", "interactor/route_plan", + "layout/collect_bands", "layout/layout", "layout/poly", "layout/via", diff --git a/src/layout/collect_bands.rs b/src/layout/collect_bands.rs new file mode 100644 index 0000000..323100b --- /dev/null +++ b/src/layout/collect_bands.rs @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: 2025 Topola contributors +// +// SPDX-License-Identifier: MIT + +use core::iter; + +use geo::{Coord, Line, Point}; +use planar_incr_embed::RelaxedPath; +use rstar::AABB; + +use crate::{ + drawing::{ + band::BandUid, + dot::DotIndex, + graph::{MakePrimitive, PrimitiveIndex}, + loose::LooseIndex, + primitive::MakePrimitiveShape, + rules::AccessRules, + Collect, + }, + geometry::{ + compound::ManageCompounds, + primitive::{AccessPrimitiveShape, PrimitiveShape, SegShape}, + shape::AccessShape, + GenericNode, + }, + graph::GetPetgraphIndex, + layout::{Layout, NodeIndex}, + math::{intersect_linestring_and_beam, LineIntersection, NormalLine}, +}; + +impl Layout { + fn bands_between_positions( + &self, + layer: usize, + left_pos: Point, + right_pos: Point, + ) -> impl Iterator + '_ { + let ltr_line = geo::Line { + start: left_pos.into(), + end: right_pos.into(), + }; + let fake_seg = SegShape { + from: left_pos, + to: right_pos, + 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(); + + 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(move |(loose, shape)| { + let band_uid = self.drawing.loose_band_uid(loose).ok()?; + 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, loose)) + }) + } + + /// 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, + ) -> impl Iterator { + assert_ne!(left, right); + let left_pos = self.node_shape(left).center(); + let right_pos = self.node_shape(right).center(); + let mut bands: Vec<_> = self + .bands_between_positions(layer, left_pos, right_pos) + .filter(|(_, band_uid, _)| { + // filter entries which are connected to either lhs or rhs (and possibly both) + let (bts1, bts2) = band_uid.into(); + let (bts1, bts2) = (bts1.petgraph_index(), bts2.petgraph_index()); + let geometry = self.drawing.geometry(); + [(bts1, left), (bts1, right), (bts2, left), (bts2, right)] + .iter() + .all(|&(x, y)| !geometry.is_joined_with(x, y)) + }) + .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, loose)| (band_uid, loose)) + } + + /// Finds all bands on `layer` between direction `left` and node `right` + /// and returns them ordered from `left` to `right`. + pub fn bands_between_node_and_boundary( + &self, + layer: usize, + left: Coord, + right: NodeIndex, + ) -> Option> { + // First, decode the `left` direction into a point on the boundary + let right_pos = self.node_shape(right).center(); + let left_pos = intersect_linestring_and_beam( + self.drawing.boundary().exterior(), + &Line { + start: right_pos.0, + end: right_pos.0 + left, + }, + )?; + + let mut bands: Vec<_> = self + .bands_between_positions(layer, left_pos, right_pos) + .filter(|(_, band_uid, _)| { + // filter entries which are connected to rhs + let (bts1, bts2) = band_uid.into(); + let (bts1, bts2) = (bts1.petgraph_index(), bts2.petgraph_index()); + let geometry = self.drawing.geometry(); + [(bts1, right), (bts2, right)] + .iter() + .all(|&(x, y)| !geometry.is_joined_with(x, y)) + }) + .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. + + Some( + bands + .into_iter() + .map(|(_, band_uid, loose)| (band_uid, loose)), + ) + } + + fn does_compound_have_core(&self, primary: NodeIndex, core: DotIndex) -> bool { + let core: PrimitiveIndex = core.into(); + match primary { + GenericNode::Primitive(pi) => pi == core, + GenericNode::Compound(compound) => self + .drawing + .geometry() + .compound_members(compound) + .any(|(_, pi)| pi == core), + } + } + + /// 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_with_alignment( + &self, + layer: usize, + left: NodeIndex, + right: NodeIndex, + ) -> impl Iterator> + '_ { + let mut alignment_idx: u8 = 0; + + // resolve end-points possibly to compounds such that + // `does_compound_have_core` produces correct results. + let maybe_to_compound = |x: NodeIndex| match x { + GenericNode::Primitive(pi) => self + .drawing + .geometry() + .compounds(pi) + .next() + .map(|(_, idx)| idx), + GenericNode::Compound(compound) => Some(compound), + }; + let resolve_node = + |x: NodeIndex| maybe_to_compound(x).map(GenericNode::Compound).unwrap_or(x); + let (left, right) = (resolve_node(left), resolve_node(right)); + + self.bands_between_nodes(layer, left, right) + .map(move |(band_uid, loose)| { + // first, tag entry with core + let maybe_core = match loose { + LooseIndex::Bend(lbi) => Some(self.drawing.geometry().core(lbi.into())), + _ => None, + }; + Some((band_uid, maybe_core)) + }) + .chain(iter::once(None)) + .flat_map(move |item| { + // insert alignment pseudo-paths if necessary + let alignment_incr = match item { + Some((_, maybe_core)) => match (alignment_idx, maybe_core) { + (0, Some(core)) if self.does_compound_have_core(left, core) => 0, + (_, Some(core)) if self.does_compound_have_core(left, core) => { + panic!("invalid band ordering") + } + + (_, Some(core)) if self.does_compound_have_core(right, core) => 2u8 + .checked_sub(alignment_idx) + .expect("invalid band ordering"), + + (0, _) => 1, + (1, _) => 0, + _ => panic!("invalid band ordering"), + }, + None => 2u8 + .checked_sub(alignment_idx) + .expect("invalid band ordering"), + }; + + alignment_idx += alignment_incr; + + iter::repeat_n(RelaxedPath::Weak(()), alignment_incr.into()).chain( + item.map(|(band_uid, _)| RelaxedPath::Normal(band_uid)) + .into_iter(), + ) + }) + } +} diff --git a/src/layout/layout.rs b/src/layout/layout.rs index b6d3b68..d5ee3bb 100644 --- a/src/layout/layout.rs +++ b/src/layout/layout.rs @@ -2,18 +2,15 @@ // // SPDX-License-Identifier: MIT -use core::iter; - use contracts_try::debug_ensures; use derive_getters::Getters; use enum_dispatch::enum_dispatch; -use geo::{Coord, Line, Point}; -use planar_incr_embed::RelaxedPath; +use geo::Point; use rstar::AABB; use crate::{ drawing::{ - band::{BandTermsegIndex, BandUid}, + band::BandTermsegIndex, bend::{BendIndex, BendWeight, LooseBendWeight}, dot::{ DotIndex, DotWeight, FixedDotIndex, FixedDotWeight, GeneralDotWeight, LooseDotIndex, @@ -21,19 +18,17 @@ use crate::{ }, gear::GearIndex, graph::{GetMaybeNet, IsInLayer, MakePrimitive, PrimitiveIndex, PrimitiveWeight}, - loose::LooseIndex, primitive::MakePrimitiveShape, rules::AccessRules, seg::{ FixedSegIndex, FixedSegWeight, LoneLooseSegIndex, LoneLooseSegWeight, SegIndex, SegWeight, SeqLooseSegIndex, SeqLooseSegWeight, }, - Cane, Collect, Drawing, DrawingEdit, DrawingException, Infringement, + Cane, Drawing, DrawingEdit, DrawingException, Infringement, }, geometry::{ compound::ManageCompounds, edit::ApplyGeometryEdit, - primitive::{AccessPrimitiveShape, PrimitiveShape, SegShape}, shape::{AccessShape, Shape}, GenericNode, GetLayer, GetSetPos, }, @@ -42,7 +37,7 @@ use crate::{ poly::{add_poly_with_nodes_intern, MakePolygon, PolyWeight}, via::{Via, ViaWeight}, }, - math::{intersect_linestring_and_beam, LineIntersection, NormalLine, RotationSense}, + math::RotationSense, }; /// Represents a weight for various compounds @@ -379,238 +374,6 @@ impl Layout { } } - fn bands_between_positions_internal( - &self, - layer: usize, - left_pos: Point, - right_pos: Point, - ) -> impl Iterator + '_ { - let ltr_line = geo::Line { - start: left_pos.into(), - end: right_pos.into(), - }; - let fake_seg = SegShape { - from: left_pos, - to: right_pos, - 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(); - - 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(move |(loose, shape)| { - let band_uid = self.drawing.loose_band_uid(loose).ok()?; - 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, loose)) - }) - } - - /// 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, - ) -> impl Iterator { - assert_ne!(left, right); - let left_pos = self.node_shape(left).center(); - let right_pos = self.node_shape(right).center(); - let mut bands: Vec<_> = self - .bands_between_positions_internal(layer, left_pos, right_pos) - .filter(|(_, band_uid, _)| { - // filter entries which are connected to either lhs or rhs (and possibly both) - let (bts1, bts2) = band_uid.into(); - let (bts1, bts2) = (bts1.petgraph_index(), bts2.petgraph_index()); - let geometry = self.drawing.geometry(); - [(bts1, left), (bts1, right), (bts2, left), (bts2, right)] - .iter() - .all(|&(x, y)| !geometry.is_joined_with(x, y)) - }) - .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, loose)| (band_uid, loose)) - } - - /// Finds all bands on `layer` between direction `left` and node `right` - /// and returns them ordered from `left` to `right`. - pub fn bands_between_node_and_boundary( - &self, - layer: usize, - left: Coord, - right: NodeIndex, - ) -> Option> { - // First, decode the `left` direction into a point on the boundary - let right_pos = self.node_shape(right).center(); - let left_pos = intersect_linestring_and_beam( - self.drawing.boundary().exterior(), - &Line { - start: right_pos.0, - end: right_pos.0 + left, - }, - )?; - - let mut bands: Vec<_> = self - .bands_between_positions_internal(layer, left_pos, right_pos) - .filter(|(_, band_uid, _)| { - // filter entries which are connected to rhs - let (bts1, bts2) = band_uid.into(); - let (bts1, bts2) = (bts1.petgraph_index(), bts2.petgraph_index()); - let geometry = self.drawing.geometry(); - [(bts1, right), (bts2, right)] - .iter() - .all(|&(x, y)| !geometry.is_joined_with(x, y)) - }) - .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. - - Some( - bands - .into_iter() - .map(|(_, band_uid, loose)| (band_uid, loose)), - ) - } - - fn does_compound_have_core(&self, primary: NodeIndex, core: DotIndex) -> bool { - let core: PrimitiveIndex = core.into(); - match primary { - GenericNode::Primitive(pi) => pi == core, - GenericNode::Compound(compound) => self - .drawing - .geometry() - .compound_members(compound) - .any(|(_, pi)| pi == core), - } - } - - /// 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_with_alignment( - &self, - layer: usize, - left: NodeIndex, - right: NodeIndex, - ) -> impl Iterator> + '_ { - let mut alignment_idx: u8 = 0; - - // resolve end-points possibly to compounds such that - // `does_compound_have_core` produces correct results. - let maybe_to_compound = |x: NodeIndex| match x { - GenericNode::Primitive(pi) => self - .drawing - .geometry() - .compounds(pi) - .next() - .map(|(_, idx)| idx), - GenericNode::Compound(compound) => Some(compound), - }; - let resolve_node = - |x: NodeIndex| maybe_to_compound(x).map(GenericNode::Compound).unwrap_or(x); - let (left, right) = (resolve_node(left), resolve_node(right)); - - self.bands_between_nodes(layer, left, right) - .map(move |(band_uid, loose)| { - // first, tag entry with core - let maybe_core = match loose { - LooseIndex::Bend(lbi) => Some(self.drawing.geometry().core(lbi.into())), - _ => None, - }; - Some((band_uid, maybe_core)) - }) - .chain(iter::once(None)) - .flat_map(move |item| { - // insert alignment pseudo-paths if necessary - let alignment_incr = match item { - Some((_, maybe_core)) => match (alignment_idx, maybe_core) { - (0, Some(core)) if self.does_compound_have_core(left, core) => 0, - (_, Some(core)) if self.does_compound_have_core(left, core) => { - panic!("invalid band ordering") - } - - (_, Some(core)) if self.does_compound_have_core(right, core) => 2u8 - .checked_sub(alignment_idx) - .expect("invalid band ordering"), - - (0, _) => 1, - (1, _) => 0, - _ => panic!("invalid band ordering"), - }, - None => 2u8 - .checked_sub(alignment_idx) - .expect("invalid band ordering"), - }; - - alignment_idx += alignment_incr; - - iter::repeat_n(RelaxedPath::Weak(()), alignment_incr.into()).chain( - item.map(|(band_uid, _)| RelaxedPath::Normal(band_uid)) - .into_iter(), - ) - }) - } - pub fn rules(&self) -> &R { self.drawing.rules() } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 8ff5cfa..3e619b1 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -4,6 +4,7 @@ //! Layout module for handling board geometry. +mod collect_bands; mod layout; pub mod poly; pub mod via;