[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 functools import wraps
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..library import ILibrary, TreeView
|
from ..library import ILibrary, TreeView
|
||||||
|
|
@ -579,22 +580,54 @@ class RenderPather(PatherMixin):
|
||||||
|
|
||||||
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
|
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
|
||||||
assert batch[0].tool is not None
|
assert batch[0].tool is not None
|
||||||
name = lib << batch[0].tool.render(batch, port_names=tool_port_names)
|
# Tools render in local space (first port at 0,0, rotation 0).
|
||||||
pat.ports[portspec] = batch[0].start_port.copy()
|
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:
|
if append:
|
||||||
pat.plug(lib[name], {portspec: tool_port_names[0]}, append=append)
|
# pat.plug() translates and rotates the tool's local output to the start port.
|
||||||
del lib[name] # NOTE if the rendered pattern has refs, those are now in `pat` but not flattened
|
pat.plug(lib[name], {portspec: actual_in}, append=append)
|
||||||
|
del lib[name]
|
||||||
else:
|
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():
|
for portspec, steps in self.paths.items():
|
||||||
|
if not steps:
|
||||||
|
continue
|
||||||
|
|
||||||
batch: list[RenderStep] = []
|
batch: list[RenderStep] = []
|
||||||
|
# Initialize continuity check with the start of the entire path.
|
||||||
|
prev_end = steps[0].start_port
|
||||||
|
|
||||||
for step in steps:
|
for step in steps:
|
||||||
appendable_op = step.opcode in ('L', 'S', 'U')
|
appendable_op = step.opcode in ('L', 'S', 'U')
|
||||||
same_tool = batch and step.tool == batch[0].tool
|
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 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)
|
render_batch(portspec, batch, append)
|
||||||
batch = []
|
batch = []
|
||||||
|
|
||||||
|
|
@ -603,8 +636,14 @@ class RenderPather(PatherMixin):
|
||||||
batch.append(step)
|
batch.append(step)
|
||||||
|
|
||||||
# Opcodes which break the batch go below this line
|
# Opcodes which break the batch go below this line
|
||||||
if not appendable_op and portspec in pat.ports:
|
if not appendable_op:
|
||||||
del pat.ports[portspec]
|
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 the last batch didn't end yet
|
||||||
if batch:
|
if batch:
|
||||||
|
|
@ -626,7 +665,11 @@ class RenderPather(PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
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
|
return self
|
||||||
|
|
||||||
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
||||||
|
|
@ -640,7 +683,11 @@ class RenderPather(PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
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
|
return self
|
||||||
|
|
||||||
def mirror(self, axis: int) -> Self:
|
def mirror(self, axis: int) -> Self:
|
||||||
|
|
@ -654,6 +701,9 @@ class RenderPather(PatherMixin):
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
self.pattern.mirror(axis)
|
self.pattern.mirror(axis)
|
||||||
|
for steps in self.paths.values():
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
steps[i] = step.mirrored(axis)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_dead(self) -> Self:
|
def set_dead(self) -> Self:
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,43 @@ class RenderStep:
|
||||||
if self.opcode != 'P' and self.tool is None:
|
if self.opcode != 'P' and self.tool is None:
|
||||||
raise BuildError('Got tool=None but the opcode is not "P"')
|
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:
|
class Tool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -991,29 +1028,43 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||||
**kwargs, # noqa: ARG002 (unused)
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
path_vertices = [batch[0].start_port.offset]
|
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
||||||
for step in batch:
|
# 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
|
assert step.tool == self
|
||||||
|
|
||||||
port_rot = step.start_port.rotation
|
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':
|
if step.opcode == 'L':
|
||||||
length, bend_run = step.data
|
|
||||||
|
length, _ = step.data
|
||||||
dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0)
|
dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0)
|
||||||
#path_vertices.append(step.start_port.offset)
|
|
||||||
path_vertices.append(step.start_port.offset + dxy)
|
path_vertices.append(step.start_port.offset + dxy)
|
||||||
else:
|
else:
|
||||||
raise BuildError(f'Unrecognized opcode "{step.opcode}"')
|
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
|
# 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')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
||||||
pat.ports = {
|
pat.ports = {
|
||||||
port_names[0]: batch[0].start_port.copy().rotate(pi),
|
port_names[0]: local_batch[0].start_port.copy().rotate(pi),
|
||||||
port_names[1]: batch[-1].end_port.copy().rotate(pi),
|
port_names[1]: local_batch[-1].end_port.copy().rotate(pi),
|
||||||
}
|
}
|
||||||
return tree
|
return tree
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue