mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682814878d |
@@ -856,13 +856,30 @@ 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.
|
||||
// One per texture displacement layer slot, indexed by triangle like the masks above, but
|
||||
// empty for a slot nothing paints and no longer than its last painted triangle.
|
||||
// See push_texture_displacement().
|
||||
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
|
||||
// BBS
|
||||
std::vector<std::string> face_properties;
|
||||
|
||||
bool empty() { return vertices.empty() || triangles.empty(); }
|
||||
|
||||
// Records the paint mask of the triangle just appended, for one layer slot. A slot no
|
||||
// triangle names keeps its vector empty instead of holding one empty string per triangle:
|
||||
// eight slots over a million triangles would otherwise cost hundreds of megabytes, for
|
||||
// every project, whether or not it uses the feature. The vector is padded up to the
|
||||
// triangles seen so far the first time a value turns up, so it stays indexed by triangle,
|
||||
// and both readers stop at its size, so trailing triangles without a value need nothing.
|
||||
void push_texture_displacement(int slot, std::string &&mask_string) {
|
||||
std::vector<std::string> &mask = texture_displacement[slot];
|
||||
if (mask_string.empty() && mask.empty())
|
||||
return;
|
||||
assert(!triangles.empty());
|
||||
mask.resize(triangles.size() - 1);
|
||||
mask.push_back(std::move(mask_string));
|
||||
}
|
||||
|
||||
// backup & restore
|
||||
void swap(Geometry& o) {
|
||||
std::swap(vertices, o.vertices);
|
||||
@@ -3355,34 +3372,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
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));
|
||||
};
|
||||
|
||||
// The pixels come from the archive and nowhere else. A layer the archive carries no image for -
|
||||
// saved while its texture had not been loaded, or written by a build that stored only the path -
|
||||
// keeps its settings without an image, which the user recovers by picking the texture again.
|
||||
// `layer.path` is left alone on purpose: it names a file on the machine the project was made on,
|
||||
// so reading it here would let any project choose a local file to open - a UNC path among them -
|
||||
// and have the next save copy it into the archive. The EmbossShape SVGs are read from the archive
|
||||
// only, for the same reason.
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
if (!layer.path_in_3mf.empty()) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
load_image_from_disk(layer);
|
||||
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;
|
||||
@@ -4045,8 +4051,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
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]));
|
||||
m_curr_object->geometry.push_texture_displacement(
|
||||
slot, 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));
|
||||
}
|
||||
@@ -5917,8 +5923,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
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]));
|
||||
current_object->geometry.push_texture_displacement(
|
||||
slot, 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));
|
||||
}
|
||||
|
||||
@@ -5310,101 +5310,108 @@ bool texture_displacement_layers_from_json(const std::string
|
||||
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);
|
||||
}
|
||||
|
||||
TextureDisplacementOptions out_options;
|
||||
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));
|
||||
// Past the parse, a value of the wrong type still throws: get<int>() on a string, get<float>() on
|
||||
// null. Nothing between here and the project load catches that, so one bad number would fail the
|
||||
// whole file instead of this one block. Report it malformed and let the volume load without it.
|
||||
try {
|
||||
if (const auto it = root.find("options"); it != root.end() && it->is_object()) {
|
||||
const nlohmann::json &opt = *it;
|
||||
read(opt, "displace_border", out_options.displace_border);
|
||||
read(opt, "smooth_enabled", out_options.smooth_enabled);
|
||||
read(opt, "smooth_strength", out_options.smooth_strength);
|
||||
read(opt, "smooth_iterations", out_options.smooth_iterations);
|
||||
read(opt, "smooth_skip_border", out_options.smooth_skip_border);
|
||||
read(opt, "pipeline_v2", out_options.pipeline_v2);
|
||||
read(opt, "v2_refine_mm", out_options.v2_refine_mm);
|
||||
read(opt, "v2_regularize", out_options.v2_regularize);
|
||||
read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k);
|
||||
read(opt, "v2_relocate", out_options.v2_relocate);
|
||||
read(opt, "v2_flip_edges", out_options.v2_flip_edges);
|
||||
read(opt, "color_mix_enabled", out_options.color_mix_enabled);
|
||||
read(opt, "color_despeckle", out_options.color_despeckle);
|
||||
read(opt, "color_mix_count", out_options.color_mix_count);
|
||||
read(opt, "active_slot", out_options.active_slot);
|
||||
read(opt, "panel_mode", out_options.panel_mode);
|
||||
out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1);
|
||||
out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1);
|
||||
}
|
||||
|
||||
if (const auto it = root.find("layers"); it != root.end() && it->is_array()) {
|
||||
for (const nlohmann::json &j : *it) {
|
||||
if (!j.is_object())
|
||||
continue;
|
||||
TextureDisplacementLayer l;
|
||||
read(j, "slot", l.slot);
|
||||
read(j, "name", l.name);
|
||||
read(j, "path", l.path);
|
||||
read(j, "path_in_3mf", l.path_in_3mf);
|
||||
|
||||
read(j, "depth_mm", l.depth_mm);
|
||||
read(j, "tiling_scale", l.tiling_scale);
|
||||
read(j, "rotation_deg", l.rotation_deg);
|
||||
if (const auto o = j.find("offset"); o != j.end())
|
||||
l.offset = vec2_from_json(*o, l.offset);
|
||||
read(j, "invert", l.invert);
|
||||
read(j, "midlevel", l.midlevel);
|
||||
read(j, "smoothing", l.smoothing);
|
||||
|
||||
read(j, "edge_smoothing", l.edge_smoothing);
|
||||
read(j, "edge_smoothing_amount", l.edge_smoothing_amount);
|
||||
read(j, "auto_connect_islands", l.auto_connect_islands);
|
||||
|
||||
read(j, "tile_enabled", l.tile_enabled);
|
||||
read(j, "color_enabled", l.color_enabled);
|
||||
int tile_method = int(l.tile_method), projection = int(l.projection_method), blend = int(l.blend_mode);
|
||||
read_enum(j, "tile_method", tile_method);
|
||||
read_enum(j, "projection_method", projection);
|
||||
read_enum(j, "blend_mode", blend);
|
||||
l.tile_method = TextureTileMethod(tile_method);
|
||||
l.projection_method = TextureProjectionMethod(std::clamp(projection, 0, 4));
|
||||
l.blend_mode = TextureBlendMode(blend);
|
||||
|
||||
read(j, "lscm_seam_angle_deg", l.lscm_seam_angle_deg);
|
||||
read(j, "island_padding_mm", l.island_padding_mm);
|
||||
|
||||
if (const auto v = j.find("view_project_right"); v != j.end())
|
||||
l.view_project_right = vec3_from_json(*v, l.view_project_right);
|
||||
if (const auto v = j.find("view_project_up"); v != j.end())
|
||||
l.view_project_up = vec3_from_json(*v, l.view_project_up);
|
||||
read(j, "view_project_projective", l.view_project_projective);
|
||||
if (const auto v = j.find("view_project_matrix"); v != j.end() && v->is_array() && v->size() == 12)
|
||||
for (size_t i = 0; i < 12; ++i)
|
||||
l.view_project_matrix[i] = (*v)[i].get<float>();
|
||||
|
||||
if (const auto v = j.find("lscm_seam_edges"); v != j.end() && v->is_array())
|
||||
for (size_t i = 0; i + 1 < v->size(); i += 2)
|
||||
l.lscm_seam_edges.emplace_back((*v)[i].get<int>(), (*v)[i + 1].get<int>());
|
||||
|
||||
if (const auto v = j.find("islands"); v != j.end() && v->is_array())
|
||||
for (const nlohmann::json &e : *v) {
|
||||
if (!e.is_array() || e.size() != 4)
|
||||
continue;
|
||||
TextureIsland island;
|
||||
island.offset = Vec2f(e[0].get<float>(), e[1].get<float>());
|
||||
island.rotation_deg = e[2].get<float>();
|
||||
island.scale = e[3].get<float>();
|
||||
l.islands.push_back(island);
|
||||
}
|
||||
|
||||
read(j, "island_groups", l.island_groups);
|
||||
|
||||
if (const auto v = j.find("lscm_uv_overrides"); v != j.end() && v->is_array())
|
||||
for (const nlohmann::json &e : *v) {
|
||||
if (!e.is_array() || e.size() != 3)
|
||||
continue;
|
||||
l.lscm_uv_overrides.emplace_back(e[0].get<int>(), Vec2f(e[1].get<float>(), e[2].get<float>()));
|
||||
}
|
||||
|
||||
out_layers.push_back(std::move(l));
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
layers = std::move(out_layers);
|
||||
|
||||
+326
-13
@@ -28,9 +28,12 @@
|
||||
#include <miniz.h>
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
@@ -349,6 +352,24 @@ static bool replace_in_3mf_entry(const std::string& path, const std::string& suf
|
||||
return replaced;
|
||||
}
|
||||
|
||||
// Writes `model` out as a one-plate project, which is what the application's Save does with it.
|
||||
static void store_project(const std::string& path, Model& model)
|
||||
{
|
||||
ScopedTemporaryDir backup_dir("orca_project_src");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
DynamicPrintConfig cfg;
|
||||
PlateData plate;
|
||||
plate.plate_index = 0;
|
||||
StoreParams sp;
|
||||
sp.path = path.c_str();
|
||||
sp.model = &model;
|
||||
sp.config = &cfg;
|
||||
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
sp.plate_data_list.push_back(&plate);
|
||||
REQUIRE(store_bbs_3mf(sp));
|
||||
}
|
||||
|
||||
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
|
||||
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
|
||||
static void store_painted_cube(const std::string& path)
|
||||
@@ -363,19 +384,7 @@ static void store_painted_cube(const std::string& path)
|
||||
selector.set_facet(1, EnforcerBlockerType::Extruder3);
|
||||
REQUIRE(volume->mmu_segmentation_facets.set(selector));
|
||||
}
|
||||
ScopedTemporaryDir backup_dir("orca_paint_src");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
DynamicPrintConfig cfg;
|
||||
PlateData plate;
|
||||
plate.plate_index = 0;
|
||||
StoreParams sp;
|
||||
sp.path = path.c_str();
|
||||
sp.model = &model;
|
||||
sp.config = &cfg;
|
||||
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
sp.plate_data_list.push_back(&plate);
|
||||
REQUIRE(store_bbs_3mf(sp));
|
||||
store_project(path, model);
|
||||
}
|
||||
|
||||
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
|
||||
@@ -2080,3 +2089,307 @@ TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern
|
||||
CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json");
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
|
||||
// Encodes a flat grayscale PNG the way the texture panel's import does, so what the archive carries
|
||||
// is a real texture rather than an opaque blob.
|
||||
static std::shared_ptr<std::vector<unsigned char>> make_gray_png(uint8_t value, size_t w = 4, size_t h = 4)
|
||||
{
|
||||
ScopedTemporaryFile file(".png");
|
||||
const std::vector<uint8_t> pixels(w * h, value);
|
||||
REQUIRE(png::write_gray_to_file(file.string(), w, h, pixels));
|
||||
|
||||
boost::nowide::ifstream in(file.string(), std::ios::binary);
|
||||
std::vector<unsigned char> bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
REQUIRE_FALSE(bytes.empty());
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
}
|
||||
|
||||
// Every entry name in a 3mf archive, with the separators normalised the way the importer sees them.
|
||||
static std::vector<std::string> zip_entry_names(const std::string& path)
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
REQUIRE(open_zip_reader(&zip, path));
|
||||
const mz_uint n = mz_zip_reader_get_num_files(&zip);
|
||||
for (mz_uint i = 0; i < n; ++i) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (!mz_zip_reader_file_stat(&zip, i, &st) || st.m_is_directory)
|
||||
continue;
|
||||
std::string name(st.m_filename);
|
||||
std::replace(name.begin(), name.end(), '\\', '/');
|
||||
names.push_back(std::move(name));
|
||||
}
|
||||
close_zip_reader(&zip);
|
||||
return names;
|
||||
}
|
||||
|
||||
static const std::string TEXTURE_DISPLACEMENT_ENTRY_DIR = "Metadata/texture_displacement/";
|
||||
|
||||
// Stores a one-plate project holding a cube with one texture displacement layer, painted on that
|
||||
// layer's own slot. The caller fills the layer in, and gets back the mask string the exporter wrote
|
||||
// for `painted_facet`, which is what the load has to reproduce.
|
||||
//
|
||||
// The painted facet is deliberately not the first one: the importer keeps a slot's masks only as far
|
||||
// as its last painted triangle, so a facet in the middle is the case where that run has to be padded
|
||||
// in front and left short at the back.
|
||||
static void store_texture_displaced_cube(const std::string& path, const TextureDisplacementLayer& layer,
|
||||
const TextureDisplacementOptions& options, std::string& painted_mask,
|
||||
int painted_facet = 5)
|
||||
{
|
||||
Model model;
|
||||
ModelObject* object = model.add_object();
|
||||
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
||||
object->add_instance();
|
||||
|
||||
volume->texture_displacement_layers.push_back(layer);
|
||||
volume->texture_displacement_options = options;
|
||||
REQUIRE(painted_facet < int(volume->mesh().its.indices.size()));
|
||||
{
|
||||
TriangleSelector selector(volume->mesh());
|
||||
selector.set_facet(painted_facet, EnforcerBlockerType::ENFORCER);
|
||||
REQUIRE(volume->texture_displacement_facet(layer.slot).set(selector));
|
||||
}
|
||||
painted_mask = volume->texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facet);
|
||||
REQUIRE_FALSE(painted_mask.empty());
|
||||
|
||||
store_project(path, model);
|
||||
}
|
||||
|
||||
// Returns the only volume of the only object in the project at `path`, loaded through the importer.
|
||||
static const ModelVolume& load_single_volume(const std::string& path, Model& model, const ScopedTemporaryDir& backup_dir)
|
||||
{
|
||||
REQUIRE(load_project(path, model, backup_dir));
|
||||
REQUIRE(model.objects.size() == 1);
|
||||
REQUIRE(model.objects.front()->volumes.size() == 1);
|
||||
return *model.objects.front()->volumes.front();
|
||||
}
|
||||
|
||||
// A texture displacement stack is the one paint tool whose settings live outside the volume's config:
|
||||
// the layer stack goes into the archive as its own JSON file, each layer's texture as a PNG beside it,
|
||||
// and the paint as a per-triangle attribute (see add_texture_displacement). A project only reopens
|
||||
// with its relief intact if all three come back together.
|
||||
TEST_CASE("A texture displacement layer, its texture and its paint survive a project round trip", "[3mf][TextureDisplacement]")
|
||||
{
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 3; // not the first slot, so the per-slot attribute names have to line up
|
||||
layer.name = "Bark";
|
||||
layer.path = "/textures/bark.png";
|
||||
layer.depth_mm = 1.25f;
|
||||
layer.tiling_scale = 7.5f;
|
||||
layer.midlevel = 0.5f;
|
||||
layer.invert = true;
|
||||
layer.tile_method = TextureTileMethod::MirroredRepeat;
|
||||
layer.projection_method = TextureProjectionMethod::LSCM;
|
||||
layer.blend_mode = TextureBlendMode::Subtract;
|
||||
layer.lscm_seam_edges = { { 1, 2 }, { 3, 5 } };
|
||||
layer.image_data = make_gray_png(128);
|
||||
|
||||
TextureDisplacementOptions options;
|
||||
options.smooth_enabled = true;
|
||||
options.smooth_iterations = 4;
|
||||
options.active_slot = layer.slot;
|
||||
options.panel_mode = 1;
|
||||
|
||||
const int painted_facet = 5;
|
||||
std::string painted;
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
store_texture_displaced_cube(temp.string(), layer, options, painted, painted_facet);
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||
Model model;
|
||||
const ModelVolume& volume = load_single_volume(temp.string(), model, backup_dir);
|
||||
|
||||
REQUIRE(volume.texture_displacement_layers.size() == 1);
|
||||
const TextureDisplacementLayer& read = volume.texture_displacement_layers.front();
|
||||
CHECK(read.slot == layer.slot);
|
||||
CHECK(read.name == layer.name);
|
||||
CHECK(read.path == layer.path);
|
||||
CHECK_THAT(read.depth_mm, Catch::Matchers::WithinAbs(layer.depth_mm, 1e-6f));
|
||||
CHECK_THAT(read.tiling_scale, Catch::Matchers::WithinAbs(layer.tiling_scale, 1e-6f));
|
||||
CHECK_THAT(read.midlevel, Catch::Matchers::WithinAbs(layer.midlevel, 1e-6f));
|
||||
CHECK(read.invert == layer.invert);
|
||||
CHECK(read.tile_method == layer.tile_method);
|
||||
CHECK(read.projection_method == layer.projection_method);
|
||||
CHECK(read.blend_mode == layer.blend_mode);
|
||||
CHECK(read.lscm_seam_edges == layer.lscm_seam_edges);
|
||||
|
||||
// The texture rides along in the archive, named after the slot it belongs to, byte for byte.
|
||||
CHECK(boost::algorithm::starts_with(read.path_in_3mf, TEXTURE_DISPLACEMENT_ENTRY_DIR));
|
||||
CHECK(boost::algorithm::ends_with(read.path_in_3mf, "_" + std::to_string(layer.slot) + ".png"));
|
||||
REQUIRE(read.image_data);
|
||||
CHECK(*read.image_data == *layer.image_data);
|
||||
|
||||
CHECK(volume.texture_displacement_options.smooth_enabled);
|
||||
CHECK(volume.texture_displacement_options.smooth_iterations == options.smooth_iterations);
|
||||
CHECK(volume.texture_displacement_options.active_slot == options.active_slot);
|
||||
CHECK(volume.texture_displacement_options.panel_mode == options.panel_mode);
|
||||
|
||||
// The paint comes back on the facet it was put on, on its own slot and nowhere else.
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facet) == painted);
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(0).empty());
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
if (slot != layer.slot)
|
||||
CHECK(volume.texture_displacement_facet(slot).empty());
|
||||
}
|
||||
|
||||
// A layer names the file its texture was imported from, which the library panel uses to point at the
|
||||
// entry the layer came from. That name must not be opened: the project may come from anyone, the path
|
||||
// may name any file the user can read - on Windows a UNC path names a host as well - and the next save
|
||||
// would copy whatever was found there into the archive. A layer the archive has no image for comes
|
||||
// back without pixels instead, which the user fixes by picking the texture again.
|
||||
TEST_CASE("A texture displacement layer whose image the archive lacks is not read from the path it names", "[3mf][TextureDisplacement][Regression]")
|
||||
{
|
||||
// A file that really is there, so the test fails if the importer ever reaches for it.
|
||||
ScopedTemporaryFile texture(".png");
|
||||
REQUIRE(png::write_gray_to_file(texture.string(), 4, 4, std::vector<uint8_t>(16, 200)));
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.name = "Bark";
|
||||
layer.path = texture.string(); // and no image_data: nothing for the exporter to put in the archive
|
||||
|
||||
std::string painted;
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
store_texture_displaced_cube(temp.string(), layer, TextureDisplacementOptions{}, painted);
|
||||
|
||||
for (const std::string& name : zip_entry_names(temp.string()))
|
||||
if (boost::algorithm::starts_with(name, TEXTURE_DISPLACEMENT_ENTRY_DIR))
|
||||
CHECK_FALSE(boost::algorithm::ends_with(name, ".png"));
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||
Model model;
|
||||
const ModelVolume& volume = load_single_volume(temp.string(), model, backup_dir);
|
||||
|
||||
REQUIRE(volume.texture_displacement_layers.size() == 1);
|
||||
CHECK(volume.texture_displacement_layers.front().path == texture.string());
|
||||
CHECK_FALSE(bool(volume.texture_displacement_layers.front().image_data));
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(5) == painted);
|
||||
}
|
||||
|
||||
// The stack's JSON is arbitrary text by the time it is read back, and a value of the wrong type in it
|
||||
// throws out of the parse. That has to cost the volume its layers, not the project: the model, and the
|
||||
// paint the user can still build on, load as they would from a build without the feature.
|
||||
TEST_CASE("A malformed texture displacement block leaves the rest of the project loadable", "[3mf][TextureDisplacement][Regression]")
|
||||
{
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.name = "Bark";
|
||||
layer.image_data = make_gray_png(128);
|
||||
|
||||
std::string painted;
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
store_texture_displaced_cube(temp.string(), layer, TextureDisplacementOptions{}, painted);
|
||||
|
||||
REQUIRE(rewrite_3mf_entries(temp.string(), [](std::string& name, std::string& data) {
|
||||
if (!boost::algorithm::starts_with(name, TEXTURE_DISPLACEMENT_ENTRY_DIR) ||
|
||||
!boost::algorithm::ends_with(name, ".json"))
|
||||
return false;
|
||||
// `offset` goes through a typed accessor, so a string in it throws; a plain setting like
|
||||
// depth_mm would be shrugged off by the parse instead, leaving the layer loadable.
|
||||
data = R"({"version":1,"layers":[{"slot":0,"offset":["deep","deep"]}]})";
|
||||
return true;
|
||||
}));
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||
Model model;
|
||||
bool loaded = false;
|
||||
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(model.objects.size() == 1);
|
||||
REQUIRE(model.objects.front()->volumes.size() == 1);
|
||||
const ModelVolume& volume = *model.objects.front()->volumes.front();
|
||||
|
||||
CHECK(volume.texture_displacement_layers.empty());
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(5) == painted);
|
||||
}
|
||||
|
||||
// A slot's masks are kept only as far as its last painted triangle, so where the paint falls in the
|
||||
// list decides whether that run has to be padded in front of the paint, cut short behind it, or both.
|
||||
TEST_CASE("Texture displacement paint survives wherever it falls in the triangle list", "[3mf][TextureDisplacement]")
|
||||
{
|
||||
const int painted_facet = GENERATE(0, 5, 11); // first, middle and last facet of a cube
|
||||
INFO("painted facet " << painted_facet);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.name = "Bark";
|
||||
layer.image_data = make_gray_png(128);
|
||||
|
||||
std::string painted;
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
store_texture_displaced_cube(temp.string(), layer, TextureDisplacementOptions{}, painted, painted_facet);
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||
Model model;
|
||||
const ModelVolume& volume = load_single_volume(temp.string(), model, backup_dir);
|
||||
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facet) == painted);
|
||||
for (int facet = 0; facet < int(volume.mesh().its.indices.size()); ++facet)
|
||||
if (facet != painted_facet)
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(facet).empty());
|
||||
}
|
||||
|
||||
// Each volume's stack is written under the volume's own id, and the paint of a whole object arrives as
|
||||
// one run of triangles that the volumes take their slices of. Two volumes using the same slot are the
|
||||
// case where both of those have to tell the volumes apart.
|
||||
TEST_CASE("Two volumes of one object keep their own texture displacement layers", "[3mf][TextureDisplacement]")
|
||||
{
|
||||
const auto first_image = make_gray_png(64);
|
||||
const auto second_image = make_gray_png(192);
|
||||
REQUIRE(*first_image != *second_image);
|
||||
|
||||
// The facet each volume is painted on, volume-local, so a mask landing on the wrong volume shows up.
|
||||
const int painted_facets[2] = { 0, 7 };
|
||||
std::string painted[2];
|
||||
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
{
|
||||
Model model;
|
||||
ModelObject* object = model.add_object();
|
||||
object->add_instance();
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0; // the same slot in both, which is what makes their names collide
|
||||
layer.name = i == 0 ? "First" : "Second";
|
||||
layer.depth_mm = i == 0 ? 0.5f : 1.5f;
|
||||
layer.image_data = i == 0 ? first_image : second_image;
|
||||
volume->texture_displacement_layers.push_back(layer);
|
||||
|
||||
TriangleSelector selector(volume->mesh());
|
||||
selector.set_facet(painted_facets[i], EnforcerBlockerType::ENFORCER);
|
||||
REQUIRE(volume->texture_displacement_facet(layer.slot).set(selector));
|
||||
painted[i] = volume->texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facets[i]);
|
||||
REQUIRE_FALSE(painted[i].empty());
|
||||
}
|
||||
store_project(temp.string(), model);
|
||||
}
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
|
||||
Model model;
|
||||
REQUIRE(load_project(temp.string(), model, backup_dir));
|
||||
REQUIRE(model.objects.size() == 1);
|
||||
REQUIRE(model.objects.front()->volumes.size() == 2);
|
||||
|
||||
const std::shared_ptr<std::vector<unsigned char>> images[2] = { first_image, second_image };
|
||||
const std::string names[2] = { "First", "Second" };
|
||||
const float depths[2] = { 0.5f, 1.5f };
|
||||
std::string entries[2];
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
INFO("volume " << i);
|
||||
const ModelVolume& volume = *model.objects.front()->volumes[i];
|
||||
REQUIRE(volume.texture_displacement_layers.size() == 1);
|
||||
const TextureDisplacementLayer& layer = volume.texture_displacement_layers.front();
|
||||
CHECK(layer.name == names[i]);
|
||||
CHECK_THAT(layer.depth_mm, Catch::Matchers::WithinAbs(depths[i], 1e-6f));
|
||||
REQUIRE(layer.image_data);
|
||||
CHECK(*layer.image_data == *images[i]);
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facets[i]) == painted[i]);
|
||||
CHECK(volume.texture_displacement_facet(layer.slot).get_triangle_as_string(painted_facets[1 - i]).empty());
|
||||
entries[i] = layer.path_in_3mf;
|
||||
}
|
||||
// Both volumes paint slot 0, so only the volume id keeps their textures apart in the archive.
|
||||
CHECK(entries[0] != entries[1]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2300,6 +2300,47 @@ TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry",
|
||||
CHECK(!texture_displacement_layers_from_json("not json at all", unused_layers, unused_options));
|
||||
}
|
||||
|
||||
// The vectors, the islands and the UV overrides are read through nlohmann's typed accessors, which
|
||||
// throw on a value of the wrong type rather than returning anything. Nothing between here and the
|
||||
// project load catches that, so a document carrying one has to come back as "malformed block".
|
||||
TEST_CASE("Texture displacement JSON reports a value of the wrong type instead of throwing", "[TextureDisplacement]")
|
||||
{
|
||||
const std::string json = GENERATE(
|
||||
R"({"layers":[{"slot":0,"offset":["a","b"]}]})",
|
||||
R"({"layers":[{"slot":0,"view_project_right":[1,null,3]}]})",
|
||||
R"({"layers":[{"slot":0,"view_project_matrix":[1,2,3,4,5,6,7,8,9,10,11,"x"]}]})",
|
||||
R"({"layers":[{"slot":0,"lscm_seam_edges":["a","b"]}]})",
|
||||
R"({"layers":[{"slot":0,"islands":[[1,2,"a",4]]}]})",
|
||||
R"({"layers":[{"slot":0,"lscm_uv_overrides":[[1,"a",3]]}]})");
|
||||
INFO(json);
|
||||
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementOptions options;
|
||||
bool parsed = true;
|
||||
REQUIRE_NOTHROW(parsed = texture_displacement_layers_from_json(json, layers, options));
|
||||
CHECK_FALSE(parsed);
|
||||
CHECK(layers.empty());
|
||||
}
|
||||
|
||||
// A plain setting is read through a helper that shrugs a wrong type off, which is the deliberate
|
||||
// other half of the case above: the field keeps its default and the rest of the layer still loads.
|
||||
TEST_CASE("Texture displacement JSON keeps the default for a setting of the wrong type", "[TextureDisplacement]")
|
||||
{
|
||||
const std::string json = R"({"options":{"smooth_strength":"a lot"},"layers":[{"slot":"two","name":"Old","depth_mm":0.75}]})";
|
||||
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementOptions options;
|
||||
REQUIRE(texture_displacement_layers_from_json(json, layers, options));
|
||||
REQUIRE(layers.size() == 1);
|
||||
|
||||
const TextureDisplacementLayer fresh;
|
||||
const TextureDisplacementOptions defaults;
|
||||
CHECK(layers[0].slot == fresh.slot);
|
||||
CHECK(layers[0].name == "Old");
|
||||
CHECK_THAT(layers[0].depth_mm, WithinAbs(0.75f, 1e-6f));
|
||||
CHECK_THAT(options.smooth_strength, WithinAbs(defaults.smooth_strength, 1e-6f));
|
||||
}
|
||||
|
||||
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
|
||||
{
|
||||
TextureDisplacementLayer gray;
|
||||
|
||||
Reference in New Issue
Block a user