cad: Fix outline reconstruction and add text feature

This commit is contained in:
jaseg 2023-04-04 20:06:16 +02:00
parent ce8d045178
commit 44ca8349eb
3 changed files with 105 additions and 11 deletions

View file

@ -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')

View file

@ -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

View file

@ -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