mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Texture gizmo: one-run pipeline by default, auto resolution, colour fixes, debug view
This commit is contained in:
@@ -331,6 +331,8 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
GUI/Jobs/TextureDisplacementBakeJob.hpp
|
GUI/Jobs/TextureDisplacementBakeJob.hpp
|
||||||
GUI/Jobs/TextureDisplacementPrepareJob.cpp
|
GUI/Jobs/TextureDisplacementPrepareJob.cpp
|
||||||
GUI/Jobs/TextureDisplacementPrepareJob.hpp
|
GUI/Jobs/TextureDisplacementPrepareJob.hpp
|
||||||
|
GUI/Jobs/TextureDisplacementDebugJob.cpp
|
||||||
|
GUI/Jobs/TextureDisplacementDebugJob.hpp
|
||||||
GUI/Jobs/TextureDisplacementPreviewJob.cpp
|
GUI/Jobs/TextureDisplacementPreviewJob.cpp
|
||||||
GUI/Jobs/TextureDisplacementPreviewJob.hpp
|
GUI/Jobs/TextureDisplacementPreviewJob.hpp
|
||||||
GUI/Jobs/ThreadSafeQueue.hpp
|
GUI/Jobs/ThreadSafeQueue.hpp
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
#define slic3r_GLGizmoTextureDisplacement_hpp_
|
#define slic3r_GLGizmoTextureDisplacement_hpp_
|
||||||
|
|
||||||
#include "GLGizmoPainterBase.hpp"
|
#include "GLGizmoPainterBase.hpp"
|
||||||
|
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
|
||||||
#include "libslic3r/TextureDisplacement.hpp"
|
#include "libslic3r/TextureDisplacement.hpp"
|
||||||
#include "slic3r/GUI/GLModel.hpp"
|
#include "slic3r/GUI/GLModel.hpp"
|
||||||
#include "slic3r/GUI/GLTexture.hpp"
|
#include "slic3r/GUI/GLTexture.hpp"
|
||||||
@@ -46,7 +47,11 @@ public:
|
|||||||
const std::vector<TextureDisplacementLayer> &layers,
|
const std::vector<TextureDisplacementLayer> &layers,
|
||||||
const TextureDisplacementPrepareParams ¶ms,
|
const TextureDisplacementPrepareParams ¶ms,
|
||||||
const std::vector<PrintableColor> &palette,
|
const std::vector<PrintableColor> &palette,
|
||||||
const DisplacementProgressFn &progress);
|
const DisplacementProgressFn &progress,
|
||||||
|
// Optional step capture: receives the mesh
|
||||||
|
// after the remesh and after the refinement,
|
||||||
|
// so a debug run can show the whole recipe.
|
||||||
|
BakeStageRecorder *debug = nullptr);
|
||||||
// The volume's eight texture-displacement masks, gathered into the array every pure function here
|
// The volume's eight texture-displacement masks, gathered into the array every pure function here
|
||||||
// (and every job input) takes.
|
// (and every job input) takes.
|
||||||
static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv);
|
static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv);
|
||||||
@@ -135,8 +140,10 @@ protected:
|
|||||||
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Texture displacement painting"); }
|
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Texture displacement painting"); }
|
||||||
std::string get_action_snapshot_name() const override { return _u8L("Texture displacement editing"); }
|
std::string get_action_snapshot_name() const override { return _u8L("Texture displacement editing"); }
|
||||||
|
|
||||||
EnforcerBlockerType get_left_button_state_type() const override { return EnforcerBlockerType::ENFORCER; }
|
// The panel's Paint / Erase toggle swaps what the two buttons do, so the right button always does the
|
||||||
EnforcerBlockerType get_right_button_state_type() const override { return EnforcerBlockerType::NONE; }
|
// opposite of the left. Shift still erases in both modes (GLGizmoPainterBase::gizmo_event()).
|
||||||
|
EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; }
|
||||||
|
EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool on_init() override;
|
bool on_init() override;
|
||||||
@@ -154,6 +161,13 @@ private:
|
|||||||
|
|
||||||
void add_texture_layer();
|
void add_texture_layer();
|
||||||
void remove_texture_layer(int slot);
|
void remove_texture_layer(int slot);
|
||||||
|
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
|
||||||
|
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
|
||||||
|
// the slot each one occupies, so their paint masks move with them on every model part.
|
||||||
|
void move_texture_layer(int slot, int to_index);
|
||||||
|
// Exchanges everything two slots hold - layer definition, paint masks, panel caches. No snapshot and
|
||||||
|
// no selector reload; move_texture_layer() does both once around a run of these.
|
||||||
|
void swap_layer_slots(int slot_a, int slot_b);
|
||||||
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
|
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
|
||||||
// `own_snapshot` false when the caller has already taken an undo step that is meant to cover the
|
// `own_snapshot` false when the caller has already taken an undo step that is meant to cover the
|
||||||
// displacement too - see bake_standard().
|
// displacement too - see bake_standard().
|
||||||
@@ -299,18 +313,21 @@ private:
|
|||||||
};
|
};
|
||||||
const LibraryTexture *get_library_texture(const std::string &path);
|
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
|
// The texture library: a popup grid of thumbnails beside the panel, shipped textures and the user's
|
||||||
// shows a large preview image on the left and the texture's name on the right, plus an adjacent
|
// own under separate headings, with a tile that imports an image from disk. Opened for
|
||||||
// button that imports an image file from disk into the user texture folder. Shipped and
|
// m_picker_slot by a click on a layer's thumbnail or name. `panel_min`/`panel_max` are the panel
|
||||||
// user-imported textures are listed under separate headings.
|
// window's screen rectangle, which the popup is placed against.
|
||||||
void render_texture_picker(TextureDisplacementLayer &layer);
|
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
|
||||||
|
int m_picker_slot = -1;
|
||||||
|
bool m_picker_open_request = false;
|
||||||
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
||||||
void import_custom_texture(TextureDisplacementLayer &layer);
|
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
|
// Paint / Erase, from the panel. See get_left_button_state_type().
|
||||||
// cannot drift apart. Returns true when the row is clicked.
|
bool m_erase_mode = false;
|
||||||
bool texture_row(const char *id, const std::string &name, GLTexture *thumbnail, bool selected, float width);
|
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
|
||||||
float texture_row_height() const;
|
// Panel state only, not saved with the project.
|
||||||
|
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
|
||||||
|
|
||||||
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
// "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
|
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||||
@@ -400,7 +417,21 @@ private:
|
|||||||
// Handles a toolbar command forwarded from the UV pane that needs the layer data the canvas
|
// 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
|
// 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.
|
// UVEditorCanvas::Command) so this header needn't pull in glad/wxGLCanvas via the canvas header.
|
||||||
void on_uv_command(int cmd);
|
void on_uv_command(int cmd, float value);
|
||||||
|
// Pane commands, queued by on_uv_command() and run from the panel render: pane clicks arrive in wx event
|
||||||
|
// handlers, outside the 3D canvas's GL frame, and several of these rebuild GPU meshes.
|
||||||
|
std::vector<std::pair<int, float>> m_uv_command_queue;
|
||||||
|
void process_uv_commands();
|
||||||
|
void run_uv_command(int cmd, float value);
|
||||||
|
// Sends the UV editor pane what its controls show (see UVEditorCanvas::PaneState).
|
||||||
|
void push_uv_pane_state();
|
||||||
|
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
|
||||||
|
// background buttons, so both switch views the same way.
|
||||||
|
void apply_view_mode(int mode);
|
||||||
|
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
|
||||||
|
static constexpr int UV_THUMB_PX = 64;
|
||||||
|
const void *m_uv_thumb_source = nullptr;
|
||||||
|
std::vector<unsigned char> m_uv_thumb_rgb;
|
||||||
// Splits one unwrap chart in two by marking the mesh edges that straddle the plane through its
|
// 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.
|
// 3D centroid, perpendicular to its longest axis, as seams (#17). The re-unwrap then separates it.
|
||||||
void cut_island(TextureDisplacementLayer &layer, int chart);
|
void cut_island(TextureDisplacementLayer &layer, int chart);
|
||||||
@@ -543,6 +574,41 @@ private:
|
|||||||
bool m_remesh_keep_sharp_edges = true;
|
bool m_remesh_keep_sharp_edges = true;
|
||||||
void remesh_model();
|
void remesh_model();
|
||||||
|
|
||||||
|
// ---- Bake stage debug view ----
|
||||||
|
//
|
||||||
|
// A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the
|
||||||
|
// only useful question is which stage made it wrong. "Capture stages" runs the same recipe Bake
|
||||||
|
// runs, keeps every intermediate mesh, and draws the selected one through the ordinary
|
||||||
|
// true-displacement preview - so this needs no rendering code of its own.
|
||||||
|
//
|
||||||
|
// The run commits nothing: TextureDisplacementDebugJob never touches the Model.
|
||||||
|
std::vector<BakeStageSnapshot> m_debug_stages;
|
||||||
|
// Which stage is on screen. -1 means the debug view is off and the live preview owns
|
||||||
|
// m_preview_glmodel again; rebuild_preview() checks this before replacing it.
|
||||||
|
int m_debug_stage = -1;
|
||||||
|
bool m_debug_in_progress = false;
|
||||||
|
// The open / non-manifold counts come from a sort over every half-edge, which on a multi-million
|
||||||
|
// triangle stage costs more than the stage did. On by default because a stage that tore the mesh
|
||||||
|
// is exactly what this exists to find.
|
||||||
|
bool m_debug_check_topology = true;
|
||||||
|
|
||||||
|
// The default pipeline's automatic resolution/budget for the volume, cached on what it depends on.
|
||||||
|
const V2Resolution &v2_recommendation(const ModelVolume &mv);
|
||||||
|
V2Resolution m_v2_rec;
|
||||||
|
std::string m_v2_rec_key;
|
||||||
|
|
||||||
|
// Queues the capture run. Uses Standard mode's recipe (remesh, refine, displace) unless Pro mode
|
||||||
|
// has already prepared the mesh or the experimental pipeline is on, either of which has nothing
|
||||||
|
// to prepare.
|
||||||
|
void run_stage_debug();
|
||||||
|
// Puts stage `index` on screen. A stage over the memory cap has counts but no geometry and is
|
||||||
|
// skipped.
|
||||||
|
void show_debug_stage(int index);
|
||||||
|
// Drops the captured stages and hands m_preview_glmodel back to the live preview.
|
||||||
|
void exit_debug_view();
|
||||||
|
// The panel: capture button, stage list, and what each stage produced.
|
||||||
|
void render_debug_stage_panel(ModelVolume *mv);
|
||||||
|
|
||||||
// Live, pre-bake preview of the true displaced geometry (built by the same algorithm Bake
|
// 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
|
// 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.
|
// back to the standard paint-mask overlay like every other painting gizmo.
|
||||||
@@ -621,6 +687,10 @@ private:
|
|||||||
// the shader projects on its own. Shared by the bump preview and the UV-check overlay.
|
// 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,
|
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||||
const TextureDisplacementLayer &layer) const;
|
const TextureDisplacementLayer &layer) const;
|
||||||
|
// `patch` with its vertices moved into world millimetres - the space the bake maps the texture in
|
||||||
|
// (see build_texture_displacement()). Returned by value because the caller usually still needs the
|
||||||
|
// original: the patch doubles as render geometry, which is drawn through the volume's own matrix.
|
||||||
|
indexed_triangle_set patch_in_world(const indexed_triangle_set &patch) const;
|
||||||
|
|
||||||
// UV-check overlay drawn over the painted patch to sanity-check the unwrap (#13/#14). Built by
|
// 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"
|
// rebuild_uvcheck_mesh(), drawn by render_uvcheck_mesh() with the "texture_displacement_uvcheck"
|
||||||
@@ -655,6 +725,7 @@ private:
|
|||||||
enum class UVBackground { None, Height, Checker };
|
enum class UVBackground { None, Height, Checker };
|
||||||
UVBackground m_uv_editor_bg = UVBackground::None;
|
UVBackground m_uv_editor_bg = UVBackground::None;
|
||||||
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
|
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
|
||||||
|
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
|
||||||
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
|
// 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
|
// 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.
|
// while the Distortion check mode is on. Empty otherwise.
|
||||||
@@ -751,6 +822,18 @@ private:
|
|||||||
// open/close within a session, so the choice sticks while working.
|
// open/close within a session, so the choice sticks while working.
|
||||||
bool m_undocked = false;
|
bool m_undocked = false;
|
||||||
|
|
||||||
|
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
|
||||||
|
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
|
||||||
|
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
|
||||||
|
// moves a *target* offset instead; the real scroll is eased toward it over the following frames.
|
||||||
|
float m_panel_scroll_target = 0.f;
|
||||||
|
float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
|
||||||
|
// Last frame's body content height and footer height. The body is a child window that has to be
|
||||||
|
// given its height before its content is laid out, so it is sized from the previous frame: as tall
|
||||||
|
// as its content, capped so the footer still fits above the bottom of the canvas.
|
||||||
|
float m_panel_body_h = 0.f;
|
||||||
|
float m_panel_footer_h = 0.f;
|
||||||
|
|
||||||
// See the "Adjust Texture" block of private methods above.
|
// See the "Adjust Texture" block of private methods above.
|
||||||
bool m_adjust_texture_mode = false;
|
bool m_adjust_texture_mode = false;
|
||||||
bool m_adjust_anchor_valid = false;
|
bool m_adjust_anchor_valid = false;
|
||||||
@@ -774,14 +857,7 @@ private:
|
|||||||
|
|
||||||
std::map<std::string, wxString> m_desc;
|
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
|
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
|
||||||
// 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
|
|
||||||
|
|
||||||
// Icons for the panel's selection-mode and view-mode button rows. Loaded through IconManager with
|
|
||||||
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
|
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
|
||||||
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
|
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
|
||||||
// matching the toolbar's selected/unselected look. Uploaded once on first panel render.
|
// matching the toolbar's selected/unselected look. Uploaded once on first panel render.
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
color));
|
color, m_input.volume_to_world));
|
||||||
|
|
||||||
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
|
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
|
||||||
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
|
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
|
||||||
@@ -152,6 +152,7 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
|
|||||||
input.base_mesh = volume.mesh().its;
|
input.base_mesh = volume.mesh().its;
|
||||||
input.layers = volume.texture_displacement_layers;
|
input.layers = volume.texture_displacement_layers;
|
||||||
input.options = volume.texture_displacement_options;
|
input.options = volume.texture_displacement_options;
|
||||||
|
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ struct TextureDisplacementBakeInput
|
|||||||
std::vector<TextureDisplacementLayer> layers;
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
TextureDisplacementFacetsData facets_data;
|
TextureDisplacementFacetsData facets_data;
|
||||||
TextureDisplacementOptions options;
|
TextureDisplacementOptions options;
|
||||||
|
// Mesh coordinates -> world millimetres. Captured here with everything else so the worker never
|
||||||
|
// reaches back into the live Model for it. See build_texture_displacement().
|
||||||
|
Transform3d volume_to_world = Transform3d::Identity();
|
||||||
// Captured on the main thread. Empty unless some layer is colouring, in which case the bake also
|
// Captured on the main thread. Empty unless some layer is colouring, in which case the bake also
|
||||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||||
// paint gizmo writes - alongside the displaced geometry.
|
// paint gizmo writes - alongside the displaced geometry.
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#include "TextureDisplacementDebugJob.hpp"
|
||||||
|
|
||||||
|
#include "libslic3r/Model.hpp"
|
||||||
|
|
||||||
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
|
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||||
|
#include "slic3r/GUI/I18N.hpp"
|
||||||
|
#include "slic3r/GUI/Plater.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r::GUI {
|
||||||
|
|
||||||
|
TextureDisplacementDebugJob::TextureDisplacementDebugJob(
|
||||||
|
TextureDisplacementDebugInput &&input, std::function<void(std::vector<BakeStageSnapshot>)> on_finished)
|
||||||
|
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureDisplacementDebugJob::process(Ctl &ctl)
|
||||||
|
{
|
||||||
|
const std::string status = _u8L("Capturing bake stages");
|
||||||
|
ctl.update_status(1, status);
|
||||||
|
|
||||||
|
// Only ever touches m_input (captured by value before this job was queued) and local state - and,
|
||||||
|
// unlike the bake job, never writes anything back either.
|
||||||
|
int last_reported = 1;
|
||||||
|
const auto report = [&ctl, &status, &last_reported](int percent) {
|
||||||
|
if (ctl.was_canceled())
|
||||||
|
return false;
|
||||||
|
if (percent > last_reported) {
|
||||||
|
last_reported = percent;
|
||||||
|
ctl.update_status(percent, status);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
BakeStageRecorder recorder;
|
||||||
|
recorder.enable(true);
|
||||||
|
recorder.set_check_topology(m_input.check_topology);
|
||||||
|
recorder.set_mesh_cap(m_input.mesh_cap);
|
||||||
|
|
||||||
|
indexed_triangle_set mesh = m_input.base_mesh;
|
||||||
|
TextureDisplacementFacetsData masks = m_input.facets_data;
|
||||||
|
|
||||||
|
// The default pipeline can only move vertices the mesh already has, so its recipe starts with the
|
||||||
|
// preparation - which is where "first it remeshes, then it refines" actually happens. The
|
||||||
|
// experimental pipeline refines as part of the bake and has nothing to prepare.
|
||||||
|
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
|
||||||
|
const TextureDisplacementPrepareResult prepared =
|
||||||
|
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
|
||||||
|
m_input.color.palette,
|
||||||
|
// Preparation is roughly half the run; the bake
|
||||||
|
// takes the progress bar from there.
|
||||||
|
[&report](int pct) { return report(1 + pct / 2); },
|
||||||
|
&recorder);
|
||||||
|
if (ctl.was_canceled())
|
||||||
|
return;
|
||||||
|
// An empty result means nothing needed doing, which is not a failure - the bake below simply
|
||||||
|
// runs on the mesh as it stands. paint_lost means the remesh dropped every layer's paint, so
|
||||||
|
// there is nothing left to displace and the stages recorded so far are the whole story.
|
||||||
|
if (prepared.paint_lost) {
|
||||||
|
m_stages = recorder.take();
|
||||||
|
ctl.update_status(100, status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!prepared.mesh.indices.empty()) {
|
||||||
|
mesh = prepared.mesh;
|
||||||
|
masks = prepared.masks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
|
||||||
|
[&report](int pct) { return report(50 + pct / 2); },
|
||||||
|
/* color */ nullptr, m_input.volume_to_world, &recorder);
|
||||||
|
|
||||||
|
m_stages = recorder.take();
|
||||||
|
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureDisplacementDebugJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||||
|
{
|
||||||
|
if (!m_on_finished)
|
||||||
|
return;
|
||||||
|
// The handler must run on every outcome - the caller uses it to clear its in-progress latch, and
|
||||||
|
// an empty vector is its signal that nothing usable came back.
|
||||||
|
if (canceled || eptr)
|
||||||
|
m_on_finished({});
|
||||||
|
else
|
||||||
|
m_on_finished(std::move(m_stages));
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color,
|
||||||
|
const TextureDisplacementPrepareParams &prepare_params,
|
||||||
|
bool run_prepare, bool check_topology,
|
||||||
|
std::function<void(std::vector<BakeStageSnapshot>)> on_finished)
|
||||||
|
{
|
||||||
|
TextureDisplacementDebugInput input;
|
||||||
|
input.volume_id = volume.id();
|
||||||
|
input.base_mesh = volume.mesh().its;
|
||||||
|
input.layers = volume.texture_displacement_layers;
|
||||||
|
input.options = volume.texture_displacement_options;
|
||||||
|
input.prepare_params = prepare_params;
|
||||||
|
input.run_prepare = run_prepare;
|
||||||
|
input.check_topology = check_topology;
|
||||||
|
input.color = color;
|
||||||
|
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||||
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
|
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||||
|
|
||||||
|
auto &worker = wxGetApp().plater()->get_ui_job_worker();
|
||||||
|
queue_job(worker, std::make_unique<TextureDisplacementDebugJob>(std::move(input), std::move(on_finished)));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#ifndef slic3r_TextureDisplacementDebugJob_hpp_
|
||||||
|
#define slic3r_TextureDisplacementDebugJob_hpp_
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "libslic3r/ObjectID.hpp"
|
||||||
|
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
|
||||||
|
#include "libslic3r/TextureDisplacement.hpp"
|
||||||
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
|
|
||||||
|
#include "Job.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
class ModelVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Slic3r::GUI {
|
||||||
|
|
||||||
|
// Runs a bake with stage capture on and commits nothing.
|
||||||
|
//
|
||||||
|
// A bake is a chain of stages that each rewrite the whole mesh - remesh, refine, displace, smooth, or
|
||||||
|
// for the experimental pipeline subdivide, regularize, relocate, displace, decimate, repair - and when
|
||||||
|
// the result looks wrong the only useful question is which of them made it wrong. This runs the same
|
||||||
|
// recipe the Bake button runs, keeps every intermediate mesh, and hands them back for the gizmo to
|
||||||
|
// step through.
|
||||||
|
//
|
||||||
|
// Deliberately separate from TextureDisplacementBakeJob rather than a flag on it: this must never
|
||||||
|
// touch the Model, and keeping the committing path free of a "but not this time" branch is what
|
||||||
|
// guarantees that.
|
||||||
|
struct TextureDisplacementDebugInput
|
||||||
|
{
|
||||||
|
ObjectID volume_id;
|
||||||
|
indexed_triangle_set base_mesh;
|
||||||
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
|
TextureDisplacementFacetsData facets_data;
|
||||||
|
TextureDisplacementOptions options;
|
||||||
|
// The preparation stages - remesh and adaptive refinement. Only the default pipeline uses them;
|
||||||
|
// the experimental one refines as part of the bake and ignores this entirely.
|
||||||
|
TextureDisplacementPrepareParams prepare_params;
|
||||||
|
bool run_prepare = true;
|
||||||
|
// Mesh coordinates -> world millimetres, as the bake gets it. The captured stages are brought
|
||||||
|
// back into mesh coordinates before they are handed over, so the gizmo can draw them directly.
|
||||||
|
Transform3d volume_to_world = Transform3d::Identity();
|
||||||
|
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run
|
||||||
|
// produces no colour of its own.
|
||||||
|
TextureColorSettings color;
|
||||||
|
|
||||||
|
// The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a
|
||||||
|
// multi-million triangle stage costs more than the stage did. On by default because a stage that
|
||||||
|
// tore the mesh is exactly what this exists to find.
|
||||||
|
bool check_topology = true;
|
||||||
|
// Stages above this keep their counts but not their geometry - see BakeStageRecorder.
|
||||||
|
size_t mesh_cap = 4'000'000;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TextureDisplacementDebugJob : public Job
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TextureDisplacementDebugJob(TextureDisplacementDebugInput &&input,
|
||||||
|
std::function<void(std::vector<BakeStageSnapshot>)> on_finished);
|
||||||
|
|
||||||
|
void process(Ctl &ctl) override;
|
||||||
|
void finalize(bool canceled, std::exception_ptr &eptr) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TextureDisplacementDebugInput m_input;
|
||||||
|
std::vector<BakeStageSnapshot> m_stages;
|
||||||
|
std::function<void(std::vector<BakeStageSnapshot>)> m_on_finished;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Captures `volume`'s current mesh, layers and paint and queues one debug run. `on_finished` always
|
||||||
|
// runs exactly once on the UI thread, with an empty vector if the run was cancelled or threw, so the
|
||||||
|
// caller can clear its own in-progress state. Must be called from the main thread.
|
||||||
|
void queue_texture_displacement_debug(const ModelVolume &volume, const TextureColorSettings &color,
|
||||||
|
const TextureDisplacementPrepareParams &prepare_params,
|
||||||
|
bool run_prepare, bool check_topology,
|
||||||
|
std::function<void(std::vector<BakeStageSnapshot>)> on_finished);
|
||||||
|
|
||||||
|
} // namespace Slic3r::GUI
|
||||||
|
|
||||||
|
#endif // slic3r_TextureDisplacementDebugJob_hpp_
|
||||||
@@ -43,7 +43,7 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
|||||||
(!m_current_generation ||
|
(!m_current_generation ||
|
||||||
m_current_generation->load() == m_generation);
|
m_current_generation->load() == m_generation);
|
||||||
},
|
},
|
||||||
color);
|
color, m_input.volume_to_world);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ struct TextureDisplacementPreviewInput
|
|||||||
std::vector<TextureDisplacementLayer> layers;
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
TextureDisplacementFacetsData facets_data;
|
TextureDisplacementFacetsData facets_data;
|
||||||
TextureDisplacementOptions options;
|
TextureDisplacementOptions options;
|
||||||
|
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
|
||||||
|
// is and the two cannot disagree. See build_texture_displacement().
|
||||||
|
Transform3d volume_to_world = Transform3d::Identity();
|
||||||
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
|
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
|
||||||
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
|
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
|
||||||
TextureColorSettings color;
|
TextureColorSettings color;
|
||||||
|
|||||||
@@ -7484,6 +7484,12 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
|||||||
.Right()
|
.Right()
|
||||||
.Hide()
|
.Hide()
|
||||||
.BestSize(wxSize(40 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit())));
|
.BestSize(wxSize(40 * wxGetApp().em_unit(), 40 * wxGetApp().em_unit())));
|
||||||
|
// Closing the pane with its own X has to reach the gizmo, or its next update would simply show the pane again.
|
||||||
|
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent &evt) {
|
||||||
|
evt.Skip();
|
||||||
|
if (evt.GetPane() != nullptr && evt.GetPane()->window == uv_editor_panel && uv_editor_canvas != nullptr)
|
||||||
|
uv_editor_canvas->run_command(UVEditorCanvas::Command::PaneClosed);
|
||||||
|
});
|
||||||
|
|
||||||
auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL);
|
auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0);
|
panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <boost/algorithm/string/case_conv.hpp>
|
#include <boost/algorithm/string/case_conv.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
@@ -15,7 +17,10 @@
|
|||||||
#include "libslic3r/PNGReadWrite.hpp"
|
#include "libslic3r/PNGReadWrite.hpp"
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
|
|
||||||
|
#include "libslic3r/AppConfig.hpp"
|
||||||
|
|
||||||
#include "slic3r/GUI/GUI.hpp"
|
#include "slic3r/GUI/GUI.hpp"
|
||||||
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
#include "slic3r/GUI/I18N.hpp"
|
#include "slic3r/GUI/I18N.hpp"
|
||||||
|
|
||||||
namespace Slic3r::GUI {
|
namespace Slic3r::GUI {
|
||||||
@@ -168,6 +173,70 @@ bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
|||||||
std::vector<TextureLibraryEntry> g_library;
|
std::vector<TextureLibraryEntry> g_library;
|
||||||
bool g_library_scanned = false;
|
bool g_library_scanned = false;
|
||||||
|
|
||||||
|
// The user's own textures, most recently used first, kept in the app config. File names are stored rather
|
||||||
|
// than paths so the order survives a moved data directory; '/' separates them because no platform allows
|
||||||
|
// it inside a file name. Bounded so a long-lived config does not grow a line without end.
|
||||||
|
const char *const RECENT_SECTION = "texture_displacement";
|
||||||
|
const char *const RECENT_KEY = "recent_textures";
|
||||||
|
const size_t RECENT_MAX = 200;
|
||||||
|
|
||||||
|
std::vector<std::string> recent_texture_names()
|
||||||
|
{
|
||||||
|
std::vector<std::string> names;
|
||||||
|
if (wxGetApp().app_config == nullptr)
|
||||||
|
return names;
|
||||||
|
const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY);
|
||||||
|
for (size_t begin = 0; begin <= value.size();) {
|
||||||
|
const size_t end = std::min(value.find('/', begin), value.size());
|
||||||
|
if (end > begin)
|
||||||
|
names.push_back(value.substr(begin, end - begin));
|
||||||
|
begin = end + 1;
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
void store_recent_texture_names(std::vector<std::string> names)
|
||||||
|
{
|
||||||
|
if (wxGetApp().app_config == nullptr)
|
||||||
|
return;
|
||||||
|
if (names.size() > RECENT_MAX)
|
||||||
|
names.resize(RECENT_MAX);
|
||||||
|
std::string value;
|
||||||
|
for (const std::string &name : names)
|
||||||
|
value += (value.empty() ? "" : "/") + name;
|
||||||
|
wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep
|
||||||
|
// the name order scan_dir() gave them, after every used one.
|
||||||
|
void sort_user_textures(std::vector<TextureLibraryEntry> &library)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> names = recent_texture_names();
|
||||||
|
std::unordered_map<std::string, int> rank;
|
||||||
|
for (size_t i = 0; i < names.size(); ++i)
|
||||||
|
rank.emplace(names[i], int(i));
|
||||||
|
const auto rank_of = [&rank](const TextureLibraryEntry &e) {
|
||||||
|
const auto it = rank.find(boost::filesystem::path(e.path).filename().string());
|
||||||
|
return it == rank.end() ? std::numeric_limits<int>::max() : it->second;
|
||||||
|
};
|
||||||
|
const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; });
|
||||||
|
std::stable_sort(first_user, library.end(),
|
||||||
|
[&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if `path` is a file directly inside user_texture_dir() - the only place textures may be removed
|
||||||
|
// from, or have their use recorded.
|
||||||
|
bool in_user_texture_dir(const std::string &path)
|
||||||
|
{
|
||||||
|
const std::string dir = user_texture_dir();
|
||||||
|
if (dir.empty())
|
||||||
|
return false;
|
||||||
|
boost::system::error_code ec;
|
||||||
|
const bool same = boost::filesystem::equivalent(boost::filesystem::path(path).parent_path(),
|
||||||
|
boost::filesystem::path(dir), ec);
|
||||||
|
return !ec && same;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::string user_texture_dir()
|
std::string user_texture_dir()
|
||||||
@@ -192,11 +261,47 @@ const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
|
|||||||
const std::string user_dir = user_texture_dir();
|
const std::string user_dir = user_texture_dir();
|
||||||
if (!user_dir.empty())
|
if (!user_dir.empty())
|
||||||
scan_dir(boost::filesystem::path(user_dir), true, g_library);
|
scan_dir(boost::filesystem::path(user_dir), true, g_library);
|
||||||
|
sort_user_textures(g_library);
|
||||||
|
|
||||||
g_library_scanned = true;
|
g_library_scanned = true;
|
||||||
return g_library;
|
return g_library;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void touch_user_texture(const std::string &path)
|
||||||
|
{
|
||||||
|
if (!in_user_texture_dir(path))
|
||||||
|
return;
|
||||||
|
const std::string name = boost::filesystem::path(path).filename().string();
|
||||||
|
std::vector<std::string> names = recent_texture_names();
|
||||||
|
names.erase(std::remove(names.begin(), names.end(), name), names.end());
|
||||||
|
names.insert(names.begin(), name);
|
||||||
|
store_recent_texture_names(std::move(names));
|
||||||
|
if (g_library_scanned)
|
||||||
|
sort_user_textures(g_library);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool remove_user_texture(const std::string &path, std::string &error)
|
||||||
|
{
|
||||||
|
if (!in_user_texture_dir(path)) {
|
||||||
|
error = _u8L("Only your own imported textures can be removed.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::filesystem::remove(boost::filesystem::path(path), ec);
|
||||||
|
if (ec) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Could not delete texture " << path << ": " << ec.message();
|
||||||
|
error = _u8L("Could not delete the texture file.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string name = boost::filesystem::path(path).filename().string();
|
||||||
|
std::vector<std::string> names = recent_texture_names();
|
||||||
|
names.erase(std::remove(names.begin(), names.end(), name), names.end());
|
||||||
|
store_recent_texture_names(std::move(names));
|
||||||
|
texture_library(true); // drop it from the list
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error)
|
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error)
|
||||||
{
|
{
|
||||||
const std::string user_dir = user_texture_dir();
|
const std::string user_dir = user_texture_dir();
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ struct TextureLibraryEntry
|
|||||||
bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer
|
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,
|
// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted
|
||||||
// then the user's own from <data_dir>/textures/displacement, each group sorted by name.
|
// by name, then the user's own from <data_dir>/textures/displacement - most recently used first (see
|
||||||
|
// touch_user_texture()), then any never used, by name.
|
||||||
//
|
//
|
||||||
// The two live in separate directories deliberately: an app update replaces the resources tree
|
// 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
|
// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or
|
||||||
@@ -27,6 +28,17 @@ struct TextureLibraryEntry
|
|||||||
// into either folder by hand while the app was running.
|
// into either folder by hand while the app was running.
|
||||||
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
|
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
|
||||||
|
|
||||||
|
// Moves one of the user's own textures to the front of the recently-used order texture_library() lists
|
||||||
|
// them in, and stores that order in the app config so it survives a restart. No-op for anything that is
|
||||||
|
// not in user_texture_dir().
|
||||||
|
void touch_user_texture(const std::string &path);
|
||||||
|
|
||||||
|
// Deletes one of the user's own imported textures from user_texture_dir() and forgets it. Anything outside
|
||||||
|
// that folder is refused, so a shipped texture can never be removed this way. Layers already using the
|
||||||
|
// texture are unaffected: they hold the image bytes themselves, and a project stores those. Returns false
|
||||||
|
// with `error` set on failure.
|
||||||
|
bool remove_user_texture(const std::string &path, std::string &error);
|
||||||
|
|
||||||
// <data_dir>/textures/displacement, created if it does not exist yet. Empty string on failure.
|
// <data_dir>/textures/displacement, created if it does not exist yet. Empty string on failure.
|
||||||
std::string user_texture_dir();
|
std::string user_texture_dir();
|
||||||
|
|
||||||
|
|||||||
+930
-238
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,17 @@
|
|||||||
#include <wx/button.h>
|
#include <wx/button.h>
|
||||||
#include <wx/tglbtn.h>
|
#include <wx/tglbtn.h>
|
||||||
#include <wx/stattext.h>
|
#include <wx/stattext.h>
|
||||||
|
#include <wx/statbmp.h>
|
||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
|
|
||||||
#include "libslic3r/Point.hpp"
|
#include "libslic3r/Point.hpp"
|
||||||
#include "GLModel.hpp"
|
#include "GLModel.hpp"
|
||||||
#include "GLTexture.hpp"
|
#include "GLTexture.hpp"
|
||||||
|
|
||||||
|
// Orca's own widgets (slic3r/GUI/Widgets), declared at global scope.
|
||||||
|
class SpinInput;
|
||||||
|
class CheckBox;
|
||||||
|
|
||||||
namespace Slic3r::GUI {
|
namespace Slic3r::GUI {
|
||||||
|
|
||||||
// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch - shows the result of
|
// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch - shows the result of
|
||||||
@@ -92,14 +97,50 @@ public:
|
|||||||
void set_snap_enabled(bool enabled) { m_snap_enabled = enabled; }
|
void set_snap_enabled(bool enabled) { m_snap_enabled = enabled; }
|
||||||
bool snap_enabled() const { return m_snap_enabled; }
|
bool snap_enabled() const { return m_snap_enabled; }
|
||||||
|
|
||||||
// High-level actions the pane toolbar triggers. The canvas handles the view-only ones (framing,
|
// 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 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.
|
// 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 };
|
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
|
||||||
void run_command(Command cmd);
|
// SelectMode / Background) and is 0 otherwise.
|
||||||
using CommandFn = std::function<void(Command)>;
|
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); }
|
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
|
// 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.
|
// apply right now), so the pane can show it Blender-style along the bottom.
|
||||||
using StatusFn = std::function<void(const wxString &)>;
|
using StatusFn = std::function<void(const wxString &)>;
|
||||||
@@ -133,11 +174,21 @@ public:
|
|||||||
// The primary (last-clicked) island, still the pivot for rotate/scale and the target of the
|
// 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.
|
// single-island toolbar commands (Cut/Join/Unjoin). -1 if nothing is selected.
|
||||||
int selected_island() const { return m_selected_island; }
|
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
|
// 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
|
// >= 0. The gizmo reads this to decide which islands a drag moves together, unioned with each
|
||||||
// selected island's join group.
|
// selected island's join group.
|
||||||
const std::vector<int> &selected_islands() const { return m_selection; }
|
const std::vector<int> &selected_islands() const { return m_selection; }
|
||||||
void reset_view();
|
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:
|
private:
|
||||||
void on_paint(wxPaintEvent &evt);
|
void on_paint(wxPaintEvent &evt);
|
||||||
@@ -155,7 +206,11 @@ private:
|
|||||||
// The UV region worth looking at: every island, plus always at least the texture's first tile, so
|
// 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.
|
// there is something sensibly framed even before anything is painted.
|
||||||
void content_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
|
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.
|
// 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();
|
void fit_view_to_content();
|
||||||
|
|
||||||
// Half-extents of the visible UV region. Split out because both rendering and every mouse
|
// Half-extents of the visible UV region. Split out because both rendering and every mouse
|
||||||
@@ -185,6 +240,8 @@ private:
|
|||||||
void end_gesture();
|
void end_gesture();
|
||||||
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
|
// Rebuilds the status line from the current gesture/selection and pushes it to m_on_status.
|
||||||
void update_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
|
wxGLContext *m_context = nullptr; // owned by OpenGLManager/GUI_App, not by this canvas
|
||||||
|
|
||||||
@@ -195,13 +252,22 @@ private:
|
|||||||
// Boundary vertices per island, for snapping - a patch's boundary is a tiny fraction of it, and
|
// 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.
|
// rescanning the whole uv array on every snap test would not be.
|
||||||
std::vector<std::vector<int>> m_island_boundary_verts;
|
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_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
|
// One set of models per island, so an island can be drawn through its own transform. Built once
|
||||||
// per unwrap, never on a drag.
|
// 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_wireframe; // interior edges
|
||||||
std::vector<GLModel> m_island_boundary; // outline
|
std::vector<GLModel> m_island_fill; // filled; also the stencil mask that keeps islands undimmed
|
||||||
std::vector<GLModel> m_island_fill; // filled, for the selected island's wash
|
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_tile_outline_glmodel; // the texture's first tile, [0,1]^2 - the "you are here"
|
||||||
GLModel m_grid_glmodel;
|
GLModel m_grid_glmodel;
|
||||||
@@ -274,8 +340,10 @@ private:
|
|||||||
bool m_vertex_edit_moved = false;
|
bool m_vertex_edit_moved = false;
|
||||||
// Lazily-built small filled square, drawn at an edited/hovered vertex as a handle.
|
// Lazily-built small filled square, drawn at an edited/hovered vertex as a handle.
|
||||||
GLModel m_vertex_marker_glmodel;
|
GLModel m_vertex_marker_glmodel;
|
||||||
// Rebuilt each frame at the cursor while a +/- add-remove hint is shown (Vertex/Edge mode).
|
// What a click would grab right now, highlighted so the user sees the target before clicking.
|
||||||
GLModel m_cursor_sign_glmodel;
|
int m_hover_island = -1;
|
||||||
|
int m_hover_vertex = -1;
|
||||||
|
std::pair<int, int> m_hover_edge{ -1, -1 };
|
||||||
int m_selected_island = -1;
|
int m_selected_island = -1;
|
||||||
// The full multi-selection; m_selected_island is its primary (last-clicked) member. Kept as a small
|
// 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.
|
// vector rather than a set because it is tiny and iteration order (primary last) is convenient.
|
||||||
@@ -315,12 +383,20 @@ private:
|
|||||||
UVVertexEditFn m_on_vertex_edit;
|
UVVertexEditFn m_on_vertex_edit;
|
||||||
CommandFn m_on_command;
|
CommandFn m_on_command;
|
||||||
StatusFn m_on_status;
|
StatusFn m_on_status;
|
||||||
|
PaneState m_pane_state;
|
||||||
|
PaneStateFn m_on_pane_state;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hosts a UVEditorCanvas together with a small icon toolbar (frame, snap, average scale, cut,
|
class UVToolButton; // a drawn icon button, defined in UVEditorCanvas.cpp
|
||||||
// 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 UV editor pane that goes into Plater's "uv_editor" AUI pane: a header with the active layer, the
|
||||||
// the gizmo still talks to the inner canvas, reached via canvas().
|
// 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
|
class UVEditorPanel : public wxPanel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -329,10 +405,35 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void on_tool(wxCommandEvent &evt);
|
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;
|
UVEditorCanvas *m_canvas = nullptr;
|
||||||
wxToggleButton *m_snap_button = nullptr;
|
|
||||||
wxStaticText *m_status = 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
|
} // namespace Slic3r::GUI
|
||||||
|
|||||||
Reference in New Issue
Block a user