#ifndef slic3r_UVEditorCanvas_hpp_ #define slic3r_UVEditorCanvas_hpp_ #include #include #include #include #include "libslic3r/Color.hpp" #include #include // 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 #include #include #include #include #include #include #include #include #include #include #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; // 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 uvs; std::vector indices; std::vector vertex_island; // per uv std::vector> 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 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 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 &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 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 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 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 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 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> &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 &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_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 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(); // 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 m_transforms; // Per-island fill colour override (distortion heatmap); empty means use the default wash (#7). std::vector 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> 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>> m_island_boundary_edges; std::vector> m_island_tris; std::vector> 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 m_island_wireframe; // interior edges std::vector 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 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 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 m_sel_vertices; std::vector> 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 &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 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 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 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_