[Polygon / Path / PolyCollection] Force polygon/path offset to (0, 0)

And disallow setting it.

This offset was basically just a footgun.
This commit is contained in:
Jan Petykiewicz 2025-10-26 19:13:10 -07:00
parent ffc8dccbef
commit fe231e558a
8 changed files with 107 additions and 53 deletions

View File

@ -10,10 +10,11 @@ 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, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
from ..traits import PositionableImpl
@functools.total_ordering @functools.total_ordering
class Arc(Shape): class Arc(PositionableImpl, Shape):
""" """
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
center. It has a position, two radii, a start and stop angle, a rotation, and a width. center. It has a position, two radii, a start and stop angle, a rotation, and a width.

View File

@ -10,10 +10,11 @@ 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, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
from ..traits import PositionableImpl
@functools.total_ordering @functools.total_ordering
class Circle(Shape): class Circle(PositionableImpl, Shape):
""" """
A circle, which has a position and radius. A circle, which has a position and radius.
""" """

View File

@ -11,10 +11,11 @@ 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, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
from ..traits import PositionableImpl
@functools.total_ordering @functools.total_ordering
class Ellipse(Shape): class Ellipse(PositionableImpl, Shape):
""" """
An ellipse, which has a position, two radii, and a rotation. An ellipse, which has a position, two radii, and a rotation.
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.

View File

@ -1,4 +1,4 @@
from typing import Any, cast from typing import Any, cast, Self
from collections.abc import Sequence from collections.abc import Sequence
import copy import copy
import functools import functools
@ -30,8 +30,7 @@ class PathCap(Enum):
@functools.total_ordering @functools.total_ordering
class Path(Shape): class Path(Shape):
""" """
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, A path, consisting of a bunch of vertices (Nx2 ndarray), a width, and an end-cap shape.
and an offset.
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates. Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
@ -40,7 +39,7 @@ class Path(Shape):
__slots__ = ( __slots__ = (
'_vertices', '_width', '_cap', '_cap_extensions', '_vertices', '_width', '_cap', '_cap_extensions',
# Inherited # Inherited
'_offset', '_repetition', '_annotations', '_repetition', '_annotations',
) )
_vertices: NDArray[numpy.float64] _vertices: NDArray[numpy.float64]
_width: float _width: float
@ -160,6 +159,28 @@ class Path(Shape):
raise PatternError('Wrong number of vertices') raise PatternError('Wrong number of vertices')
self.vertices[:, 1] = val self.vertices[:, 1] = val
# Offset property for `Positionable`
@property
def offset(self) -> NDArray[numpy.float64]:
"""
[x, y] offset
"""
return numpy.zeros(2)
@offset.setter
def offset(self, val: ArrayLike) -> None:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
def set_offset(self, val: ArrayLike) -> Self:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
return self
def translate(self, offset: ArrayLike) -> Self:
self._vertices += numpy.atleast_2d(offset)
return self
def __init__( def __init__(
self, self,
vertices: ArrayLike, vertices: ArrayLike,
@ -177,10 +198,8 @@ class Path(Shape):
if raw: if raw:
assert isinstance(vertices, numpy.ndarray) assert isinstance(vertices, numpy.ndarray)
assert isinstance(offset, numpy.ndarray)
assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None
self._vertices = vertices self._vertices = vertices
self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations self._annotations = annotations
self._width = width self._width = width
@ -188,18 +207,19 @@ class Path(Shape):
self._cap_extensions = cap_extensions self._cap_extensions = cap_extensions
else: else:
self.vertices = vertices self.vertices = vertices
self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations self.annotations = annotations
self.width = width self.width = width
self.cap = cap self.cap = cap
self.cap_extensions = cap_extensions self.cap_extensions = cap_extensions
if numpy.any(offset):
self.translate(offset)
if rotation:
self.rotate(rotation) self.rotate(rotation)
def __deepcopy__(self, memo: dict | None = None) -> 'Path': def __deepcopy__(self, memo: dict | None = None) -> 'Path':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy()
new._vertices = self._vertices.copy() new._vertices = self._vertices.copy()
new._cap = copy.deepcopy(self._cap, memo) new._cap = copy.deepcopy(self._cap, memo)
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
@ -209,7 +229,6 @@ class Path(Shape):
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
return ( return (
type(self) is type(other) type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self.vertices, other.vertices) and numpy.array_equal(self.vertices, other.vertices)
and self.width == other.width and self.width == other.width
and self.cap == other.cap and self.cap == other.cap
@ -234,8 +253,6 @@ class Path(Shape):
if self.cap_extensions is None: if self.cap_extensions is None:
return True return True
return tuple(self.cap_extensions) < tuple(other.cap_extensions) return tuple(self.cap_extensions) < tuple(other.cap_extensions)
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition: if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition) return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations) return annotations_lt(self.annotations, other.annotations)
@ -292,7 +309,7 @@ class Path(Shape):
if self.width == 0: if self.width == 0:
verts = numpy.vstack((v, v[::-1])) verts = numpy.vstack((v, v[::-1]))
return [Polygon(offset=self.offset, vertices=verts)] return [Polygon(vertices=verts)]
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
@ -343,7 +360,7 @@ class Path(Shape):
o1.append(v[-1] - perp[-1]) o1.append(v[-1] - perp[-1])
verts = numpy.vstack((o0, o1[::-1])) verts = numpy.vstack((o0, o1[::-1]))
polys = [Polygon(offset=self.offset, vertices=verts)] polys = [Polygon(vertices=verts)]
if self.cap == PathCap.Circle: if self.cap == PathCap.Circle:
#for vert in v: # not sure if every vertex, or just ends? #for vert in v: # not sure if every vertex, or just ends?
@ -355,7 +372,7 @@ class Path(Shape):
def get_bounds_single(self) -> NDArray[numpy.float64]: def get_bounds_single(self) -> NDArray[numpy.float64]:
if self.cap == PathCap.Circle: if self.cap == PathCap.Circle:
bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, bounds = numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
numpy.max(self.vertices, axis=0) + self.width / 2)) numpy.max(self.vertices, axis=0) + self.width / 2))
elif self.cap in ( elif self.cap in (
PathCap.Flush, PathCap.Flush,
@ -390,7 +407,7 @@ class Path(Shape):
def normalized_form(self, norm_value: float) -> normalized_shape_tuple: def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
# Note: this function is going to be pretty slow for many-vertexed paths, relative to # Note: this function is going to be pretty slow for many-vertexed paths, relative to
# other shapes # other shapes
offset = self.vertices.mean(axis=0) + self.offset offset = self.vertices.mean(axis=0)
zeroed_vertices = self.vertices - offset zeroed_vertices = self.vertices - offset
scale = zeroed_vertices.std() scale = zeroed_vertices.std()
@ -460,5 +477,5 @@ class Path(Shape):
return extensions return extensions
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0) centroid = self.vertices.mean(axis=0)
return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>' return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'

View File

@ -10,6 +10,7 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple from . import Shape, normalized_shape_tuple
from .polygon import Polygon from .polygon import Polygon
from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import rotation_matrix_2d, annotations_lt, annotations_eq, rep2key, annotations_t from ..utils import rotation_matrix_2d, annotations_lt, annotations_eq, rep2key, annotations_t
@ -27,7 +28,7 @@ class PolyCollection(Shape):
'_vertex_lists', '_vertex_lists',
'_vertex_offsets', '_vertex_offsets',
# Inherited # Inherited
'_offset', '_repetition', '_annotations', '_repetition', '_annotations',
) )
_vertex_lists: NDArray[numpy.float64] _vertex_lists: NDArray[numpy.float64]
@ -67,6 +68,27 @@ class PolyCollection(Shape):
for slc in self.vertex_slices: for slc in self.vertex_slices:
yield self._vertex_lists[slc] yield self._vertex_lists[slc]
# Offset property for `Positionable`
@property
def offset(self) -> NDArray[numpy.float64]:
"""
[x, y] offset
"""
return numpy.zeros(2)
@offset.setter
def offset(self, val: ArrayLike) -> None:
raise PatternError('PolyCollection offset is forced to (0, 0)')
def set_offset(self, val: ArrayLike) -> Self:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
return self
def translate(self, offset: ArrayLike) -> Self:
self._vertex_lists += numpy.atleast_2d(offset)
return self
def __init__( def __init__(
self, self,
vertex_lists: ArrayLike, vertex_lists: ArrayLike,
@ -81,25 +103,23 @@ class PolyCollection(Shape):
if raw: if raw:
assert isinstance(vertex_lists, numpy.ndarray) assert isinstance(vertex_lists, numpy.ndarray)
assert isinstance(vertex_offsets, numpy.ndarray) assert isinstance(vertex_offsets, numpy.ndarray)
assert isinstance(offset, numpy.ndarray)
self._vertex_lists = vertex_lists self._vertex_lists = vertex_lists
self._vertex_offsets = vertex_offsets self._vertex_offsets = vertex_offsets
self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations self._annotations = annotations
else: else:
self._vertex_lists = numpy.asarray(vertex_lists, dtype=float) self._vertex_lists = numpy.asarray(vertex_lists, dtype=float)
self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp) self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp)
self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations self.annotations = annotations
if numpy.any(offset):
self.translate(offset)
if rotation: if rotation:
self.rotate(rotation) self.rotate(rotation)
def __deepcopy__(self, memo: dict | None = None) -> Self: def __deepcopy__(self, memo: dict | None = None) -> Self:
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy()
new._vertex_lists = self._vertex_lists.copy() new._vertex_lists = self._vertex_lists.copy()
new._vertex_offsets = self._vertex_offsets.copy() new._vertex_offsets = self._vertex_offsets.copy()
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
@ -108,7 +128,6 @@ class PolyCollection(Shape):
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
return ( return (
type(self) is type(other) type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self._vertex_lists, other._vertex_lists) and numpy.array_equal(self._vertex_lists, other._vertex_lists)
and numpy.array_equal(self._vertex_offsets, other._vertex_offsets) and numpy.array_equal(self._vertex_offsets, other._vertex_offsets)
and self.repetition == other.repetition and self.repetition == other.repetition
@ -134,8 +153,6 @@ class PolyCollection(Shape):
return vv.shape[0] < oo.shape[0] return vv.shape[0] < oo.shape[0]
if len(self.vertex_lists) != len(other.vertex_lists): if len(self.vertex_lists) != len(other.vertex_lists):
return len(self.vertex_lists) < len(other.vertex_lists) return len(self.vertex_lists) < len(other.vertex_lists)
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition: if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition) return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations) return annotations_lt(self.annotations, other.annotations)
@ -147,14 +164,13 @@ class PolyCollection(Shape):
) -> list['Polygon']: ) -> list['Polygon']:
return [Polygon( return [Polygon(
vertices = vv, vertices = vv,
offset = self.offset,
repetition = copy.deepcopy(self.repetition), repetition = copy.deepcopy(self.repetition),
annotations = copy.deepcopy(self.annotations), annotations = copy.deepcopy(self.annotations),
) for vv in self.polygon_vertices] ) for vv in self.polygon_vertices]
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
return numpy.vstack((self.offset + numpy.min(self._vertex_lists, axis=0), return numpy.vstack((numpy.min(self._vertex_lists, axis=0),
self.offset + numpy.max(self._vertex_lists, axis=0))) numpy.max(self._vertex_lists, axis=0)))
def rotate(self, theta: float) -> Self: def rotate(self, theta: float) -> Self:
if theta != 0: if theta != 0:
@ -175,7 +191,7 @@ class PolyCollection(Shape):
# other shapes # other shapes
meanv = self._vertex_lists.mean(axis=0) meanv = self._vertex_lists.mean(axis=0)
zeroed_vertices = self._vertex_lists - [meanv] zeroed_vertices = self._vertex_lists - [meanv]
offset = meanv + self.offset offset = meanv
scale = zeroed_vertices.std() scale = zeroed_vertices.std()
normed_vertices = zeroed_vertices / scale normed_vertices = zeroed_vertices / scale
@ -203,5 +219,5 @@ class PolyCollection(Shape):
) )
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertex_lists.mean(axis=0) centroid = self.vertex_lists.mean(axis=0)
return f'<PolyCollection centroid {centroid} p{len(self.vertex_offsets)}>' return f'<PolyCollection centroid {centroid} p{len(self.vertex_offsets)}>'

View File

@ -1,4 +1,4 @@
from typing import Any, cast, TYPE_CHECKING from typing import Any, cast, TYPE_CHECKING, Self
import copy import copy
import functools import functools
@ -20,7 +20,7 @@ if TYPE_CHECKING:
class Polygon(Shape): class Polygon(Shape):
""" """
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
implicitly-closed boundary, and an offset. implicitly-closed boundary.
Note that the setter for `Polygon.vertices` creates a copy of the Note that the setter for `Polygon.vertices` creates a copy of the
passed vertex coordinates. passed vertex coordinates.
@ -30,7 +30,7 @@ class Polygon(Shape):
__slots__ = ( __slots__ = (
'_vertices', '_vertices',
# Inherited # Inherited
'_offset', '_repetition', '_annotations', '_repetition', '_annotations',
) )
_vertices: NDArray[numpy.float64] _vertices: NDArray[numpy.float64]
@ -85,6 +85,28 @@ class Polygon(Shape):
raise PatternError('Wrong number of vertices') raise PatternError('Wrong number of vertices')
self.vertices[:, 1] = val self.vertices[:, 1] = val
# Offset property for `Positionable`
@property
def offset(self) -> NDArray[numpy.float64]:
"""
[x, y] offset
"""
return numpy.zeros(2)
@offset.setter
def offset(self, val: ArrayLike) -> None:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
def set_offset(self, val: ArrayLike) -> Self:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
return self
def translate(self, offset: ArrayLike) -> Self:
self._vertices += numpy.atleast_2d(offset)
return self
def __init__( def __init__(
self, self,
vertices: ArrayLike, vertices: ArrayLike,
@ -99,21 +121,20 @@ class Polygon(Shape):
assert isinstance(vertices, numpy.ndarray) assert isinstance(vertices, numpy.ndarray)
assert isinstance(offset, numpy.ndarray) assert isinstance(offset, numpy.ndarray)
self._vertices = vertices self._vertices = vertices
self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations self._annotations = annotations
else: else:
self.vertices = vertices self.vertices = vertices
self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations self.annotations = annotations
if numpy.any(offset):
self.translate(offset)
if rotation: if rotation:
self.rotate(rotation) self.rotate(rotation)
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon': def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy()
new._vertices = self._vertices.copy() new._vertices = self._vertices.copy()
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new return new
@ -121,7 +142,6 @@ class Polygon(Shape):
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
return ( return (
type(self) is type(other) type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self.vertices, other.vertices) and numpy.array_equal(self.vertices, other.vertices)
and self.repetition == other.repetition and self.repetition == other.repetition
and annotations_eq(self.annotations, other.annotations) and annotations_eq(self.annotations, other.annotations)
@ -141,8 +161,6 @@ class Polygon(Shape):
if eq_lt_masked.size > 0: if eq_lt_masked.size > 0:
return eq_lt_masked.flat[0] return eq_lt_masked.flat[0]
return self.vertices.shape[0] < other.vertices.shape[0] return self.vertices.shape[0] < other.vertices.shape[0]
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition: if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition) return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations) return annotations_lt(self.annotations, other.annotations)
@ -363,8 +381,8 @@ class Polygon(Shape):
return [copy.deepcopy(self)] return [copy.deepcopy(self)]
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0), return numpy.vstack((numpy.min(self.vertices, axis=0),
self.offset + numpy.max(self.vertices, axis=0))) numpy.max(self.vertices, axis=0)))
def rotate(self, theta: float) -> 'Polygon': def rotate(self, theta: float) -> 'Polygon':
if theta != 0: if theta != 0:
@ -384,7 +402,7 @@ class Polygon(Shape):
# other shapes # other shapes
meanv = self.vertices.mean(axis=0) meanv = self.vertices.mean(axis=0)
zeroed_vertices = self.vertices - meanv zeroed_vertices = self.vertices - meanv
offset = meanv + self.offset offset = meanv
scale = zeroed_vertices.std() scale = zeroed_vertices.std()
normed_vertices = zeroed_vertices / scale normed_vertices = zeroed_vertices / scale
@ -438,5 +456,5 @@ class Polygon(Shape):
return self return self
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0) centroid = self.vertices.mean(axis=0)
return f'<Polygon centroid {centroid} v{len(self.vertices)}>' return f'<Polygon centroid {centroid} v{len(self.vertices)}>'

View File

@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike
from ..traits import ( from ..traits import (
Rotatable, Mirrorable, Copyable, Scalable, Rotatable, Mirrorable, Copyable, Scalable,
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl, Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -26,7 +26,7 @@ normalized_shape_tuple = tuple[
DEFAULT_POLY_NUM_VERTICES = 24 DEFAULT_POLY_NUM_VERTICES = 24
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta): PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
""" """
Class specifying functions common to all shapes. Class specifying functions common to all shapes.
@ -134,7 +134,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
mins, maxs = bounds mins, maxs = bounds
vertex_lists = [] vertex_lists = []
p_verts = polygon.vertices + polygon.offset p_verts = polygon.vertices
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True): for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True):
dv = v_next - v dv = v_next - v
@ -282,7 +282,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
offset = (numpy.where(keep_x)[0][0], offset = (numpy.where(keep_x)[0][0],
numpy.where(keep_y)[0][0]) numpy.where(keep_y)[0][0])
rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy) rastered = float_raster.raster((polygon.vertices).T, gx, gy)
binary_rastered = (numpy.abs(rastered) >= 0.5) binary_rastered = (numpy.abs(rastered) >= 0.5)
supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1) supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1)

View File

@ -9,7 +9,7 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..traits import RotatableImpl from ..traits import PositionableImpl, RotatableImpl
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool
# Loaded on use: # Loaded on use:
@ -18,7 +18,7 @@ from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotatio
@functools.total_ordering @functools.total_ordering
class Text(RotatableImpl, Shape): class Text(PositionableImpl, RotatableImpl, Shape):
""" """
Text (to be printed e.g. as a set of polygons). Text (to be printed e.g. as a set of polygons).
This is distinct from non-printed Label objects. This is distinct from non-printed Label objects.