masque/masque/test/test_pather_rendering.py

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