remove per-shape polygonization state

This commit is contained in:
Jan Petykiewicz 2023-02-23 11:25:40 -08:00
parent 7a4a96ff5f
commit 23c64b4f63
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 ..ref import Ref
from ..library import MutableLibrary
from ..library import MutableLibrary, Tree
from ..error import PortError, BuildError
from ..ports import PortList, Port
from ..abstract import Abstract
@ -492,7 +492,6 @@ class Builder(PortList):
return s
class Pather(Builder):
"""
TODO DOCUMENT Builder
@ -657,7 +656,8 @@ class Pather(Builder):
if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict):
tools = source.tools
new = Pather.from_builder(Builder.interface(
new = Pather.from_builder(
Builder.interface(
source=source,
library=library,
in_prefix=in_prefix,

View File

@ -214,18 +214,18 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
def polygonize(
self: P,
poly_num_points: Optional[int] = None,
poly_max_arclen: Optional[float] = None,
num_points: Optional[int] = None,
max_arclen: Optional[float] = None,
) -> P:
"""
Calls `.to_polygons(...)` on all the shapes in this Pattern, replacing them with the returned polygons.
Arguments are passed directly to `shape.to_polygons(...)`.
Args:
poly_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'
num_points: Number of points to use for each polygon. Can be overridden by
`max_arclen` if that results in more points. Optional, defaults to shapes'
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.
Returns:
@ -233,7 +233,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
"""
old_shapes = self.shapes
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)))
return self
@ -713,7 +713,7 @@ class NamedPattern(Pattern):
def __deepcopy__(self, memo: Optional[Dict] = None) -> Pattern:
return Pattern.__deepcopy__(self, memo)
def as_pattern(self) -> Pattern:
def as_pattern(self) -> Pattern:
return Pattern(
shapes=self.shapes,
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.
"""
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 .circle import Circle

View File

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

View File

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

View File

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

View File

@ -243,8 +243,8 @@ class Path(Shape):
def to_polygons(
self,
poly_num_points: Optional[int] = None,
poly_max_arclen: Optional[float] = None,
num_vertices: Optional[int] = None,
max_arclen: Optional[float] = None,
) -> List['Polygon']:
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[0], v[-1]]:
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
@ -372,8 +372,12 @@ class Path(Shape):
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer),
(offset, scale / norm_value, rotation, False),
lambda: Path(reordered_vertices * norm_value, width=self.width * norm_value,
cap=self.cap, layer=self.layer))
lambda: Path(
reordered_vertices * norm_value,
width=self.width * norm_value,
cap=self.cap,
layer=self.layer,
))
def clean_vertices(self) -> 'Path':
"""

View File

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

View File

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

View File

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