Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream

This commit is contained in:
SoftFever
2026-10-01 14:51:35 +08:00
committed by GitHub
967 changed files with 32649 additions and 175 deletions
@@ -308,12 +308,18 @@ DynamicPrintConfig slice_config(PresetBundle &bundle)
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
if (pin_filament_map) {
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
for (size_t i = 0; i < fmap.size(); ++i)
fmap[i] = int(i % nozzles) + 1;
}
// A fresh printer selection uses its declared nozzle volumes, just like the
// app. Otherwise a high-flow preset is silently sliced with Standard tuning.
bundle.reset_default_nozzle_volume_type();
bundle.project_config.option<ConfigOptionInts>("filament_volume_map", true)->values =
bundle.get_default_nozzle_volume_types_for_filaments(fmap);
DynamicPrintConfig cfg = bundle.full_config();
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
+24
View File
@@ -480,6 +480,30 @@ set(lisbslic3r_sources
Tesselate.cpp
Tesselate.hpp
TextConfiguration.hpp
TextureDisplacement.cpp
TextureDisplacement.hpp
TextureBake/TextureBakeIndex.cpp
TextureBake/TextureBakeIndex.hpp
TextureBake/TextureBakeSubdivide.cpp
TextureBake/TextureBakeSubdivide.hpp
TextureBake/TextureBakeRegularize.cpp
TextureBake/TextureBakeRegularize.hpp
TextureBake/TextureBakeRelocate.cpp
TextureBake/TextureBakeRelocate.hpp
TextureBake/TextureBakeFlip.cpp
TextureBake/TextureBakeFlip.hpp
TextureBake/TextureBakeDebug.cpp
TextureBake/TextureBakeDebug.hpp
TextureBake/TextureBakeDisplace.cpp
TextureBake/TextureBakeDisplace.hpp
TextureBake/TextureBakeDecimate.cpp
TextureBake/TextureBakeDecimate.hpp
TextureBake/TextureBakeRepair.cpp
TextureBake/TextureBakeRepair.hpp
TextureBake/TextureBakePipeline.cpp
TextureBake/TextureBakePipeline.hpp
TextureBake/TextureBakeMesh.cpp
TextureBake/TextureBakeMesh.hpp
Thread.cpp
Thread.hpp
Time.cpp
+5 -1
View File
@@ -2702,6 +2702,9 @@ public:
virtual ConfigOption* optptr(const t_config_option_key &opt_key, bool create = false) = 0;
// Collect names of all configuration values maintained by this configuration store.
virtual t_config_option_keys keys() const = 0;
// Set this config's options on target member by member, when target is of this config's static type or
// derives from it, and return true. apply() prefers this to looking every key up by name.
virtual bool apply_to(ConfigBase &/*target*/) const { return false; }
protected:
// Verify whether the opt_key has not been obsoleted or renamed.
@@ -2753,7 +2756,8 @@ public:
// Apply all keys of other ConfigBase defined by this->def() to this ConfigBase.
// An UnknownOptionException is thrown in case some option keys of other are not defined by this->def(),
// or this ConfigBase is of a StaticConfig type and it does not support some of the keys, and ignore_nonexistent is not set.
void apply(const ConfigBase &other, bool ignore_nonexistent = false) { this->apply_only(other, other.keys(), ignore_nonexistent); }
void apply(const ConfigBase &other, bool ignore_nonexistent = false)
{ if (! other.apply_to(*this)) this->apply_only(other, other.keys(), ignore_nonexistent); }
// Apply explicitely enumerated keys of other ConfigBase defined by this->def() to this ConfigBase.
// An UnknownOptionException is thrown in case some option keys are not defined by this->def(),
// or this ConfigBase is of a StaticConfig type and it does not support some of the keys, and ignore_nonexistent is not set.
+27 -8
View File
@@ -1258,7 +1258,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
config.set_key_value("old_filament_temp", new ConfigOptionInt(old_filament_temp));
int interface_temp = full_config.filament_tower_interface_print_temp.get_at(new_filament_id);
if (interface_temp == -1)
interface_temp = full_config.nozzle_temperature_range_high.get_at(new_filament_id);
interface_temp = full_config.nozzle_temperature_range_high.get_at(new_fi);
if (full_config.enable_tower_interface_features && tcr.is_contact)
new_filament_temp = interface_temp;
config.set_key_value("new_filament_temp", new ConfigOptionInt(new_filament_temp));
@@ -1309,7 +1309,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
flush_temps[idx] = use_fast_flush ? m_print_config->filament_flush_temp_fast.get_at(fi)
: m_print_config->filament_flush_temp.get_at(fi);
if (flush_temps[idx] == 0)
flush_temps[idx] = m_print_config->nozzle_temperature_range_high.get_at(idx);
flush_temps[idx] = m_print_config->nozzle_temperature_range_high.get_at(fi);
filament_cooling_before_tower[idx] = m_print_config->filament_cooling_before_tower.get_at(fi);
}
if (tcr.is_contact || gcodegen.m_layer_index == 0)
@@ -1563,7 +1563,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
const bool will_go_down = !is_approx(z, current_z);
const bool is_ramming = (gcodegen.config().single_extruder_multi_material) ||
(!gcodegen.config().single_extruder_multi_material &&
gcodegen.config().filament_multitool_ramming.get_at(tcr.initial_tool));
gcodegen.config().filament_multitool_ramming.get_at(gcodegen.get_filament_config_index(tcr.initial_tool)));
// Orca: user-facing override (Printer Settings > Wipe tower > "Tool change on wipe tower").
// Forces the toolhead to travel over the wipe tower before issuing Tx even on multi-toolhead
// printers without ramming, where Orca would otherwise emit Tx in place (potentially over the part).
@@ -1620,7 +1620,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (gcodegen.config().enable_tower_interface_features && tcr.is_contact) {
interface_temp = gcodegen.config().filament_tower_interface_print_temp.get_at(new_extruder_id);
if (interface_temp == -1)
interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id);
interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(gcodegen.get_filament_config_index(new_extruder_id));
toolchange_temp_override = interface_temp;
}
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override,
@@ -4342,6 +4342,12 @@ size_t GCode::get_filament_config_index(int filament_id) const
return filament_id;
}
size_t GCode::get_filament_config_index(int filament_id, size_t layer_id) const
{
// Orca: uncached, as the stages after the generator run concurrently with it.
return m_print ? m_print->get_filament_config_indx(filament_id, (int) layer_id, false) : filament_id;
}
size_t GCode::get_nozzle_config_index(int filament_id) const
{
if (m_print) {
@@ -9409,10 +9415,13 @@ void GCode::update_placeholder_parser_with_variant_params()
// Helper: remap config arrays from variant index space to filament_id index space.
// After remapping, gcode templates can use param[filament_id] directly.
std::vector<size_t> config_index(num_filaments);
for (size_t i = 0; i < num_filaments; ++i)
config_index[i] = get_filament_config_index(i);
auto remap_by_filament = [&](const auto &src) {
std::decay_t<decltype(src.values)> dst(num_filaments);
for (size_t i = 0; i < num_filaments; ++i)
dst[i] = src.get_at(get_filament_config_index(i));
dst[i] = src.get_at(config_index[i]);
return dst;
};
@@ -9427,6 +9436,16 @@ void GCode::update_placeholder_parser_with_variant_params()
this->placeholder_parser().set("first_layer_temperature", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_initial_layer)));
this->placeholder_parser().set("pressure_advance", new ConfigOptionFloats(remap_by_filament(m_config.pressure_advance)));
this->placeholder_parser().set("enable_pressure_advance", new ConfigOptionBools(remap_by_filament(m_config.enable_pressure_advance)));
this->placeholder_parser().set("fan_min_speed", new ConfigOptionFloats(remap_by_filament(m_config.fan_min_speed)));
this->placeholder_parser().set("fan_max_speed", new ConfigOptionFloats(remap_by_filament(m_config.fan_max_speed)));
this->placeholder_parser().set("additional_cooling_fan_speed", new ConfigOptionInts(remap_by_filament(m_config.additional_cooling_fan_speed)));
this->placeholder_parser().set("filament_minimal_purge_on_wipe_tower", new ConfigOptionFloats(remap_by_filament(m_config.filament_minimal_purge_on_wipe_tower)));
this->placeholder_parser().set("filament_multitool_ramming", new ConfigOptionBools(remap_by_filament(m_config.filament_multitool_ramming)));
this->placeholder_parser().set("filament_multitool_ramming_volume", new ConfigOptionFloats(remap_by_filament(m_config.filament_multitool_ramming_volume)));
this->placeholder_parser().set("filament_multitool_ramming_flow", new ConfigOptionFloats(remap_by_filament(m_config.filament_multitool_ramming_flow)));
this->placeholder_parser().set("nozzle_temperature_range_low", new ConfigOptionInts(remap_by_filament(m_config.nozzle_temperature_range_low)));
const auto nozzle_temperature_range_high = remap_by_filament(m_config.nozzle_temperature_range_high);
this->placeholder_parser().set("nozzle_temperature_range_high", new ConfigOptionInts(nozzle_temperature_range_high));
// --- printer_options_with_variant_1: in m_config these are already merged as filament-indexed ---
this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(remap_by_filament(m_config.retraction_distances_when_cut)));
@@ -9447,7 +9466,7 @@ void GCode::update_placeholder_parser_with_variant_params()
if (flush_v_speed[i] == 0)
flush_v_speed[i] = filament_max_v[i];
if (flush_temps[i] == 0)
flush_temps[i] = m_config.nozzle_temperature_range_high.get_at(i);
flush_temps[i] = nozzle_temperature_range_high[i];
}
this->placeholder_parser().set("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed));
this->placeholder_parser().set("flush_temperatures", new ConfigOptionInts(flush_temps));
@@ -9706,7 +9725,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
{
int interface_temp = m_config.filament_tower_interface_print_temp.get_at(new_filament_id);
if (interface_temp == -1)
interface_temp = m_config.nozzle_temperature_range_high.get_at(new_filament_id);
interface_temp = m_config.nozzle_temperature_range_high.get_at(new_fi);
dyn_config.set_key_value("filament_tower_interface_print_temp", new ConfigOptionInt(interface_temp));
}
if (toolchange_temp_override > 0) {
@@ -9745,7 +9764,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
flush_temps[idx] = use_fast_flush ? m_print->config().filament_flush_temp_fast.get_at(fi)
: m_print->config().filament_flush_temp.get_at(fi);
if (flush_temps[idx] == 0)
flush_temps[idx] = m_print->config().nozzle_temperature_range_high.get_at(idx);
flush_temps[idx] = m_print->config().nozzle_temperature_range_high.get_at(fi);
filament_cooling_before_tower[idx] = m_print->config().filament_cooling_before_tower.get_at(fi);
}
std::fill(filament_cooling_before_tower.begin(), filament_cooling_before_tower.end(), 0);
+3
View File
@@ -296,6 +296,9 @@ public:
// resolver keys filament-indexed arrays, the nozzle resolver keys (extruder x volume-type)
// slot arrays. Both degenerate to filament_id / extruder index on single-volume printers.
size_t get_filament_config_index(int filament_id) const;
// The filament resolver for a given layer, for the export pipeline stages after the generator,
// which run behind the current layer and concurrently with the generator.
size_t get_filament_config_index(int filament_id, size_t layer_id) const;
size_t get_nozzle_config_index(int filament_id) const;
// Object and support extrusions of the same PrintObject at the same print_z.
+6 -4
View File
@@ -18,7 +18,7 @@
namespace Slic3r {
CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_toolchange_prefix(gcodegen.writer().toolchange_prefix()), m_current_extruder(0), m_current_nozzle(0)
CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_gcodegen(gcodegen), m_toolchange_prefix(gcodegen.writer().toolchange_prefix()), m_current_extruder(0), m_current_nozzle(0)
{
this->reset(gcodegen.writer().get_position());
@@ -737,10 +737,12 @@ std::string CoolingBuffer::apply_layer_cooldown(
&ironing_fan_control, &ironing_fan_speed
](bool immediately_apply) {
#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_current_extruder)
float fan_min_speed = EXTRUDER_CONFIG(fan_min_speed);
// The per-variant options take the extruder variant the filament prints with on this layer
const size_t config_index = m_gcodegen.get_filament_config_index(m_current_extruder, layer_id);
float fan_min_speed = m_config.fan_min_speed.get_at(config_index);
float fan_speed_new = EXTRUDER_CONFIG(reduce_fan_stop_start_freq) ? fan_min_speed : 0;
//BBS
int additional_fan_speed_new = EXTRUDER_CONFIG(additional_cooling_fan_speed);
int additional_fan_speed_new = m_config.additional_cooling_fan_speed.get_at(config_index);
int close_fan_the_first_x_layers = EXTRUDER_CONFIG(close_fan_the_first_x_layers);
// Is the fan speed ramp enabled?
int full_fan_speed_layer = EXTRUDER_CONFIG(full_fan_speed_layer);
@@ -776,7 +778,7 @@ std::string CoolingBuffer::apply_layer_cooldown(
// additional_fan_speed_new is left at its configured value (auxiliary fan is independent of the
// part-cooling override).
} else if (int(layer_id) >= close_fan_the_first_x_layers) {
float fan_max_speed = EXTRUDER_CONFIG(fan_max_speed);
float fan_max_speed = m_config.fan_max_speed.get_at(config_index);
float slow_down_layer_time = float(EXTRUDER_CONFIG(slow_down_layer_time));
float fan_cooling_layer_time = float(EXTRUDER_CONFIG(fan_cooling_layer_time));
//BBS: always enable the fan speed interpolation according to layer time
+2
View File
@@ -54,6 +54,8 @@ private:
// Referencs GCode::m_config, which is FullPrintConfig. While the PrintObjectConfig slice of FullPrintConfig is being modified,
// the PrintConfig slice of FullPrintConfig is constant, thus no thread synchronization is required.
const PrintConfig &m_config;
// Resolves the filament config index of the per-variant options.
const GCode &m_gcodegen;
unsigned int m_current_extruder;
unsigned int m_current_nozzle;
//BBS: current fan speed
+14 -8
View File
@@ -1230,15 +1230,21 @@ void ToolOrdering::cal_most_used_extruder(const PrintConfig &config)
float ToolOrdering::cal_max_additional_fan(const PrintConfig &config)
{
// record
std::set<unsigned int> used_filaments;
for (const LayerTools &layer_tools : m_layer_tools)
used_filaments.insert(layer_tools.extruders.begin(), layer_tools.extruders.end());
if (used_filaments.empty())
return 0;
// Orca: additional_cooling_fan_speed can hold one value per extruder variant a filament prints with;
// filament_self_index maps such a column to its filament.
const std::vector<int> &self_index = config.filament_self_index.values;
const size_t columns = std::max(config.additional_cooling_fan_speed.size(), size_t(*used_filaments.rbegin()) + 1);
float max_fan = 0;
for (LayerTools &layer_tools : m_layer_tools) {
std::vector<unsigned int> filaments = layer_tools.extruders;
std::set<int> layer_extruder_count;
// count once only
for (unsigned int &filament : filaments)
if (max_fan < config.additional_cooling_fan_speed.get_at(filament))
max_fan = config.additional_cooling_fan_speed.get_at(filament);
for (size_t column = 0; column < columns; ++column) {
const unsigned int filament_id = self_index.size() == columns ? self_index[column] - 1 : column;
if (used_filaments.count(filament_id) && max_fan < config.additional_cooling_fan_speed.get_at(column))
max_fan = config.additional_cooling_fan_speed.get_at(column);
}
return max_fan;
}
+152
View File
@@ -26,6 +26,13 @@
#include <CGAL/property_map.h>
#include <CGAL/boost/graph/copy_face_graph.h>
#include <CGAL/boost/graph/Face_filtered_graph.h>
// For parameterize_lscm()
#include <CGAL/Polygon_mesh_processing/border.h>
#include <CGAL/Polygon_mesh_processing/connected_components.h>
#include <CGAL/Polygon_mesh_processing/detect_features.h>
#include <CGAL/Surface_mesh_parameterization/Error_code.h>
#include <CGAL/Surface_mesh_parameterization/LSCM_parameterizer_3.h>
#include <CGAL/Surface_mesh_parameterization/parameterize.h>
// BBS: for boolean using mcut
#include "mcut/include/mcut/mcut.h"
@@ -249,6 +256,151 @@ indexed_triangle_set cgal_to_indexed_triangle_set(const CGALMesh &cgalmesh)
return cgal_to_indexed_triangle_set(cgalmesh.m);
}
// /////////////////////////////////////////////////////////////////////////////
// Isotropic remeshing
// /////////////////////////////////////////////////////////////////////////////
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
unsigned n_iterations, double sharp_angle_deg,
unsigned n_relaxation_steps)
{
if (mesh.indices.empty() || target_edge_length <= 0.0)
return mesh;
_EpicMesh cgal_mesh;
triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh);
if (cgal_mesh.is_empty() || cgal_mesh.number_of_faces() == 0)
return mesh;
// Surface_mesh::add_face() refuses any face that would make the mesh non-manifold and returns a
// null descriptor instead. Remeshing a mesh that silently lost faces that way produces holes in
// the output, so bail out and let the caller report it rather than hand back a punctured model.
if (cgal_mesh.number_of_faces() != mesh.indices.size())
return mesh;
using edge_descriptor = boost::graph_traits<_EpicMesh>::edge_descriptor;
try {
// Sharp edges and open borders are pinned before remeshing. Without that, the tangential
// relaxation pass slides vertices along the surface and rounds every hard feature off - a
// cube comes back with wobbly, eroded edges, which is the most visible way "remeshing does
// not work properly". protect_constraints() forbids splitting or collapsing them, but it
// requires each constrained edge to already be shorter than 4/3 * target, hence the split
// first (passing the map so the halves inherit the constraint). This mirrors CGAL's own
// isotropic_remeshing example.
auto ecm = cgal_mesh.add_property_map<edge_descriptor, bool>("e:is_constrained", false).first;
if (sharp_angle_deg > 0.0)
CGALProc::detect_sharp_edges(cgal_mesh, sharp_angle_deg, ecm);
for (edge_descriptor e : edges(cgal_mesh)) {
const auto h = halfedge(e, cgal_mesh);
if (is_border(h, cgal_mesh) || is_border(opposite(h, cgal_mesh), cgal_mesh))
put(ecm, e, true);
}
std::vector<edge_descriptor> constrained;
for (edge_descriptor e : edges(cgal_mesh))
if (get(ecm, e))
constrained.push_back(e);
if (!constrained.empty())
CGALProc::split_long_edges(constrained, target_edge_length, cgal_mesh,
CGALParams::edge_is_constrained_map(ecm));
CGALProc::isotropic_remeshing(faces(cgal_mesh), target_edge_length, cgal_mesh,
CGALParams::number_of_iterations(n_iterations)
.number_of_relaxation_steps(n_relaxation_steps)
.edge_is_constrained_map(ecm)
.protect_constraints(true));
} catch (const std::exception &) {
return mesh; // CGAL throws on some non-manifold / degenerate inputs; leave the mesh untouched
}
if (cgal_mesh.number_of_faces() == 0)
return mesh;
// isotropic_remeshing edits in place, and its edge collapses only *mark* vertices and faces as
// removed - the underlying arrays keep the holes until the garbage is collected. That matters
// because cgal_to_indexed_triangle_set() numbers its output vertices by iteration order (which
// skips removed slots) while reading each face's corner as the raw integer value of the vertex
// descriptor (which does not). Past the first collapse the two disagree, so every triangle
// points at the wrong vertices, and any descriptor beyond the live vertex count is dropped
// together with its triangle. Compacting first makes descriptor == iteration order again.
cgal_mesh.collect_garbage();
return cgal_to_indexed_triangle_set(cgal_mesh);
}
// /////////////////////////////////////////////////////////////////////////////
// UV parameterization
// /////////////////////////////////////////////////////////////////////////////
std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &mesh)
{
namespace SMP = CGAL::Surface_mesh_parameterization;
if (mesh.indices.empty())
return std::nullopt;
_EpicMesh cgal_mesh;
triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh);
using vertex_descriptor = boost::graph_traits<_EpicMesh>::vertex_descriptor;
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
// LSCM assumes a single topological disk: one connected component, one boundary loop. A patch
// with several disconnected painted islands, or with a hole in it, doesn't qualify -- bail out
// rather than silently parameterizing just one arbitrary piece of it.
{
std::vector<std::size_t> component_id(num_faces(cgal_mesh));
const std::size_t num_components = CGAL::Polygon_mesh_processing::connected_components(
cgal_mesh, CGAL::make_property_map(component_id));
if (num_components != 1)
return std::nullopt;
}
const halfedge_descriptor border = CGAL::Polygon_mesh_processing::longest_border(cgal_mesh).first;
if (border == halfedge_descriptor())
return std::nullopt; // no boundary at all -- a closed patch, which isn't a disk either
// ...and exactly one boundary loop. One connected component is not enough on its own: a patch with
// a hole in it (paint a ring, or erase the middle of a stroke) is a single component with two
// loops, and LSCM will happily "parameterize" it into an overlapping, folded-over chart rather
// than fail. Walk the border halfedges and check every one of them belongs to the longest loop.
{
std::size_t border_halfedges = 0;
for (halfedge_descriptor h : halfedges(cgal_mesh))
if (is_border(h, cgal_mesh))
++border_halfedges;
std::size_t loop_length = 0;
halfedge_descriptor h = border;
do {
++loop_length;
h = next(h, cgal_mesh);
} while (h != border && loop_length <= border_halfedges);
if (loop_length != border_halfedges)
return std::nullopt; // more than one boundary loop -- not a topological disk
}
using Point_2 = EpicKernel::Point_2;
using UV_pmap = _EpicMesh::Property_map<vertex_descriptor, Point_2>;
UV_pmap uv_map = cgal_mesh.add_property_map<vertex_descriptor, Point_2>("h:uv", Point_2(0, 0)).first;
using Parameterizer = SMP::LSCM_parameterizer_3<_EpicMesh>;
const SMP::Error_code err = SMP::parameterize(cgal_mesh, Parameterizer(), border, uv_map);
if (err != SMP::OK)
return std::nullopt;
// triangle_mesh_to_cgal() adds vertices in the exact same order as mesh.vertices (see above),
// and Surface_mesh assigns indices sequentially on insertion into a fresh mesh, so a
// vertex_descriptor's index here is guaranteed to match the original input vertex index --
// the same assumption cgal_to_indexed_triangle_set() above already relies on.
std::vector<Vec2f> result(mesh.vertices.size(), Vec2f::Zero());
for (vertex_descriptor vd : vertices(cgal_mesh)) {
const std::size_t idx = std::size_t(vd);
if (idx < result.size()) {
const Point_2 &uv = uv_map[vd];
result[idx] = Vec2f(float(uv.x()), float(uv.y()));
}
}
return result;
}
// /////////////////////////////////////////////////////////////////////////////
// Boolean operations for CGAL meshes
// /////////////////////////////////////////////////////////////////////////////
+23
View File
@@ -3,6 +3,8 @@
#include <memory>
#include <exception>
#include <optional>
#include <vector>
#include <libslic3r/TriangleMesh.hpp>
#include <Eigen/Geometry>
@@ -73,6 +75,27 @@ bool empty(const CGALMesh &mesh);
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
// component with exactly one boundary loop, true for a typical single brush stroke/patch but not
// guaranteed for multiple disconnected painted islands merged into one mesh.
std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &mesh);
// Isotropic remeshing (CGAL): rebuilds the mesh so its triangles are close to a uniform target edge
// length, splitting oversized triangles and collapsing undersized ones. Used to even out a model with
// wildly varying triangle sizes so texture displacement has a consistent vertex density to work with.
// Edges whose dihedral angle exceeds `sharp_angle_deg`, and any open border, are held fixed so hard
// features survive instead of being eroded by the relaxation pass; pass 0 to remesh everything.
// Returns the input unchanged if remeshing fails (e.g. a non-manifold or self-intersecting input).
// `n_relaxation_steps` is the number of tangential relaxation passes run inside each iteration. That
// relaxation is what actually evens out the triangle distribution - splitting and collapsing alone
// only bring edge *lengths* near the target, leaving the vertices wherever they happened to land. CGAL
// defaults it to 1, which on a few iterations is not enough to look uniform.
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
unsigned n_iterations = 3, double sharp_angle_deg = 40.0,
unsigned n_relaxation_steps = 1);
}
namespace mcut {
+10
View File
@@ -2087,6 +2087,11 @@ void ModelVolume::reset_extra_facets()
this->seam_facets.reset();
this->mmu_segmentation_facets.reset();
this->fuzzy_skin_facets.reset();
// Texture-displacement paint data has no remap-across-topology-change support yet (see
// build_texture_displacement()'s documented limitation), so it must be dropped here rather
// 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();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const
@@ -2989,6 +2994,11 @@ void ModelVolume::assign_new_unique_ids_recursive()
seam_facets.set_new_unique_id();
mmu_segmentation_facets.set_new_unique_id();
fuzzy_skin_facets.set_new_unique_id();
// As set_new_unique_id() already does: the undo/redo stack stores FacetsAnnotation contents keyed
// by ObjectID, so a clone left sharing these ids with its source can be handed the source's mask
// on an undo - after which a paint mask and the mesh it was recorded against no longer match.
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
texture_displacement_facet(i).set_new_unique_id();
}
void ModelVolume::rotate(double angle, Axis axis)
+101 -5
View File
@@ -19,6 +19,7 @@
#include "TextConfiguration.hpp"
#include "EmbossShape.hpp"
#include "TriangleSelector.hpp"
#include "TextureDisplacement.hpp"
//BBS: add bbs 3mf
#include "Format/bbs_3mf.hpp"
@@ -28,6 +29,7 @@
#include "Format/STL.hpp"
#include "Format/OBJ.hpp"
#include <array>
#include <map>
#include <memory>
#include <string>
@@ -901,6 +903,69 @@ public:
// List of mesh facets painted for fuzzy skin.
FacetsAnnotation fuzzy_skin_facets;
// One independent paint mask per texture-displacement layer slot (see texture_displacement_layers
// below). Unlike the other facets fields above, a triangle may be painted (ENFORCER) in more
// than one of these simultaneously -- that overlap is what makes the layers "blend".
//
// These are 8 plain named fields rather than a std::array<FacetsAnnotation, N>: FacetsAnnotation's
// default/copy constructors are private and friended only to ModelVolume, but std::array's own
// implicitly-defined default/copy constructors are generated with std::array's access rights,
// not ModelVolume's -- so an array of FacetsAnnotation ends up with its default/copy
// constructors implicitly deleted regardless of the friend declaration. Use
// texture_displacement_facet(slot) below for array-like indexed access.
FacetsAnnotation texture_displacement_facets_0;
FacetsAnnotation texture_displacement_facets_1;
FacetsAnnotation texture_displacement_facets_2;
FacetsAnnotation texture_displacement_facets_3;
FacetsAnnotation texture_displacement_facets_4;
FacetsAnnotation texture_displacement_facets_5;
FacetsAnnotation texture_displacement_facets_6;
FacetsAnnotation texture_displacement_facets_7;
FacetsAnnotation& texture_displacement_facet(int slot) {
switch (slot) {
case 0: return texture_displacement_facets_0;
case 1: return texture_displacement_facets_1;
case 2: return texture_displacement_facets_2;
case 3: return texture_displacement_facets_3;
case 4: return texture_displacement_facets_4;
case 5: return texture_displacement_facets_5;
case 6: return texture_displacement_facets_6;
default: assert(slot == 7); return texture_displacement_facets_7;
}
}
const FacetsAnnotation& texture_displacement_facet(int slot) const { return const_cast<ModelVolume*>(this)->texture_displacement_facet(slot); }
// Small helpers for the constructor asserts below (kept out of line-noise at each call site).
bool texture_displacement_facets_ids_valid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).id().valid() || texture_displacement_facet(i).id() == this->id())
return false;
return true;
}
bool texture_displacement_facets_ids_invalid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (texture_displacement_facet(i).id().valid())
return false;
return true;
}
bool texture_displacement_facets_all_empty() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).empty())
return false;
return true;
}
// Texture assets (height maps) and their projection/displacement parameters. Element order
// is not meaningful for baking (layers are applied in TextureDisplacementLayer::slot order,
// see build_texture_displacement()); it only reflects UI insertion order.
std::vector<TextureDisplacementLayer> texture_displacement_layers;
// Whole-stack displacement settings (border handling, post-process smoothing) - see
// TextureDisplacementOptions. They live beside the layers rather than on one of them because
// they are not a property of any single layer.
TextureDisplacementOptions texture_displacement_options;
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1046,13 +1111,18 @@ public:
this->seam_facets.set_new_unique_id();
this->mmu_segmentation_facets.set_new_unique_id();
this->fuzzy_skin_facets.set_new_unique_id();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).set_new_unique_id();
}
bool is_fdm_support_painted() const { return !this->supported_facets.empty(); }
bool is_seam_painted() const { return !this->seam_facets.empty(); }
bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); }
bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); }
bool is_any_painted() const { return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted(); }
bool is_texture_displacement_painted() const { return !this->texture_displacement_facets_all_empty(); }
bool is_any_painted() const {
return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted() || is_texture_displacement_painted();
}
// Orca: Implement prusa's filament shrink compensation approach
// Returns 0-based indices of extruders painted by multi-material painting gizmo.
@@ -1105,6 +1175,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1121,6 +1192,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1135,6 +1207,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1148,10 +1221,17 @@ private:
name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull),
config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation),
supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets),
fuzzy_skin_facets(other.fuzzy_skin_facets), cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
fuzzy_skin_facets(other.fuzzy_skin_facets),
texture_displacement_facets_0(other.texture_displacement_facets_0), texture_displacement_facets_1(other.texture_displacement_facets_1),
texture_displacement_facets_2(other.texture_displacement_facets_2), texture_displacement_facets_3(other.texture_displacement_facets_3),
texture_displacement_facets_4(other.texture_displacement_facets_4), texture_displacement_facets_5(other.texture_displacement_facets_5),
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),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{
assert(this->id().valid());
assert(this->config.id().valid());
assert(this->id().valid());
assert(this->config.id().valid());
assert(this->supported_facets.id().valid());
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
@@ -1201,6 +1281,8 @@ private:
assert(this->seam_facets.empty());
assert(this->mmu_segmentation_facets.empty());
assert(this->fuzzy_skin_facets.empty());
assert(this->texture_displacement_facets_all_empty());
assert(this->texture_displacement_layers.empty());
}
ModelVolume& operator=(ModelVolume &rhs) = delete;
@@ -1208,13 +1290,17 @@ private:
friend class cereal::access;
friend class UndoRedo::StackImpl;
// Used for deserialization, therefore no IDs are allocated.
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1), object(nullptr) {
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1),
texture_displacement_facets_0(-1), texture_displacement_facets_1(-1), texture_displacement_facets_2(-1), texture_displacement_facets_3(-1),
texture_displacement_facets_4(-1), texture_displacement_facets_5(-1), texture_displacement_facets_6(-1), texture_displacement_facets_7(-1),
object(nullptr) {
assert(this->id().invalid());
assert(this->config.id().invalid());
assert(this->supported_facets.id().invalid());
assert(this->seam_facets.id().invalid());
assert(this->mmu_segmentation_facets.id().invalid());
assert(this->fuzzy_skin_facets.id().invalid());
assert(this->texture_displacement_facets_ids_invalid());
}
template<class Archive> void load(Archive &ar) {
bool has_convex_hull;
@@ -1234,6 +1320,13 @@ private:
mesh_changed |= t != mmu_segmentation_facets.timestamp();
cereal::load_by_value(ar, fuzzy_skin_facets);
mesh_changed |= t != fuzzy_skin_facets.timestamp();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
FacetsAnnotation &f = texture_displacement_facet(i);
Timestamp tf = f.timestamp();
cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp();
}
ar(texture_displacement_layers, texture_displacement_options);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1255,6 +1348,9 @@ private:
cereal::save_by_value(ar, seam_facets);
cereal::save_by_value(ar, mmu_segmentation_facets);
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);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);
+48 -24
View File
@@ -54,9 +54,43 @@ static void png_read_callback(png_struct *png_ptr,
// Retrieve our input buffer through the png_ptr
auto reader = static_cast<IStream *>(png_get_io_ptr(png_ptr));
if (!reader || !reader->is_ok()) return;
// libpng expects a short read to be reported through png_error(); returning quietly would leave
// it decoding whatever happened to be in outBytes.
if (!reader || !reader->is_ok() ||
reader->read(static_cast<std::uint8_t *>(outBytes), byteCountToRead) != byteCountToRead)
png_error(png_ptr, "PNG data is truncated");
}
reader->read(static_cast<std::uint8_t *>(outBytes), byteCountToRead);
// libpng reports a corrupt or truncated image by longjmp()ing back to the jump buffer set with
// setjmp(). The frame it lands in must own nothing that needs destroying: with exceptions enabled
// MSVC unwinds the stack as part of longjmp, and returning from a frame unwound that way crashes -
// which is what a truncated texture did on Windows while working everywhere else. So the calls that
// can fail live in these two helpers, which hold nothing but pointers, and every C++ object the
// decoders need stays in their own frames.
static bool png_read_header_guarded(png_struct *png, png_info *info, IStream *in_buf, int sig_bytes)
{
if (setjmp(png_jmpbuf(png)))
return false;
png_set_read_fn(png, static_cast<void *>(in_buf), png_read_callback);
// Tell that we have already read the first bytes to check the signature
png_set_sig_bytes(png, sig_bytes);
png_read_info(png, info);
return true;
}
// `bottom_up` fills the buffer last row first, which is the order the colour decoder hands back.
static bool png_read_rows_guarded(png_struct *png, png_info *info, png_bytep dst, size_t rows, size_t rowbytes,
bool bottom_up, bool read_end)
{
if (setjmp(png_jmpbuf(png)))
return false;
for (size_t i = 0; i < rows; ++i)
png_read_row(png, dst + (bottom_up ? rows - 1 - i : i) * rowbytes, nullptr);
if (read_end)
png_read_end(png, info);
return true;
}
bool decode_png(IStream &in_buf, ImageGreyscale &out_img)
@@ -77,12 +111,8 @@ bool decode_png(IStream &in_buf, ImageGreyscale &out_img)
dsc.info = png_create_info_struct(dsc.png);
if(!dsc.info) return false;
png_set_read_fn(dsc.png, static_cast<void *>(&in_buf), png_read_callback);
// Tell that we have already read the first bytes to check the signature
png_set_sig_bytes(dsc.png, PNG_SIG_BYTES);
png_read_info(dsc.png, dsc.info);
if (!png_read_header_guarded(dsc.png, dsc.info, &in_buf, PNG_SIG_BYTES))
return false;
out_img.cols = png_get_image_width(dsc.png, dsc.info);
out_img.rows = png_get_image_height(dsc.png, dsc.info);
@@ -94,11 +124,8 @@ bool decode_png(IStream &in_buf, ImageGreyscale &out_img)
out_img.buf.resize(out_img.rows * out_img.cols);
auto readbuf = static_cast<png_bytep>(out_img.buf.data());
for (size_t r = 0; r < out_img.rows; ++r)
png_read_row(dsc.png, readbuf + r * out_img.cols, nullptr);
return true;
return png_read_rows_guarded(dsc.png, dsc.info, static_cast<png_bytep>(out_img.buf.data()), out_img.rows,
out_img.cols, /* bottom_up */ false, /* read_end */ false);
}
bool decode_colored_png(IStream &in_buf, ImageColorscale &out_img)
@@ -128,12 +155,10 @@ bool decode_colored_png(IStream &in_buf, ImageColorscale &out_img)
return false;
}
png_set_read_fn(dsc.png, static_cast<void *>(&in_buf), png_read_callback);
// Tell that we have already read the first bytes to check the signature
png_set_sig_bytes(dsc.png, PNG_SIG_BYTES);
png_read_info(dsc.png, dsc.info);
if (!png_read_header_guarded(dsc.png, dsc.info, &in_buf, PNG_SIG_BYTES)) {
BOOST_LOG_TRIVIAL(error) << "decode_colored_png: corrupt or truncated PNG data";
return false;
}
out_img.cols = png_get_image_width(dsc.png, dsc.info);
out_img.rows = png_get_image_height(dsc.png, dsc.info);
@@ -162,13 +187,12 @@ bool decode_colored_png(IStream &in_buf, ImageColorscale &out_img)
int interlace_type = png_get_interlace_type(dsc.png, dsc.info);
BOOST_LOG_TRIVIAL(info) << boost::format("filter_type %1%, compression_type %2%, interlace_type %3%, rowbytes %4%")%filter_type %compression_type %interlace_type %rowbytes;
auto readbuf = static_cast<png_bytep>(out_img.buf.data());
for (size_t r = out_img.rows; r > 0; r--)
{
png_read_row(dsc.png, readbuf + (r - 1) * rowbytes, nullptr);
if (!png_read_rows_guarded(dsc.png, dsc.info, static_cast<png_bytep>(out_img.buf.data()), out_img.rows, rowbytes,
/* bottom_up */ true, /* read_end */ true)) {
BOOST_LOG_TRIVIAL(error) << "decode_colored_png: corrupt or truncated PNG data";
return false;
}
png_read_end(dsc.png, dsc.info);
png_destroy_read_struct(&dsc.png, &dsc.info, NULL);
return true;
+14 -3
View File
@@ -4452,8 +4452,17 @@ std::vector<Preset *> PresetBundle::get_filament_presets_for_machine(const std::
return compatible;
}
int PresetBundle::get_filament_variant_index(const DynamicPrintConfig &filament_config, const DynamicPrintConfig &printer_config,
int extruder_id, NozzleVolumeType nozzle_volume_type)
{
const auto *extruder_types = printer_config.option<ConfigOptionEnumsGeneric>("extruder_type");
const ExtruderType extruder_type = extruder_types && !extruder_types->empty() ? ExtruderType(extruder_types->get_at(extruder_id)) : etDirectDrive;
return std::max(0, filament_config.get_index_for_extruder(1, "", extruder_type, nozzle_volume_type, "filament_extruder_variant"));
}
bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(
const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id)
const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id,
int extruder_id, NozzleVolumeType nozzle_volume_type)
{
bool is_equation = true;
@@ -4476,13 +4485,15 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m
// Compare only once
if (!compared) {
compared = true;
const Preset *printer = printers.find_preset(printer_str);
const int variant_index = printer ? get_filament_variant_index(preset->config, printer->config, extruder_id, nozzle_volume_type) : 0;
bool min_temp_equation = false, max_temp_equation = false;
int min_nozzle_temp = std::stoi(nozzle_temp_min);
int max_nozzle_temp = std::stoi(nozzle_temp_max);
ConfigOption *opt_min = const_cast<Preset *>(preset)->config.option("nozzle_temperature_range_low");
if (opt_min) {
ConfigOptionInts *opt_min_ints = dynamic_cast<ConfigOptionInts *>(opt_min);
min_nozzle_temp = opt_min_ints->get_at(0);
min_nozzle_temp = opt_min_ints->get_at(variant_index);
if (std::to_string(min_nozzle_temp) == nozzle_temp_min)
min_temp_equation = true;
else {
@@ -4493,7 +4504,7 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m
ConfigOption *opt_max = const_cast<Preset *>(preset)->config.option("nozzle_temperature_range_high");
if (opt_max) {
ConfigOptionInts *opt_max_ints = dynamic_cast<ConfigOptionInts *>(opt_max);
max_nozzle_temp = opt_max_ints->get_at(0);
max_nozzle_temp = opt_max_ints->get_at(variant_index);
if (std::to_string(max_nozzle_temp) == nozzle_temp_max)
max_temp_equation = true;
else {
+10 -1
View File
@@ -407,13 +407,22 @@ public:
std::vector<Preset *> get_filament_presets_for_machine(const std::string &printer_type,
const std::string &nozzle_diameter_str,
bool include_user_presets);
// Orca: the variant index of a filament preset's per-variant options on extruder extruder_id of a printer
// preset, with the nozzle volume type the machine reports; 0 when the filament has no such variant.
static int get_filament_variant_index(const DynamicPrintConfig &filament_config,
const DynamicPrintConfig &printer_config,
int extruder_id,
NozzleVolumeType nozzle_volume_type);
// extruder_id and nozzle_volume_type identify the tray's nozzle, whose variant the temperature range is compared for.
bool check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(const std::string &printer_type,
std::string & nozzle_diameter_str,
std::string & setting_id,
std::string & tag_uid,
std::string & nozzle_temp_min,
std::string & nozzle_temp_max,
std::string & preset_setting_id);
std::string & preset_setting_id,
int extruder_id,
NozzleVolumeType nozzle_volume_type);
Preset * get_similar_printer_preset(std::string printer_model, std::string printer_variant);
PresetCollection prints;
+11 -8
View File
@@ -4329,9 +4329,9 @@ bool Print::is_dynamic_group_reorder() const
return true;
}
int Print::get_filament_config_indx(int filament_id, int layer_id)
int Print::get_filament_config_indx(int filament_id, int layer_id, bool use_cache)
{
return get_config_index(filament_id, layer_id, m_config.filament_extruder_variant.values, m_filament_self_index, m_filament_index_map);
return get_config_index(filament_id, layer_id, m_config.filament_extruder_variant.values, m_filament_self_index, use_cache ? &m_filament_index_map : nullptr);
}
void Print::update_filament_self_index_cache()
@@ -4374,7 +4374,7 @@ int Print::get_nozzle_config_index(int filament_id, int layer_id)
return get_config_index(filament_id, layer_id, m_default_region_config.print_extruder_variant.values, m_default_region_config.print_extruder_id.values, m_nozzle_index_map);
}
int Print::get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, FilamentIndexMap &index_map)
int Print::get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, FilamentIndexMap *index_map)
{
auto group_result = get_layered_nozzle_group_result();
// Orca: defensive — when no grouping producer has published a result yet, fall back to the
@@ -4385,7 +4385,8 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector<std
if (!nozzle_info.has_value()) {
// Orca: this fallback runs per-filament/per-layer in the g-code hot path — log once per filament
// (reset each slice) instead of flooding thousands of identical lines that bury the real error.
if (m_missing_nozzle_group_logged.insert(filament_id).second)
// Without the cache, the log set is left alone too; the cached caller reports the same filament.
if (index_map && m_missing_nozzle_group_logged.insert(filament_id).second)
BOOST_LOG_TRIVIAL(error) << __FUNCTION__
<< boost::format(", Line %1%: could not found group_nozzle_info corresponding to filament_id %2%, layer_id %3% (further occurrences for this filament suppressed)") % __LINE__ % filament_id %
layer_id;
@@ -4394,15 +4395,17 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector<std
ExtruderType extruder_type = ExtruderType(m_config.extruder_type.get_at(nozzle_info->extruder_id));
NozzleVolumeType nozzle_volume_type = nozzle_info->volume_type;
if (!index_map)
return get_config_index_base(nozzle_volume_type, extruder_type, filament_id + 1, variant_list, self_index_list);
FilamentIndexKey key{filament_id, extruder_type, nozzle_volume_type};
auto iter = index_map.find(key);
if (iter == index_map.end()) {
auto iter = index_map->find(key);
if (iter == index_map->end()) {
int index = get_config_index_base(nozzle_volume_type, extruder_type, filament_id + 1, variant_list, self_index_list);
index_map[key] = index;
(*index_map)[key] = index;
return index;
} else {
return index_map[key];
return iter->second;
}
}
+4 -2
View File
@@ -1222,7 +1222,9 @@ public:
// Post-slicing config-slot resolvers: map a (filament, layer) pair to the index of its
// per-(extruder x volume type) column in the expanded variant arrays, cached by grouping context.
int get_filament_config_indx(int filament_id, int layer_id);
// Orca: without use_cache, the filament resolver leaves the cache alone, for the G-code export
// pipeline's cooling stage, which runs concurrently with the generator stage filling it.
int get_filament_config_indx(int filament_id, int layer_id, bool use_cache = true);
int get_nozzle_config_index(int filament_id, int layer_id);
// Orca: Implement prusa's filament shrink compensation approach
@@ -1282,7 +1284,7 @@ protected:
};
using FilamentIndexMap = std::unordered_map<FilamentIndexKey, int, FilamentIndexKeyHash>;
using PrintIndexMap = std::unordered_map<PrintIndexKey, int, PrintIndexKeyHash>;
int get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, FilamentIndexMap &index_map);
int get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, FilamentIndexMap *index_map);
int get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, PrintIndexMap &index_map);
// Invalidates the step, and its depending steps in Print.
+25
View File
@@ -9516,6 +9516,16 @@ std::set<std::string> filament_options_with_variant = {
"adaptive_pressure_advance_model",
"adaptive_pressure_advance_overhangs",
"adaptive_pressure_advance_bridges",
// Orca: cooling fans, multi-tool ramming and recommended nozzle temperature range
"fan_min_speed",
"fan_max_speed",
"additional_cooling_fan_speed",
"filament_minimal_purge_on_wipe_tower",
"filament_multitool_ramming",
"filament_multitool_ramming_volume",
"filament_multitool_ramming_flow",
"nozzle_temperature_range_low",
"nozzle_temperature_range_high",
"activate_air_filtration",
"activate_air_filtration_during_print",
"activate_air_filtration_on_completion",
@@ -11958,6 +11968,21 @@ PRINT_CONFIG_CACHE_INITIALIZE((
SLAMaterialConfig, SLAPrintConfig, SLAPrintObjectConfig, SLAPrinterConfig, SLAFullPrintConfig))
static int print_config_static_initialized = print_config_static_initializer();
// The same set() calls ConfigBase::apply_only() makes, without looking every key up by name. Out of line so the
// option list is expanded for this once, not in every file that includes PrintConfig.hpp.
#define PRINT_CONFIG_APPLY_TO_DEFINITION(r, data, CLASS_NAME) \
bool CLASS_NAME::apply_to(ConfigBase &target) const \
{ \
auto *dst = dynamic_cast<CLASS_NAME*>(&target); \
if (dst == nullptr) \
return false; \
visit_option_pairs(*dst, *this, [](const char*, ConfigOption &a, const ConfigOption &b) { a.set(&b); return true; }); \
return true; \
}
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_APPLY_TO_DEFINITION, _, (PrintObjectConfig)(PrintRegionConfig)(MachineEnvelopeConfig)(GCodeConfig)
(SLAMaterialConfig)(SLAPrintConfig)(SLAPrintObjectConfig)(SLAPrinterConfig))
#undef PRINT_CONFIG_APPLY_TO_DEFINITION
//BBS: remove unused command currently
CLIActionsConfigDef::CLIActionsConfigDef()
{
+14 -5
View File
@@ -1033,9 +1033,10 @@ public: \
#define PRINT_CONFIG_CLASS_ELEMENT_DEFINITION(r, data, elem) BOOST_PP_TUPLE_ELEM(0, elem) BOOST_PP_TUPLE_ELEM(1, elem);
#define PRINT_CONFIG_CLASS_ELEMENT_VISIT(r, data, elem) if (! f(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(1, elem)), this->BOOST_PP_TUPLE_ELEM(1, elem), rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return;
#define PRINT_CONFIG_CLASS_ELEMENT_VISIT_SELF(r, data, elem) if (! f(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(1, elem)), self.BOOST_PP_TUPLE_ELEM(1, elem), rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return;
// Each option list is expanded into the members and again into for_each_option_pair(), which calls
// f(key, this->option, rhs.option) in declaration order and stops when f returns false. hash(),
// operator==, operator< and initialize() iterate the options through that visitor.
// operator==, operator<, initialize() and apply_to() iterate the options through that visitor.
#define PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \
size_t hash() const throw() \
{ \
@@ -1067,11 +1068,16 @@ class CLASS_NAME : public StaticPrintConfig { \
STATIC_PRINT_CONFIG_CACHE(CLASS_NAME) \
public: \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ) \
template<typename F> void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const \
{ \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ) \
} \
template<typename F> void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const { visit_option_pairs(*this, rhs, f); } \
/* Defined in PrintConfig.cpp. */ \
bool apply_to(ConfigBase &target) const override; \
PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \
private: \
/* The one expansion of the option list, for a const self and for apply_to()'s mutable target. */ \
template<typename Self, typename F> static void visit_option_pairs(Self &self, const CLASS_NAME &rhs, F &&f) \
{ \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT_SELF, _, PARAMETER_DEFINITION_SEQ) \
} \
};
#define PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM(r, data, i, elem) BOOST_PP_COMMA_IF(i) public elem
@@ -1092,6 +1098,8 @@ class CLASS_NAME : PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST(CLASSES_PARENTS_TUPLE)
public: \
PARAMETER_DEFINITION \
template<typename F> void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const { PARAMETER_VISIT } \
/* Its parents each apply themselves to a target member by member, so this one keeps the lookup by name. */ \
bool apply_to(ConfigBase &/*target*/) const override { return false; } \
size_t hash() const throw() \
{ \
size_t seed = 0; \
@@ -2182,6 +2190,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE0(
#undef STATIC_PRINT_CONFIG_CACHE_DERIVED
#undef PRINT_CONFIG_CLASS_ELEMENT_DEFINITION
#undef PRINT_CONFIG_CLASS_ELEMENT_VISIT
#undef PRINT_CONFIG_CLASS_ELEMENT_VISIT_SELF
#undef PRINT_CONFIG_CLASS_COMMON_BODY
#undef PRINT_CONFIG_CLASS_DEFINE
#undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST
@@ -0,0 +1,219 @@
#include "TextureBakeDebug.hpp"
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace {
// Half-edge key, low index first so both sides of an edge form the same one.
inline uint64_t edge_key(int a, int b)
{
const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b));
return (uint64_t(lo) << 32) | uint64_t(hi);
}
} // namespace
void bake_stage_topology(const BakeStageMesh &mesh, size_t &open_edges, size_t &non_manifold_edges,
size_t &degenerate)
{
open_edges = non_manifold_edges = degenerate = 0;
// Sorted half-edges rather than a hash map: same answer, but it is one allocation and a sort
// instead of three million node allocations, which on a stage this size is the whole cost.
std::vector<uint64_t> keys;
keys.reserve(mesh.indices.size() * 3);
for (const Vec3i32 &t : mesh.indices) {
if (t[0] == t[1] || t[1] == t[2] || t[0] == t[2]) {
++degenerate;
continue; // a collapsed face has no edges worth counting
}
const Vec3f &a = mesh.vertices[size_t(t[0])];
if ((mesh.vertices[size_t(t[1])] - a).cross(mesh.vertices[size_t(t[2])] - a).squaredNorm() <= 0.f)
++degenerate; // zero area but three distinct corners: still counted as an edge carrier
for (int e = 0; e < 3; ++e)
keys.push_back(edge_key(t[e], t[(e + 1) % 3]));
}
std::sort(keys.begin(), keys.end());
for (size_t i = 0; i < keys.size();) {
size_t j = i + 1;
while (j < keys.size() && keys[j] == keys[i])
++j;
const size_t incident = j - i;
if (incident == 1)
++open_edges;
else if (incident > 2)
++non_manifold_edges;
i = j;
}
}
void BakeStageRecorder::finish(BakeStageSnapshot &s)
{
s.triangles = s.mesh.indices.size();
s.vertices = s.mesh.vertices.size();
if (m_check_topology)
bake_stage_topology(s.mesh, s.open_edges, s.non_manifold_edges, s.degenerate);
s.topology_checked = m_check_topology;
if (s.triangles > m_mesh_cap) {
s.mesh_dropped = true;
s.mesh.vertices.clear();
s.mesh.vertices.shrink_to_fit();
s.mesh.indices.clear();
s.mesh.indices.shrink_to_fit();
}
m_stages.push_back(std::move(s));
}
void BakeStageRecorder::capture(const char *name, const TextureBake::TriSoup &soup, double ms,
const std::string &detail)
{
if (!m_enabled)
return;
BakeStageSnapshot s;
s.name = name;
s.detail = detail;
s.ms = ms;
// The pipeline works on non-indexed soup, so welding here is what turns it back into something
// renderable. The geometry grid, matching to_indexed_triangle_set(), so the debug view shows the
// same sharing the bake's own output would have.
const size_t n = soup.pos.size();
TextureBake::QuantizedPointMap map(TextureBake::WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
s.mesh.vertices.reserve(n / 3);
for (size_t i = 0; i < n; ++i) {
id[i] = map.get_or_set(soup.pos[i], int(s.mesh.vertices.size()));
if (map.inserted())
s.mesh.vertices.push_back(soup.pos[i]);
}
s.mesh.indices.reserve(n / 3);
for (size_t t = 0; t + 2 < n; t += 3) {
// Corners that welded together carry no area; the bake's own conversion drops them too, so
// dropping them here keeps the stage count honest against what would be committed.
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
continue;
s.mesh.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
}
finish(s);
}
void BakeStageRecorder::capture(const char *name, const std::vector<Vec3f> &vertices,
const std::vector<Vec3i32> &indices, double ms,
const std::string &detail)
{
if (!m_enabled)
return;
BakeStageSnapshot s;
s.name = name;
s.detail = detail;
s.ms = ms;
s.mesh.vertices = vertices;
s.mesh.indices = indices;
finish(s);
}
void BakeStageRecorder::capture_note(const char *name, double ms, const std::string &detail)
{
if (!m_enabled)
return;
BakeStageSnapshot s;
s.name = name;
s.detail = detail;
s.ms = ms;
s.topology_checked = false;
m_stages.push_back(std::move(s));
}
void BakeStageRecorder::rebase(size_t from, const Transform3d *to_local, bool flip_winding)
{
for (size_t i = from; i < m_stages.size(); ++i) {
BakeStageMesh &m = m_stages[i].mesh;
if (to_local != nullptr)
for (Vec3f &v : m.vertices)
v = (*to_local * v.cast<double>()).cast<float>();
if (flip_winding)
for (Vec3i32 &t : m.indices)
std::swap(t[1], t[2]);
}
}
double BakeStageRecorder::total_ms() const
{
double sum = 0.0;
for (const BakeStageSnapshot &s : m_stages)
sum += s.ms;
return sum;
}
size_t dump_bake_stages(const std::vector<BakeStageSnapshot> &stages, const std::string &dir)
{
boost::system::error_code ec;
boost::filesystem::create_directories(dir, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "BakeStageRecorder: cannot create " << dir << ": " << ec.message();
return 0;
}
size_t written = 0;
for (size_t i = 0; i < stages.size(); ++i) {
const BakeStageSnapshot &s = stages[i];
if (s.mesh.empty())
continue;
// Stage names carry spaces and punctuation; keep the filename to what every shell and viewer
// handles without quoting.
std::string safe;
for (const char c : s.name)
safe += (std::isalnum(static_cast<unsigned char>(c)) != 0) ? c : '_';
char path[1024];
std::snprintf(path, sizeof(path), "%s/%02zu_%s.obj", dir.c_str(), i, safe.c_str());
std::FILE *f = std::fopen(path, "wb");
if (f == nullptr) {
BOOST_LOG_TRIVIAL(error) << "BakeStageRecorder: cannot write " << path;
continue;
}
std::fprintf(f, "# texture bake stage %zu: %s\n", i, s.name.c_str());
if (!s.detail.empty())
std::fprintf(f, "# %s\n", s.detail.c_str());
std::fprintf(f, "# %zu triangles, %.2f ms\n", s.triangles, s.ms);
for (const Vec3f &v : s.mesh.vertices)
std::fprintf(f, "v %.6f %.6f %.6f\n", double(v.x()), double(v.y()), double(v.z()));
for (const Vec3i32 &t : s.mesh.indices) // OBJ indices are 1-based
std::fprintf(f, "f %d %d %d\n", t[0] + 1, t[1] + 1, t[2] + 1);
std::fclose(f);
++written;
}
char summary[1024];
std::snprintf(summary, sizeof(summary), "%s/stages.txt", dir.c_str());
if (std::FILE *f = std::fopen(summary, "wb"); f != nullptr) {
std::fprintf(f, "%-3s %-24s %10s %12s %10s %8s %8s %8s %s\n", "#", "stage", "ms", "triangles",
"vertices", "open", "nonman", "degen", "detail");
double total = 0.0;
for (size_t i = 0; i < stages.size(); ++i) {
const BakeStageSnapshot &s = stages[i];
total += s.ms;
std::fprintf(f, "%-3zu %-24s %10.2f %12zu %10zu ", i, s.name.c_str(), s.ms, s.triangles,
s.vertices);
if (s.topology_checked)
std::fprintf(f, "%8zu %8zu %8zu", s.open_edges, s.non_manifold_edges, s.degenerate);
else
std::fprintf(f, "%8s %8s %8s", "-", "-", "-");
std::fprintf(f, " %s%s\n", s.detail.c_str(), s.mesh_dropped ? " [mesh over cap, not written]" : "");
}
std::fprintf(f, "\ntotal %.2f ms across %zu stages\n", total, stages.size());
std::fclose(f);
}
return written;
}
} // namespace Slic3r
@@ -0,0 +1,123 @@
#pragma once
// Step-by-step capture of a bake.
//
// A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the
// only useful question is which stage made it wrong. This records the geometry, the wall time and the
// topology after every stage, which is what the gizmo's debug view steps through and what the
// benchmark's --dump-stages writes out.
//
// Deliberately independent of TriangleMesh: a stage is held as a plain vertex/index pair, which is
// layout-compatible with indexed_triangle_set's own members (stl_vertex is Vec3f,
// stl_triangle_vertex_indices is Vec3i32), so the GUI assigns rather than converts and the standalone
// benchmark does not have to link admesh to use this.
//
// Recording is off unless enable(true) was called, and every capture site is a null-pointer check, so
// a normal bake pays nothing for this being here.
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
struct BakeStageMesh
{
std::vector<Vec3f> vertices;
std::vector<Vec3i32> indices;
bool empty() const { return indices.empty(); }
size_t triangle_count() const { return indices.size(); }
};
struct BakeStageSnapshot
{
std::string name; // "remesh", "subdivide", ...
std::string detail; // whatever the stage has to say: collapse counts, rejected moves, ...
BakeStageMesh mesh; // empty when the stage was over the memory cap - see mesh_dropped
double ms = 0.0;
size_t triangles = 0;
size_t vertices = 0;
// Filled only when the recorder was asked to check topology: it is a sort over every half-edge,
// which on a multi-million triangle stage costs more than the stage being measured.
size_t open_edges = 0;
size_t non_manifold_edges = 0;
size_t degenerate = 0;
bool topology_checked = false;
// The geometry was dropped to stay inside the memory cap; every count above is still real.
bool mesh_dropped = false;
};
// Edge and area defects of a captured stage. Split out so a caller can run it on its own.
void bake_stage_topology(const BakeStageMesh &mesh, size_t &open_edges, size_t &non_manifold_edges,
size_t &degenerate);
// Writes `<dir>/NN_name.obj` for every stage that still holds geometry, plus a `stages.txt` summary.
// Returns how many meshes were written. Existing files with the same names are overwritten.
//
// A free function rather than a recorder method because by the time anyone wants the files the
// recorder is usually gone and only the stages survive - that is how the gizmo holds them.
size_t dump_bake_stages(const std::vector<BakeStageSnapshot> &stages, const std::string &dir);
class BakeStageRecorder
{
public:
// Nothing is recorded until this is on.
void enable(bool on) { m_enabled = on; }
bool enabled() const { return m_enabled; }
// The edge scan is optional because it is O(n log n) over every half-edge, and a debug run that
// only wants to see the geometry should not pay for it on every stage.
void set_check_topology(bool on) { m_check_topology = on; }
bool check_topology() const { return m_check_topology; }
// Stages above this keep their counts but not their geometry. A debug run holds every stage at
// once, and a 4 M triangle stage is about 150 MB on its own, so without a cap stepping through a
// fine bake would need more memory than the bake did.
void set_mesh_cap(size_t triangles) { m_mesh_cap = triangles; }
size_t mesh_cap() const { return m_mesh_cap; }
// `ms` is passed in rather than measured here: the caller is already timing the stage, and the
// capture itself (a weld, a copy, possibly an edge scan) must not land inside that measurement.
void capture(const char *name, const TextureBake::TriSoup &soup, double ms,
const std::string &detail = {});
void capture(const char *name, const std::vector<Vec3f> &vertices,
const std::vector<Vec3i32> &indices, double ms, const std::string &detail = {});
// For a stage that changed nothing a caller can still show, e.g. a skipped remesh.
void capture_note(const char *name, double ms, const std::string &detail);
// Index of the next stage to be recorded. Paired with rebase() to fix up a range afterwards.
size_t mark() const { return m_stages.size(); }
// Brings stages [from, end) into the caller's own space and winding. The bake runs in world
// millimetres and, for a mirrored placement, against a reversed winding; the debug view draws in
// the volume's local frame, so a captured range has to be brought back the same way the bake's
// own result is. `to_local` may be null for no transform.
void rebase(size_t from, const Transform3d *to_local, bool flip_winding);
const std::vector<BakeStageSnapshot> &stages() const { return m_stages; }
std::vector<BakeStageSnapshot> take() { return std::move(m_stages); }
void clear() { m_stages.clear(); }
bool empty() const { return m_stages.empty(); }
// Total recorded wall time, which is the bake's own time minus whatever it does outside a stage.
double total_ms() const;
size_t dump_obj(const std::string &dir) const { return dump_bake_stages(m_stages, dir); }
private:
void finish(BakeStageSnapshot &s);
std::vector<BakeStageSnapshot> m_stages;
bool m_enabled = false;
bool m_check_topology = true;
size_t m_mesh_cap = 4'000'000;
};
} // namespace Slic3r
@@ -0,0 +1,562 @@
#include "TextureBakeDecimate.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <queue>
#include <boost/log/trivial.hpp>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
namespace {
// Symmetric 4x4 quadric, as its 10 upper-triangle values.
struct Quadric
{
std::array<double, 10> q{};
void add_plane(double a, double b, double c, double d)
{
q[0] += a * a; q[1] += a * b; q[2] += a * c; q[3] += a * d;
q[4] += b * b; q[5] += b * c; q[6] += b * d;
q[7] += c * c; q[8] += c * d;
q[9] += d * d;
}
void operator+=(const Quadric &o)
{
for (int i = 0; i < 10; ++i)
q[size_t(i)] += o.q[size_t(i)];
}
double eval(double x, double y, double z) const
{
return q[0] * x * x + 2 * q[1] * x * y + 2 * q[2] * x * z + 2 * q[3] * x +
q[4] * y * y + 2 * q[5] * y * z + 2 * q[6] * y +
q[7] * z * z + 2 * q[8] * z + q[9];
}
};
double eval_sum(const std::vector<Quadric> &qs, int v1, int v2, const Vec3d &p)
{
return qs[size_t(v1)].eval(p.x(), p.y(), p.z()) + qs[size_t(v2)].eval(p.x(), p.y(), p.z());
}
// The position minimising the summed quadric, if the system is well conditioned enough to trust.
bool solve_q(const std::vector<Quadric> &qs, int v1, int v2, Vec3d &out)
{
const auto &A = qs[size_t(v1)].q;
const auto &B = qs[size_t(v2)].q;
const double a00 = A[0] + B[0], a01 = A[1] + B[1], a02 = A[2] + B[2];
const double a11 = A[4] + B[4], a12 = A[5] + B[5], a22 = A[7] + B[7];
const double b0 = -(A[3] + B[3]), b1 = -(A[6] + B[6]), b2 = -(A[8] + B[8]);
const double det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02) +
a02 * (a01 * a12 - a11 * a02);
const double max_el = std::max({ std::abs(a00), std::abs(a01), std::abs(a02), std::abs(a11),
std::abs(a12), std::abs(a22) });
// Scaled with the matrix, so it means the same at any model scale.
const double threshold = max_el * max_el * max_el * 1e-10;
if (std::abs(det) < std::max(threshold, 1e-30))
return false;
const double inv = 1.0 / det;
out.x() = inv * (b0 * (a11 * a22 - a12 * a12) - a01 * (b1 * a22 - a12 * b2) + a02 * (b1 * a12 - a11 * b2));
out.y() = inv * (a00 * (b1 * a22 - a12 * b2) - b0 * (a01 * a22 - a12 * a02) + a02 * (a01 * b2 - b1 * a02));
out.z() = inv * (a00 * (a11 * b2 - b1 * a12) - a01 * (a01 * b2 - b1 * a02) + b0 * (a01 * a12 - a11 * a02));
return true;
}
Vec3d face_normal_unit(const std::vector<Vec3d> &pos, int a, int b, int c)
{
const Vec3d n = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]);
const double len = n.norm();
return (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
}
// Versions are captured at push time; a mismatch on pop means a later collapse invalidated the entry.
// Lazy deletion, far cheaper than removing entries eagerly - but it means the heap accumulates stale
// duplicates, so its size has to be reserved up front and it is compacted once the dead entries
// dominate (see maybe_compact below).
//
// The collapse target is not stored. A matching version stamp means neither endpoint's quadric nor its
// position has changed since the push, so the target recomputes to exactly the same value on pop - and
// the entry drops from 40 bytes to 24. Sifting is most of this stage's time, and it is memory traffic.
struct HeapEntry
{
double cost;
int v1, v2;
uint32_t ver1, ver2;
bool operator>(const HeapEntry &o) const { return cost > o.cost; }
};
} // namespace
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat,
double harvest_tol, const std::vector<uint8_t> &locked_faces,
const DecimateProgressFn &on_progress, const std::vector<int> &face_color)
{
DecimateResult result;
const size_t n = geometry.pos.size();
if (n < 3) {
result.geometry = geometry;
return result;
}
// The finest grid. Anything coarser fuses distinct fine-feature vertices on a displaced mesh,
// leaving it non-manifold before decimation starts and producing open edges afterwards.
QuantizedPointMap vert_map(WELD_GRID_DECIMATION, std::min(n, size_t(1) << 22));
std::vector<Vec3d> pos;
std::vector<int> remap(n);
for (size_t i = 0; i < n; ++i) {
const int idx = vert_map.get_or_set(geometry.pos[i], int(pos.size()));
if (vert_map.inserted())
pos.push_back(geometry.pos[i].cast<double>());
remap[i] = idx;
}
const size_t vert_count = pos.size();
const size_t face_count = n / 3;
std::vector<int> faces(face_count * 3);
for (size_t i = 0; i < n; ++i)
faces[i] = remap[i];
if (face_count <= target_triangles && !harvest_flat) {
result.geometry = geometry;
return result;
}
// An edge with a locked endpoint never reaches the heap.
std::vector<uint8_t> locked_vert;
size_t locked_face_count = 0;
if (!locked_faces.empty()) {
locked_vert.assign(vert_count, 0);
for (size_t f = 0; f < face_count && f < locked_faces.size(); ++f) {
if (!locked_faces[f])
continue;
++locked_face_count;
for (int k = 0; k < 3; ++k)
locked_vert[size_t(faces[f * 3 + size_t(k)])] = 1;
}
}
// With the locked faces alone at the target, chasing it would grind the free region to its guard
// limit for nothing - harvest only, and say so.
const bool locked_over_budget =
!locked_vert.empty() && face_count > target_triangles && locked_face_count >= target_triangles;
result.locked_over_budget = locked_over_budget;
if (locked_over_budget && !harvest_flat) {
result.geometry = geometry;
return result;
}
std::vector<Quadric> quadrics(vert_count);
{
// The plane per face is independent; accumulating it into the three incident vertices is not,
// so only the first half is parallel.
std::vector<Vec4d> planes(face_count, Vec4d::Zero());
tbb::parallel_for(tbb::blocked_range<size_t>(0, face_count),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t f = range.begin(); f < range.end(); ++f) {
const int a = faces[f * 3], b = faces[f * 3 + 1], c = faces[f * 3 + 2];
if (a < 0)
continue;
const Vec3d nrm = face_normal_unit(pos, a, b, c);
if (nrm.isZero())
continue;
planes[f] = Vec4d(nrm.x(), nrm.y(), nrm.z(), -nrm.dot(pos[size_t(a)]));
}
});
for (size_t f = 0; f < face_count; ++f) {
const Vec4d &pl = planes[f];
if (pl.head<3>().isZero())
continue;
for (int k = 0; k < 3; ++k)
quadrics[size_t(faces[f * 3 + size_t(k)])].add_plane(pl.x(), pl.y(), pl.z(), pl.w());
}
}
// Two penalty planes per endpoint on a sharp interior edge, each perpendicular to one adjacent
// face and containing the edge, constraining the vertex to the crease line.
{
struct EdgeRec { int va, vb, f0, f1; uint8_t count; };
std::vector<EdgeRec> edges;
QuantizedPointMap edge_idx(1.0, std::min(face_count * 3, size_t(1) << 22));
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
for (int e = 0; e < 3; ++e) {
const int va = faces[f * 3 + size_t(e)];
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
const int lo = std::min(va, vb), hi = std::max(va, vb);
const int ei = edge_idx.get_or_set_key(lo, hi, 0, int(edges.size()));
if (edge_idx.inserted())
edges.push_back({ lo, hi, int(f), -1, 1 });
else if (edges[size_t(ei)].count == 1) {
edges[size_t(ei)].f1 = int(f);
edges[size_t(ei)].count = 2;
} else
// Non-manifold; never feeds a crease.
edges[size_t(ei)].count = 3;
}
}
const double sqrt_w = std::sqrt(DECIMATE_CREASE_WEIGHT);
for (const EdgeRec &er : edges) {
if (er.count != 2)
continue; // boundary or non-manifold
const Vec3d n0 = face_normal_unit(pos, faces[size_t(er.f0) * 3], faces[size_t(er.f0) * 3 + 1],
faces[size_t(er.f0) * 3 + 2]);
const Vec3d n1 = face_normal_unit(pos, faces[size_t(er.f1) * 3], faces[size_t(er.f1) * 3 + 1],
faces[size_t(er.f1) * 3 + 2]);
const bool color_edge = face_color.size() > std::max(size_t(er.f0), size_t(er.f1)) &&
face_color[size_t(er.f0)] != face_color[size_t(er.f1)];
if (!color_edge && n0.dot(n1) >= DECIMATE_CREASE_COS)
continue; // smooth enough to be no crease, and no colour changes across it
const Vec3d e = pos[size_t(er.vb)] - pos[size_t(er.va)];
const double elen = e.norm();
if (elen <= 0.0)
continue;
const Vec3d ed = e / elen;
for (const Vec3d &fn : { n0, n1 }) {
Vec3d pn = fn.cross(ed);
const double plen = pn.norm();
if (plen < 1e-10)
continue; // edge parallel to the face normal
pn /= plen;
const double d = -pn.dot(pos[size_t(er.va)]);
// sqrt(w) on the inputs gives w times the accumulated products.
for (const int v : { er.va, er.vb })
quadrics[size_t(v)].add_plane(pn.x() * sqrt_w, pn.y() * sqrt_w, pn.z() * sqrt_w,
d * sqrt_w);
}
}
}
// Vertex-face incidence as intrusive linked lists of slots over flat arrays, with each list's length.
const size_t S = face_count * 3;
std::vector<int> vf_head(vert_count, -1), vf_count(vert_count, 0), slot_face(S), slot_vert(S),
slot_next(S, -1), slot_prev(S, -1), face_slot(S, -1);
for (size_t f = 0; f < face_count; ++f)
for (int k = 0; k < 3; ++k) {
const int s = int(f) * 3 + k;
const int v = faces[size_t(s)];
slot_face[size_t(s)] = int(f);
slot_vert[size_t(s)] = v;
slot_next[size_t(s)] = vf_head[size_t(v)];
slot_prev[size_t(s)] = -1;
if (vf_head[size_t(v)] >= 0)
slot_prev[size_t(vf_head[size_t(v)])] = s;
vf_head[size_t(v)] = s;
face_slot[size_t(s)] = s;
++vf_count[size_t(v)];
}
const auto unlink_slot = [&](int s) {
const int p = slot_prev[size_t(s)], nx = slot_next[size_t(s)], v = slot_vert[size_t(s)];
if (p >= 0) slot_next[size_t(p)] = nx;
else vf_head[size_t(v)] = nx;
if (nx >= 0) slot_prev[size_t(nx)] = p;
--vf_count[size_t(v)];
};
const auto move_slot = [&](int s, int nv) {
unlink_slot(s);
slot_next[size_t(s)] = vf_head[size_t(nv)];
slot_prev[size_t(s)] = -1;
if (vf_head[size_t(nv)] >= 0)
slot_prev[size_t(vf_head[size_t(nv)])] = s;
vf_head[size_t(nv)] = s;
slot_vert[size_t(s)] = nv;
++vf_count[size_t(nv)];
};
std::vector<uint8_t> active(vert_count, 1);
std::vector<uint32_t> version(vert_count, 0);
std::vector<uint32_t> nb_stamp(vert_count, 0), lk_stamp(vert_count, 0);
uint32_t epoch = 1, lk_epoch = 1;
size_t active_faces = face_count;
// A plain vector driven by the heap algorithms, so the capacity can be reserved. Lazy deletion
// means roughly one entry per edge plus one per re-push after each collapse; the reserve below is
// sized from the edge count and simply grows if a mesh needs more.
std::vector<HeapEntry> heap;
heap.reserve(std::min<size_t>(face_count * 3, size_t(1) << 24));
const auto heap_push = [&](HeapEntry e) {
heap.push_back(e);
std::push_heap(heap.begin(), heap.end(), std::greater<HeapEntry>());
};
const auto heap_pop = [&]() {
std::pop_heap(heap.begin(), heap.end(), std::greater<HeapEntry>());
const HeapEntry e = heap.back();
heap.pop_back();
return e;
};
size_t pops = 0, stale_pops = 0, compactions = 0;
// An entry is stale once either endpoint has been removed or moved by a later collapse.
const auto is_stale = [&](const HeapEntry &e) {
return !active[size_t(e.v1)] || !active[size_t(e.v2)] || version[size_t(e.v1)] != e.ver1 ||
version[size_t(e.v2)] != e.ver2;
};
// Measured on a 2.4 M -> 750 k run, 82% of pops were stale: every collapse re-pushes the survivor's
// edges and orphans the old ones, so the heap grows to several times the live edge set and every
// sift walks that much further through memory. Dropping the dead entries and re-heapifying once
// they dominate costs one linear pass, amortised against the growth that triggered it.
//
// Keyed to the live face count rather than to the heap's own size: lazy popping keeps the heap from
// ever doubling, but the live edge set (about 1.5 per face) shrinks as decimation proceeds, so by the
// end the heap is several times what is still collapsible. Compact once it passes twice that.
const auto maybe_compact = [&]() {
if (heap.size() < std::max<size_t>(size_t(1) << 16, active_faces * 3))
return;
heap.erase(std::remove_if(heap.begin(), heap.end(), is_stale), heap.end());
std::make_heap(heap.begin(), heap.end(), std::greater<HeapEntry>());
++compactions;
};
// Where an edge collapses to. Also re-run on pop instead of stored - see HeapEntry.
const auto collapse_target = [&](int v1, int v2) -> Vec3d {
Vec3d p;
if (!solve_q(quadrics, v1, v2, p)) {
const Vec3d mid = (pos[size_t(v1)] + pos[size_t(v2)]) * 0.5;
const double e1 = eval_sum(quadrics, v1, v2, pos[size_t(v1)]);
const double e2 = eval_sum(quadrics, v1, v2, pos[size_t(v2)]);
const double em = eval_sum(quadrics, v1, v2, mid);
const double emin = std::min({ e1, e2, em });
const double etol = emin * 1e-2 + 1e-12;
// The midpoint when the three are near-equal, i.e. flat: it moves adjacent triangles
// least, so fewer normal flips and no stalling on coplanar geometry.
if (em <= emin + etol) p = mid;
else if (e1 <= e2) p = pos[size_t(v1)];
else p = pos[size_t(v2)];
}
return p;
};
const auto push_edge = [&](int v1, int v2) {
const Vec3d p = collapse_target(v1, v2);
// The cost is evaluated at the exact target and the collapse moves to its float rounding -
// the same split as when the rounded target was stored in the entry, so no ordering changes.
// Where quadric costs are all near zero, shorter edges first keeps triangle quality up.
const double len2 = (pos[size_t(v2)] - pos[size_t(v1)]).squaredNorm();
heap_push({ eval_sum(quadrics, v1, v2, p) + len2 * 1e-8, v1, v2, version[size_t(v1)],
version[size_t(v2)] });
};
{
QuantizedPointMap seed_seen(1.0, std::min(face_count * 3, size_t(1) << 22));
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
for (int e = 0; e < 3; ++e) {
const int va = faces[f * 3 + size_t(e)];
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
if (!locked_vert.empty() && (locked_vert[size_t(va)] || locked_vert[size_t(vb)]))
continue;
seed_seen.get_or_set_key(std::min(va, vb), std::max(va, vb), 0, 1);
if (seed_seen.inserted())
push_edge(va, vb);
}
}
}
// 0 means a stale entry, 1 a boundary edge, 2 or more safe.
const auto shared_face_count = [&](int v1, int v2) {
int count = 0;
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k)
if (faces[size_t(f) * 3 + size_t(k)] == v2) {
if (++count >= 2)
return 2;
break;
}
}
return count;
};
// Safe only when the sole common neighbours of the endpoints are the apexes of the faces the edge
// already shares; any other would pile a third triangle onto an edge after the collapse.
const auto has_link_violation = [&](int v1, int v2, uint32_t ep) {
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k)
if (const int x = faces[size_t(f) * 3 + size_t(k)]; x != v1)
lk_stamp[size_t(x)] = ep;
}
int shared = 0;
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
const int a = faces[size_t(f) * 3], b = faces[size_t(f) * 3 + 1], c = faces[size_t(f) * 3 + 2];
if (a == v2 || b == v2 || c == v2) {
++shared;
const int apex = (a != v1 && a != v2) ? a : (b != v1 && b != v2) ? b : c;
lk_stamp[size_t(apex)] = ep + 1; // a legal shared-face apex
}
}
if (shared > 2)
return true; // already non-manifold
for (int s = vf_head[size_t(v2)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k) {
const int x = faces[size_t(f) * 3 + size_t(k)];
if (x != v2 && x != v1 && lk_stamp[size_t(x)] == ep)
return true;
}
}
return false;
};
// Squared-dot, so no square root or division. Faces containing the other endpoint are the ones
// being removed, so they are skipped.
const auto check_flipped = [&](int vc, int vo, const Vec3d &np) {
for (int s = vf_head[size_t(vc)]; s >= 0; s = slot_next[size_t(s)]) {
const size_t f = size_t(slot_face[size_t(s)]);
if (faces[f * 3] < 0)
continue;
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
if (fa == vo || fb == vo || fc == vo)
continue;
const Vec3d oa = pos[size_t(fa)], ob = pos[size_t(fb)], oc = pos[size_t(fc)];
const Vec3d on = (ob - oa).cross(oc - oa);
const Vec3d na = (fa == vc) ? np : oa;
const Vec3d nb = (fb == vc) ? np : ob;
const Vec3d nc = (fc == vc) ? np : oc;
const Vec3d nn = (nb - na).cross(nc - na);
const double raw = on.dot(nn);
if (raw < 0.0)
return true;
if (raw * raw < DECIMATE_FLIP_DOT * DECIMATE_FLIP_DOT * on.squaredNorm() * nn.squaredNorm())
return true;
}
return false;
};
const size_t init_faces = active_faces;
const size_t to_remove = std::max<size_t>(1, init_faces > target_triangles
? init_faces - target_triangles : init_faces);
const double harvest_ceil = harvest_tol * harvest_tol;
bool reached_target = locked_over_budget;
double last_progress = 0.0;
while (!heap.empty()) {
if (active_faces <= target_triangles) {
if (!harvest_flat)
break;
reached_target = true;
}
const HeapEntry top = heap_pop();
++pops;
if (on_progress) {
// Every 16 k pops as well as whenever the fraction moves: this is the only place a cancel
// is seen, and past the target the fraction stops moving.
const double p = std::min(1.0, double(init_faces - active_faces) / double(to_remove));
if (p - last_progress > 0.005 || (pops & 0x3fff) == 0) {
last_progress = p;
if (!on_progress(p))
break;
}
}
// The popped entry is the cheapest left, so exceeding the tolerance ends the run.
if (reached_target && top.cost > harvest_ceil)
break;
const int v1 = top.v1, v2 = top.v2;
if (is_stale(top)) {
++stale_pops;
continue;
}
// Ahead of the checks below, which walk the fans (see DECIMATE_MAX_VALENCE). The two shared
// faces, counted in both fans, go.
if (vf_count[size_t(v1)] + vf_count[size_t(v2)] - 4 > DECIMATE_MAX_VALENCE)
continue;
if (shared_face_count(v1, v2) < 2)
continue;
lk_epoch += 2; // +2 so ep and ep+1 cannot collide with the next call
if (has_link_violation(v1, v2, lk_epoch))
continue;
const Vec3d target = collapse_target(v1, v2).cast<float>().cast<double>();
if (check_flipped(v1, v2, target) || check_flipped(v2, v1, target))
continue;
if (!reached_target && top.cost > harvest_ceil)
result.target_cost_detail = true;
// v1 survives at the new position, v2 goes.
pos[size_t(v1)] = target;
quadrics[size_t(v1)] += quadrics[size_t(v2)];
++version[size_t(v1)];
for (int s = vf_head[size_t(v2)]; s >= 0;) {
const size_t f = size_t(slot_face[size_t(s)]);
const int s_next = slot_next[size_t(s)]; // read before the list is modified
if (faces[f * 3] >= 0) {
for (int k = 0; k < 3; ++k)
if (faces[f * 3 + size_t(k)] == v2) {
faces[f * 3 + size_t(k)] = v1;
break;
}
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
if (fa == fb || fb == fc || fa == fc) {
for (int k = 0; k < 3; ++k)
if (const int sk = face_slot[f * 3 + size_t(k)]; sk >= 0) {
unlink_slot(sk);
face_slot[f * 3 + size_t(k)] = -1;
}
faces[f * 3] = faces[f * 3 + 1] = faces[f * 3 + 2] = -1;
--active_faces;
} else
move_slot(s, v1);
}
s = s_next;
}
active[size_t(v2)] = 0;
++epoch;
for (int sv = vf_head[size_t(v1)]; sv >= 0; sv = slot_next[size_t(sv)]) {
const size_t f = size_t(slot_face[size_t(sv)]);
if (faces[f * 3] < 0)
continue;
for (int k = 0; k < 3; ++k) {
const int nb = faces[f * 3 + size_t(k)];
if (nb == v1 || nb_stamp[size_t(nb)] == epoch)
continue;
nb_stamp[size_t(nb)] = epoch;
// v1 is never locked - a locked edge never entered the heap.
if (active[size_t(nb)] && (locked_vert.empty() || !locked_vert[size_t(nb)]))
push_edge(v1, nb);
}
}
maybe_compact();
}
BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: pops=" << pops << " stale=" << stale_pops
<< " compactions=" << compactions << " heap_peak=" << heap.capacity()
<< " faces=" << active_faces;
// Rebuild from the surviving faces, with per-face normals.
TriSoup &out = result.geometry;
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
const Vec3f a = pos[size_t(faces[f * 3])].cast<float>();
const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast<float>();
const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast<float>();
const Vec3f nrm = (b - a).cross(c - a).normalized();
out.pos.insert(out.pos.end(), { a, b, c });
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,68 @@
#pragma once
// Quadric error metric decimation (Garland & Heckbert), with two additions that matter on a
// displaced mesh.
//
// Crease quadrics: an interior edge sharper than the threshold gets penalty planes at both endpoints,
// perpendicular to each adjacent face and through the edge, weighted so such edges collapse last or
// not at all. A texture's hard step keeps its geometry while the flat ground around it reduces.
//
// Flat-face harvesting: the loop keeps going past the triangle target while each collapse's error
// stays under an absolute bound, so flat faces that cost nothing to remove are not left behind.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Reject a collapse deviating more than about 78 degrees from the old face normal.
static constexpr double DECIMATE_FLIP_DOT = 0.2;
// Edges sharper than 60 degrees are treated as creases.
static constexpr double DECIMATE_CREASE_COS = 0.5;
// Quadric penalty weight for a crease plane.
static constexpr double DECIMATE_CREASE_WEIGHT = 1e4;
// Most faces a collapse may leave around its surviving vertex. Uncapped, a pinned vertex on flat
// ground grows a fan of thousands of slivers, and validating each of its edges walks the whole fan,
// which turns a decimation of a second or two into minutes. At 64 the triangle count moves by about
// 1% at most, and by far less on large bakes.
static constexpr int DECIMATE_MAX_VALENCE = 64;
// Upper bound in mm on the deviation a harvested collapse may introduce; the real one is smaller,
// since the cost sums squared distances over all incident faces.
//
// Absolute, not relative to the cost at which the target was crossed. A relative band fails in the
// case with the most to shed: when the target is reached with a large flat surplus left, the crossing
// cost is essentially zero, so the band is too and nothing is harvested.
static constexpr double DECIMATE_DEFAULT_HARVEST_TOL = 0.005;
// Returns false to cancel.
using DecimateProgressFn = std::function<bool(double fraction)>;
struct DecimateResult
{
TriSoup geometry;
// The locked faces alone met the target, so it was unreachable without touching preserved
// geometry.
bool locked_over_budget = false;
// Reaching the target took a collapse costing more than harvest_tol, so the target removed detail
// and not only flat faces a harvest would have taken anyway.
bool target_cost_detail = false;
};
// `locked_faces`: one entry per input triangle; a vertex touching one may neither move nor be
// removed, which also pins the ring between the two regions.
// `face_color`: optional, one entry per input triangle. An edge between two faces of different
// colour is treated as a crease, so the simplified triangles never span a colour boundary and the
// boundary keeps its place - a per-triangle colour read off the result then has nothing to smear.
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat = true,
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL,
const std::vector<uint8_t> &locked_faces = {},
const DecimateProgressFn &on_progress = {},
const std::vector<int> &face_color = {});
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,285 @@
#include "TextureBakeDisplace.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
const DisplaceSettings &settings, const DisplaceBounds &bounds,
const DisplaceProgressFn &on_progress)
{
TriSoup out;
const size_t count = geometry.pos.size();
if (count == 0 || !sample)
return geometry;
out.pos.resize(count);
out.nrm.resize(count);
// Everything below is keyed by this id, which is what makes one vector per position expressible.
const bool need_id_positions = settings.boundary_falloff > 0.f;
QuantizedPointMap dedup(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
std::vector<int> vertex_id(count);
std::vector<Vec3f> id_pos;
int next_id = 0;
for (size_t i = 0; i < count; ++i) {
const int id = dedup.get_or_set(geometry.pos[i], next_id);
if (dedup.inserted()) {
++next_id;
if (need_id_positions)
id_pos.push_back(geometry.pos[i]);
}
vertex_id[i] = id;
}
const size_t unique_count = size_t(next_id);
// Pass 1: area-weighted smooth normals per position, plus what masking and falloff need.
std::vector<Vec3d> smooth_nrm(unique_count, Vec3d::Zero());
std::vector<double> masked_area(unique_count, 0.0), total_area(unique_count, 0.0);
const bool have_weights = !geometry.exclude_weight.empty();
std::vector<uint8_t> user_excluded_face(have_weights ? count / 3 : 0, 0);
std::vector<uint8_t> excluded_pos(have_weights ? unique_count : 0, 0);
for (size_t t = 0; t + 2 < count; t += 3) {
const Vec3d a = geometry.pos[t].cast<double>();
const Vec3d face_n = (geometry.pos[t + 1].cast<double>() - a).cross(geometry.pos[t + 2].cast<double>() - a);
const double face_area = face_n.norm(); // twice the triangle area, so weighting is natural
const double nz = face_area > 1e-12 ? face_n.z() / face_area : 0.0;
const double face_angle = std::acos(std::min(1.0, std::abs(nz))) * (180.0 / M_PI);
const bool angle_masked =
nz < 0.0 ? (settings.bottom_angle_limit > 0.f && face_angle <= settings.bottom_angle_limit)
: (settings.top_angle_limit > 0.f && face_angle <= settings.top_angle_limit);
// Thresholded high, not at a half: merging by maximum leaves a face bordering an excluded one
// with two corners at 1.0, averaging about 0.67, which a half threshold would misread.
bool user_excluded = false;
if (have_weights) {
const float avg = (geometry.exclude_weight[t] + geometry.exclude_weight[t + 1] +
geometry.exclude_weight[t + 2]) / 3.f;
user_excluded = avg > 0.99f;
if (user_excluded)
user_excluded_face[t / 3] = 1;
}
for (int v = 0; v < 3; ++v) {
const size_t vid = size_t(vertex_id[t + size_t(v)]);
if (user_excluded && have_weights)
excluded_pos[vid] = 1;
// Subdivision split vertices at sharp edges, so these are smooth across soft edges and
// sharp across hard ones - no faceting on round surfaces, no rounding of corners.
smooth_nrm[vid] += geometry.nrm[t + size_t(v)].cast<double>() * face_area;
if (angle_masked)
masked_area[vid] += face_area;
total_area[vid] += face_area;
}
}
// The pre-normalisation magnitude over the total area says how much the neighbouring faces agree:
// near 1 they do, near 0 they cancelled, meaning a knife edge with no usable surface direction.
std::vector<double> reliability(unique_count, 0.0);
for (size_t id = 0; id < unique_count; ++id) {
const double len = smooth_nrm[id].norm();
reliability[id] = (len > 0.0 && total_area[id] > 0.0) ? len / total_area[id] : 0.0;
smooth_nrm[id] = (len > 0.0) ? Vec3d(smooth_nrm[id] / len) : Vec3d(0.0, 0.0, 1.0);
}
// Pass 1.5: the smoothed blend normal - see the header for why it is separate.
std::vector<Vec3d> blend_nrm = smooth_nrm;
if (settings.blend_normal_smoothing > 0 && unique_count > 0) {
// CSR adjacency over the welded graph, deliberately a multigraph: duplicates weight a pair by
// how often it shares an edge, so a well-connected surface couples more strongly.
std::vector<uint32_t> degree(unique_count, 0);
const auto add_degree = [&](int a, int b) {
if (a != b) { ++degree[size_t(a)]; ++degree[size_t(b)]; }
};
for (size_t t = 0; t + 2 < count; t += 3) {
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
add_degree(a, b); add_degree(b, c); add_degree(c, a);
}
std::vector<uint32_t> csr_start(unique_count + 1, 0);
for (size_t id = 0; id < unique_count; ++id)
csr_start[id + 1] = csr_start[id] + degree[id];
std::vector<uint32_t> neighbors(csr_start[unique_count]);
std::vector<uint32_t> cursor(unique_count, 0);
const auto add_edge = [&](int a, int b) {
if (a == b)
return;
neighbors[csr_start[size_t(a)] + cursor[size_t(a)]++] = uint32_t(b);
neighbors[csr_start[size_t(b)] + cursor[size_t(b)]++] = uint32_t(a);
};
for (size_t t = 0; t + 2 < count; t += 3) {
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
add_edge(a, b); add_edge(b, c); add_edge(c, a);
}
std::vector<Vec3d> cur = smooth_nrm, nxt(unique_count, Vec3d::Zero());
for (int iter = 0; iter < settings.blend_normal_smoothing; ++iter) {
// Jacobi, so every vertex reads the previous iteration and the rows are independent.
tbb::parallel_for(tbb::blocked_range<size_t>(0, unique_count, 4096), [&](const tbb::blocked_range<size_t> &r) {
for (size_t id = r.begin(); id < r.end(); ++id) {
const uint32_t s = csr_start[id], e = csr_start[id + 1];
if (e == s) {
nxt[id] = cur[id];
continue;
}
Vec3d sum = Vec3d::Zero();
for (uint32_t k = s; k < e; ++k)
sum += cur[neighbors[k]];
sum /= double(e - s);
const double len = sum.norm();
// Cancelling neighbours mean a knife edge; keep what we had.
nxt[id] = (len > 1e-12) ? Vec3d(sum / len) : cur[id];
}
});
cur.swap(nxt);
}
blend_nrm = std::move(cur);
}
// A boundary position borders both masked and unmasked faces, or sits on the exclusion seam.
// Every other position gets its distance to the nearest one, ramped to 1 at the falloff distance.
std::vector<double> falloff;
if (settings.boundary_falloff > 0.f && unique_count > 0) {
std::vector<Vec3f> boundary;
for (size_t id = 0; id < unique_count; ++id) {
const double frac = total_area[id] > 0.0 ? masked_area[id] / total_area[id] : 0.0;
const bool on_excl = !excluded_pos.empty() && excluded_pos[id] != 0;
if (on_excl || (frac > 0.0 && frac < 1.0))
boundary.push_back(id_pos[id]);
}
falloff.assign(unique_count, 1.0);
if (!boundary.empty()) {
// A uniform grid: the query is nearest-point only, so a tree costs more than it saves.
Vec3f lo = boundary.front(), hi = boundary.front();
for (const Vec3f &p : boundary) {
lo = lo.cwiseMin(p);
hi = hi.cwiseMax(p);
}
const Vec3f span = (hi - lo).cwiseMax(Vec3f(1e-6f, 1e-6f, 1e-6f));
const int res = std::clamp(int(std::ceil(std::cbrt(double(boundary.size())) * 2.0)), 4, 128);
const Vec3f cell = span / float(res);
const float cell_min = cell.minCoeff();
const auto cell_of = [&](const Vec3f &p) {
Vec3i32 c;
for (int k = 0; k < 3; ++k)
c[k] = std::clamp(int((p[k] - lo[k]) / span[k] * float(res)), 0, res - 1);
return c;
};
const auto cell_index = [&](int x, int y, int z) {
return size_t(z) * size_t(res) * size_t(res) + size_t(y) * size_t(res) + size_t(x);
};
std::vector<std::vector<int>> grid(size_t(res) * size_t(res) * size_t(res));
for (size_t i = 0; i < boundary.size(); ++i) {
const Vec3i32 c = cell_of(boundary[i]);
grid[cell_index(c.x(), c.y(), c.z())].push_back(int(i));
}
const double radius = double(settings.boundary_falloff);
for (size_t id = 0; id < unique_count; ++id) {
const Vec3f &p = id_pos[id];
const Vec3i32 c = cell_of(p);
double best = std::numeric_limits<double>::max();
// Anything in shell r is at least (r - 1) cells away, so once the best found is within
// that bound nothing closer can be hiding further out.
for (int r = 0; r < res; ++r) {
for (int dz = -r; dz <= r; ++dz)
for (int dy = -r; dy <= r; ++dy)
for (int dx = -r; dx <= r; ++dx) {
// The shell only; its interior was covered by a smaller r.
if (r > 0 && std::abs(dx) != r && std::abs(dy) != r && std::abs(dz) != r)
continue;
const int qx = c.x() + dx, qy = c.y() + dy, qz = c.z() + dz;
if (qx < 0 || qy < 0 || qz < 0 || qx >= res || qy >= res || qz >= res)
continue;
for (const int bi : grid[cell_index(qx, qy, qz)])
best = std::min(best, double((boundary[size_t(bi)] - p).norm()));
}
if (best <= double(r) * double(cell_min))
break;
}
falloff[id] = (best == std::numeric_limits<double>::max() || radius <= 0.0)
? 1.0
: std::clamp(best / radius, 0.0, 1.0);
}
}
}
// Pass 2: one sample per unique position. A representative corner is picked first so the sampling
// itself is a flat parallel loop - it is a texture fetch plus projection maths per layer, and by
// far the most expensive thing in this stage.
std::vector<double> grey(unique_count, 0.0);
std::vector<int> representative(unique_count, -1);
for (size_t i = 0; i < count; ++i)
if (representative[size_t(vertex_id[i])] < 0)
representative[size_t(vertex_id[i])] = int(i);
tbb::parallel_for(tbb::blocked_range<size_t>(0, unique_count),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t vid = range.begin(); vid < range.end(); ++vid) {
const int rep = representative[vid];
if (rep < 0)
continue;
grey[vid] = double(sample(geometry.pos[size_t(rep)],
smooth_nrm[vid].cast<float>(),
blend_nrm[vid].cast<float>()));
}
});
// Pass 3: move every copy of a position by the identical vector. Each iteration writes only its
// own output slot, so the loop is independent per corner.
tbb::parallel_for(tbb::blocked_range<size_t>(0, count), [&](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
const Vec3f &p = geometry.pos[i];
const size_t vid = size_t(vertex_id[i]);
// Only angle masking uses the per-position blend, so an excluded face never dims its
// neighbours through a shared vertex.
const bool face_excluded = !user_excluded_face.empty() && user_excluded_face[i / 3] != 0;
// Pinned where an included face shares a position with an excluded one, sealing the boundary.
const bool sealed_boundary =
!face_excluded && !excluded_pos.empty() && excluded_pos[vid] != 0;
const double masked_frac = total_area[vid] > 0.0 ? masked_area[vid] / total_area[vid] : 0.0;
const double centered = settings.symmetric ? (grey[vid] - 0.5) : grey[vid];
const double ramp = falloff.empty() ? 1.0 : falloff[vid];
const double disp = (face_excluded || sealed_boundary)
? 0.0
: ramp * (1.0 - masked_frac) * centered * double(settings.amplitude);
Vec3d moved = p.cast<double>() + smooth_nrm[vid] * disp;
// Stop a partly masked vertex poking through the surface it borders.
if (masked_frac > 0.0) {
if (settings.bottom_angle_limit > 0.f && moved.z() < double(p.z())) moved.z() = double(p.z());
if (settings.top_angle_limit > 0.f && moved.z() > double(p.z())) moved.z() = double(p.z());
}
if (settings.no_downward_z && moved.z() < double(p.z()))
moved.z() = double(p.z());
// A vertex starting on the bottom plane stays there: otherwise a downward-facing face pulls
// *up* where the sample is below mid-grey, leaving bed-contact vertices at differing heights.
if (settings.no_downward_z && double(p.z()) <= double(bounds.min.z()) + 1e-5)
moved.z() = double(p.z());
out.pos[i] = moved.cast<float>();
}
});
// Per-face, not averaged across shared positions: averaging can flip an excluded face's normal
// when its neighbours moved outward.
tbb::parallel_for(tbb::blocked_range<size_t>(0, count / 3), [&](const tbb::blocked_range<size_t> &r) {
for (size_t f = r.begin(); f < r.end(); ++f) {
const size_t t = f * 3;
const Vec3f n = (out.pos[t + 1] - out.pos[t]).cross(out.pos[t + 2] - out.pos[t]).normalized();
out.nrm[t] = out.nrm[t + 1] = out.nrm[t + 2] = n;
}
});
out.exclude_weight = geometry.exclude_weight;
return out;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,68 @@
#pragma once
// Displacement along surface normals.
//
// The mesh is non-indexed, so at a shared edge two triangles hold the same position with different
// face normals; displacing each copy along its own normal sends them to different points and opens a
// crack. So one smooth (area-weighted) normal per unique position drives both the sample lookup and
// the displacement direction, every copy moves by the same vector, and the result is watertight by
// construction. Displaced normals are then smooth at hard edges, but the geometry is still faceted,
// so printed edges stay sharp.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Height at a point, called once per unique welded position. `smooth_normal` is the vector the
// displacement will move along; `blend_normal` is that after smoothing, for projection blend weights.
using HeightSampleFn = std::function<float(const Vec3f &position, const Vec3f &smooth_normal,
const Vec3f &blend_normal)>;
struct DisplaceSettings
{
// Displacement height in mm, applied to the sampled value.
float amplitude = 0.4f;
// Sample around a mid-grey rest level rather than displacing outward only.
bool symmetric = false;
// Faces flatter than these (degrees from horizontal) are held back, leaving bed-contact and top
// surfaces alone. 0 disables that side.
float bottom_angle_limit = 5.f;
float top_angle_limit = 0.f;
// Never move a vertex below its original Z, so no new overhang. The sideways component is kept.
bool no_downward_z = false;
// Distance in mm over which displacement ramps up from a mask boundary. 0 leaves a hard edge.
float boundary_falloff = 0.f;
// Laplacian iterations on the blend normal only - the displacement direction must stay the exact
// smooth normal or copies of a position move differently and the mesh cracks. Inside a blend band
// the weight gradient is largest, so a few degrees of vertex-to-vertex jitter multiplies the
// difference between two unrelated height samples into visible seam noise. A no-op on an
// already-smooth surface.
int blend_normal_smoothing = 32;
};
// Model extents; only the minimum Z is read, for the bottom-plane clamp.
struct DisplaceBounds
{
Vec3f min = Vec3f::Zero();
Vec3f max = Vec3f::Zero();
};
// Returns false to cancel.
using DisplaceProgressFn = std::function<bool(double fraction)>;
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
const DisplaceSettings &settings, const DisplaceBounds &bounds,
const DisplaceProgressFn &on_progress = {});
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,212 @@
#include "TextureBakeFlip.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/parallel_sort.h>
namespace Slic3r {
namespace TextureBake {
FlipResult flip_edges_to_height(const TriSoup &geometry, const std::vector<int> &face_parent_id,
const HeightSampleFn &sample, const FlipSettings &settings,
const std::vector<uint8_t> &locked)
{
FlipResult result;
result.geometry = geometry;
result.face_parent_id = face_parent_id;
const size_t count = geometry.pos.size();
const size_t tri_ct = count / 3;
if (count == 0 || !sample || settings.passes <= 0)
return result;
// Weld, so a flip rewrites triangles in terms of shared vertices rather than positions.
QuantizedPointMap weld(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
std::vector<int> vid(count);
std::vector<Vec3f> pos;
std::vector<float> weight; // exclude weight per unique vertex, the max over its copies
const bool has_weight = !geometry.exclude_weight.empty();
for (size_t i = 0; i < count; ++i) {
vid[i] = weld.get_or_set(geometry.pos[i], int(pos.size()));
if (weld.inserted()) {
pos.push_back(geometry.pos[i]);
weight.push_back(has_weight ? geometry.exclude_weight[i] : 0.f);
} else if (has_weight) {
weight[size_t(vid[i])] = std::max(weight[size_t(vid[i])], geometry.exclude_weight[i]);
}
}
const size_t nv = pos.size();
// Triangles as vertex ids; a locked or excluded triangle never takes part.
std::vector<std::array<int, 3>> tri(tri_ct);
std::vector<uint8_t> fixed(tri_ct, 0);
for (size_t t = 0; t < tri_ct; ++t) {
tri[t] = { vid[t * 3], vid[t * 3 + 1], vid[t * 3 + 2] };
if (!locked.empty() && t < locked.size() && locked[t])
fixed[t] = 1;
if (has_weight && geometry.exclude_weight[t * 3] > 0.99f)
fixed[t] = 1;
}
// Height per unique vertex along its area-weighted normal, the direction displacement will use.
std::vector<Vec3f> nrm(nv, Vec3f::Zero());
std::vector<Vec3f> face_n(tri_ct);
const auto rebuild_normals = [&]() {
std::fill(nrm.begin(), nrm.end(), Vec3f::Zero());
for (size_t t = 0; t < tri_ct; ++t) {
const Vec3f fn = (pos[size_t(tri[t][1])] - pos[size_t(tri[t][0])]).cross(pos[size_t(tri[t][2])] - pos[size_t(tri[t][0])]);
face_n[t] = fn;
for (int k = 0; k < 3; ++k)
nrm[size_t(tri[t][size_t(k)])] += fn;
}
for (Vec3f &n : nrm) {
const float l = n.norm();
n = (l > 0.f) ? Vec3f(n / l) : Vec3f(0.f, 0.f, 1.f);
}
};
rebuild_normals();
std::vector<float> h(nv, 0.f);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
for (size_t v = r.begin(); v < r.end(); ++v)
h[v] = sample(pos[v], nrm[v], nrm[v]);
});
float h_lo = std::numeric_limits<float>::max(), h_hi = -h_lo;
for (const float x : h) { h_lo = std::min(h_lo, x); h_hi = std::max(h_hi, x); }
const float range = h_hi - h_lo;
if (!(range > 0.f))
return result; // flat: every diagonal is as good as the other
const float min_gain = float(settings.min_gain_fraction) * range;
const float planar = float(settings.min_planar_cos);
struct Candidate
{
uint32_t t1, t2; // the two triangles
uint8_t k1, k2; // corner index in each where the shared edge starts (t1: a->c, t2: c->a)
float gain;
};
for (int pass = 0; pass < settings.passes; ++pass) {
// Half-edges keyed by their undirected edge, sorted so the two halves of an interior edge land
// next to each other; a run of exactly two with opposite directions is a manifold interior
// edge. Sorting beats hashing here by an order of magnitude on a few million triangles.
struct Half { uint64_t key; uint32_t corner; };
std::vector<Half> half;
half.reserve(count);
for (size_t t = 0; t < tri_ct; ++t)
for (int k = 0; k < 3; ++k) {
const int from = tri[t][size_t(k)], to = tri[t][size_t((k + 1) % 3)];
if (from == to) continue;
const uint32_t lo = uint32_t(std::min(from, to)), hi = uint32_t(std::max(from, to));
half.push_back({ (uint64_t(lo) << 32) | hi, uint32_t(t * 3 + size_t(k)) });
}
tbb::parallel_sort(half.begin(), half.end(), [](const Half &x, const Half &y) {
return x.key != y.key ? x.key < y.key : x.corner < y.corner;
});
std::vector<std::pair<uint32_t, uint32_t>> edges; // (corner in t1, corner in t2), t1 < t2
edges.reserve(half.size() / 2);
for (size_t i = 0; i < half.size();) {
size_t j = i + 1;
while (j < half.size() && half[j].key == half[i].key) ++j;
if (j - i == 2) {
// Opposite directions: the lower vertex id is `from` in exactly one of the two.
const uint32_t c1 = half[i].corner, c2 = half[i + 1].corner;
const int f1 = tri[c1 / 3][size_t(c1 % 3)], f2 = tri[c2 / 3][size_t(c2 % 3)];
if (f1 != f2)
edges.emplace_back(c1, c2);
}
i = j;
}
std::vector<Candidate> cands(edges.size());
std::vector<uint8_t> valid(edges.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, edges.size()), [&](const tbb::blocked_range<size_t> &r) {
for (size_t i = r.begin(); i < r.end(); ++i) {
const uint32_t c1 = uint32_t(edges[i].first), c2 = uint32_t(edges[i].second);
const uint32_t t1 = c1 / 3, t2 = c2 / 3;
const int k1 = int(c1 % 3), k2 = int(c2 % 3);
if (fixed[t1] || fixed[t2]) continue;
if (!face_parent_id.empty() && face_parent_id[t1] != face_parent_id[t2]) continue;
// a->c is the shared edge in t1, with b opposite; t2 runs c->a with d opposite.
const int a = tri[t1][size_t(k1)], c = tri[t1][size_t((k1 + 1) % 3)], b = tri[t1][size_t((k1 + 2) % 3)];
const int d = tri[t2][size_t((k2 + 2) % 3)];
if (b == d) continue;
// Level quads have nothing to gain; the test is on the corners, before any sampling.
const float hmin = std::min({ h[size_t(a)], h[size_t(b)], h[size_t(c)], h[size_t(d)] });
const float hmax = std::max({ h[size_t(a)], h[size_t(b)], h[size_t(c)], h[size_t(d)] });
if (hmax - hmin < min_gain) continue;
// Coplanar enough to have a real alternative, and convex so the alternative is valid:
// the new triangles (a, d, b) and (d, c, b) must both face the way the quad does, with
// a decent share of its area.
const Vec3f n1 = face_n[t1], n2 = face_n[t2];
const float l1 = n1.norm(), l2 = n2.norm();
if (l1 <= 0.f || l2 <= 0.f || n1.dot(n2) < planar * l1 * l2) continue;
const Vec3f quad_n = (n1 + n2).normalized();
const Vec3f &pa = pos[size_t(a)], &pb = pos[size_t(b)], &pc = pos[size_t(c)], &pd = pos[size_t(d)];
const Vec3f m1 = (pd - pa).cross(pb - pa), m2 = (pc - pd).cross(pb - pd);
const float area_old = l1 + l2, area_new = m1.dot(quad_n) + m2.dot(quad_n);
const float min_part = 0.05f * area_old;
if (m1.dot(quad_n) < min_part || m2.dot(quad_n) < min_part) continue;
if (std::abs(area_new - area_old) > 0.02f * area_old) continue; // not the same quad: folded
// The diagonals' midpoint errors.
const auto mid_err = [&](int u, int w) {
const Vec3f p = 0.5f * (pos[size_t(u)] + pos[size_t(w)]);
Vec3f n = nrm[size_t(u)] + nrm[size_t(w)];
const float l = n.norm();
n = (l > 0.f) ? Vec3f(n / l) : quad_n;
return std::abs(sample(p, n, n) - 0.5f * (h[size_t(u)] + h[size_t(w)]));
};
const float gain = mid_err(a, c) - mid_err(b, d);
if (gain < min_gain) continue;
cands[i] = { t1, t2, uint8_t(k1), uint8_t(k2), gain };
valid[i] = 1;
}
});
std::vector<Candidate> chosen;
for (size_t i = 0; i < cands.size(); ++i)
if (valid[i]) chosen.push_back(cands[i]);
std::sort(chosen.begin(), chosen.end(), [](const Candidate &x, const Candidate &y) {
return x.gain != y.gain ? x.gain > y.gain : (x.t1 != y.t1 ? x.t1 < y.t1 : x.t2 < y.t2);
});
// Best first, and a triangle changes at most once per pass.
std::vector<uint8_t> touched(tri_ct, 0);
size_t applied = 0;
for (const Candidate &cd : chosen) {
if (touched[cd.t1] || touched[cd.t2]) continue;
const int a = tri[cd.t1][cd.k1], c = tri[cd.t1][size_t((cd.k1 + 1) % 3)], b = tri[cd.t1][size_t((cd.k1 + 2) % 3)];
const int d = tri[cd.t2][size_t((cd.k2 + 2) % 3)];
tri[cd.t1] = { a, d, b };
tri[cd.t2] = { d, c, b };
touched[cd.t1] = touched[cd.t2] = 1;
++applied;
}
result.flipped += applied;
if (applied == 0)
break;
rebuild_normals(); // face normals feed the planarity test of the next pass
}
if (result.flipped == 0)
return result;
// Back to the soup: positions, weights and per-face normals from the (possibly rewritten) triangles.
for (size_t t = 0; t < tri_ct; ++t) {
for (int k = 0; k < 3; ++k) {
const size_t i = t * 3 + size_t(k);
const int v = tri[t][size_t(k)];
result.geometry.pos[i] = pos[size_t(v)];
if (has_weight)
result.geometry.exclude_weight[i] = weight[size_t(v)];
}
Vec3f n = (result.geometry.pos[t * 3 + 1] - result.geometry.pos[t * 3]).cross(result.geometry.pos[t * 3 + 2] - result.geometry.pos[t * 3]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
result.geometry.nrm[t * 3] = result.geometry.nrm[t * 3 + 1] = result.geometry.nrm[t * 3 + 2] = n;
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,57 @@
#pragma once
// Data-dependent edge flipping: choose each quad's diagonal to follow the height field, before any
// displacement happens.
//
// Refinement produces a regular grid, and a step in the height map that crosses that grid at an
// angle lands on alternating corners: one triangle of a quad gets a raised corner, the next does not,
// and the displaced wall comes out as a sawtooth the size of the grid. Finer triangles make the teeth
// smaller, never straight. The cause is the diagonal, not the density: with the diagonal running
// along the step both triangles of the quad sit cleanly on one side or the other, and the wall is a
// straight line between them.
//
// So, for every interior edge, compare the two diagonals of the quad it spans by how well each
// interpolates the field at its own midpoint - the sampled height there against the mean of its two
// endpoints - and keep the better one. A quad whose four corners are level is skipped outright, so
// flat regions cost nothing; a non-planar quad (a model crease) is never touched, nor is one whose
// triangles belong to different source faces or straddle the painted boundary.
#include <cstdint>
#include <vector>
#include "TextureBakeDisplace.hpp"
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct FlipSettings
{
// Passes over all edges. Flips interact through shared triangles, so a pass applies non-conflicting
// ones and the next pass picks up the rest; two or three settle a grid.
int passes = 3;
// A flip has to reduce the midpoint error by at least this fraction of the height range, so noise
// on a rough surface does not toggle diagonals for nothing.
double min_gain_fraction = 0.02;
// The two triangles have to be this coplanar (cosine of their normals' angle) for the quad to have
// a meaningful alternative diagonal at all: across a real crease there is none.
double min_planar_cos = 0.985; // ~10 degrees
};
struct FlipResult
{
TriSoup geometry;
std::vector<int> face_parent_id;
size_t flipped = 0;
};
// `face_parent_id` may be empty; when given it is carried through unchanged (a flip never crosses a
// parent boundary). `locked` flags triangles that must not change (per triangle, may be empty).
FlipResult flip_edges_to_height(const TriSoup &geometry, const std::vector<int> &face_parent_id,
const HeightSampleFn &sample, const FlipSettings &settings,
const std::vector<uint8_t> &locked);
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,25 @@
#include "TextureBakeIndex.hpp"
#include <algorithm>
namespace Slic3r {
namespace TextureBake {
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant)
{
WeldResult out;
QuantizedPointMap map(quant, std::min<size_t>(positions.size(), size_t(1) << 22));
out.vertex_id.resize(positions.size());
int next_id = 0;
for (size_t i = 0; i < positions.size(); ++i) {
const int id = map.get_or_set(positions[i], next_id);
if (map.inserted())
++next_id;
out.vertex_id[i] = id;
}
out.unique_count = next_id;
return out;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,158 @@
#pragma once
// Vertex welding for the texture bake pipeline. The pipeline works on non-indexed triangle soup, so
// a shared point exists once per incident triangle with float noise between the copies; welding maps
// each quantised position to one integer id.
//
// The three grids below are deliberately not unified - changing one at a call site changes
// watertightness. 100 um matches the precision files are written with; 10 um keeps small fillet
// vertices distinct (they merge at 100 um, giving needle artifacts after displacement) while still
// absorbing float noise; 1 um is what collapse positioning needs.
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <vector>
#include "../Point.hpp"
namespace Slic3r {
namespace TextureBake {
static constexpr double WELD_GRID_EXPORT = 1e4; // 100 um
static constexpr double WELD_GRID_GEOMETRY = 1e5; // 10 um
static constexpr double WELD_GRID_DECIMATION = 1e6; // 1 um
// Round half toward positive infinity. Quantised coordinates hit exact halves often enough that the
// tie rule matters.
inline int64_t grid_round(double v) { return int64_t(std::floor(v + 0.5)); }
// Open-addressing table, linear probing: no allocation per lookup, exact integer key comparison.
// Values must be non-negative; -1 is the empty sentinel and what get() returns on a miss.
//
// Key and value live together in one 32-byte cell. They used to be four parallel arrays, which made a
// single probe touch four cache lines - and probing this table was 13% of a whole bake, because every
// stage welds the full soup through it.
class QuantizedPointMap
{
public:
explicit QuantizedPointMap(double quant, size_t expected = 256) : m_quant(quant)
{
size_t cap = 16;
const size_t target = std::max<size_t>(16, size_t(std::ceil(double(expected) / 0.6)));
while (cap < target)
cap *= 2;
alloc(cap);
}
size_t size() const { return m_size; }
// Whether the last get_or_set() inserted rather than found.
bool inserted() const { return m_inserted; }
int get(float x, float y, float z)
{
return m_cells[slot(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant),
grid_round(double(z) * m_quant))].val;
}
int get(const Vec3f &p) { return get(p.x(), p.y(), p.z()); }
// The value already stored for this position's grid cell; if there is none, store `value` and
// return it. inserted() then says which of the two happened.
int get_or_set(float x, float y, float z, int value)
{
return get_or_set_key(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant),
grid_round(double(z) * m_quant), value);
}
int get_or_set(const Vec3f &p, int value) { return get_or_set(p.x(), p.y(), p.z(), value); }
// The same table as a set of integer tuples (edge marking, midpoint cache). Quantisation is
// bypassed: routing ids through the float overloads loses precision above 2^24.
int get_key(int64_t a, int64_t b, int64_t c) { return m_cells[slot(a, b, c)].val; }
int get_or_set_key(int64_t a, int64_t b, int64_t c, int value)
{
const size_t i = slot(a, b, c);
Cell &cell = m_cells[i];
if (cell.val != -1) {
m_inserted = false;
return cell.val;
}
cell.qx = a; cell.qy = b; cell.qz = c;
cell.val = value;
m_inserted = true;
if (++m_size > size_t(double(m_cap) * 0.7))
grow();
return value;
}
private:
struct Cell
{
int64_t qx = 0, qy = 0, qz = 0;
int32_t val = -1;
};
void alloc(size_t cap)
{
m_cap = cap;
m_mask = cap - 1;
m_cells.assign(cap, Cell{});
}
size_t slot(int64_t qx, int64_t qy, int64_t qz) const
{
// Unsigned multiplies: the signed versions overflowed on nearly every key, which is undefined
// behaviour. The resulting bits are identical on every target OrcaSlicer builds for.
//
// A stronger 64-bit finalizer was tried and measured no faster - the probing that shows up in a
// profile is subdivide's parallel mark count, spread over every core, not long probe chains.
uint32_t h = (uint32_t(qx) * 0x9E3779B1u) ^ (uint32_t(qy) * 0x85EBCA77u) ^ (uint32_t(qz) * 0xC2B2AE3Du);
h ^= h >> 15;
size_t i = size_t(h) & m_mask;
// Equality is checked against the stored 64-bit keys, so truncating to 32 bits for the hash
// costs collisions at worst, never a wrong answer.
while (m_cells[i].val != -1) {
const Cell &c = m_cells[i];
if (c.qx == qx && c.qy == qy && c.qz == qz)
return i;
i = (i + 1) & m_mask;
}
return i;
}
void grow()
{
std::vector<Cell> old = std::move(m_cells);
alloc(m_cap * 2);
for (const Cell &c : old)
if (c.val != -1)
m_cells[slot(c.qx, c.qy, c.qz)] = c;
}
double m_quant;
size_t m_cap = 0, m_mask = 0, m_size = 0;
bool m_inserted = false;
std::vector<Cell> m_cells;
};
// Three consecutive entries per triangle. The indexers turn this into shared vertices where a stage
// needs adjacency.
struct TriSoup
{
std::vector<Vec3f> pos;
std::vector<Vec3f> nrm; // parallel to pos
std::vector<float> exclude_weight; // parallel to pos; empty when nothing is excluded
size_t triangle_count() const { return pos.size() / 3; }
bool empty() const { return pos.empty(); }
};
// Assign each vertex the sequential id of its quantised position, first occurrence winning.
struct WeldResult
{
std::vector<int> vertex_id;
int unique_count = 0;
};
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant);
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,62 @@
#include "TextureBakeMesh.hpp"
#include <algorithm>
namespace Slic3r {
namespace TextureBake {
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded)
{
TriSoup out;
const size_t n = its.indices.size();
out.pos.resize(n * 3);
out.nrm.resize(n * 3);
const bool have_excl = face_excluded.size() == n;
if (have_excl)
out.exclude_weight.resize(n * 3);
for (size_t t = 0; t < n; ++t) {
const stl_triangle_vertex_indices &tri = its.indices[t];
const Vec3f a = its.vertices[size_t(tri[0])];
const Vec3f b = its.vertices[size_t(tri[1])];
const Vec3f c = its.vertices[size_t(tri[2])];
Vec3f nrm = (b - a).cross(c - a);
const float len = nrm.norm();
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
out.pos[t * 3] = a;
out.pos[t * 3 + 1] = b;
out.pos[t * 3 + 2] = c;
// Per-face on purpose: the accurate indexer derives smooth normals and splits at sharp edges
// itself, so averaged ones would pre-empt that.
out.nrm[t * 3] = out.nrm[t * 3 + 1] = out.nrm[t * 3 + 2] = nrm;
if (have_excl) {
const float w = face_excluded[t] ? 1.f : 0.f;
out.exclude_weight[t * 3] = out.exclude_weight[t * 3 + 1] = out.exclude_weight[t * 3 + 2] = w;
}
}
return out;
}
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
{
indexed_triangle_set out;
const size_t n = soup.pos.size();
out.indices.reserve(n / 3);
QuantizedPointMap map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
for (size_t i = 0; i < n; ++i) {
id[i] = map.get_or_set(soup.pos[i], int(out.vertices.size()));
if (map.inserted())
out.vertices.push_back(soup.pos[i]);
}
for (size_t t = 0; t + 2 < n; t += 3) {
// Welded-together corners carry no area.
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
continue;
out.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
}
return out;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,19 @@
#pragma once
// Conversion between the pipeline's triangle soup and the indexed mesh used elsewhere. The pipeline
// stays on soup because each stage welds on its own grid, and those differences are load-bearing.
#include "TextureBakeIndex.hpp"
#include "../TriangleMesh.hpp"
namespace Slic3r {
namespace TextureBake {
// `face_excluded`: one entry per input triangle, becoming the soup's per-corner exclusion weight.
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded = {});
// Welds at the geometry grid.
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup);
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,375 @@
#include "TextureBakePipeline.hpp"
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include "TextureBakeDebug.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <string>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace TextureBake {
void clamp_below_bottom(TriSoup &geometry, float bottom_z)
{
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
bool dirty = false;
for (int k = 0; k < 3; ++k)
if (geometry.pos[t + size_t(k)].z() < bottom_z) {
geometry.pos[t + size_t(k)].z() = bottom_z;
dirty = true;
}
if (!dirty)
continue;
Vec3f n = (geometry.pos[t + 1] - geometry.pos[t]).cross(geometry.pos[t + 2] - geometry.pos[t]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
geometry.nrm[t] = geometry.nrm[t + 1] = geometry.nrm[t + 2] = n;
}
}
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol)
{
const size_t vert_count = geometry.pos.size();
const size_t tri_count = vert_count / 3;
if (tri_count == 0 || tol <= 0.0)
return 0;
// Weld at the finest grid: by this point copies of one position are bit-identical, because every
// earlier stage moved them by the same vector.
QuantizedPointMap weld(WELD_GRID_DECIMATION, std::min(vert_count, size_t(1) << 22));
std::vector<int> vid(vert_count);
int unique = 0;
for (size_t i = 0; i < vert_count; ++i) {
vid[i] = weld.get_or_set(geometry.pos[i], unique);
if (weld.inserted())
++unique;
}
// Incident corners per position, CSR style.
std::vector<uint32_t> start(size_t(unique) + 1, 0);
for (size_t i = 0; i < vert_count; ++i)
++start[size_t(vid[i]) + 1];
for (size_t id = 0; id < size_t(unique); ++id)
start[id + 1] += start[id];
std::vector<uint32_t> inc(vert_count), cursor(size_t(unique), 0);
for (size_t i = 0; i < vert_count; ++i)
inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i);
const double fold_cos = std::cos(75.0 * M_PI / 180.0);
std::vector<uint8_t> dirty_tri(tri_count, 0);
for (size_t id = 0; id < size_t(unique); ++id) {
const float z = geometry.pos[inc[start[id]]].z();
if (z == bottom_z || std::abs(double(z) - double(bottom_z)) > tol)
continue;
// Simulate the move: every incident triangle must keep positive area and must not fold.
bool ok = true;
for (uint32_t k = start[id]; k < start[id + 1] && ok; ++k) {
const size_t t = size_t(inc[k]) / 3;
Vec3f p[3];
for (int v = 0; v < 3; ++v) {
p[v] = geometry.pos[t * 3 + size_t(v)];
if (vid[t * 3 + size_t(v)] == int(id))
p[v].z() = bottom_z;
}
const Vec3d on = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]).cast<double>();
const Vec3d nn = (p[1] - p[0]).cross(p[2] - p[0]).cast<double>();
const double o2 = on.squaredNorm(), n2 = nn.squaredNorm();
if (n2 < 1e-20) { ok = false; break; } // would collapse to zero area
if (o2 < 1e-20) continue; // already degenerate, cannot judge a rotation
const double dot = on.dot(nn);
if (dot < 0.0 || dot * dot < fold_cos * fold_cos * o2 * n2)
ok = false;
}
if (!ok)
continue;
for (uint32_t k = start[id]; k < start[id + 1]; ++k) {
geometry.pos[inc[k]].z() = bottom_z;
dirty_tri[size_t(inc[k]) / 3] = 1;
}
}
size_t dirty = 0;
for (size_t t = 0; t < tri_count; ++t) {
if (!dirty_tri[t])
continue;
++dirty;
Vec3f n = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
geometry.nrm[t * 3] = geometry.nrm[t * 3 + 1] = geometry.nrm[t * 3 + 2] = n;
}
return dirty;
}
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
const PipelineSettings &settings, const DisplaceBounds &bounds,
PipelineMode mode, const std::vector<uint8_t> &face_excluded,
const PipelineProgressFn &on_progress, BakeStageRecorder *debug,
const ColorSampleFn &color_sample)
{
PipelineResult result;
const auto report = [&](const char *stage, double f) {
return !on_progress || on_progress(stage, f);
};
// Per-stage wall time. The stages differ in cost by orders of magnitude depending on the model, so
// without this it is guesswork which one to attack.
auto clock_now = [] { return std::chrono::steady_clock::now(); };
auto t_stage = clock_now();
// One call site for both the log line and the debug capture, so a stage cannot appear in one and
// be missing from the other. The capture happens after the elapsed time is read: welding the soup
// and scanning its edges costs more than some of the stages do, and must not land inside the
// measurement it is reporting.
const auto lap = [&](const char *stage, const TriSoup &geometry, const std::string &detail = {}) {
const double ms = std::chrono::duration<double, std::milli>(clock_now() - t_stage).count();
BOOST_LOG_TRIVIAL(info) << "TextureBake " << stage << ": " << ms << " ms, "
<< geometry.triangle_count() << " tris";
if (debug != nullptr)
debug->capture(stage, geometry, ms, detail);
t_stage = clock_now();
};
if (input.empty() || !sample) {
result.geometry = input;
return result;
}
if (debug != nullptr)
debug->capture("input", input, 0.0, "as handed to the pipeline");
t_stage = clock_now(); // the capture above is not part of the first stage
// 1. Refine to the target edge length.
SubdivideResult sub = subdivide(
input, settings.refine_length, face_excluded, /* fast */ false, settings.safety_cap,
[&](double f, size_t, double) { return report("subdivide", f); }, settings.paint_within);
result.safety_cap_hit = sub.safety_cap_hit;
lap("subdivide", sub.geometry);
if (!report("subdivide", 1.0)) {
result.canceled = true;
return result;
}
// 2. Dissolve the slivers refinement inherited, then recover the edges that lengthened.
if (settings.regularize) {
RegularizeOptions ropts = settings.regularize_opts;
ropts.preserve_excluded = settings.preserve_untextured;
RegularizeResult reg = regularize_mesh(sub.geometry, sub.face_parent_id,
settings.refine_length, ropts);
result.collapse_count = reg.collapse_count;
lap("regularize", reg.geometry, std::to_string(reg.collapse_count) + " collapses");
if (!report("regularize", 1.0)) {
result.canceled = true;
return result;
}
if (reg.collapse_count > 0) {
// Excluded faces are carried on the soup itself, so the flag is re-derived rather than
// indexed across the collapse.
std::vector<uint8_t> excl;
if (!reg.geometry.exclude_weight.empty()) {
excl.assign(reg.geometry.triangle_count(), 0);
for (size_t t = 0; t < excl.size(); ++t)
excl[t] = reg.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
sub = subdivide(reg.geometry, settings.refine_length * settings.regularize_second_pass_mul,
excl, false, settings.safety_cap,
[&](double f, size_t, double) { return report("re-subdivide", f); },
settings.paint_within);
result.safety_cap_hit = result.safety_cap_hit || sub.safety_cap_hit;
// The second pass renumbers faces, so the parent map has to be composed through it.
std::vector<int> composed(sub.face_parent_id.size());
for (size_t i = 0; i < composed.size(); ++i) {
const int mid = sub.face_parent_id[i];
composed[i] = (mid >= 0 && size_t(mid) < reg.face_parent_id.size())
? reg.face_parent_id[size_t(mid)] : -1;
}
sub.face_parent_id = std::move(composed);
lap("re-subdivide", sub.geometry);
} else {
sub.geometry = std::move(reg.geometry);
sub.face_parent_id = std::move(reg.face_parent_id);
}
}
// 2b. Paint finer than the input triangles. The caller includes a source triangle when any part of
// it is painted; now that the faces are small, ask once more per face and switch the unpainted
// ones off. They are pinned like the excluded region from here on: their own corners at weight 1,
// and the displacement's boundary sealing pins the stroke's rim on the painted side.
if (settings.painted) {
const size_t nf = sub.geometry.triangle_count();
const bool have_w = !sub.geometry.exclude_weight.empty();
std::vector<uint8_t> unpainted(nf, 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
if (have_w && sub.geometry.exclude_weight[t * 3] > 0.99f)
continue; // excluded from the start, never asked
const Vec3f &a = sub.geometry.pos[t * 3], &b = sub.geometry.pos[t * 3 + 1], &c = sub.geometry.pos[t * 3 + 2];
if (!settings.painted((a + b + c) / 3.f))
unpainted[t] = 1;
}
});
size_t switched = 0;
for (size_t t = 0; t < nf; ++t)
switched += unpainted[t];
if (switched > 0) {
if (sub.geometry.exclude_weight.empty())
sub.geometry.exclude_weight.assign(sub.geometry.pos.size(), 0.f);
for (size_t t = 0; t < nf; ++t)
if (unpainted[t])
sub.geometry.exclude_weight[t * 3] = sub.geometry.exclude_weight[t * 3 + 1] =
sub.geometry.exclude_weight[t * 3 + 2] = 1.f;
}
lap("paint", sub.geometry, std::to_string(switched) + " faces switched off");
if (!report("paint", 1.0)) {
result.canceled = true;
return result;
}
}
// 3. Align the mesh to the height field's edges, then displace.
if (settings.relocate) {
std::vector<uint8_t> locked;
if (settings.preserve_untextured && !sub.geometry.exclude_weight.empty()) {
locked.assign(sub.geometry.triangle_count(), 0);
for (size_t t = 0; t < locked.size(); ++t)
locked[t] = sub.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
RelocateResult rel = relocate_to_contours(sub.geometry, sample, settings.relocate_opts, locked);
BOOST_LOG_TRIVIAL(info) << "TextureBake relocate: moved=" << rel.moved
<< " rejected=" << rel.rejected;
sub.geometry = std::move(rel.geometry);
lap("relocate", sub.geometry,
"moved " + std::to_string(rel.moved) + ", rejected " + std::to_string(rel.rejected));
}
// 3b. Diagonals along the height field's steps, so they displace into straight walls.
if (settings.flip_edges) {
std::vector<uint8_t> locked;
if (settings.preserve_untextured && !sub.geometry.exclude_weight.empty()) {
locked.assign(sub.geometry.triangle_count(), 0);
for (size_t t = 0; t < locked.size(); ++t)
locked[t] = sub.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
FlipResult fl = flip_edges_to_height(sub.geometry, sub.face_parent_id, sample, settings.flip_opts, locked);
sub.geometry = std::move(fl.geometry);
sub.face_parent_id = std::move(fl.face_parent_id);
lap("align edges", sub.geometry, std::to_string(fl.flipped) + " flips");
if (!report("align edges", 1.0)) {
result.canceled = true;
return result;
}
}
TriSoup displaced = apply_displacement(sub.geometry, sample, settings.displace, bounds,
[&](double f) { return report("displace", f); });
lap("displace", displaced);
if (!report("displace", 1.0)) {
result.canceled = true;
return result;
}
// 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys.
std::vector<int> parent = std::move(sub.face_parent_id);
const size_t displaced_before_decimate = displaced.triangle_count();
if (mode == PipelineMode::Export) {
std::vector<uint8_t> locked;
size_t preserved = 0;
{
if (settings.preserve_untextured && !displaced.exclude_weight.empty()) {
locked.assign(displaced.triangle_count(), 0);
// The corner average, as the displacement stage judges it: after the flip stage's
// per-vertex merge an included face touching the excluded region carries one corner
// at weight 1, and must stay free to collapse and to take colour.
for (size_t t = 0; t < locked.size(); ++t)
locked[t] = (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f ? 1 : 0;
// That includes the faces the paint test switched off: the harvest would re-triangulate
// them into long slivers, which a later bake painted there would refine instead of the
// grid the graded refinement left.
preserved = size_t(std::count(locked.begin(), locked.end(), uint8_t(1)));
}
}
// The budget is what this bake may spend on what it refines. Geometry it only preserves - the
// unpainted surface, and on it the relief of an earlier bake - is counted on top of it: charged
// against the same budget, a second bake over a fresh area had to evict the first one's
// triangles to fit, so every bake after the first came out coarser than the one before.
const size_t target = settings.max_triangles + preserved;
const bool over_budget = displaced.triangle_count() > target;
// Flat faces are harvested whether or not the budget bites. Refinement is driven by the target
// edge length alone, so it leaves as fine a mesh over the flat parts of a texture as over its
// detail, and nothing else removes those: under its budget a bake kept every redundant triangle
// unless the budget was lowered until decimation had to run. Only collapses costing less than
// harvest_tol are taken, so this does not reach the relief.
const bool harvest_only = !over_budget && settings.harvest_flat && displaced.triangle_count() > 0;
if (over_budget || harvest_only) {
// Colour per face on the fine mesh, so colour boundaries become creases the collapse
// respects. Excluded (unpainted) faces take no colour.
std::vector<int> face_color;
if (color_sample) {
const size_t nf = displaced.triangle_count();
face_color.assign(nf, -1);
const bool have_w = !displaced.exclude_weight.empty();
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
continue;
const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
}
});
}
// Harvesting alone is asked for by handing it the count it already has: nothing is then
// over the target, so the loop only ever pops collapses under the tolerance.
const size_t before = displaced.triangle_count();
DecimateResult dec = decimate(displaced, over_budget ? target : before, settings.harvest_flat,
settings.harvest_tol, locked,
[&](double f) { return report("decimate", f); }, face_color);
result.locked_over_budget = dec.locked_over_budget;
result.budget_limited = result.simplified = dec.target_cost_detail;
displaced = std::move(dec.geometry);
lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested");
BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count()
<< (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")";
parent.clear(); // no longer meaningful
}
result.triangles_refined = displaced_before_decimate;
result.triangles_budget = target;
if (!report("decimate", 1.0)) {
result.canceled = true;
return result;
}
}
// 5. Flatten the bed-contact surface.
{
const bool clamped = settings.clamp_below_plate || settings.displace.bottom_angle_limit > 0.f;
if (clamped)
clamp_below_bottom(displaced, bounds.min.z());
size_t snapped = 0;
if (settings.bottom_snap_tol > 0.0)
snapped = snap_bottom_to_flat(displaced, bounds.min.z(), settings.bottom_snap_tol);
if (clamped || settings.bottom_snap_tol > 0.0)
lap("bottom clamp + snap", displaced, std::to_string(snapped) + " triangles snapped flat");
}
// 6. Close the T-junctions decimation left behind. Only meaningful when it ran.
if (mode == PipelineMode::Export && parent.empty()) {
displaced = resolve_t_junctions(displaced);
lap("repair", displaced);
}
result.geometry = std::move(displaced);
result.face_parent_id = std::move(parent);
return result;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,156 @@
#pragma once
// The bake pipeline:
//
// subdivide -> [regularize -> re-subdivide] -> [paint test] -> [relocate] -> [flip edges]
// -> displace -> [decimate] -> bottom clamp -> bottom snap -> [resolve T-junctions]
//
// Regularization sits between two subdivisions on purpose: it dissolves the slivers refinement
// inherited, which lengthens some edges past the target, and the second pass brings those back.
// Before any subdivision it would have nothing to work on, since the slivers come from refining a
// needle; after a single pass it would leave the mesh coarser than asked for.
//
// Decimation and repair are export-only - decimation drops the output-to-input face mapping a bake
// needs to carry per-face data forward.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeDecimate.hpp"
#include "TextureBakeDisplace.hpp"
#include "TextureBakeIndex.hpp"
#include "TextureBakeRegularize.hpp"
#include "TextureBakeFlip.hpp"
#include "TextureBakeRelocate.hpp"
#include "TextureBakeRepair.hpp"
#include "TextureBakeSubdivide.hpp"
namespace Slic3r {
// Optional step-by-step capture; see TextureBakeDebug.hpp. A pointer, and forward declared, so the
// pipeline header stays free of the mesh types the recorder converts into.
class BakeStageRecorder;
namespace TextureBake {
enum class PipelineMode
{
// Keeps the face-parent mapping; skips decimation and repair.
Bake,
// The full sequence, including decimation and repair.
Export,
};
struct PipelineSettings
{
// Target edge length for the refinement, in mm.
double refine_length = 1.0;
// Sliver removal between the two subdivision passes.
bool regularize = true;
RegularizeOptions regularize_opts;
// Slightly above the first pass, so it recovers the edges regularization lengthened instead of
// re-refining what it just merged.
double regularize_second_pass_mul = 1.1;
// Slide vertices onto the height map's own edges before displacing, so a step lands on a mesh
// edge instead of being quantised to wherever the grid fell.
bool relocate = false;
RelocateSettings relocate_opts;
// Choose each quad's diagonal to follow the height field before displacing, so a step that crosses
// the grid at an angle comes out as a straight wall instead of a sawtooth. See TextureBakeFlip.hpp.
bool flip_edges = true;
FlipSettings flip_opts;
DisplaceSettings displace;
// Optional. Asked once per refined face (its centroid, in the soup's coordinates) after the
// refinement stages and before displacement, for faces whose source triangle was included:
// false marks the face as unpainted (no displacement), so paint finer than the input triangles
// is honoured. Faces excluded from the start are never asked. Called from several threads at
// once, so it must be safe to call concurrently.
std::function<bool(const Vec3f &centroid)> painted;
// Optional. Whether the paint comes within a radius of a point, which grades the refinement (see
// subdivide()): only the paint and its surroundings reach refine_length, and the unpainted surface
// further out stays in whole pieces of its own grid. That is what a later bake painted there
// refines, so it meets the same grid a first bake does. Called from several threads at once.
SubdivideWithinFn paint_within;
// Export mode only.
// What this bake may spend on what it refines. Geometry it only preserves (see preserve_untextured),
// the unpainted surface and an earlier bake's relief on it, is counted on top of it, so that relief
// does not have to be evicted to fit this one.
size_t max_triangles = 750'000;
// Keep removing zero-cost flat faces past the target. Only applies when decimation runs, i.e. when
// the displaced mesh is over the budget - an under-budget mesh is never decimated.
bool harvest_flat = true;
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL;
// Lock the untextured region against both regularization and decimation.
bool preserve_untextured = true;
// Push anything that displaced below the plate back up to it. Downward movement is otherwise left
// alone, so relief on the underside is kept - only what would sink through the plate is stopped.
bool clamp_below_plate = false;
// Snap vertices within this of the bottom plane onto it. 0 disables.
double bottom_snap_tol = 0.1;
int safety_cap = SUBDIVIDE_SAFETY_CAP;
};
// Stage name and a fraction within it. Returning false cancels the run.
using PipelineProgressFn = std::function<bool(const char *stage, double fraction)>;
// Colour class of a point of the surface (a palette index, -1 for none), for the decimation's
// colour-boundary creases. Only consulted when the mesh is over budget.
using ColorSampleFn = std::function<int(const Vec3f &centroid, const Vec3f &normal)>;
struct PipelineResult
{
TriSoup geometry;
// Output face -> input face. Empty in Export mode, where decimation invalidates it.
std::vector<int> face_parent_id;
bool safety_cap_hit = false;
bool locked_over_budget = false;
size_t collapse_count = 0;
bool canceled = false;
// What the refinement produced, before decimation, and the count it had to fit into (the budget
// plus the preserved geometry). budget_limited says the refined mesh did not fit without collapsing
// more than flat faces: the result carries less of the texture than the resolution asked for,
// which is what a caller warns about.
size_t triangles_refined = 0;
size_t triangles_budget = 0;
bool budget_limited = false;
// The budget coarsened the mesh, so its triangles are no longer the fine, even ones the refinement
// laid down - they can be as large as the features on them, which a caller that post-processes per
// triangle (the colour despeckle) has to know. Harvesting flat faces does not count: what it
// removes carried nothing. Not the same question as "did a collapse run", which is why
// face_parent_id being empty is no longer used to answer it.
bool simplified = false;
};
// `debug`, when given and enabled, receives the mesh after every stage that ran - which is the only
// way to tell which stage a bad result came from, since each one rewrites the whole mesh.
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
const PipelineSettings &settings, const DisplaceBounds &bounds,
PipelineMode mode, const std::vector<uint8_t> &face_excluded = {},
const PipelineProgressFn &on_progress = {},
BakeStageRecorder *debug = nullptr, const ColorSampleFn &color_sample = {});
// Snap anything that ended below the model's original bottom back up to it.
void clamp_below_bottom(TriSoup &geometry, float bottom_z);
// Flatten the bed-contact surface by snapping positions within `tol` of the bottom plane onto it.
//
// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture relief
// near the base, folding them coplanar into the bottom face. Folded faces overlap the plate, so edges
// there pick up four incident faces - non-manifold edges and phantom shells on re-import. All copies
// of a position move together, and the move is rejected if any incident triangle would go degenerate
// or rotate more than about 75 degrees. A real bed-contact sliver rotates by a fraction of a degree
// and still snaps. Returns how many triangles moved.
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol = 0.1);
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,393 @@
#include "TextureBakeRegularize.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
namespace {
// Vertex-to-triangle lists as intrusive doubly linked lists of corner slots over flat arrays. Slot s
// is corner (triangle * 3 + k), owned by corners[s]. Deleted and moved corners are unlinked, so a
// collapse costs no allocation.
struct SlotLists
{
std::vector<int> head, next, prev;
void init(size_t vertex_count, size_t slot_count)
{
head.assign(vertex_count, -1);
next.assign(slot_count, -1);
prev.assign(slot_count, -1);
}
void link(int s, const std::vector<int> &corners)
{
const int v = corners[size_t(s)];
const int h = head[size_t(v)];
prev[size_t(s)] = -1;
next[size_t(s)] = h;
if (h != -1)
prev[size_t(h)] = s;
head[size_t(v)] = s;
}
void unlink(int s, const std::vector<int> &corners)
{
const int p = prev[size_t(s)], n = next[size_t(s)];
if (p != -1) next[size_t(p)] = n;
else head[size_t(corners[size_t(s)])] = n;
if (n != -1) prev[size_t(n)] = p;
}
};
} // namespace
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
double max_edge_length, const RegularizeOptions &opts)
{
RegularizeResult result;
const size_t tri_count = geometry.triangle_count();
if (tri_count == 0 || max_edge_length <= 0.0) {
result.geometry = geometry;
result.face_parent_id = face_parent_id;
return result;
}
const double base_max_len_sq = (max_edge_length * opts.slack) * (max_edge_length * opts.slack);
const double aggr_max_len_sq =
(max_edge_length * opts.aggressive_slack) * (max_edge_length * opts.aggressive_slack);
const double extreme_aspect2 = opts.extreme_sliver_aspect * opts.extreme_sliver_aspect;
const double aspect_thr2 = opts.aspect_threshold * opts.aspect_threshold;
// Double precision: a collapse writes a midpoint back and later collapses read it, so rounding
// would accumulate.
QuantizedPointMap pos_map(WELD_GRID_GEOMETRY, std::min(tri_count * 3, size_t(1) << 22));
std::vector<Vec3d> vert;
std::vector<int> corners(tri_count * 3);
vert.reserve(tri_count);
for (size_t i = 0; i < tri_count * 3; ++i) {
const Vec3f &p = geometry.pos[i];
const int id = pos_map.get_or_set(p, int(vert.size()));
if (pos_map.inserted())
vert.push_back(p.cast<double>());
corners[i] = id;
}
const size_t vert_count = vert.size();
std::vector<Vec3d> tri_nrm(tri_count, Vec3d::Zero());
std::vector<uint8_t> tri_deleted(tri_count, 0);
result.face_parent_id = face_parent_id;
if (result.face_parent_id.size() != tri_count)
result.face_parent_id.assign(tri_count, 0);
const auto sq_dist = [&](int a, int b) { return (vert[size_t(a)] - vert[size_t(b)]).squaredNorm(); };
const auto recompute_face_normal = [&](size_t t) {
const Vec3d &a = vert[size_t(corners[t * 3])];
const Vec3d n = (vert[size_t(corners[t * 3 + 1])] - a).cross(vert[size_t(corners[t * 3 + 2])] - a);
const double len = n.norm();
tri_nrm[t] = (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
};
for (size_t t = 0; t < tri_count; ++t)
recompute_face_normal(t);
// Never updated - the normal gate measures against these, so drift cannot compound across rounds.
const std::vector<Vec3d> orig_nrm = tri_nrm;
// Squared thinness, the longest edge over the shortest altitude:
// thinness = lmax / hmin = lmax^2 / (2 * area), so thinness^2 = lmax^4 / |AB x AC|^2
//
// Not lmax/lmin, which misses what matters here: three near-collinear points can have all edges
// similar, so an edge ratio reports about 2 and the gate skips a triangle with near-zero area
// whose corners sample three unrelated texels. An equilateral scores about 1.15.
const auto tri_aspect_sq = [&](size_t t) -> double {
const Vec3d &a = vert[size_t(corners[t * 3])];
const Vec3d ab = vert[size_t(corners[t * 3 + 1])] - a;
const Vec3d ac = vert[size_t(corners[t * 3 + 2])] - a;
const Vec3d bc = vert[size_t(corners[t * 3 + 2])] - vert[size_t(corners[t * 3 + 1])];
const double lmax2 = std::max({ ab.squaredNorm(), ac.squaredNorm(), bc.squaredNorm() });
const double cross2 = ab.cross(ac).squaredNorm();
return cross2 > 0.0 ? lmax2 * lmax2 / cross2 : std::numeric_limits<double>::infinity();
};
SlotLists slots;
slots.init(vert_count, tri_count * 3);
for (size_t s = 0; s < tri_count * 3; ++s)
slots.link(int(s), corners);
// O(1) membership without clearing a set per collapse.
std::vector<uint32_t> vert_stamp(vert_count, 0), tri_stamp(tri_count, 0);
uint32_t stamp_gen = 0;
// Both endpoints of a hard edge are barred from being collapse endpoints, preserving such
// corners exactly while leaving flat-face interiors free.
//
// Skipped when either triangle is an extreme sliver: a sliver's normal is dominated by where its
// far apex sits, so noise pivots it tens of degrees with no feature behind it, and freezing on
// that would lock the very chains this pass exists to dissolve. Genuine features are bordered by
// well-shaped triangles and are unaffected.
std::vector<uint8_t> frozen_vert(vert_count, 0);
{
std::vector<double> tri_thin2(tri_count);
for (size_t t = 0; t < tri_count; ++t)
tri_thin2[t] = tri_aspect_sq(t);
QuantizedPointMap edge_seen(1.0, std::min(tri_count * 3, size_t(1) << 22));
for (size_t t = 0; t < tri_count; ++t)
for (int e = 0; e < 3; ++e) {
const int u = corners[t * 3 + size_t(e)];
const int v = corners[t * 3 + size_t((e + 1) % 3)];
const int lo = std::min(u, v), hi = std::max(u, v);
const int other = edge_seen.get_or_set_key(lo, hi, 0, int(t));
if (edge_seen.inserted())
continue;
if (tri_thin2[t] > extreme_aspect2 || tri_thin2[size_t(other)] > extreme_aspect2)
continue;
if (tri_nrm[t].dot(tri_nrm[size_t(other)]) < opts.sharp_edge_cos) {
frozen_vert[size_t(u)] = 1;
frozen_vert[size_t(v)] = 1;
}
}
}
// Exclusion freeze. The weight is constant across a face's corners, so the first one answers.
if (opts.preserve_excluded && !geometry.exclude_weight.empty())
for (size_t t = 0; t < tri_count; ++t)
if (geometry.exclude_weight[t * 3] > 0.99f)
for (int k = 0; k < 3; ++k)
frozen_vert[size_t(corners[t * 3 + size_t(k)])] = 1;
std::vector<int> wing_scratch, affected_scratch;
const auto third_vertex = [&](size_t t, int u, int v) {
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
if (a != u && a != v) return a;
if (b != u && b != v) return b;
return c;
};
const auto triangles_sharing_edge = [&](int u, int v) -> std::vector<int> & {
wing_scratch.clear();
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
if (corners[t * 3] == v || corners[t * 3 + 1] == v || corners[t * 3 + 2] == v)
wing_scratch.push_back(int(t));
}
return wing_scratch;
};
RegularizeRejectStats &stats = result.reject_stats;
const auto try_collapse = [&](int u, int v) -> bool {
if (u == v)
return false;
if (frozen_vert[size_t(u)] || frozen_vert[size_t(v)]) { ++stats.frozen; return false; }
// Two wings means a manifold interior edge.
std::vector<int> &wings = triangles_sharing_edge(u, v);
if (wings.size() != 2) { ++stats.wing_count; return false; }
const size_t w0 = size_t(wings[0]), w1 = size_t(wings[1]);
const int apex1 = third_vertex(w0, u, v), apex2 = third_vertex(w1, u, v);
if (apex1 == apex2) { ++stats.folded_apex; return false; }
// The edge cap loosens if *either* wing is extreme, since the re-subdivision recovers an
// over-long edge. The normal cap needs *both*, which is what protects fillets.
const double w1a = tri_aspect_sq(w0), w2a = tri_aspect_sq(w1);
const bool either_extreme = w1a > extreme_aspect2 || w2a > extreme_aspect2;
const bool both_extreme = w1a > extreme_aspect2 && w2a > extreme_aspect2;
const double eff_max_len_sq = either_extreme ? aggr_max_len_sq : base_max_len_sq;
const double eff_normal_cos =
both_extreme ? opts.aggressive_normal_delta_cos : opts.max_normal_delta_cos;
// A vertex sharing a triangle with both endpoints, other than the wing apexes, would go
// non-manifold. Stamp one side's neighbours, scan the other against them.
++stamp_gen;
for (int s = slots.head[size_t(v)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k)
if (const int x = corners[t * 3 + size_t(k)]; x != v)
vert_stamp[size_t(x)] = stamp_gen;
}
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k) {
const int x = corners[t * 3 + size_t(k)];
if (x != u && x != v && x != apex1 && x != apex2 && vert_stamp[size_t(x)] == stamp_gen) {
++stats.link_condition;
return false;
}
}
}
const Vec3d m = (vert[size_t(u)] + vert[size_t(v)]) * 0.5;
// Everything using either endpoint; the wings are being deleted.
++stamp_gen;
affected_scratch.clear();
for (const int endpoint : { u, v })
for (int s = slots.head[size_t(endpoint)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t] || t == w0 || t == w1)
continue;
if (tri_stamp[t] != stamp_gen) {
tri_stamp[t] = stamp_gen;
affected_scratch.push_back(int(t));
}
}
// Validate every affected triangle before touching anything.
for (const int ti : affected_scratch) {
const size_t t = size_t(ti);
Vec3d p[3];
for (int k = 0; k < 3; ++k) {
const int x = corners[t * 3 + size_t(k)];
p[k] = (x == u || x == v) ? m : vert[size_t(x)];
}
const double ab2 = (p[1] - p[0]).squaredNorm();
const double bc2 = (p[2] - p[1]).squaredNorm();
const double ca2 = (p[0] - p[2]).squaredNorm();
if (ab2 > eff_max_len_sq || bc2 > eff_max_len_sq || ca2 > eff_max_len_sq) {
++stats.edge_cap;
return false;
}
const Vec3d n = (p[1] - p[0]).cross(p[2] - p[0]);
const double nlen = n.norm();
if (nlen <= 0.0) { ++stats.degenerate; return false; }
if ((n / nlen).dot(orig_nrm[t]) < eff_normal_cos) { ++stats.normal_change; return false; }
}
// Apply: move u to the merged position and redirect every reference to v.
vert[size_t(u)] = m;
for (const size_t w : { w0, w1 }) {
tri_deleted[w] = 1;
for (int k = 0; k < 3; ++k)
slots.unlink(int(w * 3) + k, corners);
}
// A non-wing triangle contains v exactly once, so moving its slots suffices.
for (int s = slots.head[size_t(v)]; s != -1;) {
const int ns = slots.next[size_t(s)];
slots.unlink(s, corners);
corners[size_t(s)] = u;
slots.link(s, corners);
recompute_face_normal(size_t(s) / 3);
s = ns;
}
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (!tri_deleted[t])
recompute_face_normal(t);
}
return true;
};
// Per-triangle thinness for a round's candidate scan; -1 marks "not a candidate". Scored in
// parallel, since the scan only reads the mesh, then gathered serially in index order.
std::vector<double> round_aspect(tri_count);
for (int round = 0; round < opts.maxrounds; ++round) {
// Rebuilt each round so earlier collapses inform the priorities.
tbb::parallel_for(tbb::blocked_range<size_t>(0, tri_count), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
round_aspect[t] = -1.0;
if (tri_deleted[t])
continue;
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
if (std::min({ sq_dist(a, b), sq_dist(b, c), sq_dist(c, a) }) <= 0.0)
continue;
const double aspect2 = tri_aspect_sq(t);
if (aspect2 >= aspect_thr2)
round_aspect[t] = aspect2;
}
});
// Worst first; ties keep ascending triangle order so the pass is deterministic. Sorting the
// (thinness, triangle) pairs themselves gives exactly the order the index sort with its
// indirect comparator did, without that comparator's extra lookup on every comparison.
std::vector<std::pair<double, int>> cand;
for (size_t t = 0; t < tri_count; ++t)
if (round_aspect[t] >= 0.0)
cand.emplace_back(round_aspect[t], int(t));
std::sort(cand.begin(), cand.end(), [](const std::pair<double, int> &x, const std::pair<double, int> &y) {
return x.first != y.first ? x.first > y.first : x.second < y.second;
});
size_t round_collapses = 0;
for (const std::pair<double, int> &entry : cand) {
const size_t t = size_t(entry.second);
if (tri_deleted[t])
continue;
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
// All three edges, shortest first: a sliver straddling a seam has its shortest edge
// crossing it, which the normal gate refuses, while a long edge along one surface
// collapses safely. Trying only the shortest would leave those stuck.
struct Cand { double len2; int u, v; };
Cand e[3] = { { sq_dist(a, b), a, b }, { sq_dist(b, c), b, c }, { sq_dist(c, a), c, a } };
std::stable_sort(std::begin(e), std::end(e),
[](const Cand &x, const Cand &y) { return x.len2 < y.len2; });
if (try_collapse(e[0].u, e[0].v) || try_collapse(e[1].u, e[1].v) ||
try_collapse(e[2].u, e[2].v))
++round_collapses;
}
result.collapse_count += round_collapses;
if (round_collapses == 0)
break;
}
// Drop deleted triangles and rebuild the soup.
const bool have_weights = !geometry.exclude_weight.empty();
const size_t survivors = tri_count - size_t(std::count(tri_deleted.begin(), tri_deleted.end(), uint8_t(1)));
std::vector<int> out_parent;
TriSoup &out = result.geometry;
out_parent.reserve(survivors);
out.pos.reserve(survivors * 3);
if (have_weights)
out.exclude_weight.reserve(survivors * 3);
for (size_t t = 0; t < tri_count; ++t) {
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k)
out.pos.push_back(vert[size_t(corners[t * 3 + size_t(k)])].cast<float>());
if (have_weights) {
// Constant across a face's corners.
const float w = geometry.exclude_weight[t * 3];
out.exclude_weight.insert(out.exclude_weight.end(), { w, w, w });
}
out_parent.push_back(result.face_parent_id[t]);
}
result.face_parent_id = std::move(out_parent);
// Rebuilt from the compacted geometry - the collapses moved vertices.
out.nrm.assign(out.pos.size(), Vec3f::Zero());
{
QuantizedPointMap weld(WELD_GRID_GEOMETRY, out.pos.size());
std::vector<int> vid(out.pos.size());
int next = 0;
for (size_t i = 0; i < out.pos.size(); ++i) {
vid[i] = weld.get_or_set(out.pos[i], next);
if (weld.inserted())
++next;
}
std::vector<Vec3d> vn(size_t(next), Vec3d::Zero());
for (size_t t = 0; t * 3 < out.pos.size(); ++t) {
const Vec3d a = out.pos[t * 3].cast<double>();
const Vec3d n = (out.pos[t * 3 + 1].cast<double>() - a).cross(out.pos[t * 3 + 2].cast<double>() - a);
for (int k = 0; k < 3; ++k)
vn[size_t(vid[t * 3 + size_t(k)])] += n;
}
for (size_t i = 0; i < out.pos.size(); ++i) {
const Vec3d &n = vn[size_t(vid[i])];
const double l = n.norm();
out.nrm[i] = (l > 0.0) ? Vec3d(n / l).cast<float>() : Vec3f(0.f, 0.f, 1.f);
}
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,73 @@
#pragma once
// Sliver removal by short-edge collapse.
//
// Subdivision turns tessellation needles into chains of slivers that are within the edge-length
// budget but still poor triangles. A sliver's three vertices land on three unrelated texels, so the
// relief picks up noise that is an artifact of the tessellation rather than of the image.
//
// A candidate's edge is collapsed to its midpoint only if it passes three gates: no affected
// triangle may exceed the target edge times a slack factor; every affected triangle must keep its
// face normal within a bound of its *original* direction (which is what stops curved surfaces being
// flattened); and the link condition must hold, or the result would be non-manifold. Boundary and
// non-manifold edges are skipped outright. Rounds repeat until one achieves nothing.
#include <cstdint>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct RegularizeOptions
{
// Candidate threshold. Set to catch real slivers - chains measure in the hundreds - without
// sweeping up moderate fillet triangles, which sit between 2 and 5.
double aspect_threshold = 5.0;
// The base tier is loose on purpose: non-sliver boundary collapses must keep succeeding, since
// those give a chain the room to dissolve. A tight base leaves chains worse than before. The
// aggressive tier applies when at least one wing is an extreme sliver.
double slack = 3.0;
double aggressive_slack = 8.0;
// Thinness above which a wing counts as extreme: longest edge over shortest altitude.
double extreme_sliver_aspect = 8.0;
// Measured against each triangle's normal from before any collapse ran, so rounds of small
// allowed drift cannot compound into corner damage. Asymmetric two-tier: the loose bound needs
// *both* wings extreme, which matches a needle chain on a curved face but not a sliver beside a
// fillet, so fillets keep the tight bound.
double max_normal_delta_cos = 0.965925826289; // cos(15 degrees)
double aggressive_normal_delta_cos = 0.906307787037; // cos(25 degrees)
// Vertices on edges sharper than this are frozen, so hard features keep every original vertex.
double sharp_edge_cos = 0.866025403784; // cos(30 degrees)
int maxrounds = 8;
// Freeze excluded faces entirely, so untextured geometry is never modified.
bool preserve_excluded = false;
};
// Which gate blocked a collapse - the only practical way to tell why a region failed to merge.
struct RegularizeRejectStats
{
size_t frozen = 0, wing_count = 0, link_condition = 0, edge_cap = 0, normal_change = 0,
degenerate = 0, folded_apex = 0;
};
struct RegularizeResult
{
TriSoup geometry;
std::vector<int> face_parent_id;
size_t collapse_count = 0;
RegularizeRejectStats reject_stats;
};
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
double max_edge_length, const RegularizeOptions &opts = {});
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,205 @@
#include "TextureBakeRelocate.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
namespace {
// Any two unit vectors orthogonal to n. Which two does not matter - the gradient is expressed in this
// basis and converted straight back, so the result is basis independent.
void tangent_basis(const Vec3f &n, Vec3f &t1, Vec3f &t2)
{
const Vec3f a = (std::abs(n.x()) < 0.9f) ? Vec3f(1.f, 0.f, 0.f) : Vec3f(0.f, 1.f, 0.f);
t1 = n.cross(a).normalized();
t2 = n.cross(t1).normalized();
}
} // namespace
RelocateResult relocate_to_contours(const TriSoup &geometry, const HeightSampleFn &sample,
const RelocateSettings &settings, const std::vector<uint8_t> &locked)
{
RelocateResult result;
result.geometry = geometry;
const size_t count = geometry.pos.size();
const size_t tri_ct = count / 3;
if (count == 0 || !sample || settings.iterations <= 0)
return result;
// Weld, so every copy of a position moves together and the mesh cannot come apart.
QuantizedPointMap weld(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
std::vector<int> vid(count);
std::vector<Vec3f> pos;
for (size_t i = 0; i < count; ++i) {
vid[i] = weld.get_or_set(geometry.pos[i], int(pos.size()));
if (weld.inserted())
pos.push_back(geometry.pos[i]);
}
const size_t nv = pos.size();
// Incident corners per position, CSR style, plus the mean incident edge length that sets the scale
// for both the finite difference and the move limit.
std::vector<uint32_t> start(nv + 1, 0);
for (size_t i = 0; i < count; ++i)
++start[size_t(vid[i]) + 1];
for (size_t v = 0; v < nv; ++v)
start[v + 1] += start[v];
std::vector<uint32_t> inc(count), cursor(nv, 0);
for (size_t i = 0; i < count; ++i)
inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i);
std::vector<uint8_t> frozen(nv, 0);
if (!locked.empty())
for (size_t t = 0; t < tri_ct && t < locked.size(); ++t)
if (locked[t])
for (int k = 0; k < 3; ++k)
frozen[size_t(vid[t * 3 + size_t(k)])] = 1;
std::vector<float> edge_len(nv, 0.f), normal_len(nv, 0.f);
std::vector<Vec3f> nrm(nv, Vec3f::Zero());
const auto rebuild_frames = [&]() {
std::fill(nrm.begin(), nrm.end(), Vec3f::Zero());
std::fill(edge_len.begin(), edge_len.end(), 0.f);
std::vector<uint32_t> deg(nv, 0);
for (size_t t = 0; t < tri_ct; ++t) {
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
const Vec3f fn = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]);
for (int k = 0; k < 3; ++k) {
const int u = vid[t * 3 + size_t(k)], w = vid[t * 3 + size_t((k + 1) % 3)];
nrm[size_t(u)] += fn;
edge_len[size_t(u)] += (pos[size_t(w)] - pos[size_t(u)]).norm();
++deg[size_t(u)];
}
}
for (size_t v = 0; v < nv; ++v) {
const float l = nrm[v].norm();
nrm[v] = (l > 0.f) ? Vec3f(nrm[v] / l) : Vec3f(0.f, 0.f, 1.f);
edge_len[v] = deg[v] > 0 ? edge_len[v] / float(deg[v]) : 0.f;
}
};
rebuild_frames();
// The level to snap onto, taken from the height actually present on this patch rather than assumed.
// A texture that never reaches full black or white would otherwise be measured against a range it
// does not occupy.
double h_lo = std::numeric_limits<double>::max(), h_hi = -h_lo;
{
std::vector<float> h0(nv, 0.f);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
for (size_t v = r.begin(); v < r.end(); ++v)
h0[v] = sample(pos[v], nrm[v], nrm[v]);
});
for (const float h : h0) {
h_lo = std::min(h_lo, double(h));
h_hi = std::max(h_hi, double(h));
}
}
const double h_range = h_hi - h_lo;
if (!(h_range > 0.0))
return result; // a flat height field has no contour to snap to
const double target = h_lo + h_range * settings.contour_level;
// A gradient is worth acting on when the height changes by this much across one edge length.
const double min_grad = h_range * settings.min_gradient_fraction;
std::vector<uint8_t> ever_moved(nv, 0);
for (int iter = 0; iter < settings.iterations; ++iter) {
std::vector<Vec3f> proposal(nv);
std::vector<uint8_t> want(nv, 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
for (size_t v = r.begin(); v < r.end(); ++v) {
if (frozen[v] || edge_len[v] <= 0.f)
continue;
const Vec3f n = nrm[v];
Vec3f t1, t2;
tangent_basis(n, t1, t2);
const float eps = edge_len[v] * float(settings.gradient_step_fraction);
if (eps <= 0.f)
continue;
// Central differences in the tangent plane. Sampling the field itself, not the mesh,
// so the gradient is the image's, at whatever resolution the mesh happens to have.
const double h = double(sample(pos[v], n, n));
const double gx = (double(sample(pos[v] + t1 * eps, n, n)) -
double(sample(pos[v] - t1 * eps, n, n))) / (2.0 * double(eps));
const double gy = (double(sample(pos[v] + t2 * eps, n, n)) -
double(sample(pos[v] - t2 * eps, n, n))) / (2.0 * double(eps));
const double g2 = gx * gx + gy * gy;
if (g2 <= 0.0)
continue;
// Scale-free test: how much the height changes across one edge, versus the patch range.
if (std::sqrt(g2) * double(edge_len[v]) < min_grad)
continue;
// Newton step onto the level set h = target, expressed back in 3D.
const double s = -(h - target) / g2;
Vec3f d = t1 * float(s * gx) + t2 * float(s * gy);
const float cap = edge_len[v] * float(settings.max_move_fraction);
const float len = d.norm();
if (len <= 0.f)
continue;
if (len > cap)
d *= cap / len;
proposal[v] = pos[v] + d;
want[v] = 1;
}
});
// Apply one at a time: a move is only valid against the neighbourhood as it stands, and two
// adjacent vertices moving together can invert a triangle neither would have on its own.
size_t applied = 0;
for (size_t v = 0; v < nv; ++v) {
if (!want[v])
continue;
const Vec3f old = pos[v];
pos[v] = proposal[v];
bool ok = true;
for (uint32_t k = start[v]; k < start[v + 1] && ok; ++k) {
const size_t t = size_t(inc[k]) / 3;
const Vec3f &a = pos[size_t(vid[t * 3])];
const Vec3f n2 = (pos[size_t(vid[t * 3 + 1])] - a).cross(pos[size_t(vid[t * 3 + 2])] - a);
// Compared against the frame this vertex carried before the move: a triangle that
// flips or collapses means the move crossed a neighbour.
if (n2.squaredNorm() <= 0.f || n2.normalized().dot(nrm[v]) < 0.f)
ok = false;
}
if (ok) {
++applied;
ever_moved[v] = 1;
} else {
pos[v] = old;
++result.rejected;
}
}
if (applied == 0)
break;
rebuild_frames();
}
for (size_t v = 0; v < nv; ++v)
if (ever_moved[v])
++result.moved;
// Write the relocated positions back to every copy, and rebuild the per-face normals.
for (size_t i = 0; i < count; ++i)
result.geometry.pos[i] = pos[size_t(vid[i])];
for (size_t t = 0; t < tri_ct; ++t) {
Vec3f n = (result.geometry.pos[t * 3 + 1] - result.geometry.pos[t * 3])
.cross(result.geometry.pos[t * 3 + 2] - result.geometry.pos[t * 3]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
result.geometry.nrm[t * 3] = result.geometry.nrm[t * 3 + 1] = result.geometry.nrm[t * 3 + 2] = n;
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,72 @@
#pragma once
// Tangential relocation: slide vertices along the surface so triangle edges land on the height map's
// own edges, before any displacement happens.
//
// Displacement moves vertices along the normal only, so a step in the height map - the wall of a
// mortar groove, the rim of an embossed shape - is reproduced wherever the triangle grid happens to
// fall, as a staircase quantised to triangle boundaries. Refining further only makes the steps
// smaller; it never straightens them, because the edge in the image still does not coincide with any
// edge in the mesh.
//
// This pass fixes the cause rather than the symptom. For a vertex sitting near a step it takes a
// Newton step onto the contour: with h the sampled height and g its tangential gradient, the move
//
// d = -(h - target) * g / |g|^2
//
// lands on the level set h = target to first order. The ring of vertices nearest each step therefore
// snaps onto it, the triangle edges between them follow the step, and the displaced result has a
// straight wall instead of a sawtooth.
//
// Vertices in flat regions have no gradient to speak of and are left alone, so the pass costs nothing
// where there is nothing to align.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeDisplace.hpp"
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct RelocateSettings
{
// Passes. Each is a Newton step, so a couple converge for vertices that start reasonably close;
// more mainly helps ones that begin further away.
int iterations = 3;
// How far a vertex may move in one pass, as a fraction of the mean length of its incident edges.
// Below a half it cannot pass a neighbour, which is what keeps the triangulation valid without
// needing a full topological check.
double max_move_fraction = 0.35;
// A vertex is only pulled when the height varies enough across its own footprint to mean
// something - as a fraction of the height range over the whole patch. Below this the gradient is
// noise, and chasing it would scramble flat regions.
double min_gradient_fraction = 0.05;
// The level to snap onto, as a position in the sampled height range: 0.5 is midway between the
// lowest and highest point of the relief, which is where the wall of a step is steepest.
double contour_level = 0.5;
// Sampling offset for the finite-difference gradient, as a fraction of the local edge length.
double gradient_step_fraction = 0.25;
};
struct RelocateResult
{
TriSoup geometry;
size_t moved = 0; // positions that were relocated at least once
size_t rejected = 0; // moves refused because a triangle would have inverted
};
// `locked`, when non-empty, has one entry per input triangle; a vertex touching a locked triangle is
// never moved, so an excluded region keeps its exact vertex positions.
RelocateResult relocate_to_contours(const TriSoup &geometry, const HeightSampleFn &sample,
const RelocateSettings &settings = {},
const std::vector<uint8_t> &locked = {});
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,215 @@
#include "TextureBakeRepair.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <unordered_map>
#include <unordered_set>
namespace Slic3r {
namespace TextureBake {
namespace {
inline uint64_t edge_key(int a, int b)
{
const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b));
return (uint64_t(lo) << 32) | uint64_t(hi);
}
// On the export grid a squared cross product is either 0 (collinear) or at least about 1e-16, the
// smallest real triangle being one grid unit per leg, so this separates the two cleanly.
constexpr double DEGENERATE_AREA_SQ = 1e-18;
} // namespace
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant)
{
EdgeDefects out;
const size_t n = geometry.pos.size();
out.triangles = n / 3;
QuantizedPointMap vmap(quant, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
int next = 0;
for (size_t i = 0; i < n; ++i) {
id[i] = vmap.get_or_set(geometry.pos[i], next);
if (vmap.inserted())
++next;
}
std::unordered_map<uint64_t, int> counts;
for (size_t t = 0; t + 2 < n; t += 3) {
const int a = id[t], b = id[t + 1], c = id[t + 2];
if (a == b || b == c || a == c)
continue;
const int tri[3] = { a, b, c };
for (int e = 0; e < 3; ++e)
++counts[edge_key(tri[e], tri[(e + 1) % 3])];
}
for (const auto &[key, c] : counts) {
(void) key;
if (c == 1) ++out.open;
else if (c > 2) ++out.non_manifold;
}
return out;
}
size_t count_area_slivers(const TriSoup &geometry)
{
size_t n = 0;
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
const Vec3d u = (geometry.pos[t + 1] - geometry.pos[t]).cast<double>();
const Vec3d v = (geometry.pos[t + 2] - geometry.pos[t]).cast<double>();
// The threshold a slicer applies: area below 1e-12 mm^2.
if (u.cross(v).squaredNorm() < 1e-24)
++n;
}
return n;
}
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
{
const size_t n_tri = geometry.triangle_count();
const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol;
const double Q = opts.weld_quant;
// Snapped, not just welded: keeping unrounded coordinates lets a thin triangle pass the
// degeneracy test here and then collapse to collinear once the file is written, punching the very
// hole this pass prevents. Snapping makes the check see what will be written.
QuantizedPointMap vmap(Q, std::min(n_tri * 3, size_t(1) << 22));
std::vector<Vec3d> vert;
std::vector<int> vid(n_tri * 3);
for (size_t i = 0; i < n_tri * 3; ++i) {
const Vec3f &p = geometry.pos[i];
const int id = vmap.get_or_set(p, int(vert.size()));
if (vmap.inserted())
vert.emplace_back(double(grid_round(double(p.x()) * Q)) / Q,
double(grid_round(double(p.y()) * Q)) / Q,
double(grid_round(double(p.z()) * Q)) / Q);
vid[i] = id;
}
// Dropped: faces whose corners welded together, and needles - distinct but collinear on this
// grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly
// the on-edge-vertex topology the pass below closes.
std::vector<std::array<int, 3>> faces;
faces.reserve(n_tri);
for (size_t t = 0; t < n_tri; ++t) {
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
if (a == b || b == c || a == c)
continue;
const Vec3d u = vert[size_t(b)] - vert[size_t(a)];
const Vec3d w = vert[size_t(c)] - vert[size_t(a)];
if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ)
continue;
faces.push_back({ a, b, c });
}
for (int iter = 0; iter < opts.max_iters; ++iter) {
std::unordered_map<uint64_t, int> e_count;
for (const auto &f : faces)
for (int e = 0; e < 3; ++e)
++e_count[edge_key(f[size_t(e)], f[size_t((e + 1) % 3)])];
std::unordered_set<int> bverts;
for (const auto &[key, c] : e_count) {
if (c != 1)
continue;
bverts.insert(int(uint32_t(key >> 32)));
bverts.insert(int(uint32_t(key & 0xFFFFFFFFu)));
}
if (bverts.empty())
break;
const std::vector<int> bv(bverts.begin(), bverts.end());
struct Split { int a, b; std::vector<int> mids; };
std::unordered_map<size_t, Split> splits;
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto &f = faces[fi];
for (int e = 0; e < 3; ++e) {
const int a = f[size_t(e)], b = f[size_t((e + 1) % 3)];
if (e_count[edge_key(a, b)] != 1)
continue; // only a boundary edge carries an unresolved T-junction
const Vec3d A = vert[size_t(a)];
const Vec3d ev = vert[size_t(b)] - A;
const double elen2 = ev.squaredNorm();
if (elen2 < 1e-20)
continue;
std::vector<std::pair<double, int>> found;
for (const int c : bv) {
if (c == a || c == b)
continue;
const Vec3d cv = vert[size_t(c)] - A;
const double tp = cv.dot(ev) / elen2;
if (tp <= 1e-4 || tp >= 1.0 - 1e-4)
continue; // strictly between the ends
if ((cv - ev * tp).squaredNorm() < on_tol2)
found.emplace_back(tp, c);
}
if (!found.empty()) {
std::sort(found.begin(), found.end(),
[](const auto &x, const auto &y) { return x.first < y.first; });
Split sp{ a, b, {} };
for (const auto &m : found)
sp.mids.push_back(m.second);
splits.emplace(fi, std::move(sp));
break; // one site per face per pass; iteration handles cascades
}
}
}
if (splits.empty())
break;
std::vector<std::array<int, 3>> next;
next.reserve(faces.size() + splits.size() * 2);
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto it = splits.find(fi);
if (it == splits.end()) {
next.push_back(faces[fi]);
continue;
}
const auto &f = faces[fi];
const auto &sp = it->second;
const int apex = (f[0] != sp.a && f[0] != sp.b) ? f[0]
: (f[1] != sp.a && f[1] != sp.b) ? f[1]
: f[2];
// Walk the base the way the face already traverses it, so the winding survives.
bool dir_ab = false;
for (int e = 0; e < 3; ++e)
if (f[size_t(e)] == sp.a && f[size_t((e + 1) % 3)] == sp.b) {
dir_ab = true;
break;
}
std::vector<int> seq;
if (dir_ab) {
seq.push_back(sp.a);
seq.insert(seq.end(), sp.mids.begin(), sp.mids.end());
seq.push_back(sp.b);
} else {
seq.push_back(sp.b);
seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend());
seq.push_back(sp.a);
}
for (size_t s = 0; s + 1 < seq.size(); ++s)
next.push_back({ seq[s], seq[s + 1], apex });
}
faces.swap(next);
}
TriSoup out;
out.pos.reserve(faces.size() * 3);
out.nrm.reserve(faces.size() * 3);
for (const auto &f : faces) {
const Vec3f a = vert[size_t(f[0])].cast<float>();
const Vec3f b = vert[size_t(f[1])].cast<float>();
const Vec3f c = vert[size_t(f[2])].cast<float>();
Vec3f nrm = (b - a).cross(c - a);
const float len = nrm.norm();
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
out.pos.insert(out.pos.end(), { a, b, c });
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
}
return out;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,48 @@
#pragma once
// T-junction resolution and edge-defect accounting.
//
// Decimation can collapse a long edge whose interior still carries neighbouring triangles' vertices.
// Those then sit *on* an edge rather than at an end: watertight vertex-for-vertex, but the edge has
// one incident face on one side, which a slicer reads as an open boundary. This splits the offending
// face into a fan so every on-edge vertex becomes a real corner.
#include <cstdint>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct EdgeDefects
{
size_t open = 0, non_manifold = 0, triangles = 0;
};
// Welds at the export grid first: counting on the un-snapped mesh reports defects the file does not
// have and misses ones it does.
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant = WELD_GRID_EXPORT);
// Triangles a slicer would drop as degenerate. Each one, removed, punches a hole - so a non-zero
// count means watertight only on paper.
size_t count_area_slivers(const TriSoup &geometry);
struct RepairOptions
{
// Coordinates are snapped onto this grid, matching the precision files are written with.
double weld_quant = WELD_GRID_EXPORT;
// How far off an edge a vertex may sit and still count as on it. Well above the harvest
// tolerance, since harvesting leaves a region flat only to within that, making a collapsed edge a
// chord the on-edge vertices deviate from by about as much. Still far below the weld grid.
double on_seg_tol = 0.02;
// Splitting one face can expose another behind it, so the pass cascades.
int max_iters = 16;
};
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {});
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,484 @@
#include "TextureBakeSubdivide.hpp"
#include <algorithm>
#include <cmath>
#include <unordered_map>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/parallel_reduce.h>
namespace Slic3r {
namespace TextureBake {
namespace {
double edge_len_sq(const VertStore &v, int a, int b)
{
return (v.pos[size_t(a)] - v.pos[size_t(b)]).squaredNorm();
}
// Both indexers accumulate raw, area-weighted cross products and normalise once at the end.
void normalize_store_normals(VertStore &verts)
{
for (Vec3d &n : verts.nrm) {
const double len = n.norm();
n = (len > 0.0) ? Vec3d(n / len) : Vec3d(0.0, 0.0, 1.0);
}
}
// Keyed by the raw parent-vertex pair rather than by position: two sharp-edge copies of one point
// need their own midpoints, since their normals differ even though the position does not.
int get_midpoint(VertStore &verts, QuantizedPointMap &cache, int a, int b,
QuantizedPointMap *pos_canon_map)
{
const int lo = std::min(a, b), hi = std::max(a, b);
if (const int cached = cache.get_key(lo, hi, 0); cached != -1)
return cached;
const Vec3d m = (verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5;
Vec3d n = verts.nrm[size_t(a)] + verts.nrm[size_t(b)];
const double nl = n.norm();
n = (nl > 0.0) ? Vec3d(n / nl) : verts.nrm[size_t(a)];
const int idx = verts.push(m, n);
if (!verts.wgt.empty())
verts.wgt.push_back((verts.wgt[size_t(a)] + verts.wgt[size_t(b)]) * 0.5);
if (!verts.canon.empty() && pos_canon_map != nullptr)
verts.canon.push_back(pos_canon_map->get_or_set(float(m.x()), float(m.y()), float(m.z()), idx));
cache.get_or_set_key(lo, hi, 0, idx);
return idx;
}
struct PassResult
{
std::vector<int> indices;
std::vector<uint8_t> face_excluded;
std::vector<int> face_parent_id;
bool changed = false;
bool capped = false;
};
// Three steps, so that no T-junction can appear:
// 1. Mark every too-long edge globally, so both triangles on a shared edge decide alike.
// 1.5 Predict the exact resulting count from the marks (0->1, 1->2, 2->3, 3->4) and abort the
// *whole* pass if it exceeds the cap - a partial pass leaves split parents beside unsplit
// neighbours, the very crack step 1 prevents.
// 2. Rebuild, allocating once at the now-known size.
PassResult subdivide_pass(VertStore &verts, const std::vector<int> &indices, double max_edge_length,
int safety_cap, const std::vector<uint8_t> &face_excluded,
QuantizedPointMap *pos_canon_map, const std::vector<int> &face_parent_id,
const SubdivideWithinFn &within)
{
PassResult out;
const double max_sq = max_edge_length * max_edge_length;
const size_t tri_count = indices.size() / 3;
const bool have_canon = !verts.canon.empty();
// Sized from the pass rather than grown from 64 k: a fine pass marks on the order of one edge per
// triangle, and growing to that by doubling rehashes the whole table a dozen times.
const size_t expect = std::max<size_t>(size_t(1) << 16, tri_count);
QuantizedPointMap mid_cache(1.0, expect);
QuantizedPointMap split_edges(1.0, expect);
// With canonical ids the key is the canonical *position* id, so split copies either side of a
// sharp edge see one another's decision; without them the vertex index serves.
const auto key_of = [&](int v) -> int64_t { return have_canon ? verts.canon[size_t(v)] : v; };
const auto mark_edge = [&](int a, int b) {
const int64_t u = key_of(a), v = key_of(b);
if (u < v) split_edges.get_or_set_key(u, v, 0, 1);
else split_edges.get_or_set_key(v, u, 0, 1);
};
const auto is_marked = [&](int a, int b) {
const int64_t u = key_of(a), v = key_of(b);
return (u < v ? split_edges.get_key(u, v, 0) : split_edges.get_key(v, u, 0)) != -1;
};
// Graded, an edge splits while it is longer than max_edge_length + SUBDIVIDE_GRADE * (d - len/2),
// d its midpoint's distance from the region; less half the length bounds the edge's own distance
// from below, so the test errs toward splitting. Solved for d, that is the region coming within the
// radius below. It reads the positions alone, so two triangles sharing the edge agree.
const auto too_long = [&](int a, int b) {
const double len_sq = edge_len_sq(verts, a, b);
if (len_sq <= max_sq)
return false;
if (!within)
return true;
const double len = std::sqrt(len_sq);
return within((verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5,
0.5 * len + (len - max_edge_length) / SUBDIVIDE_GRADE);
};
// Step 1. An excluded triangle marks none of its own edges while left alone, so its interior never
// refines; its boundary edges are still marked by an included neighbour, and ungraded it follows
// that split. Graded, one the refinement reaches marks its own too-long edges too, and so on
// outward. An earlier bake's relief meets the paint along edges already at the target length, so
// it is never reached and stays as it was.
std::vector<uint8_t> want(tri_count, 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, tri_count), [&](const tbb::blocked_range<size_t> &range) {
for (size_t t = range.begin(); t < range.end(); ++t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
want[t] = uint8_t(too_long(a, b)) | uint8_t(too_long(b, c) << 1) | uint8_t(too_long(c, a) << 2);
}
});
const auto mark_wanted = [&](size_t t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
if (want[t] & 1) mark_edge(a, b);
if (want[t] & 2) mark_edge(b, c);
if (want[t] & 4) mark_edge(c, a);
};
std::vector<size_t> unreached;
for (size_t t = 0; t < tri_count; ++t)
if (face_excluded.empty() || !face_excluded[t])
mark_wanted(t);
else if (within && want[t] != 0)
unreached.push_back(t);
// Marks only accumulate, so the order the reached triangles are taken in does not matter.
for (size_t n = 0; n != unreached.size();) {
n = unreached.size();
unreached.erase(std::remove_if(unreached.begin(), unreached.end(), [&](size_t t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
const bool reached = is_marked(a, b) || is_marked(b, c) || is_marked(c, a);
if (reached)
mark_wanted(t);
return reached;
}), unreached.end());
}
if (split_edges.size() == 0) {
out.indices = indices;
out.face_excluded = face_excluded;
out.face_parent_id = face_parent_id;
return out; // changed stays false: nothing left to refine
}
// Step 1.5. Read-only against the finished mark set, so it runs in parallel - and it keeps each
// triangle's three marks (bit 0 = ab, 1 = bc, 2 = ca), so the serial rebuild below reads a byte
// instead of probing the hash map three more times per triangle.
std::vector<uint8_t> tri_marks(tri_count);
const size_t predicted = tbb::parallel_reduce(
tbb::blocked_range<size_t>(0, tri_count), size_t(0),
[&](const tbb::blocked_range<size_t> &range, size_t acc) {
for (size_t t = range.begin(); t < range.end(); ++t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
const uint8_t m = uint8_t(is_marked(a, b)) | uint8_t(is_marked(b, c) << 1) |
uint8_t(is_marked(c, a) << 2);
tri_marks[t] = m;
const int n = (m & 1) + ((m >> 1) & 1) + ((m >> 2) & 1);
acc += (n == 0) ? 1 : size_t(n + 1);
}
return acc;
},
std::plus<size_t>());
if (predicted > size_t(safety_cap)) {
out.indices = indices;
out.face_excluded = face_excluded;
out.face_parent_id = face_parent_id;
out.capped = true;
return out; // coarser than asked for, but watertight
}
// Step 2.
out.indices.resize(predicted * 3);
if (!face_excluded.empty())
out.face_excluded.resize(predicted);
if (!face_parent_id.empty())
out.face_parent_id.resize(predicted);
size_t wi = 0, fi = 0;
const auto emit_face_data = [&](uint8_t excl, int pid, int times) {
for (int k = 0; k < times; ++k) {
if (!out.face_excluded.empty()) out.face_excluded[fi] = excl;
if (!out.face_parent_id.empty()) out.face_parent_id[fi] = pid;
++fi;
}
};
const auto emit = [&](int x, int y, int z) {
out.indices[wi++] = x; out.indices[wi++] = y; out.indices[wi++] = z;
};
for (size_t t = 0; t < tri_count; ++t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
const uint8_t excl = face_excluded.empty() ? uint8_t(0) : face_excluded[t];
const int pid = face_parent_id.empty() ? 0 : face_parent_id[t];
const bool s_ab = (tri_marks[t] & 1) != 0, s_bc = (tri_marks[t] & 2) != 0, s_ca = (tri_marks[t] & 4) != 0;
const int n = int(s_ab) + int(s_bc) + int(s_ca);
if (n == 0) {
emit(a, b, c);
emit_face_data(excl, pid, 1);
} else if (n == 3) {
// a
// / \
// mCA-mAB
// / \ / \
// c--mBC--b
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, m_ab, m_ca);
emit(m_ab, b, m_bc);
emit(m_ca, m_bc, c);
emit(m_ab, m_bc, m_ca);
emit_face_data(excl, pid, 4);
} else if (n == 1) {
if (s_ab) {
const int m = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
emit(a, m, c);
emit(m, b, c);
} else if (s_bc) {
const int m = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
emit(a, b, m);
emit(a, m, c);
} else {
const int m = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, b, m);
emit(m, b, c);
}
emit_face_data(excl, pid, 2);
} else {
// A corner triangle on the untouched-edge vertex, then the remaining quadrilateral split
// along the midpoint-to-midpoint diagonal, which keeps the winding consistent.
//
// A sliver parent propagates: that inner diagonal inherits half the short edge and hands
// the sliver to two children per pass. No better diagonal exists - one avoiding the
// midpoints must pass through one of them, giving a zero-area triangle. Regularization
// removes such slivers before the mesh reaches here.
if (!s_ab) { // fan from c
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, b, m_bc);
emit(a, m_bc, m_ca);
emit(c, m_ca, m_bc);
} else if (!s_bc) { // fan from a
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, m_ab, m_ca);
emit(m_ab, b, c);
emit(m_ab, c, m_ca);
} else { // fan from b
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
emit(b, m_bc, m_ab);
emit(a, m_ab, m_bc);
emit(a, m_bc, c);
}
emit_face_data(excl, pid, 3);
}
}
out.changed = true;
return out;
}
} // namespace
IndexedMesh to_indexed_fast(const TriSoup &geometry)
{
// Preview path: a plain position merge - no clustering, no sharp-edge splitting, no canonical ids.
IndexedMesh out;
const size_t n = geometry.pos.size();
QuantizedPointMap vert_map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
out.indices.resize(n);
const bool has_w = !geometry.exclude_weight.empty();
for (size_t i = 0; i < n; ++i) {
const Vec3f &p = geometry.pos[i];
const Vec3f nf = geometry.nrm.empty() ? Vec3f(0.f, 0.f, 1.f) : geometry.nrm[i];
const int idx = vert_map.get_or_set(p, int(out.verts.count()));
if (vert_map.inserted()) {
out.verts.push(p.cast<double>(), nf.cast<double>());
if (has_w)
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
} else {
out.verts.nrm[size_t(idx)] += nf.cast<double>();
// Merge exclusion by maximum: any excluded face marks the shared vertex.
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(idx)])
out.verts.wgt[size_t(idx)] = double(geometry.exclude_weight[i]);
}
out.indices[i] = idx;
}
normalize_store_normals(out.verts);
return out;
}
IndexedMesh to_indexed(const TriSoup &geometry)
{
// Export path. Two vertices at one position merge only when their face normals agree to within
// SUBDIVIDE_SHARP_ANGLE_DEG, which keeps a cylinder from faceting while stopping a cube's edge
// normal from leaking into the flat face interiors as subdivision carries it inward.
IndexedMesh out;
out.has_canon = true;
const size_t n = geometry.pos.size();
const bool has_w = !geometry.exclude_weight.empty();
const double sharp_cos = std::cos(SUBDIVIDE_SHARP_ANGLE_DEG * M_PI / 180.0);
// Per-face normals: unit for the angle test, raw for the area-weighted accumulation.
std::vector<Vec3d> face_unit(n), face_raw(n);
tbb::parallel_for(tbb::blocked_range<size_t>(0, n / 3), [&](const tbb::blocked_range<size_t> &range) {
for (size_t f = range.begin(); f < range.end(); ++f) {
const size_t t = f * 3;
const Vec3d a = geometry.pos[t].cast<double>();
const Vec3d r = (geometry.pos[t + 1].cast<double>() - a).cross(
geometry.pos[t + 2].cast<double>() - a);
const double len = r.norm();
const Vec3d u = (len > 0.0) ? Vec3d(r / len) : Vec3d(0.0, 0.0, 1.0);
for (int v = 0; v < 3; ++v) {
face_unit[t + size_t(v)] = u;
face_raw[t + size_t(v)] = r;
}
}
});
out.indices.resize(n);
out.pos_canon_map = QuantizedPointMap(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
struct Cluster { int idx; Vec3d fn_unit; };
std::unordered_map<int, std::vector<Cluster>> clusters_by_canon;
for (size_t i = 0; i < n; ++i) {
const Vec3f &p = geometry.pos[i];
// The first vertex at a position becomes its canonical id; later split copies share it.
const int canon_id = out.pos_canon_map.get_or_set(p, int(out.verts.count()));
const bool fresh_position = out.pos_canon_map.inserted();
const auto add_vertex = [&](int canon) {
const int idx = out.verts.push(p.cast<double>(), face_raw[i]);
if (has_w)
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
out.verts.canon.push_back(canon);
return idx;
};
if (fresh_position) {
const int idx = add_vertex(canon_id);
clusters_by_canon[canon_id].push_back({ idx, face_unit[i] });
out.indices[i] = idx;
continue;
}
std::vector<Cluster> &clusters = clusters_by_canon[canon_id];
bool matched = false;
for (Cluster &cl : clusters) {
if (cl.fn_unit.dot(face_unit[i]) < sharp_cos)
continue;
out.verts.nrm[size_t(cl.idx)] += face_raw[i];
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(cl.idx)])
out.verts.wgt[size_t(cl.idx)] = double(geometry.exclude_weight[i]);
// Track the running average, so gradual curvature stays in one cluster instead of
// fragmenting when a distant face exceeds the threshold against the seed's fixed normal.
cl.fn_unit += face_unit[i];
if (const double rl = cl.fn_unit.norm(); rl > 0.0)
cl.fn_unit /= rl;
out.indices[i] = cl.idx;
matched = true;
break;
}
if (!matched) {
// A sharp-edge split: a new vertex at the same position, sharing its canonical id.
const int idx = add_vertex(canon_id);
clusters.push_back({ idx, face_unit[i] });
out.indices[i] = idx;
}
}
normalize_store_normals(out.verts);
return out;
}
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
const std::vector<uint8_t> &face_excluded)
{
TriSoup out;
const size_t tri_count = indices.size() / 3;
out.pos.resize(tri_count * 3);
out.nrm.resize(tri_count * 3);
const bool want_weights = !face_excluded.empty() || !verts.wgt.empty();
if (want_weights)
out.exclude_weight.resize(tri_count * 3);
// Each triangle writes only its own three slots, so this is a plain parallel gather.
tbb::parallel_for(tbb::blocked_range<size_t>(0, tri_count), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
// The per-face flag, not the interpolated weight: merging by maximum can push an *included*
// face's corners to 1 when it borders two excluded neighbours, wrongly excluding it.
const bool have_face_flag = !face_excluded.empty();
const float face_w = have_face_flag ? (face_excluded[t] ? 1.f : 0.f) : 0.f;
for (int v = 0; v < 3; ++v) {
const size_t vidx = size_t(indices[t * 3 + size_t(v)]);
out.pos[t * 3 + size_t(v)] = verts.pos[vidx].cast<float>();
out.nrm[t * 3 + size_t(v)] = verts.nrm[vidx].cast<float>();
if (want_weights)
out.exclude_weight[t * 3 + size_t(v)] =
have_face_flag ? face_w : float(verts.wgt[vidx]);
}
}
});
return out;
}
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
const std::vector<uint8_t> &face_excluded, bool fast, int safety_cap,
const SubdivideProgressFn &on_progress, const SubdivideWithinFn &within)
{
SubdivideResult result;
if (geometry.empty() || max_edge_length <= 0.0) {
result.geometry = geometry;
return result;
}
IndexedMesh indexed = fast ? to_indexed_fast(geometry) : to_indexed(geometry);
QuantizedPointMap *canon_map = indexed.has_canon ? &indexed.pos_canon_map : nullptr;
const size_t initial_tris = indexed.indices.size() / 3;
std::vector<int> current_indices = std::move(indexed.indices); // nothing reads it again
std::vector<uint8_t> current_excluded = face_excluded;
std::vector<int> current_parent(initial_tris);
for (size_t i = 0; i < initial_tris; ++i)
current_parent[i] = int(i);
for (int iter = 0; iter < SUBDIVIDE_MAX_ITERATIONS; ++iter) {
if (current_indices.size() / 3 >= size_t(safety_cap)) {
result.safety_cap_hit = true;
break;
}
PassResult pass = subdivide_pass(indexed.verts, current_indices, max_edge_length, safety_cap,
current_excluded, canon_map, current_parent, within);
current_indices = std::move(pass.indices);
if (!pass.face_excluded.empty())
current_excluded = std::move(pass.face_excluded);
if (!pass.face_parent_id.empty())
current_parent = std::move(pass.face_parent_id);
if (pass.capped || current_indices.size() / 3 >= size_t(safety_cap))
result.safety_cap_hit = true;
if (on_progress) {
// Reported after the pass, so the value falls each iteration instead of lagging a step.
// A max is order-independent, so the scan reduces in parallel to the same value.
const double max_edge_sq = tbb::parallel_reduce(
tbb::blocked_range<size_t>(0, current_indices.size() / 3), 0.0,
[&](const tbb::blocked_range<size_t> &r, double acc) {
for (size_t f = r.begin(); f < r.end(); ++f) {
const int a = current_indices[f * 3], b = current_indices[f * 3 + 1],
c = current_indices[f * 3 + 2];
acc = std::max({ acc, edge_len_sq(indexed.verts, a, b), edge_len_sq(indexed.verts, b, c),
edge_len_sq(indexed.verts, c, a) });
}
return acc;
},
[](double x, double y) { return std::max(x, y); });
if (!on_progress(std::min(0.95, double(iter + 1) / SUBDIVIDE_MAX_ITERATIONS),
current_indices.size() / 3, std::sqrt(max_edge_sq)))
break; // whole passes only, so what we have is still crack-free
}
if (!pass.changed || result.safety_cap_hit)
break;
}
result.geometry = to_non_indexed(indexed.verts, current_indices, current_excluded);
result.face_parent_id = std::move(current_parent);
return result;
}
} // namespace TextureBake
} // namespace Slic3r
@@ -0,0 +1,96 @@
#pragma once
// Adaptive subdivision to a target edge length, by global marked-edge (red-green) refinement rather
// than longest-edge bisection. Marking is global, so two triangles sharing an edge always agree and
// the result is crack-free by construction. A triangle is rebuilt from its marked-edge count: 0
// keeps, 1 bisects, 2 fans into three, 3 does the regular 1->4 split.
//
// The 1->4 case is what keeps the tessellation regular - its children are similar to the parent. An
// irregular one shows up after displacement as adjacent triangles tilting alternately, i.e. noise.
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Memory guard for the stages downstream. At roughly 145 bytes per triangle this is about 2.9 GB.
static constexpr int SUBDIVIDE_SAFETY_CAP = 16'000'000;
// Vertices at one position stay separate when their faces disagree by more than this: a cube keeps
// hard edges, a cylinder keeps averaged ones.
static constexpr double SUBDIVIDE_SHARP_ANGLE_DEG = 30.0;
// A depth bound, not a work bound: the loop stops as soon as a pass changes nothing.
static constexpr int SUBDIVIDE_MAX_ITERATIONS = 12;
// Graded refinement: millimetres of edge length allowed per millimetre of distance from the region.
static constexpr double SUBDIVIDE_GRADE = 1.0;
// Built by the indexers, appended to by the passes. Double precision so repeated midpointing does
// not drift.
struct VertStore
{
std::vector<Vec3d> pos;
std::vector<Vec3d> nrm;
std::vector<double> wgt; // exclusion weights; empty when the caller supplied none
std::vector<int> canon; // canonical position ids; empty in fast mode
size_t count() const { return pos.size(); }
int push(const Vec3d &p, const Vec3d &n)
{
const int idx = int(pos.size());
pos.push_back(p);
nrm.push_back(n);
return idx;
}
};
struct IndexedMesh
{
VertStore verts;
std::vector<int> indices; // 3 per triangle
QuantizedPointMap pos_canon_map{ WELD_GRID_GEOMETRY, 256 };
bool has_canon = false;
};
// Fraction, triangle count, longest remaining edge. Returning false cancels; what comes back is
// still watertight, because passes apply whole or not at all.
using SubdivideProgressFn = std::function<bool(double fraction, size_t triangles, double longest_edge)>;
// Whether the region being refined comes within `radius` mm of `point`. Called from several threads
// at once.
using SubdivideWithinFn = std::function<bool(const Vec3d &point, double radius)>;
struct SubdivideResult
{
TriSoup geometry;
// Output triangle -> input triangle it descends from, so per-face data survives with no remap.
std::vector<int> face_parent_id;
bool safety_cap_hit = false;
};
// `face_excluded`: one entry per input triangle; non-zero means its interior is not refined unless the
// graded refinement reaches it (see `within`). Its edges still split when an included neighbour marks
// them, so no T-junction appears at the boundary.
// `fast` selects the cheap position-only indexer for previews.
// `within`, when given, grades the refinement: an edge splits only while it is longer than
// max_edge_length plus SUBDIVIDE_GRADE times its distance from the region. Away from the region the
// triangles are then left as whole pieces of the input's own 1->4 grid rather than refined throughout.
// An excluded triangle the refinement reaches - one of its edges splits - splits the same graded way,
// instead of fanning from its far corner to follow the edge it shares with an included one.
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
const std::vector<uint8_t> &face_excluded = {}, bool fast = false,
int safety_cap = SUBDIVIDE_SAFETY_CAP,
const SubdivideProgressFn &on_progress = {},
const SubdivideWithinFn &within = {});
// Displacement needs the same welding and sharp-edge clustering.
IndexedMesh to_indexed(const TriSoup &geometry);
IndexedMesh to_indexed_fast(const TriSoup &geometry);
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
const std::vector<uint8_t> &face_excluded);
} // namespace TextureBake
} // namespace Slic3r
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -1519,9 +1519,11 @@ void TriangleSelector::get_facets(std::vector<indexed_triangle_set>& facets_per_
}
}
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source) const
{
indexed_triangle_set out;
if (out_source)
out_source->clear();
size_t num_vertices = 0;
for (const Vertex &v : m_vertices)
@@ -1535,8 +1537,13 @@ indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType sta
out.vertices.emplace_back(v.v);
}
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle)
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) {
this->get_facets_strict_recursive(m_triangles[itriangle], m_neighbors[itriangle], state, out.indices);
// Everything the recursion just appended came from this original triangle, whatever depth it
// was split to. Recording it here keeps the recursive helpers untouched.
if (out_source)
out_source->resize(out.indices.size(), itriangle);
}
for (auto &triangle : out.indices)
for (int i = 0; i < 3; ++ i)
+8 -1
View File
@@ -362,7 +362,14 @@ public:
// Get facets at a given state. Don't triangulate T-joints.
indexed_triangle_set get_facets(EnforcerBlockerType state) const;
// Get facets at a given state. Triangulate T-joints.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state) const;
// Sub-triangles in `state`, with the *whole* mesh's referenced vertex array (only .indices is
// filtered by state, so two calls with different states share one indexing).
//
// `out_source`, when given, is filled parallel to the returned .indices with the index of the
// original mesh triangle each sub-triangle came from. That is what lets a caller carry partial
// paint - the pieces of a triangle a brush stroke only partly covered - across a refinement of
// the same surface, instead of having to round each source triangle to wholly painted or not.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source = nullptr) const;
// Get edges around the selected area by seed fill.
std::vector<Vec2i32> get_seed_fill_contour() const;
+16
View File
@@ -214,6 +214,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmosManager.hpp
GUI/Gizmos/GLGizmoSVG.cpp
GUI/Gizmos/GLGizmoSVG.hpp
GUI/Gizmos/GLGizmoTextureDisplacement.cpp
GUI/Gizmos/GLGizmoTextureDisplacement.hpp
GUI/Gizmos/GLGizmoUtils.cpp
GUI/Gizmos/GLGizmoUtils.hpp
#GUI/Gizmos/GLGizmoText.cpp
@@ -342,6 +344,14 @@ set(SLIC3R_GUI_SOURCES
GUI/Jobs/SLAImportDialog.hpp
GUI/Jobs/SLAImportJob.cpp
GUI/Jobs/SLAImportJob.hpp
GUI/Jobs/TextureDisplacementBakeJob.cpp
GUI/Jobs/TextureDisplacementBakeJob.hpp
GUI/Jobs/TextureDisplacementPrepareJob.cpp
GUI/Jobs/TextureDisplacementPrepareJob.hpp
GUI/Jobs/TextureDisplacementDebugJob.cpp
GUI/Jobs/TextureDisplacementDebugJob.hpp
GUI/Jobs/TextureDisplacementPreviewJob.cpp
GUI/Jobs/TextureDisplacementPreviewJob.hpp
GUI/Jobs/ThreadSafeQueue.hpp
GUI/Jobs/UpgradeNetworkJob.cpp
GUI/Jobs/UpgradeNetworkJob.hpp
@@ -539,6 +549,10 @@ set(SLIC3R_GUI_SOURCES
GUI/TaskManager.hpp
GUI/TextLines.cpp
GUI/TextLines.hpp
GUI/TextureLibrary.cpp
GUI/TextureLibrary.hpp
GUI/TextureProjectorFrame.cpp
GUI/TextureProjectorFrame.hpp
GUI/TickCode.cpp
GUI/TickCode.hpp
GUI/TroubleshootDialog.cpp
@@ -555,6 +569,8 @@ set(SLIC3R_GUI_SOURCES
GUI/UserManager.hpp
GUI/UserNotification.cpp
GUI/UserNotification.hpp
GUI/UVEditorCanvas.cpp
GUI/UVEditorCanvas.hpp
GUI/WebDownPluginDlg.cpp
GUI/WebDownPluginDlg.hpp
GUI/WebGuideDialog.cpp
+21 -4
View File
@@ -1000,11 +1000,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
// update if nozzle_temperature_range is found
const int variant_index = get_filament_variant_index(*filament_it, nozzle_diameter_str);
ConfigOption *opt_min = filament_it->config.option("nozzle_temperature_range_low");
if (opt_min) {
ConfigOptionInts *opt_min_ints = dynamic_cast<ConfigOptionInts *>(opt_min);
if (opt_min_ints) {
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index));
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
}
}
@@ -1012,7 +1013,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
if (opt_max) {
ConfigOptionInts *opt_max_ints = dynamic_cast<ConfigOptionInts *>(opt_max);
if (opt_max_ints) {
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index));
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
}
}
@@ -1218,6 +1219,21 @@ void AMSMaterialsSetting::on_select_cali_result(wxCommandEvent &evt)
}
}
int AMSMaterialsSetting::get_filament_variant_index(const Preset &filament, const std::string &nozzle_diameter_str)
{
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (!obj || !preset_bundle)
return 0;
const std::set<std::string> printer_names =
preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str);
const Preset *printer = printer_names.empty() ? nullptr : preset_bundle->printers.find_preset(*printer_names.begin());
if (!printer)
return 0;
const int extruder_id = obj->get_extruder_id_by_ams_id(std::to_string(ams_id));
return PresetBundle::get_filament_variant_index(filament.config, printer->config, obj->get_preset_extruder_index(extruder_id),
DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(extruder_id)));
}
void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
{
// Get the flag whether to open the filament setting dialog from the device page
@@ -1249,11 +1265,12 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
if (it->filament_id != filament_id)
continue;
// ) if nozzle_temperature_range is found
const int variant_index = get_filament_variant_index(*it, nozzle_diameter_str);
ConfigOption* opt_min = it->config.option("nozzle_temperature_range_low");
if (opt_min) {
ConfigOptionInts* opt_min_ints = dynamic_cast<ConfigOptionInts*>(opt_min);
if (opt_min_ints) {
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(0));
wxString text_nozzle_temp_min = wxString::Format("%d", opt_min_ints->get_at(variant_index));
m_input_nozzle_min->GetTextCtrl()->SetValue(text_nozzle_temp_min);
}
}
@@ -1261,7 +1278,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
if (opt_max) {
ConfigOptionInts* opt_max_ints = dynamic_cast<ConfigOptionInts*>(opt_max);
if (opt_max_ints) {
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(0));
wxString text_nozzle_temp_max = wxString::Format("%d", opt_max_ints->get_at(variant_index));
m_input_nozzle_max->GetTextCtrl()->SetValue(text_nozzle_temp_max);
}
}
+2
View File
@@ -150,6 +150,8 @@ protected:
void update_widgets();
void update_filament_editing(bool is_printing);
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
int get_filament_variant_index(const Preset &filament, const std::string &nozzle_diameter_str);
protected:
StateColor m_btn_bg_green;
+12 -12
View File
@@ -78,12 +78,12 @@ void ConfigManipulation::set_option_label(const std::string& opt_key, const wxSt
cb_set_option_label(opt_key, label, opt_index);
}
void ConfigManipulation::check_nozzle_recommended_temperature_range(DynamicPrintConfig *config) {
void ConfigManipulation::check_nozzle_recommended_temperature_range(DynamicPrintConfig *config, unsigned int variant_index) {
if (is_msg_dlg_already_exist)
return;
int temperature_range_low, temperature_range_high;
if (!get_temperature_range(config, temperature_range_low, temperature_range_high)) return;
if (!get_temperature_range(config, temperature_range_low, temperature_range_high, variant_index)) return;
// Get the selected filament type
std::string filament_type = "";
@@ -123,16 +123,16 @@ void ConfigManipulation::check_nozzle_recommended_temperature_range(DynamicPrint
}
}
void ConfigManipulation::check_nozzle_temperature_range(DynamicPrintConfig *config)
void ConfigManipulation::check_nozzle_temperature_range(DynamicPrintConfig *config, unsigned int variant_index)
{
if (is_msg_dlg_already_exist)
return;
int temperature_range_low, temperature_range_high;
if (!get_temperature_range(config, temperature_range_low, temperature_range_high)) return;
if (!get_temperature_range(config, temperature_range_low, temperature_range_high, variant_index)) return;
if (config->has("nozzle_temperature")) {
if (config->opt_int("nozzle_temperature", 0) < temperature_range_low || config->opt_int("nozzle_temperature", 0) > temperature_range_high) {
if (config->opt_int("nozzle_temperature", variant_index) < temperature_range_low || config->opt_int("nozzle_temperature", variant_index) > temperature_range_high) {
wxString msg_text = _(L("The nozzle may become clogged when the temperature is out of the recommended range.\nPlease make sure whether to use this temperature to print.\n\n"));
msg_text += wxString::Format(_L("The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius."), temperature_range_low, temperature_range_high);
MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK);
@@ -143,17 +143,17 @@ void ConfigManipulation::check_nozzle_temperature_range(DynamicPrintConfig *conf
}
}
void ConfigManipulation::check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config)
void ConfigManipulation::check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config, unsigned int variant_index)
{
if (is_msg_dlg_already_exist)
return;
int temperature_range_low, temperature_range_high;
if (!get_temperature_range(config, temperature_range_low, temperature_range_high)) return;
if (!get_temperature_range(config, temperature_range_low, temperature_range_high, variant_index)) return;
if (config->has("nozzle_temperature_initial_layer")) {
if (config->opt_int("nozzle_temperature_initial_layer", 0) < temperature_range_low ||
config->opt_int("nozzle_temperature_initial_layer", 0) > temperature_range_high)
if (config->opt_int("nozzle_temperature_initial_layer", variant_index) < temperature_range_low ||
config->opt_int("nozzle_temperature_initial_layer", variant_index) > temperature_range_high)
{
wxString msg_text = _(L("The nozzle may become clogged when the temperature is out of the recommended range.\nPlease make sure whether to use this temperature to print.\n\n"));
msg_text += wxString::Format(_L("The recommended nozzle temperature for this filament type is [%d, %d] degrees Celsius."), temperature_range_low, temperature_range_high);
@@ -1295,15 +1295,15 @@ int ConfigManipulation::show_spiral_mode_settings_dialog(bool is_object_config)
return answer;
}
bool ConfigManipulation::get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high)
bool ConfigManipulation::get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high, unsigned int variant_index)
{
bool range_low_exist = false, range_high_exist = false;
if (config->has("nozzle_temperature_range_low")) {
range_low = config->opt_int("nozzle_temperature_range_low", (unsigned int) 0);
range_low = config->opt_int("nozzle_temperature_range_low", variant_index);
range_low_exist = true;
}
if (config->has("nozzle_temperature_range_high")) {
range_high = config->opt_int("nozzle_temperature_range_high", (unsigned int) 0);
range_high = config->opt_int("nozzle_temperature_range_high", variant_index);
range_high_exist = true;
}
return range_low_exist && range_high_exist;
+4 -4
View File
@@ -79,9 +79,9 @@ public:
void apply_null_fff_config(DynamicPrintConfig *config, std::vector<std::string> const &keys, std::map<ObjectBase*, ModelConfig*> const & configs);
//BBS: FFF filament nozzle temperature range
void check_nozzle_recommended_temperature_range(DynamicPrintConfig *config);
void check_nozzle_temperature_range(DynamicPrintConfig* config);
void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config);
void check_nozzle_recommended_temperature_range(DynamicPrintConfig *config, unsigned int variant_index);
void check_nozzle_temperature_range(DynamicPrintConfig* config, unsigned int variant_index);
void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config, unsigned int variant_index);
void check_adaptive_pressure_advance_model(DynamicPrintConfig* config);
void check_filament_max_volumetric_speed(DynamicPrintConfig *config);
void check_chamber_temperature(DynamicPrintConfig* config);
@@ -104,7 +104,7 @@ public:
int show_spiral_mode_settings_dialog(bool is_object_config = false);
private:
bool get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high);
bool get_temperature_range(DynamicPrintConfig *config, int &range_low, int &range_high, unsigned int variant_index);
};
} // GUI
+15 -16
View File
@@ -5696,7 +5696,7 @@ void MachineObject::update_filament_list()
PresetBundle *preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle;
// custom filament
typedef std::map<std::string, std::pair<int, int>> map_pair;
typedef std::map<std::string, std::pair<std::vector<int>, std::vector<int>>> map_pair;
std::map<std::string, map_pair> map_list;
for (auto &pair : m_nozzle_filament_data) {
map_list[pair.second.printer_preset_name] = map_pair{};
@@ -5708,19 +5708,13 @@ void MachineObject::update_filament_list()
for (const std::string &printer_str : printer_strs->values) {
if (map_list.find(printer_str) != map_list.end()) {
auto & filament_list = map_list[printer_str];
ConfigOption *opt_min = const_cast<Preset &>(preset).config.option("nozzle_temperature_range_low");
int min_temp = -1;
if (opt_min) {
ConfigOptionInts *opt_min_ints = dynamic_cast<ConfigOptionInts *>(opt_min);
min_temp = opt_min_ints->get_at(0);
}
ConfigOption *opt_max = const_cast<Preset &>(preset).config.option("nozzle_temperature_range_high");
int max_temp = -1;
if (opt_max) {
ConfigOptionInts *opt_max_ints = dynamic_cast<ConfigOptionInts *>(opt_max);
max_temp = opt_max_ints->get_at(0);
}
filament_list[preset.filament_id] = std::make_pair(min_temp, max_temp);
// Every variant's range, so a change to any of them rechecks the trays
std::vector<int> min_temps{-1}, max_temps{-1};
if (auto *opt_min = preset.config.option<ConfigOptionInts>("nozzle_temperature_range_low"))
min_temps = opt_min->values;
if (auto *opt_max = preset.config.option<ConfigOptionInts>("nozzle_temperature_range_high"))
max_temps = opt_max->values;
filament_list[preset.filament_id] = std::make_pair(min_temps, max_temps);
break;
}
}
@@ -5856,10 +5850,13 @@ void MachineObject::check_ams_filament_valid()
need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id);
try {
std::string preset_setting_id;
const int extruder_id = ams->GetExtruderId();
bool is_equation = preset_bundle->check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(printer_model, nozzle_diameter_str,
curr_tray->setting_id, curr_tray->tag_uid,
curr_tray->nozzle_temp_min,
curr_tray->nozzle_temp_max, preset_setting_id);
curr_tray->nozzle_temp_max, preset_setting_id,
get_preset_extruder_index(extruder_id),
DevNozzle::ToNozzleVolumeType(m_extder_system->GetNozzleFlowType(extruder_id)));
if (!is_equation) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << " ams filament is not match min max temp and reset, ams_id: " << ams_id << " tray_id"
<< slot_id << "filament_id: " << curr_tray->setting_id;
@@ -5924,7 +5921,9 @@ void MachineObject::check_ams_filament_valid()
this->printer_type),
nozzle_diameter_str, vt_tray.setting_id,
vt_tray.tag_uid, vt_tray.nozzle_temp_min,
vt_tray.nozzle_temp_max, preset_setting_id);
vt_tray.nozzle_temp_max, preset_setting_id,
get_preset_extruder_index(index),
DevNozzle::ToNozzleVolumeType(m_extder_system->GetNozzleFlowType(index)));
if (!is_equation) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__
<< " vt_tray filament is not match min max temp and reset, filament_id: " << vt_tray.setting_id;
+4 -1
View File
@@ -341,6 +341,8 @@ public:
bool is_main_extruder_on_left() const { return false; } // only means the extruder is on the left hand when extruder id is 0
bool is_multi_extruders() const;
int get_extruder_id_by_ams_id(const std::string& ams_id);
// Orca: the printer preset's extruder index of device extruder extder_id
int get_preset_extruder_index(int extder_id) const { return is_multi_extruders() ? (is_main_extruder_on_left() ? extder_id : 1 - extder_id) : 0; }
/* E3D has extra nozzle flow type info */
bool has_extra_flow_type{false};
@@ -957,7 +959,8 @@ public:
{
std::set<std::string> checked_filament;
std::string printer_preset_name;
std::map<std::string, std::pair<int, int>> filament_list; // filament_id, pair<min temp, max temp>
// filament_id, pair<min temps, max temps>, one per filament variant
std::map<std::string, std::pair<std::vector<int>, std::vector<int>>> filament_list;
};
std::map<std::string, FilamentData> m_nozzle_filament_data;
void update_filament_list();
+5
View File
@@ -106,6 +106,11 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" }, { "FLIP_TRIANGLE_NORMALS"sv });
else
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
// Fast shaded preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
valid &= append_shader("texture_displacement_shaded", { prefix + "texture_displacement_shaded.vs", prefix + "texture_displacement_shaded.fs" });
// UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the
// painted patch, to sanity-check the unwrap.
valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" });
return { valid, error };
}
+44 -10
View File
@@ -173,7 +173,8 @@ bool GLTexture::load_from_svg_file(const std::string& filename, bool use_mipmaps
return false;
}
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy)
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy,
bool use_mipmaps)
{
m_width = w;
m_height = h;
@@ -197,18 +198,51 @@ bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
bool use_mipmaps = true;
if (use_mipmaps) {
// we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards
int lod_w = m_width;
int lod_h = m_height;
// We generate the mipmap chain ourselves rather than calling glGenerateMipmap(), which this
// codebase has historically considered unreliable on some graphics cards.
//
// Each level is a 2x2 box filter of the level above it. Note this used to re-upload the
// *level-0* buffer at every level instead, which does not downscale anything - it just
// reinterprets the image's first lod_w * lod_h texels as the whole smaller level, i.e. every
// level below 0 held a crop of the top-left corner. It went unnoticed for as long as every
// caller drew these textures at roughly their native size (where only level 0 is ever
// sampled); it shows up the moment one is drawn small enough to select a lower level, as a
// texture that visibly turns into something else as it shrinks.
std::vector<unsigned char> scratch;
const std::vector<unsigned char> *src = &data;
int src_w = m_width;
int src_h = m_height;
GLint level = 0;
while (lod_w > 1 || lod_h > 1) {
while (src_w > 1 || src_h > 1) {
++level;
lod_w = std::max(lod_w / 2, 1);
lod_h = std::max(lod_h / 2, 1);
n_pixels = lod_w * lod_h;
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
const int lod_w = std::max(src_w / 2, 1);
const int lod_h = std::max(src_h / 2, 1);
std::vector<unsigned char> lod(size_t(lod_w) * size_t(lod_h) * 4);
for (int y = 0; y < lod_h; ++y) {
// min() rather than a plain 2*y+1: an odd source extent leaves the last output texel
// with only one source row/column to average, not two.
const int y0 = std::min(2 * y, src_h - 1);
const int y1 = std::min(2 * y + 1, src_h - 1);
for (int x = 0; x < lod_w; ++x) {
const int x0 = std::min(2 * x, src_w - 1);
const int x1 = std::min(2 * x + 1, src_w - 1);
for (int c = 0; c < 4; ++c) {
const unsigned int sum = (*src)[(size_t(y0) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y0) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)];
lod[(size_t(y) * size_t(lod_w) + size_t(x)) * 4 + size_t(c)] = (unsigned char)(sum / 4);
}
}
}
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)lod.data()));
scratch = std::move(lod);
src = &scratch;
src_w = lod_w;
src_h = lod_h;
}
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level));
+4 -1
View File
@@ -101,7 +101,10 @@ namespace GUI {
bool load_from_file(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy);
bool load_from_svg_file(const std::string& filename, bool use_mipmaps, bool compress, bool apply_anisotropy, unsigned int max_size_px);
//BBS load GLTexture from raw pixel data
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false);
// `data` is RGBA, w * h * 4 bytes. With use_mipmaps, a real box-filtered mipmap chain is
// built, so the texture may safely be drawn smaller than its pixel size.
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false,
bool use_mipmaps = true);
// meanings of states: (std::pair<int, bool>)
// first field (int):
// 0 -> no changes
+5
View File
@@ -8882,6 +8882,11 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch
if (active_preset_combox)
tab->reactive_preset_combo_box();
}
// Preset loading can resize the filament list without an extruder-count change event.
// Refresh the controls even when the list already matches the printer's nozzle count.
if (printer_technology == ptFFF)
this->plater()->on_filament_count_change(preset_bundle->filament_presets.size());
// BBS: model config
for (Tab *tab : model_tabs_list)
if (tab->supports_printer_technology(printer_technology)) {
+2 -1
View File
@@ -27,7 +27,8 @@ enum class PainterGizmoType {
FDM_SUPPORTS,
SEAM,
MM_SEGMENTATION,
FUZZY_SKIN
FUZZY_SKIN,
TEXTURE_DISPLACEMENT
};
class TriangleSelectorGUI : public TriangleSelector {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,946 @@
#ifndef slic3r_GLGizmoTextureDisplacement_hpp_
#define slic3r_GLGizmoTextureDisplacement_hpp_
#include "GLGizmoPainterBase.hpp"
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLTexture.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/IconManager.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include <array>
#include <atomic>
#include <map>
#include <memory>
#include <string>
namespace Slic3r::GUI {
class TextureProjectorFrame;
// Paint-style gizmo that assigns one or more texture-displacement "layers" (see
// libslic3r/TextureDisplacement.hpp) to painted areas of a model, and can bake the result into
// real mesh geometry. See the project plan for the overall architecture; in short:
// - each layer owns its own independent paint mask (ModelVolume::texture_displacement_facets),
// reusing the same TriangleSelector/FacetsAnnotation machinery as every other paint gizmo -
// only one layer is "active" (paintable) at a time, selected in the panel below;
// - "Bake" runs build_texture_displacement() in a background job and commits the result exactly
// like the Emboss/SVG "project on surface" gizmo does.
class GLGizmoTextureDisplacement : public GLGizmoPainterBase
{
public:
GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
// The whole of mesh preparation - remesh, carry the paint across, refine where the texture bends -
// as one pure function over plain data: no ModelVolume, no Model, no GUI, no undo. That is what lets
// TextureDisplacementPrepareJob run it on the job worker instead of on the UI thread, where CGAL's
// remesher and a several-hundred-thousand-triangle refinement together freeze the window for tens
// of seconds with nothing to look at and no way to cancel.
//
// `progress` is called with 0..100 and aborts the run when it returns false; an aborted run reports
// an empty result. An empty result is also how "nothing needed doing" is reported - see
// TextureDisplacementPrepareResult.
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementPrepareParams &params,
const std::vector<PrintableColor> &palette,
const DisplacementProgressFn &progress,
// Optional step capture: receives the mesh
// after the remesh and after the refinement,
// so a debug run can show the whole recipe.
BakeStageRecorder *debug = nullptr);
// The volume's eight texture-displacement masks, gathered into the array every pure function here
// (and every job input) takes.
static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv);
using PaletteEntry = PrintableColor;
// The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can
// address), plus - when `mixing` - every pair of them at evenly spaced ratios.
//
// Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too
// finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and
// blue banded together read as a desaturated grey-green, and that is what the preview must promise
// - blending them subtractively would show a green the printer cannot produce this way.
//
// How many ratios depends on how many filaments there are, so the palette stays bounded: the
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
// filaments there are already plenty of colours without mixing any of them.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing);
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
// a grey of similar brightness).
//
// Precomputed into a lookup cube rather than matched per call: the subdivision's colour criterion
// samples up to seven points per triangle and re-samples both children of every split, so a live
// match would dominate the refinement. The returned closure owns the cube, so it is safe to hand
// to a worker thread and outlives the palette it was built from.
static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette);
// Turns a palette index plus a position into the filament to print there, interleaving the two
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither
// cells. See ColorResolveFn for why this is separate from the quantizer.
static ColorResolveFn make_mix_resolver(const std::vector<PaletteEntry> &palette, ColorMixMode mode,
float layer_height, float cell_mm);
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
// height, despeckle. Empty when no layer is actually colouring.
TextureColorSettings color_settings_for(const ModelVolume &mv);
// The printable palette for the current filaments and mixing setting, rebuilt only when either
// actually changes - see the definition for why that caching is not optional.
const std::vector<PaletteEntry> &cached_palette();
std::vector<PaletteEntry> m_palette_cache;
std::vector<ColorRGBA> m_palette_filaments;
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer;
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
static std::vector<ColorRGBA> filament_palette();
// The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be
// read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything.
static float print_layer_height();
// The Z band height, in mm. One print layer is the ideal, but the interleave is realised per
// *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle
// grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour,
// it does so in the print too, not only on screen. The refinement edge is chosen from the model's
// diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of
// layers at least two facet rows tall: still exact on the printer, and representable by the mesh
// that has to carry it. Used by both the bake settings and the preview shader, so the two agree.
float color_band_mm(const ModelVolume &mv);
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer
// sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex
// colour attribute, and so no change to GLModel's vertex layouts.
//
// The *index buffer* is what gets reordered, never m_preview_its: the paint overlay and the
// wireframe index into that by the volume's own triangle numbering (the bake is
// topology-preserving), and permuting it would silently misplace both.
struct PreviewColorRun
{
std::pair<size_t, size_t> range; // into the GLModel's index buffer, in elements
ColorRGBA color;
};
std::vector<PreviewColorRun> m_preview_color_runs;
// True if any of the volume's layers would actually colour something: colour turned on, and a
// texture that has colour to give. What decides whether a palette is captured into a job at all,
// and so whether the colour criterion and the mmu write ever run.
static bool any_layer_colors(const ModelVolume &mv);
void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
// rotation handles instead of painting); otherwise forwards to the normal painting handling.
bool on_mouse(const wxMouseEvent &mouse_event) override;
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
std::string get_gizmo_entering_text() const override { return _u8L("Entering Texture displacement painting"); }
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Texture displacement painting"); }
std::string get_action_snapshot_name() const override { return _u8L("Texture displacement editing"); }
// The panel's Paint / Erase toggle swaps what the two buttons do, so the right button always does the
// opposite of the left. Shift still erases in both modes (GLGizmoPainterBase::gizmo_event()).
EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; }
EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; }
private:
bool on_init() override;
void update_model_object() override;
void update_from_model_object(bool first_update) override;
void on_opening() override {}
void on_shutdown() override;
PainterGizmoType get_painter_type() const override;
// Phase 1 restricts the texture layer list to the first model-part volume of the current
// object (the common single-volume case); multi-part objects only get texture layers on
// their first part until a later phase. Returns nullptr if there is no model part.
ModelVolume* texture_volume();
const ModelVolume* texture_volume() const;
void add_texture_layer();
void remove_texture_layer(int slot);
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
// the slot each one occupies, so their paint masks move with them on every model part.
void move_texture_layer(int slot, int to_index);
// Exchanges everything two slots hold - layer definition, paint masks, panel caches. No snapshot and
// no selector reload; move_texture_layer() does both once around a run of these.
void swap_layer_slots(int slot_a, int slot_b);
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
// `own_snapshot` false when the caller has already taken an undo step that is meant to cover the
// displacement too - see bake_standard().
void bake(bool own_snapshot = true);
// Standard (0) vs Pro (1), driven by the two-position slider in the panel header.
//
// Pro is the panel as it has always been: every geometry-preparation control is visible and the
// user drives Remesh, Subdivide and Bake themselves, in whatever order they like. Standard hides
// all of that, pins it to one fixed recipe, and folds it into the Bake button - paint, press Bake,
// done - so the common case does not require knowing that a height map can only move vertices that
// already exist. Nothing about Pro changed when Standard was added.
int m_panel_mode = 0;
bool pro_mode() const { return m_panel_mode != 0; }
// Pins every control Standard mode hides to its preset value. Idempotent, called each frame while
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
// changed something, so the caller can invalidate the preview.
bool apply_standard_mode_presets(ModelVolume *mv);
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
// The order matters and is the whole reason this is one button - a height map can only move
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
// the paint if it were not carried across (see prepare_mesh()).
void bake_standard();
// Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every
// mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's
// Bake differ only in which stages `params` enables and in what happens afterwards:
// - `snapshot_name` is the single undo step the commit opens. With `then_bake` it is also the step
// the displacement job that follows commits into, rather than pushing its own - an undo landing
// between the two would leave a mesh carrying every added triangle and no relief on it, and
// baking again from there would prepare it a second time.
// - `unchanged_msg`, when not empty, is shown if the run had nothing to do. Standard's Bake passes
// nothing: a mesh that already meets the criteria is not an error there, it just goes straight
// on to the displacement.
void queue_prepare(const TextureDisplacementPrepareParams &params, const std::string &snapshot_name,
bool then_bake, const std::string &unchanged_msg);
// Set from queue_prepare() until its job's result has been committed. Distinct from
// m_bake_in_progress because Standard's Bake sets both in turn, and because every button that would
// read or replace the mesh has to stay disabled for the whole of it.
bool m_prepare_in_progress = false;
// How one layer's paint sits on the pre-subdivision mesh, precise enough to carry across the
// refinement without rounding each source triangle to wholly painted or not.
//
// Rounding is what made the outline of a painted region come out ragged: a source triangle near a
// smooth brush boundary is wholly painted essentially at random, so "painted iff the source was
// full" turns a clean curve into a noisy fringe of isolated painted and unpainted triangles - and
// once the border band refines the mesh there, that fringe is reproduced faithfully instead of
// being blurred away by coarse geometry.
struct LayerPaintMap
{
std::vector<uint8_t> full; // per source triangle: covered edge to edge
std::vector<int> part_start; // CSR offsets into `part`, size (source tris + 1)
std::vector<std::array<Vec3f, 3>> part; // painted pieces of partly covered source triangles
bool empty() const { return full.empty(); }
};
// Rebuilds every layer's mask on a subdivided mesh from `source` (new triangle -> the input triangle
// it descends from) and the pre-subdivision coverage in `paint`.
static TextureDisplacementFacetsData masks_after_subdivision(
const TriangleMesh &new_mesh, const std::vector<int> &source,
const std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> &paint);
// False means the remesh failed or changed nothing (CGAL signals failure by handing the input back),
// and `out` must not be used. `target_edge_mm` is a request rather than a promise: it is clamped
// against the part's surface area first, because CGAL's cost grows with the square of 1/target.
static bool plan_remesh(const indexed_triangle_set &src, float target_edge_mm, float sharp_angle_deg,
indexed_triangle_set &out);
// Marks every facet of every model-part volume as painted for the currently active layer -
// "whole model" as an alternative to brushing/clicking every triangle by hand.
void select_whole_model();
// The mesh raycasters are built one per model-part volume, in that order; this is the texture
// volume's slot among them, or -1 if it has none (no selection, or the lists disagree).
int texture_volume_raycaster_index() const;
// Paints exactly the facets currently visible from the camera onto the active layer, replacing
// whatever that layer had painted. "Visible" is two tests: the facet faces the camera, and its
// centroid is not hidden behind other geometry (a real raycast, so a concave part's far inner
// wall is correctly excluded). When `uv_clip` is given (the projection frame's matrix), facets
// whose centroid falls outside the frame's uv unit square are skipped first - which both clips
// the selection to the frame and spares the raycast for everything outside it. Costs one ray
// query per surviving facet, so it is a one-shot action, never a per-frame one. Returns the
// number of facets selected.
int select_visible_faces(const std::array<float, 12> *uv_clip = nullptr);
// When set, "Capture current view" also re-selects the visible faces, so the viewpoint the
// projector was captured from and the area it projects onto stay the same. Independent of the
// projection frame below: this takes every visible facet, the frame clips to its rectangle.
// Off by default, because turning it on replaces whatever the layer had painted.
bool m_project_only_visible = false;
// The projection-frame overlay for a ViewProjected layer: a semi-transparent window dragged over
// the 3D view whose border becomes the projection's edge. Created lazily and owned here; hidden
// rather than destroyed when closed, so reopening keeps it where the user left it.
TextureProjectorFrame *m_projector_frame = nullptr;
int m_projector_opacity = 140;
// What the overlay's texture was last built from, so repeated updates don't rebuild the bitmap
// from unchanged pixels. Same shape as the m_thumbnail_source/m_thumbnail_smoothing pair above.
const void *m_projector_tex_source = nullptr;
float m_projector_tex_smoothing = -1.f;
void show_projector(bool show);
// Pushes the active layer's texture into the overlay. Cheap, and a no-op while it is hidden.
void update_projector();
// Reads the overlay's rectangle and commits it as the layer's projection: builds the exact
// projective local->uv matrix from the camera and that rectangle, turns tiling off so the border
// is a hard edge, and repaints the layer with the visible facets inside the frame. Returns the
// number of facets selected, or -1 if the frame could not be used at all.
int apply_projection_frame();
// Uniformly subdivides the volume's mesh (see libslic3r::subdivide_mesh_uniform()) so a
// low-poly input model has enough vertices to actually show texture-displacement detail.
// A real, committed geometry change (like Bake), so it needs its own snapshot; unlike Bake it
// has no target region, so any not-yet-baked paint on the volume is dropped rather than
// remapped (texture-displacement paint has no remap-across-topology-change support yet).
void subdivide_model();
// The layer height map's width / height, for apply_uv_transform()'s non-square handling. 1 when
// there is no usable texture.
static float layer_texture_aspect(const TextureDisplacementLayer &layer);
// Returns a cached GPU thumbnail of layer's texture (decoding + uploading it the first time it
// is requested, or whenever its image_data changes), or nullptr if it has no usable texture.
// Panel-sized: box-filtered down to THUMBNAIL_MAX_PX, which is right for a list row and wrong for
// anything the shader samples - see get_layer_height_texture().
GLTexture *get_layer_thumbnail(const TextureDisplacementLayer &layer);
// The same texture at full resolution, for the fast-preview shader. One slot, shared by whichever
// layer is active, because that is the only one the preview shader ever shades.
GLTexture *get_layer_height_texture(const TextureDisplacementLayer &layer);
// The layer's colour texture for the fast preview's per-fragment quantization. Null when the
// layer is not colouring or its texture is grayscale.
GLTexture *get_layer_color_texture(const TextureDisplacementLayer &layer);
// A texture from the picker's library (see slic3r/GUI/TextureLibrary.hpp), read and uploaded
// once and then kept for the gizmo's lifetime. The decoded bytes are held alongside the GPU
// thumbnail so that picking the texture can hand the layer this very same image_data buffer -
// which both avoids re-reading the file and lets decode_height_texture()'s own cache (keyed by
// exactly this pointer) hit immediately on the first bake/preview.
struct LibraryTexture
{
std::shared_ptr<std::vector<unsigned char>> image_data;
std::unique_ptr<GLTexture> thumbnail;
};
const LibraryTexture *get_library_texture(const std::string &path);
// The texture library: a popup grid of thumbnails beside the panel, shipped textures and the user's
// own under separate headings, with a tile that imports an image from disk. Opened for
// m_picker_slot by a click on a layer's thumbnail or name. `panel_min`/`panel_max` are the panel
// window's screen rectangle, which the popup is placed against.
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
int m_picker_slot = -1;
bool m_picker_open_request = false;
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer);
// Paint / Erase, from the panel. See get_left_button_state_type().
bool m_erase_mode = false;
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
// Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
// "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
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
// the centroid/average-normal of the active layer's current paint patch (see
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
//
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
// the texture should appear to move as the handle is dragged - it could not be visually
// confirmed while writing it (no way to render/see pixels in this environment), so it may
// need a one-line sign flip once actually tested.
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
void render_adjust_texture_gizmo();
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint();
// Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
// dominant-axis convention so dragging on-canvas maps consistently onto offset.
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
// Deliberately *fixed* - independent of the layer's offset - so that moving the handle cannot
// move the plane the handle's own motion is derived from, which would be a feedback loop.
Vec3f adjust_plane_point() const;
// Where the handle is actually drawn, in mesh-local coordinates. This is NOT just the patch's
// centroid: the handle *represents the texture's placement*, so it has to travel as `offset`
// changes. Pinning it to the centroid is why dragging it looked broken - the texture slid but
// the handle stayed put. Undoing apply_uv_transform()'s scale and rotation turns the layer's
// offset back into a displacement in mm within the patch's tangent plane, which is what gets
// added to the anchor here. That is exactly consistent with the drag arithmetic in
// on_mouse_adjust_texture(): the handle then tracks the cursor 1:1, and sits back on the anchor
// precisely when offset is zero.
Vec3f adjust_handle_center(const TextureDisplacementLayer &layer) const;
// The layer painted by the active slot, or nullptr if that slot has no layer yet.
TextureDisplacementLayer *active_layer();
const TextureDisplacementLayer *active_layer() const;
// Recomputes m_preview_glmodel from the volume's current (unbaked) paint state, using the same
// build_texture_displacement() algorithm as Bake. Called whenever the paint mask changes
// (stroke end, layer switch, undo/redo reload, post-bake refresh) rather than every frame -
// this is real mesh work (PNG sampling, vertex welding), not something to redo per paint stroke
// drag sample or idle repaint. With several painted layers this can be slow, so the actual
// computation runs in a background TextureDisplacementPreviewJob; this function only queues
// it and returns immediately, and m_preview_glmodel is updated later when it completes.
void rebuild_preview();
void render_preview_mesh();
// Alternate, GPU-only preview: perturbs shading normals from the active layer's height texture
// instead of actually moving vertices, using the resources/shaders/*/texture_displacement_shaded.*
// shader. Faster than the true-displacement preview (no CPU meshing at all - just a per-vertex
// paint-weight buffer built at the same cadence as rebuild_preview()) but only shows the *active*
// layer, and the relief it shows is a shading illusion, not real geometry - "Bake" always produces
// the true, exact result either way.
void rebuild_shaded_preview_mesh();
void render_shaded_preview_mesh();
// Feeds the active layer's painted patch + LSCM unwrap (if it's using that projection method)
// into Plater's docked UV-editor pane and shows it, or hides the pane if the active layer
// isn't using LSCM (or nothing is painted). Called whenever something that could change what
// the pane should show happens: paint changes, layer switch, projection method change, bake,
// and on shutdown (to hide it).
void update_uv_editor();
// Applies one island edit reported by the UV editor's drag/rotate gestures to the active layer.
// Deltas are incremental (see UVEditorCanvas::IslandEditFn); `finished` ends the gesture, which
// is when - and only when - the 3D preview is rebuilt, since doing that per mouse-move would
// queue a mesh recompute for every pixel of a drag.
void on_island_edited(int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished);
// Applies a committed vertex/edge edit from the UV editor's Vertex/Edge modes: each entry is an
// unwrapped-vertex index and its new raw-unwrap coordinate. Maps the unwrapped index to a mesh
// vertex and stores a per-vertex UV override on the layer (see lscm_uv_overrides), then rebuilds the
// preview so the baked geometry follows.
void on_uv_vertex_edited(const std::vector<std::pair<int, Vec2f>> &edits);
// UV-editor sub-element select mode, mirrored into the canvas: 0 = Island, 1 = Vertex, 2 = Edge.
int m_uv_select_mode = 0;
// One affine per island (columns: x basis, y basis, translation), mapping the unwrap's raw mm
// coordinates to texture UVs - the same type as UVEditorCanvas::IslandTransform, spelled out
// here so this header needn't drag in wxGLCanvas/glad. Cheap to recompute (it is per *island*,
// not per vertex), which is what lets an island drag update the pane without re-uploading a
// single vertex.
std::vector<Eigen::Matrix<float, 2, 3>> uv_editor_island_transforms(const TextureDisplacementLayer &layer);
// Handles a toolbar command forwarded from the UV pane that needs the layer data the canvas
// doesn't hold (average island scale, cut island). Takes the command as an int (a cast of
// UVEditorCanvas::Command) so this header needn't pull in glad/wxGLCanvas via the canvas header.
void on_uv_command(int cmd, float value);
// Pane commands, queued by on_uv_command() and run from the panel render: pane clicks arrive in wx event
// handlers, outside the 3D canvas's GL frame, and several of these rebuild GPU meshes.
std::vector<std::pair<int, float>> m_uv_command_queue;
void process_uv_commands();
void run_uv_command(int cmd, float value);
// Sends the UV editor pane what its controls show (see UVEditorCanvas::PaneState).
void push_uv_pane_state();
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
// background buttons, so both switch views the same way.
void apply_view_mode(int mode);
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
static constexpr int UV_THUMB_PX = 64;
const void *m_uv_thumb_source = nullptr;
std::vector<unsigned char> m_uv_thumb_rgb;
// Splits one unwrap chart in two by marking the mesh edges that straddle the plane through its
// 3D centroid, perpendicular to its longest axis, as seams (#17). The re-unwrap then separates it.
void cut_island(TextureDisplacementLayer &layer, int chart);
// Captures the current camera's right/up axes into the layer's projector (#6), transformed into
// the volume's local space so the projection is stable as the object is later moved/rotated.
void capture_view_projection(TextureDisplacementLayer &layer);
// Manual seam marking (#9): a mode where clicking the model toggles the nearest mesh edge in the
// active layer's lscm_seam_edges, so the unwrap can be cut exactly where the user wants - the
// Blender "mark seam" workflow. Painting is suppressed while it is on.
bool m_seam_edit_mode = false;
GLModel m_seam_glmodel; // the current seam edges, highlighted on the mesh
bool on_mouse_seam(const wxMouseEvent &mouse_event);
void toggle_seam_at(const Vec2d &mouse_pos);
void rebuild_seam_overlay();
void render_seam_overlay();
// The mesh edge nearest the mouse, in the volume's own vertex indices, or {-1,-1} if the ray misses.
// Factored out of toggle_seam_at() so the same pick can drive a live hover highlight (below) that
// shows which edge a click would toggle - the "I don't know how it works" feedback the user hit.
std::pair<int, int> seam_edge_at(const Vec2d &mouse_pos) const;
std::pair<int, int> m_seam_hover_edge{ -1, -1 };
// The vertex a click would pick in shortest-path mode, so the target is visible on hover the same
// way the edge is in normal mode. -1 when nothing is under the cursor (or not in path mode).
int m_seam_hover_vertex = -1;
GLModel m_seam_hover_glmodel;
void rebuild_seam_hover_overlay();
// Shortest-path seam marking, for dense meshes where clicking every single triangle edge is
// tedious: in this sub-mode a click picks the nearest vertex, and the next click marks every edge
// on the shortest surface path between the two as a seam - so a whole seam line is drawn with two
// clicks. The end vertex becomes the next start, so a multi-segment seam chains click by click.
bool m_seam_path_mode = false;
int m_seam_path_anchor = -1; // mesh vertex the path starts from, or -1
GLModel m_seam_anchor_glmodel; // the anchor's incident edges, highlighted
int seam_vertex_at(const Vec2d &mouse_pos) const; // nearest mesh vertex under the cursor
void mark_seam_path(int v_from, int v_to); // seam every edge on the shortest path
void rebuild_seam_anchor_overlay();
// Set while an island gesture is in flight, so the undo snapshot is taken once at the start of
// the drag (capturing the state *before* it) rather than on every motion event.
bool m_island_drag_active = false;
// Which of the up to TEXTURE_DISPLACEMENT_MAX_LAYERS paint masks the brush currently writes
// into. Always a valid slot index (0 by default) so the base class's per-volume selector
// machinery always has something to work with, even before any texture has been added -
// painting into a slot with no texture assigned is harmless, it just has no visible/bake
// effect until a texture is added to that slot.
int m_active_layer_slot = 0;
// Set when m_triangle_selectors could not be loaded from the stored paint masks, because a mask
// was recorded against a different topology: TriangleSelector::deserialize() rejects that and
// returns without a word, leaving the selector empty even though the mask is not. While this is
// set, an *empty* selector says nothing about the paint, so update_model_object() must not flush
// one back - serializing it over the mask destroys the user's paint for good, and the bake then
// reports "nothing is painted" about the data the flush had just deleted.
//
// Deliberately not a blanket refusal to flush: once the user paints, the selector holds real
// content again and writing it back is exactly right - it replaces the unusable mask with one
// recorded against the current mesh. So only the empty-over-non-empty case is held back.
bool m_selectors_stale = false;
bool m_bake_in_progress = false;
// When set, the true-displacement geometry is rebuilt on every parameter change (live), instead of
// only once the slider being dragged is released. On by default so painting/added textures show
// straight away without needing to nudge a slider first.
bool m_auto_update = true;
// Subdivision is now count-based (split the whole mesh 1..5 times) rather than a target edge
// length, and is previewed as a wireframe before it is committed: nothing is written to the model
// until "Apply". While previewing, the would-be subdivided mesh is drawn as a wireframe overlay so
// the added density is visible; "Done" ends the preview without touching the model. The normal
// "Show mesh wireframe" toggle is left alone, so a wireframe the user already had on stays on.
// 0 is a real value meaning "no subdivision": it previews nothing and Apply is a no-op. Apply
// snaps the slider back to it, because each pass quadruples the triangle count - leaving the
// count where it was would immediately re-preview N more passes on top of the mesh that was just
// committed, i.e. the most expensive thing the panel can do, on every Apply.
int m_subdivide_count = 1;
bool m_subdivide_editing = false;
int m_subdivide_preview_tris = -1; // triangle count of the previewed result, shown in the panel
GLModel m_subdivide_preview_glmodel;
void rebuild_subdivide_preview();
void render_subdivide_preview();
// Adaptive subdivision: refine only the painted area, down to a target edge length, via
// conformal longest-edge bisection (subdivide_mesh_adaptive()). Unlike the count-based uniform
// path it does not touch the unpainted rest of the model, and - because it is driven by the paint
// - it can carry that paint forward across the topology change (children of a painted triangle
// are painted), so the region survives the subdivision instead of being dropped.
bool m_subdivide_adaptive = false;
float m_subdivide_target_mm = 0.f; // 0 = not yet seeded; filled from the mesh on first show
// Feature-adaptive sub-mode: put the triangles where the *displaced surface* bends (texture
// curvature) rather than spreading them evenly. `detail_mm` is the chord-error tolerance ("Detail"
// slider: how far the true surface may sit off the flat triangle before it is split); the target
// above stays in play as a coarse baseline ("Max edge"), and `min_edge_mm` is the hard floor
// ("Min edge"). See subdivide_mesh_adaptive().
bool m_subdivide_feature = false;
float m_subdivide_detail_mm = 0.05f;
float m_subdivide_min_edge_mm = 0.1f;
// Edge length the band straddling the paint's boundary is refined to (0 = leave it alone). Applies
// in both adaptive sub-modes, because it is not a texture-detail criterion: the bake steps the
// surface from full displacement to zero across that boundary whatever the texture is doing, and
// the chord-error test cannot see that step at all - its sampler has no per-point paint test, so
// just outside the paint it goes on reporting the same smooth height field. Without this the
// transition keeps the input's density and the rim of an unpainted island comes out as a ring of
// large, steeply tilted triangles. See collect_paint_region() and subdivide_mesh_adaptive().
float m_subdivide_border_mm = 0.4f;
// Edge length triangles straddling a *colour* boundary are refined to (0 = ignore colour). Its own
// control rather than a share of "Detail (mm)" because the two measure different things: Detail is
// a chord error in mm of surface deviation, this is a triangle size in mm along a step the chord
// test cannot see at all - the height field is perfectly smooth across a change of filament, so
// without this a colour boundary lands on whatever triangles the relief happened to need, which on
// a flat surface is none. Only ever costs anything where a boundary actually runs.
float m_subdivide_color_mm = 0.3f;
// How many thousand triangles refinement may *add* (the mesh's own count is added on before it is
// passed as subdivide_mesh_adaptive()'s absolute cap, so the control still means something on a
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
// that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from
// turning into an out-of-memory, or an unrenderable preview wireframe.
//
// The default used to be 1500 (i.e. +1.5 M triangles), which is what made Standard mode's Bake
// take minutes: every stage after the subdivision - the displacement itself, the convex hull, the
// GLModel upload, and the re-slice changed_object() triggers - then runs on a mesh two orders of
// magnitude denser than the input. 750k is still far finer than any FDM nozzle resolves at the
// 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more.
int m_subdivide_budget_k = 750;
void subdivide_model_adaptive();
// Fills `region` (per current-mesh triangle, a REFINE_* bitmask) from the union of every layer's
// painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer
// coverage map the subdivision carries forward - the expensive half, skipped by the live preview,
// which only needs the region. Returns false when nothing is painted at all.
static bool collect_paint_region(const TriangleMesh &mesh, const TextureDisplacementFacetsData &facets,
std::vector<uint8_t> &region,
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint);
// Runs the volume's TextureDisplacementOptions smoothing over the *already committed* geometry,
// restricted to the painted area. The same settings are folded into Preview/Bake automatically;
// this is the escape hatch for relief that has already been baked in, where there is no
// displacement pass left to attach them to. Topology-preserving, so unlike subdivide and remesh it
// keeps every paint channel - including texture displacement - exactly as it was.
void smooth_model();
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
// the geometry, but unlike subdivide it keeps every paint channel: prepare_mesh() carries the
// texture-displacement masks across spatially, which is also what lets Standard mode remesh *after*
// the user has painted.
float m_remesh_target_edge_mm = 0.f;
// Dihedral angle above which an edge counts as a hard feature and is held fixed by the remesher.
// Off by default would round every sharp edge off, so this is on; 0 disables the protection.
float m_remesh_sharp_angle_deg = 40.f;
bool m_remesh_keep_sharp_edges = true;
void remesh_model();
// ---- Bake stage debug view ----
//
// A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the
// only useful question is which stage made it wrong. "Capture stages" runs the same recipe Bake
// runs, keeps every intermediate mesh, and draws the selected one through the ordinary
// true-displacement preview - so this needs no rendering code of its own.
//
// The run commits nothing: TextureDisplacementDebugJob never touches the Model.
std::vector<BakeStageSnapshot> m_debug_stages;
// Which stage is on screen. -1 means the debug view is off and the live preview owns
// m_preview_glmodel again; rebuild_preview() checks this before replacing it.
int m_debug_stage = -1;
bool m_debug_in_progress = false;
// The open / non-manifold counts come from 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 tore the mesh
// is exactly what this exists to find.
bool m_debug_check_topology = true;
// World-millimetre area of everything painted on `mv`, cached on the mesh and the paint generation:
// it is what the refinement has to cover, so it is what says whether a budget can pay for a
// resolution. 0 when nothing is painted.
double painted_area_mm2(const ModelVolume &mv);
double m_painted_area_mm2 = 0.;
std::string m_painted_area_key;
// Triangles the refinement needs to reach `edge_mm` over that area. Bisection converges to four of
// them to a square of the target edge - measured within 3% on a test model over a 5x range of
// resolutions - but the real count depends on the shape of the triangles it starts from, so this
// is only ever used to say a budget is clearly too small, never as a promise of what will come out.
size_t estimated_refined_triangles(const ModelVolume &mv, float edge_mm);
// The default pipeline's automatic resolution/budget for the volume, cached on what it depends on.
const V2Resolution &v2_recommendation(const ModelVolume &mv);
V2Resolution m_v2_rec;
std::string m_v2_rec_key;
// Queues the capture run. Uses Standard mode's recipe (remesh, refine, displace) unless Pro mode
// has already prepared the mesh or the experimental pipeline is on, either of which has nothing
// to prepare.
void run_stage_debug();
// Puts stage `index` on screen. A stage over the memory cap has counts but no geometry and is
// skipped.
void show_debug_stage(int index);
// Drops the captured stages and hands m_preview_glmodel back to the live preview.
void exit_debug_view();
// The panel: capture button, stage list, and what each stage produced.
void render_debug_stage_panel(ModelVolume *mv);
// Live, pre-bake preview of the true displaced geometry (built by the same algorithm Bake
// uses). Empty/uninitialized whenever nothing is painted yet, in which case the gizmo falls
// back to the standard paint-mask overlay like every other painting gizmo.
GLModel m_preview_glmodel;
// Set while a layer parameter slider has changed since the last rebuild_preview() call but the
// mouse button driving the drag hasn't been released yet - see on_render_input_window().
bool m_preview_params_dirty = false;
// See rebuild_shaded_preview_mesh()/render_shaded_preview_mesh(). Off by default: the Normal
// (true-displacement) view shows the real geometry Bake will produce, across every layer, so it is
// the honest first impression. The cheap, instant-updating Fast view is one click away in the View
// row for painting on a heavy model.
bool m_use_shaded_preview = false;
// Set from the UV editor's per-move island edits instead of rebuilding the (potentially large) shaded
// mesh synchronously inside that mouse handler - doing the rebuild there stalled both the UV pane
// and the 3D view. The rebuild is instead coalesced to once per 3D frame (render_painter_gizmo).
bool m_shaded_preview_dirty = false;
GLModel m_shaded_preview_glmodel;
// Translucent tint over the active layer's painted triangles, drawn on top of whichever preview
// is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used
// in either preview mode - it is coincident with the surface and simply covers it - so the only
// paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all
// until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
// stroke.
GLModel m_paint_overlay_glmodel;
// The islands selected in the UV editor, tinted on the model so the pane's selection can be seen
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
GLModel m_island_overlay_glmodel;
std::vector<int> m_island_overlay_selection;
void rebuild_island_overlay(const std::vector<int> &selection);
void render_island_overlay();
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
// separate from m_shaded_preview_dirty so a stroke refreshes only the small painted patch per frame,
bool m_paint_overlay_dirty = false;
void rebuild_paint_overlay();
void render_paint_overlay(GLModel &overlay);
// Every *other* layer's paint, muted, so all layers stay visible while one is edited. Rebuilt only when that
// paint, the active layer or the preview it is lifted onto changes (m_other_paint_key).
GLModel m_other_paint_glmodel;
std::string m_other_paint_key;
void rebuild_other_paint_overlay();
// Whether render_shaded_preview_mesh() would actually draw something. Checked before the real volume
// is hidden: with no layer, no texture or no shader the shaded path draws nothing, and hiding the
// volume for it left the model invisible.
bool shaded_preview_ready() const;
// Whether the current displacement mesh carries a precomputed per-vertex uv (LSCM) that the shader
// should sample at directly, rather than projecting in-shader. Set by rebuild_shaded_preview_mesh().
bool m_shaded_preview_uses_vertex_uv = false;
// The projection frame handed to the preview shader, captured when the mesh is built. Cylindrical and
// Spherical are reconstructed in the fragment shader (there is no per-vertex uv for them) and wrap
// around the whole patch, which no fragment can work out for itself. See layer_projection_frame().
int m_shaded_projection_mode = 0;
Vec3f m_shaded_patch_center = Vec3f::Zero();
Vec3f m_shaded_patch_axis = Vec3f::UnitZ();
// The palette the fast preview's per-triangle filament indices were built against, captured when
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked
// into the mesh can never be resolved against a different set of filaments than they were computed
// from - loading a filament mid-session would otherwise recolour a stale preview at random.
std::vector<PaletteEntry> m_shaded_preview_palette;
// GPU island drag: while an island is dragged in the UV editor, the displacement mesh is baked once (with
// the dragged island's vertices flagged, v_normal.y = 1) and then moved purely through the shader's
// island_delta uniform - one uniform update per mouse move, no rebuild - so it tracks the cursor
// as smoothly as Adjust placement. m_shaded_active_chart is the dragged island (or -1);
// m_shaded_active_face flags the dragged islands' *triangles*, indexed by painted-patch face;
// m_shaded_baked_active_xf is that island's placement baked into the current mesh, against which the
// live delta is measured; m_shaded_island_delta is the resulting final-uv-space affine handed to the
// shader (identity except mid-drag).
//
// Per triangle rather than per vertex deliberately: a seam vertex belongs to every chart touching
// it, so flagging the dragged chart's base vertices also flagged the corners its neighbours use.
// island_active is an interpolated varying, so those neighbouring triangles then had island_delta
// applied too - dragging one island moved every adjacent island's texture while the editor, which
// is per chart, correctly moved only the one. A triangle belongs to exactly one chart.
int m_shaded_active_chart = -1;
std::vector<uint8_t> m_shaded_active_face;
Eigen::Matrix<float, 2, 3> m_shaded_baked_active_xf = Eigen::Matrix<float, 2, 3>::Identity();
Eigen::Matrix<float, 2, 3> m_shaded_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
// Flags `charts`' triangles in m_shaded_active_face, sized to `patch_face_count` (the painted patch
// the displacement mesh is being built from). Cleared if the unwrap carries no face map.
void compute_shaded_active_faces(const std::vector<int> &charts, size_t patch_face_count);
// The set of islands the current UV-editor drag moves together: the pane's multi-selection unioned
// with each selected island's join group (see build_island_move_set()). Populated at drag start and
// cleared when it finishes. A move applies the same offset to every island in it; rotate/scale act
// only on the primary. Empty when no move drag is in flight.
std::vector<int> m_island_move_set;
// All islands that must move with `primary`: the pane's multi-selection plus, for each of those, the
// charts sharing its join group in `layer`. Always contains `primary`.
std::vector<int> build_island_move_set(const TextureDisplacementLayer &layer, int primary) const;
// The join-group id of chart `c`: its explicit entry in `groups`, or `c` itself (its own singleton)
// when unset. Two charts move together iff this matches.
static int island_group_of(const std::vector<int> &groups, int c);
// 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) 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;
// 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;
// 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
// can never wrap around a different centre, or pick a different axis, than the bake will.
int layer_projection_frame(const indexed_triangle_set &local_patch, const TextureDisplacementLayer &layer,
Vec3f &center, Vec3f &axis) const;
// `patch` with its vertices moved into world millimetres - the space the bake maps the texture in
// (see build_texture_displacement()). Returned by value because the caller usually still needs the
// original: the patch doubles as render geometry, which is drawn through the volume's own matrix.
indexed_triangle_set patch_in_world(const indexed_triangle_set &patch) const;
// UV-check overlay drawn over the painted patch to sanity-check the unwrap (#13/#14). Built by
// rebuild_uvcheck_mesh(), drawn by render_uvcheck_mesh() with the "texture_displacement_uvcheck"
// shader. Checker works for any projection; Distortion needs the per-vertex LSCM uv.
enum class UVCheckMode { None, Checker, Distortion };
UVCheckMode m_uv_check_mode = UVCheckMode::None;
GLModel m_uvcheck_glmodel;
bool m_uvcheck_uses_vertex_uv = false;
// As m_shaded_projection_mode and friends, for the Checker overlay.
int m_uvcheck_projection_mode = 0;
Vec3f m_uvcheck_patch_center = Vec3f::Zero();
Vec3f m_uvcheck_patch_axis = Vec3f::UnitZ();
void rebuild_uvcheck_mesh();
void render_uvcheck_mesh();
// The UV editor pane is opened only on the user's explicit request (this toggle in the panel),
// never automatically just because a patch exists - auto-popping it whenever there was "a
// selection to process" is exactly what the user asked to stop. update_uv_editor() keeps the pane
// hidden unless this is set. Reset on gizmo shutdown so reopening the gizmo doesn't reopen the pane.
bool m_show_uv_editor = false;
// The unwrap is expensive, so it is recomputed only when the user explicitly asks for it (the
// "Unwrap" button), not on every paint stroke or slider nudge. This is set by that button and
// consumed by the next update_uv_editor() call, which is the only path that re-solves the unwrap;
// every other call merely refreshes the cheap per-island affine transforms over the existing one.
bool m_uv_unwrap_pending = false;
// Set alongside m_uv_unwrap_pending only by the Unwrap button, so the connected-net auto-layout runs
// on a genuine re-unwrap but not on a refresh re-solve (a vertex-edit commit or undo), which must
// leave island placements untouched.
bool m_uv_apply_connected_net = false;
// Signature of the per-vertex UV overrides last reflected in the pane. When it changes without the
// user pressing Unwrap - a vertex/edge edit committing, or an undo/redo reverting one - the pane
// is re-solved so its geometry follows, even though a plain edit otherwise never re-solves (#Feat2).
size_t m_uv_overrides_sig = 0;
// What the UV pane's background currently holds, so update_uv_editor() only re-uploads it when the
// choice actually changes (the height texture is large; re-sending it every stroke would be waste).
enum class UVBackground { None, Height, Checker };
UVBackground m_uv_editor_bg = UVBackground::None;
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
// while the Distortion check mode is on. Empty otherwise.
std::vector<ColorRGBA> m_uv_editor_distortion_colors;
void compute_uv_editor_distortion_colors(const indexed_triangle_set &patch);
// Plain triangle-edge overlay on the mesh (#8), toggled independently of the check modes.
bool m_wireframe_overlay = false;
GLModel m_wireframe_overlay_glmodel;
size_t m_wireframe_overlay_vcount = 0; // topology signature, so it rebuilds only on a real change
void rebuild_wireframe_overlay(); // from the base mesh (shaded/paint mode)
void build_wireframe_from_its(const indexed_triangle_set &its); // from an explicit mesh, no early-out
void refresh_wireframe(); // pick base vs displaced source for the current view
void render_wireframe_overlay();
// The displaced preview geometry the last preview job produced, kept so the wireframe overlay can be
// drawn on the raised surface actually shown in the true-displacement view (#: "wireframe in real mode").
indexed_triangle_set m_preview_its;
// Raised on every rebuild_preview() call; a background TextureDisplacementPreviewJob's result
// is only applied if this hasn't moved on since the job was queued (see rebuild_preview()),
// so a burst of edits can't have an earlier, now-stale job clobber a later one's result.
//
// Shared with the worker thread (hence the atomic) so a running job can notice mid-computation
// that it has been superseded and abort, instead of running to completion for a result that will
// only be discarded on arrival.
std::shared_ptr<std::atomic<uint64_t>> m_preview_generation = std::make_shared<std::atomic<uint64_t>>(0);
// At most one preview job is ever queued. The UI job worker is a single FIFO queue shared with
// Bake (and with arrange/orient/send), and rebuild_preview() is called on every stroke end, every
// slider release and - with "Auto update" on - every frame of a slider drag. Queuing one full
// displacement per call built a backlog that took minutes to drain: the preview appeared frozen,
// and a Bake pressed afterwards sat behind the whole queue. So a request made while a job is in
// flight is recorded here and issued once that job settles, collapsing any number of edits into a
// single follow-up run.
bool m_preview_job_running = false;
bool m_preview_job_pending = false;
void queue_preview_job();
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
// Full-resolution height texture for the preview shader, keyed the same way (see
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
// blur the bake will apply.
std::unique_ptr<GLTexture> m_height_tex;
const void *m_height_tex_source = nullptr;
float m_height_tex_smoothing = -1.f;
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
std::unique_ptr<GLTexture> m_color_tex;
const void *m_color_tex_source = nullptr;
float m_color_tex_smoothing = -1.f;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures;
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
// none of this, so re-extracting the patch and re-solving on those edits would be pure waste.
// Held as the real values rather than a hash: TriangleSplittingData has an exact operator==, so
// there is no reason to accept a hash's (however unlikely) chance of showing a stale unwrap.
struct UVEditorState
{
int slot = -1;
const void *image_data = nullptr;
float seam_angle = -1.f;
float padding = -2.f;
TriangleSelector::TriangleSplittingData facets;
// Manual/auto seam edges also change the unwrap, so a change here must force a re-solve just
// like the facets do (marking a seam leaves the paint mask untouched).
std::vector<std::pair<int, int>> seam_edges;
bool operator==(const UVEditorState &other) const
{
return slot == other.slot && image_data == other.image_data && seam_angle == other.seam_angle &&
padding == other.padding && facets == other.facets && seam_edges == other.seam_edges;
}
};
UVEditorState m_uv_editor_state;
// Bounds of the UVs last handed to the pane, purely so the panel can show where the unwrap
// actually landed - it is packed in mm and then divided by the tile size, so it is easy for it
// to end up far outside the texture's first tile without any of that being visible.
Vec2f m_uv_editor_bbox_min = Vec2f::Zero();
Vec2f m_uv_editor_bbox_max = Vec2f::Zero();
// The unwrap m_uv_editor_state produced, kept so that changing tiling/rotation/offset only costs
// re-running apply_uv_transform() over it, not another extraction and solve.
PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
// open/close within a session, so the choice sticks while working.
bool m_undocked = false;
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
// moves a *target* offset instead; the real scroll is eased toward it over the following frames.
float m_panel_scroll_target = 0.f;
float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
// Last frame's body content height and footer height. The body is a child window that has to be
// given its height before its content is laid out, so it is sized from the previous frame: as tall
// as its content, capped so the footer still fits above the bottom of the canvas.
float m_panel_body_h = 0.f;
float m_panel_footer_h = 0.f;
// See the "Adjust Texture" block of private methods above.
bool m_adjust_texture_mode = false;
bool m_adjust_anchor_valid = false;
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
// the corresponding arrow, moves offset along only that one axis.
enum class AdjustHandle { None, Pan, AxisU, AxisV };
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
// moment the current drag started; every subsequent frame's delta is measured against this,
// rather than accumulated frame-to-frame, to avoid drift.
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
// both the U and V axis arrows), transformed into place at render time.
GLModel m_adjust_panel_glmodel;
GLModel m_adjust_arrow_glmodel;
std::map<std::string, wxString> m_desc;
// 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
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
// matching the toolbar's selected/unselected look. Uploaded once on first panel render.
IconManager m_panel_icons;
std::map<std::string, IconManager::Icons> m_panel_icon_map; // file name -> [normal, colour, disabled]
bool m_panel_icons_tried = false;
void ensure_panel_icons();
};
} // namespace Slic3r::GUI
#endif // slic3r_GLGizmoTextureDisplacement_hpp_
+15
View File
@@ -23,6 +23,7 @@
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
@@ -171,6 +172,13 @@ void GLGizmosManager::switch_gizmos_icon_filename()
case(EType::FuzzySkin):
gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg");
break;
case(EType::TextureDisplacement):
// One shared icon in both themes (no dedicated dark variant yet) - but it must still be
// *this* gizmo's icon. Handing it the fuzzy-skin one here quietly replaced the icon set at
// construction, so the toolbar ended up showing two identical fuzzy-skin buttons after any
// light/dark switch.
gizmo->set_icon_filename("toolbar_texture_displacement.svg");
break;
case(EType::MeshBoolean):
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
break;
@@ -228,6 +236,8 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam));
m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::FuzzySkin));
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
@@ -550,6 +560,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
return dynamic_cast<GLGizmoCut3D*>(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == FuzzySkin)
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == TextureDisplacement)
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == MeshBoolean)
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == BrimEars)
@@ -563,6 +575,7 @@ bool GLGizmosManager::is_paint_gizmo()
return m_current == EType::FdmSupports ||
m_current == EType::MmSegmentation ||
m_current == EType::FuzzySkin ||
m_current == EType::TextureDisplacement ||
m_current == EType::Seam;
}
@@ -1531,6 +1544,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
return "Color Painting";
case GLGizmosManager::EType::FuzzySkin:
return "Fuzzy Skin Painting";
case GLGizmosManager::EType::TextureDisplacement:
return "Texture Displacement";
default:
return "";
}
@@ -84,6 +84,7 @@ public:
Seam,
FuzzySkin,
MmSegmentation,
TextureDisplacement,
Emboss,
Svg,
Measure,
+5 -1
View File
@@ -2631,7 +2631,11 @@ void ImGuiWrapper::push_toolbar_style(const float scale)
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 1.00f)); // 10
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 0.00f)); // 11
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, COL_GREEN_LIGHT); // 12
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));//13
// The checkbox/radio frame behind this is drawn fully transparent (see FrameBg above,
// alpha 0), showing the light window background through it - a white check mark there is
// invisible. Dark mode doesn't have this problem (its window background is dark), so only
// this branch needs a check mark color with real contrast against a light background.
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.f, 156 / 255.f, 136 / 255.f, 1.00f));//13
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.42f, 0.42f, 0.42f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));
@@ -0,0 +1,212 @@
#include "TextureDisplacementBakeJob.hpp"
#include <algorithm>
#include "libslic3r/Model.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
namespace Slic3r::GUI {
TextureDisplacementBakeJob::TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function<void()> on_finished)
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementBakeJob::process(Ctl &ctl)
{
const std::string status = _u8L("Baking texture displacement");
ctl.update_status(1, status);
// Only ever touches m_input (captured by value before this job was queued) and local state -
// never the live Model - so this is safe to run concurrently with the UI thread.
//
// The progress hook matters for more than cosmetics: the framework's progress notification only
// grows a close button once it reaches 100%, so a job that reports 0 and nothing else leaves an
// uncloseable notification pinned on screen. It also carries the Cancel button's effect into the
// bake, which on a subdivided mesh can run for several seconds.
// Colour, when any layer asks for it, is computed in the same pass as the displacement: both need
// the same per-layer projection and UV work, and doing it twice would double the expensive part.
TextureColorRequest color_request;
TextureColorRequest *color = nullptr;
if (!m_input.color.empty()) {
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
if (!m_input.color.palette_pure.empty())
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
m_input.color.dither_cell_mm);
color_request.despeckle_passes = m_input.color.despeckle_passes;
color_request.out_triangle = &m_triangle_color;
if (color_request.quantize)
color = &color_request;
}
int last_reported = 1;
m_result = TriangleMesh(build_texture_displacement(
m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
// The notification repaints (and wakes the idle loop) on every call, so only push a
// message when the displayed integer percentage actually moves.
if (percent > last_reported) {
last_reported = percent;
ctl.update_status(percent, status);
}
return true;
},
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.
ctl.update_status(100, status);
}
void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr)
{
struct OnExit
{
std::function<void()> fn;
~OnExit() { if (fn) fn(); }
} on_exit{m_on_finished};
if (canceled || eptr || m_result.empty())
return;
// A bake that moved nothing - no layer could be sampled, or every sample was zero - must not be
// committed: committing is what clears the baked layers' paint, so the user would see the painted
// region simply vanish with no relief in its place and no idea why. Keep the paint and say so.
{
const indexed_triangle_set &out = m_result.its;
bool unchanged = out.indices.size() == m_input.base_mesh.indices.size() &&
out.vertices.size() == m_input.base_mesh.vertices.size();
for (size_t i = 0; unchanged && i < out.vertices.size(); ++i)
unchanged = (out.vertices[i] - m_input.base_mesh.vertices[i]).cwiseAbs().maxCoeff() < 1e-5f;
if (unchanged) {
show_error(nullptr, _u8L("The bake produced no displacement, so nothing was changed and the paint was kept. "
"Check that the painted layer has a texture and a non-zero depth."));
return;
}
}
Plater *plater = wxGetApp().plater();
const auto commit = [this, plater]() {
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
if (volume == nullptr)
return;
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
// 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. 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();
if (!existing.bitstream.empty())
selector.deserialize(existing, false);
for (size_t i = 0; i < m_triangle_color.size(); ++i)
if (m_triangle_color[i] > 0)
selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i]));
volume->mmu_segmentation_facets.set(selector);
}
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
// texture layer definitions themselves are left untouched so the user can keep sculpting with
// the same textures.
//
// A mask the bake did *not* consume only still means what it did if the topology is unchanged,
// which is true of the classic path (it moves existing vertices) but not of the one-run
// 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; });
if (topology_changed || (it != m_input.layers.end() && !it->empty()))
volume->texture_displacement_facet(slot).reset();
}
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
auto it = std::find(objs.begin(), objs.end(), object);
if (it != objs.end())
obj_list->update_info_items(size_t(it - objs.begin()));
}
plater->changed_object(*object);
};
// Standard mode's Bake is remesh -> subdivide -> displace under a single snapshot, and this job runs
// long after that snapshot's scope has closed. Adding one here would put an undo step *between* the
// subdivision and the displacement: the first Undo would land on a mesh carrying every added
// triangle and no relief at all, and pressing Bake again from there would subdivide that mesh a
// second time. So the caller says who owns the undo step.
if (m_input.take_snapshot) {
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction);
commit();
} else {
commit();
}
// The refinement went finer than the budget could keep: the simplification had to take detail back
// out to fit, so what was baked carries less of the texture than the resolution asked for. Said
// here, with the numbers, because it is the only place that knows them - and not as an error
// dialog: the result is a usable mesh, just not the one the settings described.
if (m_stats.budget_limited) {
// Thousands under a million, so a small budget is not reported as "0.1 M".
const auto count = [](size_t n) {
return n >= 1000000 ? Slic3r::format("%1$.1f M", double(n) / 1000000.) :
Slic3r::format("%1% k", (n + 500) / 1000);
};
wxGetApp().notification_manager()->push_notification(
NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel,
Slic3r::format(_u8L("The triangle budget limited the detail: this resolution needs %1% triangles, "
"the budget kept %2%. Raise Budget or use a coarser Resolution for the full detail."),
count(m_stats.triangles_refined), count(m_stats.triangles_budget)));
}
}
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
std::function<void()> on_finished, bool take_snapshot)
{
TextureDisplacementBakeInput input;
input.color = color;
input.take_snapshot = take_snapshot;
input.volume_id = volume.id();
input.base_mesh = volume.mesh().its;
input.layers = volume.texture_displacement_layers;
input.options = volume.texture_displacement_options;
input.volume_to_world = texture_displacement_volume_to_world(volume);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementBakeJob>(std::move(input), std::move(on_finished)));
}
} // namespace Slic3r::GUI
@@ -0,0 +1,70 @@
#ifndef slic3r_TextureDisplacementBakeJob_hpp_
#define slic3r_TextureDisplacementBakeJob_hpp_
#include <functional>
#include <vector>
#include "libslic3r/Color.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "Job.hpp"
namespace Slic3r::GUI {
// Everything process() needs, captured by value on the main thread when the job is queued so the
// worker thread never touches the live Model concurrently with the UI (mirrors how EmbossJob's
// DataBase is captured before process() runs).
struct TextureDisplacementBakeInput
{
ObjectID volume_id;
indexed_triangle_set base_mesh;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementFacetsData facets_data;
TextureDisplacementOptions options;
// Mesh coordinates -> world millimetres. Captured here with everything else so the worker never
// reaches back into the live Model for it. See build_texture_displacement().
Transform3d volume_to_world = Transform3d::Identity();
// Captured on the main thread. Empty unless some layer is colouring, in which case the bake also
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
// paint gizmo writes - alongside the displaced geometry.
TextureColorSettings color;
// 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.
bool take_snapshot = true;
};
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
// then commits the result on the main thread - mirrors EmbossJob's UpdateJob/update_volume()
// bake-and-commit pattern (see EmbossJob.cpp).
class TextureDisplacementBakeJob : public Job
{
public:
TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function<void()> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
private:
TextureDisplacementBakeInput m_input;
TriangleMesh m_result;
// What the bake spent, for the message it leaves behind when the budget capped the detail.
TextureBakeStats m_stats;
// Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value
// (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest.
std::vector<uint8_t> m_triangle_color;
std::function<void()> m_on_finished;
};
// Captures `volume`'s current mesh/layers/paint data and queues a TextureDisplacementBakeJob on
// the app's UI job worker. `on_finished` is always called once the job settles (success, failure,
// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called
// from the main thread.
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
std::function<void()> on_finished, bool take_snapshot = true);
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementBakeJob_hpp_
@@ -0,0 +1,113 @@
#include "TextureDisplacementDebugJob.hpp"
#include "libslic3r/Model.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Plater.hpp"
namespace Slic3r::GUI {
TextureDisplacementDebugJob::TextureDisplacementDebugJob(
TextureDisplacementDebugInput &&input, std::function<void(std::vector<BakeStageSnapshot>)> on_finished)
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementDebugJob::process(Ctl &ctl)
{
const std::string status = _u8L("Capturing bake stages");
ctl.update_status(1, status);
// Only ever touches m_input (captured by value before this job was queued) and local state - and,
// unlike the bake job, never writes anything back either.
int last_reported = 1;
const auto report = [&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
if (percent > last_reported) {
last_reported = percent;
ctl.update_status(percent, status);
}
return true;
};
BakeStageRecorder recorder;
recorder.enable(true);
recorder.set_check_topology(m_input.check_topology);
recorder.set_mesh_cap(m_input.mesh_cap);
indexed_triangle_set mesh = m_input.base_mesh;
TextureDisplacementFacetsData masks = m_input.facets_data;
// 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, m_input.prepare_params,
m_input.color.palette,
// Preparation is roughly half the run; the bake
// takes the progress bar from there.
[&report](int pct) { return report(1 + pct / 2); },
&recorder);
if (ctl.was_canceled())
return;
// An empty result means nothing needed doing, which is not a failure - the bake below simply
// runs on the mesh as it stands. paint_lost means the remesh dropped every layer's paint, so
// there is nothing left to displace and the stages recorded so far are the whole story.
if (prepared.paint_lost) {
m_stages = recorder.take();
ctl.update_status(100, status);
return;
}
if (!prepared.mesh.indices.empty()) {
mesh = prepared.mesh;
masks = prepared.masks;
}
}
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);
m_stages = recorder.take();
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
}
void TextureDisplacementDebugJob::finalize(bool canceled, std::exception_ptr &eptr)
{
if (!m_on_finished)
return;
// The handler must run on every outcome - the caller uses it to clear its in-progress latch, and
// an empty vector is its signal that nothing usable came back.
if (canceled || eptr)
m_on_finished({});
else
m_on_finished(std::move(m_stages));
}
void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color,
const TextureDisplacementPrepareParams &prepare_params,
bool run_prepare, bool check_topology,
std::function<void(std::vector<BakeStageSnapshot>)> on_finished)
{
TextureDisplacementDebugInput input;
input.volume_id = volume.id();
input.base_mesh = volume.mesh().its;
input.layers = volume.texture_displacement_layers;
input.options = volume.texture_displacement_options;
input.prepare_params = prepare_params;
input.run_prepare = run_prepare;
input.check_topology = check_topology;
input.color = color;
input.volume_to_world = texture_displacement_volume_to_world(volume);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementDebugJob>(std::move(input), std::move(on_finished)));
}
} // namespace Slic3r::GUI
@@ -0,0 +1,82 @@
#ifndef slic3r_TextureDisplacementDebugJob_hpp_
#define slic3r_TextureDisplacementDebugJob_hpp_
#include <functional>
#include <vector>
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "Job.hpp"
namespace Slic3r {
class ModelVolume;
}
namespace Slic3r::GUI {
// Runs a bake with stage capture on and commits nothing.
//
// A bake is a chain of stages that each rewrite the whole mesh - remesh, refine, displace, smooth, or
// for the experimental pipeline subdivide, regularize, relocate, displace, decimate, repair - and when
// the result looks wrong the only useful question is which of them made it wrong. This runs the same
// recipe the Bake button runs, keeps every intermediate mesh, and hands them back for the gizmo to
// step through.
//
// Deliberately separate from TextureDisplacementBakeJob rather than a flag on it: this must never
// touch the Model, and keeping the committing path free of a "but not this time" branch is what
// guarantees that.
struct TextureDisplacementDebugInput
{
ObjectID volume_id;
indexed_triangle_set base_mesh;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementFacetsData facets_data;
TextureDisplacementOptions options;
// The preparation stages - remesh and adaptive refinement. Only the default pipeline uses them;
// the experimental one refines as part of the bake and ignores this entirely.
TextureDisplacementPrepareParams prepare_params;
bool run_prepare = true;
// Mesh coordinates -> world millimetres, as the bake gets it. The captured stages are brought
// back into mesh coordinates before they are handed over, so the gizmo can draw them directly.
Transform3d volume_to_world = Transform3d::Identity();
// 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 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
// tore the mesh is exactly what this exists to find.
bool check_topology = true;
// Stages above this keep their counts but not their geometry - see BakeStageRecorder.
size_t mesh_cap = 4'000'000;
};
class TextureDisplacementDebugJob : public Job
{
public:
TextureDisplacementDebugJob(TextureDisplacementDebugInput &&input,
std::function<void(std::vector<BakeStageSnapshot>)> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
private:
TextureDisplacementDebugInput m_input;
std::vector<BakeStageSnapshot> m_stages;
std::function<void(std::vector<BakeStageSnapshot>)> m_on_finished;
};
// Captures `volume`'s current mesh, layers and paint and queues one debug run. `on_finished` always
// runs exactly once on the UI thread, with an empty vector if the run was cancelled or threw, so the
// caller can clear its own in-progress state. Must be called from the main thread.
void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color,
const TextureDisplacementPrepareParams &prepare_params,
bool run_prepare, bool check_topology,
std::function<void(std::vector<BakeStageSnapshot>)> on_finished);
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementDebugJob_hpp_
@@ -0,0 +1,117 @@
#include "TextureDisplacementPrepareJob.hpp"
#include <algorithm>
#include <optional>
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
namespace Slic3r::GUI {
TextureDisplacementPrepareJob::TextureDisplacementPrepareJob(
TextureDisplacementPrepareInput &&input, std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementPrepareJob::process(Ctl &ctl)
{
const std::string status = _u8L("Preparing mesh for texture displacement");
ctl.update_status(1, status);
// Only ever touches m_input (captured by value before this job was queued) and local state - never
// the live Model - so this is safe to run concurrently with the UI thread.
//
// The progress hook is not just cosmetic: the framework's notification only grows a close button
// once it reaches 100%, and below that it shows Cancel, which is wired through here. Reporting is
// throttled to whole percentage points because every call repaints the notification and wakes the
// idle loop.
int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.params, m_input.color.palette,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
if (percent > last_reported) {
last_reported = percent;
ctl.update_status(percent, status);
}
return true;
});
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
}
void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &eptr)
{
TextureDisplacementPrepareOutcome outcome = TextureDisplacementPrepareOutcome::Failed;
struct OnExit
{
std::function<void(TextureDisplacementPrepareOutcome)> fn;
const TextureDisplacementPrepareOutcome *outcome;
~OnExit() { if (fn) fn(*outcome); }
} on_exit{m_on_finished, &outcome};
if (canceled || eptr)
return;
if (m_result.paint_lost) {
outcome = TextureDisplacementPrepareOutcome::PaintLost;
return;
}
if (m_result.mesh.indices.empty()) {
outcome = TextureDisplacementPrepareOutcome::Unchanged;
return;
}
Plater *plater = wxGetApp().plater();
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
// The lookup doubles as a staleness check: anything that replaces a volume's mesh also gives it a
// new id (set_new_unique_id()), so a prepare computed from a mesh that has since been replaced -
// by an undo, another bake, or a boolean - simply fails to find its volume and commits nothing.
if (volume == nullptr)
return;
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
{
Plater::TakeSnapshot snapshot(plater, m_input.snapshot_name, UndoRedo::SnapshotType::GizmoAction);
// The four standard paint channels ride across on ModelVolume's own spatial remap. The eight
// texture-displacement masks do not go through it - prepare_mesh() has already carried them,
// exactly, from the source triangles the refinement records - so they are put back after
// restore_painting(), which resets every extra facet before remapping the channels it knows.
std::optional<TriangleSelector::SavedPainting> saved_painting = volume->save_painting();
volume->set_mesh(TriangleMesh(std::move(m_result.mesh)));
volume->set_new_unique_id();
volume->calculate_convex_hull();
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)]));
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
auto it = std::find(objs.begin(), objs.end(), object);
if (it != objs.end())
obj_list->update_info_items(size_t(it - objs.begin()));
}
plater->changed_object(*object);
}
outcome = TextureDisplacementPrepareOutcome::Committed;
}
void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input,
std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
{
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementPrepareJob>(std::move(input), std::move(on_finished)));
}
} // namespace Slic3r::GUI
@@ -0,0 +1,77 @@
#ifndef slic3r_TextureDisplacementPrepareJob_hpp_
#define slic3r_TextureDisplacementPrepareJob_hpp_
#include <functional>
#include <string>
#include <vector>
#include "Job.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
namespace Slic3r {
class ModelVolume;
}
namespace Slic3r::GUI {
// Getting a mesh ready to receive displacement - the isotropic remesh, carrying the paint onto it, and
// the adaptive refinement - off the UI thread.
//
// All three used to run inline behind a wxBusyCursor, which on any part big enough to matter meant tens
// of seconds with the window not repainting and no way to stop it: CGAL's remesher is single threaded
// and its cost grows with the square of 1/target_edge, and the refinement that follows spends a budget
// of hundreds of thousands of triangles. Indistinguishable from a hang, and reported as one.
//
// The work itself is GLGizmoTextureDisplacement::prepare_mesh(), which is pure - it takes an
// indexed_triangle_set and the layers' masks and returns new ones, touching no Model and no GUI - so all
// this job adds is the worker thread, the progress notification with its Cancel button, and the commit.
enum class TextureDisplacementPrepareOutcome
{
Committed, // the volume now carries the prepared mesh and the paint carried onto it
Unchanged, // nothing needed doing - the mesh already met the criteria; the model was not touched
PaintLost, // the remesh landed but no layer's paint survived it, so nothing was committed
Failed, // cancelled, threw, or the volume went away while the job ran
};
struct TextureDisplacementPrepareInput
{
ObjectID volume_id;
indexed_triangle_set base_mesh;
TextureDisplacementFacetsData masks;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementPrepareParams params;
// Captured on the main thread. Empty when no layer is colouring, in which case the refinement
// 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 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;
};
class TextureDisplacementPrepareJob : public Job
{
TextureDisplacementPrepareInput m_input;
TextureDisplacementPrepareResult m_result;
std::function<void(TextureDisplacementPrepareOutcome)> m_on_finished;
public:
TextureDisplacementPrepareJob(TextureDisplacementPrepareInput &&input,
std::function<void(TextureDisplacementPrepareOutcome)> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
};
// `on_finished` runs on the UI thread once the result has been committed (or found not to need
// committing), and always runs exactly once.
void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input,
std::function<void(TextureDisplacementPrepareOutcome)> on_finished);
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementPrepareJob_hpp_
@@ -0,0 +1,66 @@
#include "TextureDisplacementPreviewJob.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
namespace Slic3r::GUI {
TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished)
: m_input(std::move(input)), m_generation(generation), m_current_generation(std::move(current_generation)),
m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementPreviewJob::process(Ctl &ctl)
{
// No ctl.update_status() anywhere in here on purpose - see the class comment. A preview is
// invisible bookkeeping; the only thing on screen should be the preview itself.
// Only ever touches m_input (captured by value before this job was queued) and local state -
// never the live Model - so this is safe to run concurrently with the UI thread.
TextureColorRequest color_request;
TextureColorRequest *color = nullptr;
if (!m_input.color.empty()) {
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
if (!m_input.color.palette_pure.empty())
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
m_input.color.dither_cell_mm);
color_request.despeckle_passes = m_input.color.despeckle_passes;
color_request.out_triangle = &m_result.triangle_color;
if (color_request.quantize)
color = &color_request;
}
m_result.mesh = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data,
m_input.options,
[this, &ctl](int) {
// Bail the moment this preview stops being the current
// one; build_texture_displacement() then returns an
// empty mesh and finalize() drops it.
return !ctl.was_canceled() &&
(!m_current_generation ||
m_current_generation->load() == m_generation);
},
color, m_input.volume_to_world);
}
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
{
if (!m_on_finished)
return;
// The handler must run on *every* outcome, cancellation included, because the caller uses it to
// clear its "a job is in flight" latch. Returning early on `canceled` - which is what a cancel_all()
// from Plater (project load/close, app exit) delivers - left that latch stuck true and no preview
// was ever queued again for the rest of the session. An empty result is the caller's signal that
// nothing usable came back; it already handles that.
if (canceled || eptr)
m_on_finished(TextureDisplacementPreviewResult{}, m_generation);
else
m_on_finished(std::move(m_result), m_generation);
}
} // namespace Slic3r::GUI
@@ -0,0 +1,82 @@
#ifndef slic3r_TextureDisplacementPreviewJob_hpp_
#define slic3r_TextureDisplacementPreviewJob_hpp_
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
#include <vector>
#include "libslic3r/Color.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "Job.hpp"
namespace Slic3r::GUI {
// Everything process() needs, captured by value on the main thread when the job is queued -
// mirrors TextureDisplacementBakeInput, but a preview never writes back to the Model.
struct TextureDisplacementPreviewInput
{
indexed_triangle_set base_mesh;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementFacetsData facets_data;
TextureDisplacementOptions options;
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
// is and the two cannot disagree. See build_texture_displacement().
Transform3d volume_to_world = Transform3d::Identity();
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
TextureColorSettings color;
};
// A preview result: the displaced mesh, and - when the input carried a palette - one filament index
// per triangle (an EnforcerBlockerType value; 0 means "no colour from the texture").
struct TextureDisplacementPreviewResult
{
indexed_triangle_set mesh;
std::vector<uint8_t> triangle_color;
};
// Computes the true (unbaked) displaced-mesh preview in the background. With several painted
// layers this is real, non-trivial CPU work (PNG sampling, per-layer vertex welding), which used
// to run synchronously on every paint stroke and parameter tweak and made editing feel slow with
// more than one or two layers. Unlike Bake, this never touches the live Model - a preview is
// purely informational, there is nothing to commit.
//
// Deliberately reports no status: the Job framework turns the first update_status() call into an
// on-screen progress notification, and a preview firing one on every stroke and slider release
// buried the user in notifications that only closed at 100%.
class TextureDisplacementPreviewJob : public Job
{
public:
// `generation` is an opaque token the caller controls (typically an incrementing counter):
// on_finished should only actually be applied by the caller if it still matches the caller's
// current generation when the job completes, so that a burst of edits queuing several of
// these jobs in a row can't have an earlier, now-stale result clobber a later one that
// finishes first.
//
// `current_generation` is the caller's live counter, shared with the worker thread. The job
// polls it *while computing* and aborts as soon as it no longer matches - so a preview that has
// already been superseded stops burning CPU instead of running to completion for a result that
// will only be thrown away. That matters because the UI job worker runs one job at a time in FIFO
// order: without it, a Bake queued behind a handful of stale previews waits for every one of them.
TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
private:
TextureDisplacementPreviewInput m_input;
uint64_t m_generation;
std::shared_ptr<const std::atomic<uint64_t>> m_current_generation;
TextureDisplacementPreviewResult m_result;
std::function<void(TextureDisplacementPreviewResult, uint64_t)> m_on_finished;
};
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementPreviewJob_hpp_
+99 -7
View File
@@ -103,6 +103,7 @@
#include "Selection.hpp"
#include "GLToolbar.hpp"
#include "GUI_Preview.hpp"
#include "UVEditorCanvas.hpp"
#include "3DBed.hpp"
#include "PartPlate.hpp"
#include "Camera.hpp"
@@ -1797,11 +1798,14 @@ bool Sidebar::priv::switch_diameter_to(const wxString &diameter)
Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
// The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no
// single extruder's diameter, so the preset's own variant answers first.
if (printer_preset.config.opt_string("printer_variant") == diameter.ToStdString()) {
const std::string &printer_variant = printer_preset.config.opt_string("printer_variant");
if (printer_variant == diameter.ToStdString()) {
return true;
}
// A named variant ("0.4 High Flow") shares its diameter with the standard profile, which selecting
// the plain diameter switches back to, so only a preset naming no variant is kept by its diameter.
auto* nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(printer_preset.config.option("nozzle_diameter"));
if (nozzle_diameter && nozzle_diameter->size() > 0) {
if (printer_variant.empty() && nozzle_diameter && nozzle_diameter->size() > 0) {
auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]);
// If the selected diameter is the same as current nozzle, don't switch profiles
if (current_nozzle_dia == diameter.ToStdString()) {
@@ -3933,13 +3937,18 @@ void Sidebar::update_presets(Preset::Type preset_type)
combo_flow->Show(combo_flow->GetCount() > 0);
};
auto update_extruder_diameter = [&diameters, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) {
auto update_extruder_diameter = [&diameters, &nozzle_diameter, &diameter](int extruder_index,ExtruderGroup & extruder) {
extruder.combo_diameter->Clear();
if (extruder_index >= int(nozzle_diameter->values.size()))
return;
int select = -1;
// ORCA get the actual nozzle diameter from printer config
auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]);
// Named variants such as "0.4HS" and "0.4 High Flow" share a physical diameter.
// Retain the variant selection unless the diameter was customized.
const bool keep_variant = diameter.substr(0, diameter.find_first_not_of("0123456789.")) == nozzle_dia &&
std::find(diameters.begin(), diameters.end(), diameter) != diameters.end();
const std::string &selected_variant = keep_variant ? diameter : nozzle_dia;
// ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias
if(!diameters.empty() && diameters[0].empty() && !nozzle_dia.empty()){
diameters[0] = nozzle_dia;
@@ -3949,7 +3958,7 @@ void Sidebar::update_presets(Preset::Type preset_type)
diameters.push_back(nozzle_dia);
}
for (size_t i = 0; i < diameters.size(); ++i) {
if (diameters[i] == nozzle_dia)
if (diameters[i] == selected_variant)
select = extruder.combo_diameter->GetCount();
extruder.combo_diameter->Append(diameters[i], {});
}
@@ -7108,6 +7117,13 @@ struct Plater::priv
GLToolbar collapse_toolbar;
Preview *preview;
AssembleView* assemble_view { nullptr };
// Docked/resizable 2D pane showing GLGizmoTextureDisplacement's LSCM unwrap of a painted
// patch; a sibling AUI pane alongside "sidebar"/"main", not part of the view3D/preview/
// assemble_view sizer - see its registration below and Plater::get_uv_editor_canvas(). The
// pane hosts the panel (toolbar + canvas + status line); uv_editor_canvas is its inner canvas,
// cached so the gizmo can reach it directly.
UVEditorPanel* uv_editor_panel { nullptr };
UVEditorCanvas* uv_editor_canvas { nullptr };
bool first_enter_assemble{ true };
std::unique_ptr<NotificationManager> notification_manager;
@@ -7357,6 +7373,8 @@ struct Plater::priv
void undo();
void redo();
// True, and tells the user, while a background job is working on the model - see the definition.
bool undo_redo_blocked_by_job();
void undo_redo_to(size_t time_to_load);
// BBS: backup
@@ -7810,6 +7828,26 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
.BottomDockable(false)
.BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit())));
// UV editor pane for GLGizmoTextureDisplacement's LSCM unwrap preview - a resizable/dockable
// sibling of "sidebar"/"main" like everything else registered on this same AUI manager, not a
// change to the view3D/preview/assemble_view sizer above. Hidden by default: only relevant
// while that gizmo is active with a layer using the "Unwrap (LSCM)" projection method (see
// Plater::show_uv_editor()), so it stays out of the way of everyone else's window layout.
uv_editor_panel = new UVEditorPanel(q);
uv_editor_canvas = uv_editor_panel->canvas();
m_aui_mgr.AddPane(uv_editor_panel, wxAuiPaneInfo()
.Name("uv_editor")
.Caption(_L("UV Editor"))
.Right()
.Hide()
.BestSize(wxSize(40 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit())));
// Closing the pane with its own X has to reach the gizmo, or its next update would simply show the pane again.
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent &evt) {
evt.Skip();
if (evt.GetPane() != nullptr && evt.GetPane()->window == uv_editor_panel && uv_editor_canvas != nullptr)
uv_editor_canvas->run_command(UVEditorCanvas::Command::PaneClosed);
});
auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL);
panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0);
panel_sizer->Add(preview, 1, wxEXPAND | wxALL, 0);
@@ -7851,6 +7889,13 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
BOOST_LOG_TRIVIAL(info) << "Removed floating AUI state from saved window layout for Wayland";
}
// The UV editor is a transient, gizmo-driven pane (see show_uv_editor()); a saved layout
// from a session that happened to close with it open would otherwise restore it visible on
// startup, with nothing painted in it. Force it hidden here so it only ever appears when the
// texture-displacement gizmo asks for it.
if (wxAuiPaneInfo &uv_pane = m_aui_mgr.GetPane("uv_editor"); uv_pane.IsOk())
uv_pane.Hide();
sidebar_layout.is_collapsed = !sidebar.IsShown();
}
@@ -15006,8 +15051,25 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed
BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info();
}
// A background job holds the model it is working on: the texture displacement bake, for one, hands its
// result to the volume when it finishes, and it was queued against the geometry as it was at the time.
// Undoing while it runs restores an older state under it - a different transform, a different mesh -
// and the result then lands on geometry it was never computed for. Undo and redo therefore wait for
// the job, and say so rather than doing nothing.
bool Plater::priv::undo_redo_blocked_by_job()
{
if (m_worker.is_idle())
return false;
notification_manager->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
_u8L("Cannot undo or redo while an operation is running. Stop it first."));
return true;
}
void Plater::priv::undo()
{
if (this->undo_redo_blocked_by_job())
return;
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time()));
// BBS: undo-redo until modify record
@@ -15025,6 +15087,8 @@ void Plater::priv::undo()
void Plater::priv::redo()
{
if (this->undo_redo_blocked_by_job())
return;
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time()));
// BBS: undo-redo until modify record
@@ -16670,7 +16734,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
/// --- scale ---
/// -- scale --
// model is created for a 0.4 nozzle, scale z with nozzle size.
const ConfigOptionFloats* nozzle_diameter_config = printer_config->option<ConfigOptionFloats>("nozzle_diameter");
std::vector<int> extruder_types = printer_config->option<ConfigOptionEnumsGeneric>("extruder_type")->values;
@@ -21147,6 +21211,33 @@ GLCanvas3D* Plater::get_assmeble_canvas3D()
return nullptr;
}
UVEditorCanvas* Plater::get_uv_editor_canvas()
{
return p->uv_editor_canvas;
}
void Plater::show_uv_editor(bool show)
{
if (p->uv_editor_panel == nullptr)
return;
const wxAuiPaneInfo &pane = p->m_aui_mgr.GetPane(p->uv_editor_panel);
if (!pane.IsOk() || pane.IsShown() == show)
return;
// Deferred, because GLGizmoTextureDisplacement calls this from its ImGui panel - that is, from
// the middle of the 3D canvas's GL frame. Showing an AUI pane re-lays out the window and
// delivers the resulting size/paint events synchronously, and the UV canvas painting itself
// makes its own surface current in the app's *shared* GL context, which mid-frame is the one
// the 3D canvas is drawing into. Doing the layout once the frame is over avoids that entirely.
CallAfter([this, show]() {
wxAuiPaneInfo &deferred_pane = p->m_aui_mgr.GetPane(p->uv_editor_panel);
if (!deferred_pane.IsOk() || deferred_pane.IsShown() == show)
return;
deferred_pane.Show(show);
p->m_aui_mgr.Update();
});
}
GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview)
{
return p->get_current_canvas3D(exclude_preview);
@@ -22886,8 +22977,9 @@ bool Plater::can_copy_to_clipboard() const
return true;
}
bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_undo_snapshot(); }
bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_redo_snapshot(); }
// The job check keeps the buttons in step with priv::undo()/redo(), which refuse while one runs.
bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->m_worker.is_idle() && p->undo_redo_stack().has_undo_snapshot(); }
bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->m_worker.is_idle() && p->undo_redo_stack().has_redo_snapshot(); }
bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); }
//BBS
bool Plater::can_fillcolor() const { return p->can_fillcolor(); }
+7
View File
@@ -672,6 +672,13 @@ public:
GLCanvas3D* get_assmeble_canvas3D();
wxWindow* get_select_machine_dialog();
// Docked UV-editor pane used by GLGizmoTextureDisplacement's LSCM projection preview (see
// UVEditorCanvas.hpp). Returns nullptr only before the main window is fully constructed.
class UVEditorCanvas* get_uv_editor_canvas();
// Shows or hides the UV-editor AUI pane, updating its docked layout accordingly. Safe to call
// repeatedly (e.g. every time the gizmo's active layer/projection method changes).
void show_uv_editor(bool show);
void arrange();
void orient();
void find_new_position(const ModelInstancePtrs &instances);
+36 -32
View File
@@ -4396,16 +4396,17 @@ void TabFilament::build()
optgroup->append_single_option_line("temperature_vitrification", "material_basic_information#softening-temperature");
optgroup->append_single_option_line("idle_temperature", "material_basic_information#idle-temperature");
Line line = { L("Recommended nozzle temperature"), L("Recommended nozzle temperature range of this filament. 0 means not set") };
line.append_option(optgroup->get_option("nozzle_temperature_range_low"));
line.append_option(optgroup->get_option("nozzle_temperature_range_high"));
line.append_option(optgroup->get_option("nozzle_temperature_range_low", 0));
line.append_option(optgroup->get_option("nozzle_temperature_range_high", 0));
optgroup->append_line(line);
optgroup->m_on_change = [this, optgroup](t_config_option_key opt_key, boost::any value) {
DynamicPrintConfig &filament_config = m_preset_bundle->filaments.get_edited_preset().config;
update_dirty();
if (!m_postpone_update_ui && (opt_key == "nozzle_temperature_range_low" || opt_key == "nozzle_temperature_range_high")) {
m_config_manipulation.check_nozzle_recommended_temperature_range(&filament_config);
const std::string opt_key_without_idx = opt_key.substr(0, opt_key.find('#'));
if (!m_postpone_update_ui && (opt_key_without_idx == "nozzle_temperature_range_low" || opt_key_without_idx == "nozzle_temperature_range_high")) {
m_config_manipulation.check_nozzle_recommended_temperature_range(&filament_config, selected_variant_index());
}
on_value_change(opt_key, value);
};
@@ -4520,6 +4521,7 @@ void TabFilament::build()
DynamicPrintConfig& filament_config = m_preset_bundle->filaments.get_edited_preset().config;
update_dirty();
const std::string opt_key_without_idx = opt_key.substr(0, opt_key.find('#'));
/*if (opt_key == "cool_plate_temp" || opt_key == "cool_plate_temp_initial_layer") {
m_config_manipulation.check_bed_temperature_difference(BedType::btPC, &filament_config);
}
@@ -4532,11 +4534,11 @@ void TabFilament::build()
else if (opt_key == "textured_plate_temp" || opt_key == "textured_plate_temp_initial_layer") {
m_config_manipulation.check_bed_temperature_difference(BedType::btPTE, &filament_config);
}
else */if (opt_key == "nozzle_temperature") {
m_config_manipulation.check_nozzle_temperature_range(&filament_config);
else */if (opt_key_without_idx == "nozzle_temperature") {
m_config_manipulation.check_nozzle_temperature_range(&filament_config, selected_variant_index());
}
else if (opt_key == "nozzle_temperature_initial_layer") {
m_config_manipulation.check_nozzle_temperature_initial_layer_range(&filament_config);
else if (opt_key_without_idx == "nozzle_temperature_initial_layer") {
m_config_manipulation.check_nozzle_temperature_initial_layer_range(&filament_config, selected_variant_index());
}
on_value_change(opt_key, value);
@@ -4571,12 +4573,12 @@ void TabFilament::build()
optgroup = page->new_optgroup(L("Part cooling fan"), L"param_cooling_part_fan");
line = { L("Min fan speed threshold"), L("The part cooling fan will run at the minimum fan speed when the estimated layer time is longer than the threshold value. When the layer time is shorter than the threshold, the fan speed will be interpolated between the minimum and maximum fan speed according to layer printing time.") };
line.label_path = "material_cooling#material-part-cooling-fan";
line.append_option(optgroup->get_option("fan_min_speed"));
line.append_option(optgroup->get_option("fan_min_speed", 0));
line.append_option(optgroup->get_option("fan_cooling_layer_time"));
optgroup->append_line(line);
line = { L("Max fan speed threshold"), L("The part cooling fan will run at maximum speed when the estimated layer time is shorter than the threshold value.") };
line.label_path = "material_cooling#material-part-cooling-fan";
line.append_option(optgroup->get_option("fan_max_speed"));
line.append_option(optgroup->get_option("fan_max_speed", 0));
line.append_option(optgroup->get_option("slow_down_layer_time"));
optgroup->append_line(line);
optgroup->append_single_option_line("reduce_fan_stop_start_freq", "material_cooling#keep-fan-always-on");
@@ -4592,7 +4594,7 @@ void TabFilament::build()
optgroup->append_single_option_line("ironing_fan_speed", "material_cooling#ironing-fan-speed"); // ORCA: Add support for ironing fan speed control
optgroup = page->new_optgroup(L("Auxiliary part cooling fan"), L"param_cooling_aux_fan");
optgroup->append_single_option_line("additional_cooling_fan_speed", "material_cooling#auxiliary-part-cooling-fan");
optgroup->append_single_option_line("additional_cooling_fan_speed", "material_cooling#auxiliary-part-cooling-fan", 0);
optgroup = page->new_optgroup(L("Exhaust fan"),L"param_cooling_exhaust");
@@ -4656,7 +4658,7 @@ void TabFilament::build()
page = add_options_page(L("Multimaterial"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders
optgroup = page->new_optgroup(L("Wipe tower parameters"), "param_tower");
optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup->append_single_option_line("filament_minimal_purge_on_wipe_tower", "material_multimaterial#multimaterial-wipe-tower-parameters", 0);
optgroup->append_single_option_line("filament_tower_interface_pre_extrusion_dist", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup->append_single_option_line("filament_tower_interface_pre_extrusion_length", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup->append_single_option_line("filament_tower_ironing_area", "material_multimaterial#multimaterial-wipe-tower-parameters");
@@ -4700,9 +4702,9 @@ void TabFilament::build()
});
optgroup = page->new_optgroup(L("Tool change parameters with multi extruder MM printers"), "param_toolchange_multi_extruder");
optgroup->append_single_option_line("filament_multitool_ramming", "material_multimaterial#tool-change-parameters-with-multi-extruder");
optgroup->append_single_option_line("filament_multitool_ramming_volume", "material_multimaterial#multi-tool-ramming-volume");
optgroup->append_single_option_line("filament_multitool_ramming_flow", "material_multimaterial#multi-tool-ramming-flow");
optgroup->append_single_option_line("filament_multitool_ramming", "material_multimaterial#tool-change-parameters-with-multi-extruder", 0);
optgroup->append_single_option_line("filament_multitool_ramming_volume", "material_multimaterial#multi-tool-ramming-volume", 0);
optgroup->append_single_option_line("filament_multitool_ramming_flow", "material_multimaterial#multi-tool-ramming-flow", 0);
page = add_options_page(L("Dependencies"), "advanced");
optgroup = page->new_optgroup(L("Compatible printers"), "param_dependencies_printers");
@@ -4770,8 +4772,14 @@ void TabFilament::update_description_lines()
// this->update_volumetric_flow_preset_hints();
}
unsigned int TabFilament::selected_variant_index() const
{
return m_variant_combo ? std::max(0, m_variant_combo->GetSelection()) : 0;
}
void TabFilament::toggle_options()
{
const unsigned int variant_index = selected_variant_index();
if (!m_active_page)
return;
bool is_BBL_printer = false;
@@ -4813,7 +4821,7 @@ void TabFilament::toggle_options()
}
}
toggle_line("additional_cooling_fan_speed", printer_cfg.opt_bool("auxiliary_fan"));
toggle_line("additional_cooling_fan_speed", printer_cfg.opt_bool("auxiliary_fan"), 256 + variant_index);
bool support_air_filtration = printer_cfg.opt_bool("support_air_filtration");
for (auto el : {"activate_air_filtration", "during_print_exhaust_fan_speed", "complete_print_exhaust_fan_speed"})
@@ -4829,11 +4837,8 @@ void TabFilament::toggle_options()
}
if (m_active_page->title() == L("Filament"))
{
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
const unsigned int variant_idx = (unsigned int) std::max(selection, 0);
bool pa = m_config->opt_bool("enable_pressure_advance", variant_idx);
toggle_option("pressure_advance", pa, 0);
bool pa = m_config->opt_bool("enable_pressure_advance", variant_index);
toggle_option("pressure_advance", pa, 256 + variant_index);
//Orca: Enable the plates that should be visible when multi bed support is enabled or a BBL printer is selected; otherwise, enable only the plate visible for the selected bed type.
DynamicConfig& proj_cfg = m_preset_bundle->project_config;
@@ -4863,7 +4868,7 @@ void TabFilament::toggle_options()
// If adaptive PA is not enabled, hide the adaptive PA model section
toggle_option("adaptive_pressure_advance", pa, 0);
toggle_option("adaptive_pressure_advance_overhangs", pa, 0);
bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", variant_idx);
bool has_adaptive_pa = m_config->opt_bool("adaptive_pressure_advance", variant_index);
toggle_line("adaptive_pressure_advance_overhangs", has_adaptive_pa && pa, 0);
toggle_line("adaptive_pressure_advance_model", has_adaptive_pa && pa, 0);
toggle_line("adaptive_pressure_advance_bridges", has_adaptive_pa && pa, 0);
@@ -4874,9 +4879,9 @@ void TabFilament::toggle_options()
toggle_line("activate_chamber_temp_control", printer_cfg.opt_bool("support_chamber_temp_control"));
std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_idx);
std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_index);
bool enable_fit = volumetric_speed_cos != "0 0 0 0 0 0";
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_idx);
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_index);
}
if (m_active_page->title() == L("Setting Overrides"))
@@ -4884,20 +4889,19 @@ void TabFilament::toggle_options()
if (m_active_page->title() == L("Multimaterial")) {
// Orca: hide specific settings for BBL printers
for (auto el : {"filament_minimal_purge_on_wipe_tower", "filament_loading_speed_start", "filament_loading_speed",
toggle_option("filament_minimal_purge_on_wipe_tower", !is_BBL_printer, 256 + variant_index);
for (auto el : {"filament_loading_speed_start", "filament_loading_speed",
"filament_unloading_speed_start", "filament_unloading_speed", "filament_toolchange_delay", "filament_cooling_moves",
"filament_cooling_initial_speed", "filament_cooling_final_speed"})
toggle_option(el, !is_BBL_printer);
bool multitool_ramming = m_config->opt_bool("filament_multitool_ramming", 0);
toggle_option("filament_multitool_ramming_volume", multitool_ramming);
toggle_option("filament_multitool_ramming_flow", multitool_ramming);
bool multitool_ramming = m_config->opt_bool("filament_multitool_ramming", variant_index);
toggle_option("filament_multitool_ramming_volume", multitool_ramming, 256 + variant_index);
toggle_option("filament_multitool_ramming_flow", multitool_ramming, 256 + variant_index);
bool is_BBL_multi_extruder = is_BBL_printer && printer_cfg.option<ConfigOptionFloats>("nozzle_diameter")->size() > 1;
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
const int extruder_idx = std::max(selection, 0);
toggle_line("long_retractions_when_ec", is_BBL_multi_extruder, 256 + extruder_idx);
toggle_line("retraction_distances_when_ec", is_BBL_multi_extruder && m_config->opt_bool("long_retractions_when_ec", extruder_idx), 256 + extruder_idx);
toggle_line("long_retractions_when_ec", is_BBL_multi_extruder, 256 + variant_index);
toggle_line("retraction_distances_when_ec", is_BBL_multi_extruder && m_config->opt_bool("long_retractions_when_ec", variant_index), 256 + variant_index);
}
}
+2
View File
@@ -616,6 +616,8 @@ private:
void add_filament_overrides_page();
void update_filament_overrides_page(const DynamicPrintConfig* printers_config);
void update_volumetric_flow_preset_hints();
// The variant index the variant switch shows, 0 without one.
unsigned int selected_variant_index() const;
std::map<std::string, ::CheckBox*> m_overrides_options;
+373
View File
@@ -0,0 +1,373 @@
#include "TextureLibrary.hpp"
#include <algorithm>
#include <fstream>
#include <limits>
#include <unordered_map>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/system/error_code.hpp>
#include <wx/image.h>
#include <wx/mstream.h>
#include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
namespace Slic3r::GUI {
namespace {
// Extensions the picker will list. The shipped folder only ever contains .png; the rest are here
// so a user who drops a .jpg straight into their own folder still sees it (load_texture_image_data()
// converts anything it can open).
bool is_image_file(const boost::filesystem::path &path)
{
std::string ext = path.extension().string();
boost::algorithm::to_lower(ext);
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp";
}
void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<TextureLibraryEntry> &out)
{
boost::system::error_code ec;
if (!boost::filesystem::is_directory(dir, ec))
return;
const size_t first = out.size();
for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) {
if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path()))
continue;
out.push_back({ it->path().stem().string(), it->path().string(), is_user });
}
std::sort(out.begin() + first, out.end(),
[](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; });
}
// True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A
// photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of
// the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer
// the user a "colour" that is a row of identical greys.
bool image_has_color(const wxImage &image)
{
const unsigned char *rgb = image.GetData();
if (rgb == nullptr)
return false;
const size_t n = size_t(image.GetWidth()) * size_t(image.GetHeight());
for (size_t i = 0; i < n; ++i)
if (rgb[i * 3] != rgb[i * 3 + 1] || rgb[i * 3 + 1] != rgb[i * 3 + 2])
return true;
return false;
}
// Colour images are stored as-is (as an RGB PNG) so the texture can colour the model as well as
// displace it; decode_height_texture() takes the height from their luminance, with the same
// coefficients ConvertToGreyscale() uses, so the relief is identical either way. wxImage writes the
// PNG here rather than Slic3r::png, which only encodes grayscale.
bool encode_color_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std::string &error)
{
if (image.GetWidth() <= 0 || image.GetHeight() <= 0) {
error = _u8L("The selected image is empty.");
return false;
}
wxMemoryOutputStream stream;
// Alpha would be lost on the way into DecodedHeightTexture anyway, and a partly transparent
// texture reading as black relief is worse than reading as its own colour over the background.
wxImage opaque = image;
if (opaque.HasAlpha())
opaque.ClearAlpha();
if (!opaque.SaveFile(stream, wxBITMAP_TYPE_PNG)) {
error = _u8L("Failed to prepare the texture for use.");
return false;
}
const size_t size = size_t(stream.GetLength());
out.resize(size);
stream.CopyTo(out.data(), size);
if (out.empty()) {
error = _u8L("Failed to prepare the texture for use.");
return false;
}
return true;
}
// Slic3r::png only writes PNGs to a file, so the encode round-trips through a temp file rather than
// staying in memory. It happens once per import / per texture pick, not per frame, so the I/O is
// not worth avoiding with a second PNG encoder.
bool encode_gray_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std::string &error)
{
const wxImage gray = image.ConvertToGreyscale();
const int w = gray.GetWidth();
const int h = gray.GetHeight();
if (w <= 0 || h <= 0) {
error = _u8L("The selected image is empty.");
return false;
}
// wxImage always stores 3 bytes per pixel; after ConvertToGreyscale the three are equal.
std::vector<uint8_t> pixels(size_t(w) * size_t(h));
const unsigned char *rgb = gray.GetData();
for (size_t i = 0; i < pixels.size(); ++i)
pixels[i] = rgb[i * 3];
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("orca_texdisp_%%%%%%%%.png");
if (!Slic3r::png::write_gray_to_file(tmp.string(), size_t(w), size_t(h), pixels)) {
error = _u8L("Failed to prepare the texture for use.");
return false;
}
{
std::ifstream ifs(tmp.string(), std::ios::binary);
out.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp, ec);
if (out.empty()) {
error = _u8L("Failed to prepare the texture for use.");
return false;
}
return true;
}
// Grayscale in, grayscale out; colour in, colour out.
bool encode_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std::string &error)
{
return image_has_color(image) ? encode_color_png_bytes(image, out, error)
: encode_gray_png_bytes(image, out, error);
}
std::vector<unsigned char> read_file_bytes(const std::string &path)
{
std::ifstream ifs(path, std::ios::binary);
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is
// the height and whose RGB is available to colour the model). Anything else is converted on load.
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
{
if (bytes.empty())
return false;
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
if (!png::is_png(rbuf))
return false;
png::ImageGreyscale gray;
if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0)
return true;
png::ImageColorscale color;
return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 &&
color.bytes_per_pixel >= 3 &&
color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel);
}
std::vector<TextureLibraryEntry> g_library;
bool g_library_scanned = false;
// The user's own textures, most recently used first, kept in the app config. File names are stored rather
// than paths so the order survives a moved data directory; '/' separates them because no platform allows
// it inside a file name. Bounded so a long-lived config does not grow a line without end.
const char *const RECENT_SECTION = "texture_displacement";
const char *const RECENT_KEY = "recent_textures";
const size_t RECENT_MAX = 200;
std::vector<std::string> recent_texture_names()
{
std::vector<std::string> names;
if (wxGetApp().app_config == nullptr)
return names;
const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY);
for (size_t begin = 0; begin <= value.size();) {
const size_t end = std::min(value.find('/', begin), value.size());
if (end > begin)
names.push_back(value.substr(begin, end - begin));
begin = end + 1;
}
return names;
}
void store_recent_texture_names(std::vector<std::string> names)
{
if (wxGetApp().app_config == nullptr)
return;
if (names.size() > RECENT_MAX)
names.resize(RECENT_MAX);
std::string value;
for (const std::string &name : names)
value += (value.empty() ? "" : "/") + name;
wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value);
}
// Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep
// the name order scan_dir() gave them, after every used one.
void sort_user_textures(std::vector<TextureLibraryEntry> &library)
{
const std::vector<std::string> names = recent_texture_names();
std::unordered_map<std::string, int> rank;
for (size_t i = 0; i < names.size(); ++i)
rank.emplace(names[i], int(i));
const auto rank_of = [&rank](const TextureLibraryEntry &e) {
const auto it = rank.find(boost::filesystem::path(e.path).filename().string());
return it == rank.end() ? std::numeric_limits<int>::max() : it->second;
};
const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; });
std::stable_sort(first_user, library.end(),
[&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); });
}
// True if `path` is a file directly inside user_texture_dir() - the only place textures may be removed
// from, or have their use recorded.
bool in_user_texture_dir(const std::string &path)
{
const std::string dir = user_texture_dir();
if (dir.empty())
return false;
boost::system::error_code ec;
const bool same = boost::filesystem::equivalent(boost::filesystem::path(path).parent_path(),
boost::filesystem::path(dir), ec);
return !ec && same;
}
} // namespace
std::string user_texture_dir()
{
const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement";
boost::system::error_code ec;
boost::filesystem::create_directories(dir, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "Could not create the user texture directory " << dir.string() << ": " << ec.message();
return {};
}
return dir.string();
}
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
{
if (g_library_scanned && !force_rescan)
return g_library;
g_library.clear();
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library);
const std::string user_dir = user_texture_dir();
if (!user_dir.empty())
scan_dir(boost::filesystem::path(user_dir), true, g_library);
sort_user_textures(g_library);
g_library_scanned = true;
return g_library;
}
void touch_user_texture(const std::string &path)
{
if (!in_user_texture_dir(path))
return;
const std::string name = boost::filesystem::path(path).filename().string();
std::vector<std::string> names = recent_texture_names();
names.erase(std::remove(names.begin(), names.end(), name), names.end());
names.insert(names.begin(), name);
store_recent_texture_names(std::move(names));
if (g_library_scanned)
sort_user_textures(g_library);
}
bool remove_user_texture(const std::string &path, std::string &error)
{
if (!in_user_texture_dir(path)) {
error = _u8L("Only your own imported textures can be removed.");
return false;
}
boost::system::error_code ec;
boost::filesystem::remove(boost::filesystem::path(path), ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "Could not delete texture " << path << ": " << ec.message();
error = _u8L("Could not delete the texture file.");
return false;
}
const std::string name = boost::filesystem::path(path).filename().string();
std::vector<std::string> names = recent_texture_names();
names.erase(std::remove(names.begin(), names.end(), name), names.end());
store_recent_texture_names(std::move(names));
texture_library(true); // drop it from the list
return true;
}
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error)
{
const std::string user_dir = user_texture_dir();
if (user_dir.empty()) {
error = _u8L("Could not create the folder for imported textures.");
return std::nullopt;
}
wxImage image;
if (!image.LoadFile(from_u8(source_path)) || !image.IsOk()) {
error = _u8L("Could not load the selected image.");
return std::nullopt;
}
std::vector<unsigned char> bytes;
if (!encode_png_bytes(image, bytes, error))
return std::nullopt;
// Never overwrite an existing texture (the user's or, if they picked the same name twice, their
// own earlier import) - uniquify instead.
const std::string stem = boost::filesystem::path(source_path).stem().string();
boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png");
for (int i = 2; boost::filesystem::exists(dest); ++i)
dest = boost::filesystem::path(user_dir) / (stem + " (" + std::to_string(i) + ").png");
{
boost::nowide::ofstream ofs(dest.string(), std::ios::binary);
ofs.write(reinterpret_cast<const char *>(bytes.data()), std::streamsize(bytes.size()));
if (!ofs.good()) {
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
}
}
texture_library(true); // pick the new file up
const std::string dest_str = dest.string();
for (const TextureLibraryEntry &e : g_library)
if (e.path == dest_str)
return e;
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
}
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error)
{
std::vector<unsigned char> bytes = read_file_bytes(path);
if (bytes.empty()) {
error = _u8L("Could not read the texture file.");
return nullptr;
}
if (is_supported_height_map(bytes))
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
// Not a PNG at all (a jpg somebody copied into the folder by hand, say): convert it the same way
// an import would - keeping its colour if it has any - but leave the file on disk alone.
wxImage image;
if (!image.LoadFile(from_u8(path)) || !image.IsOk()) {
error = _u8L("Could not load the selected image.");
return nullptr;
}
std::vector<unsigned char> converted;
if (!encode_png_bytes(image, converted, error))
return nullptr;
return std::make_shared<std::vector<unsigned char>>(std::move(converted));
}
} // namespace Slic3r::GUI
+62
View File
@@ -0,0 +1,62 @@
#ifndef slic3r_TextureLibrary_hpp_
#define slic3r_TextureLibrary_hpp_
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace Slic3r::GUI {
// One selectable height-map texture in the texture-displacement gizmo's texture picker.
struct TextureLibraryEntry
{
std::string name; // display name (the file's stem, e.g. "Wood Grain")
std::string path; // absolute path on disk
bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer
};
// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted
// by name, then the user's own from <data_dir>/textures/displacement - most recently used first (see
// touch_user_texture()), then any never used, by name.
//
// The two live in separate directories deliberately: an app update replaces the resources tree
// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or
// delete. `is_user` is what the picker uses to show them under separate headings.
//
// Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped
// into either folder by hand while the app was running.
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
// Moves one of the user's own textures to the front of the recently-used order texture_library() lists
// them in, and stores that order in the app config so it survives a restart. No-op for anything that is
// not in user_texture_dir().
void touch_user_texture(const std::string &path);
// Deletes one of the user's own imported textures from user_texture_dir() and forgets it. Anything outside
// that folder is refused, so a shipped texture can never be removed this way. Layers already using the
// texture are unaffected: they hold the image bytes themselves, and a project stores those. Returns false
// with `error` set on failure.
bool remove_user_texture(const std::string &path, std::string &error);
// <data_dir>/textures/displacement, created if it does not exist yet. Empty string on failure.
std::string user_texture_dir();
// Reads any image format wxWidgets can open, converts it to the 8-bit grayscale PNG that
// libslic3r's decode_height_texture() understands, and saves it into user_texture_dir() (uniquified
// if that name is taken). The conversion has to happen here rather than in libslic3r, which has no
// image toolkit and so only ever handles the one already-normalized format.
//
// Returns the newly imported entry, or nullopt with `error` set. `source_path` is only read.
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error);
// Encoded bytes of `path`, ready to hand to TextureDisplacementLayer::image_data. Files already in
// the supported 8-bit grayscale PNG form (everything in the two library folders, by construction)
// are passed through verbatim; anything else - e.g. a colour PNG the user copied into the folder
// by hand - is converted on the fly, so a valid image never silently produces a blank layer.
// Returns nullptr with `error` set if the file cannot be read or decoded at all.
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error);
} // namespace Slic3r::GUI
#endif // slic3r_TextureLibrary_hpp_
+97
View File
@@ -0,0 +1,97 @@
#include "TextureProjectorFrame.hpp"
#include <algorithm>
#include <wx/dcclient.h>
#include <wx/image.h>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
namespace Slic3r { namespace GUI {
TextureProjectorFrame::TextureProjectorFrame(wxWindow *parent)
: wxFrame(parent, wxID_ANY, _L("Projection frame - drag over the model, then Apply"), wxDefaultPosition,
wxSize(360, 360),
// Caption and resize border so moving and sizing are the native gestures the user
// already knows - "align it by moving the window" only works if the window moves the
// ordinary way. FLOAT_ON_PARENT keeps it above the 3D view without the antisocial
// always-on-top-of-everything behaviour of wxSTAY_ON_TOP.
wxCAPTION | wxRESIZE_BORDER | wxCLOSE_BOX | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
Bind(wxEVT_PAINT, &TextureProjectorFrame::on_paint, this);
// A resize changes the gate, so the texture has to be re-stretched under it.
Bind(wxEVT_SIZE, [this](wxSizeEvent &evt) { Refresh(); evt.Skip(); });
SetTransparent(wxByte(m_alpha));
// Hide rather than destroy: the gizmo owns this window's lifetime, and reopening should keep the
// frame exactly where it was left - its position is the placement.
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent &evt) {
if (evt.CanVeto()) {
evt.Veto();
Hide();
} else
evt.Skip();
});
}
void TextureProjectorFrame::set_texture(const std::vector<unsigned char> &gray, int width, int height)
{
if (width <= 0 || height <= 0 || gray.size() < size_t(width) * size_t(height)) {
m_bitmap = wxBitmap();
Refresh();
return;
}
wxImage img(width, height);
unsigned char *dst = img.GetData();
for (size_t i = 0, n = size_t(width) * size_t(height); i < n; ++i) {
const unsigned char v = gray[i];
dst[i * 3 + 0] = v;
dst[i * 3 + 1] = v;
dst[i * 3 + 2] = v;
}
m_bitmap = wxBitmap(img);
Refresh();
}
void TextureProjectorFrame::set_opacity(int alpha)
{
m_alpha = std::clamp(alpha, 20, 255);
SetTransparent(wxByte(m_alpha));
Refresh();
}
wxRect TextureProjectorFrame::client_rect_on_screen() const
{
const wxSize sz = GetClientSize();
return wxRect(ClientToScreen(wxPoint(0, 0)), sz);
}
void TextureProjectorFrame::on_paint(wxPaintEvent &)
{
wxPaintDC dc(this);
const wxSize sz = GetClientSize();
if (sz.x <= 0 || sz.y <= 0)
return;
dc.SetBackground(wxBrush(wxColour(20, 20, 20)));
dc.Clear();
if (m_bitmap.IsOk()) {
// Stretched to fill the client area rather than kept at its own aspect: the gate maps to the
// uv unit square whatever its shape, so a non-square window genuinely does project a
// stretched texture. Showing it any other way would misrepresent the bake.
wxImage scaled = m_bitmap.ConvertToImage().Scale(sz.x, sz.y, wxIMAGE_QUALITY_NORMAL);
dc.DrawBitmap(wxBitmap(scaled), 0, 0, false);
}
// The border is the projection's hard edge, so it is drawn explicitly - with the window
// translucent, the native frame alone reads poorly against a busy 3D scene.
dc.SetPen(wxPen(wxColour(0, 200, 180), 2));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(0, 0, sz.x, sz.y);
}
}} // namespace Slic3r::GUI
+56
View File
@@ -0,0 +1,56 @@
#ifndef slic3r_TextureProjectorFrame_hpp_
#define slic3r_TextureProjectorFrame_hpp_
// The projection-frame overlay for TextureProjectionMethod::ViewProjected.
//
// A semi-transparent, resizable window that the user drags over the 3D view like a slide projector's
// gate: whatever the model shows through this window is what the texture is projected onto, and the
// window's border is the hard edge of the projection. "Apply" then reads the window's client
// rectangle, converts it into the 3D canvas's own pixel space, and builds an exact projective map
// from it (see GLGizmoTextureDisplacement::apply_projection_frame()).
//
// The window itself is deliberately dumb - it owns no placement state and reports nothing
// continuously. Its position and size *are* the placement, and they are read on demand at Apply,
// which is also when the (expensive) visible-facet selection runs. Moving the window is therefore
// free, and nothing recomputes until the user asks for it.
//
// Plain 2D (wxGraphicsContext), not a wxGLCanvas: a second GL canvas would have to share the app's
// one real wxGLContext, the cause of bugs #10 and #14 in TEXTURE_DISPLACEMENT.md. A paint-DC window
// has no such failure mode, and this one only ever draws a bitmap and a border.
#include <vector>
#include <wx/bitmap.h>
#include <wx/frame.h>
namespace Slic3r { namespace GUI {
class TextureProjectorFrame : public wxFrame
{
public:
explicit TextureProjectorFrame(wxWindow *parent);
// The same 8-bit grayscale pixels build_texture_displacement() samples, so what is aligned here
// is what gets baked. Pass width/height <= 0 to clear it.
void set_texture(const std::vector<unsigned char> &grayscale_pixels, int width, int height);
// Whole-window opacity, 0..255. Low enough to see the model through it, high enough to judge
// where the texture lands - the useful range is roughly 60..200.
void set_opacity(int alpha);
int opacity() const { return m_alpha; }
// The client area (the gate itself, excluding caption and borders) in screen coordinates. This
// is what the projection is built from, so it deliberately excludes the window decorations -
// the user aligns what they see, which is the client area.
wxRect client_rect_on_screen() const;
private:
wxBitmap m_bitmap;
int m_alpha = 140;
void on_paint(wxPaintEvent &);
};
}} // namespace Slic3r::GUI
#endif // slic3r_TextureProjectorFrame_hpp_
File diff suppressed because it is too large Load Diff
+441
View File
@@ -0,0 +1,441 @@
#ifndef slic3r_UVEditorCanvas_hpp_
#define slic3r_UVEditorCanvas_hpp_
#include <algorithm>
#include <functional>
#include <utility>
#include <vector>
// Must come before wx/glcanvas.h in any translation unit that includes this header: glcanvas.h
// pulls in the platform's real GL/gl.h, and glad/gl.h errors out if that happens first (it wants
// to be the one to define the standard include guards GL/gl.h itself defines).
#include <glad/gl.h>
#include <wx/glcanvas.h>
#include <wx/panel.h>
#include <wx/button.h>
#include <wx/tglbtn.h>
#include <wx/stattext.h>
#include <wx/statbmp.h>
#include <wx/sizer.h>
#include "libslic3r/Point.hpp"
#include "GLModel.hpp"
#include "GLTexture.hpp"
// Orca's own widgets (slic3r/GUI/Widgets), declared at global scope.
class SpinInput;
class CheckBox;
namespace Slic3r::GUI {
// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch - shows the result of
// GLGizmoTextureDisplacement's LSCM projection method as its own resizable pane (see Plater's
// "uv_editor" AUI pane) rather than folding 2D UV-space rendering into the main 3D viewport.
//
// Islands can be laid out by hand, roughly the way Blender's UV editor works: click one to select
// it, drag to move it, right-drag or press R to rotate it, S to scale it, both about its own centre.
// Islands are free to overlap - nothing re-packs them behind the user's back. The texture underneath
// is always drawn upright and axis-aligned, and it is the islands that move over it, which is what
// makes "rotate this island" a meaningful gesture rather than just spinning the whole texture.
//
// **Geometry is uploaded in the unwrap's own (raw, mm) coordinates, once**, and each island is drawn
// through its own affine matrix passed as a shader uniform. That matters: a patch can easily run to
// a million triangles, and the earlier design - which pre-transformed every UV on the CPU and
// re-uploaded the whole wireframe on every mouse-move event - made a drag cost a couple of hundred
// milliseconds per frame. Moving an island now touches a 2x3 matrix and nothing else.
//
// Uses the app's single shared wxGLContext (via wxGetApp().init_glcontext(), the same call
// View3D/Preview/AssembleView each make in GUI_Preview.cpp) rather than an independent context of
// its own, specifically so it can reuse the app's already-registered "flat"/"flat_texture"
// shaders and GLModel as-is - GLModel::render() looks up its shader via a GUI_App-wide "current
// shader", which only means anything for canvases sharing the app's one real GL context.
class UVEditorCanvas : public wxGLCanvas
{
public:
explicit UVEditorCanvas(wxWindow *parent);
// Maps one island's raw unwrap coordinate to a texture UV: the island's own hand placement and
// then the layer's tiling/rotation/offset, composed into a single affine (columns: x basis,
// y basis, translation).
using IslandTransform = Eigen::Matrix<float, 2, 3>;
// The unwrap to display, in the unwrap's own mm coordinates - *not* texture UVs. Changing this
// is the expensive path (it rebuilds every vertex buffer), so it must only be called when the
// unwrap itself changes, never merely because an island moved. Pass an empty `indices` to show
// nothing.
struct Islands
{
std::vector<Vec2f> uvs;
std::vector<Vec3i32> indices;
std::vector<int> vertex_island; // per uv
std::vector<std::pair<int, int>> boundary_edges; // island outlines, indices into uvs
int island_count = 0;
};
void set_islands(Islands islands);
// The cheap path: one transform per island. Safe to call on every mouse-move of a drag.
void set_island_transforms(std::vector<IslandTransform> transforms);
// One fill colour per island, overriding the default light-green wash - used to paint the UV
// distortion heatmap over the islands when the gizmo's "Distortion" check mode is on (#7/#14).
// Pass empty to go back to the default wash. Cheap: it never touches a vertex buffer.
void set_island_fill_colors(std::vector<ColorRGBA> colors);
// The layer's own tiling scale and rotation. Needed to map a gesture, which happens in texture-UV
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
// lives (see apply_uv_transform()). The tile settings come along because the background has to
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
// texture it will actually sample. Pass width/height <= 0 to clear it.
void set_background_texture(const std::vector<unsigned char> &grayscale_pixels, int width, int height);
// Snap a dragged island's boundary to a neighbouring island's when they come close (#2). Off is
// the honest default for overlap-friendly layouts; the pane toolbar toggles it.
void set_snap_enabled(bool enabled) { m_snap_enabled = enabled; }
bool snap_enabled() const { return m_snap_enabled; }
// High-level actions the pane's controls trigger. The canvas handles the view-only ones (framing,
// the snap toggle) itself and forwards the rest to whoever owns the island data (the gizmo), via
// the command callback - the canvas has the selection and the view, the gizmo has the layer.
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
// SelectMode / Background) and is 0 otherwise.
enum class Command {
FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected,
Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits,
SetBackground, PickTexture, PaneClosed
};
void run_command(Command cmd, float value = 0.f);
using CommandFn = std::function<void(Command, float)>;
void set_command_callback(CommandFn fn) { m_on_command = std::move(fn); }
// What the canvas shows under the islands; mirrors the gizmo's Normal/Checker/Distortion views.
enum class Background { Height, Checker, Distortion };
// Everything the pane's own controls show that the gizmo owns: the active layer and its unwrap settings.
// The gizmo pushes it whenever any of it may have changed, and the pane redraws its header, settings row
// and tool strip from it - so the controls never hold state of their own that could drift from the layer.
struct PaneState
{
bool has_layer = false; // an active layer mapped with Unwrap
wxString layer_name;
float tile_mm = 0.f;
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
int thumbnail_px = 0;
float seam_angle_deg = 40.f;
bool connect_islands = true;
bool unwrapped = false; // an unwrap exists for this layer
bool unwrap_stale = false; // paint, seams or the seam angle changed since it was made
bool mark_seams = false;
bool seam_path = false;
bool has_seams = false;
bool has_uv_edits = false;
Background background = Background::Height;
int island_count = 0;
size_t face_count = 0;
};
void set_pane_state(PaneState state);
const PaneState &pane_state() const { return m_pane_state; }
using PaneStateFn = std::function<void(const PaneState &)>;
void set_pane_state_callback(PaneStateFn fn) { m_on_pane_state = std::move(fn); }
// Called whenever the one-line status/hint text changes (current gesture + the shortcuts that
// apply right now), so the pane can show it Blender-style along the bottom.
using StatusFn = std::function<void(const wxString &)>;
void set_status_callback(StatusFn fn) { m_on_status = std::move(fn); }
// Reports an island edit as it happens. The deltas are *incremental* (one mouse event's worth)
// and already converted into the units a TextureIsland stores - unwrap mm, degrees, and a scale
// *factor* to multiply the island's existing scale by. They are incremental on purpose: the owner
// applies them and hands back fresh transforms, and if the gesture tracked geometry rather than
// raw mouse motion that round trip would feed back into itself. `finished` marks the end of a
// gesture, so the owner can rebuild the 3D preview once rather than on every motion event.
using IslandEditFn =
std::function<void(int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished)>;
void set_island_edit_callback(IslandEditFn fn) { m_on_island_edit = std::move(fn); }
// What a click grabs: a whole island (move/rotate/scale, groups move together), a single vertex, or
// a single edge (both its endpoints). Vertex/Edge are free-form UV editing - they move the actual
// unwrap coordinates, which the owner then folds into the layer's per-vertex UV overrides so the
// change is baked, not just shown (see set_vertex_edit_callback).
enum class SelectMode { Island, Vertex, Edge };
void set_select_mode(SelectMode mode);
SelectMode select_mode() const { return m_select_mode; }
// Reports a committed vertex/edge edit: the list of (unwrapped-vertex index, its new raw-unwrap
// coordinate in mm). Fired once, on mouse release, since it re-solves the displacement preview; the
// pane shows the edit live from its own geometry in the meantime. The owner maps the unwrapped index
// to a mesh vertex (via the unwrap's source_vertex) and stores the override.
using UVVertexEditFn = std::function<void(const std::vector<std::pair<int, Vec2f>> &edits)>;
void set_vertex_edit_callback(UVVertexEditFn fn) { m_on_vertex_edit = std::move(fn); }
// The primary (last-clicked) island, still the pivot for rotate/scale and the target of the
// single-island toolbar commands (Cut/Join/Unjoin). -1 if nothing is selected.
int selected_island() const { return m_selected_island; }
// Whether there is an unwrap on screen at all.
bool has_islands() const { return m_islands.island_count > 0 && !m_islands.indices.empty(); }
// The full multi-selection (Shift adds, Ctrl toggles). Always contains m_selected_island when it is
// >= 0. The gizmo reads this to decide which islands a drag moves together, unioned with each
// selected island's join group.
const std::vector<int> &selected_islands() const { return m_selection; }
void reset_view();
// Rebuilds every GPU object at the next paint, from the data the canvas keeps on the CPU. Called when the
// pane is shown again, so a hidden-and-reshown canvas never depends on GL objects surviving its native
// window being torn down.
void invalidate_gl()
{
m_gl_reset_pending = true;
Refresh();
}
private:
void on_paint(wxPaintEvent &evt);
void on_size(wxSizeEvent &evt);
void on_mouse(wxMouseEvent &evt);
void on_key(wxKeyEvent &evt);
void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas
void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op
void render();
void rebuild_island_models();
void rebuild_background_texture();
void rebuild_background_quad();
void rebuild_grid();
// The UV region worth looking at: every island, plus always at least the texture's first tile, so
// there is something sensibly framed even before anything is painted.
void content_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
// What is actually *drawn*, which is content_bounds() snapped out to whole tiles whenever the
// backdrop tiles (see rebuild_background_quad()). Both the framing and the backdrop go through
// this so they cannot disagree.
void framed_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
// Frames framed_bounds(). Bound to Home, and run once each time an unwrap first appears.
void fit_view_to_content();
// Half-extents of the visible UV region. Split out because both rendering and every mouse
// gesture need them, and they have to agree exactly or picking lands in the wrong place.
void view_half_extents(float &half_w, float &half_h) const;
Vec2f screen_to_uv(const wxPoint &px) const;
// Raw unwrap coordinate -> texture UV, through the island's own transform.
Vec2f island_uv(size_t vertex) const;
// The island under `uv`, or -1. Prefers the current selection when islands overlap, so that
// dragging one that sits under another doesn't hand the drag to its neighbour halfway through.
int island_at(const Vec2f &uv) const;
// Nearest unwrapped vertex to `uv` within a screen-space threshold, or -1 (Vertex mode picking).
int vertex_at(const Vec2f &uv) const;
// Nearest island-boundary edge to `uv` within a screen-space threshold, as its two unwrapped-vertex
// indices, or {-1,-1} (Edge mode picking).
std::pair<int, int> edge_at(const Vec2f &uv) const;
// Moves one unwrapped vertex by a texture-UV delta, converting it back into the vertex's own raw
// unwrap space through its island's inverse transform, and marks the mesh dirty so it redraws.
void move_vertex_raw(int unwrapped_vertex, const Vec2f &delta_uv);
Vec2f island_centroid(int island) const;
// Converts a delta in texture-UV space into the unwrap's mm space, undoing the layer's scale and
// rotation - the inverse of what apply_uv_transform() did on the way in.
Vec2f uv_delta_to_unwrap(const Vec2f &delta_uv) const;
// The correction that would bring the selected island's nearest boundary vertex onto a boundary
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
Vec2f snap_correction(int island) const;
void end_gesture();
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
void update_status();
// Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed.
void update_hover(const wxPoint &pos);
wxGLContext *m_context = nullptr; // owned by OpenGLManager/GUI_App, not by this canvas
Islands m_islands;
std::vector<IslandTransform> m_transforms;
// Per-island fill colour override (distortion heatmap); empty means use the default wash (#7).
std::vector<ColorRGBA> m_island_fill_colors;
// Boundary vertices per island, for snapping - a patch's boundary is a tiny fraction of it, and
// rescanning the whole uv array on every snap test would not be.
std::vector<std::vector<int>> m_island_boundary_verts;
// Per island: its outline edges, its triangles (indices into m_islands.indices) and the bounding box of its
// raw coordinates - so picking tests one island's triangles only when the cursor is inside its box.
std::vector<std::vector<std::pair<int, int>>> m_island_boundary_edges;
std::vector<std::vector<int>> m_island_tris;
std::vector<std::pair<Vec2f, Vec2f>> m_island_raw_bounds;
bool m_mesh_dirty = true;
bool m_gl_reset_pending = false;
// One set of models per island, so an island can be drawn through its own transform. Built once
// per unwrap, never on a drag. Outlines are not among them: they are drawn as screen-width quads,
// rebuilt every paint (see render()), because GL wide lines are not reliably wider than 1 px.
std::vector<GLModel> m_island_wireframe; // interior edges
std::vector<GLModel> m_island_fill; // filled; also the stencil mask that keeps islands undimmed
GLModel m_stroke_glmodel; // scratch model for the quads of one stroke pass
GLModel m_dim_quad_glmodel; // full-viewport quad that dims the texture outside the islands
int m_stencil_bits = -1; // of the default framebuffer; -1 = not queried yet
GLModel m_tile_outline_glmodel; // the texture's first tile, [0,1]^2 - the "you are here"
GLModel m_grid_glmodel;
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
float m_tiling_scale = 1.f;
float m_rotation_deg = 0.f;
bool m_tile_enabled = true;
bool m_tile_mirrored = false;
bool m_snap_enabled = false;
std::vector<unsigned char> m_background_pixels; // RGBA, expanded from the grayscale input
int m_background_width = 0, m_background_height = 0;
bool m_background_dirty = false; // the pixels need (re)uploading
bool m_background_quad_dirty = true; // only the quad's extent changed
GLTexture m_background_texture;
// Covers content_bounds(), not just [0,1]: the unwrap is packed in mm and then divided by the
// layer's tile size, so it routinely spans many tiles, and a single-unit-square backdrop would
// leave most of the islands sitting over bare background. Texcoord == position, so the GL wrap
// mode repeats it exactly the way DecodedHeightTexture::sample() does.
GLModel m_background_glmodel;
// 2D pan/zoom. m_pan is the UV-space point at the center of the view, m_zoom half the UV-space
// extent visible across the shorter screen edge. v runs *down* the screen, matching both the
// texture's own row order and every other UV editor's convention.
Vec2f m_pan = Vec2f(0.5f, 0.5f);
float m_zoom = 0.75f;
bool m_needs_fit = true; // fit the view to the next unwrap that arrives
enum class Gesture
{
None,
Pan,
MoveIsland,
RotateIsland, // right-drag: rotation tracks the mouse, ends when the button is released
RotateIslandModal, // 'R': rotation tracks the mouse until a click confirms or Esc cancels
ScaleIslandModal, // 'S': likewise, distance from the centre drives the scale
MoveVertex, // Vertex mode: drag one unwrapped vertex
MoveEdge, // Edge mode: drag both endpoints of one boundary edge
};
Gesture m_gesture = Gesture::None;
SelectMode m_select_mode = SelectMode::Island;
// The sub-element being edited in Vertex/Edge mode (unwrapped-vertex indices), or -1/{-1,-1}. This
// is the *primary* (last-picked) element of the multi-selection below.
int m_active_vertex = -1;
std::pair<int, int> m_active_edge{ -1, -1 };
// Multi-selection for Vertex/Edge modes, mirroring the island selection: plain click replaces, Shift
// adds, Ctrl toggles, and a drag moves the whole set together. m_active_vertex/m_active_edge stay the
// primary. Kept as small vectors (tiny, and order doesn't matter here).
std::vector<int> m_sel_vertices;
std::vector<std::pair<int, int>> m_sel_edges;
bool is_vertex_selected(int v) const
{
return std::find(m_sel_vertices.begin(), m_sel_vertices.end(), v) != m_sel_vertices.end();
}
bool is_edge_selected(const std::pair<int, int> &e) const
{
return std::find(m_sel_edges.begin(), m_sel_edges.end(), e) != m_sel_edges.end();
}
// Unique unwrapped-vertex endpoints of every selected edge (an endpoint shared by two selected edges
// is returned once, so a drag doesn't move it twice).
std::vector<int> selected_edge_endpoints() const;
// Last known mouse position over the canvas, and whether the pointer is currently inside it. Used to
// draw the +/- add/remove sign next to the cursor in Vertex/Edge mode.
wxPoint m_cursor_px{ 0, 0 };
bool m_cursor_inside = false;
// Set once a Vertex/Edge drag actually moves, so a bare click (select without drag) doesn't commit a
// no-op edit and take an undo snapshot for nothing.
bool m_vertex_edit_moved = false;
// Lazily-built small filled square, drawn at an edited/hovered vertex as a handle.
GLModel m_vertex_marker_glmodel;
// What a click would grab right now, highlighted so the user sees the target before clicking.
int m_hover_island = -1;
int m_hover_vertex = -1;
std::pair<int, int> m_hover_edge{ -1, -1 };
int m_selected_island = -1;
// The full multi-selection; m_selected_island is its primary (last-clicked) member. Kept as a small
// vector rather than a set because it is tiny and iteration order (primary last) is convenient.
std::vector<int> m_selection;
bool is_selected(int island) const
{
return std::find(m_selection.begin(), m_selection.end(), island) != m_selection.end();
}
wxPoint m_drag_last_px;
Vec2f m_gesture_last_uv = Vec2f::Zero();
float m_gesture_last_angle = 0.f;
float m_gesture_last_dist = 0.f;
// Rotation is tracked as two running totals over the gesture: the raw mouse rotation, and how much
// has actually been applied. With Shift held the applied total is quantised to 15-degree steps
// (Blender-style angle snapping), so the two diverge - and driving the applied total off the raw
// one, rather than snapping each incremental delta, is what makes the snap stable instead of
// juddering. The raw/applied split also survives crossing +/-180 degrees, which a single wrapped
// angle would not. m_rot_applied doubles as the modal-rotate undo amount for Esc.
float m_rot_raw_deg = 0.f;
float m_rot_applied_deg = 0.f;
// The island's absolute on-screen rotation when the gesture began (decoded from its transform), so
// Shift can snap to *global* 15-degree marks (0/15/30...) rather than 15 degrees relative to
// wherever the island happened to start (#10). Also drives the angle read-out and the dial.
float m_rot_base_deg = 0.f;
float m_rot_display_deg = 0.f; // current absolute angle, for the status line and dial needle
float m_modal_scale_accum = 1.f; // so Esc can undo exactly what the modal scale applied, as a factor
// A protractor drawn around the island while it rotates: a ring, a tick every 15 degrees, and a
// needle at the current angle, so the rotation is legible (#11). Rebuilt each frame during a
// rotation gesture (cheap: a few hundred short lines) and left empty otherwise.
GLModel m_dial_glmodel;
void rebuild_rotation_dial();
// The island's current absolute rotation in degrees, decoded from its transform's first column.
float island_rotation_deg(int island) const;
IslandEditFn m_on_island_edit;
UVVertexEditFn m_on_vertex_edit;
CommandFn m_on_command;
StatusFn m_on_status;
PaneState m_pane_state;
PaneStateFn m_on_pane_state;
};
class UVToolButton; // a drawn icon button, defined in UVEditorCanvas.cpp
// The UV editor pane that goes into Plater's "uv_editor" AUI pane: a header with the active layer, the
// canvas background and Unwrap; a settings row with the unwrap's seam angle and island connection; a
// narrow tool strip down the left (selection mode, seams, island actions, snap and framing); the canvas;
// and a status line naming the current gesture, with the unwrap summary on its right.
//
// It holds no editing state of its own. Every control sends a Command through the canvas to the gizmo,
// and the gizmo pushes PaneState back, from which the controls are redrawn (see apply_state()). The gizmo
// still talks to the inner canvas, reached via canvas().
class UVEditorPanel : public wxPanel
{
public:
explicit UVEditorPanel(wxWindow *parent);
UVEditorCanvas *canvas() { return m_canvas; }
private:
void on_tool(wxCommandEvent &evt);
void apply_state(const UVEditorCanvas::PaneState &state);
// Selection-mode toggles, and the tools that need a selected island, follow the canvas.
void refresh_selection_tools();
UVEditorCanvas *m_canvas = nullptr;
UVToolButton *m_thumb = nullptr; // the layer's texture; a click opens the texture library
wxStaticText *m_layer_name = nullptr;
wxStaticText *m_tile = nullptr;
UVToolButton *m_background[3]{};
UVToolButton *m_unwrap = nullptr;
::SpinInput *m_seam_angle = nullptr;
::CheckBox *m_connect = nullptr;
UVToolButton *m_select[3]{};
UVToolButton *m_mark_seams = nullptr;
UVToolButton *m_seam_path = nullptr;
UVToolButton *m_clear_seams = nullptr;
UVToolButton *m_avg_scale = nullptr;
UVToolButton *m_cut = nullptr;
UVToolButton *m_join = nullptr;
UVToolButton *m_unjoin = nullptr;
UVToolButton *m_clear_edits = nullptr;
UVToolButton *m_snap = nullptr;
UVToolButton *m_frame = nullptr;
wxStaticText *m_status = nullptr;
wxStaticText *m_stats = nullptr;
};
} // namespace Slic3r::GUI
#endif // slic3r_UVEditorCanvas_hpp_
+23 -8
View File
@@ -819,7 +819,9 @@ int OrcaCloudServiceAgent::user_logout(bool request)
}
}
clear_session();
// An explicit logout also wipes the backend the token storage option is not using, so a token
// stranded by switching that option cannot sign the account back in later.
clear_session(/*all_backends=*/request);
return BAMBU_NETWORK_SUCCESS;
}
@@ -1604,7 +1606,9 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
}
}
(void) stored;
if (stored) {
secret_stored = true;
}
}
bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
@@ -1644,6 +1648,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
}
if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) {
secret_stored = true;
out_secret = plain;
// Upgrade legacy payloads to signed format
if (payload.rfind("v2:", 0) != 0) {
@@ -1661,6 +1666,7 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) {
out_secret.assign(static_cast<const char*>(secret.GetData()), secret.GetSize());
if (!out_secret.empty()) {
secret_stored = true;
return true;
}
}
@@ -1670,11 +1676,20 @@ bool OrcaCloudServiceAgent::load_user_secret(std::string& out_secret)
return false;
}
void OrcaCloudServiceAgent::clear_user_secret()
void OrcaCloudServiceAgent::clear_user_secret(bool all_backends)
{
wxSecretStore store = wxSecretStore::GetDefault();
if (store.IsOk()) {
store.Delete(SECRET_STORE_SERVICE);
// Nothing this process loaded or saved: leave the store alone. Deleting would only cost a
// keychain round trip (or a hang while the keychain is unresponsive) and could remove a
// login another instance just saved.
if (!secret_stored.exchange(false) && !all_backends) {
return;
}
if (all_backends || !m_use_encrypted_token_file) {
wxSecretStore store = wxSecretStore::GetDefault();
if (store.IsOk()) {
store.Delete(SECRET_STORE_SERVICE);
}
}
compute_fallback_path();
@@ -2023,13 +2038,13 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
return success;
}
void OrcaCloudServiceAgent::clear_session()
void OrcaCloudServiceAgent::clear_session(bool all_backends)
{
{
std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{};
}
clear_user_secret();
clear_user_secret(all_backends);
}
// ============================================================================
+7 -2
View File
@@ -326,7 +326,7 @@ public:
void persist_user_secret(const std::string& secret);
bool load_user_secret(std::string& out_secret);
void clear_user_secret();
void clear_user_secret(bool all_backends = false);
// Token refresh helpers
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
@@ -344,7 +344,7 @@ public:
bool persist = true);
// Accepts either nested Orca cloud / GoTrue session JSON or flat WebView token JSON.
bool set_user_session(const nlohmann::json& session_json, bool notify_login = true);
void clear_session();
void clear_session(bool all_backends = false);
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
@@ -413,6 +413,11 @@ private:
// Member variables - auth state
PkceBundle pkce_bundle;
std::string secret_fallback_path;
// Set once this process has read a secret from the store or written one. Unless the user logs
// out explicitly, clear_user_secret() only touches the store while it is set, so a logged-out
// instance (the GUI polls the login status every 2 s) makes no keychain calls and cannot wipe
// a login another instance saved.
std::atomic_bool secret_stored{false};
SessionHandler session_handler;
OnLoginCompleteHandler on_login_complete_handler;
SessionInfo session;