mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +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/TextureDisplacementPrepareJob.cpp
|
||||
GUI/Jobs/TextureDisplacementPrepareJob.hpp
|
||||
GUI/Jobs/TextureDisplacementDebugJob.cpp
|
||||
GUI/Jobs/TextureDisplacementDebugJob.hpp
|
||||
GUI/Jobs/TextureDisplacementPreviewJob.cpp
|
||||
GUI/Jobs/TextureDisplacementPreviewJob.hpp
|
||||
GUI/Jobs/ThreadSafeQueue.hpp
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
#define slic3r_GLGizmoTextureDisplacement_hpp_
|
||||
|
||||
#include "GLGizmoPainterBase.hpp"
|
||||
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
@@ -46,7 +47,11 @@ public:
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
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
|
||||
// (and every job input) takes.
|
||||
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_action_snapshot_name() const override { return _u8L("Texture displacement editing"); }
|
||||
|
||||
EnforcerBlockerType get_left_button_state_type() const override { return EnforcerBlockerType::ENFORCER; }
|
||||
EnforcerBlockerType get_right_button_state_type() const override { return EnforcerBlockerType::NONE; }
|
||||
// The panel's Paint / Erase toggle swaps what the two buttons do, so the right button always does the
|
||||
// opposite of the left. Shift still erases in both modes (GLGizmoPainterBase::gizmo_event()).
|
||||
EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; }
|
||||
EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; }
|
||||
|
||||
private:
|
||||
bool on_init() override;
|
||||
@@ -154,6 +161,13 @@ private:
|
||||
|
||||
void add_texture_layer();
|
||||
void remove_texture_layer(int slot);
|
||||
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
|
||||
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
|
||||
// the slot each one occupies, so their paint masks move with them on every model part.
|
||||
void move_texture_layer(int slot, int to_index);
|
||||
// Exchanges everything two slots hold - layer definition, paint masks, panel caches. No snapshot and
|
||||
// no selector reload; move_texture_layer() does both once around a run of these.
|
||||
void swap_layer_slots(int slot_a, int slot_b);
|
||||
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
|
||||
// `own_snapshot` false when the caller has already taken an undo step that is meant to cover the
|
||||
// displacement too - see bake_standard().
|
||||
@@ -299,18 +313,21 @@ private:
|
||||
};
|
||||
const LibraryTexture *get_library_texture(const std::string &path);
|
||||
|
||||
// The layer's texture chooser: a drop-down whose closed state and every one of whose entries
|
||||
// shows a large preview image on the left and the texture's name on the right, plus an adjacent
|
||||
// button that imports an image file from disk into the user texture folder. Shipped and
|
||||
// user-imported textures are listed under separate headings.
|
||||
void render_texture_picker(TextureDisplacementLayer &layer);
|
||||
// The texture library: a popup grid of thumbnails beside the panel, shipped textures and the user's
|
||||
// own under separate headings, with a tile that imports an image from disk. Opened for
|
||||
// m_picker_slot by a click on a layer's thumbnail or name. `panel_min`/`panel_max` are the panel
|
||||
// window's screen rectangle, which the popup is placed against.
|
||||
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
|
||||
int m_picker_slot = -1;
|
||||
bool m_picker_open_request = false;
|
||||
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
||||
void import_custom_texture(TextureDisplacementLayer &layer);
|
||||
// Draws a picker row (image left, name right) on top of a full-width Selectable, and leaves the
|
||||
// cursor below it. Shared by the drop-down's closed state and its individual entries so the two
|
||||
// cannot drift apart. Returns true when the row is clicked.
|
||||
bool texture_row(const char *id, const std::string &name, GLTexture *thumbnail, bool selected, float width);
|
||||
float texture_row_height() const;
|
||||
|
||||
// Paint / Erase, from the panel. See get_left_button_state_type().
|
||||
bool m_erase_mode = false;
|
||||
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
|
||||
// Panel state only, not saved with the project.
|
||||
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
|
||||
|
||||
// "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
|
||||
@@ -400,7 +417,21 @@ private:
|
||||
// Handles a toolbar command forwarded from the UV pane that needs the layer data the canvas
|
||||
// doesn't hold (average island scale, cut island). Takes the command as an int (a cast of
|
||||
// UVEditorCanvas::Command) so this header needn't pull in glad/wxGLCanvas via the canvas header.
|
||||
void on_uv_command(int cmd);
|
||||
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
|
||||
// 3D centroid, perpendicular to its longest axis, as seams (#17). The re-unwrap then separates it.
|
||||
void cut_island(TextureDisplacementLayer &layer, int chart);
|
||||
@@ -543,6 +574,41 @@ private:
|
||||
bool m_remesh_keep_sharp_edges = true;
|
||||
void remesh_model();
|
||||
|
||||
// ---- Bake stage debug view ----
|
||||
//
|
||||
// A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the
|
||||
// only useful question is which stage made it wrong. "Capture stages" runs the same recipe Bake
|
||||
// runs, keeps every intermediate mesh, and draws the selected one through the ordinary
|
||||
// true-displacement preview - so this needs no rendering code of its own.
|
||||
//
|
||||
// The run commits nothing: TextureDisplacementDebugJob never touches the Model.
|
||||
std::vector<BakeStageSnapshot> m_debug_stages;
|
||||
// Which stage is on screen. -1 means the debug view is off and the live preview owns
|
||||
// m_preview_glmodel again; rebuild_preview() checks this before replacing it.
|
||||
int m_debug_stage = -1;
|
||||
bool m_debug_in_progress = false;
|
||||
// The open / non-manifold counts come from a sort over every half-edge, which on a multi-million
|
||||
// triangle stage costs more than the stage did. On by default because a stage that tore the mesh
|
||||
// is exactly what this exists to find.
|
||||
bool m_debug_check_topology = true;
|
||||
|
||||
// The default pipeline's automatic resolution/budget for the volume, cached on what it depends on.
|
||||
const V2Resolution &v2_recommendation(const ModelVolume &mv);
|
||||
V2Resolution m_v2_rec;
|
||||
std::string m_v2_rec_key;
|
||||
|
||||
// Queues the capture run. Uses Standard mode's recipe (remesh, refine, displace) unless Pro mode
|
||||
// has already prepared the mesh or the experimental pipeline is on, either of which has nothing
|
||||
// to prepare.
|
||||
void run_stage_debug();
|
||||
// Puts stage `index` on screen. A stage over the memory cap has counts but no geometry and is
|
||||
// skipped.
|
||||
void show_debug_stage(int index);
|
||||
// Drops the captured stages and hands m_preview_glmodel back to the live preview.
|
||||
void exit_debug_view();
|
||||
// The panel: capture button, stage list, and what each stage produced.
|
||||
void render_debug_stage_panel(ModelVolume *mv);
|
||||
|
||||
// Live, pre-bake preview of the true displaced geometry (built by the same algorithm Bake
|
||||
// uses). Empty/uninitialized whenever nothing is painted yet, in which case the gizmo falls
|
||||
// back to the standard paint-mask overlay like every other painting gizmo.
|
||||
@@ -621,6 +687,10 @@ private:
|
||||
// the shader projects on its own. Shared by the bump preview and the UV-check overlay.
|
||||
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
// `patch` with its vertices moved into world millimetres - the space the bake maps the texture in
|
||||
// (see build_texture_displacement()). Returned by value because the caller usually still needs the
|
||||
// original: the patch doubles as render geometry, which is drawn through the volume's own matrix.
|
||||
indexed_triangle_set patch_in_world(const indexed_triangle_set &patch) const;
|
||||
|
||||
// UV-check overlay drawn over the painted patch to sanity-check the unwrap (#13/#14). Built by
|
||||
// rebuild_uvcheck_mesh(), drawn by render_uvcheck_mesh() with the "texture_displacement_uvcheck"
|
||||
@@ -655,6 +725,7 @@ private:
|
||||
enum class UVBackground { None, Height, Checker };
|
||||
UVBackground m_uv_editor_bg = UVBackground::None;
|
||||
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
|
||||
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
|
||||
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
|
||||
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
|
||||
// while the Distortion check mode is on. Empty otherwise.
|
||||
@@ -751,6 +822,18 @@ private:
|
||||
// open/close within a session, so the choice sticks while working.
|
||||
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.
|
||||
bool m_adjust_texture_mode = false;
|
||||
bool m_adjust_anchor_valid = false;
|
||||
@@ -774,14 +857,7 @@ private:
|
||||
|
||||
std::map<std::string, wxString> m_desc;
|
||||
|
||||
// The tool's SVG (toolbar_texture_displacement.svg) uploaded once as a GL texture, so it can be
|
||||
// used as an ImGui image button in the panel (currently the "add layer" affordance next to the
|
||||
// Texture layers heading). Lazily loaded on first use, when a GL context is guaranteed current.
|
||||
GLTexture m_tool_icon;
|
||||
bool m_tool_icon_tried = false;
|
||||
unsigned int tool_icon_id(); // 0 if the icon could not be loaded
|
||||
|
||||
// Icons for the panel's selection-mode and view-mode button rows. Loaded through IconManager with
|
||||
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
|
||||
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
|
||||
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
|
||||
// matching the toolbar's selected/unselected look. Uploaded once on first panel render.
|
||||
|
||||
@@ -61,7 +61,7 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
}
|
||||
return true;
|
||||
},
|
||||
color));
|
||||
color, m_input.volume_to_world));
|
||||
|
||||
// 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.
|
||||
@@ -152,6 +152,7 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
|
||||
input.base_mesh = volume.mesh().its;
|
||||
input.layers = volume.texture_displacement_layers;
|
||||
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)
|
||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ struct TextureDisplacementBakeInput
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
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
|
||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||
// 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->load() == m_generation);
|
||||
},
|
||||
color);
|
||||
color, m_input.volume_to_world);
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
|
||||
@@ -23,6 +23,9 @@ struct TextureDisplacementPreviewInput
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
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
|
||||
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
|
||||
TextureColorSettings color;
|
||||
|
||||
@@ -7484,6 +7484,12 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
.Right()
|
||||
.Hide()
|
||||
.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);
|
||||
panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
@@ -15,7 +17,10 @@
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
@@ -168,6 +173,70 @@ bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
std::vector<TextureLibraryEntry> g_library;
|
||||
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
|
||||
|
||||
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();
|
||||
if (!user_dir.empty())
|
||||
scan_dir(boost::filesystem::path(user_dir), true, g_library);
|
||||
sort_user_textures(g_library);
|
||||
|
||||
g_library_scanned = true;
|
||||
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)
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
// Every available height-map texture: the ones shipped in resources/textures/displacement first,
|
||||
// then the user's own from <data_dir>/textures/displacement, each group sorted by name.
|
||||
// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted
|
||||
// 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
|
||||
// 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.
|
||||
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.
|
||||
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/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
|
||||
@@ -92,14 +97,50 @@ public:
|
||||
void set_snap_enabled(bool enabled) { m_snap_enabled = enabled; }
|
||||
bool snap_enabled() const { return m_snap_enabled; }
|
||||
|
||||
// High-level actions the pane toolbar triggers. The canvas handles the view-only ones (framing,
|
||||
// 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.
|
||||
enum class Command { FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected };
|
||||
void run_command(Command cmd);
|
||||
using CommandFn = std::function<void(Command)>;
|
||||
// `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 &)>;
|
||||
@@ -133,11 +174,21 @@ public:
|
||||
// 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);
|
||||
@@ -155,7 +206,11 @@ private:
|
||||
// The UV region worth looking at: every island, plus always at least the texture's first tile, so
|
||||
// there is something sensibly framed even before anything is painted.
|
||||
void content_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
|
||||
// Frames content_bounds(). Bound to Home, and run once each time an unwrap first appears.
|
||||
// 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
|
||||
@@ -185,6 +240,8 @@ private:
|
||||
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
|
||||
|
||||
@@ -195,13 +252,22 @@ private:
|
||||
// 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.
|
||||
// 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_boundary; // outline
|
||||
std::vector<GLModel> m_island_fill; // filled, for the selected island's wash
|
||||
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;
|
||||
@@ -274,8 +340,10 @@ private:
|
||||
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;
|
||||
// Rebuilt each frame at the cursor while a +/- add-remove hint is shown (Vertex/Edge mode).
|
||||
GLModel m_cursor_sign_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.
|
||||
@@ -315,12 +383,20 @@ private:
|
||||
UVVertexEditFn m_on_vertex_edit;
|
||||
CommandFn m_on_command;
|
||||
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,
|
||||
// project-from-view) and a Blender-style status line along the bottom that names the current gesture
|
||||
// and the shortcuts in play (#18). This is what actually goes into Plater's "uv_editor" AUI pane;
|
||||
// the gizmo still talks to the inner canvas, reached via canvas().
|
||||
class 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:
|
||||
@@ -329,10 +405,35 @@ public:
|
||||
|
||||
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;
|
||||
wxToggleButton *m_snap_button = nullptr;
|
||||
wxStaticText *m_status = nullptr;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user