summaryrefslogtreecommitdiff
path: root/gerbonara/cad/kicad/base_types.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/cad/kicad/base_types.py')
-rw-r--r--gerbonara/cad/kicad/base_types.py151
1 files changed, 144 insertions, 7 deletions
diff --git a/gerbonara/cad/kicad/base_types.py b/gerbonara/cad/kicad/base_types.py
index f763585..263f19d 100644
--- a/gerbonara/cad/kicad/base_types.py
+++ b/gerbonara/cad/kicad/base_types.py
@@ -1,14 +1,17 @@
-from .sexp import *
-from .sexp_mapper import *
+import string
import time
-
from dataclasses import field, replace
import math
import uuid
from contextlib import contextmanager
from itertools import cycle
-from ...utils import rotate_point
+from .sexp import *
+from .sexp_mapper import *
+from ...newstroke import Newstroke
+from ...utils import rotate_point, Tag, MM
+from ... import apertures as ap
+from ... import graphic_objects as go
LAYER_MAP_K2G = {
@@ -46,7 +49,16 @@ class Color:
r: int = None
g: int = None
b: int = None
- a: int = None
+ a: float = None
+
+ def __bool__(self):
+ return self.r or self.b or self.g or not math.isclose(self.a, 0, abs_tol=1e-3)
+
+ def svg(self, default=None):
+ if default and not self:
+ return default
+
+ return f'rgba({self.r} {self.g} {self.b} {self.a})'
@sexp_type('stroke')
@@ -54,7 +66,32 @@ class Stroke:
width: Named(float) = 0.254
type: Named(AtomChoice(Atom.dash, Atom.dot, Atom.dash_dot_dot, Atom.dash_dot, Atom.default, Atom.solid)) = Atom.default
color: Color = None
+
+ def svg_color(self, default=None):
+ if self.color:
+ return self.color.svg(default)
+ else:
+ return default
+ def svg_attrs(self, default_color=None):
+ w = self.width
+ if not (color := self.color or default_color):
+ return {}
+
+ attrs = {'stroke': color,
+ 'stroke_linecap': 'round',
+ 'stroke_widtj': self.width}
+
+ if self.type not in (Atom.default, Atom.solid):
+ attrs['stroke_dasharray'] = {
+ Atom.dash: f'{w*5:.3f},{w*5:.3f}',
+ Atom.dot: f'{w*2:.3f},{w*2:.3f}',
+ Atom.dash_dot: f'{w*5:.3f},{w*3:.3f}{w:.3f},{w*3:.3f}',
+ Atom.dash_dot_dot: f'{w*5:.3f},{w*3:.3f}{w:.3f},{w*3:.3f}{w:.3f},{w*3:.3f}',
+ }[self.type]
+
+ return attrs
+
class Dasher:
def __init__(self, obj):
@@ -140,6 +177,19 @@ class Dasher:
if stroked:
yield lx, ly, x2, y2
+ def svg(self, **kwargs):
+ if 'fill' not in kwargs:
+ kwargs['fill'] = 'none'
+ if 'stroke' not in kwargs:
+ kwargs['stroke'] = 'black'
+ if 'stroke_width' not in kwargs:
+ kwargs['stroke_width'] = 0.254
+ if 'stroke_linecap' not in kwargs:
+ kwargs['stroke_linecap'] = 'round'
+
+ d = ' '.join(f'M {x1:.3f} {y1:.3f} L {x2:.3f} {y2:.3f}' for x1, y1, x2, y2 in self)
+ return Tag('path', d=d, **kwargs)
+
@sexp_type('xy')
class XYCoord:
@@ -158,7 +208,7 @@ class XYCoord:
else:
self.x, self.y = x, y
- def isclose(self, other, tol=1e-6):
+ def isclose(self, other, tol=1e-3):
return math.isclose(self.x, other.x, tol) and math.isclose(self.y, other.y, tol)
def with_offset(self, x=0, y=0):
@@ -168,6 +218,7 @@ class XYCoord:
x, y = rotate_point(self.x, self.y, angle, cx, cy)
return replace(self, x=x, y=y)
+
@sexp_type('pts')
class PointList:
xy : List(XYCoord) = field(default_factory=list)
@@ -226,6 +277,83 @@ class TextEffect:
hide: Flag() = False
justify: OmitDefault(Justify) = field(default_factory=Justify)
+class TextMixin:
+ @property
+ def size(self):
+ return self.effects.font.size.y or 1.27
+
+ @size.setter
+ def size(self, value):
+ self.effects.font.size.x = self.effects.font.size.y = value
+
+ @property
+ def line_width(self):
+ return self.effects.font.thickness or 0.254
+
+ @line_width.setter
+ def line_width(self, value):
+ self.effects.font.thickness = value
+
+ def bounding_box(self, default=None):
+ if not self.text or not self.text.strip():
+ return default
+
+ lines = list(self.render())
+ x1 = min(min(l.x1, l.x2) for l in lines)
+ y1 = min(min(l.y1, l.y2) for l in lines)
+ x2 = max(max(l.x1, l.x2) for l in lines)
+ y2 = max(max(l.y1, l.y2) for l in lines)
+ r = self.effects.font.thickness/2
+ return (x1-r, y1-r), (x2+r, y2+r)
+
+ def svg_path_data(self):
+ for line in self.render():
+ yield f'M {line.x1:.3f} {line.y1:.3f} L {line.x2:.3f} {line.y2:.3f}'
+
+ def to_svg(self, color='black'):
+ d = ' '.join(self.svg_path_data())
+ yield Tag('path', d=d, fill='none', stroke=color, stroke_width=f'{self.line_width:.3f}')
+
+ def render(self, variables={}):
+ if not self.effects or self.effects.hide or not self.effects.font:
+ return
+
+ font = Newstroke.load()
+ text = string.Template(self.text).safe_substitute(variables)
+ strokes = list(font.render(text, size=self.size))
+ min_x = min(x for st in strokes for x, y in st)
+ min_y = min(y for st in strokes for x, y in st)
+ max_x = max(x for st in strokes for x, y in st)
+ max_y = max(y for st in strokes for x, y in st)
+ w = max_x - min_x
+ h = max_y - min_y
+
+ offx = -min_x + {
+ None: -w/2,
+ Atom.right: -w,
+ Atom.left: 0
+ }[self.effects.justify.h if self.effects.justify else None]
+
+ offy = {
+ None: self.size/2,
+ Atom.top: self.size,
+ Atom.bottom: 0
+ }[self.effects.justify.v if self.effects.justify else None]
+
+ aperture = ap.CircleAperture(self.line_width or 0.2, unit=MM)
+ for stroke in strokes:
+ out = []
+
+ 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
+ out.append((x, y))
+
+ for p1, p2 in zip(out[:-1], out[1:]):
+ yield go.Line(*p1, *p2, aperture=aperture, unit=MM)
+
+
@sexp_type('tstamp')
class Timestamp:
@@ -293,7 +421,7 @@ class Property:
@sexp_type('property')
-class DrawnProperty:
+class DrawnProperty(TextMixin):
key: str = None
value: str = None
id: Named(int) = None
@@ -303,6 +431,15 @@ class DrawnProperty:
tstamp: Timestamp = None
effects: TextEffect = field(default_factory=TextEffect)
+ # Alias value for text mixin
+ @property
+ def text(self):
+ return self.value
+
+ @text.setter
+ def text(self, value):
+ self.value = value
+
if __name__ == '__main__':
class Foo: