mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682814878d | ||
|
|
30e48183e3 | ||
|
|
a243941b72 | ||
|
|
6a1a5d51e9 | ||
|
|
e625845b34 | ||
|
|
01b340dee1 | ||
|
|
4c2a63e11e | ||
|
|
f8cf34c55b | ||
|
|
d007134001 | ||
|
|
00d11a7ba0 | ||
|
|
8ecfee6a21 | ||
|
|
fbf4652e12 | ||
|
|
be9ff50130 | ||
|
|
dee1f1715f | ||
|
|
fcb1f62bd9 | ||
|
|
a13089d357 | ||
|
|
fb6c93963d | ||
|
|
7688f7a16c |
@@ -7478,7 +7478,7 @@ msgid "Update"
|
|||||||
msgstr "更新"
|
msgstr "更新"
|
||||||
|
|
||||||
msgid "Assistant(HMS)"
|
msgid "Assistant(HMS)"
|
||||||
msgstr "助理(HMS)"
|
msgstr "助手(HMS)"
|
||||||
|
|
||||||
#, c-format, boost-format
|
#, c-format, boost-format
|
||||||
msgid "Network plug-in v%s"
|
msgid "Network plug-in v%s"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 876 B |
@@ -22,6 +22,7 @@
|
|||||||
#include "libslic3r/BrimEarsPoint.hpp"
|
#include "libslic3r/BrimEarsPoint.hpp"
|
||||||
#include "libslic3r/Config.hpp"
|
#include "libslic3r/Config.hpp"
|
||||||
#include "libslic3r/CustomGCode.hpp"
|
#include "libslic3r/CustomGCode.hpp"
|
||||||
|
#include "libslic3r/TextureDisplacement.hpp"
|
||||||
#include "libslic3r_version.h"
|
#include "libslic3r_version.h"
|
||||||
|
|
||||||
#include "bbs_3mf.hpp"
|
#include "bbs_3mf.hpp"
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -354,6 +356,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_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
|
||||||
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
|
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
|
||||||
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
|
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
|
// BBS
|
||||||
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
||||||
|
|
||||||
@@ -847,11 +856,30 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
std::vector<std::string> custom_seam;
|
std::vector<std::string> custom_seam;
|
||||||
std::vector<std::string> mmu_segmentation;
|
std::vector<std::string> mmu_segmentation;
|
||||||
std::vector<std::string> fuzzy_skin;
|
std::vector<std::string> fuzzy_skin;
|
||||||
|
// One per texture displacement layer slot, indexed by triangle like the masks above, but
|
||||||
|
// empty for a slot nothing paints and no longer than its last painted triangle.
|
||||||
|
// See push_texture_displacement().
|
||||||
|
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
|
||||||
// BBS
|
// BBS
|
||||||
std::vector<std::string> face_properties;
|
std::vector<std::string> face_properties;
|
||||||
|
|
||||||
bool empty() { return vertices.empty() || triangles.empty(); }
|
bool empty() { return vertices.empty() || triangles.empty(); }
|
||||||
|
|
||||||
|
// Records the paint mask of the triangle just appended, for one layer slot. A slot no
|
||||||
|
// triangle names keeps its vector empty instead of holding one empty string per triangle:
|
||||||
|
// eight slots over a million triangles would otherwise cost hundreds of megabytes, for
|
||||||
|
// every project, whether or not it uses the feature. The vector is padded up to the
|
||||||
|
// triangles seen so far the first time a value turns up, so it stays indexed by triangle,
|
||||||
|
// and both readers stop at its size, so trailing triangles without a value need nothing.
|
||||||
|
void push_texture_displacement(int slot, std::string &&mask_string) {
|
||||||
|
std::vector<std::string> &mask = texture_displacement[slot];
|
||||||
|
if (mask_string.empty() && mask.empty())
|
||||||
|
return;
|
||||||
|
assert(!triangles.empty());
|
||||||
|
mask.resize(triangles.size() - 1);
|
||||||
|
mask.push_back(std::move(mask_string));
|
||||||
|
}
|
||||||
|
|
||||||
// backup & restore
|
// backup & restore
|
||||||
void swap(Geometry& o) {
|
void swap(Geometry& o) {
|
||||||
std::swap(vertices, o.vertices);
|
std::swap(vertices, o.vertices);
|
||||||
@@ -867,6 +895,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
custom_seam.clear();
|
custom_seam.clear();
|
||||||
mmu_segmentation.clear();
|
mmu_segmentation.clear();
|
||||||
fuzzy_skin.clear();
|
fuzzy_skin.clear();
|
||||||
|
for (std::vector<std::string> &slot : texture_displacement)
|
||||||
|
slot.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1200,6 +1230,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
/*IdToSlaSupportPointsMap m_sla_support_points;
|
/*IdToSlaSupportPointsMap m_sla_support_points;
|
||||||
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
||||||
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
|
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<std::string, std::string> 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_metadata_name;
|
||||||
std::string m_curr_characters;
|
std::string m_curr_characters;
|
||||||
std::string m_name;
|
std::string m_name;
|
||||||
@@ -1729,8 +1764,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
//plate->pattern_file = it->second->pattern_file;
|
//plate->pattern_file = it->second->pattern_file;
|
||||||
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
||||||
plate->top_file = it->second->top_file;
|
plate->top_file = it->second->top_file;
|
||||||
plate->pick_file = it->second->pick_file.empty();
|
plate->pick_file = it->second->pick_file;
|
||||||
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
||||||
plate->config = it->second->config;
|
plate->config = it->second->config;
|
||||||
|
|
||||||
if (!plate->thumbnail_file.empty())
|
if (!plate->thumbnail_file.empty())
|
||||||
@@ -2073,6 +2108,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
else if (_is_svg_shape_file(name)) {
|
else if (_is_svg_shape_file(name)) {
|
||||||
_extract_embossed_svg_shape_file(name, archive, stat);
|
_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)) {
|
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
|
||||||
m_parsing_slice_info = true;
|
m_parsing_slice_info = true;
|
||||||
//extract slice info from archive
|
//extract slice info from archive
|
||||||
@@ -3310,6 +3352,48 @@ 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<TextureDisplacementLayer> layers;
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
|
||||||
|
add_error("Malformed texture displacement data: " + json_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The pixels come from the archive and nowhere else. A layer the archive carries no image for -
|
||||||
|
// saved while its texture had not been loaded, or written by a build that stored only the path -
|
||||||
|
// keeps its settings without an image, which the user recovers by picking the texture again.
|
||||||
|
// `layer.path` is left alone on purpose: it names a file on the machine the project was made on,
|
||||||
|
// so reading it here would let any project choose a local file to open - a UNC path among them -
|
||||||
|
// and have the next save copy it into the archive. The EmbossShape SVGs are read from the archive
|
||||||
|
// only, for the same reason.
|
||||||
|
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<std::vector<unsigned char>>(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){
|
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());
|
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
|
||||||
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
||||||
@@ -3966,6 +4050,9 @@ 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.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.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));
|
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)
|
||||||
|
m_curr_object->geometry.push_texture_displacement(
|
||||||
|
slot, bbs_get_attribute_value_string(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot]));
|
||||||
// BBS
|
// BBS
|
||||||
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||||
}
|
}
|
||||||
@@ -5317,6 +5404,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
volume->mmu_segmentation_facets.touch();
|
volume->mmu_segmentation_facets.touch();
|
||||||
volume->fuzzy_skin_facets.shrink_to_fit();
|
volume->fuzzy_skin_facets.shrink_to_fit();
|
||||||
volume->fuzzy_skin_facets.touch();
|
volume->fuzzy_skin_facets.touch();
|
||||||
|
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||||
|
const std::vector<std::string> &mask = sub_object->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 < 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);
|
volume->set_type(volume_data->part_type);
|
||||||
@@ -5332,6 +5431,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
for (const Metadata& metadata : volume_data->metadata) {
|
for (const Metadata& metadata : volume_data->metadata) {
|
||||||
if (metadata.key == NAME_KEY)
|
if (metadata.key == NAME_KEY)
|
||||||
volume->name = metadata.value;
|
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"))
|
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||||
//for old format
|
//for old format
|
||||||
@@ -5496,12 +5597,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
volume->seam_facets.shrink_to_fit();
|
volume->seam_facets.shrink_to_fit();
|
||||||
volume->mmu_segmentation_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<std::string> &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);
|
volume->set_type(volume_data.part_type);
|
||||||
|
|
||||||
// apply the remaining volume's metadata
|
// apply the remaining volume's metadata
|
||||||
for (const Metadata& metadata : volume_data.metadata) {
|
for (const Metadata& metadata : volume_data.metadata) {
|
||||||
if (metadata.key == NAME_KEY)
|
if (metadata.key == NAME_KEY)
|
||||||
volume->name = metadata.value;
|
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"))
|
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||||
//for old format
|
//for old format
|
||||||
@@ -5805,6 +5922,9 @@ 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.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.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));
|
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)
|
||||||
|
current_object->geometry.push_texture_displacement(
|
||||||
|
slot, bbs_get_attribute_value_string(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot]));
|
||||||
// BBS
|
// BBS
|
||||||
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||||
}
|
}
|
||||||
@@ -6808,6 +6928,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
|
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
|
||||||
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
|
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
|
||||||
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
|
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
|
||||||
|
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
|
||||||
|
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
|
||||||
|
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
|
||||||
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
|
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
|
||||||
stream << "</Types>";
|
stream << "</Types>";
|
||||||
|
|
||||||
@@ -7651,6 +7774,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
output_buffer += "\"";
|
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
|
// BBS
|
||||||
if (i < its.properties.size()) {
|
if (i < its.properties.size()) {
|
||||||
std::string prop_str = its.properties[i].to_string();
|
std::string prop_str = its.properties[i].to_string();
|
||||||
@@ -8100,7 +8237,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
return file_path;
|
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<TextureDisplacementLayer> 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;
|
std::stringstream stream;
|
||||||
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
||||||
@@ -8211,6 +8383,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
|
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
|
||||||
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
|
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<TextConfiguration> &tc = volume->text_configuration;
|
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
|
||||||
tc.has_value())
|
tc.has_value())
|
||||||
|
|||||||
@@ -3295,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||||||
m_is_role_based_fan_on.fill(false);
|
m_is_role_based_fan_on.fill(false);
|
||||||
m_role_based_fan_marker_layer.fill(-1);
|
m_role_based_fan_marker_layer.fill(-1);
|
||||||
|
|
||||||
m_fan_mover.release();
|
m_fan_mover.reset();
|
||||||
m_ordering_cache.clear();
|
m_ordering_cache.clear();
|
||||||
|
|
||||||
// Belt printer: initialize belt-specific writer via virtual hook.
|
// Belt printer: initialize belt-specific writer via virtual hook.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include "nlohmann/json.hpp"
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <ratio>
|
#include <ratio>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -5166,5 +5167,256 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
|
|||||||
return out;
|
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<float>(), j[1].get<float>());
|
||||||
|
}
|
||||||
|
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<float>(), j[1].get<float>(), j[2].get<float>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<class T> 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<T>();
|
||||||
|
} 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<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &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<TextureDisplacementLayer> &layers,
|
||||||
|
TextureDisplacementOptions &options)
|
||||||
|
{
|
||||||
|
nlohmann::json root;
|
||||||
|
try {
|
||||||
|
root = nlohmann::json::parse(text);
|
||||||
|
} catch (...) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!root.is_object())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
TextureDisplacementOptions out_options;
|
||||||
|
std::vector<TextureDisplacementLayer> out_layers;
|
||||||
|
// Past the parse, a value of the wrong type still throws: get<int>() on a string, get<float>() on
|
||||||
|
// null. Nothing between here and the project load catches that, so one bad number would fail the
|
||||||
|
// whole file instead of this one block. Report it malformed and let the volume load without it.
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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<float>();
|
||||||
|
|
||||||
|
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<int>(), (*v)[i + 1].get<int>());
|
||||||
|
|
||||||
|
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<float>(), e[1].get<float>());
|
||||||
|
island.rotation_deg = e[2].get<float>();
|
||||||
|
island.scale = e[3].get<float>();
|
||||||
|
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<int>(), Vec2f(e[1].get<float>(), e[2].get<float>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
out_layers.push_back(std::move(l));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers = std::move(out_layers);
|
||||||
|
options = out_options;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -397,14 +397,38 @@ struct TextureDisplacementOptions
|
|||||||
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
||||||
int color_despeckle = 2;
|
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<class Archive> void serialize(Archive &ar)
|
template<class Archive> void serialize(Archive &ar)
|
||||||
{
|
{
|
||||||
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
||||||
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
|
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<TextureDisplacementLayer> &layers,
|
||||||
|
const TextureDisplacementOptions &options);
|
||||||
|
// Returns false and leaves both outputs untouched when the text is not valid JSON.
|
||||||
|
bool texture_displacement_layers_from_json(const std::string &text,
|
||||||
|
std::vector<TextureDisplacementLayer> &layers,
|
||||||
|
TextureDisplacementOptions &options);
|
||||||
|
|
||||||
// How much detail a height texture carries: central differences of the grey image, the mean gradient
|
// 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
|
// 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.
|
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <wx/checklst.h>
|
#include <wx/checklst.h>
|
||||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||||
|
#include "slic3r/GUI/Widgets/DropDown.hpp"
|
||||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include "slic3r/GUI/wxExtensions.hpp"
|
#include "slic3r/GUI/wxExtensions.hpp"
|
||||||
@@ -172,7 +173,26 @@ void AMSMaterialsSetting::create()
|
|||||||
|
|
||||||
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
||||||
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, 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);
|
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);
|
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);
|
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);
|
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
|
||||||
|
|
||||||
@@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
|
|||||||
AMSMaterialsSetting::~AMSMaterialsSetting()
|
AMSMaterialsSetting::~AMSMaterialsSetting()
|
||||||
{
|
{
|
||||||
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
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);
|
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;
|
std::string selected_ams_id;
|
||||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||||
if (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++) {
|
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
|
||||||
std::string filament_id = filament_item.filament_id;
|
|
||||||
if (it->filament_id.compare(filament_id) == 0) {
|
|
||||||
selected_ams_id = it->filament_id;
|
selected_ams_id = it->filament_id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
|||||||
ams_setting_id = "";
|
ams_setting_id = "";
|
||||||
|
|
||||||
// the combobox item
|
// 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)
|
// check filament info (rack-gated per-nozzle blacklist evaluation)
|
||||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||||
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
|
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()) {
|
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
|
||||||
wxString info_msg;
|
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();
|
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<std::string> AMSMaterialsSetting::load_favorite_filament_ids() const
|
||||||
|
{
|
||||||
|
std::set<std::string> favorite_ids;
|
||||||
|
std::vector<std::string> 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<std::string>& favorite_ids)
|
||||||
|
{
|
||||||
|
std::vector<std::string> 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<DropDown::Item> 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<std::string> favorite_ids = load_favorite_filament_ids();
|
||||||
|
std::vector<wxString> 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<std::string> 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<std::string> 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)
|
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
|
||||||
{
|
{
|
||||||
if (!obj) return;
|
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_k_val->GetTextCtrl()->SetValue(k);
|
||||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||||
|
|
||||||
wxArrayString filament_items;
|
|
||||||
wxString bambu_filament_name;
|
wxString bambu_filament_name;
|
||||||
wxString hint_filament_name; // the hint type to be selected
|
|
||||||
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
|
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
|
||||||
std::unordered_map<wxString, wxString> query_filament_types; //
|
std::unordered_map<wxString, wxString> query_filament_types; //
|
||||||
|
|
||||||
std::set<std::string> filament_id_set;
|
std::set<std::string> 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
|
// 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.
|
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
|
||||||
map_filament_items.clear();
|
map_filament_items.clear();
|
||||||
@@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
|||||||
if (alias.empty())
|
if (alias.empty())
|
||||||
continue;
|
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);
|
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||||
|
|
||||||
FilamentInfos filament_infos;
|
FilamentInfos filament_infos;
|
||||||
@@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
|||||||
map_filament_items[alias] = filament_infos;
|
map_filament_items[alias] = filament_infos;
|
||||||
|
|
||||||
if (filament_it->filament_id == ams_filament_id) {
|
if (filament_it->filament_id == ams_filament_id) {
|
||||||
hint_filament_name = from_u8(alias);
|
|
||||||
bambu_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;
|
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
|
rebuild_filament_dropdown_items(ams_filament_id);
|
||||||
int selection_idx = -1;
|
int selection_idx = m_comboBox_filament->GetSelection();
|
||||||
{
|
|
||||||
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);
|
|
||||||
post_select_event(selection_idx);
|
post_select_event(selection_idx);
|
||||||
|
|
||||||
if (selection_idx < 0) {
|
if (selection_idx < 0) {
|
||||||
@@ -1289,6 +1420,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
|||||||
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
||||||
|
|
||||||
m_filament_type = "";
|
m_filament_type = "";
|
||||||
|
const FilamentInfos* selected_info = selected_filament_info();
|
||||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||||
if (preset_bundle) {
|
if (preset_bundle) {
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
@@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
|||||||
std::string nozzle_diameter_str = stream.str();
|
std::string nozzle_diameter_str = stream.str();
|
||||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
// 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.
|
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
if (selected_info) {
|
||||||
if (!selected.empty()) {
|
const std::string& filament_id = selected_info->filament_id;
|
||||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
|
||||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
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)) {
|
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||||
if (it->filament_id != filament_id)
|
if (it->filament_id != filament_id)
|
||||||
@@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
|||||||
//reset cali
|
//reset cali
|
||||||
int cali_select_idx = -1;
|
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_k_val->Enable(false);
|
||||||
m_input_n_val->Enable(false);
|
m_input_n_val->Enable(false);
|
||||||
m_button_confirm->Disable(); // ORCA No need to change style
|
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_filament_id = "";
|
||||||
ams_setting_id = "";
|
ams_setting_id = "";
|
||||||
|
|
||||||
if (preset_bundle) {
|
if (selected_info) {
|
||||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
ams_filament_id = selected_info->filament_id;
|
||||||
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
|
ams_setting_id = selected_info->setting_id;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wxArrayString items;
|
wxArrayString items;
|
||||||
@@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
|||||||
m_button_reset->Rescale(); // ORCA
|
m_button_reset->Rescale(); // ORCA
|
||||||
m_button_confirm->Rescale(); // ORCA
|
m_button_confirm->Rescale(); // ORCA
|
||||||
m_button_close->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();
|
this->Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
||||||
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
||||||
@@ -156,6 +157,7 @@ protected:
|
|||||||
void create_panel_kn(wxWindow* parent);
|
void create_panel_kn(wxWindow* parent);
|
||||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||||
void on_select_filament(wxCommandEvent& evt);
|
void on_select_filament(wxCommandEvent& evt);
|
||||||
|
void on_toggle_favorite_filament(wxCommandEvent& evt);
|
||||||
void on_select_cali_result(wxCommandEvent &evt);
|
void on_select_cali_result(wxCommandEvent &evt);
|
||||||
void on_select_ok(wxCommandEvent &event);
|
void on_select_ok(wxCommandEvent &event);
|
||||||
void on_select_reset(wxCommandEvent &event);
|
void on_select_reset(wxCommandEvent &event);
|
||||||
@@ -163,6 +165,11 @@ protected:
|
|||||||
void on_clr_picker(wxMouseEvent &event);
|
void on_clr_picker(wxMouseEvent &event);
|
||||||
bool is_virtual_tray();
|
bool is_virtual_tray();
|
||||||
void update_widgets();
|
void update_widgets();
|
||||||
|
void rebuild_filament_dropdown_items(const std::string& selected_filament_id);
|
||||||
|
void update_favorite_stars();
|
||||||
|
std::set<std::string> load_favorite_filament_ids() const;
|
||||||
|
void save_favorite_filament_ids(const std::set<std::string>& favorite_ids);
|
||||||
|
void toggle_favorite_filament(const std::string& filament_id);
|
||||||
|
|
||||||
void update_filament_editing(bool is_printing);
|
void update_filament_editing(bool is_printing);
|
||||||
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
|
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
|
||||||
@@ -202,12 +209,7 @@ protected:
|
|||||||
|
|
||||||
int m_pa_cali_select_id = 0;
|
int m_pa_cali_select_id = 0;
|
||||||
|
|
||||||
// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there)
|
|
||||||
#ifdef __APPLE__
|
|
||||||
wxComboBox *m_comboBox_filament;
|
|
||||||
#else
|
|
||||||
ComboBox *m_comboBox_filament;
|
ComboBox *m_comboBox_filament;
|
||||||
#endif
|
|
||||||
ComboBox * m_comboBox_cali_result;
|
ComboBox * m_comboBox_cali_result;
|
||||||
TextInput* m_readonly_filament;
|
TextInput* m_readonly_filament;
|
||||||
|
|
||||||
@@ -215,7 +217,20 @@ protected:
|
|||||||
std::string filament_id;
|
std::string filament_id;
|
||||||
std::string setting_id;
|
std::string setting_id;
|
||||||
};
|
};
|
||||||
std::map<std::string, FilamentInfos> map_filament_items;
|
struct FilamentListItem {
|
||||||
|
FilamentInfos filament;
|
||||||
|
bool is_filament{false};
|
||||||
|
};
|
||||||
|
const FilamentInfos* selected_filament_info() const;
|
||||||
|
|
||||||
|
// Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back.
|
||||||
|
std::map<wxString, FilamentInfos> map_filament_items;
|
||||||
|
std::vector<wxString> m_sorted_filament_items;
|
||||||
|
std::vector<FilamentListItem> m_filament_list_items;
|
||||||
|
ScalableBitmap m_star_filled;
|
||||||
|
ScalableBitmap m_star_empty;
|
||||||
|
// Favorites changed while the list was open; their rows are regrouped before it opens again.
|
||||||
|
bool m_favorites_regroup{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
|
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
|
||||||
|
|||||||
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
|||||||
int obj_idx, vol_idx;
|
int obj_idx, vol_idx;
|
||||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||||
if (obj_idx != -1) {
|
if (obj_idx != -1) {
|
||||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||||
cnv->reload_scene(true, true);
|
cnv->reload_scene(true, true);
|
||||||
update_info_items(obj_idx);
|
update_info_items(obj_idx);
|
||||||
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
|||||||
GLCanvas3D* cnv = plater->canvas3D();
|
GLCanvas3D* cnv = plater->canvas3D();
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case InfoItemType::CustomSupports:
|
case InfoItemType::CustomSupports: {
|
||||||
cnv->get_gizmos_manager().reset_all_states();
|
cnv->get_gizmos_manager().reset_all_states();
|
||||||
Plater::TakeSnapshot(plater, "Remove support painting");
|
Plater::TakeSnapshot snapshot(plater, "Remove support painting");
|
||||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||||
mv->supported_facets.reset();
|
mv->supported_facets.reset();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// BBS: remove CustomSeam
|
// BBS: remove CustomSeam
|
||||||
|
|
||||||
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case InfoItemType::MmSegmentation:
|
case InfoItemType::MmSegmentation: {
|
||||||
cnv->get_gizmos_manager().reset_all_states();
|
cnv->get_gizmos_manager().reset_all_states();
|
||||||
Plater::TakeSnapshot(plater, "Remove color painting");
|
Plater::TakeSnapshot snapshot(plater, "Remove color painting");
|
||||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||||
mv->mmu_segmentation_facets.reset();
|
mv->mmu_segmentation_facets.reset();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case InfoItemType::FuzzySkin:
|
case InfoItemType::FuzzySkin: {
|
||||||
cnv->get_gizmos_manager().reset_all_states();
|
cnv->get_gizmos_manager().reset_all_states();
|
||||||
Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
||||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||||
mv->fuzzy_skin_facets.reset();
|
mv->fuzzy_skin_facets.reset();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// BBS: remove Sinking
|
// BBS: remove Sinking
|
||||||
case InfoItemType::Undef : assert(false); break;
|
case InfoItemType::Undef : assert(false); break;
|
||||||
@@ -6718,7 +6721,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
|||||||
else if (col == colSinking) {
|
else if (col == colSinking) {
|
||||||
Plater * plater = wxGetApp().plater();
|
Plater * plater = wxGetApp().plater();
|
||||||
GLCanvas3D *cnv = plater->canvas3D();
|
GLCanvas3D *cnv = plater->canvas3D();
|
||||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||||
int obj_idx, vol_idx;
|
int obj_idx, vol_idx;
|
||||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||||
|
|||||||
@@ -3558,6 +3558,7 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const
|
|||||||
|
|
||||||
void GLGizmoTextureDisplacement::update_model_object()
|
void GLGizmoTextureDisplacement::update_model_object()
|
||||||
{
|
{
|
||||||
|
store_panel_state();
|
||||||
bool updated = false;
|
bool updated = false;
|
||||||
ModelObject *mo = m_c->selection_info()->model_object();
|
ModelObject *mo = m_c->selection_info()->model_object();
|
||||||
int idx = -1;
|
int idx = -1;
|
||||||
@@ -3593,6 +3594,24 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
|||||||
{
|
{
|
||||||
wxBusyCursor wait;
|
wxBusyCursor wait;
|
||||||
|
|
||||||
|
// Opening the gizmo puts the panel back where it was left: the layer and the mode come off the
|
||||||
|
// volume, because they are the user's place in this model's stack, and the dock state off the
|
||||||
|
// application config, because it is about their window. Only on open - this also runs on an
|
||||||
|
// undo/redo and on a layer switch, where overriding what the user is doing now would be wrong.
|
||||||
|
if (first_update) {
|
||||||
|
if (const ModelVolume *mv = texture_volume(); mv != nullptr) {
|
||||||
|
const TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||||
|
m_panel_mode = std::clamp(opts.panel_mode, 0, 1);
|
||||||
|
// Only if that slot still holds a layer: the stack can have been edited by anything that
|
||||||
|
// does not go through the panel, and a selection pointing at nothing shows an empty panel.
|
||||||
|
const auto &stack = mv->texture_displacement_layers;
|
||||||
|
if (std::any_of(stack.begin(), stack.end(),
|
||||||
|
[&opts](const TextureDisplacementLayer &l) { return l.slot == opts.active_slot; }))
|
||||||
|
m_active_layer_slot = opts.active_slot;
|
||||||
|
}
|
||||||
|
m_undocked = wxGetApp().app_config->get_bool(UNDOCKED_CONFIG_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
const ModelObject *mo = m_c->selection_info()->model_object();
|
const ModelObject *mo = m_c->selection_info()->model_object();
|
||||||
m_triangle_selectors.clear();
|
m_triangle_selectors.clear();
|
||||||
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
|
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
|
||||||
@@ -3641,6 +3660,14 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
|||||||
rebuild_preview();
|
rebuild_preview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLGizmoTextureDisplacement::store_panel_state()
|
||||||
|
{
|
||||||
|
if (ModelVolume *mv = texture_volume(); mv != nullptr) {
|
||||||
|
mv->texture_displacement_options.active_slot = m_active_layer_slot;
|
||||||
|
mv->texture_displacement_options.panel_mode = m_panel_mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::set_active_layer(int slot)
|
void GLGizmoTextureDisplacement::set_active_layer(int slot)
|
||||||
{
|
{
|
||||||
if (slot == m_active_layer_slot)
|
if (slot == m_active_layer_slot)
|
||||||
@@ -3649,6 +3676,7 @@ void GLGizmoTextureDisplacement::set_active_layer(int slot)
|
|||||||
// reflect - otherwise they would be silently lost.
|
// reflect - otherwise they would be silently lost.
|
||||||
update_model_object();
|
update_model_object();
|
||||||
m_active_layer_slot = slot;
|
m_active_layer_slot = slot;
|
||||||
|
store_panel_state();
|
||||||
update_from_model_object(false);
|
update_from_model_object(false);
|
||||||
// The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync
|
// The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync
|
||||||
// instead of leaving it pointing at the previous layer's (now stale) paint patch.
|
// instead of leaving it pointing at the previous layer's (now stale) paint patch.
|
||||||
@@ -5986,6 +6014,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
"not what you wanted and you know why."));
|
"not what you wanted and you know why."));
|
||||||
if (clicked && !on) {
|
if (clicked && !on) {
|
||||||
m_panel_mode = m;
|
m_panel_mode = m;
|
||||||
|
store_panel_state();
|
||||||
if (!pro_mode()) {
|
if (!pro_mode()) {
|
||||||
// Leaving the subdivision preview open would strand a wireframe whose controls just
|
// Leaving the subdivision preview open would strand a wireframe whose controls just
|
||||||
// disappeared, so close it as part of the switch.
|
// disappeared, so close it as part of the switch.
|
||||||
@@ -6008,6 +6037,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside "
|
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside "
|
||||||
"the toolbar.")))
|
"the toolbar.")))
|
||||||
m_undocked = !m_undocked;
|
m_undocked = !m_undocked;
|
||||||
|
wxGetApp().app_config->set_bool(UNDOCKED_CONFIG_KEY, m_undocked);
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ private:
|
|||||||
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
|
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
|
||||||
// changed something, so the caller can invalidate the preview.
|
// changed something, so the caller can invalidate the preview.
|
||||||
bool apply_standard_mode_presets(ModelVolume *mv);
|
bool apply_standard_mode_presets(ModelVolume *mv);
|
||||||
|
// Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file
|
||||||
|
// can carry it. Cheap and idempotent - called wherever either of the two changes.
|
||||||
|
void store_panel_state();
|
||||||
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
||||||
// The order matters and is the whole reason this is one button - a height map can only move
|
// The order matters and is the whole reason this is one button - a height map can only move
|
||||||
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
||||||
@@ -971,9 +974,12 @@ private:
|
|||||||
PatchUnwrap m_uv_editor_unwrap;
|
PatchUnwrap m_uv_editor_unwrap;
|
||||||
|
|
||||||
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
|
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
|
||||||
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
|
// grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application
|
||||||
// open/close within a session, so the choice sticks while working.
|
// config rather than in the project: it describes the user's window, not the model, so opening
|
||||||
|
// someone else's project must not move their panel.
|
||||||
bool m_undocked = false;
|
bool m_undocked = false;
|
||||||
|
// The config key behind m_undocked.
|
||||||
|
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
|
||||||
|
|
||||||
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
|
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
|
||||||
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
|
|||||||
e.SetId(GetId());
|
e.SetId(GetId());
|
||||||
GetEventHandler()->ProcessEvent(e);
|
GetEventHandler()->ProcessEvent(e);
|
||||||
});
|
});
|
||||||
|
drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||||
|
e.SetEventObject(this);
|
||||||
|
e.SetId(GetId());
|
||||||
|
GetEventHandler()->ProcessEvent(e);
|
||||||
|
});
|
||||||
drop.Bind(EVT_DISMISS, [this](auto &) {
|
drop.Bind(EVT_DISMISS, [this](auto &) {
|
||||||
drop_down = false;
|
drop_down = false;
|
||||||
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
||||||
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
|
|||||||
int style)
|
int style)
|
||||||
{
|
{
|
||||||
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
|
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
|
||||||
item.style = style;
|
item.style = style;
|
||||||
items.push_back(item);
|
items.push_back(item);
|
||||||
SetClientDataType(wxClientData_Void);
|
SetClientDataType(wxClientData_Void);
|
||||||
@@ -297,6 +302,12 @@ void ComboBox::ForceDropdownOpen()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComboBox::CloseDropdown()
|
||||||
|
{
|
||||||
|
if (drop.IsShown())
|
||||||
|
drop.DismissAndNotify();
|
||||||
|
}
|
||||||
|
|
||||||
unsigned int ComboBox::GetCount() const { return items.size(); }
|
unsigned int ComboBox::GetCount() const { return items.size(); }
|
||||||
|
|
||||||
void ComboBox::set_replace_text(wxString text, wxString image_name)
|
void ComboBox::set_replace_text(wxString text, wxString image_name)
|
||||||
@@ -350,6 +361,13 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
|
|||||||
drop.Invalidate();
|
drop.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap)
|
||||||
|
{
|
||||||
|
if (n >= items.size()) return;
|
||||||
|
items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||||
|
drop.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||||
unsigned int pos,
|
unsigned int pos,
|
||||||
void ** clientData,
|
void ** clientData,
|
||||||
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
|||||||
{
|
{
|
||||||
if (pos > this->items.size()) return -1;
|
if (pos > this->items.size()) return -1;
|
||||||
for (int i = 0; i < items.GetCount(); ++i) {
|
for (int i = 0; i < items.GetCount(); ++i) {
|
||||||
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||||
this->items.insert(this->items.begin() + pos, item);
|
this->items.insert(this->items.begin() + pos, item);
|
||||||
++pos;
|
++pos;
|
||||||
}
|
}
|
||||||
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
|
|||||||
case WXK_UP:
|
case WXK_UP:
|
||||||
case WXK_DOWN:
|
case WXK_DOWN:
|
||||||
case WXK_LEFT:
|
case WXK_LEFT:
|
||||||
case WXK_RIGHT:
|
case WXK_RIGHT: {
|
||||||
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
|
// Step over disabled rows: the mouse cannot select them either.
|
||||||
SetSelection(GetSelection() - 1);
|
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
|
||||||
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
|
int n = GetSelection() + step;
|
||||||
SetSelection(GetSelection() + 1);
|
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
|
||||||
} else {
|
n += step;
|
||||||
|
if (n < 0 || n >= int(items.size()))
|
||||||
break;
|
break;
|
||||||
}
|
SetSelection(n);
|
||||||
sendComboBoxEvent();
|
sendComboBoxEvent();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case WXK_TAB:
|
case WXK_TAB:
|
||||||
HandleAsNavigationKey(event);
|
HandleAsNavigationKey(event);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -96,10 +96,12 @@ public:
|
|||||||
|
|
||||||
wxBitmap GetItemBitmap(unsigned int n);
|
wxBitmap GetItemBitmap(unsigned int n);
|
||||||
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||||
|
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||||
bool is_drop_down(){return drop_down;}
|
bool is_drop_down(){return drop_down;}
|
||||||
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
||||||
|
|
||||||
void ForceDropdownOpen();
|
void ForceDropdownOpen();
|
||||||
|
void CloseDropdown();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include "slic3r/GUI/wxExtensions.hpp"
|
#include "slic3r/GUI/wxExtensions.hpp"
|
||||||
|
|
||||||
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||||
|
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||||
|
|
||||||
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
||||||
|
|
||||||
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The x-range [first, second) of the column at the right of the rows that holds the items' action
|
||||||
|
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
|
||||||
|
// clicks against it, so the drawn icon and its clickable area always match.
|
||||||
|
std::pair<int, int> DropDown::actionColumn() const
|
||||||
|
{
|
||||||
|
if (actionIconWidth <= 0)
|
||||||
|
return {0, 0};
|
||||||
|
int right = rowSize.x;
|
||||||
|
if (rowSize.y * count > GetSize().y) // position bar
|
||||||
|
right -= 6;
|
||||||
|
return {right - actionIconWidth - FromDIP(16), right};
|
||||||
|
}
|
||||||
|
|
||||||
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
|
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
|
||||||
{
|
{
|
||||||
// save dc
|
// save dc
|
||||||
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
|
|||||||
rcContent.width -= szBmp.x + 5;
|
rcContent.width -= szBmp.x + 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto action_column = actionColumn();
|
||||||
|
if (action_column.second > action_column.first)
|
||||||
|
rcContent.width = action_column.first - rcContent.x;
|
||||||
|
|
||||||
std::set<wxString> groups;
|
std::set<wxString> groups;
|
||||||
// draw texts & icons
|
// draw texts & icons
|
||||||
int index = 0;
|
int index = 0;
|
||||||
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
|
|||||||
auto text = group.IsEmpty()
|
auto text = group.IsEmpty()
|
||||||
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
||||||
: item.text;
|
: item.text;
|
||||||
|
if (item.action_icon.IsOk()) {
|
||||||
|
auto action_size = GetBmpSize(item.action_icon);
|
||||||
|
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
|
||||||
|
rcContent.y + (rcContent.height - action_size.y) / 2);
|
||||||
|
dc.DrawBitmap(item.action_icon, action_pt);
|
||||||
|
}
|
||||||
|
|
||||||
if (!text_off && !text.IsEmpty()) {
|
if (!text_off && !text.IsEmpty()) {
|
||||||
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
||||||
if (pt.x + tSize.x > rcContent.GetRight()) {
|
if (pt.x + tSize.x > rcContent.GetRight()) {
|
||||||
if (is_hover && item.tip.IsEmpty())
|
int max_text_width = rcContent.GetRight() - pt.x;
|
||||||
SetToolTip(text);
|
if (max_text_width <= 0) {
|
||||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
|
text.clear();
|
||||||
rcContent.GetRight() - pt.x);
|
} else {
|
||||||
|
if (is_hover && item.tip.IsEmpty())
|
||||||
|
SetToolTip(text);
|
||||||
|
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pt.y += (rcContent.height - textSize.y) / 2;
|
pt.y += (rcContent.height - textSize.y) / 2;
|
||||||
dc.SetFont(GetFont());
|
dc.SetFont(GetFont());
|
||||||
@@ -529,6 +558,7 @@ void DropDown::messureSize()
|
|||||||
if (!need_sync) return;
|
if (!need_sync) return;
|
||||||
textSize = wxSize();
|
textSize = wxSize();
|
||||||
iconSize = wxSize();
|
iconSize = wxSize();
|
||||||
|
actionIconWidth = 0;
|
||||||
count = 0;
|
count = 0;
|
||||||
wxClientDC dc(GetParent() ? GetParent() : this);
|
wxClientDC dc(GetParent() ? GetParent() : this);
|
||||||
dc.SetFont(GetFont());
|
dc.SetFont(GetFont());
|
||||||
@@ -565,8 +595,13 @@ void DropDown::messureSize()
|
|||||||
size1.x += size2.x + (text_off ? 0 : 5);
|
size1.x += size2.x + (text_off ? 0 : 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (item.action_icon.IsOk())
|
||||||
|
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
|
||||||
if (size1.x > textSize.x) textSize = size1;
|
if (size1.x > textSize.x) textSize = size1;
|
||||||
}
|
}
|
||||||
|
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
|
||||||
|
if (actionIconWidth > 0)
|
||||||
|
textSize.x += actionIconWidth + FromDIP(16);
|
||||||
if (!align_icon) iconSize.x = 0;
|
if (!align_icon) iconSize.x = 0;
|
||||||
wxSize szContent = textSize;
|
wxSize szContent = textSize;
|
||||||
if (szContent.x < FromDIP(120) && !use_content_width)
|
if (szContent.x < FromDIP(120) && !use_content_width)
|
||||||
@@ -636,6 +671,11 @@ void DropDown::messureSize()
|
|||||||
e.SetId(GetId());
|
e.SetId(GetId());
|
||||||
GetEventHandler()->ProcessEvent(e);
|
GetEventHandler()->ProcessEvent(e);
|
||||||
});
|
});
|
||||||
|
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||||
|
e.SetEventObject(this);
|
||||||
|
e.SetId(GetId());
|
||||||
|
GetEventHandler()->ProcessEvent(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
need_sync = false;
|
need_sync = false;
|
||||||
}
|
}
|
||||||
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
|
|||||||
if (hover_item < 0)
|
if (hover_item < 0)
|
||||||
return;
|
return;
|
||||||
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
|
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
|
||||||
|
int index = hoverIndex();
|
||||||
|
if (index >= 0 && items[index].action_icon.IsOk()) {
|
||||||
|
const auto action_column = actionColumn();
|
||||||
|
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
|
||||||
|
sendDropDownActionEvent(index);
|
||||||
|
paintNow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
sendDropDownEvent();
|
sendDropDownEvent();
|
||||||
if (mainDropDown)
|
if (mainDropDown)
|
||||||
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
||||||
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
|
|||||||
GetEventHandler()->ProcessEvent(event);
|
GetEventHandler()->ProcessEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DropDown::sendDropDownActionEvent(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
|
||||||
|
return;
|
||||||
|
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
|
||||||
|
event.SetEventObject(this);
|
||||||
|
event.SetInt(index);
|
||||||
|
event.SetString(items[index].text);
|
||||||
|
GetEventHandler()->ProcessEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
void DropDown::Dismiss()
|
void DropDown::Dismiss()
|
||||||
{
|
{
|
||||||
if (subDropDown && subDropDown->IsShown())
|
if (subDropDown && subDropDown->IsShown())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <utility>
|
||||||
#include <wx/gdicmn.h>
|
#include <wx/gdicmn.h>
|
||||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||||
#include <boost/date_time/posix_time/ptime.hpp>
|
#include <boost/date_time/posix_time/ptime.hpp>
|
||||||
@@ -27,6 +28,7 @@ class wxWindow;
|
|||||||
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
|
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
|
||||||
|
|
||||||
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||||
|
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||||
|
|
||||||
class DropDown : public PopupWindow
|
class DropDown : public PopupWindow
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,7 @@ public:
|
|||||||
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
||||||
wxBitmap icon;
|
wxBitmap icon;
|
||||||
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
||||||
|
wxBitmap action_icon;
|
||||||
void * data{nullptr};
|
void * data{nullptr};
|
||||||
wxString group_key{};
|
wxString group_key{};
|
||||||
wxString group_label{};
|
wxString group_label{};
|
||||||
@@ -65,6 +68,7 @@ private:
|
|||||||
|
|
||||||
wxSize textSize;
|
wxSize textSize;
|
||||||
wxSize iconSize;
|
wxSize iconSize;
|
||||||
|
int actionIconWidth = 0;
|
||||||
wxSize rowSize;
|
wxSize rowSize;
|
||||||
|
|
||||||
StateHandler state_handler;
|
StateHandler state_handler;
|
||||||
@@ -135,6 +139,7 @@ private:
|
|||||||
int hoverIndex();
|
int hoverIndex();
|
||||||
|
|
||||||
int selectedItem();
|
int selectedItem();
|
||||||
|
std::pair<int, int> actionColumn() const;
|
||||||
|
|
||||||
friend class ComboBox;
|
friend class ComboBox;
|
||||||
void messureSize();
|
void messureSize();
|
||||||
@@ -148,6 +153,7 @@ private:
|
|||||||
void mouseWheelMoved(wxMouseEvent &event);
|
void mouseWheelMoved(wxMouseEvent &event);
|
||||||
|
|
||||||
void sendDropDownEvent();
|
void sendDropDownEvent();
|
||||||
|
void sendDropDownActionEvent(int index);
|
||||||
|
|
||||||
|
|
||||||
DECLARE_EVENT_TABLE()
|
DECLARE_EVENT_TABLE()
|
||||||
|
|||||||
+363
-13
@@ -28,9 +28,12 @@
|
|||||||
#include <miniz.h>
|
#include <miniz.h>
|
||||||
#include "libslic3r/TriangleMesh.hpp"
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
#include "libslic3r/TriangleSelector.hpp"
|
#include "libslic3r/TriangleSelector.hpp"
|
||||||
|
#include "libslic3r/TextureDisplacement.hpp"
|
||||||
|
#include "libslic3r/PNGReadWrite.hpp"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <ios>
|
#include <ios>
|
||||||
|
#include <iterator>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
#include <boost/filesystem/operations.hpp>
|
#include <boost/filesystem/operations.hpp>
|
||||||
@@ -349,6 +352,24 @@ static bool replace_in_3mf_entry(const std::string& path, const std::string& suf
|
|||||||
return replaced;
|
return replaced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Writes `model` out as a one-plate project, which is what the application's Save does with it.
|
||||||
|
static void store_project(const std::string& path, Model& model)
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir backup_dir("orca_project_src");
|
||||||
|
model.set_backup_path(backup_dir.string());
|
||||||
|
|
||||||
|
DynamicPrintConfig cfg;
|
||||||
|
PlateData plate;
|
||||||
|
plate.plate_index = 0;
|
||||||
|
StoreParams sp;
|
||||||
|
sp.path = path.c_str();
|
||||||
|
sp.model = &model;
|
||||||
|
sp.config = &cfg;
|
||||||
|
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||||
|
sp.plate_data_list.push_back(&plate);
|
||||||
|
REQUIRE(store_bbs_3mf(sp));
|
||||||
|
}
|
||||||
|
|
||||||
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
|
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
|
||||||
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
|
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
|
||||||
static void store_painted_cube(const std::string& path)
|
static void store_painted_cube(const std::string& path)
|
||||||
@@ -363,19 +384,7 @@ static void store_painted_cube(const std::string& path)
|
|||||||
selector.set_facet(1, EnforcerBlockerType::Extruder3);
|
selector.set_facet(1, EnforcerBlockerType::Extruder3);
|
||||||
REQUIRE(volume->mmu_segmentation_facets.set(selector));
|
REQUIRE(volume->mmu_segmentation_facets.set(selector));
|
||||||
}
|
}
|
||||||
ScopedTemporaryDir backup_dir("orca_paint_src");
|
store_project(path, model);
|
||||||
model.set_backup_path(backup_dir.string());
|
|
||||||
|
|
||||||
DynamicPrintConfig cfg;
|
|
||||||
PlateData plate;
|
|
||||||
plate.plate_index = 0;
|
|
||||||
StoreParams sp;
|
|
||||||
sp.path = path.c_str();
|
|
||||||
sp.model = &model;
|
|
||||||
sp.config = &cfg;
|
|
||||||
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
|
||||||
sp.plate_data_list.push_back(&plate);
|
|
||||||
REQUIRE(store_bbs_3mf(sp));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
|
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
|
||||||
@@ -2043,3 +2052,344 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[
|
|||||||
CHECK_FALSE(parser.check_3mf_from_prusa(path));
|
CHECK_FALSE(parser.check_3mf_from_prusa(path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern file paths", "[3mf][Regression]")
|
||||||
|
{
|
||||||
|
Model model;
|
||||||
|
REQUIRE(load_stl((std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl").c_str(), &model));
|
||||||
|
model.add_default_instances();
|
||||||
|
ScopedTemporaryDir backup_dir("orca_pick");
|
||||||
|
model.set_backup_path(backup_dir.string());
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
|
||||||
|
auto plate = std::make_unique<PlateData>();
|
||||||
|
plate->plate_index = 0;
|
||||||
|
plate->pick_file = (backup_dir.path() / "pick.png").string();
|
||||||
|
plate->pattern_bbox_file = (backup_dir.path() / "plate.json").string();
|
||||||
|
|
||||||
|
ScopedTemporaryFile temp(".3mf");
|
||||||
|
const std::string test_file = temp.string();
|
||||||
|
StoreParams store_params;
|
||||||
|
store_params.path = test_file.c_str();
|
||||||
|
store_params.model = &model;
|
||||||
|
store_params.config = &config;
|
||||||
|
store_params.plate_data_list.push_back(plate.get());
|
||||||
|
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||||
|
REQUIRE(store_bbs_3mf(store_params));
|
||||||
|
|
||||||
|
boost::nowide::ifstream in(test_file, std::ios::binary);
|
||||||
|
Model dst_model;
|
||||||
|
DynamicPrintConfig dst_config;
|
||||||
|
PlateDataPtrs dst_plates;
|
||||||
|
Semver file_version;
|
||||||
|
dst_model.set_backup_path(backup_dir.string());
|
||||||
|
REQUIRE(load_gcode_3mf_from_stream(in, &dst_config, &dst_model, &dst_plates, &file_version));
|
||||||
|
REQUIRE(dst_plates.size() == 1);
|
||||||
|
CHECK(dst_plates.front()->pick_file == "Metadata/pick_1.png");
|
||||||
|
CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json");
|
||||||
|
release_PlateData_list(dst_plates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encodes a flat grayscale PNG the way the texture panel's import does, so what the archive carries
|
||||||
|
// is a real texture rather than an opaque blob.
|
||||||
|
static std::shared_ptr<std::vector<unsigned char>> make_gray_png(uint8_t value, size_t w = 4, size_t h = 4)
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile file(".png");
|
||||||
|
const std::vector<uint8_t> pixels(w * h, value);
|
||||||
|
REQUIRE(png::write_gray_to_file(file.string(), w, h, pixels));
|
||||||
|
|
||||||
|
boost::nowide::ifstream in(file.string(), std::ios::binary);
|
||||||
|
std::vector<unsigned char> bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||||
|
REQUIRE_FALSE(bytes.empty());
|
||||||
|
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every entry name in a 3mf archive, with the separators normalised the way the importer sees them.
|
||||||
|
static std::vector<std::string> zip_entry_names(const std::string& path)
|
||||||
|
{
|
||||||
|
std::vector<std::string> names;
|
||||||
|
mz_zip_archive zip;
|
||||||
|
mz_zip_zero_struct(&zip);
|
||||||
|
REQUIRE(open_zip_reader(&zip, path));
|
||||||
|
const mz_uint n = mz_zip_reader_get_num_files(&zip);
|
||||||
|
for (mz_uint i = 0; i < n; ++i) {
|
||||||
|
mz_zip_archive_file_stat st;
|
||||||
|
if (!mz_zip_reader_file_stat(&zip, i, &st) || st.m_is_directory)
|
||||||
|
continue;
|
||||||
|
std::string name(st.m_filename);
|
||||||
|
std::replace(name.begin(), name.end(), '\\', '/');
|
||||||
|
names.push_back(std::move(name));
|
||||||
|
}
|
||||||
|
close_zip_reader(&zip);
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::string TEXTURE_DISPLACEMENT_ENTRY_DIR = "Metadata/texture_displacement/";
|
||||||
|
|
||||||
|
// Stores a one-plate project holding a cube with one texture displacement layer, painted on that
|
||||||
|
// layer's own slot. The caller fills the layer in, and gets back the mask string the exporter wrote
|
||||||
|
// for `painted_facet`, which is what the load has to reproduce.
|
||||||
|
//
|
||||||
|
// The painted facet is deliberately not the first one: the importer keeps a slot's masks only as far
|
||||||
|
// as its last painted triangle, so a facet in the middle is the case where that run has to be padded
|
||||||
|
// in front and left short at the back.
|
||||||
|
static void store_texture_displaced_cube(const std::string& path, const TextureDisplacementLayer& layer,
|
||||||
|
const TextureDisplacementOptions& options, std::string& painted_mask,
|
||||||
|
int painted_facet = 5)
|
||||||
|
{
|
||||||
|
Model model;
|
||||||
|
ModelObject* object = model.add_object();
|
||||||
|
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
||||||
|
object->add_instance();
|
||||||
|
|
||||||
|
volume->texture_displacement_layers.push_back(layer);
|
||||||
|
volume->texture_displacement_options = options;
|
||||||
|
REQUIRE(painted_facet < int(volume->mesh().its.indices.size()));
|
||||||
|
{
|
||||||
|
TriangleSelector selector(volume->mesh());
|
||||||
|
selector.set_facet(painted_facet, EnforcerBlockerType::ENFORCER);
|
||||||
|
REQUIRE(volume->texture_displacement_facet(layer.slot).set(selector));
|
||||||
|
}
|
||||||
|
painted_mask = volume->texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facet);
|
||||||
|
REQUIRE_FALSE(painted_mask.empty());
|
||||||
|
|
||||||
|
store_project(path, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the only volume of the only object in the project at `path`, loaded through the importer.
|
||||||
|
static const ModelVolume& load_single_volume(const std::string& path, Model& model, const ScopedTemporaryDir& backup_dir)
|
||||||
|
{
|
||||||
|
REQUIRE(load_project(path, model, backup_dir));
|
||||||
|
REQUIRE(model.objects.size() == 1);
|
||||||
|
REQUIRE(model.objects.front()->volumes.size() == 1);
|
||||||
|
return *model.objects.front()->volumes.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A texture displacement stack is the one paint tool whose settings live outside the volume's config:
|
||||||
|
// the layer stack goes into the archive as its own JSON file, each layer's texture as a PNG beside it,
|
||||||
|
// and the paint as a per-triangle attribute (see add_texture_displacement). A project only reopens
|
||||||
|
// with its relief intact if all three come back together.
|
||||||
|
TEST_CASE("A texture displacement layer, its texture and its paint survive a project round trip", "[3mf][TextureDisplacement]")
|
||||||
|
{
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.slot = 3; // not the first slot, so the per-slot attribute names have to line up
|
||||||
|
layer.name = "Bark";
|
||||||
|
layer.path = "/textures/bark.png";
|
||||||
|
layer.depth_mm = 1.25f;
|
||||||
|
layer.tiling_scale = 7.5f;
|
||||||
|
layer.midlevel = 0.5f;
|
||||||
|
layer.invert = true;
|
||||||
|
layer.tile_method = TextureTileMethod::MirroredRepeat;
|
||||||
|
layer.projection_method = TextureProjectionMethod::LSCM;
|
||||||
|
layer.blend_mode = TextureBlendMode::Subtract;
|
||||||
|
layer.lscm_seam_edges = { { 1, 2 }, { 3, 5 } };
|
||||||
|
layer.image_data = make_gray_png(128);
|
||||||
|
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
options.smooth_enabled = true;
|
||||||
|
options.smooth_iterations = 4;
|
||||||
|
options.active_slot = layer.slot;
|
||||||
|
options.panel_mode = 1;
|
||||||
|
|
||||||
|
const int painted_facet = 5;
|
||||||
|
std::string painted;
|
||||||
|
ScopedTemporaryFile temp(".3mf");
|
||||||
|
store_texture_displaced_cube(temp.string(), layer, options, painted, painted_facet);
|
||||||
|
|
||||||
|
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||||
|
Model model;
|
||||||
|
const ModelVolume& volume = load_single_volume(temp.string(), model, backup_dir);
|
||||||
|
|
||||||
|
REQUIRE(volume.texture_displacement_layers.size() == 1);
|
||||||
|
const TextureDisplacementLayer& read = volume.texture_displacement_layers.front();
|
||||||
|
CHECK(read.slot == layer.slot);
|
||||||
|
CHECK(read.name == layer.name);
|
||||||
|
CHECK(read.path == layer.path);
|
||||||
|
CHECK_THAT(read.depth_mm, Catch::Matchers::WithinAbs(layer.depth_mm, 1e-6f));
|
||||||
|
CHECK_THAT(read.tiling_scale, Catch::Matchers::WithinAbs(layer.tiling_scale, 1e-6f));
|
||||||
|
CHECK_THAT(read.midlevel, Catch::Matchers::WithinAbs(layer.midlevel, 1e-6f));
|
||||||
|
CHECK(read.invert == layer.invert);
|
||||||
|
CHECK(read.tile_method == layer.tile_method);
|
||||||
|
CHECK(read.projection_method == layer.projection_method);
|
||||||
|
CHECK(read.blend_mode == layer.blend_mode);
|
||||||
|
CHECK(read.lscm_seam_edges == layer.lscm_seam_edges);
|
||||||
|
|
||||||
|
// The texture rides along in the archive, named after the slot it belongs to, byte for byte.
|
||||||
|
CHECK(boost::algorithm::starts_with(read.path_in_3mf, TEXTURE_DISPLACEMENT_ENTRY_DIR));
|
||||||
|
CHECK(boost::algorithm::ends_with(read.path_in_3mf, "_" + std::to_string(layer.slot) + ".png"));
|
||||||
|
REQUIRE(read.image_data);
|
||||||
|
CHECK(*read.image_data == *layer.image_data);
|
||||||
|
|
||||||
|
CHECK(volume.texture_displacement_options.smooth_enabled);
|
||||||
|
CHECK(volume.texture_displacement_options.smooth_iterations == options.smooth_iterations);
|
||||||
|
CHECK(volume.texture_displacement_options.active_slot == options.active_slot);
|
||||||
|
CHECK(volume.texture_displacement_options.panel_mode == options.panel_mode);
|
||||||
|
|
||||||
|
// The paint comes back on the facet it was put on, on its own slot and nowhere else.
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facet) == painted);
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(0).empty());
|
||||||
|
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||||
|
if (slot != layer.slot)
|
||||||
|
CHECK(volume.texture_displacement_facet(slot).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A layer names the file its texture was imported from, which the library panel uses to point at the
|
||||||
|
// entry the layer came from. That name must not be opened: the project may come from anyone, the path
|
||||||
|
// may name any file the user can read - on Windows a UNC path names a host as well - and the next save
|
||||||
|
// would copy whatever was found there into the archive. A layer the archive has no image for comes
|
||||||
|
// back without pixels instead, which the user fixes by picking the texture again.
|
||||||
|
TEST_CASE("A texture displacement layer whose image the archive lacks is not read from the path it names", "[3mf][TextureDisplacement][Regression]")
|
||||||
|
{
|
||||||
|
// A file that really is there, so the test fails if the importer ever reaches for it.
|
||||||
|
ScopedTemporaryFile texture(".png");
|
||||||
|
REQUIRE(png::write_gray_to_file(texture.string(), 4, 4, std::vector<uint8_t>(16, 200)));
|
||||||
|
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.slot = 0;
|
||||||
|
layer.name = "Bark";
|
||||||
|
layer.path = texture.string(); // and no image_data: nothing for the exporter to put in the archive
|
||||||
|
|
||||||
|
std::string painted;
|
||||||
|
ScopedTemporaryFile temp(".3mf");
|
||||||
|
store_texture_displaced_cube(temp.string(), layer, TextureDisplacementOptions{}, painted);
|
||||||
|
|
||||||
|
for (const std::string& name : zip_entry_names(temp.string()))
|
||||||
|
if (boost::algorithm::starts_with(name, TEXTURE_DISPLACEMENT_ENTRY_DIR))
|
||||||
|
CHECK_FALSE(boost::algorithm::ends_with(name, ".png"));
|
||||||
|
|
||||||
|
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||||
|
Model model;
|
||||||
|
const ModelVolume& volume = load_single_volume(temp.string(), model, backup_dir);
|
||||||
|
|
||||||
|
REQUIRE(volume.texture_displacement_layers.size() == 1);
|
||||||
|
CHECK(volume.texture_displacement_layers.front().path == texture.string());
|
||||||
|
CHECK_FALSE(bool(volume.texture_displacement_layers.front().image_data));
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(5) == painted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stack's JSON is arbitrary text by the time it is read back, and a value of the wrong type in it
|
||||||
|
// throws out of the parse. That has to cost the volume its layers, not the project: the model, and the
|
||||||
|
// paint the user can still build on, load as they would from a build without the feature.
|
||||||
|
TEST_CASE("A malformed texture displacement block leaves the rest of the project loadable", "[3mf][TextureDisplacement][Regression]")
|
||||||
|
{
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.slot = 0;
|
||||||
|
layer.name = "Bark";
|
||||||
|
layer.image_data = make_gray_png(128);
|
||||||
|
|
||||||
|
std::string painted;
|
||||||
|
ScopedTemporaryFile temp(".3mf");
|
||||||
|
store_texture_displaced_cube(temp.string(), layer, TextureDisplacementOptions{}, painted);
|
||||||
|
|
||||||
|
REQUIRE(rewrite_3mf_entries(temp.string(), [](std::string& name, std::string& data) {
|
||||||
|
if (!boost::algorithm::starts_with(name, TEXTURE_DISPLACEMENT_ENTRY_DIR) ||
|
||||||
|
!boost::algorithm::ends_with(name, ".json"))
|
||||||
|
return false;
|
||||||
|
// `offset` goes through a typed accessor, so a string in it throws; a plain setting like
|
||||||
|
// depth_mm would be shrugged off by the parse instead, leaving the layer loadable.
|
||||||
|
data = R"({"version":1,"layers":[{"slot":0,"offset":["deep","deep"]}]})";
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||||
|
Model model;
|
||||||
|
bool loaded = false;
|
||||||
|
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
|
||||||
|
REQUIRE(loaded);
|
||||||
|
REQUIRE(model.objects.size() == 1);
|
||||||
|
REQUIRE(model.objects.front()->volumes.size() == 1);
|
||||||
|
const ModelVolume& volume = *model.objects.front()->volumes.front();
|
||||||
|
|
||||||
|
CHECK(volume.texture_displacement_layers.empty());
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(5) == painted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A slot's masks are kept only as far as its last painted triangle, so where the paint falls in the
|
||||||
|
// list decides whether that run has to be padded in front of the paint, cut short behind it, or both.
|
||||||
|
TEST_CASE("Texture displacement paint survives wherever it falls in the triangle list", "[3mf][TextureDisplacement]")
|
||||||
|
{
|
||||||
|
const int painted_facet = GENERATE(0, 5, 11); // first, middle and last facet of a cube
|
||||||
|
INFO("painted facet " << painted_facet);
|
||||||
|
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.slot = 0;
|
||||||
|
layer.name = "Bark";
|
||||||
|
layer.image_data = make_gray_png(128);
|
||||||
|
|
||||||
|
std::string painted;
|
||||||
|
ScopedTemporaryFile temp(".3mf");
|
||||||
|
store_texture_displaced_cube(temp.string(), layer, TextureDisplacementOptions{}, painted, painted_facet);
|
||||||
|
|
||||||
|
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||||
|
Model model;
|
||||||
|
const ModelVolume& volume = load_single_volume(temp.string(), model, backup_dir);
|
||||||
|
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facet) == painted);
|
||||||
|
for (int facet = 0; facet < int(volume.mesh().its.indices.size()); ++facet)
|
||||||
|
if (facet != painted_facet)
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(facet).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each volume's stack is written under the volume's own id, and the paint of a whole object arrives as
|
||||||
|
// one run of triangles that the volumes take their slices of. Two volumes using the same slot are the
|
||||||
|
// case where both of those have to tell the volumes apart.
|
||||||
|
TEST_CASE("Two volumes of one object keep their own texture displacement layers", "[3mf][TextureDisplacement]")
|
||||||
|
{
|
||||||
|
const auto first_image = make_gray_png(64);
|
||||||
|
const auto second_image = make_gray_png(192);
|
||||||
|
REQUIRE(*first_image != *second_image);
|
||||||
|
|
||||||
|
// The facet each volume is painted on, volume-local, so a mask landing on the wrong volume shows up.
|
||||||
|
const int painted_facets[2] = { 0, 7 };
|
||||||
|
std::string painted[2];
|
||||||
|
|
||||||
|
ScopedTemporaryFile temp(".3mf");
|
||||||
|
{
|
||||||
|
Model model;
|
||||||
|
ModelObject* object = model.add_object();
|
||||||
|
object->add_instance();
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.slot = 0; // the same slot in both, which is what makes their names collide
|
||||||
|
layer.name = i == 0 ? "First" : "Second";
|
||||||
|
layer.depth_mm = i == 0 ? 0.5f : 1.5f;
|
||||||
|
layer.image_data = i == 0 ? first_image : second_image;
|
||||||
|
volume->texture_displacement_layers.push_back(layer);
|
||||||
|
|
||||||
|
TriangleSelector selector(volume->mesh());
|
||||||
|
selector.set_facet(painted_facets[i], EnforcerBlockerType::ENFORCER);
|
||||||
|
REQUIRE(volume->texture_displacement_facet(layer.slot).set(selector));
|
||||||
|
painted[i] = volume->texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facets[i]);
|
||||||
|
REQUIRE_FALSE(painted[i].empty());
|
||||||
|
}
|
||||||
|
store_project(temp.string(), model);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||||
|
Model model;
|
||||||
|
REQUIRE(load_project(temp.string(), model, backup_dir));
|
||||||
|
REQUIRE(model.objects.size() == 1);
|
||||||
|
REQUIRE(model.objects.front()->volumes.size() == 2);
|
||||||
|
|
||||||
|
const std::shared_ptr<std::vector<unsigned char>> images[2] = { first_image, second_image };
|
||||||
|
const std::string names[2] = { "First", "Second" };
|
||||||
|
const float depths[2] = { 0.5f, 1.5f };
|
||||||
|
std::string entries[2];
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
INFO("volume " << i);
|
||||||
|
const ModelVolume& volume = *model.objects.front()->volumes[i];
|
||||||
|
REQUIRE(volume.texture_displacement_layers.size() == 1);
|
||||||
|
const TextureDisplacementLayer& layer = volume.texture_displacement_layers.front();
|
||||||
|
CHECK(layer.name == names[i]);
|
||||||
|
CHECK_THAT(layer.depth_mm, Catch::Matchers::WithinAbs(depths[i], 1e-6f));
|
||||||
|
REQUIRE(layer.image_data);
|
||||||
|
CHECK(*layer.image_data == *images[i]);
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facets[i]) == painted[i]);
|
||||||
|
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facets[1 - i]).empty());
|
||||||
|
entries[i] = layer.path_in_3mf;
|
||||||
|
}
|
||||||
|
// Both volumes paint slot 0, so only the volume id keeps their textures apart in the archive.
|
||||||
|
CHECK(entries[0] != entries[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2157,6 +2157,190 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
|
|||||||
CHECK(second <= single * 5 / 4);
|
CHECK(second <= single * 5 / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The .3mf stores the layer stack as JSON (see texture_displacement_layers_to_json). Two properties
|
||||||
|
// matter: everything the UV editor and the panel can set has to survive a round trip, and a document
|
||||||
|
// written by a build that knew fewer fields has to keep loading, with the missing ones left at their
|
||||||
|
// defaults rather than zeroed.
|
||||||
|
TEST_CASE("Texture displacement layers survive a JSON round trip", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
std::vector<TextureDisplacementLayer> layers(2);
|
||||||
|
TextureDisplacementLayer &a = layers[0];
|
||||||
|
a.slot = 0;
|
||||||
|
a.name = "Bark";
|
||||||
|
a.path = "/textures/bark.png";
|
||||||
|
a.depth_mm = 1.25f;
|
||||||
|
a.tiling_scale = 7.5f;
|
||||||
|
a.rotation_deg = 30.f;
|
||||||
|
a.offset = Vec2f(0.25f, -0.5f);
|
||||||
|
a.invert = true;
|
||||||
|
a.midlevel = 0.125f;
|
||||||
|
a.smoothing = 0.75f;
|
||||||
|
a.edge_smoothing = true;
|
||||||
|
a.edge_smoothing_amount = 0.25f;
|
||||||
|
a.auto_connect_islands = false;
|
||||||
|
a.tile_enabled = false;
|
||||||
|
a.tile_method = TextureTileMethod::MirroredRepeat;
|
||||||
|
a.projection_method = TextureProjectionMethod::LSCM;
|
||||||
|
a.blend_mode = TextureBlendMode::Subtract;
|
||||||
|
a.color_enabled = true;
|
||||||
|
a.lscm_seam_angle_deg = 45.f;
|
||||||
|
a.island_padding_mm = 0.5f;
|
||||||
|
a.lscm_seam_edges = { { 1, 2 }, { 3, 5 } };
|
||||||
|
a.islands = { TextureIsland{ Vec2f(1.f, 2.f), 15.f, 1.5f }, TextureIsland{} };
|
||||||
|
a.island_groups = { 0, 0 };
|
||||||
|
a.lscm_uv_overrides = { { -4, Vec2f(0.5f, 0.75f) } };
|
||||||
|
|
||||||
|
TextureDisplacementLayer &b = layers[1];
|
||||||
|
b.slot = 1;
|
||||||
|
b.projection_method = TextureProjectionMethod::ViewProjected;
|
||||||
|
b.view_project_right = Vec3f(0.f, 1.f, 0.f);
|
||||||
|
b.view_project_up = Vec3f(0.f, 0.f, 1.f);
|
||||||
|
b.view_project_projective = true;
|
||||||
|
for (size_t i = 0; i < b.view_project_matrix.size(); ++i)
|
||||||
|
b.view_project_matrix[i] = float(i) + 0.5f;
|
||||||
|
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
options.displace_border = false;
|
||||||
|
options.smooth_enabled = true;
|
||||||
|
options.smooth_strength = 0.6f;
|
||||||
|
options.smooth_iterations = 5;
|
||||||
|
options.pipeline_v2 = false;
|
||||||
|
options.v2_max_triangles_k = 250;
|
||||||
|
options.color_mix_count = 3;
|
||||||
|
options.color_despeckle = 4;
|
||||||
|
options.active_slot = 3;
|
||||||
|
options.panel_mode = 1;
|
||||||
|
|
||||||
|
const std::string json = texture_displacement_layers_to_json(layers, options);
|
||||||
|
|
||||||
|
std::vector<TextureDisplacementLayer> read_layers;
|
||||||
|
TextureDisplacementOptions read_options;
|
||||||
|
REQUIRE(texture_displacement_layers_from_json(json, read_layers, read_options));
|
||||||
|
REQUIRE(read_layers.size() == layers.size());
|
||||||
|
|
||||||
|
const TextureDisplacementLayer &ra = read_layers[0];
|
||||||
|
CHECK(ra.name == a.name);
|
||||||
|
CHECK(ra.path == a.path);
|
||||||
|
CHECK_THAT(ra.depth_mm, WithinAbs(a.depth_mm, 1e-6f));
|
||||||
|
CHECK_THAT(ra.tiling_scale, WithinAbs(a.tiling_scale, 1e-6f));
|
||||||
|
CHECK_THAT(ra.rotation_deg, WithinAbs(a.rotation_deg, 1e-6f));
|
||||||
|
CHECK(ra.offset.isApprox(a.offset));
|
||||||
|
CHECK(ra.invert == a.invert);
|
||||||
|
CHECK_THAT(ra.midlevel, WithinAbs(a.midlevel, 1e-6f));
|
||||||
|
CHECK_THAT(ra.smoothing, WithinAbs(a.smoothing, 1e-6f));
|
||||||
|
CHECK(ra.edge_smoothing == a.edge_smoothing);
|
||||||
|
CHECK_THAT(ra.edge_smoothing_amount, WithinAbs(a.edge_smoothing_amount, 1e-6f));
|
||||||
|
CHECK(ra.auto_connect_islands == a.auto_connect_islands);
|
||||||
|
CHECK(ra.tile_enabled == a.tile_enabled);
|
||||||
|
CHECK(ra.tile_method == a.tile_method);
|
||||||
|
CHECK(ra.projection_method == a.projection_method);
|
||||||
|
CHECK(ra.blend_mode == a.blend_mode);
|
||||||
|
CHECK(ra.color_enabled == a.color_enabled);
|
||||||
|
CHECK_THAT(ra.lscm_seam_angle_deg, WithinAbs(a.lscm_seam_angle_deg, 1e-6f));
|
||||||
|
CHECK_THAT(ra.island_padding_mm, WithinAbs(a.island_padding_mm, 1e-6f));
|
||||||
|
CHECK(ra.lscm_seam_edges == a.lscm_seam_edges);
|
||||||
|
REQUIRE(ra.islands.size() == a.islands.size());
|
||||||
|
CHECK(ra.islands[0].offset.isApprox(a.islands[0].offset));
|
||||||
|
CHECK_THAT(ra.islands[0].rotation_deg, WithinAbs(a.islands[0].rotation_deg, 1e-6f));
|
||||||
|
CHECK_THAT(ra.islands[0].scale, WithinAbs(a.islands[0].scale, 1e-6f));
|
||||||
|
CHECK(ra.island_groups == a.island_groups);
|
||||||
|
REQUIRE(ra.lscm_uv_overrides.size() == 1);
|
||||||
|
CHECK(ra.lscm_uv_overrides[0].first == -4);
|
||||||
|
CHECK(ra.lscm_uv_overrides[0].second.isApprox(Vec2f(0.5f, 0.75f)));
|
||||||
|
|
||||||
|
const TextureDisplacementLayer &rb = read_layers[1];
|
||||||
|
CHECK(rb.projection_method == TextureProjectionMethod::ViewProjected);
|
||||||
|
CHECK(rb.view_project_right.isApprox(b.view_project_right));
|
||||||
|
CHECK(rb.view_project_up.isApprox(b.view_project_up));
|
||||||
|
CHECK(rb.view_project_projective);
|
||||||
|
CHECK(rb.view_project_matrix == b.view_project_matrix);
|
||||||
|
|
||||||
|
CHECK(read_options.displace_border == options.displace_border);
|
||||||
|
CHECK(read_options.smooth_enabled == options.smooth_enabled);
|
||||||
|
CHECK_THAT(read_options.smooth_strength, WithinAbs(options.smooth_strength, 1e-6f));
|
||||||
|
CHECK(read_options.smooth_iterations == options.smooth_iterations);
|
||||||
|
CHECK(read_options.pipeline_v2 == options.pipeline_v2);
|
||||||
|
CHECK(read_options.v2_max_triangles_k == options.v2_max_triangles_k);
|
||||||
|
CHECK(read_options.color_mix_count == options.color_mix_count);
|
||||||
|
CHECK(read_options.color_despeckle == options.color_despeckle);
|
||||||
|
// Where the panel was left comes back with the rest, so a reopened project is on the same layer.
|
||||||
|
CHECK(read_options.active_slot == options.active_slot);
|
||||||
|
CHECK(read_options.panel_mode == options.panel_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
// What a build that knew fewer fields would have written: one layer, almost nothing set.
|
||||||
|
const std::string sparse = R"({"version":1,"layers":[{"slot":2,"name":"Old","depth_mm":0.75}]})";
|
||||||
|
|
||||||
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
REQUIRE(texture_displacement_layers_from_json(sparse, layers, options));
|
||||||
|
REQUIRE(layers.size() == 1);
|
||||||
|
|
||||||
|
const TextureDisplacementLayer &l = layers[0];
|
||||||
|
const TextureDisplacementLayer fresh;
|
||||||
|
const TextureDisplacementOptions defaults;
|
||||||
|
CHECK(l.slot == 2);
|
||||||
|
CHECK(l.name == "Old");
|
||||||
|
CHECK_THAT(l.depth_mm, WithinAbs(0.75f, 1e-6f));
|
||||||
|
// Everything absent keeps the struct's own default rather than becoming zero.
|
||||||
|
CHECK_THAT(l.tiling_scale, WithinAbs(fresh.tiling_scale, 1e-6f));
|
||||||
|
CHECK(l.auto_connect_islands == fresh.auto_connect_islands);
|
||||||
|
CHECK(l.tile_enabled == fresh.tile_enabled);
|
||||||
|
CHECK(l.projection_method == fresh.projection_method);
|
||||||
|
CHECK_THAT(l.lscm_seam_angle_deg, WithinAbs(fresh.lscm_seam_angle_deg, 1e-6f));
|
||||||
|
CHECK(options.color_mix_count == defaults.color_mix_count);
|
||||||
|
CHECK(options.pipeline_v2 == defaults.pipeline_v2);
|
||||||
|
CHECK(options.active_slot == defaults.active_slot);
|
||||||
|
CHECK(options.panel_mode == defaults.panel_mode);
|
||||||
|
|
||||||
|
std::vector<TextureDisplacementLayer> unused_layers;
|
||||||
|
TextureDisplacementOptions unused_options;
|
||||||
|
CHECK(!texture_displacement_layers_from_json("not json at all", unused_layers, unused_options));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The vectors, the islands and the UV overrides are read through nlohmann's typed accessors, which
|
||||||
|
// throw on a value of the wrong type rather than returning anything. Nothing between here and the
|
||||||
|
// project load catches that, so a document carrying one has to come back as "malformed block".
|
||||||
|
TEST_CASE("Texture displacement JSON reports a value of the wrong type instead of throwing", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
const std::string json = GENERATE(
|
||||||
|
R"({"layers":[{"slot":0,"offset":["a","b"]}]})",
|
||||||
|
R"({"layers":[{"slot":0,"view_project_right":[1,null,3]}]})",
|
||||||
|
R"({"layers":[{"slot":0,"view_project_matrix":[1,2,3,4,5,6,7,8,9,10,11,"x"]}]})",
|
||||||
|
R"({"layers":[{"slot":0,"lscm_seam_edges":["a","b"]}]})",
|
||||||
|
R"({"layers":[{"slot":0,"islands":[[1,2,"a",4]]}]})",
|
||||||
|
R"({"layers":[{"slot":0,"lscm_uv_overrides":[[1,"a",3]]}]})");
|
||||||
|
INFO(json);
|
||||||
|
|
||||||
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
bool parsed = true;
|
||||||
|
REQUIRE_NOTHROW(parsed = texture_displacement_layers_from_json(json, layers, options));
|
||||||
|
CHECK_FALSE(parsed);
|
||||||
|
CHECK(layers.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A plain setting is read through a helper that shrugs a wrong type off, which is the deliberate
|
||||||
|
// other half of the case above: the field keeps its default and the rest of the layer still loads.
|
||||||
|
TEST_CASE("Texture displacement JSON keeps the default for a setting of the wrong type", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
const std::string json = R"({"options":{"smooth_strength":"a lot"},"layers":[{"slot":"two","name":"Old","depth_mm":0.75}]})";
|
||||||
|
|
||||||
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
REQUIRE(texture_displacement_layers_from_json(json, layers, options));
|
||||||
|
REQUIRE(layers.size() == 1);
|
||||||
|
|
||||||
|
const TextureDisplacementLayer fresh;
|
||||||
|
const TextureDisplacementOptions defaults;
|
||||||
|
CHECK(layers[0].slot == fresh.slot);
|
||||||
|
CHECK(layers[0].name == "Old");
|
||||||
|
CHECK_THAT(layers[0].depth_mm, WithinAbs(0.75f, 1e-6f));
|
||||||
|
CHECK_THAT(options.smooth_strength, WithinAbs(defaults.smooth_strength, 1e-6f));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
||||||
{
|
{
|
||||||
TextureDisplacementLayer gray;
|
TextureDisplacementLayer gray;
|
||||||
|
|||||||
Reference in New Issue
Block a user