mirror of https://codeberg.org/topola/topola.git
minimal backend for `planar-brute-embed` topological navmesh generation (#161)
Reviewed-on: https://codeberg.org/topola/topola/pulls/161 Co-authored-by: Ellen Emilia Anna Zscheile <fogti+devel@ytrizja.de> Co-committed-by: Ellen Emilia Anna Zscheile <fogti+devel@ytrizja.de>
This commit is contained in:
parent
2fac10a8d6
commit
bb86aaed2a
|
|
@ -52,6 +52,7 @@ contracts-try = "0.7"
|
||||||
derive-getters.workspace = true
|
derive-getters.workspace = true
|
||||||
enum_dispatch = "0.3"
|
enum_dispatch = "0.3"
|
||||||
geo.workspace = true
|
geo.workspace = true
|
||||||
|
log.workspace = true
|
||||||
petgraph.workspace = true
|
petgraph.workspace = true
|
||||||
rstar.workspace = true
|
rstar.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -62,13 +62,7 @@ impl<'a> ResolvedSelector<'a> {
|
||||||
let (layer, loose) = match node {
|
let (layer, loose) = match node {
|
||||||
NodeIndex::Primitive(primitive) => (
|
NodeIndex::Primitive(primitive) => (
|
||||||
primitive.primitive(board.layout().drawing()).layer(),
|
primitive.primitive(board.layout().drawing()).layer(),
|
||||||
match primitive {
|
primitive.try_into().ok(),
|
||||||
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,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
NodeIndex::Compound(compound) => {
|
NodeIndex::Compound(compound) => {
|
||||||
match board.layout().drawing().compound_weight(compound) {
|
match board.layout().drawing().compound_weight(compound) {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,19 @@ impl From<LooseIndex> for PrimitiveIndex {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<PrimitiveIndex> for LooseIndex {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(primitive: PrimitiveIndex) -> Result<LooseIndex, ()> {
|
||||||
|
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)]
|
#[enum_dispatch(GetPrevNextLoose, GetDrawing, GetPetgraphIndex)]
|
||||||
pub enum Loose<'a, CW: Copy, R: AccessRules> {
|
pub enum Loose<'a, CW: Copy, R: AccessRules> {
|
||||||
Dot(LooseDot<'a, CW, R>),
|
Dot(LooseDot<'a, CW, R>),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use std::f64::consts::TAU;
|
||||||
|
|
||||||
use enum_dispatch::enum_dispatch;
|
use enum_dispatch::enum_dispatch;
|
||||||
use geo::algorithm::line_measures::{Distance, Euclidean};
|
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 rstar::{RTreeObject, AABB};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -148,6 +148,13 @@ impl SegShape {
|
||||||
|
|
||||||
polygon![p1.0, p2.0, p3.0, p4.0]
|
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 {
|
impl MeasureLength for SegShape {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use rstar::AABB;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
drawing::{
|
drawing::{
|
||||||
band::BandTermsegIndex,
|
band::{BandTermsegIndex, BandUid},
|
||||||
bend::{BendIndex, BendWeight, LooseBendWeight},
|
bend::{BendIndex, BendWeight, LooseBendWeight},
|
||||||
dot::{
|
dot::{
|
||||||
DotIndex, DotWeight, FixedDotIndex, FixedDotWeight, GeneralDotWeight, LooseDotIndex,
|
DotIndex, DotWeight, FixedDotIndex, FixedDotWeight, GeneralDotWeight, LooseDotIndex,
|
||||||
|
|
@ -18,20 +18,28 @@ use crate::{
|
||||||
},
|
},
|
||||||
gear::GearIndex,
|
gear::GearIndex,
|
||||||
graph::{GetMaybeNet, IsInLayer, MakePrimitive, PrimitiveIndex, PrimitiveWeight},
|
graph::{GetMaybeNet, IsInLayer, MakePrimitive, PrimitiveIndex, PrimitiveWeight},
|
||||||
primitive::MakePrimitiveShape,
|
loose::LooseIndex,
|
||||||
|
primitive::{GetWeight, MakePrimitiveShape, Primitive},
|
||||||
rules::AccessRules,
|
rules::AccessRules,
|
||||||
seg::{
|
seg::{
|
||||||
FixedSegIndex, FixedSegWeight, LoneLooseSegIndex, LoneLooseSegWeight, SegIndex,
|
FixedSegIndex, FixedSegWeight, LoneLooseSegIndex, LoneLooseSegWeight, SegIndex,
|
||||||
SegWeight, SeqLooseSegIndex, SeqLooseSegWeight,
|
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},
|
graph::{GenericIndex, GetPetgraphIndex},
|
||||||
layout::{
|
layout::{
|
||||||
poly::{MakePolygon, Poly, PolyWeight},
|
poly::{MakePolygon, Poly, PolyWeight},
|
||||||
via::{Via, ViaWeight},
|
via::{Via, ViaWeight},
|
||||||
},
|
},
|
||||||
|
math::{LineIntersection, NormalLine},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Represents a weight for various compounds
|
/// Represents a weight for various compounds
|
||||||
|
|
@ -336,6 +344,119 @@ impl<R: AccessRules> Layout<R> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<Point> {
|
||||||
|
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<BandUid> {
|
||||||
|
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 {
|
pub fn rules(&self) -> &R {
|
||||||
self.drawing.rules()
|
self.drawing.rules()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,95 @@ pub use specctra_core::math::{Circle, PointWithRotation};
|
||||||
mod tangents;
|
mod tangents;
|
||||||
pub use 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<Line> 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<f64> {
|
||||||
|
// 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<Point> {
|
pub fn intersect_circles(circle1: &Circle, circle2: &Circle) -> Vec<Point> {
|
||||||
let delta = circle2.pos - circle1.pos;
|
let delta = circle2.pos - circle1.pos;
|
||||||
let d = Euclidean::distance(&circle2.pos, &circle1.pos);
|
let d = Euclidean::distance(&circle2.pos, &circle1.pos);
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,13 @@ use geo::{geometry::Point, Line};
|
||||||
use specctra_core::math::Circle;
|
use specctra_core::math::Circle;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use super::seq_perp_dot_product;
|
use super::{seq_perp_dot_product, NormalLine};
|
||||||
|
|
||||||
#[derive(Error, Debug, Clone, Copy, PartialEq)]
|
#[derive(Error, Debug, Clone, Copy, PartialEq)]
|
||||||
#[error("no tangents for {0:?} and {1:?}")] // TODO add real error message
|
#[error("no tangents for {0:?} and {1:?}")] // TODO add real error message
|
||||||
pub struct NoTangents(pub Circle, pub Circle);
|
pub struct NoTangents(pub Circle, pub Circle);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
fn _tangent(center: Point, r1: f64, r2: f64) -> Result<NormalLine, ()> {
|
||||||
pub struct CanonicalLine {
|
|
||||||
pub a: f64,
|
|
||||||
pub b: f64,
|
|
||||||
pub c: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _tangent(center: Point, r1: f64, r2: f64) -> Result<CanonicalLine, ()> {
|
|
||||||
let epsilon = 1e-9;
|
let epsilon = 1e-9;
|
||||||
let dr = r2 - r1;
|
let dr = r2 - r1;
|
||||||
let norm = center.x() * center.x() + center.y() * center.y();
|
let norm = center.x() * center.x() + center.y() * center.y();
|
||||||
|
|
@ -31,15 +24,15 @@ fn _tangent(center: Point, r1: f64, r2: f64) -> Result<CanonicalLine, ()> {
|
||||||
|
|
||||||
let sqrt_discriminant = f64::sqrt(f64::abs(discriminant));
|
let sqrt_discriminant = f64::sqrt(f64::abs(discriminant));
|
||||||
|
|
||||||
Ok(CanonicalLine {
|
Ok(NormalLine {
|
||||||
a: (center.x() * dr + center.y() * sqrt_discriminant) / norm,
|
x: (center.x() * dr + center.y() * sqrt_discriminant) / norm,
|
||||||
b: (center.y() * dr - center.x() * sqrt_discriminant) / norm,
|
y: (center.y() * dr - center.x() * sqrt_discriminant) / norm,
|
||||||
c: r1,
|
offset: r1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _tangents(circle1: Circle, circle2: Circle) -> Result<[CanonicalLine; 4], ()> {
|
fn _tangents(circle1: Circle, circle2: Circle) -> Result<[NormalLine; 4], ()> {
|
||||||
let mut tgs: [CanonicalLine; 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)?,
|
_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() {
|
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)
|
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.y * (line.y * pt.x() - line.x * pt.y()) - line.x * line.offset)
|
||||||
/ (line.a * line.a + line.b * line.b),
|
/ (line.x * line.x + line.y * line.y),
|
||||||
(line.a * (-line.b * pt.x() + line.a * pt.y()) - line.b * line.c)
|
(line.x * (-line.y * pt.x() + line.x * pt.y()) - line.y * line.offset)
|
||||||
/ (line.a * line.a + line.b * line.b),
|
/ (line.x * line.x + line.y * line.y),
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue