[Pather] fix using trees when append=True

This commit is contained in:
Jan Petykiewicz 2026-04-09 16:32:25 -07:00
commit bdc4dfdd06
3 changed files with 136 additions and 2 deletions

View file

@ -16,7 +16,7 @@ from numpy import pi
from numpy.typing import ArrayLike from numpy.typing import ArrayLike
from ..pattern import Pattern from ..pattern import Pattern
from ..library import ILibrary, TreeView from ..library import ILibrary, TreeView, SINGLE_USE_PREFIX
from ..error import BuildError, PortError from ..error import BuildError, PortError
from ..ports import PortList, Port from ..ports import PortList, Port
from ..abstract import Abstract from ..abstract import Abstract
@ -1067,9 +1067,24 @@ class Pather(PortList):
tool_port_names = ('A', 'B') tool_port_names = ('A', 'B')
pat = Pattern() pat = Pattern()
def validate_tree(portspec: str, batch: list[RenderStep], tree: ILibrary) -> None:
missing = sorted(
name
for name in tree.dangling_refs(tree.top())
if isinstance(name, str) and name.startswith(SINGLE_USE_PREFIX)
)
if not missing:
return
tool_name = type(batch[0].tool).__name__
raise BuildError(
f'Tool {tool_name}.render() returned missing single-use refs for {portspec}: {missing}'
)
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None: def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
assert batch[0].tool is not None assert batch[0].tool is not None
tree = batch[0].tool.render(batch, port_names=tool_port_names) tree = batch[0].tool.render(batch, port_names=tool_port_names)
validate_tree(portspec, batch, tree)
name = self.library << tree name = self.library << tree
if portspec in pat.ports: if portspec in pat.ports:
del pat.ports[portspec] del pat.ports[portspec]

View file

@ -451,7 +451,9 @@ class Tool:
else: else:
continue continue
pat.plug(seg_tree.top_pattern(), {port_names[1]: port_names[0]}, append=True) seg_name = lib << seg_tree
pat.plug(lib[seg_name], {port_names[1]: port_names[0]}, append=True)
del lib[seg_name]
return lib return lib

View file

@ -684,6 +684,123 @@ def test_pather_uturn_failed_fallback_is_atomic() -> None:
assert len(p.paths['A']) == 0 assert len(p.paths['A']) == 0
def test_pather_render_auto_renames_single_use_tool_children() -> None:
class FullTreeTool(Tool):
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202
ptype = out_ptype or in_ptype or 'wire'
return Port((length, 0), rotation=pi, ptype=ptype), {'length': length}
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202
tree = Library()
top = Pattern(ports={
port_names[0]: Port((0, 0), 0, ptype='wire'),
port_names[1]: Port((1, 0), pi, ptype='wire'),
})
child = Pattern(annotations={'batch': [len(batch)]})
top.ref('_seg')
tree['_top'] = top
tree['_seg'] = child
return tree
lib = Library()
p = Pather(lib, tools=FullTreeTool(), auto_render=False)
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
p.straight('A', 10)
p.render()
p.straight('A', 10)
p.render()
assert len(lib) == 2
assert set(lib.keys()) == set(p.pattern.refs.keys())
assert len(set(p.pattern.refs.keys())) == 2
assert all(name.startswith('_seg') for name in lib)
assert p.pattern.referenced_patterns() <= set(lib.keys())
def test_tool_render_fallback_preserves_segment_subtrees() -> None:
class TraceTreeTool(Tool):
def traceL(self, ccw, length, *, in_ptype=None, out_ptype=None, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001
tree = Library()
top = Pattern(ports={
port_names[0]: Port((0, 0), 0, ptype='wire'),
port_names[1]: Port((length, 0), pi, ptype='wire'),
})
child = Pattern(annotations={'length': [length]})
top.ref('_seg')
tree['_top'] = top
tree['_seg'] = child
return tree
lib = Library()
p = Pather(lib, tools=TraceTreeTool(), auto_render=False)
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
p.straight('A', 10)
p.render()
assert '_seg' in lib
assert '_seg' in p.pattern.refs
assert p.pattern.referenced_patterns() <= set(lib.keys())
def test_pather_render_rejects_missing_single_use_tool_refs() -> None:
class MissingSingleUseTool(Tool):
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202
ptype = out_ptype or in_ptype or 'wire'
return Port((length, 0), rotation=pi, ptype=ptype), {'length': length}
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202
tree = Library()
top = Pattern(ports={
port_names[0]: Port((0, 0), 0, ptype='wire'),
port_names[1]: Port((1, 0), pi, ptype='wire'),
})
top.ref('_seg')
tree['_top'] = top
return tree
lib = Library()
lib['_seg'] = Pattern(annotations={'stale': [1]})
p = Pather(lib, tools=MissingSingleUseTool(), auto_render=False)
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
p.straight('A', 10)
with pytest.raises(BuildError, match='missing single-use refs'):
p.render()
assert list(lib.keys()) == ['_seg']
assert not p.pattern.refs
def test_pather_render_allows_missing_non_single_use_tool_refs() -> None:
class SharedRefTool(Tool):
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202
ptype = out_ptype or in_ptype or 'wire'
return Port((length, 0), rotation=pi, ptype=ptype), {'length': length}
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202
tree = Library()
top = Pattern(ports={
port_names[0]: Port((0, 0), 0, ptype='wire'),
port_names[1]: Port((1, 0), pi, ptype='wire'),
})
top.ref('shared')
tree['_top'] = top
return tree
lib = Library()
lib['shared'] = Pattern(annotations={'shared': [1]})
p = Pather(lib, tools=SharedRefTool(), auto_render=False)
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
p.straight('A', 10)
p.render()
assert 'shared' in p.pattern.refs
assert p.pattern.referenced_patterns() <= set(lib.keys())
def test_renderpather_rename_to_none_keeps_pending_geometry_without_port() -> None: def test_renderpather_rename_to_none_keeps_pending_geometry_without_port() -> None:
lib = Library() lib = Library()
tool = PathTool(layer='M1', width=1000) tool = PathTool(layer='M1', width=1000)