renderpather, get_bounds includes repetitions, Boundable
This commit is contained in:
parent
41dd123efe
commit
d14d5438a4
@ -41,7 +41,7 @@ from .library import (
|
||||
)
|
||||
from .ports import Port, PortList
|
||||
from .abstract import Abstract
|
||||
from .builder import Builder, Tool, Pather, RenderPather, render_step_t
|
||||
from .builder import Builder, Tool, Pather, RenderPather, RenderStep
|
||||
from .utils import ports2data, oneshot
|
||||
|
||||
|
||||
|
@ -2,4 +2,4 @@ from .builder import Builder
|
||||
from .pather import Pather
|
||||
from .renderpather import RenderPather
|
||||
from .utils import ell
|
||||
from .tools import Tool, render_step_t
|
||||
from .tools import Tool, RenderStep
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Self, Sequence, Mapping, Final
|
||||
from typing import Self, Sequence, Mapping
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
@ -15,7 +15,7 @@ from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
from ..utils import rotation_matrix_2d
|
||||
from ..utils import SupportsBool
|
||||
from .tools import Tool, render_step_t
|
||||
from .tools import Tool, RenderStep
|
||||
from .utils import ell
|
||||
from .builder import Builder
|
||||
|
||||
@ -35,8 +35,7 @@ class RenderPather(PortList):
|
||||
_dead: bool
|
||||
""" If True, plug()/place() are skipped (for debugging) """
|
||||
|
||||
paths: defaultdict[str, list[render_step_t]]
|
||||
# op, start_port, dx, dy, o_ptype tool
|
||||
paths: defaultdict[str, list[RenderStep]]
|
||||
|
||||
tools: dict[str | None, Tool]
|
||||
"""
|
||||
@ -228,11 +227,10 @@ class RenderPather(PortList):
|
||||
|
||||
# get rid of plugged ports
|
||||
for ki, vi in map_in.items():
|
||||
if ki in self.paths:
|
||||
self.paths[ki].append(RenderStep('P', None, self.ports[ki].copy(), None))
|
||||
del self.ports[ki]
|
||||
map_out[vi] = None
|
||||
if ki in self.paths:
|
||||
self.paths[ki].append(('P', None, 0.0, 0.0, 'unk', None))
|
||||
|
||||
self.place(other, offset=translation, rotation=rotation, pivot=pivot,
|
||||
mirrored=mirrored, port_map=map_out, skip_port_check=True)
|
||||
return self
|
||||
@ -270,7 +268,7 @@ class RenderPather(PortList):
|
||||
continue
|
||||
ports[new_name] = port
|
||||
if new_name in self.paths:
|
||||
self.paths[new_name].append(('P', None, 0.0, 0.0, 'unk', None))
|
||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), None))
|
||||
|
||||
for name, port in ports.items():
|
||||
p = port.deepcopy()
|
||||
@ -303,18 +301,9 @@ class RenderPather(PortList):
|
||||
|
||||
tool = self.tools.get(portspec, self.tools[None])
|
||||
# ask the tool for bend size (fill missing dx or dy), check feasibility, and get out_ptype
|
||||
bend_radius, out_ptype = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
||||
data = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
||||
|
||||
if ccw is None:
|
||||
bend_run = 0.0
|
||||
elif bool(ccw):
|
||||
bend_run = bend_radius
|
||||
else:
|
||||
bend_run = -bend_radius
|
||||
|
||||
dx, dy = rotation_matrix_2d(port_rot + pi) @ [length, bend_run]
|
||||
|
||||
step: Final = ('L', port.deepcopy(), dx, dy, out_ptype, tool)
|
||||
step = RenderStep('L', tool, port.copy(), data)
|
||||
self.paths[portspec].append(step)
|
||||
|
||||
# Update port
|
||||
@ -415,19 +404,19 @@ class RenderPather(PortList):
|
||||
bb = Builder(lib)
|
||||
|
||||
for portspec, steps in self.paths.items():
|
||||
batch: list[render_step_t] = []
|
||||
batch: list[RenderStep] = []
|
||||
tool0 = batch[0].tool
|
||||
port0 = batch[0].start_port
|
||||
assert tool0 is not None
|
||||
for step in steps:
|
||||
opcode, _start_port, _dx, _dy, _out_ptype, tool = step
|
||||
|
||||
appendable_op = opcode in ('L', 'S', 'U')
|
||||
same_tool = batch and tool == batch[-1]
|
||||
appendable_op = step.opcode in ('L', 'S', 'U')
|
||||
same_tool = batch and step.tool == tool0
|
||||
|
||||
if batch and (not appendable_op or not same_tool):
|
||||
# If we can't continue a batch, render it
|
||||
assert tool is not None
|
||||
assert batch[0][1] is not None
|
||||
name = lib << tool.render(batch, portnames=tool_port_names)
|
||||
bb.ports[portspec] = batch[0][1]
|
||||
assert batch[0].tool is not None
|
||||
name = lib << batch[0].tool.render(batch, port_names=tool_port_names)
|
||||
bb.ports[portspec] = port0.copy()
|
||||
bb.plug(name, {portspec: tool_port_names[0]})
|
||||
batch = []
|
||||
|
||||
@ -441,10 +430,9 @@ class RenderPather(PortList):
|
||||
|
||||
if batch:
|
||||
# A batch didn't end yet
|
||||
assert tool is not None
|
||||
assert batch[0][1] is not None
|
||||
name = lib << tool.render(batch, portnames=tool_port_names)
|
||||
bb.ports[portspec] = batch[0][1]
|
||||
assert batch[0].tool is not None
|
||||
name = lib << batch[0].tool.render(batch, port_names=tool_port_names)
|
||||
bb.ports[portspec] = batch[0].start_port.copy()
|
||||
bb.plug(name, {portspec: tool_port_names[0]})
|
||||
|
||||
bb.ports.clear()
|
||||
|
@ -1,12 +1,14 @@
|
||||
"""
|
||||
Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides)
|
||||
"""
|
||||
from typing import Sequence, Literal, Callable
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Sequence, Literal, Callable, Any
|
||||
from abc import ABCMeta, abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from ..utils import SupportsBool, rotation_matrix_2d
|
||||
from ..utils import SupportsBool, rotation_matrix_2d, layer_t
|
||||
from ..ports import Port
|
||||
from ..pattern import Pattern
|
||||
from ..abstract import Abstract
|
||||
@ -15,10 +17,16 @@ from ..error import BuildError
|
||||
from .builder import Builder
|
||||
|
||||
|
||||
render_step_t = (
|
||||
tuple[Literal['L', 'S', 'U'], Port, float, float, str, 'Tool']
|
||||
| tuple[Literal['P'], None, float, float, str, None]
|
||||
)
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RenderStep:
|
||||
opcode: Literal['L', 'S', 'U', 'P']
|
||||
tool: 'Tool' | None
|
||||
start_port: Port
|
||||
data: Any
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.opcode != 'P' and self.tool is None:
|
||||
raise BuildError('Got tool=None but the opcode is not "P"')
|
||||
|
||||
|
||||
class Tool:
|
||||
@ -42,7 +50,7 @@ class Tool:
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[float, str]:
|
||||
) -> Any:
|
||||
raise NotImplementedError(f'planL() not implemented for {type(self)}')
|
||||
|
||||
def planS(
|
||||
@ -54,26 +62,30 @@ class Tool:
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> str: # out_ptype only?
|
||||
) -> Any:
|
||||
raise NotImplementedError(f'planS() not implemented for {type(self)}')
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[render_step_t],
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
assert batch[0][-1] == self
|
||||
assert not batch or batch[0].tool == self
|
||||
raise NotImplementedError(f'render() not implemented for {type(self)}')
|
||||
|
||||
|
||||
abstract_tuple_t = tuple[Abstract, str, str]
|
||||
|
||||
|
||||
class BasicTool(Tool, metaclass=ABCMeta):
|
||||
straight: tuple[Callable[[float], Pattern], str, str]
|
||||
bend: tuple[Abstract, str, str] # Assumed to be clockwise
|
||||
transitions: dict[str, tuple[Abstract, str, str]]
|
||||
bend: abstract_tuple_t # Assumed to be clockwise
|
||||
transitions: dict[str, abstract_tuple_t]
|
||||
default_out_ptype: str
|
||||
|
||||
def path(
|
||||
self,
|
||||
@ -85,25 +97,62 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> Pattern:
|
||||
straight_length, _ccw, in_transition, out_transition = self.planL(
|
||||
ccw,
|
||||
length,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
)
|
||||
|
||||
# TODO check all the math for L-shaped bends
|
||||
straight_length = length
|
||||
bend_run = 0
|
||||
gen_straight, sport_in, sport_out = self.straight
|
||||
tree = Library()
|
||||
bb = Builder(library=tree, name='_path').add_port_pair(names=port_names)
|
||||
if in_transition:
|
||||
ipat, iport_theirs, _iport_ours = in_transition
|
||||
bb.plug(ipat, {port_names[1]: iport_theirs})
|
||||
if not numpy.isclose(straight_length, 0):
|
||||
straight = tree << {'_straight': gen_straight(straight_length)}
|
||||
bb.plug(straight, {port_names[1]: sport_in})
|
||||
if ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
brot = bend.ports[bport_in].rotation
|
||||
assert brot is not None
|
||||
bend_dxy = numpy.abs(
|
||||
rotation_matrix_2d(-brot) @ (
|
||||
bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw)))
|
||||
if out_transition:
|
||||
opat, oport_theirs, oport_ours = out_transition
|
||||
bb.plug(opat, {port_names[1]: oport_ours})
|
||||
|
||||
return bb.pattern
|
||||
|
||||
def planL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[float, SupportsBool | None, abstract_tuple_t | None, abstract_tuple_t | None]:
|
||||
# TODO check all the math for L-shaped bends
|
||||
if ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
|
||||
angle_in = bend.ports[bport_in].rotation
|
||||
angle_out = bend.ports[bport_out].rotation
|
||||
assert angle_in is not None
|
||||
assert angle_out is not None
|
||||
|
||||
bend_dxy = rotation_matrix_2d(-angle_in) @ (
|
||||
bend.ports[bport_out].offset
|
||||
- bend.ports[bport_in].offset
|
||||
)
|
||||
)
|
||||
|
||||
straight_length -= bend_dxy[0]
|
||||
bend_run += bend_dxy[1]
|
||||
bend_angle = angle_out - angle_in
|
||||
|
||||
if bool(ccw):
|
||||
bend_dxy[1] *= -1
|
||||
bend_angle *= -1
|
||||
else:
|
||||
bend_dxy = numpy.zeros(2)
|
||||
bend_angle = 0
|
||||
|
||||
in_transition = self.transitions.get('unk' if in_ptype is None else in_ptype, None)
|
||||
if in_transition is not None:
|
||||
@ -114,9 +163,6 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
ipat.ports[iport_ours].offset
|
||||
- ipat.ports[iport_theirs].offset
|
||||
)
|
||||
|
||||
straight_length -= itrans_dxy[0]
|
||||
bend_run += itrans_dxy[1]
|
||||
else:
|
||||
itrans_dxy = numpy.zeros(2)
|
||||
|
||||
@ -125,33 +171,176 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
opat, oport_theirs, oport_ours = out_transition
|
||||
orot = opat.ports[oport_ours].rotation
|
||||
assert orot is not None
|
||||
otrans_dxy = rotation_matrix_2d(-orot) @ (
|
||||
|
||||
otrans_dxy = rotation_matrix_2d(-orot + bend_angle) @ (
|
||||
opat.ports[oport_theirs].offset
|
||||
- opat.ports[oport_ours].offset
|
||||
)
|
||||
if ccw:
|
||||
otrans_dxy[0] *= -1
|
||||
|
||||
straight_length -= otrans_dxy[1]
|
||||
bend_run += otrans_dxy[0]
|
||||
else:
|
||||
otrans_dxy = numpy.zeros(2)
|
||||
|
||||
if straight_length < 0:
|
||||
raise BuildError(f'Asked to draw path with total length {length:g}, shorter than required bends and tapers:\n'
|
||||
f'bend: {bend_dxy[0]:g} in_taper: {abs(itrans_dxy[0])} out_taper: {otrans_dxy[1]}')
|
||||
if out_transition is not None:
|
||||
out_ptype_actual = opat.ports[oport_theirs].ptype
|
||||
elif ccw is not None:
|
||||
out_ptype_actual = bend.ports[bport_out].ptype
|
||||
else:
|
||||
out_ptype_actual = self.default_out_ptype
|
||||
|
||||
straight_length = length - bend_dxy[0] - itrans_dxy[0] - otrans_dxy[0]
|
||||
bend_run = bend_dxy[1] + itrans_dxy[1] + otrans_dxy
|
||||
|
||||
if straight_length < 0:
|
||||
raise BuildError(
|
||||
f'Asked to draw path with total length {length:,g}, shorter than required bends and transitions:\n'
|
||||
f'bend: {bend_dxy[0]:,g} in_trans: {itrans_dxy[0]:,g} out_trans: {otrans_dxy[0]:,g}'
|
||||
)
|
||||
|
||||
return float(straight_length), ccw, in_transition, out_transition
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
append: bool = True,
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
|
||||
gen_straight, sport_in, sport_out = self.straight
|
||||
tree = Library()
|
||||
bb = Builder(library=tree, name='_path').add_port_pair(names=port_names)
|
||||
bb = Builder(library=tree, name='_path').add_port_pair(names=(port_names[0], port_names[1]))
|
||||
|
||||
gen_straight, sport_in, _sport_out = self.straight
|
||||
for step in batch:
|
||||
straight_length, ccw, in_transition, out_transition = step.data
|
||||
assert step.tool == self
|
||||
|
||||
if step.opcode == 'L':
|
||||
if in_transition:
|
||||
ipat, iport_theirs, _iport_ours = in_transition
|
||||
bb.plug(ipat, {port_names[1]: iport_theirs})
|
||||
if not numpy.isclose(straight_length, 0):
|
||||
straight = tree << {'_straight': gen_straight(straight_length)}
|
||||
bb.plug(straight, {port_names[1]: sport_in})
|
||||
straight_pat = gen_straight(straight_length)
|
||||
if append:
|
||||
bb.plug(straight_pat, {port_names[1]: sport_in}, append=True)
|
||||
else:
|
||||
straight = tree << {'_straight': straight_pat}
|
||||
bb.plug(straight, {port_names[1]: sport_in}, append=True)
|
||||
if ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
bb.plug(bend, {port_names[1]: bport_in}, mirrored=(False, bool(ccw)))
|
||||
if out_transition:
|
||||
opat, oport_theirs, oport_ours = out_transition
|
||||
bb.plug(opat, {port_names[1]: oport_ours})
|
||||
return tree
|
||||
|
||||
return bb.pattern
|
||||
|
||||
|
||||
class PathTool(Tool, metaclass=ABCMeta):
|
||||
straight: tuple[Callable[[float], Pattern], str, str]
|
||||
bend: abstract_tuple_t # Assumed to be clockwise
|
||||
transitions: dict[str, abstract_tuple_t]
|
||||
ptype: str
|
||||
width: float
|
||||
layer: layer_t
|
||||
|
||||
def __init__(self, layer: layer_t, width: float, ptype: str = 'unk') -> None:
|
||||
Tool.__init__(self)
|
||||
self.layer = layer
|
||||
self.width = width
|
||||
self.ptype: str
|
||||
|
||||
def path(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> Pattern:
|
||||
dxy = self.planL(
|
||||
ccw,
|
||||
length,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
)
|
||||
|
||||
pat = Pattern()
|
||||
pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
|
||||
|
||||
if ccw is None:
|
||||
out_rot = pi
|
||||
elif bool(ccw):
|
||||
out_rot = -pi / 2
|
||||
else:
|
||||
out_rot = pi / 2
|
||||
|
||||
pat.ports = {
|
||||
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
|
||||
port_names[1]: Port(dxy, rotation=out_rot, ptype=self.ptype),
|
||||
}
|
||||
|
||||
return pat
|
||||
|
||||
def planL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[float, float]:
|
||||
# TODO check all the math for L-shaped bends
|
||||
|
||||
if out_ptype and out_ptype != self.ptype:
|
||||
raise BuildError(f'Requested {out_ptype=} does not match path ptype {self.ptype}')
|
||||
|
||||
if ccw is not None:
|
||||
bend_dxy = numpy.array([1, -1]) * self.width / 2
|
||||
bend_angle = pi / 2
|
||||
|
||||
if bool(ccw):
|
||||
bend_dxy[1] *= -1
|
||||
bend_angle *= -1
|
||||
else:
|
||||
bend_dxy = numpy.zeros(2)
|
||||
bend_angle = 0
|
||||
|
||||
straight_length = length - bend_dxy[0]
|
||||
bend_run = bend_dxy[1]
|
||||
|
||||
if straight_length < 0:
|
||||
raise BuildError(
|
||||
f'Asked to draw path with total length {length:,g}, shorter than required bend: {bend_dxy[0]:,g}'
|
||||
)
|
||||
|
||||
return length, bend_run
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
|
||||
path_vertices = [batch[0].start_port.offset]
|
||||
for step in batch:
|
||||
assert step.tool == self
|
||||
|
||||
port_rot = step.start_port.rotation
|
||||
assert port_rot is not None
|
||||
|
||||
if step.opcode == 'L':
|
||||
length, bend_run = step.data
|
||||
dxy = rotation_matrix_2d(port_rot + pi) @ (length, bend_run)
|
||||
path_vertices.append(step.start_port.offset + dxy)
|
||||
else:
|
||||
raise BuildError(f'Unrecognized opcode "{step.opcode}"')
|
||||
|
||||
tree, pat = Library.mktree('_path')
|
||||
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
||||
|
||||
return tree
|
||||
|
@ -78,7 +78,7 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
self.translate(+pivot)
|
||||
return self
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the bounds of the label.
|
||||
|
||||
|
@ -8,7 +8,7 @@ from itertools import chain
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy
|
||||
from numpy import inf, pi
|
||||
from numpy import inf, pi, nan
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
# .visualize imports matplotlib and matplotlib.collections
|
||||
|
||||
@ -310,7 +310,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
self,
|
||||
library: Mapping[str, 'Pattern'] | None = None,
|
||||
recurse: bool = True,
|
||||
cache: MutableMapping[str, NDArray[numpy.float64]] | None = None,
|
||||
cache: MutableMapping[str, NDArray[numpy.float64] | None] | None = None,
|
||||
) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
||||
@ -326,19 +326,24 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
if self.is_empty():
|
||||
return None
|
||||
|
||||
cbounds = numpy.array([
|
||||
n_elems = sum(1 for _ in chain_elements(self.shapes, self.labels))
|
||||
ebounds = numpy.full((n_elems, 2, 2), nan)
|
||||
for ee, entry in enumerate(chain_elements(self.shapes, self.labels)):
|
||||
maybe_ebounds = cast(Bounded, entry).get_bounds()
|
||||
if maybe_ebounds is not None:
|
||||
ebounds[ee] = maybe_ebounds
|
||||
mask = ~numpy.isnan(ebounds[:, 0, 0])
|
||||
|
||||
if mask.any():
|
||||
cbounds = numpy.vstack((
|
||||
numpy.min(ebounds[mask, 0, :]),
|
||||
numpy.max(ebounds[mask, 1, :]),
|
||||
))
|
||||
else:
|
||||
cbounds = numpy.array((
|
||||
(+inf, +inf),
|
||||
(-inf, -inf),
|
||||
])
|
||||
|
||||
for entry in chain_elements(self.shapes, self.labels):
|
||||
bounds = cast(Bounded, entry).get_bounds()
|
||||
if bounds is None:
|
||||
continue
|
||||
if entry.repetition is not None:
|
||||
bounds += entry.repetition.get_bounds()
|
||||
cbounds[0] = numpy.minimum(cbounds[0], bounds[0])
|
||||
cbounds[1] = numpy.maximum(cbounds[1], bounds[1])
|
||||
))
|
||||
|
||||
if recurse and self.has_refs():
|
||||
if library is None:
|
||||
@ -381,9 +386,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
if bounds is None:
|
||||
continue
|
||||
|
||||
if ref.repetition is not None:
|
||||
bounds += ref.repetition.get_bounds()
|
||||
|
||||
cbounds[0] = numpy.minimum(cbounds[0], bounds[0])
|
||||
cbounds[1] = numpy.maximum(cbounds[1], bounds[1])
|
||||
|
||||
|
@ -144,7 +144,7 @@ class Ref(
|
||||
self.repetition.mirror(axis)
|
||||
return self
|
||||
|
||||
def get_bounds(
|
||||
def get_bounds_single(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
*,
|
||||
@ -166,22 +166,6 @@ class Ref(
|
||||
return None
|
||||
return self.as_pattern(pattern=pattern).get_bounds(library) # TODO can just take pattern's bounds and then transform those!
|
||||
|
||||
def get_bounds_nonempty(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
*,
|
||||
library: Mapping[str, 'Pattern'] | None = None,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds(pattern, library=library)
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
def __repr__(self) -> str:
|
||||
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
scale = f' d{self.scale:g}' if self.scale != 1 else ''
|
||||
|
@ -237,7 +237,11 @@ class Grid(Repetition):
|
||||
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
||||
"""
|
||||
a_extent = self.a_vector * (self.a_count - 1)
|
||||
b_extent = self.b_vector * ((self.b_count - 1) if (self.b_vector is not None) else 0) # type: NDArray[numpy.float64] | float
|
||||
if self.b_count is None:
|
||||
b_extent = numpy.zeros(2)
|
||||
else:
|
||||
assert self.b_vector is not None
|
||||
b_extent = self.b_vector * (self.b_count - 1)
|
||||
|
||||
corners = numpy.stack(((0, 0), a_extent, b_extent, a_extent + b_extent))
|
||||
xy_min = numpy.min(corners, axis=0)
|
||||
|
@ -241,7 +241,7 @@ class Arc(Shape):
|
||||
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
|
||||
return [poly]
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
'''
|
||||
Equation for rotated ellipse is
|
||||
`x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)`
|
||||
|
@ -89,7 +89,7 @@ class Circle(Shape):
|
||||
|
||||
return [Polygon(xys, offset=self.offset)]
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
return numpy.vstack((self.offset - self.radius,
|
||||
self.offset + self.radius))
|
||||
|
||||
|
@ -152,7 +152,7 @@ class Ellipse(Shape):
|
||||
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
|
||||
return [poly]
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii)
|
||||
return numpy.vstack((self.offset - rot_radii[0],
|
||||
self.offset + rot_radii[1]))
|
||||
|
@ -309,21 +309,23 @@ class Path(Shape):
|
||||
|
||||
return polys
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
if self.cap == PathCap.Circle:
|
||||
bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
|
||||
numpy.max(self.vertices, axis=0) + self.width / 2))
|
||||
elif self.cap in (PathCap.Flush,
|
||||
elif self.cap in (
|
||||
PathCap.Flush,
|
||||
PathCap.Square,
|
||||
PathCap.SquareCustom):
|
||||
PathCap.SquareCustom,
|
||||
):
|
||||
bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
|
||||
polys = self.to_polygons()
|
||||
for poly in polys:
|
||||
poly_bounds = poly.get_bounds_nonempty()
|
||||
poly_bounds = poly.get_bounds_single_nonempty()
|
||||
bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :])
|
||||
bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :])
|
||||
else:
|
||||
raise PatternError(f'get_bounds() not implemented for endcaps: {self.cap}')
|
||||
raise PatternError(f'get_bounds_single() not implemented for endcaps: {self.cap}')
|
||||
|
||||
return bounds
|
||||
|
||||
|
@ -327,7 +327,7 @@ class Polygon(Shape):
|
||||
) -> list['Polygon']:
|
||||
return [copy.deepcopy(self)]
|
||||
|
||||
def get_bounds(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),
|
||||
self.offset + numpy.max(self.vertices, axis=0)))
|
||||
|
||||
|
@ -5,7 +5,7 @@ import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from ..traits import (
|
||||
Rotatable, Mirrorable, Copyable, Scalable, Bounded,
|
||||
Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
)
|
||||
|
||||
@ -25,7 +25,7 @@ normalized_shape_tuple = tuple[
|
||||
DEFAULT_POLY_NUM_VERTICES = 24
|
||||
|
||||
|
||||
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, Bounded,
|
||||
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
||||
"""
|
||||
Class specifying functions common to all shapes.
|
||||
@ -118,7 +118,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, Bounded
|
||||
|
||||
polygon_contours = []
|
||||
for polygon in self.to_polygons():
|
||||
bounds = polygon.get_bounds()
|
||||
bounds = polygon.get_bounds_single()
|
||||
if bounds is None:
|
||||
continue
|
||||
|
||||
@ -250,7 +250,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, Bounded
|
||||
polygon_contours = []
|
||||
for polygon in self.to_polygons():
|
||||
# Get rid of unused gridlines (anything not within 2 lines of the polygon bounds)
|
||||
bounds = polygon.get_bounds()
|
||||
bounds = polygon.get_bounds_single()
|
||||
if bounds is None:
|
||||
continue
|
||||
|
||||
|
@ -2,7 +2,7 @@ from typing import Sequence, Any
|
||||
import copy
|
||||
|
||||
import numpy
|
||||
from numpy import pi, inf
|
||||
from numpy import pi, nan
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from . import Shape, Polygon, normalized_shape_tuple
|
||||
@ -151,15 +151,20 @@ class Text(RotatableImpl, Shape):
|
||||
mirrored=(mirror_x, False),
|
||||
))
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
# rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
|
||||
# just convert to polygons instead
|
||||
bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
|
||||
polys = self.to_polygons()
|
||||
for poly in polys:
|
||||
poly_bounds = poly.get_bounds()
|
||||
bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :])
|
||||
bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :])
|
||||
pbounds = numpy.full((len(polys), 2, 2), nan)
|
||||
# bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
|
||||
for pp, poly in enumerate(polys):
|
||||
pbounds[pp] = poly.get_bounds_nonempty()
|
||||
# bounds[0] = numpy.minimum(bounds[0], poly_bounds[0])
|
||||
# bounds[1] = numpy.maximum(bounds[1], poly_bounds[1])
|
||||
bounds = numpy.vstack((
|
||||
numpy.min(pbounds[: 0, :], axis=0),
|
||||
numpy.max(pbounds[: 1, :], axis=0),
|
||||
))
|
||||
|
||||
return bounds
|
||||
|
||||
|
@ -61,27 +61,6 @@ class Positionable(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
|
||||
class Bounded(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def get_bounds(self) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Returns `None` for an empty entity.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_bounds_nonempty(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds()
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
|
||||
class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
"""
|
||||
Simple implementation of Positionable
|
||||
@ -121,3 +100,26 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
def translate(self, offset: ArrayLike) -> Self:
|
||||
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
|
||||
return self
|
||||
|
||||
|
||||
class Bounded(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def get_bounds(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Returns `None` for an empty entity.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_bounds_nonempty(self, *args, **kwargs) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds(*args, **kwargs)
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
from typing import Self, TYPE_CHECKING
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from ..error import MasqueError
|
||||
from .positionable import Bounded
|
||||
|
||||
|
||||
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
|
||||
@ -50,15 +54,19 @@ class Repeatable(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
|
||||
class RepeatableImpl(Repeatable, metaclass=ABCMeta):
|
||||
class RepeatableImpl(Repeatable, Bounded, metaclass=ABCMeta):
|
||||
"""
|
||||
Simple implementation of `Repeatable`
|
||||
Simple implementation of `Repeatable` and extension of `Bounded` to include repetition bounds.
|
||||
"""
|
||||
__slots__ = _empty_slots
|
||||
|
||||
_repetition: 'Repetition | None'
|
||||
""" Repetition object, or None (single instance only) """
|
||||
|
||||
@abstractmethod
|
||||
def get_bounds_single(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
|
||||
pass
|
||||
|
||||
#
|
||||
# Non-abstract properties
|
||||
#
|
||||
@ -79,3 +87,24 @@ class RepeatableImpl(Repeatable, metaclass=ABCMeta):
|
||||
def set_repetition(self, repetition: 'Repetition | None') -> Self:
|
||||
self.repetition = repetition
|
||||
return self
|
||||
|
||||
def get_bounds_single_nonempty(self, *args, **kwargs) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds_single(*args, **kwargs)
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
def get_bounds(self, *args, **kwargs) -> NDArray[numpy.float64] | None:
|
||||
bounds = self.get_bounds_single(*args, **kwargs)
|
||||
|
||||
if bounds is not None and self.repetition is not None:
|
||||
rep_bounds = self.repetition.get_bounds()
|
||||
if rep_bounds is None:
|
||||
return None
|
||||
bounds += rep_bounds
|
||||
return bounds
|
||||
|
Loading…
Reference in New Issue
Block a user