Add texture displacement gizmo and UV editor

This commit is contained in:
ExPikaPaka
2026-07-16 08:43:21 +02:00
parent a393b21642
commit 05083bb6ab
15 changed files with 5405 additions and 102 deletions

View File

@@ -301,6 +301,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Jobs/SLAImportJob.hpp
GUI/Jobs/TextureDisplacementBakeJob.cpp
GUI/Jobs/TextureDisplacementBakeJob.hpp
GUI/Jobs/TextureDisplacementPreviewJob.cpp
GUI/Jobs/TextureDisplacementPreviewJob.hpp
GUI/Jobs/ThreadSafeQueue.hpp
GUI/Jobs/UpgradeNetworkJob.cpp
GUI/Jobs/UpgradeNetworkJob.hpp
@@ -470,6 +472,8 @@ set(SLIC3R_GUI_SOURCES
GUI/TaskManager.hpp
GUI/TextLines.cpp
GUI/TextLines.hpp
GUI/TextureLibrary.cpp
GUI/TextureLibrary.hpp
GUI/TickCode.cpp
GUI/TickCode.hpp
GUI/TroubleshootDialog.cpp
@@ -486,6 +490,8 @@ set(SLIC3R_GUI_SOURCES
GUI/UserManager.hpp
GUI/UserNotification.cpp
GUI/UserNotification.hpp
GUI/UVEditorCanvas.cpp
GUI/UVEditorCanvas.hpp
GUI/WebDownPluginDlg.cpp
GUI/WebDownPluginDlg.hpp
GUI/WebGuideDialog.cpp

View File

@@ -171,7 +171,8 @@ bool GLTexture::load_from_svg_file(const std::string& filename, bool use_mipmaps
return false;
}
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy)
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy,
bool use_mipmaps)
{
m_width = w;
m_height = h;
@@ -195,18 +196,51 @@ bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
bool use_mipmaps = true;
if (use_mipmaps) {
// we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards
int lod_w = m_width;
int lod_h = m_height;
// We generate the mipmap chain ourselves rather than calling glGenerateMipmap(), which this
// codebase has historically considered unreliable on some graphics cards.
//
// Each level is a 2x2 box filter of the level above it. Note this used to re-upload the
// *level-0* buffer at every level instead, which does not downscale anything -- it just
// reinterprets the image's first lod_w * lod_h texels as the whole smaller level, i.e. every
// level below 0 held a crop of the top-left corner. It went unnoticed for as long as every
// caller drew these textures at roughly their native size (where only level 0 is ever
// sampled); it shows up the moment one is drawn small enough to select a lower level, as a
// texture that visibly turns into something else as it shrinks.
std::vector<unsigned char> scratch;
const std::vector<unsigned char> *src = &data;
int src_w = m_width;
int src_h = m_height;
GLint level = 0;
while (lod_w > 1 || lod_h > 1) {
while (src_w > 1 || src_h > 1) {
++level;
lod_w = std::max(lod_w / 2, 1);
lod_h = std::max(lod_h / 2, 1);
n_pixels = lod_w * lod_h;
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
const int lod_w = std::max(src_w / 2, 1);
const int lod_h = std::max(src_h / 2, 1);
std::vector<unsigned char> lod(size_t(lod_w) * size_t(lod_h) * 4);
for (int y = 0; y < lod_h; ++y) {
// min() rather than a plain 2*y+1: an odd source extent leaves the last output texel
// with only one source row/column to average, not two.
const int y0 = std::min(2 * y, src_h - 1);
const int y1 = std::min(2 * y + 1, src_h - 1);
for (int x = 0; x < lod_w; ++x) {
const int x0 = std::min(2 * x, src_w - 1);
const int x1 = std::min(2 * x + 1, src_w - 1);
for (int c = 0; c < 4; ++c) {
const unsigned int sum = (*src)[(size_t(y0) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y0) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)];
lod[(size_t(y) * size_t(lod_w) + size_t(x)) * 4 + size_t(c)] = (unsigned char)(sum / 4);
}
}
}
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)lod.data()));
scratch = std::move(lod);
src = &scratch;
src_w = lod_w;
src_h = lod_h;
}
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level));

View File

@@ -100,7 +100,10 @@ namespace GUI {
bool load_from_file(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy);
bool load_from_svg_file(const std::string& filename, bool use_mipmaps, bool compress, bool apply_anisotropy, unsigned int max_size_px);
//BBS load GLTexture from raw pixel data
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false);
// `data` is RGBA, w * h * 4 bytes. With use_mipmaps, a real box-filtered mipmap chain is
// built, so the texture may safely be drawn smaller than its pixel size.
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false,
bool use_mipmaps = true);
// meanings of states: (std::pair<int, bool>)
// first field (int):
// 0 -> no changes

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,15 @@
#include "GLGizmoPainterBase.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLTexture.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include <array>
#include <map>
#include <memory>
#include <string>
namespace Slic3r::GUI {
@@ -22,6 +30,10 @@ public:
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;
@@ -54,6 +66,173 @@ private:
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
void bake();
// 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();
// 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();
// 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.
GLTexture *get_layer_thumbnail(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 layer's texture chooser: a drop-down whose closed state and every one of whose entries
// shows a large preview image on the left and the texture's name on the right, plus an adjacent
// button that imports an image file from disk into the user texture folder. Shipped and
// user-imported textures are listed under separate headings.
void render_texture_picker(TextureDisplacementLayer &layer);
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer);
// Draws a picker row (image left, name right) on top of a full-width Selectable, and leaves the
// cursor below it. Shared by the drop-down's closed state and its individual entries so the two
// cannot drift apart. Returns true when the row is clicked.
bool texture_row(const char *id, const std::string &name, GLTexture *thumbnail, bool selected, float width);
float texture_row_height() const;
// "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();
// 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
// (a classic bump map) instead of actually moving vertices, using the
// resources/shaders/*/texture_displacement_bump.* 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 any bump is a shading
// illusion, not real geometry -- "Bake" always produces the true, exact result either way.
void rebuild_bump_preview_mesh();
void render_bump_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);
// 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 --
@@ -62,7 +241,214 @@ private:
int m_active_layer_slot = 0;
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.
int m_subdivide_count = 1;
bool m_subdivide_editing = false;
int m_subdivide_preview_count = -1; // the count m_subdivide_preview_glmodel was built for
GLModel m_subdivide_preview_glmodel;
void rebuild_subdivide_preview();
void render_subdivide_preview();
// 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).
float m_remesh_target_edge_mm = 0.f;
void remesh_model();
// 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_bump_preview_mesh()/render_bump_preview_mesh().
bool m_use_bump_preview = false;
// Set from the UV editor's per-move island edits instead of rebuilding the (potentially large) bump
// 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_bump_preview_dirty = false;
GLModel m_bump_preview_glmodel;
// Whether the current bump mesh carries a precomputed per-vertex uv (LSCM) that the shader
// should sample at directly, rather than projecting in-shader. Set by rebuild_bump_preview_mesh().
bool m_bump_preview_uses_vertex_uv = false;
// GPU island drag: while an island is dragged in the UV editor, the bump 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_bump_active_chart is the dragged island (or -1);
// m_bump_active_vertex flags its base vertices; m_bump_baked_active_xf is that island's placement
// baked into the current mesh, against which the live delta is measured; m_bump_island_delta is the
// resulting final-uv-space affine handed to the shader (identity except mid-drag).
int m_bump_active_chart = -1;
std::vector<uint8_t> m_bump_active_vertex;
Eigen::Matrix<float, 2, 3> m_bump_baked_active_xf = Eigen::Matrix<float, 2, 3>::Identity();
Eigen::Matrix<float, 2, 3> m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
void compute_bump_active_vertices(const std::vector<int> &charts);
// 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 bump preview and the UV-check overlay.
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer) 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;
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
// 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 (bump/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;
// Bumped 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.
uint64_t m_preview_generation = 0;
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it (and the
// fast/bump preview, which samples this texture, actually shows the blur).
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures;
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
// every stroke end and every slider release -- but depth/tiling/rotation/offset/blend change
// none of this, so re-extracting the patch and re-solving on those edits would be pure waste.
// Held as the real values rather than a hash: TriangleSplittingData has an exact operator==, so
// there is no reason to accept a hash's (however unlikely) chance of showing a stale unwrap.
struct UVEditorState
{
int slot = -1;
const void *image_data = nullptr;
float seam_angle = -1.f;
float padding = -2.f;
TriangleSelector::TriangleSplittingData facets;
// Manual/auto seam edges also change the unwrap, so a change here must force a re-solve just
// like the facets do (marking a seam leaves the paint mask untouched).
std::vector<std::pair<int, int>> seam_edges;
bool operator==(const UVEditorState &other) const
{
return slot == other.slot && image_data == other.image_data && seam_angle == other.seam_angle &&
padding == other.padding && facets == other.facets && seam_edges == other.seam_edges;
}
};
UVEditorState m_uv_editor_state;
// Bounds of the UVs last handed to the pane, purely so the panel can show where the unwrap
// actually landed -- it is packed in mm and then divided by the tile size, so it is easy for it
// to end up far outside the texture's first tile without any of that being visible.
Vec2f m_uv_editor_bbox_min = Vec2f::Zero();
Vec2f m_uv_editor_bbox_max = Vec2f::Zero();
// The unwrap m_uv_editor_state produced, kept so that changing tiling/rotation/offset only costs
// re-running apply_uv_transform() over it, not another extraction and solve.
PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
// open/close within a session, so the choice sticks while working.
bool m_undocked = false;
// 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;
// The tool's SVG (toolbar_texture_displacement.svg) uploaded once as a GL texture, so it can be
// used as an ImGui image button in the panel (currently the "add layer" affordance next to the
// Texture layers heading). Lazily loaded on first use, when a GL context is guaranteed current.
GLTexture m_tool_icon;
bool m_tool_icon_tried = false;
unsigned int tool_icon_id(); // 0 if the icon could not be loaded
};
} // namespace Slic3r::GUI

View File

@@ -217,9 +217,8 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam));
m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::FuzzySkin));
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
// TODO: placeholder icon reused from the fuzzy-skin gizmo; swap in a dedicated
// toolbar_texture_displacement(_dark).svg once one exists.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::TextureDisplacement));
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));

View File

@@ -2554,7 +2554,11 @@ void ImGuiWrapper::push_toolbar_style(const float scale)
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 1.00f)); // 10
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 0.00f)); // 11
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, COL_GREEN_LIGHT); // 12
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));//13
// The checkbox/radio frame behind this is drawn fully transparent (see FrameBg above,
// alpha 0), showing the light window background through it -- a white check mark there is
// invisible. Dark mode doesn't have this problem (its window background is dark), so only
// this branch needs a check mark color with real contrast against a light background.
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.f, 156 / 255.f, 136 / 255.f, 1.00f));//13
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.42f, 0.42f, 0.42f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));

View File

@@ -0,0 +1,29 @@
#include "TextureDisplacementPreviewJob.hpp"
#include "slic3r/GUI/I18N.hpp"
namespace Slic3r::GUI {
TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
std::function<void(indexed_triangle_set, uint64_t)> on_finished)
: m_input(std::move(input)), m_generation(generation), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementPreviewJob::process(Ctl &ctl)
{
ctl.update_status(0, _u8L("Computing texture displacement preview"));
// Only ever touches m_input (captured by value before this job was queued) and local state --
// never the live Model -- so this is safe to run concurrently with the UI thread.
m_result = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data);
}
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
{
if (canceled || eptr || !m_on_finished)
return;
m_on_finished(std::move(m_result), m_generation);
}
} // namespace Slic3r::GUI

View File

@@ -0,0 +1,52 @@
#ifndef slic3r_TextureDisplacementPreviewJob_hpp_
#define slic3r_TextureDisplacementPreviewJob_hpp_
#include <cstdint>
#include <functional>
#include <vector>
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "Job.hpp"
namespace Slic3r::GUI {
// Everything process() needs, captured by value on the main thread when the job is queued --
// mirrors TextureDisplacementBakeInput, but a preview never writes back to the Model.
struct TextureDisplacementPreviewInput
{
indexed_triangle_set base_mesh;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementFacetsData facets_data;
};
// Computes the true (unbaked) displaced-mesh preview in the background. With several painted
// layers this is real, non-trivial CPU work (PNG sampling, per-layer vertex welding), which used
// to run synchronously on every paint stroke and parameter tweak and made editing feel slow with
// more than one or two layers. Unlike Bake, this never touches the live Model -- a preview is
// purely informational, there is nothing to commit.
class TextureDisplacementPreviewJob : public Job
{
public:
// `generation` is an opaque token the caller controls (typically an incrementing counter):
// on_finished should only actually be applied by the caller if it still matches the caller's
// current generation when the job completes, so that a burst of edits queuing several of
// these jobs in a row can't have an earlier, now-stale result clobber a later one that
// finishes first.
TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
std::function<void(indexed_triangle_set, uint64_t)> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
private:
TextureDisplacementPreviewInput m_input;
uint64_t m_generation;
indexed_triangle_set m_result;
std::function<void(indexed_triangle_set, uint64_t)> m_on_finished;
};
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementPreviewJob_hpp_

View File

@@ -90,6 +90,7 @@
#include "Selection.hpp"
#include "GLToolbar.hpp"
#include "GUI_Preview.hpp"
#include "UVEditorCanvas.hpp"
#include "3DBed.hpp"
#include "PartPlate.hpp"
#include "Camera.hpp"
@@ -4468,6 +4469,13 @@ struct Plater::priv
GLToolbar collapse_toolbar;
Preview *preview;
AssembleView* assemble_view { nullptr };
// Docked/resizable 2D pane showing GLGizmoTextureDisplacement's LSCM unwrap of a painted
// patch; a sibling AUI pane alongside "sidebar"/"main", not part of the view3D/preview/
// assemble_view sizer -- see its registration below and Plater::get_uv_editor_canvas(). The
// pane hosts the panel (toolbar + canvas + status line); uv_editor_canvas is its inner canvas,
// cached so the gizmo can reach it directly.
UVEditorPanel* uv_editor_panel { nullptr };
UVEditorCanvas* uv_editor_canvas { nullptr };
bool first_enter_assemble{ true };
std::unique_ptr<NotificationManager> notification_manager;
@@ -5133,6 +5141,20 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
.BottomDockable(false)
.BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit())));
// UV editor pane for GLGizmoTextureDisplacement's LSCM unwrap preview -- a resizable/dockable
// sibling of "sidebar"/"main" like everything else registered on this same AUI manager, not a
// change to the view3D/preview/assemble_view sizer above. Hidden by default: only relevant
// while that gizmo is active with a layer using the "Unwrap (LSCM)" projection method (see
// Plater::show_uv_editor()), so it stays out of the way of everyone else's window layout.
uv_editor_panel = new UVEditorPanel(q);
uv_editor_canvas = uv_editor_panel->canvas();
m_aui_mgr.AddPane(uv_editor_panel, wxAuiPaneInfo()
.Name("uv_editor")
.Caption(_L("UV Editor"))
.Right()
.Hide()
.BestSize(wxSize(40 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit())));
auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL);
panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0);
panel_sizer->Add(preview, 1, wxEXPAND | wxALL, 0);
@@ -5162,6 +5184,13 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
BOOST_LOG_TRIVIAL(info) << "Removed floating AUI state from saved window layout for Wayland";
}
// The UV editor is a transient, gizmo-driven pane (see show_uv_editor()); a saved layout
// from a session that happened to close with it open would otherwise restore it visible on
// startup, with nothing painted in it. Force it hidden here so it only ever appears when the
// texture-displacement gizmo asks for it.
if (wxAuiPaneInfo &uv_pane = m_aui_mgr.GetPane("uv_editor"); uv_pane.IsOk())
uv_pane.Hide();
sidebar_layout.is_collapsed = !sidebar.IsShown();
}
@@ -17149,6 +17178,33 @@ GLCanvas3D* Plater::get_assmeble_canvas3D()
return nullptr;
}
UVEditorCanvas* Plater::get_uv_editor_canvas()
{
return p->uv_editor_canvas;
}
void Plater::show_uv_editor(bool show)
{
if (p->uv_editor_panel == nullptr)
return;
const wxAuiPaneInfo &pane = p->m_aui_mgr.GetPane(p->uv_editor_panel);
if (!pane.IsOk() || pane.IsShown() == show)
return;
// Deferred, because GLGizmoTextureDisplacement calls this from its ImGui panel -- that is, from
// the middle of the 3D canvas's GL frame. Showing an AUI pane re-lays out the window and
// delivers the resulting size/paint events synchronously, and the UV canvas painting itself
// makes its own surface current in the app's *shared* GL context, which mid-frame is the one
// the 3D canvas is drawing into. Doing the layout once the frame is over avoids that entirely.
CallAfter([this, show]() {
wxAuiPaneInfo &deferred_pane = p->m_aui_mgr.GetPane(p->uv_editor_panel);
if (!deferred_pane.IsOk() || deferred_pane.IsShown() == show)
return;
deferred_pane.Show(show);
p->m_aui_mgr.Update();
});
}
GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview)
{
return p->get_current_canvas3D(exclude_preview);

View File

@@ -611,6 +611,13 @@ public:
GLCanvas3D* get_assmeble_canvas3D();
wxWindow* get_select_machine_dialog();
// Docked UV-editor pane used by GLGizmoTextureDisplacement's LSCM projection preview (see
// UVEditorCanvas.hpp). Returns nullptr only before the main window is fully constructed.
class UVEditorCanvas* get_uv_editor_canvas();
// Shows or hides the UV-editor AUI pane, updating its docked layout accordingly. Safe to call
// repeatedly (e.g. every time the gizmo's active layer/projection method changes).
void show_uv_editor(bool show);
void arrange();
void orient();
void find_new_position(const ModelInstancePtrs &instances);

View File

@@ -0,0 +1,208 @@
#include "TextureLibrary.hpp"
#include <algorithm>
#include <fstream>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/system/error_code.hpp>
#include <wx/image.h>
#include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/Utils.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/I18N.hpp"
namespace Slic3r::GUI {
namespace {
// Extensions the picker will list. The shipped folder only ever contains .png; the rest are here
// so a user who drops a .jpg straight into their own folder still sees it (load_texture_image_data()
// converts anything it can open).
bool is_image_file(const boost::filesystem::path &path)
{
std::string ext = path.extension().string();
boost::algorithm::to_lower(ext);
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp";
}
void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<TextureLibraryEntry> &out)
{
boost::system::error_code ec;
if (!boost::filesystem::is_directory(dir, ec))
return;
const size_t first = out.size();
for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) {
if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path()))
continue;
out.push_back({ it->path().stem().string(), it->path().string(), is_user });
}
std::sort(out.begin() + first, out.end(),
[](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; });
}
// Slic3r::png only writes PNGs to a file, so the encode round-trips through a temp file rather than
// staying in memory. It happens once per import / per texture pick, not per frame, so the I/O is
// not worth avoiding with a second PNG encoder.
bool encode_gray_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std::string &error)
{
const wxImage gray = image.ConvertToGreyscale();
const int w = gray.GetWidth();
const int h = gray.GetHeight();
if (w <= 0 || h <= 0) {
error = _u8L("The selected image is empty.");
return false;
}
// wxImage always stores 3 bytes per pixel; after ConvertToGreyscale the three are equal.
std::vector<uint8_t> pixels(size_t(w) * size_t(h));
const unsigned char *rgb = gray.GetData();
for (size_t i = 0; i < pixels.size(); ++i)
pixels[i] = rgb[i * 3];
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("orca_texdisp_%%%%%%%%.png");
if (!Slic3r::png::write_gray_to_file(tmp.string(), size_t(w), size_t(h), pixels)) {
error = _u8L("Failed to prepare the texture for use.");
return false;
}
{
std::ifstream ifs(tmp.string(), std::ios::binary);
out.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp, ec);
if (out.empty()) {
error = _u8L("Failed to prepare the texture for use.");
return false;
}
return true;
}
std::vector<unsigned char> read_file_bytes(const std::string &path)
{
std::ifstream ifs(path, std::ios::binary);
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
// True if these bytes are already the 8-bit grayscale PNG libslic3r can decode, i.e. can be stored
// on a layer as-is. Mirrors exactly what decode_height_texture() accepts.
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
{
if (bytes.empty())
return false;
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
if (!png::is_png(rbuf))
return false;
png::ImageGreyscale img;
return png::decode_png(rbuf, img) && img.cols > 0 && img.rows > 0;
}
std::vector<TextureLibraryEntry> g_library;
bool g_library_scanned = false;
} // namespace
std::string user_texture_dir()
{
const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement";
boost::system::error_code ec;
boost::filesystem::create_directories(dir, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "Could not create the user texture directory " << dir.string() << ": " << ec.message();
return {};
}
return dir.string();
}
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
{
if (g_library_scanned && !force_rescan)
return g_library;
g_library.clear();
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library);
const std::string user_dir = user_texture_dir();
if (!user_dir.empty())
scan_dir(boost::filesystem::path(user_dir), true, g_library);
g_library_scanned = true;
return g_library;
}
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error)
{
const std::string user_dir = user_texture_dir();
if (user_dir.empty()) {
error = _u8L("Could not create the folder for imported textures.");
return std::nullopt;
}
wxImage image;
if (!image.LoadFile(from_u8(source_path)) || !image.IsOk()) {
error = _u8L("Could not load the selected image.");
return std::nullopt;
}
std::vector<unsigned char> bytes;
if (!encode_gray_png_bytes(image, bytes, error))
return std::nullopt;
// Never overwrite an existing texture (the user's or, if they picked the same name twice, their
// own earlier import) -- uniquify instead.
const std::string stem = boost::filesystem::path(source_path).stem().string();
boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png");
for (int i = 2; boost::filesystem::exists(dest); ++i)
dest = boost::filesystem::path(user_dir) / (stem + " (" + std::to_string(i) + ").png");
{
boost::nowide::ofstream ofs(dest.string(), std::ios::binary);
ofs.write(reinterpret_cast<const char *>(bytes.data()), std::streamsize(bytes.size()));
if (!ofs.good()) {
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
}
}
texture_library(true); // pick the new file up
const std::string dest_str = dest.string();
for (const TextureLibraryEntry &e : g_library)
if (e.path == dest_str)
return e;
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
}
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error)
{
std::vector<unsigned char> bytes = read_file_bytes(path);
if (bytes.empty()) {
error = _u8L("Could not read the texture file.");
return nullptr;
}
if (is_supported_height_map(bytes))
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
// Not an 8-bit grayscale PNG (a colour image somebody copied into the folder by hand, say):
// convert it the same way an import would, but leave the file on disk alone.
wxImage image;
if (!image.LoadFile(from_u8(path)) || !image.IsOk()) {
error = _u8L("Could not load the selected image.");
return nullptr;
}
std::vector<unsigned char> converted;
if (!encode_gray_png_bytes(image, converted, error))
return nullptr;
return std::make_shared<std::vector<unsigned char>>(std::move(converted));
}
} // namespace Slic3r::GUI

View File

@@ -0,0 +1,50 @@
#ifndef slic3r_TextureLibrary_hpp_
#define slic3r_TextureLibrary_hpp_
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace Slic3r::GUI {
// One selectable height-map texture in the texture-displacement gizmo's texture picker.
struct TextureLibraryEntry
{
std::string name; // display name (the file's stem, e.g. "Wood Grain")
std::string path; // absolute path on disk
bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer
};
// Every available height-map texture: the ones shipped in resources/textures/displacement first,
// then the user's own from <data_dir>/textures/displacement, each group sorted by name.
//
// The two live in separate directories deliberately: an app update replaces the resources tree
// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or
// delete. `is_user` is what the picker uses to show them under separate headings.
//
// Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped
// into either folder by hand while the app was running.
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
// <data_dir>/textures/displacement, created if it does not exist yet. Empty string on failure.
std::string user_texture_dir();
// Reads any image format wxWidgets can open, converts it to the 8-bit grayscale PNG that
// libslic3r's decode_height_texture() understands, and saves it into user_texture_dir() (uniquified
// if that name is taken). The conversion has to happen here rather than in libslic3r, which has no
// image toolkit and so only ever handles the one already-normalized format.
//
// Returns the newly imported entry, or nullopt with `error` set. `source_path` is only read.
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error);
// Encoded bytes of `path`, ready to hand to TextureDisplacementLayer::image_data. Files already in
// the supported 8-bit grayscale PNG form (everything in the two library folders, by construction)
// are passed through verbatim; anything else -- e.g. a colour PNG the user copied into the folder
// by hand -- is converted on the fly, so a valid image never silently produces a blank layer.
// Returns nullptr with `error` set if the file cannot be read or decoded at all.
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error);
} // namespace Slic3r::GUI
#endif // slic3r_TextureLibrary_hpp_

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,316 @@
#ifndef slic3r_UVEditorCanvas_hpp_
#define slic3r_UVEditorCanvas_hpp_
#include <algorithm>
#include <functional>
#include <utility>
#include <vector>
// Must come before wx/glcanvas.h in any translation unit that includes this header: glcanvas.h
// pulls in the platform's real GL/gl.h, and glad/gl.h errors out if that happens first (it wants
// to be the one to define the standard include guards GL/gl.h itself defines).
#include <glad/gl.h>
#include <wx/glcanvas.h>
#include <wx/panel.h>
#include <wx/button.h>
#include <wx/tglbtn.h>
#include <wx/stattext.h>
#include <wx/sizer.h>
#include "libslic3r/Point.hpp"
#include "GLModel.hpp"
#include "GLTexture.hpp"
namespace Slic3r::GUI {
// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch -- shows the result of
// GLGizmoTextureDisplacement's LSCM projection method as its own resizable pane (see Plater's
// "uv_editor" AUI pane) rather than folding 2D UV-space rendering into the main 3D viewport.
//
// Islands can be laid out by hand, roughly the way Blender's UV editor works: click one to select
// it, drag to move it, right-drag or press R to rotate it, S to scale it, both about its own centre.
// Islands are free to overlap -- nothing re-packs them behind the user's back. The texture underneath
// is always drawn upright and axis-aligned, and it is the islands that move over it, which is what
// makes "rotate this island" a meaningful gesture rather than just spinning the whole texture.
//
// **Geometry is uploaded in the unwrap's own (raw, mm) coordinates, once**, and each island is drawn
// through its own affine matrix passed as a shader uniform. That matters: a patch can easily run to
// a million triangles, and the earlier design -- which pre-transformed every UV on the CPU and
// re-uploaded the whole wireframe on every mouse-move event -- made a drag cost a couple of hundred
// milliseconds per frame. Moving an island now touches a 2x3 matrix and nothing else.
//
// Uses the app's single shared wxGLContext (via wxGetApp().init_glcontext(), the same call
// View3D/Preview/AssembleView each make in GUI_Preview.cpp) rather than an independent context of
// its own, specifically so it can reuse the app's already-registered "flat"/"flat_texture"
// shaders and GLModel as-is -- GLModel::render() looks up its shader via a GUI_App-wide "current
// shader", which only means anything for canvases sharing the app's one real GL context.
class UVEditorCanvas : public wxGLCanvas
{
public:
explicit UVEditorCanvas(wxWindow *parent);
// Maps one island's raw unwrap coordinate to a texture UV: the island's own hand placement and
// then the layer's tiling/rotation/offset, composed into a single affine (columns: x basis,
// y basis, translation).
using IslandTransform = Eigen::Matrix<float, 2, 3>;
// The unwrap to display, in the unwrap's own mm coordinates -- *not* texture UVs. Changing this
// is the expensive path (it rebuilds every vertex buffer), so it must only be called when the
// unwrap itself changes, never merely because an island moved. Pass an empty `indices` to show
// nothing.
struct Islands
{
std::vector<Vec2f> uvs;
std::vector<Vec3i32> indices;
std::vector<int> vertex_island; // per uv
std::vector<std::pair<int, int>> boundary_edges; // island outlines, indices into uvs
int island_count = 0;
};
void set_islands(Islands islands);
// The cheap path: one transform per island. Safe to call on every mouse-move of a drag.
void set_island_transforms(std::vector<IslandTransform> transforms);
// One fill colour per island, overriding the default light-green wash -- used to paint the UV
// distortion heatmap over the islands when the gizmo's "Distortion" check mode is on (#7/#14).
// Pass empty to go back to the default wash. Cheap: it never touches a vertex buffer.
void set_island_fill_colors(std::vector<ColorRGBA> colors);
// The layer's own tiling scale and rotation. Needed to map a gesture, which happens in texture-UV
// space, back into the unwrap's mm space -- which is where a TextureIsland's offset actually
// lives (see apply_uv_transform()). The tile settings come along because the background has to
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
// texture it will actually sample. Pass width/height <= 0 to clear it.
void set_background_texture(const std::vector<unsigned char> &grayscale_pixels, int width, int height);
// Snap a dragged island's boundary to a neighbouring island's when they come close (#2). Off is
// the honest default for overlap-friendly layouts; the pane toolbar toggles it.
void set_snap_enabled(bool enabled) { m_snap_enabled = enabled; }
bool snap_enabled() const { return m_snap_enabled; }
// High-level actions the pane toolbar triggers. The canvas handles the view-only ones (framing,
// the snap toggle) itself and forwards the rest to whoever owns the island data (the gizmo), via
// the command callback -- the canvas has the selection and the view, the gizmo has the layer.
enum class Command { FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected };
void run_command(Command cmd);
using CommandFn = std::function<void(Command)>;
void set_command_callback(CommandFn fn) { m_on_command = std::move(fn); }
// Called whenever the one-line status/hint text changes (current gesture + the shortcuts that
// apply right now), so the pane can show it Blender-style along the bottom.
using StatusFn = std::function<void(const wxString &)>;
void set_status_callback(StatusFn fn) { m_on_status = std::move(fn); }
// Reports an island edit as it happens. The deltas are *incremental* (one mouse event's worth)
// and already converted into the units a TextureIsland stores -- unwrap mm, degrees, and a scale
// *factor* to multiply the island's existing scale by. They are incremental on purpose: the owner
// applies them and hands back fresh transforms, and if the gesture tracked geometry rather than
// raw mouse motion that round trip would feed back into itself. `finished` marks the end of a
// gesture, so the owner can rebuild the 3D preview once rather than on every motion event.
using IslandEditFn =
std::function<void(int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished)>;
void set_island_edit_callback(IslandEditFn fn) { m_on_island_edit = std::move(fn); }
// What a click grabs: a whole island (move/rotate/scale, groups move together), a single vertex, or
// a single edge (both its endpoints). Vertex/Edge are free-form UV editing -- they move the actual
// unwrap coordinates, which the owner then folds into the layer's per-vertex UV overrides so the
// change is baked, not just shown (see set_vertex_edit_callback).
enum class SelectMode { Island, Vertex, Edge };
void set_select_mode(SelectMode mode);
SelectMode select_mode() const { return m_select_mode; }
// Reports a committed vertex/edge edit: the list of (unwrapped-vertex index, its new raw-unwrap
// coordinate in mm). Fired once, on mouse release, since it re-solves the displacement preview; the
// pane shows the edit live from its own geometry in the meantime. The owner maps the unwrapped index
// to a mesh vertex (via the unwrap's source_vertex) and stores the override.
using UVVertexEditFn = std::function<void(const std::vector<std::pair<int, Vec2f>> &edits)>;
void set_vertex_edit_callback(UVVertexEditFn fn) { m_on_vertex_edit = std::move(fn); }
// The primary (last-clicked) island, still the pivot for rotate/scale and the target of the
// single-island toolbar commands (Cut/Join/Unjoin). -1 if nothing is selected.
int selected_island() const { return m_selected_island; }
// The full multi-selection (Shift adds, Ctrl toggles). Always contains m_selected_island when it is
// >= 0. The gizmo reads this to decide which islands a drag moves together, unioned with each
// selected island's join group.
const std::vector<int> &selected_islands() const { return m_selection; }
void reset_view();
private:
void on_paint(wxPaintEvent &evt);
void on_size(wxSizeEvent &evt);
void on_mouse(wxMouseEvent &evt);
void on_key(wxKeyEvent &evt);
void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op
void render();
void rebuild_island_models();
void rebuild_background_texture();
void rebuild_background_quad();
void rebuild_grid();
// The UV region worth looking at: every island, plus always at least the texture's first tile, so
// there is something sensibly framed even before anything is painted.
void content_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
// Frames content_bounds(). Bound to Home, and run once each time an unwrap first appears.
void fit_view_to_content();
// Half-extents of the visible UV region. Split out because both rendering and every mouse
// gesture need them, and they have to agree exactly or picking lands in the wrong place.
void view_half_extents(float &half_w, float &half_h) const;
Vec2f screen_to_uv(const wxPoint &px) const;
// Raw unwrap coordinate -> texture UV, through the island's own transform.
Vec2f island_uv(size_t vertex) const;
// The island under `uv`, or -1. Prefers the current selection when islands overlap, so that
// dragging one that sits under another doesn't hand the drag to its neighbour halfway through.
int island_at(const Vec2f &uv) const;
// Nearest unwrapped vertex to `uv` within a screen-space threshold, or -1 (Vertex mode picking).
int vertex_at(const Vec2f &uv) const;
// Nearest island-boundary edge to `uv` within a screen-space threshold, as its two unwrapped-vertex
// indices, or {-1,-1} (Edge mode picking).
std::pair<int, int> edge_at(const Vec2f &uv) const;
// Moves one unwrapped vertex by a texture-UV delta, converting it back into the vertex's own raw
// unwrap space through its island's inverse transform, and marks the mesh dirty so it redraws.
void move_vertex_raw(int unwrapped_vertex, const Vec2f &delta_uv);
Vec2f island_centroid(int island) const;
// Converts a delta in texture-UV space into the unwrap's mm space, undoing the layer's scale and
// rotation -- the inverse of what apply_uv_transform() did on the way in.
Vec2f uv_delta_to_unwrap(const Vec2f &delta_uv) const;
// The correction that would bring the selected island's nearest boundary vertex onto a boundary
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
Vec2f snap_correction(int island) const;
void end_gesture();
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
void update_status();
wxGLContext *m_context = nullptr; // owned by OpenGLManager/GUI_App, not by this canvas
Islands m_islands;
std::vector<IslandTransform> m_transforms;
// Per-island fill colour override (distortion heatmap); empty means use the default wash (#7).
std::vector<ColorRGBA> m_island_fill_colors;
// Boundary vertices per island, for snapping -- a patch's boundary is a tiny fraction of it, and
// rescanning the whole uv array on every snap test would not be.
std::vector<std::vector<int>> m_island_boundary_verts;
bool m_mesh_dirty = true;
// One set of models per island, so an island can be drawn through its own transform. Built once
// per unwrap, never on a drag.
std::vector<GLModel> m_island_wireframe; // interior edges
std::vector<GLModel> m_island_boundary; // outline
std::vector<GLModel> m_island_fill; // filled, for the selected island's wash
GLModel m_tile_outline_glmodel; // the texture's first tile, [0,1]^2 -- the "you are here"
GLModel m_grid_glmodel;
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
float m_tiling_scale = 1.f;
float m_rotation_deg = 0.f;
bool m_tile_enabled = true;
bool m_tile_mirrored = false;
bool m_snap_enabled = false;
std::vector<unsigned char> m_background_pixels; // RGBA, expanded from the grayscale input
int m_background_width = 0, m_background_height = 0;
bool m_background_dirty = false; // the pixels need (re)uploading
bool m_background_quad_dirty = true; // only the quad's extent changed
GLTexture m_background_texture;
// Covers content_bounds(), not just [0,1]: the unwrap is packed in mm and then divided by the
// layer's tile size, so it routinely spans many tiles, and a single-unit-square backdrop would
// leave most of the islands sitting over bare background. Texcoord == position, so the GL wrap
// mode repeats it exactly the way DecodedHeightTexture::sample() does.
GLModel m_background_glmodel;
// 2D pan/zoom. m_pan is the UV-space point at the center of the view, m_zoom half the UV-space
// extent visible across the shorter screen edge. v runs *down* the screen, matching both the
// texture's own row order and every other UV editor's convention.
Vec2f m_pan = Vec2f(0.5f, 0.5f);
float m_zoom = 0.75f;
bool m_needs_fit = true; // fit the view to the next unwrap that arrives
enum class Gesture
{
None,
Pan,
MoveIsland,
RotateIsland, // right-drag: rotation tracks the mouse, ends when the button is released
RotateIslandModal, // 'R': rotation tracks the mouse until a click confirms or Esc cancels
ScaleIslandModal, // 'S': likewise, distance from the centre drives the scale
MoveVertex, // Vertex mode: drag one unwrapped vertex
MoveEdge, // Edge mode: drag both endpoints of one boundary edge
};
Gesture m_gesture = Gesture::None;
SelectMode m_select_mode = SelectMode::Island;
// The sub-element being edited in Vertex/Edge mode (unwrapped-vertex indices), or -1/{-1,-1}.
int m_active_vertex = -1;
std::pair<int, int> m_active_edge{ -1, -1 };
// Set once a Vertex/Edge drag actually moves, so a bare click (select without drag) doesn't commit a
// no-op edit and take an undo snapshot for nothing.
bool m_vertex_edit_moved = false;
// Lazily-built small filled square, drawn at an edited/hovered vertex as a handle.
GLModel m_vertex_marker_glmodel;
int m_selected_island = -1;
// The full multi-selection; m_selected_island is its primary (last-clicked) member. Kept as a small
// vector rather than a set because it is tiny and iteration order (primary last) is convenient.
std::vector<int> m_selection;
bool is_selected(int island) const
{
return std::find(m_selection.begin(), m_selection.end(), island) != m_selection.end();
}
wxPoint m_drag_last_px;
Vec2f m_gesture_last_uv = Vec2f::Zero();
float m_gesture_last_angle = 0.f;
float m_gesture_last_dist = 0.f;
// Rotation is tracked as two running totals over the gesture: the raw mouse rotation, and how much
// has actually been applied. With Shift held the applied total is quantised to 15-degree steps
// (Blender-style angle snapping), so the two diverge -- and driving the applied total off the raw
// one, rather than snapping each incremental delta, is what makes the snap stable instead of
// juddering. The raw/applied split also survives crossing +/-180 degrees, which a single wrapped
// angle would not. m_rot_applied doubles as the modal-rotate undo amount for Esc.
float m_rot_raw_deg = 0.f;
float m_rot_applied_deg = 0.f;
// The island's absolute on-screen rotation when the gesture began (decoded from its transform), so
// Shift can snap to *global* 15-degree marks (0/15/30...) rather than 15 degrees relative to
// wherever the island happened to start (#10). Also drives the angle read-out and the dial.
float m_rot_base_deg = 0.f;
float m_rot_display_deg = 0.f; // current absolute angle, for the status line and dial needle
float m_modal_scale_accum = 1.f; // so Esc can undo exactly what the modal scale applied, as a factor
// A protractor drawn around the island while it rotates: a ring, a tick every 15 degrees, and a
// needle at the current angle, so the rotation is legible (#11). Rebuilt each frame during a
// rotation gesture (cheap: a few hundred short lines) and left empty otherwise.
GLModel m_dial_glmodel;
void rebuild_rotation_dial();
// The island's current absolute rotation in degrees, decoded from its transform's first column.
float island_rotation_deg(int island) const;
IslandEditFn m_on_island_edit;
UVVertexEditFn m_on_vertex_edit;
CommandFn m_on_command;
StatusFn m_on_status;
};
// Hosts a UVEditorCanvas together with a small icon toolbar (frame, snap, average scale, cut,
// project-from-view) and a Blender-style status line along the bottom that names the current gesture
// and the shortcuts in play (#18). This is what actually goes into Plater's "uv_editor" AUI pane;
// the gizmo still talks to the inner canvas, reached via canvas().
class UVEditorPanel : public wxPanel
{
public:
explicit UVEditorPanel(wxWindow *parent);
UVEditorCanvas *canvas() { return m_canvas; }
private:
void on_tool(wxCommandEvent &evt);
UVEditorCanvas *m_canvas = nullptr;
wxToggleButton *m_snap_button = nullptr;
wxStaticText *m_status = nullptr;
};
} // namespace Slic3r::GUI
#endif // slic3r_UVEditorCanvas_hpp_