refactor(layout/layout): Put bands_between_* methods into separate file collect_bands.rs

This commit is contained in:
Ellen Emilia Anna Zscheile 2025-06-28 11:32:38 +02:00
parent d3dc826be4
commit 59473c36c8
4 changed files with 270 additions and 241 deletions

View File

@ -56,6 +56,7 @@ allowed_scopes = [
"interactor/interaction",
"interactor/interactor",
"interactor/route_plan",
"layout/collect_bands",
"layout/layout",
"layout/poly",
"layout/via",

264
src/layout/collect_bands.rs Normal file
View File

@ -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<R: AccessRules> Layout<R> {
fn bands_between_positions(
&self,
layer: usize,
left_pos: Point,
right_pos: Point,
) -> impl Iterator<Item = (f64, BandUid, LooseIndex)> + '_ {
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(&ltr_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<Item = (BandUid, LooseIndex)> {
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<impl Iterator<Item = (BandUid, LooseIndex)>> {
// 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<Item = RelaxedPath<BandUid, ()>> + '_ {
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(),
)
})
}
}

View File

@ -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<R: AccessRules> Layout<R> {
}
}
fn bands_between_positions_internal(
&self,
layer: usize,
left_pos: Point,
right_pos: Point,
) -> impl Iterator<Item = (f64, BandUid, LooseIndex)> + '_ {
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(&ltr_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<Item = (BandUid, LooseIndex)> {
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<impl Iterator<Item = (BandUid, LooseIndex)>> {
// 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<Item = RelaxedPath<BandUid, ()>> + '_ {
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()
}

View File

@ -4,6 +4,7 @@
//! Layout module for handling board geometry.
mod collect_bands;
mod layout;
pub mod poly;
pub mod via;