Compare commits

..
Author SHA1 Message Date
ExPikaPaka 682814878d Address the review on saving texture displacement
Read each layer's texture from the archive only. `layer.path` comes from the project
file, so following it let any .3mf pick a local file to read, a UNC path among them,
and have the next save copy what it found into the archive. Reverts the disk fallback
added in 8ecfee6a2.

Store a paint mask only for the slots a triangle names. Eight empty strings per
triangle cost every project hundreds of megabytes on a large mesh, whether or not it
uses the feature. A slot's run is padded to the triangles seen so far on its first
value, so it stays indexed by triangle, and ends at its last painted one.

Report a malformed layer block instead of throwing out of the project load. The typed
accessors throw on a value of the wrong type and nothing above them caught that, so
one bad number cost the whole file rather than one volume's relief.

Cover the store and load round trip in test_3mf.cpp: the layer, its image, the options
and the paint, the two regressions above, paint at either end of the triangle list, two
volumes sharing a slot, and the wrong-typed documents at the unit level.
2026-10-12 14:20:46 +02:00
42 changed files with 1378 additions and 1531 deletions
-8
View File
@@ -1,18 +1,10 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
# no headers for that object. ccache reproduces /showIncludes output on a hit.
#
# Clang also records the modification time of every input in a precompiled header
# and rejects the header once one differs. ccache caches the compile that creates
# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
# with the old file's time, and every compile that uses it fails. The build system
# already rebuilds the header when an input changes. This covers the dependencies,
# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif ()
endforeach ()
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.5,2.5h11v11h-11z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M8,2.5v11M2.5,8h11" style="fill:none;stroke:#949494;stroke-dasharray:1.5,1.5"/><path d="M4.5,11.5l3.5-6l3.5,6z" style="fill:#009688"/></svg>

Before

Width:  |  Height:  |  Size: 340 B

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Qidi",
"version": "02.04.00.17",
"version": "02.04.00.16",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [
@@ -83,7 +83,7 @@
"extruder_clearance_height_to_lid": "120",
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"thumbnails_format": "PNG",
"default_filament_profile": [
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
@@ -100,7 +100,7 @@
"default_filament_profile": [
"Generic PLA @Qidi"
],
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
"scan_first_layer": "0"
}
@@ -24,7 +24,7 @@
"210x210/COLPIC",
"110x110/PNG"
],
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"scan_first_layer": "0"
}
+34 -24
View File
@@ -64,6 +64,7 @@
#include <ostream>
#include <sstream>
#include <set>
#include <iterator>
#include <memory>
#include <optional>
#include <map>
@@ -855,14 +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, indexed like `triangles` but stored only as far as the
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
// 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,11 +3372,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
add_error("Malformed texture displacement data: " + json_path);
return;
}
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
// the project must never read the file it names: that would let whoever made the project pick any
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
// `path` stays a label of where the texture came from; a layer the archive carries no image for
// loads without one, its settings intact, until the user picks a texture again.
// The pixels come from the archive and nowhere else. A layer the archive carries no image for -
// saved while its texture had not been loaded, or written by a build that stored only the path -
// keeps its settings without an image, which the user recovers by picking the texture again.
// `layer.path` is left alone on purpose: it names a file on the machine the project was made on,
// so reading it here would let any project choose a local file to open - a UNC path among them -
// and have the next save copy it into the archive. The EmbossShape SVGs are read from the archive
// only, for the same reason.
for (TextureDisplacementLayer &layer : layers) {
if (layer.path_in_3mf.empty())
continue;
@@ -3368,7 +3387,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
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());
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;
@@ -4031,11 +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)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
mask.resize(m_curr_object->geometry.triangles.size());
mask.back() = paint;
}
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));
}
@@ -5390,7 +5407,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
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; // no triangle painted on this slot
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)
@@ -5906,11 +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)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
mask.resize(current_object->geometry.triangles.size());
mask.back() = paint;
}
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));
}
@@ -7341,14 +7355,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (iter != m_shared_meshes.end())
{
const ModelVolume* shared_volume = iter->second.second;
bool same_texture_paint = true;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
if ((shared_volume->supported_facets.equals(volume->supported_facets))
&& (shared_volume->seam_facets.equals(volume->seam_facets))
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
&& same_texture_paint)
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
{
auto data = iter->second.first;
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
+349
View File
@@ -0,0 +1,349 @@
#include "JumpPointSearch.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Point.hpp"
#include "libslic3r/AStar.hpp"
#include "libslic3r/KDTreeIndirect.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#include <limits>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <oneapi/tbb/scalable_allocator.h>
//#define DEBUG_FILES
#ifdef DEBUG_FILES
#include "libslic3r/SVG.hpp"
#endif
namespace Slic3r {
// execute fn for each pixel on the line. If fn returns false, terminate the iteration
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
coord_t dx = abs(x1 - x0);
coord_t dy = abs(y1 - y0);
coord_t x = x0;
coord_t y = y0;
coord_t n = 1 + dx + dy;
coord_t x_inc = (x1 > x0) ? 1 : -1;
coord_t y_inc = (y1 > y0) ? 1 : -1;
coord_t error = dx - dy;
dx *= 2;
dy *= 2;
for (; n > 0; --n) {
if (!fn(x, y)) return;
if (error > 0) {
x += x_inc;
error -= dy;
} else {
y += y_inc;
error += dx;
}
}
}
// will draw the line twice, second time with and offset of 1 in the direction of normal
// may call the fn on the same coordiantes multiple times!
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
normal.x() = ceil(normal.x());
normal.y() = ceil(normal.y());
Point start_offset = Point(x0, y0) + (normal).cast<coord_t>();
Point end_offset = Point(x1, y1) + (normal).cast<coord_t>();
dda(x0, y0, x1, y1, fn);
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
}
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
{
public:
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
struct Node
{
CellPositionType position;
CellPositionType incoming_dir;
};
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
private:
CellPositionType target;
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
{
CellPositionType next = start + forward_dir;
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
if (is_passable(next)) {
return next;
} else {
return start;
}
}
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
{
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
return false;
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
return false;
}
}
public:
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
{
const CellPositionType & pos = from.position;
const CellPositionType & forward_dir = from.incoming_dir;
std::vector<CellPositionType> dirs_to_check{};
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
dirs_to_check = all_directions;
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
}
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
}
dirs_to_check.push_back(horizontal_check_dir);
dirs_to_check.push_back(vertical_check_dir);
dirs_to_check.push_back(forward_dir);
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
dirs_to_check.push_back(forward_dir);
}
for (const CellPositionType &dir : dirs_to_check) {
CellPositionType jp = find_jump_point(pos, dir);
if (jp != pos) fn(Node{jp, dir});
}
}
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
};
void JPSPathFinder::clear()
{
inpassable.clear();
max_search_box.max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
max_search_box.min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
add_obstacles(bed_shape);
}
void JPSPathFinder::add_obstacles(const Lines &obstacles)
{
auto store_obstacle = [&](coord_t x, coord_t y) {
max_search_box.max.x() = std::max(max_search_box.max.x(), x);
max_search_box.max.y() = std::max(max_search_box.max.y(), y);
max_search_box.min.x() = std::min(max_search_box.min.x(), x);
max_search_box.min.y() = std::min(max_search_box.min.y(), y);
inpassable.insert(Pixel{x, y});
return true;
};
for (const Line &l : obstacles) {
Pixel start = pixelize(l.a);
Pixel end = pixelize(l.b);
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
}
}
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
{
Pixel start = pixelize(p0);
Pixel end = pixelize(p1);
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
if (inpassable.find(start) != inpassable.end()) {
dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
start = Pixel(x, y);
return false;
}
return true;
});
}
if (inpassable.find(end) != inpassable.end()) {
dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
end = Pixel(x, y);
return false;
}
return true;
});
}
BoundingBox search_box = max_search_box;
search_box.max -= Pixel(1, 1);
search_box.min += Pixel(1, 1);
BoundingBox bounding_square(Points{start, end});
bounding_square.max += Pixel(5, 5);
bounding_square.min -= Pixel(5, 5);
coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
search_box.max = search_box.max.cwiseMin(bounding_square.max);
search_box.min = search_box.min.cwiseMax(bounding_square.min);
auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
std::unordered_map<size_t, QNode> astar_cache{};
std::vector<Pixel, PointsAllocator<Pixel>> out_path;
std::vector<decltype(tracer)::Node> out_nodes;
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
// path not found - just reconstruct the best path from astar cache.
// Note that astar_cache is NOT empty - at least the starting point should always be there
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
std::vector<size_t> keys;
keys.reserve(astar_cache.size());
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
out_path.push_back(end);
while (closest_qnode != astar::Unassigned) {
out_path.push_back(astar_cache[closest_qnode].node.position);
closest_qnode = astar_cache[closest_qnode].parent;
}
} else {
for (const auto &node : out_nodes) { out_path.push_back(node.position); }
out_path.push_back(start);
}
#ifdef DEBUG_FILES
auto scaled_points = [](const Points &ps) {
Points r;
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
return r;
};
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
svg.draw(scaled_point(end), "purple", scale_(0.4));
svg.draw(scaled_point(start), "green", scale_(0.4));
#endif
std::vector<Pixel, PointsAllocator<Pixel>> tmp_path;
tmp_path.reserve(out_path.size());
// Some path found, reverse and remove points that do not change direction
std::reverse(out_path.begin(), out_path.end());
{
tmp_path.push_back(out_path.front()); // first point
for (size_t i = 1; i < out_path.size() - 1; i++) {
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) { tmp_path.push_back(out_path[i]); }
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
#endif
tmp_path.clear();
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
// insert the Px+k point to result and continue search from Px+k
{
tmp_path.push_back(out_path.front()); // first point
size_t index_of_last_stored_point = 0;
for (size_t i = 1; i < out_path.size(); i++) {
if (i - index_of_last_stored_point < 2) continue;
bool passable = true;
auto store_obstacle = [&](coord_t x, coord_t y) {
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
passable = false;
return false;
}
return true;
};
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
if (!passable) {
tmp_path.push_back(out_path[i - 1]);
index_of_last_stored_point = i - 1;
}
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
svg.Close();
#endif
// before returing the path, transform it from pixels back to points.
// Also replace the first and last pixel by input points so that result path patches input params exactly.
for (Pixel &p : out_path) { p = unpixelize(p); }
out_path.front() = p0;
out_path.back() = p1;
return Polyline(out_path);
}
} // namespace Slic3r
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "Line.hpp"
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#include "BoundingBox.hpp"
#include "Polygon.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <unordered_map>
#include <unordered_set>
namespace Slic3r {
class JPSPathFinder
{
using Pixel = Point;
std::unordered_set<Pixel, PointHash> inpassable;
coordf_t print_z;
BoundingBox max_search_box;
Lines bed_shape;
const coord_t resolution = scaled(1.5);
Pixel pixelize(const Point &p) { return p / resolution; }
Point unpixelize(const Pixel &p) { return p * resolution; }
public:
JPSPathFinder() = default;
void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
void clear();
void add_obstacles(const Lines &obstacles);
Polyline find_path(const Point &start, const Point &end);
};
} // namespace Slic3r
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
-22
View File
@@ -353,24 +353,6 @@ Model Model::read_from_file(const std::string&
ObjInfo obj_info;
ObjParser::MtlData mtl_data;
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
// Keep the model's own UV channel on the volume, whatever the colour import then does with
// the mesh: it is what TextureProjectionMethod::ModelUV maps a texture-displacement layer
// through, so a model that arrives with a UV layout is textured on that layout instead of on
// a fresh unwrap. load_obj() fills obj_info.uvs per face of the mesh it built, in the same
// order (and only when every face named a known material, hence the size check). V is flipped
// on the way in, to the convention DecodedHeightTexture samples in - the same flip
// obj_to_textured_mesh() does for the colour importer.
if (result && !obj_info.uvs.empty() && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
ModelVolume &volume = *model.objects.back()->volumes[0];
const indexed_triangle_set &its = volume.mesh().its;
if (obj_info.uvs.size() == its.indices.size()) {
volume.texture_uv.resize(its.indices.size() * 3);
for (size_t fi = 0; fi < obj_info.uvs.size(); ++fi)
for (int k = 0; k < 3; ++k)
volume.texture_uv[fi * 3 + size_t(k)] =
Vec2f(obj_info.uvs[fi][size_t(k)].x(), 1.f - obj_info.uvs[fi][size_t(k)].y());
}
}
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
// Textured OBJ: hand the mesh + materials to the texture-to-color importer
// instead of the flat per-face colour dialog.
@@ -2151,10 +2133,6 @@ void ModelVolume::reset_extra_facets()
// than left referring to a mesh that no longer matches it.
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).reset();
// Same reasoning for the model's own UV channel: it is indexed by triangle, so against a mesh
// this no longer matches it would map the texture by another mesh's triangle numbering. A caller
// that can carry it across - see transfer_uv_channel() - puts it back after this.
this->reset_texture_uv();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const
+2 -23
View File
@@ -974,26 +974,6 @@ public:
// they are not a property of any single layer.
TextureDisplacementOptions texture_displacement_options;
// The model's own texture coordinates, as the file it was imported from carried them: three per
// triangle of mesh().its, in its triangle order (corner k of triangle i at 3i+k), V already
// flipped to the sampling convention (V = 0 is the image's top row). Empty when the source had
// none, which is every STL and every 3mf - only a textured OBJ brings one in today.
//
// Per corner rather than per vertex because that is what a UV layout is: the two sides of a seam
// are the same vertex with two different coordinates, and collapsing them would smear the texture
// across every island boundary. TextureProjectionMethod::ModelUV maps through it, which is what
// lets a model that already carries a UV channel be textured on the artist's own layout instead
// of on a fresh unwrap.
//
// Indexed by triangle, so anything that *replaces* the mesh has to bring it across (or drop it):
// see transfer_uv_channel(), and reset_texture_uv() for the cases that cannot.
std::vector<Vec2f> texture_uv;
bool has_texture_uv() const { return !texture_uv.empty() && texture_uv.size() == mesh().its.indices.size() * 3; }
// Forgets the channel. For a mesh change that nothing can carry it across - a split, a boolean -
// where leaving it would map the texture by another mesh's triangle numbering.
void reset_texture_uv() { texture_uv.clear(); }
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1256,7 +1236,6 @@ private:
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
texture_displacement_layers(other.texture_displacement_layers),
texture_displacement_options(other.texture_displacement_options),
texture_uv(other.texture_uv),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{
assert(this->id().valid());
@@ -1355,7 +1334,7 @@ private:
cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp();
}
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
ar(texture_displacement_layers, texture_displacement_options);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1379,7 +1358,7 @@ private:
cereal::save_by_value(ar, fuzzy_skin_facets);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
cereal::save_by_value(ar, texture_displacement_facet(i));
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
ar(texture_displacement_layers, texture_displacement_options);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);
+86 -297
View File
@@ -1571,61 +1571,6 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
return islands;
}
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh)
{
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_paint.bitstream.empty())
return {};
const TriangleMesh src(src_mesh);
TriangleSelector selector(src);
selector.deserialize(src_paint, false);
// Every sub-triangle of the source, each carrying its own state - NONE included, so the soup
// covers the whole surface and a foot point always lands on something. One soup rather than one
// tree per state: a foot point has one nearest triangle, and that triangle's state is the answer.
std::vector<indexed_triangle_set> per_state;
selector.get_facets(per_state);
indexed_triangle_set soup;
std::vector<uint8_t> label;
for (size_t st = 0; st < per_state.size(); ++st) {
const indexed_triangle_set &part = per_state[st];
if (part.indices.empty())
continue;
const int base = int(soup.vertices.size());
soup.vertices.insert(soup.vertices.end(), part.vertices.begin(), part.vertices.end());
for (const stl_triangle_vertex_indices &t : part.indices) {
soup.indices.emplace_back(t[0] + base, t[1] + base, t[2] + base);
label.push_back(uint8_t(st));
}
}
if (soup.indices.empty())
return {};
const AABBTreeIndirect::Tree3f tree =
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(soup.vertices, soup.indices);
std::vector<uint8_t> out(dst_mesh.indices.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t j = range.begin(); j < range.end(); ++j) {
const stl_triangle_vertex_indices &t = dst_mesh.indices[j];
// The centroid, not a corner: a facet takes one state, and its middle is the point that
// is inside it whatever the triangle looks like.
const Vec3f centroid = (dst_mesh.vertices[size_t(t[0])] + dst_mesh.vertices[size_t(t[1])] +
dst_mesh.vertices[size_t(t[2])]) / 3.f;
size_t hit = 0;
Vec3f foot;
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(soup.vertices, soup.indices,
tree, centroid, hit, foot);
if (d2 >= 0.f && hit < label.size())
out[j] = label[hit];
}
});
return out;
}
void average_island_scales(std::vector<TextureIsland> &islands)
{
if (islands.empty())
@@ -1728,110 +1673,6 @@ std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, co
return corner;
}
// Barycentric weights of `p` inside triangle (a, b, c), as the pair that belongs with the edges
// (b - a) and (c - a): p ~= (1 - w1 - w2) * a + w1 * b + w2 * c. `p` is projected onto the triangle's
// plane, so a point a hair off the surface still reports the weights of its foot point. A degenerate
// triangle reports its centroid, the only coordinate that means anything there.
static void barycentric_weights(const Vec3f &a, const Vec3f &b, const Vec3f &c, const Vec3f &p, float &w1, float &w2)
{
const Vec3f e0 = b - a, e1 = c - a, ep = p - a;
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
const float den = d00 * d11 - d01 * d01;
if (std::abs(den) <= 1e-20f) {
w1 = w2 = 1.f / 3.f;
return;
}
w1 = (d11 * dp0 - d01 * dp1) / den;
w2 = (d00 * dp1 - d01 * dp0) / den;
}
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
const std::vector<Vec2f> &model_uv,
const indexed_triangle_set &patch,
const std::vector<int> &patch_source)
{
if (patch.indices.empty() || model_uv.size() != base_mesh.indices.size() * 3 ||
patch_source.size() != patch.indices.size())
return {};
std::vector<Vec2f> corner(patch.indices.size() * 3, Vec2f::Zero());
for (size_t j = 0; j < patch.indices.size(); ++j) {
const int S = patch_source[j];
if (S < 0 || size_t(S) >= base_mesh.indices.size())
continue; // no source triangle: leave (0, 0), which the callers read as "no placement"
const stl_triangle_vertex_indices &src = base_mesh.indices[size_t(S)];
const Vec3f &a = base_mesh.vertices[size_t(src[0])];
const Vec3f &b = base_mesh.vertices[size_t(src[1])];
const Vec3f &c = base_mesh.vertices[size_t(src[2])];
const Vec2f *uv = &model_uv[size_t(S) * 3];
const stl_triangle_vertex_indices &tri = patch.indices[j];
for (int k = 0; k < 3; ++k) {
// An unsplit patch triangle is its source triangle, so its corners land exactly on a, b
// and c and this reproduces their uvs unchanged; a sub-triangle the brush cut gets the
// interpolated coordinate, which is the whole point of going through the barycentric.
float w1 = 0.f, w2 = 0.f;
barycentric_weights(a, b, c, patch.vertices[size_t(tri[k])], w1, w2);
corner[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
}
}
return corner;
}
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner)
{
if (patch.indices.empty() || corner.size() != patch.indices.size() * 3)
return {};
std::vector<Vec2f> per_vertex(patch.vertices.size(), Vec2f::Zero());
std::vector<char> seen(patch.vertices.size(), 0);
for (size_t j = 0; j < patch.indices.size(); ++j)
for (int k = 0; k < 3; ++k) {
const int vi = patch.indices[j][k];
if (vi < 0 || size_t(vi) >= per_vertex.size() || seen[size_t(vi)])
continue;
seen[size_t(vi)] = 1;
per_vertex[size_t(vi)] = corner[j * 3 + size_t(k)];
}
return per_vertex;
}
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
const std::vector<Vec2f> &src_uv,
const indexed_triangle_set &dst_mesh,
float max_distance)
{
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_uv.size() != src_mesh.indices.size() * 3)
return {};
const AABBTreeIndirect::Tree3f tree =
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(src_mesh.vertices, src_mesh.indices);
const float limit2 = max_distance >= 0.f ? max_distance * max_distance : std::numeric_limits<float>::max();
std::vector<Vec2f> out(dst_mesh.indices.size() * 3, Vec2f::Zero());
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t j = range.begin(); j < range.end(); ++j) {
const stl_triangle_vertex_indices &tri = dst_mesh.indices[j];
for (int k = 0; k < 3; ++k) {
const Vec3f &p = dst_mesh.vertices[size_t(tri[k])];
size_t hit = 0;
Vec3f foot;
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
src_mesh.vertices, src_mesh.indices, tree, p, hit, foot);
if (d2 < 0.f || d2 > limit2 || hit >= src_mesh.indices.size())
continue; // nowhere near the source surface: (0, 0), as documented
const stl_triangle_vertex_indices &st = src_mesh.indices[hit];
float w1 = 0.f, w2 = 0.f;
barycentric_weights(src_mesh.vertices[size_t(st[0])], src_mesh.vertices[size_t(st[1])],
src_mesh.vertices[size_t(st[2])], foot, w1, w2);
const Vec2f *uv = &src_uv[hit * 3];
out[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
}
}
});
return out;
}
namespace {
// apply_uv_transform()'s per-layer constants, worked out once. Triplanar sampling runs the transform
// three times per point, and recomputing the rotation's cos/sin and the tiling reciprocal on every one
@@ -1841,16 +1682,13 @@ struct UVTransform
float scale, cs, sn, aspect;
Vec2f offset;
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
// model - the millimetre tiling scale and the non-square aspect correction - and keeps the
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
UVTransform(const TextureDisplacementLayer &layer, float aspect_)
{
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
cs = std::cos(rad);
sn = std::sin(rad);
aspect = normalized_uv ? 1.f : aspect_;
aspect = aspect_;
offset = layer.offset;
}
Vec2f operator()(const Vec2f &planar) const
@@ -1875,11 +1713,9 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
}
} // namespace
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
{
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
// see the header. UVTransform does the same, and the two must stay in step.
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
const Vec2f scaled = planar * scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
@@ -1897,7 +1733,7 @@ Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &la
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
// turning it.
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
if (aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect;
return rotated + layer.offset;
@@ -1922,9 +1758,6 @@ float blend_displacement(float accumulated, float value, TextureBlendMode mode)
value;
return accumulated / divisor;
}
// Replaces instead of combining, so `accumulated` is deliberately unused: whatever the layers
// below put here is gone where this one is painted.
case TextureBlendMode::Override: return value;
case TextureBlendMode::Add:
default: return accumulated + value;
}
@@ -1954,21 +1787,14 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
// apply_uv_transform()). Every projection except the projective "from view" one funnels through
// here, so this one line is what makes them all aspect-correct.
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
// A ModelUV layer's precomputed uv is the model's own, already normalized; every other coordinate
// reaching the transform - the unwrap's, and every analytic projection's - is in millimetres. The
// `lscm_uv` test is what keeps the two apart: a ModelUV layer with no channel gets none, falls
// through to the millimetre methods below, and must be scaled like them.
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
lscm_uv != nullptr;
const UVTransform xf(layer, aspect, normalized_uv);
const UVTransform xf(layer, aspect);
auto sample_at = [&](const Vec2f &planar) {
return texture.sample(xf(planar), layer.tile_enabled, layer.tile_method);
};
// A precomputed per-patch uv - the LSCM solve, or the model's own channel - wins over the layer's
// own method (see the header): the caller only passes it when the patch actually has one, so a
// patch that failed to unwrap, or a volume with no UV channel, falls through to the analytic
// methods below as its documented fallback.
// Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
// caller only passes it when the patch actually parameterized successfully, so a patch that
// failed to unwrap falls through to the analytic methods below as its documented fallback.
if (lscm_uv != nullptr)
return sample_at(*lscm_uv);
@@ -2026,12 +1852,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
// the two differ in what "nothing here" means. Height returns 0, which is a perfectly good height
// (no displacement); colour has no such neutral value - black is a colour - so every path that
// returns 0 there has to report false here instead, and the caller leaves the triangle uncoloured.
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
// Same reasoning as sample_layer_height(), and it has to match it exactly or a layer's colour and
// its relief would be read from different places in the image.
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
lscm_uv != nullptr;
const UVTransform xf(layer, aspect, normalized_uv);
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
const UVTransform xf(layer, aspect);
auto sample_at = [&](const Vec2f &planar) {
return texture.sample_color(xf(planar), layer.tile_enabled, layer.tile_method);
};
@@ -2045,8 +1867,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
};
if (lscm_uv != nullptr) {
// The coverage test goes through the same transform, so an untiled ModelUV layer is "there"
// exactly where the model's own uv lands inside the image.
if (!covered(*lscm_uv))
return false;
out = sample_at(*lscm_uv);
@@ -2391,10 +2211,9 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
const TextureColorRequest *color,
bool flip_normals,
BakeStageRecorder *debug,
TextureBakeStats *stats,
const std::vector<Vec2f> *model_uv)
TextureBakeStats *stats)
{
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data, model_uv);
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data);
if (!combined)
return mesh; // nothing decodable to displace with
@@ -2564,8 +2383,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
// part of it (half a tile in the neighbour's colour, a tile edge that wanders).
const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize);
const ColorFieldSampler color_sampler =
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure, model_uv) :
ColorFieldSampler{};
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure) : ColorFieldSampler{};
//
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
@@ -2697,8 +2515,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
const TextureColorRequest *color,
bool flip_normals,
BakeStageRecorder *debug,
TextureBakeStats *stats,
const std::vector<Vec2f> *model_uv)
TextureBakeStats *stats)
{
// The classic path moves the vertices the mesh already has, so there is no budget to report on.
(void) stats;
@@ -2735,7 +2552,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
if (options.pipeline_v2)
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals,
debug, stats, model_uv);
debug, stats);
// Layers are combined in slot order, like stacked layers in an image editor: each one folds its
// own displacement into the running total via its blend mode (see TextureBlendMode).
@@ -2853,19 +2670,13 @@ static indexed_triangle_set build_texture_displacement_in_place(
selector_dirty = true;
const bool color_this_layer = want_color && layer->color_enabled;
// The model's own channel is per base-mesh *triangle*, so reading it needs to know which base
// triangle each (possibly brush-split) patch triangle came from - the colour pass's map, asked
// for here as well.
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
model_uv != nullptr && model_uv->size() == mesh.indices.size() * 3;
// A flat-colour image is matched against the filaments alone (see TextureColorRequest).
const ColorQuantizeFn &layer_quantize =
(color_this_layer && color->quantize_pure && analyze_texture_detail(*layer).flat_colors) ? color->quantize_pure
: color->quantize;
std::vector<int> patch_source; // sub-triangle -> base mesh triangle; built when colouring or mapping by ModelUV
std::vector<int> patch_source; // sub-triangle -> base mesh triangle, only built when colouring
const indexed_triangle_set patch =
selector.get_facets_strict(EnforcerBlockerType::ENFORCER,
(color_this_layer || model_uv_layer) ? &patch_source : nullptr);
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, color_this_layer ? &patch_source : nullptr);
if (patch.indices.empty())
continue;
// get_facets_strict() returns the same vertex array whichever state is asked for (only the
@@ -2896,25 +2707,18 @@ static indexed_triangle_set build_texture_displacement_in_place(
// A real unwrap of the whole patch, computed once here rather than per vertex - it is a
// per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this
// for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()).
// ModelUV takes its corners from the volume's own channel instead of solving anything, and
// collapses them to one per vertex the same way the unwrap does.
const std::vector<Vec2f> model_corner_uvs = model_uv_layer ?
compute_model_corner_uvs(mesh, *model_uv, patch, patch_source) :
std::vector<Vec2f>{};
const std::vector<Vec2f> patch_uvs = model_uv_layer ? collapse_corner_uvs(patch, model_corner_uvs) :
(layer->projection_method == TextureProjectionMethod::LSCM) ?
const std::vector<Vec2f> lscm_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
compute_lscm_uvs(patch, *layer) :
std::vector<Vec2f>{};
// The colour pass below samples per *triangle*, so it takes the per-corner unwrap instead: the
// per-vertex collapse above would hand a triangle at a seam the island layout did not join its
// neighbour's placement, painting one triangle per face from the wrong part of the texture.
// (The displacement itself stays on patch_uvs - a vertex has one position, so one height.)
const std::vector<Vec2f> patch_corner_uvs = model_uv_layer ? model_corner_uvs :
(layer->projection_method == TextureProjectionMethod::LSCM) ?
// (The displacement itself stays on lscm_uvs - a vertex has one position, so one height.)
const std::vector<Vec2f> lscm_corner_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
compute_lscm_corner_uvs(patch, *layer) :
std::vector<Vec2f>{};
const bool corner_uv_ok = !patch_corner_uvs.empty() &&
patch_corner_uvs.size() == patch.indices.size() * 3;
const bool corner_uv_ok = !lscm_corner_uvs.empty() &&
lscm_corner_uvs.size() == patch.indices.size() * 3;
// Colour, if this layer carries any. Area-weighted over each base triangle's *painted* part,
// so a triangle the brush only clipped a corner off takes the colour of that corner rather
@@ -2944,7 +2748,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
// the per-vertex height sampling uses, evaluated at the centroid instead.
Vec3f n = Vec3f::Zero();
Vec2f uv = Vec2f::Zero();
bool have_uv = !patch_uvs.empty();
bool have_uv = !lscm_uvs.empty();
for (int k = 0; k < 3; ++k) {
const int vi = t[k];
if (vi < int(vertex_normals.size()))
@@ -2952,9 +2756,9 @@ static indexed_triangle_set build_texture_displacement_in_place(
if (!have_uv)
continue;
if (corner_uv_ok)
uv += patch_corner_uvs[j * 3 + size_t(k)];
else if (size_t(vi) < patch_uvs.size())
uv += patch_uvs[size_t(vi)];
uv += lscm_corner_uvs[j * 3 + size_t(k)];
else if (size_t(vi) < lscm_uvs.size())
uv += lscm_uvs[size_t(vi)];
else
have_uv = false;
}
@@ -3028,7 +2832,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
[&](const tbb::blocked_range<size_t> &range) {
for (size_t k = range.begin(); k < range.end(); ++k) {
const size_t vi = size_t(layer_vertices[k]);
const Vec2f *lscm_uv = patch_uvs.empty() ? nullptr : &patch_uvs[vi];
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi];
const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi],
patch_centroid, patch_axis, lscm_uv);
// midlevel is the height that means "stay put", so anything below it displaces
@@ -3161,8 +2965,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
const TextureColorRequest *color,
const Transform3d &volume_to_world,
BakeStageRecorder *debug,
TextureBakeStats *stats,
const std::vector<Vec2f> *model_uv)
TextureBakeStats *stats)
{
// An untransformed volume on an untransformed instance is by far the common case, and the round
// trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take
@@ -3172,7 +2975,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
const Transform3d frame = texture_displacement_bake_frame(volume_to_world);
if (frame.matrix().isApprox(Transform3d::Identity().matrix()))
return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color,
false, debug, stats, model_uv);
false, debug, stats);
const Transform3d to_local = frame.inverse();
// A mirroring placement leaves the positions correct but every winding-derived normal pointing
@@ -3186,10 +2989,8 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
v = (frame * v.cast<double>()).cast<float>();
const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0;
// The UV channel rides along untouched: a uv is a coordinate in the texture, not on the model, so
// the frame the mesh is measured in does not reach it.
indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options,
progress, color, mirrored, debug, stats, model_uv);
progress, color, mirrored, debug, stats);
// Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in
// the volume's local frame, so the stages are brought back the same way the result is.
if (debug != nullptr)
@@ -3261,8 +3062,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data,
volume.texture_displacement_options, {}, nullptr,
texture_displacement_volume_to_world(volume), nullptr, nullptr,
volume.has_texture_uv() ? &volume.texture_uv : nullptr);
texture_displacement_volume_to_world(volume));
}
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
@@ -3326,18 +3126,16 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
namespace {
// One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr so
// the returned closure owns it for as long as the subdivider keeps calling back.
// A per-triangle uv map turned into something a *point* sampler can use. Neither of the two maps that
// are not a formula from position to uv - the LSCM unwrap and the model's own UV channel - can be
// evaluated at an arbitrary point directly, so a point is placed on the painted patch (the nearest
// patch triangle, and its barycentric coordinates there) and the uv is interpolated from that
// triangle's own per-corner uvs. Exact for a point on the base surface, which is where both samplers
// are queried: the displacement samples refined positions before moving them, and the colour pass
// samples the foot point on the painted pieces.
// An unwrap turned into something a *point* sampler can use. LSCM has no formula from position to
// uv - it is a per-triangle map - so a point is placed on the painted patch (the nearest patch
// triangle, and its barycentric coordinates there) and the uv is interpolated from that triangle's own
// per-corner uvs. Exact for a point on the base surface, which is where both samplers are queried: the
// displacement samples refined positions before moving them, and the colour pass samples the foot
// point on the painted pieces.
struct LscmLookup {
indexed_triangle_set patch; // the layer's painted patch, as the uvs were computed on
indexed_triangle_set patch; // the layer's painted patch, as the unwrap was solved on
AABBTreeIndirect::Tree3f tree;
// compute_lscm_corner_uvs(patch, layer), or compute_model_corner_uvs() for a ModelUV layer
std::vector<Vec2f> corner;
std::vector<Vec2f> corner; // compute_lscm_corner_uvs(patch, layer)
// False when `pos` is not on this layer's patch (farther than `tol`): there is no uv there, so the
// layer contributes nothing - the same as a non-tiled texture outside its placement.
@@ -3350,9 +3148,15 @@ struct LscmLookup {
if (d2 < 0.f || d2 > tol * tol || hit >= patch.indices.size())
return false;
const stl_triangle_vertex_indices &t = patch.indices[hit];
float w1 = 0.f, w2 = 0.f; // a degenerate triangle takes its centroid's uv
barycentric_weights(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], patch.vertices[size_t(t[2])],
foot, w1, w2);
const Vec3f &a = patch.vertices[size_t(t[0])], &b = patch.vertices[size_t(t[1])], &c = patch.vertices[size_t(t[2])];
const Vec3f e0 = b - a, e1 = c - a, ep = foot - a;
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
const float den = d00 * d11 - d01 * d01;
float w1 = 1.f / 3.f, w2 = 1.f / 3.f; // a degenerate triangle takes its centroid's uv
if (std::abs(den) > 1e-20f) {
w1 = (d11 * dp0 - d01 * dp1) / den;
w2 = (d00 * dp1 - d01 * dp0) / den;
}
const Vec2f *c3 = &corner[hit * 3];
uv = (1.f - w1 - w2) * c3[0] + w1 * c3[1] + w2 * c3[2];
return true;
@@ -3411,7 +3215,7 @@ struct PreparedLayer {
// where a layer is placed. `need_color` additionally drops layers that cannot contribute colour.
std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
const indexed_triangle_set &base_mesh, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data, bool need_color, const std::vector<Vec2f> *model_uv)
const TextureDisplacementFacetsData &facets_data, bool need_color)
{
auto prepared = std::make_shared<std::vector<PreparedLayer>>();
@@ -3442,14 +3246,9 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
if (tex.empty() || (need_color && !tex.has_color()))
continue;
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
model_uv != nullptr && model_uv->size() == base_mesh.indices.size() * 3;
TriangleSelector selector(selector_mesh);
selector.deserialize(data, false);
std::vector<int> patch_source; // only the model's own channel needs it; see the classic path
const indexed_triangle_set patch =
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, model_uv_layer ? &patch_source : nullptr);
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
if (patch.indices.empty())
continue;
@@ -3459,16 +3258,12 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
texture_displacement_patch_frame(patch, vertex_normals, centroid, axis, average_normal);
std::shared_ptr<const LscmLookup> lscm;
if (model_uv_layer || layer->projection_method == TextureProjectionMethod::LSCM) {
if (layer->projection_method == TextureProjectionMethod::LSCM) {
// Solved on the very patch the classic path and the GUI solve it on (same geometry, seam
// angle and edges), so it hits the unwrap cache and lands exactly where the UV editor
// shows it, hand-placed islands and UV edits included. A ModelUV layer needs no solve at
// all - its corners are read straight out of the volume's channel - but it is looked up
// through the same per-triangle machinery, for the same reason: there is no formula from a
// point to its uv.
// shows it, hand-placed islands and UV edits included.
auto l = std::make_shared<LscmLookup>();
l->corner = model_uv_layer ? compute_model_corner_uvs(base_mesh, *model_uv, patch, patch_source) :
compute_lscm_corner_uvs(patch, *layer);
l->corner = compute_lscm_corner_uvs(patch, *layer);
if (l->corner.size() == patch.indices.size() * 3) {
l->patch = patch;
l->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(l->patch.vertices, l->patch.indices);
@@ -3494,12 +3289,11 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize,
ColorQuantizeFn quantize_pure,
const std::vector<Vec2f> *model_uv)
ColorQuantizeFn quantize_pure)
{
if (!quantize)
return nullptr;
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true, model_uv);
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true);
if (prepared->empty())
return nullptr;
// Per layer: a flat-colour image is matched against the filaments alone, when that quantizer
@@ -3530,10 +3324,9 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
const std::vector<Vec2f> *model_uv)
const TextureDisplacementFacetsData &facets_data)
{
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false, model_uv);
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false);
if (prepared->empty())
return nullptr;
@@ -5517,44 +5310,40 @@ 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;
// read() skips a value of the wrong type, but the vector, matrix, seam, island and UV-override arrays
// below read their numbers with get<>(), which throws on one; that rejects the document, like text that
// does not parse.
// 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);
// The panel and the paint masks are indexed by slot, so a layer without a valid one rejects the
// document rather than reaching them.
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
return false;
read(j, "name", l.name);
read(j, "path", l.path);
read(j, "path_in_3mf", l.path_in_3mf);
@@ -5621,7 +5410,7 @@ bool texture_displacement_layers_from_json(const std::string
out_layers.push_back(std::move(l));
}
}
} catch (const nlohmann::json::exception &) {
} catch (...) {
return false;
}
+5 -118
View File
@@ -86,17 +86,6 @@ enum class TextureProjectionMethod : int
// that is inherent to view projection and is the user's call, not a bug. The projector's two
// in-plane axes live in TextureDisplacementLayer::view_project_right/up.
ViewProjected = 4,
// The model's own texture coordinates, as the file it was imported from carried them (see
// ModelVolume::texture_uv). Nothing is unwrapped or projected: the artist's layout *is* the
// placement, so a texture authored against that layout - the model's own albedo map, say - lands
// exactly where it was painted. Automatically picked for a new layer on a volume that has a UV
// channel, since re-unwrapping a model that already carries one can only be worse than using it.
//
// The uv is already normalized, so unlike every method above it is not measured in millimetres:
// "Tile size" and the non-square aspect correction do not apply to it (the layer's rotation and
// offset still do). Falls back to Triplanar when the volume has no channel, exactly as LSCM falls
// back when a patch cannot be flattened.
ModelUV = 5,
};
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
@@ -132,11 +121,6 @@ enum class TextureBlendMode : int
Subtract = 1, // acc - value (carve this layer's relief out of the layers below)
Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer)
Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor)
// acc = value: this layer replaces what is under it instead of combining with it, so where it is
// painted only its own relief remains. The image editor's "Normal" mode, under the name the panel
// uses. Note the stack's *colour* already behaves this way - a higher slot's colour overwrites a
// lower one's where they overlap - so this is what makes the displacement agree with it.
Override = 4,
};
// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`.
@@ -440,10 +424,7 @@ struct TextureDisplacementOptions
// 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 a valid JSON object, a number in a
// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is
// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape
// keeps its default.
// 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);
@@ -584,13 +565,7 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
// square case and leaves the coordinate exactly as it always was.
//
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
// the image's proportions) applies, and only the rotation and offset are left.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
bool normalized_uv = false);
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
@@ -627,31 +602,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr);
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
//
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
// simplification - they leave the surface where it was - and useless after a bake, which moves the
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
// is.
//
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
// on its own.
//
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
// carry, which is not the same as "carry nothing".
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh);
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
@@ -765,55 +715,6 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map.
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
// Three UVs per patch triangle, read out of the model's *own* UV channel - the
// TextureProjectionMethod::ModelUV counterpart of compute_lscm_corner_uvs(), and laid out the same way
// (corner k of triangle i at index 3i+k).
//
// `model_uv` is the volume's channel: three uvs per triangle of `base_mesh`, in its triangle order
// (see ModelVolume::texture_uv). `patch_source` maps each patch triangle back to the base-mesh
// triangle it was cut from - TriangleSelector::get_facets_strict()'s out_source. A patch triangle is
// always a piece of exactly one base triangle, so each of its corners is interpolated barycentrically
// inside that triangle's three uvs. That is what keeps a sub-triangle the brush split (whose corners
// are not base-mesh vertices at all) on the texture, and what keeps the two sides of a UV seam
// reading from their own island instead of from one averaged coordinate.
//
// Returns an empty vector when the channel does not match `base_mesh`, when `patch_source` does not
// match `patch`, or when either is empty - the callers treat that as "no placement" and fall back,
// exactly as they do for an unwrap that failed.
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
const std::vector<Vec2f> &model_uv,
const indexed_triangle_set &patch,
const std::vector<int> &patch_source);
// One UV per patch vertex, collapsed from a per-corner array: the first corner to reach a vertex wins.
// This is the per-corner/per-vertex split compute_lscm_uvs() documents, applied to any corner array -
// displacement is per-vertex, so a vertex on a seam has to settle on one of its copies.
// Returns an empty vector if `corner` is not three per patch triangle.
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner);
// Resamples a per-corner UV channel from `src_mesh` onto `dst_mesh`, for when something replaces a
// volume's mesh and the model's own UV channel has to come along: the colour import's painted mesh,
// the gizmo's remesh/subdivide, the default bake pipeline's rebuild. Each destination corner takes the
// uv of its closest point on the source surface, interpolated barycentrically inside the source
// triangle it lands on - the same transfer LscmLookup does per point, run over every corner.
//
// Exact wherever the two meshes describe the same surface, which is the case for every caller here
// (they subdivide, repair or displace it, they do not reshape it). A corner that lands nowhere near
// the source - farther than `max_distance` (negative: no limit) - takes (0, 0).
//
// One thing a positional transfer cannot reproduce: a *seam* vertex, where one position carries a
// different uv in each island that meets there. Position alone does not say which island a corner
// belongs to, so corners on a seam all take whichever island the nearest-triangle lookup lands on,
// and the texture can be off by up to one triangle in a band along the island borders. Doing better
// needs a triangle-to-triangle correspondence, which only some of the stages that replace a mesh
// have (a remesh has none at all).
//
// Returns an empty vector when `src_uv` is not three per source triangle, or when either mesh is empty.
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
const std::vector<Vec2f> &src_uv,
const indexed_triangle_set &dst_mesh,
float max_distance = -1.f);
// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs).
inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); }
@@ -872,10 +773,6 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
//
// `color`, when given, also reports which filament each triangle should print in - see
// TextureColorRequest.
//
// `model_uv`, when given, is the volume's own UV channel (three uvs per base-mesh triangle, see
// ModelVolume::texture_uv) and is what a TextureProjectionMethod::ModelUV layer maps through. A layer
// set to ModelUV without it falls back to Triplanar.
struct TextureColorRequest
{
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
@@ -950,9 +847,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
// stage, already brought back into `base_mesh`'s frame.
BakeStageRecorder *debug = nullptr,
// When given, receives what the bake spent.
TextureBakeStats *stats = nullptr,
// The model's own UV channel; see above.
const std::vector<Vec2f> *model_uv = nullptr);
TextureBakeStats *stats = nullptr);
// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own.
// The mesh is shared by every instance, so a multi-instance object can only be baked for one of
@@ -1027,15 +922,11 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize,
ColorQuantizeFn quantize_pure = nullptr,
// The model's own UV channel, for ModelUV layers; see
// build_texture_displacement().
const std::vector<Vec2f> *model_uv = nullptr);
ColorQuantizeFn quantize_pure = nullptr);
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
const std::vector<Vec2f> *model_uv = nullptr);
const TextureDisplacementFacetsData &facets_data);
// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a
// shared cache so a midpoint is computed once and reused by both triangles on either side of that
@@ -1240,10 +1131,6 @@ struct TextureDisplacementPrepareResult
{
indexed_triangle_set mesh;
TextureDisplacementFacetsData masks;
// The model's own UV channel, resampled onto `mesh` (see transfer_uv_channel()). Empty when the
// volume had none. Like the masks it is handed back rather than left to the caller to recompute,
// because only the run itself knows which stages replaced the mesh.
std::vector<Vec2f> uv;
// The remesh landed but no layer's paint survived being carried onto it. Nothing is committed:
// everything downstream is driven by that paint, so baking on would bake a flat mesh.
bool paint_lost = false;
-21
View File
@@ -25,7 +25,6 @@
#include "TextureToColor/ColorUtils.hpp"
#include "Model.hpp"
#include "TextureDisplacement.hpp"
#include "TextureToColor/TriMesh.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
@@ -555,29 +554,9 @@ bool apply_painted_mesh_to_volume(
<< ", unit_scale=" << unit_scale << ")";
}
// The model's own UV channel is indexed by triangle, and this replaces the triangles, so it has
// to be resampled onto them. Kept rather than dropped because it is the layout the model was
// authored against: a texture-displacement layer set to ModelUV maps through it, and the colour
// import is exactly the path a model that carries one arrives by.
const indexed_triangle_set old_its = volume.has_texture_uv() ? volume.mesh().its : indexed_triangle_set{};
const std::vector<Vec2f> old_uv = volume.texture_uv;
volume.set_mesh(std::move(new_mesh));
volume.calculate_convex_hull();
// Resampled here and not further down, while the two meshes still share a frame: the painted mesh
// was just shifted by the same source.mesh_offset the old one was centred by, so the only thing
// between them is whatever drift the repair introduced. The re-centering below translates the mesh
// again, and a translation is exactly what a closest-point transfer cannot see through - it would
// read every uv off a point that far away. A uv is a coordinate in the texture, so it does not care
// about either shift; it just has to be read before the second one happens.
if (!old_its.indices.empty())
volume.texture_uv = transfer_uv_channel(old_its, old_uv, volume.mesh().its);
else
// No channel to carry, so make sure none is left behind either: a leftover of the right length
// would read as a valid channel against a mesh it was never measured on.
volume.reset_texture_uv();
// Re-center the replaced mesh so its bbox center sits at the origin,
// matching what center_geometry_after_creation did for the original mesh.
// CGAL repair / subdivision may shift the bbox center (drift); without
+4 -1
View File
@@ -1,4 +1,5 @@
#pragma once
#ifndef libslic3r_Timer_hpp_
#define libslic3r_Timer_hpp_
#include "TriangleMesh.hpp"
@@ -9,3 +10,5 @@ public:
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
};
} // namespace Slic3r
#endif // libslic3r_Timer_hpp_
+1
View File
@@ -109,6 +109,7 @@ extern std::string format_memsize_MB(size_t n);
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
// Return string to be added to the boost::log output to inform about the current process memory allocation.
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
// Latter is used to get the memory info from SysInfoDialog.
extern std::string log_memory_info(bool ignore_loglevel = false);
extern void disable_multi_threading();
// Returns the size of physical memory (RAM) in bytes.
+3 -1
View File
@@ -1744,7 +1744,9 @@ std::string format_diameter_to_str(double diameter, int precision)
return oss.str();
}
// Returns platform-specific string to be used as log output.
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
// The latter parses the string with (semi)colons as separators, it should look about as
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
std::string log_memory_info(bool ignore_loglevel)
{
std::string out;
+3
View File
@@ -416,6 +416,8 @@ set(SLIC3R_GUI_SOURCES
GUI/MultiSendMachineModel.hpp
GUI/MultiTaskManagerPage.cpp
GUI/MultiTaskManagerPage.hpp
GUI/MultiTaskModel.cpp
GUI/MultiTaskModel.hpp
GUI/NetworkTestDialog.cpp
GUI/NetworkTestDialog.hpp
GUI/Notebook.cpp
@@ -800,6 +802,7 @@ set(SLIC3R_GUI_SOURCES
Utils/PrintHost.hpp
Utils/Process.cpp
Utils/Process.hpp
Utils/Profile.hpp
Utils/RaycastManager.cpp
Utils/RaycastManager.hpp
Utils/Repetier.cpp
+4 -1
View File
@@ -1,4 +1,5 @@
#pragma once
#ifndef slic3r_GUI_SingleChoice_hpp_
#define slic3r_GUI_SingleChoice_hpp_
#include "GUI_Utils.hpp"
#include "Plater.hpp"
@@ -32,3 +33,5 @@ private:
};
}} // namespace Slic3r::GUI
#endif
+19
View File
@@ -0,0 +1,19 @@
#include <exception>
#include <stdexcept>
#include "libslic3r/Exception.hpp"
namespace Slic3r {
class ConfigError : public Slic3r::RuntimeError {
using Slic3r::RuntimeError::RuntimeError;
};
namespace GUI {
class ConfigGUITypeError : public ConfigError {
using ConfigError::ConfigError;
};
} // namespace GUI
} // namespace Slic3r
+6 -7
View File
@@ -3232,16 +3232,9 @@ bool GUI_App::on_init_inner()
#ifdef _MSW_DARK_MODE
#ifndef __WINDOWS__
// The label colours and the dark mode flag were set above from the saved mode. When the system theme
// differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session.
const bool saved_dark_mode = dark_mode();
wxSystemAppearance app = wxSystemSettings::GetAppearance();
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
GUI::wxGetApp().app_config->save();
if (dark_mode() != saved_dark_mode) {
init_label_colours();
Update_dark_mode_flag();
}
#endif // __APPLE__
@@ -4862,6 +4855,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
}
void GUI_App::system_info()
{
//SysInfoDialog dlg;
//dlg.ShowModal();
}
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
{
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
+1
View File
@@ -519,6 +519,7 @@ public:
void check_printer_presets();
void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const;
@@ -494,17 +494,7 @@ bool GLGizmoTextureDisplacement::on_init()
{ctrl + _L("Mouse wheel"), m_desc["gap_area"]},
toggle_wireframe_shortcut
};
// Both ways into the same input box on any slider in the panel: ImGui turns a slider into one when
// it is Ctrl+clicked or when it is given the keyboard focus, which is what the edit icon asks for.
m_desc["type_value"] = _L("Type an exact value into a slider");
m_shortcuts_sliders = {
{ctrl + _L("Left mouse button"), m_desc["type_value"]},
{_L("Edit icon at a slider's right end"), m_desc["type_value"]},
};
for (std::vector<std::pair<wxString, wxString>> *list : {&m_shortcuts_brush, &m_shortcuts_bucket_fill, &m_shortcuts_gap_fill})
list->insert(list->end(), m_shortcuts_sliders.begin(), m_shortcuts_sliders.end());
return true;
}
@@ -1231,33 +1221,9 @@ indexed_triangle_set GLGizmoTextureDisplacement::patch_in_world(const indexed_tr
return world;
}
std::vector<Vec2f> GLGizmoTextureDisplacement::model_uv_corners(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const
{
const ModelVolume *mv = texture_volume();
if (layer.projection_method != TextureProjectionMethod::ModelUV || patch_source == nullptr || mv == nullptr ||
!mv->has_texture_uv())
return {};
// The channel is indexed against the volume's *local* mesh, and a uv is a coordinate in the
// texture rather than on the model, so unlike every projected method this never goes through
// patch_in_world() - there is no millimetre measurement here for a world scale to change.
std::vector<Vec2f> uv = compute_model_corner_uvs(mv->mesh().its, mv->texture_uv, local_patch, *patch_source);
for (Vec2f &p : uv)
// Already a uv: no tiling scale, no aspect correction - see apply_uv_transform().
p = apply_uv_transform(p, layer, 1.f, /* normalized_uv */ true);
return uv;
}
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const
const TextureDisplacementLayer &layer) const
{
if (layer.projection_method == TextureProjectionMethod::ModelUV)
// Collapsed to one per vertex the same way the unwrap is - a vertex has one position, so the
// displacement can only read one height for it.
return collapse_corner_uvs(local_patch, model_uv_corners(local_patch, layer, patch_source));
// The bake maps the texture in world millimetres, so "Tile size (mm)" means the same thing on a
// scaled instance as it does on an untouched one (see build_texture_displacement()). Everything
// that has to agree with the bake - the fast preview's uvs, the checker/distortion overlays -
@@ -1317,11 +1283,8 @@ int GLGizmoTextureDisplacement::layer_projection_frame(const indexed_triangle_se
}
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const
const TextureDisplacementLayer &layer) const
{
if (layer.projection_method == TextureProjectionMethod::ModelUV)
return model_uv_corners(local_patch, layer, patch_source);
if (layer.projection_method == TextureProjectionMethod::LSCM) {
const indexed_triangle_set patch = patch_in_world(local_patch);
const float aspect = layer_texture_aspect(layer);
@@ -1331,7 +1294,7 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const in
return uv;
}
// Single-valued per point: fan the per-vertex result out over the corners.
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer, patch_source);
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer);
if (per_vertex.size() != local_patch.vertices.size())
return {};
std::vector<Vec2f> corner(local_patch.indices.size() * 3);
@@ -1352,11 +1315,7 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
// Uses the *live* selector (not the flushed model facet data), so this reflects an in-progress
// stroke immediately rather than only once it ends - the point of this preview mode is to be
// the fast, no-CPU-meshing one.
// The source map comes along because a ModelUV layer's uvs are read per base-mesh triangle; every
// other mapping ignores it.
std::vector<int> patch_source;
const indexed_triangle_set patch =
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
if (patch.indices.empty())
return;
@@ -1380,8 +1339,7 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
// Per *corner*, not per vertex: the mesh below is flat (unshared) anyway, so each triangle can
// carry its own chart's UVs - see compute_layer_corner_uvs().
const TextureDisplacementLayer *active = active_layer();
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active, &patch_source) :
std::vector<Vec2f>{};
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active) : std::vector<Vec2f>{};
m_shaded_preview_uses_vertex_uv = corner_uv.size() == patch.indices.size() * 3;
if (!m_shaded_preview_uses_vertex_uv)
corner_uv.clear();
@@ -2038,9 +1996,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
const ModelVolume *mv = texture_volume();
if (mv == nullptr || m_triangle_selectors.empty())
return;
std::vector<int> patch_source; // only ModelUV reads it; see compute_layer_vertex_uvs()
const indexed_triangle_set patch =
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
if (patch.indices.empty())
return;
// Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a
@@ -2054,7 +2010,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
// The checker samples wherever the projection puts it; the projections the shader can't
// reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader.
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer, &patch_source);
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
const bool have_uvs = uv.size() == patch.vertices.size();
m_uvcheck_uses_vertex_uv = have_uvs;
m_uvcheck_projection_mode = have_uvs ? 0 :
@@ -2063,7 +2019,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
// different uv in each island it borders, so the shared-vertex form drew one triangle per face from
// a neighbouring island's placement. Only the *drawing* needs this; the distortion metric below is
// a per-vertex average by construction and keeps using `uv`.
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer, &patch_source);
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer);
const bool have_corner_uvs = have_uvs && corner_uv.size() == patch.indices.size() * 3;
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
@@ -2339,8 +2295,6 @@ void GLGizmoTextureDisplacement::queue_preview_job()
if (input.options.pipeline_v2)
input.options.v2_max_triangles_k = 0;
input.volume_to_world = texture_displacement_volume_to_world(*mv);
if (mv->has_texture_uv())
input.model_uv = mv->texture_uv;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
// Captured here rather than read in the handler: the palette is main-thread state, and the preview
@@ -3750,13 +3704,10 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
// Paint / Erase, brush cursor shape, mapping, tiling, placement and the dock toggle.
"texture_displacement_add.svg", "texture_displacement_add_negative.svg", "circle_paint.svg",
"menu_obj_sphere.svg", "menu_obj_cube.svg", "menu_obj_cylinder.svg", "texture_displacement_map_unwrap.svg",
"texture_displacement_map_view.svg", "texture_displacement_map_model_uv.svg",
"texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
"texture_displacement_map_view.svg", "texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
// The "type a value instead" icon at the right end of every slider.
"edit.svg",
};
std::vector<std::string> paths;
paths.reserve(names.size());
@@ -3800,11 +3751,6 @@ void GLGizmoTextureDisplacement::add_texture_layer()
TextureDisplacementLayer layer;
layer.slot = free_slot;
// A model that came with a UV channel is mapped by it. Re-unwrapping such a model throws away the
// layout it was authored against and can only be worse than using it, so the default follows the
// model rather than the other way round; the Mapping row still offers every other method.
if (mv->has_texture_uv())
layer.projection_method = TextureProjectionMethod::ModelUV;
// Start the layer off on the first library texture rather than on nothing at all: a textureless
// layer looks broken (painting on it appears to do nothing, because there is no height map to
// displace by). The user swaps it for another from the layer's own picker.
@@ -4950,8 +4896,7 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const std::vector<Vec2f> &model_uv,
const TextureDisplacementPrepareParams &params,
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
BakeStageRecorder *debug)
{
@@ -4978,13 +4923,6 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
bool had_paint = false;
for (const TriangleSelector::TriangleSplittingData &m : masks)
had_paint = had_paint || TriangleSelector::has_facets(m, EnforcerBlockerType::ENFORCER);
// The model's own UV channel for whatever `mesh` currently is. Every stage below keeps the input's
// surface - it evens out, refines or cuts the triangles lying on it - so each corner reads its uv
// straight off `base`'s channel at its own position instead of being carried stage by stage, which
// would compound each stage's approximation. Untouched while nothing has replaced the mesh yet.
const auto uv_now = [&]() {
return (model_uv.empty() || !changed) ? model_uv : transfer_uv_channel(base, model_uv, mesh.its);
};
if (!report(1))
return out;
@@ -5031,11 +4969,9 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// Feature-adaptive: sample the combined displacement so refinement follows texture
// curvature. A null sampler (only LSCM layers, or nothing decodable) falls back to the
// length baseline alone.
const std::vector<Vec2f> uv_stage = uv_now();
const std::vector<Vec2f> *uv_ptr = uv_stage.empty() ? nullptr : &uv_stage;
HeightFieldSampler sampler;
if (params.subdiv_feature)
sampler = make_combined_displacement_sampler(mesh.its, layers, current, uv_ptr);
sampler = make_combined_displacement_sampler(mesh.its, layers, current);
// Colour boundaries need triangles of their own - the chord test cannot see them, since
// the height field is perfectly smooth across a change of filament.
ColorFieldSampler color;
@@ -5043,7 +4979,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// boundaries are refined where the bake will actually change filament.
if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
make_palette_quantizer(color_settings.palette_pure), uv_ptr);
make_palette_quantizer(color_settings.palette_pure));
// The refinement follows perceived colour: a mix is one colour here, however the slicer
// interleaves its filaments layer by layer.
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
@@ -5102,10 +5038,8 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
std::vector<uint8_t> cut_region(mesh.its.indices.size(), 0);
for (size_t i = 0; i < cut_region.size() && i < source.size(); ++i)
cut_region[i] = (region[size_t(source[i])] & REFINE_PAINTED) ? 1 : 0;
const std::vector<Vec2f> uv_refined = uv_now();
const HeightFieldSampler refined_sampler =
make_combined_displacement_sampler(mesh.its, layers, current,
uv_refined.empty() ? nullptr : &uv_refined);
make_combined_displacement_sampler(mesh.its, layers, current);
std::vector<int> cut_source;
size_t cuts = 0;
indexed_triangle_set cut = cut_mesh_at_steps(mesh.its, cut_region, refined_sampler,
@@ -5129,7 +5063,6 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
if (!report(100) || !changed)
return out; // an empty result: nothing to commit, which is not a failure
out.uv = uv_now();
out.mesh = std::move(mesh.its);
out.masks = std::move(current);
return out;
@@ -5781,8 +5714,6 @@ void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareP
input.layers = mv->texture_displacement_layers;
input.params = params;
input.snapshot_name = snapshot_name;
if (mv->has_texture_uv())
input.model_uv = mv->texture_uv;
if (params.subdiv_color_edge_mm > 0.f)
input.color = color_settings_for(*mv);
@@ -6016,8 +5947,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
return s + "...";
};
// Label column + a slider filling the rest of the row, value printed inside the track.
// `right_inset` keeps a layer card's inner padding.
// Label column + a slider filling the rest of the row, value printed inside the track. Ctrl+click on
// any of these types a value in. `right_inset` keeps a layer card's inner padding.
const auto slider_label = [&](const wxString &label) {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
@@ -6025,107 +5956,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
};
// ---- Typed values on sliders ----
// Every slider in the panel goes through typed_slider(), so all of them take a number the same
// two ways: Ctrl+click on the track, or the edit icon at the right end for the same box without
// the modifier. Both end up in ImGui's own "temp input" mode - the icon only asks ImGui to focus
// the slider, which is what turns it into an input box (see SliderScalar) - so the parsing,
// the clamping and the Enter/click-away behaviour are ImGui's, not a second implementation.
// Retires a click whose row is no longer being drawn - the panel changed mode in between - so that
// it cannot fire later when that row comes back. Before any row is drawn, so a live one still wins.
if (!m_slider_type_request.empty() && ImGui::GetFrameCount() > m_slider_type_request_frame + 1)
m_slider_type_request.clear();
// Exactly as tall as the slider it sits beside, so it lines up with the track without the row
// having to be re-centred around it; icon_button() pads the glyph itself down to about 0.7 of this.
const float edit_icon_w = frame_h;
// What the slider's own width has to give up for the icon beside it.
const float edit_icon_room = edit_icon_w + gap_s;
// One end of a range, through the slider's own format, so the bounds in the tooltip read exactly
// like the value in the track. A parenthesised note in the format - "(auto)" - is dropped: it
// describes the current state, not the bound.
const auto fmt_bound = [](const char *format, auto v) {
char buf[64];
std::snprintf(buf, sizeof(buf), format, v);
std::string out(buf);
const size_t paren = out.find('(');
if (paren != std::string::npos)
out.erase(paren);
while (!out.empty() && out.back() == ' ')
out.pop_back();
return out;
};
const auto range_text = [](const std::string &lo, const std::string &hi) {
return Slic3r::format(_u8L("Range: %1% to %2%"), lo, hi);
};
const auto edit_icon_tip = [&](const std::string &range) {
return _u8L("Type a value instead of dragging. Ctrl+click on the slider does the same.") + "\n\n" + range;
};
// Draws `slider` at `item_w` (SetNextItemWidth's convention: negative measures back from the right
// edge, and the caller has already left edit_icon_room in it), then the edit icon. `tip` and the
// range go on both. Returns whatever the slider returned.
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::string &range,
const std::function<bool()> &slider) -> bool {
// Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next*
// frame, so it has to be issued before the slider it names, not from the icon beside it.
if (m_slider_type_request == id) {
ImGui::SetKeyboardFocusHere();
m_slider_type_request.clear();
m_parent.set_as_dirty(); // the focus only lands on a frame that actually gets drawn
}
ImGui::SetNextItemWidth(item_w);
const bool changed = slider();
// AllowWhenDisabled: several of these rows are drawn inside disabled_begin(), and a greyed
// slider is exactly when the user needs to be told what it is and why it is not moving.
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
wxString full = tip;
if (!full.IsEmpty())
full += "\n\n";
m_imgui->tooltip(full + from_u8(range), wrap_w);
}
// SameLine() puts the cursor back at the slider's own top (ItemSize records the Y the item was
// drawn at), so an icon of the same height lands on the track wherever the row placed it.
ImGui::SameLine(0.f, gap_s);
if (icon_button(int(ImGui::GetID(id)), "edit.svg", edit_icon_w, _L("Edit"), from_u8(edit_icon_tip(range)))) {
m_slider_type_request = id;
m_slider_type_request_frame = ImGui::GetFrameCount();
m_parent.set_as_dirty(); // the next frame is where the request above is turned into focus
}
return changed;
};
// The two forms every slider in the panel is one of. They derive the range from the bounds they are
// already given, so no row states its bounds or its format twice and the tooltip cannot drift from
// the track. `item_w` is as typed_row() takes it.
const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max, const char *format,
bool log, const wxString &tip) -> bool {
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderFloat(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
});
};
const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max, const char *format,
bool log, const wxString &tip) -> bool {
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderInt(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
});
};
// Label column + the slider filling the rest of the row.
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
bool log, float right_inset, const wxString &tip) -> bool {
bool log, float right_inset) -> bool {
slider_label(label);
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
};
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
float right_inset, const wxString &tip) -> bool {
float right_inset) -> bool {
slider_label(label);
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
};
const auto layer_name = [](const TextureDisplacementLayer &l) {
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
@@ -6349,10 +6190,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); };
vsep(icon_md);
if (is_brush_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm)));
centre_on_row(frame_h);
typed_slider("##cursor_radius",
std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room),
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"));
ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm",
ImGuiSliderFlags_AlwaysClamp);
hover_tip(m_desc.at("cursor_size"));
vsep(icon_sm);
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
centre_on_row(icon_sm);
@@ -6365,10 +6207,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Sphere - paints only within a ball around the point under the cursor")))
m_cursor_type = TriangleSelector::CursorType::SPHERE;
} else if (is_area_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX()));
centre_on_row(frame_h);
typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room),
&m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false,
_L("Angle threshold - the fill stops at edges sharper than this"));
ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°",
ImGuiSliderFlags_AlwaysClamp);
hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this"));
} else {
centre_on_row(frame_h);
ImGui::AlignTextToFramePadding();
@@ -6638,25 +6481,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// The three controls nearly every layer needs; everything else waits behind "More settings".
// Depth and tile size are logarithmic, so both ends of their wide ranges stay usable.
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad,
_L("Height of the relief: how far white in the texture lifts the surface, in "
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
// Mapped by the model's own UV layout there is no millimetre scale to set: the layout is the
// placement (see TextureProjectionMethod::ModelUV), so the slider would move nothing. The row
// states why rather than hiding: float_row()'s tooltip is shown for a disabled slider too.
const bool tile_size_na = layer.projection_method == TextureProjectionMethod::ModelUV;
m_imgui->disabled_begin(tile_size_na);
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad,
tile_size_na ?
_L("Mapped by the model's own UV layout, which is already the placement - one "
"copy of the texture covers it exactly, so there is no tile size to set.") :
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
"more often and makes its detail finer; with Tile off, this is the size of the "
"single copy."));
m_imgui->disabled_end();
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
"edge of the model."));
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad);
hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in "
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad);
hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
"more often and makes its detail finer; with Tile off, this is the size of the "
"single copy."));
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad);
hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
"edge of the model."));
if (m_layer_expanded[size_t(layer.slot)]) {
{
@@ -6671,22 +6505,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards;
// at 0.5 mid-grey is neutral and darker texels cut inwards.
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad,
_L("Which gray stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
"still push out - one image both embosses and engraves.\n\n"
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
"cut can pass through the other side."));
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad);
hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
"still push out - one image both embosses and engraves.\n\n"
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
"cut can pass through the other side."));
if (layer.midlevel > 0.f && layer.depth_mm > 1.f) {
ImGui::PushTextWrapPos(content_rx - card_pad);
m_imgui->warning_text(_L("Deep inward displacement may self-intersect."));
ImGui::PopTextWrapPos();
}
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad,
_L("Blurs the image before it is used, which rounds off hard steps and removes "
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
"costs fine detail."));
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad);
hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes "
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
"costs fine detail."));
// Edge fade: the relief flattens toward the boundary of the painted area.
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing);
@@ -6694,11 +6528,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"blends into the bare surface instead of stopping at a step."));
ImGui::SameLine();
m_imgui->disabled_begin(!layer.edge_smoothing);
m_preview_params_dirty |= typed_slider("##edge_amount", -(card_pad + edit_icon_room),
&layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", false,
_L("How far in the fade reaches, as a share of the painted area. Small "
"values soften a narrow band at the edge; 1 flattens almost all of it."));
ImGui::SetNextItemWidth(-card_pad);
m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f",
ImGuiSliderFlags_AlwaysClamp);
m_imgui->disabled_end();
hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values "
"soften a narrow band at the edge; 1 flattens almost all of it."));
// Invert and Colours share a row.
{
@@ -6743,23 +6578,23 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Every mix can become a filament slot, and there are only so many beside the
// physical filaments.
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad,
_L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created.")))
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created."));
// After the slider, so a change shows in the same frame.
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad,
_L("Cleans up single stray triangles of the wrong color, which detail finer "
"than the mesh leaves behind. Raise it if the result looks speckled, "
"lower it if small features are being swallowed.")))
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("Cleans up single stray triangles of the wrong color, which detail finer "
"than the mesh leaves behind. Raise it if the result looks speckled, "
"lower it if small features are being swallowed."));
}
}
// Mapping, as one icon per TextureProjectionMethod, in the enum's own order.
// Mapping, as five icons in TextureProjectionMethod's own order.
{
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
@@ -6787,26 +6622,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
{ "texture_displacement_map_view.svg", _L("From view"),
_L("From view - projects straight onto the painted area from where you are looking, like a slide "
"projector.") },
{ "texture_displacement_map_model_uv.svg", _L("Model UV"),
_L("Model UV - uses the texture coordinates the model itself came with, so a texture made for "
"that model lands exactly where it was painted. Nothing is unwrapped, and Tile size does "
"not apply: the model's own layout is the placement.") },
};
// Only a volume that actually carries a UV channel can be mapped by it. Shown
// greyed out rather than hidden, so the reason is visible instead of the control
// simply being missing.
const wxString model_uv_na =
(mv != nullptr && mv->has_texture_uv()) ?
wxString() :
_L("This model carries no texture coordinates of its own. Only a model imported with a UV "
"channel - a textured OBJ, say - can be mapped by them.");
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
if (mi > 0)
ImGui::SameLine(0.f, gap_s);
const auto method = static_cast<TextureProjectionMethod>(mi);
const wxString na = (method == TextureProjectionMethod::ModelUV) ? model_uv_na : wxString();
if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label,
mappings[mi].tip, na) &&
mappings[mi].tip) &&
layer.projection_method != method) {
// Capture the current view the moment "From view" is chosen, so it does something
// sensible immediately rather than projecting from a stale/default direction.
@@ -6873,10 +6695,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"gets the texture, and its border becomes the edge of the projection."));
if (projector_open) {
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad,
_L("How solid the frame window looks while you place it. Lower to see the "
"model through it; it does not affect the result.")))
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad))
m_projector_frame->set_opacity(m_projector_opacity);
hover_tip(_u8L("How solid the frame window looks while you place it. Lower to see the "
"model through it; it does not affect the result."));
if (m_imgui->button(_u8L("Apply projection frame"))) {
const int painted_count = apply_projection_frame();
@@ -6936,33 +6758,18 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement");
const std::string blend_override = _u8L_CONTEXT("Override", "Texture Displacement");
const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement");
const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement");
const std::string blend_divide = _u8L("Divide");
const char *blend_items[] = { blend_add.c_str(), blend_override.c_str(), blend_subtract.c_str(),
blend_multiply.c_str(), blend_divide.c_str() };
// The combo's order is the panel's, not the enum's - Override reads as the sibling of Add and
// belongs next to it, while TextureBlendMode's numbering is fixed by what projects already
// store. Hence the explicit mapping rather than a cast of the index.
static const TextureBlendMode blend_modes[] = { TextureBlendMode::Add, TextureBlendMode::Override,
TextureBlendMode::Subtract, TextureBlendMode::Multiply,
TextureBlendMode::Divide };
static_assert(IM_ARRAYSIZE(blend_items) == IM_ARRAYSIZE(blend_modes),
"every Blend entry needs its mode");
const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() };
// The first layer has nothing before it to combine with - build_texture_displacement() makes it
// add regardless - so its combo shows Add and cannot be changed.
const bool base_layer = li == 0;
int blend_mode = 0;
if (!base_layer)
for (int bi = 0; bi < int(IM_ARRAYSIZE(blend_modes)); ++bi)
if (blend_modes[bi] == layer.blend_mode)
blend_mode = bi;
int blend_mode = base_layer ? 0 : static_cast<int>(layer.blend_mode);
ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm));
m_imgui->disabled_begin(base_layer);
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer &&
blend_mode >= 0 && blend_mode < int(IM_ARRAYSIZE(blend_modes))) {
layer.blend_mode = blend_modes[blend_mode];
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) {
layer.blend_mode = static_cast<TextureBlendMode>(blend_mode);
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
@@ -6970,11 +6777,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->tooltip(base_layer ?
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
_u8L("What this layer does where it overlaps the ones below. Add and "
"Subtract pile relief on or carve it away. Override replaces them, "
"so only this layer's relief is left where it is painted. Multiply "
"and Divide scale what is underneath, which turns this layer into a "
"mask over it - there, Depth acts as a strength, and 1 mm leaves "
"white areas untouched."),
"Subtract pile relief on or carve it away. Multiply and Divide scale "
"what is underneath, which turns this layer into a mask over it - "
"there, Depth acts as a strength, and 1 mm leaves white areas "
"untouched."),
wrap_w);
ImGui::SameLine(0.f, gap_s);
@@ -7193,57 +6999,57 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// pattern can sample four points that all happen to land at similar heights, report no error,
// and stall before refinement ever starts.
if (float_row("##subdiv_target", m_subdivide_feature ? _L("Max edge") : _L("Target edge"), &m_subdivide_target_mm,
0.001f, 20.f, "%.3f mm", true, 0.f,
m_subdivide_feature ?
_L("No triangle in the painted area stays larger than this, even where the texture "
"is flat. Keep it near the size of the smallest feature you want to come out.") :
_L("Triangles in the painted area are split until none is larger than this. Smaller "
"means finer detail and, fast, a lot more triangles.")))
0.001f, 20.f, "%.3f mm", true, 0.f))
preview_live();
hover_tip(m_subdivide_feature ?
_u8L("No triangle in the painted area stays larger than this, even where the texture "
"is flat. Keep it near the size of the smallest feature you want to come out.") :
_u8L("Triangles in the painted area are split until none is larger than this. Smaller "
"means finer detail and, fast, a lot more triangles."));
if (m_subdivide_feature) {
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f,
_L("How far the mesh may sit from the shape the texture describes, in millimetres. "
"Smaller follows fine detail and costs triangles; larger only chases the big "
"features.")))
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f))
preview_live();
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f,
_L("A floor on triangle size, so a sharp step in the texture cannot be chased "
"forever. Lower it for finer relief, raise it if the count runs away at hard "
"edges.")))
hover_tip(_u8L("How far the mesh may sit from the shape the texture describes, in millimetres. "
"Smaller follows fine detail and costs triangles; larger only chases the big "
"features."));
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f))
preview_live();
hover_tip(_u8L("A floor on triangle size, so a sharp step in the texture cannot be chased "
"forever. Lower it for finer relief, raise it if the count runs away at hard "
"edges."));
}
// Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which
// exists whether or not "Follow texture detail" is on. 0 turns the band off.
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
_L("Triangle size along the outline of the painted area, where the relief drops back to the "
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
"outline. 0 turns it off.")))
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
preview_live();
hover_tip(_u8L("Triangle size along the outline of the painted area, where the relief drops back to the "
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
"outline. 0 turns it off."));
// Only worth showing when a layer is actually colouring: with no colour there is no boundary for it
// to refine and the control would do nothing whatever it is set to.
if (mv != nullptr && any_layer_colors(*mv)) {
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
_L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
"color edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of color. 0 turns it off.")))
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
preview_live();
hover_tip(_u8L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
"color edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of color. 0 turns it off."));
}
ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str());
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
} else {
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f,
_L("Splits every triangle of the model into four, this many times over. Each step "
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
"only the painted area unless you need the whole model finer."))) {
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f)) {
if (m_subdivide_editing)
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
m_parent.set_as_dirty();
}
hover_tip(_u8L("Splits every triangle of the model into four, this many times over. Each step "
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
"only the painted area unless you need the whole model finer."));
}
{
@@ -7305,9 +7111,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f;
}
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f,
_L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
"even mesh; this is what makes one out of an uneven import."));
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f);
hover_tip(_u8L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
"even mesh; this is what makes one out of an uneven import."));
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it "
@@ -7315,10 +7121,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"a cube comes back with wobbly edges."));
ImGui::SameLine();
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
typed_slider("##remesh_sharp_angle", -(1.f + edit_icon_room), &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", false,
_L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
"keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly."));
ImGui::SetNextItemWidth(-1.f);
ImGui::SliderFloat("##remesh_sharp_angle", &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", ImGuiSliderFlags_AlwaysClamp);
m_imgui->disabled_end();
hover_tip(_u8L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
"keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly."));
m_imgui->disabled_begin(mv == nullptr || busy);
if (ImGui::Button(_u8L("Remesh").c_str()))
@@ -7346,16 +7153,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"on a layer is a different thing: it blurs the image before it is used."));
if (opts.smooth_enabled) {
float percent = opts.smooth_strength * 100.f;
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f,
_L("How far each pass pulls a vertex towards its neighbours. High values round the "
"relief off quickly; low values need more passes but keep more of the detail."))) {
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) {
opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f);
m_preview_params_dirty = true;
}
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f,
_L("How many smoothing passes to run. More passes spread the smoothing further across "
"the surface; Strength decides how much each one moves.")))
hover_tip(_u8L("How far each pass pulls a vertex towards its neighbours. High values round the "
"relief off quickly; low values need more passes but keep more of the detail."));
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f))
m_preview_params_dirty = true;
hover_tip(_u8L("How many smoothing passes to run. More passes spread the smoothing further across "
"the surface; Strength decides how much each one moves."));
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Ignore outer ring"), opts.smooth_skip_border);
hover_tip(_u8L("Keeps the outer ring of the painted area out of the smoothing. Its neighbours "
@@ -7398,16 +7205,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k;
m_imgui->disabled_begin(auto_budget);
// TRN Slider value: %d is the triangle budget in thousands
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f,
_L("How many thousand triangles this bake may spend on the area you painted. "
"Relief already baked into the rest of the model is kept on top of it, so a "
"second bake somewhere else gets the same budget as the first. Raise it if the "
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
"the refinement produced, however many that is."))) {
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f)) {
opts.v2_max_triangles_k = shown_k;
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
hover_tip(_u8L("How many thousand triangles this bake may spend on the area you painted. "
"Relief already baked into the rest of the model is kept on top of it, so a "
"second bake somewhere else gets the same budget as the first. Raise it if the "
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
"the refinement produced, however many that is."));
}
if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) {
// Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines
@@ -7514,28 +7321,26 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"default. Untick to set both yourself - worth doing for a texture much finer or "
"much coarser than the part it sits on."));
ImGui::SameLine();
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm;
// Held in a local: the format outlives the call that reads it, and the slider and the
// range in its tooltip are then formatted the same way.
const std::string v2edge_format = auto_res ? _u8L("%.2f mm (auto)") : std::string("%.2f mm");
const wxString v2edge_tip =
auto_res && rec.edge_mm > 0.f ?
from_u8(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
"budget %2% k. Untick Auto to set them yourself."),
rec.edge_mm, rec.budget_k)) :
_L("How fine the mesh is made under the paint, in millimetres. It has to be "
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
"four times as many, and once they no longer fit the budget the bake simplifies "
"back down and the detail goes with it.");
m_imgui->disabled_begin(busy || auto_res);
if (typed_slider("##v2edge", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, &shown, 0.02f, 2.f,
v2edge_format.c_str(), true, v2edge_tip)) {
if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? _u8L("%.2f mm (auto)").c_str() : "%.2f mm",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
opts.v2_refine_mm = shown;
m_preview_params_dirty = true;
m_parent.set_as_dirty();
}
m_imgui->disabled_end();
if (auto_res && rec.edge_mm > 0.f)
hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
"budget %2% k. Untick Auto to set them yourself."),
rec.edge_mm, rec.budget_k));
else
hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be "
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
"four times as many, and once they no longer fit the budget the bake simplifies "
"back down and the detail goes with it."));
// What this resolution costs over what is painted, against what the budget allows. Refining
// past the budget is not an error - the bake simplifies back down to it - but the result
@@ -7567,17 +7372,18 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->text(_L("Triangles"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
m_imgui->disabled_begin(busy);
if (typed_slider_int("##subdivbudget", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room,
&m_subdivide_budget_k, 10, 2000, "+%d k", true,
_L("How many thousand triangles the refinement may add to the model. The triangle "
"that fits worst is always split first, so even a run that spends the lot has "
"spent it where it shows most. Raise it if the relief still looks coarse."))) {
if (ImGui::SliderInt("##subdivbudget", &m_subdivide_budget_k, 10, 2000, "+%d k",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
if (m_subdivide_editing)
rebuild_subdivide_preview();
m_parent.set_as_dirty();
}
m_imgui->disabled_end();
hover_tip(_u8L("How many thousand triangles the refinement may add to the model. The triangle "
"that fits worst is always split first, so even a run that spends the lot has "
"spent it where it shows most. Raise it if the relief still looks coarse."));
}
const float button_h = std::round(frame_h * 1.25f);
@@ -7596,7 +7402,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
: m_shortcuts_sliders; // the sliders are there whether or not a painting tool is
: std::vector<std::pair<wxString, wxString>>{};
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
@@ -61,12 +61,6 @@ public:
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
// `base`'s own UV channel, when it has one
// (ModelVolume::texture_uv). Resampled onto
// the result and reported back in it, and fed
// to the samplers so a ModelUV layer drives
// the refinement through its own layout.
const std::vector<Vec2f> &model_uv,
const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings,
const DisplacementProgressFn &progress,
@@ -413,12 +407,6 @@ private:
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
// Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
// later - and clears it; the frame number is what retires a request whose row has gone off screen
// in the meantime. Panel state only, not saved with the project.
std::string m_slider_type_request;
int m_slider_type_request_frame = 0;
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
@@ -833,31 +821,18 @@ private:
// Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first).
static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count);
// Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an
// unwrap), ModelUV (the model's own channel) and ViewProjected (a projector plane the shader
// doesn't know). One entry per patch/base vertex, already through apply_uv_transform(). Empty for
// Triplanar/Cylindrical/Spherical, which the shader projects on its own. Shared by the shaded
// preview and the UV-check overlay.
//
// `patch_source` maps each patch triangle back to its base-mesh triangle
// (TriangleSelector::get_facets_strict()'s out_source). Only ModelUV needs it - its channel is per
// base triangle - and without it a ModelUV layer reports no uvs and the preview falls back.
// unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base
// vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which
// the shader projects on its own. Shared by the shaded preview and the UV-check overlay.
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source = nullptr) const;
const TextureDisplacementLayer &layer) const;
// The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what
// the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different
// UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined
// seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is
// single-valued per point, so there a corner's UV is just its vertex's.
std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source = nullptr) const;
// The model's own channel over `local_patch`, already through apply_uv_transform() - three per
// patch triangle. Empty when the volume has no channel, or when `patch_source` is missing. Shared
// by the two functions above so the per-vertex and per-corner forms can never disagree.
std::vector<Vec2f> model_uv_corners(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const;
const TextureDisplacementLayer &layer) const;
// The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two
// wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the
// shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview
@@ -1047,9 +1022,6 @@ private:
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
@@ -8,7 +8,6 @@
#include "libslic3r/TextureDisplacement.hpp"
#include <exception>
#include <cstddef>
#include <cstdint>
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory>
@@ -27,7 +26,6 @@
#include "slic3r/Utils/UndoRedo.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <array>
namespace Slic3r::GUI {
@@ -76,8 +74,7 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
}
return true;
},
color, m_input.volume_to_world, nullptr, &m_stats,
m_input.model_uv.empty() ? nullptr : &m_input.model_uv));
color, m_input.volume_to_world, nullptr, &m_stats));
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
@@ -129,71 +126,16 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
{ mix.a_percent(), 100 - mix.a_percent() });
});
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
// the triangles matter - the annotations are per facet and reference no vertex index - so a
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
// reason to remap anything.
const bool topology_changed = m_input.options.pipeline_v2 ||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
// no longer existed, which is what scrambled the colours of an imported coloured model even
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
//
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
// could only lose detail.
std::array<TriangleSelector::TriangleSplittingData, 4> before;
if (topology_changed) {
before[0] = volume->supported_facets.get_data();
before[1] = volume->seam_facets.get_data();
before[2] = volume->mmu_segmentation_facets.get_data();
before[3] = volume->fuzzy_skin_facets.get_data();
}
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
if (topology_changed) {
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
&volume->mmu_segmentation_facets,
&volume->fuzzy_skin_facets };
for (size_t ch = 0; ch < target.size(); ++ch) {
const std::vector<uint8_t> state =
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
// Nothing was painted in this channel, or there is nothing to paint it onto: either
// way the annotation must not keep describing the mesh that is gone.
if (state.empty()) {
target[ch]->reset();
continue;
}
TriangleSelector selector(volume->mesh());
bool any = false;
for (size_t i = 0; i < state.size(); ++i)
if (state[i] != 0) {
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
any = true;
}
if (any)
target[ch]->set(selector);
else
target[ch]->reset();
}
}
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
// and one the user never painted at all stays at NONE, which already means "the volume's own
// filament". That is what confines the effect to the painted area without having to invent a
// colour for everything outside it. What was already painted is whatever the transfer above
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
// topology-preserving, so triangle i is still triangle i.
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
TriangleSelector selector(volume->mesh());
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
@@ -217,6 +159,7 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
@@ -224,14 +167,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
volume->texture_displacement_facet(slot).reset();
}
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
// from: a displaced vertex came from its foot point on that surface, which is the position its
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
if (!m_input.model_uv.empty())
volume->texture_uv = topology_changed ? transfer_uv_channel(m_input.base_mesh, m_input.model_uv,
volume->mesh().its) :
m_input.model_uv;
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
@@ -284,8 +219,6 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
input.take_snapshot = take_snapshot;
input.volume_id = volume.id();
input.base_mesh = volume.mesh().its;
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
input.layers = volume.texture_displacement_layers;
input.options = volume.texture_displacement_options;
input.volume_to_world = texture_displacement_volume_to_world(volume);
@@ -34,9 +34,6 @@ struct TextureDisplacementBakeInput
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
// paint gizmo writes - alongside the displaced geometry.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
// layer maps through, and what the commit puts back on the baked mesh.
std::vector<Vec2f> model_uv;
// Whether this job pushes its own undo step when it commits. False when the caller has already
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
// and subdivides first and has to undo as a single action.
@@ -17,7 +17,6 @@
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory>
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TriangleMesh.hpp"
namespace Slic3r::GUI {
@@ -53,14 +52,13 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
indexed_triangle_set mesh = m_input.base_mesh;
TextureDisplacementFacetsData masks = m_input.facets_data;
std::vector<Vec2f> uv = m_input.model_uv;
// The default pipeline can only move vertices the mesh already has, so its recipe starts with the
// preparation - which is where "first it remeshes, then it refines" actually happens. The
// experimental pipeline refines as part of the bake and has nothing to prepare.
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
const TextureDisplacementPrepareResult prepared =
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_params,
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
m_input.color,
// Preparation is roughly half the run; the bake
// takes the progress bar from there.
@@ -79,14 +77,12 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
if (!prepared.mesh.indices.empty()) {
mesh = prepared.mesh;
masks = prepared.masks;
uv = prepared.uv; // resampled onto the prepared mesh, as the prepare commit stores it
}
}
build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
[&report](int pct) { return report(50 + pct / 2); },
/* color */ nullptr, m_input.volume_to_world, &recorder, /* stats */ nullptr,
uv.empty() ? nullptr : &uv);
/* color */ nullptr, m_input.volume_to_world, &recorder);
m_stages = recorder.take();
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
@@ -119,8 +115,6 @@ void queue_texture_displacement_debug(const ModelVolume &volume, const TextureCo
input.check_topology = check_topology;
input.color = color;
input.volume_to_world = texture_displacement_volume_to_world(volume);
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
@@ -47,9 +47,6 @@ struct TextureDisplacementDebugInput
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run
// produces no colour of its own.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so the recipe captured here is the one a
// ModelUV layer actually bakes through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
// The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a
// multi-million triangle stage costs more than the stage did. On by default because a stage that
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
// idle loop.
int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.model_uv, m_input.params, m_input.color,
m_input.params, m_input.color,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
@@ -106,10 +106,6 @@ void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &
volume->restore_painting(saved_painting);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
// restore_painting(), which resets it along with the extra facets for the same reason: it is
// indexed by triangle and the triangles have changed.
volume->texture_uv = std::move(m_result.uv);
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
@@ -9,7 +9,6 @@
#include "Job.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
namespace Slic3r {
@@ -48,10 +47,6 @@ struct TextureDisplacementPrepareInput
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
// perceived colour, never the interleaving that realises a mix.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
// resamples it onto the mesh it produces, and the commit puts it back - like the masks, it is
// indexed by triangle and so cannot survive the stages on its own.
std::vector<Vec2f> model_uv;
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the
// displacement job that follows commits into this same step rather than pushing its own.
std::string snapshot_name;
@@ -48,8 +48,7 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
(!m_current_generation ||
m_current_generation->load() == m_generation);
},
color, m_input.volume_to_world, nullptr, nullptr,
m_input.model_uv.empty() ? nullptr : &m_input.model_uv);
color, m_input.volume_to_world);
}
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
@@ -29,9 +29,6 @@ struct TextureDisplacementPreviewInput
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
// same layout it will bake through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
};
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
+6
View File
@@ -0,0 +1,6 @@
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
+11
View File
@@ -0,0 +1,11 @@
#ifndef slic3r_MultiTaskModel_hpp_
#define slic3r_MultiTaskModel_hpp_
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
#endif
+232
View File
@@ -0,0 +1,232 @@
#include "SysInfoDialog.hpp"
#include "I18N.hpp"
#include "3DScene.hpp"
#include "GUI.hpp"
#include "../Utils/UndoRedo.hpp"
#include "Plater.hpp"
#include <string>
#include <Eigen/Core>
#include <wx/clipbrd.h>
#include <wx/platinfo.h>
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "wxExtensions.hpp"
#include "libslic3r/BlacklistedLibraryCheck.hpp"
#include "libslic3r/Color.hpp"
#include "format.hpp"
#ifdef _WIN32
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <psapi.h>
#endif /* _WIN32 */
namespace Slic3r {
namespace GUI {
std::string get_main_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
if (!format_as_html)
out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end;
out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end;
out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end;
out << line_end;
out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end;
out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end;
out << b_start <<
#if defined _WIN32
"Windows Version: "
#else
// Hopefully some kind of unix / linux.
"System Version: "
#endif
<< b_end << wxPlatformInfo::Get().GetOperatingSystemDescription() << line_end;
out << b_start << "Total RAM size [MB]: " << b_end << Slic3r::format_memsize_MB(Slic3r::total_physical_memory());
return out.str();
}
std::string get_mem_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
std::string mem_info_str = log_memory_info(true);
std::istringstream mem_info(mem_info_str);
std::string value;
while (std::getline(mem_info, value, ':')) {
out << b_start << (value+": ") << b_end;
std::getline(mem_info, value, ';');
out << value << line_end;
}
const Slic3r::UndoRedo::Stack &stack = wxGetApp().plater()->undo_redo_stack_main();
out << b_start << "RAM size reserved for the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.get_memory_limit()) << line_end;
out << b_start << "RAM size occupied by the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.memsize()) << line_end << line_end;
return out.str();
}
SysInfoDialog::SysInfoDialog()
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY, (wxGetApp().is_editor() ? wxString(SLIC3R_APP_FULL_NAME) : wxString(GCODEVIEWER_APP_NAME)) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
wxColour bgr_clr = wxGetApp().get_window_default_clr();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
SetBackgroundColour(bgr_clr);
SetFont(wxGetApp().normal_font());
wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->SetMinSize(wxSize(50 * wxGetApp().em_unit(), -1));
auto main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10);
// logo
m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192);
m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp());
hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL);
wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL);
hsizer->Add(vsizer, 1, wxEXPAND|wxLEFT, 20);
// title
{
wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize);
wxFont title_font = wxGetApp().bold_font();
title_font.SetFamily(wxFONTFAMILY_ROMAN);
title_font.SetPointSize(22);
title->SetFont(title_font);
vsizer->Add(title, 0, wxEXPAND | wxTOP, wxGetApp().em_unit()/*50*/);
}
// main_info_text
wxFont font = get_default_font(this);
const auto text_clr = wxGetApp().get_label_clr_default();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue()));
auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()));
const int fs = font.GetPointSize() - 1;
int size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER);
{
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_html->SetBorders(2);
const auto text = wxString::Format(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
get_main_info(true));
m_html->SetPage(text);
vsizer->Add(m_html, 1, wxEXPAND | wxBOTTOM, wxGetApp().em_unit());
}
// opengl_info
m_opengl_info_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO);
{
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * wxGetApp().em_unit()));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_opengl_info_html->SetBorders(10);
wxString blacklisted_libraries_message;
#ifdef WIN32
std::wstring blacklisted_libraries = BlacklistedLibraryCheck::get_instance().get_blacklisted_string().c_str();
if (! blacklisted_libraries.empty())
blacklisted_libraries_message = wxString("<br><b>") + _L("Blacklisted libraries loaded into OrcaSlicer process:") + "</b><br>" + blacklisted_libraries;
#endif // WIN32
const auto text = GUI::format_wxstr(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s<br>%s<br>%s<br>%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
blacklisted_libraries_message,
get_mem_info(true), wxGetApp().get_gl_info(false),
"<b>" + _L("SIMD is supported:") + "</b> " + Eigen::SimdInstructionSetsInUse());
m_opengl_info_html->SetPage(text);
main_sizer->Add(m_opengl_info_html, 1, wxEXPAND | wxBOTTOM, 15);
}
wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK);
m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize);
buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5);
m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this);
this->SetEscapeId(wxID_OK);
this->Bind(wxEVT_BUTTON, &SysInfoDialog::onCloseDialog, this, wxID_OK);
main_sizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3);
wxGetApp().UpdateDlgDarkUI(this, true);
// this->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
// logo->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
SetSizer(main_sizer);
main_sizer->SetSizeHints(this);
}
void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect)
{
m_logo_bmp.msw_rescale();
m_logo->SetBitmap(m_logo_bmp.bmp());
wxFont font = get_default_font(this);
const int fs = font.GetPointSize() - 1;
int font_size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_html->Refresh();
const int& em = em_unit();
msw_buttons_rescale(this, em, { wxID_OK, m_btn_copy_to_clipboard->GetId() });
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * em));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_opengl_info_html->Refresh();
const wxSize& size = wxSize(65 * em, 55 * em);
SetMinSize(size);
Fit();
Refresh();
}
void SysInfoDialog::onCopyToClipboard(wxEvent &)
{
wxTheClipboard->Open();
const auto text = get_main_info(false) + "\n" + wxGetApp().get_gl_info(true);
wxTheClipboard->SetData(new wxTextDataObject(text));
wxTheClipboard->Close();
}
void SysInfoDialog::onCloseDialog(wxEvent &)
{
this->EndModal(wxID_CLOSE);
}
} // namespace GUI
} // namespace Slic3r
+36
View File
@@ -0,0 +1,36 @@
#ifndef slic3r_GUI_SysInfoDialog_hpp_
#define slic3r_GUI_SysInfoDialog_hpp_
#include <wx/event.h>
#include <wx/wx.h>
#include <wx/html/htmlwin.h>
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
namespace Slic3r {
namespace GUI {
class SysInfoDialog : public DPIDialog
{
ScalableBitmap m_logo_bmp;
wxStaticBitmap* m_logo;
wxHtmlWindow* m_opengl_info_html;
wxHtmlWindow* m_html;
wxButton* m_btn_copy_to_clipboard;
public:
SysInfoDialog();
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
void onCopyToClipboard(wxEvent &);
void onCloseDialog(wxEvent &);
};
} // namespace GUI
} // namespace Slic3r
#endif
View File
View File
+15
View File
@@ -0,0 +1,15 @@
#ifndef slic3r_wxinit_hpp_
#define slic3r_wxinit_hpp_
#include <wx/wx.h>
#include <wx/intl.h>
#include <wx/html/htmlwin.h>
// Perl redefines a _ macro, so we undef this one
#undef _
// We do want to use translation however, so define it as __ so we can do a find/replace
// later when we no longer need to undef _
#define __(s) wxGetTranslation((s))
#endif
+19
View File
@@ -0,0 +1,19 @@
#ifndef slic3r_GUI_Profile_hpp_
#define slic3r_GUI_Profile_hpp_
// Profiling support using the Shiny intrusive profiler
//#define SLIC3R_PROFILE_GUI
#if defined(SLIC3R_PROFILE) && defined(SLIC3R_PROFILE_GUI)
#include <Shiny/Shiny.h>
#define SLIC3R_GUI_PROFILE_FUNC() PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name) PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE() PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x) PROFILE_OUTPUT(x)
#else
#define SLIC3R_GUI_PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x)
#endif
#endif // slic3r_GUI_Profile_hpp_
+311 -163
View File
@@ -29,9 +29,11 @@
#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>
@@ -39,7 +41,6 @@
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <functional>
#include <initializer_list>
#include <catch2/catch_tostring.hpp>
#include <Eigen/Core>
@@ -351,10 +352,10 @@ static bool replace_in_3mf_entry(const std::string& path, const std::string& suf
return replaced;
}
// Stores `model` as a one-plate project.
static void store_project(Model& model, const std::string& path, SaveStrategy strategy)
// 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_paint_src");
ScopedTemporaryDir backup_dir("orca_project_src");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig cfg;
@@ -364,7 +365,7 @@ static void store_project(Model& model, const std::string& path, SaveStrategy st
sp.path = path.c_str();
sp.model = &model;
sp.config = &cfg;
sp.strategy = strategy;
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
sp.plate_data_list.push_back(&plate);
REQUIRE(store_bbs_3mf(sp));
}
@@ -383,7 +384,7 @@ static void store_painted_cube(const std::string& path)
selector.set_facet(1, EnforcerBlockerType::Extruder3);
REQUIRE(volume->mmu_segmentation_facets.set(selector));
}
store_project(model, path, SaveStrategy::Zip64 | SaveStrategy::Silence);
store_project(path, model);
}
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
@@ -509,163 +510,6 @@ TEST_CASE("A project with malformed paint data loads without the damaged facet",
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1);
}
// Paints `triangles` of `volume` on texture displacement layer slot `slot`.
static void paint_texture_slot(ModelVolume& volume, int slot, std::initializer_list<int> triangles)
{
TriangleSelector selector(volume.mesh());
for (const int triangle : triangles)
selector.set_facet(triangle, EnforcerBlockerType::ENFORCER);
REQUIRE(volume.texture_displacement_facet(slot).set(selector));
}
// Every triangle's texture paint on every slot, in the form the exporter writes it, so two volumes
// compare equal only when the same triangles are painted on the same slots.
static std::vector<std::string> texture_paint(const ModelVolume& volume)
{
std::vector<std::string> paint;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
for (size_t i = 0; i < volume.mesh().its.indices.size(); ++i)
paint.push_back(volume.texture_displacement_facet(slot).get_triangle_as_string(int(i)));
return paint;
}
// A cube with one texture displacement layer in slot 2 and texture paint on gapped triangles of two
// slots. The layer's image bytes need not decode: the archive stores them as they are.
static ModelVolume& add_texture_displaced_cube(Model& model)
{
ModelObject* object = model.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
TextureDisplacementLayer layer;
layer.slot = 2;
layer.name = "Bark";
layer.depth_mm = 1.25f;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::vector<unsigned char>{ 0x89, 'P', 'N', 'G', 1, 2, 3 });
volume->texture_displacement_layers.push_back(layer);
volume->texture_displacement_options.active_slot = 2;
paint_texture_slot(*volume, 2, { 3, 7 });
paint_texture_slot(*volume, 5, { 10 });
return *volume;
}
TEST_CASE("Texture displacement layers, textures and paint survive a project round trip", "[3mf]")
{
// A single model file, and one file per object (read by a separate importer).
const SaveStrategy strategy = GENERATE(SaveStrategy::Zip64 | SaveStrategy::Silence,
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel);
INFO("save strategy " << int(strategy));
ScopedTemporaryFile temp(".3mf");
Model source;
const ModelVolume& painted = add_texture_displaced_cube(source);
const std::vector<std::string> paint = texture_paint(painted);
REQUIRE(std::count_if(paint.begin(), paint.end(), [](const std::string& s) { return !s.empty(); }) == 3);
store_project(source, temp.string(), strategy);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 1);
const ModelVolume& volume = *model.objects.front()->volumes.front();
REQUIRE(volume.texture_displacement_layers.size() == 1);
const TextureDisplacementLayer& layer = volume.texture_displacement_layers.front();
CHECK(layer.slot == 2);
CHECK(layer.name == "Bark");
CHECK_THAT(layer.depth_mm, Catch::Matchers::WithinAbs(1.25f, 1e-6f));
REQUIRE(layer.image_data);
CHECK(*layer.image_data == *painted.texture_displacement_layers.front().image_data);
CHECK(volume.texture_displacement_options.active_slot == 2);
CHECK(texture_paint(volume) == paint);
}
TEST_CASE("Volumes sharing a mesh keep their own texture displacement paint", "[3mf]")
{
Model source;
ModelObject* first_object = source.add_object();
ModelVolume* first = first_object->add_volume(make_cube(10., 10., 10.));
first_object->add_instance();
ModelVolume* second = source.add_object(*first_object)->volumes.front();
REQUIRE(second->mesh_ptr() == first->mesh_ptr());
paint_texture_slot(*first, 0, { 3 });
paint_texture_slot(*second, 0, { 7 });
// What a project save from the app uses: one file per object, an identical mesh written once.
ScopedTemporaryFile temp(".3mf");
store_project(source, temp.string(),
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::ShareMesh);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 2);
CHECK(texture_paint(*model.objects[0]->volumes.front()) == texture_paint(*first));
CHECK(texture_paint(*model.objects[1]->volumes.front()) == texture_paint(*second));
}
TEST_CASE("A texture displacement layer never loads its image from outside the project", "[3mf]")
{
// A file the layer names, on the machine opening the project, that the project did not carry.
ScopedTemporaryFile texture(".png");
{
boost::nowide::ofstream file(texture.string(), std::ios::binary);
file << "not part of the project";
}
REQUIRE(boost::filesystem::file_size(texture.path()) > 0);
Model source;
ModelObject* object = source.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
TextureDisplacementLayer layer;
layer.slot = 0;
layer.name = "Elsewhere";
layer.path = texture.string();
layer.depth_mm = 0.75f;
volume->texture_displacement_layers.push_back(layer);
ScopedTemporaryFile temp(".3mf");
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 1);
const std::vector<TextureDisplacementLayer>& layers = model.objects.front()->volumes.front()->texture_displacement_layers;
REQUIRE(layers.size() == 1);
CHECK(layers.front().name == "Elsewhere");
CHECK_THAT(layers.front().depth_mm, Catch::Matchers::WithinAbs(0.75f, 1e-6f));
CHECK_FALSE(layers.front().image_data);
}
TEST_CASE("A project with a malformed texture displacement value still loads", "[3mf][Regression]")
{
const auto [from, to] = GENERATE(table<std::string, std::string>({
{ "\"islands\":[]", "\"islands\":[[null,0,0,1]]" }, // a number that is null
{ "\"slot\":2", "\"slot\":8" }, // a slot past the last
}));
INFO(from << " -> " << to);
ScopedTemporaryFile temp(".3mf");
Model source;
const ModelVolume& painted = add_texture_displaced_cube(source);
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
REQUIRE(replace_in_3mf_entry(temp.string(), ".json", from, to));
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);
const ModelVolume& volume = *model.objects.front()->volumes.front();
// The layer stack is dropped as a whole; the paint, stored apart from it, still loads.
CHECK(volume.texture_displacement_layers.empty());
CHECK(texture_paint(volume) == texture_paint(painted));
}
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
// store_bbs_3mf is the sole exporter the app calls, and 3mf.cpp's load_3mf is reached only for
// files fingerprinted as PrusaSlicer's, which never carry a recipe. This locks in both halves:
@@ -2245,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]);
}
+35 -373
View File
@@ -32,7 +32,6 @@
#include <fstream>
#include <limits>
#include <set>
#include <string>
#include <boost/filesystem.hpp>
#include "libslic3r/AABBMesh.hpp"
@@ -2301,32 +2300,45 @@ 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));
}
TEST_CASE("Texture displacement JSON with a wrong-typed element or an out-of-range slot is rejected", "[TextureDisplacement]")
// 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 layer = GENERATE(as<std::string>{},
R"({"slot":0,"offset":["x",0]})",
R"({"slot":0,"view_project_right":[null,0,0]})",
R"({"slot":0,"view_project_matrix":[0,0,0,0,0,0,0,0,0,0,0,"x"]})",
R"({"slot":0,"lscm_seam_edges":[1,null]})",
R"({"slot":0,"islands":[[0,0,"x",1]]})",
R"({"slot":0,"lscm_uv_overrides":[[null,0,0]]})",
R"({"slot":8})",
R"({"slot":-1})",
R"({"slot":"x"})",
R"({"name":"No slot"})");
INFO(layer);
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(1);
layers.front().name = "Kept";
TextureDisplacementOptions options;
options.color_mix_count = 7;
bool parsed = true;
REQUIRE_NOTHROW(parsed = texture_displacement_layers_from_json(R"({"version":1,"layers":[)" + layer + "]}", layers, options));
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);
CHECK(layers.front().name == "Kept");
CHECK(options.color_mix_count == 7);
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]")
@@ -2346,353 +2358,3 @@ TEST_CASE("whether a layer's texture has colour agrees with its decode", "[Textu
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
}
// ---------------------------------------------------------------------------------------------
// The model's own UV channel, and the Override blend mode
// ---------------------------------------------------------------------------------------------
// A grayscale image that is black in its left half and white in its right half, so where a sample
// lands along u is readable straight off the height it comes back with.
static std::shared_ptr<std::vector<unsigned char>> make_left_right_split_png(size_t w = 16, size_t h = 16)
{
std::vector<uint8_t> pixels(w * h);
for (size_t y = 0; y < h; ++y)
for (size_t x = 0; x < w; ++x)
pixels[y * w + x] = (x < w / 2) ? 0 : 255;
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
REQUIRE_FALSE(bytes.empty());
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// Where a sheet coordinate lands in uv: across [UV_LO, UV_LO + UV_SPAN] rather than the full [0, 1].
// The inset keeps the tests off the exact edge of an untiled placement, where there is deliberately no
// texture at all rather than its border texel (see DecodedHeightTexture::sample()) - a real model's
// channel does reach 1.0, and a vertex sitting exactly there displaces by nothing on purpose.
static constexpr float UV_LO = 0.1f;
static constexpr float UV_SPAN = 0.8f;
static float sheet_uv_at(float coord, float size, bool mirror = false)
{
const float t = coord / size;
return UV_LO + UV_SPAN * (mirror ? 1.f - t : t);
}
// One UV per triangle corner: u follows x, v follows y. `mirror_u` mirrors u, which is what the tests
// use to show the bake is really reading the channel rather than projecting the position.
static std::vector<Vec2f> sheet_uv_channel(const indexed_triangle_set &sheet, float size, bool mirror_u = false)
{
std::vector<Vec2f> uv(sheet.indices.size() * 3);
for (size_t f = 0; f < sheet.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = sheet.vertices[size_t(sheet.indices[f][k])];
uv[f * 3 + size_t(k)] = Vec2f(sheet_uv_at(p.x(), size, mirror_u), sheet_uv_at(p.y(), size));
}
return uv;
}
TEST_CASE("Override blend replaces the displacement under it instead of combining with it",
"[TextureDisplacement]")
{
CHECK_THAT(blend_displacement(3.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
// Nothing underneath yet is still just the value, so an Override layer at the bottom of the
// stack behaves exactly like the Add the bake forces there.
CHECK_THAT(blend_displacement(0.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
// The sign carries, and a zero sample really does flatten what is under it - that is the point.
CHECK_THAT(blend_displacement(3.f, -1.f, TextureBlendMode::Override), WithinAbs(-1.f, 1e-6f));
CHECK_THAT(blend_displacement(3.f, 0.f, TextureBlendMode::Override), WithinAbs(0.f, 1e-6f));
}
TEST_CASE("an Override layer leaves only its own relief where two layers overlap", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
TextureDisplacementLayer base;
base.slot = 0;
base.depth_mm = 2.0f;
base.tiling_scale = 5.0f;
base.image_data = make_flat_gray_png(255); // full depth everywhere
TextureDisplacementLayer second = base;
second.slot = 1;
second.depth_mm = 0.5f;
second.blend_mode = TextureBlendMode::Override;
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(cube);
facets[1] = facets[0];
const indexed_triangle_set overridden = build_texture_displacement(cube, {base, second}, facets, classic_options());
REQUIRE(overridden.vertices.size() == cube.vertices.size());
// Add would have piled the two up to 2.5 mm; Override keeps the upper layer's 0.5 mm alone.
TextureDisplacementLayer added = second;
added.blend_mode = TextureBlendMode::Add;
const indexed_triangle_set summed = build_texture_displacement(cube, {base, added}, facets, classic_options());
REQUIRE(summed.vertices.size() == cube.vertices.size());
for (size_t i = 0; i < cube.vertices.size(); ++i) {
if (!above_resting_plane(cube, i))
continue;
CHECK_THAT((overridden.vertices[i] - cube.vertices[i]).norm(), WithinAbs(0.5f, 1e-3f));
CHECK_THAT((summed.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.5f, 1e-3f));
}
}
TEST_CASE("the ModelUV mapping displaces through the model's own UV channel", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 1.f);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.image_data = make_left_right_split_png();
layer.tile_enabled = false; // the layout is the placement; nothing should repeat
layer.projection_method = TextureProjectionMethod::ModelUV;
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(sheet);
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
const std::vector<Vec2f> flipped = sheet_uv_channel(sheet, size, /* mirror_u */ true);
const indexed_triangle_set out = build_texture_displacement(sheet, {layer}, facets, classic_options(), {}, nullptr,
Transform3d::Identity(), nullptr, nullptr, &uv);
REQUIRE(out.vertices.size() == sheet.vertices.size());
// The image is black for u < 0.5 and white above it, and the channel's u rises with x across the
// sheet, so it comes out flat on its left half and raised by the full depth on its right.
size_t low = 0, high = 0;
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
const float x = sheet.vertices[i].x();
const float dz = out.vertices[i].z() - sheet.vertices[i].z();
if (x < 0.4f * size) {
CHECK_THAT(dz, WithinAbs(0.f, 1e-3f));
++low;
} else if (x > 0.6f * size) {
CHECK_THAT(dz, WithinAbs(layer.depth_mm, 1e-3f));
++high;
}
}
REQUIRE(low > 0);
REQUIRE(high > 0);
// Mirroring the channel mirrors the relief: the mapping really is the channel's, not a planar
// projection of the position (which would be unchanged by it).
const indexed_triangle_set mirrored = build_texture_displacement(sheet, {layer}, facets, classic_options(), {},
nullptr, Transform3d::Identity(), nullptr, nullptr,
&flipped);
REQUIRE(mirrored.vertices.size() == sheet.vertices.size());
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
const float x = sheet.vertices[i].x();
if (x < 0.4f * size)
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(layer.depth_mm, 1e-3f));
else if (x > 0.6f * size)
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(0.f, 1e-3f));
}
}
TEST_CASE("a ModelUV layer falls back to Triplanar when the volume carries no UV channel",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 2.f);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.tiling_scale = 5.0f;
layer.image_data = make_flat_gray_png(255);
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(sheet);
layer.projection_method = TextureProjectionMethod::Triplanar;
const indexed_triangle_set as_triplanar = build_texture_displacement(sheet, {layer}, facets, classic_options());
layer.projection_method = TextureProjectionMethod::ModelUV;
const indexed_triangle_set no_channel = build_texture_displacement(sheet, {layer}, facets, classic_options());
REQUIRE(no_channel.vertices.size() == as_triplanar.vertices.size());
for (size_t i = 0; i < as_triplanar.vertices.size(); ++i)
CHECK_THAT((no_channel.vertices[i] - as_triplanar.vertices[i]).norm(), WithinAbs(0.f, 1e-5f));
}
TEST_CASE("a bake with colour off on every layer reports no colour at all", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.tiling_scale = 5.0f;
layer.image_data = make_flat_rgb_png(200, 40, 10); // has colour to give...
layer.color_enabled = false; // ...but is not asked for it
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(cube);
std::vector<uint8_t> triangle_color;
TextureColorRequest request;
request.quantize = [](const Vec3f &) { return 0; }; // would colour everything, if asked
request.out_triangle = &triangle_color;
const indexed_triangle_set out = build_texture_displacement(cube, {layer}, facets, classic_options(), {}, &request);
REQUIRE_FALSE(out.vertices.empty());
// Every entry 0 ("this triangle takes no colour from the texture"), so the commit leaves whatever
// the model was already painted with exactly as it was.
for (const uint8_t entry : triangle_color)
CHECK(entry == 0);
}
TEST_CASE("transfer_uv_channel resamples a UV channel onto a subdivided copy of the same surface",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 5.f);
const indexed_triangle_set fine = make_flat_sheet(size, 1.f);
const std::vector<Vec2f> uv = sheet_uv_channel(coarse, size);
const std::vector<Vec2f> moved = transfer_uv_channel(coarse, uv, fine);
REQUIRE(moved.size() == fine.indices.size() * 3);
// The channel is affine in position here (u = x/size, v = y/size), so a correct transfer
// reproduces it exactly at every corner of the finer mesh.
for (size_t f = 0; f < fine.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = fine.vertices[size_t(fine.indices[f][k])];
CHECK_THAT(moved[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
CHECK_THAT(moved[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
}
// A mismatched channel is refused rather than read past its end.
CHECK(transfer_uv_channel(coarse, {Vec2f::Zero()}, fine).empty());
CHECK(transfer_uv_channel(coarse, uv, indexed_triangle_set{}).empty());
}
TEST_CASE("the model's per-corner UVs survive a brush split and collapse to one per vertex",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 5.f);
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
// The whole sheet as a patch, with the source map the channel is read through.
const TriangleMesh tm(sheet);
TriangleSelector selector(tm);
for (int f = 0; f < int(sheet.indices.size()); ++f)
selector.set_facet(f, EnforcerBlockerType::ENFORCER);
std::vector<int> source;
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER, &source);
REQUIRE(patch.indices.size() == source.size());
const std::vector<Vec2f> corner = compute_model_corner_uvs(sheet, uv, patch, source);
REQUIRE(corner.size() == patch.indices.size() * 3);
// An unsplit patch triangle *is* its source triangle, so its corners reproduce the channel.
for (size_t f = 0; f < patch.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = patch.vertices[size_t(patch.indices[f][k])];
CHECK_THAT(corner[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
CHECK_THAT(corner[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
}
const std::vector<Vec2f> per_vertex = collapse_corner_uvs(patch, corner);
REQUIRE(per_vertex.size() == patch.vertices.size());
for (size_t i = 0; i < patch.vertices.size(); ++i) {
CHECK_THAT(per_vertex[i].x(), WithinAbs(sheet_uv_at(patch.vertices[i].x(), size), 1e-4f));
CHECK_THAT(per_vertex[i].y(), WithinAbs(sheet_uv_at(patch.vertices[i].y(), size), 1e-4f));
}
// Mismatched inputs report nothing rather than guessing.
CHECK(compute_model_corner_uvs(sheet, {Vec2f::Zero()}, patch, source).empty());
CHECK(compute_model_corner_uvs(sheet, uv, patch, {}).empty());
CHECK(collapse_corner_uvs(patch, {Vec2f::Zero()}).empty());
}
TEST_CASE("facet paint is carried onto a rebuilt mesh by each facet's foot point", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
// Two filaments, split down the middle - the pattern the transfer has to reproduce.
const TriangleMesh coarse_mesh(coarse);
TriangleSelector painter(coarse_mesh);
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
painter.set_facet(int(t), x < 0.5f * size ? EnforcerBlockerType::Extruder1 : EnforcerBlockerType::Extruder2);
}
const TriangleSelector::TriangleSplittingData paint = painter.serialize();
// A finer mesh, lifted clear of the original surface the way a bake's relief lifts it. This is
// what ModelVolume::restore_painting()'s 0.01 mm overlap remap cannot follow.
indexed_triangle_set lifted = make_flat_sheet(size, 1.f);
for (Vec3f &v : lifted.vertices)
v.z() += 0.5f;
const std::vector<uint8_t> state = transfer_facet_paint(coarse, paint, lifted);
REQUIRE(state.size() == lifted.indices.size());
size_t left = 0, right = 0;
for (size_t t = 0; t < lifted.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = lifted.indices[t];
const float x = (lifted.vertices[size_t(tri[0])].x() + lifted.vertices[size_t(tri[1])].x() +
lifted.vertices[size_t(tri[2])].x()) / 3.f;
// Only away from the split: a facet straddling it may legitimately land on either side.
if (x < 0.4f * size) {
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder1));
++left;
} else if (x > 0.6f * size) {
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder2));
++right;
}
}
REQUIRE(left > 0);
REQUIRE(right > 0);
// An unpainted source has nothing to carry, which is not the same as carrying NONE everywhere:
// the caller has to tell those apart to decide between resetting the annotation and writing one.
CHECK(transfer_facet_paint(coarse, TriangleSelector::TriangleSplittingData{}, lifted).empty());
CHECK(transfer_facet_paint(coarse, paint, indexed_triangle_set{}).empty());
}
TEST_CASE("an unpainted part of the source carries no paint onto the rebuilt mesh", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
// Only the left half is painted at all; the right half must come back NONE rather than borrowing
// the nearest painted colour from across the model.
const TriangleMesh coarse_mesh(coarse);
TriangleSelector painter(coarse_mesh);
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
if (x < 0.5f * size)
painter.set_facet(int(t), EnforcerBlockerType::Extruder3);
}
const std::vector<uint8_t> state = transfer_facet_paint(coarse, painter.serialize(), coarse);
REQUIRE(state.size() == coarse.indices.size());
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
if (x < 0.4f * size)
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder3));
else if (x > 0.6f * size)
CHECK(state[t] == uint8_t(EnforcerBlockerType::NONE));
}
}