Skip to content

Instantly share code, notes, and snippets.

@penk
Created August 16, 2026 12:00
Show Gist options
  • Select an option

  • Save penk/0b0b01f6d3ea442ed7166019fa649cb9 to your computer and use it in GitHub Desktop.

Select an option

Save penk/0b0b01f6d3ea442ed7166019fa649cb9 to your computer and use it in GitHub Desktop.
2-wall vase mode for Cura
diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json
index 6601d5c..0a21abe 100644
--- a/resources/definitions/fdmprinter.def.json
+++ b/resources/definitions/fdmprinter.def.json
@@ -1097,7 +1097,7 @@
"description": "The thickness of the walls in the horizontal direction. This value divided by the wall line width defines the number of walls.",
"unit": "mm",
"default_value": 0.8,
- "value": "wall_line_width_0 if magic_spiralize else 0.8",
+ "value": "wall_line_width_0 + (magic_spiralize_wall_count - 1) * wall_line_width_x if magic_spiralize else 0.8",
"minimum_value": "0",
"minimum_value_warning": "line_width",
"maximum_value_warning": "10 * line_width",
@@ -1117,7 +1117,7 @@
"maximum_value_warning": "10",
"maximum_value": "999999",
"type": "int",
- "value": "1 if magic_spiralize else max(1, round((wall_thickness - wall_line_width_0) / wall_line_width_x) + 1) if wall_thickness != 0 else 0",
+ "value": "magic_spiralize_wall_count if magic_spiralize else max(1, round((wall_thickness - wall_line_width_0) / wall_line_width_x) + 1) if wall_thickness != 0 else 0",
"limit_to_extruder": "wall_x_extruder_nr",
"settable_per_mesh": true
}
@@ -8719,12 +8719,24 @@
"magic_spiralize":
{
"label": "Spiralize Outer Contour",
- "description": "Spiralize smooths out the Z move of the outer edge. This will create a steady Z increase over the whole print. This feature turns a solid model into a single walled print with a solid bottom. This feature should only be enabled when each layer only contains a single part.",
+ "description": "Spiralize smooths out the Z move of the outer edge. This will create a steady Z increase over the whole print. This feature turns a solid model into a one- or two-walled print with a solid bottom. This feature should only be enabled when each layer only contains a single part.",
"type": "bool",
"default_value": false,
"settable_per_mesh": false,
"settable_per_extruder": false
},
+ "magic_spiralize_wall_count":
+ {
+ "label": "Spiral Wall Count (Experimental)",
+ "description": "The number of walls used for spiralized printing. Two walls are joined into one continuous stitched extrusion path.",
+ "type": "int",
+ "default_value": 1,
+ "minimum_value": "1",
+ "maximum_value": "2",
+ "enabled": "magic_spiralize",
+ "settable_per_mesh": false,
+ "settable_per_extruder": false
+ },
"smooth_spiralized_contours":
{
"label": "Smooth Spiralized Contours",
@@ -10093,4 +10105,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Settings/TestDefinitionContainer.py b/tests/Settings/TestDefinitionContainer.py
index f532aca..f97812f 100644
--- a/tests/Settings/TestDefinitionContainer.py
+++ b/tests/Settings/TestDefinitionContainer.py
@@ -37,6 +37,21 @@ def definition_container():
return result
+def test_spiralizeWallCount():
+ """Spiralize defaults to one wall but permits the experimental two-wall engine path."""
+ definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", "fdmprinter.def.json")
+ with open(definition_path, encoding = "utf-8") as data:
+ settings = flattenSettings(json.load(data)["settings"])
+
+ spiral_wall_count = settings["magic_spiralize_wall_count"]
+ assert spiral_wall_count["default_value"] == 1
+ assert spiral_wall_count["minimum_value"] == "1"
+ assert spiral_wall_count["maximum_value"] == "2"
+
+ assert settings["wall_line_count"]["value"].startswith("magic_spiralize_wall_count if magic_spiralize else")
+ assert settings["wall_thickness"]["value"] == "wall_line_width_0 + (magic_spiralize_wall_count - 1) * wall_line_width_x if magic_spiralize else 0.8"
+
+
@pytest.mark.parametrize("file_path", definition_filepaths)
def test_definitionIds(file_path):
"""
@@ -243,4 +258,4 @@ def test_noNewSettings(file_path: str):
return # FDMPrinter and FDMExtruder, being the basis for all printers and extruders, are allowed to define new settings since they will be available for all printers then.
with open(file_path, encoding = "utf-8") as f:
doc = json.load(f)
- assert "settings" not in doc
\ No newline at end of file
+ assert "settings" not in doc
diff --git a/include/ExtruderPlan.h b/include/ExtruderPlan.h
index 63d03b9..bfc5356 100644
--- a/include/ExtruderPlan.h
+++ b/include/ExtruderPlan.h
@@ -38,6 +38,8 @@ class ExtruderPlan
friend class LayerPlan;
#ifdef BUILD_TESTS
friend class ExtruderPlanPathsParameterizedTest;
+ friend class LayerPlanTest_SingleWallSpiralPathRemainsOneLoop_Test;
+ friend class LayerPlanTest_TwoWallSpiralIsOneContinuousZRamp_Test;
FRIEND_TEST(ExtruderPlanPathsParameterizedTest, BackPressureCompensationZeroIsUncompensated);
FRIEND_TEST(ExtruderPlanPathsParameterizedTest, BackPressureCompensationFull);
FRIEND_TEST(ExtruderPlanPathsParameterizedTest, BackPressureCompensationHalf);
diff --git a/include/FffGcodeWriter.h b/include/FffGcodeWriter.h
index 60e0d33..198f9b3 100644
--- a/include/FffGcodeWriter.h
+++ b/include/FffGcodeWriter.h
@@ -42,6 +42,7 @@ class FffGcodeWriter : public NoCopy
{
friend class FffProcessor; // Because FffProcessor exposes finalize (TODO)
friend class DISABLED_FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test;
+ friend class FffGcodeWriterTest_SpliceTwoWallSpiral_Test;
private:
coord_t max_object_height; //!< The maximal height of all previously sliced meshgroups, used to avoid collision when moving to the next meshgroup to print.
@@ -733,6 +734,9 @@ private:
*/
void findLayerSeamsForSpiralize(SliceDataStorage& storage, size_t total_layers);
+ /*! Splice an outer wall and a reversed inner wall at the outer seam into one closed spiral revolution. */
+ static Polygon spliceTwoWallSpiral(const Shape& walls, size_t outer_seam_vertex_idx);
+
/*!
* Calculate the index of the vertex that is considered to be the seam for the given layer
* \param storage where the slice data is stored.
diff --git a/include/LayerPlan.h b/include/LayerPlan.h
index 1e13ef4..15c113f 100644
--- a/include/LayerPlan.h
+++ b/include/LayerPlan.h
@@ -61,6 +61,8 @@ class LayerPlan : public NoCopy
friend class AddTravelTest;
friend class DISABLED_FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test;
friend class AntiOozeAmountsTest;
+ friend class LayerPlanTest_SingleWallSpiralPathRemainsOneLoop_Test;
+ friend class LayerPlanTest_TwoWallSpiralIsOneContinuousZRamp_Test;
FRIEND_TEST(AntiOozeAmountsTest, ComputeAntiOozeAmounts);
FRIEND_TEST(OverhangSpeedTest, SpeedFactorAppliedWhenMasksSet);
FRIEND_TEST(OverhangSpeedTest, SpeedFactorSplitAtOverhangBoundary);
@@ -930,6 +932,8 @@ public:
const bool empty() const;
private:
+ static coord_t calculateSpiralizedZOffset(coord_t layer_thickness, double length, double total_length);
+
/*!
* \brief Compute the preferred or minimum combing boundary
*
diff --git a/include/WallsComputation.h b/include/WallsComputation.h
index 868ec04..303651d 100644
--- a/include/WallsComputation.h
+++ b/include/WallsComputation.h
@@ -65,11 +65,19 @@ private:
*
* \param part The part for which to generate the spiral inset.
* \param line_width_0 The width of the outer (spiralized) wall.
- * \param wall_0_inset The part for which to generate the spiral inset.
+ * \param line_width_x The width of the inner wall when generating an experimental two-wall spiral.
+ * \param wall_0_inset How far to inset the outer wall.
+ * \param wall_x_inset How far to inset the inner wall.
* \param recompute_outline_based_on_outer_wall Whether we need to recompute the print outline according to the
* generated spiral inset.
*/
- void generateSpiralInsets(SliceLayerPart* part, coord_t line_width_0, coord_t wall_0_inset, bool recompute_outline_based_on_outer_wall);
+ void generateSpiralInsets(
+ SliceLayerPart* part,
+ coord_t line_width_0,
+ coord_t line_width_x,
+ coord_t wall_0_inset,
+ coord_t wall_x_inset,
+ bool recompute_outline_based_on_outer_wall);
};
} // namespace cura
diff --git a/include/sliceDataStorage.h b/include/sliceDataStorage.h
index f37c0ab..99bb5cc 100644
--- a/include/sliceDataStorage.h
+++ b/include/sliceDataStorage.h
@@ -64,6 +64,7 @@ public:
Shape print_outline; //!< An approximation to the outline of what's actually printed, based on the outer wall.
//!< Too small parts will be omitted compared to the outline.
Shape spiral_wall; //!< The centerline of the wall used by spiralize mode. Only computed if spiralize mode is enabled.
+ bool spiral_wall_is_multi_wall{ false }; //!< Whether spiral_wall contains the experimental outer and inner wall pair.
Shape inner_area; //!< The area of the outline, minus the walls. This will be filled with either skin or infill.
std::vector<SkinPart> skin_parts; //!< The skin parts which are filled for 100% with lines and/or insets.
std::vector<VariableWidthLines> wall_toolpaths; //!< toolpaths for walls, will replace(?) the insets. Binned by inset_idx.
@@ -397,6 +398,7 @@ public:
std::vector<int> spiralize_seam_vertex_indices; //!< the index of the seam vertex for each layer
std::vector<Shape*> spiralize_wall_outlines; //!< the wall outline polygons for each layer
+ std::vector<bool> spiralize_wall_is_multi_wall; //!< whether each spiral outline contains an outer and inner wall pair
//!< Pointer to primer tower handler object (a null pointer indicates that there is no prime tower)
PrimeTower* prime_tower_{ nullptr };
diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp
index 3c46770..60b81dd 100644
--- a/src/FffGcodeWriter.cpp
+++ b/src/FffGcodeWriter.cpp
@@ -250,6 +250,7 @@ void FffGcodeWriter::findLayerSeamsForSpiralize(SliceDataStorage& storage, size_
storage.spiralize_wall_outlines.assign(total_layers, nullptr); // default is no information available
storage.spiralize_seam_vertex_indices.assign(total_layers, 0);
+ storage.spiralize_wall_is_multi_wall.assign(total_layers, false);
int last_layer_nr = -1; // layer number of the last non-empty layer processed (for any extruder or mesh)
@@ -278,6 +279,7 @@ void FffGcodeWriter::findLayerSeamsForSpiralize(SliceDataStorage& storage, size_
storage.spiralize_seam_vertex_indices[layer_nr] = findSpiralizedLayerSeamVertexIndex(storage, mesh, layer_nr, last_layer_nr);
// save the wall outline for this layer so it can be used in the spiralize interpolation calculation
storage.spiralize_wall_outlines[layer_nr] = &layer.parts[0].spiral_wall;
+ storage.spiralize_wall_is_multi_wall[layer_nr] = layer.parts[0].spiral_wall_is_multi_wall;
last_layer_nr = layer_nr;
// ignore any further meshes/extruders for this layer
done_this_layer = true;
@@ -288,6 +290,31 @@ void FffGcodeWriter::findLayerSeamsForSpiralize(SliceDataStorage& storage, size_
}
}
+Polygon FffGcodeWriter::spliceTwoWallSpiral(const Shape& walls, const size_t outer_seam_vertex_idx)
+{
+ const Polygon& outer = walls[0];
+ const Polygon& inner = walls[1];
+ const size_t outer_seam = outer_seam_vertex_idx % outer.size();
+ const ClosestPointPolygon inner_seam = PolygonUtils::findClosest(outer[outer_seam], inner);
+
+ Polygon composite;
+ composite.reserve(outer.size() + inner.size() + 3);
+
+ // Repeat each seam vertex after completing its contour. The two remaining segments are therefore the opposing
+ // radial stitch moves, rather than the crossing diagonals of a figure eight.
+ for (size_t point_idx = 0; point_idx <= outer.size(); ++point_idx)
+ {
+ composite.push_back(outer[(outer_seam + point_idx) % outer.size()]);
+ }
+ composite.push_back(inner_seam.location_);
+ for (size_t point_idx = 0; point_idx < inner.size(); ++point_idx)
+ {
+ composite.push_back(inner[(inner_seam.point_idx_ + inner.size() - point_idx) % inner.size()]);
+ }
+ composite.push_back(inner_seam.location_);
+ return composite;
+}
+
void FffGcodeWriter::setConfigFanSpeedLayerTime()
{
for (const ExtruderTrain& train : Application::getInstance().current_slice_->scene.extruders)
@@ -2529,7 +2556,7 @@ void FffGcodeWriter::processSpiralizedWall(
// wall doesn't have usable outline
return;
}
- const Polygon* last_wall_outline = &(part.spiral_wall[0]); // default to current wall outline
+ const Shape* last_walls = &part.spiral_wall; // default to current wall outline
int last_seam_vertex_idx = -1; // last layer seam vertex index
int layer_nr = gcode_layer.getLayerNr();
if (layer_nr > 0)
@@ -2537,7 +2564,7 @@ void FffGcodeWriter::processSpiralizedWall(
if (storage.spiralize_wall_outlines[layer_nr - 1] != nullptr)
{
// use the wall outline from the previous layer
- last_wall_outline = &(storage.spiralize_wall_outlines[layer_nr - 1]->front());
+ last_walls = storage.spiralize_wall_outlines[layer_nr - 1];
// and the seam vertex index pre-computed for that layer
last_seam_vertex_idx = storage.spiralize_seam_vertex_indices[layer_nr - 1];
}
@@ -2545,10 +2572,27 @@ void FffGcodeWriter::processSpiralizedWall(
const bool is_bottom_layer = (layer_nr == mesh.settings.get<LayerIndex>("initial_bottom_layers"));
const bool is_top_layer = ((size_t)layer_nr == (storage.spiralize_wall_outlines.size() - 1) || storage.spiralize_wall_outlines[layer_nr + 1] == nullptr);
const int seam_vertex_idx = storage.spiralize_seam_vertex_indices[layer_nr]; // use pre-computed seam vertex index for current layer
- // output a wall slice that is interpolated between the last and current walls
+ if (part.spiral_wall_is_multi_wall)
+ {
+ const Polygon wall_outline = spliceTwoWallSpiral(part.spiral_wall, seam_vertex_idx);
+ Polygon last_wall_outline;
+ if (layer_nr > 0 && storage.spiralize_wall_is_multi_wall[layer_nr - 1])
+ {
+ last_wall_outline = spliceTwoWallSpiral(*last_walls, last_seam_vertex_idx);
+ last_seam_vertex_idx = 0;
+ }
+ else
+ {
+ last_wall_outline = last_walls->front();
+ }
+ gcode_layer.spiralizeWallSlice(mesh_config.inset0_config, wall_outline, last_wall_outline, 0, last_seam_vertex_idx, is_top_layer, is_bottom_layer);
+ return;
+ }
+
+ // Preserve the existing behaviour for ordinary single-wall spiralization and unusual split contours.
for (const Polygon& wall_outline : part.spiral_wall)
{
- gcode_layer.spiralizeWallSlice(mesh_config.inset0_config, wall_outline, *last_wall_outline, seam_vertex_idx, last_seam_vertex_idx, is_top_layer, is_bottom_layer);
+ gcode_layer.spiralizeWallSlice(mesh_config.inset0_config, wall_outline, last_walls->front(), seam_vertex_idx, last_seam_vertex_idx, is_top_layer, is_bottom_layer);
}
}
diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp
index a717a5f..7e43b7f 100644
--- a/src/LayerPlan.cpp
+++ b/src/LayerPlan.cpp
@@ -3091,6 +3091,11 @@ void LayerPlan::spiralizeWallSlice(
}
}
+coord_t LayerPlan::calculateSpiralizedZOffset(const coord_t layer_thickness, const double length, const double total_length)
+{
+ return std::llround(layer_thickness * length / total_length);
+}
+
bool ExtruderPlan::forceMinimalLayerTime(double minTime, double time_other_extr_plans)
{
const double minimalSpeed = fan_speed_layer_time_settings_.cool_min_speed;
@@ -3806,7 +3811,7 @@ void LayerPlan::writeGCode(GCodeExport& gcode)
length += vSizeMM(p0 - p1_2d);
p0 = p1_2d;
- const coord_t z_offset = end_layer ? layer_thickness_ / 2 : std::round(layer_thickness_ * length / totalLength);
+ const coord_t z_offset = end_layer ? layer_thickness_ / 2 : calculateSpiralizedZOffset(layer_thickness_, length, totalLength);
const double extrude_speed = speed * spiral_path.speed_back_pressure_factor;
writeExtrusionRelativeZ(
gcode,
diff --git a/src/WallsComputation.cpp b/src/WallsComputation.cpp
index 40f65fd..640dd60 100644
--- a/src/WallsComputation.cpp
+++ b/src/WallsComputation.cpp
@@ -72,7 +72,7 @@ void WallsComputation::generateWalls(SliceLayerPart* part, SectionType section_t
const bool mesh_group_support_paint = Application::getInstance().current_slice_->scene.current_mesh_group->has_painted_support;
const bool recompute_outline_based_on_outer_wall = (settings_.get<bool>("support_enable") || mesh_group_support_paint) && ! settings_.get<bool>("fill_outline_gaps");
- generateSpiralInsets(part, line_width_0, wall_0_inset, recompute_outline_based_on_outer_wall);
+ generateSpiralInsets(part, line_width_0, line_width_x, wall_0_inset, wall_x_inset, recompute_outline_based_on_outer_wall);
if (layer_nr_ <= static_cast<LayerIndex>(settings_.get<size_t>("initial_bottom_layers")))
{
WallToolPaths wall_tool_paths(part->outline, line_width_0, line_width_x, wall_count, wall_0_inset, settings_, layer_nr_, section_type);
@@ -104,6 +104,20 @@ void WallsComputation::generateWalls(SliceLayer* layer, SectionType section)
generateWalls(&part, section);
}
+ // The experimental two-wall spiral only supports a layer containing one part. Keep the established single-wall
+ // behaviour for every part when this layer is not simple enough.
+ if (layer->parts.size() != 1)
+ {
+ for (SliceLayerPart& part : layer->parts)
+ {
+ if (part.spiral_wall_is_multi_wall)
+ {
+ part.spiral_wall.resize(1);
+ part.spiral_wall_is_multi_wall = false;
+ }
+ }
+ }
+
// Remove the parts which did not generate a wall. As these parts are too small to print,
// and later code can now assume that there is always minimal 1 wall line.
bool check_wall_and_spiral = settings_.get<size_t>("wall_line_count") >= 1 && ! settings_.get<bool>("fill_outline_gaps");
@@ -117,17 +131,38 @@ void WallsComputation::generateWalls(SliceLayer* layer, SectionType section)
layer->parts.erase(iterator_remove, layer->parts.end());
}
-void WallsComputation::generateSpiralInsets(SliceLayerPart* part, coord_t line_width_0, coord_t wall_0_inset, bool recompute_outline_based_on_outer_wall)
+void WallsComputation::generateSpiralInsets(
+ SliceLayerPart* part,
+ const coord_t line_width_0,
+ const coord_t line_width_x,
+ const coord_t wall_0_inset,
+ const coord_t wall_x_inset,
+ const bool recompute_outline_based_on_outer_wall)
{
+ part->spiral_wall_is_multi_wall = false;
part->spiral_wall = part->outline.offset(-line_width_0 / 2 - wall_0_inset);
// Optimize the wall. This prevents buffer underruns in the printer firmware, and reduces processing time in CuraEngine.
const ExtruderTrain& train_wall = settings_.get<ExtruderTrain&>("wall_0_extruder_nr");
part->spiral_wall = Simplify(train_wall.settings_).polygon(part->spiral_wall);
part->spiral_wall.removeDegenerateVerts();
+
+ // A two-wall vase is represented as two contours until the layer seam is known. The g-code writer later cuts
+ // both contours at that seam and stitches them into one revolution. Only accept the simple, unambiguous case.
+ if (settings_.get<size_t>("wall_line_count") == 2 && part->spiral_wall.size() == 1 && part->spiral_wall.front().isValid())
+ {
+ Shape inner_wall = part->outline.offset(-(line_width_0 + line_width_x / 2 + wall_x_inset));
+ inner_wall = Simplify(train_wall.settings_).polygon(inner_wall);
+ inner_wall.removeDegenerateVerts();
+ if (inner_wall.size() == 1 && inner_wall.front().isValid() && part->spiral_wall.inside(inner_wall.front().front(), true))
+ {
+ part->spiral_wall.push_back(std::move(inner_wall.front()));
+ part->spiral_wall_is_multi_wall = true;
+ }
+ }
if (recompute_outline_based_on_outer_wall)
{
- part->print_outline = part->spiral_wall.offset(line_width_0 / 2, ClipperLib::jtSquare);
+ part->print_outline = part->spiral_wall.empty() ? Shape{} : Shape{ part->spiral_wall.front() }.offset(line_width_0 / 2, ClipperLib::jtSquare);
}
else
{
diff --git a/tests/FffGcodeWriterTest.cpp b/tests/FffGcodeWriterTest.cpp
index 4470120..3d6b532 100644
--- a/tests/FffGcodeWriterTest.cpp
+++ b/tests/FffGcodeWriterTest.cpp
@@ -98,6 +98,26 @@ public:
}
};
+TEST(FffGcodeWriterTest, SpliceTwoWallSpiral)
+{
+ Shape walls;
+ walls.emplace_back(Polygon({ { 0, 0 }, { 1000, 0 }, { 1000, 1000 }, { 0, 1000 } }, false));
+ walls.emplace_back(Polygon({ { 200, 200 }, { 800, 200 }, { 800, 800 }, { 200, 800 } }, false));
+
+ constexpr size_t outer_seam = 1;
+ const Polygon composite = FffGcodeWriter::spliceTwoWallSpiral(walls, outer_seam);
+
+ ASSERT_EQ(composite.size(), walls[0].size() + walls[1].size() + 3);
+ EXPECT_EQ(composite.front(), walls[0][outer_seam]);
+ EXPECT_EQ(composite[walls[0].size()], walls[0][outer_seam]);
+ EXPECT_EQ(composite[walls[0].size() + 1], Point2LL(800, 200));
+ EXPECT_EQ(composite[walls[0].size() + 2], walls[1][0]); // Inner contour runs opposite to outer.
+ EXPECT_EQ(composite.back(), Point2LL(800, 200));
+
+ // The implicit closing segment is the first connector in reverse, making a slit/stitch instead of a figure eight.
+ EXPECT_EQ(composite.front() - composite.back(), composite.front() - composite[walls[0].size() + 1]);
+}
+
TEST_F(DISABLED_FffGcodeWriterTest, SurfaceGetsExtraInfillLinesUnderIt)
{
// SETUP
diff --git a/tests/LayerPlanTest.cpp b/tests/LayerPlanTest.cpp
index aba3d2f..dba1bca 100644
--- a/tests/LayerPlanTest.cpp
+++ b/tests/LayerPlanTest.cpp
@@ -660,6 +660,115 @@ TEST_F(OverhangSpeedTest, SpeedFactorSplitAtOverhangBoundary)
EXPECT_TRUE(found_reduced_speed) << "Segment crossing into overhang should have reduced speed";
}
+TEST_F(LayerPlanTest, TwoWallSpiralIsOneContinuousZRamp)
+{
+ settings->add("smooth_spiralized_contours", "true");
+ const GCodePathConfig config{
+ .type = PrintFeatureType::OuterWall,
+ .line_width = MM2INT(0.4),
+ .layer_thickness = MM2INT(0.1),
+ .flow = 1.0_r,
+ .speed_derivatives = SpeedDerivatives{ Velocity{ 50.0 }, Acceleration{ 1000.0 }, Velocity{ 10.0 } },
+ };
+
+ // Outer loop, radial stitch, reversed inner loop, and the implicit closing stitch back to the start.
+ const Polygon composite(
+ {
+ { 0, 0 },
+ { 10000, 0 },
+ { 10000, 10000 },
+ { 0, 10000 },
+ { 0, 0 },
+ { 1000, 1000 },
+ { 1000, 9000 },
+ { 9000, 9000 },
+ { 9000, 1000 },
+ { 1000, 1000 },
+ },
+ false);
+
+ constexpr int seam_vertex = 0;
+ constexpr int no_previous_seam = -1;
+ constexpr bool not_top_layer = false;
+ constexpr bool not_bottom_layer = false;
+ layer_plan.spiralizeWallSlice(config, composite, composite, seam_vertex, no_previous_seam, not_top_layer, not_bottom_layer);
+
+ const auto& paths = layer_plan.extruder_plans_.back().paths_;
+ const auto first_extrusion = std::find_if(
+ paths.begin(),
+ paths.end(),
+ [](const GCodePath& path)
+ {
+ return ! path.isTravelPath();
+ });
+ ASSERT_NE(first_extrusion, paths.end());
+ EXPECT_TRUE(
+ std::all_of(
+ first_extrusion,
+ paths.end(),
+ [](const GCodePath& path)
+ {
+ return ! path.isTravelPath() && ! path.retract && path.spiralize;
+ }))
+ << "There must be no travel or retraction after starting the composite extrusion";
+
+ std::vector<Point3LL> extrusion_points;
+ for (auto path = first_extrusion; path != paths.end(); ++path)
+ {
+ extrusion_points.insert(extrusion_points.end(), path->points.begin(), path->points.end());
+ }
+ ASSERT_EQ(extrusion_points.size(), composite.size());
+ EXPECT_EQ(extrusion_points.back().toPoint2LL(), composite.front());
+
+ double total_length = 0.0;
+ Point2LL previous = composite.front();
+ for (const Point3LL& point : extrusion_points)
+ {
+ total_length += vSizeMM(point.toPoint2LL() - previous);
+ previous = point.toPoint2LL();
+ }
+ ASSERT_GT(total_length, 0.0);
+
+ coord_t previous_z = 0;
+ double accumulated_length = 0.0;
+ previous = composite.front();
+ for (const Point3LL& point : extrusion_points)
+ {
+ accumulated_length += vSizeMM(point.toPoint2LL() - previous);
+ previous = point.toPoint2LL();
+ const coord_t z = LayerPlan::calculateSpiralizedZOffset(MM2INT(0.1), accumulated_length, total_length);
+ EXPECT_GE(z, previous_z);
+ previous_z = z;
+ }
+ EXPECT_EQ(previous_z, MM2INT(0.1)) << "The entire composite revolution must rise by exactly one layer";
+}
+
+TEST_F(LayerPlanTest, SingleWallSpiralPathRemainsOneLoop)
+{
+ settings->add("smooth_spiralized_contours", "true");
+ const GCodePathConfig config{
+ .type = PrintFeatureType::OuterWall,
+ .line_width = MM2INT(0.4),
+ .layer_thickness = MM2INT(0.1),
+ .flow = 1.0_r,
+ .speed_derivatives = SpeedDerivatives{ Velocity{ 50.0 }, Acceleration{ 1000.0 }, Velocity{ 10.0 } },
+ };
+ const Polygon wall({ { 0, 0 }, { 10000, 0 }, { 10000, 10000 }, { 0, 10000 } }, false);
+
+ layer_plan.spiralizeWallSlice(config, wall, wall, 0, -1, false, false);
+
+ size_t extrusion_point_count = 0;
+ for (const GCodePath& path : layer_plan.extruder_plans_.back().paths_)
+ {
+ if (! path.isTravelPath())
+ {
+ EXPECT_TRUE(path.spiralize);
+ extrusion_point_count += path.points.size();
+ }
+ }
+ EXPECT_EQ(extrusion_point_count, wall.size());
+}
+
TEST(NozzleTempInsertTest, SortNozzleTempInsterts)
{
std::vector<NozzleTempInsert> nozzle_temp_inserts{
diff --git a/tests/WallsComputationTest.cpp b/tests/WallsComputationTest.cpp
index 6ee51b7..9ecf170 100644
--- a/tests/WallsComputationTest.cpp
+++ b/tests/WallsComputationTest.cpp
@@ -10,7 +10,10 @@
#include <gtest/gtest.h>
+#include "Application.h"
+#include "ExtruderTrain.h"
#include "InsetOrderOptimizer.h" //Unit also under test.
+#include "Slice.h"
#include "geometry/OpenPolyline.h"
#include "geometry/Polygon.h" //To create example polygons.
#include "settings/Settings.h" //Settings to generate walls with.
@@ -74,6 +77,7 @@ public:
// Settings for a simple 2 walls, about as basic as possible.
settings.add("alternate_extra_perimeter", "false");
settings.add("fill_outline_gaps", "false");
+ settings.add("initial_bottom_layers", "5");
settings.add("initial_layer_line_width_factor", "100");
settings.add("magic_spiralize", "false");
settings.add("meshfix_maximum_deviation", "0.1");
@@ -97,6 +101,15 @@ public:
settings.add("wall_transition_length", "1");
settings.add("wall_x_extruder_nr", "0");
settings.add("wall_distribution_count", "2");
+ settings.add("support_enable", "false");
+
+ Application::getInstance().current_slice_ = std::make_shared<Slice>(1);
+ Application::getInstance().current_slice_->scene.extruders.emplace_back(0, &settings);
+ }
+
+ void TearDown() override
+ {
+ Application::getInstance().current_slice_.reset();
}
};
@@ -145,6 +158,63 @@ TEST_F(WallsComputationTest, GenerateWallsZeroWalls)
EXPECT_EQ(layer.parts.size(), 1) << "There is still just 1 part.";
}
+TEST_F(WallsComputationTest, SpiralizeTwoWallsGeneratesNestedContours)
+{
+ settings.add("magic_spiralize", "true");
+ settings.add("wall_line_count", "2");
+ SliceLayer layer;
+ layer.parts.emplace_back();
+ layer.parts.back().outline.push_back(square_shape);
+
+ walls_computation.generateWalls(&layer, SectionType::WALL);
+
+ ASSERT_EQ(layer.parts.size(), 1);
+ const SliceLayerPart& part = layer.parts.front();
+ ASSERT_TRUE(part.spiral_wall_is_multi_wall);
+ ASSERT_EQ(part.spiral_wall.size(), 2);
+ EXPECT_EQ(part.spiral_wall[0].orientation(), part.spiral_wall[1].orientation());
+ EXPECT_TRUE(Shape{ part.spiral_wall[0] }.inside(part.spiral_wall[1].front(), true));
+}
+
+TEST_F(WallsComputationTest, SpiralizeSingleWallRemainsUnchanged)
+{
+ settings.add("magic_spiralize", "true");
+ settings.add("wall_line_count", "1");
+ SliceLayer layer;
+ layer.parts.emplace_back();
+ layer.parts.back().outline.push_back(square_shape);
+
+ walls_computation.generateWalls(&layer, SectionType::WALL);
+
+ ASSERT_EQ(layer.parts.size(), 1);
+ EXPECT_FALSE(layer.parts.front().spiral_wall_is_multi_wall);
+ EXPECT_EQ(layer.parts.front().spiral_wall.size(), 1);
+}
+
+TEST_F(WallsComputationTest, SpiralizeTwoWallsFallsBackForMultipleParts)
+{
+ settings.add("magic_spiralize", "true");
+ settings.add("wall_line_count", "2");
+ SliceLayer layer;
+ layer.parts.resize(2);
+ layer.parts[0].outline.push_back(square_shape);
+ Polygon second_square = square_shape.front();
+ for (Point2LL& point : second_square)
+ {
+ point += Point2LL(MM2INT(30), 0);
+ }
+ layer.parts[1].outline.push_back(second_square);
+
+ walls_computation.generateWalls(&layer, SectionType::WALL);
+
+ ASSERT_EQ(layer.parts.size(), 2);
+ for (const SliceLayerPart& part : layer.parts)
+ {
+ EXPECT_FALSE(part.spiral_wall_is_multi_wall);
+ EXPECT_EQ(part.spiral_wall.size(), 1);
+ }
+}
+
/*!
* Tests if the inner area is properly set.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment