Avoid crossing perimeters: Further refactoring for clarity, code review.

This commit is contained in:
Vojtech Bubnik 2020-11-17 15:18:19 +01:00
parent 62ab17bf6e
commit f206b743fd
8 changed files with 363 additions and 393 deletions

View File

@ -238,7 +238,14 @@ public:
{ {
const Slic3r::Points &ipts = *m_contours[contour_and_segment_idx.first]; const Slic3r::Points &ipts = *m_contours[contour_and_segment_idx.first];
size_t ipt = contour_and_segment_idx.second; size_t ipt = contour_and_segment_idx.second;
return std::pair<const Slic3r::Point&, const Slic3r::Point&>(ipts[ipt], ipts[(ipt + 1 == ipts.size()) ? 0 : ipt + 1]); return std::pair<const Slic3r::Point&, const Slic3r::Point&>(ipts[ipt], ipts[ipt + 1 == ipts.size() ? 0 : ipt + 1]);
}
Line line(const std::pair<size_t, size_t> &contour_and_segment_idx) const
{
const Slic3r::Points &ipts = *m_contours[contour_and_segment_idx.first];
size_t ipt = contour_and_segment_idx.second;
return Line(ipts[ipt], ipts[ipt + 1 == ipts.size() ? 0 : ipt + 1]);
} }
protected: protected:

View File

@ -42,11 +42,11 @@ void ExPolygon::scale(double factor)
hole.scale(factor); hole.scale(factor);
} }
void ExPolygon::translate(double x, double y) void ExPolygon::translate(const Point &p)
{ {
contour.translate(x, y); contour.translate(p);
for (Polygon &hole : holes) for (Polygon &hole : holes)
hole.translate(x, y); hole.translate(p);
} }
void ExPolygon::rotate(double angle) void ExPolygon::rotate(double angle)

View File

@ -42,7 +42,8 @@ public:
operator Polylines() const; operator Polylines() const;
void clear() { contour.points.clear(); holes.clear(); } void clear() { contour.points.clear(); holes.clear(); }
void scale(double factor); void scale(double factor);
void translate(double x, double y); void translate(double x, double y) { this->translate(Point(coord_t(x), coord_t(y))); }
void translate(const Point &vector);
void rotate(double angle); void rotate(double angle);
void rotate(double angle, const Point &center); void rotate(double angle, const Point &center);
double area() const; double area() const;

View File

@ -4,6 +4,7 @@
#include "../Print.hpp" #include "../Print.hpp"
#include "../Polygon.hpp" #include "../Polygon.hpp"
#include "../ExPolygon.hpp" #include "../ExPolygon.hpp"
#include "../Geometry.hpp"
#include "../ClipperUtils.hpp" #include "../ClipperUtils.hpp"
#include "../SVG.hpp" #include "../SVG.hpp"
#include "AvoidCrossingPerimeters.hpp" #include "AvoidCrossingPerimeters.hpp"
@ -16,7 +17,7 @@ namespace Slic3r {
struct TravelPoint struct TravelPoint
{ {
Point point; Point point;
// Index of the polygon containing this point. A negative value indicates that the point is not on any border // Index of the polygon containing this point. A negative value indicates that the point is not on any border.
int border_idx; int border_idx;
}; };
@ -26,17 +27,11 @@ struct Intersection
size_t border_idx; size_t border_idx;
// Index of the line on the polygon containing this point of intersection. // Index of the line on the polygon containing this point of intersection.
size_t line_idx; size_t line_idx;
// Point of intersection projected on the travel path.
Point point_transformed;
// Point of intersection. // Point of intersection.
Point point; Point point;
Intersection(size_t border_idx, size_t line_idx, const Point &point_transformed, const Point &point)
: border_idx(border_idx), line_idx(line_idx), point_transformed(point_transformed), point(point){};
inline bool operator<(const Intersection &other) const { return this->point_transformed.x() < other.point_transformed.x(); }
}; };
// Finding all intersections of a set of contours with a line segment.
struct AllIntersectionsVisitor struct AllIntersectionsVisitor
{ {
AllIntersectionsVisitor(const EdgeGrid::Grid &grid, std::vector<Intersection> &intersections) AllIntersectionsVisitor(const EdgeGrid::Grid &grid, std::vector<Intersection> &intersections)
@ -45,9 +40,8 @@ struct AllIntersectionsVisitor
AllIntersectionsVisitor(const EdgeGrid::Grid &grid, AllIntersectionsVisitor(const EdgeGrid::Grid &grid,
std::vector<Intersection> &intersections, std::vector<Intersection> &intersections,
const Matrix2d &transform_to_x_axis,
const Line &travel_line) const Line &travel_line)
: grid(grid), intersections(intersections), transform_to_x_axis(transform_to_x_axis), travel_line(travel_line) : grid(grid), intersections(intersections), travel_line(travel_line)
{} {}
void reset() { void reset() {
@ -58,16 +52,11 @@ struct AllIntersectionsVisitor
{ {
// Called with a row and colum of the grid cell, which is intersected by a line. // Called with a row and colum of the grid cell, which is intersected by a line.
auto cell_data_range = grid.cell_data_range(iy, ix); auto cell_data_range = grid.cell_data_range(iy, ix);
for (auto it_contour_and_segment = cell_data_range.first; it_contour_and_segment != cell_data_range.second; for (auto it_contour_and_segment = cell_data_range.first; it_contour_and_segment != cell_data_range.second; ++it_contour_and_segment) {
++it_contour_and_segment) {
// End points of the line segment and their vector.
auto segment = grid.segment(*it_contour_and_segment);
Point intersection_point; Point intersection_point;
if (travel_line.intersection(Line(segment.first, segment.second), &intersection_point) && if (travel_line.intersection(grid.line(*it_contour_and_segment), &intersection_point) &&
intersection_set.find(*it_contour_and_segment) == intersection_set.end()) { intersection_set.find(*it_contour_and_segment) == intersection_set.end()) {
intersections.emplace_back(it_contour_and_segment->first, it_contour_and_segment->second, intersections.push_back({ it_contour_and_segment->first, it_contour_and_segment->second, intersection_point });
(transform_to_x_axis * intersection_point.cast<double>()).cast<coord_t>(), intersection_point);
intersection_set.insert(*it_contour_and_segment); intersection_set.insert(*it_contour_and_segment);
} }
} }
@ -77,57 +66,42 @@ struct AllIntersectionsVisitor
const EdgeGrid::Grid &grid; const EdgeGrid::Grid &grid;
std::vector<Intersection> &intersections; std::vector<Intersection> &intersections;
Matrix2d transform_to_x_axis;
Line travel_line; Line travel_line;
std::unordered_set<std::pair<size_t, size_t>, boost::hash<std::pair<size_t, size_t>>> intersection_set; std::unordered_set<std::pair<size_t, size_t>, boost::hash<std::pair<size_t, size_t>>> intersection_set;
}; };
// Create a rotation matrix for projection on the given vector template<bool forward>
static Matrix2d rotation_by_direction(const Point &direction) static Point find_first_different_vertex(const Polygon &polygon, const size_t point_idx, const Point &point)
{
Matrix2d rotation;
rotation.block<1, 2>(0, 0) = direction.cast<double>() / direction.cast<double>().norm();
rotation(1, 0) = -rotation(0, 1);
rotation(1, 1) = rotation(0, 0);
return rotation;
}
static Point find_first_different_vertex(const Polygon &polygon, const size_t point_idx, const Point &point, bool forward)
{ {
assert(point_idx < polygon.size()); assert(point_idx < polygon.size());
if (point != polygon.points[point_idx]) auto line_idx = int(point_idx);
return polygon.points[point_idx]; //FIXME endless loop if all points are equal to point?
if constexpr (forward)
int line_idx = int(point_idx); for (; point == polygon.points[line_idx]; line_idx = line_idx + 1 < int(polygon.points.size()) ? line_idx + 1 : 0);
if (forward)
for (; point == polygon.points[line_idx]; line_idx = (((line_idx + 1) < int(polygon.points.size())) ? (line_idx + 1) : 0));
else else
for (; point == polygon.points[line_idx]; line_idx = (((line_idx - 1) >= 0) ? (line_idx - 1) : (int(polygon.points.size()) - 1))); for (; point == polygon.points[line_idx]; line_idx = line_idx - 1 >= 0 ? line_idx - 1 : int(polygon.points.size()) - 1);
return polygon.points[line_idx]; return polygon.points[line_idx];
} }
//FIXME will be in Point.h in the master
template<typename T, int Options>
inline Eigen::Matrix<T, 2, 1, Eigen::DontAlign> perp(const Eigen::MatrixBase<Eigen::Matrix<T, 2, 1, Options>>& v) { return Eigen::Matrix<T, 2, 1, Eigen::DontAlign>(-v.y(), v.x()); }
static Vec2d three_points_inward_normal(const Point &left, const Point &middle, const Point &right) static Vec2d three_points_inward_normal(const Point &left, const Point &middle, const Point &right)
{ {
assert(left != middle); assert(left != middle);
assert(middle != right); assert(middle != right);
return (perp(Point(middle - left)).cast<double>().normalized() + perp(Point(right - middle)).cast<double>().normalized()).normalized();
Vec2d normal_1(-1 * (middle.y() - left.y()), middle.x() - left.x());
Vec2d normal_2(-1 * (right.y() - middle.y()), right.x() - middle.x());
normal_1.normalize();
normal_2.normalize();
return (normal_1 + normal_2).normalized();
} }
// Compute normal of the polygon's vertex in an inward direction // Compute normal of the polygon's vertex in an inward direction
static Vec2d get_polygon_vertex_inward_normal(const Polygon &polygon, const size_t point_idx) static Vec2d get_polygon_vertex_inward_normal(const Polygon &polygon, const size_t point_idx)
{ {
const size_t left_idx = (point_idx <= 0) ? (polygon.size() - 1) : (point_idx - 1); const size_t left_idx = point_idx == 0 ? polygon.size() - 1 : point_idx - 1;
const size_t right_idx = (point_idx >= (polygon.size() - 1)) ? 0 : (point_idx + 1); const size_t right_idx = point_idx + 1 == polygon.size() ? 0 : point_idx + 1;
const Point &middle = polygon.points[point_idx]; const Point &middle = polygon.points[point_idx];
const Point &left = find_first_different_vertex(polygon, left_idx, middle, false); const Point &left = find_first_different_vertex<false>(polygon, left_idx, middle);
const Point &right = find_first_different_vertex(polygon, right_idx, middle, true); const Point &right = find_first_different_vertex<true>(polygon, right_idx, middle);
return three_points_inward_normal(left, middle, right); return three_points_inward_normal(left, middle, right);
} }
@ -140,114 +114,11 @@ static Point get_polygon_vertex_offset(const Polygon &polygon, const size_t poin
// Compute offset (in the direction of inward normal) of the point(passed on "middle") based on the nearest points laying on the polygon (left_idx and right_idx). // Compute offset (in the direction of inward normal) of the point(passed on "middle") based on the nearest points laying on the polygon (left_idx and right_idx).
static Point get_middle_point_offset(const Polygon &polygon, const size_t left_idx, const size_t right_idx, const Point &middle, const coord_t offset) static Point get_middle_point_offset(const Polygon &polygon, const size_t left_idx, const size_t right_idx, const Point &middle, const coord_t offset)
{ {
const Point &left = find_first_different_vertex(polygon, left_idx, middle, false); const Point &left = find_first_different_vertex<false>(polygon, left_idx, middle);
const Point &right = find_first_different_vertex(polygon, right_idx, middle, true); const Point &right = find_first_different_vertex<true>(polygon, right_idx, middle);
return middle + (three_points_inward_normal(left, middle, right) * double(offset)).cast<coord_t>(); return middle + (three_points_inward_normal(left, middle, right) * double(offset)).cast<coord_t>();
} }
static bool check_if_could_cross_perimeters(const BoundingBox &bbox, const Point &start, const Point &end)
{
bool start_out_of_bound = !bbox.contains(start), end_out_of_bound = !bbox.contains(end);
// When both endpoints are out of the bounding box, it needs to check in more detail.
if (start_out_of_bound && end_out_of_bound) {
Point intersection;
return bbox.polygon().intersection(Line(start, end), &intersection);
}
return true;
}
static std::pair<Point, Point> clamp_endpoints_by_bounding_box(const BoundingBox &bbox, const Point &start, const Point &end)
{
bool start_out_of_bound = !bbox.contains(start), end_out_of_bound = !bbox.contains(end);
Point start_clamped = start, end_clamped = end;
Points intersections;
if (start_out_of_bound || end_out_of_bound) {
bbox.polygon().intersections(Line(start, end), &intersections);
assert(intersections.size() <= 2);
}
if (start_out_of_bound && !end_out_of_bound && intersections.size() == 1) {
start_clamped = intersections[0];
} else if (!start_out_of_bound && end_out_of_bound && intersections.size() == 1) {
end_clamped = intersections[0];
} else if (start_out_of_bound && end_out_of_bound && intersections.size() == 2) {
if ((intersections[0] - start).cast<double>().norm() < (intersections[1] - start).cast<double>().norm()) {
start_clamped = intersections[0];
end_clamped = intersections[1];
} else {
start_clamped = intersections[1];
end_clamped = intersections[0];
}
}
return std::make_pair(start_clamped, end_clamped);
}
static inline float get_default_perimeter_spacing(const Print &print)
{
const std::vector<double> &nozzle_diameters = print.config().nozzle_diameter.values;
return float(scale_(*std::max_element(nozzle_diameters.begin(), nozzle_diameters.end())));
}
static float get_perimeter_spacing(const Layer &layer)
{
size_t regions_count = 0;
float perimeter_spacing = 0.f;
for (const LayerRegion *layer_region : layer.regions()) {
perimeter_spacing += layer_region->flow(frPerimeter).scaled_spacing();
++regions_count;
}
assert(perimeter_spacing >= 0.f);
if (regions_count != 0)
perimeter_spacing /= float(regions_count);
else
perimeter_spacing = get_default_perimeter_spacing(*layer.object()->print());
return perimeter_spacing;
}
static float get_perimeter_spacing_external(const Layer &layer)
{
size_t regions_count = 0;
float perimeter_spacing = 0.f;
for (const PrintObject *object : layer.object()->print()->objects())
for (Layer *l : object->layers())
if ((layer.print_z - EPSILON) <= l->print_z && l->print_z <= (layer.print_z + EPSILON))
for (const LayerRegion *layer_region : l->regions()) {
perimeter_spacing += layer_region->flow(frPerimeter).scaled_spacing();
++regions_count;
}
assert(perimeter_spacing >= 0.f);
if (regions_count != 0)
perimeter_spacing /= float(regions_count);
else
perimeter_spacing = get_default_perimeter_spacing(*layer.object()->print());
return perimeter_spacing;
}
// Check if anyone of ExPolygons contains whole travel.
template<class T> static bool any_expolygon_contains(const ExPolygons &ex_polygons, const T &travel)
{
for (const ExPolygon &ex_polygon : ex_polygons)
if (ex_polygon.contains(travel)) return true;
return false;
}
static std::pair<Polygons, Polygons> split_expolygon(const ExPolygons &ex_polygons)
{
Polygons contours, holes;
contours.reserve(ex_polygons.size());
holes.reserve(std::accumulate(ex_polygons.begin(), ex_polygons.end(), size_t(0),
[](size_t sum, const ExPolygon &ex_poly) { return sum + ex_poly.holes.size(); }));
for (const ExPolygon &ex_poly : ex_polygons) {
contours.emplace_back(ex_poly.contour);
append(holes, ex_poly.holes);
}
return std::make_pair(std::move(contours), std::move(holes));
}
static Polyline to_polyline(const std::vector<TravelPoint> &travel) static Polyline to_polyline(const std::vector<TravelPoint> &travel)
{ {
Polyline result; Polyline result;
@ -265,6 +136,8 @@ static double travel_length(const std::vector<TravelPoint> &travel) {
return total_length; return total_length;
} }
// #define AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT
#ifdef AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT #ifdef AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT
static void export_travel_to_svg(const Polygons &boundary, static void export_travel_to_svg(const Polygons &boundary,
const Line &original_travel, const Line &original_travel,
@ -294,102 +167,6 @@ static void export_travel_to_svg(const Polygons &boundary,
} }
#endif /* AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT */ #endif /* AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT */
static ExPolygons get_boundary(const Layer &layer)
{
const float perimeter_spacing = get_perimeter_spacing(layer);
const float perimeter_offset = perimeter_spacing / 2.f;
size_t polygons_count = 0;
for (const LayerRegion *layer_region : layer.regions())
polygons_count += layer_region->slices.surfaces.size();
ExPolygons boundary;
boundary.reserve(polygons_count);
for (const LayerRegion *layer_region : layer.regions())
for (const Surface &surface : layer_region->slices.surfaces) boundary.emplace_back(surface.expolygon);
boundary = union_ex(boundary);
ExPolygons perimeter_boundary = offset_ex(boundary, -perimeter_offset);
ExPolygons result_boundary;
if (perimeter_boundary.size() != boundary.size()) {
// If any part of the polygon is missing after shrinking, then for misisng parts are is used the boundary of the slice.
ExPolygons missing_perimeter_boundary = offset_ex(diff_ex(boundary,
offset_ex(perimeter_boundary, perimeter_offset + float(SCALED_EPSILON) / 2.f)),
perimeter_offset + float(SCALED_EPSILON));
perimeter_boundary = offset_ex(perimeter_boundary, perimeter_offset);
perimeter_boundary.reserve(perimeter_boundary.size() + missing_perimeter_boundary.size());
perimeter_boundary.insert(perimeter_boundary.end(), missing_perimeter_boundary.begin(), missing_perimeter_boundary.end());
// By calling intersection_ex some artifacts arose by previous operations are removed.
result_boundary = union_ex(intersection_ex(offset_ex(perimeter_boundary, -perimeter_offset), boundary));
} else {
result_boundary = std::move(perimeter_boundary);
}
auto [contours, holes] = split_expolygon(boundary);
// Add an outer boundary to avoid crossing perimeters from supports
ExPolygons outer_boundary = union_ex(
diff(static_cast<Polygons>(Geometry::convex_hull(offset(contours, 2.f * perimeter_spacing))),
offset(contours, perimeter_spacing + perimeter_offset)));
result_boundary.insert(result_boundary.end(), outer_boundary.begin(), outer_boundary.end());
ExPolygons holes_boundary = offset_ex(holes, -perimeter_spacing);
result_boundary.insert(result_boundary.end(), holes_boundary.begin(), holes_boundary.end());
result_boundary = union_ex(result_boundary);
// Collect all top layers that will not be crossed.
polygons_count = 0;
for (const LayerRegion *layer_region : layer.regions())
for (const Surface &surface : layer_region->fill_surfaces.surfaces)
if (surface.is_top()) ++polygons_count;
if (polygons_count > 0) {
ExPolygons top_layer_polygons;
top_layer_polygons.reserve(polygons_count);
for (const LayerRegion *layer_region : layer.regions())
for (const Surface &surface : layer_region->fill_surfaces.surfaces)
if (surface.is_top()) top_layer_polygons.emplace_back(surface.expolygon);
top_layer_polygons = union_ex(top_layer_polygons);
return diff_ex(result_boundary, offset_ex(top_layer_polygons, -perimeter_offset));
}
return result_boundary;
}
static ExPolygons get_boundary_external(const Layer &layer)
{
const float perimeter_spacing = get_perimeter_spacing_external(layer);
const float perimeter_offset = perimeter_spacing / 2.f;
ExPolygons boundary;
// Collect all polygons for all printed objects and their instances, which will be printed at the same time as passed "layer".
for (const PrintObject *object : layer.object()->print()->objects()) {
ExPolygons polygons_per_obj;
for (Layer *l : object->layers())
if ((layer.print_z - EPSILON) <= l->print_z && l->print_z <= (layer.print_z + EPSILON))
for (const LayerRegion *layer_region : l->regions())
for (const Surface &surface : layer_region->slices.surfaces)
polygons_per_obj.emplace_back(surface.expolygon);
for (const PrintInstance &instance : object->instances()) {
size_t boundary_idx = boundary.size();
boundary.reserve(boundary.size() + polygons_per_obj.size());
boundary.insert(boundary.end(), polygons_per_obj.begin(), polygons_per_obj.end());
for (; boundary_idx < boundary.size(); ++boundary_idx) boundary[boundary_idx].translate(instance.shift.x(), instance.shift.y());
}
}
boundary = union_ex(boundary);
auto [contours, holes] = split_expolygon(boundary);
// Polygons in which is possible traveling without crossing perimeters of another object.
// A convex hull allows removing unnecessary detour caused by following the boundary of the object.
ExPolygons result_boundary = union_ex(
diff(static_cast<Polygons>(Geometry::convex_hull(offset(contours, 2.f * perimeter_spacing))),
offset(contours, perimeter_spacing + perimeter_offset)));
// All holes are extended for forcing travel around the outer perimeter of a hole when a hole is crossed.
ExPolygons holes_boundary = union_ex(diff(offset(holes, perimeter_spacing), offset(holes, perimeter_offset)));
result_boundary.reserve(result_boundary.size() + holes_boundary.size());
result_boundary.insert(result_boundary.end(), holes_boundary.begin(), holes_boundary.end());
result_boundary = union_ex(result_boundary);
return result_boundary;
}
// Returns a direction of the shortest path along the polygon boundary // Returns a direction of the shortest path along the polygon boundary
enum class Direction { Forward, Backward }; enum class Direction { Forward, Backward };
static Direction get_shortest_direction(const Lines &lines, static Direction get_shortest_direction(const Lines &lines,
@ -422,30 +199,90 @@ static Direction get_shortest_direction(const Lines &lines,
return (total_length_forward < total_length_backward) ? Direction::Forward : Direction::Backward; return (total_length_forward < total_length_backward) ? Direction::Forward : Direction::Backward;
} }
static std::vector<TravelPoint> simplify_travel(const EdgeGrid::Grid& edge_grid, const std::vector<TravelPoint>& travel, const Polygons& boundaries, const bool use_heuristics); // Straighten the travel path as long as it does not collide with the contours stored in edge_grid.
static std::vector<TravelPoint> simplify_travel(const EdgeGrid::Grid &edge_grid, const std::vector<TravelPoint> &travel)
{
// Visitor to check for a collision of a line segment with any contour stored inside the edge_grid.
struct Visitor
{
Visitor(const EdgeGrid::Grid &grid) : grid(grid) {}
static size_t avoid_perimeters(const Polygons &boundaries, bool operator()(coord_t iy, coord_t ix)
{
assert(pt_current != nullptr);
assert(pt_next != nullptr);
// Called with a row and colum of the grid cell, which is intersected by a line.
auto cell_data_range = grid.cell_data_range(iy, ix);
this->intersect = false;
for (auto it_contour_and_segment = cell_data_range.first; it_contour_and_segment != cell_data_range.second; ++it_contour_and_segment) {
// End points of the line segment and their vector.
auto segment = grid.segment(*it_contour_and_segment);
if (Geometry::segments_intersect(segment.first, segment.second, *pt_current, *pt_next)) {
this->intersect = true;
return false;
}
}
// Continue traversing the grid along the edge.
return true;
}
const EdgeGrid::Grid &grid;
const Slic3r::Point *pt_current = nullptr;
const Slic3r::Point *pt_next = nullptr;
bool intersect = false;
} visitor(edge_grid);
std::vector<TravelPoint> simplified_path;
simplified_path.reserve(travel.size());
simplified_path.emplace_back(travel.front());
// Try to skip some points in the path.
//FIXME maybe use a binary search to trim the line?
//FIXME how about searching tangent point at long segments?
for (size_t point_idx = 1; point_idx < travel.size(); ++point_idx) {
const Point &current_point = travel[point_idx - 1].point;
TravelPoint next = travel[point_idx];
visitor.pt_current = &current_point;
for (size_t point_idx_2 = point_idx + 1; point_idx_2 < travel.size(); ++point_idx_2) {
if (travel[point_idx_2].point == current_point) {
next = travel[point_idx_2];
point_idx = point_idx_2;
continue;
}
visitor.pt_next = &travel[point_idx_2].point;
edge_grid.visit_cells_intersecting_line(*visitor.pt_current, *visitor.pt_next, visitor);
// Check if deleting point causes crossing a boundary
if (!visitor.intersect) {
next = travel[point_idx_2];
point_idx = point_idx_2;
}
}
simplified_path.emplace_back(next);
}
return simplified_path;
}
// Called by avoid_perimeters() and by simplify_travel_heuristics().
static size_t avoid_perimeters_inner(const Polygons &boundaries,
const EdgeGrid::Grid &edge_grid, const EdgeGrid::Grid &edge_grid,
const Point &start, const Point &start,
const Point &end, const Point &end,
const bool use_heuristics, std::vector<TravelPoint> &result_out)
std::vector<TravelPoint> *result_out)
{ {
const Point direction = end - start; // Find all intersections between boundaries and the line segment, sort them along the line segment.
Matrix2d transform_to_x_axis = rotation_by_direction(direction);
const Line travel_line_orig(start, end);
const Line travel_line((transform_to_x_axis * start.cast<double>()).cast<coord_t>(),
(transform_to_x_axis * end.cast<double>()).cast<coord_t>());
std::vector<Intersection> intersections; std::vector<Intersection> intersections;
{ {
AllIntersectionsVisitor visitor(edge_grid, intersections, transform_to_x_axis, travel_line_orig); AllIntersectionsVisitor visitor(edge_grid, intersections, Line(start, end));
edge_grid.visit_cells_intersecting_line(start, end, visitor); edge_grid.visit_cells_intersecting_line(start, end, visitor);
Vec2d dir = (end - start).cast<double>();
std::sort(intersections.begin(), intersections.end(), [dir](const auto &l, const auto &r) { return (r.point - l.point).template cast<double>().dot(dir) > 0.; });
} }
std::sort(intersections.begin(), intersections.end());
std::vector<TravelPoint> result; std::vector<TravelPoint> result;
result.push_back({start, -1}); result.push_back({start, -1});
for (auto it_first = intersections.begin(); it_first != intersections.end(); ++it_first) { for (auto it_first = intersections.begin(); it_first != intersections.end(); ++it_first) {
@ -500,24 +337,23 @@ static size_t avoid_perimeters(const Polygons &boundaries,
#ifdef AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT #ifdef AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT
{ {
static int iRun = 0; static int iRun = 0;
export_travel_to_svg(boundaries, travel_line_orig, result, intersections, export_travel_to_svg(boundaries, Line(start, end), result, intersections,
debug_out_path("AvoidCrossingPerimeters-initial-%d.svg", iRun++)); debug_out_path("AvoidCrossingPerimetersInner-initial-%d.svg", iRun++));
} }
#endif /* AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT */ #endif /* AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT */
if (! intersections.empty()) if (! intersections.empty())
result = simplify_travel(edge_grid, result, boundaries, use_heuristics); result = simplify_travel(edge_grid, result);
#ifdef AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT #ifdef AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT
{ {
static int iRun = 0; static int iRun = 0;
export_travel_to_svg(boundaries, travel_line_orig, result, intersections, export_travel_to_svg(boundaries, Line(start, end), result, intersections,
debug_out_path("AvoidCrossingPerimeters-final-%d.svg", iRun++)); debug_out_path("AvoidCrossingPerimetersInner-final-%d.svg", iRun++));
} }
#endif /* AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT */ #endif /* AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT */
result_out->reserve(result_out->size() + result.size()); append(result_out, std::move(result));
result_out->insert(result_out->end(), result.begin(), result.end());
return intersections.size(); return intersections.size();
} }
@ -563,10 +399,10 @@ static std::vector<TravelPoint> simplify_travel_heuristics(const EdgeGrid::Grid
visitor.reset(); visitor.reset();
visitor.travel_line.a = current.point; visitor.travel_line.a = current.point;
visitor.travel_line.b = possible_new_next.point; visitor.travel_line.b = possible_new_next.point;
visitor.transform_to_x_axis = rotation_by_direction(visitor.travel_line.vector());
edge_grid.visit_cells_intersecting_line(visitor.travel_line.a, visitor.travel_line.b, visitor); edge_grid.visit_cells_intersecting_line(visitor.travel_line.a, visitor.travel_line.b, visitor);
if (!intersections.empty()) { if (!intersections.empty()) {
std::sort(intersections.begin(), intersections.end()); Vec2d dir = (visitor.travel_line.b - visitor.travel_line.a).cast<double>();
std::sort(intersections.begin(), intersections.end(), [dir](const auto &l, const auto &r) { return (r.point - l.point).template cast<double>().dot(dir) > 0.; });
size_t last_border_idx_count = 0; size_t last_border_idx_count = 0;
for (const Intersection &intersection : intersections) for (const Intersection &intersection : intersections)
if (int(intersection.border_idx) == possible_new_next.border_idx) if (int(intersection.border_idx) == possible_new_next.border_idx)
@ -576,9 +412,9 @@ static std::vector<TravelPoint> simplify_travel_heuristics(const EdgeGrid::Grid
continue; continue;
std::vector<TravelPoint> possible_shortcut; std::vector<TravelPoint> possible_shortcut;
avoid_perimeters(boundaries, edge_grid, current.point, possible_new_next.point, false, &possible_shortcut); avoid_perimeters_inner(boundaries, edge_grid, current.point, possible_new_next.point, possible_shortcut);
double shortcut_travel = travel_length(possible_shortcut); double shortcut_travel = travel_length(possible_shortcut);
if (path_length > shortcut_travel && (path_length - shortcut_travel) > new_path_shorter_by) { if (path_length > shortcut_travel && path_length - shortcut_travel > new_path_shorter_by) {
new_path_shorter_by = path_length - shortcut_travel; new_path_shorter_by = path_length - shortcut_travel;
shortcut = possible_shortcut; shortcut = possible_shortcut;
new_next = possible_new_next; new_next = possible_new_next;
@ -600,78 +436,44 @@ static std::vector<TravelPoint> simplify_travel_heuristics(const EdgeGrid::Grid
return simplified_path; return simplified_path;
} }
static std::vector<TravelPoint> simplify_travel(const EdgeGrid::Grid &edge_grid, // Called by AvoidCrossingPerimeters::travel_to()
const std::vector<TravelPoint> &travel, static size_t avoid_perimeters(const Polygons &boundaries,
const Polygons &boundaries, const EdgeGrid::Grid &edge_grid,
const bool use_heuristics) const Point &start,
const Point &end,
Polyline &result_out)
{ {
struct Visitor // Travel line is completely or partially inside the bounding box.
{ std::vector<TravelPoint> path;
Visitor(const EdgeGrid::Grid &grid) : grid(grid) {} size_t num_intersections = avoid_perimeters_inner(boundaries, edge_grid, start, end, path);
if (num_intersections) {
path = simplify_travel_heuristics(edge_grid, path, boundaries);
std::reverse(path.begin(), path.end());
path = simplify_travel_heuristics(edge_grid, path, boundaries);
std::reverse(path.begin(), path.end());
}
bool operator()(coord_t iy, coord_t ix) result_out = to_polyline(path);
#ifdef AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT
{ {
assert(pt_current != nullptr); static int iRun = 0;
assert(pt_next != nullptr); export_travel_to_svg(boundaries, Line(start, end), path, {}, debug_out_path("AvoidCrossingPerimeters-final-%d.svg", iRun ++));
// Called with a row and colum of the grid cell, which is intersected by a line.
auto cell_data_range = grid.cell_data_range(iy, ix);
this->intersect = false;
for (auto it_contour_and_segment = cell_data_range.first; it_contour_and_segment != cell_data_range.second; ++it_contour_and_segment) {
// End points of the line segment and their vector.
auto segment = grid.segment(*it_contour_and_segment);
if (Geometry::segments_intersect(segment.first, segment.second, *pt_current, *pt_next)) {
this->intersect = true;
return false;
} }
#endif /* AVOID_CROSSING_PERIMETERS_DEBUG_OUTPUT */
return num_intersections;
} }
// Continue traversing the grid along the edge.
// Check if anyone of ExPolygons contains whole travel.
// called by need_wipe()
template<class T> static bool any_expolygon_contains(const ExPolygons &ex_polygons, const T &travel)
{
//FIXME filter by bounding boxes!
for (const ExPolygon &ex_polygon : ex_polygons)
if (ex_polygon.contains(travel))
return true; return true;
} return false;
const EdgeGrid::Grid &grid;
const Slic3r::Point *pt_current = nullptr;
const Slic3r::Point *pt_next = nullptr;
bool intersect = false;
} visitor(edge_grid);
std::vector<TravelPoint> simplified_path;
simplified_path.reserve(travel.size());
simplified_path.emplace_back(travel.front());
// Try to skip some points in the path.
for (size_t point_idx = 1; point_idx < travel.size(); ++point_idx) {
const Point &current_point = travel[point_idx - 1].point;
TravelPoint next = travel[point_idx];
visitor.pt_current = &current_point;
for (size_t point_idx_2 = point_idx + 1; point_idx_2 < travel.size(); ++point_idx_2) {
if (travel[point_idx_2].point == current_point) {
next = travel[point_idx_2];
point_idx = point_idx_2;
continue;
}
visitor.pt_next = &travel[point_idx_2].point;
edge_grid.visit_cells_intersecting_line(*visitor.pt_current, *visitor.pt_next, visitor);
// Check if deleting point causes crossing a boundary
if (!visitor.intersect) {
next = travel[point_idx_2];
point_idx = point_idx_2;
}
}
simplified_path.emplace_back(next);
}
if(use_heuristics) {
simplified_path = simplify_travel_heuristics(edge_grid, simplified_path, boundaries);
std::reverse(simplified_path.begin(),simplified_path.end());
simplified_path = simplify_travel_heuristics(edge_grid, simplified_path, boundaries);
std::reverse(simplified_path.begin(),simplified_path.end());
}
return simplified_path;
} }
static bool need_wipe(const GCode &gcodegen, static bool need_wipe(const GCode &gcodegen,
@ -692,11 +494,8 @@ static bool need_wipe(const GCode &gcodegen,
if (any_expolygon_contains(slice, original_travel)) { if (any_expolygon_contains(slice, original_travel)) {
// Check if original_travel and result_travel are not same. // Check if original_travel and result_travel are not same.
// If both are the same, then it is possible to skip testing of result_travel // If both are the same, then it is possible to skip testing of result_travel
if (result_travel.size() == 2 && result_travel.first_point() == original_travel.a && result_travel.last_point() == original_travel.b) { wipe_needed = !(result_travel.size() > 2 && result_travel.first_point() == original_travel.a && result_travel.last_point() == original_travel.b) &&
wipe_needed = false; !any_expolygon_contains(slice, result_travel);
} else {
wipe_needed = !any_expolygon_contains(slice, result_travel);
}
} else { } else {
wipe_needed = true; wipe_needed = true;
} }
@ -719,28 +518,27 @@ Polyline AvoidCrossingPerimeters::travel_to(const GCode &gcodegen, const Point &
Point end = point + scaled_origin; Point end = point + scaled_origin;
Polyline result_pl; Polyline result_pl;
size_t travel_intersection_count = 0; size_t travel_intersection_count = 0;
if (!check_if_could_cross_perimeters(use_external ? m_bbox_external : m_bbox, start, end)) { Vec2d startf = start.cast<double>();
result_pl = Polyline({start, end}); Vec2d endf = end .cast<double>();
travel_intersection_count = 0; // Trim the travel line by the bounding box.
} else { if (Geometry::liang_barsky_line_clipping(startf, endf, use_external ? m_bbox_external : m_bbox)) {
std::vector<TravelPoint> result; // Travel line is completely or partially inside the bounding box.
auto [start_clamped, end_clamped] = clamp_endpoints_by_bounding_box(use_external ? m_bbox_external : m_bbox, start, end); travel_intersection_count = use_external ?
if (use_external) avoid_perimeters(m_boundaries_external, m_grid_external, startf.cast<coord_t>(), endf.cast<coord_t>(), result_pl) :
travel_intersection_count = avoid_perimeters(m_boundaries_external, m_grid_external, start_clamped, end_clamped, true, &result); avoid_perimeters(m_boundaries, m_grid, startf.cast<coord_t>(), endf.cast<coord_t>(), result_pl);
else
travel_intersection_count = avoid_perimeters(m_boundaries, m_grid, start_clamped, end_clamped, true, &result);
result_pl = to_polyline(result);
}
result_pl.points.front() = start; result_pl.points.front() = start;
result_pl.points.back() = end; result_pl.points.back() = end;
} else {
// Travel line is completely outside the bounding box.
result_pl = {start, end};
travel_intersection_count = 0;
}
Line travel(start, end); Line travel(start, end);
double max_detour_length scale_(gcodegen.config().avoid_crossing_perimeters_max_detour); double max_detour_length scale_(gcodegen.config().avoid_crossing_perimeters_max_detour);
if ((max_detour_length > 0) && ((result_pl.length() - travel.length()) > max_detour_length)) { if (max_detour_length > 0 && (result_pl.length() - travel.length()) > max_detour_length)
result_pl = Polyline({start, end}); result_pl = {start, end};
}
if (use_external) { if (use_external) {
result_pl.translate(-scaled_origin); result_pl.translate(-scaled_origin);
*could_be_wipe_disabled = false; *could_be_wipe_disabled = false;
@ -750,6 +548,172 @@ Polyline AvoidCrossingPerimeters::travel_to(const GCode &gcodegen, const Point &
return result_pl; return result_pl;
} }
// ************************************* AvoidCrossingPerimeters::init_layer() *****************************************
// called by get_perimeter_spacing() / get_perimeter_spacing_external()
static inline float get_default_perimeter_spacing(const Print &print)
{
//FIXME better use extruders printing this PrintObject or this Print?
//FIXME maybe better use an average of printing extruders?
const std::vector<double> &nozzle_diameters = print.config().nozzle_diameter.values;
return float(scale_(*std::max_element(nozzle_diameters.begin(), nozzle_diameters.end())));
}
// called by get_boundary()
static float get_perimeter_spacing(const Layer &layer)
{
size_t regions_count = 0;
float perimeter_spacing = 0.f;
//FIXME not all regions are printing. Collect only non-empty regions?
for (const LayerRegion *layer_region : layer.regions()) {
perimeter_spacing += layer_region->flow(frPerimeter).scaled_spacing();
++ regions_count;
}
assert(perimeter_spacing >= 0.f);
if (regions_count != 0)
perimeter_spacing /= float(regions_count);
else
perimeter_spacing = get_default_perimeter_spacing(*layer.object()->print());
return perimeter_spacing;
}
// called by get_boundary_external()
static float get_perimeter_spacing_external(const Layer &layer)
{
size_t regions_count = 0;
float perimeter_spacing = 0.f;
for (const PrintObject *object : layer.object()->print()->objects())
//FIXME with different layering, layers on other objects will not be found at this object's print_z.
// Search an overlap of layers?
if (const Layer *l = object->get_layer_at_printz(layer.print_z, EPSILON); l)
//FIXME not all regions are printing. Collect only non-empty regions?
for (const LayerRegion *layer_region : l->regions()) {
perimeter_spacing += layer_region->flow(frPerimeter).scaled_spacing();
++ regions_count;
}
assert(perimeter_spacing >= 0.f);
if (regions_count != 0)
perimeter_spacing /= float(regions_count);
else
perimeter_spacing = get_default_perimeter_spacing(*layer.object()->print());
return perimeter_spacing;
}
// called by AvoidCrossingPerimeters::init_layer()->get_boundary()/get_boundary_external()
static std::pair<Polygons, Polygons> split_expolygon(const ExPolygons &ex_polygons)
{
Polygons contours, holes;
contours.reserve(ex_polygons.size());
holes.reserve(std::accumulate(ex_polygons.begin(), ex_polygons.end(), size_t(0),
[](size_t sum, const ExPolygon &ex_poly) { return sum + ex_poly.holes.size(); }));
for (const ExPolygon &ex_poly : ex_polygons) {
contours.emplace_back(ex_poly.contour);
append(holes, ex_poly.holes);
}
return std::make_pair(std::move(contours), std::move(holes));
}
// called by AvoidCrossingPerimeters::init_layer()
static ExPolygons get_boundary(const Layer &layer)
{
const float perimeter_spacing = get_perimeter_spacing(layer);
const float perimeter_offset = perimeter_spacing / 2.f;
size_t polygons_count = 0;
for (const LayerRegion *layer_region : layer.regions())
polygons_count += layer_region->slices.surfaces.size();
ExPolygons boundary;
boundary.reserve(polygons_count);
for (const LayerRegion *layer_region : layer.regions())
for (const Surface &surface : layer_region->slices.surfaces)
boundary.emplace_back(surface.expolygon);
boundary = union_ex(boundary);
ExPolygons perimeter_boundary = offset_ex(boundary, -perimeter_offset);
ExPolygons result_boundary;
if (perimeter_boundary.size() != boundary.size()) {
//FIXME ???
// If any part of the polygon is missing after shrinking, then for misisng parts are is used the boundary of the slice.
ExPolygons missing_perimeter_boundary = offset_ex(diff_ex(boundary,
offset_ex(perimeter_boundary, perimeter_offset + float(SCALED_EPSILON) / 2.f)),
perimeter_offset + float(SCALED_EPSILON));
perimeter_boundary = offset_ex(perimeter_boundary, perimeter_offset);
append(perimeter_boundary, std::move(missing_perimeter_boundary));
// By calling intersection_ex some artifacts arose by previous operations are removed.
result_boundary = intersection_ex(offset_ex(perimeter_boundary, -perimeter_offset), boundary);
} else {
result_boundary = std::move(perimeter_boundary);
}
auto [contours, holes] = split_expolygon(boundary);
// Add an outer boundary to avoid crossing perimeters from supports
ExPolygons outer_boundary = union_ex(
//FIXME flip order of offset and convex_hull
diff(static_cast<Polygons>(Geometry::convex_hull(offset(contours, 2.f * perimeter_spacing))),
offset(contours, perimeter_spacing + perimeter_offset)));
result_boundary.insert(result_boundary.end(), outer_boundary.begin(), outer_boundary.end());
ExPolygons holes_boundary = offset_ex(holes, -perimeter_spacing);
result_boundary.insert(result_boundary.end(), holes_boundary.begin(), holes_boundary.end());
result_boundary = union_ex(result_boundary);
// Collect all top layers that will not be crossed.
polygons_count = 0;
for (const LayerRegion *layer_region : layer.regions())
for (const Surface &surface : layer_region->fill_surfaces.surfaces)
if (surface.is_top()) ++polygons_count;
if (polygons_count > 0) {
ExPolygons top_layer_polygons;
top_layer_polygons.reserve(polygons_count);
for (const LayerRegion *layer_region : layer.regions())
for (const Surface &surface : layer_region->fill_surfaces.surfaces)
if (surface.is_top()) top_layer_polygons.emplace_back(surface.expolygon);
top_layer_polygons = union_ex(top_layer_polygons);
return diff_ex(result_boundary, offset_ex(top_layer_polygons, -perimeter_offset));
}
return result_boundary;
}
// called by AvoidCrossingPerimeters::init_layer()
static ExPolygons get_boundary_external(const Layer &layer)
{
const float perimeter_spacing = get_perimeter_spacing_external(layer);
const float perimeter_offset = perimeter_spacing / 2.f;
ExPolygons boundary;
// Collect all polygons for all printed objects and their instances, which will be printed at the same time as passed "layer".
for (const PrintObject *object : layer.object()->print()->objects()) {
ExPolygons polygons_per_obj;
//FIXME with different layering, layers on other objects will not be found at this object's print_z.
// Search an overlap of layers?
if (const Layer* l = object->get_layer_at_printz(layer.print_z, EPSILON); l)
for (const LayerRegion *layer_region : l->regions())
for (const Surface &surface : layer_region->slices.surfaces)
polygons_per_obj.emplace_back(surface.expolygon);
for (const PrintInstance &instance : object->instances()) {
size_t boundary_idx = boundary.size();
boundary.insert(boundary.end(), polygons_per_obj.begin(), polygons_per_obj.end());
for (; boundary_idx < boundary.size(); ++boundary_idx)
boundary[boundary_idx].translate(instance.shift);
}
}
boundary = union_ex(boundary);
auto [contours, holes] = split_expolygon(boundary);
// Polygons in which is possible traveling without crossing perimeters of another object.
// A convex hull allows removing unnecessary detour caused by following the boundary of the object.
ExPolygons result_boundary = union_ex(
//FIXME flip order of offset and convex_hull
diff(static_cast<Polygons>(Geometry::convex_hull(offset(contours, 2.f * perimeter_spacing))),
offset(contours, perimeter_spacing + perimeter_offset)));
// All holes are extended for forcing travel around the outer perimeter of a hole when a hole is crossed.
append(result_boundary, union_ex(diff(offset(holes, perimeter_spacing), offset(holes, perimeter_offset))));
return union_ex(result_boundary);
}
void AvoidCrossingPerimeters::init_layer(const Layer &layer) void AvoidCrossingPerimeters::init_layer(const Layer &layer)
{ {
m_slice.clear(); m_slice.clear();
@ -757,19 +721,25 @@ void AvoidCrossingPerimeters::init_layer(const Layer &layer)
m_boundaries_external.clear(); m_boundaries_external.clear();
for (const LayerRegion *layer_region : layer.regions()) for (const LayerRegion *layer_region : layer.regions())
//FIXME making copies?
append(m_slice, (ExPolygons) layer_region->slices); append(m_slice, (ExPolygons) layer_region->slices);
m_boundaries = to_polygons(get_boundary(layer)); m_boundaries = to_polygons(get_boundary(layer));
m_boundaries_external = to_polygons(get_boundary_external(layer)); m_boundaries_external = to_polygons(get_boundary_external(layer));
m_bbox = get_extents(m_boundaries); BoundingBox bbox(get_extents(m_boundaries));
m_bbox.offset(SCALED_EPSILON); bbox.offset(SCALED_EPSILON);
m_bbox_external = get_extents(m_boundaries_external); BoundingBox bbox_external = get_extents(m_boundaries_external);
m_bbox_external.offset(SCALED_EPSILON); bbox_external.offset(SCALED_EPSILON);
m_grid.set_bbox(m_bbox); m_bbox = BoundingBoxf(bbox.min.cast<double>(), bbox.max.cast<double>());
m_bbox_external = BoundingBoxf(bbox_external.min.cast<double>(), bbox_external.max.cast<double>());
m_grid.set_bbox(bbox);
//FIXME 1mm grid?
m_grid.create(m_boundaries, coord_t(scale_(1.))); m_grid.create(m_boundaries, coord_t(scale_(1.)));
m_grid_external.set_bbox(m_bbox_external); m_grid_external.set_bbox(bbox_external);
//FIXME 1mm grid?
m_grid_external.create(m_boundaries_external, coord_t(scale_(1.))); m_grid_external.create(m_boundaries_external, coord_t(scale_(1.)));
} }

View File

@ -47,9 +47,9 @@ private:
// Collection of boundaries used for detection of crossing perimetrs for travels outside object // Collection of boundaries used for detection of crossing perimetrs for travels outside object
Polygons m_boundaries_external; Polygons m_boundaries_external;
// Bounding box of m_boundaries // Bounding box of m_boundaries
BoundingBox m_bbox; BoundingBoxf m_bbox;
// Bounding box of m_boundaries_external // Bounding box of m_boundaries_external
BoundingBox m_bbox_external; BoundingBoxf m_bbox_external;
EdgeGrid::Grid m_grid; EdgeGrid::Grid m_grid;
EdgeGrid::Grid m_grid_external; EdgeGrid::Grid m_grid_external;
}; };

View File

@ -18,13 +18,6 @@ void MultiPoint::scale(double factor_x, double factor_y)
} }
} }
void MultiPoint::translate(double x, double y)
{
Vector v(x, y);
for (Point &pt : points)
pt += v;
}
void MultiPoint::translate(const Point &v) void MultiPoint::translate(const Point &v)
{ {
for (Point &pt : points) for (Point &pt : points)

View File

@ -28,7 +28,7 @@ public:
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; } MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
void scale(double factor); void scale(double factor);
void scale(double factor_x, double factor_y); void scale(double factor_x, double factor_y);
void translate(double x, double y); void translate(double x, double y) { this->translate(Point(coord_t(x), coord_t(y))); }
void translate(const Point &vector); void translate(const Point &vector);
void rotate(double angle) { this->rotate(cos(angle), sin(angle)); } void rotate(double angle) { this->rotate(cos(angle), sin(angle)); }
void rotate(double cos_angle, double sin_angle); void rotate(double cos_angle, double sin_angle);

View File

@ -62,7 +62,6 @@ set(XS_XSP_FILES
${XSP_DIR}/Layer.xsp ${XSP_DIR}/Layer.xsp
${XSP_DIR}/Line.xsp ${XSP_DIR}/Line.xsp
${XSP_DIR}/Model.xsp ${XSP_DIR}/Model.xsp
${XSP_DIR}/MotionPlanner.xsp
${XSP_DIR}/PerimeterGenerator.xsp ${XSP_DIR}/PerimeterGenerator.xsp
${XSP_DIR}/PlaceholderParser.xsp ${XSP_DIR}/PlaceholderParser.xsp
${XSP_DIR}/Point.xsp ${XSP_DIR}/Point.xsp