mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 06:41:10 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcb1f62bd9 |
@@ -298,6 +298,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";
|
||||
|
||||
@@ -785,6 +792,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;
|
||||
|
||||
@@ -805,6 +814,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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1138,6 +1149,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;
|
||||
@@ -2004,6 +2020,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
|
||||
@@ -3239,6 +3262,41 @@ 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;
|
||||
}
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
if (layer.path_in_3mf.empty())
|
||||
continue;
|
||||
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
|
||||
if (image == m_texture_displacement_files.end()) {
|
||||
add_error("Missing texture displacement image: " + layer.path_in_3mf);
|
||||
continue;
|
||||
}
|
||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(),
|
||||
image->second.end());
|
||||
}
|
||||
volume.texture_displacement_layers = std::move(layers);
|
||||
volume.texture_displacement_options = options;
|
||||
}
|
||||
|
||||
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
|
||||
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');
|
||||
@@ -3888,6 +3946,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));
|
||||
}
|
||||
@@ -5214,6 +5275,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);
|
||||
@@ -5229,6 +5302,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
|
||||
@@ -5393,12 +5468,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
|
||||
@@ -5695,6 +5786,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));
|
||||
}
|
||||
@@ -6698,6 +6792,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>";
|
||||
|
||||
@@ -7541,6 +7638,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();
|
||||
@@ -7990,7 +8101,44 @@ 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;
|
||||
std::string ext = boost::filesystem::path(layer.path).extension().string();
|
||||
boost::to_lower(ext);
|
||||
if (ext != ".png" && ext != ".jpg" && ext != ".jpeg")
|
||||
ext = ".png"; // the library ships PNG; anything unrecognised is stored under a type a reader expects
|
||||
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ext;
|
||||
// No deflate: PNG and JPEG are 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
|
||||
@@ -8101,6 +8249,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())
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include "nlohmann/json.hpp"
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -5041,4 +5042,245 @@ 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_mix_mode"] = int(options.color_mix_mode);
|
||||
opt["color_despeckle"] = options.color_despeckle;
|
||||
|
||||
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);
|
||||
int mix_mode = int(out_options.color_mix_mode);
|
||||
read_enum(opt, "color_mix_mode", mix_mode);
|
||||
out_options.color_mix_mode = ColorMixMode(std::clamp(mix_mode, 0, 2));
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -420,6 +420,23 @@ struct TextureDisplacementOptions
|
||||
}
|
||||
};
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -489,15 +489,7 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
// volume for a shaded pass that then draws nothing is what made the model vanish - most obviously
|
||||
// with zero layers, but equally with a layer that has no texture picked yet.
|
||||
const bool use_shaded = m_use_shaded_preview && m_shaded_preview_glmodel.is_initialized() && shaded_preview_ready();
|
||||
// Checker/Distortion are built from the *base* patch and drawn with a polygon offset, which biases
|
||||
// depth values - it does not move the geometry. It therefore cannot win against a surface that
|
||||
// genuinely stands in front, and the displaced preview does exactly that: it rises above the base
|
||||
// surface by the layer's depth. Drawn underneath a UV-check overlay it simply occludes it, which is
|
||||
// why those two modes looked like they did nothing. Leave it out and let the undisplaced volume show
|
||||
// through instead (toggle_model_objects_visibility below) - that one *is* coincident with the
|
||||
// overlay, which is what the offset assumes, and it is the surface whose mapping is being inspected.
|
||||
const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None &&
|
||||
m_preview_glmodel.is_initialized();
|
||||
const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized();
|
||||
// In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is
|
||||
// looking at, so the opaque paint-selection highlight must not be drawn on top of it - same
|
||||
// reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area
|
||||
@@ -525,13 +517,7 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
|
||||
render_triangles(selection);
|
||||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||||
}
|
||||
} else {
|
||||
// render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the
|
||||
// selector's colours), not an overlay on top of one - so it still has to run under a UV-check
|
||||
// overlay, or nothing draws the surface at all and the checker floats alone over an empty scene.
|
||||
// Deliberately without the depth bias the branch above applies: the checker/heatmap is drawn later
|
||||
// with its own -1 offset and has to win against this. Biasing both by the same amount is what made
|
||||
// the painted area cover the checker and is why this call used to be skipped outright.
|
||||
} else if (show_paint_overlay) {
|
||||
render_triangles(selection);
|
||||
}
|
||||
|
||||
@@ -5724,20 +5710,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const int cur_mode = m_use_shaded_preview ? 1 :
|
||||
m_uv_check_mode == UVCheckMode::Checker ? 2 :
|
||||
m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0;
|
||||
int new_mode = cur_mode;
|
||||
bool wf_toggle = false;
|
||||
bool open_uv_editor = false;
|
||||
// Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second
|
||||
// its stretch - so they only mean anything for a layer mapped with Unwrap (LSCM). On the default
|
||||
// triplanar mapping (or cylindrical / spherical / from view) they are faded out with the reason in the
|
||||
// tooltip, rather than being offered and then showing nothing.
|
||||
const wxString uv_view_na = active == nullptr ? _L("Add a layer first.") :
|
||||
active->projection_method != TextureProjectionMethod::LSCM ?
|
||||
_L("Only for a layer mapped with Unwrap (LSCM) - set the "
|
||||
"active layer's Mapping to Unwrap to use this view.") :
|
||||
wxString();
|
||||
// Either view over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
|
||||
if ((cur_mode == 2 || cur_mode == 3) && !uv_view_na.empty())
|
||||
int new_mode = cur_mode;
|
||||
bool wf_toggle = false;
|
||||
const wxString distortion_na = active == nullptr ? _L("Add a layer first.") :
|
||||
active->projection_method != TextureProjectionMethod::LSCM ?
|
||||
_L("Needs the active layer mapped with Unwrap (LSCM).") :
|
||||
wxString();
|
||||
// Distortion over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal.
|
||||
if (cur_mode == 3 && !distortion_na.empty())
|
||||
new_mode = 0;
|
||||
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
@@ -5756,20 +5736,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
|
||||
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
|
||||
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
new_mode = 2;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
"texture is undistorted; where they stretch, it will too")))
|
||||
new_mode = 2;
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"),
|
||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
new_mode = 3;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap"), distortion_na))
|
||||
new_mode = 3;
|
||||
vsep(icon_md);
|
||||
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"),
|
||||
_L("Wireframe - overlay the mesh edges; independent of the view above")))
|
||||
@@ -5784,13 +5756,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(_u8L("Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting "
|
||||
"or dragging a slider starts to stutter - the preview then waits until you let go."));
|
||||
|
||||
// Both are views of the unwrap, so picking one brings the UV editor up with it - including when that
|
||||
// view is already the active one and only the pane is missing.
|
||||
if (open_uv_editor && !m_show_uv_editor) {
|
||||
m_show_uv_editor = true;
|
||||
if (new_mode == cur_mode)
|
||||
update_uv_editor(); // otherwise apply_view_mode() below does it
|
||||
}
|
||||
if (new_mode != cur_mode)
|
||||
apply_view_mode(new_mode);
|
||||
if (wf_toggle) {
|
||||
|
||||
@@ -1712,7 +1712,7 @@ public:
|
||||
bool toggle, bool accent = false, int size_dip = 26)
|
||||
: wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFULL_REPAINT_ON_RESIZE)
|
||||
, m_icon_name(icon), m_icon_dip(size_dip >= 26 ? 16 : 14), m_label(label), m_toggle(toggle), m_accent(accent)
|
||||
, m_size_dip(size_dip), m_tip(tip)
|
||||
, m_size_dip(size_dip)
|
||||
{
|
||||
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
SetToolTip(tip);
|
||||
@@ -1725,7 +1725,7 @@ public:
|
||||
Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent &) { m_hover = true; Refresh(); });
|
||||
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &) { m_hover = false; m_pressed = false; Refresh(); });
|
||||
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) {
|
||||
if (usable()) {
|
||||
if (IsEnabled()) {
|
||||
m_pressed = true;
|
||||
Refresh();
|
||||
}
|
||||
@@ -1734,7 +1734,7 @@ public:
|
||||
const bool was_pressed = m_pressed;
|
||||
m_pressed = false;
|
||||
Refresh();
|
||||
if (!was_pressed || !usable() || !GetClientRect().Contains(e.GetPosition()))
|
||||
if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition()))
|
||||
return;
|
||||
if (m_toggle)
|
||||
m_on = !m_on;
|
||||
@@ -1774,30 +1774,6 @@ public:
|
||||
Refresh();
|
||||
return changed;
|
||||
}
|
||||
// Soft-disable: the button is drawn faded and swallows clicks, but stays a live window, so hovering it
|
||||
// still raises its tooltip - now with `reason` appended, saying what to do to make it usable. A window
|
||||
// really disabled with Enable(false) gets no mouse events at all on GTK and MSW, which leaves the user
|
||||
// guessing; this is the same trade-off the gizmo panel's icon_toggle() makes with its `unavailable`.
|
||||
// An empty reason makes the button usable again.
|
||||
void SetUnavailable(const wxString &reason)
|
||||
{
|
||||
if (reason == m_unavailable)
|
||||
return;
|
||||
m_unavailable = reason;
|
||||
SetToolTip(reason.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + reason);
|
||||
if (!m_unavailable.empty())
|
||||
m_pressed = false; // a reason appearing mid-press cancels the press
|
||||
Refresh();
|
||||
}
|
||||
// Replaces the plain tooltip, keeping whatever reason is currently appended to it.
|
||||
void SetTip(const wxString &tip)
|
||||
{
|
||||
if (tip == m_tip)
|
||||
return;
|
||||
m_tip = tip;
|
||||
SetToolTip(m_unavailable.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + m_unavailable);
|
||||
}
|
||||
bool usable() const { return IsEnabled() && m_unavailable.empty(); }
|
||||
|
||||
protected:
|
||||
wxSize DoGetBestSize() const override
|
||||
@@ -1816,7 +1792,7 @@ private:
|
||||
const PaneColors c = PaneColors::current();
|
||||
const wxRect r = GetClientRect();
|
||||
const wxColour teal(0x00, 0x96, 0x88);
|
||||
const bool enabled = usable();
|
||||
const bool enabled = IsEnabled();
|
||||
|
||||
wxColour fill = c.bg, border = c.frame, text = c.ink;
|
||||
if (m_accent) {
|
||||
@@ -1876,8 +1852,6 @@ private:
|
||||
bool m_toggle = false;
|
||||
bool m_accent = false;
|
||||
int m_size_dip = 26;
|
||||
wxString m_tip; // the tooltip without any m_unavailable reason appended
|
||||
wxString m_unavailable; // non-empty: faded and unclickable, and why (see SetUnavailable())
|
||||
bool m_on = false;
|
||||
bool m_badge = false;
|
||||
bool m_hover = false;
|
||||
@@ -1923,9 +1897,6 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
});
|
||||
m_layer_name->SetMinSize(wxSize(FromDIP(30), -1));
|
||||
m_tile = text(wxEmptyString, c.dim);
|
||||
m_tile->SetToolTip(_L("The active layer's tile size: how much of the model one repeat of the texture covers. The "
|
||||
"canvas is measured in tiles, so one grid cell is one repeat. Change it with Tiling in the "
|
||||
"layer's settings."));
|
||||
header->Add(m_thumb, 0, wxALIGN_CENTER_VERTICAL);
|
||||
header->Add(m_layer_name, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
|
||||
header->Add(m_tile, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap);
|
||||
@@ -1979,19 +1950,9 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
strip->Add(r, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(11));
|
||||
strip->AddSpacer(FromDIP(7));
|
||||
};
|
||||
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island",
|
||||
_L("Island - work on whole islands. Click one to select it, then drag to move it, right-drag to "
|
||||
"rotate it, or press R to rotate and S to scale with the mouse (click or Enter to confirm, Esc "
|
||||
"to cancel)."),
|
||||
true);
|
||||
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex",
|
||||
_L("Vertex - drag vertices to reshape an island by hand; Shift adds to the selection, Ctrl "
|
||||
"toggles one in or out of it."),
|
||||
true);
|
||||
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge",
|
||||
_L("Edge - drag edges to reshape an island by hand; Shift adds to the selection, Ctrl toggles "
|
||||
"one in or out of it."),
|
||||
true);
|
||||
m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", _L("Island - move, rotate and scale whole islands"), true);
|
||||
m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", _L("Vertex - drag vertices to reshape; Shift/Ctrl to multi-select"), true);
|
||||
m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", _L("Edge - drag edges to reshape; Shift/Ctrl to multi-select"), true);
|
||||
strip_rule();
|
||||
m_mark_seams = tool(ID_UV_MARK_SEAMS, "texture_displacement_uv_seam",
|
||||
_L("Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is "
|
||||
@@ -2012,11 +1973,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
m_clear_edits = tool(ID_UV_CLEAR_EDITS, "texture_displacement_uv_clear_edits",
|
||||
_L("Clear UV edits - discard all manual vertex/edge moves and return the unwrap to its automatic shape"), false);
|
||||
m_snap = tool(ID_UV_SNAP, "texture_displacement_uv_snap", _L("Snap - stick islands together when dragging one against another"), true);
|
||||
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame",
|
||||
_L("Frame all islands, fitting every one of them in view (Home or F).\n"
|
||||
"Elsewhere on the canvas: scroll to zoom around the cursor, and middle-drag - or drag empty space - "
|
||||
"to pan."),
|
||||
false);
|
||||
m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", _L("Frame all islands (Home)"), false);
|
||||
strip->AddSpacer(FromDIP(4));
|
||||
|
||||
m_canvas = new UVEditorCanvas(this);
|
||||
@@ -2027,7 +1984,6 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
|
||||
// ---- status line: the current gesture on the left, the unwrap summary on the right ----
|
||||
m_status = text(wxEmptyString, c.dim, wxST_ELLIPSIZE_END);
|
||||
m_status->SetToolTip(_L("What is selected, and the exact figures of the move, rotation or scale while you drag one."));
|
||||
m_status->SetMinSize(wxSize(FromDIP(40), -1));
|
||||
m_stats = text(wxEmptyString, c.dim);
|
||||
auto *status = new wxBoxSizer(wxHORIZONTAL);
|
||||
@@ -2103,25 +2059,19 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||
} else {
|
||||
m_thumb->SetBitmap(wxNullBitmap);
|
||||
}
|
||||
// Every tool that cannot be used right now is faded with the reason appended to its tooltip, rather than
|
||||
// being hard-disabled (which would hide the tooltip too - see UVToolButton::SetUnavailable()).
|
||||
const wxString no_layer = s.has_layer ? wxString() :
|
||||
_L("The pane follows the active texture layer, and that layer has to be mapped "
|
||||
"with Unwrap (LSCM). Add a layer and set its Mapping to Unwrap.");
|
||||
m_thumb->SetUnavailable(no_layer);
|
||||
m_layer_name->SetToolTip(s.has_layer ? m_thumb->GetToolTipText() : no_layer);
|
||||
m_thumb->Enable(s.has_layer);
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
m_background[i]->SetValue(int(s.background) == i);
|
||||
m_background[i]->SetUnavailable(no_layer);
|
||||
m_background[i]->Enable(s.has_layer);
|
||||
}
|
||||
m_unwrap->SetUnavailable(no_layer);
|
||||
m_unwrap->Enable(s.has_layer);
|
||||
m_unwrap->SetBadge(s.unwrap_stale);
|
||||
m_unwrap->SetTip(s.unwrap_stale ?
|
||||
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
|
||||
"Press to unwrap again.") :
|
||||
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
|
||||
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
|
||||
m_unwrap->SetToolTip(s.unwrap_stale ?
|
||||
_L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. "
|
||||
"Press to unwrap again.") :
|
||||
_L("Flatten the painted area into UV islands. It is computed only when you press this, not on "
|
||||
"every edit - so paint, change the seam angle or mark seams first, then press Unwrap."));
|
||||
|
||||
if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg)))
|
||||
m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg)));
|
||||
@@ -2130,26 +2080,15 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||
m_connect->Enable(s.has_layer);
|
||||
|
||||
m_mark_seams->SetValue(s.mark_seams);
|
||||
m_mark_seams->SetUnavailable(no_layer);
|
||||
m_mark_seams->Enable(s.has_layer);
|
||||
m_seam_path->SetValue(s.seam_path);
|
||||
m_seam_path->SetUnavailable(!no_layer.empty() ? no_layer :
|
||||
s.mark_seams ? wxString() :
|
||||
_L("Turn Mark seams on first - Path is a quicker way of marking them."));
|
||||
m_clear_seams->SetUnavailable(!no_layer.empty() ? no_layer :
|
||||
s.has_seams ? wxString() :
|
||||
_L("No seams are marked on this layer."));
|
||||
m_clear_edits->SetUnavailable(!no_layer.empty() ? no_layer :
|
||||
s.has_uv_edits ? wxString() :
|
||||
_L("No islands have been reshaped by hand, so there is nothing to "
|
||||
"discard."));
|
||||
m_seam_path->Enable(s.has_layer && s.mark_seams);
|
||||
m_clear_seams->Enable(s.has_layer && s.has_seams);
|
||||
m_clear_edits->Enable(s.has_layer && s.has_uv_edits);
|
||||
|
||||
m_stats->SetLabel(s.unwrapped ? wxString::Format(_L("%d islands, %s faces"), s.island_count,
|
||||
wxString(std::to_string(s.face_count))) :
|
||||
wxString());
|
||||
m_stats->SetToolTip(s.unwrapped ? _L("How the painted area came out of the unwrap: the number of separate pieces it "
|
||||
"was cut into (at the seams and at edges sharper than the seam angle), and how "
|
||||
"many triangles they hold in total.") :
|
||||
wxString());
|
||||
refresh_selection_tools();
|
||||
if (relayout)
|
||||
Layout();
|
||||
@@ -2159,24 +2098,19 @@ void UVEditorPanel::refresh_selection_tools()
|
||||
{
|
||||
const bool has_islands = m_canvas->has_islands();
|
||||
const int mode = int(m_canvas->select_mode());
|
||||
// Faded rather than hard-disabled, so the tooltip still says what is missing (see apply_state()).
|
||||
const wxString not_unwrapped = has_islands ? wxString() : _L("Press Unwrap first - there are no islands to work on yet.");
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
m_select[i]->SetValue(i == mode);
|
||||
m_select[i]->SetUnavailable(not_unwrapped);
|
||||
m_select[i]->Enable(has_islands);
|
||||
}
|
||||
const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island &&
|
||||
m_canvas->selected_island() >= 0;
|
||||
const wxString no_island = !not_unwrapped.empty() ? not_unwrapped :
|
||||
island_picked ? wxString() :
|
||||
_L("Click an island on the canvas first, in Island mode.");
|
||||
m_avg_scale->SetUnavailable(not_unwrapped);
|
||||
m_cut->SetUnavailable(no_island);
|
||||
m_join->SetUnavailable(no_island);
|
||||
m_unjoin->SetUnavailable(no_island);
|
||||
m_snap->SetUnavailable(not_unwrapped);
|
||||
m_avg_scale->Enable(has_islands);
|
||||
m_cut->Enable(island_picked);
|
||||
m_join->Enable(island_picked);
|
||||
m_unjoin->Enable(island_picked);
|
||||
m_snap->Enable(has_islands);
|
||||
m_snap->SetValue(m_canvas->snap_enabled());
|
||||
m_frame->SetUnavailable(not_unwrapped);
|
||||
m_frame->Enable(has_islands);
|
||||
}
|
||||
|
||||
void UVEditorPanel::on_tool(wxCommandEvent &evt)
|
||||
|
||||
@@ -2112,3 +2112,139 @@ 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::Mirror;
|
||||
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_mode = ColorMixMode::XYDither;
|
||||
options.color_despeckle = 4;
|
||||
|
||||
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(ra.depth_mm == Approx(a.depth_mm));
|
||||
CHECK(ra.tiling_scale == Approx(a.tiling_scale));
|
||||
CHECK(ra.rotation_deg == Approx(a.rotation_deg));
|
||||
CHECK(ra.offset.isApprox(a.offset));
|
||||
CHECK(ra.invert == a.invert);
|
||||
CHECK(ra.midlevel == Approx(a.midlevel));
|
||||
CHECK(ra.smoothing == Approx(a.smoothing));
|
||||
CHECK(ra.edge_smoothing == a.edge_smoothing);
|
||||
CHECK(ra.edge_smoothing_amount == Approx(a.edge_smoothing_amount));
|
||||
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(ra.lscm_seam_angle_deg == Approx(a.lscm_seam_angle_deg));
|
||||
CHECK(ra.island_padding_mm == Approx(a.island_padding_mm));
|
||||
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(ra.islands[0].rotation_deg == Approx(a.islands[0].rotation_deg));
|
||||
CHECK(ra.islands[0].scale == Approx(a.islands[0].scale));
|
||||
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(read_options.smooth_strength == Approx(options.smooth_strength));
|
||||
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_mode == options.color_mix_mode);
|
||||
CHECK(read_options.color_despeckle == options.color_despeckle);
|
||||
}
|
||||
|
||||
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(l.depth_mm == Approx(0.75f));
|
||||
// Everything absent keeps the struct's own default rather than becoming zero.
|
||||
CHECK(l.tiling_scale == Approx(fresh.tiling_scale));
|
||||
CHECK(l.auto_connect_islands == fresh.auto_connect_islands);
|
||||
CHECK(l.tile_enabled == fresh.tile_enabled);
|
||||
CHECK(l.projection_method == fresh.projection_method);
|
||||
CHECK(l.lscm_seam_angle_deg == Approx(fresh.lscm_seam_angle_deg));
|
||||
CHECK(options.color_mix_mode == defaults.color_mix_mode);
|
||||
CHECK(options.pipeline_v2 == defaults.pipeline_v2);
|
||||
|
||||
std::vector<TextureDisplacementLayer> unused_layers;
|
||||
TextureDisplacementOptions unused_options;
|
||||
CHECK(!texture_displacement_layers_from_json("not json at all", unused_layers, unused_options));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user