From 778e81974580d910eac5e3f977acf79744d3e085 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 29 Apr 2023 01:00:45 +0200 Subject: Freeze apertures and aperture macros, make gerbonara faster --- gerbonara/cad/kicad/footprints.py | 74 ++++++++++++++++++++++----------------- gerbonara/cad/primitives.py | 39 ++++++++++++++++----- 2 files changed, 72 insertions(+), 41 deletions(-) (limited to 'gerbonara/cad') diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py index 428d5ea..d7ccc9f 100644 --- a/gerbonara/cad/kicad/footprints.py +++ b/gerbonara/cad/kicad/footprints.py @@ -55,7 +55,7 @@ class Text: effects: TextEffect = field(default_factory=TextEffect) tstamp: Timestamp = None - def render(self, variables={}): + def render(self, variables={}, cache=None): if self.hide: # why return @@ -76,7 +76,7 @@ class TextBox: stroke: Stroke = field(default_factory=Stroke) render_cache: RenderCache = None - def render(self, variables={}): + def render(self, variables={}, cache=None): yield from gr.TextBox.render(self, variables=variables) @@ -90,7 +90,7 @@ class Line: locked: Flag() = False tstamp: Timestamp = None - def render(self, variables=None): + def render(self, variables=None, cache=None): dasher = Dasher(self) dasher.move(self.start.x, self.start.y) dasher.line(self.end.x, self.end.y) @@ -110,7 +110,7 @@ class Rectangle: locked: Flag() = False tstamp: Timestamp = None - def render(self, variables=None): + def render(self, variables=None, cache=None): x1, y1 = self.start.x, self.start.y x2, y2 = self.end.x, self.end.y x1, x2 = min(x1, x2), max(x1, x2) @@ -143,7 +143,7 @@ class Circle: locked: Flag() = False tstamp: Timestamp = None - def render(self, variables=None): + def render(self, variables=None, cache=None): x, y = self.center.x, self.center.y r = math.dist((x, y), (self.end.x, self.end.y)) # insane @@ -178,7 +178,7 @@ class Arc: tstamp: Timestamp = None - def render(self, variables=None): + def render(self, variables=None, cache=None): mx, my = self.mid.x, self.mid.y x1, y1 = self.start.x, self.start.y x2, y2 = self.end.x, self.end.y @@ -230,7 +230,7 @@ class Polygon: locked: Flag() = False tstamp: Timestamp = None - def render(self, variables=None): + def render(self, variables=None, cache=None): if len(self.pts.xy) < 2: return @@ -257,7 +257,7 @@ class Curve: locked: Flag() = False tstamp: Timestamp = None - def render(self, variables=None): + def render(self, variables=None, cache=None): raise NotImplementedError('Bezier rendering is not yet supported. Please raise an issue and provide an example file.') @@ -297,7 +297,7 @@ class Dimension: format: DimensionFormat = field(default_factory=DimensionFormat) style: DimensionStyle = field(default_factory=DimensionStyle) - def render(self, variables=None): + def render(self, variables=None, cache=None): raise NotImplementedError() @@ -383,7 +383,7 @@ class Pad: options: OmitDefault(CustomPadOptions) = None primitives: OmitDefault(CustomPadPrimitives) = None - def render(self, variables=None, margin=None): + def render(self, variables=None, margin=None, cache=None): #if self.type in (Atom.connect, Atom.np_thru_hole): # return if self.drill and self.drill.offset: @@ -391,7 +391,17 @@ class Pad: else: ox, oy = 0, 0 - yield go.Flash(self.at.x+ox, self.at.y+oy, self.aperture(margin), unit=MM) + cache_key = id(self), margin + if cache and cache_key in cache: + aperture = cache[cache_key] + + elif cache is not None: + aperture = cache[cache_key] = self.aperture(margin) + + else: + aperture = self.aperture(margin) + + yield go.Flash(self.at.x+ox, self.at.y+oy, aperture, unit=MM) def aperture(self, margin=None): rotation = -math.radians(self.at.rotation) @@ -403,10 +413,10 @@ class Pad: elif self.shape == Atom.rect: if margin > 0: return ap.ApertureMacroInstance(GenericMacros.rounded_rect, - [self.size.x+2*margin, self.size.y+2*margin, + (self.size.x+2*margin, self.size.y+2*margin, margin, 0, 0, # no hole - rotation], unit=MM) + rotation), unit=MM) else: return ap.RectangleAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(rotation) @@ -434,27 +444,27 @@ class Pad: alpha = math.atan(y / dy) if dy > 0 else 0 return ap.ApertureMacroInstance(GenericMacros.isosceles_trapezoid, - [x+dy+2*margin*math.cos(alpha), y+2*margin, + (x+dy+2*margin*math.cos(alpha), y+2*margin, 2*dy, 0, 0, # no hole - rotation], unit=MM) + rotation), unit=MM) else: return ap.ApertureMacroInstance(GenericMacros.rounded_isosceles_trapezoid, - [x+dy, y, + (x+dy, y, 2*dy, margin, 0, 0, # no hole - rotation], unit=MM) + rotation), unit=MM) elif self.shape == Atom.roundrect: x, y = self.size.x, self.size.y r = min(x, y) * self.roundrect_rratio if margin > -r: return ap.ApertureMacroInstance(GenericMacros.rounded_rect, - [x+2*margin, y+2*margin, + (x+2*margin, y+2*margin, r+margin, 0, 0, # no hole - rotation], unit=MM) + rotation), unit=MM) else: return ap.RectangleAperture(x+margin, y+margin, unit=MM).rotated(rotation) @@ -485,20 +495,20 @@ class Pad: if self.options: if self.options.anchor == Atom.rect and self.size.x > 0 and self.size.y > 0: if margin <= 0: - primitives.append(amp.CenterLine(MM, [1, self.size.x+2*margin, self.size.y+2*margin, 0, 0, 0])) + primitives.append(amp.CenterLine(MM, 1, self.size.x+2*margin, self.size.y+2*margin, 0, 0, 0)) else: # margin > 0 - primitives.append(amp.CenterLine(MM, [1, self.size.x+2*margin, self.size.y, 0, 0, 0])) - primitives.append(amp.CenterLine(MM, [1, self.size.x, self.size.y+2*margin, 0, 0, 0])) - primitives.append(amp.Circle(MM, [1, 2*margin, -self.size.x/2, -self.size.y/2])) - primitives.append(amp.Circle(MM, [1, 2*margin, -self.size.x/2, +self.size.y/2])) - primitives.append(amp.Circle(MM, [1, 2*margin, +self.size.x/2, -self.size.y/2])) - primitives.append(amp.Circle(MM, [1, 2*margin, +self.size.x/2, +self.size.y/2])) + primitives.append(amp.CenterLine(MM, 1, self.size.x+2*margin, self.size.y, 0, 0, 0)) + primitives.append(amp.CenterLine(MM, 1, self.size.x, self.size.y+2*margin, 0, 0, 0)) + primitives.append(amp.Circle(MM, 1, 2*margin, -self.size.x/2, -self.size.y/2)) + primitives.append(amp.Circle(MM, 1, 2*margin, -self.size.x/2, +self.size.y/2)) + primitives.append(amp.Circle(MM, 1, 2*margin, +self.size.x/2, -self.size.y/2)) + primitives.append(amp.Circle(MM, 1, 2*margin, +self.size.x/2, +self.size.y/2)) elif self.options.anchor == Atom.circle and self.size.x > 0: - primitives.append(amp.Circle(MM, [1, self.size.x+2*margin, 0, 0, 0])) + primitives.append(amp.Circle(MM, 1, self.size.x+2*margin, 0, 0, 0)) - macro = ApertureMacro(primitives=primitives).rotated(rotation) + macro = ApertureMacro(primitives=tuple(primitives)).rotated(rotation) return ap.ApertureMacroInstance(macro, unit=MM) def render_drill(self): @@ -645,7 +655,7 @@ class Footprint: (self.dimensions if text else []), (self.pads if pads else [])) - def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, side=None, variables={}): + def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, side=None, variables={}, cache=None): x += self.at.x y += self.at.y rotation += math.radians(self.at.rotation) @@ -687,7 +697,7 @@ class Footprint: else: margin = None - for fe in obj.render(margin=margin): + for fe in obj.render(margin=margin, cache=cache): fe.rotate(rotation) fe.offset(x, y, MM) if isinstance(fe, go.Flash) and fe.aperture: @@ -745,7 +755,7 @@ class FootprintInstance(Positioned): value: str = None variables: dict = field(default_factory=lambda: {}) - def render(self, layer_stack): + def render(self, layer_stack, cache=None): x, y, rotation = self.abs_pos x, y = MM(x, self.unit), MM(y, self.unit) @@ -763,7 +773,7 @@ class FootprintInstance(Positioned): x=x, y=y, rotation=rotation, side=self.side, text=(not self.hide_text), - variables=variables) + variables=variables, cache=cache) def bounding_box(self, unit=MM): return offset_bounds(self.sexp.bounding_box(unit), unit(self.x, self.unit), unit(self.y, self.unit)) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index ce69bae..28347b5 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -117,8 +117,9 @@ class Board: if layer_stack is None: layer_stack = LayerStack() + cache = {} for obj in chain(self.objects): - obj.render(layer_stack) + obj.render(layer_stack, cache) layer_stack['mechanical', 'outline'].objects.extend(self.outline) layer_stack['top', 'silk'].objects.extend(self.extra_silk_top) @@ -189,13 +190,13 @@ class ObjectGroup(Positioned): drill_pth: list = field(default_factory=list) objects: list = field(default_factory=list) - def render(self, layer_stack): + def render(self, layer_stack, cache=None): x, y, rotation = self.abs_pos top, bottom = ('bottom', 'top') if self.side == 'bottom' else ('top', 'bottom') for obj in self.objects: obj.parent = self - obj.render(layer_stack) + obj.render(layer_stack, cache=cache) for target, source in [ (layer_stack[top, 'copper'], self.top_copper), @@ -251,7 +252,7 @@ class Text(Positioned): layer: str = 'silk' polarity_dark: bool = True - def render(self, layer_stack): + def render(self, layer_stack, cache=None): obj_x, obj_y, rotation = self.abs_pos global newstroke_font @@ -299,6 +300,26 @@ class Text(Positioned): obj.offset(obj_x, obj_y) layer_stack[self.side, self.layer].objects.append(obj) + def bounding_box(self, unit=MM): + approx_w = len(self.text)*self.font_size*0.75 + self.stroke_width + approx_h = self.font_size + self.stroke_width + + if self.h_align == 'left': + x0 = 0 + elif self.h_align == 'center': + x0 = -approx_w/2 + elif self.h_align == 'right': + x0 = -approx_w + + if self.v_align == 'top': + y0 = -approx_h + elif self.v_align == 'middle': + y0 = -approx_h/2 + elif self.v_align == 'bottom': + y0 = 0 + + return (self.x+x0, self.y+y0), (self.x+x0+approx_w, self.y+y0+approx_h) + @dataclass class Pad(Positioned): @@ -312,7 +333,7 @@ class SMDPad(Pad): paste_aperture: Aperture silk_features: list = field(default_factory=list) - def render(self, layer_stack): + def render(self, layer_stack, cache=None): x, y, rotation = self.abs_pos layer_stack[self.side, 'copper'].objects.append(Flash(x, y, self.copper_aperture.rotated(rotation), unit=self.unit)) layer_stack[self.side, 'mask' ].objects.append(Flash(x, y, self.mask_aperture.rotated(rotation), unit=self.unit)) @@ -356,7 +377,7 @@ class THTPad(Pad): if (self.pad_top.side, self.pad_bottom.side) != ('top', 'bottom'): raise ValueError(f'The top and bottom pads must have side set to top and bottom, respectively. Currently, the top pad side is set to "{self.pad_top.side}" and the bottom pad side to "{self.pad_bottom.side}".') - def render(self, layer_stack): + def render(self, layer_stack, cache=None): x, y, rotation = self.abs_pos self.pad_top.parent = self self.pad_top.render(layer_stack) @@ -415,7 +436,7 @@ class Hole(Positioned): diameter: float mask_copper_margin: float = 0.2 - def render(self, layer_stack): + def render(self, layer_stack, cache=None): x, y, rotation = self.abs_pos hole = Flash(x, y, ExcellonTool(self.diameter, plated=False, unit=self.unit), unit=self.unit) @@ -436,7 +457,7 @@ class Via(Positioned): diameter: float hole: float - def render(self, layer_stack): + def render(self, layer_stack, cache=None): x, y, rotation = self.abs_pos aperture = CircleAperture(diameter=self.diameter, unit=self.unit) @@ -627,7 +648,7 @@ class Trace: return self._round_over(points, aperture) - def render(self, layer_stack): + def render(self, layer_stack, cache=None): layer_stack[self.side, 'copper'].objects.extend(self._to_graphic_objects()) def _route_demo(): -- cgit