[PathTool] add native S-bend
This commit is contained in:
parent
778b3d9be7
commit
e6f5136357
3 changed files with 125 additions and 20 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -407,13 +407,26 @@ def test_pather_jog_failed_fallback_is_atomic() -> None:
|
|||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='shorter than required bend'):
|
||||
p.jog('A', 1.5, length=5)
|
||||
p.jog('A', 1.5, length=1.5)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert len(p.paths['A']) == 0
|
||||
|
||||
|
||||
def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.jog('A', 1.5, length=5)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-5, -1.5))
|
||||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert len(p.paths['A']) == 0
|
||||
|
||||
|
||||
def test_pather_jog_length_solved_from_single_position_bound() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
|
|
|
|||
|
|
@ -65,6 +65,20 @@ def test_renderpather_bend(rpather_setup: tuple[Pather, PathTool, Library]) -> N
|
|||
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20], [-1, -20]], atol=1e-10)
|
||||
|
||||
|
||||
def test_renderpather_jog_uses_native_pathtool_planS(rpather_setup: tuple[Pather, PathTool, Library]) -> None:
|
||||
rp, tool, lib = rpather_setup
|
||||
rp.at("start").jog(4, length=10)
|
||||
|
||||
assert len(rp.paths["start"]) == 1
|
||||
assert rp.paths["start"][0].opcode == "S"
|
||||
|
||||
rp.render()
|
||||
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
||||
# Native PathTool S-bends place the jog width/2 before the route end.
|
||||
assert_allclose(path_shape.vertices, [[0, 0], [0, -9], [4, -9], [4, -10]], atol=1e-10)
|
||||
assert_allclose(rp.ports["start"].offset, [4, -10], atol=1e-10)
|
||||
|
||||
|
||||
def test_renderpather_mirror_preserves_planned_bend_geometry(rpather_setup: tuple[Pather, PathTool, Library]) -> None:
|
||||
rp, tool, lib = rpather_setup
|
||||
rp.at("start").straight(10).cw(10)
|
||||
|
|
@ -171,3 +185,15 @@ def test_pathtool_traceL_bend_geometry_matches_ports() -> None:
|
|||
|
||||
assert_allclose(path_shape.vertices, [[0, 0], [10, 0], [10, 1]], atol=1e-10)
|
||||
assert_allclose(pat.ports["B"].offset, [10, 1], atol=1e-10)
|
||||
|
||||
|
||||
def test_pathtool_traceS_geometry_matches_ports() -> None:
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
|
||||
tree = tool.traceS(10, 4)
|
||||
pat = tree.top_pattern()
|
||||
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
||||
|
||||
assert_allclose(path_shape.vertices, [[0, 0], [9, 0], [9, 4], [10, 4]], atol=1e-10)
|
||||
assert_allclose(pat.ports["B"].offset, [10, 4], atol=1e-10)
|
||||
assert_allclose(pat.ports["B"].rotation, pi, atol=1e-10)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue