@@ -584,89 +584,95 @@ def apply_poker_cutins(poly, plate_w, plate_h):
584584 return poly
585585
586586
587- def apply_puzzle_split (outline_poly , all_cutouts , screws , thickness = 1.5 , gap = 10.0 , tolerance = 0.1 ):
587+ def apply_puzzle_split (outline_poly , all_cutouts , screws , keys , U1 , gap = 10.0 , tolerance = 0.15 ):
588588 """
589- Split the plate geometry into two halves using a zigzag joint.
590- Assigns cutouts by centroid to guarantee they are never intersected or broken.
591- Includes a 'tolerance' gap for 3D printing fit.
589+ Split the plate geometry into two halves using a stepped staircase seam
590+ (Chaikin smoothed) that weaves between key columns, like the DeltaSplit75.
591+ Assigns cutouts by centroid to guarantee they are never intersected.
592+ Includes a 'tolerance' gap for fit.
592593 """
593- minx , miny , maxx , maxy = outline_poly .bounds
594- center_x = (minx + maxx ) / 2.0
594+ GRID = U1
595+ SEAM_BIAS = 1.0
596+ BIG_SEAM = 9999.0
595597
596- # Wider corridor search
597- tooth_h = 4.0
598- search_range = 80.0
599- step_size = 0.5
598+ # 1. Find the natural gap (the widest horizontal gap between key centers)
599+ xs = sorted ([k ['cx_u' ] * GRID for k in keys ])
600+ if len (xs ) < 2 :
601+ seam_gap = outline_poly .centroid .x
602+ else :
603+ center_x = outline_poly .centroid .x
604+ gaps = [(xs [i + 1 ] - xs [i ], (xs [i ] + xs [i + 1 ]) / 2 ) for i in range (len (xs ) - 1 )]
605+ # Sort by: 1. Largest gap (rounded, descending), 2. Closest to center (ascending)
606+ gaps .sort (key = lambda g : (- round (g [0 ], 2 ), abs (g [1 ] - center_x )))
607+ seam_gap = gaps [0 ][1 ]
600608
601- samples = []
602- for i in range (int (- search_range / step_size ), int (search_range / step_size )):
603- x = center_x + i * step_size
604- test_box = box (x - (tooth_h / 2 + 1.0 ), miny - 5 , x + (tooth_h / 2 + 1.0 ), maxy + 5 )
605- hits = 0
606- for p in all_cutouts :
607- if test_box .intersects (p ): hits += 1
608- samples .append ((x , hits ))
609+ log .info ("apply_puzzle_split: found seam gap at x=%.2f" , seam_gap )
610+
611+ # 2. Extract key geometries for seam bands calculation
612+ k_tups = [(k ['cx_u' ] * GRID , k ['cy_u' ] * GRID , k ['w' ], k ['h' ]) for k in keys ]
609613
610- zero_ranges = []
611- current_range = []
612- for x , hits in samples :
613- if hits == 0 :
614- current_range .append (x )
614+ rys = sorted ({ round ( k [ 1 ], 0 ) for k in k_tups })
615+ rows = []
616+ for y in rys :
617+ if not rows or y - rows [ - 1 ][ - 1 ] > 9 :
618+ rows .append ([ y ] )
615619 else :
616- if current_range :
617- zero_ranges .append (current_range )
618- current_range = []
619- if current_range : zero_ranges .append (current_range )
620-
621- if zero_ranges :
622- best_range = max (zero_ranges , key = len )
623- mid_x = sum (best_range ) / len (best_range )
624- log .info ("found clear corridor at x=%.2f (width %.1fmm)" , mid_x , len (best_range ) * step_size )
625- else :
626- min_hits = min (s [1 ] for s in samples )
627- best_samples = [s for s in samples if s [1 ] == min_hits ]
628- mid_x = min (best_samples , key = lambda s : abs (s [0 ] - center_x ))[0 ]
629- log .warning ("no clear corridor found; splitting with %d hits at x=%.2f" , min_hits , mid_x )
630-
631- tooth_w = 15.0
632- num_teeth = int ((maxy - miny ) / tooth_w )
633- if num_teeth < 2 : num_teeth = 2
634- step = (maxy - miny ) / num_teeth
635-
636- pts = [(mid_x , miny - 10.0 )]
637- for i in range (num_teeth ):
638- y = miny + i * step
639- pts .append ((mid_x - tooth_h / 2 , y + step * 0.2 ))
640- pts .append ((mid_x + tooth_h / 2 , y + step * 0.4 ))
641- pts .append ((mid_x + tooth_h / 2 , y + step * 0.6 ))
642- pts .append ((mid_x - tooth_h / 2 , y + step * 0.8 ))
643- pts .append ((mid_x , maxy + 10.0 ))
620+ rows [- 1 ].append (y )
621+ mids = sorted (sum (r ) / len (r ) for r in rows )
622+ bounds = [- BIG_SEAM ] + [(mids [i ] + mids [i + 1 ]) / 2 for i in range (len (mids ) - 1 )] + [BIG_SEAM ]
623+ bands = []
624+ for i , ym in enumerate (mids ):
625+ bk = [k for k in k_tups if abs (k [1 ] - ym ) < 9 ]
626+ Lk = [k [0 ] + k [2 ] * GRID / 2 for k in bk if k [0 ] < seam_gap ]
627+ Rk = [k [0 ] - k [2 ] * GRID / 2 for k in bk if k [0 ] >= seam_gap ]
628+ sx = (max (Lk ) + min ((min (Rk ) - max (Lk )) / 2 , SEAM_BIAS )) if (Lk and Rk ) else seam_gap
629+ sx = max (seam_gap - GRID , min (seam_gap + GRID , sx ))
630+ bands .append ((bounds [i ], bounds [i + 1 ], sx ))
631+
632+ # 3. Build the shared staircase centerline
633+ def chaikin (pts , iters ):
634+ for _ in range (int (iters )):
635+ out = [pts [0 ]]
636+ for a , b in zip (pts , pts [1 :]):
637+ out .append ((0.75 * a [0 ] + 0.25 * b [0 ], 0.75 * a [1 ] + 0.25 * b [1 ]))
638+ out .append ((0.25 * a [0 ] + 0.75 * b [0 ], 0.25 * a [1 ] + 0.75 * b [1 ]))
639+ out .append (pts [- 1 ])
640+ pts = out
641+ return pts
642+
643+ # Clamp bounds to plate bounds
644+ minx , miny , maxx , maxy = outline_poly .bounds
645+ ylo , yhi = miny - 30 , maxy + 30
646+ stair = []
647+ for y0 , y1 , x in bands :
648+ stair .append ((x , max (ylo , min (yhi , y0 ))))
649+ stair .append ((x , max (ylo , min (yhi , y1 ))))
650+
651+ stair = chaikin (stair , 5 ) # 5 iterations of smoothing
652+
653+ joint_line = LineString (stair )
654+
655+ # 5. Build left/right bounding polygons
656+ raw_left = [(minx - 100 , ylo )] + stair + [(minx - 100 , yhi )]
657+ poly_left_base = Polygon (raw_left )
644658
645- # Base split polygons
646- poly_left_base = Polygon (pts + [(mid_x - 5000 , maxy + 10 ), (mid_x - 5000 , miny - 10 )])
647- poly_right_base = Polygon (pts + [(mid_x + 5000 , maxy + 10 ), (mid_x + 5000 , miny - 10 )])
659+ raw_right = [(maxx + 100 , ylo )] + stair + [(maxx + 100 , yhi )]
660+ poly_right_base = Polygon (raw_right )
648661
649- # Apply tolerance to the right side by buffering the cutter
650- # This creates a small gap between the teeth
651662 if tolerance > 0 :
652- # We shrink the left part and right part slightly at the joint?
653- # Simpler: offset the zigzag line by tolerance/2 for each side.
654- joint_line = LineString (pts )
655- left_cutter = poly_left_base .difference (joint_line .buffer (tolerance / 2 , join_style = 2 ))
656- right_cutter = poly_right_base .difference (joint_line .buffer (tolerance / 2 , join_style = 2 ))
663+ left_cutter = poly_left_base .difference (joint_line .buffer (tolerance , join_style = 1 ))
664+ right_cutter = poly_right_base .difference (joint_line .buffer (tolerance , join_style = 1 ))
657665 else :
658666 left_cutter = poly_left_base
659667 right_cutter = poly_right_base
660-
668+
661669 def process_half (cutter , raw_base_cutter , x_shift = 0.0 ):
662670 new_outline = outline_poly .intersection (cutter )
663671 if x_shift != 0 :
664672 new_outline = affinity .translate (new_outline , xoff = x_shift )
665673
666674 new_cutouts = []
667675 for p in all_cutouts :
668- # We use the RAW base cutter (no tolerance) for assignment to ensure
669- # we don't lose holes that fall into the tolerance gap
670676 if raw_base_cutter .contains (p .centroid ):
671677 res = p
672678 if x_shift != 0 : res = affinity .translate (res , xoff = x_shift )
@@ -686,6 +692,7 @@ def process_half(cutter, raw_base_cutter, x_shift=0.0):
686692 return (left_out , left_cuts , left_screws ), (right_out , right_cuts , right_screws )
687693
688694
695+
689696def generate_plate (kle_path = None , out_path = None , pcb_path = None ,
690697 switch_type = 1 , stab_type = 0 , kerf = 0.0 , pad = 0.0 ,
691698 screw_diameter = 2.4 , pcb_dx = 0.0 , pcb_dy = 0.0 ,
@@ -913,7 +920,7 @@ def poly_to_segs(p):
913920
914921 if puzzle_split :
915922 log .info ("applying puzzle split to geometry" )
916- (l_out , l_cuts , l_screws ), (r_out , r_cuts , r_screws ) = apply_puzzle_split (outline_poly , all_cutouts , screws , gap = 10.0 )
923+ (l_out , l_cuts , l_screws ), (r_out , r_cuts , r_screws ) = apply_puzzle_split (outline_poly , all_cutouts , screws , keys , U1 , gap = 10.0 )
917924
918925 # Re-combine for emission
919926 outline_poly = MultiPolygon ([l_out , r_out ]) if isinstance (l_out , Polygon ) and isinstance (r_out , Polygon ) else unary_union ([l_out , r_out ])
0 commit comments