diff --git a/resources/images/param_spiralinset.svg b/resources/images/param_spiralinset.svg
new file mode 100644
index 0000000000..6ec1ab36aa
--- /dev/null
+++ b/resources/images/param_spiralinset.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 2880a3cc6b..c33b7be429 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -147,6 +147,8 @@ set(lisbslic3r_sources
Fill/FillBase.hpp
Fill/FillConcentric.cpp
Fill/FillConcentric.hpp
+ Fill/FillSpiralInset.cpp
+ Fill/FillSpiralInset.hpp
Fill/FillConcentricInternal.cpp
Fill/FillConcentricInternal.hpp
Fill/FillCornerSmoothing.cpp
diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp
index 0888e1bb55..dc772580ca 100644
--- a/src/libslic3r/Fill/Fill.cpp
+++ b/src/libslic3r/Fill/Fill.cpp
@@ -950,7 +950,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p
params.extruder = region_config.internal_solid_filament_id;
// Orca: forced fill order applies only to top/bottom surfaces filled with a
// center-based pattern; everything else stays at Default to keep batching together.
- if (params.pattern == ipConcentric || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
+ if (params.pattern == ipConcentric || params.pattern == ipSpiralInset || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
if (params.extrusion_role == erTopSolidInfill)
params.fill_order = region_config.top_surface_fill_order.value;
else if (params.extrusion_role == erBottomSurface)
@@ -1332,7 +1332,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.anchor_length = surface_fill.params.anchor_length;
params.anchor_length_max = surface_fill.params.anchor_length_max;
params.resolution = resolution;
- params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipConcentricInternal;
+ params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipSpiralInset ||
+ surface_fill.params.pattern == ipConcentricInternal;
params.layer_height = layerm->layer()->height;
params.lateral_lattice_angle_1 = surface_fill.params.lateral_lattice_angle_1;
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
@@ -1515,6 +1516,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
case ipCubic:
case ipLine:
case ipConcentric:
+ case ipSpiralInset:
case ipHoneycomb:
case ipLateralHoneycomb:
case ip3DHoneycomb:
diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp
index 45157ec42d..2b307007b8 100644
--- a/src/libslic3r/Fill/FillBase.cpp
+++ b/src/libslic3r/Fill/FillBase.cpp
@@ -15,6 +15,7 @@
#include "FillBase.hpp"
#include "FillConcentric.hpp"
+#include "FillSpiralInset.hpp"
#include "FillHoneycomb.hpp"
#include "Fill3DHoneycomb.hpp"
#include "FillGyroid.hpp"
@@ -41,6 +42,7 @@ Fill* Fill::new_from_type(const InfillPattern type)
{
switch (type) {
case ipConcentric: return new FillConcentric();
+ case ipSpiralInset: return new FillSpiralInset();
case ipHoneycomb: return new FillHoneycomb();
case ipLateralHoneycomb: return new FillLateralHoneycomb();
case ip3DHoneycomb: return new Fill3DHoneycomb();
diff --git a/src/libslic3r/Fill/FillSpiralInset.cpp b/src/libslic3r/Fill/FillSpiralInset.cpp
new file mode 100644
index 0000000000..e6323c2572
--- /dev/null
+++ b/src/libslic3r/Fill/FillSpiralInset.cpp
@@ -0,0 +1,426 @@
+#include "../ClipperUtils.hpp"
+#include "../ExPolygon.hpp"
+#include "../Surface.hpp"
+#include "../VariableWidth.hpp"
+#include "Arachne/WallToolPaths.hpp"
+
+#include "FillSpiralInset.hpp"
+
+#include
+#include
+#include
+
+namespace Slic3r {
+
+// Index of the corner the spiral should start at. Every following loop is split at the point nearest
+// the end of the one before it, so this choice propagates inwards and decides where the whole spiral
+// hands over from ring to ring. A tight corner is the worst place for it: there the next ring
+// retreats along the bisector by spacing/sin(angle), so the spiral has to strike out several spacings
+// to reach it instead of stepping across to a ring running parallel one spacing away.
+//
+// A right angle is taken first when the loop has one. It clips cleanly, since the trimming below
+// scales with 1/sin(angle) and so is at its shortest and least sensitive there, and it holds its
+// shape as the loop is offset inwards, which keeps the handover in the same place ring after ring.
+// Failing that the widest corner is the flattest stretch on offer, which is the next best handover.
+// A straight point is no corner at all and only turns up as an artefact of the offsetting, so it is
+// skipped.
+static int find_spiral_start_corner(const Polygon& loop)
+{
+ const size_t n = loop.points.size();
+ if (n < 3)
+ return 0;
+
+ // cos(85 deg): a corner within five degrees of square counts as a right angle.
+ static const double right_angle_cos = 0.08716;
+ // cos(179 deg): anything flatter than this counts as a straight point rather than a corner.
+ static const double straight_cos = -0.99985;
+
+ // Only convex corners qualify. A reflex corner spans the same angle between its two edges but
+ // bulges the other way, so the next ring in steps away from it along the bisector instead of
+ // hugging it, and starting there hands over across a long diagonal on every single ring. Loops
+ // arrive counter-clockwise, in which case a convex corner turns left, but check the winding
+ // rather than trust it. A closed loop always has at least one convex corner.
+ const double convex_turn = loop.is_counter_clockwise() ? 1.0 : -1.0;
+
+ double best_right_cos = right_angle_cos;
+ int best_right = -1;
+ double best_wide_cos = 1.0;
+ int best_wide = -1;
+ for (size_t i = 0; i < n; ++i) {
+ const Point& p_prev = loop.points[(i - 1 + n) % n];
+ const Point& p = loop.points[i];
+ const Point& p_next = loop.points[(i + 1) % n];
+
+ Vec2d e_in = (p - p_prev).cast();
+ Vec2d e_out = (p_next - p).cast();
+ double len1 = e_in.norm();
+ double len2 = e_out.norm();
+ if (len1 < 1e-6 || len2 < 1e-6)
+ continue;
+ if (convex_turn * (e_in.x() * e_out.y() - e_in.y() * e_out.x()) <= 0.0)
+ continue;
+
+ // Cosine of the angle the two edges span at the corner: 1 at a spike, 0 square, -1 straight.
+ double cos_val = -e_in.dot(e_out) / (len1 * len2);
+ if (std::abs(cos_val) < best_right_cos) {
+ best_right_cos = std::abs(cos_val);
+ best_right = int(i);
+ }
+ if (cos_val > straight_cos && cos_val < best_wide_cos) {
+ best_wide_cos = cos_val;
+ best_wide = int(i);
+ }
+ }
+ if (best_right >= 0)
+ return best_right;
+ // A loop smooth enough to have no corner at all, a circle say, hands over equally well anywhere.
+ return best_wide < 0 ? 0 : best_wide;
+}
+
+// Length to trim off the end of a loop so that it does not overlap the start of the next one.
+// The theoretical gap is distance/sin(alpha), alpha being the angle between the last segment of the
+// loop and the first segment of the next one.
+static double loop_clip_length(const Polyline& loop_path, const double gap)
+{
+ const Point& p_prev = loop_path.points[loop_path.points.size() - 2];
+ const Point& p_last = loop_path.points.back();
+ const Point& p_next = loop_path.points[1];
+ Vec2d v1 = (p_last - p_prev).cast();
+ Vec2d v2 = (p_next - p_last).cast();
+ if (v1.norm() < 1e-6 || v2.norm() < 1e-6)
+ return gap;
+
+ double alpha = std::atan2(std::abs(v1.x() * v2.y() - v1.y() * v2.x()), v1.dot(v2));
+ // Outside 45deg < alpha < 120deg the 1/sin(alpha) term would clip far too much, so fall back to the plain gap.
+ return (alpha > M_PI / 4 && alpha < 2 * M_PI / 3) ? gap / std::sin(alpha) : gap;
+}
+
+// The chaining below drives two kinds of loop: the plain offset polygons of the classic path, and
+// Arachne's variable width walls. These are the only four steps that differ between them. Widths run
+// two per segment, so every point added or removed takes a pair with it.
+static Polyline open_loop(const Polygon& loop, int start_index) { return loop.split_at_index(start_index); }
+
+static ThickPolyline open_loop(const Arachne::ExtrusionLine& loop, int start_index)
+{
+ ThickPolyline path = Arachne::to_thick_polyline(loop);
+ // start_at_index() rotates a closed path, and wants it closed with a matching width at both ends.
+ if (path.points.front() != path.points.back()) {
+ const coordf_t w_first = path.width.front(), w_last = path.width.back();
+ path.points.emplace_back(path.points.front());
+ path.width.emplace_back(w_last);
+ path.width.emplace_back(w_first);
+ }
+ path.start_at_index(start_index);
+ return path;
+}
+
+static void clip_path_end(Polyline& path, double distance) { path.clip_end(distance); }
+
+static void clip_path_end(ThickPolyline& path, double distance)
+{
+ // Polyline::clip_end() knows nothing about the widths, so walk back trimming the two together.
+ while (distance > 0 && path.points.size() >= 2) {
+ const Point last = path.points.back();
+ const coordf_t w_end = path.width.back();
+ path.points.pop_back();
+ path.width.pop_back();
+ const coordf_t w_start = path.width.back();
+ path.width.pop_back();
+
+ const Vec2d v = (path.points.back() - last).cast();
+ const double len = v.norm();
+ if (len > distance) {
+ const double t = distance / len;
+ path.points.emplace_back((last.cast() + v * t).cast());
+ path.width.emplace_back(w_start);
+ path.width.emplace_back(w_start + (w_end - w_start) * (1.0 - t));
+ return;
+ }
+ distance -= len;
+ }
+ path.clear();
+}
+
+static void append_path(Polyline& dst, Polyline&& src) { dst.append(std::move(src)); }
+
+static void append_path(ThickPolyline& dst, ThickPolyline&& src)
+{
+ if (dst.empty()) {
+ dst = std::move(src);
+ return;
+ }
+ if (dst.points.back() == src.points.front()) {
+ // Carrying straight on from the same point, so there is no run across to give a width to.
+ src.points.erase(src.points.begin());
+ src.width.erase(src.width.begin(), src.width.begin() + 2);
+ } else {
+ // The run across to the next loop tapers between the two ends it joins.
+ const coordf_t w_from = dst.width.back(), w_to = src.width.front();
+ dst.width.emplace_back(w_from);
+ dst.width.emplace_back(w_to);
+ }
+ append(dst.points, std::move(src.points));
+ append(dst.width, std::move(src.width));
+}
+
+// The classic loops all carry the same width, so the innermost one of an island can still ring an
+// unfilled pin hole, which the spiral plugs by running into the middle. Arachne's walls widen to take
+// up whatever is left over, so there is nothing there to plug and the stub would only double back
+// over the wall that just filled it.
+static bool leaves_a_centre_hole(const Polygon&) { return true; }
+static bool leaves_a_centre_hole(const Arachne::ExtrusionLine&) { return false; }
+
+static void append_path_point(Polyline& path, const Point& point) { path.points.emplace_back(point); }
+
+static void append_path_point(ThickPolyline& path, const Point& point)
+{
+ const coordf_t w = path.width.back();
+ path.points.emplace_back(point);
+ path.width.emplace_back(w);
+ path.width.emplace_back(w);
+}
+
+// Chain the loops of one surface into as few continuous spirals as its shape allows. The loops arrive
+// ordered outside in, depth first, each paired with its outline in loop_outlines; every decision here
+// is made on those outlines, so the two kinds of loop take exactly the same route.
+template
+static std::vector generate_spiral_insets(const FillParams& params,
+ const std::vector& loops,
+ const Polygons& loop_outlines,
+ const coord_t distance,
+ const ExPolygon& original_expoly)
+{
+ std::vector output;
+ PathType spiral;
+ Point current_pos(0, 0);
+ // Index into loops of the innermost loop appended to the spiral currently being built.
+ int innermost_loop = -1;
+
+ // Whether the spiral can run straight from one point to the other. The run across is extruded,
+ // not travelled, so it has to be a genuine step over to the ring alongside:
+ // - up to a ring spacing and a half it cannot leave the material, and needs no check at all,
+ // which covers all but a few of the loops;
+ // - beyond that it is tested against the surface, which catches the points that are close in a
+ // straight line but separated by a hole or a notch;
+ // - past four spacings it is refused outright. A handover does stretch at a corner, where the
+ // next ring retreats along the bisector by spacing/sin(angle), but four spacings is already a
+ // fifteen degree wedge, and down a wedge that tight the run across would trace the bisector,
+ // which is where the tail is filled from anyway. Anything longer is a traverse across the
+ // surface that prints over what it crosses. Breaking the spiral leaves the G-code to travel it.
+ const double free_hop = 1.5 * double(distance);
+ const double max_hop = 4.0 * double(distance);
+ auto reachable = [&](const Point& from, const Point& to) {
+ const double hop = from.distance_to(to);
+ if (hop > max_hop)
+ return false;
+ return hop <= free_hop || original_expoly.contains(Line(from, to));
+ };
+
+ // The centre point plugs the pin hole left in the middle of an island, it is not meant to
+ // traverse it, so it is only worth adding when the innermost loop has shrunk to about a ring.
+ const double max_center_stub = 2.0 * double(distance);
+
+ // Emit the spiral built so far as one path and start over on a fresh island.
+ auto flush_spiral = [&]() {
+ if (spiral.empty())
+ return;
+ // Run into the middle of the innermost loop so the island's centre is filled instead of being
+ // left as a pin hole. Only where there is a hole to fill: the loop has to still enclose open
+ // space once its own bead is accounted for, or the stub just runs back over that bead. And
+ // the point has to sit inside the loop and be reachable, or it runs off across the surface.
+ if (innermost_loop >= 0 && leaves_a_centre_hole(*loops[innermost_loop])) {
+ const Polygon& innermost = loop_outlines[innermost_loop];
+ const Point centroid = innermost.centroid();
+ if (!offset(innermost, -float(0.5 * double(distance))).empty() && centroid != spiral.last_point() &&
+ spiral.last_point().distance_to(centroid) <= max_center_stub && innermost.contains(centroid) &&
+ reachable(spiral.last_point(), centroid))
+ append_path_point(spiral, centroid);
+ }
+ output.emplace_back(std::move(spiral));
+ spiral.clear();
+ innermost_loop = -1;
+ current_pos = Point(0, 0);
+ };
+
+ for (size_t i = 0; i < loops.size(); ++i) {
+ const Polygon& outline = loop_outlines[i];
+ if (outline.points.empty())
+ continue;
+
+ // The loop is opened into a path with the split point repeated at both ends, so a usable one
+ // has at least 3 points. Both kinds of loop share the outline's indices, hence its start point.
+ PathType loop_path = open_loop(*loops[i], spiral.empty() ? find_spiral_start_corner(outline) :
+ current_pos.nearest_point_index(outline.points));
+ if (loop_path.size() < 3)
+ continue;
+
+ // Island jumping: the loops are ordered by their nesting, depth first, so the next one
+ // continues the current spiral exactly when it lies inside the one just laid down. Distance
+ // cannot stand in for that test: at a sharp corner the next ring retreats along the bisector
+ // by spacing/sin(angle), which leaves it several spacings away while still being the very
+ // next ring in, and the spiral would break off at every spike.
+ const bool same_island = innermost_loop >= 0 && loop_outlines[innermost_loop].contains(loop_path.points.front());
+ if (!spiral.empty() && (!same_island || !reachable(spiral.last_point(), loop_path.points.front()))) {
+ flush_spiral();
+ loop_path = open_loop(*loops[i], find_spiral_start_corner(outline));
+ if (loop_path.size() < 3)
+ continue;
+ }
+
+ // Clip the end of the loop to leave room for the run into the next one. The last loop of the
+ // surface has no successor, so it only gives up half of the gap.
+ clip_path_end(loop_path, loop_clip_length(loop_path, (i + 1 == loops.size() ? 0.5 : 1.0) * double(distance)));
+ // Clipping empties the path when the loop is shorter than the clipping length, which happens
+ // on the degenerate slivers that offsetting leaves behind. Such a loop carries no extrusion.
+ if (loop_path.size() < 2)
+ continue;
+
+ append_path(spiral, std::move(loop_path));
+ innermost_loop = int(i);
+ current_pos = spiral.last_point();
+ }
+
+ flush_spiral();
+
+ // An outward fill order runs every spiral from its centre to its outer edge, innermost island first.
+ if (params.fill_order != SurfaceFillOrder::Inward) {
+ for (PathType& path : output)
+ path.reverse();
+ std::reverse(output.begin(), output.end());
+ }
+
+ return output;
+}
+
+void FillSpiralInset::_fill_surface_single(const FillParams& params,
+ unsigned int thickness_layers,
+ const std::pair& direction,
+ ExPolygon expolygon,
+ Polylines& polylines_out)
+{
+ BoundingBox bounding_box = expolygon.contour.bounding_box();
+
+ coord_t min_spacing = scale_(this->spacing);
+ coord_t distance = coord_t(min_spacing / params.density);
+
+ if (params.density > 0.9999f && !params.dont_adjust) {
+ distance = this->_adjust_solid_spacing(bounding_box.size()(0), distance);
+ this->spacing = unscale(distance);
+ }
+
+ Polygons loops = to_polygons(expolygon);
+
+ ExPolygons last{std::move(expolygon)};
+ while (!last.empty()) {
+ last = offset2_ex(last, -(distance + min_spacing / 2), +min_spacing / 2);
+ append(loops, to_polygons(last));
+ }
+
+ // Orders the loops outside in, depth first, which is the order the chaining below expects.
+ loops = union_pt_chained_outside_in(loops);
+
+ std::vector loop_refs;
+ loop_refs.reserve(loops.size());
+ for (const Polygon& loop : loops)
+ loop_refs.emplace_back(&loop);
+
+ Polylines spiral_result = generate_spiral_insets(params, loop_refs, loops, distance, expolygon);
+
+ append(polylines_out, spiral_result);
+}
+
+void FillSpiralInset::_fill_surface_single(const FillParams& params,
+ unsigned int thickness_layers,
+ const std::pair& direction,
+ ExPolygon expolygon,
+ ThickPolylines& thick_polylines_out)
+{
+ assert(params.use_arachne);
+ assert(this->print_config != nullptr && this->print_object_config != nullptr);
+
+ // Only a solid surface is worth the variable width walls; a sparse one falls back to plain loops.
+ if (params.density <= 0.9999f || params.dont_adjust) {
+ Polylines polylines;
+ this->_fill_surface_single(params, thickness_layers, direction, expolygon, polylines);
+ append(thick_polylines_out, to_thick_polylines(std::move(polylines), scaled(this->spacing)));
+ return;
+ }
+
+ // no rotation is supported for this infill pattern
+ Point bbox_size = expolygon.contour.bounding_box().size();
+ coord_t min_spacing = scaled(this->spacing);
+
+ coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1;
+ Polygons polygons = offset(expolygon, float(min_spacing) / 2.f);
+
+ double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end());
+ Arachne::WallToolPathsParams input_params;
+ input_params.min_bead_width = 0.85 * min_nozzle_diameter;
+ input_params.min_feature_size = 0.25 * min_nozzle_diameter;
+ input_params.wall_transition_length = 1.0 * min_nozzle_diameter;
+ input_params.wall_transition_angle = 10;
+ input_params.wall_transition_filter_deviation = 0.25 * min_nozzle_diameter;
+ input_params.wall_distribution_count = 1;
+
+ Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, params.layer_height, input_params);
+ std::vector walls_by_inset = wallToolPaths.getToolPaths();
+
+ // Open walls are the thin features Arachne fits between the closed ones. They cannot join a
+ // spiral, so they go out as they are; leaving them behind is what would put the gaps back.
+ std::vector walls;
+ Polygons wall_outlines;
+ ThickPolylines open_walls;
+ for (const Arachne::VariableWidthLines& inset : walls_by_inset)
+ for (const Arachne::ExtrusionLine& wall : inset) {
+ if (wall.empty())
+ continue;
+ if (wall.is_closed) {
+ walls.emplace_back(&wall);
+ wall_outlines.emplace_back(wall.toPolygon());
+ } else {
+ open_walls.emplace_back(Arachne::to_thick_polyline(wall));
+ }
+ }
+
+ // Arachne hands the walls back grouped by inset, which is not their nesting: around a hole the
+ // wall of a given inset lies inside the wall of that same inset around the contour. Nest them by
+ // containment instead, so the spiral follows one island all the way in before starting the next,
+ // the same order union_pt_chained_outside_in gives the classic path above.
+ const size_t wall_count = walls.size();
+ std::vector nesting_depth(wall_count, 0), parent(wall_count, -1);
+ std::vector inside(wall_count * wall_count, 0);
+ for (size_t i = 0; i < wall_count; ++i)
+ for (size_t j = 0; j < wall_count; ++j)
+ if (i != j && wall_outlines[j].contains(walls[i]->junctions.front().p)) {
+ inside[i * wall_count + j] = 1;
+ ++nesting_depth[i];
+ }
+ // The innermost of the walls containing this one, which is the deepest of them, is its parent.
+ for (size_t i = 0; i < wall_count; ++i)
+ for (size_t j = 0; j < wall_count; ++j)
+ if (inside[i * wall_count + j] && (parent[i] < 0 || nesting_depth[parent[i]] < nesting_depth[j]))
+ parent[i] = int(j);
+
+ std::vector ordered;
+ Polygons outlines;
+ ordered.reserve(wall_count);
+ outlines.reserve(wall_count);
+ std::function descend = [&](int idx) {
+ ordered.emplace_back(walls[idx]);
+ outlines.emplace_back(wall_outlines[idx]);
+ for (size_t k = 0; k < wall_count; ++k)
+ if (parent[k] == idx)
+ descend(int(k));
+ };
+ for (size_t i = 0; i < wall_count; ++i)
+ if (parent[i] < 0)
+ descend(int(i));
+
+ ThickPolylines spiral_result =
+ generate_spiral_insets(params, ordered, outlines, min_spacing, expolygon);
+
+ append(thick_polylines_out, std::move(spiral_result));
+ append(thick_polylines_out, std::move(open_walls));
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/Fill/FillSpiralInset.hpp b/src/libslic3r/Fill/FillSpiralInset.hpp
new file mode 100644
index 0000000000..eb799ec2f3
--- /dev/null
+++ b/src/libslic3r/Fill/FillSpiralInset.hpp
@@ -0,0 +1,37 @@
+#ifndef slic3r_FillSpiralInset_hpp_
+#define slic3r_FillSpiralInset_hpp_
+
+#include "FillBase.hpp"
+
+namespace Slic3r {
+
+class FillSpiralInset : public Fill
+{
+public:
+ ~FillSpiralInset() override = default;
+ bool is_self_crossing() override { return false; }
+
+protected:
+ Fill* clone() const override { return new FillSpiralInset(*this); };
+ void _fill_surface_single(
+ const FillParams ¶ms,
+ unsigned int thickness_layers,
+ const std::pair &direction,
+ ExPolygon expolygon,
+ Polylines &polylines_out) override;
+
+ // Orca: solid surfaces are filled with Arachne's variable width walls, which widen to take up
+ // whatever the fixed width loops above would have left over as gaps.
+ void _fill_surface_single(
+ const FillParams ¶ms,
+ unsigned int thickness_layers,
+ const std::pair &direction,
+ ExPolygon expolygon,
+ ThickPolylines &thick_polylines_out) override;
+
+ bool no_sort() const override { return true; }
+};
+
+} // namespace Slic3r
+
+#endif // slic3r_FillSpiralInset_hpp_
diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp
index 87ad11bcf8..1d6c2b0703 100644
--- a/src/libslic3r/Layer.cpp
+++ b/src/libslic3r/Layer.cpp
@@ -419,6 +419,7 @@ coordf_t Layer::get_sparse_infill_max_void_area()
double spacing = flow.scaled_spacing() * (100 - density) / density;
switch (pattern) {
case ipConcentric:
+ case ipSpiralInset:
case ipRectilinear:
case ipLine:
case ipGyroid:
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index 100089fd24..1db476eede 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -275,6 +275,7 @@ static t_config_enum_values s_keys_map_InfillPattern {
{ "tpmsfk", ipTpmsFK },
{ "gyroid", ipGyroid },
{ "concentric", ipConcentric },
+ { "spiralinset", ipSpiralInset },
{ "hilbertcurve", ipHilbertCurve },
{ "archimedeanchords", ipArchimedeanChords },
{ "octagramspiral", ipOctagramSpiral }
@@ -371,6 +372,7 @@ static t_config_enum_values s_keys_map_SupportMaterialInterfacePattern {
{ "auto", smipAuto },
{ "rectilinear", smipRectilinear },
{ "concentric", smipConcentric },
+ { "spiralinset", smipSpiralInset },
{ "rectilinear_interlaced", smipRectilinearInterlaced},
{ "grid", smipGrid }
};
@@ -2292,6 +2294,7 @@ void PrintConfigDef::init_fff_params()
def->enum_values.push_back("rectilinear");
def->enum_values.push_back("alignedrectilinear");
def->enum_values.push_back("concentric");
+ def->enum_values.push_back("spiralinset");
def->enum_values.push_back("hilbertcurve");
def->enum_values.push_back("archimedeanchords");
def->enum_values.push_back("octagramspiral");
@@ -2300,6 +2303,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Rectilinear"));
def->enum_labels.push_back(L("Aligned Rectilinear"));
def->enum_labels.push_back(L("Concentric"));
+ def->enum_labels.push_back(L("Spiral Inset"));
def->enum_labels.push_back(L("Hilbert Curve"));
def->enum_labels.push_back(L("Archimedean Chords"));
def->enum_labels.push_back(L("Octagram Spiral"));
@@ -2382,7 +2386,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Top surface fill order");
def->category = L("Strength");
def->tooltip = L("Direction in which top surfaces are filled when using a center-based pattern "
- "(Concentric, Archimedean Chords, Octagram Spiral).\n"
+ "(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n"
"Outward starts at the center of the surface, so any excess material is pushed "
"towards the edge where it is least visible. Inward starts at the edge and ends "
"with the tight curves at the center.\n"
@@ -2401,7 +2405,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Bottom surface fill order");
def->category = L("Strength");
def->tooltip = L("Direction in which bottom surfaces are filled when using a center-based pattern "
- "(Concentric, Archimedean Chords, Octagram Spiral).\n"
+ "(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n"
"Inward starts each surface with the wider outer curves, which improves first layer "
"adhesion on build plates where the tight curves at the center may not stick. "
"Outward starts at the center, pushing any excess material towards the edge.\n"
@@ -6963,11 +6967,13 @@ void PrintConfigDef::init_fff_params()
def->enum_values.push_back("auto");
def->enum_values.push_back("rectilinear");
def->enum_values.push_back("concentric");
+ def->enum_values.push_back("spiralinset");
def->enum_values.push_back("rectilinear_interlaced");
def->enum_values.push_back("grid");
def->enum_labels.push_back(L("Default"));
def->enum_labels.push_back(L("Rectilinear"));
def->enum_labels.push_back(L("Concentric"));
+ def->enum_labels.push_back(L("Spiral Inset"));
def->enum_labels.push_back(L("Rectilinear Interlaced"));
def->enum_labels.push_back(L("Grid"));
def->mode = comAdvanced;
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index 330151c4d3..f7cbe8b2e5 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -113,7 +113,7 @@ enum InfillPattern : int {
ipCubic, ipAdaptiveCubic, ipQuarterCubic, ipSupportCubic, ipLightning,
ipHoneycomb, ip3DHoneycomb, ipLateralHoneycomb, ipLateralLattice,
ipCrossHatch, ipTpmsD, ipTpmsFK, ipGyroid,
- ipConcentric, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral,
+ ipConcentric, ipSpiralInset, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral,
ipSupportBase, ipConcentricInternal,
ipCount,
};
@@ -271,7 +271,7 @@ enum LongRectrationLevel
};
enum SupportMaterialInterfacePattern {
- smipAuto, smipRectilinear, smipConcentric, smipRectilinearInterlaced, smipGrid
+ smipAuto, smipRectilinear, smipConcentric, smipSpiralInset, smipRectilinearInterlaced, smipGrid
};
// BBS
diff --git a/src/libslic3r/Support/SupportParameters.hpp b/src/libslic3r/Support/SupportParameters.hpp
index dc0e94ed03..8d43edb444 100644
--- a/src/libslic3r/Support/SupportParameters.hpp
+++ b/src/libslic3r/Support/SupportParameters.hpp
@@ -141,6 +141,8 @@ struct SupportParameters {
this->contact_fill_pattern = ipGrid;
else if (object_config.support_interface_pattern == smipRectilinearInterlaced)
this->contact_fill_pattern = ipRectilinear;
+ else if (object_config.support_interface_pattern == smipSpiralInset)
+ this->contact_fill_pattern = ipSpiralInset;
else
this->contact_fill_pattern =
(object_config.support_interface_pattern == smipAuto && zero_gap_contact_interface) ||
diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp
index 09b7fcfad4..8eab1c785c 100644
--- a/src/slic3r/GUI/ConfigManipulation.cpp
+++ b/src/slic3r/GUI/ConfigManipulation.cpp
@@ -741,14 +741,21 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool have_infill = config->option("sparse_infill_density")->value > 0;
// sparse_infill_filament_id uses the same logic as in Print::extruders()
for (auto el : { "sparse_infill_pattern", "infill_combination", "fill_multiline","infill_direction",
- "minimum_sparse_infill_area", "sparse_infill_filament_id", "infill_anchor", "infill_anchor_max","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"})
+ "minimum_sparse_infill_area", "sparse_infill_filament_id","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"})
toggle_line(el, have_infill);
+ InfillPattern pattern = config->opt_enum("sparse_infill_pattern");
+
+ // Orca: the concentric patterns follow the surface outline instead of crossing it, so there is
+ // nothing for an infill anchor to attach to. Hide the anchor settings for them.
+ bool have_infill_anchor = have_infill && pattern != ipConcentric && pattern != ipSpiralInset;
+ toggle_line("infill_anchor", have_infill_anchor);
+ toggle_line("infill_anchor_max", have_infill_anchor);
+
bool have_combined_infill = config->opt_bool("infill_combination") && have_infill;
toggle_line("infill_combination_max_layer_height", have_combined_infill);
// Infill patterns that support multiline infill.
- InfillPattern pattern = config->opt_enum("sparse_infill_pattern");
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
@@ -831,7 +838,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("separated_infills", is_internal_infill_separable);
// Fill order is only meaningful for the center-based surface fill patterns; hide it otherwise.
- auto is_centered_fill = [](InfillPattern p) { return p == ipConcentric || p == ipArchimedeanChords || p == ipOctagramSpiral; };
+ auto is_centered_fill = [](InfillPattern p) { return p == ipConcentric || p == ipSpiralInset || p == ipArchimedeanChords || p == ipOctagramSpiral; };
toggle_line("top_surface_fill_order", has_top_shell && is_centered_fill(config->opt_enum("top_surface_pattern")));
toggle_line("bottom_surface_fill_order", has_bottom_shell && is_centered_fill(config->opt_enum("bottom_surface_pattern")));
diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp
index d26639c659..04e5b61831 100644
--- a/tests/fff_print/test_fill.cpp
+++ b/tests/fff_print/test_fill.cpp
@@ -699,6 +699,213 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set",
}
}
+// Orca: the spiral inset pattern chains the concentric loops into a single continuous path per
+// island, so it has to cope with the degenerate loops offsetting leaves behind and it must not join
+// loops that only look adjacent.
+namespace {
+
+Slic3r::Polylines spiral_inset_fill(const Slic3r::ExPolygon &surface_shape, double spacing)
+{
+ std::unique_ptr filler(Slic3r::Fill::new_from_type("spiralinset"));
+ filler->spacing = spacing;
+ // Cancel the half-spacing contraction fill_surface() applies, so the filler sees the shape as given.
+ filler->overlap = 0.5 * spacing;
+
+ Slic3r::FillParams fill_params;
+ fill_params.density = 1.f;
+ fill_params.dont_adjust = true;
+
+ Slic3r::Surface surface(Slic3r::stBottom, surface_shape);
+ return filler->fill_surface(&surface, fill_params);
+}
+
+Slic3r::ExPolygon rectangle(double x, double y, double w, double h)
+{
+ return Slic3r::ExPolygon({Slic3r::Point::new_scale(x, y), Slic3r::Point::new_scale(x + w, y),
+ Slic3r::Point::new_scale(x + w, y + h), Slic3r::Point::new_scale(x, y + h)});
+}
+
+// Area of the surface the toolpaths fail to cover, and the largest single patch of it, in mm2. Each
+// bead is measured at its own width so the variable width walls are not sold short.
+std::pair uncovered_area(const Slic3r::ExPolygon &surface_shape, const Slic3r::Polygons &covered)
+{
+ double total = 0, biggest = 0;
+ for (const Slic3r::ExPolygon &gap : Slic3r::diff_ex(Slic3r::ExPolygons{surface_shape}, Slic3r::union_(covered))) {
+ const double area = unscale(unscale(gap.area()));
+ total += area;
+ biggest = std::max(biggest, area);
+ }
+ return {total, biggest};
+}
+
+Slic3r::Polygons beads_of(const Slic3r::Polylines &paths, double width)
+{
+ return Slic3r::offset(paths, float(scale_(0.5 * width)));
+}
+
+Slic3r::Polygons beads_of(const Slic3r::ThickPolylines &paths)
+{
+ Slic3r::Polygons covered;
+ for (const Slic3r::ThickPolyline &path : paths)
+ for (size_t i = 0; i + 1 < path.points.size(); ++i) {
+ Slic3r::Polyline segment;
+ segment.points = {path.points[i], path.points[i + 1]};
+ Slic3r::append(covered, Slic3r::offset(Slic3r::Polylines{segment},
+ float(0.5 * std::max(path.width[2 * i], path.width[2 * i + 1]))));
+ }
+ return covered;
+}
+
+} // namespace
+
+TEST_CASE("Spiral inset fill drops loops shorter than the end clipping", "[Fill][Regression]")
+{
+ // A sliver whose whole perimeter is shorter than the length clipped off the end of a loop, so the
+ // clipping consumes the path entirely. Such a loop carries no extrusion and must be dropped
+ // rather than kept as an empty path and read back from.
+ const double spacing = 0.45;
+
+ Slic3r::Polylines paths;
+ REQUIRE_NOTHROW(paths = spiral_inset_fill(rectangle(0, 0, 0.05, 0.05), spacing));
+ for (const Slic3r::Polyline &path : paths)
+ CHECK(path.size() >= 2);
+
+ // The same surface at a size the clipping cannot swallow still gets filled.
+ REQUIRE_NOTHROW(paths = spiral_inset_fill(rectangle(0, 0, 5, 5), spacing));
+ REQUIRE(paths.size() == 1);
+ CHECK(paths.front().size() >= 2);
+}
+
+TEST_CASE("Spiral inset fill keeps separate islands on separate paths", "[Fill]")
+{
+ // Two lobes joined by a neck narrower than the loop spacing: the inward offsets break the surface
+ // into two islands, which cannot share one spiral, and no path may leave the surface.
+ const double spacing = 0.45;
+ Slic3r::ExPolygon dumbbell = rectangle(0, 0, 6, 6);
+ dumbbell = Slic3r::union_ex(Slic3r::ExPolygons{dumbbell, rectangle(6, 2.9, 4, 0.2), rectangle(10, 0, 6, 6)}).front();
+
+ const Slic3r::Polylines paths = spiral_inset_fill(dumbbell, spacing);
+ REQUIRE(paths.size() >= 2);
+
+ // Inflate by a hair so that loops sitting exactly on the outline still count as contained.
+ const Slic3r::ExPolygons within = Slic3r::offset_ex(dumbbell, float(SCALED_EPSILON));
+ REQUIRE(within.size() == 1);
+ for (const Slic3r::Polyline &path : paths) {
+ CHECK(path.size() >= 2);
+ CHECK(within.front().contains(path));
+ }
+}
+
+
+TEST_CASE("Spiral inset fill stays connected across sharp corners", "[Fill][Regression]")
+{
+ // At a corner of half-angle a, the next ring inward retreats along the bisector by spacing/sin(a),
+ // which leaves it several spacings from the end of the ring it continues. Judging the break by
+ // distance broke the spiral into loose rings at every spike; nesting is what decides the island.
+ const double spacing = 0.45;
+ const Slic3r::ExPolygon spike({Slic3r::Point::new_scale(0, 0), Slic3r::Point::new_scale(30, 0),
+ Slic3r::Point::new_scale(15, 4)});
+
+ const Slic3r::Polylines paths = spiral_inset_fill(spike, spacing);
+ CHECK(paths.size() == 1);
+
+ const Slic3r::ExPolygons within = Slic3r::offset_ex(spike, float(SCALED_EPSILON));
+ REQUIRE(within.size() == 1);
+ for (const Slic3r::Polyline &path : paths)
+ CHECK(within.front().contains(path));
+}
+
+TEST_CASE("Spiral inset fill starts on a convex corner", "[Fill][Regression]")
+{
+ // The only right angle on this outline is the reflex one: the two edges meeting at the origin
+ // span 90 degrees exactly as a square corner would, but the material lies outside them. The next
+ // ring in steps away from a reflex corner along the bisector instead of hugging it, so starting
+ // the spiral there sent it across a long diagonal on every single ring.
+ const double spacing = 0.45;
+ const Slic3r::ExPolygon notched({Slic3r::Point::new_scale(0, 0), Slic3r::Point::new_scale(0, 10),
+ Slic3r::Point::new_scale(-16, 18), Slic3r::Point::new_scale(-16, -2),
+ Slic3r::Point::new_scale(-8, -16), Slic3r::Point::new_scale(18, -16),
+ Slic3r::Point::new_scale(10, 0)});
+
+ const Slic3r::Polylines paths = spiral_inset_fill(notched, spacing);
+ REQUIRE(paths.size() >= 1);
+
+ // Every edge of the outline is at least 45 degrees off the bisector of that reflex corner, and
+ // so is every ring offset from it. A long segment running along the bisector can therefore only
+ // be the spiral striking out across the rings to reach the next one.
+ for (const Slic3r::Polyline &path : paths)
+ for (const Slic3r::Line &segment : path.lines()) {
+ const Vec2d v = (segment.b - segment.a).cast();
+ const double direction = std::fmod(std::atan2(v.y(), v.x()) * 180.0 / M_PI + 180.0, 180.0);
+ if (std::abs(direction - 45.0) > 25.0)
+ continue;
+ CAPTURE(direction, unscale(segment.length()));
+ CHECK(segment.length() <= scale_(1.5 * spacing));
+ }
+}
+
+TEST_CASE("Spiral inset fill closes the gaps with variable width walls", "[Fill]")
+{
+ // Fixed width loops cannot fill a region that is not a whole number of lines across and leave the
+ // remainder open, which on a ring shows up as a wedge several lines wide. Plain concentric avoids
+ // that by building solid surfaces out of Arachne's variable width walls, and so must this pattern.
+ const double spacing = 0.45;
+ Slic3r::ExPolygon ring = rectangle(0, 0, 24, 24);
+ Slic3r::Polygon hole;
+ for (int i = 0; i < 64; ++i) {
+ const double angle = -2.0 * PI * i / 64.0; // clockwise, so it reads as a hole
+ hole.points.emplace_back(Slic3r::Point::new_scale(12 + 7.3 * std::cos(angle), 12 + 7.3 * std::sin(angle)));
+ }
+ ring.holes.emplace_back(hole);
+
+ Slic3r::PrintConfig print_config;
+ Slic3r::PrintObjectConfig object_config;
+ auto make_filler = [&]() {
+ std::unique_ptr filler(Slic3r::Fill::new_from_type("spiralinset"));
+ filler->spacing = spacing;
+ filler->overlap = 0.5 * spacing; // cancel the contraction, so both see the same surface
+ filler->print_config = &print_config;
+ filler->print_object_config = &object_config;
+ return filler;
+ };
+
+ Slic3r::FillParams params;
+ params.density = 1.f;
+ params.dont_adjust = false;
+ params.layer_height = 0.2;
+
+ const Slic3r::Surface surface(Slic3r::stTop, ring);
+
+ std::unique_ptr fixed = make_filler();
+ const Slic3r::Polylines fixed_width = fixed->fill_surface(&surface, params);
+ REQUIRE(!fixed_width.empty());
+ const auto fixed_gaps = uncovered_area(ring, beads_of(fixed_width, fixed->spacing));
+
+ params.use_arachne = true;
+ std::unique_ptr variable = make_filler();
+ const Slic3r::ThickPolylines variable_width = variable->fill_surface_arachne(&surface, params);
+ REQUIRE(!variable_width.empty());
+ const auto variable_gaps = uncovered_area(ring, beads_of(variable_width));
+
+ CAPTURE(fixed_gaps.first, fixed_gaps.second, variable_gaps.first, variable_gaps.second);
+ // The wedges the fixed width loops leave behind are what the variable width walls take up.
+ CHECK(variable_gaps.second < 0.5 * fixed_gaps.second);
+ CHECK(variable_gaps.first < fixed_gaps.first);
+
+ // And it is still a spiral: far fewer paths than the ring has loops.
+ // And the walls are still chained into spirals rather than printed one path per wall. The ring is
+ // at its narrowest (12 - 7.3) mm across and is filled from both sides, so it is at least this many
+ // walls thick there and thicker elsewhere. Arachne's short thin feature walls cannot join a spiral,
+ // so only the substantial paths count towards this.
+ const size_t walls_across = size_t(2.0 * (12.0 - 7.3) / spacing);
+ size_t spirals = 0;
+ for (const Slic3r::ThickPolyline &path : variable_width)
+ if (path.length() > scale_(10.0 * spacing))
+ ++spirals;
+ CAPTURE(spirals, walls_across, variable_width.size(), fixed_width.size());
+ CHECK(2 * spirals < walls_across);
+}
+
TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]")
{
// A cell whose sides are several times the line width, so that the corners have room to be rounded.
diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp
index 28901702cc..2469789d5b 100644
--- a/tests/libslic3r/test_config_variant_expansion.cpp
+++ b/tests/libslic3r/test_config_variant_expansion.cpp
@@ -130,6 +130,12 @@ TEST_CASE("get_config_index_base resolves (volume type, extruder type, id) to a
}
}
+TEST_CASE("support interface pattern registry includes spiral inset", "[Config]")
+{
+ const auto &values = ConfigOptionEnum::get_enum_values();
+ REQUIRE(values.at("spiralinset") == SupportMaterialInterfacePattern::smipSpiralInset);
+}
+
TEST_CASE("get_extruder_nozzle_volume_count reads the per-extruder volume-type layout", "[Config]")
{
std::vector> nozzle_volume_types;