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