Files
OrcaSlicer/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
T
SoftFever c63bd5d474 Switch to the Fast view while UV islands are selected in the texture gizmo
The island highlight was hidden under the Normal view's relief. Normal comes back once no island is selected, unless another view was picked in the meantime.
2026-10-13 00:18:51 +08:00

1077 lines
74 KiB
C++

#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 <Eigen/Core>
#include <array>
#include <atomic>
#include "libslic3r/Color.hpp"
#include <cstddef>
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "admesh/stl.h"
#include <cstdint>
#include <functional>
#include "libslic3r/Point.hpp"
#include <imgui.h>
#include <map>
#include <memory>
#include <optional>
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
#include <string>
#include <vector>
#include <utility>
#include <wx/event.h>
#include <wx/string.h>
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 TextureColorSettings &color_settings,
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;
// One colour mixes are meant to reach: a bin of the textures' colour histogram, in CIELAB, and how
// much of the image it covers.
struct MixTarget
{
Vec3f lab = Vec3f::Zero();
float weight = 0.f;
};
// The colours worth mixing for: those of the colouring layers in `layers` whose image is not made of
// flat colours (TextureDetail::flat_colors - those print in single filaments only, so no mix could
// serve them). The layers weigh the same and, before only the heaviest bins are kept (which is what
// bounds rank_mixes()), together 1. Read from the image as imported, as analyze_texture_detail()
// does, so the Smoothing slider does not move the palette around.
static std::vector<MixTarget> mix_targets(const std::vector<TextureDisplacementLayer> &layers);
// Mixes of pairs of `filaments`, best first and at most `limit` of them. Each is the one that most
// improves the match to `targets` given those ranked before it, counted only where it beats the
// nearest single filament by the quantizer's prefer-pure margin - which is where the quantizer will
// actually pick it. Stops early once another mix would make no noticeable difference, so a texture
// the filaments already cover gets few mixes or none.
//
// Ratios are the short-cycle ones (k/d for d up to 6): the slicer interleaves a mix layer by layer,
// and a long cycle prints as visible bands rather than as a colour. A mix's `rgb` is the colour its
// mixed filament slot will show (blend_color_multi(), as the sidebar computes it), so the match, the
// preview and the slot all agree on what the mix looks like.
static std::vector<PaletteEntry> rank_mixes(const std::vector<ColorRGBA> &filaments,
const std::vector<MixTarget> &targets, int limit);
// The first `count` mixes of `ranking` the project can give a filament slot to. A mix `reusable`
// reports as already having a fixed slot costs nothing; any other uses up one of `free_slots`, and
// is skipped once they run out - so the palette never offers a colour a bake could not print.
static std::vector<PaletteEntry> pick_mixes(const std::vector<PaletteEntry> &ranking, int count, int free_slots,
const std::function<bool(const PaletteEntry &)> &reusable);
// The printable palette: the loaded filaments, entry i being filament i, followed by `mixes`.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments,
const std::vector<PaletteEntry> &mixes);
// 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);
// The filament (0-based) each entry of `palette` prints in, by palette index: a single filament is
// itself, a mix is the slot `slot_for_mix` returns for it. Only the mixes `triangle_color` actually
// uses (palette index + 1 per triangle, 0 for none) are asked for, since asking is what creates a
// slot. A mix that gets no slot (-1) prints in its dominant component; one nothing uses maps to -1.
//
// This is what the bake writes into the paint: a palette index is a filament only for the single
// filaments, while a mix's slot can sit anywhere among the project's mixed slots.
static std::vector<int> palette_filaments(const std::vector<PaletteEntry> &palette,
const std::vector<uint8_t> &triangle_color,
const std::function<int(const PaletteEntry &)> &slot_for_mix);
// Everything the jobs need to colour with, for the current volume: the palette, its single-filament
// part, and the despeckle passes. Empty when no layer is actually colouring. Read-only: no filament
// slot is created here, only when a bake commits (see palette_filaments()).
TextureColorSettings color_settings_for(const ModelVolume &mv);
// The printable palette for the current volume and project, rebuilt only when what it depends on
// changes - see the definition for why that caching is not optional.
const std::vector<PaletteEntry> &cached_palette();
// The quantizers for the cached palette and for its single filaments alone (flat-colour images),
// filled on first use. Only the subdivide preview matches colours on this thread - the jobs build
// their own from the palette they capture - so a palette rebuild, such as every step of a count
// drag, costs no lookup cube unless that preview asks for one.
std::pair<ColorQuantizeFn, ColorQuantizeFn> palette_quantizers();
std::vector<PaletteEntry> m_palette_cache;
ColorQuantizeFn m_palette_quantizer;
ColorQuantizeFn m_palette_pure_quantizer;
// Set when a rebuild changed the palette the previews were drawn with; cleared by rebuild_preview().
bool m_palette_changed = false;
// What the cache was built from.
std::vector<ColorRGBA> m_palette_filaments;
std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images
std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand
bool m_palette_mixing = false;
int m_palette_mix_count = 0;
int m_palette_free_slots = 0;
std::vector<std::string> m_palette_slots; // the project's mixed slots
// The loaded physical filaments, clamped to the states mmu_segmentation_facets can address.
static std::vector<ColorRGBA> filament_palette();
// The Normal preview's triangles, grouped by the palette entry they will print in. Colour is per facet
// and the palette is small, 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);
// The model's own colour paint (mmu_segmentation_facets) as the gizmo draws it over its surface: the
// sub-triangles painted in a filament, grouped by that filament. NONE - the volume's own filament - is
// left out, so those triangles keep the gizmo's neutral, as they do in the preview.
struct PaintedColors
{
indexed_triangle_set facets; // over the paint's whole vertex array
std::vector<int> source; // per triangle of `facets`: the model triangle it lies in
std::vector<int> state; // per triangle of `facets`: its filament state, 1-based
// The triangles of `facets` outside the model triangles `excluded` marks, in order. A model
// triangle past the end of `excluded` is not excluded.
std::vector<size_t> outside(const std::vector<bool> &excluded) const;
};
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
// The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the
// undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast
// while islands are selected and comes back once none are. A view the user clicks in the View row is
// theirs - the pane's background buttons are not a pick, Height only meaning "the texture view".
struct IslandViewFollow
{
bool shown = false; // the highlight, as of the last step()
bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone
bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown
// The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as
// apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself.
int step(bool shown_now, int view, bool debug_open);
// A View row button clicked, the active one included. The row is drawn after step() in the same frame,
// so `shown` is this frame's.
void picked(int view)
{
switched = false;
keep_normal = shown && view == 0;
}
};
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;
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
// with the main canvas's instance transform. The base alone would let the keyboard shortcut open it there.
bool on_is_activable() 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);
// Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file
// can carry it. Cheap and idempotent - called wherever either of the two changes.
void store_panel_state();
// 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{};
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
// later - and clears it; the frame number is what retires a request whose row has gone off screen
// in the meantime. Panel state only, not saved with the project.
std::string m_slider_type_request;
int m_slider_type_request_frame = 0;
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
// (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 view on screen, numbered as apply_view_mode() takes it.
int view_mode() const;
// Run once a frame, after the pane's commands: switches the view as m_island_view says.
void follow_island_highlight();
IslandViewFollow m_island_view;
// 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: the paint feedback over a preview (see
// render_painter_gizmo()). Cheap (the painted patch only), translucent so the preview shows through,
// 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;
// The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null.
const std::vector<int> *island_highlight_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();
// The model's colour paint, drawn over the surface so the colours a bake wrote stay visible - the
// canvas draws no volume while a paint gizmo is open, and the selectors hold only displacement paint.
// Left out wherever the preview on screen shows paint of its own (`whole_stack`: every layer's, as the
// Normal preview does; otherwise the active layer's), since an opaque overlay there would hide that
// preview. Only while a layer colours: it is the colour workflow's result, and every other paint gizmo
// shows the model neutral.
//
// Two levels: the paint's sub-triangles, which take a selector over the whole mesh and change only with
// the paint itself, and the part drawn, which follows every flushed stroke.
PaintedColors m_painted_colors;
std::string m_painted_colors_key;
GLModel m_painted_colors_glmodel;
std::string m_painted_colors_drawn_key;
std::vector<std::pair<int, std::pair<size_t, size_t>>> m_painted_colors_runs; // filament state, index range
void rebuild_painted_colors(bool whole_stack);
// False when there was nothing to draw.
bool render_painted_colors(bool whole_stack);
// 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 matches each fragment against, captured with its mesh. Empty when the
// active layer is not colouring, which is what tells the shader to fall back to the model's own
// colour. Every entry carries the colour it prints in, so drawing it needs nothing else.
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, each keyed by the image_data it was built from
// (see get_layer_thumbnail()). Held weakly rather than by address, as decode_height_texture()'s cache is:
// an image freed and replaced by another at the same address must not pass for it.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<std::weak_ptr<const std::vector<unsigned char>>, 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;
std::weak_ptr<const std::vector<unsigned char>> m_height_tex_source;
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;
std::weak_ptr<const std::vector<unsigned char>> m_color_tex_source;
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. Kept in the application
// config rather than in the project: it describes the user's window, not the model, so opening
// someone else's project must not move their panel.
bool m_undocked = false;
// The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// 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;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_brush;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
// 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_