Created
August 16, 2026 12:00
-
-
Save penk/0b0b01f6d3ea442ed7166019fa649cb9 to your computer and use it in GitHub Desktop.
2-wall vase mode for Cura
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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