diff --git a/cmake/modules/ClangClShowIncludes.cmake b/cmake/modules/ClangClShowIncludes.cmake
index 91b9a18069..fcc48fe3f7 100644
--- a/cmake/modules/ClangClShowIncludes.cmake
+++ b/cmake/modules/ClangClShowIncludes.cmake
@@ -1,10 +1,18 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
# no headers for that object. ccache reproduces /showIncludes output on a hit.
+#
+# Clang also records the modification time of every input in a precompiled header
+# and rejects the header once one differs. ccache caches the compile that creates
+# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
+# with the old file's time, and every compile that uses it fails. The build system
+# already rebuilds the header when an input changes. This covers the dependencies,
+# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
+ list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif ()
endforeach ()
diff --git a/resources/images/score_star_outline.svg b/resources/images/score_star_outline.svg
new file mode 100644
index 0000000000..eb52be9e82
--- /dev/null
+++ b/resources/images/score_star_outline.svg
@@ -0,0 +1,3 @@
+
diff --git a/resources/profiles/Qidi.json b/resources/profiles/Qidi.json
index 4b6496df31..dcb39ee957 100644
--- a/resources/profiles/Qidi.json
+++ b/resources/profiles/Qidi.json
@@ -1,6 +1,6 @@
{
"name": "Qidi",
- "version": "02.04.00.16",
+ "version": "02.04.00.17",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [
diff --git a/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json
index bd2398ae7a..30db0fc68b 100644
--- a/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json
+++ b/resources/profiles/Qidi/machine/Qidi Q1 Pro 0.4 nozzle.json
@@ -83,7 +83,7 @@
"extruder_clearance_height_to_lid": "120",
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
- "machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
+ "machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"thumbnails_format": "PNG",
"default_filament_profile": [
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
diff --git a/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json
index a41e2b6a11..968f7f63a9 100644
--- a/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json
+++ b/resources/profiles/Qidi/machine/Qidi X-CF Pro 0.4 nozzle.json
@@ -100,7 +100,7 @@
"default_filament_profile": [
"Generic PLA @Qidi"
],
- "machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
+ "machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
"scan_first_layer": "0"
}
diff --git a/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json b/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json
index 9796856379..8fe450c3c6 100644
--- a/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json
+++ b/resources/profiles/Qidi/machine/fdm_qidi_x3_common.json
@@ -24,7 +24,7 @@
"210x210/COLPIC",
"110x110/PNG"
],
- "machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
+ "machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"scan_first_layer": "0"
}
diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp
index 8a22201c51..24c2c027e5 100644
--- a/src/libslic3r/Format/bbs_3mf.cpp
+++ b/src/libslic3r/Format/bbs_3mf.cpp
@@ -22,6 +22,7 @@
#include "libslic3r/BrimEarsPoint.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/CustomGCode.hpp"
+#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r_version.h"
#include "bbs_3mf.hpp"
@@ -354,6 +355,13 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
+// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
+// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
+static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
+ "paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
+ "paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
+static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
+static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
// BBS
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
@@ -847,6 +855,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::vector custom_seam;
std::vector mmu_segmentation;
std::vector fuzzy_skin;
+ // One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
+ // slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
+ std::vector texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
// BBS
std::vector face_properties;
@@ -867,6 +878,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
custom_seam.clear();
mmu_segmentation.clear();
fuzzy_skin.clear();
+ for (std::vector &slot : texture_displacement)
+ slot.clear();
}
};
@@ -1200,6 +1213,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
/*IdToSlaSupportPointsMap m_sla_support_points;
IdToSlaDrainHolesMap m_sla_drain_holes;*/
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
+ // Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
+ // are built only after the whole archive has been walked, so by the time a volume names its JSON
+ // every file it could refer to is already in here, whatever order the archive happened to be in.
+ std::map m_texture_displacement_files;
+ void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
std::string m_curr_metadata_name;
std::string m_curr_characters;
std::string m_name;
@@ -1729,8 +1747,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//plate->pattern_file = it->second->pattern_file;
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
plate->top_file = it->second->top_file;
- plate->pick_file = it->second->pick_file.empty();
- plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
+ plate->pick_file = it->second->pick_file;
+ plate->pattern_bbox_file = it->second->pattern_bbox_file;
plate->config = it->second->config;
if (!plate->thumbnail_file.empty())
@@ -2073,6 +2091,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (_is_svg_shape_file(name)) {
_extract_embossed_svg_shape_file(name, archive, stat);
}
+ else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
+ std::string contents(stat.m_uncomp_size, '\0');
+ if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
+ m_texture_displacement_files.emplace(name, std::move(contents));
+ else
+ add_error("Error while reading texture displacement data");
+ }
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
m_parsing_slice_info = true;
//extract slice info from archive
@@ -3310,6 +3335,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}*/
+ // Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
+ // being built, which is after the whole archive has been walked - so every file it can refer to is
+ // already extracted, and no deferral is needed.
+ //
+ // A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
+ // loaded, which is the same state as a project saved by a build without the feature: recoverable by
+ // picking the texture again rather than a hard failure.
+ void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
+ {
+ const auto json = m_texture_displacement_files.find(json_path);
+ if (json == m_texture_displacement_files.end()) {
+ add_error("Missing texture displacement data: " + json_path);
+ return;
+ }
+ std::vector layers;
+ TextureDisplacementOptions options;
+ if (!texture_displacement_layers_from_json(json->second, layers, options)) {
+ add_error("Malformed texture displacement data: " + json_path);
+ return;
+ }
+ // The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
+ // the project must never read the file it names: that would let whoever made the project pick any
+ // local (or, on Windows, network) file to read, and the next save would embed it in the archive.
+ // `path` stays a label of where the texture came from; a layer the archive carries no image for
+ // loads without one, its settings intact, until the user picks a texture again.
+ for (TextureDisplacementLayer &layer : layers) {
+ if (layer.path_in_3mf.empty())
+ continue;
+ const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
+ if (image == m_texture_displacement_files.end()) {
+ add_error("Missing texture displacement image: " + layer.path_in_3mf);
+ continue;
+ }
+ layer.image_data = std::make_shared>(image->second.begin(), image->second.end());
+ }
+ volume.texture_displacement_layers = std::move(layers);
+ volume.texture_displacement_options = options;
+ }
+
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
auto file = std::make_unique(stat.m_uncomp_size, '\0');
@@ -3966,6 +4030,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
+ for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
+ if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
+ std::vector &mask = m_curr_object->geometry.texture_displacement[slot];
+ mask.resize(m_curr_object->geometry.triangles.size());
+ mask.back() = paint;
+ }
// BBS
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
}
@@ -5317,6 +5387,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->mmu_segmentation_facets.touch();
volume->fuzzy_skin_facets.shrink_to_fit();
volume->fuzzy_skin_facets.touch();
+ for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
+ const std::vector &mask = sub_object->geometry.texture_displacement[slot];
+ if (mask.empty())
+ continue; // no triangle painted on this slot
+ FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
+ facets.reserve(triangles_count);
+ for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
+ if (!mask[i].empty())
+ facets.set_triangle_from_string(i, mask[i]);
+ facets.shrink_to_fit();
+ facets.touch();
+ }
}
volume->set_type(volume_data->part_type);
@@ -5332,6 +5414,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
for (const Metadata& metadata : volume_data->metadata) {
if (metadata.key == NAME_KEY)
volume->name = metadata.value;
+ else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
+ _apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format
@@ -5496,12 +5580,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->seam_facets.shrink_to_fit();
volume->mmu_segmentation_facets.shrink_to_fit();
+ for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
+ const std::vector &mask = geometry.texture_displacement[slot];
+ if (mask.empty())
+ continue; // written by a build without the feature
+ FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
+ facets.reserve(triangles_count);
+ for (size_t i = 0; i < triangles_count; ++i) {
+ const size_t index = volume_data.first_triangle_id + i;
+ if (index < mask.size() && !mask[index].empty())
+ facets.set_triangle_from_string(i, mask[index]);
+ }
+ facets.shrink_to_fit();
+ facets.touch();
+ }
volume->set_type(volume_data.part_type);
// apply the remaining volume's metadata
for (const Metadata& metadata : volume_data.metadata) {
if (metadata.key == NAME_KEY)
volume->name = metadata.value;
+ else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
+ _apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format
@@ -5805,6 +5905,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
+ for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
+ if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
+ std::vector &mask = current_object->geometry.texture_displacement[slot];
+ mask.resize(current_object->geometry.triangles.size());
+ mask.back() = paint;
+ }
// BBS
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
}
@@ -6808,6 +6914,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " \n";
stream << " \n";
stream << " \n";
+ stream << " \n";
+ stream << " \n";
+ stream << " \n";
stream << " \n";
stream << "";
@@ -7232,10 +7341,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (iter != m_shared_meshes.end())
{
const ModelVolume* shared_volume = iter->second.second;
+ bool same_texture_paint = true;
+ for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
+ same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
if ((shared_volume->supported_facets.equals(volume->supported_facets))
&& (shared_volume->seam_facets.equals(volume->seam_facets))
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
- && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
+ && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
+ && same_texture_paint)
{
auto data = iter->second.first;
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
@@ -7651,6 +7764,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
output_buffer += "\"";
}
+ // One attribute per texture displacement layer slot. Older readers ignore attributes they do
+ // not know, so a project written here still opens in a build without the feature - it just
+ // loses the paint, which is also all it could have done with it.
+ for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
+ const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
+ if (texture_paint.empty())
+ continue;
+ output_buffer += " ";
+ output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
+ output_buffer += "=\"";
+ output_buffer += texture_paint;
+ output_buffer += "\"";
+ }
+
// BBS
if (i < its.properties.size()) {
std::string prop_str = its.properties[i].to_string();
@@ -8100,7 +8227,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return file_path;
}
- bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
+ // Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
+// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
+// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
+// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
+// every reader has to parse just to list the objects.
+static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
+ const std::string &id)
+{
+ if (volume.texture_displacement_layers.empty())
+ return;
+
+ // A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
+ // being exported is const (and may be saved again, elsewhere, with different paths).
+ std::vector layers = volume.texture_displacement_layers;
+ for (TextureDisplacementLayer &layer : layers) {
+ layer.path_in_3mf.clear();
+ if (!layer.image_data || layer.image_data->empty())
+ continue;
+ // Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
+ // greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
+ const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
+ // No deflate: a PNG is already compressed, so a second pass only costs time.
+ if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
+ MZ_NO_COMPRESSION))
+ layer.path_in_3mf = path;
+ }
+
+ const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
+ const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
+ if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
+ return;
+ stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
+ << VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
+}
+
+bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
{
std::stringstream stream;
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
@@ -8211,6 +8373,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (const std::optional &es = volume->emboss_shape; es.has_value()) {
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
}
+
+ add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
if (const std::optional &tc = volume->text_configuration;
tc.has_value())
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index cab75d5034..ec7ef83a3d 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -3295,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_is_role_based_fan_on.fill(false);
m_role_based_fan_marker_layer.fill(-1);
- m_fan_mover.release();
+ m_fan_mover.reset();
m_ordering_cache.clear();
// Belt printer: initialize belt-specific writer via virtual hook.
diff --git a/src/libslic3r/JumpPointSearch.cpp b/src/libslic3r/JumpPointSearch.cpp
deleted file mode 100644
index 9eb18017fb..0000000000
--- a/src/libslic3r/JumpPointSearch.cpp
+++ /dev/null
@@ -1,349 +0,0 @@
-#include "JumpPointSearch.hpp"
-#include "BoundingBox.hpp"
-#include "ExPolygon.hpp"
-#include "Point.hpp"
-#include "libslic3r/AStar.hpp"
-#include "libslic3r/KDTreeIndirect.hpp"
-#include "libslic3r/Polygon.hpp"
-#include "libslic3r/Polyline.hpp"
-#include "libslic3r/libslic3r.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-
-//#define DEBUG_FILES
-#ifdef DEBUG_FILES
-#include "libslic3r/SVG.hpp"
-#endif
-
-namespace Slic3r {
-
-// execute fn for each pixel on the line. If fn returns false, terminate the iteration
-template void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
-{
- coord_t dx = abs(x1 - x0);
- coord_t dy = abs(y1 - y0);
- coord_t x = x0;
- coord_t y = y0;
- coord_t n = 1 + dx + dy;
- coord_t x_inc = (x1 > x0) ? 1 : -1;
- coord_t y_inc = (y1 > y0) ? 1 : -1;
- coord_t error = dx - dy;
- dx *= 2;
- dy *= 2;
-
- for (; n > 0; --n) {
- if (!fn(x, y)) return;
-
- if (error > 0) {
- x += x_inc;
- error -= dy;
- } else {
- y += y_inc;
- error += dx;
- }
- }
-}
-
-// will draw the line twice, second time with and offset of 1 in the direction of normal
-// may call the fn on the same coordiantes multiple times!
-template void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
-{
- Vec2d normal = Point{y1 - y0, x1 - x0}.cast().normalized();
- normal.x() = ceil(normal.x());
- normal.y() = ceil(normal.y());
- Point start_offset = Point(x0, y0) + (normal).cast();
- Point end_offset = Point(x1, y1) + (normal).cast();
-
- dda(x0, y0, x1, y1, fn);
- dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
-}
-
-template class JPSTracer
-{
-public:
- // Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
- struct Node
- {
- CellPositionType position;
- CellPositionType incoming_dir;
- };
-
- JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
-
-private:
- CellPositionType target;
- CellQueryFn is_passable; // should return boolean whether the cell is passable or not
-
- CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
- {
- CellPositionType next = start + forward_dir;
- while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
-
- if (is_passable(next)) {
- return next;
- } else {
- return start;
- }
- }
-
- bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
- {
- if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
- // diagonal
- CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
- CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
-
- if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
-
- if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
-
- if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
-
- if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
-
- return false;
- } else { // horizontal or vertical
- CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
-
- if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
-
- if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
-
- return false;
- }
- }
-
-public:
- template void foreach_reachable(const Node &from, Fn &&fn) const
- {
- const CellPositionType & pos = from.position;
- const CellPositionType & forward_dir = from.incoming_dir;
- std::vector dirs_to_check{};
-
- if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
- dirs_to_check = all_directions;
- } else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
- // diagonal
- CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
- CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
-
- if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
- dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
- }
-
- if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
- dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
- }
-
- dirs_to_check.push_back(horizontal_check_dir);
- dirs_to_check.push_back(vertical_check_dir);
- dirs_to_check.push_back(forward_dir);
-
- } else { // horizontal or vertical
- CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
-
- if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
-
- if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
- dirs_to_check.push_back(forward_dir);
- }
-
- for (const CellPositionType &dir : dirs_to_check) {
- CellPositionType jp = find_jump_point(pos, dir);
- if (jp != pos) fn(Node{jp, dir});
- }
- }
-
- float distance(Node a, Node b) const { return (a.position - b.position).template cast().norm(); }
-
- float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast().norm(); }
-
- size_t unique_id(Node n) const { return (static_cast(uint16_t(n.position.x())) << 16) + static_cast(uint16_t(n.position.y())); }
-
- const std::vector all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
-};
-
-void JPSPathFinder::clear()
-{
- inpassable.clear();
- max_search_box.max = Pixel(std::numeric_limits::min(), std::numeric_limits::min());
- max_search_box.min = Pixel(std::numeric_limits::max(), std::numeric_limits::max());
- add_obstacles(bed_shape);
-}
-
-void JPSPathFinder::add_obstacles(const Lines &obstacles)
-{
- auto store_obstacle = [&](coord_t x, coord_t y) {
- max_search_box.max.x() = std::max(max_search_box.max.x(), x);
- max_search_box.max.y() = std::max(max_search_box.max.y(), y);
- max_search_box.min.x() = std::min(max_search_box.min.x(), x);
- max_search_box.min.y() = std::min(max_search_box.min.y(), y);
- inpassable.insert(Pixel{x, y});
- return true;
- };
-
- for (const Line &l : obstacles) {
- Pixel start = pixelize(l.a);
- Pixel end = pixelize(l.b);
- double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
- }
-}
-
-Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
-{
- Pixel start = pixelize(p0);
- Pixel end = pixelize(p1);
- if (inpassable.empty() || (start - end).cast().norm() < 3.0) { return Polyline{p0, p1}; }
-
- if (inpassable.find(start) != inpassable.end()) {
- dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
- if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
- start = Pixel(x, y);
- return false;
- }
- return true;
- });
- }
-
- if (inpassable.find(end) != inpassable.end()) {
- dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
- if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
- end = Pixel(x, y);
- return false;
- }
- return true;
- });
- }
-
- BoundingBox search_box = max_search_box;
- search_box.max -= Pixel(1, 1);
- search_box.min += Pixel(1, 1);
-
- BoundingBox bounding_square(Points{start, end});
- bounding_square.max += Pixel(5, 5);
- bounding_square.min -= Pixel(5, 5);
- coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
- bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
- bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
- bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
- bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
-
- // Intersection - limit the search box to a square area around the start and end, to fasten the path searching
- search_box.max = search_box.max.cwiseMin(bounding_square.max);
- search_box.min = search_box.min.cwiseMax(bounding_square.min);
-
- auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
-
- JPSTracer tracer(end, cell_query);
- using QNode = astar::QNode>;
-
- std::unordered_map astar_cache{};
- std::vector> out_path;
- std::vector out_nodes;
-
- if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
- // path not found - just reconstruct the best path from astar cache.
- // Note that astar_cache is NOT empty - at least the starting point should always be there
- auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
- std::vector keys;
- keys.reserve(astar_cache.size());
- for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
- KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
- size_t closest_qnode = find_closest_point(kd_tree, end.cast());
-
- out_path.push_back(end);
- while (closest_qnode != astar::Unassigned) {
- out_path.push_back(astar_cache[closest_qnode].node.position);
- closest_qnode = astar_cache[closest_qnode].parent;
- }
- } else {
- for (const auto &node : out_nodes) { out_path.push_back(node.position); }
- out_path.push_back(start);
- }
-
-#ifdef DEBUG_FILES
- auto scaled_points = [](const Points &ps) {
- Points r;
- for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
- return r;
- };
- auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
- ::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
- BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
- for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
- for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
- svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
- svg.draw(scaled_point(end), "purple", scale_(0.4));
- svg.draw(scaled_point(start), "green", scale_(0.4));
-#endif
-
- std::vector> tmp_path;
- tmp_path.reserve(out_path.size());
- // Some path found, reverse and remove points that do not change direction
- std::reverse(out_path.begin(), out_path.end());
- {
- tmp_path.push_back(out_path.front()); // first point
- for (size_t i = 1; i < out_path.size() - 1; i++) {
- if ((out_path[i] - out_path[i - 1]).cast().normalized() != (out_path[i + 1] - out_path[i]).cast().normalized()) { tmp_path.push_back(out_path[i]); }
- }
- tmp_path.push_back(out_path.back()); // last_point
- out_path = tmp_path;
- }
-
-#ifdef DEBUG_FILES
- svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
-#endif
-
- tmp_path.clear();
- // remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
- // usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
- // insert the Px+k point to result and continue search from Px+k
- {
- tmp_path.push_back(out_path.front()); // first point
- size_t index_of_last_stored_point = 0;
- for (size_t i = 1; i < out_path.size(); i++) {
- if (i - index_of_last_stored_point < 2) continue;
- bool passable = true;
- auto store_obstacle = [&](coord_t x, coord_t y) {
- if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
- passable = false;
- return false;
- }
- return true;
- };
- dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
- if (!passable) {
- tmp_path.push_back(out_path[i - 1]);
- index_of_last_stored_point = i - 1;
- }
- }
- tmp_path.push_back(out_path.back()); // last_point
- out_path = tmp_path;
- }
-
-#ifdef DEBUG_FILES
- svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
- svg.Close();
-#endif
-
- // before returing the path, transform it from pixels back to points.
- // Also replace the first and last pixel by input points so that result path patches input params exactly.
- for (Pixel &p : out_path) { p = unpixelize(p); }
- out_path.front() = p0;
- out_path.back() = p1;
-
- return Polyline(out_path);
-}
-
-} // namespace Slic3r
diff --git a/src/libslic3r/JumpPointSearch.hpp b/src/libslic3r/JumpPointSearch.hpp
deleted file mode 100644
index b5fbf6aaf0..0000000000
--- a/src/libslic3r/JumpPointSearch.hpp
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma once
-#include "Line.hpp"
-#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
-#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
-
-#include "BoundingBox.hpp"
-#include "Polygon.hpp"
-#include "libslic3r/Layer.hpp"
-#include "libslic3r/Point.hpp"
-#include "libslic3r/Polyline.hpp"
-#include "libslic3r/libslic3r.h"
-#include
-#include
-
-namespace Slic3r {
-
-class JPSPathFinder
-{
- using Pixel = Point;
- std::unordered_set inpassable;
- coordf_t print_z;
- BoundingBox max_search_box;
- Lines bed_shape;
-
- const coord_t resolution = scaled(1.5);
- Pixel pixelize(const Point &p) { return p / resolution; }
- Point unpixelize(const Pixel &p) { return p * resolution; }
-
-public:
- JPSPathFinder() = default;
- void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
- void clear();
- void add_obstacles(const Lines &obstacles);
- Polyline find_path(const Point &start, const Point &end);
-};
-
-} // namespace Slic3r
-
-#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp
index 6038ac2ac3..583a98b3ca 100644
--- a/src/libslic3r/TextureDisplacement.cpp
+++ b/src/libslic3r/TextureDisplacement.cpp
@@ -19,6 +19,7 @@
#include
#include
#include
+#include "nlohmann/json.hpp"
#include
#include
#include
@@ -5373,5 +5374,260 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
return out;
}
+// ---------------------------------------------------------------------------------------------
+// Project persistence. See the declarations in TextureDisplacement.hpp for why this is JSON rather
+// than the cereal stream next to them.
+// ---------------------------------------------------------------------------------------------
+namespace {
+
+nlohmann::json vec2_to_json(const Vec2f &v) { return nlohmann::json::array({ v.x(), v.y() }); }
+nlohmann::json vec3_to_json(const Vec3f &v) { return nlohmann::json::array({ v.x(), v.y(), v.z() }); }
+
+Vec2f vec2_from_json(const nlohmann::json &j, const Vec2f &fallback)
+{
+ if (!j.is_array() || j.size() != 2)
+ return fallback;
+ return Vec2f(j[0].get(), j[1].get());
+}
+Vec3f vec3_from_json(const nlohmann::json &j, const Vec3f &fallback)
+{
+ if (!j.is_array() || j.size() != 3)
+ return fallback;
+ return Vec3f(j[0].get(), j[1].get(), j[2].get());
+}
+
+// Reads key `k` into `out` when it is present and of the expected type, leaving `out` alone otherwise.
+// That "leave it alone" is the whole point: every member starts at its struct default, so a project
+// written by an older build simply keeps the defaults for whatever it did not know about.
+template void read(const nlohmann::json &j, const char *k, T &out)
+{
+ const auto it = j.find(k);
+ if (it == j.end())
+ return;
+ try {
+ out = it->get();
+ } catch (...) {
+ }
+}
+void read_enum(const nlohmann::json &j, const char *k, int &out)
+{
+ const auto it = j.find(k);
+ if (it != j.end() && it->is_number_integer())
+ out = it->get();
+}
+
+} // namespace
+
+std::string texture_displacement_layers_to_json(const std::vector &layers,
+ const TextureDisplacementOptions &options)
+{
+ nlohmann::json root;
+ root["version"] = 1;
+
+ nlohmann::json &opt = root["options"];
+ opt["displace_border"] = options.displace_border;
+ opt["smooth_enabled"] = options.smooth_enabled;
+ opt["smooth_strength"] = options.smooth_strength;
+ opt["smooth_iterations"] = options.smooth_iterations;
+ opt["smooth_skip_border"] = options.smooth_skip_border;
+ opt["pipeline_v2"] = options.pipeline_v2;
+ opt["v2_refine_mm"] = options.v2_refine_mm;
+ opt["v2_regularize"] = options.v2_regularize;
+ opt["v2_max_triangles_k"] = options.v2_max_triangles_k;
+ opt["v2_relocate"] = options.v2_relocate;
+ opt["v2_flip_edges"] = options.v2_flip_edges;
+ opt["color_mix_enabled"] = options.color_mix_enabled;
+ opt["color_despeckle"] = options.color_despeckle;
+ opt["color_mix_count"] = options.color_mix_count;
+ opt["active_slot"] = options.active_slot;
+ opt["panel_mode"] = options.panel_mode;
+
+ nlohmann::json &arr = root["layers"];
+ arr = nlohmann::json::array();
+ for (const TextureDisplacementLayer &l : layers) {
+ nlohmann::json j;
+ j["slot"] = l.slot;
+ j["name"] = l.name;
+ j["path"] = l.path;
+ j["path_in_3mf"] = l.path_in_3mf;
+
+ j["depth_mm"] = l.depth_mm;
+ j["tiling_scale"] = l.tiling_scale;
+ j["rotation_deg"] = l.rotation_deg;
+ j["offset"] = vec2_to_json(l.offset);
+ j["invert"] = l.invert;
+ j["midlevel"] = l.midlevel;
+ j["smoothing"] = l.smoothing;
+
+ j["edge_smoothing"] = l.edge_smoothing;
+ j["edge_smoothing_amount"] = l.edge_smoothing_amount;
+ j["auto_connect_islands"] = l.auto_connect_islands;
+
+ j["tile_enabled"] = l.tile_enabled;
+ j["tile_method"] = int(l.tile_method);
+ j["projection_method"] = int(l.projection_method);
+ j["blend_mode"] = int(l.blend_mode);
+ j["color_enabled"] = l.color_enabled;
+
+ j["lscm_seam_angle_deg"] = l.lscm_seam_angle_deg;
+ j["island_padding_mm"] = l.island_padding_mm;
+
+ j["view_project_right"] = vec3_to_json(l.view_project_right);
+ j["view_project_up"] = vec3_to_json(l.view_project_up);
+ j["view_project_projective"] = l.view_project_projective;
+ j["view_project_matrix"] = l.view_project_matrix;
+
+ // Flat pairs rather than nested arrays: shorter, and the reader can simply ignore a trailing
+ // odd element instead of having to validate every sub-array.
+ nlohmann::json &seams = j["lscm_seam_edges"];
+ seams = nlohmann::json::array();
+ for (const auto &[a, b] : l.lscm_seam_edges) {
+ seams.push_back(a);
+ seams.push_back(b);
+ }
+
+ nlohmann::json &islands = j["islands"];
+ islands = nlohmann::json::array();
+ for (const TextureIsland &island : l.islands)
+ islands.push_back(nlohmann::json::array(
+ { island.offset.x(), island.offset.y(), island.rotation_deg, island.scale }));
+
+ j["island_groups"] = l.island_groups;
+
+ nlohmann::json &overrides = j["lscm_uv_overrides"];
+ overrides = nlohmann::json::array();
+ for (const auto &[key, uv] : l.lscm_uv_overrides)
+ overrides.push_back(nlohmann::json::array({ key, uv.x(), uv.y() }));
+
+ arr.push_back(std::move(j));
+ }
+ return root.dump();
+}
+
+bool texture_displacement_layers_from_json(const std::string &text,
+ std::vector &layers,
+ TextureDisplacementOptions &options)
+{
+ nlohmann::json root;
+ try {
+ root = nlohmann::json::parse(text);
+ } catch (...) {
+ return false;
+ }
+ if (!root.is_object())
+ return false;
+
+ TextureDisplacementOptions out_options;
+ if (const auto it = root.find("options"); it != root.end() && it->is_object()) {
+ const nlohmann::json &opt = *it;
+ read(opt, "displace_border", out_options.displace_border);
+ read(opt, "smooth_enabled", out_options.smooth_enabled);
+ read(opt, "smooth_strength", out_options.smooth_strength);
+ read(opt, "smooth_iterations", out_options.smooth_iterations);
+ read(opt, "smooth_skip_border", out_options.smooth_skip_border);
+ read(opt, "pipeline_v2", out_options.pipeline_v2);
+ read(opt, "v2_refine_mm", out_options.v2_refine_mm);
+ read(opt, "v2_regularize", out_options.v2_regularize);
+ read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k);
+ read(opt, "v2_relocate", out_options.v2_relocate);
+ read(opt, "v2_flip_edges", out_options.v2_flip_edges);
+ read(opt, "color_mix_enabled", out_options.color_mix_enabled);
+ read(opt, "color_despeckle", out_options.color_despeckle);
+ read(opt, "color_mix_count", out_options.color_mix_count);
+ read(opt, "active_slot", out_options.active_slot);
+ read(opt, "panel_mode", out_options.panel_mode);
+ out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1);
+ out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1);
+ }
+
+ std::vector out_layers;
+ // read() skips a value of the wrong type, but the vector, matrix, seam, island and UV-override arrays
+ // below read their numbers with get<>(), which throws on one; that rejects the document, like text that
+ // does not parse.
+ try {
+ if (const auto it = root.find("layers"); it != root.end() && it->is_array()) {
+ for (const nlohmann::json &j : *it) {
+ if (!j.is_object())
+ continue;
+ TextureDisplacementLayer l;
+ read(j, "slot", l.slot);
+ // The panel and the paint masks are indexed by slot, so a layer without a valid one rejects the
+ // document rather than reaching them.
+ if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
+ return false;
+ read(j, "name", l.name);
+ read(j, "path", l.path);
+ read(j, "path_in_3mf", l.path_in_3mf);
+
+ read(j, "depth_mm", l.depth_mm);
+ read(j, "tiling_scale", l.tiling_scale);
+ read(j, "rotation_deg", l.rotation_deg);
+ if (const auto o = j.find("offset"); o != j.end())
+ l.offset = vec2_from_json(*o, l.offset);
+ read(j, "invert", l.invert);
+ read(j, "midlevel", l.midlevel);
+ read(j, "smoothing", l.smoothing);
+
+ read(j, "edge_smoothing", l.edge_smoothing);
+ read(j, "edge_smoothing_amount", l.edge_smoothing_amount);
+ read(j, "auto_connect_islands", l.auto_connect_islands);
+
+ read(j, "tile_enabled", l.tile_enabled);
+ read(j, "color_enabled", l.color_enabled);
+ int tile_method = int(l.tile_method), projection = int(l.projection_method), blend = int(l.blend_mode);
+ read_enum(j, "tile_method", tile_method);
+ read_enum(j, "projection_method", projection);
+ read_enum(j, "blend_mode", blend);
+ l.tile_method = TextureTileMethod(tile_method);
+ l.projection_method = TextureProjectionMethod(std::clamp(projection, 0, 4));
+ l.blend_mode = TextureBlendMode(blend);
+
+ read(j, "lscm_seam_angle_deg", l.lscm_seam_angle_deg);
+ read(j, "island_padding_mm", l.island_padding_mm);
+
+ if (const auto v = j.find("view_project_right"); v != j.end())
+ l.view_project_right = vec3_from_json(*v, l.view_project_right);
+ if (const auto v = j.find("view_project_up"); v != j.end())
+ l.view_project_up = vec3_from_json(*v, l.view_project_up);
+ read(j, "view_project_projective", l.view_project_projective);
+ if (const auto v = j.find("view_project_matrix"); v != j.end() && v->is_array() && v->size() == 12)
+ for (size_t i = 0; i < 12; ++i)
+ l.view_project_matrix[i] = (*v)[i].get();
+
+ if (const auto v = j.find("lscm_seam_edges"); v != j.end() && v->is_array())
+ for (size_t i = 0; i + 1 < v->size(); i += 2)
+ l.lscm_seam_edges.emplace_back((*v)[i].get(), (*v)[i + 1].get());
+
+ if (const auto v = j.find("islands"); v != j.end() && v->is_array())
+ for (const nlohmann::json &e : *v) {
+ if (!e.is_array() || e.size() != 4)
+ continue;
+ TextureIsland island;
+ island.offset = Vec2f(e[0].get(), e[1].get());
+ island.rotation_deg = e[2].get();
+ island.scale = e[3].get();
+ l.islands.push_back(island);
+ }
+
+ read(j, "island_groups", l.island_groups);
+
+ if (const auto v = j.find("lscm_uv_overrides"); v != j.end() && v->is_array())
+ for (const nlohmann::json &e : *v) {
+ if (!e.is_array() || e.size() != 3)
+ continue;
+ l.lscm_uv_overrides.emplace_back(e[0].get(), Vec2f(e[1].get(), e[2].get()));
+ }
+
+ out_layers.push_back(std::move(l));
+ }
+ }
+ } catch (const nlohmann::json::exception &) {
+ return false;
+ }
+
+ layers = std::move(out_layers);
+ options = out_options;
+ return true;
+}
} // namespace Slic3r
diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp
index 06c655b070..a859aa2f8e 100644
--- a/src/libslic3r/TextureDisplacement.hpp
+++ b/src/libslic3r/TextureDisplacement.hpp
@@ -413,14 +413,41 @@ struct TextureDisplacementOptions
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
int color_despeckle = 2;
+ // Where the panel was left, rather than anything the bake reads. It lives with the volume because
+ // that is what it describes: reopening a project should put the user back on the layer they were
+ // editing, in the mode they were editing it in, not on slot 0 in Standard.
+ int active_slot = 0;
+ int panel_mode = 0; // 0 Standard, 1 Pro
+
template void serialize(Archive &ar)
{
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
- v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
+ v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count,
+ active_slot, panel_mode);
}
};
+// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the
+// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and
+// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would
+// make every future field a project-breaking change. A JSON object tolerates both directions - an
+// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and
+// new ones degrade gracefully in older builds.
+//
+// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file
+// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is
+// responsible for writing it and for filling `image_data` back in on load.
+std::string texture_displacement_layers_to_json(const std::vector &layers,
+ const TextureDisplacementOptions &options);
+// Returns false and leaves both outputs untouched when the text is not a valid JSON object, a number in a
+// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is
+// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape
+// keeps its default.
+bool texture_displacement_layers_from_json(const std::string &text,
+ std::vector &layers,
+ TextureDisplacementOptions &options);
+
// How much detail a height texture carries: central differences of the grey image, the mean gradient
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
diff --git a/src/libslic3r/TriangleMeshDeal.hpp b/src/libslic3r/TriangleMeshDeal.hpp
index b1da431229..89d613bb85 100644
--- a/src/libslic3r/TriangleMeshDeal.hpp
+++ b/src/libslic3r/TriangleMeshDeal.hpp
@@ -1,5 +1,4 @@
-#ifndef libslic3r_Timer_hpp_
-#define libslic3r_Timer_hpp_
+#pragma once
#include "TriangleMesh.hpp"
@@ -10,5 +9,3 @@ public:
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
};
} // namespace Slic3r
-
-#endif // libslic3r_Timer_hpp_
diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp
index 24ebe9a337..d9d30573fc 100644
--- a/src/libslic3r/Utils.hpp
+++ b/src/libslic3r/Utils.hpp
@@ -109,7 +109,6 @@ extern std::string format_memsize_MB(size_t n);
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
// Return string to be added to the boost::log output to inform about the current process memory allocation.
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
-// Latter is used to get the memory info from SysInfoDialog.
extern std::string log_memory_info(bool ignore_loglevel = false);
extern void disable_multi_threading();
// Returns the size of physical memory (RAM) in bytes.
diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp
index cd9ecf100d..e8cc1759d5 100644
--- a/src/libslic3r/utils.cpp
+++ b/src/libslic3r/utils.cpp
@@ -1744,9 +1744,7 @@ std::string format_diameter_to_str(double diameter, int precision)
return oss.str();
}
-// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
-// The latter parses the string with (semi)colons as separators, it should look about as
-// "desc1: value1; desc2: value2" or similar (spaces should not matter).
+// Returns platform-specific string to be used as log output.
std::string log_memory_info(bool ignore_loglevel)
{
std::string out;
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 65c8384ef2..dd13fd5353 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -416,8 +416,6 @@ set(SLIC3R_GUI_SOURCES
GUI/MultiSendMachineModel.hpp
GUI/MultiTaskManagerPage.cpp
GUI/MultiTaskManagerPage.hpp
- GUI/MultiTaskModel.cpp
- GUI/MultiTaskModel.hpp
GUI/NetworkTestDialog.cpp
GUI/NetworkTestDialog.hpp
GUI/Notebook.cpp
@@ -802,7 +800,6 @@ set(SLIC3R_GUI_SOURCES
Utils/PrintHost.hpp
Utils/Process.cpp
Utils/Process.hpp
- Utils/Profile.hpp
Utils/RaycastManager.cpp
Utils/RaycastManager.hpp
Utils/Repetier.cpp
diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp
index 903524caaa..41047dffd9 100644
--- a/src/slic3r/GUI/AMSMaterialsSetting.cpp
+++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp
@@ -16,6 +16,7 @@
#include
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
+#include "slic3r/GUI/Widgets/DropDown.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include
#include "slic3r/GUI/wxExtensions.hpp"
@@ -172,7 +173,26 @@ void AMSMaterialsSetting::create()
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
- m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
+ m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
+ m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
+
+ // Favorites toggled in the open list are regrouped just before it opens again, so rows never move
+ // under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
+ // rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
+ auto regroup_favorites = [this]() {
+ if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
+ return;
+ const FilamentInfos* info = selected_filament_info();
+ const std::string filament_id = info ? info->filament_id : std::string();
+ rebuild_filament_dropdown_items(filament_id);
+ };
+ m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
+ m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
+ m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
+ if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
+ regroup_favorites();
+ e.Skip();
+ });
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -191,11 +211,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
-#ifdef __APPLE__
- m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
-#else
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
-#endif
+ m_star_filled = ScalableBitmap(this, "score_star_light", 16);
+ m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
@@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
AMSMaterialsSetting::~AMSMaterialsSetting()
{
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
+ m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -531,10 +550,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
std::string selected_ams_id;
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
+ const FilamentInfos* selected_info = selected_filament_info();
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
- auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
- std::string filament_id = filament_item.filament_id;
- if (it->filament_id.compare(filament_id) == 0) {
+ if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
selected_ams_id = it->filament_id;
break;
}
@@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
ams_setting_id = "";
// the combobox item
- auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
+ const FilamentInfos* selected_info = selected_filament_info();
+ const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
// check filament info (rack-gated per-nozzle blacklist evaluation)
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
- filament_item.filament_id, ams_filament_id, ams_setting_id);
+ filament_id, ams_filament_id, ams_setting_id);
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
wxString info_msg;
@@ -986,6 +1005,133 @@ static void _collect_filament_info(const wxString& shown_name,
query_filament_types[shown_name] = filament.config.get_filament_type();
}
+static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
+
+const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
+{
+ const int selection = m_comboBox_filament->GetSelection();
+ if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
+ return nullptr;
+ return &m_filament_list_items[selection].filament;
+}
+
+std::set AMSMaterialsSetting::load_favorite_filament_ids() const
+{
+ std::set favorite_ids;
+ std::vector values;
+ const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY);
+ if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) {
+ favorite_ids.insert(values.begin(), values.end());
+ }
+ return favorite_ids;
+}
+
+void AMSMaterialsSetting::save_favorite_filament_ids(const std::set& favorite_ids)
+{
+ std::vector values(favorite_ids.begin(), favorite_ids.end());
+ wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values));
+ wxGetApp().app_config->save();
+}
+
+void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id)
+{
+ std::vector items;
+ m_filament_list_items.clear();
+
+ auto append_header = [&items, this](const wxString& label) {
+ DropDown::Item item;
+ item.text = label;
+ item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED;
+ items.push_back(item);
+ m_filament_list_items.push_back(FilamentListItem{});
+ };
+
+ auto append_filament = [&items, this](const wxString& name) {
+ auto info_it = map_filament_items.find(name);
+ if (info_it == map_filament_items.end())
+ return;
+
+ DropDown::Item item;
+ item.text = name;
+ items.push_back(item);
+ m_filament_list_items.push_back(FilamentListItem{info_it->second, true});
+ };
+
+ // Favorites are listed again at the top under their own header. Without any, the list is the
+ // plain filament list.
+ const std::set favorite_ids = load_favorite_filament_ids();
+ std::vector favorites;
+ for (const wxString& name : m_sorted_filament_items) {
+ auto info_it = map_filament_items.find(name);
+ if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0)
+ favorites.push_back(name);
+ }
+ if (!favorites.empty()) {
+ append_header(_L("Favorite filaments"));
+ for (const wxString& name : favorites)
+ append_filament(name);
+ append_header(_L("All filaments"));
+ }
+ for (const wxString& name : m_sorted_filament_items)
+ append_filament(name);
+
+ int selection_idx = -1;
+ if (!selected_filament_id.empty()) {
+ for (int i = 0; i < int(m_filament_list_items.size()); ++i) {
+ if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) {
+ selection_idx = i;
+ break;
+ }
+ }
+ }
+
+ // Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it,
+ // which would leave the previous filament's name on the combo.
+ m_comboBox_filament->Clear();
+ m_comboBox_filament->SetItems(items);
+ update_favorite_stars();
+ m_comboBox_filament->SetSelection(selection_idx);
+ m_favorites_regroup = false;
+}
+
+void AMSMaterialsSetting::update_favorite_stars()
+{
+ const std::set favorite_ids = load_favorite_filament_ids();
+ for (size_t i = 0; i < m_filament_list_items.size(); ++i) {
+ const FilamentListItem& row = m_filament_list_items[i];
+ if (row.is_filament) {
+ const bool favorite = favorite_ids.count(row.filament.filament_id) > 0;
+ m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp());
+ }
+ }
+}
+
+void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id)
+{
+ if (filament_id.empty())
+ return;
+
+ std::set favorite_ids = load_favorite_filament_ids();
+ auto iter = favorite_ids.find(filament_id);
+ if (iter == favorite_ids.end())
+ favorite_ids.insert(filament_id);
+ else
+ favorite_ids.erase(iter);
+
+ save_favorite_filament_ids(favorite_ids);
+}
+
+void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt)
+{
+ const int index = evt.GetInt();
+ if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament)
+ return;
+
+ toggle_favorite_filament(m_filament_list_items[index].filament.filament_id);
+ update_favorite_stars();
+ m_favorites_regroup = true;
+}
+
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
{
if (!obj) return;
@@ -999,13 +1145,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
m_input_k_val->GetTextCtrl()->SetValue(k);
m_input_n_val->GetTextCtrl()->SetValue(n);
- wxArrayString filament_items;
wxString bambu_filament_name;
- wxString hint_filament_name; // the hint type to be selected
std::unordered_map query_filament_vendors;// some information for sort
std::unordered_map query_filament_types; //
std::set filament_id_set;
+ m_sorted_filament_items.clear();
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
map_filament_items.clear();
@@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
if (alias.empty())
continue;
- filament_items.push_back(alias);
+ m_sorted_filament_items.push_back(alias);
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
FilamentInfos filament_infos;
@@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
map_filament_items[alias] = filament_infos;
if (filament_it->filament_id == ams_filament_id) {
- hint_filament_name = from_u8(alias);
bambu_filament_name = from_u8(alias);
@@ -1215,24 +1359,11 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
return name_compare != 0 ? name_compare < 0 : left < right;
};
- std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
+ std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
}
- // traverse the hint selection idx
- int selection_idx = -1;
- {
- for(int i = 0; i < filament_items.size(); i++)
- {
- if (hint_filament_name == filament_items[i])
- {
- selection_idx = i;
- break;
- }
- }
- }
-
- m_comboBox_filament->Set(filament_items);
- m_comboBox_filament->SetSelection(selection_idx);
+ rebuild_filament_dropdown_items(ams_filament_id);
+ int selection_idx = m_comboBox_filament->GetSelection();
post_select_event(selection_idx);
if (selection_idx < 0) {
@@ -1289,6 +1420,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
int* from_printer = static_cast(m_comboBox_filament->GetClientData());
m_filament_type = "";
+ const FilamentInfos* selected_info = selected_filament_info();
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
std::ostringstream stream;
@@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
std::string nozzle_diameter_str = stream.str();
// Resolve the selection against the same list Popup() built the dropdown from, so the two
// halves of the dialog cannot disagree about which filaments this machine can use.
- const std::string selected = m_comboBox_filament->GetValue().ToStdString();
- if (!selected.empty()) {
- const std::string filament_id = map_filament_items[selected].filament_id;
+ if (selected_info) {
+ const std::string& filament_id = selected_info->filament_id;
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
if (it->filament_id != filament_id)
@@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
//reset cali
int cali_select_idx = -1;
- if ( !this->obj || m_filament_selection < 0) {
+ if ( !this->obj || m_filament_selection < 0 || !selected_info) {
m_input_k_val->Enable(false);
m_input_n_val->Enable(false);
m_button_confirm->Disable(); // ORCA No need to change style
@@ -1383,21 +1514,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
ams_filament_id = "";
ams_setting_id = "";
- if (preset_bundle) {
- for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
- auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
- if ( itor != map_filament_items.end()) {
- ams_filament_id = itor->second.filament_id;
- ams_setting_id = itor->second.setting_id;
- break;
- }
-
- if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
- ams_filament_id = it->filament_id;
- ams_setting_id = it->setting_id;
- break;
- }
- }
+ if (selected_info) {
+ ams_filament_id = selected_info->filament_id;
+ ams_setting_id = selected_info->setting_id;
}
wxArrayString items;
@@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
m_button_reset->Rescale(); // ORCA
m_button_confirm->Rescale(); // ORCA
m_button_close->Rescale(); // ORCA
+ // An open list would keep the measurements of the old stars, so close it before replacing them.
+ m_comboBox_filament->CloseDropdown();
+ m_star_filled.msw_rescale();
+ m_star_empty.msw_rescale();
+ update_favorite_stars();
this->Refresh();
}
diff --git a/src/slic3r/GUI/AMSMaterialsSetting.hpp b/src/slic3r/GUI/AMSMaterialsSetting.hpp
index 2628e6caf4..6fa215063a 100644
--- a/src/slic3r/GUI/AMSMaterialsSetting.hpp
+++ b/src/slic3r/GUI/AMSMaterialsSetting.hpp
@@ -31,6 +31,7 @@
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include
#include