mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 10:21:17 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30e48183e3 | ||
|
|
a243941b72 | ||
|
|
6a1a5d51e9 | ||
|
|
e625845b34 | ||
|
|
01b340dee1 | ||
|
|
4c2a63e11e | ||
|
|
f8cf34c55b | ||
|
|
00d11a7ba0 | ||
|
|
8ecfee6a21 | ||
|
|
fbf4652e12 | ||
|
|
be9ff50130 | ||
|
|
dee1f1715f | ||
|
|
fcb1f62bd9 | ||
|
|
a13089d357 | ||
|
|
fb6c93963d | ||
|
|
7688f7a16c |
@@ -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/Config.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include "bbs_3mf.hpp"
|
||||
@@ -63,6 +64,7 @@
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <set>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#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_SEAM_ATTR = "paint_seam";
|
||||
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
|
||||
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
|
||||
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
|
||||
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
|
||||
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
|
||||
// BBS
|
||||
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
||||
|
||||
@@ -847,6 +856,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::vector<std::string> custom_seam;
|
||||
std::vector<std::string> mmu_segmentation;
|
||||
std::vector<std::string> fuzzy_skin;
|
||||
// One per texture displacement layer slot, each parallel to `triangles` like the masks above.
|
||||
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
|
||||
// BBS
|
||||
std::vector<std::string> face_properties;
|
||||
|
||||
@@ -867,6 +878,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
custom_seam.clear();
|
||||
mmu_segmentation.clear();
|
||||
fuzzy_skin.clear();
|
||||
for (std::vector<std::string> &slot : texture_displacement)
|
||||
slot.clear();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1200,6 +1213,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
/*IdToSlaSupportPointsMap m_sla_support_points;
|
||||
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
||||
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
|
||||
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
|
||||
// are built only after the whole archive has been walked, so by the time a volume names its JSON
|
||||
// every file it could refer to is already in here, whatever order the archive happened to be in.
|
||||
std::map<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_characters;
|
||||
std::string m_name;
|
||||
@@ -1729,8 +1747,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//plate->pattern_file = it->second->pattern_file;
|
||||
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
||||
plate->top_file = it->second->top_file;
|
||||
plate->pick_file = it->second->pick_file.empty();
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
||||
plate->pick_file = it->second->pick_file;
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
||||
plate->config = it->second->config;
|
||||
|
||||
if (!plate->thumbnail_file.empty())
|
||||
@@ -2073,6 +2091,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
else if (_is_svg_shape_file(name)) {
|
||||
_extract_embossed_svg_shape_file(name, archive, stat);
|
||||
}
|
||||
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
|
||||
std::string contents(stat.m_uncomp_size, '\0');
|
||||
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
|
||||
m_texture_displacement_files.emplace(name, std::move(contents));
|
||||
else
|
||||
add_error("Error while reading texture displacement data");
|
||||
}
|
||||
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
|
||||
m_parsing_slice_info = true;
|
||||
//extract slice info from archive
|
||||
@@ -3310,6 +3335,59 @@ 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;
|
||||
}
|
||||
// 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 - falls back to the file it came from. The
|
||||
// importer copies every texture into the user's own library, so that path resolves on the
|
||||
// machine the project was made on and quietly resolves nowhere else, which is the old
|
||||
// behaviour: settings restored, pixels not.
|
||||
const auto load_image_from_disk = [](TextureDisplacementLayer &layer) {
|
||||
boost::system::error_code ec;
|
||||
if (layer.path.empty() || !boost::filesystem::is_regular_file(layer.path, ec) || ec)
|
||||
return;
|
||||
boost::nowide::ifstream file(layer.path, std::ios::binary);
|
||||
if (!file.good())
|
||||
return;
|
||||
std::vector<unsigned char> bytes((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
if (!bytes.empty())
|
||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
};
|
||||
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
if (!layer.path_in_3mf.empty()) {
|
||||
if (const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
|
||||
image != m_texture_displacement_files.end()) {
|
||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(),
|
||||
image->second.end());
|
||||
continue;
|
||||
}
|
||||
add_error("Missing texture displacement image: " + layer.path_in_3mf);
|
||||
}
|
||||
load_image_from_disk(layer);
|
||||
}
|
||||
volume.texture_displacement_layers = std::move(layers);
|
||||
volume.texture_displacement_options = options;
|
||||
}
|
||||
|
||||
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
|
||||
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
|
||||
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
||||
@@ -3966,6 +4044,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.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
m_curr_object->geometry.texture_displacement[slot].push_back(
|
||||
bbs_get_attribute_value_string(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot]));
|
||||
// BBS
|
||||
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -5317,6 +5398,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->mmu_segmentation_facets.touch();
|
||||
volume->fuzzy_skin_facets.shrink_to_fit();
|
||||
volume->fuzzy_skin_facets.touch();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<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);
|
||||
@@ -5332,6 +5425,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
for (const Metadata& metadata : volume_data->metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5496,12 +5591,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->seam_facets.shrink_to_fit();
|
||||
volume->mmu_segmentation_facets.shrink_to_fit();
|
||||
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<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);
|
||||
|
||||
// apply the remaining volume's metadata
|
||||
for (const Metadata& metadata : volume_data.metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5805,6 +5916,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.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
current_object->geometry.texture_displacement[slot].push_back(
|
||||
bbs_get_attribute_value_string(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot]));
|
||||
// BBS
|
||||
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -6808,6 +6922,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
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=\"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 << "</Types>";
|
||||
|
||||
@@ -7651,6 +7768,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
|
||||
// not know, so a project written here still opens in a build without the feature - it just
|
||||
// loses the paint, which is also all it could have done with it.
|
||||
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
|
||||
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
|
||||
if (texture_paint.empty())
|
||||
continue;
|
||||
output_buffer += " ";
|
||||
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
|
||||
output_buffer += "=\"";
|
||||
output_buffer += texture_paint;
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// BBS
|
||||
if (i < its.properties.size()) {
|
||||
std::string prop_str = its.properties[i].to_string();
|
||||
@@ -8100,7 +8231,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return file_path;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
|
||||
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
|
||||
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
|
||||
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
|
||||
// every reader has to parse just to list the objects.
|
||||
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
|
||||
const std::string &id)
|
||||
{
|
||||
if (volume.texture_displacement_layers.empty())
|
||||
return;
|
||||
|
||||
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
|
||||
// being exported is const (and may be saved again, elsewhere, with different paths).
|
||||
std::vector<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;
|
||||
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
||||
@@ -8211,6 +8377,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
|
||||
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
|
||||
}
|
||||
|
||||
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
|
||||
|
||||
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
|
||||
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_role_based_fan_marker_layer.fill(-1);
|
||||
|
||||
m_fan_mover.release();
|
||||
m_fan_mover.reset();
|
||||
m_ordering_cache.clear();
|
||||
|
||||
// Belt printer: initialize belt-specific writer via virtual hook.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include "nlohmann/json.hpp"
|
||||
#include <queue>
|
||||
#include <ratio>
|
||||
#include <string>
|
||||
@@ -157,33 +158,12 @@ struct SmoothedTextureCache
|
||||
{
|
||||
std::weak_ptr<const std::vector<unsigned char>> source;
|
||||
float smoothing = 0.f;
|
||||
// Part of the key, not of the result: smoothing is applied *after* the panel's adjustments, so a
|
||||
// blur computed over one set of adjustments is not the blur another set would give.
|
||||
TextureAdjustments adjust;
|
||||
DecodedHeightTexture texture;
|
||||
};
|
||||
std::mutex mutex;
|
||||
std::unordered_map<const void *, Entry> entries;
|
||||
};
|
||||
SmoothedTextureCache g_smoothed_texture_cache;
|
||||
|
||||
// The adjusted copy, cached between the raw decode and the smoothed one. Needed for the same reason the
|
||||
// smoothed cache is: while a slider in the image-adjustment panel moves, the thumbnail, the shaded
|
||||
// preview's height and colour textures, the UV editor backdrop and the displacement preview job all ask
|
||||
// for the same (image, adjustments) pair within one frame, and each of them running a full-size resample
|
||||
// of its own is what would make the panel unusable on a large texture.
|
||||
struct AdjustedTextureCache
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
std::weak_ptr<const std::vector<unsigned char>> source;
|
||||
TextureAdjustments adjust;
|
||||
DecodedHeightTexture texture;
|
||||
};
|
||||
std::mutex mutex;
|
||||
std::unordered_map<const void *, Entry> entries;
|
||||
};
|
||||
AdjustedTextureCache g_adjusted_texture_cache;
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
@@ -359,34 +339,6 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
|
||||
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||
// because `preview_adjust` is an edit to the image while `smoothing` is a live low-pass over whatever
|
||||
// the image is - the same order the panel presents them in, and the same order a commit bakes in.
|
||||
//
|
||||
// Nothing applies here once the panel has closed: it resets preview_adjust to identity, and identity
|
||||
// is a no-op that costs one comparison.
|
||||
if (!layer.preview_adjust.identity()) {
|
||||
bool have_adjusted = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_adjusted_texture_cache.mutex);
|
||||
auto it = g_adjusted_texture_cache.entries.find(key);
|
||||
if (it != g_adjusted_texture_cache.entries.end() && it->second.adjust == layer.preview_adjust &&
|
||||
it->second.source.lock() == layer.image_data) {
|
||||
result = it->second.texture;
|
||||
have_adjusted = true;
|
||||
}
|
||||
}
|
||||
if (!have_adjusted) {
|
||||
result = apply_texture_adjustments(result, layer.preview_adjust);
|
||||
std::lock_guard<std::mutex> lock(g_adjusted_texture_cache.mutex);
|
||||
auto &entries = g_adjusted_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.source.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data),
|
||||
layer.preview_adjust, result };
|
||||
}
|
||||
}
|
||||
|
||||
// Smoothing radius scales with the texture so the same slider value blurs the same *fraction* of
|
||||
// the image whatever resolution it came in at, and stays continuous in the slider - see
|
||||
// smooth_height_pixels(). The cap is a cost limit, not part of the mapping: the blur is
|
||||
@@ -397,7 +349,7 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
std::lock_guard<std::mutex> lock(g_smoothed_texture_cache.mutex);
|
||||
auto it = g_smoothed_texture_cache.entries.find(key);
|
||||
if (it != g_smoothed_texture_cache.entries.end() && it->second.smoothing == layer.smoothing &&
|
||||
it->second.adjust == layer.preview_adjust && it->second.source.lock() == layer.image_data)
|
||||
it->second.source.lock() == layer.image_data)
|
||||
return it->second.texture;
|
||||
}
|
||||
const float span = 0.05f * float(std::min(result.width, result.height));
|
||||
@@ -421,70 +373,12 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
auto &entries = g_smoothed_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.source.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), layer.smoothing,
|
||||
layer.preview_adjust, result };
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), layer.smoothing, result };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Fraction of the short side that TextureAdjustments::blur == 1 blurs over, and the cost cap on it.
|
||||
// Deliberately the same pair decode_height_texture() uses for the Smoothing slider - see
|
||||
// adjustment_blur_radius().
|
||||
constexpr float SOURCE_BLUR_SPAN = 0.05f;
|
||||
constexpr float SOURCE_BLUR_MAX_PX = 48.f;
|
||||
|
||||
// Radius of the unsharp mask's blurred reference, in texels. Small and only mildly resolution-dependent:
|
||||
// a sharpen is meant to act on the finest detail the image holds, so its radius tracks the texel grid
|
||||
// rather than the picture's size. 1024x1024 gives ~2 texels, the usual photo-editor default.
|
||||
float unsharp_radius(int width, int height)
|
||||
{
|
||||
return std::max(1.f, 0.002f * float(std::min(width, height)));
|
||||
}
|
||||
|
||||
// Brightness as a *lift* toward white (or toward black) rather than an additive offset: positive b maps
|
||||
// v to v + b*(1 - v), negative to v*(1 + b). Both reach the endpoint exactly at |b| == 1 while staying
|
||||
// monotonic and non-clipping in between, which an additive shift does not - it drives a whole region to
|
||||
// a flat 255 and, because this image is also the height map, that region bakes as flat relief rather
|
||||
// than as bright relief. Keeping the curve injective keeps the detail in it.
|
||||
float apply_brightness(float v, float b)
|
||||
{
|
||||
return b >= 0.f ? v + b * (1.f - v) : v * (1.f + b);
|
||||
}
|
||||
|
||||
// Contrast about the 0.5 pivot. Gain is 1/(1-c) going up (c == 1 would be an infinite-slope threshold, so
|
||||
// the caller's clamp keeps it finite) and 1+c going down, reaching a flat mid-grey at c == -1.
|
||||
float apply_contrast(float v, float c)
|
||||
{
|
||||
const float gain = c >= 0.f ? 1.f / std::max(1e-3f, 1.f - c) : 1.f + c;
|
||||
return (v - 0.5f) * gain + 0.5f;
|
||||
}
|
||||
|
||||
// Blur then sharpen one 8-bit channel in place. Shared by the grayscale path and by each of a colour
|
||||
// image's three channels, so every channel gets the identical spatial treatment and a colour image's
|
||||
// hue cannot shift just because its channels were filtered differently.
|
||||
void adjust_channel_spatial(std::vector<uint8_t> &channel, int width, int height, float blur, float sharpness)
|
||||
{
|
||||
if (blur > 0.f)
|
||||
smooth_height_pixels(channel, width, height, adjustment_blur_radius(blur, width, height));
|
||||
|
||||
if (sharpness <= 0.f)
|
||||
return;
|
||||
|
||||
// Unsharp mask: push each texel away from its local average by `amount` times the difference. The
|
||||
// blurred reference is a copy, so the sharpen reads the un-sharpened neighbourhood everywhere rather
|
||||
// than its own partly-written output.
|
||||
std::vector<uint8_t> base = channel;
|
||||
smooth_height_pixels(base, width, height, unsharp_radius(width, height));
|
||||
const float amount = 1.5f * std::clamp(sharpness, 0.f, 1.f);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, channel.size()), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t i = r.begin(); i < r.end(); ++i) {
|
||||
const float v = float(channel[i]) + amount * (float(channel[i]) - float(base[i]));
|
||||
channel[i] = uint8_t(std::lround(std::clamp(v, 0.f, 255.f)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
struct TextureDetailCache
|
||||
{
|
||||
std::mutex mutex;
|
||||
@@ -493,150 +387,6 @@ struct TextureDetailCache
|
||||
TextureDetailCache g_texture_detail_cache;
|
||||
} // namespace
|
||||
|
||||
float adjustment_blur_radius(float blur, int width, int height)
|
||||
{
|
||||
if (blur <= 0.f || width <= 0 || height <= 0)
|
||||
return 0.f;
|
||||
const float span = SOURCE_BLUR_SPAN * float(std::min(width, height));
|
||||
return std::clamp(blur, 0.f, 1.f) * std::min(span, SOURCE_BLUR_MAX_PX);
|
||||
}
|
||||
|
||||
DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, int max_px)
|
||||
{
|
||||
if (src.empty() || max_px <= 0 || (src.width <= max_px && src.height <= max_px))
|
||||
return src;
|
||||
|
||||
const int scale = std::max(1, (std::max(src.width, src.height) + max_px - 1) / max_px);
|
||||
const int w = std::max(1, src.width / scale);
|
||||
const int h = std::max(1, src.height / scale);
|
||||
const bool color = src.has_color();
|
||||
|
||||
DecodedHeightTexture out;
|
||||
out.width = w;
|
||||
out.height = h;
|
||||
out.pixels.resize(size_t(w) * size_t(h));
|
||||
if (color)
|
||||
out.rgb.resize(size_t(w) * size_t(h) * 3);
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<int>(0, h), [&](const tbb::blocked_range<int> &r) {
|
||||
for (int y = r.begin(); y < r.end(); ++y)
|
||||
for (int x = 0; x < w; ++x) {
|
||||
// The source block this destination texel covers. The max() keeps the block non-empty
|
||||
// when rounding would otherwise collapse it.
|
||||
const int x0 = x * src.width / w, x1 = std::max(x0 + 1, (x + 1) * src.width / w);
|
||||
const int y0 = y * src.height / h, y1 = std::max(y0 + 1, (y + 1) * src.height / h);
|
||||
uint32_t sum_h = 0, sum_c[3] = { 0, 0, 0 }, n = 0;
|
||||
for (int sy = y0; sy < y1 && sy < src.height; ++sy)
|
||||
for (int sx = x0; sx < x1 && sx < src.width; ++sx, ++n) {
|
||||
const size_t si = size_t(sy) * size_t(src.width) + size_t(sx);
|
||||
sum_h += src.pixels[si];
|
||||
if (color)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
sum_c[c] += src.rgb[si * 3 + size_t(c)];
|
||||
}
|
||||
const size_t di = size_t(y) * size_t(w) + size_t(x);
|
||||
out.pixels[di] = n > 0 ? uint8_t(sum_h / n) : 0;
|
||||
if (color)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
out.rgb[di * 3 + size_t(c)] = n > 0 ? uint8_t(sum_c[c] / n) : 0;
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
DecodedHeightTexture apply_texture_adjustments(const DecodedHeightTexture &src, const TextureAdjustments &adj)
|
||||
{
|
||||
DecodedHeightTexture out = src;
|
||||
if (src.empty() || adj.identity())
|
||||
return out;
|
||||
|
||||
const int w = out.width;
|
||||
const int h = out.height;
|
||||
const size_t n = size_t(w) * size_t(h);
|
||||
|
||||
const float brightness = std::clamp(adj.brightness, -1.f, 1.f);
|
||||
// Short of 1, so apply_contrast()'s gain stays finite; at this value it is already a 1000x slope,
|
||||
// i.e. a threshold in all but name.
|
||||
const float contrast = std::clamp(adj.contrast, -1.f, 0.999f);
|
||||
const float blur = std::clamp(adj.blur, 0.f, 1.f);
|
||||
const float sharpness = std::clamp(adj.sharpness, 0.f, 1.f);
|
||||
|
||||
// The tonal pass: a 256-entry lookup table, because brightness and contrast are the same scalar
|
||||
// curve for every texel and every channel. Building it once beats evaluating two curves a few
|
||||
// million times, and it is what keeps this pass off the panel's critical path.
|
||||
std::array<uint8_t, 256> tone{};
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
float v = float(i) / 255.f;
|
||||
v = apply_brightness(v, brightness);
|
||||
v = apply_contrast(v, contrast);
|
||||
tone[size_t(i)] = uint8_t(std::lround(std::clamp(v, 0.f, 1.f) * 255.f));
|
||||
}
|
||||
|
||||
if (!out.has_color()) {
|
||||
// Grayscale: one channel, and hue has nothing to turn.
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, n), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t i = r.begin(); i < r.end(); ++i)
|
||||
out.pixels[i] = tone[out.pixels[i]];
|
||||
});
|
||||
adjust_channel_spatial(out.pixels, w, h, blur, sharpness);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Colour: tone each channel through the same table, then rotate the hue about the luminance axis.
|
||||
//
|
||||
// The rotation is feColorMatrix's hueRotate matrix (SVG filters), which holds Rec.709 luma fixed.
|
||||
// The height below is taken with decode_height_texture()'s Rec.601 coefficients instead, so a hue
|
||||
// turn is not perfectly height-neutral - a few levels at full saturation. Reconciling the two would
|
||||
// mean a different rotation than every other tool's, which is the worse trade: the user is watching
|
||||
// the colour, and the height follows the colour by construction either way.
|
||||
const float theta = adj.hue_deg * float(M_PI) / 180.f;
|
||||
const float c = std::cos(theta);
|
||||
const float s = std::sin(theta);
|
||||
const float m[9] = {
|
||||
0.213f + c * 0.787f - s * 0.213f, 0.715f - c * 0.715f - s * 0.715f, 0.072f - c * 0.072f + s * 0.928f,
|
||||
0.213f - c * 0.213f + s * 0.143f, 0.715f + c * 0.285f + s * 0.140f, 0.072f - c * 0.072f - s * 0.283f,
|
||||
0.213f - c * 0.213f - s * 0.787f, 0.715f - c * 0.715f + s * 0.715f, 0.072f + c * 0.928f + s * 0.072f,
|
||||
};
|
||||
const bool turn_hue = adj.hue_deg != 0.f;
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, n), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t i = r.begin(); i < r.end(); ++i) {
|
||||
const float rr = float(tone[out.rgb[i * 3]]);
|
||||
const float gg = float(tone[out.rgb[i * 3 + 1]]);
|
||||
const float bb = float(tone[out.rgb[i * 3 + 2]]);
|
||||
const float nr = turn_hue ? m[0] * rr + m[1] * gg + m[2] * bb : rr;
|
||||
const float ng = turn_hue ? m[3] * rr + m[4] * gg + m[5] * bb : gg;
|
||||
const float nb = turn_hue ? m[6] * rr + m[7] * gg + m[8] * bb : bb;
|
||||
out.rgb[i * 3] = uint8_t(std::lround(std::clamp(nr, 0.f, 255.f)));
|
||||
out.rgb[i * 3 + 1] = uint8_t(std::lround(std::clamp(ng, 0.f, 255.f)));
|
||||
out.rgb[i * 3 + 2] = uint8_t(std::lround(std::clamp(nb, 0.f, 255.f)));
|
||||
}
|
||||
});
|
||||
|
||||
// The spatial pass wants one channel at a time (smooth_height_pixels() is a single-channel blur, and
|
||||
// reusing it is what keeps this consistent with Smoothing), so de-interleave, filter, re-interleave.
|
||||
if (blur > 0.f || sharpness > 0.f) {
|
||||
std::vector<uint8_t> channel(n);
|
||||
for (int ch = 0; ch < 3; ++ch) {
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
channel[i] = out.rgb[i * 3 + size_t(ch)];
|
||||
adjust_channel_spatial(channel, w, h, blur, sharpness);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
out.rgb[i * 3 + size_t(ch)] = channel[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Height is the luminance of the colour that came out, not the toned luminance of the colour that
|
||||
// went in: the two differ once hue rotation or a per-channel clamp has moved the channels around,
|
||||
// and a height that disagrees with the colour it is stored next to is the bug this avoids.
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, n), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t i = r.begin(); i < r.end(); ++i)
|
||||
out.pixels[i] = uint8_t(std::lround(0.299 * out.rgb[i * 3] + 0.587 * out.rgb[i * 3 + 1] +
|
||||
0.114 * out.rgb[i * 3 + 2]));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
TextureDetail out;
|
||||
@@ -5417,5 +5167,249 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Project persistence. See the declarations in TextureDisplacement.hpp for why this is JSON rather
|
||||
// than the cereal stream next to them.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
nlohmann::json vec2_to_json(const Vec2f &v) { return nlohmann::json::array({ v.x(), v.y() }); }
|
||||
nlohmann::json vec3_to_json(const Vec3f &v) { return nlohmann::json::array({ v.x(), v.y(), v.z() }); }
|
||||
|
||||
Vec2f vec2_from_json(const nlohmann::json &j, const Vec2f &fallback)
|
||||
{
|
||||
if (!j.is_array() || j.size() != 2)
|
||||
return fallback;
|
||||
return Vec2f(j[0].get<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;
|
||||
if (const auto it = root.find("options"); it != root.end() && it->is_object()) {
|
||||
const nlohmann::json &opt = *it;
|
||||
read(opt, "displace_border", out_options.displace_border);
|
||||
read(opt, "smooth_enabled", out_options.smooth_enabled);
|
||||
read(opt, "smooth_strength", out_options.smooth_strength);
|
||||
read(opt, "smooth_iterations", out_options.smooth_iterations);
|
||||
read(opt, "smooth_skip_border", out_options.smooth_skip_border);
|
||||
read(opt, "pipeline_v2", out_options.pipeline_v2);
|
||||
read(opt, "v2_refine_mm", out_options.v2_refine_mm);
|
||||
read(opt, "v2_regularize", out_options.v2_regularize);
|
||||
read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k);
|
||||
read(opt, "v2_relocate", out_options.v2_relocate);
|
||||
read(opt, "v2_flip_edges", out_options.v2_flip_edges);
|
||||
read(opt, "color_mix_enabled", out_options.color_mix_enabled);
|
||||
read(opt, "color_despeckle", out_options.color_despeckle);
|
||||
read(opt, "color_mix_count", out_options.color_mix_count);
|
||||
read(opt, "active_slot", out_options.active_slot);
|
||||
read(opt, "panel_mode", out_options.panel_mode);
|
||||
out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1);
|
||||
out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1);
|
||||
}
|
||||
|
||||
std::vector<TextureDisplacementLayer> out_layers;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
layers = std::move(out_layers);
|
||||
options = out_options;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -154,38 +154,6 @@ struct TextureIsland
|
||||
// chart to its true mm area, so a freshly unwrapped patch has uniform texel density to begin with.
|
||||
void average_island_scales(std::vector<TextureIsland> &islands);
|
||||
|
||||
// Photo-style adjustments to a layer's source image, as the gizmo's image-adjustment panel sets them.
|
||||
//
|
||||
// These are *not* layer parameters: nothing here is stored on TextureDisplacementLayer and nothing here is
|
||||
// re-applied at bake time. The panel applies them once, to the image, and the result becomes the layer's
|
||||
// image - so the adjusted pixels feed the colour and the displacement together, and a brightness change
|
||||
// really does change the relief. That is the intended behaviour: one image, one source of truth, rather
|
||||
// than a second set of knobs the bake would have to replay in the right order.
|
||||
//
|
||||
// Contrast with TextureDisplacementLayer::smoothing, which *is* a layer parameter applied on every decode:
|
||||
// `blur` here is a destructive edit of the image, `smoothing` is a live low-pass over whatever the image is.
|
||||
struct TextureAdjustments
|
||||
{
|
||||
float brightness = 0.f; // [-1, 1]; 0 unchanged, -1 black, +1 white
|
||||
float contrast = 0.f; // [-1, 1]; 0 unchanged, -1 flat mid-grey, +1 hard threshold
|
||||
float sharpness = 0.f; // [0, 1]; 0 unchanged. Unsharp-mask amount
|
||||
float blur = 0.f; // [0, 1]; 0 unchanged. Fraction of SOURCE_BLUR_SPAN, as `smoothing` is
|
||||
float hue_deg = 0.f; // [-180, 180]; 0 unchanged. No effect on a grayscale image - grey has no hue
|
||||
|
||||
bool identity() const
|
||||
{
|
||||
return brightness == 0.f && contrast == 0.f && sharpness == 0.f && blur == 0.f && hue_deg == 0.f;
|
||||
}
|
||||
// Exact equality, which is what is wanted: these come straight off sliders, and the question asked of
|
||||
// them is "is this still the value the preview on screen was computed from", not "are these close".
|
||||
bool operator==(const TextureAdjustments &o) const
|
||||
{
|
||||
return brightness == o.brightness && contrast == o.contrast && sharpness == o.sharpness &&
|
||||
blur == o.blur && hue_deg == o.hue_deg;
|
||||
}
|
||||
bool operator!=(const TextureAdjustments &o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// One texture asset plus its projection/displacement parameters. Several layers may be painted
|
||||
// onto overlapping areas of the same volume: their displacements are combined per vertex, in slot
|
||||
// order, each layer folding into the total via its own TextureBlendMode (see
|
||||
@@ -325,18 +293,6 @@ struct TextureDisplacementLayer
|
||||
// for the boundary to be drawn on.
|
||||
bool color_enabled = false;
|
||||
|
||||
// Live, unsaved image adjustments from the gizmo's image-adjustment panel, applied by
|
||||
// decode_height_texture() on top of the decoded image - so while the panel is open the relief, the
|
||||
// colour, the thumbnails and the bake all see the adjusted image, and moving a slider really does
|
||||
// change the displacement. See TextureAdjustments.
|
||||
//
|
||||
// Deliberately *not* serialized, by either save()/load() here or the .3mf: it is the state of an open
|
||||
// editor, not a property of the layer. The panel resets it when it closes, and "Save as copy" resolves
|
||||
// it by writing a new image and pointing the layer at that - which is the only way an adjustment
|
||||
// becomes permanent. A project saved mid-adjustment therefore stores the unadjusted image, which is
|
||||
// the honest answer: nothing was committed.
|
||||
TextureAdjustments preview_adjust;
|
||||
|
||||
bool empty() const { return !image_data || image_data->empty(); }
|
||||
|
||||
template<class Archive> void save(Archive &ar) const
|
||||
@@ -441,14 +397,38 @@ struct TextureDisplacementOptions
|
||||
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
||||
int color_despeckle = 2;
|
||||
|
||||
// Where the panel was left, rather than anything the bake reads. It lives with the volume because
|
||||
// that is what it describes: reopening a project should put the user back on the layer they were
|
||||
// editing, in the mode they were editing it in, not on slot 0 in Standard.
|
||||
int active_slot = 0;
|
||||
int panel_mode = 0; // 0 Standard, 1 Pro
|
||||
|
||||
template<class Archive> void serialize(Archive &ar)
|
||||
{
|
||||
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
||||
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
|
||||
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
|
||||
v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count,
|
||||
active_slot, panel_mode);
|
||||
}
|
||||
};
|
||||
|
||||
// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the
|
||||
// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and
|
||||
// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would
|
||||
// make every future field a project-breaking change. A JSON object tolerates both directions - an
|
||||
// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and
|
||||
// new ones degrade gracefully in older builds.
|
||||
//
|
||||
// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file
|
||||
// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is
|
||||
// responsible for writing it and for filling `image_data` back in on load.
|
||||
std::string texture_displacement_layers_to_json(const std::vector<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
|
||||
// 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.
|
||||
@@ -533,36 +513,6 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
||||
bool height_texture_has_color(const TextureDisplacementLayer &layer);
|
||||
|
||||
// Applies `adj` to `src` and returns the result; `src` is left alone. An empty `src`, or an identity
|
||||
// `adj`, gives back a copy.
|
||||
//
|
||||
// Order is tonal, then spatial: brightness, contrast and hue per texel first, then blur, then sharpness.
|
||||
// Sharpness last is what lets the two spatial knobs be used together - blur to kill speckle, then
|
||||
// sharpness to put the real edges back - instead of the blur simply undoing the sharpen.
|
||||
//
|
||||
// Grayscale in, grayscale out (`rgb` stays empty): promoting a grey image to RGB would hand the layer's
|
||||
// colour feature a row of identical greys to "colour" the model with, and triple the stored PNG for it.
|
||||
// For a colour image the height is recomputed as the luminance of the *adjusted* colour, with
|
||||
// decode_height_texture()'s own coefficients, so the relief and the colour can never drift apart.
|
||||
DecodedHeightTexture apply_texture_adjustments(const DecodedHeightTexture &src, const TextureAdjustments &adj);
|
||||
|
||||
// Blur radius, in texels, that TextureAdjustments::blur == 1 means on a `min(width, height)`-sized image:
|
||||
// the same 5%-of-the-short-side span the Smoothing slider uses, under the same 48-texel cost cap, so the
|
||||
// two controls bite at the same rate and a user moving between them is not surprised.
|
||||
float adjustment_blur_radius(float blur, int width, int height);
|
||||
|
||||
// Box-filters `src` down so neither side exceeds `max_px`, preserving aspect; never upscales, and
|
||||
// returns a copy when it already fits. Colour and height are filtered the same way, and the height
|
||||
// stays the height - it is not recomputed from the filtered colour, because a box filter is linear and
|
||||
// luminance is linear in the channels, so the two agree already.
|
||||
//
|
||||
// This is what lets the adjustment panel's live preview cost depend on the size of the box it is drawn
|
||||
// in rather than on the size of the image: adjusting a downscaled copy per slider frame, and the
|
||||
// full-size image only once, on commit. A box filter (rather than a plain decimation) is the point -
|
||||
// these textures are high-frequency by nature, and dropping texels would alias the preview into
|
||||
// showing detail the adjustment is not actually producing.
|
||||
DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, int max_px);
|
||||
|
||||
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
|
||||
//
|
||||
// Deliberately a callback rather than a function here: matching a colour to a filament is a
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <wx/checklst.h>
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/Widgets/DropDown.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -172,7 +173,26 @@ void AMSMaterialsSetting::create()
|
||||
|
||||
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
||||
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
|
||||
// Favorites toggled in the open list are regrouped just before it opens again, so rows never move
|
||||
// under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
|
||||
// rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
|
||||
auto regroup_favorites = [this]() {
|
||||
if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
|
||||
return;
|
||||
const FilamentInfos* info = selected_filament_info();
|
||||
const std::string filament_id = info ? info->filament_id : std::string();
|
||||
rebuild_filament_dropdown_items(filament_id);
|
||||
};
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
|
||||
if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
|
||||
regroup_favorites();
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
@@ -191,11 +211,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
|
||||
|
||||
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
|
||||
|
||||
#ifdef __APPLE__
|
||||
m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#else
|
||||
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#endif
|
||||
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
|
||||
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
|
||||
|
||||
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
|
||||
|
||||
@@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
|
||||
AMSMaterialsSetting::~AMSMaterialsSetting()
|
||||
{
|
||||
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
|
||||
@@ -531,10 +550,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
std::string selected_ams_id;
|
||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
std::string filament_id = filament_item.filament_id;
|
||||
if (it->filament_id.compare(filament_id) == 0) {
|
||||
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
|
||||
selected_ams_id = it->filament_id;
|
||||
break;
|
||||
}
|
||||
@@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
ams_setting_id = "";
|
||||
|
||||
// the combobox item
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
|
||||
|
||||
// check filament info (rack-gated per-nozzle blacklist evaluation)
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
|
||||
filament_item.filament_id, ams_filament_id, ams_setting_id);
|
||||
filament_id, ams_filament_id, ams_setting_id);
|
||||
|
||||
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
|
||||
wxString info_msg;
|
||||
@@ -986,6 +1005,133 @@ static void _collect_filament_info(const wxString& shown_name,
|
||||
query_filament_types[shown_name] = filament.config.get_filament_type();
|
||||
}
|
||||
|
||||
static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
|
||||
|
||||
const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
|
||||
{
|
||||
const int selection = m_comboBox_filament->GetSelection();
|
||||
if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
|
||||
return nullptr;
|
||||
return &m_filament_list_items[selection].filament;
|
||||
}
|
||||
|
||||
std::set<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)
|
||||
{
|
||||
if (!obj) return;
|
||||
@@ -999,13 +1145,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
m_input_k_val->GetTextCtrl()->SetValue(k);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||
|
||||
wxArrayString filament_items;
|
||||
wxString bambu_filament_name;
|
||||
wxString hint_filament_name; // the hint type to be selected
|
||||
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
|
||||
std::unordered_map<wxString, wxString> query_filament_types; //
|
||||
|
||||
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
|
||||
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
|
||||
map_filament_items.clear();
|
||||
@@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
if (alias.empty())
|
||||
continue;
|
||||
|
||||
filament_items.push_back(alias);
|
||||
m_sorted_filament_items.push_back(alias);
|
||||
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||
|
||||
FilamentInfos filament_infos;
|
||||
@@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
map_filament_items[alias] = filament_infos;
|
||||
|
||||
if (filament_it->filament_id == ams_filament_id) {
|
||||
hint_filament_name = from_u8(alias);
|
||||
bambu_filament_name = from_u8(alias);
|
||||
|
||||
|
||||
@@ -1215,24 +1359,11 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
return name_compare != 0 ? name_compare < 0 : left < right;
|
||||
};
|
||||
|
||||
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
|
||||
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
|
||||
}
|
||||
|
||||
// traverse the hint selection idx
|
||||
int selection_idx = -1;
|
||||
{
|
||||
for(int i = 0; i < filament_items.size(); i++)
|
||||
{
|
||||
if (hint_filament_name == filament_items[i])
|
||||
{
|
||||
selection_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_comboBox_filament->Set(filament_items);
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
rebuild_filament_dropdown_items(ams_filament_id);
|
||||
int selection_idx = m_comboBox_filament->GetSelection();
|
||||
post_select_event(selection_idx);
|
||||
|
||||
if (selection_idx < 0) {
|
||||
@@ -1289,6 +1420,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
||||
|
||||
m_filament_type = "";
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
std::ostringstream stream;
|
||||
@@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
||||
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
||||
if (!selected.empty()) {
|
||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
||||
if (selected_info) {
|
||||
const std::string& filament_id = selected_info->filament_id;
|
||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (it->filament_id != filament_id)
|
||||
@@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
//reset cali
|
||||
int cali_select_idx = -1;
|
||||
|
||||
if ( !this->obj || m_filament_selection < 0) {
|
||||
if ( !this->obj || m_filament_selection < 0 || !selected_info) {
|
||||
m_input_k_val->Enable(false);
|
||||
m_input_n_val->Enable(false);
|
||||
m_button_confirm->Disable(); // ORCA No need to change style
|
||||
@@ -1383,21 +1514,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
ams_filament_id = "";
|
||||
ams_setting_id = "";
|
||||
|
||||
if (preset_bundle) {
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
|
||||
if ( itor != map_filament_items.end()) {
|
||||
ams_filament_id = itor->second.filament_id;
|
||||
ams_setting_id = itor->second.setting_id;
|
||||
break;
|
||||
}
|
||||
|
||||
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
|
||||
ams_filament_id = it->filament_id;
|
||||
ams_setting_id = it->setting_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected_info) {
|
||||
ams_filament_id = selected_info->filament_id;
|
||||
ams_setting_id = selected_info->setting_id;
|
||||
}
|
||||
|
||||
wxArrayString items;
|
||||
@@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
||||
m_button_reset->Rescale(); // ORCA
|
||||
m_button_confirm->Rescale(); // ORCA
|
||||
m_button_close->Rescale(); // ORCA
|
||||
// An open list would keep the measurements of the old stars, so close it before replacing them.
|
||||
m_comboBox_filament->CloseDropdown();
|
||||
m_star_filled.msw_rescale();
|
||||
m_star_empty.msw_rescale();
|
||||
update_favorite_stars();
|
||||
this->Refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include <wx/panel.h>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
||||
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
||||
@@ -156,6 +157,7 @@ protected:
|
||||
void create_panel_kn(wxWindow* parent);
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||
void on_select_filament(wxCommandEvent& evt);
|
||||
void on_toggle_favorite_filament(wxCommandEvent& evt);
|
||||
void on_select_cali_result(wxCommandEvent &evt);
|
||||
void on_select_ok(wxCommandEvent &event);
|
||||
void on_select_reset(wxCommandEvent &event);
|
||||
@@ -163,6 +165,11 @@ protected:
|
||||
void on_clr_picker(wxMouseEvent &event);
|
||||
bool is_virtual_tray();
|
||||
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);
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
#endif
|
||||
ComboBox * m_comboBox_cali_result;
|
||||
TextInput* m_readonly_filament;
|
||||
|
||||
@@ -215,7 +217,20 @@ protected:
|
||||
std::string filament_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);
|
||||
|
||||
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
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();
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
GLCanvas3D* cnv = plater->canvas3D();
|
||||
|
||||
switch (type) {
|
||||
case InfoItemType::CustomSupports:
|
||||
case InfoItemType::CustomSupports: {
|
||||
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)
|
||||
mv->supported_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove CustomSeam
|
||||
|
||||
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
}
|
||||
break;
|
||||
|
||||
case InfoItemType::MmSegmentation:
|
||||
case InfoItemType::MmSegmentation: {
|
||||
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)
|
||||
mv->mmu_segmentation_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
case InfoItemType::FuzzySkin:
|
||||
case InfoItemType::FuzzySkin: {
|
||||
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)
|
||||
mv->fuzzy_skin_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove Sinking
|
||||
case InfoItemType::Undef : assert(false); break;
|
||||
@@ -6718,7 +6721,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
||||
else if (col == colSinking) {
|
||||
Plater * plater = wxGetApp().plater();
|
||||
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;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <cstdio>
|
||||
#include <glad/gl.h>
|
||||
#include <imgui.h>
|
||||
@@ -121,14 +120,6 @@ constexpr int THUMBNAIL_MAX_PX = 128;
|
||||
// blur of it - which is what made Fast look flatter and softer than the result it was previewing.
|
||||
constexpr int HEIGHT_TEX_MAX_PX = 2048;
|
||||
|
||||
// Size the image-adjustment panel's live preview is computed and uploaded at. The panel draws the image
|
||||
// in a box about 240 px wide, so there is nothing to show above this - and the preview is recomputed on
|
||||
// every slider move, which is what makes its cost worth bounding. Measured on a 1024x1024 with every
|
||||
// knob engaged, a full-size pass is 13-19 ms grayscale / 40-60 ms in colour (130-190 ms at 2048 in
|
||||
// colour); the same pass on a 384 px copy is 3-4 ms / 9-12 ms, and it runs off the UI thread besides.
|
||||
// The full-size image is only ever adjusted once, on commit.
|
||||
constexpr int ADJUST_PREVIEW_MAX_PX = 384;
|
||||
|
||||
// How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine
|
||||
// band (see collect_paint_region()). Two is enough to grade the size change without the band's own
|
||||
// cost growing to matter: it is a ring around a perimeter, not an area.
|
||||
@@ -534,10 +525,6 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
||||
// progress poll, and its completion handler then finds nothing to do.
|
||||
m_preview_generation->fetch_add(1);
|
||||
m_preview_job_pending = false;
|
||||
// The adjustment panel holds a full-size decode of its layer's image, which is the largest thing
|
||||
// here; a closed gizmo should not keep it alive. Discards the edit, as closing the gizmo on an open
|
||||
// panel implies.
|
||||
close_image_adjust();
|
||||
// The palette caches hold the last volume's images; a closed gizmo should not keep them alive.
|
||||
m_palette_cache.clear();
|
||||
m_palette_quantizer = nullptr;
|
||||
@@ -2463,12 +2450,9 @@ void GLGizmoTextureDisplacement::update_uv_editor()
|
||||
// before, and independently of, the unwrap - the texture belongs on screen before anything is unwrapped - and
|
||||
// only when the choice, the layer's image or its smoothing changed, because the height image is large.
|
||||
const UVBackground desired_bg = (m_uv_check_mode == UVCheckMode::Checker) ? UVBackground::Checker : UVBackground::Height;
|
||||
const bool bg_smoothing_changed = (desired_bg == UVBackground::Height) &&
|
||||
(m_uv_editor_bg_smoothing != layer->smoothing ||
|
||||
!(m_uv_editor_bg_adjust == layer->preview_adjust));
|
||||
const bool bg_smoothing_changed = (desired_bg == UVBackground::Height) && (m_uv_editor_bg_smoothing != layer->smoothing);
|
||||
if (desired_bg != m_uv_editor_bg || layer->image_data.get() != m_uv_editor_bg_image || bg_smoothing_changed) {
|
||||
m_uv_editor_bg_smoothing = layer->smoothing;
|
||||
m_uv_editor_bg_adjust = layer->preview_adjust;
|
||||
m_uv_editor_bg_image = layer->image_data.get();
|
||||
m_uv_editor_bg = desired_bg;
|
||||
if (desired_bg == UVBackground::Checker) {
|
||||
@@ -3574,6 +3558,7 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const
|
||||
|
||||
void GLGizmoTextureDisplacement::update_model_object()
|
||||
{
|
||||
store_panel_state();
|
||||
bool updated = false;
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
int idx = -1;
|
||||
@@ -3609,6 +3594,24 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
||||
{
|
||||
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();
|
||||
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
|
||||
@@ -3657,6 +3660,14 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
||||
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)
|
||||
{
|
||||
if (slot == m_active_layer_slot)
|
||||
@@ -3665,6 +3676,7 @@ void GLGizmoTextureDisplacement::set_active_layer(int slot)
|
||||
// reflect - otherwise they would be silently lost.
|
||||
update_model_object();
|
||||
m_active_layer_slot = slot;
|
||||
store_panel_state();
|
||||
update_from_model_object(false);
|
||||
// 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.
|
||||
@@ -3694,7 +3706,7 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
|
||||
"menu_obj_sphere.svg", "menu_obj_cube.svg", "menu_obj_cylinder.svg", "texture_displacement_map_unwrap.svg",
|
||||
"texture_displacement_map_view.svg", "texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
|
||||
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
|
||||
"texture_displacement_move_down.svg", "texture_displacement_drag.svg", "cog.svg",
|
||||
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
|
||||
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
|
||||
};
|
||||
std::vector<std::string> paths;
|
||||
@@ -3998,321 +4010,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
m_picker_open_request = true;
|
||||
}
|
||||
}
|
||||
bool GLGizmoTextureDisplacement::load_image_adjust_source(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (layer.empty())
|
||||
return false;
|
||||
|
||||
// The *raw* image: neither the layer's Smoothing nor the panel's own preview_adjust, so what the
|
||||
// sliders act on is always the image as it is actually stored rather than the last preview of it.
|
||||
// Without clearing preview_adjust here the adjustment would compound on itself every reload.
|
||||
TextureDisplacementLayer raw = layer;
|
||||
raw.smoothing = 0.f;
|
||||
raw.preview_adjust = TextureAdjustments{};
|
||||
|
||||
DecodedHeightTexture decoded = decode_height_texture(raw);
|
||||
if (decoded.empty())
|
||||
return false;
|
||||
|
||||
m_adjust_source = std::move(decoded);
|
||||
m_adjust_preview_source = downscale_height_texture(m_adjust_source, ADJUST_PREVIEW_MAX_PX);
|
||||
m_adjust_source_image = layer.image_data.get();
|
||||
// The unadjusted image, so the box is filled now rather than one background pass later. Pairing it
|
||||
// with an identity `shown` is what makes update_image_adjust_preview() relaunch for the sliders'
|
||||
// current values against the new source instead of deciding it is already showing them.
|
||||
m_adjust_preview_shown = TextureAdjustments{};
|
||||
m_adjust_preview_tex = upload_height_thumbnail(m_adjust_preview_source, ADJUST_PREVIEW_MAX_PX);
|
||||
m_adjust_preview_valid = m_adjust_preview_tex != nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::open_image_adjust(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (layer.empty() || layer.slot < 0)
|
||||
return;
|
||||
|
||||
close_image_adjust(); // reverts and clears any previous panel
|
||||
|
||||
if (!load_image_adjust_source(layer))
|
||||
return;
|
||||
|
||||
m_adjust = TextureAdjustments{};
|
||||
m_adjust_applied = TextureAdjustments{};
|
||||
m_adjust_slot = layer.slot;
|
||||
const ModelVolume *mv = texture_volume();
|
||||
m_adjust_volume_id = mv != nullptr ? mv->id().id : 0;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::close_image_adjust()
|
||||
{
|
||||
// Revert the layer's live adjustment. Every exit that reaches here is a discard: a commit resets
|
||||
// preview_adjust itself, before pointing the layer at the file that already carries the edit, so this
|
||||
// cannot undo a committed adjustment.
|
||||
//
|
||||
// Found by the id recorded when the panel opened rather than through texture_volume(), which answers
|
||||
// for the *current* selection. One of the ways to get here is the selection having moved on, and
|
||||
// reverting the newly selected object's layer instead would both miss the adjustment that is still
|
||||
// live on the original volume and clear one the user never touched.
|
||||
bool reverted = false;
|
||||
if (m_adjust_slot >= 0 && m_adjust_volume_id != 0)
|
||||
for (ModelObject *mo : wxGetApp().model().objects) {
|
||||
for (ModelVolume *mv : mo->volumes) {
|
||||
if (mv->id().id != m_adjust_volume_id)
|
||||
continue;
|
||||
for (TextureDisplacementLayer &l : mv->texture_displacement_layers)
|
||||
if (l.slot == m_adjust_slot && !l.preview_adjust.identity()) {
|
||||
l.preview_adjust = TextureAdjustments{};
|
||||
reverted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// std::async's future blocks here until the pass finishes. That is a few milliseconds on an image
|
||||
// this small, and the alternative - a detached thread writing into the members of a closed panel -
|
||||
// is worse.
|
||||
if (m_adjust_preview_job.valid())
|
||||
m_adjust_preview_job.wait();
|
||||
m_adjust_preview_job = {};
|
||||
|
||||
m_adjust_slot = -1;
|
||||
m_adjust = TextureAdjustments{};
|
||||
m_adjust_applied = TextureAdjustments{};
|
||||
m_adjust_source = DecodedHeightTexture{};
|
||||
m_adjust_preview_source = DecodedHeightTexture{};
|
||||
m_adjust_source_image = nullptr;
|
||||
m_adjust_volume_id = 0;
|
||||
m_adjust_preview_tex.reset();
|
||||
m_adjust_preview_valid = false;
|
||||
|
||||
// Only while the gizmo is up: on_shutdown() also comes through here, and rebuilding a preview for a
|
||||
// gizmo that is closing would be work for a model nobody is looking at. The reverted layer data is
|
||||
// what matters there, and that is already done above.
|
||||
if (reverted && m_state == On) {
|
||||
rebuild_preview();
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::update_image_adjust_preview()
|
||||
{
|
||||
if (m_adjust_preview_source.empty())
|
||||
return;
|
||||
|
||||
// Collect a finished pass. wait_for(0) rather than get() outright: this runs inside the frame, and
|
||||
// blocking on the worker here would hand back to the UI thread the very stall the worker exists to
|
||||
// spare it.
|
||||
if (m_adjust_preview_job.valid() &&
|
||||
m_adjust_preview_job.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
|
||||
const DecodedHeightTexture adjusted = m_adjust_preview_job.get();
|
||||
m_adjust_preview_job = {};
|
||||
if (std::unique_ptr<GLTexture> tex = upload_height_thumbnail(adjusted, ADJUST_PREVIEW_MAX_PX)) {
|
||||
m_adjust_preview_tex = std::move(tex);
|
||||
m_adjust_preview_shown = m_adjust_preview_running;
|
||||
m_adjust_preview_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// One pass at a time: whatever the sliders did while it ran is picked up by the next launch, so a
|
||||
// drag collapses to the value the sliders ended on instead of queuing one pass per frame.
|
||||
if (!m_adjust_preview_job.valid() && m_adjust != m_adjust_preview_shown) {
|
||||
m_adjust_preview_running = m_adjust;
|
||||
// Captured by value: the pass must not read members the UI thread goes on editing while it runs.
|
||||
m_adjust_preview_job = std::async(std::launch::async,
|
||||
[src = m_adjust_preview_source, adj = m_adjust_preview_running] {
|
||||
return apply_texture_adjustments(src, adj);
|
||||
});
|
||||
}
|
||||
|
||||
// A pass in flight has no other way to get its result on screen: without a redraw request the canvas
|
||||
// can sit idle, and the preview would only catch up on the next unrelated frame.
|
||||
if (m_adjust_preview_job.valid())
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::commit_image_adjust(TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (m_adjust_source.empty() || m_adjust.identity()) {
|
||||
// Nothing was changed, so there is nothing to save.
|
||||
close_image_adjust();
|
||||
return;
|
||||
}
|
||||
|
||||
// The full-size pass, once: 13-19 ms grayscale / 40-60 ms colour on a 1024x1024 with every knob
|
||||
// engaged. Run on the UI thread deliberately, unlike the live preview: this is a single click, and the
|
||||
// rebuild that follows it - a CPU mesh displacement of the painted patch - is already an order of
|
||||
// magnitude more work, synchronously, so moving only this off the thread would buy nothing a user
|
||||
// could perceive.
|
||||
const DecodedHeightTexture adjusted = apply_texture_adjustments(m_adjust_source, m_adjust);
|
||||
|
||||
std::string error;
|
||||
const std::string stem = layer.name.empty() ? std::string("texture") : layer.name;
|
||||
const std::optional<TextureLibraryEntry> entry = save_texture_to_library(adjusted, stem, error);
|
||||
if (!entry) {
|
||||
show_error(nullptr, error);
|
||||
return; // the panel stays open, and the layer keeps the live adjustment: the edit is not lost
|
||||
}
|
||||
|
||||
// Cleared *before* the layer is pointed at the new file, and this ordering is the whole correctness of
|
||||
// the commit: the saved image already has the adjustment baked into its pixels, so leaving
|
||||
// preview_adjust set would apply brightness, contrast and the rest a second time on top of itself.
|
||||
layer.preview_adjust = TextureAdjustments{};
|
||||
// set_layer_texture() does the rest - the name, the recently-used ordering, a fresh image_data buffer
|
||||
// (which is what invalidates every cache keyed by the old pointer), the undo snapshot and the rebuild.
|
||||
set_layer_texture(layer, *entry);
|
||||
|
||||
close_image_adjust();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_image_adjust_window(const ImVec2 &panel_min, const ImVec2 &panel_max)
|
||||
{
|
||||
if (m_adjust_slot < 0)
|
||||
return;
|
||||
|
||||
ModelVolume *mv = texture_volume();
|
||||
// A different volume is selected than the one this panel was opened on. Slots are numbered per
|
||||
// volume, so looking m_adjust_slot up here would find an unrelated layer rather than nothing.
|
||||
if (mv == nullptr || mv->id().id != m_adjust_volume_id) {
|
||||
close_image_adjust();
|
||||
return;
|
||||
}
|
||||
|
||||
TextureDisplacementLayer *layer = nullptr;
|
||||
for (TextureDisplacementLayer &l : mv->texture_displacement_layers)
|
||||
if (l.slot == m_adjust_slot)
|
||||
layer = &l;
|
||||
if (layer == nullptr) {
|
||||
// The layer went away under the panel (removed, or merged away by an undo).
|
||||
close_image_adjust();
|
||||
return;
|
||||
}
|
||||
|
||||
// The layer's image can be swapped from the texture picker while this panel is open - the panel is a
|
||||
// window now, so the rest of the gizmo stays usable behind it. Re-read the source when that happens,
|
||||
// keeping the slider positions: the user is adjusting this layer, and the 3D view has already moved to
|
||||
// the new image (decode_height_texture() reads layer.image_data, which changed), so re-reading here is
|
||||
// also what keeps the panel's photo and the model showing the same thing.
|
||||
if (layer->image_data.get() != m_adjust_source_image && !load_image_adjust_source(*layer)) {
|
||||
close_image_adjust(); // the new image cannot be decoded; nothing left to adjust
|
||||
return;
|
||||
}
|
||||
|
||||
const float img_w = std::round(m_imgui->scaled(16.f));
|
||||
const float gap = m_imgui->scaled(0.5f);
|
||||
// Placed beside the panel on first appearance only, and left wherever the user drags it after that -
|
||||
// it is a window, not a popup, so its position is theirs to keep.
|
||||
const float win_w = img_w + 2.f * ImGui::GetStyle().WindowPadding.x;
|
||||
const bool left = panel_min.x - gap - win_w >= 0.f;
|
||||
ImGui::SetNextWindowPos(ImVec2(left ? panel_min.x - gap - win_w : panel_max.x + gap,
|
||||
panel_min.y + m_imgui->scaled(4.f)), ImGuiCond_Appearing);
|
||||
|
||||
bool keep_open = true;
|
||||
if (!ImGui::Begin((_u8L("Adjust image") + "##texture_adjust").c_str(), &keep_open,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse)) {
|
||||
ImGui::End();
|
||||
if (!keep_open)
|
||||
close_image_adjust();
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect a finished pass and start one for the current slider values before the image is drawn, so
|
||||
// the newest available result is what lands on screen this frame.
|
||||
update_image_adjust_preview();
|
||||
|
||||
// The photo itself, at the image's own aspect so a non-square texture is not stretched.
|
||||
const float aspect = m_adjust_source.height > 0 ? float(m_adjust_source.width) / float(m_adjust_source.height) : 1.f;
|
||||
const float img_h = std::round(img_w / std::max(0.05f, aspect));
|
||||
if (m_adjust_preview_valid && m_adjust_preview_tex)
|
||||
ImGui::Image((ImTextureID) (intptr_t) m_adjust_preview_tex->get_id(), ImVec2(img_w, img_h));
|
||||
else
|
||||
ImGui::Dummy(ImVec2(img_w, img_h));
|
||||
|
||||
// The label column is sized off the widest of the five labels, so the tracks line up whatever the
|
||||
// translation does to their lengths.
|
||||
float label_w = 0.f;
|
||||
for (const wxString &l : { _L("Brightness"), _L("Contrast"), _L("Sharpness"), _L("Blur"), _L("Hue") })
|
||||
label_w = std::max(label_w, ImGui::CalcTextSize(into_u8(l).c_str()).x);
|
||||
label_w += m_imgui->scaled(1.2f);
|
||||
// An explicit track width rather than a fill-to-the-right one: this window is AlwaysAutoResize, so its
|
||||
// width comes from its content, and an item asking for "whatever is left" would make the width depend
|
||||
// on itself. The image above sets the content width, and the sliders match it.
|
||||
const float slider_w = std::max(m_imgui->scaled(4.f), img_w - label_w);
|
||||
|
||||
const auto row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max,
|
||||
const char *format, bool enabled, const std::string &tip) {
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
ImGui::SetNextItemWidth(slider_w);
|
||||
m_imgui->disabled_begin(!enabled);
|
||||
ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
|
||||
m_imgui->disabled_end();
|
||||
// Only on an enabled row: a disabled item does not report hover, so a tooltip on one would never
|
||||
// be shown. What has to be said about a disabled slider is written out below it instead.
|
||||
if (enabled && !tip.empty() && ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(tip, ImGui::GetFontSize() * 20.f);
|
||||
};
|
||||
|
||||
row("##adj_brightness", _L("Brightness"), &m_adjust.brightness, -1.f, 1.f, "%.2f", true, {});
|
||||
row("##adj_contrast", _L("Contrast"), &m_adjust.contrast, -1.f, 1.f, "%.2f", true, {});
|
||||
row("##adj_sharpness", _L("Sharpness"), &m_adjust.sharpness, 0.f, 1.f, "%.2f", true,
|
||||
_u8L("Brings out the finest detail in the image. Applied after Blur, so the two can be used "
|
||||
"together: blur away the speckle, then sharpen the edges that remain."));
|
||||
row("##adj_blur", _L("Blur"), &m_adjust.blur, 0.f, 1.f, "%.2f", true,
|
||||
_u8L("Blurs the image itself. The layer's own Smoothing slider softens the relief without altering "
|
||||
"the image, and is the one to reach for first."));
|
||||
// Grey has no hue to turn, and the shipped library is grayscale - so on most textures this is a
|
||||
// slider that would visibly do nothing. Disabled with the reason given, rather than hidden.
|
||||
const bool has_color = m_adjust_source.has_color();
|
||||
row("##adj_hue", _L("Hue"), &m_adjust.hue_deg, -180.f, 180.f, "%.0f°", has_color, {});
|
||||
if (!has_color) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + img_w);
|
||||
ImGui::TextDisabled("%s", _u8L("This texture is grayscale, so it has no hue to shift.").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
if (m_imgui->button(_L("Reset")))
|
||||
m_adjust = TextureAdjustments{};
|
||||
|
||||
// Hand the sliders to the layer, which is what puts the adjusted image in front of the displacement:
|
||||
// decode_height_texture() applies preview_adjust, so the relief, the colour and the thumbnails all
|
||||
// follow from here. Written every frame rather than only on a change, so an undo snapshot or a
|
||||
// reselection that resets the layer cannot leave the model showing an adjustment the panel has moved
|
||||
// on from.
|
||||
layer->preview_adjust = m_adjust;
|
||||
if (m_adjust != m_adjust_applied) {
|
||||
m_adjust_applied = m_adjust;
|
||||
// Marked dirty rather than rebuilt here: the panel's own flag is consumed by the main panel's
|
||||
// "rebuild once the mouse is released" rule, so dragging a slider recomputes the displacement
|
||||
// once, on release, instead of on every frame of the drag. That is what keeps the UI responsive
|
||||
// while a full-size resample and a mesh displacement happen behind it.
|
||||
m_preview_params_dirty = true;
|
||||
// This window is drawn after the block that consumes that flag, so the rebuild happens on a later
|
||||
// frame - which means there has to *be* one. Cheap insurance: a slider release that generated no
|
||||
// further canvas event would otherwise leave the flag set and the model unchanged until something
|
||||
// unrelated redrew.
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
m_imgui->text_wrapped(_u8L("The model updates when you release a slider. Save as copy writes the "
|
||||
"adjusted image to your texture library and points this layer at it."), img_w);
|
||||
|
||||
const bool cancel = m_imgui->button(_L("Cancel"));
|
||||
ImGui::SameLine();
|
||||
// Nothing to save until a slider has actually moved.
|
||||
m_imgui->disabled_begin(m_adjust.identity());
|
||||
const bool as_copy = m_imgui->button(_L("Save as copy"));
|
||||
m_imgui->disabled_end();
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (cancel || !keep_open)
|
||||
close_image_adjust();
|
||||
else if (as_copy)
|
||||
commit_image_adjust(*layer);
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::remove_texture_layer(int slot)
|
||||
{
|
||||
@@ -4371,7 +4068,6 @@ void GLGizmoTextureDisplacement::swap_layer_slots(int slot_a, int slot_b)
|
||||
std::swap(m_thumbnails[a], m_thumbnails[b]);
|
||||
std::swap(m_thumbnail_source[a], m_thumbnail_source[b]);
|
||||
std::swap(m_thumbnail_smoothing[a], m_thumbnail_smoothing[b]);
|
||||
std::swap(m_thumbnail_adjust[a], m_thumbnail_adjust[b]);
|
||||
std::swap(m_layer_expanded[a], m_layer_expanded[b]);
|
||||
if (m_active_layer_slot == slot_a)
|
||||
m_active_layer_slot = slot_b;
|
||||
@@ -5635,7 +5331,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
||||
|
||||
const size_t slot = size_t(layer.slot);
|
||||
if (m_thumbnails[slot] && m_thumbnail_source[slot] == layer.image_data.get() &&
|
||||
m_thumbnail_smoothing[slot] == layer.smoothing && m_thumbnail_adjust[slot] == layer.preview_adjust)
|
||||
m_thumbnail_smoothing[slot] == layer.smoothing)
|
||||
return m_thumbnails[slot].get();
|
||||
|
||||
// Reuses the already-decoded, already-cached grayscale pixels (see decode_height_texture()'s
|
||||
@@ -5649,7 +5345,6 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
||||
m_thumbnails[slot] = std::move(texture);
|
||||
m_thumbnail_source[slot] = layer.image_data.get();
|
||||
m_thumbnail_smoothing[slot] = layer.smoothing;
|
||||
m_thumbnail_adjust[slot] = layer.preview_adjust;
|
||||
return m_thumbnails[slot].get();
|
||||
}
|
||||
|
||||
@@ -5657,8 +5352,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
|
||||
{
|
||||
if (layer.empty() || !layer.color_enabled)
|
||||
return nullptr;
|
||||
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing &&
|
||||
m_color_tex_adjust == layer.preview_adjust)
|
||||
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing)
|
||||
return m_color_tex.get();
|
||||
|
||||
std::unique_ptr<GLTexture> texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
||||
@@ -5670,7 +5364,6 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
|
||||
m_color_tex = std::move(texture);
|
||||
m_color_tex_source = layer.image_data.get();
|
||||
m_color_tex_smoothing = layer.smoothing;
|
||||
m_color_tex_adjust = layer.preview_adjust;
|
||||
return m_color_tex.get();
|
||||
}
|
||||
|
||||
@@ -5681,8 +5374,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis
|
||||
|
||||
// One slot, not one per layer: the preview shader only ever shades the *active* layer, so a single
|
||||
// full-resolution upload is enough and the VRAM cost stays at one texture rather than eight.
|
||||
if (m_height_tex && m_height_tex_source == layer.image_data.get() && m_height_tex_smoothing == layer.smoothing &&
|
||||
m_height_tex_adjust == layer.preview_adjust)
|
||||
if (m_height_tex && m_height_tex_source == layer.image_data.get() && m_height_tex_smoothing == layer.smoothing)
|
||||
return m_height_tex.get();
|
||||
|
||||
std::unique_ptr<GLTexture> texture = upload_height_thumbnail(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
||||
@@ -5692,7 +5384,6 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_height_texture(const TextureDis
|
||||
m_height_tex = std::move(texture);
|
||||
m_height_tex_source = layer.image_data.get();
|
||||
m_height_tex_smoothing = layer.smoothing;
|
||||
m_height_tex_adjust = layer.preview_adjust;
|
||||
return m_height_tex.get();
|
||||
}
|
||||
|
||||
@@ -6323,6 +6014,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"not what you wanted and you know why."));
|
||||
if (clicked && !on) {
|
||||
m_panel_mode = m;
|
||||
store_panel_state();
|
||||
if (!pro_mode()) {
|
||||
// Leaving the subdivision preview open would strand a wireframe whose controls just
|
||||
// disappeared, so close it as part of the switch.
|
||||
@@ -6345,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 "
|
||||
"the toolbar.")))
|
||||
m_undocked = !m_undocked;
|
||||
wxGetApp().app_config->set_bool(UNDOCKED_CONFIG_KEY, m_undocked);
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -6749,7 +6442,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine();
|
||||
|
||||
const std::string not_painted = _u8L("not painted");
|
||||
const float right_w = (painted ? 0.f : ImGui::CalcTextSize(not_painted.c_str()).x + gap_s) + 4.f * frame_h + 3.f * gap_s;
|
||||
const float right_w = (painted ? 0.f : ImGui::CalcTextSize(not_painted.c_str()).x + gap_s) + 3.f * frame_h + 2.f * gap_s;
|
||||
const float name_w = std::max(frame_h, ImGui::GetWindowContentRegionMax().x - card_pad - ImGui::GetCursorPosX() - gap_s - right_w);
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(orca.x, orca.y, orca.z, 0.2f));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(orca.x, orca.y, orca.z, 0.35f));
|
||||
@@ -6763,16 +6456,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", not_painted.c_str());
|
||||
}
|
||||
// Between the name and the order arrows: it acts on the image the name refers to, so it
|
||||
// belongs with the name rather than with the controls that reorder the stack.
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(busy || layer.empty());
|
||||
if (icon_button(613 + layer.slot, "cog.svg", frame_h, _L("Adjust image"),
|
||||
_u8L("Brightness, contrast, sharpness, blur and hue, shown on the image and on the "
|
||||
"model as you set them. Save as copy writes the result to your texture library "
|
||||
"and points this layer at it.")))
|
||||
open_image_adjust(layer);
|
||||
m_imgui->disabled_end();
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(busy || li == 0);
|
||||
if (icon_button(611, "texture_displacement_move_up.svg", frame_h, _L("Move up"), _L("Move up - applied earlier"))) {
|
||||
@@ -7572,15 +7255,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
set_active_layer(m_picker_slot);
|
||||
|
||||
// In this window's scope, where the popup's open request and the popup itself share one ID stack.
|
||||
ImVec2 panel_rect_min, panel_rect_max;
|
||||
{
|
||||
const ImVec2 win_min = ImGui::GetWindowPos();
|
||||
const ImVec2 win_sz = ImGui::GetWindowSize();
|
||||
render_texture_library_popup(win_min, ImVec2(win_min.x + win_sz.x, win_min.y + win_sz.y));
|
||||
// Only recorded here. The image-adjustment panel is a top-level window rather than a popup, so it
|
||||
// cannot be begun inside this one; it is drawn after GizmoImguiEnd() below.
|
||||
panel_rect_min = win_min;
|
||||
panel_rect_max = ImVec2(win_min.x + win_sz.x, win_min.y + win_sz.y);
|
||||
}
|
||||
|
||||
// ImGui sliders report "changed" continuously on every frame while being dragged, not just once on
|
||||
@@ -7817,11 +7495,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_parent.request_extra_frame();
|
||||
}
|
||||
GizmoImguiEnd();
|
||||
|
||||
// Outside the gizmo's window, because it is a window of its own: a floating, non-modal panel that
|
||||
// leaves the 3D view live so the relief can be watched while the sliders move. No-op when closed.
|
||||
render_image_adjust_window(panel_rect_min, panel_rect_max);
|
||||
|
||||
ImGuiWrapper::pop_toolbar_style();
|
||||
|
||||
// Drawn last, over everything, via the foreground draw list: the +/- add-remove sign next to the 3D
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <imgui.h>
|
||||
#include <map>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
|
||||
@@ -263,6 +262,9 @@ private:
|
||||
// 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.
|
||||
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.
|
||||
// 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
|
||||
@@ -400,68 +402,6 @@ private:
|
||||
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
||||
void import_custom_texture(TextureDisplacementLayer &layer);
|
||||
|
||||
// ---- The image-adjustment panel ----
|
||||
//
|
||||
// Brightness/contrast/sharpness/blur/hue over one layer's image, shown on the image itself and
|
||||
// updating as the sliders move. What it produces is not a set of layer parameters: it replaces the
|
||||
// layer's *image*, so the adjusted pixels feed the colour and the displacement together and a
|
||||
// brightness change moves the relief too (see TextureAdjustments).
|
||||
//
|
||||
// An ordinary floating window rather than a popup, and deliberately so. A popup dims the canvas
|
||||
// behind it and closes on any click outside itself, which between them make it impossible to watch the
|
||||
// relief change on the model - which is the whole point of adjusting the image here rather than in an
|
||||
// image editor. A window leaves the 3D view live, clickable and orbitable while the panel is open.
|
||||
void render_image_adjust_window(const ImVec2 &panel_min, const ImVec2 &panel_max);
|
||||
// Opens the panel for `layer`, decoding its image into m_adjust_source. Sliders never re-decode
|
||||
// the PNG.
|
||||
void open_image_adjust(const TextureDisplacementLayer &layer);
|
||||
// Reads `layer`'s stored image into the panel's source buffers and records which image it came from.
|
||||
// Called when the panel opens and again whenever the layer's image is swapped under it, which is why
|
||||
// it leaves the slider values alone. False when the image cannot be decoded.
|
||||
bool load_image_adjust_source(const TextureDisplacementLayer &layer);
|
||||
// Resolves the panel: writes the adjusted image into the user's texture library as a new file and
|
||||
// points `layer` at it, which is the only way an adjustment becomes permanent. Closes the panel.
|
||||
void commit_image_adjust(TextureDisplacementLayer &layer);
|
||||
// Closes the panel and reverts the layer's live preview_adjust to identity, so every exit that is not
|
||||
// a commit - Cancel, the window's close button, the gizmo closing - leaves the layer as it was found.
|
||||
void close_image_adjust();
|
||||
// Collects a finished preview and starts one for the current slider values. One at a time, so a drag
|
||||
// coalesces into "whatever the sliders last said" instead of a queue of superseded frames.
|
||||
void update_image_adjust_preview();
|
||||
|
||||
int m_adjust_slot = -1; // the layer the panel is open for; -1 when closed
|
||||
TextureAdjustments m_adjust; // what the sliders currently say
|
||||
// What the layer's live preview_adjust was last set from, so a slider move can be told from a frame in
|
||||
// which nothing happened and only the former asks the displacement to be recomputed.
|
||||
TextureAdjustments m_adjust_applied;
|
||||
|
||||
// The layer's image as it was when the panel opened, and a small copy of it. The sliders are applied
|
||||
// to the small copy for the live preview and to the full-size one exactly once, on commit. Measured on
|
||||
// a 1024x1024 with every knob engaged, a full-size pass costs 13-19 ms grayscale and 40-60 ms in
|
||||
// colour (130-190 ms at 2048 in colour) - far too much to repeat per slider frame. The same pass on a
|
||||
// 384 px copy is 3-4 ms / 9-12 ms, and that is all the preview box can show anyway.
|
||||
DecodedHeightTexture m_adjust_source;
|
||||
DecodedHeightTexture m_adjust_preview_source;
|
||||
// Which image_data the two buffers above were decoded from, so the panel can notice the layer's image
|
||||
// being swapped from the texture picker while it is open and re-read it. Compared, never dereferenced.
|
||||
const void *m_adjust_source_image = nullptr;
|
||||
// And which volume the panel was opened on. Slot numbers are per volume, so without this a change of
|
||||
// selection would silently rebind the open panel to whichever layer happens to hold the same slot on
|
||||
// the newly selected object - a different image, under a panel the user opened for this one.
|
||||
size_t m_adjust_volume_id = 0;
|
||||
// The preview image on screen, re-uploaded whenever a background pass finishes.
|
||||
std::unique_ptr<GLTexture> m_adjust_preview_tex;
|
||||
|
||||
// The live preview runs on its own thread rather than on the UI thread or on the plater's job
|
||||
// worker. Not the UI thread because even the small pass is milliseconds and this runs while a slider
|
||||
// is being dragged; not the job worker because that is a single FIFO shared with Bake and the
|
||||
// displacement preview (see queue_preview_job()), where a per-frame image filter would sit behind -
|
||||
// and delay - multi-second mesh work.
|
||||
std::future<DecodedHeightTexture> m_adjust_preview_job;
|
||||
TextureAdjustments m_adjust_preview_running; // what the in-flight pass is computing
|
||||
TextureAdjustments m_adjust_preview_shown; // what m_adjust_preview_tex holds
|
||||
bool m_adjust_preview_valid = false;
|
||||
|
||||
// Paint / Erase, from the panel. See get_left_button_state_type().
|
||||
bool m_erase_mode = false;
|
||||
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
|
||||
@@ -941,7 +881,6 @@ private:
|
||||
enum class UVBackground { None, Height, Checker };
|
||||
UVBackground m_uv_editor_bg = UVBackground::None;
|
||||
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
|
||||
TextureAdjustments m_uv_editor_bg_adjust; // and the live image adjustment it was uploaded at
|
||||
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
|
||||
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
|
||||
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
|
||||
@@ -985,10 +924,6 @@ private:
|
||||
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
|
||||
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
|
||||
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
|
||||
// And the live image adjustment each was built at, so moving a slider in the image-adjustment panel
|
||||
// re-uploads it. Part of the key for the same reason smoothing is: preview_adjust changes the pixels
|
||||
// decode_height_texture() hands back, and every one of these textures is a copy of those pixels.
|
||||
std::array<TextureAdjustments, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_adjust{};
|
||||
|
||||
// Full-resolution height texture for the preview shader, keyed the same way (see
|
||||
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
|
||||
@@ -996,14 +931,12 @@ private:
|
||||
std::unique_ptr<GLTexture> m_height_tex;
|
||||
const void *m_height_tex_source = nullptr;
|
||||
float m_height_tex_smoothing = -1.f;
|
||||
TextureAdjustments m_height_tex_adjust;
|
||||
|
||||
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
|
||||
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
|
||||
std::unique_ptr<GLTexture> m_color_tex;
|
||||
const void *m_color_tex_source = nullptr;
|
||||
float m_color_tex_smoothing = -1.f;
|
||||
TextureAdjustments m_color_tex_adjust;
|
||||
|
||||
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
||||
std::map<std::string, LibraryTexture> m_library_textures;
|
||||
@@ -1041,9 +974,12 @@ private:
|
||||
PatchUnwrap m_uv_editor_unwrap;
|
||||
|
||||
// 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
|
||||
// open/close within a session, so the choice sticks while working.
|
||||
// grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application
|
||||
// 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;
|
||||
// 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).
|
||||
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include <wx/mstream.h>
|
||||
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
@@ -159,28 +158,6 @@ bool encode_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std
|
||||
: encode_gray_png_bytes(image, out, error);
|
||||
}
|
||||
|
||||
// A wxImage over decoded pixels, so the encoders above can be reached from an in-memory image as well as
|
||||
// from a file. Grayscale is expanded to the three equal channels wxImage always stores, which is what
|
||||
// image_has_color() then reads as "no colour" - so a grey image still encodes as a grayscale PNG.
|
||||
//
|
||||
// `scratch` holds the pixel buffer: the wxImage is constructed over it without taking ownership, so it
|
||||
// has to outlive every use of the returned image.
|
||||
wxImage image_from_decoded(const DecodedHeightTexture &image, std::vector<unsigned char> &scratch)
|
||||
{
|
||||
if (image.empty())
|
||||
return wxImage();
|
||||
|
||||
const size_t n = size_t(image.width) * size_t(image.height);
|
||||
scratch.resize(n * 3);
|
||||
if (image.has_color())
|
||||
scratch.assign(image.rgb.begin(), image.rgb.end());
|
||||
else
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
scratch[i * 3] = scratch[i * 3 + 1] = scratch[i * 3 + 2] = image.pixels[i];
|
||||
|
||||
return wxImage(image.width, image.height, scratch.data(), /* static_data */ true);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> read_file_bytes(const std::string &path)
|
||||
{
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
@@ -209,42 +186,6 @@ bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
std::vector<TextureLibraryEntry> g_library;
|
||||
bool g_library_scanned = false;
|
||||
|
||||
// Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture -
|
||||
// shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new
|
||||
// file's library entry. Shared by import_texture_to_library() and save_texture_to_library(), which
|
||||
// differ only in where their bytes came from.
|
||||
std::optional<TextureLibraryEntry> write_user_texture(const std::vector<unsigned char> &bytes, const std::string &stem,
|
||||
std::string &error)
|
||||
{
|
||||
const std::string user_dir = user_texture_dir();
|
||||
if (user_dir.empty()) {
|
||||
error = _u8L("Could not create the folder for imported textures.");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png");
|
||||
for (int i = 2; boost::filesystem::exists(dest); ++i)
|
||||
dest = boost::filesystem::path(user_dir) / (stem + " (" + std::to_string(i) + ").png");
|
||||
|
||||
{
|
||||
boost::nowide::ofstream ofs(dest.string(), std::ios::binary);
|
||||
ofs.write(reinterpret_cast<const char *>(bytes.data()), std::streamsize(bytes.size()));
|
||||
if (!ofs.good()) {
|
||||
error = _u8L("Failed to save the imported texture.");
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
texture_library(true); // pick the new file up
|
||||
const std::string dest_str = dest.string();
|
||||
for (const TextureLibraryEntry &e : g_library)
|
||||
if (e.path == dest_str)
|
||||
return e;
|
||||
|
||||
error = _u8L("Failed to save the imported texture.");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The user's own textures, most recently used first, kept in the app config. File names are stored rather
|
||||
// than paths so the order survives a moved data directory; '/' separates them because no platform allows
|
||||
// it inside a file name. Bounded so a long-lived config does not grow a line without end.
|
||||
@@ -376,6 +317,12 @@ bool remove_user_texture(const std::string &path, std::string &error)
|
||||
|
||||
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error)
|
||||
{
|
||||
const std::string user_dir = user_texture_dir();
|
||||
if (user_dir.empty()) {
|
||||
error = _u8L("Could not create the folder for imported textures.");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
wxImage image;
|
||||
if (!image.LoadFile(from_u8(source_path)) || !image.IsOk()) {
|
||||
error = _u8L("Could not load the selected image.");
|
||||
@@ -386,7 +333,30 @@ std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &
|
||||
if (!encode_png_bytes(image, bytes, error))
|
||||
return std::nullopt;
|
||||
|
||||
return write_user_texture(bytes, boost::filesystem::path(source_path).stem().string(), error);
|
||||
// Never overwrite an existing texture (the user's or, if they picked the same name twice, their
|
||||
// own earlier import) - uniquify instead.
|
||||
const std::string stem = boost::filesystem::path(source_path).stem().string();
|
||||
boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png");
|
||||
for (int i = 2; boost::filesystem::exists(dest); ++i)
|
||||
dest = boost::filesystem::path(user_dir) / (stem + " (" + std::to_string(i) + ").png");
|
||||
|
||||
{
|
||||
boost::nowide::ofstream ofs(dest.string(), std::ios::binary);
|
||||
ofs.write(reinterpret_cast<const char *>(bytes.data()), std::streamsize(bytes.size()));
|
||||
if (!ofs.good()) {
|
||||
error = _u8L("Failed to save the imported texture.");
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
texture_library(true); // pick the new file up
|
||||
const std::string dest_str = dest.string();
|
||||
for (const TextureLibraryEntry &e : g_library)
|
||||
if (e.path == dest_str)
|
||||
return e;
|
||||
|
||||
error = _u8L("Failed to save the imported texture.");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error)
|
||||
@@ -412,21 +382,4 @@ std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::s
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(converted));
|
||||
}
|
||||
|
||||
std::optional<TextureLibraryEntry> save_texture_to_library(const DecodedHeightTexture &image, const std::string &stem,
|
||||
std::string &error)
|
||||
{
|
||||
std::vector<unsigned char> scratch;
|
||||
const wxImage wx = image_from_decoded(image, scratch);
|
||||
if (!wx.IsOk()) {
|
||||
error = _u8L("The selected image is empty.");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> bytes;
|
||||
if (!encode_png_bytes(wx, bytes, error))
|
||||
return std::nullopt;
|
||||
|
||||
return write_user_texture(bytes, stem.empty() ? std::string("texture") : stem, error);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// One selectable height-map texture in the texture-displacement gizmo's texture picker.
|
||||
@@ -59,14 +57,6 @@ std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &
|
||||
// Returns nullptr with `error` set if the file cannot be read or decoded at all.
|
||||
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error);
|
||||
|
||||
// Writes `image` into user_texture_dir() as a new PNG named after `stem` (uniquified if taken, so an
|
||||
// existing texture is never overwritten) and returns its library entry. The in-memory counterpart of
|
||||
// import_texture_to_library(), for an image that was edited rather than read off disk.
|
||||
//
|
||||
// Returns nullopt with `error` set on failure.
|
||||
std::optional<TextureLibraryEntry> save_texture_to_library(const DecodedHeightTexture &image, const std::string &stem,
|
||||
std::string &error);
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_TextureLibrary_hpp_
|
||||
|
||||
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
|
||||
e.SetId(GetId());
|
||||
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_down = false;
|
||||
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
||||
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
|
||||
int style)
|
||||
{
|
||||
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;
|
||||
items.push_back(item);
|
||||
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(); }
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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,
|
||||
unsigned int pos,
|
||||
void ** clientData,
|
||||
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
{
|
||||
if (pos > this->items.size()) return -1;
|
||||
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);
|
||||
++pos;
|
||||
}
|
||||
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
|
||||
case WXK_UP:
|
||||
case WXK_DOWN:
|
||||
case WXK_LEFT:
|
||||
case WXK_RIGHT:
|
||||
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
|
||||
SetSelection(GetSelection() - 1);
|
||||
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
|
||||
SetSelection(GetSelection() + 1);
|
||||
} else {
|
||||
case WXK_RIGHT: {
|
||||
// Step over disabled rows: the mouse cannot select them either.
|
||||
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
|
||||
int n = GetSelection() + step;
|
||||
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
|
||||
n += step;
|
||||
if (n < 0 || n >= int(items.size()))
|
||||
break;
|
||||
}
|
||||
SetSelection(n);
|
||||
sendComboBoxEvent();
|
||||
break;
|
||||
}
|
||||
case WXK_TAB:
|
||||
HandleAsNavigationKey(event);
|
||||
break;
|
||||
|
||||
@@ -96,10 +96,12 @@ public:
|
||||
|
||||
wxBitmap GetItemBitmap(unsigned int n);
|
||||
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
bool is_drop_down(){return drop_down;}
|
||||
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
||||
|
||||
void ForceDropdownOpen();
|
||||
void CloseDropdown();
|
||||
|
||||
protected:
|
||||
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
|
||||
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
||||
|
||||
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
|
||||
#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)
|
||||
{
|
||||
// save dc
|
||||
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
|
||||
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;
|
||||
// draw texts & icons
|
||||
int index = 0;
|
||||
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
|
||||
auto text = group.IsEmpty()
|
||||
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
||||
: 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()) {
|
||||
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
||||
if (pt.x + tSize.x > rcContent.GetRight()) {
|
||||
if (is_hover && item.tip.IsEmpty())
|
||||
SetToolTip(text);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
|
||||
rcContent.GetRight() - pt.x);
|
||||
int max_text_width = rcContent.GetRight() - pt.x;
|
||||
if (max_text_width <= 0) {
|
||||
text.clear();
|
||||
} 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;
|
||||
dc.SetFont(GetFont());
|
||||
@@ -529,6 +558,7 @@ void DropDown::messureSize()
|
||||
if (!need_sync) return;
|
||||
textSize = wxSize();
|
||||
iconSize = wxSize();
|
||||
actionIconWidth = 0;
|
||||
count = 0;
|
||||
wxClientDC dc(GetParent() ? GetParent() : this);
|
||||
dc.SetFont(GetFont());
|
||||
@@ -565,8 +595,13 @@ void DropDown::messureSize()
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
wxSize szContent = textSize;
|
||||
if (szContent.x < FromDIP(120) && !use_content_width)
|
||||
@@ -636,6 +671,11 @@ void DropDown::messureSize()
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||
e.SetEventObject(this);
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
}
|
||||
need_sync = false;
|
||||
}
|
||||
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
|
||||
if (hover_item < 0)
|
||||
return;
|
||||
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();
|
||||
if (mainDropDown)
|
||||
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
||||
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
|
||||
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()
|
||||
{
|
||||
if (subDropDown && subDropDown->IsShown())
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <wx/event.h>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <wx/gdicmn.h>
|
||||
#include "slic3r/GUI/Widgets/StateColor.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
|
||||
|
||||
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
class DropDown : public PopupWindow
|
||||
{
|
||||
@@ -37,6 +39,7 @@ public:
|
||||
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap icon;
|
||||
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap action_icon;
|
||||
void * data{nullptr};
|
||||
wxString group_key{};
|
||||
wxString group_label{};
|
||||
@@ -65,6 +68,7 @@ private:
|
||||
|
||||
wxSize textSize;
|
||||
wxSize iconSize;
|
||||
int actionIconWidth = 0;
|
||||
wxSize rowSize;
|
||||
|
||||
StateHandler state_handler;
|
||||
@@ -135,6 +139,7 @@ private:
|
||||
int hoverIndex();
|
||||
|
||||
int selectedItem();
|
||||
std::pair<int, int> actionColumn() const;
|
||||
|
||||
friend class ComboBox;
|
||||
void messureSize();
|
||||
@@ -148,6 +153,7 @@ private:
|
||||
void mouseWheelMoved(wxMouseEvent &event);
|
||||
|
||||
void sendDropDownEvent();
|
||||
void sendDropDownActionEvent(int index);
|
||||
|
||||
|
||||
DECLARE_EVENT_TABLE()
|
||||
|
||||
@@ -2043,3 +2043,40 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
@@ -2158,6 +2157,149 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
|
||||
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));
|
||||
}
|
||||
|
||||
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
||||
{
|
||||
TextureDisplacementLayer gray;
|
||||
@@ -2175,291 +2317,3 @@ TEST_CASE("whether a layer's texture has colour agrees with its decode", "[Textu
|
||||
|
||||
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
|
||||
}
|
||||
|
||||
// ---- Image adjustments (the gizmo's image-adjustment panel) ----------------------------------------
|
||||
|
||||
// A grayscale ramp 0..255 across the width, so every input level is present exactly once per row and a
|
||||
// tonal curve can be read straight off the output.
|
||||
static DecodedHeightTexture make_gray_ramp(int w = 256, int h = 4)
|
||||
{
|
||||
DecodedHeightTexture t;
|
||||
t.width = w;
|
||||
t.height = h;
|
||||
t.pixels.resize(size_t(w) * size_t(h));
|
||||
for (int y = 0; y < h; ++y)
|
||||
for (int x = 0; x < w; ++x)
|
||||
t.pixels[size_t(y) * size_t(w) + size_t(x)] = uint8_t(x * 255 / std::max(1, w - 1));
|
||||
return t;
|
||||
}
|
||||
|
||||
// A one-texel checkerboard of 0 and 255 - the highest spatial frequency the grid can hold, which is what
|
||||
// a blur has to remove and a downscale has to average rather than drop.
|
||||
static DecodedHeightTexture make_gray_checkerboard(int w = 16, int h = 16)
|
||||
{
|
||||
DecodedHeightTexture t;
|
||||
t.width = w;
|
||||
t.height = h;
|
||||
t.pixels.resize(size_t(w) * size_t(h));
|
||||
for (int y = 0; y < h; ++y)
|
||||
for (int x = 0; x < w; ++x)
|
||||
t.pixels[size_t(y) * size_t(w) + size_t(x)] = ((x + y) % 2 == 0) ? 0 : 255;
|
||||
return t;
|
||||
}
|
||||
|
||||
static DecodedHeightTexture make_rgb_flat(uint8_t r, uint8_t g, uint8_t b, int w = 8, int h = 8)
|
||||
{
|
||||
DecodedHeightTexture t;
|
||||
t.width = w;
|
||||
t.height = h;
|
||||
t.pixels.resize(size_t(w) * size_t(h));
|
||||
t.rgb.resize(size_t(w) * size_t(h) * 3);
|
||||
for (size_t i = 0; i < size_t(w) * size_t(h); ++i) {
|
||||
t.rgb[i * 3] = r;
|
||||
t.rgb[i * 3 + 1] = g;
|
||||
t.rgb[i * 3 + 2] = b;
|
||||
t.pixels[i] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
TEST_CASE("an adjustment that moves no slider returns the image unchanged", "[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture src = make_gray_ramp();
|
||||
const DecodedHeightTexture out = apply_texture_adjustments(src, TextureAdjustments{});
|
||||
CHECK(out.width == src.width);
|
||||
CHECK(out.height == src.height);
|
||||
CHECK(out.pixels == src.pixels);
|
||||
CHECK(TextureAdjustments{}.identity());
|
||||
}
|
||||
|
||||
TEST_CASE("brightness reaches pure white and pure black only at the ends of its range", "[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture src = make_gray_ramp();
|
||||
|
||||
TextureAdjustments up;
|
||||
up.brightness = 1.f;
|
||||
const DecodedHeightTexture white = apply_texture_adjustments(src, up);
|
||||
CHECK(std::all_of(white.pixels.begin(), white.pixels.end(), [](uint8_t v) { return v == 255; }));
|
||||
|
||||
TextureAdjustments down;
|
||||
down.brightness = -1.f;
|
||||
const DecodedHeightTexture black = apply_texture_adjustments(src, down);
|
||||
CHECK(std::all_of(black.pixels.begin(), black.pixels.end(), [](uint8_t v) { return v == 0; }));
|
||||
}
|
||||
|
||||
TEST_CASE("brightness lifts the image without flattening it into a clipped plateau", "[TextureDisplacement]")
|
||||
{
|
||||
// The reason brightness is a lift toward white rather than an additive offset: an offset would drive
|
||||
// everything above mid-grey to a flat 255, and since this image is also the height map that region
|
||||
// would bake as flat relief. Half a stop up must leave almost nothing at pure white.
|
||||
const DecodedHeightTexture src = make_gray_ramp();
|
||||
TextureAdjustments adj;
|
||||
adj.brightness = 0.5f;
|
||||
const DecodedHeightTexture out = apply_texture_adjustments(src, adj);
|
||||
|
||||
REQUIRE(out.pixels.size() == src.pixels.size());
|
||||
const size_t clipped = size_t(std::count(out.pixels.begin(), out.pixels.end(), uint8_t(255)));
|
||||
CHECK(clipped * 100 < out.pixels.size()); // under 1%; an additive shift would clip half the ramp
|
||||
|
||||
// Every texel got brighter, and the curve stays ordered: brighter in means brighter out.
|
||||
for (size_t i = 0; i < out.pixels.size(); ++i)
|
||||
REQUIRE(out.pixels[i] >= src.pixels[i]);
|
||||
for (int x = 1; x < out.width; ++x)
|
||||
REQUIRE(out.pixels[size_t(x)] >= out.pixels[size_t(x - 1)]);
|
||||
}
|
||||
|
||||
TEST_CASE("contrast at its lowest flattens the image to mid-grey and at its highest spreads it apart",
|
||||
"[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture src = make_gray_ramp();
|
||||
|
||||
TextureAdjustments flat;
|
||||
flat.contrast = -1.f;
|
||||
const DecodedHeightTexture grey = apply_texture_adjustments(src, flat);
|
||||
// One value everywhere - the pivot. Which exact level that rounds to is not the point.
|
||||
CHECK(std::adjacent_find(grey.pixels.begin(), grey.pixels.end(), std::not_equal_to<uint8_t>()) ==
|
||||
grey.pixels.end());
|
||||
|
||||
TextureAdjustments hard;
|
||||
hard.contrast = 1.f;
|
||||
const DecodedHeightTexture split = apply_texture_adjustments(src, hard);
|
||||
// A threshold about the pivot: the dark end of the ramp goes to black, the light end to white.
|
||||
CHECK(split.pixels[0] == 0);
|
||||
CHECK(split.pixels[size_t(split.width - 1)] == 255);
|
||||
}
|
||||
|
||||
TEST_CASE("blur removes the finest detail and sharpness exaggerates an edge", "[TextureDisplacement]")
|
||||
{
|
||||
// Blur: a one-texel checkerboard is the highest frequency the grid holds, so a blur has to collapse it
|
||||
// toward its own mean. Measured as mean absolute deviation, which needs no assumption about the mean.
|
||||
const DecodedHeightTexture checker = make_gray_checkerboard(32, 32);
|
||||
const auto deviation = [](const DecodedHeightTexture &t) {
|
||||
double mean = 0.;
|
||||
for (uint8_t v : t.pixels)
|
||||
mean += double(v);
|
||||
mean /= double(t.pixels.size());
|
||||
double dev = 0.;
|
||||
for (uint8_t v : t.pixels)
|
||||
dev += std::abs(double(v) - mean);
|
||||
return dev / double(t.pixels.size());
|
||||
};
|
||||
TextureAdjustments blur;
|
||||
blur.blur = 1.f;
|
||||
CHECK(deviation(apply_texture_adjustments(checker, blur)) < 0.25 * deviation(checker));
|
||||
|
||||
// Sharpness: an unsharp mask pushes each side of a step further from the other, so the step
|
||||
// overshoots - darker than the dark side just inside it, brighter than the bright side just inside.
|
||||
DecodedHeightTexture step;
|
||||
step.width = 64;
|
||||
step.height = 8;
|
||||
step.pixels.assign(size_t(step.width) * size_t(step.height), 100);
|
||||
for (int y = 0; y < step.height; ++y)
|
||||
for (int x = step.width / 2; x < step.width; ++x)
|
||||
step.pixels[size_t(y) * size_t(step.width) + size_t(x)] = 150;
|
||||
|
||||
TextureAdjustments sharp;
|
||||
sharp.sharpness = 1.f;
|
||||
const DecodedHeightTexture sharpened = apply_texture_adjustments(step, sharp);
|
||||
const size_t row = size_t(step.height / 2) * size_t(step.width);
|
||||
const size_t mid = size_t(step.width / 2);
|
||||
CHECK(sharpened.pixels[row + mid - 1] < 100); // undershoot on the dark side of the step
|
||||
CHECK(sharpened.pixels[row + mid] > 150); // overshoot on the light side
|
||||
}
|
||||
|
||||
TEST_CASE("a grayscale image stays grayscale and its hue slider does nothing", "[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture src = make_gray_ramp();
|
||||
REQUIRE_FALSE(src.has_color());
|
||||
|
||||
TextureAdjustments hue;
|
||||
hue.hue_deg = 120.f;
|
||||
const DecodedHeightTexture out = apply_texture_adjustments(src, hue);
|
||||
// Not promoted to RGB: a grey image has no colour to offer the layer's colour feature, and storing it
|
||||
// as RGB would triple the PNG for a row of identical greys.
|
||||
CHECK_FALSE(out.has_color());
|
||||
CHECK(out.rgb.empty());
|
||||
CHECK(out.pixels == src.pixels);
|
||||
}
|
||||
|
||||
TEST_CASE("a colour image's height stays the luminance of its adjusted colour", "[TextureDisplacement]")
|
||||
{
|
||||
// The invariant that keeps the relief and the colour from drifting apart: whatever the adjustment did
|
||||
// to the channels, the height is recomputed from the channels that came out.
|
||||
const DecodedHeightTexture src = make_rgb_flat(200, 60, 30);
|
||||
REQUIRE(src.has_color());
|
||||
|
||||
TextureAdjustments adj;
|
||||
adj.brightness = 0.2f;
|
||||
adj.contrast = 0.3f;
|
||||
adj.hue_deg = 75.f;
|
||||
const DecodedHeightTexture out = apply_texture_adjustments(src, adj);
|
||||
REQUIRE(out.has_color());
|
||||
|
||||
for (size_t i = 0; i < out.pixels.size(); ++i) {
|
||||
const long expect = std::lround(0.299 * out.rgb[i * 3] + 0.587 * out.rgb[i * 3 + 1] +
|
||||
0.114 * out.rgb[i * 3 + 2]);
|
||||
REQUIRE(long(out.pixels[i]) == expect);
|
||||
}
|
||||
// The hue turn actually moved the colour, so the check above is not passing on an unchanged image.
|
||||
CHECK(out.rgb != src.rgb);
|
||||
}
|
||||
|
||||
TEST_CASE("a full turn of the hue leaves a colour where it started", "[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture src = make_rgb_flat(200, 60, 30);
|
||||
TextureAdjustments adj;
|
||||
adj.hue_deg = 360.f;
|
||||
const DecodedHeightTexture out = apply_texture_adjustments(src, adj);
|
||||
REQUIRE(out.has_color());
|
||||
// Rounding through 8-bit channels, so a level or two of slack rather than exact equality.
|
||||
for (size_t i = 0; i < out.rgb.size(); ++i)
|
||||
REQUIRE(std::abs(int(out.rgb[i]) - int(src.rgb[i])) <= 2);
|
||||
}
|
||||
|
||||
TEST_CASE("the adjustment blur radius ramps from zero and stays under its cost cap", "[TextureDisplacement]")
|
||||
{
|
||||
CHECK_THAT(adjustment_blur_radius(0.f, 512, 512), Catch::Matchers::WithinAbs(0.f, 1e-6f));
|
||||
// 5% of the short side, as the Smoothing slider uses.
|
||||
CHECK_THAT(adjustment_blur_radius(1.f, 512, 512), Catch::Matchers::WithinAbs(25.6f, 1e-3f));
|
||||
CHECK_THAT(adjustment_blur_radius(0.5f, 512, 512), Catch::Matchers::WithinAbs(12.8f, 1e-3f));
|
||||
// Capped, so a large image at the top of the slider cannot cost without bound.
|
||||
CHECK_THAT(adjustment_blur_radius(1.f, 8192, 8192), Catch::Matchers::WithinAbs(48.f, 1e-3f));
|
||||
// Sized off the short side, not the long one.
|
||||
CHECK_THAT(adjustment_blur_radius(1.f, 4096, 200), Catch::Matchers::WithinAbs(10.f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("downscaling a height texture averages the texels it drops and never upscales",
|
||||
"[TextureDisplacement]")
|
||||
{
|
||||
// Averaging, not decimation: a one-texel checkerboard halved must come out uniformly mid-grey. Picking
|
||||
// one texel per block would instead come out all black or all white - detail the adjustment never
|
||||
// produced, shown in the preview.
|
||||
const DecodedHeightTexture small = downscale_height_texture(make_gray_checkerboard(16, 16), 8);
|
||||
CHECK(small.width == 8);
|
||||
CHECK(small.height == 8);
|
||||
CHECK(std::all_of(small.pixels.begin(), small.pixels.end(), [](uint8_t v) { return v == 127; }));
|
||||
|
||||
// Already within the limit: handed back as it is.
|
||||
const DecodedHeightTexture ramp = make_gray_ramp(64, 4);
|
||||
const DecodedHeightTexture same = downscale_height_texture(ramp, 256);
|
||||
CHECK(same.width == ramp.width);
|
||||
CHECK(same.height == ramp.height);
|
||||
CHECK(same.pixels == ramp.pixels);
|
||||
|
||||
// Aspect preserved, and a colour source keeps its colour.
|
||||
const DecodedHeightTexture wide = downscale_height_texture(make_rgb_flat(200, 60, 30, 400, 100), 100);
|
||||
CHECK(wide.width == 100);
|
||||
CHECK(wide.height == 25);
|
||||
CHECK(wide.has_color());
|
||||
}
|
||||
|
||||
TEST_CASE("a layer's live image adjustment reaches its decode", "[TextureDisplacement]")
|
||||
{
|
||||
// This is what makes the image-adjustment panel's sliders move the relief: decode_height_texture()
|
||||
// applies the layer's preview_adjust, so everything downstream of the decode - the displacement, the
|
||||
// colour, the thumbnails, the bake - sees the adjusted image without being told about the panel.
|
||||
TextureDisplacementLayer layer;
|
||||
layer.image_data = make_checkerboard_png(32, 32);
|
||||
|
||||
const DecodedHeightTexture raw = decode_height_texture(layer);
|
||||
REQUIRE_FALSE(raw.empty());
|
||||
|
||||
// An untouched panel, and a closed one, cost nothing and change nothing.
|
||||
layer.preview_adjust = TextureAdjustments{};
|
||||
CHECK(decode_height_texture(layer).pixels == raw.pixels);
|
||||
|
||||
layer.preview_adjust.brightness = 0.5f;
|
||||
const DecodedHeightTexture bright = decode_height_texture(layer);
|
||||
CHECK(bright.pixels != raw.pixels);
|
||||
CHECK(bright.pixels == apply_texture_adjustments(raw, layer.preview_adjust).pixels);
|
||||
|
||||
// Back to identity gives the stored image back, so closing the panel really does revert the layer.
|
||||
layer.preview_adjust = TextureAdjustments{};
|
||||
CHECK(decode_height_texture(layer).pixels == raw.pixels);
|
||||
}
|
||||
|
||||
TEST_CASE("changing a live adjustment is not served the previous one from cache", "[TextureDisplacement]")
|
||||
{
|
||||
// The decode caches by the image_data pointer, which does not change while a slider moves - so the
|
||||
// adjustment has to be part of the key or every slider move past the first would redraw the same
|
||||
// pixels. Same for the smoothed copy, which is computed on top of the adjusted one.
|
||||
TextureDisplacementLayer layer;
|
||||
layer.image_data = make_checkerboard_png(32, 32);
|
||||
const DecodedHeightTexture raw = decode_height_texture(layer);
|
||||
REQUIRE_FALSE(raw.empty());
|
||||
|
||||
layer.preview_adjust.brightness = 0.5f;
|
||||
const std::vector<uint8_t> bright = decode_height_texture(layer).pixels;
|
||||
layer.preview_adjust.brightness = -0.5f;
|
||||
const std::vector<uint8_t> dark = decode_height_texture(layer).pixels;
|
||||
CHECK(bright != dark);
|
||||
CHECK(dark == apply_texture_adjustments(raw, layer.preview_adjust).pixels);
|
||||
|
||||
// The same question again with smoothing on, which is cached separately and keyed by both.
|
||||
layer.smoothing = 0.5f;
|
||||
layer.preview_adjust.brightness = 0.5f;
|
||||
const std::vector<uint8_t> smooth_bright = decode_height_texture(layer).pixels;
|
||||
layer.preview_adjust.brightness = -0.5f;
|
||||
const std::vector<uint8_t> smooth_dark = decode_height_texture(layer).pixels;
|
||||
CHECK(smooth_bright != smooth_dark);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user