use crate::spatial::{Aabb, Bvh};
use crate::Vec3;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
const PARALLEL_THRESHOLD: f64 = 1e-12;
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct Vec2 {
pub x: f64,
pub y: f64,
}
impl Vec2 {
#[inline]
pub fn distance_to_segment(self, start: Self, end: Self, guard: f64) -> f64 {
let segment = end.sub(start);
let length_squared = segment.dot(segment);
if length_squared <= guard {
return self.sub(start).length();
}
let parameter = (self.sub(start).dot(segment) / length_squared).clamp(0.0, 1.0);
self.sub(start.add(segment.scale(parameter))).length()
}
#[inline]
pub fn distance_to_segment_squared(self, start: Self, end: Self) -> f64 {
let segment = end.sub(start);
let length_squared = segment.dot(segment);
let parameter = if length_squared > 0.0 {
(self.sub(start).dot(segment) / length_squared).clamp(0.0, 1.0)
} else {
0.0
};
let residual = self.sub(start.add(segment.scale(parameter)));
residual.dot(residual)
}
pub(crate) fn sub(self, other: Self) -> Self {
Self {
x: self.x - other.x,
y: self.y - other.y,
}
}
pub(crate) fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
pub(crate) fn scale(self, factor: f64) -> Self {
Self {
x: self.x * factor,
y: self.y * factor,
}
}
fn distance(self, other: Self) -> f64 {
self.sub(other).length()
}
pub(crate) fn length(self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
fn cross(self, other: Self) -> f64 {
self.x * other.y - self.y * other.x
}
pub(crate) fn dot(self, other: Self) -> f64 {
self.x * other.x + self.y * other.y
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Segment2 {
pub a: Vec2,
pub b: Vec2,
#[serde(default)]
pub tag: serde_json::Value,
}
#[derive(Clone, Debug, Serialize)]
pub struct ArrangementPiece {
pub a: Vec2,
pub b: Vec2,
pub parent: Segment2,
}
#[derive(Clone, Debug, Serialize)]
pub struct CycleUse {
pub piece: ArrangementPiece,
pub forward: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct ArrangementRegion {
pub outer: Vec<CycleUse>,
pub holes: Vec<Vec<CycleUse>>,
pub area: f64,
pub outer_area: f64,
}
#[derive(Clone)]
struct Node {
point: Vec2,
outgoing: Vec<usize>,
}
struct NodeLookup {
tolerance: f64,
width: f64,
cells: Option<FxHashMap<[i64; 2], Vec<usize>>>,
}
impl NodeLookup {
fn new(tolerance: f64, indexed: bool) -> Self {
let width = tolerance * 2.0;
Self {
tolerance,
width,
cells: (indexed && tolerance >= 1e-150 && width.is_finite()).then(FxHashMap::default),
}
}
fn key(&self, point: Vec2) -> Option<[i64; 2]> {
let q = [point.x / self.width, point.y / self.width];
q.iter()
.all(|v| v.is_finite() && v.abs() <= (1u64 << 48) as f64)
.then(|| q.map(|v| v.floor() as i64))
}
fn find_or_insert(&mut self, point: Vec2, nodes: &mut Vec<Node>) -> usize {
let key = self.cells.as_ref().and_then(|_| self.key(point));
if key.is_none() {
self.cells = None;
}
let found = if let (Some(cells), Some([x, y])) = (&self.cells, key) {
let mut first = None;
for dx in -1..=1 {
for dy in -1..=1 {
if let Some(indices) = cells.get(&[x + dx, y + dy]) {
for &index in indices {
if first.is_some_and(|first| index >= first) {
break;
}
if nodes[index].point.distance(point) <= self.tolerance {
first = Some(index);
break;
}
}
}
}
}
first
} else {
nodes
.iter()
.position(|node| node.point.distance(point) <= self.tolerance)
};
if let Some(index) = found {
return index;
}
let index = nodes.len();
nodes.push(Node {
point,
outgoing: Vec::new(),
});
if let (Some(cells), Some(key)) = (&mut self.cells, key) {
cells.entry(key).or_default().push(index);
}
index
}
}
fn tolerance_parameter(segment: &Segment2, tolerance: f64) -> f64 {
let length = segment.a.distance(segment.b);
if length <= tolerance {
0.5
} else {
tolerance / length
}
}
fn interpolate(a: Vec2, b: Vec2, parameter: f64) -> Vec2 {
Vec2 {
x: a.x + (b.x - a.x) * parameter,
y: a.y + (b.y - a.y) * parameter,
}
}
pub fn segment_intersection(
a1: Vec2,
b1: Vec2,
a2: Vec2,
b2: Vec2,
tolerance: f64,
) -> Option<[f64; 2]> {
let d1 = b1.sub(a1);
let d2 = b2.sub(a2);
let denominator = d1.cross(d2);
let length1 = d1.length();
let length2 = d2.length();
if denominator.abs() <= PARALLEL_THRESHOLD * length1 * length2 {
return None;
}
let offset = a2.sub(a1);
let t1 = offset.cross(d2) / denominator;
let t2 = offset.cross(d1) / denominator;
let epsilon1 = tolerance / length1.max(tolerance);
let epsilon2 = tolerance / length2.max(tolerance);
if t1 < -epsilon1 || t1 > 1.0 + epsilon1 || t2 < -epsilon2 || t2 > 1.0 + epsilon2 {
None
} else {
Some([t1.clamp(0.0, 1.0), t2.clamp(0.0, 1.0)])
}
}
fn end_on_segment_parameters(end: &Segment2, bar: &Segment2, tolerance: f64) -> Vec<(Vec2, f64)> {
let (stem, span) = (end.b.sub(end.a), bar.b.sub(bar.a));
let squared = span.dot(span);
if squared <= 0.0 || stem.cross(span).abs() <= PARALLEL_THRESHOLD * stem.length() * span.length() {
return Vec::new();
}
let margin = tolerance_parameter(bar, tolerance);
[end.a, end.b]
.into_iter()
.filter_map(|point| {
let parameter = point.sub(bar.a).dot(span) / squared;
(parameter > margin
&& parameter < 1.0 - margin
&& point_segment_distance(point, bar.a, bar.b) <= tolerance)
.then_some((point, parameter))
})
.collect()
}
pub use crate::polygon::PolygonClass;
fn point_segment_distance(point: Vec2, a: Vec2, b: Vec2) -> f64 {
point.distance_to_segment(a, b, 1e-300)
}
fn polygon_reject_box(polygon: &[Vec2], tolerance: f64) -> [f64; 5] {
let mut bounds = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
let mut magnitude = 1.0f64;
for point in polygon {
bounds[0] = bounds[0].min(point.x);
bounds[1] = bounds[1].min(point.y);
bounds[2] = bounds[2].max(point.x);
bounds[3] = bounds[3].max(point.y);
magnitude = magnitude.max(point.x.abs()).max(point.y.abs());
}
let finite = !polygon.is_empty() && bounds.iter().all(|value| value.is_finite()) && tolerance.is_finite() && tolerance >= 0.0;
let reach = if finite { tolerance * (1.0 + 1e-9) + 64.0 * f64::EPSILON * magnitude } else { f64::NAN };
[bounds[0], bounds[1], bounds[2], bounds[3], reach]
}
struct BoundaryBand {
origin: [f64; 2],
cell: [f64; 2],
side: usize,
reach: f64,
starts: Vec<u32>,
items: Vec<u32>,
usable: bool,
}
impl BoundaryBand {
fn new(polygon: &[Vec2], tolerance: f64) -> Self {
let reject = polygon_reject_box(polygon, tolerance);
let reach = reject[4];
let count = polygon.len();
let usable = reach.is_finite() && count >= 16 && count <= (1 << 24);
let side = if usable { ((count as f64).sqrt() as usize).clamp(4, 512) } else { 0 };
let origin = [reject[0] - reach, reject[1] - reach];
let extent = [(reject[2] + reach) - origin[0], (reject[3] + reach) - origin[1]];
let cell = [(extent[0] / side.max(1) as f64).max(f64::MIN_POSITIVE), (extent[1] / side.max(1) as f64).max(f64::MIN_POSITIVE)];
let mut band = Self { origin, cell, side, reach, starts: Vec::new(), items: Vec::new(), usable };
if !band.usable {
return band;
}
let mut counts = vec![0u32; side * side + 1];
let ranges: Vec<[usize; 4]> = (0..count)
.map(|index| {
let (a, b) = (polygon[index], polygon[(index + 1) % count]);
let pad = reach + 64.0 * f64::EPSILON * a.x.abs().max(a.y.abs()).max(b.x.abs()).max(b.y.abs());
[band.column(a.x.min(b.x) - pad), band.column(a.x.max(b.x) + pad), band.row(a.y.min(b.y) - pad), band.row(a.y.max(b.y) + pad)]
})
.collect();
for range in &ranges {
for y in range[2]..=range[3] {
for x in range[0]..=range[1] {
counts[y * side + x + 1] += 1;
}
}
}
for index in 1..counts.len() {
counts[index] += counts[index - 1];
}
if counts[side * side] as usize > 64 * count + 4096 {
band.usable = false;
return band;
}
let mut cursor = counts.clone();
band.items = vec![0u32; counts[side * side] as usize];
for (edge, range) in ranges.iter().enumerate() {
for y in range[2]..=range[3] {
for x in range[0]..=range[1] {
let slot = &mut cursor[y * side + x];
band.items[*slot as usize] = edge as u32;
*slot += 1;
}
}
}
band.starts = counts;
band
}
fn column(&self, x: f64) -> usize {
(((x - self.origin[0]) / self.cell[0]).floor().max(0.0) as usize).min(self.side - 1)
}
fn row(&self, y: f64) -> usize {
(((y - self.origin[1]) / self.cell[1]).floor().max(0.0) as usize).min(self.side - 1)
}
fn classify(&self, point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
if !self.usable || !point.x.is_finite() || !point.y.is_finite() {
return point_in_polygon_class(point, polygon, tolerance);
}
let cell = self.row(point.y) * self.side + self.column(point.x);
let count = polygon.len();
for &edge in &self.items[self.starts[cell] as usize..self.starts[cell + 1] as usize] {
let edge = edge as usize;
if point.distance_to_segment(polygon[edge], polygon[(edge + 1) % count], 1e-300) <= tolerance {
return PolygonClass::Boundary;
}
}
if crate::polygon::parity_inside([point.x, point.y], count, |i| [polygon[i].x, polygon[i].y]) {
PolygonClass::Inside
} else {
PolygonClass::Outside
}
}
}
fn outside_reject_box(point: Vec2, reject: &[f64; 5]) -> bool {
let reach = reject[4] + 64.0 * f64::EPSILON * point.x.abs().max(point.y.abs());
point.x < reject[0] - reach || point.y < reject[1] - reach || point.x > reject[2] + reach || point.y > reject[3] + reach
}
fn digon_order_enabled() -> bool {
switch!("BREP_ARRANGE_DIGON_ORDER", true)
}
fn hole_box_reject_enabled() -> bool {
switch!("BREP_ARRANGE_HOLE_BOX", true)
}
fn point_in_polygon_class(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
crate::polygon::classify(point, polygon, tolerance, 1e-300)
}
pub fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> &'static str {
match point_in_polygon_class(point, polygon, tolerance) {
PolygonClass::Inside => "in",
PolygonClass::Outside => "out",
PolygonClass::Boundary => "boundary",
}
}
struct PieceInput<'a> {
a: Vec2,
b: Vec2,
parent: std::borrow::Cow<'a, Segment2>,
}
impl PieceInput<'_> {
fn materialize(&self, forward: bool) -> CycleUse {
CycleUse { piece: ArrangementPiece { a: self.a, b: self.b, parent: self.parent.as_ref().clone() }, forward }
}
}
#[derive(Clone)]
enum CycleUses {
Eager(Vec<CycleUse>),
Deferred(Vec<usize>),
}
impl CycleUses {
fn push(&mut self, half: usize, pieces: &[PieceInput<'_>]) {
match self {
Self::Eager(uses) => uses.push(pieces[half >> 1].materialize(half % 2 == 0)),
Self::Deferred(halves) => halves.push(half),
}
}
fn into_uses(self, pieces: &[PieceInput<'_>]) -> Vec<CycleUse> {
match self {
Self::Eager(uses) => uses,
Self::Deferred(halves) => halves.into_iter().map(|h| pieces[h >> 1].materialize(h % 2 == 0)).collect(),
}
}
fn copy_uses(&self, pieces: &[PieceInput<'_>]) -> Vec<CycleUse> {
match self {
Self::Eager(uses) => uses.clone(),
Self::Deferred(halves) => halves.iter().map(|&h| pieces[h >> 1].materialize(h % 2 == 0)).collect(),
}
}
}
#[derive(Clone)]
struct Cycle {
uses: CycleUses,
area: f64,
node_indices: Vec<usize>,
}
fn visit_segment_pairs(segments: &[Segment2], tolerance: f64, mut visit: impl FnMut(usize, usize)) {
let all_pairs = |visit: &mut dyn FnMut(usize, usize)| {
for first in 0..segments.len() {
for second in first + 1..segments.len() {
visit(first, second);
}
}
};
if segments.len() <= 32
|| segments.len() > u32::MAX as usize
|| !tolerance.is_finite()
|| tolerance < 0.0
|| tolerance > 1e100
{
all_pairs(&mut visit);
return;
}
let mut scale = 0.0f64;
let mut boxes = Vec::with_capacity(segments.len());
let mut extent = Aabb::empty();
for segment in segments {
let coordinates = [segment.a.x, segment.a.y, segment.b.x, segment.b.y];
let length = segment.a.distance(segment.b);
if coordinates
.iter()
.any(|v| !v.is_finite() || v.abs() > 1e100)
|| !(1e-100..=1e100).contains(&length)
{
all_pairs(&mut visit);
return;
}
for coordinate in coordinates {
scale = scale.max(coordinate.abs());
}
let bounds = Aabb {
minimum: Vec3::new(
segment.a.x.min(segment.b.x),
segment.a.y.min(segment.b.y),
0.0,
),
maximum: Vec3::new(
segment.a.x.max(segment.b.x),
segment.a.y.max(segment.b.y),
0.0,
),
};
extent.include(bounds);
boxes.push(bounds);
}
let padding = 2.0 * tolerance + 128.0 * f64::EPSILON / PARALLEL_THRESHOLD * scale;
if padding >= extent.diagonal() {
all_pairs(&mut visit);
return;
}
let direction_tiers = direction_index_on();
let small_padding = 2.0 * tolerance + 128.0 * f64::EPSILON / DIRECTION_SINE * scale;
let swept = if direction_tiers && arrange_sweep_on() { swept_pairs(&boxes, small_padding) } else { None };
let tree = swept.is_none().then(|| Bvh::build(&boxes));
let mut candidates = Vec::new();
if direction_tiers {
let width = 2.0 * DIRECTION_SINE;
let bucket_count = ((std::f64::consts::PI / width).ceil() as usize).max(1);
let mut buckets: FxHashMap<usize, Vec<usize>> = FxHashMap::default();
let bucket_of: Vec<usize> = segments
.iter()
.map(|segment| {
let angle = (segment.b.y - segment.a.y).atan2(segment.b.x - segment.a.x).rem_euclid(std::f64::consts::PI);
((angle / width) as usize).min(bucket_count - 1)
})
.collect();
for (index, bucket) in bucket_of.iter().enumerate() {
buckets.entry(*bucket).or_default().push(index);
}
let near = |a: &Aabb, b: &Aabb| {
a.minimum.x - padding <= b.maximum.x
&& b.minimum.x - padding <= a.maximum.x
&& a.minimum.y - padding <= b.maximum.y
&& b.minimum.y - padding <= a.maximum.y
};
for (first, &bounds) in boxes.iter().enumerate() {
candidates.clear();
match (&swept, &tree) {
(Some(lists), _) => candidates.extend_from_slice(&lists[first]),
(None, Some(tree)) => tree.overlapping(bounds, small_padding, &mut candidates),
(None, None) => unreachable!("the tree is built whenever the sweep is not"),
}
let bucket = bucket_of[first];
for neighbour in [(bucket + bucket_count - 1) % bucket_count, bucket, (bucket + 1) % bucket_count] {
if let Some(members) = buckets.get(&neighbour) {
candidates.extend(members.iter().copied().filter(|&second| second > first && near(&bounds, &boxes[second])));
}
if bucket_count < 3 {
break;
}
}
candidates.retain(|&second| second > first);
candidates.sort_unstable();
candidates.dedup();
for &second in &candidates {
visit(first, second);
}
}
return;
}
let Some(tree) = tree else { unreachable!("the sweep runs only under the direction tiers") };
for (first, &bounds) in boxes.iter().enumerate() {
candidates.clear();
tree.overlapping(bounds, padding, &mut candidates);
candidates.retain(|&second| second > first);
candidates.sort_unstable();
for &second in &candidates {
visit(first, second);
}
}
}
const DIRECTION_SINE: f64 = 1e-3;
fn swept_pairs(boxes: &[Aabb], padding: f64) -> Option<Vec<Vec<usize>>> {
let reach = 2.0 * padding;
let low = |index: usize, axis: usize| if axis == 0 { boxes[index].minimum.x } else { boxes[index].minimum.y };
let high = |index: usize, axis: usize| if axis == 0 { boxes[index].maximum.x } else { boxes[index].maximum.y };
let mut best: Option<(usize, Vec<usize>, usize)> = None;
for axis in [0, 1] {
let mut order: Vec<usize> = (0..boxes.len()).collect();
order.sort_by(|&a, &b| low(a, axis).total_cmp(&low(b, axis)));
let lows: Vec<f64> = order.iter().map(|&index| low(index, axis)).collect();
let windows: usize = order
.iter()
.enumerate()
.map(|(position, &a)| {
let limit = high(a, axis) + reach;
lows.partition_point(|&value| value <= limit).saturating_sub(position + 1)
})
.sum();
if best.as_ref().is_none_or(|(least, _, _)| windows < *least) {
best = Some((windows, order, axis));
}
}
let (windows, order, axis) = best?;
if windows > 32 * boxes.len() {
return None;
}
let mut lists = vec![Vec::new(); boxes.len()];
for (position, &a) in order.iter().enumerate() {
let limit = high(a, axis) + reach;
for &b in &order[position + 1..] {
if low(b, axis) > limit {
break;
}
let (first, second) = if a < b { (a, b) } else { (b, a) };
if boxes[second].intersects(boxes[first], padding) {
lists[first].push(second);
}
}
}
for list in &mut lists {
list.sort_unstable();
}
Some(lists)
}
fn arrange_sweep_on() -> bool {
switch!("BREP_ARRANGE_SWEEP", true)
}
fn direction_index_on() -> bool {
switch!("BREP_ARRANGE_DIRECTION_INDEX", true)
}
pub fn arrange_segments(
segments: &[Segment2],
tolerance: f64,
) -> Result<Vec<ArrangementRegion>, String> {
arrange_segments_impl(segments, tolerance, true, true)
}
fn arrange_segments_impl(
segments: &[Segment2],
tolerance: f64,
indexed: bool,
indexed_nodes: bool,
) -> Result<Vec<ArrangementRegion>, String> {
let defer_tags = switch!("BREP_ARRANGE_DEFER_TAGS", true);
let end_on_segment = switch!("BREP_ARRANGE_END_ON_SEGMENT", true);
let mut cuts = vec![Vec::<f64>::new(); segments.len()];
let mut intersect_pair = |first: usize, second: usize| {
let Some([first_parameter, second_parameter]) = segment_intersection(
segments[first].a,
segments[first].b,
segments[second].a,
segments[second].b,
tolerance,
) else {
if end_on_segment {
for (end, bar) in [(first, second), (second, first)] {
for (point, parameter) in end_on_segment_parameters(&segments[end], &segments[bar], tolerance) {
let shared = segments.iter().enumerate().any(|(index, other)| {
index != end && (other.a.distance(point) <= tolerance || other.b.distance(point) <= tolerance)
});
if !shared {
cuts[bar].push(parameter);
}
}
}
}
return;
};
let first_tolerance = tolerance_parameter(&segments[first], tolerance);
let second_tolerance = tolerance_parameter(&segments[second], tolerance);
if first_parameter > first_tolerance && first_parameter < 1.0 - first_tolerance {
cuts[first].push(first_parameter);
}
if second_parameter > second_tolerance && second_parameter < 1.0 - second_tolerance {
cuts[second].push(second_parameter);
}
};
if indexed {
visit_segment_pairs(segments, tolerance, &mut intersect_pair);
} else {
for first in 0..segments.len() {
for second in first + 1..segments.len() {
intersect_pair(first, second);
}
}
}
let mut pieces = Vec::new();
for (index, segment) in segments.iter().enumerate() {
let mut parameters = vec![0.0];
cuts[index].sort_by(f64::total_cmp);
parameters.extend(cuts[index].iter().copied());
parameters.push(1.0);
let parameter_tolerance = tolerance_parameter(segment, tolerance);
let mut deduplicated: Vec<f64> = Vec::new();
for parameter in parameters {
if deduplicated
.last()
.is_none_or(|previous| parameter - *previous > parameter_tolerance)
{
deduplicated.push(parameter);
} else if parameter == 1.0 {
*deduplicated.last_mut().unwrap() = 1.0;
}
}
for pair in deduplicated.windows(2) {
let a = interpolate(segment.a, segment.b, pair[0]);
let b = interpolate(segment.a, segment.b, pair[1]);
if a.distance(b) <= tolerance {
continue;
}
pieces.push(PieceInput {
a,
b,
parent: if defer_tags { std::borrow::Cow::Borrowed(segment) } else { std::borrow::Cow::Owned(segment.clone()) },
});
}
}
let mut nodes: Vec<Node> = Vec::new();
let mut lookup = NodeLookup::new(tolerance, indexed_nodes && pieces.len() > 64);
let mut piece_start = Vec::with_capacity(pieces.len());
let mut piece_end = Vec::with_capacity(pieces.len());
for piece in &pieces {
piece_start.push(lookup.find_or_insert(piece.a, &mut nodes));
piece_end.push(lookup.find_or_insert(piece.b, &mut nodes));
}
let mut alive = vec![true; pieces.len()];
loop {
let mut pruned = false;
let mut degree = vec![0usize; nodes.len()];
for index in 0..pieces.len() {
if !alive[index] {
continue;
}
if piece_start[index] == piece_end[index] {
alive[index] = false;
pruned = true;
continue;
}
degree[piece_start[index]] += 1;
degree[piece_end[index]] += 1;
}
for index in 0..pieces.len() {
if alive[index] && (degree[piece_start[index]] == 1 || degree[piece_end[index]] == 1) {
alive[index] = false;
pruned = true;
}
}
if !pruned {
break;
}
}
let half_tail = |half_edge: usize| {
if half_edge % 2 == 0 {
piece_start[half_edge >> 1]
} else {
piece_end[half_edge >> 1]
}
};
let half_head = |half_edge: usize| {
if half_edge % 2 == 0 {
piece_end[half_edge >> 1]
} else {
piece_start[half_edge >> 1]
}
};
for node in &mut nodes {
node.outgoing.clear();
}
for index in 0..pieces.len() {
if !alive[index] {
continue;
}
nodes[piece_start[index]].outgoing.push(index * 2);
nodes[piece_end[index]].outgoing.push(index * 2 + 1);
}
let angles: Vec<f64> = (0..pieces.len() * 2)
.map(|half_edge| {
let tail = nodes[half_tail(half_edge)].point;
let head = nodes[half_head(half_edge)].point;
(head.y - tail.y).atan2(head.x - tail.x)
})
.collect();
let digon_order = digon_order_enabled();
for node in &mut nodes {
node.outgoing.sort_by(|a, b| {
angles[*a].total_cmp(&angles[*b]).then_with(|| {
if !digon_order {
return std::cmp::Ordering::Equal;
}
let rank = |half_edge: usize| {
let piece = (half_edge >> 1) as i64;
if half_tail(half_edge) < half_head(half_edge) {
piece
} else {
-piece
}
};
rank(*a).cmp(&rank(*b))
})
});
}
let mut visited = rustc_hash::FxHashSet::default();
let mut positive = Vec::new();
let mut negative = Vec::new();
for index in 0..pieces.len() {
if !alive[index] {
continue;
}
for start in [index * 2, index * 2 + 1] {
if visited.contains(&start) {
continue;
}
let mut uses = if defer_tags { CycleUses::Deferred(Vec::new()) } else { CycleUses::Eager(Vec::new()) };
let mut node_indices = Vec::new();
let mut area = 0.0;
let mut half_edge = start;
let mut guard = 0;
loop {
visited.insert(half_edge);
uses.push(half_edge, &pieces);
let tail_index = half_tail(half_edge);
let head_index = half_head(half_edge);
let tail = nodes[tail_index].point;
let head = nodes[head_index].point;
area += 0.5 * (tail.x * head.y - head.x * tail.y);
node_indices.push(tail_index);
let reverse = half_edge ^ 1;
let outgoing = &nodes[head_index].outgoing;
let reverse_index = outgoing
.iter()
.position(|candidate| *candidate == reverse)
.ok_or_else(|| "arrangeSegments: reverse half-edge missing".to_string())?;
half_edge = outgoing[(reverse_index + outgoing.len() - 1) % outgoing.len()];
guard += 1;
if guard > pieces.len() * 4 + 8 {
return Err("arrangeSegments: face walk did not terminate".into());
}
if half_edge == start {
break;
}
}
let cycle = Cycle {
uses,
area,
node_indices,
};
if area > tolerance * tolerance {
positive.push(cycle);
} else if area < -tolerance * tolerance {
negative.push(cycle);
}
}
}
positive.sort_by(|a, b| a.area.total_cmp(&b.area));
let mut regions: Vec<(ArrangementRegion, Vec<Vec2>)> = positive
.into_iter()
.map(|cycle| {
let polygon = cycle
.node_indices
.iter()
.map(|index| nodes[*index].point)
.collect();
(
ArrangementRegion {
outer: cycle.uses.into_uses(&pieces),
holes: Vec::new(),
area: cycle.area,
outer_area: cycle.area,
},
polygon,
)
})
.collect();
let hole_boxes = hole_box_reject_enabled();
let boxes: Vec<[f64; 5]> = if hole_boxes {
regions.iter().map(|(_, polygon)| polygon_reject_box(polygon, tolerance)).collect()
} else {
Vec::new()
};
let mut bands: Vec<Option<BoundaryBand>> = (0..regions.len()).map(|_| None).collect();
for cycle in negative {
for (region_index, (region, polygon)) in regions.iter_mut().enumerate() {
let mut inside = false;
let mut on_boundary = false;
for node_index in &cycle.node_indices {
let point = nodes[*node_index].point;
let class = if hole_boxes && outside_reject_box(point, &boxes[region_index]) {
PolygonClass::Outside
} else if hole_boxes {
let band = bands[region_index].get_or_insert_with(|| BoundaryBand::new(polygon, tolerance));
band.classify(point, polygon, tolerance)
} else {
point_in_polygon_class(point, polygon, tolerance)
};
match class {
PolygonClass::Boundary => {
on_boundary = true;
continue;
}
PolygonClass::Inside => {
inside = true;
on_boundary = false;
break;
}
PolygonClass::Outside => {
inside = false;
on_boundary = false;
break;
}
}
}
if on_boundary {
continue;
}
if inside {
region.holes.push(cycle.uses.copy_uses(&pieces));
region.area += cycle.area;
break;
}
}
}
if crate::profile::profile_enabled() {
let holes: usize = regions.iter().flat_map(|(r, _)| r.holes.iter()).map(Vec::len).sum();
let returned: usize = holes + regions.iter().map(|(r, _)| r.outer.len()).sum::<usize>();
let clones = if defer_tags { returned } else { pieces.len() + visited.len() + holes };
crate::imprint::profile_emit(format_args!("arrange.tags segments={} pieces={} walked={} returned={returned} parent_clones={clones} deferred={defer_tags}", segments.len(), pieces.len(), visited.len()));
}
Ok(regions.into_iter().map(|(region, _)| region).collect())
}