fix(bind): don't panic on an orphan hole with no containing shell#77
fix(bind): don't panic on an orphan hole with no containing shell#77Filyus wants to merge 2 commits into
Conversation
ShapeBinder::private_solve resolves each hole's parent with `scan_list.first_less(..., ContourIndex::EMPTY, ...)`. When a hole's left-bottom anchor has no shell segment strictly to its left -- e.g. a degenerate hole whose leftmost edge is coincident with a shell edge, so the strict `first_less` cannot see the equal-x segment -- it returns the EMPTY sentinel (`data: usize::MAX`). The code then indexes `parent_for_child[target_id.index()]` and `children_count_for_parent[parent_index]` with `index() == usize::MAX >> 1`, panicking with "index out of bounds: the index is 9223372036854775807". Detect the EMPTY sentinel and treat the hole as orphaned: skip it during binding (and in the join push loop) instead of indexing with the sentinel. Such a hole has no valid parent shell, so it is dropped from the output. Hit with f64 boolean ops over geometry with many near-coincident edges at high coordinate magnitudes (a screen-space visibility resolver).
|
Clarification on the open question (orphan-hole handling): this PR drops the orphan. The correct non-dropping alternative is a point-in-polygon fallback when |
|
Hi Filyus, Thanks for the PR. At first glance, this looks like it may be a false positive, possibly found by an LLM, rather than a real issue in valid input data. Could you provide a concrete example where this panic happens? In a correct topology, this situation should not possible. This solver is designed to work only with already correct topology. You are not the first person to point out this area. Please also take a look at #70 and #72, where similar concerns were discussed. Regarding the proposed fix: dropping an orphan hole may hide invalid topology rather than solve the actual issue. Before changing this behavior, I would like to see a real reproducible case and understand whether the input topology is valid. Thanks. |
|
Yes, the fix was made by LLM, but it seems correct. The geometry's topology after projection and booleans isn't very good, but this solution worked. My scene and algorithm are quite big and complex, but I can provide an example if needed. |
|
Thanks for the clarification. A reproducible example would be very helpful here. If the problem a real issue, I think it's better to find its root cause. |
Problem
ShapeBinder::private_solve(src/bind/solver.rs) resolves each hole's parent contour with:When a hole's left-bottom anchor has no shell segment strictly to its left,
first_lessreturns theContourIndex::EMPTYsentinel (data: usize::MAX). The code then unconditionally does:Because
EMPTY.is_hole()isusize::MAX & 1 == 1(true) andEMPTY.index()isusize::MAX >> 1, this indexesparent_for_child[9223372036854775807]and panics:This happens for a degenerate hole whose leftmost edge is coincident with a shell edge — the strict
first_lesscannot see the equal-x segment, so the hole appears to have no containing shell. It is reliably triggered byf64boolean ops over geometry with many near-coincident edges at high coordinate magnitudes (here: a screen-space visibility resolver subtracting many occluder polygons whose borders coincide with the subject's borders).Fix
Detect the
EMPTYsentinel and treat the hole as orphaned: mark it (usize::MAX) and skip it during binding and in the join push loop, instead of indexingparent_for_child/children_count_for_parentwith the sentinel. An orphan hole has no valid parent shell, so it is dropped from the output (it is a degenerate sub-pixel artifact in practice).Adds
ContourIndex::is_empty()and guards both the producer (private_solve) and the consumer (scan_join's push loop).Test
Adds
bind::solver::tests::orphan_hole_without_parent_shell_is_dropped_not_panicking: it feedsShapeBinder::binda hole anchor with the only shell segment entirely to its right (so the sweep finds no parent). With the guard disabled it reproduces the exact panic (index out of bounds: the len is 1 but the index is 9223372036854775807); with the fix it asserts the orphan hole is markedusize::MAX(dropped). Full suite: 348 passed.Notes