[Pather/RenderPather/Tool/PortPather] Add U-bends

This commit is contained in:
jan 2026-03-06 22:58:32 -08:00
commit 69ac25078c
4 changed files with 195 additions and 0 deletions

View file

@ -255,6 +255,72 @@ class Pather(Builder, PatherMixin):
return s
def _pathU(
self,
portspec: str,
jog: float,
*,
length: float = 0,
plug_into: str | None = None,
**kwargs,
) -> Self:
"""
Create a U-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
of traveling exactly `length` distance along the axis of `portspec` and returning to
the same orientation with an offset `jog`.
Args:
portspec: The name of the port into which the wire will be plugged.
jog: Total manhattan distance perpendicular to the direction of travel.
Positive values are to the left of the direction of travel.
length: Extra distance to travel along the port's axis. Default 0.
plug_into: If not None, attempts to plug the wire's output port into the provided
port on `self`.
Returns:
self
"""
if self._dead:
logger.warning('Skipping geometry for _pathU() since device is dead')
tool_port_names = ('A', 'B')
tool = self.tools.get(portspec, self.tools[None])
in_ptype = self.pattern[portspec].ptype
try:
tree = tool.pathU(jog, length=length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
except (BuildError, NotImplementedError):
if self._uturn_fallback(tool, portspec, jog, length, in_ptype, plug_into, **kwargs):
return self
if not self._dead:
raise
logger.warning("Tool pathU failed for dead pather. Using dummy extension.")
# Fallback for dead pather: manually update the port instead of plugging
port = self.pattern[portspec]
port_rot = port.rotation
assert port_rot is not None
out_port = Port((length, jog), rotation=0, ptype=in_ptype)
out_port.rotate_around((0, 0), pi + port_rot)
out_port.translate(port.offset)
self.pattern.ports[portspec] = out_port
self._log_port_update(portspec)
if plug_into is not None:
self.plugged({portspec: plug_into})
return self
tname = self.library << tree
if plug_into is not None:
output = {plug_into: tool_port_names[1]}
else:
output = {}
self.plug(tname, {portspec: tool_port_names[0], **output})
return self
def plugged(self, connections: dict[str, str]) -> Self:
PortList.plugged(self, connections)
return self
def _path(
self,
portspec: str,

View file

@ -214,6 +214,19 @@ class PatherMixin(PortList, metaclass=ABCMeta):
self._pathS(port, l_actual, offset, **bounds)
return self
def uturn(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds) -> Self:
""" 180-degree turn extension. """
if isinstance(portspec, str):
portspec = [portspec]
for port in portspec:
l_actual = length
if l_actual is None:
# TODO: use bounds to determine length?
l_actual = 0
self._pathU(port, offset, length=l_actual, **bounds)
return self
def trace_into(
self,
portspec_src: str,
@ -376,6 +389,18 @@ class PatherMixin(PortList, metaclass=ABCMeta):
) -> Self:
pass
@abstractmethod
def _pathU(
self,
portspec: str,
jog: float,
*,
length: float = 0,
plug_into: str | None = None,
**kwargs,
) -> Self:
pass
def path(self, *args, **kwargs) -> Self:
import warnings
warnings.warn("path() is deprecated; use trace(), straight(), or bend() instead", DeprecationWarning, stacklevel=2)
@ -386,6 +411,11 @@ class PatherMixin(PortList, metaclass=ABCMeta):
warnings.warn("pathS() is deprecated; use jog() instead", DeprecationWarning, stacklevel=2)
return self._pathS(*args, **kwargs)
def pathU(self, *args, **kwargs) -> Self:
import warnings
warnings.warn("pathU() is deprecated; use uturn() instead", DeprecationWarning, stacklevel=2)
return self._pathU(*args, **kwargs)
@abstractmethod
def plug(
self,

View file

@ -377,6 +377,66 @@ class RenderPather(PatherMixin):
PortList.plugged(self, connections)
return self
def _pathU(
self,
portspec: str,
jog: float,
*,
length: float = 0,
plug_into: str | None = None,
**kwargs,
) -> Self:
"""
Plan a U-shaped "wire"/"waveguide" extending from the port `portspec`, with the aim
of traveling exactly `length` distance and returning to the same orientation
with an offset `jog`.
Args:
portspec: The name of the port into which the wire will be plugged.
jog: Total manhattan distance perpendicular to the direction of travel.
Positive values are to the left of the direction of travel.
length: Extra distance to travel along the port's axis. Default 0.
plug_into: If not None, attempts to plug the wire's output port into the provided
port on `self`.
Returns:
self
"""
if self._dead:
logger.warning('Skipping geometry for _pathU() since device is dead')
port = self.pattern[portspec]
in_ptype = port.ptype
port_rot = port.rotation
assert port_rot is not None
tool = self.tools.get(portspec, self.tools[None])
try:
out_port, data = tool.planU(jog, length=length, in_ptype=in_ptype, **kwargs)
except (BuildError, NotImplementedError):
if self._uturn_fallback(tool, portspec, jog, length, in_ptype, plug_into, **kwargs):
return self
if not self._dead:
raise
logger.warning("Tool planning failed for dead pather. Using dummy extension.")
out_port = Port((length, jog), rotation=0, ptype=in_ptype)
data = None
if out_port is not None:
out_port.rotate_around((0, 0), pi + port_rot)
out_port.translate(port.offset)
if not self._dead:
step = RenderStep('U', tool, port.copy(), out_port.copy(), data)
self.paths[portspec].append(step)
self.pattern.ports[portspec] = out_port.copy()
self._log_port_update(portspec)
if plug_into is not None:
self.plugged({portspec: plug_into})
return self
def _path(
self,
portspec: str,

View file

@ -260,6 +260,45 @@ class Tool:
"""
raise NotImplementedError(f'planS() not implemented for {type(self)}')
def pathU(
self,
jog: float,
*,
in_ptype: str | None = None,
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs,
) -> Library:
"""
Create a wire or waveguide that travels exactly `jog` distance along the axis
perpendicular to its input port (i.e. a U-bend).
Used by `Pather` and `RenderPather`.
The output port must have an orientation identical to the input port.
The input and output ports should be compatible with `in_ptype` and
`out_ptype`, respectively. They should also be named `port_names[0]` and
`port_names[1]`, respectively.
Args:
jog: The total offset from the input to output, along the perpendicular axis.
A positive number implies a leftwards shift (i.e. counterclockwise bend
followed by a clockwise bend)
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
port_names: The output pattern will have its input port named `port_names[0]` and
its output named `port_names[1]`.
kwargs: Custom tool-specific parameters.
Returns:
A pattern tree containing the requested U-shaped wire or waveguide
Raises:
BuildError if an impossible or unsupported geometry is requested.
"""
raise NotImplementedError(f'pathU() not implemented for {type(self)}')
def planU(
self,
jog: float,