From 495ae6e93221b38a5347d65aff35effea919a565 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 4 Apr 2023 20:06:16 +0200 Subject: cad: Fix outline reconstruction and add text feature --- gerbonara/cad/primitives.py | 100 +++++++++++++++++++++++++++++++++++++++- gerbonara/graphic_primitives.py | 6 +-- gerbonara/layers.py | 10 ++-- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index 2d1977d..4eaeb5f 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -10,6 +10,7 @@ from ..utils import LengthUnit, MM, rotate_point, svg_arc, sum_bounds, bbox_inte from ..layers import LayerStack from ..graphic_objects import Line, Arc, Flash from ..apertures import Aperture, CircleAperture, RectangleAperture, ExcellonTool +from ..newstroke import Newstroke def sgn(x): @@ -23,6 +24,9 @@ class KeepoutError(ValueError): self.keepout = keepout +newstroke_font = None + + class Board: def __init__(self, w=None, h=None, corner_radius=1.5, center=False, default_via_hole=0.4, default_via_diameter=0.8, x=0, y=0, rotation=0, unit=MM): self.x, self.y = x, y @@ -53,6 +57,9 @@ class Board: else: self.extra_silk_bottom.append(obj) + def add_text(self, *args, **kwargs): + self.objects.append(Text(*args, **kwargs)) + def add_keepout(self, bbox, unit=MM): ((_x_min, _y_min), (_x_max, _y_max)) = bbox self.keepouts.append(MM.convert_bounds_from(unit, bbox)) @@ -152,6 +159,95 @@ class Positioned: return bbox_intersect(self.bounding_box(unit), bbox) +@dataclass +class ObjectGroup(Positioned): + top_copper: list = field(default_factory=list) + top_mask: list = field(default_factory=list) + top_silk: list = field(default_factory=list) + top_paste: list = field(default_factory=list) + bottom_copper: list = field(default_factory=list) + bottom_mask: list = field(default_factory=list) + bottom_silk: list = field(default_factory=list) + bottom_paste: list = field(default_factory=list) + drill_npth: list = field(default_factory=list) + drill_pth: list = field(default_factory=list) + side: str = 'top' + + def flip(self): + self.side = 'top' if self.side == 'bottom' else 'bottom' + + def render(self, layer_stack): + x, y, rotation = self.abs_pos + top, bottom = ('bottom', 'top') if self.side == 'bottom' else ('top', 'bottom') + for target, source in [ + (layer_stack[top, 'copper'], self.top_copper), + (layer_stack[top, 'mask'], self.top_mask), + (layer_stack[top, 'silk'], self.top_silk), + (layer_stack[top, 'paste'], self.top_paste), + (layer_stack[bottom, 'copper'], self.bottom_copper), + (layer_stack[bottom, 'mask'], self.bottom_mask), + (layer_stack[bottom, 'silk'], self.bottom_silk), + (layer_stack[bottom, 'paste'], self.bottom_paste), + (layer_stack.drill_pth, self.drill_pth), + (layer_stack.drill_npth, self.drill_npth)]: + for fe in source: + target.objects.append(copy(fe).rotate(rotation).offset(x, y, self.unit)) + + +@dataclass +class Text(Positioned): + text: str + font_size: float = 2.5 + stroke_width: float = 0.25 + h_align: str = 'left' + v_align: str = 'bottom' + layer: str = 'silk' + side: str = 'top' + polarity_dark: bool = True + + def flip(self): + self.side = 'top' if self.side == 'bottom' else 'bottom' + + def render(self, layer_stack): + obj_x, obj_y, rotation = self.abs_pos + global newstroke_font + + if newstroke_font is None: + newstroke_font = Newstroke() + + strokes = list(newstroke_font.render(self.text, size=self.font_size)) + xs = [x for points in strokes for x, _y in points] + ys = [y for points in strokes for _x, y in points] + min_x, min_y, max_x, max_y = min(xs), min(ys), max(xs), max(ys) + + if self.h_align == 'left': + x0 = 0 + elif self.h_align == 'center': + x0 = -max_x/2 + elif self.h_align == 'right': + x0 = -max_x + else: + raise ValueError('h_align must be one of "left", "center", or "right".') + + if self.v_align == 'top': + y0 = -max_y + elif self.v_align == 'middle': + y0 = -max_y/2 + elif self.v_align == 'bottom': + y0 = 0 + else: + raise ValueError('v_align must be one of "top", "middle", or "bottom".') + + ap = CircleAperture(self.stroke_width, unit=self.unit) + + for stroke in strokes: + for (x1, y1), (x2, y2) in zip(stroke[:-1], stroke[1:]): + obj = Line(x0+x1, y0-y1, x0+x2, y0-y2, aperture=ap, unit=self.unit, polarity_dark=self.polarity_dark) + obj.rotate(rotation) + obj.offset(obj_x, obj_y) + layer_stack[self.side, self.layer].objects.append(obj) + + @dataclass class Pad(Positioned): pass @@ -454,7 +550,6 @@ class Trace: def _route_demo(): from ..utils import setup_svg, Tag - from ..newstroke import Newstroke def pd_obj(objs): objs = list(objs) @@ -530,7 +625,8 @@ def _board_demo(): p2 = THTPad.rect(20, 15, 0.9, 1.8) b.add(p2) b.add(Trace(0.5, p1, p2, style='ortho', roundover=1.5)) - print(b.svg()) + b.add_text(50, 50, 'Foobar') + print(b.pretty_svg()) b.layer_stack().save_to_directory('/tmp/testdir') diff --git a/gerbonara/graphic_primitives.py b/gerbonara/graphic_primitives.py index 27890b1..02eac8d 100644 --- a/gerbonara/graphic_primitives.py +++ b/gerbonara/graphic_primitives.py @@ -136,11 +136,11 @@ class ArcPoly(GraphicPrimitive): if len(self.outline) == 0: return - yield f'M {self.outline[0][0]:.6} {self.outline[0][1]:.6}' + yield f'M {float(self.outline[0][0]):.6} {float(self.outline[0][1]):.6}' for old, new, arc in self.segments: if not arc: - yield f'L {new[0]:.6} {new[1]:.6}' + yield f'L {float(new[0]):.6} {float(new[1]):.6}' else: clockwise, center = arc yield svg_arc(old, new, center, clockwise) @@ -214,7 +214,7 @@ class Arc(GraphicPrimitive): def flip(self): return replace(self, x1=self.x2, y1=self.y2, x2=self.x1, y2=self.y1, - cx=(self.x + self.cx) - self.x2, cy=(self.y + self.cy) - self.y2, clockwise=not self.clockwise) + cx=(self.x1 + self.cx) - self.x2, cy=(self.y1 + self.cy) - self.y2, clockwise=not self.clockwise) def bounding_box(self): r = self.width/2 diff --git a/gerbonara/layers.py b/gerbonara/layers.py index 6c61756..22a845f 100644 --- a/gerbonara/layers.py +++ b/gerbonara/layers.py @@ -1055,7 +1055,7 @@ class LayerStack: joins = {} for cur in lines: - for i, (x, y) in enumerate([(cur.x1, cur.y1), (cur.x2, cur.y2)]): + for (i, x, y) in [(0, cur.x1, cur.y1), (1, cur.x2, cur.y2)]: x_left = bisect.bisect_left (by_x, x, key=lambda elem: elem[0] + tol) x_right = bisect.bisect_right(by_x, x, key=lambda elem: elem[0] - tol) selected = { elem for elem_x, elem in by_x[x_left:x_right] if elem != cur } @@ -1080,11 +1080,9 @@ class LayerStack: joins[(cur, i)] = (nearest, j) joins[(nearest, j)] = (cur, i) - def flip_if(obj, i): - if i: - c = copy.copy(obj) - c.flip() - return c + def flip_if(obj, cond): + if cond: + return obj.flip() else: return obj -- cgit