Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions iOverlay/src/bind/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ pub(crate) struct ContourIndex {
impl ContourIndex {
pub(crate) const EMPTY: ContourIndex = ContourIndex { data: usize::MAX };

#[inline]
pub(crate) fn is_empty(&self) -> bool {
self.data == usize::MAX
}

#[inline]
pub(crate) fn is_hole(&self) -> bool {
self.data & 1 == 1
Expand Down
58 changes: 54 additions & 4 deletions iOverlay/src/bind/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ impl ShapeBinder {
}

let target_id = scan_list.first_less(anchor.v_segment.a.x, ContourIndex::EMPTY, anchor.v_segment);
let child_index = anchor.contour_index.index();

if target_id.is_empty() {
// No containing contour found to the left of this hole's anchor.
// This happens for a degenerate hole whose leftmost edge is
// coincident with a shell edge (the strict `first_less` cannot
// see an equal-x segment), so it has no real parent shell. Mark
// it orphaned and skip it during binding instead of indexing
// `parent_for_child`/`children_count_for_parent` with the EMPTY
// sentinel (`usize::MAX`), which panicked.
parent_for_child[child_index] = usize::MAX;
continue;
}

let parent_index = if target_id.is_hole() {
// index is a hole index
// at this moment this hole parent is known
Expand All @@ -102,10 +116,10 @@ impl ShapeBinder {
target_id.index()
};

let child_index = anchor.contour_index.index();

parent_for_child[child_index] = parent_index;
children_count_for_parent[parent_index] += 1;
if parent_index != usize::MAX {
children_count_for_parent[parent_index] += 1;
}
}

BindSolution {
Expand Down Expand Up @@ -191,6 +205,11 @@ impl<I: IntNumber + Expiration + SortKey> JoinHoles<I> for Vec<IntShape<I>> {

for (hole_index, hole) in holes.into_iter().enumerate() {
let shape_index = solution.parent_for_child[hole_index];
if shape_index == usize::MAX {
// Orphan hole with no containing shell (see ShapeBinder::bind);
// dropping it keeps a degenerate sliver from corrupting output.
continue;
}
self[shape_index].push(hole);
}
}
Expand Down Expand Up @@ -278,7 +297,8 @@ impl<I: IntNumber + SortKey> SortByAngle for [IdSegment<I>] {

#[cfg(test)]
mod tests {
use crate::bind::solver::JoinHoles;
use crate::bind::segment::{ContourIndex, IdSegment};
use crate::bind::solver::{JoinHoles, ShapeBinder};
use crate::geom::v_segment::VSegment;
use alloc::vec;
use core::cmp::Ordering;
Expand Down Expand Up @@ -340,4 +360,34 @@ mod tests {
assert_eq!(short_result, long_result);
assert_eq!(Ordering::Less, long_result);
}

#[test]
fn orphan_hole_without_parent_shell_is_dropped_not_panicking() {
// Regression: a hole whose left-bottom anchor has no shell segment
// strictly to its left makes `first_less` return `ContourIndex::EMPTY`.
// Before the fix, binding indexed `parent_for_child[EMPTY.index()]`
// (== usize::MAX >> 1) and panicked with "index out of bounds".
let anchor = IdSegment::with_segment(
ContourIndex::new_hole(0),
VSegment {
a: IntPoint::new(0, 5),
b: IntPoint::new(1, 6),
},
);
// The only shell edge lies entirely to the right of the hole, so the
// sweep never inserts it before the anchor: the hole has no parent shell.
let shell_edge = IdSegment::with_segment(
ContourIndex::new_shape(0),
VSegment {
a: IntPoint::new(10, 0),
b: IntPoint::new(20, 0),
},
);

let solution = ShapeBinder::bind::<i32>(2, vec![anchor], vec![shell_edge]);

// The orphan hole is marked (usize::MAX) and skipped by the join loop,
// instead of crashing.
assert_eq!(solution.parent_for_child[0], usize::MAX);
}
}