mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-02 06:47:04 +00:00
Add Parallax preview & fix Undo\Redo history
This commit is contained in:
@@ -747,9 +747,13 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
if (patch.indices.empty())
|
||||
return;
|
||||
|
||||
const indexed_triangle_set &base = mv->mesh().its;
|
||||
if (base.vertices.size() != patch.vertices.size())
|
||||
return; // shouldn't happen: get_facets_strict() always returns the full vertex array
|
||||
// No "patch.vertices.size() == mesh vertex count" check here, and that is the point: a *brush*
|
||||
// stroke splits triangles, so the selector appends split vertices and the patch array is longer
|
||||
// than the mesh's. An earlier version bailed out on that as "shouldn't happen", which meant the
|
||||
// bump model was never built while brushing and render_painter_gizmo() silently fell back to the
|
||||
// Normal (true-displacement) preview - Fast looked broken for brush and fine for Face/Connected
|
||||
// area, because only the brush splits. Everything below indexes the patch's own vertex array, so
|
||||
// the extra vertices are simply carried through.
|
||||
|
||||
// Unpainted triangles, so the surrounding surface still renders (the render path hides the real
|
||||
// model in bump mode). get_facets_strict() returns the same vertex array whatever state is asked.
|
||||
@@ -762,7 +766,7 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
||||
// mesh rebuilds (on drag end) with them. The other projections keep projecting in-shader.
|
||||
const TextureDisplacementLayer *active = active_layer();
|
||||
std::vector<Vec2f> vertex_uv = active != nullptr ? compute_layer_vertex_uvs(patch, *active) : std::vector<Vec2f>{};
|
||||
m_bump_preview_uses_vertex_uv = vertex_uv.size() == base.vertices.size();
|
||||
m_bump_preview_uses_vertex_uv = vertex_uv.size() == patch.vertices.size();
|
||||
if (!m_bump_preview_uses_vertex_uv)
|
||||
vertex_uv.clear();
|
||||
|
||||
@@ -945,6 +949,13 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
shader->set_uniform("rotation_rad", layer->rotation_deg * float(M_PI) / 180.f);
|
||||
shader->set_uniform("uv_offset", layer->offset);
|
||||
shader->set_uniform("invert", layer->invert);
|
||||
// The parallax step in the triplanar path needs the real height, not just its gradient, so it
|
||||
// needs the midlevel the bake subtracts - and the camera position in the volume's own local space,
|
||||
// to build the view ray it walks along. Without the parallax the pattern is welded to the base
|
||||
// surface: it does not slide as the camera orbits and does not deepen with depth_mm, which is
|
||||
// exactly when the fast preview stops looking like geometry.
|
||||
shader->set_uniform("midlevel", layer->midlevel);
|
||||
shader->set_uniform("eye_model_pos", Vec3f((trafo_matrix.inverse() * camera.get_position()).cast<float>()));
|
||||
// When set, the shader samples at the per-vertex uv baked into the mesh (LSCM) rather than
|
||||
// projecting; see rebuild_bump_preview_mesh().
|
||||
shader->set_uniform("use_vertex_uv", m_bump_preview_uses_vertex_uv);
|
||||
@@ -970,9 +981,11 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||
if (patch.indices.empty())
|
||||
return;
|
||||
const indexed_triangle_set &base = mv->mesh().its;
|
||||
if (base.vertices.size() != patch.vertices.size())
|
||||
return;
|
||||
// Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a
|
||||
// brush stroke splits a triangle, after which the patch array is longer - and since patch triangle
|
||||
// indices are used to index it, reading the mesh's array instead would run off the end. An earlier
|
||||
// version guarded that by bailing out, which quietly disabled the Checker and Distortion overlays
|
||||
// for anything painted with the brush.
|
||||
const TextureDisplacementLayer *layer = active_layer();
|
||||
if (layer == nullptr)
|
||||
return;
|
||||
@@ -980,16 +993,16 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
// The checker samples wherever the projection puts it; the projections the shader can't
|
||||
// reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader.
|
||||
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
|
||||
const bool have_uvs = uv.size() == base.vertices.size();
|
||||
const bool have_uvs = uv.size() == patch.vertices.size();
|
||||
m_uvcheck_uses_vertex_uv = have_uvs;
|
||||
|
||||
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
|
||||
std::vector<float> distortion(base.vertices.size(), 0.5f);
|
||||
std::vector<float> distortion(patch.vertices.size(), 0.5f);
|
||||
if (m_uv_check_mode == UVCheckMode::Distortion && have_uvs) {
|
||||
std::vector<float> tri_log(patch.indices.size(), 0.f);
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
const float a3 = 0.5f * (base.vertices[t[1]] - base.vertices[t[0]]).cross(base.vertices[t[2]] - base.vertices[t[0]]).norm();
|
||||
const float a3 = 0.5f * (patch.vertices[t[1]] - patch.vertices[t[0]]).cross(patch.vertices[t[2]] - patch.vertices[t[0]]).norm();
|
||||
const Vec2f e0 = uv[t[1]] - uv[t[0]];
|
||||
const Vec2f e1 = uv[t[2]] - uv[t[0]];
|
||||
const float a2 = 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x());
|
||||
@@ -1003,8 +1016,8 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end());
|
||||
median = sorted[sorted.size() / 2];
|
||||
}
|
||||
std::vector<float> sum(base.vertices.size(), 0.f);
|
||||
std::vector<int> cnt(base.vertices.size(), 0);
|
||||
std::vector<float> sum(patch.vertices.size(), 0.f);
|
||||
std::vector<int> cnt(patch.vertices.size(), 0);
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
// +/- 2 stops (4x stretch either way) spans the full blue->red range.
|
||||
const float d = std::clamp(0.5f + (tri_log[f] - median) / 4.f, 0.f, 1.f);
|
||||
@@ -1020,10 +1033,10 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
||||
|
||||
GLModel::Geometry init_data;
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 };
|
||||
init_data.reserve_vertices(base.vertices.size());
|
||||
init_data.reserve_vertices(patch.vertices.size());
|
||||
init_data.reserve_indices(patch.indices.size() * 3);
|
||||
for (size_t vi = 0; vi < base.vertices.size(); ++vi)
|
||||
init_data.add_vertex(base.vertices[vi], Vec3f(distortion[vi], 0.f, 0.f),
|
||||
for (size_t vi = 0; vi < patch.vertices.size(); ++vi)
|
||||
init_data.add_vertex(patch.vertices[vi], Vec3f(distortion[vi], 0.f, 0.f),
|
||||
have_uvs ? uv[vi] : Vec2f::Zero());
|
||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||
init_data.add_triangle(unsigned(tri[0]), unsigned(tri[1]), unsigned(tri[2]));
|
||||
@@ -2753,51 +2766,82 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
||||
return any_paint;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
bool GLGizmoTextureDisplacement::plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr || m_subdivide_target_mm <= 0.f)
|
||||
return;
|
||||
if (m_subdivide_target_mm <= 0.f)
|
||||
return false;
|
||||
|
||||
update_model_object(); // flush any in-progress stroke into the committed masks first
|
||||
|
||||
std::vector<uint8_t> region;
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> painted_tri;
|
||||
if (!collect_paint_region(region, &painted_tri)) {
|
||||
show_error(nullptr, _u8L("Paint the area you want to subdivide first - adaptive subdivision only "
|
||||
"refines where you have painted."));
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> region;
|
||||
if (!collect_paint_region(region, &out.painted_tri))
|
||||
return false;
|
||||
|
||||
// Feature-adaptive: sample the combined displacement so refinement follows texture curvature. A
|
||||
// null sampler (only LSCM layers, or nothing decodable) falls back to the uniform target below.
|
||||
// null sampler (only LSCM layers, or nothing decodable) falls back to the length baseline alone.
|
||||
HeightFieldSampler sampler;
|
||||
if (m_subdivide_feature) {
|
||||
TextureDisplacementFacetsData facets{};
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
facets[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||
sampler = make_combined_displacement_sampler(mv->mesh().its, mv->texture_displacement_layers, facets);
|
||||
facets[size_t(i)] = mv.texture_displacement_facet(i).get_data();
|
||||
sampler = make_combined_displacement_sampler(mv.mesh().its, mv.texture_displacement_layers, facets);
|
||||
}
|
||||
|
||||
// Do the (potentially slow) refinement before taking the snapshot, so a no-op leaves no empty
|
||||
// undo step - mirrors remesh_model().
|
||||
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down to); in
|
||||
// plain adaptive mode the target edge length is the only criterion, so the floor must not be
|
||||
// allowed to silently override a target the user set below it.
|
||||
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
|
||||
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
|
||||
std::vector<int> source;
|
||||
indexed_triangle_set refined;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
// The budget slider is "triangles the refinement may *add*", so the model's own count is the
|
||||
// baseline - otherwise the control would be meaningless (or a dead end) on a dense model.
|
||||
refined = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
|
||||
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
||||
&source, sampler, tol, floor);
|
||||
out.refined = subdivide_mesh_adaptive(mv.mesh().its, region, m_subdivide_target_mm,
|
||||
int(mv.mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
||||
&out.source, sampler, tol, floor);
|
||||
}
|
||||
if (refined.indices.size() == mv->mesh().its.indices.size()) {
|
||||
return out.refined.indices.size() != mv.mesh().its.indices.size();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::apply_adaptive_subdivision(ModelVolume &mv, SubdivisionPlan &&plan)
|
||||
{
|
||||
// Other paint channels ride across via the standard remap; texture-displacement paint is rebuilt
|
||||
// below from the plan's source map, which is the whole point of driving this by the paint.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv.save_painting();
|
||||
mv.set_mesh(TriangleMesh(std::move(plan.refined)));
|
||||
mv.set_new_unique_id();
|
||||
mv.calculate_convex_hull();
|
||||
mv.restore_painting(saved_painting); // resets extra facets (incl. texture-displacement) + remaps the rest
|
||||
|
||||
// Carry each layer's paint onto the new mesh: a new triangle is painted iff its source triangle
|
||||
// was fully painted in that layer. Children inherit their parent's source, so this is exact.
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
if (plan.painted_tri[size_t(slot)].empty())
|
||||
continue;
|
||||
TriangleSelector sel(mv.mesh());
|
||||
for (size_t i = 0; i < plan.source.size(); ++i)
|
||||
if (plan.painted_tri[size_t(slot)][size_t(plan.source[i])])
|
||||
sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||
mv.texture_displacement_facet(slot).set(sel);
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr)
|
||||
return;
|
||||
|
||||
update_model_object(); // flush any in-progress stroke into the committed masks first
|
||||
|
||||
if (!mv->is_texture_displacement_painted()) {
|
||||
show_error(nullptr, _u8L("Paint the area you want to subdivide first - adaptive subdivision only "
|
||||
"refines where you have painted."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Plan before the snapshot, so a no-op leaves no empty undo step - mirrors remesh_model().
|
||||
SubdivisionPlan plan;
|
||||
if (!plan_adaptive_subdivision(*mv, plan)) {
|
||||
show_error(nullptr, _u8L("Nothing to subdivide - the painted area already meets the target edge "
|
||||
"length and detail tolerance, or the triangle budget is already used up."));
|
||||
return;
|
||||
@@ -2805,26 +2849,7 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Adaptive subdivide for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||
|
||||
// Other paint channels ride across via the standard remap; texture-displacement paint is rebuilt
|
||||
// by hand below from the source map, which is the whole point of driving this by the paint.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
||||
mv->set_mesh(TriangleMesh(std::move(refined))); // refined is not needed past here; source carries the paint map
|
||||
mv->set_new_unique_id();
|
||||
mv->calculate_convex_hull();
|
||||
mv->restore_painting(saved_painting); // resets extra facets (incl. texture-displacement) + remaps the rest
|
||||
|
||||
// Carry each layer's paint onto the new mesh: a new triangle is painted iff its source triangle
|
||||
// was fully painted in that layer. Children inherit their parent's source, so this is exact.
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
if (painted_tri[slot].empty())
|
||||
continue;
|
||||
TriangleSelector sel(mv->mesh());
|
||||
for (size_t i = 0; i < source.size(); ++i)
|
||||
if (painted_tri[slot][source[i]])
|
||||
sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||
mv->texture_displacement_facet(slot).set(sel);
|
||||
}
|
||||
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
@@ -2921,6 +2946,62 @@ void GLGizmoTextureDisplacement::smooth_model()
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::replace_mesh_keep_all_paint(ModelVolume &mv, TriangleMesh &&new_mesh)
|
||||
{
|
||||
const indexed_triangle_set old_its = mv.mesh().its;
|
||||
std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS> saved_texture;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
saved_texture[size_t(i)] = mv.texture_displacement_facet(i).get_data();
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv.save_painting();
|
||||
|
||||
mv.set_mesh(std::move(new_mesh));
|
||||
mv.set_new_unique_id();
|
||||
mv.calculate_convex_hull();
|
||||
mv.restore_painting(saved_painting); // clears the extra facets, then remaps the four standard channels
|
||||
|
||||
// ... and the same spatial remap for the texture-displacement masks, which restore_painting() knows
|
||||
// nothing about. Without this a remesh would wipe the texture paint, which is fine for a standalone
|
||||
// "Remesh" click but fatal for Standard mode's pipeline: it remeshes *after* the user has painted,
|
||||
// and the subdivision and displacement that follow are both driven by that paint.
|
||||
const Transform3d to_source = Slic3r::Geometry::translation_transform(mv.mesh().get_init_shift());
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
|
||||
if (saved_texture[size_t(i)].bitstream.empty())
|
||||
continue;
|
||||
TriangleSelector::TriangleSplittingData remapped =
|
||||
TriangleSelector::remap_painting(old_its, saved_texture[size_t(i)], mv.mesh().its, to_source,
|
||||
{}); // no existing paint to merge with: set_mesh() just cleared it
|
||||
if (!remapped.bitstream.empty())
|
||||
mv.texture_displacement_facet(i).set_data(std::move(remapped));
|
||||
}
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::plan_remesh(const ModelVolume &mv, float target_edge_mm, float sharp_angle_deg,
|
||||
TriangleMesh &out)
|
||||
{
|
||||
if (target_edge_mm <= 0.f)
|
||||
return false;
|
||||
|
||||
// CGAL isotropic remeshing can be slow on a big mesh, which is why this is separated from applying
|
||||
// it: the caller runs it before taking the snapshot so a failure leaves no empty undo step.
|
||||
const indexed_triangle_set &src = mv.mesh().its;
|
||||
indexed_triangle_set remeshed;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
remeshed = MeshBoolean::cgal::remesh_isotropic(src, double(target_edge_mm), 3, double(sharp_angle_deg));
|
||||
}
|
||||
// remesh_isotropic() signals failure by handing the input straight back, so compare against it
|
||||
// structurally. Vertex count alone is not enough: a remesh that only redistributes triangles at
|
||||
// roughly the current density legitimately lands on the same count, and treating that as failure
|
||||
// meant a perfectly good result got thrown away with an error message.
|
||||
if (remeshed.indices.empty() ||
|
||||
(remeshed.vertices.size() == src.vertices.size() && remeshed.indices.size() == src.indices.size() &&
|
||||
remeshed.indices == src.indices))
|
||||
return false;
|
||||
|
||||
out = TriangleMesh(std::move(remeshed));
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::remesh_model()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
@@ -2928,39 +3009,18 @@ void GLGizmoTextureDisplacement::remesh_model()
|
||||
if (mv == nullptr || mo == nullptr || m_remesh_target_edge_mm <= 0.f)
|
||||
return;
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
|
||||
// CGAL isotropic remeshing can be slow on a big mesh; do it before taking the snapshot so a failure
|
||||
// (it returns the input unchanged) doesn't leave an empty undo step.
|
||||
const indexed_triangle_set &src = mv->mesh().its;
|
||||
indexed_triangle_set remeshed;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
remeshed = MeshBoolean::cgal::remesh_isotropic(mv->mesh().its, double(m_remesh_target_edge_mm), 3,
|
||||
m_remesh_keep_sharp_edges ? double(m_remesh_sharp_angle_deg) : 0.0);
|
||||
}
|
||||
// remesh_isotropic() signals failure by handing the input straight back, so compare against it
|
||||
// structurally. Vertex count alone is not enough: a remesh that only redistributes triangles at
|
||||
// roughly the current density legitimately lands on the same count, and treating that as failure
|
||||
// meant a perfectly good result got thrown away with an error message.
|
||||
const bool unchanged = remeshed.indices.empty() ||
|
||||
(remeshed.vertices.size() == src.vertices.size() && remeshed.indices.size() == src.indices.size() &&
|
||||
remeshed.indices == src.indices);
|
||||
if (unchanged) {
|
||||
TriangleMesh remeshed;
|
||||
if (!plan_remesh(*mv, m_remesh_target_edge_mm, m_remesh_keep_sharp_edges ? m_remesh_sharp_angle_deg : 0.f,
|
||||
remeshed)) {
|
||||
show_error(nullptr, _u8L("Remeshing did not change the model. It may be non-manifold (open edges or "
|
||||
"edges shared by more than two triangles), or the target edge length may "
|
||||
"already be met."));
|
||||
return;
|
||||
}
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Remesh model for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||
// Same save/replace/restore-painting dance as subdivide: texture-displacement paint has no remap
|
||||
// across a topology change, so it is dropped rather than left pointing at triangles that moved.
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
||||
mv->set_mesh(TriangleMesh(std::move(remeshed)));
|
||||
mv->set_new_unique_id();
|
||||
mv->calculate_convex_hull();
|
||||
mv->restore_painting(saved_painting);
|
||||
replace_mesh_keep_all_paint(*mv, std::move(remeshed));
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
@@ -3075,7 +3135,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
||||
return m_thumbnails[slot].get();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::bake()
|
||||
void GLGizmoTextureDisplacement::bake(bool own_snapshot)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
if (!mv || m_bake_in_progress)
|
||||
@@ -3101,7 +3161,112 @@ void GLGizmoTextureDisplacement::bake()
|
||||
if (m_state == On && m_c->selection_info() && m_c->selection_info()->model_object())
|
||||
update_from_model_object(false);
|
||||
m_parent.set_as_dirty();
|
||||
});
|
||||
}, own_snapshot);
|
||||
}
|
||||
|
||||
// Standard mode's fixed recipe. These are both the values the hidden controls are pinned to and what
|
||||
// bake_standard() drives its remesh and subdivision with - one set of numbers, so the preview cannot
|
||||
// disagree with the bake. Chosen to be safe on an arbitrary imported part rather than optimal on any
|
||||
// particular one: a 1 mm isotropic remesh gives the subdivider an even starting density whatever the
|
||||
// input looked like, and the subdivision then spends up to 1.5 M triangles chasing texture curvature
|
||||
// down to a 0.02 mm floor, which is finer than any FDM nozzle will resolve. The triangle budget is not
|
||||
// here: it is the one control Standard mode still shows, so it belongs to the user (its default is
|
||||
// m_subdivide_budget_k's initialiser).
|
||||
static constexpr float STD_REMESH_EDGE_MM = 1.0f;
|
||||
static constexpr float STD_REMESH_SHARP_DEG = 40.f;
|
||||
static constexpr float STD_SUBDIV_MAX_EDGE_MM = 20.f;
|
||||
static constexpr float STD_SUBDIV_DETAIL_MM = 0.02f;
|
||||
static constexpr float STD_SUBDIV_MIN_EDGE_MM = 0.02f;
|
||||
|
||||
bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
{
|
||||
bool changed = false;
|
||||
const auto pin = [&changed](auto &field, auto value) {
|
||||
if (field != value) {
|
||||
field = value;
|
||||
changed = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (mv != nullptr) {
|
||||
pin(mv->texture_displacement_options.displace_border, true);
|
||||
pin(mv->texture_displacement_options.smooth_enabled, false);
|
||||
}
|
||||
pin(m_subdivide_adaptive, true);
|
||||
pin(m_subdivide_feature, true);
|
||||
pin(m_subdivide_target_mm, STD_SUBDIV_MAX_EDGE_MM);
|
||||
pin(m_subdivide_detail_mm, STD_SUBDIV_DETAIL_MM);
|
||||
pin(m_subdivide_min_edge_mm, STD_SUBDIV_MIN_EDGE_MM);
|
||||
// Deliberately *not* pinned: the triangle budget stays visible and editable in Standard mode, so
|
||||
// pinning it would fight the user's own slider every frame.
|
||||
pin(m_remesh_target_edge_mm, STD_REMESH_EDGE_MM);
|
||||
pin(m_remesh_keep_sharp_edges, true);
|
||||
pin(m_remesh_sharp_angle_deg, STD_REMESH_SHARP_DEG);
|
||||
return changed;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::bake_standard()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mv == nullptr || mo == nullptr || m_bake_in_progress)
|
||||
return;
|
||||
|
||||
update_model_object(); // flush the active layer's in-progress strokes before anything reads them
|
||||
if (!mv->is_texture_displacement_painted()) {
|
||||
show_error(nullptr, _u8L("Nothing is painted, there is nothing to bake."));
|
||||
return;
|
||||
}
|
||||
apply_standard_mode_presets(mv); // belt and braces: never bake with values the panel is not showing
|
||||
|
||||
// Both stages are planned before the snapshot so a stage that has nothing to do is simply skipped.
|
||||
// Skipping is normal, not a failure: a mesh that is already even needs no remesh, and one that is
|
||||
// already fine enough for the texture needs no subdivision.
|
||||
TriangleMesh remeshed;
|
||||
const bool do_remesh = plan_remesh(*mv, STD_REMESH_EDGE_MM, STD_REMESH_SHARP_DEG, remeshed);
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
{
|
||||
// ONE undo step for the whole pipeline. take_snapshot() records the state *before* the change,
|
||||
// so a single Undo goes all the way back to the untouched mesh - which is the only thing "undo
|
||||
// the bake" can sensibly mean when the bake is also what prepared the geometry. The background
|
||||
// displacement job is told not to add its own (see queue_texture_displacement_bake's
|
||||
// take_snapshot), because an undo step landing between the subdivision and the displacement
|
||||
// leaves a mesh with 1.5 M extra triangles and no relief on it - and baking again from there
|
||||
// subdivides that mesh a second time.
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"),
|
||||
UndoRedo::SnapshotType::GizmoAction);
|
||||
if (do_remesh)
|
||||
replace_mesh_keep_all_paint(*mv, std::move(remeshed));
|
||||
|
||||
// The remesh carries the paint across spatially, but if that remap came back empty the rest of
|
||||
// the pipeline has nothing to work from - stop here rather than silently baking a flat mesh.
|
||||
if (!mv->is_texture_displacement_painted()) {
|
||||
show_error(nullptr, _u8L("The painted area could not be transferred onto the remeshed model. Undo, "
|
||||
"then switch to Pro mode to prepare the mesh before painting."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Planning the subdivision has to happen inside the snapshot because it reads the mesh the
|
||||
// remesh just produced. It is the expensive step, but by here we are committed anyway.
|
||||
SubdivisionPlan plan;
|
||||
if (plan_adaptive_subdivision(*mv, plan))
|
||||
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||
}
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
auto it = std::find(objs.begin(), objs.end(), mo);
|
||||
if (it != objs.end())
|
||||
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||
}
|
||||
plater->changed_object(*mo);
|
||||
update_from_model_object(false); // reload selectors against the prepared mesh + carried paint
|
||||
m_parent.set_as_dirty();
|
||||
|
||||
// ... and finally the displacement itself, in the background exactly as the Pro-mode button does -
|
||||
// except that it commits into the snapshot taken above instead of pushing another one.
|
||||
bake(/* own_snapshot */ false);
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_paint_cursor_hint()
|
||||
@@ -3237,6 +3402,42 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->tooltip(_u8L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it "
|
||||
"back beside the toolbar."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
// Standard / Pro, right-aligned on the header row. A two-position slider rather than a checkbox
|
||||
// because it is a mode, not an option: Standard hides every mesh-preparation control and folds the
|
||||
// whole recipe into Bake, Pro shows all of it and hands the ordering to the user.
|
||||
{
|
||||
const float mode_w = m_imgui->scaled(6.2f);
|
||||
ImGui::SameLine(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - mode_w));
|
||||
ImGui::PushItemWidth(mode_w);
|
||||
// The format string carries no conversion, so ImGui prints it verbatim as the slider's label -
|
||||
// which is how a two-position slider gets word labels instead of "0" and "1".
|
||||
const std::string mode_label = pro_mode() ? _u8L("Pro") : _u8L("Standard");
|
||||
if (ImGui::SliderInt("##panel_mode", &m_panel_mode, 0, 1, mode_label.c_str())) {
|
||||
m_panel_mode = std::clamp(m_panel_mode, 0, 1);
|
||||
if (!pro_mode()) {
|
||||
// Leaving the subdivision preview open would strand a wireframe whose controls just
|
||||
// disappeared, so close it as part of the switch.
|
||||
m_subdivide_editing = false;
|
||||
m_subdivide_preview_tris = -1;
|
||||
m_subdivide_preview_glmodel.reset();
|
||||
if (apply_standard_mode_presets(mv))
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Standard - paint, then press Bake: the mesh is remeshed, refined where the "
|
||||
"texture bends, and displaced in one step.\n\nPro - every mesh-preparation "
|
||||
"control is shown and you run Remesh, Subdivide and Bake yourself, in the "
|
||||
"order you want."),
|
||||
m_imgui->scaled(20.f));
|
||||
}
|
||||
// Pinned every frame while Standard is active, so what Preview shows is always what Bake will do.
|
||||
if (!pro_mode() && apply_standard_mode_presets(mv))
|
||||
m_preview_params_dirty = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Selection mode: which of TriangleSelector's existing click/brush mechanisms drives painting.
|
||||
@@ -3854,8 +4055,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
// (The "Add layer" button now lives next to the "Texture layers" heading, as an icon.)
|
||||
|
||||
// Settings for the whole stack rather than one layer, so they sit outside the layer list.
|
||||
if (mv != nullptr) {
|
||||
// Settings for the whole stack rather than one layer, so they sit outside the layer list. Standard
|
||||
// mode pins them (see apply_standard_mode_presets()) instead of showing them.
|
||||
if (pro_mode() && mv != nullptr) {
|
||||
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -3912,7 +4114,34 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
m_imgui->text(_u8L("Not enough vertices for fine detail?"));
|
||||
m_imgui->text(_u8L("Subdivision"));
|
||||
|
||||
// The triangle budget is the one subdivision control Standard mode keeps: its right value depends
|
||||
// on the part rather than on the recipe (a big model, or a fine texture, simply needs more of them),
|
||||
// and raising or lowering it is safe without understanding anything else in this section. Shared
|
||||
// with the Pro layout below so there is one definition of the widget.
|
||||
const auto budget_slider = [this]() {
|
||||
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||
if (ImGui::SliderInt((_u8L("Added triangles (k)") + "##subdivbudget").c_str(), &m_subdivide_budget_k, 10, 2000)) {
|
||||
m_subdivide_budget_k = std::clamp(m_subdivide_budget_k, 10, 2000);
|
||||
if (m_subdivide_editing)
|
||||
rebuild_subdivide_preview();
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("How many thousand triangles the refinement may add. It always splits the "
|
||||
"worst-fitting triangle first, so a run that uses the whole budget has still spent "
|
||||
"it where it shows most - raise this if the result still looks too coarse."),
|
||||
m_imgui->scaled(20.f));
|
||||
};
|
||||
|
||||
// Everything else from here to the Bake row is mesh preparation, which is exactly what Standard
|
||||
// mode takes over: it pins those to one recipe and runs them from the Bake button (see
|
||||
// bake_standard()), so showing them would only invite changing numbers that get overwritten.
|
||||
if (!pro_mode())
|
||||
budget_slider();
|
||||
if (pro_mode()) {
|
||||
|
||||
if (ImGui::Checkbox(_u8L("Only painted area (adaptive)").c_str(), &m_subdivide_adaptive)) {
|
||||
if (m_subdivide_editing)
|
||||
@@ -3996,18 +4225,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->scaled(20.f));
|
||||
}
|
||||
|
||||
// The budget. Refinement is worst-error-first, so a run that hits it has still spent its
|
||||
// triangles on the biggest deviations - raising it buys detail, it does not redistribute it.
|
||||
if (ImGui::SliderInt((_u8L("Added triangles (k)") + "##subdivbudget").c_str(), &m_subdivide_budget_k, 10, 2000)) {
|
||||
m_subdivide_budget_k = std::clamp(m_subdivide_budget_k, 10, 2000);
|
||||
preview_live();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("How many thousand triangles the refinement may add. It always splits the "
|
||||
"worst-fitting triangle first, so a run that uses the whole budget has still spent "
|
||||
"it where it shows most - raise this if the preview still looks too coarse."),
|
||||
m_imgui->scaled(20.f));
|
||||
ImGui::PopItemWidth();
|
||||
budget_slider();
|
||||
|
||||
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
|
||||
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
|
||||
@@ -4074,7 +4293,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
// Remesh: even out uneven triangle sizes (CGAL isotropic remeshing). GPU Delaunay isn't practical
|
||||
// here, but this delivers the same goal - a consistent triangle size across the whole model.
|
||||
m_imgui->text(_u8L("Uneven triangle sizes?"));
|
||||
m_imgui->text(_u8L("Remeshing"));
|
||||
if (m_remesh_target_edge_mm <= 0.f && mv != nullptr) {
|
||||
// Seed the target with the model's current mean edge length, so the default is a sensible
|
||||
// "make everything about the size it already averages".
|
||||
@@ -4114,9 +4333,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Rebuilds the whole model with triangles close to this edge length - splitting the big "
|
||||
"ones and merging the small ones - so displacement has an even density to work with. "
|
||||
"Replaces the geometry and clears any not-yet-baked paint (already-baked bumps are kept)."),
|
||||
"Replaces the geometry; your paint is carried onto the new triangles spatially, so it "
|
||||
"survives (already-baked bumps are kept too)."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
} // pro_mode()
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
m_imgui->disabled_begin(mv == nullptr || !mv->is_texture_displacement_painted());
|
||||
@@ -4136,9 +4358,23 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(m_bake_in_progress || mv == nullptr || !mv->is_texture_displacement_painted());
|
||||
if (m_imgui->button(m_bake_in_progress ? _L("Baking...") : m_desc.at("bake")))
|
||||
bake();
|
||||
if (m_imgui->button(m_bake_in_progress ? _L("Baking...") : m_desc.at("bake"))) {
|
||||
// Standard mode's Bake is the whole pipeline (remesh -> refine -> displace); Pro's is only the
|
||||
// displacement, because there the user has already prepared the mesh with the controls above.
|
||||
if (pro_mode())
|
||||
bake();
|
||||
else
|
||||
bake_standard();
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(pro_mode() ?
|
||||
_u8L("Turn the painted height maps into real geometry, by moving the vertices that are "
|
||||
"already there. Use Subdivide first if the mesh is too coarse to show the detail.") :
|
||||
_u8L("Turn the painted height maps into real geometry. The mesh is remeshed to an even "
|
||||
"density and refined where the texture bends first, so the detail has vertices to "
|
||||
"land on - all in one step."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
ImGui::Separator();
|
||||
if (m_imgui->button(_L("Close")))
|
||||
|
||||
@@ -67,7 +67,51 @@ private:
|
||||
void add_texture_layer();
|
||||
void remove_texture_layer(int slot);
|
||||
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
|
||||
void bake();
|
||||
// `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 remapped across (see replace_mesh_keep_all_paint()).
|
||||
void bake_standard();
|
||||
|
||||
// Replaces `mv`'s mesh and carries *every* paint channel onto it, texture displacement included -
|
||||
// the four standard channels via ModelVolume::restore_painting(), the eight texture-displacement
|
||||
// masks via the same TriangleSelector::remap_painting() spatial remap, which restore_painting()
|
||||
// does not cover. Shared by Remesh and by Standard mode's pipeline.
|
||||
static void replace_mesh_keep_all_paint(ModelVolume &mv, TriangleMesh &&new_mesh);
|
||||
// Remesh and adaptive Subdivide are each split into a *plan* (the heavy geometry work, touching
|
||||
// nothing) and an *apply* (pure mutation). That split is what lets both the standalone buttons and
|
||||
// Standard mode's one-button pipeline run the expensive part **before** taking the undo snapshot,
|
||||
// so a run that turns out to be a no-op does not leave an empty undo step behind - and it keeps
|
||||
// snapshot ownership with the caller, which matters because the buttons want one snapshot per click
|
||||
// while the pipeline wants a single one around remesh + subdivide together.
|
||||
struct SubdivisionPlan
|
||||
{
|
||||
indexed_triangle_set refined;
|
||||
std::vector<int> source; // new tri -> input tri
|
||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> painted_tri; // per layer, per input tri
|
||||
};
|
||||
// False means "nothing to refine" and `out` must not be used.
|
||||
bool plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const;
|
||||
static void apply_adaptive_subdivision(ModelVolume &mv, SubdivisionPlan &&plan);
|
||||
// False means the remesh failed or changed nothing (CGAL signals failure by handing the input back).
|
||||
static bool plan_remesh(const ModelVolume &mv, float target_edge_mm, float sharp_angle_deg, TriangleMesh &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.
|
||||
@@ -327,7 +371,7 @@ private:
|
||||
// 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.
|
||||
int m_subdivide_budget_k = 200;
|
||||
int m_subdivide_budget_k = 1500;
|
||||
void subdivide_model_adaptive();
|
||||
// Fills `region` (per current-mesh triangle, 1 = refine) from the union of every layer's painted
|
||||
// area. If `painted_tri` is non-null, also fills, per layer, the fully-painted triangles to carry
|
||||
@@ -345,7 +389,9 @@ private:
|
||||
// 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 and drops not-yet-baked paint (no remap across a topology change).
|
||||
// the geometry, but unlike subdivide it keeps every paint channel: replace_mesh_keep_all_paint()
|
||||
// remaps the texture-displacement masks 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.
|
||||
|
||||
@@ -41,41 +41,55 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
|
||||
Plater *plater = wxGetApp().plater();
|
||||
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||
const auto commit = [this, plater]() {
|
||||
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
|
||||
if (volume == nullptr)
|
||||
return;
|
||||
|
||||
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();
|
||||
|
||||
volume->set_mesh(std::move(m_result));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
// 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 (and paint outside the baked area, if any) are left
|
||||
// untouched so the user can keep sculpting with the same textures.
|
||||
for (const TextureDisplacementLayer &layer : m_input.layers)
|
||||
if (!layer.empty() && layer.slot >= 0 && layer.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
||||
volume->texture_displacement_facet(layer.slot).reset();
|
||||
|
||||
// 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 (and paint outside the baked area, if any) are left
|
||||
// untouched so the user can keep sculpting with the same textures.
|
||||
for (const TextureDisplacementLayer &layer : m_input.layers)
|
||||
if (!layer.empty() && layer.slot >= 0 && layer.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
||||
volume->texture_displacement_facet(layer.slot).reset();
|
||||
ModelObject *object = volume->get_object();
|
||||
if (object == nullptr)
|
||||
return;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
plater->changed_object(*object);
|
||||
}
|
||||
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished)
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished,
|
||||
bool take_snapshot)
|
||||
{
|
||||
TextureDisplacementBakeInput input;
|
||||
input.take_snapshot = take_snapshot;
|
||||
input.volume_id = volume.id();
|
||||
input.base_mesh = volume.mesh().its;
|
||||
input.layers = volume.texture_displacement_layers;
|
||||
|
||||
@@ -22,6 +22,10 @@ struct TextureDisplacementBakeInput
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
TextureDisplacementOptions options;
|
||||
// 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,
|
||||
@@ -45,7 +49,8 @@ private:
|
||||
// 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, std::function<void()> on_finished);
|
||||
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished,
|
||||
bool take_snapshot = true);
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
|
||||
Reference in New Issue
Block a user