Add beginnings of CAD module

This commit is contained in:
jaseg 2023-04-04 01:31:19 +02:00
parent 909766a3a0
commit 8d4430ea61
5 changed files with 66069 additions and 0 deletions

View file

233
gerbonara/cad/primitives.py Normal file
View file

@ -0,0 +1,233 @@
import math
from copy import copy
from itertools import zip_longest
from dataclasses import dataclass, field, KW_ONLY
from ..utils import LengthUnit, MM, rotate_point
from ..layers import LayerStack
from ..graphic_objects import Line, Arc, Flash
from ..apertures import Aperture, CircleAperture, RectangleAperture, ExcellonTool
def sgn(x):
return -1 if x < 0 else 1
class Board:
def __init__(self):
self.objects = set()
def to_layer_stack(self, layer_stack):
if layer_stack is None:
layer_stack = LayerStack()
for obj in self.objects:
obj.render(stack)
@dataclass
class Positioned:
x: float
y: float
_: KW_ONLY
rotation: float = 0.0
unit: LengthUnit = MM
parent: object = None
@property
def abs_pos(self, dx, dy, da):
x, y = rotate_point(self.x, self.y, da)
if self.parent is None:
px, py, pa = dx, dy, 0
else:
px, py, pa = self.parent.abs_pos(dx, dy, da)
return x+px, y+py, self.rotation+da+pa
@dataclass
class Pad(Positioned):
pass
@dataclass
class SMDPad(Pad):
copper_aperture: Aperture
mask_aperture: Aperture
paste_aperture: Aperture
silk_features: list
side: str = 'top'
def to_layer_stack(self, layer_stack):
x, y, rotation = self.abs_pos
stack[self.side, 'copper'].objects.append(Flash(x, y, self.copper_aperture.rotated(rotation), unit=self.unit))
stack[self.side, 'mask' ].objects.append(Flash(x, y, self.mask_aperture.rotated(rotation), unit=self.unit))
stack[self.side, 'paste' ].objects.append(Flash(x, y, self.paste_aperture.rotated(rotation), unit=self.unit))
stack[self.side, 'silk' ].objects.extend([copy(feature).rotate(rotation).offset(x, y, self.unit)
for feature in self.silk_features])
def flip(self):
self.side = 'top' if self.side == 'bottom' else 'top'
class THTPad(Pad):
drill_dia: float
pad_top: SMDPad
pad_bottom: SMDPad = None
aperture_inner: Aperture = None
plated: bool = True
def __post_init__(self):
if self.pad_bottom is None:
self.pad_bottom = copy(self.pad_top)
self.pad_bottom.flip()
self.pad_top.parent = self.pad_bottom.parent = self
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 to_layer_stack(self, layer_stack, x, y, rotation):
x, y, rotation = self.abs_pos
self.top_pad.to_layer_stack(layer_stack)
self.bottom_pad.to_layer_stack(layer_stack)
for (side, use), layer in layer_stack.inner_layers:
layer.objects.append(Flash(x, y, self.aperture_inner.rotated(rotation), unit=self.unit))
hole = Flash(self.x, self.y, ExcellonTool(self.drill_dia, plated=self.plated, unit=self.unit), unit=self.unit)
if self.plated:
layer_stack.drill_pth.objects.append(hole)
else:
layer_stack.drill_npth.objects.append(hole)
@dataclass
class Via(Positioned):
diameter: float
hole: float
def to_layer_stack(self, layer_stack):
x, y, rotation = self.abs_pos
aperture = CircleAperture(diameter=self.diameter, unit=self.unit)
tool = ExcellonTool(diameter=self.hole, unit=self.unit)
for (side, use), layer in layer_stack.copper_layers:
layer.objects.append(Flash(x, y, aperture, unit=self.unit))
layer_stack.drill_pth.objects.append(Flash(x, y, tool, unit=self.unit))
@dataclass
class Trace:
width: float
start: object = None
end: object = None
side: str = 'top'
waypoints: [(float, float)] = field(default_factory=list)
style: str = 'direct'
orientation: [str] = tuple() # 'top' or 'bottom'
roundover: float = 0
unit: LengthUnit = MM
parent: object = None
DIRECT = 'direct'
OBLIQUE = 'oblique'
ORTHO = 'ortho'
CW = 'cw'
CCW = 'ccw'
def _route(self, p1, p2, orientation):
x1, y1 = p1
x2, y2 = p2
dx = x2-x1
dy = y2-y1
yield p1
if self.style == 'direct' or \
math.isclose(x1, x2, abs_tol=1e-6) or math.isclose(y1, y2, abs_tol=1e-6) or \
(self.style == 'oblique' and math.isclose(dx, dy, abs_tol=1e-6)):
yield p2
return
p = (abs(dy) > abs(dx)) == ((dx >= 0) == (dy >= 0))
if self.style == 'oblique':
if p == (orientation == 'cw'):
if abs(dy) > abs(dx):
yield (0, sgn(dy)*(abs(dy)-abs(dx)))
else:
yield (sgn(dx)*(abs(dx)-abs(dy)), 0)
else:
if abs(dy) > abs(dx):
yield (dx, sgn(dy)*abs(dx))
else:
yield (sgn(dx)*abs(dy), dy)
else: # self.style == 'ortho'
pass
#if p == (orientation == 'cw'):
#else:
yield p2
def to_layer_stack(self, layer_stack, x, y, rotation):
start, end = self.start, self.end
if not isinstance(start, tuple):
start = start.abs_pos
if not isinstance(end, tuple):
end = end.abs_pos
points = [start, *self.waypoints, end]
aperture = CircleAperture(diameter=self.width, unit=self.unit)
for p1, p2, orientation in zip_longest(points[:-1], points[1:], self.orientation):
layer_stack[self.side, 'copper'].extend(self._route(p1, p2, orientation, aperture))
if __name__ == '__main__':
from ..utils import setup_svg, Tag
from ..newstroke import Newstroke
font = Newstroke()
tags = []
for n in range(0, 8*6):
theta = 2*math.pi / (8*6) * n
dx, dy = math.cos(theta), math.sin(theta)
tr = Trace(0.1, style='oblique')
points_cw = list(tr._route((0, 0), (dx, dy), 'cw'))
points_ccw = list(tr._route((0, 0), (dx, dy), 'ccw'))
pd = lambda points: f'M {points[0][0]}, {points[0][1]} ' + ' '.join(f'L {x}, {y}' for x, y in points[1:])
strokes = list(font.render(f'α={n/(8*6)*360}', size=0.2))
xs = [x for st in strokes for x, _y in st]
ys = [y for st in strokes for _x, y in st]
min_x, min_y, max_x, max_y = min(xs), min(ys), max(xs), max(ys)
xf = f'translate({n//6*1.1 + 0.1} {n%6*1.3 + 0.3}) scale(0.5 0.5) translate(1 1)'
txf = f'{xf} translate(0 -1.2) translate({-(max_x-min_x)/2} {-max_y})'
tags.append(Tag('circle', cx='0', cy='0', r='1',
fill='none', stroke='black', opacity='0.5', stroke_width='0.05',
transform=xf))
tags.append(Tag('path',
fill='none',
stroke='red', opacity='0.5', stroke_width='0.05', stroke_linecap='round',
transform=xf, d=pd(points_cw)))
tags.append(Tag('path',
fill='none',
stroke='blue', opacity='0.5', stroke_width='0.05', stroke_linecap='round',
transform=xf, d=pd(points_ccw)))
tags.append(Tag('path',
fill='none',
stroke='black', opacity='0.5', stroke_width='0.02', stroke_linejoin='round', stroke_linecap='round',
transform=txf, d=' '.join(pd(points) for points in strokes)))
print(setup_svg([Tag('g', tags, transform='scale(20 20)')], [(0, 0), (20*10*1.1 + 0.1, 20*10*1.3 + 0.1)]))

View file

File diff suppressed because it is too large Load diff

93
gerbonara/newstroke.py Normal file
View file

@ -0,0 +1,93 @@
#!/usr/bin/env python
from pathlib import Path
import unicodedata
import re
import ast
from importlib.resources import files
from . import data
STROKE_FONT_SCALE = 1/21
FONT_OFFSET = -10
DEFAULT_SPACE_WIDTH = 0.6
DEFAULT_CHAR_GAP = 0.2
_dec = lambda c: ord(c)-ord('R')
class Newstroke:
def __init__(self, newstroke_cpp=None):
if newstroke_cpp is None:
newstroke_cpp = files(data).joinpath('newstroke_font.cpp').read_bytes()
self.glyphs = dict(self.load(newstroke_cpp))
def render(self, text, size=1.0, space_width=DEFAULT_SPACE_WIDTH, char_gap=DEFAULT_CHAR_GAP):
text = unicodedata.normalize('NFC', text)
missing_glyph = self.glyphs['?']
x = 0
for c in text:
if c == ' ':
x += space_width*size
continue
width, strokes = self.glyphs.get(c, missing_glyph)
glyph_w = max(width, max(x for st in strokes for x, _y in st))
for st in strokes:
yield self.transform_stroke(st, translate=(x, 0), scale=(size, size))
x += glyph_w*size
@classmethod
def transform_stroke(kls, stroke, translate, scale):
dx, dy = translate
sx, sy = scale
return [(x*sx+dx, y*sy+dy) for x, y in stroke]
def load(self, newstroke_cpp):
e = []
for char, (width, strokes) in self.load_glyphs(newstroke_cpp):
yield char, (width, strokes)
@classmethod
def decode_stroke(kls, stroke, start_x):
for i in range(0, len(stroke), 2):
x = (stroke[i]-0x52-start_x)*STROKE_FONT_SCALE
y = (stroke[i+1]-0x52+FONT_OFFSET)*STROKE_FONT_SCALE
yield (x, y)
@classmethod
def decode_glyph(kls, data):
start_x, end_x = data[0]-0x52, data[1]-0x52
width = end_x - start_x
strokes = tuple(tuple(kls.decode_stroke(st, start_x)) for st in data[2:].split(b' R'))
return width*STROKE_FONT_SCALE, strokes
@classmethod
def load_glyphs(kls, newstroke_cpp):
it = iter(newstroke_cpp.splitlines())
for line in it:
if re.search(rb'char.*\*', line):
break
charcode = 0x20
for line in it:
if (match := re.search(rb'".*"', line)):
yield chr(charcode), kls.decode_glyph(match.group(0)[1:-1].replace(b'\\\\', b'\\'))
charcode += 1
else:
if b'}' in line:
break
if __name__ == '__main__':
import time
t1 = time.time()
Newstroke()
t2 = time.time()
print((t2-t1)*1000)