Files
OrcaSlicer/src/slic3r/GUI/UVEditorCanvas.hpp
T
ExPikaPakaandExPikaPaka bff19b07d2 Texture displacement: fix the Checker and Distortion views, add UV editor tooltips (#16147)
* Explain the texture displacement views that cannot be used, and show them

Checker and Distortion are views of a layer's unwrap, so they mean nothing on
any other mapping - but they were offered on all of them and simply drew
nothing when picked. Fade them out on anything but Unwrap (LSCM), with the
reason in the tooltip, and have a click bring the UV editor up on the view it
selected, since that pane is where the unwrap is actually worked on.

Neither view was visible even when it did apply. The overlay is built from the
base patch and drawn with a polygon offset, which biases depth values rather
than moving geometry, so it cannot win against the displaced preview standing
in front of it. Leave that preview out while a UV-check view is on and draw the
undisplaced surface instead, which is what the offset assumes and what the
mapping being inspected belongs to.

In the UV editor, a tool that cannot be used right now is faded rather than
disabled. A disabled window gets no mouse events on GTK or MSW, so every one of
those tools - Cut, Join, Unjoin, Clear seams, Clear UV edits, the select modes,
Snap, Frame - silently had no tooltip in the state where the user most needs to
know what is missing. Each now says what to do instead. The tile size, the
island statistics, the status line and the three select modes gained tooltips
of their own; the select modes now name the gestures they enable, which were
documented nowhere.

* UV editor: keep the mouse capture balanced

The canvas captured the mouse on every button press without checking whether
it already held one, released it in a single place, and handled no capture
loss at all. Two sequences leaked a capture: pressing a second button during a
drag nested a second one that the single release on button-up could not undo,
and a modal R/S skipped that release entirely while waiting for a confirming
click that may never come.

A leaked capture is not a local problem on macOS, where wxEVT_MOUSE_CAPTURE_LOST
is never sent and nothing recovers it. While any wx window holds a capture,
wxOSX routes every mouse event to that window and never calls through to
NSWindow, so the application stops seeing motion and enter/leave, and native
tooltips stop appearing anywhere in it.

Capture through grab_mouse()/drop_mouse() so there is at most one, give it back
on any button-up including a modal gesture (which tracks the pointer and needs
no capture), and cancel on wxEVT_MOUSE_CAPTURE_LOST: commit nothing, put back
what a modal rotate or scale already applied, and do not release a capture that
is already gone.

---------

Co-authored-by: ExPikaPaka <mrfsfyt@gmail.com>
2026-10-09 12:14:47 +08:00

459 lines
26 KiB
C++

#ifndef slic3r_UVEditorCanvas_hpp_
#define slic3r_UVEditorCanvas_hpp_
#include <Eigen/Core>
#include <algorithm>
#include <cstddef>
#include <functional>
#include "libslic3r/Color.hpp"
#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/event.h>
#include <wx/gdicmn.h>
#include <wx/glcanvas.h>
#include <wx/panel.h>
#include <wx/button.h>
#include <wx/string.h>
#include <wx/tglbtn.h>
#include <wx/stattext.h>
#include <wx/statbmp.h>
#include <wx/sizer.h>
#include "libslic3r/Point.hpp"
#include "GLModel.hpp"
#include "GLTexture.hpp"
// Orca's own widgets (slic3r/GUI/Widgets), declared at global scope.
class SpinInput;
class CheckBox;
namespace Slic3r::GUI {
// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch - shows the result of
// GLGizmoTextureDisplacement's LSCM projection method as its own resizable pane (see Plater's
// "uv_editor" AUI pane) rather than folding 2D UV-space rendering into the main 3D viewport.
//
// Islands can be laid out by hand, roughly the way Blender's UV editor works: click one to select
// it, drag to move it, right-drag or press R to rotate it, S to scale it, both about its own centre.
// Islands are free to overlap - nothing re-packs them behind the user's back. The texture underneath
// is always drawn upright and axis-aligned, and it is the islands that move over it, which is what
// makes "rotate this island" a meaningful gesture rather than just spinning the whole texture.
//
// **Geometry is uploaded in the unwrap's own (raw, mm) coordinates, once**, and each island is drawn
// through its own affine matrix passed as a shader uniform. That matters: a patch can easily run to
// a million triangles, and the earlier design - which pre-transformed every UV on the CPU and
// re-uploaded the whole wireframe on every mouse-move event - made a drag cost a couple of hundred
// milliseconds per frame. Moving an island now touches a 2x3 matrix and nothing else.
//
// Uses the app's single shared wxGLContext (via wxGetApp().init_glcontext(), the same call
// View3D/Preview/AssembleView each make in GUI_Preview.cpp) rather than an independent context of
// its own, specifically so it can reuse the app's already-registered "flat"/"flat_texture"
// shaders and GLModel as-is - GLModel::render() looks up its shader via a GUI_App-wide "current
// shader", which only means anything for canvases sharing the app's one real GL context.
class UVEditorCanvas : public wxGLCanvas
{
public:
explicit UVEditorCanvas(wxWindow *parent);
// Maps one island's raw unwrap coordinate to a texture UV: the island's own hand placement and
// then the layer's tiling/rotation/offset, composed into a single affine (columns: x basis,
// y basis, translation).
using IslandTransform = Eigen::Matrix<float, 2, 3>;
// The unwrap to display, in the unwrap's own mm coordinates - *not* texture UVs. Changing this
// is the expensive path (it rebuilds every vertex buffer), so it must only be called when the
// unwrap itself changes, never merely because an island moved. Pass an empty `indices` to show
// nothing.
struct Islands
{
std::vector<Vec2f> uvs;
std::vector<Vec3i32> indices;
std::vector<int> vertex_island; // per uv
std::vector<std::pair<int, int>> boundary_edges; // island outlines, indices into uvs
int island_count = 0;
};
void set_islands(Islands islands);
// The cheap path: one transform per island. Safe to call on every mouse-move of a drag.
void set_island_transforms(std::vector<IslandTransform> transforms);
// One fill colour per island, overriding the default light-green wash - used to paint the UV
// distortion heatmap over the islands when the gizmo's "Distortion" check mode is on (#7/#14).
// Pass empty to go back to the default wash. Cheap: it never touches a vertex buffer.
void set_island_fill_colors(std::vector<ColorRGBA> colors);
// The layer's own tiling scale and rotation. Needed to map a gesture, which happens in texture-UV
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
// lives (see apply_uv_transform()). The tile settings come along because the background has to
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
// texture it will actually sample. Pass width/height <= 0 to clear it.
void set_background_texture(const std::vector<unsigned char> &grayscale_pixels, int width, int height);
// Snap a dragged island's boundary to a neighbouring island's when they come close (#2). Off is
// the honest default for overlap-friendly layouts; the pane toolbar toggles it.
void set_snap_enabled(bool enabled) { m_snap_enabled = enabled; }
bool snap_enabled() const { return m_snap_enabled; }
// High-level actions the pane's controls trigger. The canvas handles the view-only ones (framing,
// the snap toggle) itself and forwards the rest to whoever owns the island data (the gizmo), via
// the command callback - the canvas has the selection and the view, the gizmo has the layer.
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
// SelectMode / Background) and is 0 otherwise.
enum class Command {
FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected,
Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits,
SetBackground, PickTexture, PaneClosed
};
void run_command(Command cmd, float value = 0.f);
using CommandFn = std::function<void(Command, float)>;
void set_command_callback(CommandFn fn) { m_on_command = std::move(fn); }
// What the canvas shows under the islands; mirrors the gizmo's Normal/Checker/Distortion views.
enum class Background { Height, Checker, Distortion };
// Everything the pane's own controls show that the gizmo owns: the active layer and its unwrap settings.
// The gizmo pushes it whenever any of it may have changed, and the pane redraws its header, settings row
// and tool strip from it - so the controls never hold state of their own that could drift from the layer.
struct PaneState
{
bool has_layer = false; // an active layer mapped with Unwrap
wxString layer_name;
float tile_mm = 0.f;
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
int thumbnail_px = 0;
float seam_angle_deg = 40.f;
bool connect_islands = true;
bool unwrapped = false; // an unwrap exists for this layer
bool unwrap_stale = false; // paint, seams or the seam angle changed since it was made
bool mark_seams = false;
bool seam_path = false;
bool has_seams = false;
bool has_uv_edits = false;
Background background = Background::Height;
int island_count = 0;
size_t face_count = 0;
};
void set_pane_state(PaneState state);
const PaneState &pane_state() const { return m_pane_state; }
using PaneStateFn = std::function<void(const PaneState &)>;
void set_pane_state_callback(PaneStateFn fn) { m_on_pane_state = std::move(fn); }
// Called whenever the one-line status/hint text changes (current gesture + the shortcuts that
// apply right now), so the pane can show it Blender-style along the bottom.
using StatusFn = std::function<void(const wxString &)>;
void set_status_callback(StatusFn fn) { m_on_status = std::move(fn); }
// Reports an island edit as it happens. The deltas are *incremental* (one mouse event's worth)
// and already converted into the units a TextureIsland stores - unwrap mm, degrees, and a scale
// *factor* to multiply the island's existing scale by. They are incremental on purpose: the owner
// applies them and hands back fresh transforms, and if the gesture tracked geometry rather than
// raw mouse motion that round trip would feed back into itself. `finished` marks the end of a
// gesture, so the owner can rebuild the 3D preview once rather than on every motion event.
using IslandEditFn =
std::function<void(int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished)>;
void set_island_edit_callback(IslandEditFn fn) { m_on_island_edit = std::move(fn); }
// What a click grabs: a whole island (move/rotate/scale, groups move together), a single vertex, or
// a single edge (both its endpoints). Vertex/Edge are free-form UV editing - they move the actual
// unwrap coordinates, which the owner then folds into the layer's per-vertex UV overrides so the
// change is baked, not just shown (see set_vertex_edit_callback).
enum class SelectMode { Island, Vertex, Edge };
void set_select_mode(SelectMode mode);
SelectMode select_mode() const { return m_select_mode; }
// Reports a committed vertex/edge edit: the list of (unwrapped-vertex index, its new raw-unwrap
// coordinate in mm). Fired once, on mouse release, since it re-solves the displacement preview; the
// pane shows the edit live from its own geometry in the meantime. The owner maps the unwrapped index
// to a mesh vertex (via the unwrap's source_vertex) and stores the override.
using UVVertexEditFn = std::function<void(const std::vector<std::pair<int, Vec2f>> &edits)>;
void set_vertex_edit_callback(UVVertexEditFn fn) { m_on_vertex_edit = std::move(fn); }
// The primary (last-clicked) island, still the pivot for rotate/scale and the target of the
// single-island toolbar commands (Cut/Join/Unjoin). -1 if nothing is selected.
int selected_island() const { return m_selected_island; }
// Whether there is an unwrap on screen at all.
bool has_islands() const { return m_islands.island_count > 0 && !m_islands.indices.empty(); }
// The full multi-selection (Shift adds, Ctrl toggles). Always contains m_selected_island when it is
// >= 0. The gizmo reads this to decide which islands a drag moves together, unioned with each
// selected island's join group.
const std::vector<int> &selected_islands() const { return m_selection; }
void reset_view();
// Rebuilds every GPU object at the next paint, from the data the canvas keeps on the CPU. Called when the
// pane is shown again, so a hidden-and-reshown canvas never depends on GL objects surviving its native
// window being torn down.
void invalidate_gl()
{
m_gl_reset_pending = true;
Refresh();
}
private:
void on_paint(wxPaintEvent &evt);
void on_size(wxSizeEvent &evt);
void on_mouse(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
void on_key(wxKeyEvent &evt);
void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas
void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op
void render();
void rebuild_island_models();
void rebuild_background_texture();
void rebuild_background_quad();
void rebuild_grid();
// The UV region worth looking at: every island, plus always at least the texture's first tile, so
// there is something sensibly framed even before anything is painted.
void content_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
// What is actually *drawn*, which is content_bounds() snapped out to whole tiles whenever the
// backdrop tiles (see rebuild_background_quad()). Both the framing and the backdrop go through
// this so they cannot disagree.
void framed_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
// Frames framed_bounds(). Bound to Home, and run once each time an unwrap first appears.
void fit_view_to_content();
// Half-extents of the visible UV region. Split out because both rendering and every mouse
// gesture need them, and they have to agree exactly or picking lands in the wrong place.
void view_half_extents(float &half_w, float &half_h) const;
Vec2f screen_to_uv(const wxPoint &px) const;
// Raw unwrap coordinate -> texture UV, through the island's own transform.
Vec2f island_uv(size_t vertex) const;
// The island under `uv`, or -1. Prefers the current selection when islands overlap, so that
// dragging one that sits under another doesn't hand the drag to its neighbour halfway through.
int island_at(const Vec2f &uv) const;
// Nearest unwrapped vertex to `uv` within a screen-space threshold, or -1 (Vertex mode picking).
int vertex_at(const Vec2f &uv) const;
// Nearest island-boundary edge to `uv` within a screen-space threshold, as its two unwrapped-vertex
// indices, or {-1,-1} (Edge mode picking).
std::pair<int, int> edge_at(const Vec2f &uv) const;
// Moves one unwrapped vertex by a texture-UV delta, converting it back into the vertex's own raw
// unwrap space through its island's inverse transform, and marks the mesh dirty so it redraws.
void move_vertex_raw(int unwrapped_vertex, const Vec2f &delta_uv);
Vec2f island_centroid(int island) const;
// Converts a delta in texture-UV space into the unwrap's mm space, undoing the layer's scale and
// rotation - the inverse of what apply_uv_transform() did on the way in.
Vec2f uv_delta_to_unwrap(const Vec2f &delta_uv) const;
// The correction that would bring the selected island's nearest boundary vertex onto a boundary
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
Vec2f snap_correction(int island) const;
void end_gesture();
// Cancels the gesture instead of finishing it: nothing is committed, and a modal rotate/scale is
// put back the way Esc puts it back. Used when the capture is taken away from us.
void cancel_gesture();
// One capture at a time, released exactly once. Pressing a second button mid-drag would otherwise
// nest a second capture that the single release on button-up cannot undo, and macOS never sends
// wxEVT_MOUSE_CAPTURE_LOST to recover from that. A leaked capture is not a local problem there:
// while any wx window holds one, wxOSX routes every mouse event to it and the application stops
// seeing enter/leave and motion entirely, which also takes its tooltips down.
void grab_mouse();
void drop_mouse();
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
void update_status();
// Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed.
void update_hover(const wxPoint &pos);
wxGLContext *m_context = nullptr; // owned by OpenGLManager/GUI_App, not by this canvas
Islands m_islands;
std::vector<IslandTransform> m_transforms;
// Per-island fill colour override (distortion heatmap); empty means use the default wash (#7).
std::vector<ColorRGBA> m_island_fill_colors;
// Boundary vertices per island, for snapping - a patch's boundary is a tiny fraction of it, and
// rescanning the whole uv array on every snap test would not be.
std::vector<std::vector<int>> m_island_boundary_verts;
// Per island: its outline edges, its triangles (indices into m_islands.indices) and the bounding box of its
// raw coordinates - so picking tests one island's triangles only when the cursor is inside its box.
std::vector<std::vector<std::pair<int, int>>> m_island_boundary_edges;
std::vector<std::vector<int>> m_island_tris;
std::vector<std::pair<Vec2f, Vec2f>> m_island_raw_bounds;
bool m_mesh_dirty = true;
bool m_gl_reset_pending = false;
// One set of models per island, so an island can be drawn through its own transform. Built once
// per unwrap, never on a drag. Outlines are not among them: they are drawn as screen-width quads,
// rebuilt every paint (see render()), because GL wide lines are not reliably wider than 1 px.
std::vector<GLModel> m_island_wireframe; // interior edges
std::vector<GLModel> m_island_fill; // filled; also the stencil mask that keeps islands undimmed
GLModel m_stroke_glmodel; // scratch model for the quads of one stroke pass
GLModel m_dim_quad_glmodel; // full-viewport quad that dims the texture outside the islands
int m_stencil_bits = -1; // of the default framebuffer; -1 = not queried yet
GLModel m_tile_outline_glmodel; // the texture's first tile, [0,1]^2 - the "you are here"
GLModel m_grid_glmodel;
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
float m_tiling_scale = 1.f;
float m_rotation_deg = 0.f;
bool m_tile_enabled = true;
bool m_tile_mirrored = false;
bool m_snap_enabled = false;
std::vector<unsigned char> m_background_pixels; // RGBA, expanded from the grayscale input
int m_background_width = 0, m_background_height = 0;
bool m_background_dirty = false; // the pixels need (re)uploading
bool m_background_quad_dirty = true; // only the quad's extent changed
GLTexture m_background_texture;
// Covers content_bounds(), not just [0,1]: the unwrap is packed in mm and then divided by the
// layer's tile size, so it routinely spans many tiles, and a single-unit-square backdrop would
// leave most of the islands sitting over bare background. Texcoord == position, so the GL wrap
// mode repeats it exactly the way DecodedHeightTexture::sample() does.
GLModel m_background_glmodel;
// 2D pan/zoom. m_pan is the UV-space point at the center of the view, m_zoom half the UV-space
// extent visible across the shorter screen edge. v runs *down* the screen, matching both the
// texture's own row order and every other UV editor's convention.
Vec2f m_pan = Vec2f(0.5f, 0.5f);
float m_zoom = 0.75f;
bool m_needs_fit = true; // fit the view to the next unwrap that arrives
enum class Gesture
{
None,
Pan,
MoveIsland,
RotateIsland, // right-drag: rotation tracks the mouse, ends when the button is released
RotateIslandModal, // 'R': rotation tracks the mouse until a click confirms or Esc cancels
ScaleIslandModal, // 'S': likewise, distance from the centre drives the scale
MoveVertex, // Vertex mode: drag one unwrapped vertex
MoveEdge, // Edge mode: drag both endpoints of one boundary edge
};
Gesture m_gesture = Gesture::None;
SelectMode m_select_mode = SelectMode::Island;
// The sub-element being edited in Vertex/Edge mode (unwrapped-vertex indices), or -1/{-1,-1}. This
// is the *primary* (last-picked) element of the multi-selection below.
int m_active_vertex = -1;
std::pair<int, int> m_active_edge{ -1, -1 };
// Multi-selection for Vertex/Edge modes, mirroring the island selection: plain click replaces, Shift
// adds, Ctrl toggles, and a drag moves the whole set together. m_active_vertex/m_active_edge stay the
// primary. Kept as small vectors (tiny, and order doesn't matter here).
std::vector<int> m_sel_vertices;
std::vector<std::pair<int, int>> m_sel_edges;
bool is_vertex_selected(int v) const
{
return std::find(m_sel_vertices.begin(), m_sel_vertices.end(), v) != m_sel_vertices.end();
}
bool is_edge_selected(const std::pair<int, int> &e) const
{
return std::find(m_sel_edges.begin(), m_sel_edges.end(), e) != m_sel_edges.end();
}
// Unique unwrapped-vertex endpoints of every selected edge (an endpoint shared by two selected edges
// is returned once, so a drag doesn't move it twice).
std::vector<int> selected_edge_endpoints() const;
// Last known mouse position over the canvas, and whether the pointer is currently inside it. Used to
// draw the +/- add/remove sign next to the cursor in Vertex/Edge mode.
wxPoint m_cursor_px{ 0, 0 };
bool m_cursor_inside = false;
// Set once a Vertex/Edge drag actually moves, so a bare click (select without drag) doesn't commit a
// no-op edit and take an undo snapshot for nothing.
bool m_vertex_edit_moved = false;
// Lazily-built small filled square, drawn at an edited/hovered vertex as a handle.
GLModel m_vertex_marker_glmodel;
// What a click would grab right now, highlighted so the user sees the target before clicking.
int m_hover_island = -1;
int m_hover_vertex = -1;
std::pair<int, int> m_hover_edge{ -1, -1 };
int m_selected_island = -1;
// The full multi-selection; m_selected_island is its primary (last-clicked) member. Kept as a small
// vector rather than a set because it is tiny and iteration order (primary last) is convenient.
std::vector<int> m_selection;
bool is_selected(int island) const
{
return std::find(m_selection.begin(), m_selection.end(), island) != m_selection.end();
}
wxPoint m_drag_last_px;
Vec2f m_gesture_last_uv = Vec2f::Zero();
float m_gesture_last_angle = 0.f;
float m_gesture_last_dist = 0.f;
// Rotation is tracked as two running totals over the gesture: the raw mouse rotation, and how much
// has actually been applied. With Shift held the applied total is quantised to 15-degree steps
// (Blender-style angle snapping), so the two diverge - and driving the applied total off the raw
// one, rather than snapping each incremental delta, is what makes the snap stable instead of
// juddering. The raw/applied split also survives crossing +/-180 degrees, which a single wrapped
// angle would not. m_rot_applied doubles as the modal-rotate undo amount for Esc.
float m_rot_raw_deg = 0.f;
float m_rot_applied_deg = 0.f;
// The island's absolute on-screen rotation when the gesture began (decoded from its transform), so
// Shift can snap to *global* 15-degree marks (0/15/30...) rather than 15 degrees relative to
// wherever the island happened to start (#10). Also drives the angle read-out and the dial.
float m_rot_base_deg = 0.f;
float m_rot_display_deg = 0.f; // current absolute angle, for the status line and dial needle
float m_modal_scale_accum = 1.f; // so Esc can undo exactly what the modal scale applied, as a factor
// A protractor drawn around the island while it rotates: a ring, a tick every 15 degrees, and a
// needle at the current angle, so the rotation is legible (#11). Rebuilt each frame during a
// rotation gesture (cheap: a few hundred short lines) and left empty otherwise.
GLModel m_dial_glmodel;
void rebuild_rotation_dial();
// The island's current absolute rotation in degrees, decoded from its transform's first column.
float island_rotation_deg(int island) const;
IslandEditFn m_on_island_edit;
UVVertexEditFn m_on_vertex_edit;
CommandFn m_on_command;
StatusFn m_on_status;
PaneState m_pane_state;
PaneStateFn m_on_pane_state;
};
class UVToolButton; // a drawn icon button, defined in UVEditorCanvas.cpp
// The UV editor pane that goes into Plater's "uv_editor" AUI pane: a header with the active layer, the
// canvas background and Unwrap; a settings row with the unwrap's seam angle and island connection; a
// narrow tool strip down the left (selection mode, seams, island actions, snap and framing); the canvas;
// and a status line naming the current gesture, with the unwrap summary on its right.
//
// It holds no editing state of its own. Every control sends a Command through the canvas to the gizmo,
// and the gizmo pushes PaneState back, from which the controls are redrawn (see apply_state()). The gizmo
// still talks to the inner canvas, reached via canvas().
class UVEditorPanel : public wxPanel
{
public:
explicit UVEditorPanel(wxWindow *parent);
UVEditorCanvas *canvas() { return m_canvas; }
private:
void on_tool(wxCommandEvent &evt);
void apply_state(const UVEditorCanvas::PaneState &state);
// Selection-mode toggles, and the tools that need a selected island, follow the canvas.
void refresh_selection_tools();
UVEditorCanvas *m_canvas = nullptr;
UVToolButton *m_thumb = nullptr; // the layer's texture; a click opens the texture library
wxStaticText *m_layer_name = nullptr;
wxStaticText *m_tile = nullptr;
UVToolButton *m_background[3]{};
UVToolButton *m_unwrap = nullptr;
::SpinInput *m_seam_angle = nullptr;
::CheckBox *m_connect = nullptr;
UVToolButton *m_select[3]{};
UVToolButton *m_mark_seams = nullptr;
UVToolButton *m_seam_path = nullptr;
UVToolButton *m_clear_seams = nullptr;
UVToolButton *m_avg_scale = nullptr;
UVToolButton *m_cut = nullptr;
UVToolButton *m_join = nullptr;
UVToolButton *m_unjoin = nullptr;
UVToolButton *m_clear_edits = nullptr;
UVToolButton *m_snap = nullptr;
UVToolButton *m_frame = nullptr;
wxStaticText *m_status = nullptr;
wxStaticText *m_stats = nullptr;
};
} // namespace Slic3r::GUI
#endif // slic3r_UVEditorCanvas_hpp_