This commit is contained in:
ExPikaPaka
2026-07-08 08:50:47 +02:00
parent 1d61962ea7
commit 7f2598d0d6
26 changed files with 9144 additions and 7 deletions

View File

@@ -442,6 +442,8 @@ set(lisbslic3r_sources
Tesselate.cpp
Tesselate.hpp
TextConfiguration.hpp
TextureDisplacement.cpp
TextureDisplacement.hpp
Thread.cpp
Thread.hpp
Time.cpp

View File

@@ -1977,6 +1977,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

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>
@@ -878,6 +880,64 @@ 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;
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1007,13 +1067,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.
@@ -1066,6 +1131,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());
@@ -1082,6 +1148,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());
@@ -1096,6 +1163,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());
@@ -1109,10 +1177,16 @@ 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),
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());
@@ -1162,6 +1236,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;
@@ -1169,13 +1245,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;
@@ -1195,6 +1275,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);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1216,6 +1303,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);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);

View File

@@ -0,0 +1,226 @@
#include "TextureDisplacement.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <optional>
#include "Model.hpp"
#include "PNGReadWrite.hpp"
#include "TriangleSelector.hpp"
namespace Slic3r {
float DecodedHeightTexture::sample(const Vec2f &uv) const
{
if (empty())
return 0.f;
auto wrap01 = [](float x) {
x = std::fmod(x, 1.f);
return x < 0.f ? x + 1.f : x;
};
const float fx = wrap01(uv.x()) * float(width);
const float fy = wrap01(uv.y()) * float(height);
int x0 = int(std::floor(fx)) % width;
int y0 = int(std::floor(fy)) % height;
if (x0 < 0) x0 += width;
if (y0 < 0) y0 += height;
const int x1 = (x0 + 1) % width;
const int y1 = (y0 + 1) % height;
const float tx = fx - std::floor(fx);
const float ty = fy - std::floor(fy);
auto at = [this](int x, int y) { return float(pixels[size_t(y) * size_t(width) + size_t(x)]) / 255.f; };
const float top = at(x0, y0) * (1.f - tx) + at(x1, y0) * tx;
const float bottom = at(x0, y1) * (1.f - tx) + at(x1, y1) * tx;
return top * (1.f - ty) + bottom * ty;
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{
DecodedHeightTexture result;
if (layer.empty())
return result;
const png::ReadBuf rbuf{ layer.image_data->data(), layer.image_data->size() };
if (!png::is_png(rbuf))
// Only 8-bit grayscale PNG height maps are supported. The GUI is responsible for
// converting any imported image (jpg, color png, ...) to that format on import, so this
// code never needs a dependency on wxWidgets/libjpeg to decode arbitrary user images.
return result;
png::ImageGreyscale img;
if (!png::decode_png(rbuf, img) || img.cols == 0 || img.rows == 0)
return result;
result.width = int(img.cols);
result.height = int(img.rows);
result.pixels = std::move(img.buf);
return result;
}
Vec2f project_texture_displacement_uv(const Vec3f &position, const Vec3f &normal, const TextureDisplacementLayer &layer)
{
// Planar-project onto the two axes orthogonal to the dominant component of `normal`. Using a
// single projection axis for an entire patch (rather than per-vertex normals) avoids visible
// seams where the local normal direction changes. Proper seam-aware UV parametrization is a
// later-phase improvement.
const Vec3f n = normal.cwiseAbs();
Vec2f planar;
if (n.x() >= n.y() && n.x() >= n.z())
planar = Vec2f(position.y(), position.z());
else if (n.y() >= n.x() && n.y() >= n.z())
planar = Vec2f(position.x(), position.z());
else
planar = Vec2f(position.x(), position.y());
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
planar *= scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad);
const float sn = std::sin(rad);
const Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
return rotated + layer.offset;
}
// Area-weighted vertex normals computed from the patch's own (pre-displacement) connectivity.
// Since the patch's vertices have not moved yet at the point this is called, these are identical
// to the surface normals of the mesh the patch was extracted from.
static std::vector<Vec3f> texture_displacement_vertex_normals(const indexed_triangle_set &its)
{
std::vector<Vec3f> normals(its.vertices.size(), Vec3f::Zero());
for (const stl_triangle_vertex_indices &tri : its.indices) {
const Vec3f &a = its.vertices[tri[0]];
const Vec3f &b = its.vertices[tri[1]];
const Vec3f &c = its.vertices[tri[2]];
// Cross product magnitude is twice the face area, so this naturally area-weights the
// contribution of each incident face to its vertices.
const Vec3f area_weighted_normal = (b - a).cross(c - a);
normals[tri[0]] += area_weighted_normal;
normals[tri[1]] += area_weighted_normal;
normals[tri[2]] += area_weighted_normal;
}
for (Vec3f &n : normals) {
const float len = n.norm();
n = (len > 1e-8f) ? Vec3f(n / len) : Vec3f::UnitZ();
}
return normals;
}
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data)
{
const indexed_triangle_set original_mesh = base_mesh;
indexed_triangle_set working_mesh = original_mesh;
bool mesh_modified = false;
// Layers behave like stacked image-editor layers: applied in slot order, each one sculpting
// the surface left by the layers before it. This is what makes overlapping layers "blend".
std::vector<const TextureDisplacementLayer *> ordered_layers;
for (const TextureDisplacementLayer &layer : layers)
if (!layer.empty() && layer.slot >= 0 && size_t(layer.slot) < TEXTURE_DISPLACEMENT_MAX_LAYERS)
ordered_layers.push_back(&layer);
std::sort(ordered_layers.begin(), ordered_layers.end(),
[](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; });
for (const TextureDisplacementLayer *layer : ordered_layers) {
const TriangleSelector::TriangleSplittingData &stored_data = facets_data[size_t(layer->slot)];
if (stored_data.triangles_to_split.empty())
continue;
const DecodedHeightTexture height = decode_height_texture(*layer);
if (height.empty())
continue;
TriangleSelector::TriangleSplittingData data = stored_data;
if (mesh_modified) {
// The stored paint mask is relative to the volume's original (unbaked) mesh; remap it
// onto the working mesh, which earlier layers may have already displaced/subdivided.
data = TriangleSelector::remap_painting(original_mesh, data, working_mesh, Transform3d::Identity(),
std::optional<std::reference_wrapper<const TriangleSelector::TriangleSplittingData>>{});
if (data.bitstream.empty())
continue;
}
const TriangleMesh selector_mesh(working_mesh);
TriangleSelector selector(selector_mesh);
selector.deserialize(data, false);
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
if (patch.indices.empty())
continue;
// get_facets_strict() returns the *entire* mesh's vertex array regardless of which state
// was asked for (only the returned triangle indices are filtered by state) -- so `patch`
// and `rest` share the exact same vertex indexing. That is what lets the weld below be a
// plain index check instead of a position-based lookup.
indexed_triangle_set rest = selector.get_facets_strict(EnforcerBlockerType::NONE);
assert(rest.vertices.size() == patch.vertices.size());
// A vertex that is also used by at least one *unpainted* triangle is a boundary vertex:
// its final position is ambiguous (it belongs to both the painted and untouched surface),
// so it must never be displaced -- otherwise the baked patch would tear away from the
// rest of the mesh. Only vertices used exclusively by painted triangles ("interior" to the
// patch) are eligible for displacement. This is what keeps the bake seamless without any
// remeshing/hole-filling at the boundary.
std::vector<bool> is_boundary_vertex(rest.vertices.size(), false);
for (const stl_triangle_vertex_indices &tri : rest.indices)
for (int i = 0; i < 3; ++i)
is_boundary_vertex[tri[i]] = true;
const std::vector<Vec3f> vertex_normals = texture_displacement_vertex_normals(patch);
Vec3f average_normal = Vec3f::Zero();
for (const stl_triangle_vertex_indices &tri : patch.indices)
for (int i = 0; i < 3; ++i)
average_normal += vertex_normals[tri[i]];
average_normal = (average_normal.norm() > 1e-8f) ? Vec3f(average_normal.normalized()) : Vec3f::UnitZ();
const float sign = layer->invert ? -1.f : 1.f;
std::vector<int> interior_vertex_remap(rest.vertices.size(), -1);
indexed_triangle_set working_mesh_next = std::move(rest);
for (const stl_triangle_vertex_indices &tri : patch.indices) {
stl_triangle_vertex_indices final_tri;
for (int i = 0; i < 3; ++i) {
const int vi = tri[i];
if (is_boundary_vertex[vi]) {
// Shared with the untouched surface: reuse the existing, undisplaced vertex.
final_tri[i] = vi;
continue;
}
if (interior_vertex_remap[vi] < 0) {
const Vec2f uv = project_texture_displacement_uv(patch.vertices[vi], average_normal, *layer);
const float h = height.sample(uv);
const Vec3f displaced = patch.vertices[vi] + vertex_normals[vi] * (h * layer->depth_mm * sign);
interior_vertex_remap[vi] = int(working_mesh_next.vertices.size());
working_mesh_next.vertices.push_back(displaced);
}
final_tri[i] = interior_vertex_remap[vi];
}
working_mesh_next.indices.push_back(final_tri);
}
working_mesh = std::move(working_mesh_next);
mesh_modified = true;
}
if (mesh_modified)
its_compactify_vertices(working_mesh);
return working_mesh;
}
indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
{
TextureDisplacementFacetsData facets_data;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data);
}
} // namespace Slic3r

View File

@@ -0,0 +1,120 @@
#ifndef slic3r_TextureDisplacement_hpp_
#define slic3r_TextureDisplacement_hpp_
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <cereal/cereal.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include <array>
#include "Point.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
namespace Slic3r {
class ModelVolume;
// Maximum number of simultaneous texture-displacement layers a single ModelVolume can hold.
// Each layer owns its own paint mask (ModelVolume::texture_displacement_facet(slot)), so this
// is also the number of independent EnforcerBlockerType selectors kept per volume.
static constexpr size_t TEXTURE_DISPLACEMENT_MAX_LAYERS = 8;
// One texture asset plus its projection/displacement parameters. Several layers may be painted
// onto overlapping areas of the same volume: they are applied in slot order (like layers in an
// image editor), each one displacing the surface that resulted from the layers before it. This
// is what "layered/blended" texture displacement means in this feature.
struct TextureDisplacementLayer
{
// Index into ModelVolume::texture_displacement_facets, assigned once when the layer is
// created. Not reused for the lifetime of the ModelVolume, so a deleted layer's slot simply
// becomes unused rather than being handed to a different layer.
int slot = -1;
std::string name;
// Path on the local filesystem the image was loaded from (informational; may be stale or
// empty, e.g. after loading a .3mf on a different machine).
std::string path;
// Path inside the .3mf archive once saved (empty until the project is saved once).
std::string path_in_3mf;
// Raw encoded image bytes. Only 8-bit grayscale PNG is understood by decode_height_texture()
// (libslic3r has no GUI image toolkit available); the GUI converts any imported image to that
// format before storing it here, so the baking code never needs to depend on wxWidgets.
std::shared_ptr<std::vector<unsigned char>> image_data;
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
float tiling_scale = 10.f; // size of one texture tile, in mm
float rotation_deg = 0.f;
Vec2f offset = Vec2f::Zero();
bool invert = false;
bool empty() const { return !image_data || image_data->empty(); }
template<class Archive> void save(Archive &ar) const
{
std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string();
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert);
}
template<class Archive> void load(Archive &ar)
{
std::string blob;
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
}
};
// Decoded 8-bit grayscale height sample, independent of any GUI/OpenGL texture object so it can
// be evaluated from a background bake Job as well as from GUI-side preview code.
struct DecodedHeightTexture
{
std::vector<uint8_t> pixels; // row-major, top-to-bottom, one byte per pixel
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
// Bilinearly sampled height in [0, 1] at a tiling (wrapping) normalized uv coordinate.
float sample(const Vec2f &uv) const;
};
// Decode a layer's raw image bytes into sampleable grayscale height data. Returns an empty
// DecodedHeightTexture if image_data is empty or is not an 8-bit grayscale PNG.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// Project a mesh-local position onto a layer's height texture, honouring its tiling scale,
// rotation and offset. Uses a simple dominant-axis planar projection of the given normal: proper
// seam-aware UV parametrization is a later-phase improvement (see project plan).
Vec2f project_texture_displacement_uv(const Vec3f &position, const Vec3f &normal, const TextureDisplacementLayer &layer);
// One paint mask (as stored by ModelVolume::texture_displacement_facets) per possible layer slot.
using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS>;
// Bake all painted texture-displacement layers into `base_mesh`'s geometry, restricted to the
// painted area(s) only (the rest of the mesh is left untouched). Layers are applied in slot
// order, each measuring its displacement from the surface left by the previous one. Returns the
// mesh unchanged if nothing is painted or no layer has a usable texture.
//
// Takes plain copied data rather than a ModelVolume reference so it is safe to call from a
// background thread (e.g. a bake Job's process() method) on a snapshot captured on the main
// thread, without touching the live Model concurrently with the UI.
//
// Known Phase 1-3 limitation: this does not attempt to remap texture-displacement paint data
// across topology-changing operations performed outside this gizmo (e.g. ModelObject::split(),
// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels.
// Such operations will silently drop any unbaked texture-displacement paint on the affected
// volume. This is an explicit extension point for a later phase, not an oversight.
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data);
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data from `volume`
// and forwards to the overload above.
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
} // namespace Slic3r
#endif // slic3r_TextureDisplacement_hpp_