"""Provide rasterization routines."""
from typing import List, Tuple, Optional, Dict, Any, Union
import numpy as np
from fibomat.shapes.arc import Arc
from fibomat.shapes.line import Line
from fibomat.shapes.polygon import Polygon
from fibomat.shapes.polyline import Polyline
from fibomat.shapes.rasterizedpoints import RasterizedPoints
from fibomat.shapes.shape import Shape
from fibomat.shapes.arc_spline import ArcSplineCompatible, ArcSpline
from fibomat.shapes.hollow_arc_spline import HollowArcSpline
from fibomat.linalg import Vector, translate, rotate, VectorLike, BoundingBox
from fibomat.curve_tools.intersections import curve_intersections
from fibomat.shapes._line_non_continuous import LineNonContinuous
from fibomat import _libfibomat
def _rasterize_arc_spline_non_continuous_curve(
curve: Union[ArcSpline, LineNonContinuous], pitch: float
) -> RasterizedPoints:
# pylint: disable=invalid-name,too-many-locals
pitch = float(pitch)
# n_points = int(curve.length / pitch) + 1
points = []
i_points = 0
offset = 0.
for segment in curve.segments:
if isinstance(segment, Arc):
arc: Arc = segment
offset_angle = offset / arc.radius
pitch_angle = pitch / arc.radius
if offset_angle <= arc.theta:
points_on_arc = int((arc.theta - offset_angle) / pitch_angle) + 1
if np.isclose((arc.theta - offset_angle) / pitch_angle - points_on_arc - 1, 0.):
points_on_arc += 1
if not arc.sweep_dir:
pitch_angle *= -1.
arc_start_angle = arc.start_angle - offset_angle
else:
arc_start_angle = arc.start_angle + offset_angle
center = np.array(arc.center)
theta = pitch_angle*np.arange(points_on_arc)
points.append(
center + arc.radius * np.column_stack(
(np.cos(theta + arc_start_angle), np.sin(theta + arc_start_angle))
)
)
i_points += points_on_arc
offset = pitch - (arc.length - offset - (len(theta)-1) * pitch)
else:
offset -= arc.length
elif isinstance(segment, Line):
line: Line = segment
if offset <= line.length:
direction = (line.end - line.start).normalized()
start = line.start + direction * offset
points_on_line = int((line.length-offset) / pitch) + 1
direction = np.array(direction)
start = np.array(start)
t = pitch * np.arange(points_on_line)
# points[i_points:i_points+points_on_line, :2] = start + np.repeat(t[None, :], 2, axis=0).T * direction
points.append(start + np.repeat(t[None, :], 2, axis=0).T * direction)
# points[i_points:i_points+points_on_line, 2] = 1.
i_points += points_on_line
offset = pitch - (line.length - t[-1] - offset)
else:
offset -= line.length
else:
raise RuntimeError(f'Cannot rasterize segment type {segment.__class__.__name__}')
# assert i_points == n_points
dwell_points = np.ones(shape=(i_points, 3))
dwell_points[:, :2] = np.concatenate(points)
return RasterizedPoints(dwell_points, curve.is_closed)
[docs]def rasterize(curve: Shape, pitch: float) -> RasterizedPoints:
"""Rasterize the outline of a Shape with a given pitch uniformly.
For this, the shape must be convertible to an ArcSpline or must have a `rasterize` method expecting the pitch as
input.
Args:
curve (Shape): curve
pitch (float): pitch (spacing of points)
Returns:
RasterizedPoints
Raises:
ValueError: Raised if curve is no ArcSpline, ArcSplineCompatible or not have a `rasterize` method.
"""
if isinstance(curve, ArcSpline) or isinstance(curve, LineNonContinuous):
return _rasterize_arc_spline_non_continuous_curve(curve, pitch)
if raster_method := getattr(curve, 'rasterize', None):
return raster_method(curve, pitch)
if isinstance(curve, ArcSplineCompatible):
return _rasterize_arc_spline_non_continuous_curve(curve.to_arc_spline(), pitch)
raise ValueError(f'Cannot rasterize the passed object of type {curve.__class__}.')
[docs]def rasterize_with_const_error(curve: ArcSpline, error: float) -> Union[Polyline, Polygon]:
"""Convert a arc spline to polygon or polyline.
Args:
curve: arc spline
error: maximum distance between curve an returned polyine/polygon
Returns:
Polygon if arc spline is closed and polyline otherwise
"""
approx = ArcSpline(_libfibomat.convert_arcs_to_lines(curve.arc_spline_impl, error))
if curve.is_closed:
return Polygon(approx.vertices[:, :2])
else:
return Polyline(approx.vertices[:, :2])
def _make_line(
curve: ArcSpline, intersection_interval: Tuple[Dict[str, Any], Dict[str, Any]], holes: List[ArcSpline]
) -> Optional[Line]:
start = Vector(intersection_interval[0]['pos'])
end = Vector(intersection_interval[1]['pos'])
assert np.isclose(start.y, end.y)
assert end.x > start.x
midpoint = Vector((start.x + end.x) / 2, start.y)
if curve.contains(midpoint):
for hole in holes:
if hole.contains(midpoint):
return
return Line(start, end)
[docs]def fill_with_lines(
shape: Union[ArcSpline, HollowArcSpline],
pitch: float,
alpha: float,
invert: bool,
seed: Optional[VectorLike] = None
) -> List[List[Line]]:
"""Fill a closed shape with lines which are rotated by `alpha`-
Args:
shape (Union[ArcSpline, HollowArcSpline]): closed curve to be filled
pitch (float): distance between lines
alpha (float): rotation angle of lines with respect to x-axis
invert (bool):
Returns:
List[ArcSpline]
Raises:
ValueError: Raised if angle < 0 or angle > pi.
NotImplementedError: Raised if some kind of singularities occur. This can be fixed (probably) if the shape is
rotated.
"""
# pylint: disable=invalid-name,too-many-locals
# TODO: cache spatial tree for intersection calculations
if isinstance(shape, ArcSpline):
curve = shape
if not curve.is_closed:
raise ValueError('ArcSpline is not closed.')
holes = []
elif isinstance(shape, HollowArcSpline):
curve = shape.boundary
holes = shape.holes
else:
raise TypeError(f'Shape must be ArcSpline or HollowArcSpline (got {type(shape)}).')
if not (-np.pi/2 <= alpha <= np.pi/2):
raise ValueError('alpha < -pi/2 or alpha > pi/2')
seed = (Vector(seed) - curve.center).rotated(-alpha) if seed is not None else Vector()
# seed = Vector(seed) if seed is not None else Vector()
center = curve.center
curve = curve.transformed(
translate(-center) | rotate(-alpha)
)
holes = list(map(lambda hole: hole.transformed(translate(-center) | rotate(-alpha)), holes))
extend = 2 * pitch # 2 * pitch
bbox_curve = curve.bounding_box
bbox = BoundingBox(bbox_curve.lower_left - (extend, extend), bbox_curve.upper_right + (extend, extend))
bbox_left = bbox.lower_left.x
bbox_right = bbox.upper_right.x
bbox_top = pitch * (np.fix(bbox.upper_right.y / pitch)) + seed.y - pitch * np.fix(seed.y / pitch) # pitch * np.fix((bbox.upper_right.y - seed.y) / pitch)
bbox_bottom = bbox_top - bbox.height # pitch * np.fix((bbox.center.y - bbox.height / 2) / pitch)
height = np.abs(bbox_top - bbox_bottom)
y = np.arange(-height / 2, height / 2, pitch)
ny = len(y)
if invert:
intersection_line_base = ArcSpline([(bbox_left, bbox_bottom, 0), (bbox_right, bbox_bottom, 0)], False)
pitch = -pitch
else:
intersection_line_base = ArcSpline([(bbox_left, bbox_top, 0), (bbox_right, bbox_top, 0)], False)
fill_lines: List[List[Line]] = []
for i_y in range(ny):
intersection_line = intersection_line_base.translated((0, -i_y*pitch))
intersections = curve_intersections(curve, intersection_line)['intersections']
# ignore coincidences for now
# # TODO: handle case, if cl_inter contains coincidence
# assert len(cl_intersections['coincidences']) == 0
if len(intersections) > 1:
for hole in holes:
intersections.extend(curve_intersections(hole, intersection_line)['intersections'])
intersections_sorted = sorted(
intersections,
key=lambda intersection: intersection['pos'][0]
)
fill_line = []
for intersection_interval in zip(intersections_sorted, intersections_sorted[1:]):
if part_fill_line := _make_line(curve, intersection_interval, holes):
fill_line.append(
part_fill_line.transformed(
translate(center) | rotate(alpha, origin=center)
)
)
if fill_line:
fill_lines.append(fill_line)
return fill_lines