[set_dead] improve handling of dead ports

This commit is contained in:
Jan Petykiewicz 2026-04-08 17:41:50 -07:00
commit 84106dc355
5 changed files with 350 additions and 88 deletions

View file

@ -252,19 +252,24 @@ 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]] = {}
for kk, vv in mapping.items():
if kk not in self.paths:
continue
steps = self.paths.pop(kk)
winners = self.pattern._rename_ports_impl(
mapping,
overwrite=overwrite or self._dead,
allow_collisions=self._dead,
)
moved_steps = {kk: self.paths.pop(kk) for kk in mapping if kk in self.paths}
for kk, steps in moved_steps.items():
vv = mapping[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)
target = kk if vv is None else vv
if self._dead and vv is not None and winners.get(vv) != kk:
target = kk
self.paths[target].extend(steps)
return self
def set_dead(self) -> Self:
@ -756,6 +761,7 @@ class Pather(PortList):
plug_into: str | None = None,
*,
out_rot: float | None = None,
out_ptype: str | None = None,
) -> None:
if out_rot is None:
if ccw is None:
@ -768,7 +774,7 @@ class Pather(PortList):
port = self.pattern[portspec]
port_rot = port.rotation
assert port_rot is not None
out_port = Port((length, jog), rotation=out_rot, ptype=in_ptype)
out_port = Port((length, jog), rotation=out_rot, ptype=out_ptype or in_ptype)
out_port.rotate_around((0, 0), pi + port_rot)
out_port.translate(port.offset)
self.pattern.ports[portspec] = out_port
@ -786,7 +792,15 @@ class Pather(PortList):
except (BuildError, NotImplementedError):
if not self._dead:
raise
self._apply_dead_fallback(portspec, length, 0, ccw, in_ptype, plug_into)
self._apply_dead_fallback(
portspec,
length,
0,
ccw,
in_ptype,
plug_into,
out_ptype=kwargs.get('out_ptype'),
)
return self
if out_port is not None:
self._apply_step('L', portspec, out_port, data, tool, plug_into)
@ -806,7 +820,16 @@ class Pather(PortList):
except (BuildError, NotImplementedError):
if not self._dead:
raise
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi)
self._apply_dead_fallback(
portspec,
length,
jog,
None,
in_ptype,
plug_into,
out_rot=pi,
out_ptype=kwargs.get('out_ptype'),
)
return self
self._apply_validated_double_l(
@ -840,7 +863,16 @@ class Pather(PortList):
except (BuildError, NotImplementedError):
if not self._dead:
raise
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=0)
self._apply_dead_fallback(
portspec,
length,
jog,
None,
in_ptype,
plug_into,
out_rot=0,
out_ptype=kwargs.get('out_ptype'),
)
return self
self._apply_validated_double_l(
@ -1015,8 +1047,6 @@ class Pather(PortList):
thru=thru,
**kwargs,
):
if self._dead:
return self
ops = self._plan_trace_into(
portspec_src,
portspec_dst,
@ -1238,7 +1268,7 @@ class PortPather:
else:
name_map = dict(name)
self.pather.rename_ports(name_map)
self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None]
self.ports = list(dict.fromkeys(mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None))
return self
def select(self, ports: str | Iterable[str]) -> Self:
@ -1274,33 +1304,37 @@ class PortPather:
if missing_pattern:
raise PortError(f'Ports to {action} were not found: {missing_pattern}')
targets = list(name_map.values())
duplicate_targets = {vv for vv in targets if targets.count(vv) > 1}
if duplicate_targets:
raise PortError(f'{action.capitalize()} targets would collide: {duplicate_targets}')
if not self.pather._dead:
targets = list(name_map.values())
duplicate_targets = {vv for vv in targets if targets.count(vv) > 1}
if duplicate_targets:
raise PortError(f'{action.capitalize()} targets would collide: {duplicate_targets}')
overwritten = {
dst for src, dst in name_map.items()
if dst in self.pather.pattern.ports and dst != src
}
if overwritten:
raise PortError(f'{action.capitalize()} would overwrite existing ports: {overwritten}')
overwritten = {
dst for src, dst in name_map.items()
if dst in self.pather.pattern.ports and dst != src
}
if overwritten:
raise PortError(f'{action.capitalize()} would overwrite existing ports: {overwritten}')
return name_map
def mark(self, name: str | Mapping[str, str]) -> Self:
""" Bookmark current port(s). """
name_map = self._normalize_copy_map(name, 'mark')
source_ports = {src: self.pather.pattern[src].copy() for src in name_map}
for src, dst in name_map.items():
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
self.pather.pattern.ports[dst] = source_ports[src].copy()
return self
def fork(self, name: str | Mapping[str, str]) -> Self:
""" Split and follow new name. """
name_map = self._normalize_copy_map(name, 'fork')
source_ports = {src: self.pather.pattern[src].copy() for src in name_map}
for src, dst in name_map.items():
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
self.pather.pattern.ports[dst] = source_ports[src].copy()
self.ports = [(dst if pp == src else pp) for pp in self.ports]
self.ports = list(dict.fromkeys(self.ports))
return self
def drop(self) -> Self: