526 lines
19 KiB
Python
526 lines
19 KiB
Python
from typing import TYPE_CHECKING, cast
|
|
import logging
|
|
|
|
import pytest
|
|
import numpy
|
|
from numpy import pi
|
|
from numpy.testing import assert_allclose
|
|
|
|
from ..builder import Pather
|
|
from ..builder.tools import PathTool, RenderStep, StraightOffer, 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, render='deferred')
|
|
rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
|
|
return rp, tool, lib
|
|
|
|
|
|
def add_pending_straight(pather: Pather) -> None:
|
|
pather.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
|
|
pather.straight("start", 10)
|
|
|
|
|
|
def route_in_context(pather: Pather) -> None:
|
|
with pather:
|
|
add_pending_straight(pather)
|
|
|
|
|
|
def fail_in_context(pather: Pather) -> None:
|
|
with pather:
|
|
add_pending_straight(pather)
|
|
raise RuntimeError('body failed')
|
|
|
|
|
|
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])
|
|
# The bend route is explicit straight run plus a fixed-size bend.
|
|
assert len(path_shape.vertices) == 5
|
|
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -19], [0, -20], [-1, -20]], atol=1e-10)
|
|
|
|
def test_deferred_render_jog_uses_lowest_cost_two_bend_route(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
rp, tool, lib = deferred_render_setup
|
|
rp.at("start").jog(4, length=10)
|
|
|
|
assert [step.opcode for step in rp._paths["start"]] == ["L", "L", "L", "L"]
|
|
|
|
rp.render()
|
|
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
|
assert_allclose(path_shape.vertices, [[0, 0], [0, -1], [1, -1], [3, -1], [4, -1], [4, -2], [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, 19], [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, render='deferred')
|
|
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_pather_default_auto_policy_renders_immediately_outside_context() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
p = Pather(lib, tools=tool)
|
|
p.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
|
|
|
|
p.straight("start", 10)
|
|
|
|
assert p.pattern.has_shapes()
|
|
assert not any(p._paths.values())
|
|
|
|
|
|
def test_pather_default_auto_policy_defers_until_clean_context_exit() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
|
|
with Pather(lib, tools=tool) as p:
|
|
add_pending_straight(p)
|
|
assert not p.pattern.has_shapes()
|
|
assert len(p._paths["start"]) == 1
|
|
|
|
assert p.pattern.has_shapes()
|
|
assert not any(p._paths.values())
|
|
|
|
|
|
def test_pather_context_warn_policy_logs_pending_paths(caplog: pytest.LogCaptureFixture) -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
|
|
with caplog.at_level(logging.WARNING, logger="masque.builder.pather"), Pather(lib, tools=tool, render='warn') as p:
|
|
add_pending_straight(p)
|
|
|
|
records = [
|
|
record
|
|
for record in caplog.records
|
|
if 'Pather context exited with 1 pending render step on 1 port' in record.getMessage()
|
|
]
|
|
assert len(records) == 1
|
|
assert records[0].stack_info is None
|
|
assert len(p._paths["start"]) == 1
|
|
assert not p.pattern.has_shapes()
|
|
|
|
|
|
def test_pather_context_error_policy_rejects_pending_paths() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
p = Pather(lib, tools=tool, render='error')
|
|
|
|
with pytest.raises(BuildError, match='1 pending render step on 1 port'):
|
|
route_in_context(p)
|
|
|
|
assert len(p._paths["start"]) == 1
|
|
assert not p.pattern.has_shapes()
|
|
|
|
|
|
def test_pather_context_ignore_policy_leaves_pending_paths_silent(caplog: pytest.LogCaptureFixture) -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
|
|
with caplog.at_level(logging.WARNING, logger="masque.builder.pather"), Pather(lib, tools=tool, render='ignore') as p:
|
|
add_pending_straight(p)
|
|
|
|
assert not caplog.records
|
|
assert len(p._paths["start"]) == 1
|
|
assert not p.pattern.has_shapes()
|
|
|
|
|
|
def test_pather_context_policy_does_not_mask_body_exception() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
p = Pather(lib, tools=tool, render='error')
|
|
|
|
with pytest.raises(RuntimeError, match='body failed'):
|
|
fail_in_context(p)
|
|
|
|
assert len(p._paths["start"]) == 1
|
|
assert not p.pattern.has_shapes()
|
|
|
|
|
|
def test_pather_rejects_invalid_render_policy() -> None:
|
|
with pytest.raises(BuildError, match='Invalid render policy'):
|
|
Pather(Library(), tools=PathTool(layer=(1, 0), width=2), render='later') # type: ignore[arg-type]
|
|
|
|
|
|
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_bend_offer_render_geometry_matches_ports() -> None:
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
|
|
offer = tool.primitive_offers("bend", in_ptype="wire", ccw=True)[0]
|
|
start = Port((0, 0), rotation=pi, ptype="wire")
|
|
end = offer.endpoint_at(1)
|
|
tree = tool.render((RenderStep(offer.opcode, tool, start, end, offer.commit(1)),))
|
|
pat = tree.top_pattern()
|
|
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
|
|
|
assert offer.length_domain == (1, 1)
|
|
assert_allclose(path_shape.vertices, [[0, 0], [1, 0], [1, 1]], atol=1e-10)
|
|
assert_allclose(pat.ports["B"].offset, [1, 1], atol=1e-10)
|
|
|
|
def test_pathtool_s_offer_render_geometry_matches_ports() -> None:
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
|
|
offer = tool.primitive_offers("s", in_ptype="wire")[0]
|
|
start = Port((0, 0), rotation=pi, ptype="wire")
|
|
end = offer.endpoint_at(4)
|
|
tree = tool.render((RenderStep(offer.opcode, tool, start, end, offer.commit(4)),))
|
|
pat = tree.top_pattern()
|
|
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
|
|
|
assert_allclose(path_shape.vertices, [[0, 0], [1, 0], [1, 4], [2, 4]], atol=1e-10)
|
|
assert_allclose(pat.ports["B"].offset, [2, 4], atol=1e-10)
|
|
assert_allclose(pat.ports["B"].rotation, 0, atol=1e-10)
|
|
|
|
def test_deferred_render_uturn_fallback() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
rp = Pather(lib, tools=tool, render='deferred')
|
|
rp.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
|
|
rp.at('A').uturn(offset=10000, length=5000)
|
|
|
|
assert len(rp._paths['A']) == 4
|
|
assert [step.opcode for step in rp._paths['A']] == ['L', 'L', 'L', '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 primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
|
if kind != 'straight':
|
|
return ()
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return (StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype=ptype,
|
|
endpoint_planner=lambda length: Port((length, 0), rotation=pi, ptype=ptype),
|
|
commit_planner=lambda length: {'length': length},
|
|
),)
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
|
length = batch[0].data['length']
|
|
tree = Library()
|
|
top = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((length, 0), 0, 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())
|
|
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_custom_tool_render_preserves_segment_subtrees() -> None:
|
|
class TraceTreeTool(Tool):
|
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
|
if kind != 'straight':
|
|
return ()
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return (StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype=ptype,
|
|
endpoint_planner=lambda length: Port((length, 0), rotation=pi, ptype=ptype),
|
|
commit_planner=lambda length: self._trace(length),
|
|
),)
|
|
|
|
def _trace(self, length, *, port_names=('A', 'B')) -> Library: # noqa: ANN001
|
|
tree = Library()
|
|
top = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((length, 0), 0, ptype='wire'),
|
|
})
|
|
child = Pattern(annotations={'length': [length]})
|
|
top.ref('_seg')
|
|
tree['_top'] = top
|
|
tree['_seg'] = child
|
|
return tree
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
|
assert len(batch) == 1
|
|
assert isinstance(batch[0].data, Library)
|
|
return batch[0].data
|
|
|
|
lib = Library()
|
|
p = Pather(lib, tools=TraceTreeTool())
|
|
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 primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
|
if kind != 'straight':
|
|
return ()
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return (StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype=ptype,
|
|
endpoint_planner=lambda length: Port((length, 0), rotation=pi, ptype=ptype),
|
|
commit_planner=lambda length: {'length': length},
|
|
),)
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
|
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(), render='deferred')
|
|
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 primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
|
if kind != 'straight':
|
|
return ()
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return (StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype=ptype,
|
|
endpoint_planner=lambda length: Port((length, 0), rotation=pi, ptype=ptype),
|
|
commit_planner=lambda length: {'length': length},
|
|
),)
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
|
length = batch[0].data['length']
|
|
tree = Library()
|
|
top = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((length, 0), 0, ptype='wire'),
|
|
})
|
|
top.ref('shared')
|
|
tree['_top'] = top
|
|
return tree
|
|
|
|
lib = Library()
|
|
lib['shared'] = Pattern(annotations={'shared': [1]})
|
|
p = Pather(lib, tools=SharedRefTool())
|
|
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())
|
|
|
|
@pytest.mark.parametrize('append', [True, False])
|
|
def test_pather_render_rejects_output_port_that_misses_planned_endpoint(append: bool) -> None:
|
|
class WrongEndpointTool(Tool):
|
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
|
if kind != 'straight':
|
|
return ()
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return (StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype=ptype,
|
|
endpoint_planner=lambda length: Port((length, 0), rotation=pi, ptype=ptype),
|
|
commit_planner=lambda length: {'length': length},
|
|
),)
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
|
length = batch[0].data['length']
|
|
tree = Library()
|
|
tree['_top'] = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((length + 1, 0), pi, ptype='wire'),
|
|
})
|
|
return tree
|
|
|
|
lib = Library()
|
|
p = Pather(lib, tools=WrongEndpointTool(), render='deferred')
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.straight('A', 10)
|
|
|
|
with pytest.raises(BuildError, match='does not match planned endpoint'):
|
|
p.render(append=append)
|
|
|
|
assert not p.pattern.refs
|
|
assert not lib
|
|
|
|
@pytest.mark.parametrize('append', [True, False])
|
|
def test_pather_render_rejects_output_port_with_wrong_ptype(append: bool) -> None:
|
|
class WrongPtypeTool(Tool):
|
|
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
|
if kind != 'straight':
|
|
return ()
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return (StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype=ptype,
|
|
endpoint_planner=lambda length: Port((length, 0), rotation=pi, ptype=ptype),
|
|
commit_planner=lambda length: {'length': length},
|
|
),)
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
|
length = batch[0].data['length']
|
|
tree = Library()
|
|
tree['_top'] = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((length, 0), 0, ptype='metal'),
|
|
})
|
|
return tree
|
|
|
|
lib = Library()
|
|
p = Pather(lib, tools=WrongPtypeTool(), render='deferred')
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.straight('A', 10)
|
|
|
|
with pytest.raises(BuildError, match='does not match planned endpoint'):
|
|
p.render(append=append)
|
|
|
|
assert not p.pattern.refs
|
|
assert not lib
|
|
|
|
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, render='deferred')
|
|
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
|