remove per-shape polygonization state

This commit is contained in:
Jan Petykiewicz 2023-02-23 11:25:40 -08:00 committed by jan
parent 7befe89af3
commit 9d42df831e
10 changed files with 71 additions and 107 deletions

View File

@ -9,7 +9,7 @@ from numpy.typing import ArrayLike
from ..pattern import Pattern, NamedPattern from ..pattern import Pattern, NamedPattern
from ..ref import Ref from ..ref import Ref
from ..library import MutableLibrary from ..library import MutableLibrary, Tree
from ..error import PortError, BuildError from ..error import PortError, BuildError
from ..ports import PortList, Port from ..ports import PortList, Port
from ..abstract import Abstract from ..abstract import Abstract
@ -492,7 +492,6 @@ class Builder(PortList):
return s return s
class Pather(Builder): class Pather(Builder):
""" """
TODO DOCUMENT Builder TODO DOCUMENT Builder
@ -657,7 +656,8 @@ class Pather(Builder):
if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict): if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict):
tools = source.tools tools = source.tools
new = Pather.from_builder(Builder.interface( new = Pather.from_builder(
Builder.interface(
source=source, source=source,
library=library, library=library,
in_prefix=in_prefix, in_prefix=in_prefix,

View File

@ -214,18 +214,18 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def polygonize( def polygonize(
self: P, self: P,
poly_num_points: Optional[int] = None, num_points: Optional[int] = None,
poly_max_arclen: Optional[float] = None, max_arclen: Optional[float] = None,
) -> P: ) -> P:
""" """
Calls `.to_polygons(...)` on all the shapes in this Pattern, replacing them with the returned polygons. Calls `.to_polygons(...)` on all the shapes in this Pattern, replacing them with the returned polygons.
Arguments are passed directly to `shape.to_polygons(...)`. Arguments are passed directly to `shape.to_polygons(...)`.
Args: Args:
poly_num_points: Number of points to use for each polygon. Can be overridden by num_points: Number of points to use for each polygon. Can be overridden by
`poly_max_arclen` if that results in more points. Optional, defaults to shapes' `max_arclen` if that results in more points. Optional, defaults to shapes'
internal defaults. internal defaults.
poly_max_arclen: Maximum arclength which can be approximated by a single line max_arclen: Maximum arclength which can be approximated by a single line
segment. Optional, defaults to shapes' internal defaults. segment. Optional, defaults to shapes' internal defaults.
Returns: Returns:
@ -233,7 +233,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
""" """
old_shapes = self.shapes old_shapes = self.shapes
self.shapes = list(chain.from_iterable(( self.shapes = list(chain.from_iterable((
shape.to_polygons(poly_num_points, poly_max_arclen) shape.to_polygons(num_points, max_arclen)
for shape in old_shapes))) for shape in old_shapes)))
return self return self
@ -713,7 +713,7 @@ class NamedPattern(Pattern):
def __deepcopy__(self, memo: Optional[Dict] = None) -> Pattern: def __deepcopy__(self, memo: Optional[Dict] = None) -> Pattern:
return Pattern.__deepcopy__(self, memo) return Pattern.__deepcopy__(self, memo)
def as_pattern(self) -> Pattern: def as_pattern(self) -> Pattern:
return Pattern( return Pattern(
shapes=self.shapes, shapes=self.shapes,
labels=self.labels, labels=self.labels,

View File

@ -3,7 +3,7 @@ Shapes for use with the Pattern class, as well as the Shape abstract class from
which they are derived. which they are derived.
""" """
from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from .polygon import Polygon from .polygon import Polygon
from .circle import Circle from .circle import Circle

View File

@ -6,7 +6,7 @@ import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, layer_t, annotations_t from ..utils import is_scalar, layer_t, annotations_t
@ -23,7 +23,6 @@ class Arc(Shape):
""" """
__slots__ = ( __slots__ = (
'_radii', '_angles', '_width', '_rotation', '_radii', '_angles', '_width', '_rotation',
'poly_num_points', 'poly_max_arclen',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_layer', '_repetition', '_annotations',
) )
@ -40,12 +39,6 @@ class Arc(Shape):
_width: float _width: float
""" Width of the arc """ """ Width of the arc """
poly_num_points: Optional[int]
""" Sets the default number of points for `.polygonize()` """
poly_max_arclen: Optional[float]
""" Sets the default max segement length for `.polygonize()` """
# radius properties # radius properties
@property @property
def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]: def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]:
@ -160,8 +153,6 @@ class Arc(Shape):
angles: ArrayLike, angles: ArrayLike,
width: float, width: float,
*, *,
poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: Optional[float] = None,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
@ -191,8 +182,6 @@ class Arc(Shape):
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Arc': def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Arc':
@ -206,15 +195,10 @@ class Arc(Shape):
def to_polygons( def to_polygons(
self, self,
poly_num_points: Optional[int] = None, num_vertices: Optional[int] = DEFAULT_POLY_NUM_VERTICES,
poly_max_arclen: Optional[float] = None, max_arclen: Optional[float] = None,
) -> List[Polygon]: ) -> List[Polygon]:
if poly_num_points is None: if (num_vertices is None) and (max_arclen is None):
poly_num_points = self.poly_num_points
if poly_max_arclen is None:
poly_max_arclen = self.poly_max_arclen
if (poly_num_points is None) and (poly_max_arclen is None):
raise PatternError('Max number of points and arclength left unspecified' raise PatternError('Max number of points and arclength left unspecified'
+ ' (default was also overridden)') + ' (default was also overridden)')
@ -232,18 +216,18 @@ class Arc(Shape):
perimeter = abs(a0 - a1) / (2 * pi) * ellipse_perimeter # TODO: make this more accurate perimeter = abs(a0 - a1) / (2 * pi) * ellipse_perimeter # TODO: make this more accurate
n = [] n = []
if poly_num_points is not None: if num_vertices is not None:
n += [poly_num_points] n += [num_vertices]
if poly_max_arclen is not None: if max_arclen is not None:
n += [perimeter / poly_max_arclen] n += [perimeter / max_arclen]
num_points = int(round(max(n))) num_vertices = int(round(max(n)))
wh = self.width / 2.0 wh = self.width / 2.0
if wh == r0 or wh == r1: if wh == r0 or wh == r1:
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
else: else:
thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_points, endpoint=True) thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], num_vertices, endpoint=True)
thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], num_points, endpoint=True) thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], num_vertices, endpoint=True)
sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner)) sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner))
sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer)) sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer))
@ -370,7 +354,12 @@ class Arc(Shape):
return ((type(self), radii, angles, width / norm_value, self.layer), return ((type(self), radii, angles, width / norm_value, self.layer),
(self.offset, scale / norm_value, rotation, False), (self.offset, scale / norm_value, rotation, False),
lambda: Arc(radii=radii * norm_value, angles=angles, width=width * norm_value, layer=self.layer)) lambda: Arc(
radii=radii * norm_value,
angles=angles,
width=width * norm_value,
layer=self.layer,
))
def get_cap_edges(self) -> NDArray[numpy.float64]: def get_cap_edges(self) -> NDArray[numpy.float64]:
''' '''

View File

@ -5,7 +5,7 @@ import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, layer_t, annotations_t from ..utils import is_scalar, layer_t, annotations_t
@ -16,7 +16,7 @@ class Circle(Shape):
A circle, which has a position and radius. A circle, which has a position and radius.
""" """
__slots__ = ( __slots__ = (
'_radius', 'poly_num_points', 'poly_max_arclen', '_radius',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_layer', '_repetition', '_annotations',
) )
@ -24,12 +24,6 @@ class Circle(Shape):
_radius: float _radius: float
""" Circle radius """ """ Circle radius """
poly_num_points: Optional[int]
""" Sets the default number of points for `.polygonize()` """
poly_max_arclen: Optional[float]
""" Sets the default max segement length for `.polygonize()` """
# radius property # radius property
@property @property
def radius(self) -> float: def radius(self) -> float:
@ -50,8 +44,6 @@ class Circle(Shape):
self, self,
radius: float, radius: float,
*, *,
poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: Optional[float] = None,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0, layer: layer_t = 0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
@ -71,8 +63,6 @@ class Circle(Shape):
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Circle': def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Circle':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -83,25 +73,20 @@ class Circle(Shape):
def to_polygons( def to_polygons(
self, self,
poly_num_points: Optional[int] = None, num_vertices: Optional[int] = DEFAULT_POLY_NUM_VERTICES,
poly_max_arclen: Optional[float] = None, max_arclen: Optional[float] = None,
) -> List[Polygon]: ) -> List[Polygon]:
if poly_num_points is None: if (num_vertices is None) and (max_arclen is None):
poly_num_points = self.poly_num_points
if poly_max_arclen is None:
poly_max_arclen = self.poly_max_arclen
if (poly_num_points is None) and (poly_max_arclen is None):
raise PatternError('Number of points and arclength left ' raise PatternError('Number of points and arclength left '
'unspecified (default was also overridden)') 'unspecified (default was also overridden)')
n: List[float] = [] n: List[float] = []
if poly_num_points is not None: if num_vertices is not None:
n += [poly_num_points] n += [num_vertices]
if poly_max_arclen is not None: if max_arclen is not None:
n += [2 * pi * self.radius / poly_max_arclen] n += [2 * pi * self.radius / max_arclen]
num_points = int(round(max(n))) num_vertices = int(round(max(n)))
thetas = numpy.linspace(2 * pi, 0, num_points, endpoint=False) thetas = numpy.linspace(2 * pi, 0, num_vertices, endpoint=False)
xs = numpy.cos(thetas) * self.radius xs = numpy.cos(thetas) * self.radius
ys = numpy.sin(thetas) * self.radius ys = numpy.sin(thetas) * self.radius
xys = numpy.vstack((xs, ys)).T xys = numpy.vstack((xs, ys)).T

View File

@ -6,7 +6,7 @@ import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, layer_t, annotations_t from ..utils import is_scalar, rotation_matrix_2d, layer_t, annotations_t
@ -19,7 +19,6 @@ class Ellipse(Shape):
""" """
__slots__ = ( __slots__ = (
'_radii', '_rotation', '_radii', '_rotation',
'poly_num_points', 'poly_max_arclen',
# Inherited # Inherited
'_offset', '_layer', '_repetition', '_annotations', '_offset', '_layer', '_repetition', '_annotations',
) )
@ -30,12 +29,6 @@ class Ellipse(Shape):
_rotation: float _rotation: float
""" Angle from x-axis to first radius (ccw, radians) """ """ Angle from x-axis to first radius (ccw, radians) """
poly_num_points: Optional[int]
""" Sets the default number of points for `.polygonize()` """
poly_max_arclen: Optional[float]
""" Sets the default max segement length for `.polygonize()` """
# radius properties # radius properties
@property @property
def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]: def radii(self) -> Any: # TODO mypy#3004 NDArray[numpy.float64]:
@ -95,8 +88,6 @@ class Ellipse(Shape):
self, self,
radii: ArrayLike, radii: ArrayLike,
*, *,
poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
poly_max_arclen: Optional[float] = None,
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
@ -122,8 +113,6 @@ class Ellipse(Shape):
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
[self.mirror(a) for a, do in enumerate(mirrored) if do] [self.mirror(a) for a, do in enumerate(mirrored) if do]
self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Ellipse': def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Ellipse':
memo = {} if memo is None else memo memo = {} if memo is None else memo
@ -135,15 +124,10 @@ class Ellipse(Shape):
def to_polygons( def to_polygons(
self, self,
poly_num_points: Optional[int] = None, num_vertices: Optional[int] = DEFAULT_POLY_NUM_VERTICES,
poly_max_arclen: Optional[float] = None, max_arclen: Optional[float] = None,
) -> List[Polygon]: ) -> List[Polygon]:
if poly_num_points is None: if (num_vertices is None) and (max_arclen is None):
poly_num_points = self.poly_num_points
if poly_max_arclen is None:
poly_max_arclen = self.poly_max_arclen
if (poly_num_points is None) and (poly_max_arclen is None):
raise PatternError('Number of points and arclength left unspecified' raise PatternError('Number of points and arclength left unspecified'
' (default was also overridden)') ' (default was also overridden)')
@ -156,12 +140,12 @@ class Ellipse(Shape):
perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h)))
n = [] n = []
if poly_num_points is not None: if num_vertices is not None:
n += [poly_num_points] n += [num_vertices]
if poly_max_arclen is not None: if max_arclen is not None:
n += [perimeter / poly_max_arclen] n += [perimeter / max_arclen]
num_points = int(round(max(n))) num_vertices = int(round(max(n)))
thetas = numpy.linspace(2 * pi, 0, num_points, endpoint=False) thetas = numpy.linspace(2 * pi, 0, num_vertices, endpoint=False)
sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas))
xs = r0 * cos_th xs = r0 * cos_th

View File

@ -243,8 +243,8 @@ class Path(Shape):
def to_polygons( def to_polygons(
self, self,
poly_num_points: Optional[int] = None, num_vertices: Optional[int] = None,
poly_max_arclen: Optional[float] = None, max_arclen: Optional[float] = None,
) -> List['Polygon']: ) -> List['Polygon']:
extensions = self._calculate_cap_extensions() extensions = self._calculate_cap_extensions()
@ -311,7 +311,7 @@ class Path(Shape):
#for vert in v: # not sure if every vertex, or just ends? #for vert in v: # not sure if every vertex, or just ends?
for vert in [v[0], v[-1]]: for vert in [v[0], v[-1]]:
circ = Circle(offset=vert, radius=self.width / 2, layer=self.layer) circ = Circle(offset=vert, radius=self.width / 2, layer=self.layer)
polys += circ.to_polygons(poly_num_points=poly_num_points, poly_max_arclen=poly_max_arclen) polys += circ.to_polygons(num_vertices=num_vertices, max_arclen=max_arclen)
return polys return polys
@ -372,8 +372,12 @@ class Path(Shape):
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer),
(offset, scale / norm_value, rotation, False), (offset, scale / norm_value, rotation, False),
lambda: Path(reordered_vertices * norm_value, width=self.width * norm_value, lambda: Path(
cap=self.cap, layer=self.layer)) reordered_vertices * norm_value,
width=self.width * norm_value,
cap=self.cap,
layer=self.layer,
))
def clean_vertices(self) -> 'Path': def clean_vertices(self) -> 'Path':
""" """

View File

@ -333,8 +333,8 @@ class Polygon(Shape):
def to_polygons( def to_polygons(
self, self,
poly_num_points: Optional[int] = None, # unused num_vertices: Optional[int] = None, # unused
poly_max_arclen: Optional[float] = None, # unused max_arclen: Optional[float] = None, # unused
) -> List['Polygon']: ) -> List['Polygon']:
return [copy.deepcopy(self)] return [copy.deepcopy(self)]

View File

@ -23,7 +23,7 @@ normalized_shape_tuple = Tuple[
# ## Module-wide defaults # ## Module-wide defaults
# Default number of points per polygon for shapes # Default number of points per polygon for shapes
DEFAULT_POLY_NUM_POINTS = 24 DEFAULT_POLY_NUM_VERTICES = 24
T = TypeVar('T', bound='Shape') T = TypeVar('T', bound='Shape')

View File

@ -110,8 +110,8 @@ class Text(RotatableImpl, Shape):
def to_polygons( def to_polygons(
self, self,
poly_num_points: Optional[int] = None, # unused num_vertices: Optional[int] = None, # unused
poly_max_arclen: Optional[float] = None, # unused max_arclen: Optional[float] = None, # unused
) -> List[Polygon]: ) -> List[Polygon]:
all_polygons = [] all_polygons = []
total_advance = 0.0 total_advance = 0.0
@ -146,12 +146,14 @@ class Text(RotatableImpl, Shape):
rotation %= 2 * pi rotation %= 2 * pi
return ((type(self), self.string, self.font_path, self.layer), return ((type(self), self.string, self.font_path, self.layer),
(self.offset, self.height / norm_value, rotation, mirror_x), (self.offset, self.height / norm_value, rotation, mirror_x),
lambda: Text(string=self.string, lambda: Text(
height=self.height * norm_value, string=self.string,
font_path=self.font_path, height=self.height * norm_value,
rotation=rotation, font_path=self.font_path,
mirrored=(mirror_x, False), rotation=rotation,
layer=self.layer)) mirrored=(mirror_x, False),
layer=self.layer,
))
def get_bounds(self) -> NDArray[numpy.float64]: def get_bounds(self) -> NDArray[numpy.float64]:
# rotation makes this a huge pain when using slot.advance and glyph.bbox(), so # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so