diff --git a/masque/builder/pather.py b/masque/builder/pather.py index 84595ea..37b01ac 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -236,8 +236,10 @@ class Pather(Builder): self, portspec: str, ccw: SupportsBool | None, - position: float, + position: float | None = None, *, + x: float | None = None, + y: float | None = None, tool_port_names: tuple[str, str] = ('A', 'B'), base_name: str = '_pathto', **kwargs, @@ -246,8 +248,13 @@ class Pather(Builder): logger.error('Skipping path_to() since device is dead') return self + pos_count = sum(vv is not None for vv in (position, x, y)) + if pos_count > 1: + raise BuildError('Only one of `position`, `x`, and `y` may be specified at once') + if pos_count < 1: + raise BuildError('One of `position`, `x`, and `y` must be specified') + port = self.pattern[portspec] - x, y = port.offset if port.rotation is None: raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()') @@ -256,13 +263,25 @@ class Pather(Builder): is_horizontal = numpy.isclose(port.rotation % pi, 0) if is_horizontal: - if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x): - raise BuildError(f'path_to routing to behind source port: x={x:g} to {position:g}') - length = numpy.abs(position - x) + if y is not None: + raise BuildError(f'Asked to path to y-coordinate, but port is horizontal') + if position is None: + position = x else: - if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y): - raise BuildError(f'path_to routing to behind source port: y={y:g} to {position:g}') - length = numpy.abs(position - y) + if x is not None: + raise BuildError(f'Asked to path to x-coordinate, but port is vertical') + if position is None: + position = y + + x0, y0 = port.offset + if is_horizontal: + if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0): + raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}') + length = numpy.abs(position - x0) + else: + if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0): + raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}') + length = numpy.abs(position - y0) return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs) @@ -286,7 +305,7 @@ class Pather(Builder): if 'bound_type' in kwargs: bound_types.add(kwargs['bound_type']) bound = kwargs['bound'] - for bt in ('emin', 'emax', 'pmin', 'pmax', 'min_past_furthest'): + for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'): if bt in kwargs: bound_types.add(bt) bound = kwargs[bt] diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 5ef7911..f5bf481 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -301,15 +301,23 @@ class RenderPather(PortList): self, portspec: str, ccw: SupportsBool | None, - position: float, + position: float | None = None, + *, + x: float | None = None, + y: float | None = None, **kwargs, ) -> Self: if self._dead: logger.error('Skipping path_to() since device is dead') return self + pos_count = sum(vv is not None for vv in (position, x, y)) + if pos_count > 1: + raise BuildError('Only one of `position`, `x`, and `y` may be specified at once') + if pos_count < 1: + raise BuildError('One of `position`, `x`, and `y` must be specified') + port = self.pattern[portspec] - x, y = port.offset if port.rotation is None: raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()') @@ -318,13 +326,25 @@ class RenderPather(PortList): is_horizontal = numpy.isclose(port.rotation % pi, 0) if is_horizontal: - if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x): - raise BuildError(f'path_to routing to behind source port: x={x:g} to {position:g}') - length = numpy.abs(position - x) + if y is not None: + raise BuildError(f'Asked to path to y-coordinate, but port is horizontal') + if position is None: + position = x else: - if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y): - raise BuildError(f'path_to routing to behind source port: y={y:g} to {position:g}') - length = numpy.abs(position - y) + if x is not None: + raise BuildError(f'Asked to path to x-coordinate, but port is vertical') + if position is None: + position = y + + x0, y0 = port.offset + if is_horizontal: + if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0): + raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}') + length = numpy.abs(position - x0) + else: + if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0): + raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}') + length = numpy.abs(position - y0) return self.path(portspec, ccw, length, **kwargs) @@ -345,7 +365,7 @@ class RenderPather(PortList): if 'bound_type' in kwargs: bound_types.add(kwargs['bound_type']) bound = kwargs['bound'] - for bt in ('emin', 'emax', 'pmin', 'pmax', 'min_past_furthest'): + for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'): if bt in kwargs: bound_types.add(bt) bound = kwargs[bt] diff --git a/masque/builder/utils.py b/masque/builder/utils.py index 6cc02d8..b3d200d 100644 --- a/masque/builder/utils.py +++ b/masque/builder/utils.py @@ -53,9 +53,9 @@ def ell( The distance between furthest out-port (B) and the innermost bend (D's bend). - 'max_extension' or 'emax': The total extension value for the closest-in port (C in the diagram). - - 'min_position' or 'pmin': + - 'min_position', 'pmin', 'xmin', 'ymin': The coordinate of the innermost bend (D's bend). - - 'max_position' or 'pmax': + - 'max_position', 'pmax', 'xmax', 'ymax': The coordinate of the outermost bend (A's bend). `bound` can also be a vector. If specifying an extension (e.g. 'min_extension', @@ -109,6 +109,12 @@ def ell( raise BuildError('set_rotation must be specified if no ports have rotations!') rotations = numpy.full_like(has_rotation, set_rotation, dtype=float) + is_horizontal = numpy.isclose(rotations[0] % pi, 0) + if bound_type in ('ymin', 'ymax') and is_horizontal: + raise BuildError('Asked for {bound_type} position but ports are pointing along the x-axis!') + elif bound_type in ('xmin', 'xmax') and not is_horizontal: + raise BuildError('Asked for {bound_type} position but ports are pointing along the y-axis!') + direction = rotations[0] + pi # direction we want to travel in (+pi relative to port) rot_matrix = rotation_matrix_2d(-direction) @@ -184,9 +190,9 @@ def ell( rot_bound = -bound if neg else bound min_possible = x_start + offsets - if bound_type in ('pmax', 'max_position'): + if bound_type in ('pmax', 'max_position', 'xmax', 'ymax'): extension = rot_bound - min_possible.max() - elif bound_type in ('pmin', 'min_position'): + elif bound_type in ('pmin', 'min_position', 'xmin', 'ymin'): extension = rot_bound - min_possible.min() offsets += extension