kicad: Improve API and fix kicad-nightly compat
This commit is contained in:
parent
860fa4c53b
commit
08c4091e57
6 changed files with 268 additions and 26 deletions
|
|
@ -2,12 +2,14 @@ from .sexp import *
|
|||
from .sexp_mapper import *
|
||||
import time
|
||||
|
||||
from dataclasses import field
|
||||
from dataclasses import field, replace
|
||||
import math
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from itertools import cycle
|
||||
|
||||
from ...utils import rotate_point
|
||||
|
||||
|
||||
LAYER_MAP_K2G = {
|
||||
'F.Cu': ('top', 'copper'),
|
||||
|
|
@ -144,9 +146,27 @@ class XYCoord:
|
|||
x: float = 0
|
||||
y: float = 0
|
||||
|
||||
def __init__(self, x=0, y=0):
|
||||
if isinstance(x, XYCoord):
|
||||
self.x, self.y = x.x, x.y
|
||||
elif isinstance(x, (tuple, list)):
|
||||
self.x, self.y = x
|
||||
elif hasattr(x, 'abs_pos'):
|
||||
self.x, self.y, _1, _2 = x.abs_pos
|
||||
elif hasattr(x, 'at'):
|
||||
self.x, self.y = x.at.x, x.at.y
|
||||
else:
|
||||
self.x, self.y = x, y
|
||||
|
||||
def isclose(self, other, tol=1e-6):
|
||||
return math.isclose(self.x, other.x, tol) and math.isclose(self.y, other.y, tol)
|
||||
|
||||
def with_offset(self, x=0, y=0):
|
||||
return replace(self, x=self.x+x, y=self.y+y)
|
||||
|
||||
def with_rotation(self, angle, cx=0, cy=0):
|
||||
x, y = rotate_point(self.x, self.y, angle, cx, cy)
|
||||
return replace(self, x=x, y=y)
|
||||
|
||||
@sexp_type('pts')
|
||||
class PointList:
|
||||
|
|
@ -178,6 +198,10 @@ class AtPos(XYCoord):
|
|||
def rotation_rad(self, value):
|
||||
self.rotation = math.degrees(value)
|
||||
|
||||
def with_rotation(self, angle, cx=0, cy=0):
|
||||
obj = super().with_rotation(angle, cx, cy)
|
||||
return replace(obj, rotation=self.rotation + angle)
|
||||
|
||||
|
||||
@sexp_type('font')
|
||||
class FontSpec:
|
||||
|
|
@ -206,6 +230,9 @@ class TextEffect:
|
|||
class Timestamp:
|
||||
value: str = field(default_factory=uuid.uuid4)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return Timestamp()
|
||||
|
||||
def __after_parse__(self, parent):
|
||||
self.value = str(self.value)
|
||||
|
||||
|
|
@ -219,6 +246,9 @@ class Timestamp:
|
|||
class UUID:
|
||||
value: str = field(default_factory=uuid.uuid4)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return UUID()
|
||||
|
||||
def __after_parse__(self, parent):
|
||||
self.value = str(self.value)
|
||||
|
||||
|
|
@ -232,6 +262,9 @@ class UUID:
|
|||
class EditTime:
|
||||
value: str = field(default_factory=time.time)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return EditTime()
|
||||
|
||||
def __after_parse__(self, parent):
|
||||
self.value = int(str(self.value), 16)
|
||||
|
||||
|
|
|
|||
|
|
@ -402,13 +402,16 @@ class Pad:
|
|||
@property
|
||||
def abs_pos(self):
|
||||
if self.footprint:
|
||||
px, py = self.footprint.at.x, self.footprint.at.y
|
||||
px, py, pr = self.footprint.at.x, self.footprint.at.y, self.footprint.at.rotation
|
||||
else:
|
||||
px, py = 0, 0
|
||||
px, py, pr = 0, 0, 0
|
||||
|
||||
x, y = rotate_point(self.at.x, self.at.y, -math.radians(self.at.rotation))
|
||||
x, y = rotate_point(self.at.x, self.at.y, math.radians(pr))
|
||||
return x+px, y+py, self.at.rotation, False
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.at = self.at.with_offset(x, y)
|
||||
|
||||
def find_connected(self, **filters):
|
||||
""" Find footprints connected to the same net as this pad """
|
||||
return self.footprint.board.find_footprints(net=self.net.name, **filters)
|
||||
|
|
@ -630,7 +633,6 @@ class Footprint:
|
|||
_bounding_box: tuple = None
|
||||
board: object = None
|
||||
|
||||
|
||||
def __after_parse__(self, parent):
|
||||
for pad in self.pads:
|
||||
pad.footprint = self
|
||||
|
|
@ -667,7 +669,6 @@ class Footprint:
|
|||
def find_pads(self, number=None, net=None):
|
||||
for pad in self.pads:
|
||||
if number is not None and pad.number == str(number):
|
||||
print('find_pads', number, net, pad.number)
|
||||
yield pad
|
||||
elif isinstance(net, str) and fnmatch.fnmatch(pad.net.name, net):
|
||||
yield pad
|
||||
|
|
@ -684,10 +685,18 @@ class Footprint:
|
|||
|
||||
return candidates[0]
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.at = self.at.with_offset(x, y)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@version.setter
|
||||
def version(self, value):
|
||||
if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
|
||||
raise FormatError(f'File format version {value} is not supported. Supported versions are {", ".join(map(str, SUPPORTED_FILE_FORMAT_VERSIONS))}.')
|
||||
|
||||
@property
|
||||
def reference(self):
|
||||
return self.property_value('Reference')
|
||||
|
|
@ -708,15 +717,10 @@ class Footprint:
|
|||
def value(self):
|
||||
return self.property_value('Value')
|
||||
|
||||
@reference.setter
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self.set_property('Value', value)
|
||||
|
||||
@version.setter
|
||||
def version(self, value):
|
||||
if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
|
||||
raise FormatError(f'File format version {value} is not supported. Supported versions are {", ".join(map(str, SUPPORTED_FILE_FORMAT_VERSIONS))}.')
|
||||
|
||||
def write(self, filename=None):
|
||||
with open(filename or self.original_filename, 'w') as f:
|
||||
f.write(self.serialize())
|
||||
|
|
@ -745,6 +749,42 @@ class Footprint:
|
|||
def load(kls, data, *args, **kwargs):
|
||||
return kls.parse(data, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def side(self):
|
||||
return 'front' if self.layer == 'F.Cu' else 'back'
|
||||
|
||||
@side.setter
|
||||
def side(self, value):
|
||||
if value not in ('front', 'back'):
|
||||
raise ValueError(f'side must be either "front" or "back", not {side!r}')
|
||||
|
||||
if self.side != value:
|
||||
self.flip()
|
||||
|
||||
def flip(self):
|
||||
def flip_layer(name):
|
||||
if name.startswith('F.'):
|
||||
return f'B.{name[2:]}'
|
||||
elif name.startswith('B.'):
|
||||
return f'F.{name[2:]}'
|
||||
else:
|
||||
return name
|
||||
|
||||
self.layer = flip_layer(self.layer)
|
||||
for obj in self.objects():
|
||||
if hasattr(obj, 'layer'):
|
||||
obj.layer = flip_layer(obj.layer)
|
||||
|
||||
if hasattr(obj, 'layers'):
|
||||
obj.layers = [flip_layer(name) for name in obj.layers]
|
||||
|
||||
for obj in chain(self.texts, self.text_boxes):
|
||||
obj.effects.justify.mirror = not obj.effects.justify.mirror
|
||||
|
||||
for obj in self.properties:
|
||||
obj.effects.justify.mirror = not obj.effects.justify.mirror
|
||||
obj.layer = flip_layer(obj.layer)
|
||||
|
||||
@property
|
||||
def single_sided(self):
|
||||
raise NotImplementedError()
|
||||
|
|
@ -786,9 +826,16 @@ class Footprint:
|
|||
self.at.y = math.sin(angle)*x + math.cos(angle)*y + cy
|
||||
|
||||
self.at.rotation = (self.at.rotation - math.degrees(angle)) % 360
|
||||
|
||||
for pad in self.pads:
|
||||
pad.at.rotation = (pad.at.rotation - math.degrees(angle)) % 360
|
||||
|
||||
for prop in self.properties:
|
||||
prop.at.rotation = (prop.at.rotation - math.degrees(angle)) % 360
|
||||
|
||||
for text in self.texts:
|
||||
text.at.rotation = (text.at.rotation - math.degrees(angle)) % 360
|
||||
|
||||
def set_rotation(self, angle):
|
||||
old_deg = self.at.rotation
|
||||
new_deg = self.at.rotation = -math.degrees(angle)
|
||||
|
|
@ -797,7 +844,13 @@ class Footprint:
|
|||
for pad in self.pads:
|
||||
pad.at.rotation = (pad.at.rotation + delta) % 360
|
||||
|
||||
def objects(self, text=False, pads=True):
|
||||
for prop in self.properties:
|
||||
prop.at.rotation = (prop.at.rotation + delta) % 360
|
||||
|
||||
for text in self.texts:
|
||||
text.at.rotation = (text.at.rotation + delta) % 360
|
||||
|
||||
def objects(self, text=False, pads=True, groups=True):
|
||||
return chain(
|
||||
(self.texts if text else []),
|
||||
(self.text_boxes if text else []),
|
||||
|
|
@ -808,7 +861,9 @@ class Footprint:
|
|||
self.polygons,
|
||||
self.curves,
|
||||
(self.dimensions if text else []),
|
||||
(self.pads if pads else []))
|
||||
(self.pads if pads else []),
|
||||
self.zones,
|
||||
self.groups if groups else [])
|
||||
|
||||
def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, flip=False, variables={}, cache=None):
|
||||
x += self.at.x
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ class Text:
|
|||
for p1, p2 in zip(out[:-1], out[1:]):
|
||||
yield go.Line(*p1, *p2, aperture=aperture, unit=MM)
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.at = self.at.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('gr_text_box')
|
||||
class TextBox:
|
||||
|
|
@ -100,6 +103,10 @@ class TextBox:
|
|||
|
||||
yield reg
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('gr_line')
|
||||
class Line:
|
||||
|
|
@ -123,6 +130,10 @@ class Line:
|
|||
yield go.Line(x1, y1, x2, y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM)
|
||||
# FIXME render all primitives using dasher, maybe share code w/ fp_ prefix primitives
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('fill')
|
||||
class FillMode:
|
||||
|
|
@ -159,6 +170,15 @@ class Rectangle:
|
|||
# FIXME stroke support
|
||||
yield from rect.outline_objects(aperture=ap.CircleAperture(self.width, unit=MM))
|
||||
|
||||
@property
|
||||
def top_left(self):
|
||||
return ((min(self.start.x, self.end.x), min(self.start.y, self.end.y)),
|
||||
(max(self.start.x, self.end.x), max(self.start.y, self.end.y)))
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('gr_circle')
|
||||
class Circle:
|
||||
|
|
@ -182,6 +202,10 @@ class Circle:
|
|||
if self.fill:
|
||||
yield arc.to_region()
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.center = self.center.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('gr_arc')
|
||||
class Arc:
|
||||
|
|
@ -204,6 +228,11 @@ class Arc:
|
|||
x2, y2 = self.end.x, self.end.y
|
||||
yield go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, aperture=aperture, clockwise=True, unit=MM)
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
self.mid = self.mid.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('gr_poly')
|
||||
class Polygon:
|
||||
|
|
@ -224,6 +253,9 @@ class Polygon:
|
|||
if self.fill:
|
||||
yield reg
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.pts = PointList([pt.with_offset(x, y) for pt in self.pts])
|
||||
|
||||
|
||||
@sexp_type('gr_curve')
|
||||
class Curve:
|
||||
|
|
@ -235,6 +267,9 @@ class Curve:
|
|||
def render(self, variables=None):
|
||||
raise NotImplementedError('Bezier rendering is not yet supported. Please raise an issue and provide an example file.')
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.pts = PointList([pt.with_offset(x, y) for pt in self.pts])
|
||||
|
||||
|
||||
@sexp_type('gr_bbox')
|
||||
class AnnotationBBox:
|
||||
|
|
@ -244,6 +279,10 @@ class AnnotationBBox:
|
|||
def render(self, variables=None):
|
||||
return []
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('format')
|
||||
class DimensionFormat:
|
||||
|
|
@ -273,7 +312,7 @@ class Dimension:
|
|||
dimension_type: Named(AtomChoice(Atom.aligned, Atom.leader, Atom.center, Atom.orthogonal, Atom.radial), name='type') = Atom.aligned
|
||||
layer: Named(str) = 'Dwgs.User'
|
||||
tstamp: Timestamp = field(default_factory=Timestamp)
|
||||
pts: Named(Array(XYCoord)) = field(default_factory=list)
|
||||
pts: PointList = field(default_factory=PointList)
|
||||
height: Named(float) = None
|
||||
orientation: Named(int) = None
|
||||
leader_length: Named(float) = None
|
||||
|
|
@ -284,4 +323,6 @@ class Dimension:
|
|||
def render(self, variables=None):
|
||||
raise NotImplementedError('Dimension rendering is not yet supported. Please raise an issue.')
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.pts = PointList([pt.with_offset(x, y) for pt in self.pts])
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Library for handling KiCad's PCB files (`*.kicad_mod`).
|
|||
|
||||
import math
|
||||
from pathlib import Path
|
||||
from dataclasses import field
|
||||
from dataclasses import field, KW_ONLY
|
||||
from itertools import chain
|
||||
import re
|
||||
import fnmatch
|
||||
|
|
@ -166,6 +166,9 @@ class Image:
|
|||
uuid: UUID = field(default_factory=UUID)
|
||||
data: str = ''
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.at = self.at.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('segment')
|
||||
class TrackSegment:
|
||||
|
|
@ -177,6 +180,10 @@ class TrackSegment:
|
|||
net: Named(int) = 0
|
||||
tstamp: Timestamp = field(default_factory=Timestamp)
|
||||
|
||||
def __post_init__(self):
|
||||
self.start = XYCoord(self.start)
|
||||
self.end = XYCoord(self.end)
|
||||
|
||||
def render(self, variables=None, cache=None):
|
||||
if not self.width:
|
||||
return
|
||||
|
|
@ -191,6 +198,10 @@ class TrackSegment:
|
|||
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
|
||||
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('arc')
|
||||
class TrackArc:
|
||||
|
|
@ -202,6 +213,29 @@ class TrackArc:
|
|||
locked: Flag() = False
|
||||
net: Named(int) = 0
|
||||
tstamp: Timestamp = field(default_factory=Timestamp)
|
||||
_: KW_ONLY
|
||||
center: XYCoord = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.start = XYCoord(self.start)
|
||||
self.end = XYCoord(self.end)
|
||||
if self.center is not None:
|
||||
# Convert normal p1/p2/center notation to the insanity that is kicad's midpoint notation
|
||||
center = XYCoord(self.center)
|
||||
cx, cy = center.x, center.y
|
||||
x1, y1 = self.start.x - cx, self.start.y - cy
|
||||
x2, y2 = self.end.x - cx, self.end.y - cy
|
||||
# Get a vector pointing towards the middle between "start" and "end"
|
||||
dx, dy = (x1 + x2)/2, (y1 + y2)/2
|
||||
# normalize vector, and multiply by radius to get final point
|
||||
r = math.hypot(x1, y1)
|
||||
l = math.hypot(dx, dy)
|
||||
mx = cx + dx / l * r
|
||||
my = cy + dy / l * r
|
||||
self.mid = XYCoord(mx, my)
|
||||
self.center = None
|
||||
else:
|
||||
self.mid = XYCoord(self.mid)
|
||||
|
||||
def render(self, variables=None, cache=None):
|
||||
if not self.width:
|
||||
|
|
@ -221,6 +255,11 @@ class TrackArc:
|
|||
self.mid.x, self.mid.y = rotate_point(self.mid.x, self.mid.y, angle, cx, cy)
|
||||
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.start = self.start.with_offset(x, y)
|
||||
self.mid = self.mid.with_offset(x, y)
|
||||
self.end = self.end.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('via')
|
||||
class Via:
|
||||
|
|
@ -229,13 +268,20 @@ class Via:
|
|||
at: Rename(XYCoord) = field(default_factory=XYCoord)
|
||||
size: Named(float) = 0.8
|
||||
drill: Named(float) = 0.4
|
||||
layers: Named(Array(str)) = field(default_factory=list)
|
||||
layers: Named(Array(str)) = field(default_factory=lambda: ['F.Cu', 'B.Cu'])
|
||||
remove_unused_layers: Flag() = False
|
||||
keep_end_layers: Flag() = False
|
||||
free: Wrap(Flag()) = False
|
||||
net: Named(int) = 0
|
||||
tstamp: Timestamp = field(default_factory=Timestamp)
|
||||
|
||||
@property
|
||||
def abs_pos(self):
|
||||
return self.at.x, self.at.y, 0, False
|
||||
|
||||
def __post_init__(self):
|
||||
self.at = XYCoord(self.at)
|
||||
|
||||
def render_drill(self):
|
||||
aperture = ap.ExcellonTool(self.drill, plated=True, unit=MM)
|
||||
yield go.Flash(self.at.x, self.at.y, aperture=aperture, unit=MM)
|
||||
|
|
@ -244,6 +290,15 @@ class Via:
|
|||
aperture = ap.CircleAperture(self.size, unit=MM)
|
||||
yield go.Flash(self.at.x, self.at.y, aperture, unit=MM)
|
||||
|
||||
def rotate(self, angle, cx=None, cy=None):
|
||||
if cx is None or cy is None:
|
||||
return
|
||||
|
||||
self.at.x, self.at.y = rotate_point(self.at.x, self.at.y, angle, cx, cy)
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.at = self.at.with_offset(x, y)
|
||||
|
||||
|
||||
SUPPORTED_FILE_FORMAT_VERSIONS = [20210108, 20211014, 20221018, 20230517]
|
||||
@sexp_type('kicad_pcb')
|
||||
|
|
@ -293,6 +348,43 @@ class Board:
|
|||
self.properties = [Property(key, value) for key, value in self.properties.items()]
|
||||
self.nets = [Net(index, name) for index, name in self.nets.items()]
|
||||
|
||||
def remove(self, obj):
|
||||
match obj:
|
||||
case gr.Text():
|
||||
self.texts.remove(obj)
|
||||
case gr.TextBox():
|
||||
self.text_boxes.remove(obj)
|
||||
case gr.Line():
|
||||
self.lines.remove(obj)
|
||||
case gr.Rectangle():
|
||||
self.rectangles.remove(obj)
|
||||
case gr.Circle():
|
||||
self.circles.remove(obj)
|
||||
case gr.Arc():
|
||||
self.arcs.remove(obj)
|
||||
case gr.Polygon():
|
||||
self.polygons.remove(obj)
|
||||
case gr.Curve():
|
||||
self.curves.remove(obj)
|
||||
case gr.Dimension():
|
||||
self.dimensions.remove(obj)
|
||||
case Image():
|
||||
self.images.remove(obj)
|
||||
case TrackSegment():
|
||||
self.track_segments.remove(obj)
|
||||
case TrackArc():
|
||||
self.track_arcs.remove(obj)
|
||||
case Via():
|
||||
self.vias.remove(obj)
|
||||
case Zone():
|
||||
self.zones.remove(obj)
|
||||
case Group():
|
||||
self.groups.remove(obj)
|
||||
case Footprint():
|
||||
self.footprints.remove(obj)
|
||||
case _:
|
||||
raise TypeError('Can only remove KiCad objects, cannot map generic gerbonara.cad objects for removal')
|
||||
|
||||
def add(self, obj):
|
||||
match obj:
|
||||
case gr.Text():
|
||||
|
|
@ -325,6 +417,8 @@ class Board:
|
|||
self.zones.append(obj)
|
||||
case Group():
|
||||
self.groups.append(obj)
|
||||
case Footprint():
|
||||
self.footprints.append(obj)
|
||||
case _:
|
||||
for elem in self.map_gn_cad(obj):
|
||||
self.add(elem)
|
||||
|
|
@ -471,7 +565,7 @@ class Board:
|
|||
|
||||
|
||||
def objects(self, vias=True, text=False, images=False):
|
||||
return chain(self.graphic_objects(text=text, images=images), self.tracks(vias=vias))
|
||||
return chain(self.graphic_objects(text=text, images=images), self.tracks(vias=vias), self.footprints, self.zones, self.groups)
|
||||
|
||||
|
||||
def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, flip=False, variables={}, cache=None):
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class ZoneSmoothing:
|
|||
@sexp_type('fill')
|
||||
class ZoneFill:
|
||||
yes: Flag() = False
|
||||
mode: Flag(atom=Atom.hatched) = False
|
||||
mode: Named(Flag(atom=Atom.hatch)) = False
|
||||
thermal_gap: Named(float) = 0.508
|
||||
thermal_bridge_width: Named(float) = 0.508
|
||||
smoothing: ZoneSmoothing = None
|
||||
|
|
@ -60,7 +60,7 @@ class ZoneFill:
|
|||
hatch_orientation: Named(int) = None
|
||||
hatch_smoothing_level: Named(int) = None
|
||||
hatch_smoothing_value: Named(float) = None
|
||||
hatch_border_algorithm: Named(int) = None
|
||||
hatch_border_algorithm: Named(AtomChoice(Atom.hatch_thickness, Atom.min_thickness)) = None
|
||||
hatch_min_hole_area: Named(float) = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
import textwrap
|
||||
|
||||
from dataclasses import MISSING
|
||||
import copy
|
||||
from dataclasses import MISSING, replace, fields
|
||||
from .sexp import *
|
||||
|
||||
|
||||
|
|
@ -121,9 +122,10 @@ class WrapperType:
|
|||
return getattr(self.next_type, '__atoms__', lambda: [])()
|
||||
|
||||
class Named(WrapperType):
|
||||
def __init__(self, next_type, name=None):
|
||||
def __init__(self, next_type, name=None, omit_empty=True):
|
||||
super().__init__(next_type)
|
||||
self.name_atom = Atom(name) if name else None
|
||||
self.omit_empty = omit_empty
|
||||
|
||||
def __bind_field__(self, field):
|
||||
if self.next_type is not Atom:
|
||||
|
|
@ -140,8 +142,13 @@ class Named(WrapperType):
|
|||
|
||||
def __sexp__(self, value):
|
||||
value = sexp(self.next_type, value)
|
||||
if value is not None:
|
||||
yield [self.name_atom, *value]
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if self.omit_empty and not value:
|
||||
return
|
||||
|
||||
yield [self.name_atom, *value]
|
||||
|
||||
|
||||
class Rename(WrapperType):
|
||||
|
|
@ -389,6 +396,16 @@ class _SexpTemplate:
|
|||
def sexp(self):
|
||||
return next(self.__sexp__(self))
|
||||
|
||||
@staticmethod
|
||||
def __deepcopy__(self, memo):
|
||||
return replace(self, **{f.name: copy.deepcopy(getattr(self, f.name), memo) for f in fields(self) if not f.kw_only})
|
||||
|
||||
@staticmethod
|
||||
def __copy__(self):
|
||||
# Even during a shallow copy, we need to deep copy any fields whose types have a __before_sexp__ method to avoid
|
||||
# those from being called more than once on the same object.
|
||||
return replace(self, **{f.name: copy.copy(getattr(self, f.name)) for f in fields(self) if not f.kw_only and hasattr(f.type, '__before_sexp__')})
|
||||
|
||||
|
||||
def sexp_type(name=None):
|
||||
def register(cls):
|
||||
|
|
@ -398,8 +415,10 @@ def sexp_type(name=None):
|
|||
if not hasattr(cls, key):
|
||||
setattr(cls, key, classmethod(getattr(_SexpTemplate, key)))
|
||||
|
||||
if not hasattr(cls, 'sexp'):
|
||||
setattr(cls, 'sexp', getattr(_SexpTemplate, 'sexp'))
|
||||
for key in 'sexp', '__deepcopy__', '__copy__':
|
||||
if not hasattr(cls, key):
|
||||
setattr(cls, key, getattr(_SexpTemplate, key))
|
||||
|
||||
cls.positional = []
|
||||
cls.keys = {}
|
||||
for f in fields(cls):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue