44
55import logging
66import math
7+ from itertools import pairwise
78
89import gmsh
910import numpy as np
1011
1112logger = logging .getLogger (__name__ )
1213
14+ _CORNER_TURN_THRESHOLD_DEG = 45.0
15+
1316# ---------------------------------------------------------------------------
1417# Minimalistic meshwell-style entity + boolean pipeline
1518# ---------------------------------------------------------------------------
@@ -304,6 +307,7 @@ def _create_wire_loop(
304307 meshseed : float = 0 ,
305308 loop_mode : str = "line" ,
306309 point_merge_tol : float = 0.0 ,
310+ corner_turn_threshold_deg : float = _CORNER_TURN_THRESHOLD_DEG ,
307311) -> int | None :
308312 """Create a closed curve loop from polygon vertices.
309313
@@ -322,25 +326,105 @@ def _create_wire_loop(
322326 if math .hypot (x - px , y - py ) > tol :
323327 cleaned .append ((x , y ))
324328
325- if len (cleaned ) >= 2 :
326- if math .hypot (cleaned [0 ][0 ] - cleaned [- 1 ][0 ], cleaned [0 ][1 ] - cleaned [- 1 ][1 ]) <= tol :
327- cleaned .pop ()
329+ if len (cleaned ) >= 2 and (
330+ math .hypot (cleaned [0 ][0 ] - cleaned [- 1 ][0 ], cleaned [0 ][1 ] - cleaned [- 1 ][1 ])
331+ <= tol
332+ ):
333+ cleaned .pop ()
328334
329335 if len (cleaned ) < 3 :
330336 return None
331337
332338 verts = [kernel .addPoint (x , y , z , meshseed , - 1 ) for x , y in cleaned ]
333339
340+ turn_threshold_deg = max (0.0 , min (float (corner_turn_threshold_deg ), 180.0 ))
341+
342+ def _turn_angle_deg (
343+ prev_pt : tuple [float , float ],
344+ curr_pt : tuple [float , float ],
345+ next_pt : tuple [float , float ],
346+ ) -> float | None :
347+ ax = curr_pt [0 ] - prev_pt [0 ]
348+ ay = curr_pt [1 ] - prev_pt [1 ]
349+ bx = next_pt [0 ] - curr_pt [0 ]
350+ by = next_pt [1 ] - curr_pt [1 ]
351+ norm_a = math .hypot (ax , ay )
352+ norm_b = math .hypot (bx , by )
353+ if norm_a <= tol or norm_b <= tol :
354+ return None
355+ cos_theta = (ax * bx + ay * by ) / (norm_a * norm_b )
356+ cos_theta = max (- 1.0 , min (1.0 , cos_theta ))
357+ return math .degrees (math .acos (cos_theta ))
358+
359+ def _corner_indices (points : list [tuple [float , float ]]) -> list [int ]:
360+ if len (points ) < 3 :
361+ return []
362+ corners : list [int ] = []
363+ for idx in range (len (points )):
364+ prev_pt = points [(idx - 1 ) % len (points )]
365+ curr_pt = points [idx ]
366+ next_pt = points [(idx + 1 ) % len (points )]
367+ turn_angle = _turn_angle_deg (prev_pt , curr_pt , next_pt )
368+ if turn_angle is not None and turn_angle >= turn_threshold_deg :
369+ corners .append (idx )
370+ return corners
371+
372+ def _corner_to_corner_segments (
373+ num_points : int ,
374+ corners : list [int ],
375+ ) -> list [list [int ]]:
376+ unique = sorted (set (corners ))
377+ if num_points < 3 or len (unique ) < 2 :
378+ return []
379+
380+ segments : list [list [int ]] = []
381+ for idx , start in enumerate (unique ):
382+ end = unique [(idx + 1 ) % len (unique )]
383+ segment = [start ]
384+ cursor = (start + 1 ) % num_points
385+ while cursor != end :
386+ segment .append (cursor )
387+ cursor = (cursor + 1 ) % num_points
388+ segment .append (end )
389+ segments .append (segment )
390+ return segments
391+
334392 if loop_mode in {"spline" , "bspline" } and len (verts ) >= 4 :
393+ corners = _corner_indices (cleaned )
394+ if len (corners ) >= 2 :
395+ try :
396+ segmented_curves : list [int ] = []
397+ for segment in _corner_to_corner_segments (len (verts ), corners ):
398+ segment_pts = [verts [i ] for i in segment ]
399+ if len (segment_pts ) >= 4 :
400+ if loop_mode == "bspline" :
401+ curve = kernel .addBSpline (segment_pts , - 1 )
402+ else :
403+ curve = kernel .addSpline (segment_pts , - 1 )
404+ segmented_curves .append (curve )
405+ else :
406+ for p1 , p2 in pairwise (segment_pts ):
407+ segmented_curves .append (kernel .addLine (p1 , p2 , - 1 ))
408+
409+ if len (segmented_curves ) >= 3 :
410+ return kernel .addCurveLoop (segmented_curves , tag = - 1 )
411+ except Exception :
412+ logger .debug (
413+ "Failed segmented %s loop, falling back to full-curve loop" ,
414+ loop_mode ,
415+ )
416+
335417 try :
336- curve_pts = verts + [ verts [0 ]]
418+ curve_pts = [ * verts , verts [0 ]]
337419 if loop_mode == "bspline" :
338420 curve = kernel .addBSpline (curve_pts , - 1 )
339421 else :
340422 curve = kernel .addSpline (curve_pts , - 1 )
341423 return kernel .addCurveLoop ([curve ], tag = - 1 )
342424 except Exception :
343- logger .debug ("Failed to create %s loop, falling back to line loop" , loop_mode )
425+ logger .debug (
426+ "Failed to create %s loop, falling back to line loop" , loop_mode
427+ )
344428
345429 lines = []
346430 for v in range (len (verts )):
@@ -364,6 +448,7 @@ def create_polygon_surface(
364448 loop_mode : str = "line" ,
365449 fit_tolerance_um : float = 0.0 ,
366450 min_points_for_curve_fit : int = 8 ,
451+ corner_turn_threshold_deg : float = _CORNER_TURN_THRESHOLD_DEG ,
367452) -> int | None :
368453 """Create a planar surface from polygon vertices at z height.
369454
@@ -382,6 +467,8 @@ def create_polygon_surface(
382467 fit_tolerance_um: Point merge tolerance before loop creation
383468 min_points_for_curve_fit: Minimum contour points required to attempt
384469 spline/bspline loop creation
470+ corner_turn_threshold_deg: Turn-angle threshold used to detect corners
471+ when segmenting spline/bspline loops
385472
386473 Returns:
387474 Surface tag, or None if polygon is invalid
@@ -401,6 +488,7 @@ def create_polygon_surface(
401488 meshseed ,
402489 loop_mode = effective_mode ,
403490 point_merge_tol = fit_tolerance_um ,
491+ corner_turn_threshold_deg = corner_turn_threshold_deg ,
404492 )
405493 if exterior_loop is None and effective_mode != "line" :
406494 exterior_loop = _create_wire_loop (
@@ -411,6 +499,7 @@ def create_polygon_surface(
411499 meshseed ,
412500 loop_mode = "line" ,
413501 point_merge_tol = fit_tolerance_um ,
502+ corner_turn_threshold_deg = corner_turn_threshold_deg ,
414503 )
415504 if exterior_loop is None :
416505 return None
@@ -432,6 +521,7 @@ def create_polygon_surface(
432521 meshseed ,
433522 loop_mode = "line" ,
434523 point_merge_tol = fit_tolerance_um ,
524+ corner_turn_threshold_deg = corner_turn_threshold_deg ,
435525 )
436526 if hloop is not None :
437527 hsurf = kernel .addPlaneSurface ([hloop ], tag = - 1 )
0 commit comments