[AutoTool] add U-bend

This commit is contained in:
jan 2026-03-06 23:51:56 -08:00
commit 1070815730
2 changed files with 167 additions and 1 deletions

View file

@ -264,6 +264,7 @@ class Tool:
self,
jog: float,
*,
length: float = 0,
in_ptype: str | None = None,
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
@ -597,6 +598,14 @@ class AutoTool(Tool, metaclass=ABCMeta):
b_transition: 'AutoTool.Transition | None'
out_transition: 'AutoTool.Transition | None'
@dataclass(frozen=True, slots=True)
class UData:
""" Data for planU """
ldata0: 'AutoTool.LData'
ldata1: 'AutoTool.LData'
straight2: 'AutoTool.Straight'
l2_length: float
straights: list[Straight]
""" List of straight-generators to choose from, in order of priority """
@ -942,6 +951,113 @@ class AutoTool(Tool, metaclass=ABCMeta):
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
return tree
def planU(
self,
jog: float,
*,
length: float = 0,
in_ptype: str | None = None,
out_ptype: str | None = None,
**kwargs,
) -> tuple[Port, UData]:
ccw = jog > 0
kwargs_no_out = kwargs | {'out_ptype': None}
# Use loops to find a combination of straights and bends that fits
success = False
for _straight1 in self.straights:
for _bend1 in self.bends:
for straight2 in self.straights:
for _bend2 in self.bends:
try:
# We need to know R1 and R2 to calculate the lengths.
# Use large dummy lengths to probe the bends.
p_probe1, _ = self.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out)
R1 = abs(Port((0, 0), 0).measure_travel(p_probe1)[0][1])
p_probe2, _ = self.planL(ccw, 1e9, in_ptype=p_probe1.ptype, out_ptype=out_ptype, **kwargs)
R2 = abs(Port((0, 0), 0).measure_travel(p_probe2)[0][1])
# Final x will be: x = l1_straight + R1 - R2
# We want final x = length. So: l1_straight = length - R1 + R2
# Total length for planL(0) is l1 = l1_straight + R1 = length + R2
l1 = length + R2
# Final y will be: y = R1 + l2_straight + R2 = abs(jog)
# So: l2_straight = abs(jog) - R1 - R2
l2_length = abs(jog) - R1 - R2
if l2_length >= straight2.length_range[0] and l2_length < straight2.length_range[1]:
p0, ldata0 = self.planL(ccw, l1, in_ptype=in_ptype, **kwargs_no_out)
# For the second bend, we want straight length = 0.
# Total length for planL(1) is l2 = 0 + R2 = R2.
p1, ldata1 = self.planL(ccw, R2, in_ptype=p0.ptype, out_ptype=out_ptype, **kwargs)
success = True
break
except BuildError:
continue
if success:
break
if success:
break
if success:
break
if not success:
raise BuildError(f"AutoTool failed to plan U-turn with {jog=}, {length=}")
data = self.UData(ldata0, ldata1, straight2, l2_length)
# Final port is at (length, jog) rot pi relative to input
out_port = Port((length, jog), rotation=pi, ptype=p1.ptype)
return out_port, data
def _renderU(
self,
data: UData,
tree: ILibrary,
port_names: tuple[str, str],
gen_kwargs: dict[str, Any],
) -> ILibrary:
pat = tree.top_pattern()
# 1. First L-bend
self._renderL(data.ldata0, tree, port_names, gen_kwargs)
# 2. Connecting straight
if not numpy.isclose(data.l2_length, 0):
s2_pat_or_tree = data.straight2.fn(data.l2_length, **(gen_kwargs | data.ldata0.straight_kwargs))
pmap = {port_names[1]: data.straight2.in_port_name}
if isinstance(s2_pat_or_tree, Pattern):
pat.plug(s2_pat_or_tree, pmap, append=True)
else:
s2_tree = s2_pat_or_tree
top = s2_tree.top()
s2_tree.flatten(top, dangling_ok=True)
pat.plug(s2_tree[top], pmap, append=True)
# 3. Second L-bend
self._renderL(data.ldata1, tree, port_names, gen_kwargs)
return tree
def pathU(
self,
jog: float,
*,
length: float = 0,
in_ptype: str | None = None,
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs,
) -> Library:
_out_port, data = self.planU(
jog,
length = length,
in_ptype = in_ptype,
out_ptype = out_ptype,
**kwargs,
)
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'pathU')
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
return tree
def render(
self,
batch: Sequence[RenderStep],
@ -959,6 +1075,8 @@ class AutoTool(Tool, metaclass=ABCMeta):
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
elif step.opcode == 'S':
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
elif step.opcode == 'U':
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
return tree
@ -1086,6 +1204,7 @@ class PathTool(Tool, metaclass=ABCMeta):
port_rot = step.start_port.rotation
# Masque convention: Port rotation points INTO the device.
# So the direction of travel for the path is AWAY from the port, i.e., port_rot + pi.
assert port_rot is not None
if step.opcode == 'L':