[shapes] move to per-shape purpose-built _from_raw constructors

This commit is contained in:
Jan Petykiewicz 2026-04-02 19:55:30 -07:00
commit 37d462525c
11 changed files with 288 additions and 139 deletions

View file

@ -323,26 +323,40 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> tuple[layer_
else:
raise PatternError(f'Unrecognized path type: {gpath.path_type}')
mpath = Path(
vertices=gpath.xy.astype(float),
width=gpath.width,
cap=cap,
offset=numpy.zeros(2),
annotations=_properties_to_annotations(gpath.properties),
raw=raw_mode,
)
vertices = gpath.xy.astype(float)
annotations = _properties_to_annotations(gpath.properties)
cap_extensions = None
if cap == Path.Cap.SquareCustom:
mpath.cap_extensions = gpath.extension
cap_extensions = numpy.asarray(gpath.extension, dtype=float)
if raw_mode:
mpath = Path._from_raw(
vertices=vertices,
width=gpath.width,
cap=cap,
cap_extensions=cap_extensions,
annotations=annotations,
)
else:
mpath = Path(
vertices=vertices,
width=gpath.width,
cap=cap,
cap_extensions=cap_extensions,
offset=numpy.zeros(2),
annotations=annotations,
)
return gpath.layer, mpath
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> tuple[layer_t, Polygon]:
return boundary.layer, Polygon(
vertices=boundary.xy[:-1].astype(float),
offset=numpy.zeros(2),
annotations=_properties_to_annotations(boundary.properties),
raw=raw_mode,
)
vertices = boundary.xy[:-1].astype(float)
annotations = _properties_to_annotations(boundary.properties)
if raw_mode:
poly = Polygon._from_raw(vertices=vertices, annotations=annotations)
else:
poly = Polygon(vertices=vertices, offset=numpy.zeros(2), annotations=annotations)
return boundary.layer, poly
def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.Reference]:

View file

@ -680,8 +680,23 @@ def _gpaths_to_mpaths(
cap_extensions = None
annotations = _read_annotations(prop_offs, prop_key, prop_val, ee)
path = Path(vertices=vertices, offset=ZERO_OFFSET, annotations=annotations, raw=raw_mode,
width=width, cap=cap,cap_extensions=cap_extensions)
if raw_mode:
path = Path._from_raw(
vertices=vertices,
width=width,
cap=cap,
cap_extensions=cap_extensions,
annotations=annotations,
)
else:
path = Path(
vertices=vertices,
width=width,
cap=cap,
cap_extensions=cap_extensions,
offset=ZERO_OFFSET,
annotations=annotations,
)
pat.shapes[layer].append(path)
@ -718,13 +733,13 @@ def _boundary_batches_to_polygons(
if raw_mode:
poly = Polygon._from_raw(vertices=vertices, annotations=None)
else:
poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=None, raw=False)
poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=None)
pat.shapes[layer].append(poly)
else:
if raw_mode:
polys = PolyCollection._from_raw(vertex_lists=vertices, vertex_offsets=vertex_offsets, annotations=None)
else:
polys = PolyCollection(vertex_lists=vertices, vertex_offsets=vertex_offsets, offset=ZERO_OFFSET, annotations=None, raw=False)
polys = PolyCollection(vertex_lists=vertices, vertex_offsets=vertex_offsets, offset=ZERO_OFFSET, annotations=None)
pat.shapes[layer].append(polys)
@ -755,7 +770,7 @@ def _rect_batches_to_rectcollections(
if raw_mode:
rect_collection = RectCollection._from_raw(rects=rects, annotations=None)
else:
rect_collection = RectCollection(rects=rects, offset=ZERO_OFFSET, annotations=None, raw=False)
rect_collection = RectCollection(rects=rects, offset=ZERO_OFFSET, annotations=None)
pat.shapes[layer].append(rect_collection)
@ -790,7 +805,7 @@ def _boundary_props_to_polygons(
if raw_mode:
poly = Polygon._from_raw(vertices=vertices, annotations=annotations)
else:
poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=annotations, raw=False)
poly = Polygon(vertices=vertices, offset=ZERO_OFFSET, annotations=annotations)
pat.shapes[layer].append(poly)

View file

@ -159,27 +159,36 @@ class Arc(PositionableImpl, Shape):
rotation: float = 0,
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(radii, numpy.ndarray)
assert isinstance(angles, numpy.ndarray)
assert isinstance(offset, numpy.ndarray)
self._radii = radii
self._angles = angles
self._width = width
self._offset = offset
self._rotation = rotation
self._repetition = repetition
self._annotations = annotations
else:
self.radii = radii
self.angles = angles
self.width = width
self.offset = offset
self.rotation = rotation
self.repetition = repetition
self.annotations = annotations
self.radii = radii
self.angles = angles
self.width = width
self.offset = offset
self.rotation = rotation
self.repetition = repetition
self.annotations = annotations
@classmethod
def _from_raw(
cls,
*,
radii: NDArray[numpy.float64],
angles: NDArray[numpy.float64],
width: float,
offset: NDArray[numpy.float64],
rotation: float,
annotations: annotations_t = None,
repetition: Repetition | None = None,
) -> 'Arc':
new = cls.__new__(cls)
new._radii = radii
new._angles = angles
new._width = width
new._offset = offset
new._rotation = rotation % (2 * pi)
new._repetition = repetition
new._annotations = annotations
return new
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
memo = {} if memo is None else memo

View file

@ -50,19 +50,27 @@ class Circle(PositionableImpl, Shape):
offset: ArrayLike = (0.0, 0.0),
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(offset, numpy.ndarray)
self._radius = radius
self._offset = offset
self._repetition = repetition
self._annotations = annotations
else:
self.radius = radius
self.offset = offset
self.repetition = repetition
self.annotations = annotations
self.radius = radius
self.offset = offset
self.repetition = repetition
self.annotations = annotations
@classmethod
def _from_raw(
cls,
*,
radius: float,
offset: NDArray[numpy.float64],
annotations: annotations_t = None,
repetition: Repetition | None = None,
) -> 'Circle':
new = cls.__new__(cls)
new._radius = radius
new._offset = offset
new._repetition = repetition
new._annotations = annotations
return new
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
memo = {} if memo is None else memo

View file

@ -95,22 +95,30 @@ class Ellipse(PositionableImpl, Shape):
rotation: float = 0,
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(radii, numpy.ndarray)
assert isinstance(offset, numpy.ndarray)
self._radii = radii
self._offset = offset
self._rotation = rotation
self._repetition = repetition
self._annotations = annotations
else:
self.radii = radii
self.offset = offset
self.rotation = rotation
self.repetition = repetition
self.annotations = annotations
self.radii = radii
self.offset = offset
self.rotation = rotation
self.repetition = repetition
self.annotations = annotations
@classmethod
def _from_raw(
cls,
*,
radii: NDArray[numpy.float64],
offset: NDArray[numpy.float64],
rotation: float,
annotations: annotations_t = None,
repetition: Repetition | None = None,
) -> Self:
new = cls.__new__(cls)
new._radii = radii
new._offset = offset
new._rotation = rotation % pi
new._repetition = repetition
new._annotations = annotations
return new
def __deepcopy__(self, memo: dict | None = None) -> Self:
memo = {} if memo is None else memo

View file

@ -201,34 +201,43 @@ class Path(Shape):
rotation: float = 0,
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
self._cap_extensions = None # Since .cap setter might access it
if raw:
assert isinstance(vertices, numpy.ndarray)
assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None
self._vertices = vertices
self._repetition = repetition
self._annotations = annotations
self._width = width
self._cap = cap
self._cap_extensions = cap_extensions
self.vertices = vertices
self.repetition = repetition
self.annotations = annotations
self._cap = cap
if cap == PathCap.SquareCustom and cap_extensions is None:
self._cap_extensions = numpy.zeros(2)
else:
self.vertices = vertices
self.repetition = repetition
self.annotations = annotations
self._cap = cap
if cap == PathCap.SquareCustom and cap_extensions is None:
self._cap_extensions = numpy.zeros(2)
else:
self.cap_extensions = cap_extensions
self.width = width
self.cap_extensions = cap_extensions
self.width = width
if rotation:
self.rotate(rotation)
if numpy.any(offset):
self.translate(offset)
@classmethod
def _from_raw(
cls,
*,
vertices: NDArray[numpy.float64],
width: float,
cap: PathCap,
cap_extensions: NDArray[numpy.float64] | None = None,
annotations: annotations_t = None,
repetition: Repetition | None = None,
) -> Self:
new = cls.__new__(cls)
new._vertices = vertices
new._width = width
new._cap = cap
new._cap_extensions = cap_extensions
new._repetition = repetition
new._annotations = annotations
return new
def __deepcopy__(self, memo: dict | None = None) -> 'Path':
memo = {} if memo is None else memo
new = copy.copy(self)

View file

@ -100,21 +100,11 @@ class PolyCollection(Shape):
rotation: float = 0.0,
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(vertex_lists, numpy.ndarray)
assert isinstance(vertex_offsets, numpy.ndarray)
assert numpy.issubdtype(vertex_offsets.dtype, numpy.integer)
self._vertex_lists = vertex_lists
self._vertex_offsets = vertex_offsets
self._repetition = repetition
self._annotations = annotations
else:
self._vertex_lists = numpy.asarray(vertex_lists, dtype=float)
self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp)
self.repetition = repetition
self.annotations = annotations
self._vertex_lists = numpy.asarray(vertex_lists, dtype=float)
self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp)
self.repetition = repetition
self.annotations = annotations
if rotation:
self.rotate(rotation)
if numpy.any(offset):

View file

@ -115,17 +115,10 @@ class Polygon(Shape):
rotation: float = 0.0,
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(vertices, numpy.ndarray)
self._vertices = vertices
self._repetition = repetition
self._annotations = annotations
else:
self.vertices = vertices
self.repetition = repetition
self.annotations = annotations
self.vertices = vertices
self.repetition = repetition
self.annotations = annotations
if rotation:
self.rotate(rotation)
if numpy.any(offset):

View file

@ -86,17 +86,10 @@ class RectCollection(Shape):
rotation: float = 0.0,
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(rects, numpy.ndarray)
self._rects = rects
self._repetition = repetition
self._annotations = annotations
else:
self.rects = rects
self.repetition = repetition
self.annotations = annotations
self.rects = rects
self.repetition = repetition
self.annotations = annotations
if rotation:
self.rotate(rotation)
if numpy.any(offset):

View file

@ -73,27 +73,40 @@ class Text(PositionableImpl, RotatableImpl, Shape):
mirrored: bool = False,
repetition: Repetition | None = None,
annotations: annotations_t = None,
raw: bool = False,
) -> None:
if raw:
assert isinstance(offset, numpy.ndarray)
self._offset = offset
self._string = string
self._height = height
self._rotation = rotation
self._mirrored = mirrored
self._repetition = repetition
self._annotations = annotations
else:
self.offset = offset
self.string = string
self.height = height
self.rotation = rotation
self.mirrored = mirrored
self.repetition = repetition
self.annotations = annotations
self.offset = offset
self.string = string
self.height = height
self.rotation = rotation
self.mirrored = mirrored
self.repetition = repetition
self.annotations = annotations
self.font_path = font_path
@classmethod
def _from_raw(
cls,
*,
string: str,
height: float,
font_path: str,
offset: NDArray[numpy.float64],
rotation: float,
mirrored: bool,
annotations: annotations_t = None,
repetition: Repetition | None = None,
) -> Self:
new = cls.__new__(cls)
new._offset = offset
new._string = string
new._height = height
new._rotation = rotation % (2 * pi)
new._mirrored = mirrored
new._repetition = repetition
new._annotations = annotations
new.font_path = font_path
return new
def __deepcopy__(self, memo: dict | None = None) -> Self:
memo = {} if memo is None else memo
new = copy.copy(self)

View file

@ -0,0 +1,97 @@
import numpy
from numpy import pi
from numpy.testing import assert_allclose
from ..shapes import Arc, Circle, Ellipse, Path, Text
def test_circle_raw_constructor_matches_public() -> None:
raw = Circle._from_raw(
radius=5.0,
offset=numpy.array([1.0, 2.0]),
annotations={'1': ['circle']},
)
public = Circle(
radius=5.0,
offset=(1.0, 2.0),
annotations={'1': ['circle']},
)
assert raw == public
def test_ellipse_raw_constructor_matches_public() -> None:
raw = Ellipse._from_raw(
radii=numpy.array([3.0, 5.0]),
offset=numpy.array([1.0, 2.0]),
rotation=5 * pi / 2,
annotations={'2': ['ellipse']},
)
public = Ellipse(
radii=(3.0, 5.0),
offset=(1.0, 2.0),
rotation=5 * pi / 2,
annotations={'2': ['ellipse']},
)
assert raw == public
def test_arc_raw_constructor_matches_public() -> None:
raw = Arc._from_raw(
radii=numpy.array([10.0, 6.0]),
angles=numpy.array([0.0, pi / 2]),
width=2.0,
offset=numpy.array([1.0, 2.0]),
rotation=5 * pi / 2,
annotations={'3': ['arc']},
)
public = Arc(
radii=(10.0, 6.0),
angles=(0.0, pi / 2),
width=2.0,
offset=(1.0, 2.0),
rotation=5 * pi / 2,
annotations={'3': ['arc']},
)
assert raw == public
def test_path_raw_constructor_matches_public() -> None:
raw = Path._from_raw(
vertices=numpy.array([[0.0, 0.0], [10.0, 0.0], [10.0, 5.0]]),
width=2.0,
cap=Path.Cap.SquareCustom,
cap_extensions=numpy.array([1.0, 3.0]),
annotations={'4': ['path']},
)
public = Path(
vertices=((0.0, 0.0), (10.0, 0.0), (10.0, 5.0)),
width=2.0,
cap=Path.Cap.SquareCustom,
cap_extensions=(1.0, 3.0),
annotations={'4': ['path']},
)
assert raw == public
assert raw.cap_extensions is not None
assert_allclose(raw.cap_extensions, [1.0, 3.0])
def test_text_raw_constructor_matches_public() -> None:
raw = Text._from_raw(
string='RAW',
height=12.0,
font_path='font.otf',
offset=numpy.array([1.0, 2.0]),
rotation=5 * pi / 2,
mirrored=True,
annotations={'5': ['text']},
)
public = Text(
string='RAW',
height=12.0,
font_path='font.otf',
offset=(1.0, 2.0),
rotation=5 * pi / 2,
mirrored=True,
annotations={'5': ['text']},
)
assert raw == public