@@ -138,68 +138,74 @@ def matrix(data: List[List[Numeric]]) -> Matrix:
138138 return np .array (data , dtype = np .float32 )
139139
140140
141- if not WITH_NUMPY :
142- # Pure Python implementation, slightly slower (i.e. 12.5 vs 11 s or 6.8 vs 5.2 s)
143- class PointIndex :
144- """Spatial index for fast endpoint matching during contour building."""
145-
146- def __init__ (self , elements : List [SvgPathItem ]) -> None :
147- self ._elements = elements
148- n = len (elements )
149- self ._active = [True ] * n
150- self ._starts = np .array ([(e .start [0 ], e .start [1 ]) for e in elements ]) if n > 0 else []
151- self ._ends = np .array ([(e .end [0 ], e .end [1 ]) for e in elements ]) if n > 0 else []
152- self ._start_index : Dict [Point , Set [int ]] = defaultdict (set )
153- self ._end_index : Dict [Point , Set [int ]] = defaultdict (set )
154- for i , e in enumerate (elements ):
155- self ._start_index [e .start ].add (i )
156- self ._end_index [e .end ].add (i )
157-
158- def has_active (self ) -> bool :
159- return any (self ._active ) # bool(np.any(self._active))
160-
161- def pop_first_active (self ) -> SvgPathItem :
162- """Remove and return the first active element (seed for new contour)."""
163- try :
164- i = self ._active .index (True )
165- except ValueError :
166- i = 0
167- self ._mark_used (i )
168- return self ._elements [i ]
169-
170- def find_by_end (self , ref : Point ) -> Optional [SvgPathItem ]:
171- return self ._take (ref , self ._ends , self ._end_index , flip = False )
172-
173- def find_by_start (self , ref : Point ) -> Optional [SvgPathItem ]:
174- return self ._take (ref , self ._starts , self ._start_index , flip = False )
175-
176- def find_by_start_flipped (self , ref : Point ) -> Optional [SvgPathItem ]:
177- return self ._take (ref , self ._starts , self ._start_index , flip = True )
178-
179- def find_by_end_flipped (self , ref : Point ) -> Optional [SvgPathItem ]:
180- return self ._take (ref , self ._ends , self ._end_index , flip = True )
181-
182- def _take (self , ref : Point , points : np .ndarray ,
183- index : Dict [Point , Set [int ]], flip : bool ) -> Optional [SvgPathItem ]:
184- """Find an active element matching ref, mark it used, optionally flip."""
185- i = self ._find (ref , points , index )
186- if i is None :
141+ # Pure Python implementation is slightly slower (i.e. 12.5 vs 11 s or 6.8 vs 5.2 s)
142+ class PointIndex :
143+ """Spatial index for fast endpoint matching during contour building."""
144+
145+ def __init__ (self , elements : List [SvgPathItem ]) -> None :
146+ self ._elements = elements
147+ n = len (elements )
148+ self ._active = np .ones (n , dtype = bool )
149+ self ._starts = np .array ([(e .start [0 ], e .start [1 ]) for e in elements ]) if n > 0 else np .empty ((0 , 2 ))
150+ self ._ends = np .array ([(e .end [0 ], e .end [1 ]) for e in elements ]) if n > 0 else np .empty ((0 , 2 ))
151+ self ._start_index : Dict [Point , Set [int ]] = defaultdict (set )
152+ self ._end_index : Dict [Point , Set [int ]] = defaultdict (set )
153+ for i , e in enumerate (elements ):
154+ self ._start_index [e .start ].add (i )
155+ self ._end_index [e .end ].add (i )
156+
157+ def has_active (self ) -> bool :
158+ return bool (np .any (self ._active ))
159+
160+ def pop_first_active (self ) -> SvgPathItem :
161+ """Remove and return the first active element (seed for new contour)."""
162+ i = int (np .argmax (self ._active ))
163+ self ._mark_used (i )
164+ return self ._elements [i ]
165+
166+ def find_by_end (self , ref : Point ) -> Optional [SvgPathItem ]:
167+ return self ._take (ref , self ._ends , self ._end_index , flip = False )
168+
169+ def find_by_start (self , ref : Point ) -> Optional [SvgPathItem ]:
170+ return self ._take (ref , self ._starts , self ._start_index , flip = False )
171+
172+ def find_by_start_flipped (self , ref : Point ) -> Optional [SvgPathItem ]:
173+ return self ._take (ref , self ._starts , self ._start_index , flip = True )
174+
175+ def find_by_end_flipped (self , ref : Point ) -> Optional [SvgPathItem ]:
176+ return self ._take (ref , self ._ends , self ._end_index , flip = True )
177+
178+ def _take (self , ref : Point , points : np .ndarray ,
179+ index : Dict [Point , Set [int ]], flip : bool ) -> Optional [SvgPathItem ]:
180+ """Find an active element matching ref, mark it used, optionally flip."""
181+ i = self ._find (ref , points , index )
182+ if i is None :
183+ return None
184+ self ._mark_used (i )
185+ if flip :
186+ self ._elements [i ].flip ()
187+ return self ._elements [i ]
188+
189+ def _find (self , ref : Point , points : np .ndarray ,
190+ index : Dict [Point , Set [int ]]) -> Optional [int ]:
191+ # Fast path: exact dict lookup
192+ candidates = index .get (ref )
193+ if candidates :
194+ for idx in candidates :
195+ if self ._active [idx ]:
196+ return idx
197+
198+ # Slow path: Standard Python distance on active elements
199+ if WITH_NUMPY :
200+ active_idx = np .where (self ._active )[0 ]
201+ if len (active_idx ) == 0 :
187202 return None
188- self ._mark_used (i )
189- if flip :
190- self ._elements [i ].flip ()
191- return self ._elements [i ]
192-
193- def _find (self , ref : Point , points : np .ndarray ,
194- index : Dict [Point , Set [int ]]) -> Optional [int ]:
195- # Fast path: exact dict lookup
196- candidates = index .get (ref )
197- if candidates :
198- for idx in candidates :
199- if self ._active [idx ]:
200- return idx
201-
202- # Slow path: Standard Python distance on active elements
203+ diffs = points [active_idx ] - np .array (ref )
204+ sq_dists = diffs [:, 0 ]** 2 + diffs [:, 1 ]** 2
205+ best = np .argmin (sq_dists )
206+ if sq_dists [best ] < 0.0001 : # 0.01^2, matches SvgPathItem.is_same
207+ return int (active_idx [best ])
208+ else :
203209 best_idx = None
204210 min_sq_dist = float ('inf' )
205211 ref_x , ref_y = ref [0 ], ref [1 ]
@@ -221,84 +227,12 @@ def _find(self, ref: Point, points: np.ndarray,
221227 if best_idx is not None and min_sq_dist < 0.0001 : # 0.01^2
222228 return best_idx
223229
224- return None
225-
226- def _mark_used (self , i : int ) -> None :
227- self ._active [i ] = False
228- self ._start_index [self ._elements [i ].start ].discard (i )
229- self ._end_index [self ._elements [i ].end ].discard (i )
230- else :
231- # Original implementation
232- class PointIndex :
233- """Spatial index for fast endpoint matching during contour building."""
234-
235- def __init__ (self , elements : List [SvgPathItem ]) -> None :
236- self ._elements = elements
237- n = len (elements )
238- self ._active = np .ones (n , dtype = bool )
239- self ._starts = np .array ([(e .start [0 ], e .start [1 ]) for e in elements ]) if n > 0 else np .empty ((0 , 2 ))
240- self ._ends = np .array ([(e .end [0 ], e .end [1 ]) for e in elements ]) if n > 0 else np .empty ((0 , 2 ))
241- self ._start_index : Dict [Point , Set [int ]] = defaultdict (set )
242- self ._end_index : Dict [Point , Set [int ]] = defaultdict (set )
243- for i , e in enumerate (elements ):
244- self ._start_index [e .start ].add (i )
245- self ._end_index [e .end ].add (i )
246-
247- def has_active (self ) -> bool :
248- return bool (np .any (self ._active ))
249-
250- def pop_first_active (self ) -> SvgPathItem :
251- """Remove and return the first active element (seed for new contour)."""
252- i = int (np .argmax (self ._active ))
253- self ._mark_used (i )
254- return self ._elements [i ]
255-
256- def find_by_end (self , ref : Point ) -> Optional [SvgPathItem ]:
257- return self ._take (ref , self ._ends , self ._end_index , flip = False )
258-
259- def find_by_start (self , ref : Point ) -> Optional [SvgPathItem ]:
260- return self ._take (ref , self ._starts , self ._start_index , flip = False )
261-
262- def find_by_start_flipped (self , ref : Point ) -> Optional [SvgPathItem ]:
263- return self ._take (ref , self ._starts , self ._start_index , flip = True )
264-
265- def find_by_end_flipped (self , ref : Point ) -> Optional [SvgPathItem ]:
266- return self ._take (ref , self ._ends , self ._end_index , flip = True )
267-
268- def _take (self , ref : Point , points : np .ndarray ,
269- index : Dict [Point , Set [int ]], flip : bool ) -> Optional [SvgPathItem ]:
270- """Find an active element matching ref, mark it used, optionally flip."""
271- i = self ._find (ref , points , index )
272- if i is None :
273- return None
274- self ._mark_used (i )
275- if flip :
276- self ._elements [i ].flip ()
277- return self ._elements [i ]
278-
279- def _find (self , ref : Point , points : np .ndarray ,
280- index : Dict [Point , Set [int ]]) -> Optional [int ]:
281- # Fast path: exact dict lookup
282- candidates = index .get (ref )
283- if candidates :
284- for idx in candidates :
285- if self ._active [idx ]:
286- return idx
287- # Slow path: vectorized numpy distance on active elements
288- active_idx = np .where (self ._active )[0 ]
289- if len (active_idx ) == 0 :
290- return None
291- diffs = points [active_idx ] - np .array (ref )
292- sq_dists = diffs [:, 0 ]** 2 + diffs [:, 1 ]** 2
293- best = np .argmin (sq_dists )
294- if sq_dists [best ] < 0.0001 : # 0.01^2, matches SvgPathItem.is_same
295- return int (active_idx [best ])
296- return None
230+ return None
297231
298- def _mark_used (self , i : int ) -> None :
299- self ._active [i ] = False
300- self ._start_index [self ._elements [i ].start ].discard (i )
301- self ._end_index [self ._elements [i ].end ].discard (i )
232+ def _mark_used (self , i : int ) -> None :
233+ self ._active [i ] = False
234+ self ._start_index [self ._elements [i ].start ].discard (i )
235+ self ._end_index [self ._elements [i ].end ].discard (i )
302236
303237
304238def extract_arg (args : List [Any ], index : int , default : Any = None ) -> Any :
@@ -573,6 +507,94 @@ def empty_svg(**attrs: str) -> etree.ElementTree:
573507 root .attrib [key ] = value
574508 return document
575509
510+ def get_best_path_new (elements , path ):
511+ index = PointIndex (elements )
512+ while index .has_active ():
513+ outline = [index .pop_first_active ()]
514+ size = 0
515+ while size != len (outline ) and index .has_active ():
516+ size = len (outline )
517+
518+ e = index .find_by_end (outline [0 ].start )
519+ if e is not None :
520+ outline .insert (0 , e )
521+ continue
522+
523+ e = index .find_by_start_flipped (outline [0 ].start )
524+ if e is not None :
525+ outline .insert (0 , e )
526+ continue
527+
528+ e = index .find_by_start (outline [- 1 ].end )
529+ if e is not None :
530+ outline .insert (0 , e )
531+ continue
532+
533+ e = index .find_by_end_flipped (outline [- 1 ].end )
534+ if e is not None :
535+ outline .insert (0 , e )
536+ continue
537+
538+ for i , x in enumerate (outline ):
539+ path += x .format (first = (i == 0 ))
540+
541+ return path
542+
543+ def pseudo_distance (a : Point , b : Point ) -> Numeric :
544+ a0 = a [0 ] - b [0 ]
545+ a1 = a [1 ] - b [1 ]
546+ return a0 * a0 + a1 * a1
547+
548+ def get_closest (reference : Point , elems : List [Point ]) -> int :
549+ try :
550+ return elems .index (reference )
551+ except ValueError :
552+ return int (np .argmin ([pseudo_distance (reference , x ) for x in elems ]))
553+
554+ def get_best_path (elements , path ):
555+ while len (elements ) > 0 :
556+ # Initiate seed for the outline
557+ outline = [elements [0 ]]
558+ elements = elements [1 :]
559+ size = 0
560+ # Append new segments to the ends of outline until there is none to append.
561+ while size != len (outline ) and len (elements ) > 0 :
562+ size = len (outline )
563+
564+ i = get_closest (outline [0 ].start , [x .end for x in elements ])
565+ if SvgPathItem .is_same (outline [0 ].start , elements [i ].end ):
566+ outline .insert (0 , elements [i ])
567+ del elements [i ]
568+ continue
569+
570+ i = get_closest (outline [0 ].start , [x .start for x in elements ])
571+ if SvgPathItem .is_same (outline [0 ].start , elements [i ].start ):
572+ e = elements [i ]
573+ e .flip ()
574+ outline .insert (0 , e )
575+ del elements [i ]
576+ continue
577+
578+ i = get_closest (outline [- 1 ].end , [x .start for x in elements ])
579+ if SvgPathItem .is_same (outline [- 1 ].end , elements [i ].start ):
580+ outline .insert (0 , elements [i ])
581+ del elements [i ]
582+ continue
583+
584+ i = get_closest (outline [- 1 ].end , [x .end for x in elements ])
585+ if SvgPathItem .is_same (outline [- 1 ].end , elements [i ].end ):
586+ e = elements [i ]
587+ e .flip ()
588+ outline .insert (0 , e )
589+ del elements [i ]
590+ continue
591+ # ...then, append it to path.
592+ first = True
593+ for x in outline :
594+ path += x .format (first )
595+ first = False
596+ return path
597+
576598def get_board_polygon (svg_elements : etree .Element ) -> etree .Element :
577599 """
578600 Try to connect independents segments on Edge.Cuts and form a polygon
@@ -605,35 +627,7 @@ def get_board_polygon(svg_elements: etree.Element) -> etree.Element:
605627 s = " M {0} {1} m-{2} 0 a {2} {2} 0 1 0 {3} 0 a {2} {2} 0 1 0 -{3} 0 " .format (
606628 att ["cx" ], att ["cy" ], att ["r" ], 2 * float (att ["r" ]))
607629 path += s
608- index = PointIndex (elements )
609- while index .has_active ():
610- outline = [index .pop_first_active ()]
611- size = 0
612- while size != len (outline ) and index .has_active ():
613- size = len (outline )
614-
615- e = index .find_by_end (outline [0 ].start )
616- if e is not None :
617- outline .insert (0 , e )
618- continue
619-
620- e = index .find_by_start_flipped (outline [0 ].start )
621- if e is not None :
622- outline .insert (0 , e )
623- continue
624-
625- e = index .find_by_start (outline [- 1 ].end )
626- if e is not None :
627- outline .insert (0 , e )
628- continue
629-
630- e = index .find_by_end_flipped (outline [- 1 ].end )
631- if e is not None :
632- outline .insert (0 , e )
633- continue
634-
635- for i , x in enumerate (outline ):
636- path += x .format (first = (i == 0 ))
630+ path = get_best_path_new (elements , path ) if GS .ki7 else get_best_path (elements , path )
637631 e = etree .Element ("path" , d = path , style = "fill-rule: evenodd;" )
638632 return e
639633
0 commit comments