@@ -273,12 +273,56 @@ VoronoiDiagram2DGenerator<TCoordinate>::ConstructDiagram()
273273 buildEdges.push_back (curr);
274274 EdgeInfo front = curr;
275275 EdgeInfo back = curr;
276- while (!(rawEdges[i].empty ()))
276+ // Assemble raw edges into a connected chain for this Voronoi cell.
277+ // Each iteration pops an edge from the deque and attempts to attach
278+ // it to the front or back of the growing chain. Edges that cannot
279+ // attach are pushed back for retry, because later attachments change
280+ // the chain endpoints and may make previously unattachable edges
281+ // attachable.
282+ //
283+ // A stall counter tracks progress: it resets whenever an edge
284+ // attaches, and terminates the loop when a full pass through the
285+ // deque makes no progress. Without this, certain degenerate seed
286+ // configurations (near-collinear seeds, ITK issue #4386) cause an
287+ // infinite loop because Fortune's algorithm produces near-zero-length
288+ // edges whose endpoints differ by less than floating-point tolerance
289+ // but have different vertex IDs. These degenerate edges cannot
290+ // attach because:
291+ // 1. Their endpoints don't match any chain vertex by ID.
292+ // 2. The chain may already be closed (front[0] == back[1]).
293+ // 3. The boundary-bridging logic doesn't apply when the chain
294+ // endpoints are interior (not on the domain boundary).
295+ // Such edges are safely dropped — they represent floating-point
296+ // artifacts where two boundary intersection points should be
297+ // identical in exact arithmetic.
298+ auto remainingBeforeStall = rawEdges[i].size ();
299+ while (!(rawEdges[i].empty ()) && (remainingBeforeStall != 0 ))
277300 {
301+ --remainingBeforeStall;
278302 curr = rawEdges[i].front ();
279303 rawEdges[i].pop_front ();
304+
305+ // Check if this edge is a degenerate near-zero-length artifact.
306+ // Fortune's algorithm can produce edges whose two endpoints map
307+ // to the same geometric point (within DIFF_TOLERENCE) but have
308+ // different vertex IDs. These carry no geometric information
309+ // and can be safely discarded.
310+ const PointType & edgeStart = m_OutputVD->GetVertex (curr[0 ]);
311+ const PointType & edgeEnd = m_OutputVD->GetVertex (curr[1 ]);
312+ if (!differentPoint (edgeStart, edgeEnd))
313+ {
314+ itkDebugMacro (" Dropping degenerate near-zero-length edge ["
315+ << curr[0 ] << " (" << edgeStart[0 ] << " ," << edgeStart[1 ] << " ) -> " << curr[1 ] << " ("
316+ << edgeEnd[0 ] << " ," << edgeEnd[1 ] << " )]"
317+ << " for cell " << i << " : endpoints within DIFF_TOLERENCE=" << DIFF_TOLERENCE);
318+ // Count as progress — this edge is resolved (discarded).
319+ remainingBeforeStall = rawEdges[i].size ();
320+ continue ;
321+ }
322+
280323 unsigned char frontbnd = Pointonbnd (front[0 ]);
281324 unsigned char backbnd = Pointonbnd (back[1 ]);
325+ bool edgeAttached = true ;
282326 if (curr[0 ] == back[1 ])
283327 {
284328 buildEdges.push_back (curr);
@@ -353,12 +397,30 @@ VoronoiDiagram2DGenerator<TCoordinate>::ConstructDiagram()
353397 else
354398 {
355399 rawEdges[i].push_back (curr);
400+ edgeAttached = false ;
356401 }
357402 }
358403 else
359404 {
360405 rawEdges[i].push_back (curr);
406+ edgeAttached = false ;
361407 }
408+ if (edgeAttached)
409+ {
410+ // Progress was made — chain endpoints changed, so previously
411+ // unattachable edges may now be attachable.
412+ remainingBeforeStall = rawEdges[i].size ();
413+ }
414+ }
415+ // After assembly, all edges for this cell should have been either
416+ // attached to the chain or identified as degenerate artifacts.
417+ // Any remaining edges indicate an unexpected algorithmic failure.
418+ if (!rawEdges[i].empty ())
419+ {
420+ itkExceptionMacro (" VoronoiDiagram2DGenerator::ConstructDiagram: "
421+ << rawEdges[i].size () << " non-degenerate edge(s) could not be "
422+ << " assembled into cell " << i << " boundary chain. "
423+ << " This indicates an unexpected geometric configuration." );
362424 }
363425
364426 curr = buildEdges.front ();
0 commit comments