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:
Ellen Emilia Anna Zscheile 2025-02-15 00:00:15 +00:00 committed by mikolaj
parent 2fac10a8d6
commit bb86aaed2a
7 changed files with 251 additions and 33 deletions

View File

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

View File

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

View File

@ -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)]
pub enum Loose<'a, CW: Copy, R: AccessRules> {
Dot(LooseDot<'a, CW, R>),

View File

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

View File

@ -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<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(&ltr_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()
}

View File

@ -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<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> {
let delta = circle2.pos - circle1.pos;
let d = Euclidean::distance(&circle2.pos, &circle1.pos);

View File

@ -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<CanonicalLine, ()> {
fn _tangent(center: Point, r1: f64, r2: f64) -> Result<NormalLine, ()> {
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<CanonicalLine, ()> {
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()
}