Skip to content

Commit f5ecec2

Browse files
committed
[PcbDraw] Changes in the get_board_path
- Made the numpy version and Python version share more code - Use the old mechanism for KiCad 6, the new one fails
1 parent 17acfa0 commit f5ecec2

2 files changed

Lines changed: 183 additions & 167 deletions

File tree

kibot/PcbDraw/np.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Author: Salvador E. Tropea
22
# License: MIT
33
# numpy replacement for PcbDraw
4+
import builtins
45
from operator import itemgetter
56

67
# A value that is not None
@@ -27,3 +28,24 @@ def matmul(A, B):
2728
# Ensure the number of cols in A is the same as the number of rows in B
2829
# assert len(A[0]) == len(B)
2930
return [[sum(a*b for a, b in zip(A_row, B_col)) for B_col in zip(*B)] for A_row in A]
31+
32+
33+
def ones(count, dtype=None):
34+
assert dtype == bool
35+
return [True] * count
36+
37+
38+
def empty(sizes):
39+
return []
40+
41+
42+
def any(data):
43+
return builtins.any(data)
44+
45+
46+
def argmax(data):
47+
try:
48+
i = data.index(True)
49+
except ValueError:
50+
i = 0
51+
return i

kibot/PcbDraw/plot.py

Lines changed: 161 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -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

304238
def 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+
576598
def 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

Comments
 (0)