312 lines
11 KiB
Python
312 lines
11 KiB
Python
from typing import TYPE_CHECKING, cast
|
|
|
|
import pytest
|
|
import numpy
|
|
from numpy import pi
|
|
from numpy.testing import assert_allclose
|
|
|
|
from ..builder import Pather
|
|
from ..builder.tools import PathTool, Tool
|
|
from ..error import BuildError
|
|
from ..library import Library
|
|
from ..pattern import Pattern
|
|
from ..ports import Port
|
|
|
|
if TYPE_CHECKING:
|
|
from ..shapes import Path
|
|
|
|
|
|
@pytest.fixture
|
|
def deferred_render_setup() -> tuple[Pather, PathTool, Library]:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
rp = Pather(lib, tools=tool, auto_render=False)
|
|
rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
|
|
return rp, tool, lib
|
|
|
|
def test_deferred_render_stores_pending_paths_until_render(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_setup
|
|
rp.at("start").straight(10).straight(10)
|
|
|
|
assert not rp.pattern.has_shapes()
|
|
assert len(rp.paths["start"]) == 2
|
|
|
|
rp.render()
|
|
assert rp.pattern.has_shapes()
|
|
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
|
|
|
# PathTool renders length steps in the port extension direction.
|
|
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
|
assert len(path_shape.vertices) == 3
|
|
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10)
|
|
|
|
def test_deferred_render_bend(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_setup
|
|
rp.at("start").straight(10).cw(10)
|
|
|
|
rp.render()
|
|
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
|
# Clockwise bend adds the bend endpoint after the straight segment vertex.
|
|
assert len(path_shape.vertices) == 4
|
|
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20], [-1, -20]], atol=1e-10)
|
|
|
|
def test_deferred_render_jog_uses_native_pathtool_planS(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_setup
|
|
rp.at("start").jog(4, length=10)
|
|
|
|
assert len(rp.paths["start"]) == 1
|
|
assert rp.paths["start"][0].opcode == "S"
|
|
|
|
rp.render()
|
|
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
|
# Native PathTool S-bends place the jog width/2 before the route end.
|
|
assert_allclose(path_shape.vertices, [[0, 0], [0, -9], [4, -9], [4, -10]], atol=1e-10)
|
|
assert_allclose(rp.ports["start"].offset, [4, -10], atol=1e-10)
|
|
|
|
def test_deferred_render_mirror_preserves_planned_bend_geometry(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_setup
|
|
rp.at("start").straight(10).cw(10)
|
|
|
|
rp.mirror(0)
|
|
rp.render()
|
|
|
|
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
|
assert_allclose(path_shape.vertices, [[0, 0], [0, 10], [0, 20], [-1, 20]], atol=1e-10)
|
|
|
|
def test_deferred_render_retool(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool1, lib = deferred_render_setup
|
|
tool2 = PathTool(layer=(2, 0), width=4, ptype="wire")
|
|
|
|
rp.at("start").straight(10)
|
|
rp.retool(tool2, keys=["start"])
|
|
rp.at("start").straight(10)
|
|
|
|
rp.render()
|
|
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
|
assert len(rp.pattern.shapes[(2, 0)]) == 1
|
|
|
|
def test_portpather_translate_only_affects_future_steps(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_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_deferred_render_dead_ports() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=1)
|
|
rp = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool, auto_render=False)
|
|
rp.set_dead()
|
|
|
|
rp.straight("in", -10)
|
|
|
|
assert_allclose(rp.ports["in"].offset, [10, 0], atol=1e-10)
|
|
|
|
assert len(rp.paths["in"]) == 0
|
|
|
|
rp.render()
|
|
assert not rp.pattern.has_shapes()
|
|
|
|
def test_deferred_render_rename_port(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_setup
|
|
rp.at("start").straight(10)
|
|
rp.rename_ports({"start": "new_start"})
|
|
rp.at("new_start").straight(10)
|
|
|
|
assert "start" not in rp.paths
|
|
assert len(rp.paths["new_start"]) == 2
|
|
|
|
rp.render()
|
|
assert rp.pattern.has_shapes()
|
|
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
|
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
|
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10)
|
|
assert "new_start" in rp.ports
|
|
assert_allclose(rp.ports["new_start"].offset, [0, -20], atol=1e-10)
|
|
|
|
def test_deferred_render_drop_keeps_pending_geometry_without_port(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_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")
|
|
|
|
tree = tool.traceL(True, 10)
|
|
pat = tree.top_pattern()
|
|
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
|
|
|
assert_allclose(path_shape.vertices, [[0, 0], [10, 0], [10, 1]], atol=1e-10)
|
|
assert_allclose(pat.ports["B"].offset, [10, 1], atol=1e-10)
|
|
|
|
def test_pathtool_traceS_geometry_matches_ports() -> None:
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
|
|
tree = tool.traceS(10, 4)
|
|
pat = tree.top_pattern()
|
|
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
|
|
|
assert_allclose(path_shape.vertices, [[0, 0], [9, 0], [9, 4], [10, 4]], atol=1e-10)
|
|
assert_allclose(pat.ports["B"].offset, [10, 4], atol=1e-10)
|
|
assert_allclose(pat.ports["B"].rotation, pi, atol=1e-10)
|
|
|
|
def test_deferred_render_uturn_fallback() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
rp = Pather(lib, tools=tool, auto_render=False)
|
|
rp.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
|
|
rp.at('A').uturn(offset=10000, length=5000)
|
|
|
|
assert len(rp.paths['A']) == 2
|
|
assert rp.paths['A'][0].opcode == 'L'
|
|
assert rp.paths['A'][1].opcode == 'L'
|
|
|
|
rp.render()
|
|
assert rp.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(rp.pattern.ports['A'].rotation, pi)
|
|
|
|
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_deferred_render_rename_to_none_keeps_pending_geometry_without_port() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
rp = Pather(lib, tools=tool, auto_render=False)
|
|
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
|