From a39af853c833c524a43b8a953cce60e07bb14dfb Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 21 Jul 2023 13:27:02 +0200 Subject: Schematics WIP --- gerbonara/cad/kicad/base_types.py | 15 +++++++ gerbonara/cad/kicad/symbols.py | 90 ++++++++++++++++++--------------------- gerbonara/newstroke.py | 72 ++++++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 54 deletions(-) diff --git a/gerbonara/cad/kicad/base_types.py b/gerbonara/cad/kicad/base_types.py index d29586e..3d0b0c4 100644 --- a/gerbonara/cad/kicad/base_types.py +++ b/gerbonara/cad/kicad/base_types.py @@ -270,6 +270,20 @@ class Justify: v: AtomChoice(Atom.top, Atom.bottom) = None mirror: Flag() = False + @property + def h_str(self): + if self.h is None: + return 'center' + else: + return str(self.h) + + @property + def v_str(self): + if self.v is None: + return 'middle' + else: + return str(self.v) + @sexp_type('effects') class TextEffect: @@ -277,6 +291,7 @@ class TextEffect: hide: Flag() = False justify: OmitDefault(Justify) = field(default_factory=Justify) + class TextMixin: @property def size(self): diff --git a/gerbonara/cad/kicad/symbols.py b/gerbonara/cad/kicad/symbols.py index 4767b88..94d30d3 100644 --- a/gerbonara/cad/kicad/symbols.py +++ b/gerbonara/cad/kicad/symbols.py @@ -54,6 +54,11 @@ class Pin: name: Rename(StyledText) = field(default_factory=StyledText) number: Rename(StyledText) = field(default_factory=StyledText) alternates: List(AltFunction) = field(default_factory=list) + _: SEXP_END = None + unit: object = None + + def __after_parse__(self, parent=None): + self.unit = parent @property def direction(self): @@ -96,6 +101,9 @@ class Pin: return (x1, y1), (x2, y2) def to_svg(self, colorscheme=Colorscheme.KiCad): + if self.hide: + return + x1, y1 = 0, 0 x2, y2 = self.length, 0 xform = {'transform': f'translate({self.at.x:.3f} {self.at.y:.3f}) rotate({self.at.rotation})'} @@ -109,68 +117,52 @@ class Pin: 'inverted': [ Tag('circle', **xform, **style, cx=x2-eps/3-0.2, cy=y2, r=eps/3)], 'clock': [ - Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], # NOQA: E501 + Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], 'inverted_clock': [ Tag('circle', **xform, **style, cx=x2-eps/3-0.2, cy=y2, r=eps/3), - Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], # NOQA: E501 + Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], 'input_low': [ - Tag('path', **xform, **style, d=f'M {x2} {y2} L {x2-eps} {y2-eps} L {x2-eps} {y2}')], # NOQA: E501 + Tag('path', **xform, **style, d=f'M {x2} {y2} L {x2-eps} {y2-eps} L {x2-eps} {y2}')], 'clock_low': [ - Tag('path', **xform, **style, d=f'M {x2} {y2} L {x2-eps} {y2-eps} L {x2-eps} {y2}'), # NOQA: E501 - Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], # NOQA: E501 + Tag('path', **xform, **style, d=f'M {x2} {y2} L {x2-eps} {y2-eps} L {x2-eps} {y2}'), + Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], 'output_low': [ - Tag('path', **xform, **style, d=f'M {x2} {y2-eps} L {x2-eps} {y2}')], # NOQA: E501 + Tag('path', **xform, **style, d=f'M {x2} {y2-eps} L {x2-eps} {y2}')], 'edge_clock_high': [ - Tag('path', **xform, **style, d=f'M {x2} {y2} L {x2-eps} {y2-eps} L {x2-eps} {y2}'), # NOQA: E501 - Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], # NOQA: E501 + Tag('path', **xform, **style, d=f'M {x2} {y2} L {x2-eps} {y2-eps} L {x2-eps} {y2}'), + Tag('path', **xform, **style, d=f'M {x2} {y2-eps/2} L {x2+eps/2} {y2} L {x2} {y2+eps/2}')], 'non_logic': [ - Tag('path', **xform, **style, d=f'M {x2-eps/2} {y2-eps/2} L {x2+eps/2} {y2+eps/2}'), # NOQA: E501 - Tag('path', **xform, **style, d=f'M {x2-eps/2} {y2+eps/2} L {x2+eps/2} {y2-eps/2}')], # NOQA: E501 + Tag('path', **xform, **style, d=f'M {x2-eps/2} {y2-eps/2} L {x2+eps/2} {y2+eps/2}'), + Tag('path', **xform, **style, d=f'M {x2-eps/2} {y2+eps/2} L {x2+eps/2} {y2-eps/2}')], # FIXME... }.get(self.style, []): yield tag - if self.at.rotation in (90, 270): - t_rot = 90 - else: - t_rot = 0 - - size = self.name.effects.font.size.y or 1.27 font = Newstroke.load() - strokes = list(font.render(self.name.value, size=size)) - min_x = min(x for st in strokes for x, y in st) if strokes else 0 - min_y = min(y for st in strokes for x, y in st) if strokes else 0 - max_x = max(x for st in strokes for x, y in st) if strokes else 0 - max_y = max(y for st in strokes for x, y in st) if strokes else 0 - w = max_x - min_x - h = max_y - min_y - - if self.at.rotation == 0: - offx = -min_x + self.length + 0.2 - offy = h/2 - elif self.at.rotation == 180: - offx = min_x - self.length - 0.2 - w - offy = h/2 - elif self.at.rotation == 90: - offx = -h/2 - offy = min_x - self.length - 0.2 - w - elif self.at.rotation == 270: - offx = -h/2 - offy = -min_x + self.length + 0.2 - else: - raise ValueError(f'Invalid pin rotation {self.at.rotation}') + if self.name.value != '~' and not self.unit.symbol.pin_names.hide: + yield font.render_svg(self.name.value, + size=self.name.effects.font.size.y or 1.27, + x0=self.length + 0.2, + y0=0, + h_align='left', + v_align='middle', + rotation=self.at.rotation, + stroke=colorscheme.text, + transform=f'translate({self.at.x:.3f} {self.at.y:.3f})', + ) + + if self.number.value != '~' and not self.unit.symbol.pin_numbers.hide: + yield font.render_svg(self.number.value, + size=self.number.effects.font.size.y or 1.27, + x0=self.length-0.2, + y0=0.4, + h_align='right', + v_align='bottom', + rotation=self.at.rotation, + stroke=colorscheme.text, + transform=f'translate({self.at.x:.3f} {self.at.y:.3f})', + ) - d = [] - for stroke in strokes: - points = [] - for x, y in stroke: - x, y = x+offx, y+offy - x, y = rotate_point(x, y, math.radians(self.at.rotation or 0)) - x, y = x+self.at.x, y+self.at.y - points.append(f'{x:.3f} {y:.3f}') - d.append('M '+ ' L '.join(points) + ' ') - yield Tag('path', d=' '.join(d), fill='none', stroke=colorscheme.text, stroke_width='0.254', stroke_linecap='round', stroke_linejoin='round') - print('name', self.name.value) @sexp_type('fill') diff --git a/gerbonara/newstroke.py b/gerbonara/newstroke.py index b48476a..247b674 100644 --- a/gerbonara/newstroke.py +++ b/gerbonara/newstroke.py @@ -5,9 +5,11 @@ import unicodedata import re import ast from functools import lru_cache +import math from importlib.resources import files from . import data +from .utils import rotate_point, Tag STROKE_FONT_SCALE = 1/21 @@ -29,10 +31,39 @@ class Newstroke: def load(kls): return kls() - def render(self, text, size=1.0, space_width=DEFAULT_SPACE_WIDTH, char_gap=DEFAULT_CHAR_GAP): + def render(self, text, size=1.0, x0=0, y0=0, rotation=0, h_align='left', v_align='bottom', space_width=DEFAULT_SPACE_WIDTH, char_gap=DEFAULT_CHAR_GAP, scale=(1, 1)): text = unicodedata.normalize('NFC', text) missing_glyph = self.glyphs['?'] + sx, sy = scale x = 0 + if text in ('VDDA', 'PA9', 'VSS'): + print(text, x0, y0, rotation, h_align, v_align, scale) + + if rotation >= 180: + rotation -= 180 + h_align = {'left': 'right', 'right': 'left'}.get(h_align, h_align) + x0, y0 = -x0, y0 + + x0, y0 = rotate_point(x0, y0, math.radians(-rotation)) + + alx, aly = 0, 0 + if h_align != 'left': + (minx, miny), (maxx, maxy) = bbox = self.bounding_box(text, size, space_width, char_gap) + w = maxx - minx + if h_align == 'right': + alx = -w + elif h_align == 'center': + alx = -w/2 + else: + raise ValueError(f'Invalid h_align value "{h_align}"') + + if v_align == 'top': + aly = -1.2*size + elif v_align == 'middle': + aly = -1.2*size/2 + elif v_align != 'bottom': + raise ValueError(f'Invalid v_align value "{v_align}"') + for c in text: if c == ' ': x += space_width*size @@ -42,15 +73,46 @@ class Newstroke: 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)) + yield self.transform_stroke(st, translate=(x0, y0), offset=(x+alx, aly), rotation=math.radians(-rotation), scale=(sx*size, sy*size)) x += glyph_w*size + def render_svg(self, text, size=1.0, x0=0, y0=0, rotation=0, h_align='left', v_align='bottom', space_width=DEFAULT_SPACE_WIDTH, char_gap=DEFAULT_CHAR_GAP, **svg_attrs): + if 'stroke_linecap' not in svg_attrs: + svg_attrs['stroke_linecap'] = 'round' + if 'stroke_linejoin' not in svg_attrs: + svg_attrs['stroke_linejoin'] = 'round' + if 'stroke_width' not in svg_attrs: + svg_attrs['stroke_width'] = f'{0.2*size:.3f}' + svg_attrs['fill'] = 'none' + + strokes = ['M ' + ' L '.join(f'{x:.3f} {y:.3f}' for x, y in stroke) + for stroke in self.render(text, size=size, x0=x0, y0=y0, rotation=rotation, h_align=h_align, + v_align=v_align, space_width=space_width, char_gap=char_gap, + scale=(1, -1))] + return Tag('path', d=' '.join(strokes), **svg_attrs) + + def bounding_box(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)) + x += glyph_w*size + + return (0, -0.2*size), (x, 1.2*size) + @classmethod - def transform_stroke(kls, stroke, translate, scale): - dx, dy = translate + def transform_stroke(kls, stroke, translate, offset, scale, rotation=0): + x0, y0 = translate sx, sy = scale - return [(x*sx+dx, y*sy+dy) for x, y in stroke] + dx, dy = offset + return [rotate_point(x*sx+dx+x0, y*sy+dy+y0, rotation, x0, y0) for x, y in stroke] def load_font(self, newstroke_cpp): -- cgit