[PathTool] add native S-bend

This commit is contained in:
Jan Petykiewicz 2026-04-08 23:48:48 -07:00
commit e6f5136357
3 changed files with 125 additions and 20 deletions

View file

@ -1238,6 +1238,39 @@ class PathTool(Tool, metaclass=ABCMeta):
# self.width = width
# self.ptype: str
def _check_out_ptype(self, out_ptype: str | None) -> None:
if out_ptype and out_ptype != self.ptype:
raise BuildError(f'Requested {out_ptype=} does not match path ptype {self.ptype}')
def _bend_radius(self) -> float:
return self.width / 2
def _plan_l_vertices(self, length: float, bend_run: float) -> NDArray[numpy.float64]:
vertices = [(0.0, 0.0), (length, 0.0)]
if not numpy.isclose(bend_run, 0):
vertices.append((length, bend_run))
return numpy.array(vertices, dtype=float)
def _plan_s_vertices(self, length: float, jog: float) -> NDArray[numpy.float64]:
if numpy.isclose(jog, 0):
return numpy.array([(0.0, 0.0), (length, 0.0)], dtype=float)
if length < self.width:
raise BuildError(
f'Asked to draw S-path with total length {length:,g}, shorter than required bend: {self.width:,g}'
)
# Match AutoTool's straight-then-s-bend placement so the jog happens
# width/2 before the end while still allowing smaller lateral offsets.
jog_x = length - self._bend_radius()
vertices = [
(0.0, 0.0),
(jog_x, 0.0),
(jog_x, jog),
(length, jog),
]
return numpy.array(vertices, dtype=float)
def traceL(
self,
ccw: SupportsBool | None,
@ -1248,7 +1281,7 @@ class PathTool(Tool, metaclass=ABCMeta):
port_names: tuple[str, str] = ('A', 'B'),
**kwargs, # noqa: ARG002 (unused)
) -> Library:
out_port, _data = self.planL(
out_port, data = self.planL(
ccw,
length,
in_ptype=in_ptype,
@ -1256,12 +1289,7 @@ class PathTool(Tool, metaclass=ABCMeta):
)
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
vertices: list[tuple[float, float]]
if ccw is None:
vertices = [(0.0, 0.0), (length, 0.0)]
else:
vertices = [(0.0, 0.0), (length, 0.0), tuple(out_port.offset)]
pat.path(layer=self.layer, width=self.width, vertices=vertices)
pat.path(layer=self.layer, width=self.width, vertices=self._plan_l_vertices(length, float(out_port.y)))
if ccw is None:
out_rot = pi
@ -1288,11 +1316,10 @@ class PathTool(Tool, metaclass=ABCMeta):
) -> tuple[Port, NDArray[numpy.float64]]:
# 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}')
self._check_out_ptype(out_ptype)
if ccw is not None:
bend_dxy = numpy.array([1, -1]) * self.width / 2
bend_dxy = numpy.array([1, -1]) * self._bend_radius()
bend_angle = pi / 2
if bool(ccw):
@ -1313,6 +1340,46 @@ class PathTool(Tool, metaclass=ABCMeta):
out_port = Port(data, rotation=bend_angle, ptype=self.ptype)
return out_port, data
def traceS(
self,
length: float,
jog: float,
*,
in_ptype: str | None = None,
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs, # noqa: ARG002 (unused)
) -> Library:
out_port, _data = self.planS(
length,
jog,
in_ptype=in_ptype,
out_ptype=out_ptype,
)
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
pat.path(layer=self.layer, width=self.width, vertices=self._plan_s_vertices(length, jog))
pat.ports = {
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
port_names[1]: out_port,
}
return tree
def planS(
self,
length: float,
jog: float,
*,
in_ptype: str | None = None, # noqa: ARG002 (unused)
out_ptype: str | None = None,
**kwargs, # noqa: ARG002 (unused)
) -> tuple[Port, NDArray[numpy.float64]]:
self._check_out_ptype(out_ptype)
self._plan_s_vertices(length, jog)
data = numpy.array((length, jog))
out_port = Port((length, jog), rotation=pi, ptype=self.ptype)
return out_port, data
def render(
self,
batch: Sequence[RenderStep],
@ -1341,19 +1408,18 @@ class PathTool(Tool, metaclass=ABCMeta):
# 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
transform = rotation_matrix_2d(port_rot + pi)
delta = step.end_port.offset - step.start_port.offset
local_end = rotation_matrix_2d(-(port_rot + pi)) @ delta
if step.opcode == 'L':
length, _ = step.data
dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0)
path_vertices.append(step.start_port.offset + dxy)
local_vertices = self._plan_l_vertices(float(local_end[0]), float(local_end[1]))
elif step.opcode == 'S':
local_vertices = self._plan_s_vertices(float(local_end[0]), float(local_end[1]))
else:
raise BuildError(f'Unrecognized opcode "{step.opcode}"')
# Check if the last vertex added is already at the end port location
if not numpy.allclose(path_vertices[-1], local_batch[-1].end_port.offset):
# If the path ends in a bend, we need to add the final vertex
path_vertices.append(local_batch[-1].end_port.offset)
for vertex in local_vertices[1:]:
path_vertices.append(step.start_port.offset + transform @ vertex)
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)