diff --git a/masque/builder/pather.py b/masque/builder/pather.py index dc9647a..e190203 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -170,24 +170,25 @@ class Pather(PortList): # # Core Pattern Operations (Immediate) # - def _record_break(self, names: Iterable[str | None]) -> None: - """ Record a batch-breaking step for the specified ports. """ - if not self._dead: - for n in names: - if n is not None and n in self.paths: - port = self.ports[n] - self.paths[n].append(RenderStep('P', None, port.copy(), port.copy(), None)) + def _prepare_break(self, name: str | None) -> tuple[str, RenderStep] | None: + """ Snapshot one batch-breaking step for a name with deferred geometry. """ + if self._dead or name is None: + return None + + steps = self.paths.get(name) + if not steps: + return None + + port = self.ports.get(name, steps[-1].end_port) + return name, RenderStep('P', None, port.copy(), port.copy(), None) def _prepare_breaks(self, names: Iterable[str | None]) -> list[tuple[str, RenderStep]]: """ Snapshot break markers to be committed after a successful mutation. """ - if self._dead: - return [] - prepared: list[tuple[str, RenderStep]] = [] for n in names: - if n is not None and n in self.paths: - port = self.ports[n] - prepared.append((n, RenderStep('P', None, port.copy(), port.copy(), None))) + step = self._prepare_break(n) + if step is not None: + prepared.append(step) return prepared def _commit_breaks(self, prepared: Iterable[tuple[str, RenderStep]]) -> None: @@ -246,8 +247,9 @@ class Pather(PortList): @logged_op(lambda args: list(args['connections'].keys())) def plugged(self, connections: dict[str, str]) -> Self: - self._record_break(chain(connections.keys(), connections.values())) + prepared_breaks = self._prepare_breaks(chain(connections.keys(), connections.values())) self.pattern.plugged(connections) + self._commit_breaks(prepared_breaks) return self @logged_op(lambda args: list(args['mapping'].keys())) diff --git a/masque/test/test_pather_api.py b/masque/test/test_pather_api.py index d161508..2e841ad 100644 --- a/masque/test/test_pather_api.py +++ b/masque/test/test_pather_api.py @@ -739,3 +739,68 @@ def test_pather_failed_plug_does_not_add_break_marker() -> None: assert [step.opcode for step in p.paths['A']] == ['L'] assert set(p.pattern.ports) == {'A'} + + +def test_pather_place_reused_deleted_name_keeps_break_marker() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + p.pattern.ports['A'] = Port((0, 0), rotation=0) + + p.at('A').straight(5000) + p.rename_ports({'A': None}) + + other = Pattern(ports={'X': Port((-5000, 0), rotation=0)}) + p.place(other, port_map={'X': 'A'}, append=True) + p.at('A').straight(2000) + + assert [step.opcode for step in p.paths['A']] == ['L', 'P', 'L'] + + p.render() + assert p.pattern.has_shapes() + assert 'A' in p.pattern.ports + assert numpy.allclose(p.pattern.ports['A'].offset, (-7000, 0)) + + +def test_pather_plug_reused_deleted_name_keeps_break_marker() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + p.pattern.ports['A'] = Port((0, 0), rotation=0) + p.pattern.ports['B'] = Port((0, 0), rotation=0) + + p.at('A').straight(5000) + p.rename_ports({'A': None}) + + other = Pattern( + ports={ + 'X': Port((0, 0), rotation=pi), + 'Y': Port((-5000, 0), rotation=0), + }, + ) + p.plug(other, {'B': 'X'}, map_out={'Y': 'A'}, append=True) + p.at('A').straight(2000) + + assert [step.opcode for step in p.paths['A']] == ['L', 'P', 'L'] + + p.render() + assert p.pattern.has_shapes() + assert 'A' in p.pattern.ports + assert 'B' not in p.pattern.ports + assert numpy.allclose(p.pattern.ports['A'].offset, (-7000, 0)) + + +def test_pather_failed_plugged_does_not_add_break_marker() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + p.pattern.ports['A'] = Port((0, 0), rotation=0) + + p.at('A').straight(5000) + assert [step.opcode for step in p.paths['A']] == ['L'] + + with pytest.raises(PortError, match='Connection destination ports were not found'): + p.plugged({'A': 'missing'}) + + assert [step.opcode for step in p.paths['A']] == ['L'] + assert set(p.paths) == {'A'}