[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 ..pattern import Pattern
from ..library import ILibrary, TreeView
from ..library import ILibrary, TreeView, SINGLE_USE_PREFIX
from ..error import BuildError, PortError
from ..ports import PortList, Port
from ..abstract import Abstract
@ -1067,9 +1067,24 @@ class Pather(PortList):
tool_port_names = ('A', 'B')
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:
assert batch[0].tool is not None
tree = batch[0].tool.render(batch, port_names=tool_port_names)
validate_tree(portspec, batch, tree)
name = self.library << tree
if portspec in pat.ports:
del pat.ports[portspec]

View file

@ -451,7 +451,9 @@ class Tool:
else:
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

View file

@ -684,6 +684,123 @@ def test_pather_uturn_failed_fallback_is_atomic() -> None:
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:
lib = Library()
tool = PathTool(layer='M1', width=1000)