[RenderPather / PathTool] Improve support for port transformations
So that moving a port while in the middle of planning a path doesn't break everything
This commit is contained in:
parent
4332cf14c0
commit
16875e9cd6
2 changed files with 121 additions and 20 deletions
|
|
@ -9,8 +9,9 @@ from collections import defaultdict
|
|||
from functools import wraps
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary, TreeView
|
||||
|
|
@ -579,22 +580,54 @@ class RenderPather(PatherMixin):
|
|||
|
||||
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
|
||||
assert batch[0].tool is not None
|
||||
name = lib << batch[0].tool.render(batch, port_names=tool_port_names)
|
||||
pat.ports[portspec] = batch[0].start_port.copy()
|
||||
# Tools render in local space (first port at 0,0, rotation 0).
|
||||
tree = batch[0].tool.render(batch, port_names=tool_port_names)
|
||||
|
||||
actual_in, actual_out = tool_port_names
|
||||
name = lib << tree
|
||||
|
||||
# To plug the segment at its intended location, we create a
|
||||
# 'stationary' port in our temporary pattern that matches
|
||||
# the batch's planned start.
|
||||
if portspec in pat.ports:
|
||||
del pat.ports[portspec]
|
||||
|
||||
stationary_port = batch[0].start_port.copy()
|
||||
pat.ports[portspec] = stationary_port
|
||||
|
||||
if append:
|
||||
pat.plug(lib[name], {portspec: tool_port_names[0]}, append=append)
|
||||
del lib[name] # NOTE if the rendered pattern has refs, those are now in `pat` but not flattened
|
||||
# pat.plug() translates and rotates the tool's local output to the start port.
|
||||
pat.plug(lib[name], {portspec: actual_in}, append=append)
|
||||
del lib[name]
|
||||
else:
|
||||
pat.plug(lib.abstract(name), {portspec: tool_port_names[0]}, append=append)
|
||||
pat.plug(lib.abstract(name), {portspec: actual_in}, append=append)
|
||||
|
||||
# Rename output back to portspec for the next batch.
|
||||
if portspec not in pat.ports and actual_out in pat.ports:
|
||||
pat.rename_ports({actual_out: portspec}, overwrite=True)
|
||||
|
||||
for portspec, steps in self.paths.items():
|
||||
if not steps:
|
||||
continue
|
||||
|
||||
batch: list[RenderStep] = []
|
||||
# Initialize continuity check with the start of the entire path.
|
||||
prev_end = steps[0].start_port
|
||||
|
||||
for step in steps:
|
||||
appendable_op = step.opcode in ('L', 'S', 'U')
|
||||
same_tool = batch and step.tool == batch[0].tool
|
||||
|
||||
# Check continuity with tolerance
|
||||
offsets_match = numpy.allclose(step.start_port.offset, prev_end.offset)
|
||||
rotations_match = (step.start_port.rotation is None and prev_end.rotation is None) or (
|
||||
step.start_port.rotation is not None and prev_end.rotation is not None and
|
||||
numpy.isclose(step.start_port.rotation, prev_end.rotation)
|
||||
)
|
||||
continuous = offsets_match and rotations_match
|
||||
|
||||
# If we can't continue a batch, render it
|
||||
if batch and (not appendable_op or not same_tool):
|
||||
if batch and (not appendable_op or not same_tool or not continuous):
|
||||
render_batch(portspec, batch, append)
|
||||
batch = []
|
||||
|
||||
|
|
@ -603,8 +636,14 @@ class RenderPather(PatherMixin):
|
|||
batch.append(step)
|
||||
|
||||
# Opcodes which break the batch go below this line
|
||||
if not appendable_op and portspec in pat.ports:
|
||||
del pat.ports[portspec]
|
||||
if not appendable_op:
|
||||
if portspec in pat.ports:
|
||||
del pat.ports[portspec]
|
||||
# Plugged ports should be tracked
|
||||
if step.opcode == 'P' and portspec in pat.ports:
|
||||
del pat.ports[portspec]
|
||||
|
||||
prev_end = step.end_port
|
||||
|
||||
#If the last batch didn't end yet
|
||||
if batch:
|
||||
|
|
@ -626,7 +665,11 @@ class RenderPather(PatherMixin):
|
|||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.translate_elements(offset)
|
||||
offset_arr: NDArray[numpy.float64] = numpy.asarray(offset)
|
||||
self.pattern.translate_elements(offset_arr)
|
||||
for steps in self.paths.values():
|
||||
for i, step in enumerate(steps):
|
||||
steps[i] = step.transformed(offset_arr, 0, numpy.zeros(2))
|
||||
return self
|
||||
|
||||
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
||||
|
|
@ -640,7 +683,11 @@ class RenderPather(PatherMixin):
|
|||
Returns:
|
||||
self
|
||||
"""
|
||||
self.pattern.rotate_around(pivot, angle)
|
||||
pivot_arr: NDArray[numpy.float64] = numpy.asarray(pivot)
|
||||
self.pattern.rotate_around(pivot_arr, angle)
|
||||
for steps in self.paths.values():
|
||||
for i, step in enumerate(steps):
|
||||
steps[i] = step.transformed(numpy.zeros(2), angle, pivot_arr)
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int) -> Self:
|
||||
|
|
@ -654,6 +701,9 @@ class RenderPather(PatherMixin):
|
|||
self
|
||||
"""
|
||||
self.pattern.mirror(axis)
|
||||
for steps in self.paths.values():
|
||||
for i, step in enumerate(steps):
|
||||
steps[i] = step.mirrored(axis)
|
||||
return self
|
||||
|
||||
def set_dead(self) -> Self:
|
||||
|
|
|
|||
|
|
@ -47,6 +47,43 @@ class RenderStep:
|
|||
if self.opcode != 'P' and self.tool is None:
|
||||
raise BuildError('Got tool=None but the opcode is not "P"')
|
||||
|
||||
def transformed(self, translation: NDArray[numpy.float64], rotation: float, pivot: NDArray[numpy.float64]) -> 'RenderStep':
|
||||
"""
|
||||
Return a new RenderStep with transformed start and end ports.
|
||||
"""
|
||||
new_start = self.start_port.copy()
|
||||
new_end = self.end_port.copy()
|
||||
|
||||
for pp in (new_start, new_end):
|
||||
pp.rotate_around(pivot, rotation)
|
||||
pp.translate(translation)
|
||||
|
||||
return RenderStep(
|
||||
opcode = self.opcode,
|
||||
tool = self.tool,
|
||||
start_port = new_start,
|
||||
end_port = new_end,
|
||||
data = self.data,
|
||||
)
|
||||
|
||||
def mirrored(self, axis: int) -> 'RenderStep':
|
||||
"""
|
||||
Return a new RenderStep with mirrored start and end ports.
|
||||
"""
|
||||
new_start = self.start_port.copy()
|
||||
new_end = self.end_port.copy()
|
||||
|
||||
new_start.mirror(axis)
|
||||
new_end.mirror(axis)
|
||||
|
||||
return RenderStep(
|
||||
opcode = self.opcode,
|
||||
tool = self.tool,
|
||||
start_port = new_start,
|
||||
end_port = new_end,
|
||||
data = self.data,
|
||||
)
|
||||
|
||||
|
||||
class Tool:
|
||||
"""
|
||||
|
|
@ -991,29 +1028,43 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
|
||||
path_vertices = [batch[0].start_port.offset]
|
||||
for step in batch:
|
||||
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
||||
# This allows the path to be rendered with its original orientation, simplified by
|
||||
# translation to the origin. RenderPather.render will handle the final placement
|
||||
# (including rotation alignment) via `pat.plug`.
|
||||
first_port = batch[0].start_port
|
||||
translation = -first_port.offset
|
||||
rotation = 0
|
||||
pivot = first_port.offset
|
||||
|
||||
# Localize the batch for rendering
|
||||
local_batch = [step.transformed(translation, rotation, pivot) for step in batch]
|
||||
|
||||
path_vertices = [local_batch[0].start_port.offset]
|
||||
for step in local_batch:
|
||||
assert step.tool == self
|
||||
|
||||
port_rot = step.start_port.rotation
|
||||
assert port_rot is not None
|
||||
# Masque convention: Port rotation points INTO the device.
|
||||
# So the direction of travel for the path is AWAY from the port, i.e., port_rot + pi.
|
||||
|
||||
if step.opcode == 'L':
|
||||
length, bend_run = step.data
|
||||
|
||||
length, _ = step.data
|
||||
dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0)
|
||||
#path_vertices.append(step.start_port.offset)
|
||||
path_vertices.append(step.start_port.offset + dxy)
|
||||
else:
|
||||
raise BuildError(f'Unrecognized opcode "{step.opcode}"')
|
||||
|
||||
if (path_vertices[-1] != batch[-1].end_port.offset).any():
|
||||
# Check if the last vertex added is already at the end port location
|
||||
if not numpy.allclose(path_vertices[-1], local_batch[-1].end_port.offset):
|
||||
# If the path ends in a bend, we need to add the final vertex
|
||||
path_vertices.append(batch[-1].end_port.offset)
|
||||
path_vertices.append(local_batch[-1].end_port.offset)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
||||
pat.ports = {
|
||||
port_names[0]: batch[0].start_port.copy().rotate(pi),
|
||||
port_names[1]: batch[-1].end_port.copy().rotate(pi),
|
||||
port_names[0]: local_batch[0].start_port.copy().rotate(pi),
|
||||
port_names[1]: local_batch[-1].end_port.copy().rotate(pi),
|
||||
}
|
||||
return tree
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue