[Pather] fix port rename/deletion tracking

This commit is contained in:
Jan Petykiewicz 2026-03-31 18:49:41 -07:00
commit 2b29e46b93
3 changed files with 63 additions and 4 deletions

View file

@ -228,7 +228,17 @@ class Pather(PortList):
@logged_op(lambda args: list(args['mapping'].keys()))
def rename_ports(self, mapping: dict[str, str | None], overwrite: bool = False) -> Self:
self.pattern.rename_ports(mapping, overwrite)
renamed: dict[str, list[RenderStep]] = {vv: self.paths.pop(kk) for kk, vv in mapping.items() if kk in self.paths and vv is not None}
renamed: dict[str, list[RenderStep]] = {}
for kk, vv in mapping.items():
if kk not in self.paths:
continue
steps = self.paths.pop(kk)
# Preserve deferred geometry even if the live port is deleted.
# `render()` can still materialize the saved steps using their stored start/end ports.
# Current semantics intentionally keep deleted ports' queued steps under the old key,
# so if a new live port later reuses that name it does not retarget the old geometry;
# the old and new routes merely share a render bucket until `render()` consumes them.
renamed[kk if vv is None else vv] = steps
self.paths.update(renamed)
return self
@ -789,6 +799,9 @@ class PortPather:
#
# Delegate to port
#
# These mutate only the selected live port state. They do not rewrite already planned
# RenderSteps, so deferred geometry remains as previously planned and only future routing
# starts from the updated port.
def set_ptype(self, ptype: str) -> Self:
for port in self.ports:
self.pather.pattern[port].set_ptype(ptype)
@ -865,8 +878,7 @@ class PortPather:
def drop(self) -> Self:
""" Remove selected ports from the pattern and the PortPather. """
for pp in self.ports:
del self.pather.pattern.ports[pp]
self.pather.rename_ports({pp: None for pp in self.ports})
self.ports = []
return self
@ -880,7 +892,7 @@ class PortPather:
if name is None:
self.drop()
return None
del self.pather.pattern.ports[name]
self.pather.rename_ports({name: None})
self.ports = [pp for pp in self.ports if pp != name]
return self

View file

@ -300,3 +300,20 @@ def test_pather_uturn_failed_fallback_is_atomic() -> None:
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_renderpather_rename_to_none_keeps_pending_geometry_without_port() -> None:
lib = Library()
tool = PathTool(layer='M1', width=1000)
rp = RenderPather(lib, tools=tool)
rp.pattern.ports['A'] = Port((0, 0), rotation=0)
rp.at('A').straight(5000)
rp.rename_ports({'A': None})
assert 'A' not in rp.pattern.ports
assert len(rp.paths['A']) == 1
rp.render()
assert rp.pattern.has_shapes()
assert 'A' not in rp.pattern.ports

View file

@ -90,6 +90,22 @@ def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Librar
assert len(rp.pattern.shapes[(2, 0)]) == 1
def test_portpather_translate_only_affects_future_steps(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup
pp = rp.at("start")
pp.straight(10)
pp.translate((5, 0))
pp.straight(10)
rp.render()
shapes = rp.pattern.shapes[(1, 0)]
assert len(shapes) == 2
assert_allclose(cast("Path", shapes[0]).vertices, [[0, 0], [0, -10]], atol=1e-10)
assert_allclose(cast("Path", shapes[1]).vertices, [[5, -10], [5, -20]], atol=1e-10)
assert_allclose(rp.ports["start"].offset, [5, -20], atol=1e-10)
def test_renderpather_dead_ports() -> None:
lib = Library()
tool = PathTool(layer=(1, 0), width=1)
@ -132,6 +148,20 @@ def test_renderpather_rename_port(rpather_setup: tuple[RenderPather, PathTool, L
assert_allclose(rp.ports["new_start"].offset, [0, -20], atol=1e-10)
def test_renderpather_drop_keeps_pending_geometry_without_port(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup
rp.at("start").straight(10).drop()
assert "start" not in rp.ports
assert len(rp.paths["start"]) == 1
rp.render()
assert rp.pattern.has_shapes()
assert "start" not in rp.ports
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
assert_allclose(path_shape.vertices, [[0, 0], [0, -10]], atol=1e-10)
def test_pathtool_traceL_bend_geometry_matches_ports() -> None:
tool = PathTool(layer=(1, 0), width=2, ptype="wire")